static tool loading

This commit is contained in:
2026-08-15 16:50:28 +07:00
parent b8067c2d33
commit 2543e6cbf1
13 changed files with 864 additions and 542 deletions
+132 -89
View File
@@ -1,16 +1,23 @@
module agentCore
export yiemAgent, _agentLoop, OpenAiToUserMessage, _extractToolCalls
export yiemAgent, _agentLoop, OpenAiToUserMessage, _extractToolCalls,
executePreparedToolCall, prepareToolCall, executeToolCallsSequential,
executeToolCallsParallel, executeToolCalls
using JSON, DataStructures, Dates, UUIDs, HTTP, Random, PrettyPrinting, Serialization,
DataFrames, Base.Threads, NATS
using GeneralUtils
using ..type, ..utils, ..toolRegistry
function register_all_tools(store::toolRegistry.toolStore)
# Call parent module's version which has access to tool functions
parentmodule(@__MODULE__).register_all_tools(store)
end
# ---------------------------------------------- 100 --------------------------------------------- #
"""
docstring
docstring
"""
mutable struct yiemAgent <: agent # High-level agent wrapper
_state::agentState # Current state (prompt, model, messages, tools, etc.)
@@ -84,7 +91,6 @@ on `inputChannel` and `followUpChannel` channels concurrently.
- A new `yiemAgent` instance with an active background task
"""
function yiemAgent(
toolsFolderPath::String,
llmCall,
;
systemPrompt::String="You are helpful assistant.",
@@ -106,9 +112,9 @@ function yiemAgent(
followUp = Channel(32)
outputChannel = Channel(16)
# load tools from toolsFolderPath
# load tools (statically registered at module init)
toolStore1 = toolStore(name="myagent")
loadTools(toolStore1, toolsFolderPath)
register_all_tools(toolStore1)
# Create struct with a placeholder task, then spawn and replace it
agent = yiemAgent(
@@ -291,6 +297,8 @@ function _agentLoop(agent::yiemAgent)
processingTask = nothing # reset
end
agent.agentEventSink("_agentLoop 6")
agent.agentEventSink(string(typeof(processingTask)))
agent.agentEventSink("_agentLoop 7")
end
catch e
# On any error, send error response and exit the loop
@@ -329,7 +337,7 @@ julia> # Currently returns a placeholder echo response
function _processMessage(
inputChannel::Channel,
agentEventSink,
messages::Vector{agentMessage},
agentMsgHistory::Vector{agentMessage},
systemPrompt::String,
tools::OrderedDict{String, agentTool},
prepareContext::Function,
@@ -370,12 +378,12 @@ function _processMessage(
end
agentEventSink("_processMessage 5")
user_msg = OpenAiToUserMessage(raw_msg)
push!(messages, user_msg)
push!(agentMsgHistory, user_msg)
agentEventSink("_processMessage 6")
end
agentEventSink("_processMessage 7")
# call prepareContext()
state = agentState(systemPrompt, nothing, tools, messages)
state = agentState(systemPrompt, nothing, tools, agentMsgHistory)
agentEventSink("_processMessage 8")
preparedContext = prepareContext(state, agentEventSink)
agentEventSink("_processMessage 8")
@@ -396,7 +404,7 @@ function _processMessage(
agentEventSink("hasToolCalls: $hasToolCalls\ntoolCallList: $toolCallList")
if hasToolCalls && length(toolCallList) > 0
#WORKING Build context and config for executeToolCalls
# Build context and config for executeToolCalls
config = agentLoopConfig(
beforeToolCall,
@@ -408,23 +416,21 @@ function _processMessage(
agentEventSink("_processMessage 12")
# call executeToolCalls()
batch = executeToolCalls(preparedContext, assistant_msg, toolCallList, config, signal,
agentEventSink)
toolResultBatch = executeToolCalls(preparedContext, assistant_msg, toolCallList, config,
signal, agentEventSink)
agentEventSink("_processMessage 13")
error("debug marker")
# save toolResults to messages
for tool_result in batch.messages
push!(messages, tool_result)
end
if batch.terminate
# If batch requested termination, build a final response
# save toolResults to messages
for toolResult in toolResultBatch.messages
push!(agentMsgHistory, toolResult)
end
agentEventSink("_processMessage 14")
if toolResultBatch.terminate
agentEventSink("_processMessage 15")
# If toolResultBatch requested termination, build a final response
final_content = [textContent("Tool execution completed.")]
for tool_result in batch.messages
for content_block in tool_result.content
for toolResult in toolResultBatch.messages
for content_block in toolResult.content
if content_block isa textContent
append!(final_content, [content_block])
elseif content_block isa Dict
@@ -434,6 +440,7 @@ function _processMessage(
end
end
end
agentEventSink("_processMessage 16")
final_response = assistantMessage(
role="assistant",
content=final_content,
@@ -441,7 +448,7 @@ function _processMessage(
model=assistant_msg.model,
usage=assistant_msg.usage,
stopReason="tool_use_terminated",
errorMessage=if any(x -> x.isError, batch.messages)
errorMessage=if any(x -> x.isError, toolResultBatch.messages)
"One or more tool calls failed"
else
nothing
@@ -451,12 +458,13 @@ function _processMessage(
break
end
else
agentEventSink("_processMessage 17")
# LLM did not use tool calls — this is the final response
final_response = assistant_msg
break
end
end
agentEventSink("_processMessage 18")
return final_response
end
@@ -524,7 +532,7 @@ 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, nowMillis()
nothing, f.isError, now()
)
end
@@ -563,8 +571,10 @@ function _extractToolCalls(response)
toolCallList = agentToolCall[]
# Helper: parse args (JSON string -> Dict, or pass through)
parse_args(raw) = raw isa AbstractDict ? Dict{String,Any}(raw) :
raw isa String ? JSON.parse(raw) : Dict{String,Any}()
parse_args(raw) = raw isa AbstractDict && !(raw isa Dict{String,Any}) ?
Dict{String,Any}(raw) :
raw isa String ? JSON.parse(raw) :
raw isa Dict{String,Any} ? raw : Dict{String,Any}()
# Helper: build agentToolCall (positional)
make_tc(tc_data, default_id=string(uuid4())) = begin
@@ -713,12 +723,27 @@ end
"""
shouldTerminate(finalizedCalls::Vector{finalizedOutcome}) -> Bool
The `terminate` flag is set by tool implementations, not by the agent
or the LLM. It signals that the tool itself has completed the user's
request or encountered a fatal condition, so the agent should stop
processing further turns without calling the LLM again.
Common scenarios where a tool sets `terminate: true`:
- **Task completion**: one-shot tools like `deploy`, `submit`, or
`send_payment` finish their work and report directly to the user
instead of asking the LLM "what next?"
- **Unrecoverable error**: a tool hits a fatal condition (database
connection lost, auth token expired) and stops the agent from
retrying endlessly.
- **Async handoff**: a tool triggers a long-running external operation
and wants the agent to stop now; the external system will resume
the agent later via `continue()`.
Returns `true` only when every finalized call in the batch has
`result.terminate == true`. All tools must agree — if any tool
did not request termination, the agent continues. This prevents
a single tool that happens to set `terminate: true` (e.g. for
metadata purposes) from accidentally stopping the agent when
other tools in the batch did not intend to terminate.
a single tool that happens to set `terminate: true` from accidentally
stopping the agent when other tools in the batch did not intend to terminate.
# Arguments
- `finalizedCalls`: Vector of finalized tool call outcomes
@@ -739,7 +764,7 @@ true
```
"""
function shouldTerminate(batches::Vector{finalizedOutcome})::Bool
return !isempty(batches) && all(b -> b.result.terminate, batches)
return !isempty(batches) && all(b -> b.result.terminate, batches)
end
"""
@@ -849,7 +874,7 @@ function prepareToolCall(
agentEventSink
)::Union{preparedToolCall,immediateOutcome}
agentEventSink("prepareToolCall 1")
tool = get(context.tools, toolCall.name, nothing)
tool = get(context.tools, toolCall.name, nothing) # pick a called tool from tool store
if tool === nothing
agentEventSink("prepareToolCall 2")
return immediateOutcome(createErrorToolResult("Tool $toolCall.name not found"), true)
@@ -859,8 +884,10 @@ function prepareToolCall(
agentEventSink("prepareToolCall 3")
# 1. prepare arguments (tool-specific transform)
prepared = prepareToolCallArguments(tool, toolCall)
agentEventSink(string(prepared.arguments))
agentEventSink("prepareToolCall 4")
validatedArgs = validateToolArguments(tool, prepared)
agentEventSink(string(validatedArgs))
agentEventSink("prepareToolCall 5")
# 2. beforeToolCall hook — can block
if config.beforeToolCall !== nothing
@@ -886,12 +913,12 @@ function prepareToolCall(
return preparedToolCall(tool, toolCall, validatedArgs)
catch e
bt = catch_backtrace()
err_msg = sprint() do io
errMsg = sprint() do io
showerror(io, e, bt)
println(io)
end
agentEventSink(err_msg)
agentEventSink(errMsg)
return immediateOutcome(createErrorToolResult(sprint(showerror, e)), true)
end
@@ -937,36 +964,40 @@ executePreparedToolCall(prep, nothing, emit)
# => executedOutcome(createErrorToolResult("Connection timeout"), true)
```
"""
function executePreparedToolCall(
prep::preparedToolCall,
signal::Union{Nothing,abortSignal},
agentEventSink,
)::executedOutcome
agentEventSink("executePreparedToolCall 1")
updateEvents = promise[]
accepting = true
agentEventSink(prep.toolCall.id)
agentEventSink(prep.toolCall.name)
agentEventSink("executePreparedToolCall 2")
s = string(prep.args)
agentEventSink(s)
agentEventSink("executePreparedToolCall 3")
t = string(fieldnames(typeof(prep.tool)))
agentEventSink("executePreparedToolCall 3-1 " * t)
try
result = prep.tool.execute(
prep.toolCall.id, prep.args, signal,
partialResult -> begin
if accepting
push!(updateEvents,
agentEventSink(toolExecUpdateEvent(prep.toolCall.id, prep.toolCall.name,
prep.toolCall.arguments, partialResult)))
end
end
)
accepting = false
wait.(updateEvents)
result = prep.tool.execute(prep.toolCall.id, prep.args, signal, agentEventSink)
agentEventSink(result.content[1].text)
agentEventSink("executePreparedToolCall 4")
return executedOutcome(result, false)
catch err
accepting = false
wait.(updateEvents)
return executedOutcome(createErrorToolResult(sprint(showerror, err)), true)
catch e
bt = catch_backtrace()
errMsg = sprint() do io
showerror(io, e, bt)
println(io)
end
agentEventSink(errMsg)
return executedOutcome(createErrorToolResult(sprint(showerror, e)), true)
end
end
# ── per-call finalization ───────────────────────────────────────
"""
@@ -1030,16 +1061,19 @@ function finalizeExecutedToolCall(
executed::executedOutcome,
config::agentLoopConfig,
signal::Union{Nothing,abortSignal},
agentEventSink
)::finalizedOutcome
agentEventSink("finalizeExecutedToolCall 1")
result = executed.result
isError = executed.isError
agentEventSink("finalizeExecutedToolCall 2")
if config.afterToolCall !== nothing
try
after = config.afterToolCall(
afterToolCallContext(assistantMsg, prep.toolCall, prep.args, result, isError, context), signal
afterToolCallContext(assistantMsg, prep.toolCall, prep.args, result, isError, context),
signal
)
agentEventSink("finalizeExecutedToolCall 3")
if after !== nothing
result = merge(result, dict(:content=>get(after,:content,result.content),
:details=>get(after,:details,result.details),
@@ -1047,12 +1081,19 @@ function finalizeExecutedToolCall(
:terminate=>get(after,:terminate,result.terminate)))
isError = get(after, :isError, isError)
end
catch err
result = createErrorToolResult(sprint(showerror, err))
catch e
bt = catch_backtrace()
errMsg = sprint() do io
showerror(io, e, bt)
println(io)
end
agentEventSink(errMsg)
result = createErrorToolResult(sprint(showerror, e))
isError = true
end
end
agentEventSink("finalizeExecutedToolCall 4")
return finalizedOutcome(prep.toolCall, result, isError)
end
@@ -1123,30 +1164,32 @@ function executeToolCallsSequential(
messages = toolResultMessage[]
for tc in toolCalls
agentEventSink("start tool execute $(tc.id), $(tc.name), $(tc.arguments)")
prep = prepareToolCall(context, assistantMsg, tc, config, signal, agentEventSink)
agentEventSink("executeToolCallsSequential 2")
if prep isa immediateOutcome
agentEventSink("executeToolCallsSequential 2-1")
finalized = finalizedOutcome(tc, prep.result, prep.isError)
agentEventSink("executeToolCallsSequential 2-2")
else
agentEventSink("executeToolCallsSequential 3")
executed = executePreparedToolCall(prep, signal, agentEventSink)
agentEventSink("executeToolCallsSequential 3-1")
finalized = finalizeExecutedToolCall(context, assistantMsg, prep, executed, config,
signal)
agentEventSink("executeToolCallsSequential 3-2")
end
agentEventSink("executeToolCallsSequential 4")
agentEventSink("$(finalized.toolCall.id), $(finalized.toolCall.name),
$(finalized.result), $(finalized.isError)")
push!(messages, createToolResultMessage(finalized))
push!(finalizedCalls, finalized)
agentEventSink("executeToolCallsSequential 5")
if signal !== nothing && signal.aborted
break
end
agentEventSink("start tool execute $(tc.id), $(tc.name), $(tc.arguments)")
prep = prepareToolCall(context, assistantMsg, tc, config, signal, agentEventSink)
agentEventSink("executeToolCallsSequential " * string(prep.args))
if prep isa immediateOutcome
agentEventSink("executeToolCallsSequential 2-1")
finalized = finalizedOutcome(tc, prep.result, prep.isError)
agentEventSink("executeToolCallsSequential 2-2")
else
agentEventSink("executeToolCallsSequential 3")
#XXX
executed = executePreparedToolCall(prep, signal, agentEventSink)
agentEventSink("executeToolCallsSequential 3-1")
finalized = finalizeExecutedToolCall(context, assistantMsg, prep, executed, config,
signal, agentEventSink)
agentEventSink("executeToolCallsSequential 3-2")
end
agentEventSink("executeToolCallsSequential 4")
agentEventSink("$(finalized.toolCall.id), $(finalized.toolCall.name),
$(finalized.result), $(finalized.isError)")
push!(messages, createToolResultMessage(finalized))
push!(finalizedCalls, finalized)
agentEventSink("executeToolCallsSequential 5")
if signal !== nothing && signal.aborted
break
end
end
agentEventSink("executeToolCallsSequential 6")
return agentToolCallBatch(messages, shouldTerminate(finalizedCalls))
@@ -1218,12 +1261,12 @@ function executeToolCallsParallel(
agentEventSink,
)::agentToolCallBatch
entries = union{finalizedOutcome,task{finalizedOutcome}}[]
entries = Union{finalizedOutcome,Task}[]
for tc in toolCalls
agentEventSink(toolExecStartEvent(tc.id, tc.name, tc.arguments))
prep = prepareToolCall(context, assistantMsg, tc, config, signal)
prep = prepareToolCall(context, assistantMsg, tc, config, signal, agentEventSink)
if prep isa immediateOutcome
finalized = finalizedOutcome(tc, prep.result, prep.isError)
@@ -1231,15 +1274,15 @@ function executeToolCallsParallel(
finalized.result, finalized.isError))
push!(entries, finalized)
else
task = task() do
t = Task() do
executed = executePreparedToolCall(prep, signal, agentEventSink)
finalized = finalizeExecutedToolCall(context, assistantMsg, prep, executed, config, signal)
agentEventSink(toolExecEndEvent(finalized.toolCall.id, finalized.toolCall.name,
finalized.result, finalized.isError))
return finalized
end
schedule(task)
push!(entries, task)
schedule(t)
push!(entries, t)
end
if signal !== nothing && signal.aborted
@@ -1249,7 +1292,7 @@ function executeToolCallsParallel(
finalizedCalls = finalizedOutcome[]
for entry in entries
outcome = entry isa task ? fetch(entry) : entry
outcome = entry isa Task ? fetch(entry) : entry
push!(finalizedCalls, outcome)
end