update
This commit is contained in:
+221
-170
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user