V0.8.0 verify tool use #43
+67
-34
@@ -8,7 +8,7 @@ This document describes the complete tool lifecycle in the YiemAgent framework,
|
||||
|
||||
1. [Overview](#1-overview)
|
||||
2. [Tool Definition — The `agentTool` Struct](#2-tool-definition--the-agenttool-struct)
|
||||
3. [Tool Registration — The Global Registry](#3-tool-registration--the-global-registry)
|
||||
3. [Tool Registration — Per-Agent Tool Stores](#3-tool-registration--per-agent-tool-stores)
|
||||
4. [The Agent Loop — High-Level Flow](#4-the-agent-loop--high-level-flow)
|
||||
5. [Message Processing Pipeline](#5-message-processing-pipeline)
|
||||
6. [Tool Call Extraction from LLM Response](#6-tool-call-extraction-from-llm-response)
|
||||
@@ -110,66 +110,98 @@ The `terminate` flag is checked at the batch level. See [Section 9](#9-tool-call
|
||||
|
||||
---
|
||||
|
||||
## 3. Tool Registration — The Global Registry
|
||||
## 3. Tool Registration — Per-Agent Tool Stores
|
||||
|
||||
**Source:** `tools/registry.jl`
|
||||
|
||||
### How `loadTools()` 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.
|
||||
|
||||
```julia
|
||||
function loadTools(dir::String)::OrderedDict{String, agentTool}
|
||||
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
|
||||
end
|
||||
```
|
||||
|
||||
**Source:** `tools/registry.jl:95-160`
|
||||
### How `loadTools(store, dir)` Works
|
||||
|
||||
```julia
|
||||
function loadTools(store::ToolStore, dir::String)::OrderedDict{String, agentTool}
|
||||
```
|
||||
|
||||
**Source:** `tools/registry.jl:115-180`
|
||||
|
||||
1. **Scans** `dir` for `.jl` files (excluding files matching `registry` in name)
|
||||
2. **Sorts** filenames alphabetically for deterministic registration order
|
||||
3. **Wraps** each file in a dynamically created submodule:
|
||||
```julia
|
||||
# For "getWeather.jl" → module _tool_getWeather
|
||||
module _tool_getWeather
|
||||
using ..type
|
||||
using Dates, UUIDs, DataStructures, JSON
|
||||
# (file contents here)
|
||||
end
|
||||
```
|
||||
```julia
|
||||
# For "getWeather.jl" → module _tool_getWeather
|
||||
module _tool_getWeather
|
||||
using ..type
|
||||
using Dates, UUIDs, DataStructures, JSON
|
||||
# (file contents here)
|
||||
end
|
||||
```
|
||||
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 `_tool_modules` to prevent GC of closures
|
||||
7. **Registers** the tool in `_registry` and returns an `OrderedDict{String, agentTool}`
|
||||
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}`
|
||||
|
||||
### 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 `_tool_modules` so closures (in `execute`, `validateRequiredArgs`, `prepareArguments`) don't get garbage collected
|
||||
|
||||
### Auto-Registration
|
||||
|
||||
The `_listTool()` is auto-registered in `__init__()` (line 16-18), so `listTools` is always available without explicit loading.
|
||||
- The module reference is kept alive in `store.modules` so closures (in `execute`, `validateRequiredArgs`, `prepareArguments`) don't get garbage collected
|
||||
|
||||
### Registration API
|
||||
|
||||
```julia
|
||||
# Auto-load from directory (returns OrderedDict keyed by tool name)
|
||||
tools = loadTools("src/tools") # OrderedDict{String, agentTool}
|
||||
# Create per-agent stores
|
||||
store1 = ToolStore(name="agent1")
|
||||
store2 = ToolStore(name="agent2")
|
||||
|
||||
# Manual registration (adds to global _registry)
|
||||
registerTool(my_tool)
|
||||
# Load tools into specific stores
|
||||
tools1 = loadTools(store1, "src/tools/weather_tools") # agent1 only
|
||||
tools2 = loadTools(store2, "src/tools/wine_tools") # agent2 only
|
||||
|
||||
# Manual registration (per-store)
|
||||
registerTool(store1, my_tool)
|
||||
|
||||
# Query (returns OrderedDict keyed by tool name, in registration order)
|
||||
all_tools = getTools() # OrderedDict{String, agentTool} — O(1) lookup + deterministic order
|
||||
all_tools = getTools(store1) # OrderedDict{String, agentTool} — O(1) lookup + deterministic order
|
||||
|
||||
# Clear
|
||||
clearTools() # Empties _registry
|
||||
# Clear (per-store)
|
||||
clearTools(store1) # only clears store1
|
||||
```
|
||||
|
||||
**Why `OrderedDict` for `getTools()`?** The internal `_registry` is a `Vector{agentTool}` for ordered iteration (used by `listTools`). `getTools()` builds an `OrderedDict` from `_registry` so callers get:
|
||||
**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:
|
||||
- O(1) lookup by tool name
|
||||
- Deterministic iteration order (registration order: `listTools` auto-registered first, then tools loaded alphabetically by filename)
|
||||
- Deterministic iteration order (registration order)
|
||||
- Consistency with `agentState.tools` (also `OrderedDict{String, agentTool}`)
|
||||
|
||||
### Per-Agent Isolation
|
||||
|
||||
Each `ToolStore` is completely independent — tools registered in one store do not appear in another:
|
||||
|
||||
```julia
|
||||
storeA = ToolStore(name="A")
|
||||
storeB = ToolStore(name="B")
|
||||
|
||||
registerTool(storeA, getTime_tool)
|
||||
registerTool(storeB, getWeather_tool)
|
||||
|
||||
getTools(storeA) # only contains getTime
|
||||
getTools(storeB) # only contains getWeather
|
||||
|
||||
clearTools(storeA) # storeB is unaffected
|
||||
```
|
||||
|
||||
This ensures that `yiemAgent` instances with different `tool_store` references operate with completely isolated tool sets.
|
||||
|
||||
---
|
||||
|
||||
## 4. The Agent Loop — High-Level Flow
|
||||
@@ -185,6 +217,7 @@ yiemAgent struct contains:
|
||||
- inputChannel (Channel, capacity 16) ← user sends messages here via run_agent()
|
||||
- followUpChannel (Channel, capacity 32) ← user sends follow-ups here via follow_up()
|
||||
- outputChannel (Channel, capacity 16) ← agent sends responses here via take_response()
|
||||
- _tool_store (ToolStore) ← per-agent isolated tool registry
|
||||
```
|
||||
|
||||
### Loop States
|
||||
@@ -1083,11 +1116,11 @@ The framework supports tools that modify the tool system itself at runtime.
|
||||
4. Appends `getTool()` returning an `agentTool` struct
|
||||
5. Writes the combined string to `src/tools/<name>.jl`
|
||||
|
||||
### `listTools` — Discover Available Tools
|
||||
### `listTool` — Discover Available Tools
|
||||
|
||||
**Source:** `tools/registry.jl:24-52`
|
||||
**Source:** `tools/registry.jl:54-82`
|
||||
|
||||
Returns all registered 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. Primarily useful for **collision detection** before creating a new tool via `writeTool`.
|
||||
|
||||
### Self-Tooling Workflow
|
||||
|
||||
@@ -1101,7 +1134,7 @@ Returns all registered tools. Primarily useful for **collision detection** befor
|
||||
- executeCode: "query = args[\"query\"]\nresult = search(query)\n..."
|
||||
- (optional) validateCode, prepareCode
|
||||
3. writeTool generates src/tools/searchWine.jl
|
||||
4. Agent restarts (or hot-reloads) → loadTools("src/tools") picks up the new file
|
||||
4. Agent restarts (or hot-reloads) → loadTools(agent._tool_store, "src/tools") picks up the new file
|
||||
5. Agent calls searchWine(query="cabernet")
|
||||
6. Result: "Found 5 cabernet wines..."
|
||||
```
|
||||
@@ -1362,7 +1395,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 `_tool_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 in `store.modules` to prevent garbage collection of closures.
|
||||
|
||||
---
|
||||
|
||||
|
||||
+108
-55
@@ -1,27 +1,57 @@
|
||||
module toolRegistry
|
||||
|
||||
export loadTools, registerTool, getTools, clearTools
|
||||
export ToolStore, loadTools, registerTool, getTools, clearTools, listTool
|
||||
|
||||
using Dates
|
||||
using JSON, DataStructures
|
||||
using ..type
|
||||
|
||||
# Global registry — populated at runtime by loadTools() or registerTool()
|
||||
const _registry = Vector{agentTool}()
|
||||
"""
|
||||
Per-agent isolated tool storage.
|
||||
|
||||
# Module references — kept alive to prevent GC of tool code that closures depend on
|
||||
const _tool_modules = Vector{Module}()
|
||||
Each agent gets its own `ToolStore` so tool registration is independent —
|
||||
`registerTool(store, tool)` only affects that agent's tool set.
|
||||
|
||||
# Auto-register the built-in listTools tool
|
||||
function __init__()
|
||||
registerTool(_listTool())
|
||||
# Fields
|
||||
- `tools::Vector{agentTool}` — ordered tool list (for `listTool` iteration)
|
||||
- `modules::Vector{Module}` — keeps tool submodules alive to prevent GC of closures
|
||||
- `name::String` — identifier for debugging/logs
|
||||
"""
|
||||
struct ToolStore
|
||||
tools::Vector{agentTool}
|
||||
modules::Vector{Module}
|
||||
name::String
|
||||
end
|
||||
|
||||
"""
|
||||
Create a new isolated tool store.
|
||||
|
||||
# Keyword Arguments
|
||||
- `name::String`: Identifier for this store (default: "default")
|
||||
|
||||
# Examples
|
||||
```julia
|
||||
store = ToolStore(name="agent1")
|
||||
tools = loadTools(store, "src/tools")
|
||||
registerTool(store, my_tool)
|
||||
agent = yiemAgent(tools=getTools(store), llmCall=..., _tool_store=store)
|
||||
```
|
||||
"""
|
||||
function ToolStore(; name::String="default")::ToolStore
|
||||
ToolStore(agentTool[], Module[], name)
|
||||
end
|
||||
|
||||
"""
|
||||
List tool definition — lets the agent query available tools for collision detection
|
||||
when creating new tools via writeTool.
|
||||
|
||||
# Arguments
|
||||
- `store::ToolStore`: The tool store to list from
|
||||
|
||||
Each `ToolStore` gets its own `listTool` instance bound to that store,
|
||||
so each agent sees only its own tools.
|
||||
"""
|
||||
function _listTool()::agentTool
|
||||
function listTool(store::ToolStore)::agentTool
|
||||
return agentTool(
|
||||
name = "listTools",
|
||||
label = "List Tools",
|
||||
@@ -32,7 +62,7 @@ function _listTool()::agentTool
|
||||
"required" => Any[]
|
||||
),
|
||||
execute = (toolCallId, args, signal, onPartialResult) -> begin
|
||||
tools = getTools()
|
||||
tools = getTools(store)
|
||||
if isempty(tools)
|
||||
result_text = "No tools registered."
|
||||
else
|
||||
@@ -52,47 +82,37 @@ function _listTool()::agentTool
|
||||
end
|
||||
|
||||
"""
|
||||
Load all tool modules from a directory.
|
||||
Load all tool modules from a directory into a specific ToolStore.
|
||||
|
||||
Scans `dir` for `.jl` files. Each file must define a function named
|
||||
`getTool()::agentTool`. Files are sorted alphabetically so tool
|
||||
`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.
|
||||
|
||||
# Tool file format
|
||||
Each `.jl` file defines one function `getTool()` that returns an `agentTool`.
|
||||
Inside the file you can freely define as many helper functions as you need —
|
||||
they will all be scoped under the tool's submodule.
|
||||
|
||||
```julia
|
||||
# src/tools/getWeather.jl
|
||||
|
||||
# These are namespaced — no collision with getTime.validateRequiredArgs, etc.
|
||||
function validateRequiredArgs(args::Dict{String,Any})::Union{Nothing,String}
|
||||
...
|
||||
end
|
||||
|
||||
function getTool()::agentTool
|
||||
return agentTool(
|
||||
name = "getWeather",
|
||||
...
|
||||
)
|
||||
end
|
||||
```
|
||||
|
||||
# Arguments
|
||||
- `store::ToolStore`: The tool store to register tools into
|
||||
- `dir::String`: Directory path to scan for `.jl` tool files
|
||||
|
||||
# Returns
|
||||
- `Vector{agentTool}`: All loaded tools
|
||||
- `OrderedDict{String, agentTool}`: All loaded tools keyed by name
|
||||
|
||||
# Errors
|
||||
- Throws `ArgumentError` if a tool file does not define a `getTool` function
|
||||
|
||||
# Examples
|
||||
```julia
|
||||
julia> store = ToolStore(name="agent1")
|
||||
julia> tools = loadTools(store, "src/tools")
|
||||
OrderedDict{String, agentTool} with 3 entries:
|
||||
"getWeather" => agentTool(...)
|
||||
"getTime" => agentTool(...)
|
||||
"listTools" => agentTool(...)
|
||||
```
|
||||
"""
|
||||
function loadTools(dir::String)::OrderedDict{String, agentTool}
|
||||
function loadTools(store::ToolStore, dir::String)::OrderedDict{String, agentTool}
|
||||
if !isdir(dir)
|
||||
throw(ArgumentError("Tool directory does not exist: $dir"))
|
||||
end
|
||||
@@ -141,10 +161,10 @@ function loadTools(dir::String)::OrderedDict{String, agentTool}
|
||||
# Keep module reference alive — closures in the agentTool (execute,
|
||||
# validateRequiredArgs, prepareArguments) may reference module-scoped
|
||||
# functions. Without this, GC could collect the module.
|
||||
push!(_tool_modules, mod)
|
||||
push!(_registry, tool)
|
||||
push!(store.modules, mod)
|
||||
push!(store.tools, tool)
|
||||
tools[tool.name] = tool
|
||||
println("[toolRegistry] Loaded tool: $(tool.name) — $(tool.label)")
|
||||
println("[$(store.name)] Loaded tool: $(tool.name) ($(tool.label))")
|
||||
catch e
|
||||
if e isa UndefVarError || occursin("getTool", sprint(showerror, e))
|
||||
throw(ArgumentError(
|
||||
@@ -160,42 +180,75 @@ function loadTools(dir::String)::OrderedDict{String, agentTool}
|
||||
end
|
||||
|
||||
"""
|
||||
Register a single agentTool into the global registry.
|
||||
Register a single agentTool into a specific ToolStore.
|
||||
|
||||
# Arguments
|
||||
- `store::ToolStore`: The tool store to register into
|
||||
- `tool::agentTool`: The tool to register
|
||||
|
||||
# Returns
|
||||
- `Vector{agentTool}`: Updated registry
|
||||
- `Vector{agentTool}`: Updated tool list for this store
|
||||
|
||||
# Examples
|
||||
```julia
|
||||
julia> store = ToolStore(name="agent1")
|
||||
julia> registerTool(store, my_tool)
|
||||
[toolRegistry:agent1] Registered tool: my_tool
|
||||
```
|
||||
"""
|
||||
function registerTool(tool::agentTool)::Vector{agentTool}
|
||||
push!(_registry, tool)
|
||||
println("[toolRegistry] Registered tool: $(tool.name)")
|
||||
return _registry
|
||||
function registerTool(store::ToolStore, tool::agentTool)::Vector{agentTool}
|
||||
push!(store.tools, tool)
|
||||
println("[$(store.name)] Registered tool: $(tool.name)")
|
||||
return store.tools
|
||||
end
|
||||
|
||||
"""
|
||||
Get all registered tools as an `OrderedDict{String, agentTool}` keyed by tool name.
|
||||
Get all registered tools from a specific ToolStore as an
|
||||
`OrderedDict{String, agentTool}` keyed by tool name.
|
||||
|
||||
The internal `_registry` is a `Vector` for ordered iteration (used by `listTools`).
|
||||
This function builds an `OrderedDict` from `_registry` so callers get:
|
||||
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: alphabetical by filename)
|
||||
- Deterministic iteration order (registration order)
|
||||
- Consistency with `agentState.tools` (also `OrderedDict{String, agentTool}`)
|
||||
|
||||
# Arguments
|
||||
- `store::ToolStore`: The tool store to query
|
||||
|
||||
# Returns
|
||||
- `OrderedDict{String, agentTool}`: Copy of the registry keyed by tool name, in registration order
|
||||
- `OrderedDict{String, agentTool}`: Tools keyed by name, in registration order
|
||||
|
||||
# Examples
|
||||
```julia
|
||||
julia> getTools(store)
|
||||
OrderedDict{String, agentTool} with 3 entries:
|
||||
"listTools" => agentTool(...)
|
||||
"getWeather" => agentTool(...)
|
||||
"getTime" => agentTool(...)
|
||||
```
|
||||
"""
|
||||
function getTools()::OrderedDict{String, agentTool}
|
||||
return OrderedDict{String, agentTool}(t.name => t for t in _registry)
|
||||
function getTools(store::ToolStore)::OrderedDict{String, agentTool}
|
||||
return OrderedDict{String, agentTool}(t.name => t for t in store.tools)
|
||||
end
|
||||
|
||||
"""
|
||||
Clear all registered tools from the global registry.
|
||||
Clear all registered tools from a specific ToolStore.
|
||||
|
||||
# Arguments
|
||||
- `store::ToolStore`: The tool store to clear
|
||||
|
||||
# Returns
|
||||
- `nothing`
|
||||
|
||||
# Examples
|
||||
```julia
|
||||
julia> clearTools(store)
|
||||
[toolRegistry:agent1] Registry cleared
|
||||
```
|
||||
"""
|
||||
function clearTools()::Nothing
|
||||
empty!(_registry)
|
||||
println("[toolRegistry] Registry cleared")
|
||||
function clearTools(store::ToolStore)::Nothing
|
||||
empty!(store.tools)
|
||||
println("[$(store.name)] Registry cleared")
|
||||
return nothing
|
||||
end
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ The agent can use this tool when it encounters a task that no existing tool
|
||||
can handle. Provide the tool's name, label, description, inputSchema, and
|
||||
execute logic as Julia code. The tool is written to `src/tools/<name>.jl`.
|
||||
|
||||
After calling this tool, restart the agent so `loadTools("src/tools")` picks
|
||||
After calling this tool, restart the agent so `loadTools(agent._tool_store, "src/tools")` picks
|
||||
up the new file. The new tool is immediately available.
|
||||
|
||||
# Example
|
||||
@@ -250,13 +250,13 @@ function getTool()::agentTool
|
||||
|
||||
tool_code = join(parts)
|
||||
|
||||
# Write the file — tool is loaded on next agent restart via loadTools()
|
||||
# Write the file — tool is loaded on next agent restart via loadTools(store, "src/tools")
|
||||
write(filepath, tool_code)
|
||||
|
||||
onPartialResult(Dict("status" => "Done"))
|
||||
|
||||
return agentToolResult(
|
||||
[textContent("Tool '$(tool_name)' written to $filepath. Restart the agent so loadTools() picks it up, then call listTools to verify.")],
|
||||
[textContent("Tool '$(tool_name)' written to $filepath. Restart the agent so loadTools(agent._tool_store, \"src/tools\") picks it up, then call listTools to verify.")],
|
||||
Dict{Any,Any}(
|
||||
"file" => filepath,
|
||||
"name" => tool_name,
|
||||
|
||||
+11
-6
@@ -567,9 +567,10 @@ mutable struct yiemAgent <: agent # High-level agent wrapper
|
||||
# prepareNextTurnWithContext::Union{Function, Nothing} # Same but receives context
|
||||
sessionId::Union{String, Nothing} # Optional session identifier
|
||||
maxRetryDelayMs::Union{Int64, Nothing} # Maximum delay between retries (ms)
|
||||
parallelToolExecute::Bool # Default: false
|
||||
agentEventSink::Function # agent emits its status via this function
|
||||
end
|
||||
parallelToolExecute::Bool # Default: false
|
||||
agentEventSink::Function # agent emits its status via this function
|
||||
_tool_store::Any # Reference to the ToolStore for runtime registration
|
||||
end
|
||||
|
||||
"""
|
||||
Create a new yiemAgent instance with a background loop task.
|
||||
@@ -593,15 +594,17 @@ on `inputChannel` and `followUpChannel` channels concurrently.
|
||||
- `maxRetryDelayMs::Union{Int64, Nothing}`: Maximum delay between retries in milliseconds (default: `nothing`)
|
||||
- `parallelToolExecute::Bool`: Run tool calls in parallel (default: `false`)
|
||||
- `agentEventSink::Function`: Callback to receive agent events
|
||||
- `tool_store::Union{Any, Nothing}`: ToolStore for runtime tool registration (default: `nothing`)
|
||||
|
||||
# Returns
|
||||
- A new `yiemAgent` instance with an active background task
|
||||
|
||||
# Examples
|
||||
```julia
|
||||
julia> tools = loadTools("src/tools")
|
||||
julia> agent = yiemAgent(systemPrompt="You are a helpful assistant", model=my_model, tools=tools, llmCall=...)
|
||||
yiemAgent(agentState(...), Channel(...), Channel(...), Channel(...), ..., ...)
|
||||
julia> store = ToolStore(name="agent1")
|
||||
julia> tools = loadTools(store, "src/tools")
|
||||
julia> agent = yiemAgent(systemPrompt="You are a helpful assistant", model=my_model, tools=tools, llmCall=..., tool_store=store)
|
||||
yiemAgent(agentState(...), Channel(...), Channel(...), Channel(...), ..., store)
|
||||
"""
|
||||
function yiemAgent(
|
||||
; systemPrompt::String="You are helpful assistant.",
|
||||
@@ -619,6 +622,7 @@ function yiemAgent(
|
||||
maxRetryDelayMs::Union{Int64, Nothing}=nothing,
|
||||
parallelToolExecute::Bool=false,
|
||||
agentEventSink::Function,
|
||||
tool_store::Union{Any, Nothing}=nothing,
|
||||
)
|
||||
# Create channels: input (user -> agent), followUp (async queue), output (agent -> user)
|
||||
inputChannel = Channel(16)
|
||||
@@ -643,6 +647,7 @@ function yiemAgent(
|
||||
maxRetryDelayMs,
|
||||
parallelToolExecute,
|
||||
agentEventSink,
|
||||
tool_store,
|
||||
)
|
||||
|
||||
# Spawn the background loop and attach it
|
||||
|
||||
+53
-21
@@ -6,12 +6,13 @@ using YiemAgent.type
|
||||
# Path to the real tools directory
|
||||
TOOLS_DIR = joinpath(@__DIR__, "..", "src", "tools")
|
||||
|
||||
@testset "loadTools" begin
|
||||
@testset "loadTools with ToolStore" begin
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# 1. loadTools throws on non-existent directory #
|
||||
# ------------------------------------------------------------------ #
|
||||
@test_throws ArgumentError loadTools("/nonexistent/dir/that/does/not/exist")
|
||||
store = ToolStore(name="test1")
|
||||
@test_throws ArgumentError loadTools(store, "/nonexistent/dir/that/does/not/exist")
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# 2. loadTools throws if a .jl file does not define getTool() #
|
||||
@@ -20,12 +21,13 @@ TOOLS_DIR = joinpath(@__DIR__, "..", "src", "tools")
|
||||
# ------------------------------------------------------------------ #
|
||||
bad_dir = mktempdir()
|
||||
write(joinpath(bad_dir, "noTool.jl"), "x = 42\n")
|
||||
@test_throws ArgumentError loadTools(bad_dir)
|
||||
@test_throws ArgumentError loadTools(store, bad_dir)
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# 3. loadTools loads actual tool files from src/tools/ #
|
||||
# ------------------------------------------------------------------ #
|
||||
loaded = loadTools(TOOLS_DIR)
|
||||
store2 = ToolStore(name="test2")
|
||||
loaded = loadTools(store2, TOOLS_DIR)
|
||||
@test !isempty(loaded)
|
||||
@test length(loaded) == 3
|
||||
|
||||
@@ -98,20 +100,29 @@ TOOLS_DIR = joinpath(@__DIR__, "..", "src", "tools")
|
||||
@test occursin("72°F", result_w2.content[1].text)
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# 7. getTools / registerTool / clearTools #
|
||||
# 7. getTools / registerTool / clearTools (per-store isolation) #
|
||||
# ------------------------------------------------------------------ #
|
||||
registry_tools = getTools()
|
||||
@test !isempty(registry_tools)
|
||||
@test "getTime" in keys(registry_tools)
|
||||
@test "getWeather" in keys(registry_tools)
|
||||
# listTools is auto-registered via __init__() → first key, then tools loaded alphabetically
|
||||
@test collect(keys(registry_tools))[1] == "listTools"
|
||||
@test collect(keys(registry_tools))[2] == "getTime"
|
||||
@test collect(keys(registry_tools))[3] == "getWeather"
|
||||
@test collect(keys(registry_tools))[4] == "writeTool"
|
||||
store3 = ToolStore(name="test3")
|
||||
registry_tools = getTools(store3)
|
||||
@test isempty(registry_tools)
|
||||
|
||||
clearTools()
|
||||
@test isempty(getTools())
|
||||
# listTool is not auto-registered anymore — each store starts empty
|
||||
# Register tools manually
|
||||
registerTool(store3, loaded["getTime"])
|
||||
registerTool(store3, loaded["getWeather"])
|
||||
registerTool(store3, loaded["writeTool"])
|
||||
|
||||
reg = getTools(store3)
|
||||
@test !isempty(reg)
|
||||
@test "getTime" in keys(reg)
|
||||
@test "getWeather" in keys(reg)
|
||||
@test "writeTool" in keys(reg)
|
||||
@test collect(keys(reg))[1] == "getTime"
|
||||
@test collect(keys(reg))[2] == "getWeather"
|
||||
@test collect(keys(reg))[3] == "writeTool"
|
||||
|
||||
clearTools(store3)
|
||||
@test isempty(getTools(store3))
|
||||
|
||||
test_tool = agentTool(
|
||||
name = "manualTool",
|
||||
@@ -124,8 +135,8 @@ TOOLS_DIR = joinpath(@__DIR__, "..", "src", "tools")
|
||||
validateRequiredArgs = nothing,
|
||||
parallelToolExecute = true
|
||||
)
|
||||
registerTool(test_tool)
|
||||
reg = getTools()
|
||||
registerTool(store3, test_tool)
|
||||
reg = getTools(store3)
|
||||
@test haskey(reg, "manualTool")
|
||||
@test length(reg) == 1
|
||||
@test reg["manualTool"].parallelToolExecute == true
|
||||
@@ -133,9 +144,30 @@ TOOLS_DIR = joinpath(@__DIR__, "..", "src", "tools")
|
||||
# ------------------------------------------------------------------ #
|
||||
# 8. getTools returns deep copy (mutations don't affect registry) #
|
||||
# ------------------------------------------------------------------ #
|
||||
copy1 = getTools()
|
||||
copy2 = getTools()
|
||||
copy1 = getTools(store3)
|
||||
copy2 = getTools(store3)
|
||||
@test copy1 !== copy2
|
||||
empty!(copy1)
|
||||
@test !isempty(getTools())
|
||||
@test !isempty(getTools(store3))
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# 9. Per-store isolation — two stores don't share tools #
|
||||
# ------------------------------------------------------------------ #
|
||||
storeA = ToolStore(name="isolationA")
|
||||
storeB = ToolStore(name="isolationB")
|
||||
|
||||
registerTool(storeA, loaded["getTime"])
|
||||
registerTool(storeB, loaded["getWeather"])
|
||||
|
||||
regA = getTools(storeA)
|
||||
regB = getTools(storeB)
|
||||
|
||||
@test "getTime" in keys(regA)
|
||||
@test "getWeather" ∉ keys(regA)
|
||||
@test "getWeather" in keys(regB)
|
||||
@test "getTime" ∉ keys(regB)
|
||||
|
||||
clearTools(storeA)
|
||||
@test isempty(getTools(storeA))
|
||||
@test !isempty(getTools(storeB)) # storeB unaffected
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user