diff --git a/README.md b/README.md index 73e0c3f..2f723ef 100644 --- a/README.md +++ b/README.md @@ -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 | diff --git a/README_tools.md b/README_tools.md index 9560f36..c6ab65d 100644 --- a/README_tools.md +++ b/README_tools.md @@ -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 PrepareArguments(args::Dict{String,Any})::Dict{String,Any} +function PrepareArguments(args::AbstractDict{String, Any})::AbstractDict{String, Any} # Return modified args, or args unchanged return args end # Custom validation (before execution) -function ValidateRequiredArgs(args::Dict{String,Any})::Union{Nothing,String} +function ValidateRequiredArgs(args::AbstractDict{String, Any})::Union{Nothing,String} # Return nothing to pass, or error string to fail return nothing end # Core execution function 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) diff --git a/src/YiemAgent.jl b/src/YiemAgent.jl index 6d40a85..372f80b 100644 --- a/src/YiemAgent.jl +++ b/src/YiemAgent.jl @@ -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 diff --git a/src/agentCore.jl b/src/agentCore.jl index 4048e46..15b32d3 100644 --- a/src/agentCore.jl +++ b/src/agentCore.jl @@ -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 diff --git a/src/toolRegistry.jl b/src/toolRegistry.jl index 8381d2e..105cfc5 100644 --- a/src/toolRegistry.jl +++ b/src/toolRegistry.jl @@ -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 diff --git a/src/type.jl b/src/type.jl index 0411748..0760cd9 100644 --- a/src/type.jl +++ b/src/type.jl @@ -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 diff --git a/src/utils.jl b/src/utils.jl index d7aa1fd..57d9e59 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -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) diff --git a/user_code.jl b/user_code.jl index 2123d8a..54ad652 100644 --- a/user_code.jl +++ b/user_code.jl @@ -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(