Files
YiemAgent/learning/03-AGENTLOOP_COMPONENT.md
T
2026-07-29 13:50:00 +07:00

27 KiB

AgentCore.jl - AgentLoop Component Deep Dive

AgentLoop Architecture

┌─────────────────────────────────────────────────────────────────────────────┐
│                          AgentLoop Layer                                    │
└─────────────────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────────────────┐
│ Public API                                                                  │
└─────────────────────────────────────────────────────────────────────────────┘

agentLoop()
  ├─ prompts: Vector{AgentMessage}
  ├─ context: AgentContext
  ├─ config: AgentLoopConfig
  ├─ signal: Union{Nothing, AbortSignal}
  └─ stream_fn: StreamFn
      └─ Returns: EventStream

agentLoopContinue()
  ├─ context: AgentContext
  ├─ config: AgentLoopConfig
  ├─ signal: Union{Nothing, AbortSignal}
  └─ stream_fn: StreamFn
      └─ Returns: EventStream

┌─────────────────────────────────────────────────────────────────────────────┐
│ Internal Flow                                                               │
└─────────────────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────────────────┐
│ 1. runAgentLoop() ── Entry point for new conversation                       │
│    - Creates copy of prompts                                                │
│    - Appends prompts to context.messages                                    │
│    - Emits AgentStartEvent                                                  │
│    - Emits TurnStartEvent                                                   │
│    - Emits MessageStart/End for each prompt                                 │
│    - Calls runLoop()                                                        │
└─────────────────────────────────────────────────────────────────────────────┘
                                    │
                                    ▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ 2. runLoop() ── Main event loop                                             │
│    ┌────────────────────────────────────────────────────────────────────┐   │
│    │ while true:                                                        │   │
│    │   1. Get steering/follow-up messages (if any)                      │   │
│    │   2. Emit messages as UserMessage                                  │   │
│    │   3. streamAssistantResponse()                                     │   │
│    │   4. Execute tool calls (sequential or parallel)                   │   │
│    │   5. Emit TurnEndEvent                                             │   │
│    │   6. prepare_next_turn (optional)                                  │   │
│    │   7. should_stop_after_turn? (check termination)                   │   │
│    │   8. Loop continues if not terminated                              │   │
│    └────────────────────────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────────────────────────┘
                                    │
                                    ▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ 3. streamAssistantResponse() ── LLM interaction                             │
│    - transform_context (optional)                                           │
│    - convert_to_llm (transform to Message[])                                │
│    - Call stream_fn (LLM API)                                               │
│    - Stream response deltas                                                 │
│    - Emit MessageStart/Update/End events                                    │
└─────────────────────────────────────────────────────────────────────────────┘
                                    │
                                    ▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ 4. executeToolCalls() ── Tool execution                                     │
│    ┌────────────────────────────────────────────────────────────────────┐   │
│    │ if EXECUTION_SEQUENTIAL || has_sequential_tool:                    │   │
│    │   executeToolCallsSequential()                                     │   │
│    │ else:                                                              │   │
│    │   executeToolCallsParallel()                                       │   │
│    └────────────────────────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────────────────────────┘
                                    │
                                    ▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ 5. AgentEndEvent ── Final event with all messages                           │
└─────────────────────────────────────────────────────────────────────────────┘

AgentLoopConfig

struct AgentLoopConfig
    model::Model
    reasoning::Union{ThinkingLevel, Nothing}
    session_id::Union{String, Nothing}
    on_payload::Union{Function, Nothing}
    on_response::Union{Function, Nothing}
    transport::String
    thinking_budgets::Union{Dict{String, Int64}, Nothing}
    max_retry_delay_ms::Union{Int64, Nothing}
    tool_execution::ToolExecutionMode
    before_tool_call::Union{Function, Nothing}
    after_tool_call::Union{Function, Nothing}
    prepare_next_turn::Union{Function, Nothing}
    convert_to_llm::Function
    transform_context::Union{Function, Nothing}
    get_api_key::Union{Function, Nothing}
    get_steering_messages::Union{Function, Nothing}
    get_follow_up_messages::Union{Function, Nothing}
