Compare commits

...

2 Commits

Author SHA1 Message Date
ton 5a27630ccf update 2026-08-11 12:15:05 +07:00
ton ed91260468 update 2026-08-10 20:37:28 +07:00
6 changed files with 165 additions and 93 deletions
+84 -56
View File
@@ -1,6 +1,6 @@
module toolRegistry module toolRegistry
export ToolStore, loadTools, registerTool, getTools, clearTools, listTool export toolStore, loadTools, registerTool, getTools, clearTools, listTool
using Dates using Dates
using JSON, DataStructures using JSON, DataStructures
@@ -9,47 +9,65 @@ using ..type
""" """
Per-agent isolated tool storage. Per-agent isolated tool storage.
Each agent gets its own `ToolStore` so tool registration is independent — Each agent gets its own `toolStore` so tool registration is independent —
`registerTool(store, tool)` only affects that agent's tool set. `registerTool(store, tool)` only affects that agent's tool set.
# Fields # Fields
- `tools::OrderedDict{String, agentTool}` — keyed by name for O(1) lookup + ordered iteration - `tools::OrderedDict{String, agentTool}` — keyed by name for O(1) lookup + ordered iteration
- `name::String` — identifier for debugging/logs - `name::String` — identifier for debugging/logs
""" """
struct ToolStore struct toolStore
tools::OrderedDict{String, agentTool} tools::OrderedDict{String, agentTool}
name::String name::String
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
ToolStore(OrderedDict{String, agentTool}(), name) toolStore(OrderedDict{String, agentTool}(), name)
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(
name = "listTools", name = "listTools",
label = "List Tools", label = "List Tools",
@@ -80,37 +98,38 @@ 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(...)
"listTools" => agentTool(...) "listTools" => agentTool(...)
``` ```
""" """
function loadTools(store::ToolStore, dir::String)::OrderedDict{String, agentTool} function loadTools(store::toolStore, dir::String)::OrderedDict{String, agentTool}
if !isdir(dir) if !isdir(dir)
throw(ArgumentError("Tool directory does not exist: $dir")) throw(ArgumentError("Tool directory does not exist: $dir"))
end end
@@ -168,73 +187,82 @@ 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}
store.tools[tool.name] = tool store.tools[tool.name] = tool
println("[$(store.name)] Registered tool: $(tool.name)") println("[$(store.name)] Registered tool: $(tool.name)")
return store.tools return store.tools
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(...)
``` ```
""" """
function getTools(store::ToolStore)::OrderedDict{String, agentTool} function getTools(store::toolStore)::OrderedDict{String, agentTool}
return store.tools return store.tools
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
empty!(store.tools) empty!(store.tools)
println("[$(store.name)] Registry cleared") println("[$(store.name)] Registry cleared")
return nothing return nothing
+26 -23
View File
@@ -52,20 +52,21 @@ 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
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,8 +85,8 @@ 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")
# 2. Create agent — pass tools + _tool_store # 2. Create agent — pass tools + _tool_store
@@ -102,9 +103,10 @@ agent = yiemAgent(
**Manual registration** (without `loadTools`): **Manual registration** (without `loadTools`):
```julia ```julia
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),
@@ -124,7 +126,7 @@ agent = yiemAgent(
| `messages` | `Vector{agentMessage}` | No (default: empty) | Initial conversation history | | `messages` | `Vector{agentMessage}` | No (default: empty) | Initial conversation history |
| `llmCall` | `Function` | **Yes** | `(messages::Dict) -> assistantMessage` — invokes the LLM | | `llmCall` | `Function` | **Yes** | `(messages::Dict) -> assistantMessage` — invokes the LLM |
| `agentEventSink` | `Function` | **Yes** | `(event) -> nothing` — receives tool lifecycle events | | `agentEventSink` | `Function` | **Yes** | `(event) -> nothing` — receives tool lifecycle events |
| `_tool_store` | `ToolStore` | No | Runtime tool registry for `registerTool()` | | `_tool_store` | `toolStore` | No | Runtime tool registry for `registerTool()` |
Optional hooks: `prepareContext`, `formatMsgForLLM`, `beforeToolCall`, `afterToolCall`, `sessionId`, `maxRetryDelayMs`, `parallelToolExecute`. Optional hooks: `prepareContext`, `formatMsgForLLM`, `beforeToolCall`, `afterToolCall`, `sessionId`, `maxRetryDelayMs`, `parallelToolExecute`.
@@ -278,12 +280,12 @@ The `terminate` flag is checked at the batch level. See [Section 10](#10-tool-ca
**Source:** `toolRegistry.jl` **Source:** `toolRegistry.jl`
### How `ToolStore` Works ### How `toolStore` Works
The registry uses **per-agent isolated storage** via the `ToolStore` struct. Each agent gets its own store, so tool registration is independent — `registerTool(store, tool)` only affects that agent's tool set. The registry uses **per-agent isolated storage** via the `toolStore` struct. Each agent gets its own store, so tool registration is independent — `registerTool(store, tool)` only affects that agent's tool set.
```julia ```julia
struct ToolStore struct toolStore
tools::OrderedDict{String, agentTool} # keyed by name for O(1) lookup + ordered iteration tools::OrderedDict{String, agentTool} # keyed by name for O(1) lookup + ordered iteration
name::String # identifier for debugging/logs name::String # identifier for debugging/logs
end end
@@ -294,10 +296,10 @@ end
### How `loadTools(store, dir)` Works ### How `loadTools(store, dir)` Works
```julia ```julia
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?
@@ -325,10 +328,10 @@ Each tool file is loaded into its own **namespaced submodule**. This means:
```julia ```julia
# Create per-agent stores # Create per-agent stores
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
@@ -350,11 +353,11 @@ clearTools(store1) # only clears store1
### Per-Agent Isolation ### Per-Agent Isolation
Each `ToolStore` is completely independent — tools registered in one store do not appear in another: Each `toolStore` is completely independent — tools registered in one store do not appear in another:
```julia ```julia
storeA = ToolStore(name="A") storeA = toolStore(name="A")
storeB = ToolStore(name="B") storeB = toolStore(name="B")
registerTool(storeA, getTime_tool) registerTool(storeA, getTime_tool)
registerTool(storeB, getWeather_tool) registerTool(storeB, getWeather_tool)
@@ -382,7 +385,7 @@ yiemAgent struct contains:
- inputChannel (Channel, capacity 16) ← user sends messages here via run_agent() - inputChannel (Channel, capacity 16) ← user sends messages here via run_agent()
- followUpChannel (Channel, capacity 32) ← user sends follow-ups here via follow_up() - followUpChannel (Channel, capacity 32) ← user sends follow-ups here via follow_up()
- outputChannel (Channel, capacity 16) ← agent sends responses here via take_response() - outputChannel (Channel, capacity 16) ← agent sends responses here via take_response()
- _tool_store (ToolStore) ← per-agent isolated tool registry - _tool_store (toolStore) ← per-agent isolated tool registry
``` ```
### Loop States ### Loop States
@@ -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"
+3 -3
View File
@@ -569,7 +569,7 @@ mutable struct yiemAgent <: agent # High-level agent wrapper
maxRetryDelayMs::Union{Int64, Nothing} # Maximum delay between retries (ms) maxRetryDelayMs::Union{Int64, Nothing} # Maximum delay between retries (ms)
parallelToolExecute::Bool # Default: false parallelToolExecute::Bool # Default: false
agentEventSink::Function # agent emits its status via this function agentEventSink::Function # agent emits its status via this function
_tool_store::Any # Reference to the ToolStore for runtime registration _tool_store::Any # Reference to the toolStore for runtime registration
end end
""" """
@@ -594,14 +594,14 @@ on `inputChannel` and `followUpChannel` channels concurrently.
- `maxRetryDelayMs::Union{Int64, Nothing}`: Maximum delay between retries in milliseconds (default: `nothing`) - `maxRetryDelayMs::Union{Int64, Nothing}`: Maximum delay between retries in milliseconds (default: `nothing`)
- `parallelToolExecute::Bool`: Run tool calls in parallel (default: `false`) - `parallelToolExecute::Bool`: Run tool calls in parallel (default: `false`)
- `agentEventSink::Function`: Callback to receive agent events - `agentEventSink::Function`: Callback to receive agent events
- `tool_store::Union{Any, Nothing}`: ToolStore for runtime tool registration (default: `nothing`) - `tool_store::Union{Any, Nothing}`: toolStore for runtime tool registration (default: `nothing`)
# Returns # Returns
- A new `yiemAgent` instance with an active background task - A new `yiemAgent` instance with an active background task
# Examples # Examples
```julia ```julia
julia> store = ToolStore(name="agent1") julia> store = toolStore(name="agent1")
julia> tools = loadTools(store, "src/tools") julia> tools = loadTools(store, "src/tools")
julia> agent = yiemAgent(systemPrompt="You are a helpful assistant", model=my_model, tools=tools, llmCall=..., tool_store=store) julia> agent = yiemAgent(systemPrompt="You are a helpful assistant", model=my_model, tools=tools, llmCall=..., tool_store=store)
yiemAgent(agentState(...), Channel(...), Channel(...), Channel(...), ..., store) yiemAgent(agentState(...), Channel(...), Channel(...), Channel(...), ..., store)
+48 -9
View File
@@ -6,12 +6,12 @@ using YiemAgent.type
# Path to the real tools directory # Path to the real tools directory
TOOLS_DIR = joinpath(@__DIR__, "..", "src", "tools") TOOLS_DIR = joinpath(@__DIR__, "..", "src", "tools")
@testset "loadTools with ToolStore" begin @testset "loadTools with toolStore" begin
# ------------------------------------------------------------------ # # ------------------------------------------------------------------ #
# 1. loadTools throws on non-existent directory # # 1. loadTools throws on non-existent directory #
# ------------------------------------------------------------------ # # ------------------------------------------------------------------ #
store = ToolStore(name="test1") store = toolStore(name="test1")
@test_throws ArgumentError loadTools(store, "/nonexistent/dir/that/does/not/exist") @test_throws ArgumentError loadTools(store, "/nonexistent/dir/that/does/not/exist")
# ------------------------------------------------------------------ # # ------------------------------------------------------------------ #
@@ -26,24 +26,25 @@ TOOLS_DIR = joinpath(@__DIR__, "..", "src", "tools")
# ------------------------------------------------------------------ # # ------------------------------------------------------------------ #
# 3. loadTools loads actual tool files from src/tools/ # # 3. loadTools loads actual tool files from 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 #
@@ -102,7 +103,7 @@ TOOLS_DIR = joinpath(@__DIR__, "..", "src", "tools")
# ------------------------------------------------------------------ # # ------------------------------------------------------------------ #
# 7. getTools / registerTool / clearTools (per-store isolation) # # 7. getTools / registerTool / clearTools (per-store isolation) #
# ------------------------------------------------------------------ # # ------------------------------------------------------------------ #
store3 = ToolStore(name="test3") store3 = toolStore(name="test3")
registry_tools = getTools(store3) registry_tools = getTools(store3)
@test isempty(registry_tools) @test isempty(registry_tools)
@@ -153,8 +154,8 @@ TOOLS_DIR = joinpath(@__DIR__, "..", "src", "tools")
# ------------------------------------------------------------------ # # ------------------------------------------------------------------ #
# 9. Per-store isolation — two stores don't share tools # # 9. Per-store isolation — two stores don't share tools #
# ------------------------------------------------------------------ # # ------------------------------------------------------------------ #
storeA = ToolStore(name="isolationA") storeA = toolStore(name="isolationA")
storeB = ToolStore(name="isolationB") storeB = toolStore(name="isolationB")
registerTool(storeA, loaded["getTime"]) registerTool(storeA, loaded["getTime"])
registerTool(storeB, loaded["getWeather"]) registerTool(storeB, loaded["getWeather"])
@@ -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