update
This commit is contained in:
+63
-41
@@ -9,6 +9,9 @@ using ..type
|
|||||||
# Global registry — populated at runtime by loadTools() or registerTool()
|
# Global registry — populated at runtime by loadTools() or registerTool()
|
||||||
const _registry = Vector{agentTool}()
|
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
|
# Auto-register the built-in listTools tool
|
||||||
function __init__()
|
function __init__()
|
||||||
registerTool(_listTool())
|
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
|
`getTool()::agentTool`. Files are sorted alphabetically so tool
|
||||||
registration order is deterministic.
|
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
|
# 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
|
```julia
|
||||||
# src/tools/getWeather.jl
|
# 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
|
function getTool()::agentTool
|
||||||
return agentTool(
|
return agentTool(
|
||||||
name = "getWeather",
|
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
|
end
|
||||||
```
|
```
|
||||||
@@ -106,32 +103,57 @@ function loadTools(dir::String)::Vector{agentTool}
|
|||||||
|
|
||||||
for filename in jl_files
|
for filename in jl_files
|
||||||
filepath = joinpath(dir, filename)
|
filepath = joinpath(dir, filename)
|
||||||
println("[toolRegistry] Loading tool from: $filepath")
|
|
||||||
|
|
||||||
# Include the file in the current module scope so all types resolve
|
# Derive a unique module name from the filename only (not full path).
|
||||||
# (agentTool, textContent, agentToolResult, etc. are all available)
|
# e.g. "getWeather.jl" -> "_tool_getWeather"
|
||||||
include(filepath)
|
mod_name = Symbol("_tool_", replace(rstrip(filename, '.'), ".jl" => ""))
|
||||||
|
|
||||||
# Validate that getTool was defined (include() places it in current module scope)
|
# Build the complete module as a string and eval the parsed code.
|
||||||
if !isdefined(@__MODULE__, :getTool)
|
# Julia does not allow `module ... end` inside eval(quote ...),
|
||||||
throw(ArgumentError(
|
# and constructing the module AST by hand is fragile.
|
||||||
"Tool file $(filepath) does not define a `getTool()` function. " *
|
# Instead, we generate the full module source as a string,
|
||||||
"Each tool file must define: function getTool()::agentTool ... end"
|
# 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
|
end
|
||||||
|
"""
|
||||||
|
mod = eval(Meta.parse(module_code))
|
||||||
|
|
||||||
# Call getTool() — it runs in current scope where types are visible
|
# Call getTool() via Core.eval in the submodule's scope.
|
||||||
# Use invokelatest to handle world-age semantics after include()
|
# This evaluates getTool() entirely within the new module's world,
|
||||||
tool = invokelatest(getTool)
|
# completely avoiding world-age issues — no invokelatest needed.
|
||||||
if !(tool isa agentTool)
|
# Note: all uses of `tool` must be inside the `try` block because
|
||||||
throw(ArgumentError(
|
# Julia 1.12's SSA form doesn't track `tool` as definitely assigned
|
||||||
"getTool() in $(filepath) did not return an agentTool instance, got: $(typeof(tool))"
|
# 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
|
end
|
||||||
|
|
||||||
push!(_registry, tool)
|
|
||||||
push!(tools, tool)
|
|
||||||
println("[toolRegistry] Loaded tool: $(tool.name) — $(tool.label)")
|
|
||||||
end
|
end
|
||||||
|
|
||||||
return tools
|
return tools
|
||||||
|
|||||||
Reference in New Issue
Block a user