This commit is contained in:
2026-08-11 12:15:05 +07:00
parent ed91260468
commit 5a27630ccf
5 changed files with 132 additions and 60 deletions
+74 -46
View File
@@ -22,17 +22,17 @@ struct toolStore
end end
""" """
Create a new isolated tool store. toolStore(; name="default") -> toolStore
Create a new empty tool store.
# Keyword Arguments # Keyword Arguments
- `name::String`: Identifier for this store (default: "default") - `name::String`: Display name for logging (default: `"default"`)
# Examples # Example
```julia ```julia
store = toolStore(name="agent1") julia> store = toolStore(name="agent1")
tools = loadTools(store, "src/tools") toolStore(OrderedDict{String, agentTool}(), "agent1")
registerTool(store, my_tool)
agent = yiemAgent(tools=getTools(store), llmCall=..., _tool_store=store)
``` ```
""" """
function toolStore(; name::String="default")::toolStore function toolStore(; name::String="default")::toolStore
@@ -40,14 +40,32 @@ function toolStore(; name::String="default")::toolStore
end end
""" """
List tool definition — lets the agent query available tools for collision detection listTool(store::toolStore) -> agentTool
when creating new tools via writeTool.
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 # 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, # Example
so each agent sees only its own tools. ```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 function listTool(store::toolStore)::agentTool
return agentTool( return agentTool(
@@ -80,30 +98,31 @@ function listTool(store::toolStore)::agentTool
end 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 Each `.jl` file must define `function getTool()::agentTool ... end`.
`getTool()::agentTool`. Files are sorted alphabetically so tool Files are sorted alphabetically for deterministic registration order.
registration order is deterministic. Each file is loaded into its own Julia submodule to avoid name collisions.
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.
# Arguments # Arguments
- `store::toolStore`: The tool store to register tools into - `store`: Tool store to populate
- `dir::String`: Directory path to scan for `.jl` tool files - `dir`: Directory containing `.jl` tool files
# Returns # Returns
- `OrderedDict{String, agentTool}`: All loaded tools keyed by name - The same `store.tools` dict (modified in place)
# Errors # 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
julia> store = toolStore(name="agent1") julia> store = toolStore(name="agent1");
julia> tools = loadTools(store, "src/tools")
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: OrderedDict{String, agentTool} with 3 entries:
"getWeather" => agentTool(...) "getWeather" => agentTool(...)
"getTime" => agentTool(...) "getTime" => agentTool(...)
@@ -168,24 +187,30 @@ function loadTools(store::toolStore, dir::String)::OrderedDict{String, agentTool
end end
end end
registerTool(store, listTool(store))
return store.tools return store.tools
end 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 # Arguments
- `store::toolStore`: The tool store to register into - `store`: Tool store to modify
- `tool::agentTool`: The tool to register - `tool`: The `agentTool` to register
# Returns # Returns
- `OrderedDict{String, agentTool}`: Updated tool dict for this store - The same `store.tools` dict (modified in place)
# Examples # Example
```julia ```julia
julia> store = toolStore(name="agent1") julia> store = toolStore(name="agent1");
julia> registerTool(store, my_tool)
[toolRegistry:agent1] Registered tool: my_tool 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} function registerTool(store::toolStore, tool::agentTool)::OrderedDict{String, agentTool}
@@ -195,22 +220,21 @@ function registerTool(store::toolStore, tool::agentTool)::OrderedDict{String, ag
end 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, The returned dict is the **same object** stored inside `store` — mutations
ordered iteration preserving registration order. to it (e.g. via `registerTool`) are visible through subsequent calls.
# Arguments # Arguments
- `store::toolStore`: The tool store to query - `store`: Tool store to query
# Returns # Returns
- `OrderedDict{String, agentTool}`: Tools keyed by name, in registration order - `OrderedDict{String, agentTool}`: Tools keyed by name, in registration order
# Examples # Example
```julia ```julia
julia> getTools(store) julia> tools = getTools(store)
OrderedDict{String, agentTool} with 3 entries: OrderedDict{String, agentTool} with 2 entries:
"listTools" => agentTool(...)
"getWeather" => agentTool(...) "getWeather" => agentTool(...)
"getTime" => agentTool(...) "getTime" => agentTool(...)
``` ```
@@ -220,18 +244,22 @@ function getTools(store::toolStore)::OrderedDict{String, agentTool}
end end
""" """
Clear all registered tools from a specific toolStore. Remove all tools from `store`.
# Arguments # Arguments
- `store::toolStore`: The tool store to clear - `store`: Tool store to clear
# Returns # Returns
- `nothing` - `nothing`
# Examples # Example
```julia ```julia
julia> clearTools(store) julia> clearTools(store)
[toolRegistry:agent1] Registry cleared [toolRegistry:agent1] Registry cleared
nothing
julia> getTools(store)
OrderedDict{String, agentTool} with 0 entries
``` ```
""" """
function clearTools(store::toolStore)::Nothing function clearTools(store::toolStore)::Nothing
+12 -9
View File
@@ -52,7 +52,7 @@ result = tool.execute("call-1", Dict{String,Any}(), nothing, x->x)
### Step 2: Load — `loadTools()` ### 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 ```julia
using YiemAgent, YiemAgent.toolRegistry using YiemAgent, YiemAgent.toolRegistry
@@ -60,12 +60,13 @@ using YiemAgent, YiemAgent.toolRegistry
store = toolStore(name="myAgent") store = toolStore(name="myAgent")
tools = loadTools(store, "src/tools") tools = loadTools(store, "src/tools")
# Scans src/tools/ for .jl files, wraps each in a submodule, calls getTool(), registers in store.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:** **Result extraction:**
```julia ```julia
all_tools = getTools(store) # OrderedDict{String, agentTool} all_tools = getTools(store) # OrderedDict{String, agentTool}
# Keys: "getTime", "getWeather", "writeTool" # Keys: "getTime", "getWeather", "writeTool", "listTools"
getTime_tool = all_tools["getTime"] getTime_tool = all_tools["getTime"]
# Manual registration (alternative to loadTools) # Manual registration (alternative to loadTools)
@@ -73,7 +74,7 @@ registerTool(store, my_tool)
clearTools(store) # Clear all tools from store 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 ```julia
using YiemAgent, YiemAgent.type, YiemAgent.toolRegistry 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") store = toolStore(name="myAgent")
loadTools(store, "src/tools") loadTools(store, "src/tools")
@@ -105,6 +106,7 @@ agent = yiemAgent(
store = toolStore(name="myAgent") store = toolStore(name="myAgent")
registerTool(store, getTime_tool) registerTool(store, getTime_tool)
registerTool(store, getWeather_tool) registerTool(store, getWeather_tool)
registerTool(store, listTool(store)) # needed for manual registration
agent = yiemAgent( agent = yiemAgent(
tools = getTools(store), tools = getTools(store),
@@ -297,7 +299,7 @@ end
function loadTools(store::toolStore, dir::String)::OrderedDict{String, agentTool} 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) 1. **Scans** `dir` for `.jl` files (excluding files matching `registry` in name)
2. **Sorts** filenames alphabetically for deterministic registration order 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 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 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? ### Why Submodules?
@@ -328,7 +331,7 @@ Each tool file is loaded into its own **namespaced submodule**. This means:
store1 = toolStore(name="agent1") store1 = toolStore(name="agent1")
store2 = toolStore(name="agent2") 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 tools1 = loadTools(store1, "src/tools/weather_tools") # agent1 only
tools2 = loadTools(store2, "src/tools/wine_tools") # agent2 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 ### `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 ### Self-Tooling Workflow
+2 -1
View File
@@ -43,7 +43,8 @@ Execute the getTime tool.
Returns mock time data for the given timezone or city. 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) tz = get(args, "timezone", nothing)
city = get(args, "city", "") city = get(args, "city", "")
if tz !== nothing if tz !== nothing
+2 -1
View File
@@ -3,7 +3,8 @@ Execute the getWeather tool.
Returns mock weather data for the given city and temperature units. 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", "") city = get(args, "city", "")
units = get(args, "units", "celsius") units = get(args, "units", "celsius")
temp = units == "fahrenheit" ? "72" : "22" temp = units == "fahrenheit" ? "72" : "22"
+42 -3
View File
@@ -29,21 +29,22 @@ TOOLS_DIR = joinpath(@__DIR__, "..", "src", "tools")
store2 = toolStore(name="test2") store2 = toolStore(name="test2")
loaded = loadTools(store2, TOOLS_DIR) loaded = loadTools(store2, TOOLS_DIR)
@test !isempty(loaded) @test !isempty(loaded)
@test length(loaded) == 3 @test length(loaded) == 4 # 3 files + auto-registered listTools
names = [k for k in keys(loaded)] names = [k for k in keys(loaded)]
@test "getTime" in names @test "getTime" in names
@test "getWeather" in names @test "getWeather" in names
@test "writeTool" in names @test "writeTool" in names
@test "listTools" in names
# ------------------------------------------------------------------ # # ------------------------------------------------------------------ #
# 4. loadTools returns tools sorted alphabetically by filename # # 4. loadTools returns tools sorted alphabetically by filename #
# (getTime.jl < getWeather.jl < writeTool.jl) # # (getTime.jl < getWeather.jl < writeTool.jl) + listTools at end #
# because 'T' < 'W' in ASCII #
# ------------------------------------------------------------------ # # ------------------------------------------------------------------ #
@test collect(keys(loaded))[1] == "getTime" @test collect(keys(loaded))[1] == "getTime"
@test collect(keys(loaded))[2] == "getWeather" @test collect(keys(loaded))[2] == "getWeather"
@test collect(keys(loaded))[3] == "writeTool" @test collect(keys(loaded))[3] == "writeTool"
@test collect(keys(loaded))[4] == "listTools"
# ------------------------------------------------------------------ # # ------------------------------------------------------------------ #
# 5. Verify loaded tool fields are correct # # 5. Verify loaded tool fields are correct #
@@ -171,3 +172,41 @@ TOOLS_DIR = joinpath(@__DIR__, "..", "src", "tools")
@test isempty(getTools(storeA)) @test isempty(getTools(storeA))
@test !isempty(getTools(storeB)) # storeB unaffected @test !isempty(getTools(storeB)) # storeB unaffected
end 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