This commit is contained in:
2026-08-10 20:37:28 +07:00
parent c13aeb3a74
commit ed91260468
4 changed files with 49 additions and 49 deletions
+23 -23
View File
@@ -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,14 +9,14 @@ 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
@@ -29,14 +29,14 @@ Create a new isolated tool store.
# Examples # Examples
```julia ```julia
store = ToolStore(name="agent1") store = toolStore(name="agent1")
tools = loadTools(store, "src/tools") tools = loadTools(store, "src/tools")
registerTool(store, my_tool) registerTool(store, my_tool)
agent = yiemAgent(tools=getTools(store), llmCall=..., _tool_store=store) 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
""" """
@@ -44,12 +44,12 @@ List tool definition — lets the agent query available tools for collision dete
when creating new tools via writeTool. when creating new tools via writeTool.
# Arguments # Arguments
- `store::ToolStore`: The tool store to list from - `store::toolStore`: The tool store to list from
Each `ToolStore` gets its own `listTool` instance bound to that store, Each `toolStore` gets its own `listTool` instance bound to that store,
so each agent sees only its own tools. so each agent sees only its own tools.
""" """
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,7 +80,7 @@ function listTool(store::ToolStore)::agentTool
end end
""" """
Load all tool modules from a directory into a specific ToolStore. 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
@@ -91,7 +91,7 @@ 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.
# Arguments # Arguments
- `store::ToolStore`: The tool store to register tools into - `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
@@ -102,7 +102,7 @@ and any helper functions) are namespaced and never collide with other tools.
# 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")
OrderedDict{String, agentTool} with 3 entries: OrderedDict{String, agentTool} with 3 entries:
"getWeather" => agentTool(...) "getWeather" => agentTool(...)
@@ -110,7 +110,7 @@ OrderedDict{String, agentTool} with 3 entries:
"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
@@ -172,10 +172,10 @@ function loadTools(store::ToolStore, dir::String)::OrderedDict{String, agentTool
end end
""" """
Register a single agentTool into a specific ToolStore. Register a single agentTool into a specific toolStore.
# Arguments # Arguments
- `store::ToolStore`: The tool store to register into - `store::toolStore`: The tool store to register into
- `tool::agentTool`: The tool to register - `tool::agentTool`: The tool to register
# Returns # Returns
@@ -183,25 +183,25 @@ Register a single agentTool into a specific ToolStore.
# Examples # Examples
```julia ```julia
julia> store = ToolStore(name="agent1") julia> store = toolStore(name="agent1")
julia> registerTool(store, my_tool) julia> registerTool(store, my_tool)
[toolRegistry:agent1] Registered tool: my_tool [toolRegistry:agent1] Registered tool: my_tool
``` ```
""" """
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. Get the registered tools from a specific toolStore.
Returns the internal `OrderedDict` directly — O(1) lookup by name, Returns the internal `OrderedDict` directly — O(1) lookup by name,
ordered iteration preserving registration order. ordered iteration preserving registration order.
# Arguments # Arguments
- `store::ToolStore`: The tool store to query - `store::toolStore`: The 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
@@ -215,15 +215,15 @@ OrderedDict{String, agentTool} with 3 entries:
"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. Clear all registered tools from a specific toolStore.
# Arguments # Arguments
- `store::ToolStore`: The tool store to clear - `store::toolStore`: The tool store to clear
# Returns # Returns
- `nothing` - `nothing`
@@ -234,7 +234,7 @@ julia> clearTools(store)
[toolRegistry:agent1] Registry cleared [toolRegistry:agent1] Registry cleared
``` ```
""" """
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
+17 -17
View File
@@ -52,12 +52,12 @@ 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`.
```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
``` ```
@@ -84,8 +84,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
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,7 +102,7 @@ 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)
@@ -124,7 +124,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 +278,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,7 +294,7 @@ 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:113-172`
@@ -325,8 +325,8 @@ 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
tools1 = loadTools(store1, "src/tools/weather_tools") # agent1 only tools1 = loadTools(store1, "src/tools/weather_tools") # agent1 only
@@ -350,11 +350,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 +382,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
@@ -1284,7 +1284,7 @@ The framework supports tools that modify the tool system itself at runtime.
**Source:** `toolRegistry.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`. 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
+3 -3
View File
@@ -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)
+6 -6
View File
@@ -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,7 +26,7 @@ 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) == 3
@@ -102,7 +102,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 +153,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"])