12 KiB
12 KiB
Agent Loop Sequence Diagram
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 Tool execution mode == 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 Tool execution mode == 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:
- Pending message handling
- LLM calls with streaming
- Tool execution (sequential/parallel)
- Turn lifecycle events
- Next turn preparation
- Stop condition checking
Lifecycle Events
AgentStartEvent/AgentEndEvent- Agent lifecycle boundariesTurnStartEvent/TurnEndEvent- Conversation turnsMessageStartEvent/MessageEndEvent- Message processingToolExecutionStartEvent/ToolExecutionEndEvent- Tool execution
Data Flow
- Input: User messages → Agent normalization → AgentLoop
- LLM Call: Messages transformed → LLM stream → Assistant message
- Tool Execution: Tool calls extracted → Prepared → Executed → Results
- State Update: New messages appended to context
- Output: AgentEndEvent with final messages
Queue Processing Order
- Initial steering messages (if any)
- Main loop:
- Steering/follow-up messages (if any)
- LLM call
- Tool execution (if any)
- Turn end event
- Next turn preparation
- Check stop condition
- Follow-up messages (after loop exits)