Files
YiemAgent/learning/03-AGENTLOOP_COMPONENT.md
T
2026-07-30 09:28:19 +07:00

42 KiB
Raw Blame History

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:                                                        │   │
│    │   1. Get steering/follow-up messages (if any)                      │   │
│    │   2. Emit messages as UserMessage                                  │   │
│    │   3. streamAssistantResponse()                                     │   │
│    │      - Input:  context.messages::Vector{AgentMessage}              │   │
│    │      - Output: message::AssistantMessage                           │   │
│    │   4. Execute tool calls (sequential or parallel)                   │   │
│    │      - Input:  AssistantMessage with ToolCall[]                    │   │
│    │      - Output: tool_results::Vector{ToolResultMessage}             │   │
│    │   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                             │
│    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}
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

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
               - content::Vector{MessageContent}
               - details::Any
               - usage::Union{Usage, Nothing}
               - added_tool_names::Union{Vector{String}, Nothing}
               - terminate::Union{Bool, Nothing}
         ↓
    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 any)
    result.content = result.content  patches.content
    result.details = result.details  patches.details
    is_error = is_error  patches.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                          │
└─────────────────────────────────────────────────────────────────────────┘

┌──────┐
│ 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

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

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