# 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 ```julia 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() ```julia 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 ```julia 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() ```julia 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 ```julia 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): ```julia 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() ```julia 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() ```julia 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() ```julia 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() ```julia 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() ```julia 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() ```julia 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() ```julia 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() ```julia 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 ```julia struct ExecutedToolCallBatch messages::Vector{ToolResultMessage} terminate::Bool end ``` - `messages`: All tool result messages - `terminate`: If true, stop agent after this batch ### PrepareNextTurnContext ```julia 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 ```julia 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 ```julia # 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 ```julia 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.