Files
YiemAgent/docs/4-specification.md
2026-09-12 16:09:00 +07:00

519 lines
14 KiB
Markdown

# 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