Files
YiemAgent/learning/03-AGENTLOOP_COMPONENT.md
T
2026-07-31 11:41:53 +07:00

47 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

┌─────────────────────────────────────────────────────────────────────────────┐
│ Data Flow with Type Transformations                                         │
└─────────────────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────────────────┐
│ 1. runAgentLoop() ── Entry point for new conversation                       │
│    Input:  prompts::Vector{AgentMessage}                                    │
│            context::AgentContext (system_prompt, messages, tools)           │
│            config::AgentLoopConfig                                          │
│    Output: new_messages::Vector{AgentMessage} (appended prompts + turns)    │
│                                                                             │
│    - 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                                             │
│    Input:  current_context::AgentContext                                    │
│            new_messages::Vector{AgentMessage}                               │
│    Output: N/A (writes to new_messages and context.messages)                │
│                                                                             │
│    ┌────────────────────────────────────────────────────────────────────┐   │
│    │ while true (outer loop: follow-up messages)                        │   │
│    │   has_more_tool_calls = true                                       │   │
│    │   while has_more_tool_calls || !isempty(pending_messages)          │   │
│    │       1. Emit TurnStartEvent (on subsequent turns)                 │   │
│    │       2. If pending_messages: emit MessageStart/End, drain queue   │   │
│    │       3. streamAssistantResponse()                                 │   │
│    │       4. If error/aborted: emit TurnEnd, AgentEnd, return          │   │
│    │       5. Execute tool calls (sequential or parallel)               │   │
│    │       6. has_more_tool_calls = !batch.terminate                    │   │
│    │       7. Emit TurnEndEvent                                         │   │
│    │       8. prepare_next_turn (optional config update)                │   │
│    │       9. should_stop_after_turn? (early return)                    │   │
│    │       10. pending_messages = get_steering_messages()               │   │
│    │   if !isempty(get_follow_up_messages()) → continue outer loop      │   │
│    │   break                                                            │   │
│    │   emit AgentEndEvent                                               │   │
│    └────────────────────────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────────────────────────┘
                                    │
                                    ▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ 3. streamAssistantResponse() ── LLM interaction                             │
│    Input:  context::AgentContext                                            │
│            config::AgentLoopConfig                                          │
│    Output: message::AssistantMessage                                        │
│                                                                             │
│    Data Transformations:                                                    │
│    ┌────────────────────────────────────────────────────────────────────┐   │
│    │ Step 1: transform_context (optional)                               │   │
│    │   Input:  context.messages::Vector{AgentMessage}                   │   │
│    │   Output: messages::Vector{AgentMessage} (transformed)             │   │
│    │                                                                    │   │
│    │ Step 2: convert_to_llm                                             │   │
│    │   Input:  messages::Vector{AgentMessage}                           │   │
│    │   Output: llm_messages::Vector{Message}                            │   │
│    │      - UserMessage → UserMessage                                   │   │
│    │      - AssistantMessage → AssistantMessage                         │   │
│    │      - ToolResultMessage → ToolResultMessage                       │   │
│    │      - BashExecutionMessage → UserMessage                          │   │
│    │      - CompactionSummaryMessage → UserMessage                      │   │
│    │      - BranchSummaryMessage → UserMessage                          │   │
│    │                                                                    │   │
│    │ Step 3: Call stream_fn                                             │   │
│    │   Input:  model, llm_context::Context, config                      │   │
│    │   Output: response::Stream (events)                                │   │
│    │                                                                    │   │
│    │ Step 4: Stream events                                              │   │
│    │   Events: start, text_start/delta/end, toolcall_start/delta/end    │   │
│    │   Final: AssistantMessage (with ToolCall[] in content)             │   │
│    └────────────────────────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────────────────────────┘
                                    │
                                    ▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ 4. executeToolCalls() ── Tool execution                                     │
