diff --git a/docs/agent_loop_sequence_diagram.md b/docs/agent_loop_sequence_diagram.md new file mode 100644 index 0000000..2f7025b --- /dev/null +++ b/docs/agent_loop_sequence_diagram.md @@ -0,0 +1,298 @@ +# 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 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: + 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)