This commit is contained in:
2026-10-02 21:43:10 +07:00
parent befbf62e3b
commit 48d89033cc
2 changed files with 49 additions and 42 deletions
+46 -41
View File
@@ -170,7 +170,7 @@ function yiemAgent(
tools = OrderedDict{String, agentTool}() tools = OrderedDict{String, agentTool}()
registerAllTools(tools, mcpServer; eventSink=eventSink, sessionId=sessionId) registerAllTools(tools, mcpServer; eventSink=eventSink, sessionId=sessionId)
eventSink("instantiate an agent")
# Create struct with a placeholder task, then spawn and replace it # Create struct with a placeholder task, then spawn and replace it
agent = yiemAgent( agent = yiemAgent(
agentState(systemPrompt, model, tools, messages), agentState(systemPrompt, model, tools, messages),
@@ -194,7 +194,7 @@ function yiemAgent(
# Spawn the background loop and attach it # Spawn the background loop and attach it
agent._agentLoop = @spawn _agentLoop(agent) agent._agentLoop = @spawn _agentLoop(agent)
eventSink("agent instantiated")
return agent return agent
end end
@@ -260,35 +260,36 @@ function _agentLoop(agent::yiemAgent)
agent.inputChannel -> nothing agent.inputChannel -> nothing
agent.followUpChannel -> nothing agent.followUpChannel -> nothing
""" """
agent.eventSink("_agentLoop starting")
while true while true
while newUserMsg === nothing while newUserMsg === nothing
if isready(agent.inputChannel) if isready(agent.inputChannel)
# 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.eventSink("take new user message")
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.eventSink("_processMessage() done") agent.eventSink("_processMessage() done")
# 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)
while isready(agent.followUpChannel) while isready(agent.followUpChannel)
# agent.eventSink("put followUp message to input channel") agent.eventSink("put followUp message to input channel")
followUpMsg = take!(agent.followUpChannel) followUpMsg = take!(agent.followUpChannel)
put!(agent.inputChannel, followUpMsg) put!(agent.inputChannel, followUpMsg)
end end
processingTask = nothing # reset processingTask = nothing # reset
result = nothing # reset result = nothing # reset
else # _processMessage() done and no followUp message. else # _processMessage() done and no followUp message.
# agent.eventSink("no followUp message") agent.eventSink("no followUp message")
result = deepcopy(agent._state.messages[end]) result = deepcopy(agent._state.messages[end])
# filter out reasoningContent in-place # filter out reasoningContent in-place
filter!(c -> !(c isa reasoningContent), result.content) filter!(c -> !(c isa reasoningContent), result.content)
# format output # format output
respondToUI = _assistantMessageToOpenAI(result) respondToUI = _assistantMessageToOpenAI(result)
put!(agent.outputChannel, respondToUI) put!(agent.outputChannel, respondToUI)
# agent.eventSink("put result to output channel") agent.eventSink("put result to output channel")
processingTask = nothing # reset processingTask = nothing # reset
result = nothing # reset result = nothing # reset
end end
@@ -318,6 +319,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.eventSink("spawn new _processMessage()")
# discard all messages in followUpChannel # discard all messages in followUpChannel
while isready(agent.followUpChannel) while isready(agent.followUpChannel)
_ = take!(agent.followUpChannel) _ = take!(agent.followUpChannel)
@@ -339,8 +341,10 @@ function _agentLoop(agent::yiemAgent)
) )
end end
put!(processMessageInputCh, newUserMsg) put!(processMessageInputCh, newUserMsg)
agent.eventSink("put new user msg to processMessageInputCh")
newUserMsg = nothing # reset newUserMsg = nothing # reset
end end
end end
catch e catch e
bt = catch_backtrace() bt = catch_backtrace()
@@ -348,7 +352,7 @@ function _agentLoop(agent::yiemAgent)
showerror(io, e, bt) showerror(io, e, bt)
println(io) println(io)
end end
# agent.eventSink(errMsg; publish=true) agent.eventSink(errMsg; publish=true)
# On any error, send error response and exit the loop # On any error, send error response and exit the loop
@error "Agent loop failed" error=e @error "Agent loop failed" error=e
@@ -399,7 +403,7 @@ function _processMessage(
# loop until llmCall() response didn't use tool calls # loop until llmCall() response didn't use tool calls
final_response = nothing final_response = nothing
# eventSink("▶ _processMessage: entered") eventSink("▶ _processMessage: entered")
""" example message in inputChannel """ example message in inputChannel
Dict( Dict(
@@ -426,7 +430,7 @@ function _processMessage(
end end
newUserMsg = OpenAiToUserMessage(newUserMsg_openai) newUserMsg = OpenAiToUserMessage(newUserMsg_openai)
push!(agentMsgHistory, newUserMsg) push!(agentMsgHistory, newUserMsg)
# eventSink("▶ _processMessage: user msg pushed") eventSink("▶ _processMessage: user msg pushed")
end end
# call prepareContext() # call prepareContext()
state = agentState(systemPrompt, nothing, tools, agentMsgHistory) state = agentState(systemPrompt, nothing, tools, agentMsgHistory)
@@ -434,7 +438,8 @@ function _processMessage(
# Call formatMessagesForLLM() to format for LLM # Call formatMessagesForLLM() to format for LLM
formattedMessages = formatMessagesForLLM(preparedContext, eventSink) formattedMessages = formatMessagesForLLM(preparedContext, eventSink)
# eventSink("▶ _processMessage: context") eventSink("▶ _processMessage: context")
eventSink("Thinking...")
""" 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")]))
""" """
@@ -445,10 +450,10 @@ function _processMessage(
# Add assistant message (tool calls or text) to history for next LLM turn # Add assistant message (tool calls or text) to history for next LLM turn
push!(agentMsgHistory, assistant_msg) push!(agentMsgHistory, assistant_msg)
# eventSink("▶ _processMessage: LLM response") eventSink("▶ _processMessage: LLM response")
if hasToolCalls && length(toolCallList) > 0 if hasToolCalls && length(toolCallList) > 0
# eventSink("▶ _processMessage: tool calls") eventSink("▶ _processMessage: tool calls")
# Build context and config for executeToolCalls # Build context and config for executeToolCalls
@@ -462,13 +467,13 @@ function _processMessage(
# call executeToolCalls() # call executeToolCalls()
toolResultBatch = executeToolCalls(preparedContext, assistant_msg, toolCallList, config, toolResultBatch = executeToolCalls(preparedContext, assistant_msg, toolCallList, config,
signal, eventSink) signal, eventSink)
# eventSink("▶ _processMessage: tool batch") eventSink("▶ _processMessage: tool batch")
# 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
if toolResultBatch.terminate if toolResultBatch.terminate
# eventSink("▶ _processMessage: TERMINATE branch") eventSink("▶ _processMessage: TERMINATE branch")
# 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
@@ -500,7 +505,7 @@ function _processMessage(
break break
end end
else else
# eventSink("▶ _processMessage: FINAL response") eventSink("▶ _processMessage: FINAL response")
# LLM did not use tool calls — # LLM did not use tool calls —
break break
end end
@@ -511,12 +516,12 @@ function _processMessage(
println(io) println(io)
end end
# eventSink(err_msg; publish=true) eventSink(err_msg; publish=true)
# eventSink("▶ _processMessage: CAUGHT ERROR") eventSink("▶ _processMessage: CAUGHT ERROR")
end end
end end
# eventSink("▶ _processMessage: exited") eventSink("▶ _processMessage: exited")
return nothing return nothing
end end
@@ -629,9 +634,9 @@ The message is constructed from:
function _extractToolCalls(response, eventSink=nothing) function _extractToolCalls(response, eventSink=nothing)
hasToolCalls = false hasToolCalls = false
toolCallList = agentToolCall[] toolCallList = agentToolCall[]
# if eventSink !== nothing if eventSink !== nothing
# eventSink("▶ _extractToolCalls: parsing response") eventSink("▶ _extractToolCalls: parsing response")
# end end
# Helper: parse args (JSON string -> Dict, or pass through) # Helper: parse args (JSON string -> Dict, or pass through)
parse_args(raw) = raw isa AbstractDict && !(raw isa Dict{String,Any}) ? parse_args(raw) = raw isa AbstractDict && !(raw isa Dict{String,Any}) ?
@@ -961,11 +966,11 @@ function prepareToolCall(
)::Union{preparedToolCall,immediateOutcome} )::Union{preparedToolCall,immediateOutcome}
# pick a called tool from tool store # pick a called tool from tool store
# eventSink("▶ prepareToolCall: resolving") eventSink("▶ prepareToolCall: resolving")
tool = get(context.tools, toolCall.name, nothing) tool = get(context.tools, toolCall.name, nothing)
if tool === nothing if tool === nothing
# eventSink("▶ prepareToolCall: NOT FOUND") eventSink("▶ prepareToolCall: NOT FOUND")
return immediateOutcome(createErrorToolResult("Tool $toolCall.name not found"), true) return immediateOutcome(createErrorToolResult("Tool $toolCall.name not found"), true)
end end
@@ -1053,10 +1058,10 @@ function executePreparedToolCall(
eventSink, eventSink,
)::executedOutcome )::executedOutcome
# eventSink("▶ execute: START") eventSink("▶ execute: START")
try try
result = prep.tool.execute(prep.toolCall.id, prep.args, signal, eventSink) result = prep.tool.execute(prep.toolCall.id, prep.args, signal, eventSink)
# eventSink("▶ execute: END") eventSink("▶ execute: END")
return executedOutcome(result, false) return executedOutcome(result, false)
catch e catch e
bt = catch_backtrace() bt = catch_backtrace()
@@ -1064,8 +1069,8 @@ function executePreparedToolCall(
showerror(io, e, bt) showerror(io, e, bt)
println(io) println(io)
end end
# eventSink(errMsg; publish=true) eventSink(errMsg; publish=true)
# eventSink("▶ execute: ERROR") eventSink("▶ execute: ERROR")
return executedOutcome(createErrorToolResult(sprint(showerror, e)), true) return executedOutcome(createErrorToolResult(sprint(showerror, e)), true)
end end
@@ -1139,7 +1144,7 @@ function finalizeExecutedToolCall(
eventSink eventSink
)::finalizedOutcome )::finalizedOutcome
# eventSink("▶ finalize: START") eventSink("▶ finalize: START")
result = executed.result result = executed.result
isError = executed.isError isError = executed.isError
if config.afterToolCall !== nothing if config.afterToolCall !== nothing
@@ -1165,7 +1170,7 @@ function finalizeExecutedToolCall(
isError = true isError = true
end end
end end
# eventSink("▶ finalize: END") eventSink("▶ finalize: END")
return finalizedOutcome(prep.toolCall, result, isError) return finalizedOutcome(prep.toolCall, result, isError)
end end
@@ -1233,7 +1238,7 @@ function executeToolCallsSequential(
)::agentToolCallBatch )::agentToolCallBatch
finalizedCalls = finalizedOutcome[] finalizedCalls = finalizedOutcome[]
messages = toolResultMessage[] messages = toolResultMessage[]
# eventSink("▶ executeToolCallsSequential: batch start") eventSink("▶ executeToolCallsSequential: batch start")
for tc in toolCalls for tc in toolCalls
prep = prepareToolCall(context, assistantMsg, tc, config, signal, eventSink) prep = prepareToolCall(context, assistantMsg, tc, config, signal, eventSink)
@@ -1251,7 +1256,7 @@ function executeToolCallsSequential(
break break
end end
end end
# eventSink("▶ executeToolCallsSequential: batch end") eventSink("▶ executeToolCallsSequential: batch end")
return agentToolCallBatch(messages, shouldTerminate(finalizedCalls)) return agentToolCallBatch(messages, shouldTerminate(finalizedCalls))
end end
@@ -1322,7 +1327,7 @@ function executeToolCallsParallel(
)::agentToolCallBatch )::agentToolCallBatch
entries = Union{finalizedOutcome,Task}[] entries = Union{finalizedOutcome,Task}[]
# eventSink("▶ executeToolCallsParallel: batch start") eventSink("▶ executeToolCallsParallel: batch start")
for tc in toolCalls for tc in toolCalls
toolExecStartEvent(tc.id, tc.name, tc.arguments) toolExecStartEvent(tc.id, tc.name, tc.arguments)
@@ -1362,7 +1367,7 @@ function executeToolCallsParallel(
push!(messages, createToolResultMessage(f)) push!(messages, createToolResultMessage(f))
end end
# eventSink("▶ executeToolCallsParallel: batch end") eventSink("▶ executeToolCallsParallel: batch end")
return agentToolCallBatch(messages, shouldTerminate(finalizedCalls)) return agentToolCallBatch(messages, shouldTerminate(finalizedCalls))
end end
@@ -1433,7 +1438,7 @@ function executeToolCalls(
break break
end end
end end
# eventSink("▶ executeToolCalls: dispatch") eventSink("▶ executeToolCalls: dispatch")
if config.toolExecution == "sequential" || hasSequential if config.toolExecution == "sequential" || hasSequential
return executeToolCallsSequential(context, assistantMsg, toolCalls, config, signal, return executeToolCallsSequential(context, assistantMsg, toolCalls, config, signal,
eventSink) eventSink)
@@ -1480,9 +1485,9 @@ OpenAiToUserMessage(msg)
""" """
function OpenAiToUserMessage(msg::AbstractDict, eventSink=nothing)::userMessage function OpenAiToUserMessage(msg::AbstractDict, eventSink=nothing)::userMessage
content_blocks = Vector{messageContent}() content_blocks = Vector{messageContent}()
# if eventSink !== nothing if eventSink !== nothing
# eventSink("▶ OpenAiToUserMessage: parsing") eventSink("▶ OpenAiToUserMessage: parsing")
# end end
raw_content = get(msg, "content", Any[]) raw_content = get(msg, "content", Any[])
if raw_content isa Vector if raw_content isa Vector
@@ -1506,9 +1511,9 @@ function OpenAiToUserMessage(msg::AbstractDict, eventSink=nothing)::userMessage
end end
end end
# if eventSink !== nothing if eventSink !== nothing
# eventSink("▶ OpenAiToUserMessage: done") eventSink("▶ OpenAiToUserMessage: done")
# end end
return userMessage(content=content_blocks) return userMessage(content=content_blocks)
end end
+3 -1
View File
@@ -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 function registerTool(tools::OrderedDict{String, agentTool}, tool::agentTool; eventSink=nothing
)::OrderedDict{String, agentTool} )::OrderedDict{String, agentTool}
tools[tool.name] = tool tools[tool.name] = tool
println("[toolRegistry] Registered tool: $(tool.name)") eventSink("[toolRegistry] Registered tool: $(tool.name)")
return tools return tools
end end
@@ -282,7 +282,9 @@ Register all tools for an agent:
""" """
function registerAllTools(tools::OrderedDict{String, agentTool}, mcpserver=nothing; eventSink=nothing, sessionId=nothing) function registerAllTools(tools::OrderedDict{String, agentTool}, mcpserver=nothing; eventSink=nothing, sessionId=nothing)
list_t = listTool(tools, mcpserver; eventSink=eventSink, sessionId=sessionId) list_t = listTool(tools, mcpserver; eventSink=eventSink, sessionId=sessionId)
eventSink("registering tools")
registerTool(tools, list_t; eventSink=eventSink) registerTool(tools, list_t; eventSink=eventSink)
eventSink("registering tools done")
end end
end # module end # module