The coding agent uses an event-driven state machine that separates logic from I/O.
The StateMachine is pure and synchronous. It receives AgentEvents and returns AgentActions. The caller (runner) executes actions and feeds results back as events.
User Input → Event → StateMachine → Action → Runner executes → Event → ...
| State | Description |
|---|---|
WaitingForUserInput |
Idle, ready for user message |
CallingLlm |
Making API request (tracks retry count) |
ProcessingLlmResponse |
Transient state parsing LLM response |
ExecutingTools |
Running tool calls from LLM |
Error |
Recoverable error, will retry |
ShuttingDown |
Terminal state |
UserInput(String)- User submitted a messageLlmCompleted { content, stop_reason }- API call succeededLlmError(String)- API call failedToolCompleted { call_id, result }- Tool finished executingRetryTimeout- Retry delay elapsedShutdownRequested- User requested quit
SendLlmRequest { messages }- Call Claude APIExecuteTools { calls }- Run specified toolsDisplayText(String)- Show text to userDisplayError(String)- Show error to userPromptForInput- Wait for user inputScheduleRetry { delay_ms }- Wait then send RetryTimeoutWaitForEvent- No action neededShutdown- Terminate
WaitingForUserInput --UserInput--> CallingLlm --LlmCompleted--> ProcessingLlmResponse
| |
| +---------+---------+
| | |
LlmError has tools no tools
| | |
v v v
Error ExecutingTools WaitingForUserInput
| |
RetryTimeout ToolCompleted
| (all done)
v |
CallingLlm <--------------+
ShutdownRequested transitions to ShuttingDown from any state.
- Max retries: 3
- Exponential backoff: 1s, 2s, 3s
- After max retries, returns to
WaitingForUserInputwith error message
- State machine is pure - No I/O, no side effects, fully testable
- Caller executes actions - Runner handles API calls, tool execution, user I/O
- Conversation travels with state - Full message history in each state variant
- Exhaustive matching - Rust ensures all event/state combinations are handled