│    Input:  assistant_message::AssistantMessage (contains ToolCall[])        │
│            current_context::AgentContext                                    │
│    Output: ExecutedToolCallBatch (messages::ToolResultMessage[], terminate) │
│                                                                             │
│    For each ToolCall:                                                       │
│    ┌────────────────────────────────────────────────────────────────────┐   │
│    │ prepareToolCall()                                                  │   │
│    │   Input:  tool_call::ToolCall                                      │   │
│    │   Output: PreparedToolCall or ImmediateToolCallOutcome             │   │
│    │      - validates arguments                                         │   │
│    │      - runs before_tool_call hook (optional)                       │   │
│    │      - runs prepare_arguments hook (optional)                      │   │
│    │                                                                    │   │
│    │ executePreparedToolCall() (if prepared)                            │   │
│    │   Input:  PreparedToolCall                                         │   │
│    │   Output: ExecutedToolCallOutcome                                  │   │
│    │      - calls tool.execute()                                        │   │
│    │      - returns AgentToolResultMutable                              │   │
│    │                                                                    │   │
│    │ finalizeExecutedToolCall()                                         │   │
│    │   Input:  ExecutedToolCallOutcome                                  │   │
│    │   Output: FinalizedToolCallOutcome                                 │   │
│    │      - runs after_tool_call hook (optional)                        │   │
│    │      - returns ToolCall + AgentToolResultMutable + is_error        │   │
│    │                                                                    │   │
│    │ createToolResultMessage()                                          │   │
│    │   Input:  FinalizedToolCallOutcome                                 │   │
│    │   Output: ToolResultMessage                                        │   │
│    │      - role: "toolResult"                                          │   │
│    │      - tool_call_id, tool_name, content, details                   │   │
│    │      - usage, added_tool_names, is_error, timestamp                │   │
│    └────────────────────────────────────────────────────────────────────┘   │
│                                                                             │
│    ┌────────────────────────────────────────────────────────────────────┐   │
│    │ Sequential: executeToolCallsSequential()                           │   │
│    │   - Executes tools one at a time, waits for each                   │   │
│    │   - Returns batch of ToolResultMessage[]                           │   │
│    │                                                                    │   │
│    │ Parallel: executeToolCallsParallel()                               │   │
│    │   - Creates closures for async execution                           │   │
│    │   - Executes all closures, collects results                        │   │
│    │   - Returns batch of ToolResultMessage[]                           │   │
│    └────────────────────────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────────────────────────┘
                                    │
                                    ▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ 5. AgentEndEvent ── Final event with all messages                           │
│    Output: messages::Vector{AgentMessage}                                   │
│    Contains: [UserMessage, AssistantMessage, ToolResultMessage, ...]        │
└─────────────────────────────────────────────────────────────────────────────┘

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}
    should_stop_after_turn::Union{Function, Nothing}
    max_tokens::Union{Int64, Nothing}
    temperature::Union{Float64, Nothing}
    cache_retention::Union{String, Nothing}
    headers::Union{Dict{String, String}, Nothing}
    metadata::Union{Dict{String, Any}, Nothing}
    signal::Union{Any, Nothing}
    api_key::Union{String, Nothing}
end

Notes:

  • should_stop_after_turn(context::PrepareNextTurnContext)::Bool — Default returns false. Use to implement custom termination logic (e.g., max turns, tool-specific termination).
  • max_tokens, temperature, cache_retention — Passed through to the LLM API provider.
  • signal, api_key — Per-request overrides for abort handling and authentication.
  • headers, metadata — Passed through to the LLM API provider.

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. Create current_context with prompts appended to context.messages
  3. Emit AgentStartEvent
  4. Emit TurnStartEvent
  5. For each prompt: emit MessageStartEvent, MessageEndEvent
  6. Call runLoop() — handles the main loop, tool execution, and termination
  7. Return new_messages

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 (simplified — shows structure; actual code has type annotations):

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

while true
    has_more_tool_calls = true

    # Inner loop: process pending messages AND/OR tool results
    while has_more_tool_calls || !isempty(pending_messages)
        if !first_turn
            emit(TurnStartEvent())
        else
            first_turn = false
        end

        # Emit pending messages (steering / follow-up)
        if !isempty(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 = AgentMessage[]
        end

        # Stream assistant response
        message = streamAssistantResponse(
            current_context, config, signal, emit, stream_function
        )
        push!(new_messages, message)

        # Early exit on error/abort
        if message.stop_reason in ("error", "aborted")
            emit(TurnEndEvent(message, ToolResultMessage[]))
            emit(AgentEndEvent(new_messages))
            return
        end

        # Execute tool calls (if any)
        tool_calls = filter(c -> c isa ToolCall, message.content)
        tool_results = ToolResultMessage[]
        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))

        # Optional: prepare next turn (model/thinking/context changes)
        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
            # Rebuild config with updated model/thinking + preserved fields
            config = AgentLoopConfig(
                model = next_turn_snapshot.model,
                reasoning = next_turn_snapshot.thinking_level,
                convert_to_llm = config.convert_to_llm,
                transform_context = config.transform_context,
                get_api_key = config.get_api_key,
                should_stop_after_turn = config.should_stop_after_turn,
                prepare_next_turn = config.prepare_next_turn,
                get_steering_messages = config.get_steering_messages,
                get_follow_up_messages = config.get_follow_up_messages,
                tool_execution = config.tool_execution,
                before_tool_call = config.before_tool_call,
                after_tool_call = config.after_tool_call,
                max_tokens = config.max_tokens,
                temperature = config.temperature,
                reasoning = config.reasoning,
                cache_retention = config.cache_retention,
                session_id = config.session_id,
                headers = config.headers,
                metadata = config.metadata,
                transport = config.transport,
                signal = signal,
                api_key = config.api_key,
                on_payload = config.on_payload,
                on_response = config.on_response,
                max_retry_delay_ms = config.max_retry_delay_ms,
            )
        end

        # Check termination
        if should_stop_after_turn(config, next_turn_context)
            emit(AgentEndEvent(new_messages))
            return
        end

        # Get next steering messages
        pending_messages = get_steering_messages(config)
    end

    # Check follow-up messages (processed only after all tool calls complete)
    follow_up_messages = get_follow_up_messages(config)
    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