end

Main Functions

agentLoop()

function agentLoop(
    prompts::Vector{AgentMessage},
    context::AgentContext,
    config::AgentLoopConfig,
    signal::Union{Nothing, AbortSignal},
    stream_fn::StreamFn,
)::EventStream

Purpose: Start a new conversation with initial prompts

Flow:

  1. Create event stream
  2. Spawn thread to run agent loop
  3. Return stream for event consumption
stream = agentLoop(
    [UserMessage("user", [TextContent("Hello")], timestamp)],
    AgentContext(system_prompt, messages, tools),
    config,
    nothing,
    stream_fn,
)

# Consume events
for event in stream
    if event isa MessageEndEvent
        println("Received: $(event.message)")
    end
end

runAgentLoop()

function runAgentLoop(
    prompts::Vector{AgentMessage},
    context::AgentContext,
    config::AgentLoopConfig,
    emit::AgentEventSink,
    signal::Union{Nothing, AbortSignal},
    stream_fn::StreamFn,
)::Vector{AgentMessage}

Purpose: Execute agent loop with initial prompts

Flow:

  1. Copy prompts to new_messages
  2. Append prompts to context.messages
  3. Emit AgentStartEvent
  4. For each prompt: emit MessageStartEvent, MessageEndEvent
  5. Call runLoop()

runLoop() - The Heart of AgentLoop

function runLoop(
    initial_context::AgentContext,
    new_messages::Vector{AgentMessage},
    initial_config::AgentLoopConfig,
    signal::Union{Nothing, AbortSignal},
    emit::AgentEventSink,
    stream_function::StreamFn,
)::Nothing

Main Loop:

current_context = initial_context
config = initial_config
first_turn = true
pending_messages = get_steering_messages()

while true
    # Process steering/follow-up messages
    while !isempty(pending_messages)
        if !first_turn
            emit(TurnStartEvent())
        else
            first_turn = false
        end
        
        # Emit pending messages
        for message in pending_messages
            emit(MessageStartEvent(message))
            emit(MessageEndEvent(message))
            push!(current_context.messages, message)
            push!(new_messages, message)
        end
        
        pending_messages = []
    end
    
    # Stream assistant response
    message = streamAssistantResponse(
        current_context,
        config,
        signal,
        emit,
        stream_function,
    )
    push!(new_messages, message)
    
    # Check for errors
    if message.stop_reason in ("error", "aborted")
        emit(TurnEndEvent(message, []))
        emit(AgentEndEvent(new_messages))
        return
    end
    
    # Execute tool calls
    tool_calls = filter(c -> c isa ToolCall, message.content)
    tool_results = []
    has_more_tool_calls = false
    
    if !isempty(tool_calls)
        executed_batch = if message.stop_reason == "length"
            failToolCallsFromTruncatedMessage(tool_calls, emit)
        else
            executeToolCalls(
                current_context,
                message,
                config,
                signal,
                emit,
            )
        end
        append!(tool_results, executed_batch.messages)
        has_more_tool_calls = !executed_batch.terminate
        
        for result in tool_results
            push!(current_context.messages, result)
            push!(new_messages, result)
        end
    end
    
    emit(TurnEndEvent(message, tool_results))
    
    # Prepare next turn (optional)
    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)
        current_context = next_turn_snapshot.context
        config = AgentLoopConfig(
            model = next_turn_snapshot.model,
            reasoning = next_turn_snapshot.thinking_level,
            # ... other config fields
        )
    end
    
    # Check if should stop
    if should_stop_after_turn(config, next_turn_context)
        emit(AgentEndEvent(new_messages))
        return
    end
    
    # Get next pending messages
    pending_messages = get_steering_messages()
    
    # Check follow-up messages
    follow_up_messages = get_follow_up_messages()
    if !isempty(follow_up_messages)
        pending_messages = follow_up_messages
        continue
    end
    
    break
end

emit(AgentEndEvent(new_messages))

