Design Principles
Predictable Behavior
All state transitions are explicit and testable with exhaustive pattern matching.
Clear Ownership
Each state carries its required context (conversation, retries, etc.).
Graceful Recovery
Built-in retry mechanisms with bounded attempts and backoff.
Clean Separation
State machine logic is synchronous and pure; caller manages async I/O.
State Machine Overview
The state machine receivesAgentEvents and returns AgentActions that the caller must execute. This inversion of control allows the caller to manage async operations (LLM calls, tool execution) while the state machine remains synchronous and pure.
States
AgentState Enum
State Details
WaitingForUserInput
WaitingForUserInput
Initial and terminal state for user turnsThe agent is idle and awaits user input. The conversation context preserves all prior messages.Transitions:
UserInput→CallingLlmShutdownRequested→ShuttingDown
CallingLlm
CallingLlm
Active LLM request in flightThe
retries counter tracks how many retry attempts have been made for the current request.Transitions:TextDelta→CallingLlm(streaming text)ToolCallDelta→CallingLlm(streaming tool call)Completed→ProcessingLlmResponseError(retries < max) →ErrorError(retries >= max) →WaitingForUserInputShutdownRequested→ShuttingDown
ProcessingLlmResponse
ProcessingLlmResponse
Transient state for examining an LLM responseImmediately transitions to either
ExecutingTools (if tool calls present) or WaitingForUserInput (if text-only response).Transitions:- Has tool calls →
ExecutingTools - No tool calls →
WaitingForUserInput ShutdownRequested→ShuttingDown
ExecutingTools
ExecutingTools
Tracks multiple concurrent tool executionsEach execution progresses through
Pending → Running → Completed. The state tracks all executions via Vec<ToolExecutionStatus>.Transitions:ToolCompleted(some pending) →ExecutingToolsToolCompleted(all done, mutating tools) →PostToolsHookToolCompleted(all done, no mutation) →CallingLlmShutdownRequested→ShuttingDown
PostToolsHook
PostToolsHook
Runs post-tool hooks after tool executionThis state enables features like auto-commit that need to run after file-modifying tools (e.g.,
edit_file, bash).Fields:pending_llm_request- The next LLM request to send after hooks completecompleted_tools- Information about which tools completed (for hook decision-making)
PostToolsHookCompleted→CallingLlmShutdownRequested→ShuttingDown
Error
Error
Holds failed state with retry informationThe
origin field (Llm, Tool, or Io) determines retry strategy.Transitions:RetryTimeoutFired→CallingLlmShutdownRequested→ShuttingDown
ShuttingDown
ShuttingDown
Terminal stateNo transitions out. The agent should be dropped after reaching this state.
Events
AgentEvent Enum
LlmEvent Sub-variants
ToolExecutionOutcome
Actions
AgentAction Enum
Actions are returned to the caller indicating what I/O operation to perform:The caller is responsible for executing actions and feeding events back into the state machine via
agent.handle_event(event).State Transitions
Transition Table
| Current State | Event | New State | Action |
|---|---|---|---|
WaitingForUserInput | UserInput(msg) | CallingLlm | SendLlmRequest |
CallingLlm | LlmEvent::TextDelta | CallingLlm | DisplayMessage |
CallingLlm | LlmEvent::ToolCallDelta | CallingLlm | WaitForInput |
CallingLlm | LlmEvent::Completed | ProcessingLlmResponse | (internal) |
CallingLlm | LlmEvent::Error (retries < max) | Error | WaitForInput |
CallingLlm | LlmEvent::Error (retries >= max) | WaitingForUserInput | DisplayError |
ProcessingLlmResponse | (has tool calls) | ExecutingTools | ExecuteTools |
ProcessingLlmResponse | (no tool calls) | WaitingForUserInput | WaitForInput |
ExecutingTools | ToolCompleted (some pending) | ExecutingTools | WaitForInput |
ExecutingTools | ToolCompleted (all done, mutating) | PostToolsHook | RunPostToolsHook |
ExecutingTools | ToolCompleted (all done, no mutation) | CallingLlm | SendLlmRequest |
PostToolsHook | PostToolsHookCompleted | CallingLlm | SendLlmRequest |
Error (origin=Llm) | RetryTimeoutFired | CallingLlm | SendLlmRequest |
| any state | ShutdownRequested | ShuttingDown | Shutdown |
Implementation
Core Method
The
handle_event method is synchronous and returns immediately. No async operations are performed inside the state machine.Example Usage
Design Decisions
Why Explicit State Machine vs Implicit
Testability
Every state and transition can be unit tested in isolation. Property-based tests verify invariants like “shutdown always succeeds from any state”.
Debuggability
State transitions are logged with
tracing::info!, making it easy to trace agent behavior in production.No Hidden State
All context is carried explicitly in state variants. There are no ambient flags or mutable fields that could get out of sync.
Why Events Are Processed Synchronously
Thehandle_event method is synchronous and returns immediately:
- Separation of Concerns - The state machine decides what to do; the caller decides how to do it (async, parallel, etc.)
- Backpressure - The caller controls the pace of event delivery. No internal queues or background tasks
- Determinism - Given the same sequence of events, the state machine produces the same sequence of actions (essential for testing and replay)
- Flexibility - The caller can implement different execution strategies (single-threaded, tokio, async-std) without changing the state machine
How Conversation Context Is Threaded
Each state variant carries its ownConversationContext:
UserInput→ message appended to conversationLlmEvent::Completed→ assistant message appendedToolCompleted(all done) → tool result messages appended
Testing Guidelines
Unit Tests
Verify specific state transitions in isolation:Property Tests
Verify invariants hold across all configurations:Integration Tests
Verify end-to-end flows through multiple transitions:Extension Guide
Adding a New State
Adding a New Event
Source Files
state.rs
State and event type definitions
agent.rs
State machine implementation
State Machine Spec
Detailed state machine specification
Architecture Overview
High-level system architecture