Data Flow:

Input: context.messages::Vector{AgentMessage}
         ↓
    [transform_context] (optional hook)
         ↓
    messages::Vector{AgentMessage}
         ↓
    [convert_to_llm] - Type transformation pipeline
         ↓
    llm_messages::Vector{Message}
         │
         │  AgentMessage → Message mapping:
         │  • UserMessage → UserMessage (pass-through)
         │  • AssistantMessage → AssistantMessage (pass-through)
         │  • ToolResultMessage → ToolResultMessage (pass-through)
         │  • BashExecutionMessage → UserMessage (text conversion)
         │  • CompactionSummaryMessage → UserMessage (text wrapped)
         │  • BranchSummaryMessage → UserMessage (text wrapped)
         │
         ▼
    Context(system_prompt, llm_messages, tools)
         ↓
    stream_fn(model, context, config) - LLM API call
         ↓
    Stream of AssistantMessageEvent:
    • StartEvent (partial AssistantMessage)
    • TextStartEvent/TextDeltaEvent/TextEndEvent
    • ToolCallStartEvent/ToolCallDeltaEvent/ToolCallEndEvent
    • DoneEvent (final AssistantMessage with usage, stop_reason)
         ↓
    Return: AssistantMessage
            - content::Vector{MessageContent}
            - usage::Usage
            - stop_reason::String
            - Contains ToolCall[] if tool calls requested

executeToolCalls()

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

Data Flow:

Input: assistant_message::AssistantMessage
       - content::Vector{MessageContent}
         └─ Contains ToolCall[] and/or TextContent[]
         ↓
    filter(c -> c isa ToolCall, assistant_message.content)
         ↓
    tool_calls::Vector{ToolCall}
       - type: "tool"
       - id::String
       - name::String
       - arguments::Dict{String, Any}
       - partial_json::Union{String, Nothing}
         ↓
    Check execution mode:
    • config.tool_execution (sequential/parallel)
    • Any tool.execution_mode == EXECUTION_SEQUENTIAL?
         ↓
    ┌─────────────────────────────────────────────────────────────────┐
    │ Sequential Mode (or has_sequential_tool)                        │
    │ For each tool_call in tool_calls:                               │
    │   prepareToolCall() → PreparedToolCall                          │
    │   executePreparedToolCall() → ExecutedToolCallOutcome           │
    │   finalizeExecutedToolCall() → FinalizedToolCallOutcome         │
    │   createToolResultMessage() → ToolResultMessage                 │
    │   (wait for completion before next tool)                        │
    └─────────────────────────────────────────────────────────────────┘
         ↓
    ┌─────────────────────────────────────────────────────────────────┐
    │ Parallel Mode                                                   │
    │ For each tool_call in tool_calls:                               │
    │   prepareToolCall() → (PreparedToolCall | ImmediateOutcome)     │
    │   If prepared: create closure                                   │
    │   If immediate: execute and add to finalized_calls              │
    │                                                                 │
    │ For each entry in finalized_calls:                              │
    │   If closure: execute closure                                   │
    │   If finalized: use as-is                                       │
    └─────────────────────────────────────────────────────────────────┘
         ↓
    ExecutedToolCallBatch
       - messages::Vector{ToolResultMessage}
       - terminate::Bool (true if all tools have terminate=true)

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}

Data Flow:

