diff --git a/etc.jl b/etc.jl index c6c563a..be4b0e2 100644 --- a/etc.jl +++ b/etc.jl @@ -1,11 +1,5 @@ -_processMessage 10 -JSON.Object{String, Any}("finish_reason" => "stop", "index" => 0, "message" => JSON.Object{String, Any}("role" => "assistant", "content" => "The weather in Bangkok is currently Sunny with a temperature of 22°C.", "reasoning_content" => "The user is asking for the weather in Bangkok.\nI have already retrieved the weather information in the previous turn and provided it to the user.\nThe user's current input is \"What's the weather in Bangkok?\", which is the same question as before.\nI should provide the same answer again.\nNo new tool calls are needed.\nI will simply state the weather information retrieved previously.\nWeather in Bangkok: Sunny, 22°C.\nI will output the answer directly.\n")) -_processMessage 11 -hasToolCalls: false -toolCallList: YiemAgent.type.agentToolCall[] -_processMessage 17 -_processMessage 18 - ---- -here is my log. from the log, it seems like _processMessage() is finished but somehow the log didn't show _agentLoop 5 message. _processMessage() may not exit properly but why? - +check my understand: +1) if LLM didn't use tool calls, assistantMessage get pushed into agent._state.messages and +it will be the latest message in agent._state.messages. then _agentLoop() can pick it as +the output to outputChannel +2) if LLM use tool calls but toolResultBatch.terminate is false, assistantMessageToolCall \ No newline at end of file diff --git a/etc.md b/etc.md new file mode 100644 index 0000000..4664add --- /dev/null +++ b/etc.md @@ -0,0 +1,8 @@ +check my understanding +1) if LLM didn't use tool calls, assistantMessage get pushed into agent._state.messages and it will be the latest message in agent._state.messages. then _agentLoop() can pick it as the output to outputChannel +2) if LLM use tool calls, assistantMessageToolCall get pushed into agent._state.messages. then toolResult get pushed into agent._state.messages. if toolResultBatch.terminate is false then _processMessage() loop continue +3) if LLM use tool calls, assistantMessageToolCall get pushed into agent._state.messages. then toolResult get pushed into agent._state.messages. if toolResultBatch.terminate is true then final_response message get pushed into agent._state.messages. _processMessage() loop exit. then _agentLoop() can pick it as the output to outputChannel + +Is my understanding correct? + + diff --git a/src/agentCore.jl b/src/agentCore.jl index c1a4168..5284d54 100644 --- a/src/agentCore.jl +++ b/src/agentCore.jl @@ -208,25 +208,25 @@ function _agentLoop(agent::yiemAgent) while true while newUserMsg === nothing if isready(agent.inputChannel) - agent.agentEventSink("_agentLoop 1") + agent.agentEventSink("_agentLoop 1 agent._state.messages length $(length(agent._state.messages))") # agent process new user msg immediately after the current tool call finished. newUserMsg = take!(agent.inputChannel) agent.agentEventSink("new user msg") else # check followUp message after _processMessage() is done if typeof(processingTask) == Task && istaskdone(processingTask) == true - agent.agentEventSink("_agentLoop 2") + agent.agentEventSink("_agentLoop 2 agent._state.messages length $(length(agent._state.messages))") # if agent runs is done but followUpChannel has messages, # put new message in inputChannel instead if isready(agent.followUpChannel) - agent.agentEventSink("_agentLoop 3") + agent.agentEventSink("_agentLoop 3 agent._state.messages length $(length(agent._state.messages))") while isready(agent.followUpChannel) followUpMsg = take!(agent.followUpChannel) put!(agent.inputChannel, followUpMsg) end else # _processMessage() done and no followUp message. - agent.agentEventSink("_agentLoop 4") - result = fetch(processingTask) + agent.agentEventSink("_agentLoop 4 agent._state.messages length $(length(agent._state.messages))") + result = agent._state.messages[end] put!(agent.outputChannel, result) agent.agentEventSink(result.content[1].text) processingTask = nothing # reset @@ -259,7 +259,7 @@ function _agentLoop(agent::yiemAgent) else # spawn new _processMessage() if it is not already running. if processingTask === nothing - agent.agentEventSink("_agentLoop 2") + agent.agentEventSink("_agentLoop 5 agent._state.messages length $(length(agent._state.messages))") # Dispatch message through the processing pipeline processingTask = @spawn _processMessage( processMessageInputCh, @@ -325,8 +325,8 @@ function _processMessage( beforeToolCall::Union{Function, Nothing}, afterToolCall::Union{Function, Nothing}, parallelToolExecute::Bool, -)::assistantMessage - agentEventSink("_processMessage 1") +)::Nothing + agentEventSink("_processMessage 1 _state.messages length $(length(agentMsgHistory))") # loop until llmCall() response didn't use tool calls final_response = nothing @@ -346,41 +346,46 @@ function _processMessage( while true # Drain inputChannel and convert OpenAI-format messages to userMessage type while isready(inputChannel) - agentEventSink("_processMessage 2") + agentEventSink("_processMessage 2 _state.messages length $(length(agentMsgHistory))") newUserMsg_openai = take!(inputChannel) - agentEventSink("_processMessage 3") + agentEventSink("_processMessage 3 _state.messages length $(length(agentMsgHistory))") if newUserMsg_openai === :shutdown - agentEventSink("_processMessage 4") + agentEventSink("_processMessage 4 _state.messages length $(length(agentMsgHistory))") # Re-emit shutdown signal for the loop to handle put!(inputChannel, :shutdown) break end - agentEventSink("_processMessage 5") + agentEventSink("_processMessage 5 _state.messages length $(length(agentMsgHistory))") newUserMsg = OpenAiToUserMessage(newUserMsg_openai) push!(agentMsgHistory, newUserMsg) - agentEventSink("_processMessage 6") + agentEventSink("_processMessage 6 _state.messages length $(length(agentMsgHistory))") end - agentEventSink("_processMessage 7") + agentEventSink("_processMessage 7 _state.messages length $(length(agentMsgHistory))") # call prepareContext() state = agentState(systemPrompt, nothing, tools, agentMsgHistory) - agentEventSink("_processMessage 8") + agentEventSink("_processMessage 8 _state.messages length $(length(agentMsgHistory))") preparedContext = prepareContext(state, agentEventSink) - agentEventSink("_processMessage 8") + agentEventSink("_processMessage 9 _state.messages length $(length(agentMsgHistory))") # Call formatMessagesForLLM() to format for LLM formattedMessages = formatMessagesForLLM(preparedContext, agentEventSink) - - agentEventSink("_processMessage 10") + + agentEventSink("_processMessage 10 formattedMessages $formattedMessages") """ 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")])) """ response = llmCall(formattedMessages) - agentEventSink(string(response)) + agentEventSink(" llmCall " * string(response)) - agentEventSink("_processMessage 11") + agentEventSink("_processMessage 11 _state.messages length $(length(agentMsgHistory))") # Extract tool calls from LLM response content blocks hasToolCalls, toolCallList, assistant_msg = _extractToolCalls(response) - agentEventSink("hasToolCalls: $hasToolCalls\ntoolCallList: $toolCallList") + agentEventSink("hasToolCalls: $hasToolCalls\ntoolCallList: $toolCallList _state.messages length $(length(agentMsgHistory))") + agentEventSink(string(assistant_msg)) + agentEventSink("_processMessage 11-1") + + # Add assistant message (tool calls or text) to history for next LLM turn + push!(agentMsgHistory, assistant_msg) if hasToolCalls && length(toolCallList) > 0 # Build context and config for executeToolCalls @@ -392,20 +397,20 @@ function _processMessage( ) signal = abortSignal(false) - agentEventSink("_processMessage 12") + agentEventSink("_processMessage 12 _state.messages length $(length(agentMsgHistory))") # call executeToolCalls() toolResultBatch = executeToolCalls(preparedContext, assistant_msg, toolCallList, config, signal, agentEventSink) - agentEventSink("_processMessage 13") + agentEventSink("_processMessage 13 _state.messages length $(length(agentMsgHistory))") # save toolResults to messages for toolResult in toolResultBatch.messages push!(agentMsgHistory, toolResult) end - agentEventSink("_processMessage 14") + agentEventSink("_processMessage 14 _state.messages length $(length(agentMsgHistory))") if toolResultBatch.terminate - agentEventSink("_processMessage 15") + agentEventSink("_processMessage 15 _state.messages length $(length(agentMsgHistory))") # If toolResultBatch requested termination, build a final response final_content = [textContent("Tool execution completed.")] for toolResult in toolResultBatch.messages @@ -419,7 +424,7 @@ function _processMessage( end end end - agentEventSink("_processMessage 16") + agentEventSink("_processMessage 16 _state.messages length $(length(agentMsgHistory))") final_response = assistantMessage( role="assistant", content=final_content, @@ -434,17 +439,17 @@ function _processMessage( end, timestamp=now(), ) + push!(agentMsgHistory, final_response) break end else - agentEventSink("_processMessage 17") - # LLM did not use tool calls — this is the final response - final_response = assistant_msg + agentEventSink("_processMessage 17 _state.messages length $(length(agentMsgHistory))") + # LLM did not use tool calls — break end end - agentEventSink("_processMessage 18") - return final_response + agentEventSink("_processMessage 18 _state.messages length $(length(agentMsgHistory))") + return nothing end @@ -516,24 +521,28 @@ function createToolResultMessage(f::finalizedOutcome)::toolResultMessage end """ - _extractToolCalls(response) -> Tuple{Bool, Vector{agentToolCall}, assistantMessage} + _extractToolCalls(response) -> Tuple{Bool, Vector{agentToolCall}, Union{assistantMessageToolCall, assistantMessage}} -Extracts tool calls from the LLM response and constructs an `assistantMessage`. +Extracts tool calls from the LLM response and constructs a message object. Supports two response formats: 1. **Message format** (e.g. from LMStudio.jl / vLLM): - `response["message"]["tool_calls"]` — array of tool call objects with - `"type" => "function"`, `"function" => Dict("name" => ..., "arguments" => "...")`, - and `"id"`. The `"arguments"` value is a JSON string that gets parsed via `JSON.parse`. + `response["message"]["tool_calls"]` — array of tool call objects with + `"type" => "function"`, `"function" => Dict("name" => ..., "arguments" => "...")`, + and `"id"`. The `"arguments"` value is a JSON string that gets parsed via `JSON.parse`. 2. **Content blocks format** (e.g. from OpenAI API): - `response.content` — array of content blocks. Blocks with `"type" => "tool_calls"` - contain a `"tool_calls"` array in format 1. Blocks with `"type" => "tool_call"` - have `"name"`, `"arguments"`, `"id"` at the top level (already-parsed args dict). + `response.content` — array of content blocks. Blocks with `"type" => "tool_calls"` + contain a `"tool_calls"` array in format 1. Blocks with `"type" => "tool_call"` + have `"name"`, `"arguments"`, `"id"` at the top level (already-parsed args dict). -The `assistantMessage` is constructed from: -- `reasoning_content` (string) → added as a `textContent` block -- `response.content` blocks (text/ reasoning) → added to content +When `hasToolCalls` is true, returns an `assistantMessageToolCall` with the tool calls +and reasoning content. When `hasToolCalls` is false, returns an `assistantMessage` +with text/content blocks from the response. + +The message is constructed from: +- `reasoning_content` (string) → stored in `reasoning` field (for tool calls) or `textContent` (for text) +- `response.content` blocks (text/reasoning) → added to content for text responses - Top-level `api`, `provider`, `model`, `usage` → copied to the message - `finish_reason` → used as `stopReason` @@ -541,7 +550,9 @@ The `assistantMessage` is constructed from: - `response`: LLM response object (Dict/JSON.Object or struct with `.content` field) # Returns -- `Tuple{Bool, Vector{agentToolCall}, assistantMessage}`: `(hasToolCalls, toolCallList, assistantMsg)` +- `Tuple{Bool, Vector{agentToolCall}, Union{assistantMessageToolCall, assistantMessage}}`: + `(hasToolCalls, toolCallList, message)` where message is `assistantMessageToolCall` + when tool calls exist, `assistantMessage` otherwise # 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")])) """ @@ -616,20 +627,6 @@ function _extractToolCalls(response) end # ── Construct assistantMessage from response ────────────────────── - # Check Format 1 nested message for reasoning_content, role, etc. - reasoning = get(response, "reasoning_content", nothing) - if reasoning === nothing && msg !== nothing && msg isa AbstractDict - reasoning = get(msg, "reasoning_content", nothing) - end - if reasoning isa String - reasoning_text = reasoning - elseif reasoning isa textContent - reasoning_text = reasoning.text - else - reasoning_text = "" - end - reasoning_block = !isempty(reasoning_text) ? [textContent(reasoning_text)] : textContent[] - finish_reason = get(response, "finish_reason", nothing) stop_reason = finish_reason isa AbstractString ? String(finish_reason) : "end_turn" @@ -638,6 +635,21 @@ function _extractToolCalls(response) model = get(response, "model", nothing) usage = get(response, "usage", nothing) + # Collect reasoning from reasoning_content field (Format 1: Anthropic-style) + reasoning_text = get(response, "reasoning_content", nothing) + if reasoning_text === nothing && msg !== nothing && msg isa AbstractDict + reasoning_text = get(msg, "reasoning_content", nothing) + end + if reasoning_text isa String + reasoning_text = reasoning_text + elseif reasoning_text isa textContent + reasoning_text = reasoning_text.text + else + reasoning_text = "" + end + reasoning_content = !isempty(reasoning_text) ? [textContent(reasoning_text)] : textContent[] + + # Collect content blocks from response.content array (Format 2: OpenAI-style) content_from_response = get(response, "content", nothing) content_blocks = Vector{messageContent}() if content_from_response isa Vector @@ -658,15 +670,11 @@ function _extractToolCalls(response) end end - # Combine content blocks and reasoning - if !isempty(content_blocks) && !isempty(reasoning_block) - all_content = vcat(reasoning_block, content_blocks) - elseif !isempty(content_blocks) - all_content = content_blocks - elseif !isempty(reasoning_block) - all_content = reasoning_block + # Combine reasoning_content field + content array blocks, deduplicating reasoning + if !isempty(reasoning_content) + all_content = vcat(reasoning_content, content_blocks) else - all_content = textContent[] + all_content = content_blocks end error_msg = get(response, "error_message", get(response, "errorMessage", nothing)) @@ -684,17 +692,32 @@ function _extractToolCalls(response) end end - assistant_msg = assistantMessage( - role = role, - content = all_content, - api = api isa AbstractString ? String(api) : "", - provider = provider isa AbstractString ? String(provider) : "", - model = model, - usage = usage, - stopReason = stop_reason, - errorMessage = error_msg, - timestamp = now(), - ) + if hasToolCalls + assistant_msg = assistantMessageToolCall( + role = role, + toolCalls = toolCallList, + content = all_content, + api = api isa AbstractString ? String(api) : "", + provider = provider isa AbstractString ? String(provider) : "", + model = model, + usage = usage, + stopReason = stop_reason, + errorMessage = error_msg, + timestamp = now(), + ) + else + assistant_msg = assistantMessage( + role = role, + content = all_content, + api = api isa AbstractString ? String(api) : "", + provider = provider isa AbstractString ? String(provider) : "", + model = model, + usage = usage, + stopReason = stop_reason, + errorMessage = error_msg, + timestamp = now(), + ) + end return hasToolCalls, toolCallList, assistant_msg end @@ -846,7 +869,7 @@ prepareToolCall(context, msg, tc, config, abortedSignal) """ function prepareToolCall( context::agentContext, - assistantMsg::assistantMessage, + assistantMsg::assistantMessageToolCall, toolCall::agentToolCall, config::agentLoopConfig, signal::abortSignal, @@ -863,10 +886,10 @@ function prepareToolCall( agentEventSink("prepareToolCall 3") # 1. prepare arguments (tool-specific transform) prepared = prepareToolCallArguments(tool, toolCall) - agentEventSink(string(prepared.arguments)) + agentEventSink("prepared " * string(prepared.arguments)) agentEventSink("prepareToolCall 4") validatedArgs = validateToolArguments(tool, prepared) - agentEventSink(string(validatedArgs)) + agentEventSink("validatedArgs " * string(validatedArgs)) agentEventSink("prepareToolCall 5") # 2. beforeToolCall hook — can block if config.beforeToolCall !== nothing @@ -950,14 +973,8 @@ function executePreparedToolCall( agentEventSink, )::executedOutcome agentEventSink("executePreparedToolCall 1") - agentEventSink(prep.toolCall.id) - agentEventSink(prep.toolCall.name) agentEventSink("executePreparedToolCall 2") - s = string(prep.args) - agentEventSink(s) agentEventSink("executePreparedToolCall 3") - t = string(fieldnames(typeof(prep.tool))) - agentEventSink("executePreparedToolCall 3-1 " * t) try result = prep.tool.execute(prep.toolCall.id, prep.args, signal, agentEventSink) @@ -1035,7 +1052,7 @@ finalizeExecutedToolCall(context, msg, prep, execFail, config, nothing) """ function finalizeExecutedToolCall( context::agentContext, - assistantMsg::assistantMessage, + assistantMsg::assistantMessageToolCall, prep::preparedToolCall, executed::executedOutcome, config::agentLoopConfig, @@ -1132,7 +1149,7 @@ executeToolCallsSequential(ctx, msg, [deployTc], config, nothing, emit) """ function executeToolCallsSequential( context::agentContext, - assistantMsg::assistantMessage, + assistantMsg::assistantMessageToolCall, toolCalls::Vector{agentToolCall}, config::agentLoopConfig, signal::abortSignal, @@ -1161,7 +1178,7 @@ function executeToolCallsSequential( agentEventSink("executeToolCallsSequential 3-2") end agentEventSink("executeToolCallsSequential 4") - agentEventSink("$(finalized.toolCall.id), $(finalized.toolCall.name), + agentEventSink("finalized $(finalized.toolCall.id), $(finalized.toolCall.name), $(finalized.result), $(finalized.isError)") push!(messages, createToolResultMessage(finalized)) push!(finalizedCalls, finalized) @@ -1233,7 +1250,7 @@ executeToolCallsParallel(ctx, msg, [tc1, tc2, tc3], config, abortedSignal, emit) """ function executeToolCallsParallel( context::agentContext, - assistantMsg::assistantMessage, + assistantMsg::assistantMessageToolCall, toolCalls::Vector{agentToolCall}, config::agentLoopConfig, signal::abortSignal, @@ -1335,7 +1352,7 @@ executeToolCalls(ctx, msg, [searchTc, fetchTc], configSequential, nothing, emit) """ function executeToolCalls( context::agentContext, - assistantMsg::assistantMessage, + assistantMsg::assistantMessageToolCall, toolCalls::Vector{agentToolCall}, config::agentLoopConfig, signal::abortSignal, diff --git a/src/type.jl b/src/type.jl index 54ff272..d7eee74 100644 --- a/src/type.jl +++ b/src/type.jl @@ -6,8 +6,8 @@ modelCost, llmModel, llmUsage, # Message content types textContent, imageContent, - # Message types - userMessage, assistantMessage, toolResultMessage, + # Message types + userMessage, assistantMessageToolCall, assistantMessage, toolResultMessage, # Tool types agentTool, validateRequiredArgs, # Context types @@ -108,6 +108,52 @@ function userMessage(; role="user", content=Vector{messageContent}(), timestamp= return userMessage(role, content, timestamp) end +struct assistantMessageToolCall <: agentMessage # Assistant message containing tool calls + role::String # Always "assistant" + toolCalls::Vector{agentToolCall} # Tool calls to execute + content::Vector{messageContent} # Reasoning/thinking content blocks + api::String # API name used (e.g., "openai") + provider::String # Provider name (e.g., "anthropic") + model::String # Model identifier + usage::llmUsage # Token usage for this message + stopReason::String # Why generation stopped (e.g., "tool_calls") + errorMessage::Union{String, Nothing} # Error if generation failed + timestamp::Timestamp # When the message was received +end + +""" +Create a new assistant message containing tool calls. + +# Arguments +- `role::String`: Always "assistant" +- `toolCalls::Vector{agentToolCall}`: Tool calls to execute +- `content::Vector{messageContent}`: Reasoning/thinking content blocks +- `api::String`: API name used +- `provider::String`: Provider name +- `model::String`: Model identifier +- `usage::llmUsage`: Token usage +- `stopReason::String`: Why generation stopped +- `errorMessage::Union{String, Nothing}`: Error if generation failed +- `timestamp::Timestamp`: When the message was received + +# Returns +- A new `assistantMessageToolCall` instance + +# Examples +```julia +julia> tc = agentToolCall("function", "call_1", "getWeather", Dict("city" => "Tokyo")) +julia> msg = assistantMessageToolCall(toolCalls=[tc], stopReason="tool_calls") +assistantMessageToolCall("assistant", [agentToolCall(...)], messageContent[], "", "", "", llmUsage(0, 0), "tool_calls", nothing, DateTime(...)) +``` +""" +function assistantMessageToolCall(; role="assistant", toolCalls=agentToolCall[], + content=Vector{messageContent}(), api="", provider="", model=nothing, usage=llmUsage(0, 0), + stopReason="tool_calls", errorMessage=nothing, timestamp=now()) + model_str = model isa AbstractString ? String(model) : "" + return assistantMessageToolCall(role, toolCalls, content, api, provider, model_str, + usage, stopReason, errorMessage, timestamp) +end + struct assistantMessage <: agentMessage # Message from the AI assistant role::String # Always "assistant" content::Vector{messageContent} # Text and/or image content @@ -434,13 +480,13 @@ end Context passed to the `beforeToolCall` hook. # Arguments -- `message::assistantMessage`: The assistant message containing the tool call +- `message::assistantMessageToolCall`: The assistant message containing the tool call - `toolCall::agentToolCall`: The tool call being prepared - `args::Dict{String,Any}`: Validated tool arguments - `context::agentContext`: Current conversation context """ struct beforeToolCallContext - message::assistantMessage + message::assistantMessageToolCall toolCall::agentToolCall args::Dict{String,Any} context::agentContext @@ -455,7 +501,7 @@ end Context passed to the `afterToolCall` hook. # Arguments -- `message::assistantMessage`: The assistant message containing the tool call +- `message::assistantMessageToolCall`: The assistant message containing the tool call - `toolCall::agentToolCall`: The tool call that was executed - `args::Dict{String,Any}`: Tool arguments - `result::agentToolResult`: The raw tool result @@ -463,7 +509,7 @@ Context passed to the `afterToolCall` hook. - `context::agentContext`: Current conversation context """ struct afterToolCallContext - message::assistantMessage + message::assistantMessageToolCall toolCall::agentToolCall args::Dict{String,Any} result::agentToolResult diff --git a/src/utils.jl b/src/utils.jl index f2dc740..29ac6a2 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -221,6 +221,8 @@ function formatMsgForLLM(ctx::agentContext, agentEventSink)::Dict{String, Any} for msg in ctx.messages if msg isa userMessage push!(messages, _userMessageToOpenAI(msg)) + elseif msg isa assistantMessageToolCall + push!(messages, _assistantMessageToolCallToOpenAI(msg)) elseif msg isa assistantMessage push!(messages, _assistantMessageToOpenAI(msg)) elseif msg isa toolResultMessage @@ -336,6 +338,44 @@ end """ +Convert an assistantMessageToolCall to OpenAI message format. + +Produces a message with role="assistant", content=null, and a tool_calls array: +{ + "role": "assistant", + "content": null, + "tool_calls": [ + { + "id": "call_1", + "type": "function", + "function": { + "name": "get_weather", + "arguments": "{\"location\": \"San Francisco, CA\"}" + } + } + ] +} +""" +function _assistantMessageToolCallToOpenAI(msg::assistantMessageToolCall)::Dict{String, Any} + tool_calls = Dict{String, Any}[] + for tc in msg.toolCalls + push!(tool_calls, Dict( + "id" => tc.id, + "type" => tc.type, + "function" => Dict( + "name" => tc.name, + "arguments" => JSON.json(tc.arguments) + ) + )) + end + return Dict( + "role" => "assistant", + "content" => nothing, + "tool_calls" => tool_calls + ) +end + +""" Convert an assistantMessage to OpenAI message format. """ function _assistantMessageToOpenAI(msg::assistantMessage)::Dict{String, Any}