# AgentCore.jl - AgentLoop Component Deep Dive ## AgentLoop Architecture ``` ┌─────────────────────────────────────────────────────────────────────────────┐ │ AgentLoop Layer │ └─────────────────────────────────────────────────────────────────────────────┘ ┌─────────────────────────────────────────────────────────────────────────────┐ │ Public API │ └─────────────────────────────────────────────────────────────────────────────┘ agentLoop() ├─ prompts: Vector{AgentMessage} ├─ context: AgentContext ├─ config: AgentLoopConfig ├─ signal: Union{Nothing, AbortSignal} └─ stream_fn: StreamFn └─ Returns: EventStream agentLoopContinue() ├─ context: AgentContext ├─ config: AgentLoopConfig ├─ signal: Union{Nothing, AbortSignal} └─ stream_fn: StreamFn └─ Returns: EventStream ┌─────────────────────────────────────────────────────────────────────────────┐ │ Internal Flow │ └─────────────────────────────────────────────────────────────────────────────┘ ┌─────────────────────────────────────────────────────────────────────────────┐ │ 1. runAgentLoop() ── Entry point for new conversation │ │ - Creates copy of prompts │ │ - Appends prompts to context.messages │ │ - Emits AgentStartEvent │ │ - Emits TurnStartEvent │ │ - Emits MessageStart/End for each prompt │ │ - Calls runLoop() │ └─────────────────────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────────────────┐ │ 2. runLoop() ── Main event loop │ │ ┌────────────────────────────────────────────────────────────────────┐ │ │ │ while true: │ │ │ │ 1. Get steering/follow-up messages (if any) │ │ │ │ 2. Emit messages as UserMessage │ │ │ │ 3. streamAssistantResponse() │ │ │ │ 4. Execute tool calls (sequential or parallel) │ │ │ │ 5. Emit TurnEndEvent │ │ │ │ 6. prepare_next_turn (optional) │ │ │ │ 7. should_stop_after_turn? (check termination) │ │ │ │ 8. Loop continues if not terminated │ │ │ └────────────────────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────────────────┐ │ 3. streamAssistantResponse() ── LLM interaction │ │ - transform_context (optional) │ │ - convert_to_llm (transform to Message[]) │ │ - Call stream_fn (LLM API) │ │ - Stream response deltas │ │ - Emit MessageStart/Update/End events │ └─────────────────────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────────────────┐ │ 4. executeToolCalls() ── Tool execution │ │ ┌────────────────────────────────────────────────────────────────────┐ │ │ │ if EXECUTION_SEQUENTIAL || has_sequential_tool: │ │ │ │ executeToolCallsSequential() │ │ │ │ else: │ │ │ │ executeToolCallsParallel() │ │ │ └────────────────────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────────────────┐ │ 5. AgentEndEvent ── Final event with all messages │ └─────────────────────────────────────────────────────────────────────────────┘ ``` ## AgentLoopConfig ```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} end ``` ## 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. Append prompts to context.messages 3. Emit AgentStartEvent 4. For each prompt: emit MessageStartEvent, MessageEndEvent 5. Call runLoop() ### 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**: ```julia 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() ```julia function streamAssistantResponse( context::AgentContext, config::AgentLoopConfig, signal::Union{Nothing, AbortSignal}, emit::AgentEventSink, stream_function::StreamFn, )::AssistantMessage ``` **Flow**: 1. Get messages from context 2. Apply transform_context (optional) 3. Convert to LLM messages with convert_to_llm 4. Create Context object 5. Resolve API key 6. Call stream_fn with model, context, and config 7. Stream events: - "start" → MessageStartEvent - "text_start", "text_delta", "text_end" → MessageUpdateEvent - "done", "error" → MessageEndEvent ### executeToolCalls() ```julia function executeToolCalls( current_context::AgentContext, assistant_message::AssistantMessage, config::AgentLoopConfig, signal::Union{Nothing, AbortSignal}, emit::AgentEventSink, )::ExecutedToolCallBatch ``` **Logic**: ```julia tool_calls = filter(c -> c isa ToolCall, assistant_message.content) # Check if any tool requires sequential execution has_sequential = any(tc -> begin tool = findfirst(t -> t.name == tc.name, current_context.tools) !isnothing(tool) && tool.execution_mode == EXECUTION_SEQUENTIAL end, tool_calls) # Determine execution mode if config.tool_execution == EXECUTION_SEQUENTIAL || has_sequential executeToolCallsSequential(...) else executeToolCallsParallel(...) end ``` ### executeToolCallsSequential() ```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} ``` **Flow**: 1. Find tool by name 2. If not found → ImmediateToolCallOutcome (error) 3. before_tool_call hook (optional) 4. prepareToolCallArguments() (optional) 5. validateToolArguments() 6. Return PreparedToolCall ### executePreparedToolCall() ```julia function executePreparedToolCall( prepared::PreparedToolCall, signal::Union{Nothing, AbortSignal}, emit::AgentEventSink, )::ExecutedToolCallOutcome ``` **Flow**: 1. Call tool.execute(id, args, signal, on_update) 2. Collect update events (if any) 3. Wait for all update events 4. Return ExecutedToolCallOutcome(result) ### finalizeExecutedToolCall() ```julia function finalizeExecutedToolCall( current_context::AgentContext, assistant_message::AssistantMessage, prepared::PreparedToolCall, executed::ExecutedToolCallOutcome, config::AgentLoopConfig, signal::Union{Nothing, AbortSignal}, )::FinalizedToolCallOutcome ``` **Flow**: 1. after_tool_call hook (optional) 2. Return FinalizedToolCallOutcome ### createToolResultMessage() ```julia function createToolResultMessage( finalized::FinalizedToolCallOutcome, )::ToolResultMessage ``` **Creates**: ```julia ToolResultMessage( "toolResult", finalized.tool_call.id, finalized.tool_call.name, finalized.result.content, finalized.result.details, finalized.result.usage, finalized.result.added_tool_names, finalized.is_error, timestamp, ) ``` ## Execution Modes ### Sequential Execution ``` ┌─────────────────────────────────────────────────────────────────────────┐ │ Sequential Execution Flow │ └─────────────────────────────────────────────────────────────────────────┘ ┌──────┐ │ TC1 │ ──► prepareToolCall() └──────┘ │ ▼ ┌──────────────┐ │ execute() │ ──► Wait for completion └──────────────┘ │ │ ▼ ├───────────── createToolResultMessage() │ │ ▼ ▼ ┌──────────────┐ ┌──────────┐ │ TC2 │ ──► │ │ Result1 │ └──────┘ └──────────┘ │ ▼ ┌──────────────┐ │ execute() │ └──────────────┘ │ ▼ ┌──────────────┐ │ TC3 │ ──► │ └──────┘ │ │ ▼ ├───── createToolResultMessage() │ │ ▼ ▼ ┌──────────────┐ ┌──────────┐ │ execute() │ │ │ Result2 │ └──────────────┘ └──────────┘ │ ▼ ┌──────────┐ │ Result3 │ └──────────┘ ``` ### Parallel Execution ``` ┌─────────────────────────────────────────────────────────────────────────┐ │ Parallel Execution Flow │ └─────────────────────────────────────────────────────────────────────────┘ ┌──────┐ │ TC1 │ ──► prepareToolCall() ──► create closure ──► ┐ └──────┘ │ │ ┌──────┐ │ │ TC2 │ ──► prepareToolCall() ──► create closure ──► ├─► All closures queued └──────┘ │ │ ┌──────┐ │ │ TC3 │ ──► prepareToolCall() ──► create closure ──► ┘ └──────┘ │ ▼ ┌───────────────────────┐ │ for closure in closures│ │ execute_closure() │ └───────────────────────┘ │ ▼ ┌───────────────────────┐ │ Collect all results │ └───────────────────────┘ │ ▼ ┌───────────────────────┐ │ createToolResult() │ └───────────────────────┘ ``` ## Helper Types ### ExecutedToolCallBatch ```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 ``` AgentMessage[] (internal) │ │ transform_context() ▼ AgentMessage[] (transformed) │ │ convert_to_llm() ▼ Message[] (LLM API) ``` ### 2. Tool Call Lifecycle ``` ToolCall (in assistant message) │ ├─ before_tool_call (hook) │ ├─ prepareToolCall() │ ├─ validate arguments │ └─ prepare arguments (optional) │ ├─ execute() │ ├─ Immediate: return result │ └─ Prepared: async execution │ ├─ after_tool_call (hook) │ └─ createToolResultMessage() ``` ### 3. Turn Termination ```julia # 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 ```julia 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.