319 lines
12 KiB
Markdown
319 lines
12 KiB
Markdown
# Walkthrough: YiemAgent Agent Framework
|
|
|
|
## Structural Guidance
|
|
|
|
- One **overview sequence diagram** per participant (high-level flow)
|
|
- **Detailed sub-diagrams** for complex interaction points (tool call loop, MCP discovery)
|
|
- Use sequence diagrams for request/response patterns; lifecycle states for multi-step operations
|
|
|
|
---
|
|
|
|
## External Caller
|
|
|
|
_The Developer calling `YiemAgent.jl` via `promptAgent`, `takeResponse`, `followUp`, and `stopAgent`._
|
|
|
|
### Session Start: Create Agent
|
|
|
|
**WT-001**: Developer creates an `yiemAgent` instance with `llmCall` and `mcpServer`
|
|
|
|
> Cross-reference: `2-solution_design.md:SD-001` (channel-based async), `SD-004` (MCP struct)
|
|
|
|
**Details**:
|
|
```julia
|
|
mcp = mcpServer(conn, topic, senderID)
|
|
agent = yiemAgent(llmCall, mcpServer=mcp, systemPrompt="You are a helpful assistant")
|
|
```
|
|
|
|
**Sequence**:
|
|
```mermaid
|
|
sequenceDiagram
|
|
participant Dev as Developer
|
|
part agent YiemAgent Constructor
|
|
Dev->>agent: yiemAgent(llmCall, ; mcpServer=mcp)
|
|
agent->>agent: create channels (input=16, followUp=32, output=16)
|
|
agent->>agent: registerAllTools(tools, mcp)
|
|
agent->>mcp: mcp("tools/list", sessionId)
|
|
mcp-->>agent: response (tools array + nextCursor)
|
|
agent->>agent: _wrap_mcp_tool for each tool
|
|
agent->>agent: registerTool(tools, listTools_tool)
|
|
agent->>agent: println("[toolRegistry] Discovered N MCP tools")
|
|
agent->>agent: yiemAgent struct with placeholder task
|
|
agent->>agent: @spawn _agentLoop(agent)
|
|
agent-->>Dev: yiemAgent instance
|
|
```
|
|
|
|
> **Traceability**: FR-004 (tool discovery), US-001 (agent creation)
|
|
|
|
### Send Message: User Prompt
|
|
|
|
**WT-002**: Developer sends a user message to the agent
|
|
|
|
> Cross-reference: `2-solution_design.md:SD-002` (background task dispatch)
|
|
|
|
**Sequence**:
|
|
```mermaid
|
|
sequenceDiagram
|
|
participant Dev as Developer
|
|
part agent AgentLoop
|
|
Dev->>agent: promptAgent(agent, text="What's the weather?")
|
|
agent->>agent: put!(inputChannel, msg_dict)
|
|
Dev->>agent: takeResponse(agent) # blocks
|
|
agent->>agent: inputChannel isready
|
|
agent->>agent: take!(inputChannel)
|
|
agent->>Dev: (returns agent for chaining)
|
|
```
|
|
|
|
> **Traceability**: FR-001 (channel processing), PR-001, SD-001, SD-002
|
|
|
|
### Receive Response
|
|
|
|
**WT-003**: Agent returns response on `outputChannel`
|
|
|
|
**Sequence**:
|
|
```mermaid
|
|
sequenceDiagram
|
|
participant Dev as Developer
|
|
part agent AgentLoop
|
|
AgentLoop->>AgentLoop: _processMessage runs
|
|
AgentLoop->>AgentLoop: while loop, llmCall, tool calls...
|
|
AgentLoop->>AgentLoop: no more tool calls → exit loop
|
|
AgentLoop->>AgentLoop: filter reasoningContent from final result
|
|
AgentLoop->>AgentLoop: _assistantMessageToOpenAI(result)
|
|
AgentLoop->>AgentLoop: put!(outputChannel, respondToUI)
|
|
AgentLoop->>Dev: take!(outputChannel) # unblocks
|
|
Dev->>Dev: receive OpenAI dict response
|
|
```
|
|
|
|
> **Traceability**: FR-001, FR-003, FR-006, PR-001
|
|
|
|
### Send Follow-up While Agent Busy
|
|
|
|
**WT-004**: Developer sends a follow-up message while agent is processing a tool call
|
|
|
|
**Sequence**:
|
|
```mermaid
|
|
sequenceDiagram
|
|
participant Dev as Developer
|
|
part agent AgentLoop
|
|
Dev->>agent: promptAgent(text="What's the weather in Tokyo?") # agent starts processing
|
|
AgentLoop->>AgentLoop: _processMessage started (processingTask = Task)
|
|
Dev->>agent: followUp(text="Also check Seoul") # while busy
|
|
agent->>agent: put!(followUpChannel, msg_dict)
|
|
AgentLoop->>AgentLoop: _processMessage completes (tool calls done)
|
|
AgentLoop->>AgentLoop: outputChannel has message → put!(outputChannel, response), take!(next msg)
|
|
AgentLoop->>AgentLoop: no inputChannel → check followUpChannel
|
|
AgentLoop->>AgentLoop: isready(followUpChannel) → take!(followUpMsg), put!(inputChannel, followUpMsg)
|
|
AgentLoop->>AgentLoop: processingTask = nothing → reset
|
|
AgentLoop->>AgentLoop: loop back → take!(inputChannel) from followUp
|
|
Dev->>agent: takeResponse() → blocks
|
|
AgentLoop->>Dev: next response via outputChannel
|
|
```
|
|
|
|
> **Traceability**: FR-001 (follow-up priority), PR-001, SD-001, SD-002
|
|
|
|
### Stop Agent
|
|
|
|
**WT-005**: Developer stops the agent gracefully
|
|
|
|
**Sequence**:
|
|
```mermaid
|
|
sequenceDiagram
|
|
participant Dev as Developer
|
|
part agent AgentLoop
|
|
Dev->>agent: stopAgent(agent)
|
|
agent->>agent: put!(inputChannel, :shutdown)
|
|
AgentLoop->>AgentLoop: check newUserMsg === :shutdown
|
|
AgentLoop->>AgentLoop: drain inputChannel (while isready)
|
|
AgentLoop->>AgentLoop: drain followUpChannel (while isready)
|
|
AgentLoop->>AgentLoop: break, catch, fetch(_agentLoop)
|
|
Dev->>agent: close(inputChannel, outputChannel, followUpChannel)
|
|
Dev-->>Dev: nothing (agent destroyed)
|
|
```
|
|
|
|
> **Traceability**: FR-001, NFR-201 (graceful exit), PR-001
|
|
|
|
---
|
|
|
|
## LLM Provider
|
|
|
|
_The external LLM API (e.g. Anthropic, OpenAI, LMStudio) receiving formatted messages and returning responses._
|
|
|
|
### Receive Formatted Message
|
|
|
|
**WT-010**: Agent sends LLM request in OpenAI format
|
|
|
|
> Cross-reference: `2-solution_design.md:SD-003` (formatMsgForLLM)
|
|
|
|
**Sequence**:
|
|
```mermaid
|
|
sequenceDiagram
|
|
part agent AgentLoop
|
|
AgentLoop->>AgentLoop: preparedContext = prepareContext(state)
|
|
AgentLoop->>AgentLoop: formattedMessages = formatMsgForLLM(preparedContext)
|
|
Note over AgentLoop: messages: [system, user, assistant, tool, ...]<br/>tools: [{"function": {"name": "getWeather", "parameters": {...}}}]
|
|
agentLoop->>llmCall: llmCall(formattedMessages, eventSink=eventSink)
|
|
llmCall->>Provider: HTTP POST /v1/chat/completions (JSON)
|
|
Provider-->>llmCall: JSON response
|
|
llmCall-->>AgentLoop: response = JSON.Object{String, Any}(...)
|
|
```
|
|
|
|
> **Traceability**: FR-002, SD-003, SD-009
|
|
|
|
### Receive Response: No Tool Calls
|
|
|
|
**WT-11**: Agent receives text-only response
|
|
|
|
**Sequence**:
|
|
```mermaid
|
|
sequenceDiagram
|
|
llmCall-->>AgentLoop: response = {finish_reason=> "stop", message=> {role=> "assistant", content=> "It's 22°C in Bangkok"}}
|
|
AgentLoop->>AgentLoop: _extractToolCalls(response)
|
|
AgentLoop->>AgentLoop: hasToolCalls = false
|
|
AgentLoop->>AgentLoop: assistant_msg = assistantMessage(content=[textContent("It's 22°C")], ...)
|
|
AgentLoop->>AgentLoop: push!(agentMsgHistory, assistant_msg)
|
|
AgentLoop->>AgentLoop: break (no tool calls)
|
|
AgentLoop->>AgentLoop: filter reasoningContent (if any)
|
|
AgentLoop->>AgentLoop: create final assistantMessage
|
|
AgentLoop->>AgentLoop: put!(outputChannel, OpenAI format dict)
|
|
```
|
|
|
|
> **Traceability**: FR-003 (loop exit), FR-006 (OpenAI conversion), PR-003
|
|
|
|
### Receive Response: With Tool Calls
|
|
|
|
**WT-012**: Agent receives tool call response
|
|
|
|
**Sequence**:
|
|
```mermaid
|
|
sequenceDiagram
|
|
llmCall-->>AgentLoop: response = {finish_reason=> "tool_calls",<br/>message=> {role=> "assistant",<br/>reasoning_content=> "I need to call getWeather...",<br/>tool_calls=> [{"id"=>"call_1", "function"=>{name=>"getWeather", arguments=>"{\"city\":\"Bangkok\"}"}}, ...]}}
|
|
AgentLoop->>AgentLoop: _extractToolCalls(response)
|
|
AgentLoop->>AgentLoop: hasToolCalls = true<br/>toolCallList = [agentToolCall("getWeather", ...)]
|
|
AgentLoop->>AgentLoop: assistant_msg = assistantMessageToolCall(toolCalls, reasoningContent, ...)
|
|
AgentLoop->>AgentLoop: push!(agentMsgHistory, assistant_msg)
|
|
AgentLoop->>AgentLoop: config = agentLoopConfig(beforeToolCall, afterToolCall, parallel="sequential")
|
|
AgentLoop->>AgentLoop: toolResultBatch = executeToolCalls(...)
|
|
...
|
|
```
|
|
|
|
> **Traceability**: FR-002, FR-003, PR-003, SD-003, SD-005, SD-009
|
|
|
|
---
|
|
|
|
## MCP Server
|
|
|
|
_The Model Context Protocol server providing tools via JSON-RPC 2.0 (transport via NATS or HTTP).
|
|
|
|
### Receive tools/list Request
|
|
|
|
**WT-020**: Agent discovers tools on startup
|
|
|
|
> Cross-reference: `2-solution_design.md:SD-004` (MCP integration)
|
|
|
|
**Sequence**:
|
|
```mermaid
|
|
sequenceDiagram
|
|
part agent AgentLoop
|
|
agent->>mcp: mcp("tools/list", sessionId; eventSink=eventSink)
|
|
Note over mcp: JSON-RPC 2.0: {"jsonrpc"=>"2.0", method=>"tools/list", "id"=>..., "params"=>{...}}
|
|
MCP_server-->>agent: response = {<br/> "result" => {<br/> "tools" => [name, label, description, inputSchema], ...<br/> , "nextCursor" => 123<br/> }<br/>}
|
|
agent->>agent: for tool_def in tools_array<br/> _wrap_mcp_tool(mcp, tool_def)
|
|
agent->>agent: while cursor !== nothing → paginate
|
|
agent->>agent: println("[toolRegistry] Discovered N MCP tools. Total: $(length(tools))")
|
|
```
|
|
|
|
> **Traceability**: FR-004 (MCP discovery), PR-004
|
|
|
|
### Receive tools/call Request
|
|
|
|
**WT-021**: Agent executes tool via MCP
|
|
|
|
**Sequence**:
|
|
```mermaid
|
|
sequenceDiagram
|
|
part agent Tool System
|
|
agent->>mcp: mcp("tools/call", "getWeather", args={"city" => "Bangkok"}, sessionId; eventSink=eventSink)
|
|
Note over mcp: JSON-RPC 2.0: {"jsonrpc"=>"2.0", method=>"tools/call", "params"=>{name, arguments, sessionId}}
|
|
MCP_server-->>agent: response = {<br/> "result" => {<br/> "content" => [{"type"=>"text", "text"=>"Sunny, 22°C"}],<br/> "isError" => false<br/> }<br/>}
|
|
agent->>agent: check haskey(response, "error") → no
|
|
agent->>agent: result_data = response["result"]
|
|
agent->>agent: content_text = _extract_text_content(result_data)
|
|
agent->>agent: return agentToolResult([textContent(content_text)], details={isError=>false}, nothing, false)
|
|
```
|
|
|
|
> **Traceability**: FR-004 (tool execution), PR-004, SD-004, SD-005
|
|
|
|
### Receive Error from MCP Server
|
|
|
|
**WT-022**: MCP server returns error
|
|
|
|
**Sequence**:
|
|
```mermaid
|
|
sequenceDiagram
|
|
MCP_server-->>agent: response = {<br/> "error" => {<br/> "code" => -32601,<br/> "message" => "Method not found"<br/> }<br/>}
|
|
agent->>agent: haskey(response, "error") → true
|
|
agent->>agent: return agentToolResult(<br/> [textContent("MCP error: Method not found")],<br/> details={"error" => "Method not found"},<br/> nothing, false<br/>)
|
|
agent->>AgentLoop: finalOutcome(error=true)
|
|
agent->>AgentLoop: push!(agentMsgHistory, toolResultMessage(...))
|
|
agent->>AgentLoop: next LLM turn receives error result
|
|
```
|
|
|
|
> **Traceability**: FR-004 (error handling), NFR-202 (never throw), PR-004, SD-005
|
|
|
|
---
|
|
|
|
## Error Paths and Edge Cases
|
|
|
|
**WT-030**: LLM times out (network error)
|
|
|
|
> Same participant POV — from caller's perspective, they block on `takeResponse()` until task exits
|
|
|
|
```mermaid
|
|
sequenceDiagram
|
|
participant Dev as Developer
|
|
part agent AgentLoop
|
|
Dev->>agent: takeResponse(agent) # blocks
|
|
agent->>llmCall: llmCall(messages) # calls external API
|
|
lltimout
|
|
LLM provider-->>AgentLoop: ERROR (timeout)
|
|
AgentLoop->>AgentLoop: @error "Agent loop failed"
|
|
AgentLoop->>AgentLoop: eventSink("error: ...")
|
|
AgentLoop->>Dev: fetch(_agentLoop) throws TaskFailedException
|
|
Dev->>agent: stopAgent(agent) # cleanup channels
|
|
```
|
|
|
|
**Traceability**: PR-001, NFR-201 (catch error and exit), SD-001
|
|
|
|
**WT-031**: Tool execution throws exception
|
|
|
|
```mermaid
|
|
sequenceDiagram
|
|
agent->>agent: executePreparedToolCall
|
|
agent->>tool: tool.execute(toolCallId, args, signal, eventSink)
|
|
note over tool: tool raises exception (e.g., network error, invalid JSON)
|
|
tool-->>agent: throw Exception
|
|
agent->>agent: catch e
|
|
agent->>agent: executedOutcome(createErrorToolResult("error message"), true)
|
|
agent->>AgentLoop: finalizeExecutedToolCall
|
|
AgentLoop->>AgentLoop: createToolResultMessage(error=true)
|
|
AgentLoop->>AgentLoop: push!(history, toolResultMessage)
|
|
AgentLoop->>AgentLoop: next llmCall with error in message history
|
|
```
|
|
|
|
**Traceability**: FR-005 (hook errors caught), NFR-202 (never throw), SD-005
|
|
|
|
**WT-032**: beforeToolCall hook blocks execution
|
|
|
|
```mermaid
|
|
sequenceDiagram
|
|
agent->>agent: beforeToolCall(beforeCtx, signal, eventSink)
|
|
agent->>agent: returns beforeToolCallResult(true, "User must approve")
|
|
agent->>agent: return immediateOutcome(createErrorToolResult("Tool blocked"), true)
|
|
agent->>AgentLoop: executeToolCalls → immediateOutcome
|
|
AgentLoop->>AgentLoop: finalizeExecutedToolCall
|
|
AgentLoop->>AgentLoop: createToolResultMessage(error=true)
|
|
AgentLoop->>AgentLoop: push! to history
|
|
```
|
|
|
|
**Traceability**: FR-005 (beforeToolCall can block), SD-005
|