This commit is contained in:
2026-07-28 09:57:23 +07:00
parent b18a5f34a8
commit 072d0e16af
+204 -281
View File
@@ -1,298 +1,221 @@
# Agent Loop Sequence Diagram
# Agent Loop Sequence Diagram (ASCII)
```mermaid
sequenceDiagram
participant User
participant Agent
participant AgentLoop
participant LLM
participant Tool
participant Session
```
┌─────────────┐ ┌─────────────┐ ┌─────────────────┐ ┌───────────┐
│ User │─────>│ Agent │─────>│ AgentLoop │─────>│ LLM │
└─────────────┘ └─────────────┘ └─────────────────┘ └───────────┘
┌─────────────┐ ┌─────────────────┐ ┌───────────┐ ┌─────────────┐
User │<─────│ AgentLoop │─────>│ Agent │─────>│ Session
└─────────────┘ └─────────────────┘ └───────────┘ └─────────────┘
┌─────────────┐
│ 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 execution
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
AgentLoop->>AgentLoop: before_tool_call(context, signal)
alt Hook blocks
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
end
alt Parallel execution
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
loop Main Agent Loop
Note over AgentLoop: Continue loop
end
else Follow-up messages exist
AgentLoop->>AgentLoop: set pending_messages = follow_up
loop Main Agent Loop
Note over AgentLoop: Continue loop
end
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
┌─────────────────────────────────────────────────────────────────────────────────────────┐
│ AGENT LOOP MAIN FLOW │
└─────────────────────────────────────────────────────────────────────────────────────────┘
User
│ prompt(messages)
Agent
│ normalizePromptInput()
│ runPromptMessages()
├─ alt steering_queue has messages
│ │ drain(steering_queue)
│ │ runPromptMessages()
├─ alt follow_up_queue has messages
│ │ drain(follow_up_queue)
│ │ runPromptMessages()
└─ else (normal prompt)
│ createLoopConfig()
│ createActiveRun()
AgentLoop
│ agentLoop(messages, context, config)
┌─────────────────────────────────────────────────────────────┐
│ MAIN LOOP │
├─────────────────────────────────────────────────────────────┤
│ │
│ 1. Handle queued messages │
│ ├─ drain(steering_queue) │
│ ├─ emit(MessageStartEvent) │
│ ├─ push to context.messages │
│ └─ emit(MessageEndEvent) │
│ │
│ 2. Get LLM Response │
│ ├─ transform_context?(messages) │
│ ├─ convert_to_llm(messages) │
│ └─ stream_function(model, context, config) │
│ │
│ LLM │
│ │ response stream │
│ └─ emit(MessageStartEvent) │
│ └─ emit(MessageUpdateEvent) (streaming) │
│ └─ emit(MessageEndEvent(final_message)) │
│ │
│ 3. Check for Tool Calls │
│ └─ extract_tool_calls(message.content) │
│ │
│ 4. Execute Tools (if any) │
│ ├─ emit(ToolExecutionStartEvent) │
│ ├─ prepareToolCall() │
│ │ ├─ before_tool_call? (block if needed) │
│ │ └─ validateToolArguments() │
│ ├─ execute(tool_call_id, args) │
│ │ Tool │
│ │ │ execute() │
│ │ └─ on_update(partial_result) │
│ ├─ emit(ToolExecutionEndEvent) │
│ └─ createToolResultMessage() │
│ │
│ 5. Emit Turn End Event │
│ └─ emit(TurnEndEvent(message, tool_results)) │
│ │
│ 6. Prepare Next Turn │
│ └─ prepare_next_turn(context) │
│ └─ AgentLoopTurnUpdate │
│ │
│ 7. Check Stop Condition │
│ └─ should_stop_after_turn? (break if true) │
│ │
│ 8. Get Steering/Follow-up Messages │
│ ├─ drain(steering_queue) │
│ └─ drain(follow_up_queue) │
│ │
│ 9. Continue or Break │
│ ├─ alt pending messages exist → continue loop │
│ └─ else → break │
│ │
└─────────────────────────────────────────────────────────────┘
AgentLoop
│ emit(AgentEndEvent(messages))
Agent
│ appendMessage() → Session
User (via promise)
```
## Key Components
## 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
```
┌──────────────────────────────────────────────────────────────┐
│ Agent │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ state: AgentState │ │
│ │ - system_prompt │ │
│ │ - model │ │
│ │ - messages[] │ │
│ │ - tools[] │ │
│ └──────────────────────────────────────────────────────────┘ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ Queues: │ │
│ │ - steering_queue: inject after assistant turn │ │
│ │ - follow_up_queue: run when agent stops │ │
│ └──────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────┘
```
### 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
```
┌──────────────────────────────────────────────────────────────┐
│ AgentLoop │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ agentLoop() │ │
│ │ - Main loop execution │ │
│ │ - Tool orchestration │ │
│ │ - Event emission │ │
│ └──────────────────────────────────────────────────────────┘ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ executeToolCallsSequential() │ │
│ │ executeToolCallsParallel() │ │
│ │ - Tool execution coordination │ │
│ │ - Hook invocation │ │
│ │ - Result collection │ │
│ └──────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────┘
```
### Lifecycle Events
- `AgentStartEvent` / `AgentEndEvent` - Agent lifecycle boundaries
- `TurnStartEvent` / `TurnEndEvent` - Conversation turns
- `MessageStartEvent` / `MessageEndEvent` - Message processing
- `ToolExecutionStartEvent` / `ToolExecutionEndEvent` - Tool execution
### Lifecycle Events Flow
```
AgentStartEvent
├─ TurnStartEvent
│ ├─ MessageStartEvent (user/assistant)
│ ├─ MessageEndEvent
│ ├─ ToolExecutionStartEvent
│ │ ├─ ToolExecutionUpdateEvent (streaming)
│ │ └─ ToolExecutionEndEvent
│ └─ TurnEndEvent
└─ AgentEndEvent (with final messages)
```
## Data Flow
## QUEUE PROCESSING ORDER
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:
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)
3. **Follow-up messages** (after loop exits, only if no steering)
## DATA FLOW
```
Input:
User messages → Agent.normalizePromptInput() → AgentLoop
LLM Call:
AgentMessage[] → transform_context() → convert_to_llm() →
LLM.stream() → AssistantMessage
Tool Execution:
ToolCall[] → prepareToolCall() →
before_tool_call? → execute() → after_tool_call? →
ToolResultMessage[]
Output:
AgentEndEvent(messages) → Session.appendMessage() → User promise
```
## EXECUTION MODES
### Sequential (EXECUTION_SEQUENTIAL)
```
ToolCall1 → ToolCall2 → ToolCall3
│ │ │
▼ ▼ ▼
Result1 Result2 Result3
```
### Parallel (EXECUTION_PARALLEL)
```
ToolCall1 ─┐
ToolCall2──┼→ Execute all → Wait for all → Results
ToolCall3 ─┘
```