# Specification: YiemAgent Agent Framework ## 1. Message Type System (`src/type.jl`) ### SP-001: userMessage | Field | Type | Required | Description | |-------|------|----------|-------------| | `role::String` | `String` | Yes | Always `"user"` | | `content::Vector{messageContent}` | `Vector{messageContent}` | No | Plain text and/or image content | | `timestamp::Timestamp` | `DateTime` | Yes | When the message was sent | **Keyword Constructor**: `userMessage(; role="user", content=[...], timestamp=now())` **Traceability**: - Requirement: FR-006 (message type conversion) - Solution Design: SD-006 (Julia structs for messages) ### SP-002: assistantMessage | Field | Type | Required | Description | |-------|------|----------|-------------| | `role::String` | `String` | Yes | Always `"assistant"` | | `content::Vector{messageContent}` | `Vector{messageContent}` | Yes | Text and/or image content | | `api::String` | `String` | Yes | API name used (e.g., `"openai"`) | | `provider::String` | `String` | Yes | Provider name (e.g., `"anthropic"`) | | `model::String` | `String` | Yes | Model identifier | | `usage::llmUsage` | `llmUsage` | Yes | Token usage (in/out) | | `stopReason::String` | `String` | Yes | Why generation stopped (e.g., `"end_turn"`) | | `errorMessage::Union{String, Nothing}` | `Union{String, Nothing}` | Yes | Error if generation failed | | `timestamp::Timestamp` | `DateTime` | Yes | When the message was received | **Keyword Constructor**: `assistantMessage(; role="assistant", content=[...], api="", provider="", model=nothing, usage=llmUsage(0,0), stopReason="end_turn", errorMessage=nothing, timestamp=now())` **Traceability**: - Requirement: FR-006 (message type conversion) - Solution Design: SD-006 ### SP-003: assistantMessageToolCall | Field | Type | Required | Description | |-------|------|----------|-------------| | `role::String` | `String` | Yes | Always `"assistant"` | | `toolCalls::Vector{agentToolCall}` | `Vector{agentToolCall}` | Yes | Tool calls to execute | | `content::Vector{messageContent}` | `Vector{messageContent}` | Yes | Reasoning/thinking content blocks | | `api::String` | `String` | Yes | API name used | | `provider::String` | `String` | Yes | Provider name | | `model::String` | `String` | Yes | Model identifier | | `usage::llmUsage` | `llmUsage` | Yes | Token usage | | `stopReason::String` | `String` | Yes | Why generation stopped (e.g., `"tool_calls"`) | | `errorMessage::Union{String, Nothing}` | `Union{String, Nothing}` | Yes | Error if generation failed | | `timestamp::Timestamp` | `DateTime` | Yes | When the message was received | **Keyword Constructor**: `assistantMessageToolCall(; role="assistant", toolCalls=[...], content=[], api="", provider="", model=nothing, usage=llmUsage(0,0), stopReason="tool_calls", errorMessage=nothing, timestamp=now())` **Traceability**: - Requirement: FR-003 (tool call loop) - Solution Design: SD-005 (lifecycle phases), SD-009 (format-agnostic LLM responses) ### SP-004: toolResultMessage | Field | Type | Required | Description | |-------|------|----------|-------------| | `role::String` | `String` | Yes | Always `"toolResult"` (note: `"tool"` in example) | | `toolCallId::String` | `String` | Yes | ID matching the tool call | | `toolName::String` | `String` | Yes | Name of the executed tool | | `content::Vector{messageContent}` | `Vector{messageContent}` | Yes | Tool output content | | `details::Any` | `Any` | Yes | Additional tool-specific details | | `usage::Union{llmUsage, Nothing}` | `Union{llmUsage, Nothing}` | Yes | Token usage if applicable | | `addedToolNames::Union{Vector{String}, Nothing}` | `Union{Vector{String}, Nothing}` | Yes | Tools added during execution | | `isError::Bool` | `Bool` | Yes | Whether the tool call resulted in an error | | `timestamp::Timestamp` | `DateTime` | Yes | When the result was recorded | **Keyword Constructor**: `toolResultMessage(; role="tool", toolCallId="", toolName="", content=[], details=nothing, usage=nothing, addedToolNames=nothing, isError=false, timestamp=now())` **Traceability**: - Requirement: FR-005 (tool lifecycle) - Solution Design: SD-005 ### SP-005: agentToolCall | Field | Type | Required | Description | |-------|------|----------|-------------| | `type::String` | `String` | Yes | Always `"function"` | | `id::String` | `String` | Yes | Unique tool call identifier | | `name::String` | `String` | Yes | Tool name | | `arguments::Dict{String, Any}` | `Dict{String, Any}` | Yes | Parsed tool arguments | **Traceability**: - Requirement: FR-003 (tool call extraction) - Solution Design: SD-009 (extract tool calls from LLM response) ### SP-006: messageContent Types **textContent** (`SP-006.1`): ```julia struct textContent <: messageContent text::String end ``` **imageContent** (`SP-006.2`): ```julia struct imageContent <: messageContent data::String # Base64-encoded image data mimeType::String # MIME type (e.g., "image/png") end ``` **reasoningContent** (`SP-006.3`): ```julia struct reasoningContent <: messageContent text::String end ``` **Traceability**: - Requirement: FR-006 (message content conversion) - Solution Design: SD-006 ## 2. LLM Model Configuration (`src/type.jl`) ### SP-007: llmModel | Field | Type | Required | Description | |-------|------|----------|-------------| | `id::String` | `String` | Yes | Unique model identifier | | `name::String` | `String` | Yes | Human-readable model name | | `provider::String` | `String` | Yes | Provider name (e.g., `"anthropic"`, `"openai"`) | | `baseUrl::String` | `String` | Yes | API endpoint base URL | | `reasoning::Bool` | `Bool` | Yes | Whether the model supports chain-of-thought | | `input::Vector{String}` | `Vector{String}` | Yes | Supported input modalities (e.g., `"text"`, `"image"`) | | `cost::modelCost` | `modelCost` | Yes | Pricing information | | `contextWindow::Int64` | `Int64` | Yes | Maximum context length in tokens | | `maxTokens::Int64` | `Int64` | Yes | Maximum output tokens per completion | **Traceability**: - Requirement: FR-002 (LLM agnosticism) - Solution Design: SD-003 ### SP-008: modelCost | Field | Type | Required | Description | |-------|------|----------|-------------| | `input::Float64` | `Float64` | Yes | Price per 1M input tokens | | `output::Float64` | `Float64` | Yes | Price per 1M output tokens | | `cache_read::Float64` | `Float64` | Yes | Price per 1M cached read tokens | | `cache_write::Float64` | `Float64` | Yes | Price per 1M cache write tokens | **Traceability**: - Requirement: N/A (internal tracking data) - Solution Design: SD-003 ## 3. MCP Tool Integration (`src/toolRegistry.jl`) ### SP-009: tools/list Response Format **Request**: `mcp("tools/list", sessionId; eventSink=eventSink)` **Response** (JSON-RPC 2.0): ```json { "result": { "tools": [ { "name": "tool_name", "title"?: "Human-readable title", "label"?: "Fallback title", "description": "What the tool does", "inputSchema": { "type": "object", "properties": { "field": { "type": "string", "description": "..." } }, "required": ["field"] } } ], "nextCursor"?: "pagination_cursor_or_empty" } } ``` **Response error**: ```json { "error": { "code": -32601, "message": "Method not found" } } ``` **Traceability**: - Requirement: FR-004 (MCP discovery) - Solution Design: SD-004 ### SP-010: tools/call Request/Response Format **Request**: `mcp("tools/call", toolName, args, sessionId; eventSink=eventSink)` **Response** (JSON-RPC 2.0): ```json { "result": { "content": [ {"type": "text", "text": "Tool output text"} ], "isError": false } } ``` **Response error**: ```json { "result": { "content": [ {"type": "text", "text": "Error message"} ], "isError": true } } ``` **Traceability**: - Requirement: FR-004 (tool execution) - Solution Design: SD-004 (`_wrap_mcp_tool`) ## 4. OpenAI Message Format (`src/utils.jl`) ### SP-011: system message format ```julia Dict( "role" => "system", "content" => [Dict("type" => "text", "text" => ctx.systemPrompt)] ) ``` **Traceability**: - Requirement: FR-006 - Solution Design: SD-006 ### SP-012: userMessage → OpenAI format ```julia Dict( "role" => "user", "content" => _messageContentToBlocks(msg.content) ) ``` Where `_messageContentToBlocks` converts: - `textContent(t)` → `Dict("type" => "text", "text" => t)` - `imageContent(data, mime)` → `Dict("type" => "image_url", "image_url" => Dict("url" => "data:$mime;base64,$data"))` **Traceability**: - Requirement: FR-006 - Solution Design: SD-006 ### SP-013: assistantMessageToolCall → OpenAI format ```julia Dict( "role" => "assistant", "content" => nothing, "tool_calls" => [ Dict( "id" => tc.id, "type" => tc.type, "function" => Dict( "name" => tc.name, "arguments" => JSON.json(tc.arguments) # serialized as JSON string ) ) ] ) ``` **Traceability**: - Requirement: FR-003 - Solution Design: SD-009 ### SP-014: assistantMessage → OpenAI format ```julia Dict( "role" => "assistant", "content" => _messageContentToBlocks(msg.content) ) ``` **Traceability**: - Requirement: FR-006 - Solution Design: SD-006 ### SP-015: toolResultMessage → OpenAI format ```julia Dict( "role" => "tool", "tool_call_id" => msg.toolCallId, "content" => _messageContentToBlocks(msg.content) ) ``` **Traceability**: - Requirement: FR-005, FR-006 - Solution Design: SD-005 ### SP-016: agentTool → OpenAI tools format ```julia Dict( "type" => "function", "function" => Dict( "name" => tool.name, "description" => tool.description, "parameters" => tool.inputSchema ) ) ``` **Traceability**: - Requirement: FR-004 - Solution Design: SD-004, SD-006 ## 5. LLM Response Format Agnostic Extraction (`src/agentCore.jl`) ### SP-017: format 1 — LMStudio.jl / vLLM style (dict with nested `message.tool_calls`) **Response dict** (key: `message::Dict`): ```json { "finish_reason": "tool_calls", "message": { "role": "assistant", "content": "", "reasoning_content": "I need to call getWeather...", "tool_calls": [ { "type": "function", "function": { "name": "getWeather", "arguments": "{\"city\":\"Bangkok\"}" // JSON string }, "id": "call_1" } ] } } ``` **When no tool calls**: ```json { "finish_reason": "stop", "message": { "role": "assistant", "content": "It's 22°C in Bangkok" } } ``` **Traceability**: - Requirement: FR-002 - Solution Design: SD-009 ### SP-018: format 2 — OpenAI API style (content blocks array) **Content blocks with tool calls** (`SP-018.1`): ```json { "content": [ { "type": "tool_calls", "tool_calls": [ { "type": "function", "function": { "name": "getWeather", "arguments": {"city": "Bangkok"} // already parsed dict }, "id": "call_1" } ] } ] } ``` **Content blocks with tool_call (already parsed)** (`SP-018.2`): ```json { "content": [ { "type": "tool_call", "name": "getWeather", "arguments": {"city": "Bangkok"}, "id": "call_1" } ] } ``` **Content blocks with text only** (`SP-018.3`): ```json { "content": [ {"type": "text", "text": "It's 22°C in Bangkok"} ] } ``` **Response dict extras** (common across all formats, `SP-018.4`): ```json { "api": "openai", "provider": "anthropic", "model": "gpt-4", "usage": {"inputTokens": 100, "outputTokens": 50}, "error_message": "Optional error" } ``` **Traceability**: - Requirement: FR-002 - Solution Design: SD-009 ## 6. Agent Loop Configuration (`src/type.jl` + `src/agentCore.jl`) ### SP-019: agentContext ```julia struct agentContext systemPrompt::String messages::Vector{agentMessage} tools::Union{OrderedDict{String, agentTool}, Nothing} # O(1) lookup by name end ``` **Traceability**: - Requirement: FR-003 (context preparation) - Solution Design: SD-003, SD-004 ### SP-020: agentState ```julia mutable struct agentState systemPrompt::String model::Union{llmModel, Nothing} tools::OrderedDict{String, agentTool} # insertion-ordered messages::Vector{agentMessage} # history (no system prompt) pendingToolCalls::Vector{String} # tool call IDs waiting for results errorMessage::Union{String, Nothing} # last error message end ``` **Traceability**: - Requirement: FR-001 (agent state management) - Solution Design: SD-006 ### SP-021: agentLoopConfig ```julia struct agentLoopConfig beforeToolCall::Union{Function, Nothing} afterToolCall::Union{Function, Nothing} toolExecution::String # "sequential" or "parallel" end ``` **Traceability**: - Requirement: FR-005 (lifecycle hooks) - Solution Design: SD-005 ### SP-022: abortSignal ```julia struct abortSignal aborted::Bool end ``` **Traceability**: - Requirement: NFR-101 (timeout handling) - Solution Design: SD-008 ## 7. Tool Validation Hooks (`src/utils.jl`) ### SP-023: validateRequiredArgs (default hook) **Signature**: `validateRequiredArgs(args::AbstractDict{String, Any}, inputSchema::AbstractDict{String, Any})::Union{Nothing, String}` **Behavior**: - Extract `required = get(inputSchema, "required", Any[])` - Check each required field exists in `keys(args)` - Return `nothing` if all present - Return `"Missing required arguments: $(join(missing, ", "))"` if any missing **Traceability**: - Requirement: NFR-301 (input validation) - Solution Design: SD-005 ### SP-024: validateToolArguments (wrapper hook) **Signature**: `validateToolArguments(tool::agentTool, prepared::agentToolCall)::AbstractDict{String, Any}` **Behavior**: - If `tool.validateRequiredArgs !== nothing`: call `tool.validateRequiredArgs(prepared.arguments)` - Else: call `validateRequiredArgs(prepared.arguments, tool.inputSchema)` - If returns `!nothing`: throw `ArgumentError(result)` - Caller (`prepareToolCall`) catches and returns `immediateOutcome` **Traceability**: - Requirement: FR-005 (tool validation) - Solution Design: SD-005