This commit is contained in:
2026-08-16 13:36:01 +07:00
parent fd616409dd
commit a29a82b74c
5 changed files with 212 additions and 107 deletions
+5 -11
View File
@@ -1,11 +1,5 @@
_processMessage 10 check my understand:
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")) 1) if LLM didn't use tool calls, assistantMessage get pushed into agent._state.messages and
_processMessage 11 it will be the latest message in agent._state.messages. then _agentLoop() can pick it as
hasToolCalls: false the output to outputChannel
toolCallList: YiemAgent.type.agentToolCall[] 2) if LLM use tool calls but toolResultBatch.terminate is false, assistantMessageToolCall
_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?
+8
View File
@@ -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?
+106 -89
View File
@@ -208,25 +208,25 @@ function _agentLoop(agent::yiemAgent)
while true while true
while newUserMsg === nothing while newUserMsg === nothing
if isready(agent.inputChannel) 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. # agent process new user msg immediately after the current tool call finished.
newUserMsg = take!(agent.inputChannel) newUserMsg = take!(agent.inputChannel)
agent.agentEventSink("new user msg") agent.agentEventSink("new user msg")
else else
# check followUp message after _processMessage() is done # check followUp message after _processMessage() is done
if typeof(processingTask) == Task && istaskdone(processingTask) == true 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, # if agent runs is done but followUpChannel has messages,
# put new message in inputChannel instead # put new message in inputChannel instead
if isready(agent.followUpChannel) if isready(agent.followUpChannel)
agent.agentEventSink("_agentLoop 3") agent.agentEventSink("_agentLoop 3 agent._state.messages length $(length(agent._state.messages))")
while isready(agent.followUpChannel) while isready(agent.followUpChannel)
followUpMsg = take!(agent.followUpChannel) followUpMsg = take!(agent.followUpChannel)
put!(agent.inputChannel, followUpMsg) put!(agent.inputChannel, followUpMsg)
end end
else # _processMessage() done and no followUp message. else # _processMessage() done and no followUp message.
agent.agentEventSink("_agentLoop 4") agent.agentEventSink("_agentLoop 4 agent._state.messages length $(length(agent._state.messages))")
result = fetch(processingTask) result = agent._state.messages[end]
put!(agent.outputChannel, result) put!(agent.outputChannel, result)
agent.agentEventSink(result.content[1].text) agent.agentEventSink(result.content[1].text)
processingTask = nothing # reset processingTask = nothing # reset
@@ -259,7 +259,7 @@ function _agentLoop(agent::yiemAgent)
else else
# spawn new _processMessage() if it is not already running. # spawn new _processMessage() if it is not already running.
if processingTask === nothing 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 # Dispatch message through the processing pipeline
processingTask = @spawn _processMessage( processingTask = @spawn _processMessage(
processMessageInputCh, processMessageInputCh,
@@ -325,8 +325,8 @@ function _processMessage(
beforeToolCall::Union{Function, Nothing}, beforeToolCall::Union{Function, Nothing},
afterToolCall::Union{Function, Nothing}, afterToolCall::Union{Function, Nothing},
parallelToolExecute::Bool, parallelToolExecute::Bool,
)::assistantMessage )::Nothing
agentEventSink("_processMessage 1") agentEventSink("_processMessage 1 _state.messages length $(length(agentMsgHistory))")
# loop until llmCall() response didn't use tool calls # loop until llmCall() response didn't use tool calls
final_response = nothing final_response = nothing
@@ -346,41 +346,46 @@ function _processMessage(
while true while true
# Drain inputChannel and convert OpenAI-format messages to userMessage type # Drain inputChannel and convert OpenAI-format messages to userMessage type
while isready(inputChannel) while isready(inputChannel)
agentEventSink("_processMessage 2") agentEventSink("_processMessage 2 _state.messages length $(length(agentMsgHistory))")
newUserMsg_openai = take!(inputChannel) newUserMsg_openai = take!(inputChannel)
agentEventSink("_processMessage 3") agentEventSink("_processMessage 3 _state.messages length $(length(agentMsgHistory))")
if newUserMsg_openai === :shutdown if newUserMsg_openai === :shutdown
agentEventSink("_processMessage 4") agentEventSink("_processMessage 4 _state.messages length $(length(agentMsgHistory))")
# Re-emit shutdown signal for the loop to handle # Re-emit shutdown signal for the loop to handle
put!(inputChannel, :shutdown) put!(inputChannel, :shutdown)
break break
end end
agentEventSink("_processMessage 5") agentEventSink("_processMessage 5 _state.messages length $(length(agentMsgHistory))")
newUserMsg = OpenAiToUserMessage(newUserMsg_openai) newUserMsg = OpenAiToUserMessage(newUserMsg_openai)
push!(agentMsgHistory, newUserMsg) push!(agentMsgHistory, newUserMsg)
agentEventSink("_processMessage 6") agentEventSink("_processMessage 6 _state.messages length $(length(agentMsgHistory))")
end end
agentEventSink("_processMessage 7") agentEventSink("_processMessage 7 _state.messages length $(length(agentMsgHistory))")
# call prepareContext() # call prepareContext()
state = agentState(systemPrompt, nothing, tools, agentMsgHistory) state = agentState(systemPrompt, nothing, tools, agentMsgHistory)
agentEventSink("_processMessage 8") agentEventSink("_processMessage 8 _state.messages length $(length(agentMsgHistory))")
preparedContext = prepareContext(state, agentEventSink) preparedContext = prepareContext(state, agentEventSink)
agentEventSink("_processMessage 8") agentEventSink("_processMessage 9 _state.messages length $(length(agentMsgHistory))")
# Call formatMessagesForLLM() to format for LLM # Call formatMessagesForLLM() to format for LLM
formattedMessages = formatMessagesForLLM(preparedContext, agentEventSink) formattedMessages = formatMessagesForLLM(preparedContext, agentEventSink)
agentEventSink("_processMessage 10") agentEventSink("_processMessage 10 formattedMessages $formattedMessages")
""" response example """ 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 = 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) 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 # Extract tool calls from LLM response content blocks
hasToolCalls, toolCallList, assistant_msg = _extractToolCalls(response) 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 if hasToolCalls && length(toolCallList) > 0
# Build context and config for executeToolCalls # Build context and config for executeToolCalls
@@ -392,20 +397,20 @@ function _processMessage(
) )
signal = abortSignal(false) signal = abortSignal(false)
agentEventSink("_processMessage 12") agentEventSink("_processMessage 12 _state.messages length $(length(agentMsgHistory))")
# call executeToolCalls() # call executeToolCalls()
toolResultBatch = executeToolCalls(preparedContext, assistant_msg, toolCallList, config, toolResultBatch = executeToolCalls(preparedContext, assistant_msg, toolCallList, config,
signal, agentEventSink) signal, agentEventSink)
agentEventSink("_processMessage 13") agentEventSink("_processMessage 13 _state.messages length $(length(agentMsgHistory))")
# save toolResults to messages # save toolResults to messages
for toolResult in toolResultBatch.messages for toolResult in toolResultBatch.messages
push!(agentMsgHistory, toolResult) push!(agentMsgHistory, toolResult)
end end
agentEventSink("_processMessage 14") agentEventSink("_processMessage 14 _state.messages length $(length(agentMsgHistory))")
if toolResultBatch.terminate if toolResultBatch.terminate
agentEventSink("_processMessage 15") agentEventSink("_processMessage 15 _state.messages length $(length(agentMsgHistory))")
# If toolResultBatch requested termination, build a final response # If toolResultBatch requested termination, build a final response
final_content = [textContent("Tool execution completed.")] final_content = [textContent("Tool execution completed.")]
for toolResult in toolResultBatch.messages for toolResult in toolResultBatch.messages
@@ -419,7 +424,7 @@ function _processMessage(
end end
end end
end end
agentEventSink("_processMessage 16") agentEventSink("_processMessage 16 _state.messages length $(length(agentMsgHistory))")
final_response = assistantMessage( final_response = assistantMessage(
role="assistant", role="assistant",
content=final_content, content=final_content,
@@ -434,17 +439,17 @@ function _processMessage(
end, end,
timestamp=now(), timestamp=now(),
) )
push!(agentMsgHistory, final_response)
break break
end end
else else
agentEventSink("_processMessage 17") agentEventSink("_processMessage 17 _state.messages length $(length(agentMsgHistory))")
# LLM did not use tool calls — this is the final response # LLM did not use tool calls —
final_response = assistant_msg
break break
end end
end end
agentEventSink("_processMessage 18") agentEventSink("_processMessage 18 _state.messages length $(length(agentMsgHistory))")
return final_response return nothing
end end
@@ -516,24 +521,28 @@ function createToolResultMessage(f::finalizedOutcome)::toolResultMessage
end 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: Supports two response formats:
1. **Message format** (e.g. from LMStudio.jl / vLLM): 1. **Message format** (e.g. from LMStudio.jl / vLLM):
`response["message"]["tool_calls"]` — array of tool call objects with `response["message"]["tool_calls"]` — array of tool call objects with
`"type" => "function"`, `"function" => Dict("name" => ..., "arguments" => "...")`, `"type" => "function"`, `"function" => Dict("name" => ..., "arguments" => "...")`,
and `"id"`. The `"arguments"` value is a JSON string that gets parsed via `JSON.parse`. and `"id"`. The `"arguments"` value is a JSON string that gets parsed via `JSON.parse`.
2. **Content blocks format** (e.g. from OpenAI API): 2. **Content blocks format** (e.g. from OpenAI API):
`response.content` — array of content blocks. Blocks with `"type" => "tool_calls"` `response.content` — array of content blocks. Blocks with `"type" => "tool_calls"`
contain a `"tool_calls"` array in format 1. Blocks with `"type" => "tool_call"` 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). have `"name"`, `"arguments"`, `"id"` at the top level (already-parsed args dict).
The `assistantMessage` is constructed from: When `hasToolCalls` is true, returns an `assistantMessageToolCall` with the tool calls
- `reasoning_content` (string) → added as a `textContent` block and reasoning content. When `hasToolCalls` is false, returns an `assistantMessage`
- `response.content` blocks (text/ reasoning) → added to content 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 - Top-level `api`, `provider`, `model`, `usage` → copied to the message
- `finish_reason` → used as `stopReason` - `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) - `response`: LLM response object (Dict/JSON.Object or struct with `.content` field)
# Returns # 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")])) # 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 end
# ── Construct assistantMessage from response ────────────────────── # ── 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) finish_reason = get(response, "finish_reason", nothing)
stop_reason = finish_reason isa AbstractString ? String(finish_reason) : "end_turn" stop_reason = finish_reason isa AbstractString ? String(finish_reason) : "end_turn"
@@ -638,6 +635,21 @@ function _extractToolCalls(response)
model = get(response, "model", nothing) model = get(response, "model", nothing)
usage = get(response, "usage", 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_from_response = get(response, "content", nothing)
content_blocks = Vector{messageContent}() content_blocks = Vector{messageContent}()
if content_from_response isa Vector if content_from_response isa Vector
@@ -658,15 +670,11 @@ function _extractToolCalls(response)
end end
end end
# Combine content blocks and reasoning # Combine reasoning_content field + content array blocks, deduplicating reasoning
if !isempty(content_blocks) && !isempty(reasoning_block) if !isempty(reasoning_content)
all_content = vcat(reasoning_block, content_blocks) all_content = vcat(reasoning_content, content_blocks)
elseif !isempty(content_blocks)
all_content = content_blocks
elseif !isempty(reasoning_block)
all_content = reasoning_block
else else
all_content = textContent[] all_content = content_blocks
end end
error_msg = get(response, "error_message", get(response, "errorMessage", nothing)) error_msg = get(response, "error_message", get(response, "errorMessage", nothing))
@@ -684,17 +692,32 @@ function _extractToolCalls(response)
end end
end end
assistant_msg = assistantMessage( if hasToolCalls
role = role, assistant_msg = assistantMessageToolCall(
content = all_content, role = role,
api = api isa AbstractString ? String(api) : "", toolCalls = toolCallList,
provider = provider isa AbstractString ? String(provider) : "", content = all_content,
model = model, api = api isa AbstractString ? String(api) : "",
usage = usage, provider = provider isa AbstractString ? String(provider) : "",
stopReason = stop_reason, model = model,
errorMessage = error_msg, usage = usage,
timestamp = now(), 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 return hasToolCalls, toolCallList, assistant_msg
end end
@@ -846,7 +869,7 @@ prepareToolCall(context, msg, tc, config, abortedSignal)
""" """
function prepareToolCall( function prepareToolCall(
context::agentContext, context::agentContext,
assistantMsg::assistantMessage, assistantMsg::assistantMessageToolCall,
toolCall::agentToolCall, toolCall::agentToolCall,
config::agentLoopConfig, config::agentLoopConfig,
signal::abortSignal, signal::abortSignal,
@@ -863,10 +886,10 @@ function prepareToolCall(
agentEventSink("prepareToolCall 3") agentEventSink("prepareToolCall 3")
# 1. prepare arguments (tool-specific transform) # 1. prepare arguments (tool-specific transform)
prepared = prepareToolCallArguments(tool, toolCall) prepared = prepareToolCallArguments(tool, toolCall)
agentEventSink(string(prepared.arguments)) agentEventSink("prepared " * string(prepared.arguments))
agentEventSink("prepareToolCall 4") agentEventSink("prepareToolCall 4")
validatedArgs = validateToolArguments(tool, prepared) validatedArgs = validateToolArguments(tool, prepared)
agentEventSink(string(validatedArgs)) agentEventSink("validatedArgs " * string(validatedArgs))
agentEventSink("prepareToolCall 5") agentEventSink("prepareToolCall 5")
# 2. beforeToolCall hook — can block # 2. beforeToolCall hook — can block
if config.beforeToolCall !== nothing if config.beforeToolCall !== nothing
@@ -950,14 +973,8 @@ function executePreparedToolCall(
agentEventSink, agentEventSink,
)::executedOutcome )::executedOutcome
agentEventSink("executePreparedToolCall 1") agentEventSink("executePreparedToolCall 1")
agentEventSink(prep.toolCall.id)
agentEventSink(prep.toolCall.name)
agentEventSink("executePreparedToolCall 2") agentEventSink("executePreparedToolCall 2")
s = string(prep.args)
agentEventSink(s)
agentEventSink("executePreparedToolCall 3") agentEventSink("executePreparedToolCall 3")
t = string(fieldnames(typeof(prep.tool)))
agentEventSink("executePreparedToolCall 3-1 " * t)
try try
result = prep.tool.execute(prep.toolCall.id, prep.args, signal, agentEventSink) result = prep.tool.execute(prep.toolCall.id, prep.args, signal, agentEventSink)
@@ -1035,7 +1052,7 @@ finalizeExecutedToolCall(context, msg, prep, execFail, config, nothing)
""" """
function finalizeExecutedToolCall( function finalizeExecutedToolCall(
context::agentContext, context::agentContext,
assistantMsg::assistantMessage, assistantMsg::assistantMessageToolCall,
prep::preparedToolCall, prep::preparedToolCall,
executed::executedOutcome, executed::executedOutcome,
config::agentLoopConfig, config::agentLoopConfig,
@@ -1132,7 +1149,7 @@ executeToolCallsSequential(ctx, msg, [deployTc], config, nothing, emit)
""" """
function executeToolCallsSequential( function executeToolCallsSequential(
context::agentContext, context::agentContext,
assistantMsg::assistantMessage, assistantMsg::assistantMessageToolCall,
toolCalls::Vector{agentToolCall}, toolCalls::Vector{agentToolCall},
config::agentLoopConfig, config::agentLoopConfig,
signal::abortSignal, signal::abortSignal,
@@ -1161,7 +1178,7 @@ function executeToolCallsSequential(
agentEventSink("executeToolCallsSequential 3-2") agentEventSink("executeToolCallsSequential 3-2")
end end
agentEventSink("executeToolCallsSequential 4") agentEventSink("executeToolCallsSequential 4")
agentEventSink("$(finalized.toolCall.id), $(finalized.toolCall.name), agentEventSink("finalized $(finalized.toolCall.id), $(finalized.toolCall.name),
$(finalized.result), $(finalized.isError)") $(finalized.result), $(finalized.isError)")
push!(messages, createToolResultMessage(finalized)) push!(messages, createToolResultMessage(finalized))
push!(finalizedCalls, finalized) push!(finalizedCalls, finalized)
@@ -1233,7 +1250,7 @@ executeToolCallsParallel(ctx, msg, [tc1, tc2, tc3], config, abortedSignal, emit)
""" """
function executeToolCallsParallel( function executeToolCallsParallel(
context::agentContext, context::agentContext,
assistantMsg::assistantMessage, assistantMsg::assistantMessageToolCall,
toolCalls::Vector{agentToolCall}, toolCalls::Vector{agentToolCall},
config::agentLoopConfig, config::agentLoopConfig,
signal::abortSignal, signal::abortSignal,
@@ -1335,7 +1352,7 @@ executeToolCalls(ctx, msg, [searchTc, fetchTc], configSequential, nothing, emit)
""" """
function executeToolCalls( function executeToolCalls(
context::agentContext, context::agentContext,
assistantMsg::assistantMessage, assistantMsg::assistantMessageToolCall,
toolCalls::Vector{agentToolCall}, toolCalls::Vector{agentToolCall},
config::agentLoopConfig, config::agentLoopConfig,
signal::abortSignal, signal::abortSignal,
+52 -6
View File
@@ -6,8 +6,8 @@
modelCost, llmModel, llmUsage, modelCost, llmModel, llmUsage,
# Message content types # Message content types
textContent, imageContent, textContent, imageContent,
# Message types # Message types
userMessage, assistantMessage, toolResultMessage, userMessage, assistantMessageToolCall, assistantMessage, toolResultMessage,
# Tool types # Tool types
agentTool, validateRequiredArgs, agentTool, validateRequiredArgs,
# Context types # Context types
@@ -108,6 +108,52 @@ function userMessage(; role="user", content=Vector{messageContent}(), timestamp=
return userMessage(role, content, timestamp) return userMessage(role, content, timestamp)
end 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 struct assistantMessage <: agentMessage # Message from the AI assistant
role::String # Always "assistant" role::String # Always "assistant"
content::Vector{messageContent} # Text and/or image content content::Vector{messageContent} # Text and/or image content
@@ -434,13 +480,13 @@ end
Context passed to the `beforeToolCall` hook. Context passed to the `beforeToolCall` hook.
# Arguments # 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 - `toolCall::agentToolCall`: The tool call being prepared
- `args::Dict{String,Any}`: Validated tool arguments - `args::Dict{String,Any}`: Validated tool arguments
- `context::agentContext`: Current conversation context - `context::agentContext`: Current conversation context
""" """
struct beforeToolCallContext struct beforeToolCallContext
message::assistantMessage message::assistantMessageToolCall
toolCall::agentToolCall toolCall::agentToolCall
args::Dict{String,Any} args::Dict{String,Any}
context::agentContext context::agentContext
@@ -455,7 +501,7 @@ end
Context passed to the `afterToolCall` hook. Context passed to the `afterToolCall` hook.
# Arguments # 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 - `toolCall::agentToolCall`: The tool call that was executed
- `args::Dict{String,Any}`: Tool arguments - `args::Dict{String,Any}`: Tool arguments
- `result::agentToolResult`: The raw tool result - `result::agentToolResult`: The raw tool result
@@ -463,7 +509,7 @@ Context passed to the `afterToolCall` hook.
- `context::agentContext`: Current conversation context - `context::agentContext`: Current conversation context
""" """
struct afterToolCallContext struct afterToolCallContext
message::assistantMessage message::assistantMessageToolCall
toolCall::agentToolCall toolCall::agentToolCall
args::Dict{String,Any} args::Dict{String,Any}
result::agentToolResult result::agentToolResult
+40
View File
@@ -221,6 +221,8 @@ function formatMsgForLLM(ctx::agentContext, agentEventSink)::Dict{String, Any}
for msg in ctx.messages for msg in ctx.messages
if msg isa userMessage if msg isa userMessage
push!(messages, _userMessageToOpenAI(msg)) push!(messages, _userMessageToOpenAI(msg))
elseif msg isa assistantMessageToolCall
push!(messages, _assistantMessageToolCallToOpenAI(msg))
elseif msg isa assistantMessage elseif msg isa assistantMessage
push!(messages, _assistantMessageToOpenAI(msg)) push!(messages, _assistantMessageToOpenAI(msg))
elseif msg isa toolResultMessage elseif msg isa toolResultMessage
@@ -335,6 +337,44 @@ function _userMessageToOpenAI(msg::userMessage)::Dict{String, Any}
end 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. Convert an assistantMessage to OpenAI message format.
""" """