14 KiB
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):
struct textContent <: messageContent
text::String
end
imageContent (SP-006.2):
struct imageContent <: messageContent
data::String # Base64-encoded image data
mimeType::String # MIME type (e.g., "image/png")
end
reasoningContent (SP-006.3):
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):
{
"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:
{
"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):
{
"result": {
"content": [
{"type": "text", "text": "Tool output text"}
],
"isError": false
}
}
Response error:
{
"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
Dict(
"role" => "system",
"content" => [Dict("type" => "text", "text" => ctx.systemPrompt)]
)
Traceability:
- Requirement: FR-006
- Solution Design: SD-006
SP-012: userMessage → OpenAI format
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
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
Dict(
"role" => "assistant",
"content" => _messageContentToBlocks(msg.content)
)
Traceability:
- Requirement: FR-006
- Solution Design: SD-006
SP-015: toolResultMessage → OpenAI format
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
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):
{
"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:
{
"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):
{
"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):
{
"content": [
{
"type": "tool_call",
"name": "getWeather",
"arguments": {"city": "Bangkok"},
"id": "call_1"
}
]
}
Content blocks with text only (SP-018.3):
{
"content": [
{"type": "text", "text": "It's 22°C in Bangkok"}
]
}
Response dict extras (common across all formats, SP-018.4):
{
"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
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
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
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
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
nothingif 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: calltool.validateRequiredArgs(prepared.arguments) - Else: call
validateRequiredArgs(prepared.arguments, tool.inputSchema) - If returns
!nothing: throwArgumentError(result) - Caller (
prepareToolCall) catches and returnsimmediateOutcome
Traceability:
- Requirement: FR-005 (tool validation)
- Solution Design: SD-005