diff --git a/Project.toml b/Project.toml index 5d1d1e9..fa28a2b 100644 --- a/Project.toml +++ b/Project.toml @@ -1,6 +1,6 @@ name = "YiemAgent" uuid = "e012c34b-7f78-48e0-971c-7abb83b6f0a2" -version = "0.9.0" +version = "0.9.1" authors = ["narawat lamaiin "] [deps] diff --git a/src/agentCore.jl b/src/agentCore.jl index 5c2cc74..acc80b2 100644 --- a/src/agentCore.jl +++ b/src/agentCore.jl @@ -170,7 +170,7 @@ function yiemAgent( tools = OrderedDict{String, agentTool}() registerAllTools(tools, mcpServer; eventSink=eventSink, sessionId=sessionId) - + eventSink("instantiate an agent") # Create struct with a placeholder task, then spawn and replace it agent = yiemAgent( agentState(systemPrompt, model, tools, messages), @@ -194,7 +194,7 @@ function yiemAgent( # Spawn the background loop and attach it agent._agentLoop = @spawn _agentLoop(agent) - + eventSink("agent instantiated") return agent end @@ -260,31 +260,36 @@ function _agentLoop(agent::yiemAgent) agent.inputChannel -> nothing agent.followUpChannel -> nothing """ - + agent.eventSink("_agentLoop starting") while true while newUserMsg === nothing if isready(agent.inputChannel) # agent process new user msg immediately after the current tool call finished. newUserMsg = take!(agent.inputChannel) + agent.eventSink("take new user message") else # check followUp message after _processMessage() is done if typeof(processingTask) == Task && istaskdone(processingTask) == true + agent.eventSink("_processMessage() done") # if agent runs is done but followUpChannel has messages, # put new message in inputChannel instead if isready(agent.followUpChannel) while isready(agent.followUpChannel) + agent.eventSink("put followUp message to input channel") followUpMsg = take!(agent.followUpChannel) put!(agent.inputChannel, followUpMsg) end processingTask = nothing # reset result = nothing # reset else # _processMessage() done and no followUp message. + agent.eventSink("no followUp message") result = deepcopy(agent._state.messages[end]) # filter out reasoningContent in-place filter!(c -> !(c isa reasoningContent), result.content) # format output respondToUI = _assistantMessageToOpenAI(result) put!(agent.outputChannel, respondToUI) + agent.eventSink("put result to output channel") processingTask = nothing # reset result = nothing # reset end @@ -314,6 +319,7 @@ function _agentLoop(agent::yiemAgent) else # spawn new _processMessage() if it is not already running. if processingTask === nothing + agent.eventSink("spawn new _processMessage()") # discard all messages in followUpChannel while isready(agent.followUpChannel) _ = take!(agent.followUpChannel) @@ -335,8 +341,10 @@ function _agentLoop(agent::yiemAgent) ) end put!(processMessageInputCh, newUserMsg) + agent.eventSink("put new user msg to processMessageInputCh") newUserMsg = nothing # reset end + end catch e bt = catch_backtrace() @@ -344,7 +352,7 @@ function _agentLoop(agent::yiemAgent) showerror(io, e, bt) println(io) end - eventSink(errMsg; publish=true) + agent.eventSink(errMsg; publish=true) # On any error, send error response and exit the loop @error "Agent loop failed" error=e @@ -395,6 +403,8 @@ function _processMessage( # loop until llmCall() response didn't use tool calls final_response = nothing + eventSink("▶ _processMessage: entered") + """ example message in inputChannel Dict( "role" => "user", @@ -420,6 +430,7 @@ function _processMessage( end newUserMsg = OpenAiToUserMessage(newUserMsg_openai) push!(agentMsgHistory, newUserMsg) + eventSink("▶ _processMessage: user msg pushed") end # call prepareContext() state = agentState(systemPrompt, nothing, tools, agentMsgHistory) @@ -427,6 +438,7 @@ function _processMessage( # Call formatMessagesForLLM() to format for LLM formattedMessages = formatMessagesForLLM(preparedContext, eventSink) + eventSink("▶ _processMessage: context") eventSink("Thinking...") """ response example response = JSON.Object{String, Any}("finish_reason" => "tool_calls", "index" => 0, "message" => JSON.Object{String, Any}("role" => "assistant", "content" => "", "reasoning_content" => "Here's a thinking process:\n\n1. **Identify User Request**: The user is asking for the weather in Bangkok.\n2. **Locate Relevant Tool**: I have a `getWeather` function available.\n3. **Check Function Parameters**:\n - `city` (required): City and country, e.g., 'San Francisco, CA' or 'Tokyo, Japan'\n - `units` (optional, default \"celsius\"): Temperature scale (\"celsius\" or \"fahrenheit\")\n4. **Prepare Parameters**:\n - `city`: \"Bangkok, Thailand\" (adding country for clarity, though just \"Bangkok\" might work, following the example format is safer)\n - `units`: Not specified, so I'll use the default (\"celsius\")\n5. **Execute Tool Call**: Call `getWeather` with `city: \"Bangkok, Thailand\"`\n6. **Anticipate Response**: The function will return current weather and forecast data for Bangkok. I'll then format it nicely for the user.\n - *Self-Correction/Verification during thought*: The prompt says \"city: City and country, e.g., 'San Francisco, CA' or 'Tokyo, Japan'\". I'll use \"Bangkok, Thailand\". The `units` parameter is optional, so I'll omit it to use the default.\n - Proceed with tool call.✅\n", "tool_calls" => Any[JSON.Object{String, Any}("type" => "function", "function" => JSON.Object{String, Any}("name" => "getWeather", "arguments" => "{\"city\":\"Bangkok, Thailand\"}"), "id" => "6fOilR5QPcdppbAAHluhkRyUDu3oWMAL")])) @@ -434,12 +446,15 @@ function _processMessage( response = llmCall(formattedMessages, eventSink=eventSink) # Extract tool calls from LLM response content blocks - hasToolCalls, toolCallList, assistant_msg = _extractToolCalls(response) + hasToolCalls, toolCallList, assistant_msg = _extractToolCalls(response, eventSink) # Add assistant message (tool calls or text) to history for next LLM turn push!(agentMsgHistory, assistant_msg) + eventSink("▶ _processMessage: LLM response") if hasToolCalls && length(toolCallList) > 0 + eventSink("▶ _processMessage: tool calls") + # Build context and config for executeToolCalls config = agentLoopConfig( @@ -452,11 +467,13 @@ function _processMessage( # call executeToolCalls() toolResultBatch = executeToolCalls(preparedContext, assistant_msg, toolCallList, config, signal, eventSink) + eventSink("▶ _processMessage: tool batch") # save toolResults to messages for toolResult in toolResultBatch.messages push!(agentMsgHistory, toolResult) end if toolResultBatch.terminate + eventSink("▶ _processMessage: TERMINATE branch") # If toolResultBatch requested termination, build a final response final_content = [textContent("Tool execution completed.")] for toolResult in toolResultBatch.messages @@ -488,6 +505,7 @@ function _processMessage( break end else + eventSink("▶ _processMessage: FINAL response") # LLM did not use tool calls — break end @@ -499,9 +517,11 @@ function _processMessage( end eventSink(err_msg; publish=true) + eventSink("▶ _processMessage: CAUGHT ERROR") end end + eventSink("▶ _processMessage: exited") return nothing end @@ -611,9 +631,12 @@ The message is constructed from: 1) llm_useTool = JSON.Object{String, Any}("finish_reason" => "tool_calls", "index" => 0, "message" => JSON.Object{String, Any}("role" => "assistant", "content" => "", "reasoning_content" => "Here's a thinking process:\n\n1. **Identify User Request**: The user is asking for the weather in Bangkok.\n2. **Locate Relevant Tool**: I have a `getWeather` function available.\n3. **Check Function Parameters**:\n - `city` (required): City and country, e.g., 'San Francisco, CA' or 'Tokyo, Japan'\n - `units` (optional, default \"celsius\"): Temperature scale (\"celsius\" or \"fahrenheit\")\n4. **Prepare Parameters**:\n - `city`: \"Bangkok, Thailand\" (adding country for clarity, though just \"Bangkok\" might work, following the example format is safer)\n - `units`: Not specified, so I'll use the default (\"celsius\")\n5. **Execute Tool Call**: Call `getWeather` with `city: \"Bangkok, Thailand\"`\n6. **Anticipate Response**: The function will return current weather and forecast data for Bangkok. I'll then format it nicely for the user.\n - *Self-Correction/Verification during thought*: The prompt says \"city: City and country, e.g., 'San Francisco, CA' or 'Tokyo, Japan'\". I'll use \"Bangkok, Thailand\". The `units` parameter is optional, so I'll omit it to use the default.\n - Proceed with tool call.✅\n", "tool_calls" => Any[JSON.Object{String, Any}("type" => "function", "function" => JSON.Object{String, Any}("name" => "getWeather", "arguments" => "{\"city\":\"Bangkok, Thailand\"}"), "id" => "6fOilR5QPcdppbAAHluhkRyUDu3oWMAL")])) 2) llm_notUseTool = JSON.Object{String, Any}("finish_reason" => "stop", "index" => 0, "message" => JSON.Object{String, Any}("role" => "assistant", "content" => "The weather in London, UK is currently Sunny with a temperature of 22°C.", "reasoning_content" => "The user asked for the weather in London.\nI called the `getWeather` tool for London, UK.\nThe response indicates it's Sunny and 22°C.\nI will convey this information to the user.\n")) """ -function _extractToolCalls(response) +function _extractToolCalls(response, eventSink=nothing) hasToolCalls = false toolCallList = agentToolCall[] + if eventSink !== nothing + eventSink("▶ _extractToolCalls: parsing response") + end # Helper: parse args (JSON string -> Dict, or pass through) parse_args(raw) = raw isa AbstractDict && !(raw isa Dict{String,Any}) ? @@ -943,9 +966,11 @@ function prepareToolCall( )::Union{preparedToolCall,immediateOutcome} # pick a called tool from tool store + eventSink("▶ prepareToolCall: resolving") tool = get(context.tools, toolCall.name, nothing) if tool === nothing + eventSink("▶ prepareToolCall: NOT FOUND") return immediateOutcome(createErrorToolResult("Tool $toolCall.name not found"), true) end @@ -953,6 +978,7 @@ function prepareToolCall( # 1. prepare arguments (tool-specific transform) prepared = prepareToolCallArguments(tool, toolCall) validatedArgs = validateToolArguments(tool, prepared) + eventSink("▶ prepareToolCall: validated") # 2. beforeToolCall hook — can block if config.beforeToolCall !== nothing @@ -1032,8 +1058,10 @@ function executePreparedToolCall( eventSink, )::executedOutcome + eventSink("▶ execute: START") try result = prep.tool.execute(prep.toolCall.id, prep.args, signal, eventSink) + eventSink("▶ execute: END") return executedOutcome(result, false) catch e bt = catch_backtrace() @@ -1042,6 +1070,7 @@ function executePreparedToolCall( println(io) end eventSink(errMsg; publish=true) + eventSink("▶ execute: ERROR") return executedOutcome(createErrorToolResult(sprint(showerror, e)), true) end @@ -1115,6 +1144,7 @@ function finalizeExecutedToolCall( eventSink )::finalizedOutcome + eventSink("▶ finalize: START") result = executed.result isError = executed.isError if config.afterToolCall !== nothing @@ -1140,6 +1170,7 @@ function finalizeExecutedToolCall( isError = true end end + eventSink("▶ finalize: END") return finalizedOutcome(prep.toolCall, result, isError) end @@ -1207,6 +1238,7 @@ function executeToolCallsSequential( )::agentToolCallBatch finalizedCalls = finalizedOutcome[] messages = toolResultMessage[] + eventSink("▶ executeToolCallsSequential: batch start") for tc in toolCalls prep = prepareToolCall(context, assistantMsg, tc, config, signal, eventSink) @@ -1224,6 +1256,7 @@ function executeToolCallsSequential( break end end + eventSink("▶ executeToolCallsSequential: batch end") return agentToolCallBatch(messages, shouldTerminate(finalizedCalls)) end @@ -1294,6 +1327,7 @@ function executeToolCallsParallel( )::agentToolCallBatch entries = Union{finalizedOutcome,Task}[] + eventSink("▶ executeToolCallsParallel: batch start") for tc in toolCalls toolExecStartEvent(tc.id, tc.name, tc.arguments) @@ -1333,6 +1367,7 @@ function executeToolCallsParallel( push!(messages, createToolResultMessage(f)) end + eventSink("▶ executeToolCallsParallel: batch end") return agentToolCallBatch(messages, shouldTerminate(finalizedCalls)) end @@ -1403,6 +1438,7 @@ function executeToolCalls( break end end + eventSink("▶ executeToolCalls: dispatch") if config.toolExecution == "sequential" || hasSequential return executeToolCallsSequential(context, assistantMsg, toolCalls, config, signal, eventSink) @@ -1447,8 +1483,11 @@ OpenAiToUserMessage(msg) # => userMessage("user", [textContent("Hello")], DateTime(...)) ``` """ -function OpenAiToUserMessage(msg::AbstractDict)::userMessage +function OpenAiToUserMessage(msg::AbstractDict, eventSink=nothing)::userMessage content_blocks = Vector{messageContent}() + if eventSink !== nothing + eventSink("▶ OpenAiToUserMessage: parsing") + end raw_content = get(msg, "content", Any[]) if raw_content isa Vector @@ -1472,6 +1511,9 @@ function OpenAiToUserMessage(msg::AbstractDict)::userMessage end end + if eventSink !== nothing + eventSink("▶ OpenAiToUserMessage: done") + end return userMessage(content=content_blocks) end diff --git a/src/toolRegistry.jl b/src/toolRegistry.jl index 49e5ad8..95c4784 100644 --- a/src/toolRegistry.jl +++ b/src/toolRegistry.jl @@ -198,7 +198,7 @@ Add `tool` to `tools`, overwriting any existing tool with the same name. function registerTool(tools::OrderedDict{String, agentTool}, tool::agentTool; eventSink=nothing )::OrderedDict{String, agentTool} tools[tool.name] = tool - println("[toolRegistry] Registered tool: $(tool.name)") + eventSink("[toolRegistry] Registered tool: $(tool.name)") return tools end @@ -282,7 +282,9 @@ Register all tools for an agent: """ function registerAllTools(tools::OrderedDict{String, agentTool}, mcpserver=nothing; eventSink=nothing, sessionId=nothing) list_t = listTool(tools, mcpserver; eventSink=eventSink, sessionId=sessionId) + eventSink("registering tools") registerTool(tools, list_t; eventSink=eventSink) + eventSink("registering tools done") end end # module