This commit is contained in:
2026-08-21 13:13:38 +07:00
parent c59f6bfa61
commit da21790263
8 changed files with 276 additions and 323 deletions
+101 -106
View File
@@ -9,11 +9,6 @@ using JSON, DataStructures, Dates, UUIDs, HTTP, Random, PrettyPrinting, Serializ
using GeneralUtils
using ..type, ..utils, ..toolRegistry
function register_all_tools(store::toolRegistry.toolStore, mcpServer=nothing)
# Call parent module's version which has access to tool functions
parentmodule(@__MODULE__).register_all_tools(store, mcpServer)
end
# ---------------------------------------------- 100 --------------------------------------------- #
"""
@@ -74,7 +69,7 @@ mutable struct yiemAgent <: agent # High-level agent wrapper
# return incoming_env["payloads"][1][2]
# end
#
# function (c::MyMCPClient)(method::String, toolName::String, arguments::Dict{String,Any})
# function (c::MyMCPClient)(method::String, toolName::String, arguments::AbstractDict{String, Any})
# payload = Dict("jsonrpc" => "2.0", "id" => 2, "method" => method,
# "params" => Dict("name" => toolName, "arguments" => arguments))
# payloads = [("payload", payload, "dictionary"),]
@@ -137,7 +132,7 @@ mutable struct yiemAgent <: agent # High-level agent wrapper
sessionId::Union{String, Nothing} # Optional session identifier
maxRetryDelayMs::Union{Int64, Nothing} # Maximum delay between retries (ms)
parallelToolExecute::Bool # Default: false
agentEventSink # agent emits its status via this function
eventSink # agent emits its status via this function
end
"""
@@ -161,7 +156,7 @@ on `inputChannel` and `followUpChannel` channels concurrently.
- `sessionId::Union{String, Nothing}`: Optional session identifier (default: `nothing`)
- `maxRetryDelayMs::Union{Int64, Nothing}`: Maximum delay between retries in milliseconds (default: `nothing`)
- `parallelToolExecute::Bool`: Run tool calls in parallel (default: `false`)
- `agentEventSink::Function`: Callback to receive agent events
- `eventSink::Function`: Callback to receive agent events
- `mcpServer`: Callable struct for MCP server communication. Called as
`mcpServer("tools/list")` to discover tools, or `mcpServer("tools/call", args)`
to execute a tool. Returns parsed JSON dicts. (default: `nothing`)
@@ -184,7 +179,7 @@ function yiemAgent(
sessionId::Union{String, Nothing}=nothing,
maxRetryDelayMs::Union{Int64, Nothing}=nothing,
parallelToolExecute::Bool=false,
agentEventSink=agentEventSink,
eventSink=eventSink,
mcpServer=nothing,
)
# Create channels: input (user -> agent), followUp (async queue), output (agent -> user)
@@ -194,7 +189,7 @@ function yiemAgent(
# load tools (statically registered at module init)
toolStore1 = toolStore(name="myagent")
register_all_tools(toolStore1, mcpServer)
registerAllTools(toolStore1, mcpServer; eventSink=eventSink)
# Create struct with a placeholder task, then spawn and replace it
agent = yiemAgent(
@@ -214,7 +209,7 @@ function yiemAgent(
sessionId,
maxRetryDelayMs,
parallelToolExecute,
agentEventSink,
eventSink,
)
# Spawn the background loop and attach it
@@ -289,18 +284,18 @@ function _agentLoop(agent::yiemAgent)
while true
while newUserMsg === nothing
if isready(agent.inputChannel)
agent.agentEventSink("_agentLoop 1 agent._state.messages length $(length(agent._state.messages))")
agent.eventSink("_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")
agent.eventSink("new user msg")
else
# check followUp message after _processMessage() is done
if typeof(processingTask) == Task && istaskdone(processingTask) == true
agent.agentEventSink("_agentLoop 2 agent._state.messages length $(length(agent._state.messages))")
agent.eventSink("_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._state.messages length $(length(agent._state.messages))")
agent.eventSink("_agentLoop 3 agent._state.messages length $(length(agent._state.messages))")
while isready(agent.followUpChannel)
followUpMsg = take!(agent.followUpChannel)
put!(agent.inputChannel, followUpMsg)
@@ -308,18 +303,18 @@ function _agentLoop(agent::yiemAgent)
processingTask = nothing # reset
result = nothing # reset
else # _processMessage() done and no followUp message.
agent.agentEventSink("_agentLoop 4 agent._state.messages length $(length(agent._state.messages))")
agent.eventSink("_agentLoop 4 agent._state.messages length $(length(agent._state.messages))")
result = deepcopy(agent._state.messages[end])
agent.agentEventSink("_agentLoop 4-1 ")
agent.eventSink("_agentLoop 4-1 ")
# filter out reasoningContent in-place
filter!(c -> !(c isa reasoningContent), result.content)
agent.agentEventSink("_agentLoop 5 ")
agent.eventSink("_agentLoop 5 ")
# format output
respondToUI = _assistantMessageToOpenAI(result)
agent.agentEventSink("_agentLoop 6 ")
agent.eventSink("_agentLoop 6 ")
put!(agent.outputChannel, respondToUI)
if !isempty(result.content) && result.content[1] isa textContent
agent.agentEventSink(result.content[1].text)
agent.eventSink(result.content[1].text)
end
processingTask = nothing # reset
result = nothing # reset
@@ -350,7 +345,7 @@ function _agentLoop(agent::yiemAgent)
else
# spawn new _processMessage() if it is not already running.
if processingTask === nothing
agent.agentEventSink("_agentLoop 5 agent._state.messages length $(length(agent._state.messages))")
agent.eventSink("_agentLoop 5 agent._state.messages length $(length(agent._state.messages))")
# discard all messages in followUpChannel
while isready(agent.followUpChannel)
_ = take!(agent.followUpChannel)
@@ -359,7 +354,7 @@ function _agentLoop(agent::yiemAgent)
# Dispatch message through the processing pipeline
processingTask = @spawn _processMessage(
processMessageInputCh,
agent.agentEventSink,
agent.eventSink,
agent._state.messages,
agent._state.systemPrompt,
agent._state.tools,
@@ -381,7 +376,7 @@ function _agentLoop(agent::yiemAgent)
showerror(io, e, bt)
println(io)
end
agentEventSink(errMsg)
eventSink(errMsg)
# On any error, send error response and exit the loop
@error "Agent loop failed" error=e
@@ -418,7 +413,7 @@ julia> # Currently returns a placeholder echo response
"""
function _processMessage(
inputChannel::Channel,
agentEventSink,
eventSink,
agentMsgHistory::Vector{agentMessage},
systemPrompt::String,
tools::OrderedDict{String, agentTool},
@@ -429,7 +424,7 @@ function _processMessage(
afterToolCall::Union{Function, Nothing},
parallelToolExecute::Bool,
)::Nothing
agentEventSink("_processMessage 1 _state.messages length $(length(agentMsgHistory))")
eventSink("_processMessage 1 _state.messages length $(length(agentMsgHistory))")
# loop until llmCall() response didn't use tool calls
final_response = nothing
@@ -449,43 +444,43 @@ function _processMessage(
while true
# Drain inputChannel and convert OpenAI-format messages to userMessage type
while isready(inputChannel)
agentEventSink("_processMessage 2 _state.messages length $(length(agentMsgHistory))")
eventSink("_processMessage 2 _state.messages length $(length(agentMsgHistory))")
newUserMsg_openai = take!(inputChannel)
agentEventSink("_processMessage 3 _state.messages length $(length(agentMsgHistory))")
eventSink("_processMessage 3 _state.messages length $(length(agentMsgHistory))")
if newUserMsg_openai === :shutdown
agentEventSink("_processMessage 4 _state.messages length $(length(agentMsgHistory))")
eventSink("_processMessage 4 _state.messages length $(length(agentMsgHistory))")
# Re-emit shutdown signal for the loop to handle
put!(inputChannel, :shutdown)
break
end
agentEventSink("_processMessage 5 _state.messages length $(length(agentMsgHistory))")
eventSink("_processMessage 5 _state.messages length $(length(agentMsgHistory))")
newUserMsg = OpenAiToUserMessage(newUserMsg_openai)
push!(agentMsgHistory, newUserMsg)
agentEventSink("_processMessage 6 _state.messages length $(length(agentMsgHistory))")
eventSink("_processMessage 6 _state.messages length $(length(agentMsgHistory))")
end
agentEventSink("_processMessage 7 _state.messages length $(length(agentMsgHistory))")
eventSink("_processMessage 7 _state.messages length $(length(agentMsgHistory))")
# call prepareContext()
state = agentState(systemPrompt, nothing, tools, agentMsgHistory)
agentEventSink("_processMessage 8 _state.messages length $(length(agentMsgHistory))")
preparedContext = prepareContext(state, agentEventSink, llmCall)
agentEventSink("_processMessage 9 _state.messages length $(length(agentMsgHistory))")
eventSink("_processMessage 8 _state.messages length $(length(agentMsgHistory))")
preparedContext = prepareContext(state, eventSink, llmCall)
eventSink("_processMessage 9 _state.messages length $(length(agentMsgHistory))")
# Call formatMessagesForLLM() to format for LLM
formattedMessages = formatMessagesForLLM(preparedContext, agentEventSink)
formattedMessages = formatMessagesForLLM(preparedContext, eventSink)
agentEventSink("_processMessage 10 formattedMessages $formattedMessages")
eventSink("_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(" llmCall " * string(response))
eventSink(" llmCall " * string(response))
agentEventSink("_processMessage 11 _state.messages length $(length(agentMsgHistory))")
eventSink("_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 _state.messages length $(length(agentMsgHistory))")
agentEventSink("assistant_msg " * string(assistant_msg))
agentEventSink("_processMessage 11-1")
eventSink("hasToolCalls: $hasToolCalls\ntoolCallList: $toolCallList _state.messages length $(length(agentMsgHistory))")
eventSink("assistant_msg " * string(assistant_msg))
eventSink("_processMessage 11-1")
# Add assistant message (tool calls or text) to history for next LLM turn
push!(agentMsgHistory, assistant_msg)
@@ -501,21 +496,21 @@ function _processMessage(
)
signal = abortSignal(false)
agentEventSink("_processMessage 12 _state.messages length $(length(agentMsgHistory))")
eventSink("_processMessage 12 _state.messages length $(length(agentMsgHistory))")
# call executeToolCalls()
toolResultBatch = executeToolCalls(preparedContext, assistant_msg, toolCallList, config,
signal, agentEventSink)
agentEventSink("_processMessage 13 _state.messages length $(length(agentMsgHistory))")
signal, eventSink)
eventSink("_processMessage 13 _state.messages length $(length(agentMsgHistory))")
# save toolResults to messages
for toolResult in toolResultBatch.messages
agentEventSink("toolResult " * string(toolResult))
eventSink("toolResult " * string(toolResult))
push!(agentMsgHistory, toolResult)
end
agentEventSink("_processMessage 14 _state.messages length $(length(agentMsgHistory))")
eventSink("_processMessage 14 _state.messages length $(length(agentMsgHistory))")
if toolResultBatch.terminate
agentEventSink("_processMessage 15 _state.messages length $(length(agentMsgHistory))")
eventSink("_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
@@ -529,7 +524,7 @@ function _processMessage(
end
end
end
agentEventSink("_processMessage 16 _state.messages length $(length(agentMsgHistory))")
eventSink("_processMessage 16 _state.messages length $(length(agentMsgHistory))")
final_response = assistantMessage(
role="assistant",
content=final_content,
@@ -548,12 +543,12 @@ function _processMessage(
break
end
else
agentEventSink("_processMessage 17 _state.messages length $(length(agentMsgHistory))")
eventSink("_processMessage 17 _state.messages length $(length(agentMsgHistory))")
# LLM did not use tool calls —
break
end
end
agentEventSink("_processMessage 18 _state.messages length $(length(agentMsgHistory))")
eventSink("_processMessage 18 _state.messages length $(length(agentMsgHistory))")
return nothing
end
@@ -991,48 +986,48 @@ function prepareToolCall(
toolCall::agentToolCall,
config::agentLoopConfig,
signal::abortSignal,
agentEventSink
eventSink
)::Union{preparedToolCall,immediateOutcome}
agentEventSink("prepareToolCall 1")
eventSink("prepareToolCall 1")
# pick a called tool from tool store
tool = get(context.tools, toolCall.name, nothing)
if tool === nothing
agentEventSink("prepareToolCall 2")
eventSink("prepareToolCall 2")
return immediateOutcome(createErrorToolResult("Tool $toolCall.name not found"), true)
end
try
agentEventSink("prepareToolCall 3")
eventSink("prepareToolCall 3")
# 1. prepare arguments (tool-specific transform)
prepared = prepareToolCallArguments(tool, toolCall)
agentEventSink("prepared " * string(prepared.arguments))
agentEventSink("prepareToolCall 4")
eventSink("prepared " * string(prepared.arguments))
eventSink("prepareToolCall 4")
validatedArgs = validateToolArguments(tool, prepared)
agentEventSink("validatedArgs " * string(validatedArgs))
agentEventSink("prepareToolCall 5")
eventSink("validatedArgs " * string(validatedArgs))
eventSink("prepareToolCall 5")
# 2. beforeToolCall hook — can block
if config.beforeToolCall !== nothing
agentEventSink("prepareToolCall 6")
eventSink("prepareToolCall 6")
before = config.beforeToolCall(
beforeToolCallContext(assistantMsg, toolCall, validatedArgs, context),
signal
)
agentEventSink("prepareToolCall 7")
eventSink("prepareToolCall 7")
if signal.aborted
agentEventSink("prepareToolCall 8")
eventSink("prepareToolCall 8")
return immediateOutcome(createErrorToolResult("Operation aborted"), true)
end
if before !== nothing && before.block
agentEventSink("prepareToolCall 9")
eventSink("prepareToolCall 9")
return immediateOutcome(
createErrorToolResult(get(before, :reason, "Tool execution was blocked")), true)
end
end
agentEventSink("prepareToolCall 10")
eventSink("prepareToolCall 10")
return preparedToolCall(tool, toolCall, validatedArgs)
catch e
bt = catch_backtrace()
@@ -1041,7 +1036,7 @@ function prepareToolCall(
println(io)
end
agentEventSink(errMsg)
eventSink(errMsg)
return immediateOutcome(createErrorToolResult(sprint(showerror, e)), true)
end
@@ -1091,16 +1086,16 @@ executePreparedToolCall(prep, nothing, emit)
function executePreparedToolCall(
prep::preparedToolCall,
signal::Union{Nothing,abortSignal},
agentEventSink,
eventSink,
llmCall::Union{Any,Nothing}=nothing,
)::executedOutcome
agentEventSink("executePreparedToolCall 1")
eventSink("executePreparedToolCall 1")
try #WORKING
result = prep.tool.execute(prep.toolCall.id, prep.args, signal, agentEventSink, llmCall)
agentEventSink("executePreparedToolCall 2")
agentEventSink(result.content[1].text)
agentEventSink("executePreparedToolCall 3")
result = prep.tool.execute(prep.toolCall.id, prep.args, signal, eventSink, llmCall)
eventSink("executePreparedToolCall 2")
eventSink(result.content[1].text)
eventSink("executePreparedToolCall 3")
return executedOutcome(result, false)
catch e
bt = catch_backtrace()
@@ -1108,7 +1103,7 @@ function executePreparedToolCall(
showerror(io, e, bt)
println(io)
end
agentEventSink(errMsg)
eventSink(errMsg)
return executedOutcome(createErrorToolResult(sprint(showerror, e)), true)
end
@@ -1178,19 +1173,19 @@ function finalizeExecutedToolCall(
executed::executedOutcome,
config::agentLoopConfig,
signal::Union{Nothing,abortSignal},
agentEventSink
eventSink
)::finalizedOutcome
agentEventSink("finalizeExecutedToolCall 1")
eventSink("finalizeExecutedToolCall 1")
result = executed.result
isError = executed.isError
agentEventSink("finalizeExecutedToolCall 2")
eventSink("finalizeExecutedToolCall 2")
if config.afterToolCall !== nothing
try
after = config.afterToolCall(
afterToolCallContext(assistantMsg, prep.toolCall, prep.args, result, isError, context),
signal
)
agentEventSink("finalizeExecutedToolCall 3")
eventSink("finalizeExecutedToolCall 3")
if after !== nothing
result = merge(result, dict(:content=>get(after,:content,result.content),
:details=>get(after,:details,result.details),
@@ -1204,13 +1199,13 @@ function finalizeExecutedToolCall(
showerror(io, e, bt)
println(io)
end
agentEventSink(errMsg)
eventSink(errMsg)
result = createErrorToolResult(sprint(showerror, e))
isError = true
end
end
agentEventSink("finalizeExecutedToolCall 4")
eventSink("finalizeExecutedToolCall 4")
return finalizedOutcome(prep.toolCall, result, isError)
end
@@ -1274,41 +1269,41 @@ function executeToolCallsSequential(
toolCalls::Vector{agentToolCall},
config::agentLoopConfig,
signal::abortSignal,
agentEventSink,
eventSink,
)::agentToolCallBatch
llmCall = config.llmCall
agentEventSink("executeToolCallsSequential 1")
eventSink("executeToolCallsSequential 1")
finalizedCalls = finalizedOutcome[]
messages = toolResultMessage[]
for tc in toolCalls
agentEventSink("start tool execute $(tc.id), $(tc.name), $(tc.arguments)")
prep = prepareToolCall(context, assistantMsg, tc, config, signal, agentEventSink)
agentEventSink("executeToolCallsSequential " * string(prep.args))
eventSink("start tool execute $(tc.id), $(tc.name), $(tc.arguments)")
prep = prepareToolCall(context, assistantMsg, tc, config, signal, eventSink)
eventSink("executeToolCallsSequential " * string(prep.args))
if prep isa immediateOutcome
agentEventSink("executeToolCallsSequential 2-1")
eventSink("executeToolCallsSequential 2-1")
finalized = finalizedOutcome(tc, prep.result, prep.isError)
agentEventSink("executeToolCallsSequential 2-2")
eventSink("executeToolCallsSequential 2-2")
else
agentEventSink("executeToolCallsSequential 3")
executed = executePreparedToolCall(prep, signal, agentEventSink, llmCall)
agentEventSink("executeToolCallsSequential 3-1")
eventSink("executeToolCallsSequential 3")
executed = executePreparedToolCall(prep, signal, eventSink, llmCall)
eventSink("executeToolCallsSequential 3-1")
finalized = finalizeExecutedToolCall(context, assistantMsg, prep, executed, config,
signal, agentEventSink)
agentEventSink("executeToolCallsSequential 3-2")
signal, eventSink)
eventSink("executeToolCallsSequential 3-2")
end
agentEventSink("executeToolCallsSequential 4")
agentEventSink("finalized $(finalized.toolCall.id), $(finalized.toolCall.name),
eventSink("executeToolCallsSequential 4")
eventSink("finalized $(finalized.toolCall.id), $(finalized.toolCall.name),
$(finalized.result), $(finalized.isError)")
push!(messages, createToolResultMessage(finalized))
push!(finalizedCalls, finalized)
agentEventSink("executeToolCallsSequential 5")
eventSink("executeToolCallsSequential 5")
if signal !== nothing && signal.aborted
break
end
end
agentEventSink("executeToolCallsSequential 6")
eventSink("executeToolCallsSequential 6")
return agentToolCallBatch(messages, shouldTerminate(finalizedCalls))
end
@@ -1375,27 +1370,27 @@ function executeToolCallsParallel(
toolCalls::Vector{agentToolCall},
config::agentLoopConfig,
signal::abortSignal,
agentEventSink,
eventSink,
)::agentToolCallBatch
entries = Union{finalizedOutcome,Task}[]
llmCall = config.llmCall
for tc in toolCalls
agentEventSink(toolExecStartEvent(tc.id, tc.name, tc.arguments))
eventSink(toolExecStartEvent(tc.id, tc.name, tc.arguments))
prep = prepareToolCall(context, assistantMsg, tc, config, signal, agentEventSink)
prep = prepareToolCall(context, assistantMsg, tc, config, signal, eventSink)
if prep isa immediateOutcome
finalized = finalizedOutcome(tc, prep.result, prep.isError)
agentEventSink(toolExecEndEvent(finalized.toolCall.id, finalized.toolCall.name,
eventSink(toolExecEndEvent(finalized.toolCall.id, finalized.toolCall.name,
finalized.result, finalized.isError))
push!(entries, finalized)
else
t = Task() do
executed = executePreparedToolCall(prep, signal, agentEventSink, llmCall)
executed = executePreparedToolCall(prep, signal, eventSink, llmCall)
finalized = finalizeExecutedToolCall(context, assistantMsg, prep, executed, config, signal)
agentEventSink(toolExecEndEvent(finalized.toolCall.id, finalized.toolCall.name,
eventSink(toolExecEndEvent(finalized.toolCall.id, finalized.toolCall.name,
finalized.result, finalized.isError))
return finalized
end
@@ -1478,11 +1473,11 @@ function executeToolCalls(
toolCalls::Vector{agentToolCall},
config::agentLoopConfig,
signal::abortSignal,
agentEventSink,
eventSink,
)::agentToolCallBatch
llmCall = config.llmCall
agentEventSink("_executeToolCalls 1")
eventSink("_executeToolCalls 1")
hasSequential = false
for tc in toolCalls
t = get(context.tools, tc.name, nothing)
@@ -1491,15 +1486,15 @@ function executeToolCalls(
break
end
end
agentEventSink("_executeToolCalls 2")
eventSink("_executeToolCalls 2")
if config.toolExecution == "sequential" || hasSequential
agentEventSink("_executeToolCalls 3")
eventSink("_executeToolCalls 3")
return executeToolCallsSequential(context, assistantMsg, toolCalls, config, signal,
agentEventSink)
eventSink)
else
agentEventSink("_executeToolCalls 4")
eventSink("_executeToolCalls 4")
return executeToolCallsParallel(context, assistantMsg, toolCalls, config, signal,
agentEventSink)
eventSink)
end
end