ReAct loop¶
The low-level ReAct loop primitives. Most users should reach for the
high-level Agent API (see Agent) — these classes are for
when you need to compose your own loop shape (different node order,
custom routing, batched execution).
Loop¶
ReActLoop ¶
Bases: BaseModel
ReAct (Reason + Act) loop implementation.
Implements the Think -> Execute -> Reflect cycle with: - Streaming events via AsyncIterator - Conditional routing based on state - Confidence-based termination - Tool loop detection
Usage
loop = ReActLoop(model=my_model, registry=my_tools)
async for event in loop.run("Solve this problem"): match event: case ThinkEvent(): print(f"Thinking: {event.reasoning}") case ToolCompleteEvent(): print(f"Tool {event.tool_name}: {event.result}") case TerminateEvent(): print(f"Done: {event.reason}")
run
async
¶
run(prompt: str, initial_state: AgentState | None = None, **state_kwargs: Any) -> AsyncIterator[LoopEvent]
Run the ReAct loop.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
prompt
|
str
|
User prompt to process |
required |
initial_state
|
AgentState | None
|
Optional pre-configured state |
None
|
**state_kwargs
|
Any
|
Additional state configuration |
{}
|
Yields:
| Type | Description |
|---|---|
AsyncIterator[LoopEvent]
|
Loop events (ThinkEvent, ToolStartEvent, ToolCompleteEvent, ReflectEvent, TerminateEvent) |
Source code in src/locus/loop/react.py
run_to_completion
async
¶
run_to_completion(prompt: str, initial_state: AgentState | None = None, **state_kwargs: Any) -> tuple[AgentState, list[LoopEvent]]
Run the loop and collect all events.
Convenience method that collects all events and returns the final state along with the event history.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
prompt
|
str
|
User prompt to process |
required |
initial_state
|
AgentState | None
|
Optional pre-configured state |
None
|
**state_kwargs
|
Any
|
Additional state configuration |
{}
|
Returns:
| Type | Description |
|---|---|
tuple[AgentState, list[LoopEvent]]
|
Tuple of (final_state, events) |
Source code in src/locus/loop/react.py
with_config ¶
Create a new loop with updated configuration.
Returns a new ReActLoop instance (immutable).
Source code in src/locus/loop/react.py
ReActLoopConfig ¶
Bases: BaseModel
Configuration for the ReAct loop.
create_react_loop ¶
create_react_loop(model: ModelProtocol, registry: ToolRegistry, *, max_iterations: int = 20, confidence_threshold: float = 0.85, enable_reflection: bool = True, system_prompt: str | None = None) -> ReActLoop
Factory function to create a ReActLoop.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
model
|
ModelProtocol
|
LLM model to use for reasoning |
required |
registry
|
ToolRegistry
|
Tool registry with available tools |
required |
max_iterations
|
int
|
Maximum iterations before forced termination |
20
|
confidence_threshold
|
float
|
Confidence level for completion |
0.85
|
enable_reflection
|
bool
|
Whether to include reflection step |
True
|
system_prompt
|
str | None
|
Optional system prompt |
None
|
Returns:
| Type | Description |
|---|---|
ReActLoop
|
Configured ReActLoop instance |
Source code in src/locus/loop/react.py
Nodes¶
Node ¶
Bases: BaseModel, ABC
Base class for ReAct loop nodes.
execute
abstractmethod
async
¶
Execute the node.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
state
|
AgentState
|
Current agent state |
required |
Returns:
| Type | Description |
|---|---|
NodeResult
|
Updated state and any events produced |
NodeResult ¶
Bases: BaseModel
Result from executing a node.
ThinkNode ¶
Bases: Node
Node that invokes the LLM to generate reasoning and/or tool calls.
This is the "Reason" part of ReAct.
execute
async
¶
Generate the next thought and/or tool calls.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
state
|
AgentState
|
Current agent state |
required |
Returns:
| Type | Description |
|---|---|
NodeResult
|
Updated state with assistant message and ThinkEvent |
Source code in src/locus/loop/nodes.py
ExecuteNode ¶
Bases: Node
Node that executes tool calls.
This is the "Act" part of ReAct.
execute
async
¶
Execute pending tool calls.
This is the "Act" part of ReAct. Tool calls whose tool declared
idempotent=True are de-duplicated against prior executions on
the current state: if the same (tool_name, arguments) pair has
already been executed during this agent run, the prior result is
reused and the tool function is NOT invoked again. This prevents
models that re-emit the same call from causing duplicate
side-effects (double bookings, double transfers, etc.).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
state
|
AgentState
|
Current agent state with tool calls |
required |
Returns:
| Type | Description |
|---|---|
NodeResult
|
Updated state with tool results and events |
Source code in src/locus/loop/nodes.py
146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 | |
ReflectNode ¶
Bases: Node
Node that evaluates progress and adjusts confidence.
This implements a simplified Reflexion-style self-evaluation.
execute
async
¶
Reflect on the current progress and update confidence.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
state
|
AgentState
|
Current agent state |
required |
Returns:
| Type | Description |
|---|---|
NodeResult
|
Updated state with adjusted confidence and ReflectEvent |
Source code in src/locus/loop/nodes.py
Router¶
Router ¶
Bases: BaseModel
Conditional routing logic for the ReAct loop.
Determines which node to execute next based on the current state.
route_from_think ¶
Route from the Think node.
After thinking: - If tool calls exist -> Execute - If no tool calls and should terminate -> Terminate - If no tool calls -> Reflect (if enabled) or Terminate
Source code in src/locus/loop/router.py
route_from_execute ¶
Route from the Execute node.
After executing tools: - If should terminate -> Terminate - If reflection enabled and interval met -> Reflect - Otherwise -> Think
Source code in src/locus/loop/router.py
route_from_reflect ¶
Route from the Reflect node.
After reflecting: - If should terminate -> Terminate - Otherwise -> Think (with new iteration)
Source code in src/locus/loop/router.py
route ¶
Route from the given node based on current state.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
current_node
|
NodeType
|
The node that just completed |
required |
state
|
AgentState
|
Current agent state |
required |
Returns:
| Type | Description |
|---|---|
RouteDecision
|
Decision about which node to execute next |
Source code in src/locus/loop/router.py
ConditionalRouter ¶
Bases: Router
Router with custom condition functions.
Allows injecting custom routing logic for advanced use cases.
add_condition ¶
Add a custom routing condition.
The condition function receives the state and returns either: - A RouteDecision to override default routing - None to continue with default routing
Returns a new router with the condition added (immutable).
Source code in src/locus/loop/router.py
route ¶
Route with custom conditions checked first.
Custom conditions are checked in order. The first one to return a RouteDecision wins.
Source code in src/locus/loop/router.py
route_from_think ¶
Route from the Think node.
After thinking: - If tool calls exist -> Execute - If no tool calls and should terminate -> Terminate - If no tool calls -> Reflect (if enabled) or Terminate
Source code in src/locus/loop/router.py
route_from_execute ¶
Route from the Execute node.
After executing tools: - If should terminate -> Terminate - If reflection enabled and interval met -> Reflect - Otherwise -> Think
Source code in src/locus/loop/router.py
route_from_reflect ¶
Route from the Reflect node.
After reflecting: - If should terminate -> Terminate - Otherwise -> Think (with new iteration)
Source code in src/locus/loop/router.py
NodeType ¶
Bases: StrEnum
Types of nodes in the ReAct loop.
RouteDecision ¶
Bases: BaseModel
Result of a routing decision.
Runner¶
LoopRunner ¶
Bases: BaseModel
High-level executor for ReAct loops.
Provides additional features: - Event callbacks/hooks - Error handling and retries - Timeout management - Progress tracking
run
async
¶
run(prompt: str, initial_state: AgentState | None = None, **state_kwargs: Any) -> AsyncIterator[LoopEvent]
Run the loop with callbacks and error handling.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
prompt
|
str
|
User prompt |
required |
initial_state
|
AgentState | None
|
Optional initial state |
None
|
**state_kwargs
|
Any
|
Additional state configuration |
{}
|
Yields:
| Type | Description |
|---|---|
AsyncIterator[LoopEvent]
|
Loop events |
Source code in src/locus/loop/runner.py
run_to_completion
async
¶
run_to_completion(prompt: str, initial_state: AgentState | None = None, **state_kwargs: Any) -> tuple[AgentState, list[LoopEvent]]
Run to completion and return final state with events.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
prompt
|
str
|
User prompt |
required |
initial_state
|
AgentState | None
|
Optional initial state |
None
|
**state_kwargs
|
Any
|
Additional state configuration |
{}
|
Returns:
| Type | Description |
|---|---|
tuple[AgentState, list[LoopEvent]]
|
Tuple of (final_state, events) |
Source code in src/locus/loop/runner.py
BatchRunner ¶
Bases: BaseModel
Run multiple prompts through the loop.
Supports parallel execution with concurrency limits.
run_batch
async
¶
run_batch(prompts: list[str], on_result: Callable[[str, AgentState, list[LoopEvent]], None] | None = None) -> list[tuple[str, AgentState, list[LoopEvent]]]
Run multiple prompts in parallel.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
prompts
|
list[str]
|
List of prompts to process |
required |
on_result
|
Callable[[str, AgentState, list[LoopEvent]], None] | None
|
Optional callback for each result |
None
|
Returns:
| Type | Description |
|---|---|
list[tuple[str, AgentState, list[LoopEvent]]]
|
List of (prompt, final_state, events) tuples |
Source code in src/locus/loop/runner.py
StreamingCollector ¶
Bases: BaseModel
Collect events from a streaming loop run.
Useful for capturing events while also processing them.
collect ¶
Add an event to the collection.
Source code in src/locus/loop/runner.py
create_runner ¶
create_runner(model: ModelProtocol, registry: ToolRegistry, *, max_iterations: int = 20, confidence_threshold: float = 0.85, enable_reflection: bool = True, system_prompt: str | None = None, timeout: float | None = None, on_event: Callable[[LoopEvent], None] | None = None) -> LoopRunner
Factory function to create a configured LoopRunner.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
model
|
ModelProtocol
|
LLM model for reasoning |
required |
registry
|
ToolRegistry
|
Tool registry |
required |
max_iterations
|
int
|
Maximum iterations |
20
|
confidence_threshold
|
float
|
Confidence for completion |
0.85
|
enable_reflection
|
bool
|
Enable reflection step |
True
|
system_prompt
|
str | None
|
System prompt |
None
|
timeout
|
float | None
|
Optional timeout in seconds |
None
|
on_event
|
Callable[[LoopEvent], None] | None
|
Optional event callback |
None
|
Returns:
| Type | Description |
|---|---|
LoopRunner
|
Configured LoopRunner |