Files
YiemAgent/docs/agent_loop_diagram.md
T
2026-07-28 15:01:16 +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                                                             │
│                                                                                                                 │
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