Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5a27630ccf | |||
| ed91260468 |
+84
-56
@@ -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
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user