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
+13 -2
View File
@@ -1,7 +1,6 @@
module YiemAgent
# export agent
export register_all_tools
""" Order by dependencies of each file. The 1st included file must not depend on any other
files and each file can only depend on the file included before it.
@@ -13,9 +12,21 @@ module YiemAgent
include("utils.jl")
using .utils
include("tools/getWeather.jl")
include("tools/getTime.jl")
include("tools/writeTool.jl")
include("toolRegistry.jl")
using .toolRegistry
function register_all_tools(store::toolRegistry.toolStore)
registerTool(store, getWeatherTool())
registerTool(store, getTimeTool())
registerTool(store, writeToolTool())
registerTool(store, listTool(store))
return store.tools
end
# include("llmfunction.jl")
# using .llmfunction
+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
+4 -96
View File
@@ -1,6 +1,6 @@
module toolRegistry
export toolStore, loadTools, registerTool, getTools, clearTools, listTool
export toolStore, registerTool, getTools, clearTools, listTool
using Dates
using JSON, DataStructures
@@ -45,7 +45,7 @@ end
Return an `agentTool` definition for listing registered tools.
Each call produces a **new** tool object that captures (closes over)
`store`. `loadTools` auto-registers one so the LLM can discover tools
`store`. `register_all_tools` auto-registers one so the LLM can discover tools
at runtime.
# Arguments
@@ -55,7 +55,7 @@ at runtime.
```julia
julia> store = toolStore(name="agent1");
julia> loadTools(store, "src/tools") # auto-registers listTools
julia> register_all_tools(store) # auto-registers listTools
[toolRegistry:agent1] Loaded tool: getWeather (Weather Lookup)
[toolRegistry:agent1] Registered tool: listTools
@@ -97,99 +97,7 @@ function listTool(store::toolStore)::agentTool
)
end
"""
Load `.jl` tool files from `dir` into `store`, then auto-register
`listTool` so the LLM can discover available tools at runtime.
Each `.jl` file must define `function getTool()::agentTool ... end`.
Files are sorted alphabetically for deterministic registration order.
Each file is loaded into its own Julia submodule to avoid name collisions.
# Arguments
- `store`: Tool store to populate
- `dir`: Directory containing `.jl` tool files
# Returns
- The same `store.tools` dict (modified in place)
# Errors
- Throws `ArgumentError` if `dir` does not exist or a file lacks `getTool()`
# Example
```julia
julia> store = toolStore(name="agent1");
julia> loadTools(store, "src/tools")
[toolRegistry:agent1] Loaded tool: getWeather (Weather Lookup)
[toolRegistry:agent1] Loaded tool: getTime (Time Lookup)
[toolRegistry:agent1] Registered tool: listTools
OrderedDict{String, agentTool} with 3 entries:
"getWeather" => agentTool(...)
"getTime" => agentTool(...)
"listTools" => agentTool(...)
```
"""
function loadTools(store::toolStore, dir::String)::OrderedDict{String, agentTool}
if !isdir(dir)
throw(ArgumentError("Tool directory does not exist: $dir"))
end
jl_files = filter(f -> endswith(f, ".jl") && !occursin(r"(?i)registry", f), readdir(dir))
sort!(jl_files)
for filename in jl_files
filepath = joinpath(dir, filename)
# Derive a unique module name from the filename only (not full path).
# e.g. "getWeather.jl" -> "_tool_getWeather"
mod_name = Symbol("_tool_", replace(rstrip(filename, '.'), ".jl" => ""))
# Build the complete module as a string and eval the parsed code.
# Julia does not allow `module ... end` inside eval(quote ...),
# and constructing the module AST by hand is fragile.
# Instead, we generate the full module source as a string,
# parse it, and eval the resulting expression.
# Each tool file declares its own dependencies via `using` statements
# at the top of the file — the registry only injects `using ..type`
# to make core types (agentTool, textContent, etc.) available.
file_content = read(filepath, String)
module_code = """
module $(mod_name)
using ..type
$(file_content)
end
"""
mod = eval(Meta.parse(module_code))
# Call getTool() via Core.eval in the submodule's scope.
# This evaluates getTool() entirely within the new module's world,
# completely avoiding world-age issues — no invokelatest needed.
# Note: all uses of `tool` must be inside the `try` block because
# Julia 1.12's SSA form doesn't track `tool` as definitely assigned
# after a `try-catch` where it's only assigned inside `try`.
try
tool = Core.eval(mod, :(getTool()))
if !(tool isa agentTool)
throw(ArgumentError(
"getTool() in $(filepath) did not return an agentTool instance, got: $(typeof(tool))"
))
end
store.tools[tool.name] = tool
println("[$(store.name)] Loaded tool: $(tool.name) ($(tool.label))")
catch e
if e isa UndefVarError || occursin("getTool", sprint(showerror, e))
throw(ArgumentError(
"Tool file $(filepath) does not define a `getTool()` function in module $(mod_name). " *
"Each tool file must define: function getTool()::agentTool ... end"
))
end
rethrow(e)
end
end
registerTool(store, listTool(store))
return store.tools
end
# Note: register_all_tools is defined in YiemAgent.jl where tool functions are in scope
"""
registerTool(store::toolStore, tool::agentTool) -> OrderedDict{String, agentTool}
+7 -6
View File
@@ -1,3 +1,4 @@
using .type
using Dates
"""
@@ -15,7 +16,7 @@ Demonstrates custom validation beyond simple required-field checking:
- `nothing` if validation passes
- `String` error message if validation fails
"""
function validateRequiredArgs(args::Dict{String,Any})::Union{Nothing,String}
function getTimeValidateRequiredArgs(args::Dict{String,Any})::Union{Nothing,String}
tz = get(args, "timezone", nothing)
city = get(args, "city", "")
@@ -43,8 +44,8 @@ Execute the getTime tool.
Returns mock time data for the given timezone or city.
"""
function executeTool(toolCallId::String, args::Dict{String,Any}, signal::Union{Nothing,abortSignal},
onPartialResult::Function)::agentToolResult
function getTimeExecute(toolCallId::String, args::Dict{String,Any}, signal::Union{Nothing,abortSignal},
onPartialResult)
tz = get(args, "timezone", nothing)
city = get(args, "city", "")
if tz !== nothing
@@ -61,7 +62,7 @@ end
"""
Define and return the getTime agentTool.
"""
function getTool()::agentTool
function getTimeTool()::agentTool
return agentTool(
name = "getTime",
label = "Time Lookup",
@@ -74,9 +75,9 @@ function getTool()::agentTool
),
"required" => []
),
execute = executeTool,
execute = getTimeExecute,
prepareArguments = nothing,
validateRequiredArgs = validateRequiredArgs,
validateRequiredArgs = getTimeValidateRequiredArgs,
parallelToolExecute = false
)
end
+21 -12
View File
@@ -1,24 +1,33 @@
using msghandler
using .type
"""
Execute the getWeather tool.
Returns mock weather data for the given city and temperature units.
"""
function executeTool(toolCallId::String, args::Dict{String,Any}, signal::Union{Nothing,abortSignal},
onPartialResult::Function)::agentToolResult
city = get(args, "city", "")
units = get(args, "units", "celsius")
temp = units == "fahrenheit" ? "72" : "22"
unit_symbol = units == "celsius" ? "°C" : "°F"
return agentToolResult(
[textContent("Weather in $(city): Sunny, $(temp)$(unit_symbol)")],
Dict{Any,Any}(), nothing, false
)
function getWeatherExecute(toolCallId::String, args::Dict{String,Any}, signal::Union{Nothing,abortSignal},
agentEventSink)
agentEventSink("Getting weather...")
city = get(args, "city", "")
units = get(args, "units", "celsius")
temp = units == "fahrenheit" ? "72" : "22"
unit_symbol = units == "celsius" ? "°C" : "°F"
return agentToolResult(
[textContent("Weather in $(city): Sunny, $(temp)$(unit_symbol)")],
Dict{Any,Any}(),
nothing,
false
)
end
"""
Define and return the getWeather agentTool.
"""
function getTool()::agentTool
function getWeatherTool()::agentTool
return agentTool(
name = "getWeather",
label = "Weather Lookup",
@@ -31,7 +40,7 @@ function getTool()::agentTool
),
"required" => ["city"]
),
execute = executeTool,
execute = getWeatherExecute,
prepareArguments = nothing,
validateRequiredArgs = nothing,
parallelToolExecute = false
+12 -9
View File
@@ -1,3 +1,4 @@
using .type
using JSON
"""
@@ -7,15 +8,17 @@ The agent can use this tool when it encounters a task that no existing tool
can handle. Provide the tool's name, label, description, inputSchema, and
execute logic as Julia code. The tool is written to `src/tools/<name>.jl`.
After calling this tool, restart the agent so `loadTools(agent._tool_store, "src/tools")` picks
up the new file. The new tool is immediately available.
After calling this tool, add the new file to `YiemAgent.jl` with an `include()`
statement (after `include("toolRegistry.jl")`), then restart the agent.
The new tool must be registered in `register_all_tools()` in `toolRegistry.jl`.
# Example
1. Agent calls writeTool with a spec for a "searchWine" tool
2. writeTool generates src/tools/searchWine.jl
3. Restart agent — loadTools() picks up the new file
4. Agent calls searchWine with args
3. Developer adds `include("tools/searchWine.jl")` to YiemAgent.jl
4. Developer adds `registerTool(store, searchWineTool())` to register_all_tools()
5. Restart agent — new tool is available
# How It Works
@@ -24,7 +27,7 @@ tool logic as `executeCode`, and writeTool wraps it in Julia boilerplate:
- Converts `inputSchema` Dict into Julia `Dict{String,Any}(...)` string
- Indents `executeCode` with 4 spaces
- Wraps it inside `function executeTool(...)::agentToolResult ... end`
- Appends `getTool()` returning an `agentTool` struct
- Appends `writeToolTool()` returning an `agentTool` struct
- Writes the combined string to `src/tools/<name>.jl`
# Important Notes
@@ -105,7 +108,7 @@ end
"""
Define and return the writeTool agentTool.
"""
function getTool()::agentTool
function writeToolTool()::agentTool
return agentTool(
name = "writeTool",
label = "Create Tool",
@@ -127,7 +130,7 @@ function getTool()::agentTool
),
"required" => ["name", "label", "description", "inputSchema", "executeCode"]
),
execute = (toolCallId::String, args::Dict{String,Any}, signal::Union{Nothing,abortSignal}, onPartialResult::Function) -> begin
execute = (toolCallId::String, args::Dict{String,Any}, signal::Union{Nothing,abortSignal}, onPartialResult) -> begin
tool_name = get(args, "name", "")::String
tool_label = get(args, "label", tool_name)::String
tool_description = get(args, "description", "")::String
@@ -250,13 +253,13 @@ function getTool()::agentTool
tool_code = join(parts)
# Write the file — tool is loaded on next agent restart via loadTools(store, "src/tools")
# Write the file — tool must be included in YiemAgent.jl and registered in register_all_tools()
write(filepath, tool_code)
onPartialResult(Dict("status" => "Done"))
return agentToolResult(
[textContent("Tool '$(tool_name)' written to $filepath. Restart the agent so loadTools(agent._tool_store, \"src/tools\") picks it up, then call listTools to verify.")],
[textContent("Tool '$(tool_name)' written to $filepath. Add include(\"tools/$(tool_name).jl\") to YiemAgent.jl and registerTool(store, $(tool_name)Tool()) to register_all_tools(), then restart the agent.")],
Dict{Any,Any}(
"file" => filepath,
"name" => tool_name,
+2 -2
View File
@@ -264,7 +264,7 @@ struct agentTool # A tool available to the agent
label::String # Human-readable tool name
description::String # What the tool does
inputSchema::Any # Tool parameters schema (JSON schema, MCP inputSchema format)
execute::Function # Tool execution function
execute # Tool execution function
prepareArguments::Union{Function, Nothing} # Optional argument preparation callback
validateRequiredArgs::Union{Function, Nothing} # Optional validation hook for required args
parallelToolExecute::Bool # Override: run tool calls sequentially or in parallel
@@ -274,7 +274,7 @@ end
Keyword constructor for agentTool — allows `agentTool(name=..., label=..., ...)`.
"""
function agentTool(; name::String, label::String, description::String, inputSchema::Any,
execute::Function, prepareArguments::Union{Function, Nothing}=nothing,
execute, prepareArguments::Union{Function, Nothing}=nothing,
validateRequiredArgs::Union{Function, Nothing}=nothing,
parallelToolExecute::Bool=false)
return agentTool(name, label, description, inputSchema, execute,
+57 -3
View File
@@ -241,7 +241,38 @@ function formatMsgForLLM(ctx::agentContext, agentEventSink)::Dict{String, Any}
return openaiReadyMsg
end
#TODO
"""
beforeToolCall(context::beforeToolCallContext, signal::abortSignal) -> beforeToolCallResult
Callback invoked before executing a tool call. Use this hook to inspect
the tool call and decide whether to allow, block, or modify it.
Common use cases:
- Request user approval via UI before running destructive tools.
- Validate business rules that cannot be expressed in the JSON schema.
- Check final context (e.g. session state, rate limits, permissions).
# Arguments
- `context::beforeToolCallContext`: Contains the assistant message, tool call,
validated arguments, and current conversation context.
- `signal::abortSignal`: Signal that may be set to abort the operation.
# Returns
- `beforeToolCallResult(false, "N/A")` to allow the call to proceed.
- `beforeToolCallResult(true, "Reason")` to block the call with a reason.
- `nothing` is treated as allow (equivalent to `beforeToolCallResult(false, "N/A")`).
# Example
```julia
function beforeToolCall(context::beforeToolCallContext, signal::abortSignal)
if context.toolCall.name == "deleteFile"
# Block file deletion unless explicitly approved
return beforeToolCallResult(true, "User must approve file deletion")
end
return beforeToolCallResult(false, "N/A")
end
```
"""
function beforeToolCall(context::beforeToolCallContext, signal::abortSignal
)::beforeToolCallResult
@@ -254,8 +285,31 @@ function beforeToolCall(context::beforeToolCallContext, signal::abortSignal
return beforeToolCallResult(false, "N/A")
end
#TODO
function afterToolCall(context::beforeToolCallContext, signal::abortSignal
"""
afterToolCall(context::afterToolCallContext, signal::abortSignal) -> Union{agentToolResult, Nothing}
Callback invoked after a tool call finishes executing (before and after errors).
Use this hook to post-process the tool result before it is fed back to the LLM.
Common use cases:
- Mask sensitive data (API keys, tokens) from result content.
- Normalize usage tracking data into a consistent format.
- Inspect the result and set `terminate: true` based on business logic
(e.g. "if deployment failed, stop the agent rather than retrying").
- Wrap error results in friendlier messages for the LLM to understand.
# Arguments
- `context::afterToolCallContext`: Contains the assistant message, tool call,
arguments, raw result, error status, and current conversation context.
- `signal::abortSignal`: Signal that may be set to abort the operation.
# Returns
- `nothing` to pass the result through unchanged.
- `agentToolResult(...)` to return a modified result (content, details, usage,
terminate flag can all be overridden).
"""
function afterToolCall(context::afterToolCallContext, signal::abortSignal
)::Union{agentToolResult, Nothing}
# modify context.result if needed and return agentToolResult