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
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
+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?
+107 -90
View File
@@ -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,
+52 -6
View File
@@ -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
+40
View File
@@ -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}