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

1023 lines
42 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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
```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
```
**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
- 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()
```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 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()
```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 │
└─────────────────────────────────────────────────────────────────────────┘
┌──────┐
│ 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
```
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
# 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.