update
This commit is contained in:
+13
-7
@@ -310,13 +310,13 @@ function _agentLoop(agent::yiemAgent)
|
|||||||
else # _processMessage() done and no followUp message.
|
else # _processMessage() done and no followUp message.
|
||||||
agent.agentEventSink("_agentLoop 4 agent._state.messages length $(length(agent._state.messages))")
|
agent.agentEventSink("_agentLoop 4 agent._state.messages length $(length(agent._state.messages))")
|
||||||
result = deepcopy(agent._state.messages[end])
|
result = deepcopy(agent._state.messages[end])
|
||||||
|
agent.agentEventSink("_agentLoop 4-1 ")
|
||||||
# filter out reasoningContent in-place
|
# filter out reasoningContent in-place
|
||||||
filter!(c -> !(c isa reasoningContent), result.content)
|
filter!(c -> !(c isa reasoningContent), result.content)
|
||||||
|
agent.agentEventSink("_agentLoop 5 ")
|
||||||
# format output
|
# format output
|
||||||
respondToUI = _assistantMessageToOpenAI(result)
|
respondToUI = _assistantMessageToOpenAI(result)
|
||||||
|
agent.agentEventSink("_agentLoop 6 ")
|
||||||
put!(agent.outputChannel, respondToUI)
|
put!(agent.outputChannel, respondToUI)
|
||||||
if !isempty(result.content) && result.content[1] isa textContent
|
if !isempty(result.content) && result.content[1] isa textContent
|
||||||
agent.agentEventSink(result.content[1].text)
|
agent.agentEventSink(result.content[1].text)
|
||||||
@@ -376,6 +376,13 @@ function _agentLoop(agent::yiemAgent)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
catch e
|
catch e
|
||||||
|
bt = catch_backtrace()
|
||||||
|
errMsg = sprint() do io
|
||||||
|
showerror(io, e, bt)
|
||||||
|
println(io)
|
||||||
|
end
|
||||||
|
agentEventSink(errMsg)
|
||||||
|
|
||||||
# On any error, send error response and exit the loop
|
# On any error, send error response and exit the loop
|
||||||
@error "Agent loop failed" error=e
|
@error "Agent loop failed" error=e
|
||||||
end
|
end
|
||||||
@@ -1085,13 +1092,12 @@ function executePreparedToolCall(
|
|||||||
llmCall::Union{Any,Nothing}=nothing,
|
llmCall::Union{Any,Nothing}=nothing,
|
||||||
)::executedOutcome
|
)::executedOutcome
|
||||||
agentEventSink("executePreparedToolCall 1")
|
agentEventSink("executePreparedToolCall 1")
|
||||||
agentEventSink("executePreparedToolCall 2")
|
|
||||||
agentEventSink("executePreparedToolCall 3")
|
|
||||||
|
|
||||||
try
|
try #WORKING
|
||||||
result = prep.tool.execute(prep.toolCall.id, prep.args, signal, agentEventSink, llmCall)
|
result = prep.tool.execute(prep.toolCall.id, prep.args, signal, agentEventSink, llmCall)
|
||||||
|
agentEventSink("executePreparedToolCall 2")
|
||||||
agentEventSink(result.content[1].text)
|
agentEventSink(result.content[1].text)
|
||||||
agentEventSink("executePreparedToolCall 4")
|
agentEventSink("executePreparedToolCall 3")
|
||||||
return executedOutcome(result, false)
|
return executedOutcome(result, false)
|
||||||
catch e
|
catch e
|
||||||
bt = catch_backtrace()
|
bt = catch_backtrace()
|
||||||
|
|||||||
+2
-2
@@ -100,7 +100,7 @@ function _wrap_mcp_tool(mcpserver, tool_def::Dict{String,Any})::agentTool
|
|||||||
inputSchema=params,
|
inputSchema=params,
|
||||||
execute=(toolCallId::String, args::Dict{String,Any},
|
execute=(toolCallId::String, args::Dict{String,Any},
|
||||||
signal::Union{Nothing,abortSignal},
|
signal::Union{Nothing,abortSignal},
|
||||||
onPartialResult::Function,
|
agentEventSink,
|
||||||
llmCall=nothing) -> begin
|
llmCall=nothing) -> begin
|
||||||
try
|
try
|
||||||
response = mcpserver("tools/call", name, args)
|
response = mcpserver("tools/call", name, args)
|
||||||
@@ -256,7 +256,7 @@ function listTool(store::toolStore, mcpserver)::agentTool
|
|||||||
),
|
),
|
||||||
execute=(toolCallId::String, args::Dict{String,Any},
|
execute=(toolCallId::String, args::Dict{String,Any},
|
||||||
signal::Union{Nothing,abortSignal},
|
signal::Union{Nothing,abortSignal},
|
||||||
onPartialResult::Function, llmCall=nothing) -> begin
|
agentEventSink, llmCall=nothing) -> begin
|
||||||
# Discover and register MCP tools (idempotent — skips already registered)
|
# Discover and register MCP tools (idempotent — skips already registered)
|
||||||
new_count, tool_list = _register_mcp_tools(mcpserver, store)
|
new_count, tool_list = _register_mcp_tools(mcpserver, store)
|
||||||
|
|
||||||
|
|||||||
@@ -1,367 +0,0 @@
|
|||||||
using Test
|
|
||||||
using Dates
|
|
||||||
using YiemAgent
|
|
||||||
using YiemAgent.toolRegistry
|
|
||||||
using YiemAgent.type
|
|
||||||
using YiemAgent.agentCore
|
|
||||||
|
|
||||||
@testset "register_all_tools with toolStore" begin
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------ #
|
|
||||||
# 1. register_all_tools registers all static tools + listTools #
|
|
||||||
# ------------------------------------------------------------------ #
|
|
||||||
store = toolStore(name="test1")
|
|
||||||
loaded = register_all_tools(store)
|
|
||||||
@test !isempty(loaded)
|
|
||||||
@test length(loaded) == 4 # getWeather + getTime + writeTool + listTools
|
|
||||||
|
|
||||||
names = [k for k in keys(loaded)]
|
|
||||||
@test "getTime" in names
|
|
||||||
@test "getWeather" in names
|
|
||||||
@test "writeTool" in names
|
|
||||||
@test "listTools" in names
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------ #
|
|
||||||
# 2. register_all_tools returns tools in registration order #
|
|
||||||
# ------------------------------------------------------------------ #
|
|
||||||
@test collect(keys(loaded))[1] == "getWeather"
|
|
||||||
@test collect(keys(loaded))[2] == "getTime"
|
|
||||||
@test collect(keys(loaded))[3] == "writeTool"
|
|
||||||
@test collect(keys(loaded))[4] == "listTools"
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------ #
|
|
||||||
# 3. Verify loaded tool fields are correct #
|
|
||||||
# ------------------------------------------------------------------ #
|
|
||||||
# getTime
|
|
||||||
time_tool = loaded["getTime"]
|
|
||||||
@test time_tool.name == "getTime"
|
|
||||||
@test time_tool.label == "Time Lookup"
|
|
||||||
@test time_tool.validateRequiredArgs !== nothing
|
|
||||||
@test time_tool.parallelToolExecute == false
|
|
||||||
@test time_tool.inputSchema["required"] == Any[]
|
|
||||||
|
|
||||||
# getWeather
|
|
||||||
weather = loaded["getWeather"]
|
|
||||||
@test weather.name == "getWeather"
|
|
||||||
@test weather.label == "Weather Lookup"
|
|
||||||
@test weather.execute !== nothing
|
|
||||||
@test weather.parallelToolExecute == false
|
|
||||||
@test weather.inputSchema["required"] == ["city"]
|
|
||||||
|
|
||||||
# writeTool
|
|
||||||
wt = loaded["writeTool"]
|
|
||||||
@test wt.name == "writeTool"
|
|
||||||
@test wt.label == "Create Tool"
|
|
||||||
@test wt.execute !== nothing
|
|
||||||
@test "name" in wt.inputSchema["required"]
|
|
||||||
@test "executeCode" in wt.inputSchema["required"]
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------ #
|
|
||||||
# 4. Tool execution returns valid results #
|
|
||||||
# ------------------------------------------------------------------ #
|
|
||||||
sig = nothing
|
|
||||||
op = x -> x # no-op partial result callback
|
|
||||||
|
|
||||||
# execute getTime
|
|
||||||
result_t = time_tool.execute("call-1", Dict{String,Any}("city" => "Tokyo"), sig, op)
|
|
||||||
@test result_t isa agentToolResult
|
|
||||||
@test result_t.content[1] isa textContent
|
|
||||||
@test occursin("Tokyo", result_t.content[1].text)
|
|
||||||
|
|
||||||
# execute getTime with timezone
|
|
||||||
result_tz = time_tool.execute("call-2", Dict{String,Any}("timezone" => "America/New_York"), sig, op)
|
|
||||||
@test result_tz isa agentToolResult
|
|
||||||
@test occursin("America/New_York", result_tz.content[1].text)
|
|
||||||
|
|
||||||
# execute getWeather
|
|
||||||
result_w = weather.execute("call-3", Dict{String,Any}("city" => "Bangkok"), sig, op)
|
|
||||||
@test result_w isa agentToolResult
|
|
||||||
@test result_w.content[1] isa textContent
|
|
||||||
@test occursin("Bangkok", result_w.content[1].text)
|
|
||||||
|
|
||||||
# execute getWeather with units
|
|
||||||
result_w2 = weather.execute("call-4", Dict{String,Any}("city" => "London", "units" => "fahrenheit"), sig, op)
|
|
||||||
@test occursin("72\u00b0F", result_w2.content[1].text)
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------ #
|
|
||||||
# 5. getTools / registerTool / clearTools (per-store isolation) #
|
|
||||||
# ------------------------------------------------------------------ #
|
|
||||||
store3 = toolStore(name="test3")
|
|
||||||
registry_tools = getTools(store3)
|
|
||||||
@test isempty(registry_tools)
|
|
||||||
|
|
||||||
# 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",
|
|
||||||
label = "Manual Tool",
|
|
||||||
description = "Registered manually",
|
|
||||||
inputSchema = Dict{String,Any}("type" => "object", "properties" => Dict{String,Any}(), "required" => Any[]),
|
|
||||||
execute = (toolCallId, args, signal, onPartialResult) ->
|
|
||||||
agentToolResult([textContent("manual")], Dict{Any,Any}(), nothing, false),
|
|
||||||
prepareArguments = nothing,
|
|
||||||
validateRequiredArgs = nothing,
|
|
||||||
parallelToolExecute = true
|
|
||||||
)
|
|
||||||
registerTool(store3, test_tool)
|
|
||||||
reg = getTools(store3)
|
|
||||||
@test haskey(reg, "manualTool")
|
|
||||||
@test length(reg) == 1
|
|
||||||
@test reg["manualTool"].parallelToolExecute == true
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------ #
|
|
||||||
# 6. getTools returns direct reference (mutations affect registry) #
|
|
||||||
# ------------------------------------------------------------------ #
|
|
||||||
copy1 = getTools(store3)
|
|
||||||
copy2 = getTools(store3)
|
|
||||||
@test copy1 === copy2 # same reference, not a deep copy
|
|
||||||
empty!(copy1)
|
|
||||||
@test isempty(getTools(store3)) # mutation propagates
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------ #
|
|
||||||
# 7. 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
|
|
||||||
|
|
||||||
@testset "listTool" begin
|
|
||||||
store = toolStore(name="test_list")
|
|
||||||
register_all_tools(store) # auto-registers getWeather, getTime, writeTool + listTools
|
|
||||||
|
|
||||||
# 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)
|
|
||||||
@test list_t isa agentTool
|
|
||||||
@test list_t.name == "listTools"
|
|
||||||
@test list_t.label == "List Tools"
|
|
||||||
@test isempty(list_t.inputSchema["required"])
|
|
||||||
|
|
||||||
# Verify all tools appear (3 loaded + listTools = 4)
|
|
||||||
result = list_t.execute("call-1", Dict{String,Any}(), nothing, x -> x)
|
|
||||||
@test result isa agentToolResult
|
|
||||||
@test result.content[1] isa textContent
|
|
||||||
@test occursin("listTools", result.content[1].text)
|
|
||||||
@test occursin("getWeather", result.content[1].text)
|
|
||||||
@test occursin("getTime", result.content[1].text)
|
|
||||||
@test occursin("writeTool", result.content[1].text)
|
|
||||||
@test result.details["count"] == 4
|
|
||||||
|
|
||||||
# Each listTool call creates an independent closure
|
|
||||||
storeB = toolStore(name="test_listB")
|
|
||||||
registerTool(storeB, store.tools["getWeather"])
|
|
||||||
list_tB = listTool(storeB)
|
|
||||||
|
|
||||||
resultA = list_t.execute("call-3", Dict{String,Any}(), nothing, x -> x)
|
|
||||||
resultB = list_tB.execute("call-4", Dict{String,Any}(), nothing, x -> x)
|
|
||||||
|
|
||||||
@test occursin("getWeather", resultA.content[1].text)
|
|
||||||
@test occursin("getWeather", resultB.content[1].text)
|
|
||||||
@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
|
|
||||||
Reference in New Issue
Block a user