diff --git a/src/toolRegistry.jl b/src/toolRegistry.jl index 3d786b3..ef33baa 100644 --- a/src/toolRegistry.jl +++ b/src/toolRegistry.jl @@ -22,17 +22,17 @@ struct toolStore end """ -Create a new isolated tool store. + toolStore(; name="default") -> toolStore + +Create a new empty tool store. # Keyword Arguments -- `name::String`: Identifier for this store (default: "default") +- `name::String`: Display name for logging (default: `"default"`) -# Examples +# Example ```julia -store = toolStore(name="agent1") -tools = loadTools(store, "src/tools") -registerTool(store, my_tool) -agent = yiemAgent(tools=getTools(store), llmCall=..., _tool_store=store) +julia> store = toolStore(name="agent1") +toolStore(OrderedDict{String, agentTool}(), "agent1") ``` """ function toolStore(; name::String="default")::toolStore @@ -40,14 +40,32 @@ function toolStore(; name::String="default")::toolStore end """ -List tool definition — lets the agent query available tools for collision detection -when creating new tools via writeTool. + listTool(store::toolStore) -> agentTool + +Return an `agentTool` definition for listing registered tools. + +Each call produces a **new** tool object that captures (closes over) +`store`. `loadTools` auto-registers one so the LLM can discover tools +at runtime. # Arguments -- `store::toolStore`: The tool store to list from +- `store`: The tool store whose tools will be listed when the tool runs -Each `toolStore` gets its own `listTool` instance bound to that store, -so each agent sees only its own tools. +# Example +```julia +julia> store = toolStore(name="agent1"); + +julia> loadTools(store, "src/tools") # auto-registers listTools +[toolRegistry:agent1] Loaded tool: getWeather (Weather Lookup) +[toolRegistry:agent1] Registered tool: listTools + +julia> tools = getTools(store) +OrderedDict{String, agentTool} with 4 entries: + "getWeather" => agentTool(...) + "getTime" => agentTool(...) + "writeTool" => agentTool(...) + "listTools" => agentTool(...) +``` """ function listTool(store::toolStore)::agentTool return agentTool( @@ -80,30 +98,31 @@ function listTool(store::toolStore)::agentTool end """ -Load all tool modules from a directory into a specific toolStore. +Load `.jl` tool files from `dir` into `store`, then auto-register +`listTool` so the LLM can discover available tools at runtime. -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. +Each `.jl` file must define `function getTool()::agentTool ... end`. +Files are sorted alphabetically for deterministic registration order. +Each file is loaded into its own Julia submodule to avoid name collisions. # Arguments -- `store::toolStore`: The tool store to register tools into -- `dir::String`: Directory path to scan for `.jl` tool files +- `store`: Tool store to populate +- `dir`: Directory containing `.jl` tool files # Returns -- `OrderedDict{String, agentTool}`: All loaded tools keyed by name +- The same `store.tools` dict (modified in place) # Errors -- Throws `ArgumentError` if a tool file does not define a `getTool` function +- Throws `ArgumentError` if `dir` does not exist or a file lacks `getTool()` -# Examples +# Example ```julia -julia> store = toolStore(name="agent1") -julia> tools = loadTools(store, "src/tools") +julia> store = toolStore(name="agent1"); + +julia> loadTools(store, "src/tools") +[toolRegistry:agent1] Loaded tool: getWeather (Weather Lookup) +[toolRegistry:agent1] Loaded tool: getTime (Time Lookup) +[toolRegistry:agent1] Registered tool: listTools OrderedDict{String, agentTool} with 3 entries: "getWeather" => agentTool(...) "getTime" => agentTool(...) @@ -168,24 +187,30 @@ function loadTools(store::toolStore, dir::String)::OrderedDict{String, agentTool end end + registerTool(store, listTool(store)) return store.tools end """ -Register a single agentTool into a specific toolStore. + registerTool(store::toolStore, tool::agentTool) -> OrderedDict{String, agentTool} + +Add `tool` to `store`, overwriting any existing tool with the same name. # Arguments -- `store::toolStore`: The tool store to register into -- `tool::agentTool`: The tool to register +- `store`: Tool store to modify +- `tool`: The `agentTool` to register # Returns -- `OrderedDict{String, agentTool}`: Updated tool dict for this store +- The same `store.tools` dict (modified in place) -# Examples +# Example ```julia -julia> store = toolStore(name="agent1") -julia> registerTool(store, my_tool) -[toolRegistry:agent1] Registered tool: my_tool +julia> store = toolStore(name="agent1"); + +julia> registerTool(store, listTool(store)) +[toolRegistry:agent1] Registered tool: listTools +OrderedDict{String, agentTool} with 1 entry: + "listTools" => agentTool(...) ``` """ function registerTool(store::toolStore, tool::agentTool)::OrderedDict{String, agentTool} @@ -195,22 +220,21 @@ function registerTool(store::toolStore, tool::agentTool)::OrderedDict{String, ag end """ -Get the registered tools from a specific toolStore. +Return the tools registered in `store`. -Returns the internal `OrderedDict` directly — O(1) lookup by name, -ordered iteration preserving registration order. +The returned dict is the **same object** stored inside `store` — mutations +to it (e.g. via `registerTool`) are visible through subsequent calls. # Arguments -- `store::toolStore`: The tool store to query +- `store`: Tool store to query # Returns - `OrderedDict{String, agentTool}`: Tools keyed by name, in registration order -# Examples +# Example ```julia -julia> getTools(store) -OrderedDict{String, agentTool} with 3 entries: - "listTools" => agentTool(...) +julia> tools = getTools(store) +OrderedDict{String, agentTool} with 2 entries: "getWeather" => agentTool(...) "getTime" => agentTool(...) ``` @@ -220,18 +244,22 @@ function getTools(store::toolStore)::OrderedDict{String, agentTool} end """ -Clear all registered tools from a specific toolStore. +Remove all tools from `store`. # Arguments -- `store::toolStore`: The tool store to clear +- `store`: Tool store to clear # Returns - `nothing` -# Examples +# Example ```julia julia> clearTools(store) [toolRegistry:agent1] Registry cleared +nothing + +julia> getTools(store) +OrderedDict{String, agentTool} with 0 entries ``` """ function clearTools(store::toolStore)::Nothing diff --git a/src/tools/README.md b/src/tools/README.md index f981e3c..3404b54 100644 --- a/src/tools/README.md +++ b/src/tools/README.md @@ -52,7 +52,7 @@ result = tool.execute("call-1", Dict{String,Any}(), nothing, x->x) ### Step 2: Load — `loadTools()` -Load all tool modules from a directory into a `toolStore`. Each `.jl` file must define `getTool()::agentTool`. +Load all tool modules from a directory into a `toolStore`. Each `.jl` file must define `getTool()::agentTool`. `listTool` is auto-registered so the LLM can discover available tools. ```julia using YiemAgent, YiemAgent.toolRegistry @@ -60,12 +60,13 @@ using YiemAgent, YiemAgent.toolRegistry store = toolStore(name="myAgent") tools = loadTools(store, "src/tools") # Scans src/tools/ for .jl files, wraps each in a submodule, calls getTool(), registers in store.tools +# Also auto-registers listTools for runtime discovery ``` **Result extraction:** ```julia all_tools = getTools(store) # OrderedDict{String, agentTool} -# Keys: "getTime", "getWeather", "writeTool" +# Keys: "getTime", "getWeather", "writeTool", "listTools" getTime_tool = all_tools["getTime"] # Manual registration (alternative to loadTools) @@ -73,7 +74,7 @@ registerTool(store, my_tool) clearTools(store) # Clear all tools from store ``` -**Source:** `toolRegistry.jl:113-172` +**Source:** `toolRegistry.jl:126-178` --- @@ -84,7 +85,7 @@ Wire the loaded tools into a new `yiemAgent` instance. The `tools` parameter is ```julia using YiemAgent, YiemAgent.type, YiemAgent.toolRegistry -# 1. Set up toolStore and load tools +# 1. Set up toolStore and load tools (auto-registers listTools) store = toolStore(name="myAgent") loadTools(store, "src/tools") @@ -105,6 +106,7 @@ agent = yiemAgent( store = toolStore(name="myAgent") registerTool(store, getTime_tool) registerTool(store, getWeather_tool) +registerTool(store, listTool(store)) # needed for manual registration agent = yiemAgent( tools = getTools(store), @@ -297,7 +299,7 @@ end function loadTools(store::toolStore, dir::String)::OrderedDict{String, agentTool} ``` -**Source:** `toolRegistry.jl:113-172` +**Source:** `toolRegistry.jl:126-178` 1. **Scans** `dir` for `.jl` files (excluding files matching `registry` in name) 2. **Sorts** filenames alphabetically for deterministic registration order @@ -312,7 +314,8 @@ function loadTools(store::toolStore, dir::String)::OrderedDict{String, agentTool ``` 4. **Evaluates** `getTool()` within the submodule scope using `Core.eval(mod, :(getTool()))` — this avoids world-age issues 5. **Validates** the return value is an `agentTool` instance -6. **Registers** the tool in `store.tools` and returns an `OrderedDict{String, agentTool}` +6. **Registers** the tool in `store.tools` +7. **Auto-registers** `listTool(store)` so the LLM can discover available tools at runtime ### Why Submodules? @@ -328,7 +331,7 @@ Each tool file is loaded into its own **namespaced submodule**. This means: store1 = toolStore(name="agent1") store2 = toolStore(name="agent2") -# Load tools into specific stores +# Load tools into specific stores (auto-registers listTools) tools1 = loadTools(store1, "src/tools/weather_tools") # agent1 only tools2 = loadTools(store2, "src/tools/wine_tools") # agent2 only @@ -1282,9 +1285,9 @@ The framework supports tools that modify the tool system itself at runtime. ### `listTool` — Discover Available Tools -**Source:** `toolRegistry.jl:54-82` +**Source:** `toolRegistry.jl:55-82` -Each `toolStore` gets its own `listTool` instance bound to that store via `listTool(store)`, so each agent sees only its own tools. Primarily useful for **collision detection** before creating a new tool via `writeTool`. +Each `toolStore` gets its own `listTool` instance bound to that store via `listTool(store)`, so each agent sees only its own tools. `loadTools` auto-registers one, so the LLM can discover available tools at runtime. Also useful for **collision detection** before creating a new tool via `writeTool`. ### Self-Tooling Workflow diff --git a/src/tools/getTime.jl b/src/tools/getTime.jl index 94b7752..88098d7 100644 --- a/src/tools/getTime.jl +++ b/src/tools/getTime.jl @@ -43,7 +43,8 @@ Execute the getTime tool. Returns mock time data for the given timezone or city. """ -function executeTool(toolCallId::String, args::Dict{String,Any}, signal::Union{Nothing,abortSignal}, onPartialResult::Function)::agentToolResult +function executeTool(toolCallId::String, args::Dict{String,Any}, signal::Union{Nothing,abortSignal}, + onPartialResult::Function)::agentToolResult tz = get(args, "timezone", nothing) city = get(args, "city", "") if tz !== nothing diff --git a/src/tools/getWeather.jl b/src/tools/getWeather.jl index e98d6a9..61639cf 100644 --- a/src/tools/getWeather.jl +++ b/src/tools/getWeather.jl @@ -3,7 +3,8 @@ Execute the getWeather tool. Returns mock weather data for the given city and temperature units. """ -function executeTool(toolCallId::String, args::Dict{String,Any}, signal::Union{Nothing,abortSignal}, onPartialResult::Function)::agentToolResult +function executeTool(toolCallId::String, args::Dict{String,Any}, signal::Union{Nothing,abortSignal}, + onPartialResult::Function)::agentToolResult city = get(args, "city", "") units = get(args, "units", "celsius") temp = units == "fahrenheit" ? "72" : "22" diff --git a/test/toolTest.jl b/test/toolTest.jl index 69e59b3..b239f36 100644 --- a/test/toolTest.jl +++ b/test/toolTest.jl @@ -29,21 +29,22 @@ TOOLS_DIR = joinpath(@__DIR__, "..", "src", "tools") store2 = toolStore(name="test2") loaded = loadTools(store2, TOOLS_DIR) @test !isempty(loaded) - @test length(loaded) == 3 + @test length(loaded) == 4 # 3 files + auto-registered listTools names = [k for k in keys(loaded)] @test "getTime" in names @test "getWeather" in names @test "writeTool" in names + @test "listTools" in names # ------------------------------------------------------------------ # # 4. loadTools returns tools sorted alphabetically by filename # - # (getTime.jl < getWeather.jl < writeTool.jl) # - # because 'T' < 'W' in ASCII # + # (getTime.jl < getWeather.jl < writeTool.jl) + listTools at end # # ------------------------------------------------------------------ # @test collect(keys(loaded))[1] == "getTime" @test collect(keys(loaded))[2] == "getWeather" @test collect(keys(loaded))[3] == "writeTool" + @test collect(keys(loaded))[4] == "listTools" # ------------------------------------------------------------------ # # 5. Verify loaded tool fields are correct # @@ -171,3 +172,41 @@ TOOLS_DIR = joinpath(@__DIR__, "..", "src", "tools") @test isempty(getTools(storeA)) @test !isempty(getTools(storeB)) # storeB unaffected end + +@testset "listTool" begin + store = toolStore(name="test_list") + loaded = loadTools(store, TOOLS_DIR) # auto-registers getWeather, getTime, writeTool + listTools + + # loadTools auto-registers listTool + @test "listTools" in keys(loaded) + + # listTool returns an agentTool, not a string or array + list_t = listTool(store) + @test list_t isa agentTool + @test list_t.name == "listTools" + @test list_t.label == "List Tools" + @test isempty(list_t.inputSchema["required"]) + + # Verify all tools appear (3 loaded + listTools = 4) + result = list_t.execute("call-1", Dict{String,Any}(), nothing, x -> x) + @test result isa agentToolResult + @test result.content[1] isa textContent + @test occursin("listTools", result.content[1].text) + @test occursin("getWeather", result.content[1].text) + @test occursin("getTime", result.content[1].text) + @test occursin("writeTool", result.content[1].text) + @test result.details["count"] == 4 + + # Each listTool call creates an independent closure + storeB = toolStore(name="test_listB") + registerTool(storeB, loaded["getWeather"]) + list_tB = listTool(storeB) + + resultA = list_t.execute("call-3", Dict{String,Any}(), nothing, x -> x) + resultB = list_tB.execute("call-4", Dict{String,Any}(), nothing, x -> x) + + @test occursin("getWeather", resultA.content[1].text) + @test occursin("getWeather", resultB.content[1].text) + @test occursin("getTime", resultA.content[1].text) + @test occursin("getTime", resultB.content[1].text) == false # storeB only has getWeather +end