This commit is contained in:
2026-08-11 12:15:05 +07:00
parent ed91260468
commit a35402a4b1
5 changed files with 132 additions and 60 deletions
+74 -46
View File
@@ -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
+12 -9
View File
@@ -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
+2 -1
View File
@@ -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
+2 -1
View File
@@ -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"
+42 -3
View File
@@ -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