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 hard max_steps guard 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-ish params spec (name → type), and a callable. .validate(args) checks required params and types (rejecting bool-as-int). registry.dispatch(name, args) returns a result or a structured ToolError* — 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 an Action(thought, name, args) or a FinalAnswer(thought, answer); whitespace-robust; raises ValueError on 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 at max_steps with a partial trace. The Trace records every Thought / Action / Observation.
  • MemoryBufferMemory(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), and TieredMemory combining buffer + rolling summary + semantic recall into one context().

Key concepts

ConceptWhat to understand
Agent = LLM in a loopa 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 callingthe structured call (name + JSON args) + validation is what turns "the model said words" into "a function ran safely"
Errors as observationsa returned ToolError lets the agent recover; a raised exception kills the run
The max_steps guardthe single line that separates "an agent" from "an unbounded bill / infinite loop"
Context window is finiteyou cannot keep the whole history in the prompt → buffer + summary + semantic recall
Semantic recallembed turns/facts, recall by cosine — the in-process miniature of Phase 11's vector store

Files

FilePurpose
lab.pyskeleton with # TODO markers — your implementation
solution.pycomplete reference; python solution.py runs a worked example
test_lab.pythe proof — run it red, make it green
requirements.txtpytest 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 observation 5 feeds 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 ToolError is returned and what breaks if you raise it instead.
  • You can explain why BufferMemory evicting the oldest turn and TieredMemory folding 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 miniatureThe production mechanismWhere to verify it
Tool + validateOpenAI/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 JSONthe tools=[…] schema; a pydantic arg model; tool_use blocks
ToolRegistry.dispatch returning errorsa tool runtime that catches exceptions and feeds the error back to the model as a tool_resultLangChain ToolException, the OpenAI tool-call → tool-result round-trip
parse_actionthe ReAct output parser turning model text into a structured actionLangChain's ReActSingleInputOutputParser
ReActAgent.run + max_stepsthe agent executor with max_iterations / recursion-limit guardsLangChain AgentExecutor(max_iterations=…), LangGraph recursion_limit
BufferMemory / summarize_memoryconversation memory: window buffer + summary-buffer memoryLangChain ConversationBufferWindowMemory, ConversationSummaryMemory
SemanticMemory.recalllong-term/vector memory over past interactionsLlamaIndex/LangChain vector memory; a real ANN index (Phase 11)
TieredMemorythe 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_steps guard 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.