streamAssistantResponse()

function streamAssistantResponse(
    context::AgentContext,
    config::AgentLoopConfig,
    signal::Union{Nothing, AbortSignal},
    emit::AgentEventSink,
    stream_function::StreamFn,
)::AssistantMessage

Flow:

  1. Get messages from context
  2. Apply transform_context (optional)
  3. Convert to LLM messages with convert_to_llm
  4. Create Context object
  5. Resolve API key
  6. Call stream_fn with model, context, and config
  7. Stream events:
    • "start" → MessageStartEvent
    • "text_start", "text_delta", "text_end" → MessageUpdateEvent
    • "done", "error" → MessageEndEvent

executeToolCalls()

function executeToolCalls(
    current_context::AgentContext,
    assistant_message::AssistantMessage,
    config::AgentLoopConfig,
    signal::Union{Nothing, AbortSignal},
    emit::AgentEventSink,
)::ExecutedToolCallBatch

Logic:

tool_calls = filter(c -> c isa ToolCall, assistant_message.content)

# Check if any tool requires sequential execution
has_sequential = any(tc -> begin
    tool = findfirst(t -> t.name == tc.name, current_context.tools)
    !isnothing(tool) && tool.execution_mode == EXECUTION_SEQUENTIAL
end, tool_calls)

# Determine execution mode
if config.tool_execution == EXECUTION_SEQUENTIAL || has_sequential
    executeToolCallsSequential(...)
else
    executeToolCallsParallel(...)
end

executeToolCallsSequential()

function executeToolCallsSequential(
    current_context::AgentContext,
    assistant_message::AssistantMessage,
    tool_calls::Vector{ToolCall},
    config::AgentLoopConfig,
    signal::Union{Nothing, AbortSignal},
    emit::AgentEventSink,
)::ExecutedToolCallBatch

Flow (for each tool call):

  1. Emit ToolExecutionStartEvent
  2. prepareToolCall() → PreparedToolCall or ImmediateToolCallOutcome
  3. If prepared: executePreparedToolCall()
  4. finalizeExecutedToolCall()
  5. Emit ToolExecutionEndEvent
  6. Emit ToolResultMessage
  7. Check if signal.aborted → break

executeToolCallsParallel()

function executeToolCallsParallel(
    current_context::AgentContext,
    assistant_message::AssistantMessage,
    tool_calls::Vector{ToolCall},
    config::AgentLoopConfig,
    signal::Union{Nothing, AbortSignal},
    emit::AgentEventSink,
)::ExecutedToolCallBatch

Flow:

  1. For each tool call:
    • If immediate: execute and add to finalized_calls
    • If prepared: create closure, add to finalized_calls
  2. For each entry in finalized_calls:
    • If closure: execute closure
    • If finalized: use as-is
  3. Collect all tool results
  4. Return batch

prepareToolCall()

function prepareToolCall(
    current_context::AgentContext,
    assistant_message::AssistantMessage,
    tool_call::ToolCall,
    config::AgentLoopConfig,
    signal::Union{Nothing, AbortSignal},
)::Union{PreparedToolCall, ImmediateToolCallOutcome}

Flow:

  1. Find tool by name
  2. If not found → ImmediateToolCallOutcome (error)
  3. before_tool_call hook (optional)
  4. prepareToolCallArguments() (optional)
  5. validateToolArguments()
  6. Return PreparedToolCall

executePreparedToolCall()

function executePreparedToolCall(
    prepared::PreparedToolCall,
    signal::Union{Nothing, AbortSignal},
    emit::AgentEventSink,
)::ExecutedToolCallOutcome

Flow:

  1. Call tool.execute(id, args, signal, on_update)
  2. Collect update events (if any)
  3. Wait for all update events
  4. Return ExecutedToolCallOutcome(result)

finalizeExecutedToolCall()

function finalizeExecutedToolCall(
    current_context::AgentContext,
    assistant_message::AssistantMessage,
    prepared::PreparedToolCall,
    executed::ExecutedToolCallOutcome,
    config::AgentLoopConfig,
    signal::Union{Nothing, AbortSignal},
)::FinalizedToolCallOutcome

