V0.8.0 verify tool use #43
+1
-1
@@ -13,7 +13,7 @@ module YiemAgent
|
||||
include("utils.jl")
|
||||
using .utils
|
||||
|
||||
include("tools/registry.jl")
|
||||
include("toolRegistry.jl")
|
||||
using .toolRegistry
|
||||
|
||||
# include("llmfunction.jl")
|
||||
|
||||
@@ -13,13 +13,11 @@ Each agent gets its own `ToolStore` so tool registration is independent —
|
||||
`registerTool(store, tool)` only affects that agent's tool set.
|
||||
|
||||
# Fields
|
||||
- `tools::Vector{agentTool}` — ordered tool list (for `listTool` iteration)
|
||||
- `modules::Vector{Module}` — keeps tool submodules alive to prevent GC of closures
|
||||
- `tools::OrderedDict{String, agentTool}` — keyed by name for O(1) lookup + ordered iteration
|
||||
- `name::String` — identifier for debugging/logs
|
||||
"""
|
||||
struct ToolStore
|
||||
tools::Vector{agentTool}
|
||||
modules::Vector{Module}
|
||||
tools::OrderedDict{String, agentTool}
|
||||
name::String
|
||||
end
|
||||
|
||||
@@ -38,7 +36,7 @@ agent = yiemAgent(tools=getTools(store), llmCall=..., _tool_store=store)
|
||||
```
|
||||
"""
|
||||
function ToolStore(; name::String="default")::ToolStore
|
||||
ToolStore(agentTool[], Module[], name)
|
||||
ToolStore(OrderedDict{String, agentTool}(), name)
|
||||
end
|
||||
|
||||
"""
|
||||
@@ -66,7 +64,7 @@ function listTool(store::ToolStore)::agentTool
|
||||
if isempty(tools)
|
||||
result_text = "No tools registered."
|
||||
else
|
||||
lines = String["- $(t.name): $(t.label) — $(t.description)" for t in tools]
|
||||
lines = String["- $(t.name): $(t.label) — $(t.description)" for (k, t) in tools]
|
||||
result_text = "Available tools:\n" * join(lines, "\n")
|
||||
end
|
||||
return agentToolResult(
|
||||
@@ -117,7 +115,6 @@ function loadTools(store::ToolStore, dir::String)::OrderedDict{String, agentTool
|
||||
throw(ArgumentError("Tool directory does not exist: $dir"))
|
||||
end
|
||||
|
||||
tools = OrderedDict{String, agentTool}()
|
||||
jl_files = filter(f -> endswith(f, ".jl") && !occursin(r"(?i)registry", f), readdir(dir))
|
||||
sort!(jl_files)
|
||||
|
||||
@@ -158,12 +155,7 @@ function loadTools(store::ToolStore, dir::String)::OrderedDict{String, agentTool
|
||||
"getTool() in $(filepath) did not return an agentTool instance, got: $(typeof(tool))"
|
||||
))
|
||||
end
|
||||
# Keep module reference alive — closures in the agentTool (execute,
|
||||
# validateRequiredArgs, prepareArguments) may reference module-scoped
|
||||
# functions. Without this, GC could collect the module.
|
||||
push!(store.modules, mod)
|
||||
push!(store.tools, tool)
|
||||
tools[tool.name] = tool
|
||||
store.tools[tool.name] = tool
|
||||
println("[$(store.name)] Loaded tool: $(tool.name) ($(tool.label))")
|
||||
catch e
|
||||
if e isa UndefVarError || occursin("getTool", sprint(showerror, e))
|
||||
@@ -176,7 +168,7 @@ function loadTools(store::ToolStore, dir::String)::OrderedDict{String, agentTool
|
||||
end
|
||||
end
|
||||
|
||||
return tools
|
||||
return store.tools
|
||||
end
|
||||
|
||||
"""
|
||||
@@ -187,7 +179,7 @@ Register a single agentTool into a specific ToolStore.
|
||||
- `tool::agentTool`: The tool to register
|
||||
|
||||
# Returns
|
||||
- `Vector{agentTool}`: Updated tool list for this store
|
||||
- `OrderedDict{String, agentTool}`: Updated tool dict for this store
|
||||
|
||||
# Examples
|
||||
```julia
|
||||
@@ -196,21 +188,17 @@ julia> registerTool(store, my_tool)
|
||||
[toolRegistry:agent1] Registered tool: my_tool
|
||||
```
|
||||
"""
|
||||
function registerTool(store::ToolStore, tool::agentTool)::Vector{agentTool}
|
||||
push!(store.tools, tool)
|
||||
function registerTool(store::ToolStore, tool::agentTool)::OrderedDict{String, agentTool}
|
||||
store.tools[tool.name] = tool
|
||||
println("[$(store.name)] Registered tool: $(tool.name)")
|
||||
return store.tools
|
||||
end
|
||||
|
||||
"""
|
||||
Get all registered tools from a specific ToolStore as an
|
||||
`OrderedDict{String, agentTool}` keyed by tool name.
|
||||
Get the registered tools from a specific ToolStore.
|
||||
|
||||
The internal vector is for ordered iteration (used by `listTool`).
|
||||
This function builds an `OrderedDict` so callers get:
|
||||
- O(1) lookup by name
|
||||
- Deterministic iteration order (registration order)
|
||||
- Consistency with `agentState.tools` (also `OrderedDict{String, agentTool}`)
|
||||
Returns the internal `OrderedDict` directly — O(1) lookup by name,
|
||||
ordered iteration preserving registration order.
|
||||
|
||||
# Arguments
|
||||
- `store::ToolStore`: The tool store to query
|
||||
@@ -228,7 +216,7 @@ OrderedDict{String, agentTool} with 3 entries:
|
||||
```
|
||||
"""
|
||||
function getTools(store::ToolStore)::OrderedDict{String, agentTool}
|
||||
return OrderedDict{String, agentTool}(t.name => t for t in store.tools)
|
||||
return store.tools
|
||||
end
|
||||
|
||||
"""
|
||||
+12
-11
@@ -112,7 +112,7 @@ The `terminate` flag is checked at the batch level. See [Section 9](#9-tool-call
|
||||
|
||||
## 3. Tool Registration — Per-Agent Tool Stores
|
||||
|
||||
**Source:** `tools/registry.jl`
|
||||
**Source:** `toolRegistry.jl`
|
||||
|
||||
### How `ToolStore` Works
|
||||
|
||||
@@ -120,19 +120,20 @@ The registry uses **per-agent isolated storage** via the `ToolStore` struct. Eac
|
||||
|
||||
```julia
|
||||
struct ToolStore
|
||||
tools::Vector{agentTool} # ordered tool list (for listTool iteration)
|
||||
modules::Vector{Module} # keeps tool submodules alive to prevent GC
|
||||
name::String # identifier for debugging/logs
|
||||
tools::OrderedDict{String, agentTool} # keyed by name for O(1) lookup + ordered iteration
|
||||
name::String # identifier for debugging/logs
|
||||
end
|
||||
```
|
||||
|
||||
`store.tools` is an `OrderedDict` — it provides O(1) lookup by tool name and preserves insertion order for iteration. `getTools(store)` returns this `OrderedDict` directly (not a copy), so mutations on the returned value affect the store.
|
||||
|
||||
### How `loadTools(store, dir)` Works
|
||||
|
||||
```julia
|
||||
function loadTools(store::ToolStore, dir::String)::OrderedDict{String, agentTool}
|
||||
```
|
||||
|
||||
**Source:** `tools/registry.jl:115-180`
|
||||
**Source:** `toolRegistry.jl:113-177`
|
||||
|
||||
1. **Scans** `dir` for `.jl` files (excluding files matching `registry` in name)
|
||||
2. **Sorts** filenames alphabetically for deterministic registration order
|
||||
@@ -147,15 +148,14 @@ 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. **Stores** the module reference in `store.modules` to prevent GC of closures
|
||||
7. **Registers** the tool in `store.tools` and returns an `OrderedDict{String, agentTool}`
|
||||
6. **Registers** the tool in `store.tools` and returns an `OrderedDict{String, agentTool}`
|
||||
|
||||
### Why Submodules?
|
||||
|
||||
Each tool file is loaded into its own **namespaced submodule**. This means:
|
||||
- `validateRequiredArgs`, `prepareArguments`, `executeTool`, and helper functions defined in `getTime.jl` are scoped under `_tool_getTime`
|
||||
- No name collisions between tools — `getTime.validateRequiredArgs` is distinct from `getWeather.validateRequiredArgs`
|
||||
- The module reference is kept alive in `store.modules` so closures (in `execute`, `validateRequiredArgs`, `prepareArguments`) don't get garbage collected
|
||||
- The module reference is kept alive by the functions stored in `agentTool` (closures in `execute`, `validateRequiredArgs`, `prepareArguments`) so they don't get garbage collected
|
||||
|
||||
### Registration API
|
||||
|
||||
@@ -178,10 +178,11 @@ all_tools = getTools(store1) # OrderedDict{String, agentTool} — O(1) lookup +
|
||||
clearTools(store1) # only clears store1
|
||||
```
|
||||
|
||||
**Why `OrderedDict` for `getTools()`?** The internal `store.tools` is a `Vector{agentTool}` for ordered iteration (used by `listTool`). `getTools()` builds an `OrderedDict` from `store.tools` so callers get:
|
||||
`getTools(store)` returns the internal `OrderedDict` directly, giving callers:
|
||||
- O(1) lookup by tool name
|
||||
- Deterministic iteration order (registration order)
|
||||
- Consistency with `agentState.tools` (also `OrderedDict{String, agentTool}`)
|
||||
- No copy overhead — mutations on the returned value affect the store
|
||||
|
||||
### Per-Agent Isolation
|
||||
|
||||
@@ -1118,7 +1119,7 @@ The framework supports tools that modify the tool system itself at runtime.
|
||||
|
||||
### `listTool` — Discover Available Tools
|
||||
|
||||
**Source:** `tools/registry.jl:54-82`
|
||||
**Source:** `toolRegistry.jl:54-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`.
|
||||
|
||||
@@ -1395,7 +1396,7 @@ module _tool_myTool
|
||||
end
|
||||
```
|
||||
|
||||
All functions in the file are scoped under `_tool_myTool`, preventing name collisions with other tools. The module reference is kept alive in `store.modules` to prevent garbage collection of closures.
|
||||
All functions in the file are scoped under `_tool_myTool`, preventing name collisions with other tools. The module reference is kept alive by the function objects stored in `agentTool`, preventing garbage collection of closures.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -142,13 +142,13 @@ TOOLS_DIR = joinpath(@__DIR__, "..", "src", "tools")
|
||||
@test reg["manualTool"].parallelToolExecute == true
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# 8. getTools returns deep copy (mutations don't affect registry) #
|
||||
# 8. getTools returns direct reference (mutations affect registry) #
|
||||
# ------------------------------------------------------------------ #
|
||||
copy1 = getTools(store3)
|
||||
copy2 = getTools(store3)
|
||||
@test copy1 !== copy2
|
||||
@test copy1 === copy2 # same reference, not a deep copy
|
||||
empty!(copy1)
|
||||
@test !isempty(getTools(store3))
|
||||
@test isempty(getTools(store3)) # mutation propagates
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# 9. Per-store isolation — two stores don't share tools #
|
||||
Reference in New Issue
Block a user