module toolRegistry export loadTools, registerTool, getTools, clearTools using Dates using JSON, DataStructures 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()) end """ List tool definition — lets the agent query available tools for collision detection when creating new tools via writeTool. """ function _listTool()::agentTool return agentTool( name = "listTools", label = "List Tools", description = "List all available tools with their names, labels, and descriptions. Use this before creating a new tool to check for name collisions.", inputSchema = Dict{String,Any}( "type" => "object", "properties" => Dict{String,Any}(), "required" => Any[] ), execute = (toolCallId, args, signal, onPartialResult) -> begin tools = getTools() if isempty(tools) result_text = "No tools registered." else lines = String["- $(t.name): $(t.label) — $(t.description)" for t in tools] result_text = "Available tools:\n" * join(lines, "\n") end return agentToolResult( [textContent(result_text)], Dict{Any,Any}("count" => length(tools)), nothing, false ) end, prepareArguments = nothing, validateRequiredArgs = nothing, parallelToolExecute = false ) end """ Load all tool modules from a directory. 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`. 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", ... ) end ``` # Arguments - `dir::String`: Directory path to scan for `.jl` tool files # Returns - `Vector{agentTool}`: All loaded tools # Errors - Throws `ArgumentError` if a tool file does not define a `getTool` function """ function loadTools(dir::String)::OrderedDict{String, agentTool} if !isdir(dir) throw(ArgumentError("Tool directory does not exist: $dir")) end tools = OrderedDict{String, agentTool}() 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. # 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() 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) tools[tool.name] = 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 return tools end """ Register a single agentTool into the global registry. # Arguments - `tool::agentTool`: The tool to register # Returns - `Vector{agentTool}`: Updated registry """ function registerTool(tool::agentTool)::Vector{agentTool} push!(_registry, tool) println("[toolRegistry] Registered tool: $(tool.name)") return _registry end """ Get all registered tools. # Returns - `Vector{agentTool}`: Copy of the registry """ function getTools()::Vector{agentTool} return deepcopy(_registry) end """ Clear all registered tools from the global registry. """ function clearTools()::Nothing empty!(_registry) println("[toolRegistry] Registry cleared") return nothing end end # module