« Phase 01 README · Warmup · Hitchhiker's · Deep Dive · Principal Deep Dive · Staff Notes

Phase 01 — Core Contributor Notes: The Agent Loop — ReAct, ReWOO & Plan-Execute-Replan

Our runtime is a stdlib miniature of four real systems: LangGraph, the OpenAI Agents SDK, Google ADK, and LangChain's AgentExecutor. This document is the maintainer's-eye view — how each actually implements the loop under the hood, why the abstractions look the way they do, the sharp edges a committer knows, and exactly what our miniature simplifies. The patterns below are accurate to how these systems are built; where an exact signature would be invented, the pattern is described instead.

LangGraph: the loop is a graph, the guard is a recursion limit

LangGraph is the most instructive comparison because it refuses to hide the loop — it is the loop, made explicit as a StateGraph. You declare nodes (a model node, a tool node) and edges (control flow), and the runtime executes the graph as a sequence of supersteps, threading a shared, typically reducer-merged state object through each. Our scratchpad is that state; our transition function is a node.

A ReAct agent in LangGraph is a two-node cycle: a model node proposes, a conditional edge routes to a tool node if there is a tool call or to the end if there is a final answer, and the tool node loops back to the model node. That conditional edge is our if isinstance(parsed, FinalAnswer) branch. The prebuilt ReAct agent is exactly this graph wired for you.

The critical maintainer detail is the recursion_limit. LangGraph does not count "LLM calls"; it counts supersteps, and when the count exceeds the limit it raises GraphRecursionError. That is our max_steps guard with a different name and a sharper edge: it raises rather than returning a partial trace, so a committer knows the limit is a safety rail whose default (25) is frequently too low for real plan-execute graphs and whose exhaustion is a hard exception you must catch, not a graceful stop. Plan-execute-replan is a LangGraph tutorial template: a planner node, an executor, and a re-plan edge that routes back to the planner when a step fails — structurally identical to our while replans <= max_replans with the failure rebuilt into the planner's context. The other thing a committer knows: LangGraph state is persisted through checkpointers, so a graph can be interrupted and resumed. That is the durability seam our runtime does not have — our scratchpad lives only in memory for one run.

OpenAI Agents SDK: Runner.run, max_turns, handoffs, guardrails

The Agents SDK packages the same loop behind Runner.run(agent, input). Under the hood the Runner runs the agent loop: call the model, and if the model produced tool calls, execute them, append the results, and call the model again — until the model returns a final output with no tool call, or a handoff fires, or the turn budget is hit. The final-output condition is our FinalAnswer branch; the tool-execution-then-loop is our Action branch.

Two SDK features map directly to phase concepts. max_turns is our max_steps — the iteration guard, and exceeding it raises MaxTurnsExceeded, again the "raise on limit" convention rather than our partial-trace return. Handoffs are the multi-agent generalization (Phase 07): instead of only looping back to the same model, a step can transfer control to another agent, which the SDK models as a special tool call. Guardrails are the trust boundary made a first-class feature — input and output validation that can tripwire the run — the productionized cousin of our parse_react_step strictness and the ToolError discipline. A committer knows the SDK's loop is intentionally thin: it deliberately does the minimum and pushes orchestration structure to the developer, which is why it maps so cleanly onto our run facade.

Google ADK: LlmAgent plus workflow agents

ADK draws a line our three modes also draw: reasoning agents versus deterministic workflow structure. An LlmAgent is the reasoning unit — a model with tools, i.e. a ReAct-style loop. Around it, ADK provides workflow agents that impose structure without a model in the loop: SequentialAgent runs children in order, ParallelAgent runs them concurrently, LoopAgent repeats a child until a condition. This is exactly our ReWOO/plan-execute distinction expressed as composition: ReWOO is a SequentialAgent (or ParallelAgent for independent steps) over tool executions with LlmAgents bookending as planner and solver; plan-execute-replan is a LoopAgent wrapping a planner-plus-executor. The maintainer's insight ADK encodes is that not everything that composes agents needs to be an agent — the workflow agents are cheap, deterministic control flow, and pushing structure into them instead of into an LlmAgent's prompt is the whole cost argument of ReWOO, made into a type.

