ReAct
from loomflow import ReActThe 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
| Type | int | None |
| Default | None |
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:
- Check budget (
deps.budget.allows_step()). Block / warn as needed. - Open
loom.turntelemetry span. - Open
loom.model.streamspan. Stream chunks throughruntime.stream_step("model_call_<turn>", model.stream, messages). - Aggregate text + tool_calls + usage from chunks. Emit each chunk
as
MODEL_CHUNKevent. - If no tool calls, append assistant message and break.
- Otherwise, dispatch tools in parallel inside an
anyio.create_task_group. Each_run_single_toolopens its ownloom.toolspan, runs hooks → permissions → sandboxedruntime.step("tool_call_<turn>_<slot>", tool_host.call, ...). Audittool_callandtool_resultper call. - 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
| Event | When |
|---|---|
BUDGET_WARNING | Budget reached soft_warning_at threshold. |
BUDGET_EXCEEDED | Budget cap exceeded; loop terminates. |
MODEL_CHUNK | Per streamed chunk from the model. |
TOOL_CALL | Before each tool dispatch. |
TOOL_RESULT | After each tool returns (ok or error). |
ERROR | Unrecoverable 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.