Files
YiemAgent/docs/3-walkthrough.md
T
2026-09-12 16:09:00 +07:00

12 KiB

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:

mcp = mcpServer(conn, topic, senderID)
agent = yiemAgent(llmCall, mcpServer=mcp, systemPrompt="You are a helpful assistant")

Sequence:

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:

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:

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:

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:

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:

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:

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:

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:

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:

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:

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

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

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

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