V0.9.1 fix ghost response #57
+1
-1
@@ -1,6 +1,6 @@
|
||||
name = "YiemAgent"
|
||||
uuid = "e012c34b-7f78-48e0-971c-7abb83b6f0a2"
|
||||
version = "0.9.0"
|
||||
version = "0.9.1"
|
||||
authors = ["narawat lamaiin <narawat@outlook.com>"]
|
||||
|
||||
[deps]
|
||||
|
||||
+49
-7
@@ -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
|
||||
|
||||
|
||||
+3
-1
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user