From a4edfd2de5b4f0731ca8460e314ab0430f40d3a7 Mon Sep 17 00:00:00 2001 From: narawat Date: Tue, 28 Jul 2026 10:59:38 +0700 Subject: [PATCH] update --- docs/agent_loop_diagram.md | 575 +++++++++++++++++-------------------- 1 file changed, 260 insertions(+), 315 deletions(-) diff --git a/docs/agent_loop_diagram.md b/docs/agent_loop_diagram.md index 2e35f96..92e62cb 100644 --- a/docs/agent_loop_diagram.md +++ b/docs/agent_loop_diagram.md @@ -1,325 +1,270 @@ # Agent Loop Diagram -## Overview +``` +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ AGENT LOOP │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ -This document describes the agent loop execution flow in AgentCore.jl, showing how the agent processes messages, executes tools, and handles steering/follow-up messages. +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ 1. INITIALIZATION │ +│ ┌──────────────────────┐ │ +│ │ Agent.start(prompt) │ │ +│ └──────────┬───────────┘ │ +│ │ │ +│ ▼ │ +│ ┌──────────────────────┐ │ +│ │ normalizePrompt() │ ← Convert input (String/Message/Vector) to AgentMessage[] │ +│ └──────────┬───────────┘ │ +│ │ │ +│ ▼ │ +│ ┌──────────────────────┐ │ +│ │ runPromptMessages() │ │ +│ └──────────┬───────────┘ │ +│ │ │ +│ ▼ │ +│ ┌───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ │ +│ │ 2. AGENT LOOP START (agentLoop/runAgentLoop) │ │ +│ │ │ │ +│ │ emit(AgentStartEvent) │ │ +│ │ emit(TurnStartEvent) │ │ +│ │ │ │ +│ │ for prompt in prompts: │ │ +│ │ emit(MessageStartEvent(prompt)) │ │ +│ │ emit(MessageEndEvent(prompt)) │ │ +│ │ │ │ +│ │ ┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ │ │ +│ │ │ 3. MAIN LOOP (while true) │ │ │ +│ │ │ │ │ │ +│ │ │ ┌───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ │ │ │ +│ │ │ │ 4. PENDING MESSAGE HANDLING │ │ │ │ +│ │ │ │ │ │ │ │ +│ │ │ │ pending_messages = getSteeringMessages() │ │ │ │ +│ │ │ │ │ │ │ │ +│ │ │ │ while has pending_messages OR has_tool_calls: │ │ │ │ +│ │ │ │ │ │ │ │ +│ │ │ │ if pending_messages: │ │ │ │ +│ │ │ │ for msg in pending_messages: │ │ │ │ +│ │ │ │ emit(MessageStartEvent) │ │ │ │ +│ │ │ │ emit(MessageEndEvent) │ │ │ │ +│ │ │ │ push to context.messages │ │ │ │ +│ │ │ │ pending_messages = [] │ │ │ │ +│ │ │ │ │ │ │ │ +│ │ │ └───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ │ │ │ +│ │ │ │ │ │ +│ │ │ ┌───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ │ │ │ +│ │ │ │ 5. STREAM ASSISTANT RESPONSE │ │ │ │ +│ │ │ │ │ │ │ │ +│ │ │ │ a) transform_context (if configured) │ │ │ │ +│ │ │ │ │ │ │ │ +│ │ │ │ b) convert_to_llm(messages) │ │ │ │ +│ │ │ │ ┌──────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ │ │ │ │ +│ │ │ │ │ Converts AgentMessage[] to Message[] (filters out compaction/branch summaries, converts bash/custom) │ │ │ │ │ +│ │ │ │ └──────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ │ │ │ │ +│ │ │ │ │ │ │ │ +│ │ │ │ c) create Context(system_prompt, llm_messages, tools) │ │ │ │ +│ │ │ │ │ │ │ │ +│ │ │ │ d) stream_function(model, context, config) │ │ │ │ +│ │ │ │ ┌──────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ │ │ │ │ +│ │ │ │ │ LLM Stream Events: │ │ │ │ │ +│ │ │ │ │ • start → create partial AssistantMessage │ │ │ │ │ +│ │ │ │ │ • text_start/delta/end → update partial message │ │ │ │ │ +│ │ │ │ │ • thinking_start/delta/end → update partial message │ │ │ │ │ +│ │ │ │ │ • toolcall_start/delta/end → update partial message │ │ │ │ │ +│ │ │ │ │ • done → finalize message │ │ │ │ │ +│ │ │ │ │ • error → handle error │ │ │ │ │ +│ │ │ │ └──────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ │ │ │ │ +│ │ │ │ │ │ │ │ +│ │ │ │ e) emit(MessageStartEvent(final_message)) │ │ │ │ +│ │ │ │ f) emit(MessageEndEvent(final_message)) │ │ │ │ +│ │ │ │ g) push to context.messages & new_messages │ │ │ │ +│ │ │ │ │ │ │ │ +│ │ │ └───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ │ │ │ +│ │ │ │ │ │ +│ │ │ ┌───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ │ │ │ +│ │ │ │ 6. CHECK STOP REASON │ │ │ │ +│ │ │ │ │ │ │ │ +│ │ │ │ if stop_reason in ("error", "aborted"): │ │ │ │ +│ │ │ │ emit(TurnEndEvent) │ │ │ │ +│ │ │ │ emit(AgentEndEvent) ← EXIT LOOP │ │ │ │ +│ │ │ │ return │ │ │ │ +│ │ │ │ │ │ │ │ +│ │ │ └───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ │ │ │ +│ │ │ │ │ │ +│ │ │ ┌───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ │ │ │ +│ │ │ │ 7. EXTRACT TOOL CALLS │ │ │ │ +│ │ │ │ │ │ │ │ +│ │ │ │ tool_calls = filter(content, isa ToolCall) │ │ │ │ +│ │ │ │ │ │ │ │ +│ │ │ │ if tool_calls: │ │ │ │ +│ │ │ │ │ │ │ │ +│ │ │ │ ┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ │ │ │ │ +│ │ │ │ │ 8. EXECUTE TOOL CALLS │ │ │ │ │ +│ │ │ │ │ │ │ │ │ │ +│ │ │ │ │ if config.tool_execution == SEQUENTIAL │ │ │ │ │ +│ │ │ │ │ OR has_sequential_tool: │ │ │ │ │ +│ │ │ │ │ executeToolCallsSequential() │ │ │ │ │ +│ │ │ │ │ else: │ │ │ │ │ +│ │ │ │ │ executeToolCallsParallel() │ │ │ │ │ +│ │ │ │ │ │ │ │ │ │ +│ │ │ │ │ ┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ │ │ │ │ │ +│ │ │ │ │ │ For EACH tool_call: │ │ │ │ │ │ +│ │ │ │ │ │ │ │ │ │ │ │ +│ │ │ │ │ │ emit(ToolExecutionStartEvent) │ │ │ │ │ │ +│ │ │ │ │ │ │ │ │ │ │ │ +│ │ │ │ │ │ prepareToolCall(): │ │ │ │ │ │ +│ │ │ │ │ │ • Find tool by name │ │ │ │ │ │ +│ │ │ │ │ │ • prepare_arguments (if configured) │ │ │ │ │ │ +│ │ │ │ │ │ • validateToolArguments │ │ │ │ │ │ +│ │ │ │ │ │ • before_tool_call hook (if configured) │ │ │ │ │ │ +│ │ │ │ │ │ └─→ PreparedToolCall("prepared") or ImmediateToolCallOutcome("immediate") │ │ │ │ │ │ +│ │ │ │ │ │ │ │ │ │ │ │ +│ │ │ │ │ │ if "immediate": │ │ │ │ │ │ +│ │ │ │ │ │ FinalizedToolCallOutcome (synchronous tool) │ │ │ │ │ │ +│ │ │ │ │ │ else: │ │ │ │ │ │ +│ │ │ │ │ │ executePreparedToolCall() → ExecutedToolCallOutcome │ │ │ │ │ │ +│ │ │ │ │ │ finalizeExecutedToolCall() (after_tool_call hook) │ │ │ │ │ │ +│ │ │ │ │ │ └─→ FinalizedToolCallOutcome │ │ │ │ │ │ +│ │ │ │ │ │ │ │ │ │ │ │ +│ │ │ │ │ │ emit(ToolExecutionEndEvent) │ │ │ │ │ │ +│ │ │ │ │ │ createToolResultMessage() │ │ │ │ │ │ +│ │ │ │ │ │ emitToolResultMessage() │ │ │ │ │ │ +│ │ │ │ │ │ │ │ │ │ │ │ +│ │ │ │ │ └─────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ │ │ │ │ │ +│ │ │ │ │ │ │ │ │ │ +│ │ │ │ │ Append tool_result_messages to context.messages & new_messages │ │ │ │ │ +│ │ │ │ │ │ │ │ │ │ +│ │ │ │ └─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ │ │ │ │ +│ │ │ │ │ │ │ │ +│ │ │ └───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ │ │ │ +│ │ │ │ │ │ +│ │ │ emit(TurnEndEvent(message, tool_results)) │ │ │ +│ │ │ │ │ │ +│ │ │ ┌───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ │ │ │ +│ │ │ │ 9. PREPARE NEXT TURN │ │ │ │ +│ │ │ │ │ │ │ │ +│ │ │ │ next_turn_context = PrepareNextTurnContext( │ │ │ │ +│ │ │ │ message, tool_results, current_context, new_messages │ │ │ │ +│ │ │ │ ) │ │ │ │ +│ │ │ │ │ │ │ │ +│ │ │ │ next_turn_snapshot = prepare_next_turn(config, next_turn_context) │ │ │ │ +│ │ │ │ │ │ │ │ +│ │ │ │ if !isnothing(next_turn_snapshot): │ │ │ │ +│ │ │ │ update context, model, thinking_level │ │ │ │ +│ │ │ │ │ │ │ │ +│ │ │ │ if should_stop_after_turn(config, next_turn_context): │ │ │ │ +│ │ │ │ emit(AgentEndEvent) ← EXIT LOOP │ │ │ │ +│ │ │ │ return │ │ │ │ +│ │ │ │ │ │ │ │ +│ │ │ └───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ │ │ │ +│ │ │ │ │ │ +│ │ │ pending_messages = getSteeringMessages() ← Check for new steering messages │ │ │ +│ │ │ │ │ │ +│ │ └───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ │ │ +│ │ │ │ +│ │ follow_up_messages = getFollowUpMessages() │ │ +│ │ │ │ +│ │ if follow_up_messages: │ │ +│ │ pending_messages = follow_up_messages ← Continue loop to handle follow-ups │ │ +│ │ continue │ │ +│ │ │ │ +│ │ break ← EXIT MAIN LOOP (no more pending messages) │ │ +│ │ │ │ +│ │ emit(AgentEndEvent(new_messages)) │ │ +│ │ │ │ +│ └───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ │ +│ │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ -## Architecture Layers +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ HELPER STRUCTURES │ +│ │ +│ AgentMessage (union of): │ +│ • UserMessage ("user") │ +│ • AssistantMessage ("assistant") │ +│ • ToolResultMessage ("toolResult") │ +│ • BranchSummaryMessage ("branchSummary") - converted to UserMessage │ +│ • CompactionSummaryMessage ("compactionSummary") - converted to UserMessage │ +│ • BashExecutionMessage ("bash") - converted to UserMessage if not excluded │ +│ • CustomMessage ("custom") - converted to UserMessage │ +│ │ +│ Message (LLM interface): │ +│ • UserMessage (role: "user") │ +│ • AssistantMessage (role: "assistant") │ +│ • ToolResultMessage (role: "toolResult") │ +│ │ +│ Steering vs Follow-up: │ +│ • Steering: Injected AFTER current assistant turn finishes (can continue conversation) │ +│ • Follow-up: Run ONLY after agent would otherwise stop (final messages) │ +│ │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ + +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ DATA FLOW SUMMARY │ +│ │ +│ User Input LLM Response │ +│ │ convert_to_llm() │ │ +│ ├───────────────────────────────────→ AgentMessage[] ──────────────→ Message[] ──────────→ │ ─────────────────→ AssistantMessage (streamed) │ +│ │ (filter+convert) │ │ +│ │ │ │ │ +│ │ ▼ │ │ +│ │ Context (system_prompt, │ │ +│ │ messages, tools) │ │ +│ │ │ │ +│ │ │ │ +│ │ stream_function() ─────────┤ ─────────→ LLM API │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ ▼ │ +│ │ LLM Stream Events (text_delta, toolcall_delta, ...) │ +│ │ │ │ +│ │ ▼ │ +│ │ AssistantMessage (finalized) │ +│ │ │ │ +│ │ ▼ │ +│ │ check tool_calls? ──────────→ YES ────────────→ execute tools │ +│ │ │ │ │ +│ │ │ │ │ +│ │ NO │ │ +│ │ │ │ │ +│ │ │ ▼ ▼ +│ │ │ emit ToolResultMessage +│ │ │ │ │ +│ │ │ │ │ +│ │ ▼ ▼ +│ │ append to context.messages ────────────────────────────────→ ────→ next turn +│ │ │ +│ │ ▼ +│ │ check pending/follow-up +│ │ │ +│ │ │ +│ └─────────────────────────────────────────────────────────────────────────────────→ ────────────────────────────────────────────────────────────┘ +│ (loop back to main loop or end) +│ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ -``` -┌─────────────────────────────────────────────────────────────────────────┐ -│ Agent (High-Level) │ -│ - State management, event streaming, queueing │ -│ - Steering queue (steer()) and Follow-up queue (followUp()) │ -└─────────────────────────────────────────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────────────────┐ -│ AgentLoop (Low-Level) │ -│ - Core loop execution with tool calling │ -│ - Tool execution (parallel/sequential) │ -│ - Event emission lifecycle │ -└─────────────────────────────────────────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────────────────┐ -│ LLM Provider │ -│ - Stream function calls the LLM API │ -│ - Returns assistant response (text + tool calls) │ -└─────────────────────────────────────────────────────────────────────────┘ ``` -## Main Agent Loop Flow +## Key Flow Points -```mermaid -flowchart TD - Start[Start Agent Loop] --> EmitStart[Emitter: AgentStartEvent] - EmitStart --> EmitTurnStart[Emitter: TurnStartEvent] - - subgraph SteeringQueue "Check Steering Queue" - SteeringQueue --> HasSteering{Has pending
steering messages?} - HasSteering -- Yes --> ProcessSteering[Process steering messages
Emit MessageStart/End] - ProcessSteering --> AddToContext[Add to context messages] - AddToContext --> PollSteering - HasSteering -- No --> PollSteering - end - - PollSteering{Next iteration:
Poll steering queue again} - - subgraph AssistantLoop "Assistant Response Loop" - PollSteering --> StreamResponse[Stream Assistant Response from LLM] - StreamResponse --> CheckStop{Stop reason?} - - CheckStop -- error/aborted --> EmitTurnEnd1[Emit: TurnEndEvent] - EmitTurnEnd1 --> EmitAgentEnd1[Emit: AgentEndEvent] - EmitAgentEnd1 --> End1[End Loop] - - CheckStop -- length/truncated --> FailTools[Fail all tool calls
with error message] - CheckStop -- normal --> ExtractTools[Extract ToolCall objects] - - FailTools --> HasMoreTools - ExtractTools --> HasMoreTools{Has tool
calls?} - - HasMoreTools -- Yes --> ExecuteTools[Execute Tools] - HasMoreTools -- No --> CheckFollowUp - - subgraph ToolExecution "Tool Execution" - direction TB - ExecuteTools --> ExecMode{Execution Mode?} - - ExecMode -- Sequential --> SeqLoop[For each tool call:] - ExecMode -- Parallel --> ParaLoop[For each tool call:] - - SeqLoop --> SeqPrepare[Prepare tool call] - ParaLoop --> ParaPrepare[Prepare tool call] - - SeqPrepare --> SeqExec[Execute sequentially] - ParaPrepare --> ParaExec[Execute in parallel
& collect results] - - SeqExec --> SeqFinalize[Finalize tool call
Emit: ToolExecutionEndEvent] - ParaExec --> ParaFinalize[Finalize tool calls
Emit: ToolExecutionEndEvent] - - SeqFinalize --> SeqResults[Create ToolResultMessages] - ParaFinalize --> ParaResults[Create ToolResultMessages] - - SeqResults --> AddToolResults - ParaResults --> AddToolResults[Add tool results to context
Emit: MessageStart/End] - end - - AddToolResults --> HasMoreTools - - CheckFollowUp{Has pending
follow-up messages?} - CheckFollowUp -- Yes --> ProcessFollowUp[Process follow-up messages
Emit MessageStart/End] - ProcessFollowUp --> AddToContext2[Add to context messages] - AddToContext2 --> CheckPrepareNext - - CheckPrepareNext{Should prepare
next turn?} - CheckPrepareNext -- Yes --> PrepareNext[Call prepare_next_turn hook] - PrepareNext --> UpdateContext[Update context & config] - UpdateContext --> CheckStop2 - CheckPrepareNext -- No --> CheckStop2 - - CheckStop2{Stop after turn?} - CheckStop2 -- Yes --> EmitAgentEnd2[Emit: AgentEndEvent] - EmitAgentEnd2 --> End2[End Loop] - CheckStop2 -- No --> PollSteering - end - - CheckFollowUp -- No --> CheckPrepareNext -``` +1. **Input Normalization**: User input (String/Message/Vector) → AgentMessage[] +2. **LLM Boundary**: AgentMessage[] → Message[] (filtered, converted for LLM) +3. **Streaming**: LLM returns event stream → AssistantMessage (built incrementally) +4. **Tool Execution**: + - Extract ToolCall objects from AssistantMessage.content + - Execute in parallel (default) or sequential mode + - Emit ToolResultMessage for each +5. **Message Queues**: + - Steering: Injected between turns (keeps conversation going) + - Follow-up: Injected after agent stops (final messages) +6. **Context Updates**: All messages appended to context.messages for next iteration +7. **Loop Control**: Continue until no pending messages AND agent signals stop -## Detailed Event Lifecycle +## Hook Points -```mermaid -sequenceDiagram - participant Agent - participant Loop as AgentLoop - participant Emitter as Event Sink - participant LLM - participant Tools - - Agent->>Loop: agentLoop(prompts, context, config) - Loop->>Emitter: AgentStartEvent - Loop->>Emitter: TurnStartEvent - - note over Loop: Process prompts - Loop->>Emitter: MessageStartEvent(prompt) - Loop->>Emitter: MessageEndEvent(prompt) - - Loop->>LLM: stream_function(model, context, config) - LLM-->>Loop: AssistantResponse with ToolCalls - - note over Loop: Process response - Loop->>Emitter: MessageStartEvent(assistant) - - alt Has Tool Calls - Loop->>Tools: Execute tools - Tools-->>Loop: ToolResults - - note over Loop: Emit tool execution events - Loop->>Emitter: ToolExecutionStartEvent - Loop->>Emitter: ToolExecutionUpdateEvent (if streaming) - Loop->>Emitter: ToolExecutionEndEvent - - Loop->>Emitter: MessageStartEvent(toolResult) - Loop->>Emitter: MessageEndEvent(toolResult) - end - - Loop->>Emitter: MessageEndEvent(assistant) - Loop->>Emitter: TurnEndEvent(message, tool_results) - - note over Loop: Check for follow-up/steering - alt Has more work - Loop->>Loop: Continue loop - else Done - Loop->>Emitter: AgentEndEvent(messages) - end -``` - -## Steering vs Follow-up Messages - -```mermaid -graph LR - subgraph "Main Conversation" - A[User Message] --> B[Assistant Response] - B --> C[Tool Execution] - C --> D[Tool Result] - D --> E{Decision Point} - end - - E -->|Continue loop| B - E -->|Stop & check queues| F - - subgraph "Steering Queue" - steer[steer(message)] --> SQueue[Queued after
assistant turn] - SQueue --> SProcess[Processed in
next turn iteration] - end - - subgraph "Follow-up Queue" - follow[followUp(message)] --> FQueue[Queued to run
when agent would stop] - FQueue --> FProcess[Processed only
when no more work] - end - - F --> SProcess - F --> FProcess -``` - -## Tool Execution Modes - -### Sequential Execution -```mermaid -flowchart LR - Tool1[Tool Call 1] --> Prepare1[Prepare] - Prepare1 --> Execute1[Execute] - Execute1 --> Finalize1[Finalize] - Finalize1 --> Result1[Tool Result 1] - - Result1 --> Prepare2[Prepare] - Prepare2 --> Execute2[Execute] - Execute2 --> Finalize2[Finalize] - Finalize2 --> Result2[Tool Result 2] - - note right of Execute1 "Must complete before next tool" - note right of Execute2 "Tools run one at a time" -``` - -### Parallel Execution -```mermaid -flowchart LR - Tool1[Tool Call 1] --> Prepare1[Prepare] - Tool2[Tool Call 2] --> Prepare2[Prepare] - Tool3[Tool Call 3] --> Prepare3[Prepare] - - Prepare1 --> Execute1[Execute] - Prepare2 --> Execute2[Execute] - Prepare3 --> Execute3[Execute] - - Execute1 --> Finalize1[Finalize] --> Result1[Tool Result 1] - Execute2 --> Finalize2[Finalize] --> Result2[Tool Result 2] - Execute3 --> Finalize3[Finalize] --> Result3[Tool Result 3] - - note right of Prepare1 "Preparation can happen" - note right of Execute1 "All tools execute" - note right of Finalize1 "Results collected in order" -``` - -## Message Conversion at LLM Boundary - -```mermaid -flowchart LR - subgraph "Agent Messages (Internal)" - AM1[UserMessage] - AM2[AssistantMessage] - AM3[ToolResultMessage] - AM4[CompactionSummaryMessage] - AM5[BranchSummaryMessage] - AM6[CustomMessage] - end - - AM1 --> Convert[convertToLlm] - AM2 --> Convert - AM3 --> Convert - AM4 --> Convert - AM5 --> Convert - AM6 --> Convert - - Convert --> LM1[UserMessage] - Convert --> LM2[AssistantMessage] - Convert --> LM3[ToolResultMessage] - Convert --> LM4[UserMessage (summary)] - Convert --> LM5[UserMessage (branch)] - Convert --> LM6[UserMessage (custom)] - - LM1 --> LLM[LLM API] - LM2 --> LLM - LM3 --> LLM - LM4 --> LLM - LM5 --> LLM - LM6 --> LLM -``` - -## Key Data Structures - -### AgentContext -```julia -struct AgentContext - system_prompt::String - messages::Vector{AgentMessage} - tools::Union{Vector{AgentTool}, Nothing} -end -``` - -### AgentLoopConfig -```julia -struct AgentLoopConfig - model::Model - reasoning::Union{ThinkingLevel, Nothing} - session_id::Union{String, Nothing} - convert_to_llm::Function - transform_context::Union{Function, Nothing} - get_api_key::Union{Function, Nothing} - get_steering_messages::Function - get_follow_up_messages::Function - before_tool_call::Union{Function, Nothing} - after_tool_call::Union{Function, Nothing} - prepare_next_turn::Union{Function, Nothing} - should_stop_after_turn::Function - tool_execution::ToolExecutionMode - # ... other options -end -``` - -## Entry Points - -1. **`prompt(agent, input)`** - Start new conversation - - Validates no active run - - Normalizes input to messages - - Calls `runPromptMessages` - -2. **`continue!(agent)`** - Continue from last message - - Checks last message is not assistant - - Drains steering/follow-up queues first - - Calls agent loop continuation - -## Termination Conditions - -The agent loop terminates when: -1. LLM response has stop_reason = "error" or "aborted" -2. No tool calls and no pending steering/follow-up messages -3. `should_stop_after_turn` returns true -4. Agent is aborted via abort signal - -## Event Summary - -| Event | When Emitted | -|-------|--------------| -| `AgentStartEvent` | Loop begins | -| `TurnStartEvent` | Each conversation turn | -| `MessageStartEvent` | Message added to context | -| `MessageEndEvent` | Message fully processed | -| `MessageUpdateEvent` | Streaming updates | -| `ToolExecutionStartEvent` | Tool execution begins | -| `ToolExecutionUpdateEvent` | Tool execution progress | -| `ToolExecutionEndEvent` | Tool execution completes | -| `TurnEndEvent` | Turn completes | -| `AgentEndEvent` | Loop terminates | +- `transform_context`: Modify messages before LLM call +- `convert_to_llm`: Transform AgentMessage[] to Message[] +- `before_tool_call`: Block or modify tool execution +- `after_tool_call`: Modify tool results +- `prepare_next_turn`: Update context/model/thinking level between turns +- `should_stop_after_turn`: Signal when agent should terminate