This commit is contained in:
2026-08-09 22:08:04 +07:00
parent 5cc35c78f4
commit 750eff483b
+63 -41
View File
@@ -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