diff --git a/src/agentCore.jl b/src/agentCore.jl index 7e1022d..ab0d5f4 100644 --- a/src/agentCore.jl +++ b/src/agentCore.jl @@ -310,13 +310,13 @@ function _agentLoop(agent::yiemAgent) else # _processMessage() done and no followUp message. agent.agentEventSink("_agentLoop 4 agent._state.messages length $(length(agent._state.messages))") result = deepcopy(agent._state.messages[end]) - + agent.agentEventSink("_agentLoop 4-1 ") # filter out reasoningContent in-place filter!(c -> !(c isa reasoningContent), result.content) - + agent.agentEventSink("_agentLoop 5 ") # format output respondToUI = _assistantMessageToOpenAI(result) - + agent.agentEventSink("_agentLoop 6 ") put!(agent.outputChannel, respondToUI) if !isempty(result.content) && result.content[1] isa textContent agent.agentEventSink(result.content[1].text) @@ -376,6 +376,13 @@ function _agentLoop(agent::yiemAgent) end end 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 @error "Agent loop failed" error=e end @@ -1085,13 +1092,12 @@ function executePreparedToolCall( llmCall::Union{Any,Nothing}=nothing, )::executedOutcome agentEventSink("executePreparedToolCall 1") - agentEventSink("executePreparedToolCall 2") - agentEventSink("executePreparedToolCall 3") - try + try #WORKING result = prep.tool.execute(prep.toolCall.id, prep.args, signal, agentEventSink, llmCall) + agentEventSink("executePreparedToolCall 2") agentEventSink(result.content[1].text) - agentEventSink("executePreparedToolCall 4") + agentEventSink("executePreparedToolCall 3") return executedOutcome(result, false) catch e bt = catch_backtrace() diff --git a/src/toolRegistry.jl b/src/toolRegistry.jl index 26e56b7..8381d2e 100644 --- a/src/toolRegistry.jl +++ b/src/toolRegistry.jl @@ -100,7 +100,7 @@ function _wrap_mcp_tool(mcpserver, tool_def::Dict{String,Any})::agentTool inputSchema=params, execute=(toolCallId::String, args::Dict{String,Any}, signal::Union{Nothing,abortSignal}, - onPartialResult::Function, + agentEventSink, llmCall=nothing) -> begin try response = mcpserver("tools/call", name, args) @@ -256,7 +256,7 @@ function listTool(store::toolStore, mcpserver)::agentTool ), execute=(toolCallId::String, args::Dict{String,Any}, signal::Union{Nothing,abortSignal}, - onPartialResult::Function, llmCall=nothing) -> begin + agentEventSink, llmCall=nothing) -> begin # Discover and register MCP tools (idempotent — skips already registered) new_count, tool_list = _register_mcp_tools(mcpserver, store) diff --git a/test/toolTest.jl b/test/toolTest.jl deleted file mode 100644 index f613397..0000000 --- a/test/toolTest.jl +++ /dev/null @@ -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