V0.8.0 verify tool use #43

Merged
ton merged 6 commits from v0.8.0-verify_tool_use into v0.8.0 2026-08-10 13:10:57 +00:00
4 changed files with 29 additions and 40 deletions
Showing only changes of commit c5cb18f0f1 - Show all commits
+1 -1
View File
@@ -13,7 +13,7 @@ module YiemAgent
include("utils.jl")
using .utils
include("tools/registry.jl")
include("toolRegistry.jl")
using .toolRegistry
# include("llmfunction.jl")
+13 -25
View File
@@ -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
View File
@@ -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.
---
+3 -3
View File
@@ -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 #