273 lines
9.2 KiB
Julia
273 lines
9.2 KiB
Julia
struct preparedToolCall
|
|
tool::AgentTool
|
|
toolCall::AgentToolCall
|
|
args::Any
|
|
end
|
|
|
|
struct immediateOutcome
|
|
result::AgentToolResult
|
|
isError::Bool
|
|
end
|
|
|
|
struct executedOutcome
|
|
result::AgentToolResult
|
|
isError::Bool
|
|
end
|
|
|
|
struct finalizedOutcome
|
|
toolCall::AgentToolCall
|
|
result::AgentToolResult
|
|
isError::Bool
|
|
end
|
|
|
|
struct toolCallBatch
|
|
messages::Vector{ToolResultMessage}
|
|
terminate::Bool
|
|
end
|
|
|
|
# ── helpers ─────────────────────────────────────────────────────
|
|
|
|
function createErrorToolResult(msg::String)::AgentToolResult
|
|
return AgentToolResult([TextContent("text", msg)], Dict{Any,Any}())
|
|
end
|
|
|
|
function createToolResultMessage(f::finalizedOutcome)::ToolResultMessage
|
|
return ToolResultMessage(
|
|
"toolResult", f.toolCall.id, f.toolCall.name,
|
|
f.result.content, f.result.details, f.result.usage,
|
|
get(f.result, :addedToolNames, String[]), f.isError, now_millis()
|
|
)
|
|
end
|
|
|
|
function shouldTerminate(batches::Vector{finalizedOutcome})::Bool
|
|
return !isempty(batches) && all(b -> b.result.terminate, batches)
|
|
end
|
|
|
|
# ── per-call preparation ────────────────────────────────────────
|
|
|
|
function prepareToolCall(
|
|
context::AgentContext,
|
|
assistantMsg::AssistantMessage,
|
|
toolCall::AgentToolCall,
|
|
config::AgentLoopConfig,
|
|
signal::Union{Nothing,AbortSignal},
|
|
)::Union{preparedToolCall,immediateOutcome}
|
|
|
|
tool = find(t -> t.name == toolCall.name, context.tools)
|
|
if tool === nothing
|
|
return immediateOutcome(createErrorToolResult("Tool $toolCall.name not found"), true)
|
|
end
|
|
|
|
try
|
|
# 1. prepare arguments (tool-specific transform)
|
|
preparedArgs = prepareToolCallArguments(tool, toolCall)
|
|
validatedArgs = validateToolArguments(tool, preparedArgs)
|
|
|
|
# 2. beforeToolCall hook
|
|
if config.before_tool_call !== nothing
|
|
before = config.before_tool_call(
|
|
AssistantMsgCtx(assistantMsg, toolCall, validatedArgs, context), signal
|
|
)
|
|
if signal !== nothing && signal.aborted
|
|
return immediateOutcome(createErrorToolResult("Operation aborted"), true)
|
|
end
|
|
if before !== nothing && before.block
|
|
return immediateOutcome(
|
|
createErrorToolResult(get(before, :reason, "Tool execution was blocked")), true)
|
|
end
|
|
end
|
|
|
|
return preparedToolCall(tool, toolCall, validatedArgs)
|
|
catch err
|
|
return immediateOutcome(createErrorToolResult(sprint(showerror, err)), true)
|
|
end
|
|
end
|
|
|
|
# ── per-call execution ──────────────────────────────────────────
|
|
|
|
function executePreparedToolCall(
|
|
prep::preparedToolCall,
|
|
signal::Union{Nothing,AbortSignal},
|
|
emit::AgentEventSink,
|
|
)::executedOutcome
|
|
|
|
updateEvents = Promise[]
|
|
accepting = true
|
|
|
|
try
|
|
result = prep.tool.execute(
|
|
prep.toolCall.id, prep.args, signal,
|
|
partialResult -> begin
|
|
if accepting
|
|
push!(updateEvents,
|
|
emit(ToolExecUpdateEvent(prep.toolCall.id, prep.toolCall.name,
|
|
prep.toolCall.arguments, partialResult)))
|
|
end
|
|
end
|
|
)
|
|
accepting = false
|
|
wait.(updateEvents)
|
|
return executedOutcome(result, false)
|
|
catch err
|
|
accepting = false
|
|
wait.(updateEvents)
|
|
return executedOutcome(createErrorToolResult(sprint(showerror, err)), true)
|
|
end
|
|
end
|
|
|
|
# ── per-call finalization ───────────────────────────────────────
|
|
|
|
function finalizeExecutedToolCall(
|
|
context::AgentContext,
|
|
assistantMsg::AssistantMessage,
|
|
prep::preparedToolCall,
|
|
executed::executedOutcome,
|
|
config::AgentLoopConfig,
|
|
signal::Union{Nothing,AbortSignal},
|
|
)::finalizedOutcome
|
|
|
|
result = executed.result
|
|
isError = executed.isError
|
|
|
|
if config.afterToolCalls !== nothing
|
|
try
|
|
after = config.afterToolCalls(
|
|
AfterCtx(assistantMsg, prep.toolCall, prep.args, result, isError, context), signal
|
|
)
|
|
if after !== nothing
|
|
result = merge(result, Dict(:content=>get(after,:content,result.content),
|
|
:details=>get(after,:details,result.details),
|
|
:usage=>get(after,:usage,result.usage),
|
|
:terminate=>get(after,:terminate,result.terminate)))
|
|
isError = get(after, :is_error, isError)
|
|
end
|
|
catch err
|
|
result = createErrorToolResult(sprint(showerror, err))
|
|
isError = true
|
|
end
|
|
end
|
|
|
|
return finalizedOutcome(prep.toolCall, result, isError)
|
|
end
|
|
|
|
function emitToolExecutionEnd(finalized::finalizedOutcome, emit::AgentEventSink)
|
|
emit(ToolExecEndEvent(finalized.toolCall.id, finalized.toolCall.name,
|
|
finalized.result, finalized.isError))
|
|
end
|
|
|
|
# ── sequential execution ────────────────────────────────────────
|
|
|
|
function executeToolCallsSequential(
|
|
context::AgentContext,
|
|
assistantMsg::AssistantMessage,
|
|
toolCalls::Vector{AgentToolCall},
|
|
config::AgentLoopConfig,
|
|
signal::Union{Nothing,AbortSignal},
|
|
emit::AgentEventSink,
|
|
)::toolCallBatch
|
|
|
|
finalizedCalls = finalizedOutcome[]
|
|
messages = ToolResultMessage[]
|
|
|
|
for tc in toolCalls
|
|
emit(ToolExecStartEvent(tc.id, tc.name, tc.arguments))
|
|
|
|
prep = prepareToolCall(context, assistantMsg, tc, config, signal)
|
|
|
|
if prep isa immediateOutcome
|
|
finalized = finalizedOutcome(tc, prep.result, prep.isError)
|
|
else
|
|
executed = executePreparedToolCall(prep, signal, emit)
|
|
finalized = finalizeExecutedToolCall(context, assistantMsg, prep, executed, config, signal)
|
|
end
|
|
|
|
emitToolExecutionEnd(finalized, emit)
|
|
push!(messages, createToolResultMessage(finalized))
|
|
push!(finalizedCalls, finalized)
|
|
|
|
if signal !== nothing && signal.aborted
|
|
break
|
|
end
|
|
end
|
|
|
|
return toolCallBatch(messages, shouldTerminate(finalizedCalls))
|
|
end
|
|
|
|
# ── parallel execution ──────────────────────────────────────────
|
|
|
|
function executeToolCallsParallel(
|
|
context::AgentContext,
|
|
assistantMsg::AssistantMessage,
|
|
toolCalls::Vector{AgentToolCall},
|
|
config::AgentLoopConfig,
|
|
signal::Union{Nothing,AbortSignal},
|
|
emit::AgentEventSink,
|
|
)::toolCallBatch
|
|
|
|
# Each entry: finalizedOutcome (already done) or Task → finalizedOutcome (pending)
|
|
entries = Union{finalizedOutcome,Task{finalizedOutcome}}[]
|
|
|
|
for tc in toolCalls
|
|
emit(ToolExecStartEvent(tc.id, tc.name, tc.arguments))
|
|
|
|
prep = prepareToolCall(context, assistantMsg, tc, config, signal)
|
|
|
|
if prep isa immediateOutcome
|
|
finalized = finalizedOutcome(tc, prep.result, prep.isError)
|
|
emitToolExecutionEnd(finalized, emit)
|
|
push!(entries, finalized)
|
|
else
|
|
# spawn lazy computation task
|
|
task = Task() do
|
|
executed = executePreparedToolCall(prep, signal, emit)
|
|
finalized = finalizeExecutedToolCall(context, assistantMsg, prep, executed, config, signal)
|
|
emitToolExecutionEnd(finalized, emit)
|
|
return finalized
|
|
end
|
|
schedule(task)
|
|
push!(entries, task)
|
|
end
|
|
|
|
if signal !== nothing && signal.aborted
|
|
break
|
|
end
|
|
end
|
|
|
|
# Wait for all tasks, collect in order
|
|
finalizedCalls = finalizedOutcome[]
|
|
for entry in entries
|
|
outcome = entry isa Task ? fetch(entry) : entry
|
|
push!(finalizedCalls, outcome)
|
|
end
|
|
|
|
messages = ToolResultMessage[]
|
|
for f in finalizedCalls
|
|
push!(messages, createToolResultMessage(f))
|
|
end
|
|
|
|
return toolCallBatch(messages, shouldTerminate(finalizedCalls))
|
|
end
|
|
|
|
# ── dispatcher ──────────────────────────────────────────────────
|
|
|
|
function executeToolCalls(
|
|
context::AgentContext,
|
|
assistantMsg::AssistantMessage,
|
|
toolCalls::Vector{AgentToolCall},
|
|
config::AgentLoopConfig,
|
|
signal::Union{Nothing,AbortSignal},
|
|
emit::AgentEventSink,
|
|
)::toolCallBatch
|
|
|
|
# Check if any tool is marked sequential, or config forces sequential
|
|
hasSequential = any(tc ->
|
|
any(t -> t.name == tc.name && get(t.executionMode, "parallel") == "sequential",
|
|
context.tools), toolCalls)
|
|
|
|
if config.tool_execution == "sequential" || hasSequential
|
|
return executeToolCallsSequential(context, assistantMsg, toolCalls, config, signal, emit)
|
|
else
|
|
return executeToolCallsParallel(context, assistantMsg, toolCalls, config, signal, emit)
|
|
end
|
|
end
|