140 lines
5.1 KiB
Markdown
140 lines
5.1 KiB
Markdown
# YiemAgent
|
|
|
|
Julia framework for building agents with tool use and MCP (Model Context Protocol) support.
|
|
|
|
## Getting Started
|
|
|
|
1. Install dependencies: `]add JSON, DataStructures, UUIDs, Dates, NATS, DataFrames`
|
|
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, eventSink=yourSink)
|
|
```
|
|
4. Call `runAgent(agent, message)` then `takeResponse(agent)`
|
|
|
|
## Architecture
|
|
|
|
```
|
|
src/
|
|
├── YiemAgent.jl # Module entry point
|
|
├── type.jl # Core types (messages, tools, agent state)
|
|
├── utils.jl # Message formatting, context preparation, validation
|
|
├── agentCore.jl # Agent loop, tool execution pipeline
|
|
├── api.jl # Public API (runAgent, takeResponse, followUp, stopAgent)
|
|
└── toolRegistry.jl # Tool store, MCP discovery, listTools registration
|
|
```
|
|
|
|
## MCP Protocol
|
|
|
|
YiemAgent uses JSON-RPC 2.0 for MCP communication. The `mcpServer` callable struct must implement:
|
|
|
|
```julia
|
|
# tools/list — returns tool definitions
|
|
mcpServer("tools/list") # → Dict("jsonrpc"=>"2.0", "id"=>1, "result"=>Dict("tools"=>[...], "nextCursor"=>...))
|
|
|
|
# tools/call — executes a tool
|
|
mcpServer("tools/call", toolName, arguments) # → Dict("jsonrpc"=>"2.0", "id"=>2, "result"=>Dict("content"=>[...], "isError"=>...))
|
|
```
|
|
|
|
Protocol error responses include `"error"` instead of `"result"`:
|
|
```julia
|
|
Dict("jsonrpc"=>"2.0", "id"=>2, "error"=>Dict("code"=>-32602, "message"=>"..."))
|
|
```
|
|
|
|
### Implementing the MCP Server Client
|
|
|
|
You must provide a callable struct that communicates with your MCP server. Example using NATS:
|
|
|
|
```julia
|
|
struct mcpServer
|
|
natsConn::NATS.Connection
|
|
topic::String
|
|
senderID::String
|
|
fileserver_url::String
|
|
end
|
|
|
|
# tools/list implementation
|
|
function (c::mcpServer)(method::String)
|
|
if method != "tools/list"
|
|
error("mcpServer: unexpected method '$method' (expected 'tools/list')")
|
|
end
|
|
|
|
payload = Dict("jsonrpc" => "2.0", "id" => 1, "method" => method, "params" => Dict{String, Any}())
|
|
payloads = [("payload", payload, "dictionary")]
|
|
_, msg_envelope_json_str = msghandler.smartpack(
|
|
c.topic, payloads;
|
|
sender_id=c.senderID,
|
|
msg_purpose="mcp_tools_list",
|
|
fileserver_url=c.fileserver_url)
|
|
|
|
reply = NATS.request(c.natsConn, c.topic, msg_envelope_json_str, timeout=180)
|
|
incoming_env = msghandler.smartunpack(String(reply.payload))
|
|
return incoming_env["payloads"][1][2]
|
|
end
|
|
|
|
# tools/call implementation
|
|
function (c::mcpServer)(method::String, toolName::String, arguments::Dict{String, Any})
|
|
if method != "tools/call"
|
|
error("mcpServer: unexpected method '$method' (expected 'tools/call')")
|
|
end
|
|
|
|
payload = Dict(
|
|
"jsonrpc" => "2.0",
|
|
"id" => 2,
|
|
"method" => method,
|
|
"params" => Dict(
|
|
"name" => toolName,
|
|
"arguments" => arguments
|
|
)
|
|
)
|
|
payloads = [("payload", payload, "dictionary")]
|
|
_, msg_envelope_json_str = msghandler.smartpack(
|
|
c.topic, payloads;
|
|
sender_id=c.senderID,
|
|
msg_purpose="mcp_tool_call",
|
|
fileserver_url=c.fileserver_url)
|
|
|
|
reply = NATS.request(c.natsConn, c.topic, msg_envelope_json_str, timeout=180)
|
|
incoming_env = msghandler.smartunpack(String(reply.payload))
|
|
return incoming_env["payloads"][1][2]
|
|
end
|
|
```
|
|
|
|
See `test/runtest.jl` for the complete working example.
|
|
|
|
## Tool Discovery
|
|
|
|
Tools are discovered dynamically via MCP server:
|
|
|
|
1. `listTool` is the only pre-registered tool
|
|
2. When the LLM calls `listTools()`, tools are discovered from the MCP server and registered at runtime
|
|
3. Supports pagination via `nextCursor` for large tool sets
|
|
|
|
## Agent API
|
|
|
|
| Function | Description |
|
|
|----------|-------------|
|
|
| `runAgent(agent, msg)` | Send a message to the agent's input channel |
|
|
| `takeResponse(agent)` | Block and take the agent's response from output channel |
|
|
| `followUp(agent, msg)` | Send a follow-up message while agent is still processing |
|
|
| `stopAgent(agent)` | Gracefully stop the agent and close channels |
|
|
|
|
## Agent Lifecycle
|
|
|
|
1. `yiemAgent()` spawns a background task (`_agentLoop`) listening on `inputChannel` and `followUpChannel`
|
|
2. User messages enter via `runAgent()` or `followUp()`
|
|
3. `_processMessage()` handles LLM calls, tool execution, and conversation history
|
|
4. Tool execution follows three phases: `prepareToolCall` → `executePreparedToolCall` → `finalizeExecutedToolCall`
|
|
5. `beforeToolCall`/`afterToolCall` hooks allow pre/post-processing of tool calls
|
|
6. Tool `execute` functions can set `terminate=true` to stop the agent loop
|
|
|
|
## Hooks
|
|
|
|
| Hook | Signature | Purpose |
|
|
|------|-----------|---------|
|
|
| `prepareContext` | `(state, sink, llmCall) -> ctx` | Transform messages/context before LLM call |
|
|
| `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 |
|
|
| `eventSink` | `(msg) -> nothing` | Callback for agent events/debug messages |
|