This commit is contained in:
2026-08-10 14:55:09 +07:00
parent 1b69f69c7d
commit c78f4b023d
5 changed files with 242 additions and 119 deletions
+67 -34
View File
@@ -8,7 +8,7 @@ This document describes the complete tool lifecycle in the YiemAgent framework,
1. [Overview](#1-overview) 1. [Overview](#1-overview)
2. [Tool Definition — The `agentTool` Struct](#2-tool-definition--the-agenttool-struct) 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) 4. [The Agent Loop — High-Level Flow](#4-the-agent-loop--high-level-flow)
5. [Message Processing Pipeline](#5-message-processing-pipeline) 5. [Message Processing Pipeline](#5-message-processing-pipeline)
6. [Tool Call Extraction from LLM Response](#6-tool-call-extraction-from-llm-response) 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` **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 ```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) 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
3. **Wraps** each file in a dynamically created submodule: 3. **Wraps** each file in a dynamically created submodule:
```julia ```julia
# For "getWeather.jl" → module _tool_getWeather # For "getWeather.jl" → module _tool_getWeather
module _tool_getWeather module _tool_getWeather
using ..type using ..type
using Dates, UUIDs, DataStructures, JSON using Dates, UUIDs, DataStructures, JSON
# (file contents here) # (file contents here)
end end
``` ```
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. **Stores** the module reference in `_tool_modules` to prevent GC of closures 6. **Stores** the module reference in `store.modules` to prevent GC of closures
7. **Registers** the tool in `_registry` and returns an `OrderedDict{String, agentTool}` 7. **Registers** the tool in `store.tools` and returns an `OrderedDict{String, agentTool}`
### Why Submodules? ### Why Submodules?
Each tool file is loaded into its own **namespaced submodule**. This means: 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` - `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` - 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 - The module reference is kept alive in `store.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.
### Registration API ### Registration API
```julia ```julia
# Auto-load from directory (returns OrderedDict keyed by tool name) # Create per-agent stores
tools = loadTools("src/tools") # OrderedDict{String, agentTool} store1 = ToolStore(name="agent1")
store2 = ToolStore(name="agent2")
# Manual registration (adds to global _registry) # Load tools into specific stores
registerTool(my_tool) 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) # 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 # Clear (per-store)
clearTools() # Empties _registry 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 - 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}`) - 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 ## 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() - 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
``` ```
### Loop States ### 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 4. Appends `getTool()` returning an `agentTool` struct
5. Writes the combined string to `src/tools/<name>.jl` 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 ### 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..." - executeCode: "query = args[\"query\"]\nresult = search(query)\n..."
- (optional) validateCode, prepareCode - (optional) validateCode, prepareCode
3. writeTool generates src/tools/searchWine.jl 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") 5. Agent calls searchWine(query="cabernet")
6. Result: "Found 5 cabernet wines..." 6. Result: "Found 5 cabernet wines..."
``` ```
@@ -1362,7 +1395,7 @@ module _tool_myTool
end 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
View File
@@ -1,27 +1,57 @@
module toolRegistry module toolRegistry
export loadTools, registerTool, getTools, clearTools export ToolStore, loadTools, registerTool, getTools, clearTools, listTool
using Dates using Dates
using JSON, DataStructures using JSON, DataStructures
using ..type 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 Each agent gets its own `ToolStore` so tool registration is independent —
const _tool_modules = Vector{Module}() `registerTool(store, tool)` only affects that agent's tool set.
# Auto-register the built-in listTools tool # Fields
function __init__() - `tools::Vector{agentTool}` — ordered tool list (for `listTool` iteration)
registerTool(_listTool()) - `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 end
""" """
List tool definition — lets the agent query available tools for collision detection List tool definition — lets the agent query available tools for collision detection
when creating new tools via writeTool. 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( return agentTool(
name = "listTools", name = "listTools",
label = "List Tools", label = "List Tools",
@@ -32,7 +62,7 @@ function _listTool()::agentTool
"required" => Any[] "required" => Any[]
), ),
execute = (toolCallId, args, signal, onPartialResult) -> begin execute = (toolCallId, args, signal, onPartialResult) -> begin
tools = getTools() tools = getTools(store)
if isempty(tools) if isempty(tools)
result_text = "No tools registered." result_text = "No tools registered."
else else
@@ -52,47 +82,37 @@ function _listTool()::agentTool
end 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 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. registration order is deterministic.
Each `.jl` file is loaded into its own **submodule** so that all functions Each `.jl` file is loaded into its own **submodule** so that all functions
defined in the file (`validateRequiredArgs`, `prepareArguments`, `executeTool`, defined in the file (`validateRequiredArgs`, `prepareArguments`, `executeTool`,
and any helper functions) are namespaced and never collide with other tools. 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 # Arguments
- `store::ToolStore`: The tool store to register tools into
- `dir::String`: Directory path to scan for `.jl` tool files - `dir::String`: Directory path to scan for `.jl` tool files
# Returns # Returns
- `Vector{agentTool}`: All loaded tools - `OrderedDict{String, agentTool}`: All loaded tools keyed by name
# Errors # Errors
- Throws `ArgumentError` if a tool file does not define a `getTool` function - 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) if !isdir(dir)
throw(ArgumentError("Tool directory does not exist: $dir")) throw(ArgumentError("Tool directory does not exist: $dir"))
end end
@@ -141,10 +161,10 @@ function loadTools(dir::String)::OrderedDict{String, agentTool}
# Keep module reference alive — closures in the agentTool (execute, # Keep module reference alive — closures in the agentTool (execute,
# validateRequiredArgs, prepareArguments) may reference module-scoped # validateRequiredArgs, prepareArguments) may reference module-scoped
# functions. Without this, GC could collect the module. # functions. Without this, GC could collect the module.
push!(_tool_modules, mod) push!(store.modules, mod)
push!(_registry, tool) push!(store.tools, tool)
tools[tool.name] = tool tools[tool.name] = tool
println("[toolRegistry] Loaded tool: $(tool.name) $(tool.label)") println("[$(store.name)] Loaded tool: $(tool.name) ($(tool.label))")
catch e catch e
if e isa UndefVarError || occursin("getTool", sprint(showerror, e)) if e isa UndefVarError || occursin("getTool", sprint(showerror, e))
throw(ArgumentError( throw(ArgumentError(
@@ -160,42 +180,75 @@ function loadTools(dir::String)::OrderedDict{String, agentTool}
end end
""" """
Register a single agentTool into the global registry. Register a single agentTool into a specific ToolStore.
# Arguments # Arguments
- `store::ToolStore`: The tool store to register into
- `tool::agentTool`: The tool to register - `tool::agentTool`: The tool to register
# Returns # 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} function registerTool(store::ToolStore, tool::agentTool)::Vector{agentTool}
push!(_registry, tool) push!(store.tools, tool)
println("[toolRegistry] Registered tool: $(tool.name)") println("[$(store.name)] Registered tool: $(tool.name)")
return _registry return store.tools
end 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`). The internal vector is for ordered iteration (used by `listTool`).
This function builds an `OrderedDict` from `_registry` so callers get: This function builds an `OrderedDict` so callers get:
- O(1) lookup by name - 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}`) - Consistency with `agentState.tools` (also `OrderedDict{String, agentTool}`)
# Arguments
- `store::ToolStore`: The tool store to query
# Returns # 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} function getTools(store::ToolStore)::OrderedDict{String, agentTool}
return OrderedDict{String, agentTool}(t.name => t for t in _registry) return OrderedDict{String, agentTool}(t.name => t for t in store.tools)
end 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 function clearTools(store::ToolStore)::Nothing
empty!(_registry) empty!(store.tools)
println("[toolRegistry] Registry cleared") println("[$(store.name)] Registry cleared")
return nothing return nothing
end end
+3 -3
View File
@@ -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 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`. 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. up the new file. The new tool is immediately available.
# Example # Example
@@ -250,13 +250,13 @@ function getTool()::agentTool
tool_code = join(parts) 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) write(filepath, tool_code)
onPartialResult(Dict("status" => "Done")) onPartialResult(Dict("status" => "Done"))
return agentToolResult( 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}( Dict{Any,Any}(
"file" => filepath, "file" => filepath,
"name" => tool_name, "name" => tool_name,
+11 -6
View File
@@ -567,9 +567,10 @@ mutable struct yiemAgent <: agent # High-level agent wrapper
# prepareNextTurnWithContext::Union{Function, Nothing} # Same but receives context # prepareNextTurnWithContext::Union{Function, Nothing} # Same but receives context
sessionId::Union{String, Nothing} # Optional session identifier sessionId::Union{String, Nothing} # Optional session identifier
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
end _tool_store::Any # Reference to the ToolStore for runtime registration
end
""" """
Create a new yiemAgent instance with a background loop task. 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`) - `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`)
# 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> tools = loadTools("src/tools") julia> store = ToolStore(name="agent1")
julia> agent = yiemAgent(systemPrompt="You are a helpful assistant", model=my_model, tools=tools, llmCall=...) julia> tools = loadTools(store, "src/tools")
yiemAgent(agentState(...), Channel(...), Channel(...), Channel(...), ..., ...) 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( function yiemAgent(
; systemPrompt::String="You are helpful assistant.", ; systemPrompt::String="You are helpful assistant.",
@@ -619,6 +622,7 @@ function yiemAgent(
maxRetryDelayMs::Union{Int64, Nothing}=nothing, maxRetryDelayMs::Union{Int64, Nothing}=nothing,
parallelToolExecute::Bool=false, parallelToolExecute::Bool=false,
agentEventSink::Function, agentEventSink::Function,
tool_store::Union{Any, Nothing}=nothing,
) )
# Create channels: input (user -> agent), followUp (async queue), output (agent -> user) # Create channels: input (user -> agent), followUp (async queue), output (agent -> user)
inputChannel = Channel(16) inputChannel = Channel(16)
@@ -643,6 +647,7 @@ function yiemAgent(
maxRetryDelayMs, maxRetryDelayMs,
parallelToolExecute, parallelToolExecute,
agentEventSink, agentEventSink,
tool_store,
) )
# Spawn the background loop and attach it # Spawn the background loop and attach it
+53 -21
View File
@@ -6,12 +6,13 @@ 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" begin @testset "loadTools with ToolStore" begin
# ------------------------------------------------------------------ # # ------------------------------------------------------------------ #
# 1. loadTools throws on non-existent directory # # 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() # # 2. loadTools throws if a .jl file does not define getTool() #
@@ -20,12 +21,13 @@ TOOLS_DIR = joinpath(@__DIR__, "..", "src", "tools")
# ------------------------------------------------------------------ # # ------------------------------------------------------------------ #
bad_dir = mktempdir() bad_dir = mktempdir()
write(joinpath(bad_dir, "noTool.jl"), "x = 42\n") 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/ # # 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 !isempty(loaded)
@test length(loaded) == 3 @test length(loaded) == 3
@@ -98,20 +100,29 @@ TOOLS_DIR = joinpath(@__DIR__, "..", "src", "tools")
@test occursin("72°F", result_w2.content[1].text) @test occursin("72°F", result_w2.content[1].text)
# ------------------------------------------------------------------ # # ------------------------------------------------------------------ #
# 7. getTools / registerTool / clearTools # # 7. getTools / registerTool / clearTools (per-store isolation) #
# ------------------------------------------------------------------ # # ------------------------------------------------------------------ #
registry_tools = getTools() store3 = ToolStore(name="test3")
@test !isempty(registry_tools) registry_tools = getTools(store3)
@test "getTime" in keys(registry_tools) @test isempty(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"
clearTools() # listTool is not auto-registered anymore — each store starts empty
@test isempty(getTools()) # 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( test_tool = agentTool(
name = "manualTool", name = "manualTool",
@@ -124,8 +135,8 @@ TOOLS_DIR = joinpath(@__DIR__, "..", "src", "tools")
validateRequiredArgs = nothing, validateRequiredArgs = nothing,
parallelToolExecute = true parallelToolExecute = true
) )
registerTool(test_tool) registerTool(store3, test_tool)
reg = getTools() reg = getTools(store3)
@test haskey(reg, "manualTool") @test haskey(reg, "manualTool")
@test length(reg) == 1 @test length(reg) == 1
@test reg["manualTool"].parallelToolExecute == true @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) # # 8. getTools returns deep copy (mutations don't affect registry) #
# ------------------------------------------------------------------ # # ------------------------------------------------------------------ #
copy1 = getTools() copy1 = getTools(store3)
copy2 = getTools() copy2 = getTools(store3)
@test copy1 !== copy2 @test copy1 !== copy2
empty!(copy1) 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 end