1023 lines
42 KiB
Markdown
1023 lines
42 KiB
Markdown
# 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.
|