Skip to Content

ReAct

ReAct is the default architecture. Each turn, the model reads what has happened so far, decides what to do, and either calls tools or gives its final answer; tool results feed into the next turn. It suits most tasks, especially when you can’t predict how many tool calls the job will need. From Yao et al. 2022, ReAct: Synergizing Reasoning and Acting in Language Models.

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_exceeded".

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