update
This commit is contained in:
+63
-41
@@ -9,6 +9,9 @@ using ..type
|
||||
# Global registry — populated at runtime by loadTools() or registerTool()
|
||||
const _registry = Vector{agentTool}()
|
||||
|
||||
# Module references — kept alive to prevent GC of tool code that closures depend on
|
||||
const _tool_modules = Vector{Module}()
|
||||
|
||||
# Auto-register the built-in listTools tool
|
||||
function __init__()
|
||||
registerTool(_listTool())
|
||||
@@ -55,33 +58,27 @@ Scans `dir` for `.jl` files. Each file must define a function named
|
||||
`getTool()::agentTool`. Files are sorted alphabetically so tool
|
||||
registration order is deterministic.
|
||||
|
||||
Each `.jl` file is loaded into its own **submodule** so that all functions
|
||||
defined in the file (`validateRequiredArgs`, `prepareArguments`, `executeTool`,
|
||||
and any helper functions) are namespaced and never collide with other tools.
|
||||
|
||||
# Tool file format
|
||||
Each `.jl` file defines one function `getTool()` that returns an `agentTool`:
|
||||
Each `.jl` file defines one function `getTool()` that returns an `agentTool`.
|
||||
Inside the file you can freely define as many helper functions as you need —
|
||||
they will all be scoped under the tool's submodule.
|
||||
|
||||
```julia
|
||||
# src/tools/getWeather.jl
|
||||
|
||||
# These are namespaced — no collision with getTime.validateRequiredArgs, etc.
|
||||
function validateRequiredArgs(args::Dict{String,Any})::Union{Nothing,String}
|
||||
...
|
||||
end
|
||||
|
||||
function getTool()::agentTool
|
||||
return agentTool(
|
||||
name = "getWeather",
|
||||
label = "Weather Lookup",
|
||||
description = "Fetch current weather and forecast for a given city.",
|
||||
inputSchema = Dict{String,Any}(
|
||||
"type" => "object",
|
||||
"properties" => Dict(
|
||||
"city" => Dict("type" => "string", "description" => "City and country"),
|
||||
"units" => Dict("type" => "string", "enum" => ["celsius", "fahrenheit"], "default" => "celsius")
|
||||
),
|
||||
"required" => ["city"]
|
||||
),
|
||||
execute = (toolCallId, args, signal, onPartialResult) -> begin
|
||||
city = args["city"]
|
||||
return agentToolResult(
|
||||
[textContent("Sunny, 22C in Bangkok")],
|
||||
Dict{Any,Any}(), nothing, false
|
||||
)
|
||||
end,
|
||||
prepareArguments = nothing,
|
||||
parallelToolExecute = false
|
||||
...
|
||||
)
|
||||
end
|
||||
```
|
||||
@@ -106,32 +103,57 @@ function loadTools(dir::String)::Vector{agentTool}
|
||||
|
||||
for filename in jl_files
|
||||
filepath = joinpath(dir, filename)
|
||||
println("[toolRegistry] Loading tool from: $filepath")
|
||||
|
||||
# Include the file in the current module scope so all types resolve
|
||||
# (agentTool, textContent, agentToolResult, etc. are all available)
|
||||
include(filepath)
|
||||
# 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" => ""))
|
||||
|
||||
# Validate that getTool was defined (include() places it in current module scope)
|
||||
if !isdefined(@__MODULE__, :getTool)
|
||||
throw(ArgumentError(
|
||||
"Tool file $(filepath) does not define a `getTool()` function. " *
|
||||
"Each tool file must define: function getTool()::agentTool ... end"
|
||||
))
|
||||
# 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.
|
||||
# Also import Dates, UUIDs, DataStructures, JSON — common dependencies
|
||||
# that tool files use (and that the ..type module transitively uses).
|
||||
file_content = read(filepath, String)
|
||||
module_code = """
|
||||
module $(mod_name)
|
||||
using ..type
|
||||
using Dates, UUIDs, DataStructures, JSON
|
||||
$(file_content)
|
||||
end
|
||||
"""
|
||||
mod = eval(Meta.parse(module_code))
|
||||
|
||||
# Call getTool() — it runs in current scope where types are visible
|
||||
# Use invokelatest to handle world-age semantics after include()
|
||||
tool = invokelatest(getTool)
|
||||
if !(tool isa agentTool)
|
||||
throw(ArgumentError(
|
||||
"getTool() in $(filepath) did not return an agentTool instance, got: $(typeof(tool))"
|
||||
))
|
||||
# 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
|
||||
# Keep module reference alive — closures in the agentTool (execute,
|
||||
# validateRequiredArgs, prepareArguments) may reference module-scoped
|
||||
# functions. Without this, GC could collect the module.
|
||||
push!(_tool_modules, mod)
|
||||
push!(_registry, tool)
|
||||
push!(tools, tool)
|
||||
println("[toolRegistry] 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
|
||||
|
||||
push!(_registry, tool)
|
||||
push!(tools, tool)
|
||||
println("[toolRegistry] Loaded tool: $(tool.name) — $(tool.label)")
|
||||
end
|
||||
|
||||
return tools
|
||||
|
||||
Reference in New Issue
Block a user