From 93f0b51ba403dd60bf287ffc5b2a69617fa853b5 Mon Sep 17 00:00:00 2001 From: narawat Date: Tue, 28 Jul 2026 14:59:41 +0700 Subject: [PATCH] update --- docs/agent_loop_diagram.md | 343 +++++++++++++++++++------------------ 1 file changed, 175 insertions(+), 168 deletions(-) diff --git a/docs/agent_loop_diagram.md b/docs/agent_loop_diagram.md index 0c5311f..e51e5fe 100644 --- a/docs/agent_loop_diagram.md +++ b/docs/agent_loop_diagram.md @@ -1,14 +1,12 @@ -Here's a cleaned-up version with proper alignment: - -``` ┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ │ AGENT LOOP DIAGRAM │ └─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ ┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ │ 1. INITIALIZATION │ +├─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤ │ │ -│ Agent.start(prompt) │ +│ Agent.prompt(user_input) │ │ │ │ │ ▼ │ │ normalizePrompt() ← Convert input (String/Message/Vector) to AgentMessage[] │ @@ -21,7 +19,11 @@ Here's a cleaned-up version with proper alignment: │ ▼ ┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ -│ 2. AGENT LOOP START (agentLoop/runAgentLoop) │ +│ 2. AGENT LOOP START (runAgentLoop) │ +├─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤ +│ │ +│ new_messages = copy(prompts) ← User messages copied to new_messages │ +│ current_context.messages = vcat(context.messages, copy(prompts)) ← User messages added to context │ │ │ │ emit(AgentStartEvent) │ │ emit(TurnStartEvent) │ @@ -29,117 +31,81 @@ Here's a cleaned-up version with proper alignment: │ for prompt in prompts: │ │ emit(MessageStartEvent(prompt)) │ │ emit(MessageEndEvent(prompt)) │ +│ │ │ +│ ├─→ push to current_context.messages (for LLM) │ +│ └─→ push to new_messages (track what we've added) │ +│ │ +└─────────┼───────────────────────────────────────────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ 3. MAIN LOOP (runLoop - while true) │ +├─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤ +│ │ +│ pending_messages = get_steering_messages() ← Check steering queue (empty on first turn) │ │ │ │ ┌───────────────────────────────────────────────────────────────────────────────────────────────────────────┐ │ -│ │ 3. MAIN LOOP (while true) │ │ +│ │ While has pending_messages OR has_tool_calls: │ │ │ │ │ │ │ │ ┌─────────────────────────────────────────────────────────────────────────────────────────────────────┐ │ │ │ │ │ 4. PENDING MESSAGE HANDLING │ │ │ +│ │ │ (Handles steering messages queued via agent.steer() AFTER previous turn) │ │ │ │ │ │ │ │ │ -│ │ │ 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 = [] │ │ │ +│ │ │ if !isempty(pending_messages): │ │ │ +│ │ │ for msg in pending_messages: │ │ │ +│ │ │ emit(MessageStartEvent(msg)) │ │ │ +│ │ │ emit(MessageEndEvent(msg)) │ │ │ +│ │ │ push to current_context.messages │ │ │ +│ │ │ push to new_messages │ │ │ +│ │ │ pending_messages = [] │ │ │ │ │ └─────────────────────────────────────────────────────────────────────────────────────────────────────┘ │ │ │ │ │ │ │ │ ┌─────────────────────────────────────────────────────────────────────────────────────────────────────┐ │ │ │ │ │ 5. STREAM ASSISTANT RESPONSE │ │ │ │ │ │ │ │ │ -│ │ │ a) transform_context (if configured) │ │ │ +│ │ │ message = streamAssistantResponse() │ │ │ +│ │ │ ├─ transform_context (if configured) │ │ │ +│ │ │ ├─ convert_to_llm(messages) → Message[] │ │ │ +│ │ │ │ ┌───────────────────────────────────────────────────────────────────────────────────────┐ │ │ │ +│ │ │ │ │ Converts AgentMessage[] to Message[] │ │ │ │ +│ │ │ │ │ Filters: keeps user, assistant, toolResult │ │ │ │ +│ │ │ │ └───────────────────────────────────────────────────────────────────────────────────────┘ │ │ │ +│ │ │ ├─ stream_function(model, context) │ │ │ +│ │ │ │ ┌───────────────────────────────────────────────────────────────────────────────────────┐ │ │ │ +│ │ │ │ │ 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 │ │ │ │ +│ │ │ │ └───────────────────────────────────────────────────────────────────────────────────────┘ │ │ │ +│ │ │ └─ push to current_context.messages & new_messages │ │ │ │ │ │ │ │ │ -│ │ │ 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 │ │ │ +│ │ │ emit(MessageStartEvent(message)) │ │ │ +│ │ │ emit(MessageEndEvent(message)) │ │ │ │ │ └─────────────────────────────────────────────────────────────────────────────────────────────────────┘ │ │ │ │ │ │ -│ │ ┌─────────────────────────────────────────────────────────────────────────────────────────────────────┐ │ │ -│ │ │ 6. CHECK STOP REASON │ │ │ -│ │ │ │ │ │ -│ │ │ if stop_reason in ("error", "aborted"): │ │ │ -│ │ │ emit(TurnEndEvent) │ │ │ -│ │ │ emit(AgentEndEvent) ← EXIT LOOP │ │ │ -│ │ │ return │ │ │ -│ │ └─────────────────────────────────────────────────────────────────────────────────────────────────────┘ │ │ +│ │ if message.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 │ │ │ │ -│ │ │ └───────────────────────────────────────────────────────────────────────────────────────────┘ │ │ │ -│ │ │ │ │ │ -│ │ └─────────────────────────────────────────────────────────────────────────────────────────────────────┘ │ │ +│ │ tool_calls = filter(message.content, ToolCall) │ │ +│ │ if !isempty(tool_calls): │ │ +│ │ executeToolCalls() → ToolResultMessage[] │ │ +│ │ for result in tool_results: │ │ +│ │ push to current_context.messages │ │ +│ │ push to new_messages │ │ +│ │ emit(MessageStartEvent(result)) │ │ +│ │ emit(MessageEndEvent(result)) │ │ │ │ │ │ │ │ emit(TurnEndEvent(message, tool_results)) │ │ │ │ │ │ │ │ ┌─────────────────────────────────────────────────────────────────────────────────────────────────────┐ │ │ -│ │ │ 9. PREPARE NEXT TURN │ │ │ -│ │ │ │ │ │ -│ │ │ next_turn_context = PrepareNextTurnContext( │ │ │ -│ │ │ message, tool_results, current_context, new_messages │ │ │ -│ │ │ ) │ │ │ +│ │ │ 6. PREPARE NEXT TURN │ │ │ │ │ │ │ │ │ +│ │ │ next_turn_context = PrepareNextTurnContext(...) │ │ │ │ │ │ next_turn_snapshot = prepare_next_turn(config, next_turn_context) │ │ │ │ │ │ │ │ │ │ │ │ if !isnothing(next_turn_snapshot): │ │ │ @@ -150,14 +116,14 @@ Here's a cleaned-up version with proper alignment: │ │ │ return │ │ │ │ │ └─────────────────────────────────────────────────────────────────────────────────────────────────────┘ │ │ │ │ │ │ -│ │ pending_messages = getSteeringMessages() ← Check for new steering messages │ │ +│ │ pending_messages = get_steering_messages() ← Check for new steering messages │ │ │ │ │ │ │ └───────────────────────────────────────────────────────────────────────────────────────────────────────────┘ │ │ │ -│ follow_up_messages = getFollowUpMessages() │ +│ follow_up_messages = get_follow_up_messages() │ │ │ -│ if follow_up_messages: │ -│ pending_messages = follow_up_messages ← Continue loop to handle follow-ups │ +│ if !isempty(follow_up_messages): │ +│ pending_messages = follow_up_messages ← Continue loop for follow-ups │ │ continue │ │ │ │ break ← EXIT MAIN LOOP (no more pending messages) │ @@ -167,81 +133,122 @@ Here's a cleaned-up version with proper alignment: └─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ ┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ -│ HELPER STRUCTURES │ +│ 4. STEERING QUEUE MECHANISM │ +├─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤ │ │ -│ 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 │ +│ Steering messages are queued via agent.steer(message) │ +│ They are ONLY processed at the START of a loop iteration │ +│ AFTER the previous assistant turn completes │ │ │ -│ Message (LLM interface): │ -│ • UserMessage (role: "user") │ -│ • AssistantMessage (role: "assistant") │ -│ • ToolResultMessage (role: "toolResult") │ +│ Flow: │ +│ user asks → agent responds → [user can steer here] │ +│ │ │ +│ └─→ pending_messages = get_steering() ← Steering messages injected here │ │ │ -│ 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) │ +│ Follow-up messages are queued via agent.followUp(message) │ +│ They run ONLY after agent would otherwise stop │ │ │ └─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ ┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ -│ DATA FLOW SUMMARY │ +│ COMPLETE CYCLE EXAMPLE: User asks → Agent responds → User asks 2nd → Agent responds │ +├─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤ │ │ -│ User Input LLM Response │ -│ │ │ │ -│ │ │ │ -│ ├──────────► AgentMessage[] ───────────►│ │ -│ │ (filter+convert) │ │ -│ │ │ │ -│ │ convert_to_llm() │ │ -│ ├──────────► Message[] ────────────────►│ │ -│ │ │ │ -│ │ Context │ │ -│ │ (system_prompt, │ │ -│ │ messages, tools) │ │ -│ ├──────────► stream_function() ────────►│ │ -│ │ │ │ -│ │ │ │ -│ │ LLM API │ │ -│ │ │ │ -│ │ ▼ │ -│ │ LLM Stream Events │ -│ │ (text_delta, toolcall_delta, ...) │ -│ │ │ │ -│ │ ▼ │ -│ │ AssistantMessage (finalized) │ -│ │ │ │ -│ │ ▼ │ -│ │ check tool_calls? │ -│ │ │ │ │ -│ │ │ │ │ -│ │ YES NO │ -│ │ │ │ │ -│ │ ▼ │ │ -│ │ execute tools │ │ -│ │ │ │ │ -│ │ ▼ │ │ -│ │ emit ToolResultMessage│ │ -│ │ │ │ │ -│ │ └─────┬─────┘ │ -│ │ │ │ -│ │ ▼ │ -│ │ append to context.messages │ -│ │ │ │ -│ │ ▼ │ -│ │ next turn │ -│ │ │ │ -│ │ ▼ │ -│ │ check pending/follow-up │ -│ │ │ │ -│ │ ▼ │ -│ └───────────────────────────────────────┴───────────────────────────────────────────────────────────────►│ -│ (loop back to main loop or end) │ +│ TURN #1: User asks "What is Julia?" │ +│ ───────────────────────────────────────── │ +│ 1. Agent.prompt("What is Julia?") │ +│ normalizePrompt() → [UserMessage("What is Julia?")] │ +│ runPromptMessages() │ +│ │ +│ 2. runAgentLoop() │ +│ new_messages = [UserMessage("What is Julia?")] │ +│ current_context.messages = [...existing..., UserMessage("What is Julia?")] │ +│ emit(AgentStartEvent), emit(TurnStartEvent) │ +│ emit(MessageStart/End) for user message │ +│ │ +│ 3. runLoop() │ +│ pending_messages = get_steering() = [] ← Steering queue is empty │ +│ │ +│ 4. streamAssistantResponse() │ +│ convert_to_llm([UserMessage]) → Message[] │ +│ LLM call with [UserMessage] │ +│ receive AssistantMessage: "Julia is a programming language..." │ +│ push AssistantMessage to current_context.messages │ +│ push AssistantMessage to new_messages │ +│ emit(MessageStart/End) for assistant message │ +│ │ +│ 5. check stop_reason → continue (no tools, no error) │ +│ │ +│ 6. emit(TurnEndEvent) │ +│ │ +│ 7. prepare_next_turn() → nothing (default) │ +│ │ +│ 8. should_stop_after_turn() → false (default) │ +│ │ +│ 9. pending_messages = get_steering() = [] ← No steering messages │ +│ │ +│ 10. follow_up_messages = get_follow_up() = [] │ +│ │ +│ 11. break ← Exit main loop │ +│ │ +│ 12. emit(AgentEndEvent) │ +│ │ +│ ┌───────────────────────────────────────────────────────────────────────────────────────────────────────────┐ │ +│ │ Current context.messages: │ │ +│ │ [UserMessage("What is Julia?"), AssistantMessage("Julia is...")] │ │ +│ │ │ │ +│ │ steering_queue: [] │ │ +│ │ follow_up_queue: [] │ │ +│ └───────────────────────────────────────────────────────────────────────────────────────────────────────────┘ │ +│ │ +│ │ +│ TURN #2: User asks "How does it work?" │ +│ ───────────────────────────────────────── │ +│ 1. Agent.prompt("How does it work?") │ +│ normalizePrompt() → [UserMessage("How does it work?")] │ +│ runPromptMessages() │ +│ │ +│ 2. runAgentLoop() │ +│ new_messages = [UserMessage("How does it work?")] │ +│ current_context.messages = [...previous..., UserMessage("How does it work?")] │ +│ emit(AgentStartEvent), emit(TurnStartEvent) │ +│ emit(MessageStart/End) for user message │ +│ │ +│ 3. runLoop() │ +│ pending_messages = get_steering() = [] │ +│ │ +│ 4. streamAssistantResponse() │ +│ convert_to_llm([UserMsg1, AssistantMsg1, UserMsg2]) → Message[] │ +│ LLM call with FULL conversation history (context preserved!) │ +│ receive AssistantMessage: "It works by..." │ +│ push AssistantMessage to current_context.messages │ +│ push AssistantMessage to new_messages │ +│ │ +│ 5. emit(TurnEndEvent), emit(AgentEndEvent) │ +│ │ +│ ┌───────────────────────────────────────────────────────────────────────────────────────────────────────────┐ │ +│ │ Current context.messages: │ │ +│ │ [UserMsg1, AssistantMsg1, UserMsg2, AssistantMsg2] │ │ +│ └───────────────────────────────────────────────────────────────────────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ -``` + +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ KEY INSIGHTS │ +├─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤ +│ │ +│ 1. User prompts are NOT added to steering queue │ +│ They go directly into context.messages via vcat() in runAgentLoop() │ +│ │ +│ 2. Steering queue is for messages injected AFTER a turn │ +│ Via agent.steer(message) - used for continuation without new prompt │ +│ │ +│ 3. Context is preserved across turns │ +│ Each turn appends to context.messages, so LLM sees full history │ +│ │ +│ 4. New turn = New prompt OR steering/follow-up messages │ +│ - New Agent.prompt() call starts new turn with new messages │ +│ - Steering messages continue from current state │ +│ - Follow-up messages run when agent would stop │ +│ │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ \ No newline at end of file