From 072d0e16afcd1e5b82a4b37f1d80ed16e991bcdf Mon Sep 17 00:00:00 2001 From: narawat Date: Tue, 28 Jul 2026 09:57:23 +0700 Subject: [PATCH] update --- docs/agent_loop_sequence_diagram.md | 485 ++++++++++++---------------- 1 file changed, 204 insertions(+), 281 deletions(-) diff --git a/docs/agent_loop_sequence_diagram.md b/docs/agent_loop_sequence_diagram.md index f2da540..38ad805 100644 --- a/docs/agent_loop_sequence_diagram.md +++ b/docs/agent_loop_sequence_diagram.md @@ -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 ─┘ +```