mcp works

This commit is contained in:
2026-08-21 22:34:27 +07:00
parent da21790263
commit d2592ab6d6
5 changed files with 161 additions and 197 deletions
+107 -99
View File
@@ -187,13 +187,13 @@ function yiemAgent(
followUp = Channel(32) followUp = Channel(32)
outputChannel = Channel(16) outputChannel = Channel(16)
# load tools (statically registered at module init)
toolStore1 = toolStore(name="myagent") tools = OrderedDict{String, agentTool}()
registerAllTools(toolStore1, mcpServer; eventSink=eventSink) registerAllTools(tools, mcpServer; eventSink=eventSink)
# Create struct with a placeholder task, then spawn and replace it # Create struct with a placeholder task, then spawn and replace it
agent = yiemAgent( agent = yiemAgent(
agentState(systemPrompt, model, getTools(toolStore1), messages), agentState(systemPrompt, model, tools, messages),
inputChannel, inputChannel,
followUp, followUp,
outputChannel, outputChannel,
@@ -442,112 +442,123 @@ function _processMessage(
""" """
while true while true
# Drain inputChannel and convert OpenAI-format messages to userMessage type try
while isready(inputChannel) # Drain inputChannel and convert OpenAI-format messages to userMessage type
eventSink("_processMessage 2 _state.messages length $(length(agentMsgHistory))") while isready(inputChannel)
newUserMsg_openai = take!(inputChannel) eventSink("_processMessage 2 _state.messages length $(length(agentMsgHistory))")
eventSink("_processMessage 3 _state.messages length $(length(agentMsgHistory))") newUserMsg_openai = take!(inputChannel)
if newUserMsg_openai === :shutdown eventSink("_processMessage 3 _state.messages length $(length(agentMsgHistory))")
eventSink("_processMessage 4 _state.messages length $(length(agentMsgHistory))") if newUserMsg_openai === :shutdown
# Re-emit shutdown signal for the loop to handle eventSink("_processMessage 4 _state.messages length $(length(agentMsgHistory))")
put!(inputChannel, :shutdown) # Re-emit shutdown signal for the loop to handle
break put!(inputChannel, :shutdown)
break
end
eventSink("_processMessage 5 _state.messages length $(length(agentMsgHistory))")
newUserMsg = OpenAiToUserMessage(newUserMsg_openai)
push!(agentMsgHistory, newUserMsg)
eventSink("_processMessage 6 _state.messages length $(length(agentMsgHistory))")
end end
eventSink("_processMessage 5 _state.messages length $(length(agentMsgHistory))") eventSink("_processMessage 7 _state.messages length $(length(agentMsgHistory))")
newUserMsg = OpenAiToUserMessage(newUserMsg_openai) # call prepareContext()
push!(agentMsgHistory, newUserMsg) state = agentState(systemPrompt, nothing, tools, agentMsgHistory)
eventSink("_processMessage 6 _state.messages length $(length(agentMsgHistory))") eventSink("_processMessage 8 _state.messages length $(length(agentMsgHistory))")
end preparedContext = prepareContext(state, eventSink)
eventSink("_processMessage 7 _state.messages length $(length(agentMsgHistory))") eventSink("_processMessage 9 _state.messages length $(length(agentMsgHistory))")
# call prepareContext() # Call formatMessagesForLLM() to format for LLM
state = agentState(systemPrompt, nothing, tools, agentMsgHistory) formattedMessages = formatMessagesForLLM(preparedContext, eventSink)
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, eventSink)
eventSink("_processMessage 10 formattedMessages $formattedMessages") eventSink("_processMessage 10 formattedMessages $formattedMessages")
""" response example """ response example
response = JSON.Object{String, Any}("finish_reason" => "tool_calls", "index" => 0, "message" => JSON.Object{String, Any}("role" => "assistant", "content" => "", "reasoning_content" => "Here's a thinking process:\n\n1. **Identify User Request**: The user is asking for the weather in Bangkok.\n2. **Locate Relevant Tool**: I have a `getWeather` function available.\n3. **Check Function Parameters**:\n - `city` (required): City and country, e.g., 'San Francisco, CA' or 'Tokyo, Japan'\n - `units` (optional, default \"celsius\"): Temperature scale (\"celsius\" or \"fahrenheit\")\n4. **Prepare Parameters**:\n - `city`: \"Bangkok, Thailand\" (adding country for clarity, though just \"Bangkok\" might work, following the example format is safer)\n - `units`: Not specified, so I'll use the default (\"celsius\")\n5. **Execute Tool Call**: Call `getWeather` with `city: \"Bangkok, Thailand\"`\n6. **Anticipate Response**: The function will return current weather and forecast data for Bangkok. I'll then format it nicely for the user.\n - *Self-Correction/Verification during thought*: The prompt says \"city: City and country, e.g., 'San Francisco, CA' or 'Tokyo, Japan'\". I'll use \"Bangkok, Thailand\". The `units` parameter is optional, so I'll omit it to use the default.\n - Proceed with tool call.✅\n", "tool_calls" => Any[JSON.Object{String, Any}("type" => "function", "function" => JSON.Object{String, Any}("name" => "getWeather", "arguments" => "{\"city\":\"Bangkok, Thailand\"}"), "id" => "6fOilR5QPcdppbAAHluhkRyUDu3oWMAL")])) response = JSON.Object{String, Any}("finish_reason" => "tool_calls", "index" => 0, "message" => JSON.Object{String, Any}("role" => "assistant", "content" => "", "reasoning_content" => "Here's a thinking process:\n\n1. **Identify User Request**: The user is asking for the weather in Bangkok.\n2. **Locate Relevant Tool**: I have a `getWeather` function available.\n3. **Check Function Parameters**:\n - `city` (required): City and country, e.g., 'San Francisco, CA' or 'Tokyo, Japan'\n - `units` (optional, default \"celsius\"): Temperature scale (\"celsius\" or \"fahrenheit\")\n4. **Prepare Parameters**:\n - `city`: \"Bangkok, Thailand\" (adding country for clarity, though just \"Bangkok\" might work, following the example format is safer)\n - `units`: Not specified, so I'll use the default (\"celsius\")\n5. **Execute Tool Call**: Call `getWeather` with `city: \"Bangkok, Thailand\"`\n6. **Anticipate Response**: The function will return current weather and forecast data for Bangkok. I'll then format it nicely for the user.\n - *Self-Correction/Verification during thought*: The prompt says \"city: City and country, e.g., 'San Francisco, CA' or 'Tokyo, Japan'\". I'll use \"Bangkok, Thailand\". The `units` parameter is optional, so I'll omit it to use the default.\n - Proceed with tool call.✅\n", "tool_calls" => Any[JSON.Object{String, Any}("type" => "function", "function" => JSON.Object{String, Any}("name" => "getWeather", "arguments" => "{\"city\":\"Bangkok, Thailand\"}"), "id" => "6fOilR5QPcdppbAAHluhkRyUDu3oWMAL")]))
""" """
response = llmCall(formattedMessages) response = llmCall(formattedMessages)
eventSink(" llmCall " * string(response)) eventSink("LLM response " * string(response))
eventSink("_processMessage 11 _state.messages length $(length(agentMsgHistory))") eventSink("_processMessage 11 _state.messages length $(length(agentMsgHistory))")
# Extract tool calls from LLM response content blocks # Extract tool calls from LLM response content blocks
hasToolCalls, toolCallList, assistant_msg = _extractToolCalls(response) hasToolCalls, toolCallList, assistant_msg = _extractToolCalls(response)
eventSink("hasToolCalls: $hasToolCalls\ntoolCallList: $toolCallList _state.messages length $(length(agentMsgHistory))") eventSink("hasToolCalls: $hasToolCalls\ntoolCallList: $toolCallList _state.messages length $(length(agentMsgHistory))")
eventSink("assistant_msg " * string(assistant_msg)) eventSink("assistant_msg " * string(assistant_msg))
eventSink("_processMessage 11-1") eventSink("_processMessage 11-1")
# Add assistant message (tool calls or text) to history for next LLM turn # Add assistant message (tool calls or text) to history for next LLM turn
push!(agentMsgHistory, assistant_msg) push!(agentMsgHistory, assistant_msg)
if hasToolCalls && length(toolCallList) > 0 if hasToolCalls && length(toolCallList) > 0
# Build context and config for executeToolCalls # Build context and config for executeToolCalls
config = agentLoopConfig( config = agentLoopConfig(
beforeToolCall, beforeToolCall,
afterToolCall, afterToolCall,
parallelToolExecute ? "parallel" : "sequential", parallelToolExecute ? "parallel" : "sequential",
llmCall, )
)
signal = abortSignal(false) signal = abortSignal(false)
eventSink("_processMessage 12 _state.messages length $(length(agentMsgHistory))") eventSink("_processMessage 12 _state.messages length $(length(agentMsgHistory))")
# call executeToolCalls() # call executeToolCalls()
toolResultBatch = executeToolCalls(preparedContext, assistant_msg, toolCallList, config, toolResultBatch = executeToolCalls(preparedContext, assistant_msg, toolCallList, config,
signal, eventSink) signal, eventSink)
eventSink("_processMessage 13 _state.messages length $(length(agentMsgHistory))") eventSink("_processMessage 13 _state.messages length $(length(agentMsgHistory))")
# save toolResults to messages # save toolResults to messages
for toolResult in toolResultBatch.messages
eventSink("toolResult " * string(toolResult))
push!(agentMsgHistory, toolResult)
end
eventSink("_processMessage 14 _state.messages length $(length(agentMsgHistory))")
if toolResultBatch.terminate
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 for toolResult in toolResultBatch.messages
for content_block in toolResult.content eventSink("toolResult " * string(toolResult))
if content_block isa textContent push!(agentMsgHistory, toolResult)
append!(final_content, [content_block]) end
elseif content_block isa Dict eventSink("_processMessage 14 _state.messages length $(length(agentMsgHistory))")
if haskey(content_block, :text) if toolResultBatch.terminate
push!(final_content, textContent(content_block[:text])) 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
for content_block in toolResult.content
if content_block isa textContent
append!(final_content, [content_block])
elseif content_block isa Dict
if haskey(content_block, :text)
push!(final_content, textContent(content_block[:text]))
end
end end
end end
end end
eventSink("_processMessage 16 _state.messages length $(length(agentMsgHistory))")
final_response = assistantMessage(
role="assistant",
content=final_content,
api=assistant_msg.api,
model=assistant_msg.model,
usage=assistant_msg.usage,
stopReason="tool_use_terminated",
errorMessage=if any(x -> x.isError, toolResultBatch.messages)
"One or more tool calls failed"
else
nothing
end,
timestamp=now(),
)
push!(agentMsgHistory, final_response)
break
end end
eventSink("_processMessage 16 _state.messages length $(length(agentMsgHistory))") else
final_response = assistantMessage( eventSink("_processMessage 17 _state.messages length $(length(agentMsgHistory))")
role="assistant", # LLM did not use tool calls —
content=final_content,
api=assistant_msg.api,
model=assistant_msg.model,
usage=assistant_msg.usage,
stopReason="tool_use_terminated",
errorMessage=if any(x -> x.isError, toolResultBatch.messages)
"One or more tool calls failed"
else
nothing
end,
timestamp=now(),
)
push!(agentMsgHistory, final_response)
break break
end end
else catch e
eventSink("_processMessage 17 _state.messages length $(length(agentMsgHistory))") bt = catch_backtrace()
# LLM did not use tool calls — err_msg = sprint() do io
break showerror(io, e, bt)
println(io)
end
eventSink(err_msg)
end end
end end
eventSink("_processMessage 18 _state.messages length $(length(agentMsgHistory))") eventSink("_processMessage 18 _state.messages length $(length(agentMsgHistory))")
return nothing return nothing
end end
@@ -1087,14 +1098,13 @@ function executePreparedToolCall(
prep::preparedToolCall, prep::preparedToolCall,
signal::Union{Nothing,abortSignal}, signal::Union{Nothing,abortSignal},
eventSink, eventSink,
llmCall::Union{Any,Nothing}=nothing,
)::executedOutcome )::executedOutcome
eventSink("executePreparedToolCall 1") eventSink("executePreparedToolCall 1")
try #WORKING try #WORKING
result = prep.tool.execute(prep.toolCall.id, prep.args, signal, eventSink, llmCall) result = prep.tool.execute(prep.toolCall.id, prep.args, signal, eventSink)
eventSink("executePreparedToolCall 2") eventSink("executePreparedToolCall 2")
eventSink(result.content[1].text) eventSink("tool execute raw result: " * sprint(show, result))
eventSink("executePreparedToolCall 3") eventSink("executePreparedToolCall 3")
return executedOutcome(result, false) return executedOutcome(result, false)
catch e catch e
@@ -1271,7 +1281,6 @@ function executeToolCallsSequential(
signal::abortSignal, signal::abortSignal,
eventSink, eventSink,
)::agentToolCallBatch )::agentToolCallBatch
llmCall = config.llmCall
eventSink("executeToolCallsSequential 1") eventSink("executeToolCallsSequential 1")
finalizedCalls = finalizedOutcome[] finalizedCalls = finalizedOutcome[]
messages = toolResultMessage[] messages = toolResultMessage[]
@@ -1287,7 +1296,8 @@ function executeToolCallsSequential(
eventSink("executeToolCallsSequential 2-2") eventSink("executeToolCallsSequential 2-2")
else else
eventSink("executeToolCallsSequential 3") eventSink("executeToolCallsSequential 3")
executed = executePreparedToolCall(prep, signal, eventSink, llmCall) executed = executePreparedToolCall(prep, signal, eventSink)
eventSink("executeToolCallsSequential 3-1") eventSink("executeToolCallsSequential 3-1")
finalized = finalizeExecutedToolCall(context, assistantMsg, prep, executed, config, finalized = finalizeExecutedToolCall(context, assistantMsg, prep, executed, config,
signal, eventSink) signal, eventSink)
@@ -1374,7 +1384,6 @@ function executeToolCallsParallel(
)::agentToolCallBatch )::agentToolCallBatch
entries = Union{finalizedOutcome,Task}[] entries = Union{finalizedOutcome,Task}[]
llmCall = config.llmCall
for tc in toolCalls for tc in toolCalls
eventSink(toolExecStartEvent(tc.id, tc.name, tc.arguments)) eventSink(toolExecStartEvent(tc.id, tc.name, tc.arguments))
@@ -1388,7 +1397,7 @@ function executeToolCallsParallel(
push!(entries, finalized) push!(entries, finalized)
else else
t = Task() do t = Task() do
executed = executePreparedToolCall(prep, signal, eventSink, llmCall) executed = executePreparedToolCall(prep, signal, eventSink)
finalized = finalizeExecutedToolCall(context, assistantMsg, prep, executed, config, signal) finalized = finalizeExecutedToolCall(context, assistantMsg, prep, executed, config, signal)
eventSink(toolExecEndEvent(finalized.toolCall.id, finalized.toolCall.name, eventSink(toolExecEndEvent(finalized.toolCall.id, finalized.toolCall.name,
finalized.result, finalized.isError)) finalized.result, finalized.isError))
@@ -1476,7 +1485,6 @@ function executeToolCalls(
eventSink, eventSink,
)::agentToolCallBatch )::agentToolCallBatch
llmCall = config.llmCall
eventSink("_executeToolCalls 1") eventSink("_executeToolCalls 1")
hasSequential = false hasSequential = false
for tc in toolCalls for tc in toolCalls
+6 -6
View File
@@ -18,7 +18,7 @@ The agent processes messages from `inputChannel` in the background task.
# Arguments # Arguments
- `agent::yiemAgent`: The agent instance to send a message to - `agent::yiemAgent`: The agent instance to send a message to
- `msg`: The message to send (any type accepted by the agent's processing pipeline) - `msg`: The message to send, in OpenAI message format (e.g. `Dict("role" => "user", "content" => "Hello!")`)
# Returns # Returns
- The same `agent` instance for chaining - The same `agent` instance for chaining
@@ -29,11 +29,11 @@ The agent processes messages from `inputChannel` in the background task.
# Examples # Examples
```jldoctest ```jldoctest
julia> runAgent(agent, "Hello!") julia> runAgent(agent, Dict("role" => "user", "content" => "Hello!"))
yiemAgent(...) yiemAgent(...)
``` ```
""" """
function runAgent(agent::yiemAgent, msg) function runAgent(agent::yiemAgent, msg::AbstractDict{String, Any})
put!(agent.inputChannel, msg) put!(agent.inputChannel, msg)
return agent return agent
end end
@@ -70,7 +70,7 @@ and before any tool call results are sent.
# Arguments # Arguments
- `agent::yiemAgent`: The agent instance to send a follow-up message to - `agent::yiemAgent`: The agent instance to send a follow-up message to
- `msg`: The follow-up message to send - `msg`: The follow-up message to send, in OpenAI message format (e.g. `Dict("role" => "user", "content" => "Also consider red wines")`)
# Returns # Returns
- The same `agent` instance for chaining - The same `agent` instance for chaining
@@ -82,11 +82,11 @@ and before any tool call results are sent.
# Examples # Examples
```jldoctest ```jldoctest
julia> followUp(agent, "Also consider red wines") julia> followUp(agent, Dict("role" => "user", "content" => "Also consider red wines"))
yiemAgent(...) yiemAgent(...)
``` ```
""" """
function followUp(agent::yiemAgent, msg) function followUp(agent::yiemAgent, msg::AbstractDict{String, Any})
put!(agent.followUpChannel, msg) put!(agent.followUpChannel, msg)
return agent return agent
end end
+40 -82
View File
@@ -1,36 +1,12 @@
module toolRegistry module toolRegistry
export toolStore, registerTool, registerAllTools, getTools, clearTools, listTool export registerTool, registerAllTools, clearTools, listTool
using Dates using Dates
using JSON, DataStructures using JSON, DataStructures
using ..type using ..type
"""
Per-agent isolated tool storage.
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
- `name::String` — identifier for debugging/logs
"""
struct toolStore
tools::OrderedDict{String, agentTool}
name::String
end
"""
toolStore(; name="default") -> toolStore
Create a new empty tool store.
# Keyword Arguments
- `name::String`: Display name for logging (default: `"default"`)
"""
function toolStore(; name::String="default")::toolStore
toolStore(OrderedDict{String, agentTool}(), name)
end
# ── MCP helper functions ──────────────────────────────────────────── # ── MCP helper functions ────────────────────────────────────────────
@@ -40,7 +16,7 @@ Extract text from MCP tool result content array.
Handles JSON-RPC 2.0 result content format: Handles JSON-RPC 2.0 result content format:
{"content": [{"type": "text", "text": "..."}], "isError": false} {"content": [{"type": "text", "text": "..."}], "isError": false}
""" """
function _extract_text_content(result::Dict)::String function _extract_text_content(result::AbstractDict)::String
content = get(result, "content", Any[]) content = get(result, "content", Any[])
if content isa Vector && !isempty(content) if content isa Vector && !isempty(content)
lines = String[] lines = String[]
@@ -90,8 +66,7 @@ function _wrap_mcp_tool(mcpserver, tool_def::AbstractDict{String, Any}; eventSin
inputSchema=params, inputSchema=params,
execute=(toolCallId::String, args::AbstractDict{String, Any}, execute=(toolCallId::String, args::AbstractDict{String, Any},
signal::Union{Nothing,abortSignal}, signal::Union{Nothing,abortSignal},
eventSink, eventSink) -> begin
llmCall=nothing) -> begin
try try
response = mcpserver("tools/call", name, args) response = mcpserver("tools/call", name, args)
@@ -140,7 +115,7 @@ Handles pagination via `nextCursor`. Skips tools already registered.
# Returns # Returns
- `Int`: number of new tools registered - `Int`: number of new tools registered
""" """
function register_mcp_tools(mcpserver, store::toolStore; eventSink=nothing)::Int function register_mcp_tools(tools::OrderedDict{String, agentTool}, mcpserver; eventSink=nothing)::Int
if mcpserver === nothing if mcpserver === nothing
return 0 return 0
end end
@@ -148,13 +123,14 @@ function register_mcp_tools(mcpserver, store::toolStore; eventSink=nothing)::Int
new_count = 0 new_count = 0
try try
eventSink("register_mcp_tools 1")
response = mcpserver("tools/list") response = mcpserver("tools/list")
eventSink("register_mcp_tools 2")
# Parse JSON-RPC 2.0 response envelope # Parse JSON-RPC 2.0 response envelope
if haskey(response, "error") if haskey(response, "error")
rpc_error = response["error"] rpc_error = response["error"]
err_msg = get(rpc_error, "message", "Unknown MCP error") err_msg = get(rpc_error, "message", "Unknown MCP error")
println("[toolRegistry:$(store.name)] MCP tools/list failed: $err_msg") println("[toolRegistry] MCP tools/list failed: $err_msg")
return 0 return 0
end end
if haskey(response, "result") if haskey(response, "result")
@@ -166,12 +142,12 @@ function register_mcp_tools(mcpserver, store::toolStore; eventSink=nothing)::Int
for tool_def in tools_array for tool_def in tools_array
name = tool_def["name"] name = tool_def["name"]
if haskey(store.tools, name) if haskey(tools, name)
continue continue
end end
@show "tool_def $(typeof(tool_def))"
wrapped = _wrap_mcp_tool(mcpserver, tool_def, eventSink=eventSink) wrapped = _wrap_mcp_tool(mcpserver, tool_def, eventSink=eventSink)
store.tools[name] = wrapped tools[name] = wrapped
new_count += 1 new_count += 1
end end
@@ -185,16 +161,16 @@ function register_mcp_tools(mcpserver, store::toolStore; eventSink=nothing)::Int
cursor = get(response, "nextCursor", nothing) cursor = get(response, "nextCursor", nothing)
for tool_def in tools_array for tool_def in tools_array
name = tool_def["name"] name = tool_def["name"]
if haskey(store.tools, name) if haskey(tools, name)
continue continue
end end
wrapped = _wrap_mcp_tool(mcpserver, tool_def, eventSink=eventSink) wrapped = _wrap_mcp_tool(mcpserver, tool_def, eventSink=eventSink)
store.tools[name] = wrapped tools[name] = wrapped
new_count += 1 new_count += 1
end end
end end
println("[toolRegistry:$(store.name)] Discovered $new_count MCP tools. Total: $(length(store.tools))") println("[toolRegistry] Discovered $new_count MCP tools. Total: $(length(tools))")
catch e catch e
bt = catch_backtrace() bt = catch_backtrace()
err_msg = sprint() do io err_msg = sprint() do io
@@ -205,75 +181,60 @@ function register_mcp_tools(mcpserver, store::toolStore; eventSink=nothing)::Int
eventSink(err_msg) eventSink(err_msg)
errMsg = sprint(showerror, e) errMsg = sprint(showerror, e)
println("[toolRegistry:$(store.name)] MCP tools/list failed: $errMsg") println("[toolRegistry] MCP tools/list failed: $errMsg")
end end
return new_count return new_count
end end
""" """
registerTool(store::toolStore, tool::agentTool) -> OrderedDict{String, agentTool} registerTool(tools::OrderedDict{String, agentTool}, tool::agentTool) -> OrderedDict{String, agentTool}
Add `tool` to `store`, overwriting any existing tool with the same name. Add `tool` to `tools`, overwriting any existing tool with the same name.
# Arguments # Arguments
- `store`: Tool store to modify - `tools`: Tool dict to modify
- `tool`: The `agentTool` to register - `tool`: The `agentTool` to register
# Returns # Returns
- The same `store.tools` dict (modified in place) - The same `tools` dict (modified in place)
""" """
function registerTool(store::toolStore, tool::agentTool; eventSink=nothing function registerTool(tools::OrderedDict{String, agentTool}, tool::agentTool; eventSink=nothing
)::OrderedDict{String, agentTool} )::OrderedDict{String, agentTool}
store.tools[tool.name] = tool tools[tool.name] = tool
println("[$(store.name)] Registered tool: $(tool.name)") println("[toolRegistry] Registered tool: $(tool.name)")
return store.tools return tools
end end
""" """
Return the tools registered in `store`. Remove all tools from the dict.
The returned dict is the **same object** stored inside `store`.
# Arguments # Arguments
- `store`: Tool store to query - `tools`: Tool dict to clear
# Returns
- `OrderedDict{String, agentTool}`: Tools keyed by name, in registration order
"""
function getTools(store::toolStore)::OrderedDict{String, agentTool}
return store.tools
end
"""
Remove all tools from `store`.
# Arguments
- `store`: Tool store to clear
# Returns # Returns
- `nothing` - `nothing`
""" """
function clearTools(store::toolStore)::Nothing function clearTools(tools::OrderedDict{String, agentTool})::Nothing
empty!(store.tools) empty!(tools)
println("[$(store.name)] Registry cleared") println("[toolRegistry] Registry cleared")
return nothing return nothing
end end
# ── listTools tool (auto-discover new MCP tools at runtime) ───────── # ── listTools tool (auto-discover new MCP tools at runtime) ─────────
""" """
listTool(store::toolStore, mcpserver) -> agentTool listTool(tools::OrderedDict{String, agentTool}, mcpserver) -> agentTool
MCP-aware listTools tool (JSON-RPC 2.0 protocol). MCP-aware listTools tool (JSON-RPC 2.0 protocol).
First call: queries the MCP server via `mcpserver("tools/list")`, registers First call: queries the MCP server via `mcpserver("tools/list")`, registers
all discovered tools into the shared `store.tools` (in-place mutation, with all discovered tools into `tools` (in-place mutation, with
pagination via nextCursor), then returns the full tool list. pagination via nextCursor), then returns the full tool list.
Subsequent calls: returns the current list (tools remain registered). Subsequent calls: returns the current list (tools remain registered).
""" """
function listTool(store::toolStore, mcpserver)::agentTool function listTool(tools::OrderedDict{String, agentTool}, mcpserver; eventSink=nothing)::agentTool
return agentTool( return agentTool(
name="listTools", name="listTools",
label="List Tools", label="List Tools",
@@ -285,16 +246,17 @@ function listTool(store::toolStore, mcpserver)::agentTool
), ),
execute=(toolCallId::String, args::AbstractDict{String, Any}, execute=(toolCallId::String, args::AbstractDict{String, Any},
signal::Union{Nothing,abortSignal}, signal::Union{Nothing,abortSignal},
eventSink, llmCall=nothing) -> begin eventSink) -> begin
# Discover and register MCP tools (idempotent — skips already registered) # Discover and register MCP tools (idempotent — skips already registered)
new_count = register_mcp_tools(mcpserver, store) new_count = register_mcp_tools(tools, mcpserver; eventSink=eventSink)
eventSink("tools dump: " * sprint(show, tools))
# Always include listTools itself in the count # Always include listTools itself in the count
total = length(store.tools) total = length(tools)
lines = String[ lines = String[
"- $(t.name): $(t.label)$(t.description)" "- $(t.name): $(t.label)$(t.description)"
for (k, t) in store.tools for (k, t) in tools
] ]
result_text = "Available tools ($total):\n" * join(lines, "\n") result_text = "Available tools ($total):\n" * join(lines, "\n")
@@ -313,23 +275,19 @@ end
# ── High-level API ────────────────────────────────────────────────── # ── High-level API ──────────────────────────────────────────────────
""" """
registerAllTools(store::toolStore, mcpserver) -> toolStore registerAllTools(tools::OrderedDict{String, agentTool}, mcpserver)
Register all tools for an agent: Register all tools for an agent:
1. Auto-discover existing tools from the MCP server 1. Auto-discover existing tools from the MCP server
2. Register the `listTools` tool so the agent can discover new tools at runtime 2. Register the `listTools` tool so the agent can discover new tools at runtime
# Arguments # Arguments
- `store`: The tool store to populate - `tools`: The tool dict to populate
- `mcpserver`: A callable struct that communicates with the MCP server - `mcpserver`: A callable struct that communicates with the MCP server
# Returns
- The populated `toolStore`
""" """
function registerAllTools(store::toolStore, mcpserver=nothing; eventSink=nothing)::toolStore function registerAllTools(tools::OrderedDict{String, agentTool}, mcpserver=nothing; eventSink=nothing)
register_mcp_tools(mcpserver, store; eventSink=eventSink) list_t = listTool(tools, mcpserver; eventSink=eventSink)
registerTool(store, listTool(store, mcpserver); eventSink=eventSink) registerTool(tools, list_t; eventSink=eventSink)
return store
end end
end # module end # module
+2 -4
View File
@@ -358,7 +358,6 @@ struct agentContext # Snapshot of the agent's conversa
systemPrompt::String # System prompt for the agent systemPrompt::String # System prompt for the agent
messages::Vector{agentMessage} # Conversation messages messages::Vector{agentMessage} # Conversation messages
tools::Union{OrderedDict{String, agentTool}, Nothing} # Available tools keyed by name tools::Union{OrderedDict{String, agentTool}, Nothing} # Available tools keyed by name
llmCall::Union{Any, Nothing} # LLM call function (for tools that need it)
end end
@@ -408,7 +407,7 @@ function agentState(
agentState( agentState(
systemPrompt, systemPrompt,
model, model,
deepcopy(tools), tools,
deepcopy(messages), deepcopy(messages),
Vector{String}(), Vector{String}(),
nothing, nothing,
@@ -443,7 +442,7 @@ end
""" """
Configuration for the agent tool execution loop. Configuration for the agent tool execution loop.
# Arguments # Fields
- `beforeToolCall::Union{Function, Nothing}`: Callback before tool execution - `beforeToolCall::Union{Function, Nothing}`: Callback before tool execution
- `afterToolCall::Union{Function, Nothing}`: Callback after tool execution - `afterToolCall::Union{Function, Nothing}`: Callback after tool execution
- `toolExecution::String`: Execution mode — "sequential" or "parallel" - `toolExecution::String`: Execution mode — "sequential" or "parallel"
@@ -452,7 +451,6 @@ struct agentLoopConfig
beforeToolCall::Union{Function, Nothing} beforeToolCall::Union{Function, Nothing}
afterToolCall::Union{Function, Nothing} afterToolCall::Union{Function, Nothing}
toolExecution::String toolExecution::String
llmCall::Union{Any, Nothing} # LLM call function (for tools like searchWine)
end end
""" """
+2 -2
View File
@@ -109,7 +109,7 @@ prepareContext(state).messages == deepcopy(state.messages)
# end # end
``` ```
""" """
function prepareContext(state::agentState, eventSink, llmCall=nothing)::agentContext function prepareContext(state::agentState, eventSink)::agentContext
#TODO filter tools from state.tools based on user intend in user message and tool description #TODO filter tools from state.tools based on user intend in user message and tool description
filteredTools = state.tools filteredTools = state.tools
@@ -120,7 +120,7 @@ function prepareContext(state::agentState, eventSink, llmCall=nothing)::agentCon
#TODO add system prompt, adjust/modify and inject additional context into messages #TODO add system prompt, adjust/modify and inject additional context into messages
preparedMessages = deepcopy(state.messages) # messages that will be send to LLM preparedMessages = deepcopy(state.messages) # messages that will be send to LLM
agentCtx = agentContext(preparedSystemPrompt, preparedMessages, filteredTools, llmCall) agentCtx = agentContext(preparedSystemPrompt, preparedMessages, filteredTools)
return agentCtx return agentCtx
end end