This commit is contained in:
2026-07-28 14:59:41 +07:00
parent 0ed8be5daa
commit 93f0b51ba4
+175 -168
View File
@@ -1,14 +1,12 @@
Here's a cleaned-up version with proper alignment:
```
┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ ┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ AGENT LOOP DIAGRAM │ │ AGENT LOOP DIAGRAM │
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ └─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ ┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ 1. INITIALIZATION │ │ 1. INITIALIZATION │
├─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ │ │ │
│ Agent.start(prompt) │ Agent.prompt(user_input)
│ │ │ │ │ │
│ ▼ │ │ ▼ │
│ normalizePrompt() ← Convert input (String/Message/Vector) to AgentMessage[] │ │ 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(AgentStartEvent) │
│ emit(TurnStartEvent) │ │ emit(TurnStartEvent) │
@@ -29,117 +31,81 @@ Here's a cleaned-up version with proper alignment:
│ for prompt in prompts: │ │ for prompt in prompts: │
│ emit(MessageStartEvent(prompt)) │ │ emit(MessageStartEvent(prompt)) │
│ emit(MessageEndEvent(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 │ │ │ │ │ │ 4. PENDING MESSAGE HANDLING │ │ │
│ │ │ (Handles steering messages queued via agent.steer() AFTER previous turn) │ │ │
│ │ │ │ │ │ │ │ │ │ │ │
│ │ │ pending_messages = getSteeringMessages() │ │ │ │ │ │ if !isempty(pending_messages): │ │ │
│ │ │ │ │ │ │ │ │ for msg in pending_messages: │ │ │
│ │ │ while has pending_messages OR has_tool_calls: │ │ │ │ │ │ emit(MessageStartEvent(msg)) │ │ │
│ │ │ │ │ │ │ │ │ emit(MessageEndEvent(msg)) │ │ │
│ │ │ if pending_messages: │ │ │ │ │ │ push to current_context.messages │ │ │
│ │ │ for msg in pending_messages: │ │ │ │ │ │ push to new_messages │ │ │
│ │ │ emit(MessageStartEvent) │ │ │ │ │ │ pending_messages = [] │ │ │
│ │ │ emit(MessageEndEvent) │ │ │
│ │ │ push to context.messages │ │ │
│ │ │ pending_messages = [] │ │ │
│ │ └─────────────────────────────────────────────────────────────────────────────────────────────────────┘ │ │ │ │ └─────────────────────────────────────────────────────────────────────────────────────────────────────┘ │ │
│ │ │ │ │ │ │ │
│ │ ┌─────────────────────────────────────────────────────────────────────────────────────────────────────┐ │ │ │ │ ┌─────────────────────────────────────────────────────────────────────────────────────────────────────┐ │ │
│ │ │ 5. STREAM ASSISTANT RESPONSE │ │ │ │ │ │ 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) │ │ │ │ │ │ emit(MessageStartEvent(message)) │ │ │
│ │ │ ┌───────────────────────────────────────────────────────────────────────────────────────────┐ │ │ │ │ │ │ emit(MessageEndEvent(message)) │ │ │
│ │ │ │ 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 │ │ │
│ │ └─────────────────────────────────────────────────────────────────────────────────────────────────────┘ │ │ │ │ └─────────────────────────────────────────────────────────────────────────────────────────────────────┘ │ │
│ │ │ │ │ │ │ │
│ │ ┌─────────────────────────────────────────────────────────────────────────────────────────────────────┐ │ │ │ │ if message.stop_reason in ("error", "aborted"): │ │
│ │ │ 6. CHECK STOP REASON │ │ │ │ emit(TurnEndEvent) │ │
│ │ │ │ │ │ emit(AgentEndEvent) ← EXIT LOOP │ │
│ │ if stop_reason in ("error", "aborted"): │ │ │ │ return │ │
│ │ │ emit(TurnEndEvent) │ │ │
│ │ │ emit(AgentEndEvent) ← EXIT LOOP │ │ │
│ │ │ return │ │ │
│ │ └─────────────────────────────────────────────────────────────────────────────────────────────────────┘ │ │
│ │ │ │ │ │ │ │
│ │ ┌─────────────────────────────────────────────────────────────────────────────────────────────────────┐ │ │ │ │ tool_calls = filter(message.content, ToolCall) │ │
│ │ │ 7. EXTRACT TOOL CALLS │ │ │ │ if !isempty(tool_calls): │ │
│ │ │ │ │ │ executeToolCalls() → ToolResultMessage[] │ │
│ │ tool_calls = filter(content, isa ToolCall) │ │ │ │ for result in tool_results: │ │
│ │ │ │ │ │ push to current_context.messages │ │
│ │ if tool_calls: │ │ │ │ push to new_messages │ │
│ │ │ │ │ │ emit(MessageStartEvent(result)) │ │
│ │ ┌───────────────────────────────────────────────────────────────────────────────────────────┐ │ │ │ │ emit(MessageEndEvent(result)) │ │
│ │ │ │ 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)) │ │ │ │ emit(TurnEndEvent(message, tool_results)) │ │
│ │ │ │ │ │ │ │
│ │ ┌─────────────────────────────────────────────────────────────────────────────────────────────────────┐ │ │ │ │ ┌─────────────────────────────────────────────────────────────────────────────────────────────────────┐ │ │
│ │ │ 9. PREPARE NEXT TURN │ │ │ │ │ │ 6. PREPARE NEXT TURN │ │ │
│ │ │ │ │ │
│ │ │ next_turn_context = PrepareNextTurnContext( │ │ │
│ │ │ message, tool_results, current_context, new_messages │ │ │
│ │ │ ) │ │ │
│ │ │ │ │ │ │ │ │ │ │ │
│ │ │ next_turn_context = PrepareNextTurnContext(...) │ │ │
│ │ │ next_turn_snapshot = prepare_next_turn(config, next_turn_context) │ │ │ │ │ │ next_turn_snapshot = prepare_next_turn(config, next_turn_context) │ │ │
│ │ │ │ │ │ │ │ │ │ │ │
│ │ │ if !isnothing(next_turn_snapshot): │ │ │ │ │ │ if !isnothing(next_turn_snapshot): │ │ │
@@ -150,14 +116,14 @@ Here's a cleaned-up version with proper alignment:
│ │ │ return │ │ │ │ │ │ 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: │ if !isempty(follow_up_messages): │
│ pending_messages = follow_up_messages ← Continue loop to handle follow-ups │ │ pending_messages = follow_up_messages ← Continue loop for follow-ups
│ continue │ │ continue │
│ │ │ │
│ break ← EXIT MAIN LOOP (no more pending messages) │ │ 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): Steering messages are queued via agent.steer(message)
• UserMessage ("user") They are ONLY processed at the START of a loop iteration
• AssistantMessage ("assistant") AFTER the previous assistant turn completes
│ • 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): Flow:
• UserMessage (role: "user") user asks → agent responds → [user can steer here]
• AssistantMessage (role: "assistant")
• ToolResultMessage (role: "toolResult") └─→ pending_messages = get_steering() ← Steering messages injected here
│ │ │ │
Steering vs Follow-up: Follow-up messages are queued via agent.followUp(message)
• Steering: Injected AFTER current assistant turn finishes (can continue conversation) They run ONLY after agent would otherwise stop
│ • Follow-up: Run ONLY after agent would otherwise stop (final messages) │
│ │ │ │
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ └─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ ┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
DATA FLOW SUMMARY COMPLETE CYCLE EXAMPLE: User asks → Agent responds → User asks 2nd → Agent responds
├─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ │ │ │
User Input LLM Response TURN #1: User asks "What is Julia?"
─────────────────────────────────────────
1. Agent.prompt("What is Julia?")
├──────────► AgentMessage[] ───────────►│ normalizePrompt() → [UserMessage("What is Julia?")]
│ (filter+convert) runPromptMessages()
convert_to_llm() 2. runAgentLoop()
├──────────► Message[] ────────────────►│ new_messages = [UserMessage("What is Julia?")]
│ │ current_context.messages = [...existing..., UserMessage("What is Julia?")]
│ Context │ emit(AgentStartEvent), emit(TurnStartEvent)
│ (system_prompt, emit(MessageStart/End) for user message
messages, tools) │
├──────────► stream_function() ────────►│ 3. runLoop()
│ │ pending_messages = get_steering() = [] ← Steering queue is empty
│ LLM API 4. streamAssistantResponse()
convert_to_llm([UserMessage]) → Message[]
LLM call with [UserMessage]
│ LLM Stream Events receive AssistantMessage: "Julia is a programming language..."
│ (text_delta, toolcall_delta, ...) push AssistantMessage to current_context.messages
push AssistantMessage to new_messages
│ ▼ emit(MessageStart/End) for assistant message
AssistantMessage (finalized)
│ │ 5. check stop_reason → continue (no tools, no error)
check tool_calls? 6. emit(TurnEndEvent)
│ │
│ │ 7. prepare_next_turn() → nothing (default)
YES NO
│ │ 8. should_stop_after_turn() → false (default)
▼ │
│ execute tools │ 9. pending_messages = get_steering() = [] ← No steering messages
│ │
│ ▼ 10. follow_up_messages = get_follow_up() = []
emit ToolResultMessage│
│ │ 11. break ← Exit main loop
└─────┬─────┘
12. emit(AgentEndEvent)
│ append to context.messages ┌───────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ Current context.messages:
│ ▼ │ [UserMessage("What is Julia?"), AssistantMessage("Julia is...")]
next turn │ │
│ steering_queue: []
│ follow_up_queue: []
│ check pending/follow-up └───────────────────────────────────────────────────────────────────────────────────────────────────────────┘
└───────────────────────────────────────┴───────────────────────────────────────────────────────────────► TURN #2: User asks "How does it work?"
(loop back to main loop or end) ─────────────────────────────────────────
│ 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 │
│ │ │ │
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ └─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
```