Skip to Content

ReAct

The canonical observe / think / act loop, and the default architecture. Yao et al. 2022. ReAct: Synergizing Reasoning and Acting in Language Models.

┌────────── loop until no tool calls ──────────┐ │ │ prompt ───► Model ───► tool calls? ──yes──► run tools ──► results │ (parallel) └─────────► no calls ───► final output

What each turn does

  1. Check budget (token / cost / wall-clock); emit BUDGET_WARNING or BUDGET_EXCEEDED if needed.
  2. Call the model with the current message stack + available tools.
  3. Stream tokens and aggregate text + tool_calls + usage.
  4. If the assistant has no tool calls, append the final message and break.
  5. Otherwise dispatch every tool call in parallel under a single anyio.create_task_group. Through before_tool hooks, then permissions, then the tool host. Append the results as role=tool messages.
  6. Loop back to step 1.

Usage

from loomflow import Agent # Default — no kwarg needed agent = Agent("You are helpful.", model="claude-opus-4-7") # Or pass the spec explicitly agent = Agent("...", model="...", architecture="react") # Or an instance for tuning from loomflow import ReAct agent = Agent( "...", model="claude-opus-4-7", architecture=ReAct(), )

max_turns lives on the Agent, not on ReAct. Pass Agent(..., max_turns=20) (default 50). Hitting the cap returns a RunResult with interrupted=True and interruption_reason="max_turns".

Parallel tool dispatch

Tool calls in the same model turn run concurrently. The model emits multiple tool_calls in one assistant message; the framework fans them out under one task group, awaits them all, then continues. Tool results are appended in arrival order. Deterministic for the model’s next turn.

A failing tool doesn’t poison the others: each tool’s exception is captured in its own ToolResult(ok=False) and the loop continues with the partial set.

When ReAct is the right call

  • The number of steps is hard to predict upfront → ReAct’s one-step-at-a-time observation pays off.
  • Tools occasionally fail or return surprising data that should re-shape the plan.
  • You want the simplest possible loop with good production behaviour.

When to swap it out

Replay correctness. Every model call and every tool dispatch is journaled by (session_id, step_name). With a SqliteRuntime or PostgresRuntime, a crashed run resumes by re-using the cached results for completed steps and only re-executing the un-completed work. See Runtime.

Last updated on