Flow:

  1. after_tool_call hook (optional)
  2. Return FinalizedToolCallOutcome

createToolResultMessage()

function createToolResultMessage(
    finalized::FinalizedToolCallOutcome,
)::ToolResultMessage

Creates:

ToolResultMessage(
    "toolResult",
    finalized.tool_call.id,
    finalized.tool_call.name,
    finalized.result.content,
    finalized.result.details,
    finalized.result.usage,
    finalized.result.added_tool_names,
    finalized.is_error,
    timestamp,
)

Execution Modes

Sequential Execution

┌─────────────────────────────────────────────────────────────────────────┐
│                      Sequential Execution Flow                          │
└─────────────────────────────────────────────────────────────────────────┘

┌──────┐
│ TC1  │  ──► prepareToolCall()
└──────┘           │
                   ▼
            ┌──────────────┐
            │ execute()    │  ──► Wait for completion
            └──────────────┘           │
                   │                 ▼
                   ├───────────── createToolResultMessage()
                   │                 │
                   ▼                 ▼
            ┌──────────────┐    ┌──────────┐
            │ TC2  │  ──►   │  │ Result1 │
            └──────┘          └──────────┘
                   │
                   ▼
            ┌──────────────┐
            │ execute()    │
            └──────────────┘
                   │
                   ▼
            ┌──────────────┐
            │ TC3  │  ──►   │
            └──────┘          │
                   │         ▼
                   ├───── createToolResultMessage()
                   │         │
                   ▼         ▼
            ┌──────────────┐ ┌──────────┐
            │ execute()  │ │ │ Result2  │
            └──────────────┘ └──────────┘
                   │
                   ▼
            ┌──────────┐
            │ Result3  │
            └──────────┘

Parallel Execution

┌─────────────────────────────────────────────────────────────────────────┐
│                       Parallel Execution Flow                           │
└─────────────────────────────────────────────────────────────────────────┘

┌──────┐
│ TC1  │  ──► prepareToolCall() ──► create closure ──► ┐
└──────┘                                               │
                                                       │
┌──────┐                                               │
│ TC2  │  ──► prepareToolCall() ──► create closure ──► ├─► All closures queued
└──────┘                                               │
                                                       │
┌──────┐                                               │
│ TC3  │  ──► prepareToolCall() ──► create closure ──► ┘
└──────┘

                      │
                      ▼
          ┌───────────────────────┐
          │ for closure in closures│
          │   execute_closure()   │
          └───────────────────────┘
                      │
                      ▼
          ┌───────────────────────┐
          │ Collect all results   │
          └───────────────────────┘
                      │
                      ▼
          ┌───────────────────────┐
          │ createToolResult()    │
          └───────────────────────┘

Helper Types

ExecutedToolCallBatch

struct ExecutedToolCallBatch
    messages::Vector{ToolResultMessage}
    terminate::Bool
end
  • messages: All tool result messages
  • terminate: If true, stop agent after this batch

PrepareNextTurnContext

struct PrepareNextTurnContext
    message::AssistantMessage
    tool_results::Vector{ToolResultMessage}
    context::AgentContext
    new_messages::Vector{AgentMessage}
end

Used by prepare_next_turn hook to decide next steps.

Before/After Tool Call Contexts

struct BeforeToolCallContext
    assistant_message::AssistantMessage
    tool_call::ToolCall
    args::Any
    context::AgentContext
end

struct BeforeToolCallResult
    block::Union{Bool, Nothing}
    reason::Union{String, Nothing}
end

struct AfterToolCallContext
    assistant_message::AssistantMessage
    tool_call::ToolCall
    args::Any
    result::AgentToolResult
    is_error::Bool
    context::AgentContext
end

struct AfterToolCallResult
    content::Union{Vector{MessageContent}, Nothing}
    details::Union{Any, Nothing}
    is_error::Union{Bool, Nothing}
    usage::Union{Usage, Nothing}
    terminate::Union{Bool, Nothing}
