298 lines
12 KiB
Markdown
298 lines
12 KiB
Markdown
# Agent Loop Sequence Diagram
|
|
|
|
```mermaid
|
|
sequenceDiagram
|
|
participant User
|
|
participant Agent
|
|
participant AgentLoop
|
|
participant LLM
|
|
participant Tool
|
|
participant Session
|
|
|
|
User->>Agent: prompt(messages)
|
|
activate Agent
|
|
Agent->>Agent: normalizePromptInput(messages)
|
|
Agent->>Agent: runPromptMessages(messages)
|
|
|
|
alt Has queued steering messages
|
|
Agent->>Agent: drain(steering_queue)
|
|
Agent->>Agent: runPromptMessages(queued_steering)
|
|
else Has follow-up messages
|
|
Agent->>Agent: drain(follow_up_queue)
|
|
Agent->>Agent: runPromptMessages(queued_follow_ups)
|
|
else Normal prompt
|
|
Agent->>Agent: createLoopConfig(options)
|
|
Agent->>Agent: createActiveRun()
|
|
|
|
Note over Agent,Session: Start AgentLoop
|
|
Agent->>AgentLoop: agentLoop(messages, context, config, signal, stream_fn)
|
|
activate AgentLoop
|
|
|
|
Note over AgentLoop: Emit AgentStartEvent
|
|
AgentLoop->>AgentLoop: emit(AgentStartEvent())
|
|
|
|
loop For each prompt message
|
|
AgentLoop->>AgentLoop: emit(MessageStartEvent(msg))
|
|
AgentLoop->>AgentLoop: emit(MessageEndEvent(msg))
|
|
end
|
|
|
|
loop Main Agent Loop
|
|
Note over AgentLoop: 1. Handle queued messages
|
|
|
|
alt Steering/Follow-up messages exist
|
|
AgentLoop->>AgentLoop: drain(steering_queue)
|
|
loop For each pending message
|
|
AgentLoop->>AgentLoop: emit(MessageStartEvent(msg))
|
|
AgentLoop->>AgentLoop: push to context.messages
|
|
AgentLoop->>AgentLoop: emit(MessageEndEvent(msg))
|
|
end
|
|
end
|
|
|
|
Note over AgentLoop: 2. Get LLM Response
|
|
|
|
AgentLoop->>AgentLoop: transform_context?(messages)
|
|
AgentLoop->>AgentLoop: convert_to_llm(messages)
|
|
AgentLoop->>LLM: stream_function(model, context, config)
|
|
activate LLM
|
|
LLM-->>AgentLoop: response stream
|
|
|
|
alt Streaming enabled
|
|
loop For each event in stream
|
|
AgentLoop->>AgentLoop: emit(MessageStartEvent)
|
|
AgentLoop->>AgentLoop: emit(MessageUpdateEvent)
|
|
end
|
|
LLM-->>AgentLoop: final message
|
|
else Non-streaming
|
|
LLM-->>AgentLoop: complete response
|
|
end
|
|
|
|
deactivate LLM
|
|
AgentLoop->>AgentLoop: emit(MessageEndEvent(final_message))
|
|
AgentLoop->>AgentLoop: push message to context.messages
|
|
|
|
Note over AgentLoop: 3. Check for Tool Calls
|
|
|
|
AgentLoop->>AgentLoop: extract_tool_calls(message.content)
|
|
|
|
alt Tool calls exist
|
|
Note over AgentLoop: 4. Execute Tools
|
|
|
|
alt SEQUENTIAL or has_sequential_tool_call
|
|
AgentLoop->>AgentLoop: executeToolCallsSequential(...)
|
|
activate AgentLoop
|
|
|
|
loop For each tool call
|
|
AgentLoop->>AgentLoop: emit(ToolExecutionStartEvent)
|
|
|
|
alt Tool not found
|
|
AgentLoop->>AgentLoop: createErrorToolResult
|
|
AgentLoop->>AgentLoop: emit(ToolExecutionEndEvent)
|
|
else Tool found
|
|
alt before_tool_call hook exists
|
|
AgentLoop->>AgentLoop: before_tool_call(context, signal)
|
|
alt Hook blocks execution
|
|
AgentLoop->>AgentLoop: createErrorToolResult
|
|
AgentLoop->>AgentLoop: emit(ToolExecutionEndEvent)
|
|
else Execution allowed
|
|
AgentLoop->>Agent: prepareToolCall(tool, tool_call, args)
|
|
activate Agent
|
|
|
|
alt Has prepare_arguments
|
|
Agent->>Agent: prepare_arguments(args)
|
|
deactivate Agent
|
|
activate AgentLoop
|
|
AgentLoop->>AgentLoop: validateToolArguments
|
|
end
|
|
|
|
alt Has before_tool_call hook
|
|
AgentLoop->>Agent: before_tool_call(context, signal)
|
|
alt Hook blocks
|
|
AgentLoop->>AgentLoop: createErrorToolResult
|
|
else
|
|
AgentLoop->>Tool: execute(tool_call_id, args, signal, on_update)
|
|
activate Tool
|
|
Tool-->>AgentLoop: result
|
|
deactivate Tool
|
|
end
|
|
else No hooks
|
|
AgentLoop->>Tool: execute(tool_call_id, args, signal, on_update)
|
|
activate Tool
|
|
Tool-->>AgentLoop: result
|
|
deactivate Tool
|
|
end
|
|
|
|
alt Has after_tool_call hook
|
|
AgentLoop->>Agent: after_tool_call(context, signal)
|
|
alt Hook modifies result
|
|
AgentLoop->>AgentLoop: apply after_result
|
|
end
|
|
end
|
|
|
|
AgentLoop->>AgentLoop: emit(ToolExecutionEndEvent)
|
|
AgentLoop->>AgentLoop: createToolResultMessage
|
|
AgentLoop->>AgentLoop: emit(MessageStartEvent)
|
|
AgentLoop->>AgentLoop: emit(MessageEndEvent)
|
|
AgentLoop->>AgentLoop: push to context.messages
|
|
end
|
|
end
|
|
deactivate Agent
|
|
activate AgentLoop
|
|
end
|
|
|
|
alt Signal aborted
|
|
break
|
|
end
|
|
end
|
|
|
|
AgentLoop->>AgentLoop: return ExecutedToolCallBatch
|
|
deactivate AgentLoop
|
|
activate AgentLoop
|
|
else PARALLEL
|
|
AgentLoop->>AgentLoop: executeToolCallsParallel(...)
|
|
activate AgentLoop
|
|
|
|
loop For each tool call
|
|
alt Tool not found or immediate
|
|
AgentLoop->>AgentLoop: execute synchronously
|
|
else Needs execution
|
|
AgentLoop->>AgentLoop: spawn async task
|
|
end
|
|
end
|
|
|
|
AgentLoop->>AgentLoop: wait for all tasks
|
|
AgentLoop->>AgentLoop: collect results
|
|
AgentLoop->>AgentLoop: return ExecutedToolCallBatch
|
|
deactivate AgentLoop
|
|
activate AgentLoop
|
|
end
|
|
|
|
Note over AgentLoop: 5. Emit Turn End Event
|
|
AgentLoop->>AgentLoop: emit(TurnEndEvent(message, tool_results))
|
|
|
|
Note over AgentLoop: 6. Prepare Next Turn
|
|
|
|
alt Has prepare_next_turn hook
|
|
AgentLoop->>Agent: prepare_next_turn(context)
|
|
activate Agent
|
|
alt Returns AgentLoopTurnUpdate
|
|
Agent->>Agent: update context
|
|
Agent->>Agent: update model
|
|
Agent->>Agent: update thinking_level
|
|
Agent->>AgentLoop: return updated config
|
|
deactivate Agent
|
|
activate AgentLoop
|
|
end
|
|
end
|
|
|
|
Note over AgentLoop: 7. Check Stop Condition
|
|
|
|
alt should_stop_after_turn returns true
|
|
AgentLoop->>AgentLoop: emit(AgentEndEvent)
|
|
break
|
|
end
|
|
|
|
Note over AgentLoop: 8. Get Next Steering Messages
|
|
AgentLoop->>AgentLoop: drain(steering_queue)
|
|
|
|
else No tool calls
|
|
Note over AgentLoop: 5. Emit Turn End Event
|
|
AgentLoop->>AgentLoop: emit(TurnEndEvent(message, []))
|
|
|
|
Note over AgentLoop: 6. Prepare Next Turn
|
|
alt Has prepare_next_turn hook
|
|
AgentLoop->>Agent: prepare_next_turn(context)
|
|
activate Agent
|
|
alt Returns AgentLoopTurnUpdate
|
|
Agent->>Agent: update context
|
|
Agent->>Agent: update model
|
|
Agent->>Agent: update thinking_level
|
|
Agent->>AgentLoop: return updated config
|
|
deactivate Agent
|
|
activate AgentLoop
|
|
end
|
|
end
|
|
|
|
Note over AgentLoop: 7. Check Stop Condition
|
|
alt should_stop_after_turn returns true
|
|
AgentLoop->>AgentLoop: emit(AgentEndEvent)
|
|
break
|
|
end
|
|
|
|
Note over AgentLoop: 8. Get Next Steering Messages
|
|
AgentLoop->>AgentLoop: drain(steering_queue)
|
|
end
|
|
|
|
alt Pending messages exist
|
|
continue loop
|
|
end
|
|
|
|
Note over AgentLoop: 9. Get Follow-up Messages
|
|
AgentLoop->>AgentLoop: drain(follow_up_queue)
|
|
|
|
alt Follow-up messages exist
|
|
AgentLoop->>AgentLoop: set pending_messages = follow_up
|
|
continue loop
|
|
end
|
|
|
|
break
|
|
end
|
|
|
|
Note over AgentLoop: 10. Final Agent End Event
|
|
AgentLoop->>AgentLoop: emit(AgentEndEvent(messages))
|
|
deactivate AgentLoop
|
|
deactivate Agent
|
|
|
|
Note over Agent,Session: Persist to Session
|
|
Agent->>Session: appendMessage(message)
|
|
|
|
User<<--Agent: messages (via promise)
|
|
end
|
|
|
|
Note over Agent: Resume normal operation
|
|
deactivate Agent
|
|
```
|
|
|
|
## Key Components
|
|
|
|
### Agent Layer
|
|
- **Agent**: High-level wrapper managing state, queuing, and events
|
|
- **steering_queue**: Messages injected after assistant turn completes
|
|
- **follow_up_queue**: Messages that run only when agent would otherwise stop
|
|
|
|
### AgentLoop Layer
|
|
- **agentLoop**: Entry point for new prompts
|
|
- **agentLoopContinue**: Continue from existing transcript
|
|
- **runLoop**: Main execution loop handling:
|
|
1. Pending message handling
|
|
2. LLM calls with streaming
|
|
3. Tool execution (sequential/parallel)
|
|
4. Turn lifecycle events
|
|
5. Next turn preparation
|
|
6. Stop condition checking
|
|
|
|
### Lifecycle Events
|
|
- `AgentStartEvent` / `AgentEndEvent` - Agent lifecycle boundaries
|
|
- `TurnStartEvent` / `TurnEndEvent` - Conversation turns
|
|
- `MessageStartEvent` / `MessageEndEvent` - Message processing
|
|
- `ToolExecutionStartEvent` / `ToolExecutionEndEvent` - Tool execution
|
|
|
|
## Data Flow
|
|
|
|
1. **Input**: User messages → Agent normalization → AgentLoop
|
|
2. **LLM Call**: Messages transformed → LLM stream → Assistant message
|
|
3. **Tool Execution**: Tool calls extracted → Prepared → Executed → Results
|
|
4. **State Update**: New messages appended to context
|
|
5. **Output**: AgentEndEvent with final messages
|
|
|
|
## Queue Processing Order
|
|
|
|
1. Initial steering messages (if any)
|
|
2. Main loop:
|
|
- Steering/follow-up messages (if any)
|
|
- LLM call
|
|
- Tool execution (if any)
|
|
- Turn end event
|
|
- Next turn preparation
|
|
- Check stop condition
|
|
3. Follow-up messages (after loop exits)
|