Input: tool_call::ToolCall
       - id::String
       - name::String
       - arguments::Dict{String, Any}
         ↓
    findfirst(t -> t.name == tool_call.name, current_context.tools)
         ↓
    If tool is nothing:
    → ImmediateToolCallOutcome("immediate", error_result, is_error=true)
         ↓
    If tool exists:
    [before_tool_call hook] (optional)
       Input: BeforeToolCallContext(assistant_message, tool_call, args, context)
       Output: BeforeToolCallResult (block, reason) or nothing
       If block=true → ImmediateToolCallOutcome(error)
         ↓
    prepareToolCallArguments(tool, tool_call)
       Input: tool_call.arguments::Dict{String, Any}
       Output: prepared_arguments::Any
       (Optional: transform arguments before validation)
         ↓
    validateToolArguments(tool, prepared_tool_call)
       Input: prepared_tool_call.arguments
       Output: validated_args::Any
       (Optional: JSON schema validation)
         ↓
    Return: PreparedToolCall("prepared", tool_call, tool, validated_args)
            - kind: "prepared"
            - tool_call: ToolCall (original)
            - tool: AgentTool
            - args: validated arguments

executePreparedToolCall()

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

Data Flow:

Input: prepared::PreparedToolCall
       - tool_call::ToolCall
       - tool::AgentTool
       - args::Any (validated)
         ↓
    tool.execute(tool_call.id, args, signal, on_update)
       Input: tool_call_id::String
              args::Any
              signal::Union{Any, Nothing}
              on_update::Function (partial_result → void)
        Output: AgentToolResultMutable (defined in agent_loop.jl)
                - content::Vector{MessageContent}
                - details::Any
                - usage::Union{Usage, Nothing}
                - added_tool_names::Union{Vector{String}, Nothing}
                - terminate::Union{Bool, Nothing}
          ↓
      Note: tool.execute signature is
            (tool_call_id, args, signal, on_update, context)
            where context is the tool's captured context closure parameter
          ↓
     Collect update events from on_update callbacks
         ↓
    Return: ExecutedToolCallOutcome(result, is_error=false)
            - result::AgentToolResultMutable

finalizeExecutedToolCall()

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

Data Flow:

Input: executed::ExecutedToolCallOutcome
       - result::AgentToolResultMutable
       - is_error::Bool
         ↓
    [after_tool_call hook] (optional)
       Input: AfterToolCallContext(
                  assistant_message,
                  tool_call,
                  args,
                  result,
                  is_error,
                  context
              )
        Output: AfterToolCallResult (optional patches)
                - content::Union{Vector{MessageContent}, Nothing}
                - details::Union{Any, Nothing}
                - is_error::Union{Bool, Nothing}
                - usage::Union{Usage, Nothing}
                - terminate::Union{Bool, Nothing}
          ↓
     Apply patches to result (if patches not nothing, replace non-nothing fields)
     result = AgentToolResultMutable(
         patches.content  != nothing ? patches.content  : result.content,
         patches.details  != nothing ? patches.details  : result.details,
         patches.usage    != nothing ? patches.usage    : result.usage,
         result.added_tool_names,          # not patched
         patches.terminate != nothing ? patches.terminate : result.terminate,
     )
     is_error = patches.is_error != nothing ? patches.is_error : is_error
          ↓
    Return: FinalizedToolCallOutcome
            - tool_call::ToolCall (original)
            - result::AgentToolResultMutable (final)
            - is_error::Bool

createToolResultMessage()

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

Data Flow:

Input: finalized::FinalizedToolCallOutcome
       - tool_call::ToolCall
       - result::AgentToolResultMutable
         - content::Vector{MessageContent}
         - details::Any
         - usage::Union{Usage, Nothing}
         - added_tool_names::Union{Vector{String}, Nothing}
       - is_error::Bool
         ↓
    Build ToolResultMessage:
    • role: "toolResult"
    • tool_call_id: tool_call.id
    • tool_name: tool_call.name
    • content: result.content
    • details: result.details
    • usage: result.usage
    • added_tool_names: result.added_tool_names
    • is_error: is_error
    • timestamp: Int64(Dates.now(Dates.UTC).datetime)
         ↓
    Output: ToolResultMessage
            - role::String ("toolResult")
            - tool_call_id::String
            - tool_name::String
            - content::Vector{MessageContent}
            - details::Any
            - usage::Union{Usage, Nothing}
            - added_tool_names::Union{Vector{String}, Nothing}
            - is_error::Bool
            - timestamp::Timestamp (Int64)

Execution Modes

Sequential Execution

┌─────────────────────────────────────────────────────────────────────────┐
│                      Sequential Execution Flow (Strict Order)            │
└─────────────────────────────────────────────────────────────────────────┘

  TC1                          TC2                          TC3
  │                            │                            │
  ▼                            ▼                            ▼
┌──────────────────┐    ┌──────────────────┐    ┌──────────────────┐
│ prepareToolCall()│───▶│ prepareToolCall()│───▶│ prepareToolCall()│
└──────────────────┘    └──────────────────┘    └──────────────────┘
          │                       │                       │
          ▼                       ▼                       ▼
