519 lines
14 KiB
Markdown
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
|