This commit is contained in:
2026-07-31 11:41:53 +07:00
parent c9a7661e93
commit 7876ff21eb
8 changed files with 1671 additions and 2168 deletions
+221 -170
View File
@@ -53,19 +53,22 @@ agentLoopContinue()
│ 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 │ │
│ │ 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 │ │
│ └────────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────────┘
@@ -179,9 +182,23 @@ struct AgentLoopConfig
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()
@@ -236,11 +253,13 @@ function runAgentLoop(
**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()
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
@@ -255,109 +274,124 @@ function runLoop(
)::Nothing
```
**Main Loop**:
**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()
pending_messages = get_steering_messages(config)
while true
# Process steering/follow-up messages
while !isempty(pending_messages)
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
for message in pending_messages
emit(MessageStartEvent(message))
emit(MessageEndEvent(message))
push!(current_context.messages, message)
push!(new_messages, message)
# 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
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,
# 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
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)
# 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
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()
# 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
@@ -422,7 +456,7 @@ function executeToolCalls(
current_context::AgentContext,
assistant_message::AssistantMessage,
config::AgentLoopConfig,
signal::Union{Nothing, AbortSignal>,
signal::Union{Nothing, AbortSignal},
emit::AgentEventSink,
)::ExecutedToolCallBatch
```
@@ -572,7 +606,7 @@ Input: tool_call::ToolCall
```julia
function executePreparedToolCall(
prepared::PreparedToolCall,
signal::Union{Nothing, AbortSignal>,
signal::Union{Nothing, AbortSignal},
emit::AgentEventSink,
)::ExecutedToolCallOutcome
```
@@ -590,14 +624,18 @@ Input: prepared::PreparedToolCall
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
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
@@ -612,7 +650,7 @@ function finalizeExecutedToolCall(
prepared::PreparedToolCall,
executed::ExecutedToolCallOutcome,
config::AgentLoopConfig,
signal::Union{Nothing, AbortSignal>,
signal::Union{Nothing, AbortSignal},
)::FinalizedToolCallOutcome
```
@@ -632,18 +670,23 @@ Input: executed::ExecutedToolCallOutcome
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
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)
@@ -699,45 +742,39 @@ Input: finalized::FinalizedToolCallOutcome
```
┌─────────────────────────────────────────────────────────────────────────┐
│ Sequential Execution Flow
│ Sequential Execution Flow (Strict Order)
└─────────────────────────────────────────────────────────────────────────┘
┌──────┐
│ TC1 │ ──► prepareToolCall()
└──────┘ │
┌──────────────┐
│ execute() │ ──► Wait for completion
└──────────────┘
├───────────── createToolResultMessage()
▼ ▼
────────────── ──────────
TC2 ──► │ Result1
└──────┘ └──────────┘
┌──────────────┐
│ execute() │
└──────────────┘
┌──────────────┐
│ TC3 │ ──► │
└──────┘
───── createToolResultMessage()
▼ ▼
┌──────────────┐ ┌──────────┐
│ execute() │ │ │ Result2 │
└──────────────┘ └──────────┘
┌──────────┐
│ Result3 │
└──────────┘
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
@@ -948,16 +985,16 @@ ToolCall (in AssistantMessage.content)
### 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
# 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)
# Reasons to stop:
# - Max turns reached
# - Tool returned terminate=true
# - Error or abort
# - Steering/follow-up queues empty
# 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
@@ -977,6 +1014,12 @@ using AgentCore
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,
@@ -986,6 +1029,14 @@ config = AgentLoopConfig(
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