vex_api/a2a/
task.rs

1//! A2A Task types
2//!
3//! Types for A2A task requests and responses.
4//!
5//! # Security
6//!
7//! - Nonce + timestamp for replay protection
8//! - Task IDs are UUIDs (unguessable)
9//! - Responses include Merkle hash for verification
10
11use chrono::{DateTime, Utc};
12use serde::{Deserialize, Serialize};
13use uuid::Uuid;
14
15/// A2A Task request from another agent
16#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct TaskRequest {
18    /// Unique task ID (created by caller or generated)
19    #[serde(default = "Uuid::new_v4")]
20    pub id: Uuid,
21    /// Skill ID that the agent should use
22    pub skill: String,
23    /// Input data for the task
24    pub input: serde_json::Value,
25    /// Calling agent's identifier
26    #[serde(skip_serializing_if = "Option::is_none")]
27    pub caller_agent: Option<String>,
28    /// Nonce for replay protection
29    #[serde(skip_serializing_if = "Option::is_none")]
30    pub nonce: Option<String>,
31    /// Request timestamp
32    #[serde(default = "Utc::now")]
33    pub timestamp: DateTime<Utc>,
34}
35
36/// A2A Task response
37#[derive(Debug, Clone, Serialize, Deserialize)]
38pub struct TaskResponse {
39    /// Task ID (matches request)
40    pub id: Uuid,
41    /// Current status
42    pub status: TaskStatus,
43    /// Result data (if completed)
44    #[serde(skip_serializing_if = "Option::is_none")]
45    pub result: Option<serde_json::Value>,
46    /// Error message (if failed)
47    #[serde(skip_serializing_if = "Option::is_none")]
48    pub error: Option<String>,
49    /// Merkle hash of the result for verification
50    #[serde(skip_serializing_if = "Option::is_none")]
51    pub merkle_hash: Option<String>,
52    /// Response timestamp
53    pub timestamp: DateTime<Utc>,
54}
55
56/// Task execution status
57#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
58#[serde(rename_all = "lowercase")]
59pub enum TaskStatus {
60    /// Task is queued
61    Pending,
62    /// Task is running
63    Running,
64    /// Task completed successfully
65    Completed,
66    /// Task failed
67    Failed,
68    /// Task was cancelled
69    Cancelled,
70}
71
72impl TaskRequest {
73    /// Create a new task request
74    pub fn new(skill: impl Into<String>, input: serde_json::Value) -> Self {
75        Self {
76            id: Uuid::new_v4(),
77            skill: skill.into(),
78            input,
79            caller_agent: None,
80            nonce: None,
81            timestamp: Utc::now(),
82        }
83    }
84
85    /// Add caller agent info
86    pub fn with_caller(mut self, agent: impl Into<String>) -> Self {
87        self.caller_agent = Some(agent.into());
88        self
89    }
90
91    /// Add nonce for replay protection
92    pub fn with_nonce(mut self, nonce: impl Into<String>) -> Self {
93        self.nonce = Some(nonce.into());
94        self
95    }
96}
97
98impl TaskResponse {
99    /// Create a pending response
100    pub fn pending(id: Uuid) -> Self {
101        Self {
102            id,
103            status: TaskStatus::Pending,
104            result: None,
105            error: None,
106            merkle_hash: None,
107            timestamp: Utc::now(),
108        }
109    }
110
111    /// Create a completed response
112    pub fn completed(id: Uuid, result: serde_json::Value, merkle_hash: impl Into<String>) -> Self {
113        Self {
114            id,
115            status: TaskStatus::Completed,
116            result: Some(result),
117            error: None,
118            merkle_hash: Some(merkle_hash.into()),
119            timestamp: Utc::now(),
120        }
121    }
122
123    /// Create a failed response
124    pub fn failed(id: Uuid, error: impl Into<String>) -> Self {
125        Self {
126            id,
127            status: TaskStatus::Failed,
128            result: None,
129            error: Some(error.into()),
130            merkle_hash: None,
131            timestamp: Utc::now(),
132        }
133    }
134}
135
136#[cfg(test)]
137mod tests {
138    use super::*;
139
140    #[test]
141    fn test_task_request_new() {
142        let req = TaskRequest::new("verify", serde_json::json!({"claim": "test"}));
143        assert_eq!(req.skill, "verify");
144        assert!(req.caller_agent.is_none());
145    }
146
147    #[test]
148    fn test_task_request_builder() {
149        let req = TaskRequest::new("verify", serde_json::json!({}))
150            .with_caller("other-agent")
151            .with_nonce("abc123");
152
153        assert_eq!(req.caller_agent, Some("other-agent".to_string()));
154        assert_eq!(req.nonce, Some("abc123".to_string()));
155    }
156
157    #[test]
158    fn test_task_response_completed() {
159        let id = Uuid::new_v4();
160        let resp = TaskResponse::completed(id, serde_json::json!({"verified": true}), "hash123");
161
162        assert_eq!(resp.id, id);
163        assert_eq!(resp.status, TaskStatus::Completed);
164        assert!(resp.result.is_some());
165        assert_eq!(resp.merkle_hash, Some("hash123".to_string()));
166    }
167
168    #[test]
169    fn test_task_response_failed() {
170        let id = Uuid::new_v4();
171        let resp = TaskResponse::failed(id, "Verification failed");
172
173        assert_eq!(resp.status, TaskStatus::Failed);
174        assert_eq!(resp.error, Some("Verification failed".to_string()));
175    }
176
177    #[test]
178    fn test_serialization() {
179        let req = TaskRequest::new("hash", serde_json::json!({"text": "hello"}));
180        let json = serde_json::to_string(&req).unwrap();
181        assert!(json.contains("hash"));
182
183        let resp = TaskResponse::pending(req.id);
184        let json = serde_json::to_string(&resp).unwrap();
185        assert!(json.contains("pending"));
186    }
187}