static tool loading

This commit is contained in:
2026-08-15 16:50:28 +07:00
parent b8067c2d33
commit 2543e6cbf1
13 changed files with 864 additions and 542 deletions
+192 -37
View File
@@ -1,35 +1,19 @@
using Test
using Dates
using YiemAgent
using YiemAgent.toolRegistry
using YiemAgent.type
using YiemAgent.agentCore
# Path to the real tools directory
TOOLS_DIR = joinpath(@__DIR__, "..", "src", "tools")
@testset "loadTools with toolStore" begin
@testset "register_all_tools with toolStore" begin
# ------------------------------------------------------------------ #
# 1. loadTools throws on non-existent directory #
# 1. register_all_tools registers all static tools + listTools #
# ------------------------------------------------------------------ #
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() #
# Must run BEFORE any other loadTools call (getTool binding #
# persists in module scope after include()). #
# ------------------------------------------------------------------ #
bad_dir = mktempdir()
write(joinpath(bad_dir, "noTool.jl"), "x = 42\n")
@test_throws ArgumentError loadTools(store, bad_dir)
# ------------------------------------------------------------------ #
# 3. loadTools loads actual tool files from src/tools/ #
# ------------------------------------------------------------------ #
store2 = toolStore(name="test2")
loaded = loadTools(store2, TOOLS_DIR)
loaded = register_all_tools(store)
@test !isempty(loaded)
@test length(loaded) == 4 # 3 files + auto-registered listTools
@test length(loaded) == 4 # getWeather + getTime + writeTool + listTools
names = [k for k in keys(loaded)]
@test "getTime" in names
@@ -38,16 +22,15 @@ TOOLS_DIR = joinpath(@__DIR__, "..", "src", "tools")
@test "listTools" in names
# ------------------------------------------------------------------ #
# 4. loadTools returns tools sorted alphabetically by filename #
# (getTime.jl < getWeather.jl < writeTool.jl) + listTools at end #
# 2. register_all_tools returns tools in registration order #
# ------------------------------------------------------------------ #
@test collect(keys(loaded))[1] == "getTime"
@test collect(keys(loaded))[2] == "getWeather"
@test collect(keys(loaded))[1] == "getWeather"
@test collect(keys(loaded))[2] == "getTime"
@test collect(keys(loaded))[3] == "writeTool"
@test collect(keys(loaded))[4] == "listTools"
# ------------------------------------------------------------------ #
# 5. Verify loaded tool fields are correct #
# 3. Verify loaded tool fields are correct #
# ------------------------------------------------------------------ #
# getTime
time_tool = loaded["getTime"]
@@ -74,7 +57,7 @@ TOOLS_DIR = joinpath(@__DIR__, "..", "src", "tools")
@test "executeCode" in wt.inputSchema["required"]
# ------------------------------------------------------------------ #
# 6. Tool execution returns valid results #
# 4. Tool execution returns valid results #
# ------------------------------------------------------------------ #
sig = nothing
op = x -> x # no-op partial result callback
@@ -98,16 +81,15 @@ TOOLS_DIR = joinpath(@__DIR__, "..", "src", "tools")
# execute getWeather with units
result_w2 = weather.execute("call-4", Dict{String,Any}("city" => "London", "units" => "fahrenheit"), sig, op)
@test occursin("72°F", result_w2.content[1].text)
@test occursin("72\u00b0F", result_w2.content[1].text)
# ------------------------------------------------------------------ #
# 7. getTools / registerTool / clearTools (per-store isolation) #
# 5. getTools / registerTool / clearTools (per-store isolation) #
# ------------------------------------------------------------------ #
store3 = toolStore(name="test3")
registry_tools = getTools(store3)
@test isempty(registry_tools)
# listTool is not auto-registered anymore — each store starts empty
# Register tools manually
registerTool(store3, loaded["getTime"])
registerTool(store3, loaded["getWeather"])
@@ -143,7 +125,7 @@ TOOLS_DIR = joinpath(@__DIR__, "..", "src", "tools")
@test reg["manualTool"].parallelToolExecute == true
# ------------------------------------------------------------------ #
# 8. getTools returns direct reference (mutations affect registry) #
# 6. getTools returns direct reference (mutations affect registry) #
# ------------------------------------------------------------------ #
copy1 = getTools(store3)
copy2 = getTools(store3)
@@ -152,7 +134,7 @@ TOOLS_DIR = joinpath(@__DIR__, "..", "src", "tools")
@test isempty(getTools(store3)) # mutation propagates
# ------------------------------------------------------------------ #
# 9. Per-store isolation — two stores don't share tools #
# 7. Per-store isolation — two stores don't share tools #
# ------------------------------------------------------------------ #
storeA = toolStore(name="isolationA")
storeB = toolStore(name="isolationB")
@@ -175,10 +157,10 @@ end
@testset "listTool" begin
store = toolStore(name="test_list")
loaded = loadTools(store, TOOLS_DIR) # auto-registers getWeather, getTime, writeTool + listTools
register_all_tools(store) # auto-registers getWeather, getTime, writeTool + listTools
# loadTools auto-registers listTool
@test "listTools" in keys(loaded)
# register_all_tools auto-registers listTool
@test "listTools" in keys(store.tools)
# listTool returns an agentTool, not a string or array
list_t = listTool(store)
@@ -199,7 +181,7 @@ end
# Each listTool call creates an independent closure
storeB = toolStore(name="test_listB")
registerTool(storeB, loaded["getWeather"])
registerTool(storeB, store.tools["getWeather"])
list_tB = listTool(storeB)
resultA = list_t.execute("call-3", Dict{String,Any}(), nothing, x -> x)
@@ -210,3 +192,176 @@ end
@test occursin("getTime", resultA.content[1].text)
@test occursin("getTime", resultB.content[1].text) == false # storeB only has getWeather
end
@testset "executePreparedToolCall with static tools" begin
# Tests executePreparedToolCall with statically loaded tools.
# The world-age issue is resolved because tool.execute comes from
# a statically included module, not a dynamically created one.
store = toolStore(name="test_static")
register_all_tools(store)
weather_tool = store.tools["getWeather"]
# Create a preparedToolCall that mimics what prepareToolCall() returns
tool_call = agentToolCall(
"function", "call-static-1", "getWeather",
Dict{String,Any}("city" => "San Francisco")
)
prep = preparedToolCall(
weather_tool, tool_call, Dict{String,Any}("city" => "San Francisco")
)
sig = abortSignal(false)
# This call goes through: executePreparedToolCall -> prep.tool.execute(...)
result = executePreparedToolCall(
prep, sig, x -> nothing
)
@test result isa executedOutcome
@test result.isError == false
@test result.result.content[1] isa textContent
@test occursin("San Francisco", result.result.content[1].text)
end
@testset "executePreparedToolCall with validation (static tools)" begin
# Tests executePreparedToolCall with a tool that has custom validation hooks.
# This exercises the full tool execution path including validation.
store = toolStore(name="test_static_validate")
register_all_tools(store)
time_tool = store.tools["getTime"]
tool_call = agentToolCall(
"function", "call-static-2", "getTime",
Dict{String,Any}("timezone" => "America/New_York")
)
prep = preparedToolCall(
time_tool, tool_call, Dict{String,Any}("timezone" => "America/New_York")
)
sig = abortSignal(false)
result = executePreparedToolCall(
prep, sig, x -> nothing
)
@test result isa executedOutcome
@test result.isError == false
@test result.result.content[1] isa textContent
@test occursin("America/New_York", result.result.content[1].text)
end
@testset "executeToolCallsSequential with static tools (full pipeline)" begin
# Tests the full tool execution pipeline: executeToolCallsSequential
# which calls prepareToolCall -> executePreparedToolCall -> finalizeExecutedToolCall
# with statically loaded tools.
store = toolStore(name="test_full_pipeline")
register_all_tools(store)
# Build agentContext from the store's tools
tools = getTools(store)
ctx = agentContext(
"test system prompt",
agentMessage[],
tools
)
# Create an assistant message containing tool calls
assistant_msg = assistantMessage(
role="assistant",
content=Vector{messageContent}(),
api="openai",
provider="test",
model="test-model",
usage=llmUsage(0, 0),
stopReason="tool_calls",
errorMessage=nothing,
timestamp=now()
)
# Create tool calls for multiple statically loaded tools
tool_calls = [
agentToolCall(
"function", "call-seq-1", "getWeather",
Dict{String,Any}("city" => "Tokyo")
),
agentToolCall(
"function", "call-seq-2", "getTime",
Dict{String,Any}("timezone" => "Europe/London")
),
]
config = agentLoopConfig(
nothing, nothing, "sequential"
)
sig = abortSignal(false)
# Execute the full pipeline
batch = executeToolCallsSequential(
ctx, assistant_msg, tool_calls, config, sig, x -> nothing
)
@test batch.messages isa Vector{toolResultMessage}
@test length(batch.messages) == 2
@test batch.messages[1].toolName == "getWeather"
@test batch.messages[1].isError == false
@test occursin("Tokyo", batch.messages[1].content[1].text)
@test batch.messages[2].toolName == "getTime"
@test batch.messages[2].isError == false
@test occursin("Europe/London", batch.messages[2].content[1].text)
end
@testset "executeToolCallsParallel with static tools (full pipeline)" begin
# Same as above but tests parallel execution path.
store = toolStore(name="test_parallel")
register_all_tools(store)
tools = getTools(store)
ctx = agentContext(
"test system prompt",
agentMessage[],
tools
)
assistant_msg = assistantMessage(
role="assistant",
content=Vector{messageContent}(),
api="openai",
provider="test",
model="test-model",
usage=llmUsage(0, 0),
stopReason="tool_calls",
errorMessage=nothing,
timestamp=now()
)
tool_calls = [
agentToolCall(
"function", "call-par-1", "getWeather",
Dict{String,Any}("city" => "Paris")
),
agentToolCall(
"function", "call-par-2", "getTime",
Dict{String,Any}("city" => "Sydney")
),
]
config = agentLoopConfig(
nothing, nothing, "parallel"
)
sig = abortSignal(false)
batch = executeToolCallsParallel(
ctx, assistant_msg, tool_calls, config, sig, x -> nothing
)
@test batch.messages isa Vector{toolResultMessage}
@test length(batch.messages) == 2
@test batch.messages[1].toolName == "getWeather"
@test batch.messages[1].isError == false
@test batch.messages[2].toolName == "getTime"
@test batch.messages[2].isError == false
end