From 750eff483bd71e4a7db8d3ae9429026648396e00 Mon Sep 17 00:00:00 2001 From: narawat Date: Sun, 9 Aug 2026 22:08:04 +0700 Subject: [PATCH] update --- src/tools/registry.jl | 104 +++++++++++++++++++++++++----------------- 1 file changed, 63 insertions(+), 41 deletions(-) diff --git a/src/tools/registry.jl b/src/tools/registry.jl index f7aa585..4cc0a01 100644 --- a/src/tools/registry.jl +++ b/src/tools/registry.jl @@ -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