Files
YiemAgent/docs/agent_loop_diagram.md
T
2026-07-28 14:59:41 +07:00

37 KiB

┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ │ AGENT LOOP DIAGRAM │ └─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ │ 1. INITIALIZATION │ ├─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤ │ │ │ Agent.prompt(user_input) │ │ │ │ │ ▼ │ │ normalizePrompt() ← Convert input (String/Message/Vector) to AgentMessage[] │ │ │ │ │ ▼ │ │ runPromptMessages() │ │ │ │ │ ▼ │ └─────────┼───────────────────────────────────────────────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ │ 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) │ │ │ │ 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) │ │ │ │ ┌───────────────────────────────────────────────────────────────────────────────────────────────────────────┐ │ │ │ While has pending_messages OR has_tool_calls: │ │ │ │ │ │ │ │ ┌─────────────────────────────────────────────────────────────────────────────────────────────────────┐ │ │ │ │ │ 4. PENDING MESSAGE HANDLING │ │ │ │ │ │ (Handles steering messages queued via agent.steer() AFTER previous turn) │ │ │ │ │ │ │ │ │ │ │ │ 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 │ │ │ │ │ │ │ │ │ │ │ │ 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 │ │ │ │ │ │ │ │ │ │ │ │ emit(MessageStartEvent(message)) │ │ │ │ │ │ emit(MessageEndEvent(message)) │ │ │ │ │ └─────────────────────────────────────────────────────────────────────────────────────────────────────┘ │ │ │ │ │ │ │ │ if message.stop_reason in ("error", "aborted"): │ │ │ │ emit(TurnEndEvent) │ │ │ │ emit(AgentEndEvent) ← EXIT LOOP │ │ │ │ return │ │ │ │ │ │ │ │ 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)) │ │ │ │ │ │ │ │ ┌─────────────────────────────────────────────────────────────────────────────────────────────────────┐ │ │ │ │ │ 6. PREPARE NEXT TURN │ │ │ │ │ │ │ │ │ │ │ │ next_turn_context = PrepareNextTurnContext(...) │ │ │ │ │ │ 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 = get_steering_messages() ← Check for new steering messages │ │ │ │ │ │ │ └───────────────────────────────────────────────────────────────────────────────────────────────────────────┘ │ │ │ │ follow_up_messages = get_follow_up_messages() │ │ │ │ if !isempty(follow_up_messages): │ │ pending_messages = follow_up_messages ← Continue loop for follow-ups │ │ continue │ │ │ │ break ← EXIT MAIN LOOP (no more pending messages) │ │ │ │ emit(AgentEndEvent(new_messages)) │ │ │ └─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ │ 4. STEERING QUEUE MECHANISM │ ├─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤ │ │ │ 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 │ │ │ │ Flow: │ │ user asks → agent responds → [user can steer here] │ │ │ │ │ └─→ pending_messages = get_steering() ← Steering messages injected here │ │ │ │ Follow-up messages are queued via agent.followUp(message) │ │ They run ONLY after agent would otherwise stop │ │ │ └─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ │ COMPLETE CYCLE EXAMPLE: User asks → Agent responds → User asks 2nd → Agent responds │ ├─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤ │ │ │ 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 │ │ │ └─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