LangChain AgentExecutor: the original text-ReAct loop

AgentExecutor is the historical anchor and the one that most directly mirrors parse_react_step. It ran a loop that called an LLM chain, parsed the free-text output with an output parser into an AgentAction or AgentFinish, ran the tool, appended the result to a running list of intermediate steps, and repeated — bounded by max_iterations (and optionally max_execution_time). Our Action/FinalAnswer split is literally AgentAction/AgentFinish; our scratchpad append is its intermediate-steps accumulation. The sharp edge every LangChain committer remembers is the OutputParserException: the free-text format broke constantly, and AgentExecutor grew a handle_parsing_errors flag to feed the parse failure back to the model as an observation rather than crash — which is precisely our PARSE_ERROR path. LangChain has since steered users toward LangGraph for anything nontrivial because a linear executor cannot express re-plan edges or persistence cleanly; that migration is the lived history behind why the modern answer is a graph.

Native tool calls replaced the text format — but the text format persists

The single biggest evolution since the ReAct paper: models now emit native tool calls — structured JSON in a dedicated response field (the tool_calls array), not free text you regex. This is strictly better: no Action:/Action Input: string-scraping, no OutputParserException from a stray code fence, arguments arrive as JSON the runtime validates against the tool's schema. Every framework above prefers it. So why does the text format still matter, and why does the lab teach it? Because it still shows up: in traces and prompt templates, in reasoning-scratchpad formats that interleave thought with structured calls, in older stacks and open models without reliable native tool-calling, and — most importantly — because the boundary discipline is identical. Native tool calls still cross a trust boundary; you still validate the JSON against a schema and turn a malformed or unknown call into a structured error, not a crash. Phase 02 is that native path; parse_react_step teaches the invariant it shares.

How the frameworks count turns and enforce limits

A committer-level nuance: these systems count different things and it matters. LangGraph counts supersteps (graph transitions). The Agents SDK counts turns (model round-trips within a run). LangChain counted iterations (executor loop passes). Our runtime counts llm_calls, tool_calls, and steps separately, which is why we can assert llm_calls == steps for ReAct but llm_calls == 2 for ReWOO — the tool calls are decoupled from the model calls. Get the counting model wrong and the limit does not mean what you think: a plan-execute graph whose parallel workers each count as a superstep can blow a recursion_limit that a naive per-model-call intuition said was generous. When a real agent trips its limit at 2 a.m., knowing which unit the limit counts is the difference between a five-minute fix and a confused hour.

What our stdlib runtime deliberately simplifies

The miniature is honest about its omissions, and a contributor should hold each against the real thing:

  • Async and parallel tools. Our worker loop is synchronous and sequential; real ReWOO/ADK executors run independent steps concurrently (asyncio, ParallelAgent). We note the fan-out as an extension; we do not implement the scheduler.
  • Transient failure, retries, backoff. dispatch turns a failure into an observation but does not retry it. Real runtimes add idempotency-aware retries with exponential backoff — Phase 08 (durability). Retrying without idempotency is a footgun the real systems make you opt into deliberately.
  • Native tool-call JSON parsing. We parse the ReAct text format; the frameworks parse the tool_calls field and schema-validate — Phase 02.
  • State persistence. No checkpointer, no resume. One run, in-memory scratchpad. LangGraph's checkpointers and the durability track are where that lands.
  • Plan quality. Our planner is an injected pure function returning a fixed plan; a real planner is an LlmAgent whose plan quality varies run to run, which is the entire reason plan-execute-replan exists in production and the reason max_replans is not optional.

The control flow, though — parse across the boundary, dispatch without raising, count the calls, guard the iterations, re-plan on failure with the error in context — is exactly what the real frameworks do. Build it once from scratch and their source stops being magic; it becomes a set of decisions you recognize, and can fix.