Files
YiemAgent/etc.jl
T
2026-08-05 04:21:44 +07:00

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