┌──────────────────┐    ┌──────────────────┐    ┌──────────────────┐
│  execute()       │    │  execute()       │    │  execute()       │
│  (blocking)      │    │  (blocking)      │    │  (blocking)      │
└──────────────────┘    └──────────────────┘    └──────────────────┘
          │                       │                       │
          ▼                       ▼                       ▼
┌──────────────────┐    ┌──────────────────┐    ┌──────────────────┐
│ finalize()       │    │ finalize()       │    │ finalize()       │
│ + emit events    │    │ + emit events    │    │ + emit events    │
└──────────────────┘    └──────────────────┘    └──────────────────┘
          │                       │                       │
          ▼                       ▼                       ▼
     Result1               Result2               Result3
          │                       │                       │
          └───────────────────────┴───────────────────────┘
                                  │
                                  ▼
                     ┌────────────────────────┐
                     │ ExecutedToolCallBatch  │
                     │  (Result1, Result2,    │
                     │   Result3, terminate)  │
                     └────────────────────────┘

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

Vector{AgentMessage} (internal conversation history)
    │
    ├─ transform_context() (optional hook)
    │   Input:  Vector{AgentMessage}
    │   Output: Vector{AgentMessage} (transformed)
    │
    └─ convert_to_llm()
       │
       │  Type mapping (single dispatch):
       │  • UserMessage → UserMessage (pass-through)
       │  • AssistantMessage → AssistantMessage (pass-through)
       │  • ToolResultMessage → ToolResultMessage (pass-through)
       │  • BashExecutionMessage → UserMessage (text conversion)
       │  • CompactionSummaryMessage → UserMessage (text wrapped)
       │  • BranchSummaryMessage → UserMessage (text wrapped)
       │
       ▼
    Vector{Message} (for LLM API)

2. Tool Call Lifecycle (with Data Transformations)

ToolCall (in AssistantMessage.content)
    │
    ├─ before_tool_call hook (optional)
    │   Input:  BeforeToolCallContext(
    │               assistant_message::AssistantMessage,
    │               tool_call::ToolCall,
    │               args::Dict{String, Any},
    │               context::AgentContext
    │           )
    │   Output: BeforeToolCallResult (block, reason) or nothing
    │
    ├─ prepareToolCall()
    │   Input:  tool_call::ToolCall
    │   Output: Union{PreparedToolCall, ImmediateToolCallOutcome}
    │       • PreparedToolCall (kind, tool_call, tool, args)
    │       • ImmediateToolCallOutcome (immediate, result, is_error)
    │
    ├─ executePreparedToolCall() (if prepared)
    │   Input:  PreparedToolCall
    │   Output: ExecutedToolCallOutcome
    │   tool.execute() returns AgentToolResultMutable
    │       • content::Vector{MessageContent}
    │       • details::Any
    │       • usage::Union{Usage, Nothing}
    │       • terminate::Union{Bool, Nothing}
    │
    ├─ finalizeExecutedToolCall()
    │   Input:  ExecutedToolCallOutcome
    │   Output: FinalizedToolCallOutcome
    │       • tool_call::ToolCall
    │       • result::AgentToolResultMutable
    │       • is_error::Bool
    │
    └─ createToolResultMessage()
        Input:  FinalizedToolCallOutcome
        Output: ToolResultMessage
            • role: "toolResult"
            • tool_call_id, tool_name
            • content::Vector{MessageContent}
            • details, usage, added_tool_names
            • is_error, timestamp

3. Turn Termination

# The outer while-true loop exits when:
# 1. No pending messages AND no tool results to reprocess (inner loop ends)
# 2. No follow-up messages to queue
# 3. should_stop_after_turn() returns true (checked after each tool-call batch)

# Termination conditions:
# - message.stop_reason in ("error", "aborted") → immediate return
# - should_stop_after_turn() hook returns true → return AgentEndEvent
# - tool result batch has terminate=true → has_more_tool_calls = false, exit inner loop
# - No pending messages, no follow-up messages → break outer loop

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,
    session_id = nothing,
    on_payload = nothing,
    on_response = nothing,
    transport = "auto",
    thinking_budgets = nothing,
    max_retry_delay_ms = nothing,
    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,
    should_stop_after_turn = myShouldStopHook,  # Default: always return false
    max_tokens = nothing,
    temperature = nothing,
    cache_retention = nothing,
    headers = nothing,
    metadata = nothing,
    signal = nothing,
    api_key = nothing,
)

# 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.