Reflexion
from loomflow.architecture import ReflexionReflexion retries a task and learns from each failed attempt.
After each attempt an evaluator scores the output; if the score is
below the threshold, a reflector writes a one-sentence “lesson” that
the agent reads on its next attempt. Use it when you can score
results reliably (tests pass, a schema validates, an answer matches
ground truth) and retries are cheaper than wrong answers. Based on
Shinn et al. 2023, Reflexion: Language Agents with Verbal
Reinforcement Learning.
For the conceptual page see Reflexion.
Class signature
class Reflexion:
name: str = "reflexion"
def __init__(
self,
*,
base: Architecture | None = None,
max_attempts: int = 3,
threshold: float = 0.8,
evaluator_prompt: str | None = None,
reflector_prompt: str | None = None,
lessons_block_name: str = "reflexion_lessons",
lesson_store: VectorStore | None = None,
top_k_lessons: int = 5,
evaluator_model: str | Model | None = None,
reflector_model: str | Model | None = None,
max_lessons: int = 10,
) -> None: ...Constructor parameters
base
| Type | Architecture | None |
| Default | None (resolves to ReAct()) |
The architecture used for each attempt. Defaults to ReAct. Wrapping
a Supervisor, Reflexion(base=Supervisor(workers=...)), gives a team
that learns better delegation across attempts. See
Recursive composition.
max_attempts
| Type | int |
| Default | 3 |
Maximum attempts before giving up. Each attempt runs the base
architecture; below-threshold attempts trigger a lesson. Must be
>= 1.
threshold
| Type | float |
| Default | 0.8 |
Evaluator score (in [0.0, 1.0]) at or above which the loop
terminates. Lower for permissive evaluation, higher for strict.
evaluator_prompt
| Type | str | None |
| Default | None (uses built-in default) |
Override the evaluator’s system prompt. The default asks for a
score: <0..1> line plus a brief justification. Provide your
own when you have a domain-specific rubric (test-pass rate, factual
accuracy against ground truth, etc.).
The reply is read with parse_score,
which understands scales: score: 7/10 is 0.7, score: 40% is 0.4,
and a bare score: 7 is read as out of 10. A reply with no score
counts as 0.0, a failed attempt.
reflector_prompt
| Type | str | None |
| Default | None (uses built-in default) |
Override the reflector’s system prompt. The default asks for a single-sentence lesson the agent can apply on its next attempt. The reflector is shown the task, the attempt’s actions and observations (its tool calls and their results, most recent last) and its output. Lessons are kept short on purpose: long lessons use up the context that makes Reflexion cheaper than a plain retry.
lessons_block_name
| Type | str |
| Default | "reflexion_lessons" |
Name of the working-memory block lessons are written to in
monotonic block mode (when no lesson_store is wired), one per
line, in the run’s own user’s memory. The agent’s seed messages
include this block on every later attempt.
lesson_store
| Type | VectorStore | None |
| Default | None |
When provided, switches Reflexion to selective recall mode: lessons are stored as embedded chunks in the vector store, and before each attempt the top-k most relevant lessons for the current prompt are retrieved and surfaced. This avoids context bloat as lessons accumulate.
from loomflow import HashEmbedder
from loomflow.architecture import Reflexion
from loomflow.vectorstore import InMemoryVectorStore
architecture = Reflexion(
lesson_store=InMemoryVectorStore(embedder=HashEmbedder()),
top_k_lessons=5,
)For cross-process learning, swap InMemoryVectorStore for
PostgresVectorStore or any other persistent backend.
Each lesson is stored with the run’s user_id in its metadata (""
for anonymous runs), and a run recalls only its own user’s lessons
plus any tagged user_id="*" (SHARED_LESSON). Lessons quote the
failing prompt, so they stay with their user. Seed curated
lessons for everyone with that tag:
from loomflow import HashEmbedder
from loomflow.architecture import Reflexion
from loomflow.architecture.reflexion import SHARED_LESSON
from loomflow.loader import Chunk
from loomflow.vectorstore import InMemoryVectorStore
store = InMemoryVectorStore(embedder=HashEmbedder())
await store.add([
Chunk(
content="Normalize dates to ISO 8601 before answering.",
metadata={"user_id": SHARED_LESSON},
),
])
architecture = Reflexion(lesson_store=store)Lessons without a user_id tag, as written before 0.17, are no
longer recalled.
top_k_lessons
| Type | int |
| Default | 5 |
Number of relevant lessons surfaced per attempt when lesson_store
is wired. Must be >= 1. Ignored in monotonic-block mode (see
max_lessons).
max_lessons
| Type | int |
| Default | 10 |
How many lessons the monotonic block keeps; the oldest are dropped
first so the prompt doesn’t grow without bound. Must be >= 1.
Ignored in selective-recall mode.
evaluator_model / reflector_model
| Type | str | Model | None |
| Default | None (use the agent’s model) |
Use a different model for scoring or for writing lessons, for
example a cheap model for the evaluator. Accepts a model string or a
Model instance; role models are not wrapped with the agent’s retry
policy. A model string is resolved with the agent’s secrets=
backend, like the main model.
Methods
declared_workers
Returns {}. Workers of the base architecture are reported by the
base’s own declared_workers (for Reflexion(base=Supervisor(...)),
ask the supervisor).
run
For each attempt:
- Check the budget (once per attempt), then emit
reflexion.attempt_started. - Selective recall (when
lesson_storeis wired): query for lessons relevant to THIS prompt, from this user or shared, and write the top-k into the working memory block. Emitreflexion.lessons_recalled. - Reset session messages so the base rebuilds its starting
context, which picks up lessons from
memory.working(). - Run
base.run(session, deps, prompt), forwarding all events. - If the base ran out of turns (its
max_turns) and attempts remain, the attempt counts as a failed trial: score 0, no evaluator call, reflect and retry. Any other interruption (a budget, a guardrail), or running out of turns on the last attempt, stops. - Evaluate. One model call (
reflexion.evaluated). An attempt that ran out of turns skips the call and scores 0 (reflexion.evaluatedwithreason="out_of_turns"). If score ≥ threshold, stop (reflexion.threshold_met). - Reflect. One model call producing a lesson from the attempt’s
actions, observations and output (
reflexion.lesson_produced). Persist it tolesson_store(selective mode) or append it to the monotonic block (reflexion.lesson_persisted).
When the last attempt scores below the threshold, Reflexion emits
reflexion.max_attempts_reached. When the last attempt runs out of
turns, the run ends interrupted
(interruption_reason="max_turns_exceeded") without that event.
Cost model
Each attempt: base + 1 (evaluator) + 1 (reflector if below threshold).
For a 3-attempt run: roughly 1.5–3× base cost depending on how often
the evaluator passes. Typical use case (test pass/fail evaluator):
1.5× cost when passing on attempt 2, 3× when needing all 3 attempts.
When Reflexion pays off
- Tasks with a clear evaluator signal (test pass/fail, JSON schema match, factual correctness against ground truth).
- Repeated runs of the same problem class, where lessons compound.
- Cases where the alternative is shipping wrong answers to users, so the extra calls pay for themselves.
Example
from loomflow import Agent, HashEmbedder
from loomflow.architecture import Reflexion, Supervisor
from loomflow.vectorstore import InMemoryVectorStore
agent = Agent(
"Manage the article pipeline.",
model="claude-opus-4-7",
architecture=Reflexion(
base=Supervisor(workers={
"researcher": researcher,
"writer": writer,
"reviewer": reviewer,
}),
max_attempts=3,
threshold=0.85,
lesson_store=InMemoryVectorStore(embedder=HashEmbedder()),
),
)Source
loomflow/architecture/reflexion.py
Without an evaluator, Reflexion does nothing useful. The lesson
generator only fires when the evaluator’s score < threshold. Provide
an evaluator_prompt that returns a meaningful score for your
domain (test runner, regex match, structured-output validity, etc.)
or the loop just retries without learning.