velocia 0.3.5

velocia – production-ready AI agent framework using ADK-Rust, A2A protocol, and AWS DynamoDB
use std::collections::HashMap;
use serde::{Deserialize, Serialize};
use uuid::Uuid;

// ── Agent Card ────────────────────────────────────────────────────────────────

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AgentCapabilities {
    #[serde(default)]
    pub streaming: bool,
    #[serde(default)]
    pub push_notifications: bool,
    #[serde(default)]
    pub state_transition_history: bool,
}

impl Default for AgentCapabilities {
    fn default() -> Self {
        Self { streaming: true, push_notifications: false, state_transition_history: false }
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AgentSkill {
    pub id: String,
    pub name: String,
    pub description: String,
    #[serde(default)]
    pub tags: Vec<String>,
    #[serde(default)]
    pub examples: Vec<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AgentCard {
    pub name: String,
    pub description: String,
    pub url: String,
    #[serde(default = "AgentCard::default_version")]
    pub version: String,
    #[serde(default = "AgentCard::default_input_modes")]
    pub default_input_modes: Vec<String>,
    #[serde(default = "AgentCard::default_output_modes")]
    pub default_output_modes: Vec<String>,
    pub capabilities: AgentCapabilities,
    #[serde(default)]
    pub skills: Vec<AgentSkill>,
    pub security: Option<Vec<HashMap<String, Vec<String>>>>,
    pub security_schemes: Option<HashMap<String, serde_json::Value>>,
}

impl AgentCard {
    fn default_version() -> String { "1.0.0".to_string() }
    fn default_input_modes() -> Vec<String> { vec!["text".to_string(), "text/plain".to_string()] }
    fn default_output_modes() -> Vec<String> { vec!["text".to_string(), "text/plain".to_string()] }
}

// ── Part ──────────────────────────────────────────────────────────────────────
// Discriminator: `kind` field with values "text" | "file" | "data"
// Matches a2a-sdk TextPart / FilePart / DataPart

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "lowercase")]
pub enum Part {
    Text {
        text: String,
        #[serde(skip_serializing_if = "Option::is_none")]
        metadata: Option<serde_json::Value>,
    },
    File {
        file: serde_json::Value,
        #[serde(skip_serializing_if = "Option::is_none")]
        metadata: Option<serde_json::Value>,
    },
    Data {
        data: serde_json::Value,
        #[serde(skip_serializing_if = "Option::is_none")]
        metadata: Option<serde_json::Value>,
    },
}

// ── Message ───────────────────────────────────────────────────────────────────

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum Role {
    User,
    Agent,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Message {
    /// Discriminator — always "message" per the A2A spec.
    #[serde(default = "Message::kind_value")]
    pub kind: String,
    pub message_id: String,
    pub role: Role,
    pub parts: Vec<Part>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub context_id: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub task_id: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub metadata: Option<serde_json::Value>,
}

impl Message {
    fn kind_value() -> String { "message".to_string() }

    pub fn user(text: impl Into<String>) -> Self {
        Self {
            kind: "message".to_string(),
            role: Role::User,
            parts: vec![Part::Text { text: text.into(), metadata: None }],
            message_id: Uuid::new_v4().to_string(),
            context_id: None,
            task_id: None,
            metadata: None,
        }
    }
}

// ── Task ──────────────────────────────────────────────────────────────────────

/// Matches a2a-sdk TaskState enum values exactly (note: "canceled" not "cancelled").
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum TaskState {
    Submitted,
    Working,
    Completed,
    Canceled,
    Failed,
    Unknown,
}

/// Matches a2a-sdk Artifact schema.
/// `artifact_id` is required. No `index` field.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Artifact {
    pub artifact_id: String,
    pub parts: Vec<Part>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub name: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub description: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub metadata: Option<serde_json::Value>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Task {
    pub id: String,
    pub context_id: String,
    pub state: TaskState,
    pub artifacts: Vec<Artifact>,
    pub history: Vec<Message>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub metadata: Option<serde_json::Value>,
}

// ── Streaming events ──────────────────────────────────────────────────────────
// These are the SSE events emitted by the agent during streaming.

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TaskStatus {
    pub state: TaskState,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub message: Option<Message>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub timestamp: Option<String>,
}

/// SSE event: artifact chunk produced by the agent.
/// `kind` = "artifact-update" is the a2a-sdk discriminator.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TaskArtifactUpdateEvent {
    #[serde(default = "TaskArtifactUpdateEvent::kind_value")]
    pub kind: String,
    pub task_id: String,
    pub context_id: String,
    pub artifact: Artifact,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub append: Option<bool>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub last_chunk: Option<bool>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub metadata: Option<serde_json::Value>,
}

impl TaskArtifactUpdateEvent {
    fn kind_value() -> String { "artifact-update".to_string() }
}

/// SSE event: task status change (including the final `final: true` event).
/// `kind` = "status-update" is the a2a-sdk discriminator.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TaskStatusUpdateEvent {
    #[serde(default = "TaskStatusUpdateEvent::kind_value")]
    pub kind: String,
    pub task_id: String,
    pub context_id: String,
    pub status: TaskStatus,
    pub r#final: bool,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub metadata: Option<serde_json::Value>,
}

impl TaskStatusUpdateEvent {
    fn kind_value() -> String { "status-update".to_string() }
}

// ── JSON-RPC envelope ─────────────────────────────────────────────────────────

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JsonRpcRequest {
    pub jsonrpc: String,
    pub method: String,
    pub id: Option<serde_json::Value>,
    pub params: serde_json::Value,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JsonRpcResponse<T> {
    pub jsonrpc: String,
    pub id: Option<serde_json::Value>,
    pub result: T,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JsonRpcError {
    pub code: i32,
    pub message: String,
    pub data: Option<serde_json::Value>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JsonRpcErrorResponse {
    pub jsonrpc: String,
    pub id: Option<serde_json::Value>,
    pub error: JsonRpcError,
}