Lab 01 — ReAct Loop, Typed Tool Registry & Tiered Memory
Phase: 12 — Agentic Reasoning: Tools & Memory Difficulty: ⭐⭐⭐☆☆ (the loop is small; the control flow and the failure modes are the work) Time: 3–5 hours
An "agent" is not a model — it is an LLM in a loop that can act and observe. This lab builds that loop from scratch: a typed tool registry (validated calls, errors returned not raised), a ReAct parser (
Thought/Action/Action Input→ structured step), the reason→act→observe loop with a hardmax_stepsguard so a confused model can't burn your budget in an infinite loop, and a tiered memory (recent buffer + rolling summary + semantic recall) that fights the finite context window. The LLM is replaced by an injected, deterministic policy — a pure function from scratchpad to next step — which is the only way to make an agent testable.
What you build
Tool/ToolError/ToolRegistry— a tool is a name, a description, a JSON-schema-ishparamsspec (name → type), and a callable..validate(args)checks required params and types (rejectingbool-as-int).registry.dispatch(name, args)returns a result or a structuredToolError* — unknown tool, bad args, and an exception inside the tool all become a returned error the agent can observe and recover from, never an uncaught crash.parse_action(text)— turn a ReAct-formatted step into anAction(thought, name, args)or aFinalAnswer(thought, answer); whitespace-robust; raisesValueErroron the malformed (no action, bad JSON, non-object input).ReActAgent(policy, registry, max_steps)—.run(task) -> Trace. The loop: ask the policy for the next step → parse → if final, stop; else dispatch the tool and append the observation to the scratchpad → repeat, stopping atmax_stepswith a partial trace. TheTracerecords every Thought / Action / Observation.- Memory —
BufferMemory(max_turns)(FIFO recent turns),summarize_memory(turns, summarizer)(compress old turns via an injected summarizer stub),SemanticMemory(add(text, embedding);recall(query_embedding, k)by cosine), andTieredMemorycombining buffer + rolling summary + semantic recall into onecontext().
Key concepts
| Concept | What to understand |
|---|---|
| Agent = LLM in a loop | a model alone only predicts text; the loop that acts on its output and feeds back observations is the agent |
| ReAct (reason + act) | interleave a Thought with an Action; the observation grounds the next thought — beats plain chain-of-thought because it can check reality |
| Typed tool calling | the structured call (name + JSON args) + validation is what turns "the model said words" into "a function ran safely" |
| Errors as observations | a returned ToolError lets the agent recover; a raised exception kills the run |
The max_steps guard | the single line that separates "an agent" from "an unbounded bill / infinite loop" |
| Context window is finite | you cannot keep the whole history in the prompt → buffer + summary + semantic recall |
| Semantic recall | embed turns/facts, recall by cosine — the in-process miniature of Phase 11's vector store |
Files
| File | Purpose |
|---|---|
lab.py | skeleton with # TODO markers — your implementation |
solution.py | complete reference; python solution.py runs a worked example |
test_lab.py | the proof — run it red, make it green |
requirements.txt | pytest only (pure stdlib otherwise; no real LLM) |
Run
pip install -r requirements.txt
pytest test_lab.py -v # against your lab.py
LAB_MODULE=solution pytest test_lab.py -v # against the reference
python solution.py # the worked example
Success criteria
- All tests pass against your implementation (and
LAB_MODULE=solution). - You can explain why the policy is injected and what would happen to the tests if it called a real model (non-deterministic → un-assertable).
- You can trace, on paper, the Thought/Action/Observation sequence of
test_react_agent_solves_multistep_task(the soul test) and say why the observation5feeds the next step. - You can explain why
test_react_agent_stops_at_max_steps(the safety soul test) is the most important test in the file for anything that touches money or production. - You can explain why a
ToolErroris returned and what breaks if youraiseit instead. - You can explain why
BufferMemoryevicting the oldest turn andTieredMemoryfolding it into a summary is the same problem the context window has — and why semantic recall is the escape.
How this maps to the real stack
| The miniature | The production mechanism | Where to verify it |
|---|---|---|
Tool + validate | OpenAI/Anthropic function/tool calling — a JSON-schema tool spec the model fills, validated before execution; Phase 08's constrained decoding makes the model emit valid JSON | the tools=[…] schema; a pydantic arg model; tool_use blocks |
ToolRegistry.dispatch returning errors | a tool runtime that catches exceptions and feeds the error back to the model as a tool_result | LangChain ToolException, the OpenAI tool-call → tool-result round-trip |
parse_action | the ReAct output parser turning model text into a structured action | LangChain's ReActSingleInputOutputParser |
ReActAgent.run + max_steps | the agent executor with max_iterations / recursion-limit guards | LangChain AgentExecutor(max_iterations=…), LangGraph recursion_limit |
BufferMemory / summarize_memory | conversation memory: window buffer + summary-buffer memory | LangChain ConversationBufferWindowMemory, ConversationSummaryMemory |
SemanticMemory.recall | long-term/vector memory over past interactions | LlamaIndex/LangChain vector memory; a real ANN index (Phase 11) |
TieredMemory | the layered context assembly real agents use (recent verbatim + rolling summary + retrieved facts) | LangGraph memory, MemGPT-style tiered memory |
Limits of the miniature (be honest in the interview): the policy is scripted, so there is no
real reasoning, hallucination, or prompt-following to debug; tools are pure Python, not network
side-effects (so no idempotency, retries, timeouts, or rate limits); the embeddings are tiny
hand-written vectors, not a learned model; and SemanticMemory is a linear scan, not an ANN
index (Phase 11). The control flow, validation, error-handling, and memory tiering are exactly
the production shape; the intelligence is stubbed.
Extensions (build these on real hardware / a real LLM)
- Swap the scripted policy for a real model call (OpenAI/Anthropic/local) and a system prompt
that documents the registry's tools; keep the loop and the
max_stepsguard unchanged. - Add a token budget alongside
max_steps: estimate scratchpad tokens each step and stop when the budget is exceeded (the real second guard). - Add Plan-and-Execute: have the policy emit a full plan first, then execute each step (fewer model calls than step-by-step ReAct — preview of Phase 13).
- Add a reflection step: on a failed observation, ask the policy to critique and retry (Reflexion-style self-correction).
- Add human-in-the-loop approval for tools flagged
dangerous=True(the containment for irreversible actions), and make a tool idempotent with an operation key. - Replace
SemanticMemory's linear scan with the ANN index you built in Phase 11.
Interview / resume
- Talking points: "What actually is an agent?" "Walk me through the ReAct loop." "How do you stop an agent from looping forever / blowing the budget?" "A tool throws — what should the agent see?" "How do you give an agent memory beyond the context window?" "How would you contain an agent that can delete data?"
- Resume bullet: Built a ReAct agent runtime from first principles — a typed, validated tool registry with errors returned as recoverable observations, a whitespace-robust ReAct parser, a reason→act→observe loop with a hard step/budget guard, and a tiered memory (recent buffer + rolling summary + semantic vector recall) — fully deterministic via an injected policy for reproducible tests.