end

Event Emission Timeline

AgentStartEvent
    │
    ├─ TurnStartEvent (turn 1)
    │   │
    │   ├─ MessageStartEvent (user prompt)
    │   ├─ MessageEndEvent (user prompt)
    │   │
    │   ├─ MessageStartEvent (assistant)
    │   ├─ MessageUpdateEvent (text delta)
    │   ├─ MessageUpdateEvent (tool call delta)
    │   ├─ MessageEndEvent (assistant)
    │   │
    │   ├─ ToolExecutionStartEvent (tc1)
    │   ├─ ToolExecutionEndEvent (tc1)
    │   │
    │   ├─ ToolExecutionStartEvent (tc2)
    │   ├─ ToolExecutionEndEvent (tc2)
    │   │
    │   └─ TurnEndEvent (assistant, tool_results)
    │
    ├─ TurnStartEvent (turn 2 - if needed)
    │   │
    │   ├─ MessageStartEvent (steering/follow-up)
    │   ├─ MessageEndEvent (steering/follow-up)
    │   │
    │   ├─ MessageStartEvent (assistant)
    │   ├─ MessageUpdateEvent (text)
    │   ├─ MessageEndEvent (assistant)
    │   │
    │   └─ TurnEndEvent (assistant, [])
    │
    └─ AgentEndEvent (final messages)

Key Concepts

1. Message Transformation Pipeline

AgentMessage[] (internal)
    │
    │ transform_context()
    ▼
AgentMessage[] (transformed)
    │
    │ convert_to_llm()
    ▼
Message[] (LLM API)

2. Tool Call Lifecycle

ToolCall (in assistant message)
    │
    ├─ before_tool_call (hook)
    │
    ├─ prepareToolCall()
    │   ├─ validate arguments
    │   └─ prepare arguments (optional)
    │
    ├─ execute()
    │   ├─ Immediate: return result
    │   └─ Prepared: async execution
    │
    ├─ after_tool_call (hook)
    │
    └─ createToolResultMessage()

3. Turn Termination

# Turn ends when:
# 1. No more pending messages
# 2. No more tool calls to execute
# 3. should_stop_after_turn() returns true

# Reasons to stop:
# - Max turns reached
# - Tool returned terminate=true
# - Error or abort
# - Steering/follow-up queues empty

Best Practices

  1. Use sequential execution for tools that modify shared state
  2. Use parallel execution for independent tool calls (better performance)
  3. Implement prepare_next_turn for dynamic model/thinking level changes
  4. Use before_tool_call for logging or blocking sensitive operations
  5. Use after_tool_call for modifying results or collecting metrics

Complete Example

using AgentCore

# Create config
config = AgentLoopConfig(
    model = my_model,
    reasoning = THINKING_MEDIUM,
    tool_execution = EXECUTION_PARALLEL,
    before_tool_call = myBeforeToolCallHook,
    after_tool_call = myAfterToolCallHook,
    prepare_next_turn = myPrepareNextTurnHook,
    convert_to_llm = myConvertToLlm,
    transform_context = myTransformContext,
    get_api_key = myGetApiKey,
    get_steering_messages = myGetSteeringMessages,
    get_follow_up_messages = myGetFollowUpMessages,
)

# Start agent loop
stream = agentLoop(
    [UserMessage("user", [TextContent("Hello")], timestamp)],
    AgentContext(system_prompt, messages, tools),
    config,
    nothing,
    stream_fn,
)

# Consume events
final_messages = []
for event in stream
    if event isa MessageEndEvent
        push!(final_messages, event.message)
    end
end

# Or use event sink
messages = []
emit(event) = push!(messages, event)

messages = runAgentLoop(
    [UserMessage(...)],
    context,
    config,
    emit,
    nothing,
    stream_fn,
)

This documentation provides a comprehensive understanding of the AgentLoop component, including its architecture, main functions, execution modes, and best practices for building AI agents with AgentCore.jl.