Skip to Content

ReAct

from loomflow import ReAct

The canonical observe / think / act loop. Yao et al. 2022. ReAct: Synergizing Reasoning and Acting in Language Models. The default architecture when no architecture= kwarg is passed to Agent.

For the conceptual page see ReAct.


Class signature

class ReAct: name: str = "react" def __init__(self, *, max_turns: int | None = None) -> None: ... def declared_workers(self) -> dict[str, Agent]: ... async def run( self, session: AgentSession, deps: Dependencies, prompt: str, ) -> AsyncIterator[Event]: ...

Constructor parameters

max_turns

Typeint | None
DefaultNone

Per-architecture override of the iteration cap. When None, ReAct uses Dependencies.max_turns (the value from the parent Agent). Passing a value here lets you cap a specific architecture instance without changing the agent-level default. Useful for nested architectures (Reflexion-of-Supervisor wants the supervisor’s loop capped tighter than the outer Reflexion’s).

from loomflow import Agent, ReAct # Cap THIS architecture at 10 turns regardless of Agent's max_turns agent = Agent( "...", model="claude-opus-4-7", architecture=ReAct(max_turns=10), )

Hitting the cap terminates cleanly with session.interrupted = True, session.interruption_reason = "max_turns".


Methods

declared_workers

Returns {}. ReAct is single-agent.

run

Drives the loop. Each turn:

  1. Check budget (deps.budget.allows_step()). Block / warn as needed.
  2. Open loom.turn telemetry span.
  3. Open loom.model.stream span. Stream chunks through runtime.stream_step("model_call_<turn>", model.stream, messages).
  4. Aggregate text + tool_calls + usage from chunks. Emit each chunk as MODEL_CHUNK event.
  5. If no tool calls, append assistant message and break.
  6. Otherwise, dispatch tools in parallel inside an anyio.create_task_group. Each _run_single_tool opens its own loom.tool span, runs hooks → permissions → sandboxed runtime.step("tool_call_<turn>_<slot>", tool_host.call, ...). Audit tool_call and tool_result per call.
  7. Append tool result messages to the conversation; loop again.

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.


Replay-correctness contract

ReAct wraps every external call with runtime.step(...):

  • runtime.stream_step("model_call_<turn>", ...) for each model call.
  • runtime.step("tool_call_<turn>_<slot>", ...) for each tool dispatch.
  • runtime.step("persist_episode_<turns>", ...) for the final memory write.

With SqliteRuntime / PostgresRuntime, a crashed run resumes by re-using the cached results for completed steps and only re-executing the un-completed work. See Replay and resume.


Events emitted

EventWhen
BUDGET_WARNINGBudget reached soft_warning_at threshold.
BUDGET_EXCEEDEDBudget cap exceeded; loop terminates.
MODEL_CHUNKPer streamed chunk from the model.
TOOL_CALLBefore each tool dispatch.
TOOL_RESULTAfter each tool returns (ok or error).
ERRORUnrecoverable error inside the loop.

STARTED and COMPLETED are emitted by the Agent, not by ReAct.


Source

loomflow/architecture/react.py

ReAct is the default. Agent("...", model="...") with no architecture= kwarg uses ReAct() with no overrides. Picks max_turns from the agent. The shipped reference implementation is ~250 lines and exercises every protocol method; read it once to understand the loop end-to-end.

Last updated on