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
+2 -2
View File
@@ -8,7 +8,7 @@ Julia framework for building agents with tool use and MCP (Model Context Protoco
2. Create a callable `mcpServer` struct that communicates with an MCP server via NATS
3. Create a `yiemAgent` with an LLM callable and MCP server:
```julia
agent = YiemAgent.yiemAgent(llmCall; mcpServer=mcpServer, agentEventSink=yourSink)
agent = YiemAgent.yiemAgent(llmCall; mcpServer=mcpServer, eventSink=yourSink)
```
4. Call `runAgent(agent, message)` then `takeResponse(agent)`
@@ -136,4 +136,4 @@ Tools are discovered dynamically via MCP server:
| `formatMsgForLLM` | `(ctx, sink) -> dict` | Convert agent context to LLM API format |
| `beforeToolCall` | `(context, signal) -> result` | Block/allow tool execution |
| `afterToolCall` | `(context, signal) -> result` | Post-process tool results |
| `agentEventSink` | `(msg) -> nothing` | Callback for agent events/debug messages |
| `eventSink` | `(msg) -> nothing` | Callback for agent events/debug messages |
+32 -32
View File
@@ -89,7 +89,7 @@ using YiemAgent, YiemAgent.type, YiemAgent.toolRegistry
# Create agent — tools are registered automatically via register_all_tools()
agent = yiemAgent(
llmCall = my_llm_call, # Function that calls the LLM API
agentEventSink = my_event_sink, # Function for TUI/logging
eventSink = my_event_sink, # Function for TUI/logging
)
```
@@ -98,7 +98,7 @@ agent = yiemAgent(
| Parameter | Type | Required | Purpose |
|-----------|------|----------|---------|
| `llmCall` | `Function` | **Yes** | `(messages::Dict) -> assistantMessage` — invokes the LLM |
| `agentEventSink` | `Function` | **Yes** | `(event) -> nothing` — receives tool lifecycle events |
| `eventSink` | `Function` | **Yes** | `(event) -> nothing` — receives tool lifecycle events |
| `systemPrompt` | `String` | No (default: "You are helpful assistant.") | System prompt text |
| `model` | `llmModel` | No | LLM model config |
| `messages` | `Vector{agentMessage}` | No (default: empty) | Initial conversation history |
@@ -179,7 +179,7 @@ Each phase has a single responsibility and produces an intermediate result:
| Phase | Function | Input | Output | Purpose |
|-------|----------|-------|--------|---------|
| Prepare | `prepareToolCall()` | `agentContext`, `assistantMessage`, `agentToolCall`, `agentLoopConfig`, `abortSignal` | `preparedToolCall` or `immediateOutcome` | Resolve tool, validate args, run pre-hook |
| Execute | `executePreparedToolCall()` | `preparedToolCall`, `abortSignal`, `agentEventSink` | `executedOutcome` | Call `tool.execute()`, stream partial results |
| Execute | `executePreparedToolCall()` | `preparedToolCall`, `abortSignal`, `eventSink` | `executedOutcome` | Call `tool.execute()`, stream partial results |
| Finalize | `finalizeExecutedToolCall()` | `agentContext`, `assistantMessage`, `preparedToolCall`, `executedOutcome`, `agentLoopConfig`, `abortSignal` | `finalizedOutcome` | Run post-hook, emit end event |
The pipeline ensures that **every tool call produces a result**, even on failure. Errors are captured as `immediateOutcome`, `executedOutcome`, or `finalizedOutcome` with `isError=true`, then converted to `toolResultMessage` objects that are fed back to the LLM conversation history.
@@ -222,7 +222,7 @@ end
```julia
execute(toolCallId::String,
args::Dict{String,Any},
args::AbstractDict{String, Any},
signal::Union{Nothing,abortSignal},
onPartialResult::Function)::agentToolResult
```
@@ -430,12 +430,12 @@ function _processMessage(agent::yiemAgent)::assistantMessage
# ── Step 2: Prepare context ─────────────────────────────────
state = agentState(systemPrompt, nothing, tools, messages)
preparedContext = prepareContext(state, agentEventSink)
preparedContext = prepareContext(state, eventSink)
# Default: deep copies systemPrompt, messages, tools from agentState → agentContext
# Override point: filter tools, inject context, modify system prompt
# ── Step 3: Format for LLM ──────────────────────────────────
formattedMessages = formatMsgForLLM(preparedContext, agentEventSink)
formattedMessages = formatMsgForLLM(preparedContext, eventSink)
# Converts agentContext → Dict("messages" => [...]) in OpenAI format
# Wraps systemPrompt as system role, converts each messageContent block
@@ -456,7 +456,7 @@ function _processMessage(agent::yiemAgent)::assistantMessage
signal = abortSignal(false)
# Execute tool calls (sequential or parallel)
batch = executeToolCalls(context, assistant_msg, toolCallList, config, signal, agentEventSink)
batch = executeToolCalls(context, assistant_msg, toolCallList, config, signal, eventSink)
# Save results to conversation history
for tool_result in batch.messages
@@ -611,7 +611,7 @@ function prepareToolCall(
function executePreparedToolCall(
prep::preparedToolCall,
signal::Union{Nothing, abortSignal},
agentEventSink,
eventSink,
)::executedOutcome
```
@@ -623,7 +623,7 @@ function executePreparedToolCall(
prep.toolCall.id,
prep.args,
signal,
agentEventSink # serves as onPartialResult callback
eventSink # serves as onPartialResult callback
)
return executedOutcome(result, false)
```
@@ -698,7 +698,7 @@ function executeToolCalls(
toolCalls::Vector{agentToolCall},
config::agentLoopConfig,
signal::Union{Nothing, abortSignal},
agentEventSink,
eventSink,
)::agentToolCallBatch
```
@@ -733,12 +733,12 @@ function executeToolCallsSequential(...)::agentToolCallBatch
messages = toolResultMessage[]
for tc in toolCalls
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)
else
executed = executePreparedToolCall(prep, signal, agentEventSink)
executed = executePreparedToolCall(prep, signal, eventSink)
finalized = finalizeExecutedToolCall(context, assistantMsg, prep, executed, config, signal)
end
@@ -763,14 +763,14 @@ function executeToolCallsParallel(...)::agentToolCallBatch
entries = union{finalizedOutcome, task{finalizedOutcome}}[]
for tc in toolCalls
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)
push!(entries, finalized) # immediate outcome — no task
else
task = task() do
executed = executePreparedToolCall(prep, signal, agentEventSink)
executed = executePreparedToolCall(prep, signal, eventSink)
finalized = finalizeExecutedToolCall(context, assistantMsg, prep, executed, config, signal)
return finalized
end
@@ -845,7 +845,7 @@ From the type documentation (`type.jl:803-815`):
**Source:** `agentCore.jl:266-307`
```julia
batch = executeToolCalls(context, response, tool_call_list, config, signal, agentEventSink)
batch = executeToolCalls(context, response, tool_call_list, config, signal, eventSink)
# Save results to conversation history
for tool_result in batch.messages
@@ -1037,13 +1037,13 @@ end
### Event Sink
The `agentEventSink` function is passed through the entire call chain:
The `eventSink` function is passed through the entire call chain:
```julia
agentEventSink = agent.agentEventSink # set during yiemAgent construction
eventSink = agent.eventSink # set during yiemAgent construction
```
The `agentEventSink` function is a user-provided callback that receives all events. This is typically used by:
The `eventSink` function is a user-provided callback that receives all events. This is typically used by:
- **TUI (Terminal UI):** Display real-time progress, tool names, results
- **Logging systems:** Record tool execution history
- **Monitoring:** Track tool usage, execution times, error rates
@@ -1057,12 +1057,12 @@ The `agentEventSink` function is a user-provided callback that receives all even
| Hook | Signature | Called | Purpose |
|------|-----------|--------|---------|
| `prepareContext` | `(state::agentState, agentEventSink) -> agentContext` | Before each LLM call | Filter tools, inject context, modify system prompt |
| `formatMsgForLLM` | `(ctx::agentContext, agentEventSink) -> Dict` | After `prepareContext` | Convert to LLM-specific format |
| `prepareContext` | `(state::agentState, eventSink) -> agentContext` | Before each LLM call | Filter tools, inject context, modify system prompt |
| `formatMsgForLLM` | `(ctx::agentContext, eventSink) -> Dict` | After `prepareContext` | Convert to LLM-specific format |
| `llmCall` | `(messages::Dict) -> assistantMessage` | After formatting | Actually invoke the LLM API |
| `beforeToolCall` | `(msgCtx::beforeToolCallContext, signal) -> Union{Nothing, Dict}` | In `prepareToolCall` | Ask for user permission, block execution, abort |
| `afterToolCall` | `(afterToolCallContext::afterToolCallContext, signal) -> Union{Nothing, Dict}` | In `finalizeExecutedToolCall` | Mutate result, mask data, flip `terminate` |
| `agentEventSink` | `(event) -> nothing` | Throughout lifecycle | Emit events for TUI, logging, monitoring |
| `eventSink` | `(event) -> nothing` | Throughout lifecycle | Emit events for TUI, logging, monitoring |
### `beforeToolCall` Hook
@@ -1125,7 +1125,7 @@ end
**Source:** `utils.jl:111-125`
```julia
function prepareContext(state::agentState, agentEventSink)::agentContext
function prepareContext(state::agentState, eventSink)::agentContext
# TODO: filter tools from state.tools based on user intent
filteredTools = state.tools
@@ -1152,7 +1152,7 @@ end
Default implementation converts `agentContext` to OpenAI-compatible format:
```julia
function formatMsgForLLM(ctx::agentContext, agentEventSink)::Dict{String, Any}
function formatMsgForLLM(ctx::agentContext, eventSink)::Dict{String, Any}
messages = Vector{Dict{String, Any}}()
# System prompt as system message
@@ -1284,7 +1284,7 @@ LOOP ITERATION 1 — LLM DECIDES TO USE A TOOL
│ Step 6: Execute tool calls
│ context = agentContext(systemPrompt, messages, tools)
│ config = agentLoopConfig(beforeToolCall, afterToolCall, "sequential")
│ batch = executeToolCalls(context, response, tool_call_list, config, signal, agentEventSink)
│ batch = executeToolCalls(context, response, tool_call_list, config, signal, eventSink)
LOOP ITERATION 1 — executeToolCallsSequential
@@ -1299,7 +1299,7 @@ LOOP ITERATION 1 — executeToolCallsSequential
│ → preparedToolCall(tool, tc, {"city" => "Tokyo"})
│ EXECUTE:
│ result = tool.execute("call_1", {"city" => "Tokyo"}, nothing, agentEventSink)
│ result = tool.execute("call_1", {"city" => "Tokyo"}, nothing, eventSink)
│ → agentToolResult([textContent("Weather in Tokyo: Sunny, 22°C")], {}, nothing, false)
│ → executedOutcome(result, false)
@@ -1365,20 +1365,20 @@ end
```julia
# Argument preparation (before validation)
function <name>PrepareArguments(args::Dict{String,Any})::Dict{String,Any}
function <name>PrepareArguments(args::AbstractDict{String, Any})::AbstractDict{String, Any}
# Return modified args, or args unchanged
return args
end
# Custom validation (before execution)
function <name>ValidateRequiredArgs(args::Dict{String,Any})::Union{Nothing,String}
function <name>ValidateRequiredArgs(args::AbstractDict{String, Any})::Union{Nothing,String}
# Return nothing to pass, or error string to fail
return nothing
end
# Core execution
function <name>Execute(toolCallId::String,
args::Dict{String,Any},
args::AbstractDict{String, Any},
signal::Union{Nothing,abortSignal},
onPartialResult::Function)::agentToolResult
# Return agentToolResult with content, details, usage, terminate
@@ -1400,17 +1400,17 @@ function helper_function(...)
end
# Optional: prepareArguments
function myToolPrepareArguments(args::Dict{String,Any})::Dict{String,Any}
function myToolPrepareArguments(args::AbstractDict{String, Any})::AbstractDict{String, Any}
return args
end
# Optional: validateRequiredArgs
function myToolValidateRequiredArgs(args::Dict{String,Any})::Union{Nothing,String}
function myToolValidateRequiredArgs(args::AbstractDict{String, Any})::Union{Nothing,String}
return nothing
end
# Required: execute function
function myToolExecute(toolCallId::String, args::Dict{String,Any},
function myToolExecute(toolCallId::String, args::AbstractDict{String, Any},
signal::Union{Nothing,abortSignal},
onPartialResult::Function)::agentToolResult
...
@@ -1481,7 +1481,7 @@ To add a new tool (e.g., `searchWine.jl`):
using .type
# using AdditionalPkg # add if needed
function searchWineExecute(toolCallId::String, args::Dict{String,Any},
function searchWineExecute(toolCallId::String, args::AbstractDict{String, Any},
signal::Union{Nothing,abortSignal}, onPartialResult)
query = get(args, "query", "")
result = search_wine_db(query)
+3 -12
View File
@@ -1,10 +1,7 @@
module YiemAgent
export register_all_tools
""" Order by dependencies of each file. The 1st included file must not depend on any other
files and each file can only depend on the file included before it.
"""
"""Order by dependencies of each file. The 1st included file must not depend on any other
files and each file can only depend on the file included before it."""
include("type.jl")
using .type
@@ -15,12 +12,6 @@ module YiemAgent
include("toolRegistry.jl")
using .toolRegistry
function register_all_tools(store::toolRegistry.toolStore, mcpserver=nothing)
# Only register listTools — all other tools come from MCP server at runtime
registerTool(store, listTool(store, mcpserver))
return store.tools
end
# include("llmfunction.jl")
# using .llmfunction
@@ -31,10 +22,10 @@ module YiemAgent
using .api
# ---------------------------------------------- 100 --------------------------------------------- #
end # module YiemAgent_v1
+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
+106 -139
View File
@@ -1,6 +1,6 @@
module toolRegistry
export toolStore, registerTool, getTools, clearTools, listTool
export toolStore, registerTool, registerAllTools, getTools, clearTools, listTool
using Dates
using JSON, DataStructures
@@ -9,8 +9,7 @@ using ..type
"""
Per-agent isolated tool storage.
Each agent gets its own `toolStore` so tool registration is independent
`registerTool(store, tool)` only affects that agent's tool set.
Each agent gets its own `toolStore` so tool registration is independent.
# Fields
- `tools::OrderedDict{String, agentTool}` — keyed by name for O(1) lookup + ordered iteration
@@ -28,12 +27,6 @@ Create a new empty tool store.
# Keyword Arguments
- `name::String`: Display name for logging (default: `"default"`)
# Example
```julia
julia> store = toolStore(name="agent1")
toolStore(OrderedDict{String, agentTool}(), "agent1")
```
"""
function toolStore(; name::String="default")::toolStore
toolStore(OrderedDict{String, agentTool}(), name)
@@ -46,8 +39,6 @@ Extract text from MCP tool result content array.
Handles JSON-RPC 2.0 result content format:
{"content": [{"type": "text", "text": "..."}], "isError": false}
Returns the joined text content, or JSON-serialized fallback if no text blocks found.
"""
function _extract_text_content(result::Dict)::String
content = get(result, "content", Any[])
@@ -69,10 +60,9 @@ end
Wrap an MCP tool definition as an `agentTool`.
The returned tool's `execute` function calls the MCP server's "tools/call"
method with the validated arguments. Responses are in JSON-RPC 2.0 format
with result/error envelopes handled by the execute function.
method with the validated arguments.
"""
function _wrap_mcp_tool(mcpserver, tool_def::Dict{String,Any})::agentTool
function _wrap_mcp_tool(mcpserver, tool_def::AbstractDict{String, Any}; eventSink=nothing)::agentTool
name = tool_def["name"]
title = get(tool_def, "title", get(tool_def, "label", name))
desc = get(tool_def, "description", "")
@@ -98,9 +88,9 @@ function _wrap_mcp_tool(mcpserver, tool_def::Dict{String,Any})::agentTool
label=title,
description=desc,
inputSchema=params,
execute=(toolCallId::String, args::Dict{String,Any},
execute=(toolCallId::String, args::AbstractDict{String, Any},
signal::Union{Nothing,abortSignal},
agentEventSink,
eventSink,
llmCall=nothing) -> begin
try
response = mcpserver("tools/call", name, args)
@@ -145,14 +135,18 @@ Discover and register MCP tools into `store.tools`.
Queries the MCP server via `mcpserver("tools/list")` (JSON-RPC 2.0 format),
parses the response envelope, and registers each discovered tool.
Handles pagination via `nextCursor` — keeps fetching until cursor is empty.
Skips tools already registered. Returns `(new_count, tool_list_text)`.
Handles pagination via `nextCursor`. Skips tools already registered.
# Returns
- `Int`: number of new tools registered
"""
function _register_mcp_tools(mcpserver, store::toolStore)::Tuple{Int, String}
function register_mcp_tools(mcpserver, store::toolStore; eventSink=nothing)::Int
if mcpserver === nothing
return (0, "")
return 0
end
new_count = 0
try
response = mcpserver("tools/list")
@@ -160,7 +154,8 @@ function _register_mcp_tools(mcpserver, store::toolStore)::Tuple{Int, String}
if haskey(response, "error")
rpc_error = response["error"]
err_msg = get(rpc_error, "message", "Unknown MCP error")
return (0, "MCP tools/list failed: $err_msg")
println("[toolRegistry:$(store.name)] MCP tools/list failed: $err_msg")
return 0
end
if haskey(response, "result")
response = response["result"]
@@ -169,13 +164,13 @@ function _register_mcp_tools(mcpserver, store::toolStore)::Tuple{Int, String}
tools_array = response["tools"]
cursor = get(response, "nextCursor", nothing)
new_count = 0
for tool_def in tools_array
name = tool_def["name"]
if haskey(store.tools, name)
continue
end
wrapped = _wrap_mcp_tool(mcpserver, tool_def)
@show "tool_def $(typeof(tool_def))"
wrapped = _wrap_mcp_tool(mcpserver, tool_def, eventSink=eventSink)
store.tools[name] = wrapped
new_count += 1
end
@@ -193,101 +188,29 @@ function _register_mcp_tools(mcpserver, store::toolStore)::Tuple{Int, String}
if haskey(store.tools, name)
continue
end
wrapped = _wrap_mcp_tool(mcpserver, tool_def)
wrapped = _wrap_mcp_tool(mcpserver, tool_def, eventSink=eventSink)
store.tools[name] = wrapped
new_count += 1
end
end
# Build readable tool list
lines = String[
"- $(t.name): $(t.label)$(t.description)"
for (k, t) in store.tools
]
tool_list_text = "Discovered $(new_count) MCP tools. Total registered: $(length(store.tools)).\nAvailable tools:\n" * join(lines, "\n")
return (new_count, tool_list_text)
println("[toolRegistry:$(store.name)] Discovered $new_count MCP tools. Total: $(length(store.tools))")
catch e
bt = catch_backtrace()
err_msg = sprint() do io
showerror(io, e, bt)
println(io)
end
eventSink(err_msg)
errMsg = sprint(showerror, e)
return (0, "MCP tools/list failed: $errMsg")
println("[toolRegistry:$(store.name)] MCP tools/list failed: $errMsg")
end
return new_count
end
"""
listTool(store::toolStore, mcpserver) -> agentTool
MCP-aware listTools tool (JSON-RPC 2.0 protocol).
First call: queries the MCP server via `mcpserver("tools/list")`, registers
all discovered tools into the shared `store.tools` (in-place mutation, with
pagination via nextCursor), then returns the full tool list.
Subsequent calls: returns the current list (tools remain registered).
This is the only pre-registered tool. All other tools come from the
MCP server and are loaded at runtime when the LLM calls listTools().
# Arguments
- `store`: The tool store to populate with MCP tools
- `mcpserver`: A callable struct that communicates with the MCP server.
Called as `mcpserver("tools/list")` or `mcpserver("tools/call", args)`.
Returns parsed JSON dicts.
# Example
```julia
# User provides an MCP server client (callable struct)
mcp = MyMCPClient("nats://localhost:4222")
store = toolStore(name="agent1")
registerTool(store, listTool(store, mcp))
# When agent calls listTools(), tools are discovered from MCP server
# and registered into store.tools in real time.
```
"""
function listTool(store::toolStore, mcpserver)::agentTool
return agentTool(
name="listTools",
label="List Tools",
description="List all available tools. First call discovers and registers all tools from the MCP server. After discovery, new tools become immediately available for use.",
inputSchema=Dict{String,Any}(
"type" => "object",
"properties" => Dict{String,Any}(),
"required" => Any[]
),
execute=(toolCallId::String, args::Dict{String,Any},
signal::Union{Nothing,abortSignal},
agentEventSink, llmCall=nothing) -> begin
# Discover and register MCP tools (idempotent — skips already registered)
new_count, tool_list = _register_mcp_tools(mcpserver, store)
# Always include listTools itself in the count
total = length(store.tools)
if new_count > 0
result_text = tool_list
else
# Already discovered — just return current list
lines = String[
"- $(t.name): $(t.label)$(t.description)"
for (k, t) in store.tools
]
result_text = "Available tools ($total):\n" * join(lines, "\n")
end
return agentToolResult(
[textContent(result_text)],
Dict{Any,Any}("count" => total),
nothing, false
)
end,
prepareArguments=nothing,
validateRequiredArgs=nothing,
parallelToolExecute=false,
)
end
# Note: register_all_tools is defined in YiemAgent.jl where tool functions are in scope
"""
registerTool(store::toolStore, tool::agentTool) -> OrderedDict{String, agentTool}
@@ -299,18 +222,9 @@ Add `tool` to `store`, overwriting any existing tool with the same name.
# Returns
- The same `store.tools` dict (modified in place)
# Example
```julia
julia> store = toolStore(name="agent1");
julia> registerTool(store, listTool(store, nothing))
[toolRegistry:agent1] Registered tool: listTools
OrderedDict{String, agentTool} with 1 entry:
"listTools" => agentTool(...)
```
"""
function registerTool(store::toolStore, tool::agentTool)::OrderedDict{String, agentTool}
function registerTool(store::toolStore, tool::agentTool; eventSink=nothing
)::OrderedDict{String, agentTool}
store.tools[tool.name] = tool
println("[$(store.name)] Registered tool: $(tool.name)")
return store.tools
@@ -319,22 +233,13 @@ end
"""
Return the tools registered in `store`.
The returned dict is the **same object** stored inside `store` — mutations
to it (e.g. via `registerTool`) are visible through subsequent calls.
The returned dict is the **same object** stored inside `store`.
# Arguments
- `store`: Tool store to query
# Returns
- `OrderedDict{String, agentTool}`: Tools keyed by name, in registration order
# Example
```julia
julia> tools = getTools(store)
OrderedDict{String, agentTool} with 2 entries:
"getWeather" => agentTool(...)
"getTime" => agentTool(...)
```
"""
function getTools(store::toolStore)::OrderedDict{String, agentTool}
return store.tools
@@ -348,16 +253,6 @@ Remove all tools from `store`.
# Returns
- `nothing`
# Example
```julia
julia> clearTools(store)
[toolRegistry:agent1] Registry cleared
nothing
julia> getTools(store)
OrderedDict{String, agentTool} with 0 entries
```
"""
function clearTools(store::toolStore)::Nothing
empty!(store.tools)
@@ -365,4 +260,76 @@ function clearTools(store::toolStore)::Nothing
return nothing
end
# ── listTools tool (auto-discover new MCP tools at runtime) ─────────
"""
listTool(store::toolStore, mcpserver) -> agentTool
MCP-aware listTools tool (JSON-RPC 2.0 protocol).
First call: queries the MCP server via `mcpserver("tools/list")`, registers
all discovered tools into the shared `store.tools` (in-place mutation, with
pagination via nextCursor), then returns the full tool list.
Subsequent calls: returns the current list (tools remain registered).
"""
function listTool(store::toolStore, mcpserver)::agentTool
return agentTool(
name="listTools",
label="List Tools",
description="List all available tools. First call discovers and registers all tools from the MCP server. After discovery, new tools become immediately available for use.",
inputSchema=Dict{String,Any}(
"type" => "object",
"properties" => Dict{String,Any}(),
"required" => Any[]
),
execute=(toolCallId::String, args::AbstractDict{String, Any},
signal::Union{Nothing,abortSignal},
eventSink, llmCall=nothing) -> begin
# Discover and register MCP tools (idempotent — skips already registered)
new_count = register_mcp_tools(mcpserver, store)
# Always include listTools itself in the count
total = length(store.tools)
lines = String[
"- $(t.name): $(t.label)$(t.description)"
for (k, t) in store.tools
]
result_text = "Available tools ($total):\n" * join(lines, "\n")
return agentToolResult(
[textContent(result_text)],
Dict{Any,Any}("count" => total),
nothing, false
)
end,
prepareArguments=nothing,
validateRequiredArgs=nothing,
parallelToolExecute=false,
)
end
# ── High-level API ──────────────────────────────────────────────────
"""
registerAllTools(store::toolStore, mcpserver) -> toolStore
Register all tools for an agent:
1. Auto-discover existing tools from the MCP server
2. Register the `listTools` tool so the agent can discover new tools at runtime
# Arguments
- `store`: The tool store to populate
- `mcpserver`: A callable struct that communicates with the MCP server
# Returns
- The populated `toolStore`
"""
function registerAllTools(store::toolStore, mcpserver=nothing; eventSink=nothing)::toolStore
register_mcp_tools(mcpserver, store; eventSink=eventSink)
registerTool(store, listTool(store, mcpserver); eventSink=eventSink)
return store
end
end # module
+8 -8
View File
@@ -487,13 +487,13 @@ Context passed to the `beforeToolCall` hook.
# Arguments
- `message::assistantMessageToolCall`: The assistant message containing the tool call
- `toolCall::agentToolCall`: The tool call being prepared
- `args::Dict{String,Any}`: Validated tool arguments
- `args::AbstractDict{String, Any}`: Validated tool arguments
- `context::agentContext`: Current conversation context
"""
struct beforeToolCallContext
message::assistantMessageToolCall
toolCall::agentToolCall
args::Dict{String,Any}
args::AbstractDict{String, Any}
context::agentContext
end
@@ -508,7 +508,7 @@ Context passed to the `afterToolCall` hook.
# Arguments
- `message::assistantMessageToolCall`: The assistant message containing the tool call
- `toolCall::agentToolCall`: The tool call that was executed
- `args::Dict{String,Any}`: Tool arguments
- `args::AbstractDict{String, Any}`: Tool arguments
- `result::agentToolResult`: The raw tool result
- `isError::Bool`: Whether execution resulted in an error
- `context::agentContext`: Current conversation context
@@ -516,7 +516,7 @@ Context passed to the `afterToolCall` hook.
struct afterToolCallContext
message::assistantMessageToolCall
toolCall::agentToolCall
args::Dict{String,Any}
args::AbstractDict{String, Any}
result::agentToolResult
isError::Bool
context::agentContext
@@ -528,12 +528,12 @@ Event emitted when a tool call execution starts.
# Arguments
- `toolCallId::String`: ID of the tool call
- `toolName::String`: Name of the tool
- `arguments::Dict{String,Any}`: Tool arguments
- `arguments::AbstractDict{String, Any}`: Tool arguments
"""
struct toolExecStartEvent
toolCallId::String
toolName::String
arguments::Dict{String,Any}
arguments::AbstractDict{String, Any}
end
"""
@@ -542,13 +542,13 @@ Event emitted with partial results during tool execution.
# Arguments
- `toolCallId::String`: ID of the tool call
- `toolName::String`: Name of the tool
- `arguments::Dict{String,Any}`: Tool arguments
- `arguments::AbstractDict{String, Any}`: Tool arguments
- `partialResult::Any`: The partial result data
"""
struct toolExecUpdateEvent
toolCallId::String
toolName::String
arguments::Dict{String,Any}
arguments::AbstractDict{String, Any}
partialResult::Any
end
+19 -19
View File
@@ -3,7 +3,7 @@ module utils
export clearhistory, availableWineToText, prepareContext, formatMsgForLLM, validateRequiredArgs,
validateToolArguments, _userMessageToOpenAI,
_assistantMessageToOpenAI, _toolResultMessageToOpenAI, _messageContentToBlocks, _toolsToOpenAI,
beforeToolCall, afterToolCall, agentEventSink
beforeToolCall, afterToolCall, eventSink
using UUIDs, Dates, DataStructures, HTTP, JSON, NATS
using GeneralUtils
@@ -109,7 +109,7 @@ prepareContext(state).messages == deepcopy(state.messages)
# end
```
"""
function prepareContext(state::agentState, agentEventSink, llmCall=nothing)::agentContext
function prepareContext(state::agentState, eventSink, llmCall=nothing)::agentContext
#TODO filter tools from state.tools based on user intend in user message and tool description
filteredTools = state.tools
@@ -156,7 +156,7 @@ formatMsgForLLm(ctx) == Dict("messages" => [
])
```
"""
function formatMsgForLLM(ctx::agentContext, agentEventSink)::Dict{String, Any}
function formatMsgForLLM(ctx::agentContext, eventSink)::Dict{String, Any}
""" openai message format example
msg = Dict(
@@ -208,7 +208,7 @@ function formatMsgForLLM(ctx::agentContext, agentEventSink)::Dict{String, Any}
openaiReadyMsg = Dict{String, Any}()
# openaiReadyMsg["model"] = "gemma-4-E4B-it-UD-Q4_K_XL"
messages = Vector{Dict{String, Any}}()
agentEventSink("formatMsgForLLM 1")
eventSink("formatMsgForLLM 1")
# System prompt as system message
if !isempty(ctx.systemPrompt)
push!(messages, Dict(
@@ -216,7 +216,7 @@ function formatMsgForLLM(ctx::agentContext, agentEventSink)::Dict{String, Any}
"content" => [Dict("type" => "text", "text" => ctx.systemPrompt)]
))
end
agentEventSink("formatMsgForLLM 2")
eventSink("formatMsgForLLM 2")
# Conversation messages
for msg in ctx.messages
if msg isa userMessage
@@ -229,10 +229,10 @@ function formatMsgForLLM(ctx::agentContext, agentEventSink)::Dict{String, Any}
push!(messages, _toolResultMessageToOpenAI(msg))
end
end
agentEventSink("formatMsgForLLM 3")
eventSink("formatMsgForLLM 3")
# Convert ctx.tools into OpenAI tools format
tools_array = _toolsToOpenAI(ctx.tools, agentEventSink)
agentEventSink("formatMsgForLLM 4")
tools_array = _toolsToOpenAI(ctx.tools, eventSink)
eventSink("formatMsgForLLM 4")
openaiReadyMsg["messages"] = messages
openaiReadyMsg["temperature"] = 0.7
@@ -321,7 +321,7 @@ end
#TODO
function agentEventSink(x)
function eventSink(x)
end
@@ -375,7 +375,7 @@ function _assistantMessageToolCallToOpenAI(msg::assistantMessageToolCall)::Dict{
)
end
"""
"""
Convert an assistantMessage to OpenAI message format.
"""
function _assistantMessageToOpenAI(msg::assistantMessage)::Dict{String, Any}
@@ -437,10 +437,10 @@ _toolsToOpenAI(nothing) # => Dict{String, Any}[]
_toolsToOpenAI(tools) # => [Dict("type" => "function", "function" => Dict("name" => "getWeather", ...))]
```
"""
function _toolsToOpenAI(tools::Union{OrderedDict{String, agentTool}, Nothing}, agentEventSink)::Vector{Dict{String, Any}}
function _toolsToOpenAI(tools::Union{OrderedDict{String, agentTool}, Nothing}, eventSink)::Vector{Dict{String, Any}}
tools_array = Vector{Dict{String, Any}}()
agentEventSink("_toolsToOpenAI 1")
agentEventSink(string(typeof(tools)))
eventSink("_toolsToOpenAI 1")
eventSink(string(typeof(tools)))
if tools !== nothing
for (_, tool) in tools
push!(tools_array, Dict(
@@ -453,13 +453,13 @@ function _toolsToOpenAI(tools::Union{OrderedDict{String, agentTool}, Nothing}, a
))
end
end
agentEventSink("_toolsToOpenAI 2")
eventSink("_toolsToOpenAI 2")
return tools_array
end
"""
validateRequiredArgs(args::Dict{String,Any}, inputSchema::Dict{String,Any}) -> Union{Nothing,String}
validateRequiredArgs(args::AbstractDict{String, Any}, inputSchema::AbstractDict{String, Any}) -> Union{Nothing,String}
Validates that all required fields listed in the tool's JSON Schema are present
in `args`. Returns `nothing` if validation passes, or a descriptive error string
@@ -470,8 +470,8 @@ with a custom validation function that performs additional checks (e.g. type
coercion, format validation, cross-field constraints).
# Arguments
- `args::Dict{String,Any}`: The arguments provided by the LLM
- `inputSchema::Dict{String,Any}`: The tool's `inputSchema` (JSON Schema format)
- `args::AbstractDict{String, Any}`: The arguments provided by the LLM
- `inputSchema::AbstractDict{String, Any}`: The tool's `inputSchema` (JSON Schema format)
# Returns
- `nothing` if all required args are present
@@ -487,7 +487,7 @@ args2 = Dict("city" => "Tokyo")
validateRequiredArgs(args2, schema) # => nothing
```
"""
function validateRequiredArgs(args::Dict{String,Any}, inputSchema::Dict{String,Any})::Union{Nothing,String}
function validateRequiredArgs(args::AbstractDict{String, Any}, inputSchema::AbstractDict{String, Any})::Union{Nothing,String}
required = get(inputSchema, "required", Any[])
if isempty(required)
return nothing
@@ -541,7 +541,7 @@ validateToolArguments(toolWithHook, tc) # => validated args or throws
validateToolArguments(toolDefault, tc) # => args or throws
```
"""
function validateToolArguments(tool::agentTool, prepared::agentToolCall)::Dict{String,Any}
function validateToolArguments(tool::agentTool, prepared::agentToolCall)::AbstractDict{String, Any}
# Use default (2-arg: args + schema) or tool-specific hook (1-arg: args only)
if isnothing(tool.validateRequiredArgs)
result = validateRequiredArgs(prepared.arguments, tool.inputSchema)
+5 -5
View File
@@ -32,7 +32,7 @@ catch e
println(io)
end
agentEventSink(err_msg)
eventSink(err_msg)
end
"""
@@ -66,13 +66,13 @@ end
struct agentEventSink
struct eventSink
natsConn::NATS.Connection
topic::String
senderID::String
end
function (aes::agentEventSink)(msg::String)
function (aes::eventSink)(msg::String)
NATS.publish(aes.natsConn, aes.topic, msg)
end
@@ -88,11 +88,11 @@ text2text_llm = text2textInstructLLM(agent_conn,
"sender",
config["externalservice"]["fileserver"]["url"])
debugNats = agentEventSink(agent_conn, "sommanion.debug", "sender")
debugNats = eventSink(agent_conn, "sommanion.debug", "sender")
agent = YiemAgent.yiemAgent(
text2text_llm;
agentEventSink=debugNats
eventSink=debugNats
)
msg = Dict(