Skip to main content

lc_a2a/
protocol.rs

1//! A2A (Agent-to-Agent) protocol types.
2//!
3//! Defines the core data types for the A2A protocol, which enables
4//! inter-agent communication over JSON-RPC style messaging.
5//!
6//! # Core Types
7//!
8//! - **AgentCard**: Metadata describing an agent's identity and capabilities.
9//! - **A2ATask**: A unit of work sent between agents.
10//! - **A2AMessage**: A message within a task (role + content).
11//! - **TaskStatus**: Lifecycle states for a task.
12//! - **A2ARequest / A2AResponse**: JSON-RPC style request/response envelope.
13
14use serde::{Deserialize, Serialize};
15use serde_json::Value;
16
17/// Agent metadata card, served at `/.well-known/agent.json`.
18///
19/// Describes an agent's identity, endpoint, and capabilities so that
20/// other agents can discover and interact with it.
21#[derive(Debug, Clone, Serialize, Deserialize)]
22pub struct AgentCard {
23    /// Human-readable agent name.
24    pub name: String,
25    /// Description of what the agent does.
26    pub description: String,
27    /// Base URL where the agent accepts A2A requests.
28    pub url: String,
29    /// List of capability identifiers (e.g. "text-generation", "tool-use").
30    pub capabilities: Vec<String>,
31    /// Protocol version string.
32    #[serde(default = "default_version")]
33    pub version: String,
34    /// Provider/organization name (optional).
35    #[serde(skip_serializing_if = "Option::is_none")]
36    pub provider: Option<String>,
37    /// Documentation URL (optional).
38    #[serde(skip_serializing_if = "Option::is_none")]
39    pub documentation_url: Option<String>,
40    /// Authentication schemes supported (optional).
41    #[serde(skip_serializing_if = "Option::is_none")]
42    pub authentication: Option<Vec<String>>,
43    /// Default input modes (e.g. ["text", "image"]).
44    #[serde(default = "default_input_modes")]
45    pub default_input_modes: Vec<String>,
46    /// Default output modes (e.g. ["text"]).
47    #[serde(default = "default_output_modes")]
48    pub default_output_modes: Vec<String>,
49}
50
51fn default_version() -> String {
52    env!("CARGO_PKG_VERSION").to_string()
53}
54
55fn default_input_modes() -> Vec<String> {
56    vec!["text".to_string()]
57}
58
59fn default_output_modes() -> Vec<String> {
60    vec!["text".to_string()]
61}
62
63impl AgentCard {
64    /// Create a new agent card.
65    pub fn new(
66        name: impl Into<String>,
67        description: impl Into<String>,
68        url: impl Into<String>,
69    ) -> Self {
70        Self {
71            name: name.into(),
72            description: description.into(),
73            url: url.into(),
74            capabilities: Vec::new(),
75            version: default_version(),
76            provider: None,
77            documentation_url: None,
78            authentication: None,
79            default_input_modes: default_input_modes(),
80            default_output_modes: default_output_modes(),
81        }
82    }
83
84    /// Add a capability.
85    pub fn with_capability(mut self, capability: impl Into<String>) -> Self {
86        self.capabilities.push(capability.into());
87        self
88    }
89
90    /// Set the version.
91    pub fn with_version(mut self, version: impl Into<String>) -> Self {
92        self.version = version.into();
93        self
94    }
95
96    /// Set the provider/organization name.
97    pub fn with_provider(mut self, provider: impl Into<String>) -> Self {
98        self.provider = Some(provider.into());
99        self
100    }
101
102    /// Set the documentation URL.
103    pub fn with_documentation_url(mut self, url: impl Into<String>) -> Self {
104        self.documentation_url = Some(url.into());
105        self
106    }
107
108    /// Set the authentication schemes.
109    pub fn with_authentication(mut self, schemes: Vec<String>) -> Self {
110        self.authentication = Some(schemes);
111        self
112    }
113}
114
115/// Task lifecycle status.
116#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
117#[serde(rename_all = "lowercase")]
118pub enum TaskStatus {
119    /// Task has been submitted but not yet started.
120    Submitted,
121    /// Task is currently being processed.
122    Working,
123    /// Task requires additional input from the user.
124    InputRequired,
125    /// Task completed successfully.
126    Completed,
127    /// Task failed.
128    Failed,
129    /// Task was cancelled.
130    Cancelled,
131}
132
133impl std::fmt::Display for TaskStatus {
134    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
135        match self {
136            TaskStatus::Submitted => write!(f, "submitted"),
137            TaskStatus::Working => write!(f, "working"),
138            TaskStatus::InputRequired => write!(f, "input_required"),
139            TaskStatus::Completed => write!(f, "completed"),
140            TaskStatus::Failed => write!(f, "failed"),
141            TaskStatus::Cancelled => write!(f, "cancelled"),
142        }
143    }
144}
145
146/// A message within an A2A task.
147#[derive(Debug, Clone, Serialize, Deserialize)]
148pub struct A2AMessage {
149    /// Role of the message sender (e.g. "user", "agent").
150    pub role: String,
151    /// Text content of the message.
152    pub content: String,
153}
154
155impl A2AMessage {
156    /// Create a new message.
157    pub fn new(role: impl Into<String>, content: impl Into<String>) -> Self {
158        Self {
159            role: role.into(),
160            content: content.into(),
161        }
162    }
163
164    /// Create a user message.
165    pub fn user(content: impl Into<String>) -> Self {
166        Self::new("user", content)
167    }
168
169    /// Create an agent message.
170    pub fn agent(content: impl Into<String>) -> Self {
171        Self::new("agent", content)
172    }
173}
174
175/// A unit of work in the A2A protocol.
176#[derive(Debug, Clone, Serialize, Deserialize)]
177pub struct A2ATask {
178    /// Unique task identifier.
179    pub id: String,
180    /// The message that initiated this task.
181    pub message: A2AMessage,
182    /// Current status of the task.
183    pub status: TaskStatus,
184}
185
186impl A2ATask {
187    /// Create a new task with `Submitted` status.
188    pub fn new(id: impl Into<String>, message: A2AMessage) -> Self {
189        Self {
190            id: id.into(),
191            message,
192            status: TaskStatus::Submitted,
193        }
194    }
195
196    /// Set the task status.
197    pub fn with_status(mut self, status: TaskStatus) -> Self {
198        self.status = status;
199        self
200    }
201}
202
203/// Result of a completed A2A task.
204#[derive(Debug, Clone, Serialize, Deserialize)]
205pub struct A2ATaskResult {
206    /// Output text from the task.
207    pub output: String,
208}
209
210impl A2ATaskResult {
211    /// Create a new task result.
212    pub fn new(output: impl Into<String>) -> Self {
213        Self {
214            output: output.into(),
215        }
216    }
217}
218
219/// A2A JSON-RPC style request.
220#[derive(Debug, Clone, Serialize, Deserialize)]
221pub struct A2ARequest {
222    /// JSON-RPC version.
223    pub jsonrpc: String,
224    /// Request identifier.
225    pub id: u64,
226    /// Method name (e.g. "tasks/send", "tasks/get").
227    pub method: String,
228    /// Method parameters.
229    #[serde(skip_serializing_if = "Option::is_none")]
230    pub params: Option<Value>,
231}
232
233impl A2ARequest {
234    /// Create a new request.
235    pub fn new(id: u64, method: impl Into<String>, params: Option<Value>) -> Self {
236        Self {
237            jsonrpc: "2.0".to_string(),
238            id,
239            method: method.into(),
240            params,
241        }
242    }
243
244    /// Create a `tasks/send` request.
245    pub fn send_task(id: u64, message: &A2AMessage) -> Self {
246        let params = serde_json::to_value(message)
247            .ok()
248            .map(|v| serde_json::json!({ "message": v }));
249        Self::new(id, "tasks/send", params)
250    }
251
252    /// Create a `tasks/get` request.
253    pub fn get_task(id: u64, task_id: &str) -> Self {
254        Self::new(
255            id,
256            "tasks/get",
257            Some(serde_json::json!({ "taskId": task_id })),
258        )
259    }
260
261    /// Create a `tasks/cancel` request.
262    pub fn cancel_task(id: u64, task_id: &str) -> Self {
263        Self::new(
264            id,
265            "tasks/cancel",
266            Some(serde_json::json!({ "taskId": task_id })),
267        )
268    }
269}
270
271/// A2A JSON-RPC style response.
272#[derive(Debug, Clone, Serialize, Deserialize)]
273pub struct A2AResponse {
274    /// JSON-RPC version.
275    pub jsonrpc: String,
276    /// Request identifier this response corresponds to.
277    pub id: u64,
278    /// Result payload (present on success).
279    #[serde(skip_serializing_if = "Option::is_none")]
280    pub result: Option<Value>,
281    /// Error payload (present on failure).
282    #[serde(skip_serializing_if = "Option::is_none")]
283    pub error: Option<A2AErrorData>,
284}
285
286impl A2AResponse {
287    /// Create a success response.
288    pub fn ok(id: u64, result: Value) -> Self {
289        Self {
290            jsonrpc: "2.0".to_string(),
291            id,
292            result: Some(result),
293            error: None,
294        }
295    }
296
297    /// Create an error response.
298    pub fn error(id: u64, code: i32, message: impl Into<String>) -> Self {
299        Self {
300            jsonrpc: "2.0".to_string(),
301            id,
302            result: None,
303            error: Some(A2AErrorData {
304                code,
305                message: message.into(),
306            }),
307        }
308    }
309
310    /// Create an error response from error data.
311    pub fn from_error_data(id: u64, error: A2AErrorData) -> Self {
312        Self {
313            jsonrpc: "2.0".to_string(),
314            id,
315            result: None,
316            error: Some(error),
317        }
318    }
319
320    /// Whether this response represents an error.
321    pub fn is_error(&self) -> bool {
322        self.error.is_some()
323    }
324
325    /// Extract the result value, or return the error data.
326    pub fn into_result(self) -> Result<Value, A2AErrorData> {
327        if let Some(err) = self.error {
328            return Err(err);
329        }
330        Ok(self.result.unwrap_or(Value::Null))
331    }
332}
333
334/// Error payload within an A2A JSON-RPC response.
335#[derive(Debug, Clone, Serialize, Deserialize)]
336pub struct A2AErrorData {
337    /// Error code.
338    pub code: i32,
339    /// Human-readable error message.
340    pub message: String,
341}
342
343impl A2AErrorData {
344    /// Create new error data.
345    pub fn new(code: i32, message: impl Into<String>) -> Self {
346        Self {
347            code,
348            message: message.into(),
349        }
350    }
351
352    /// Standard error: method not found.
353    pub fn method_not_found() -> Self {
354        Self::new(-32601, "Method not found")
355    }
356
357    /// Standard error: invalid params.
358    pub fn invalid_params(msg: impl Into<String>) -> Self {
359        Self::new(-32602, msg)
360    }
361
362    /// Standard error: internal error.
363    pub fn internal_error(msg: impl Into<String>) -> Self {
364        Self::new(-32603, msg)
365    }
366}
367
368impl std::fmt::Display for A2AErrorData {
369    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
370        write!(f, "A2A Error [{}]: {}", self.code, self.message)
371    }
372}
373
374impl std::error::Error for A2AErrorData {}
375
376#[cfg(test)]
377mod tests {
378    use super::*;
379
380    #[test]
381    fn agent_card_new() {
382        let card = AgentCard::new("test-agent", "A test agent", "http://localhost:8080");
383        assert_eq!(card.name, "test-agent");
384        assert_eq!(card.description, "A test agent");
385        assert_eq!(card.url, "http://localhost:8080");
386        assert!(card.capabilities.is_empty());
387    }
388
389    #[test]
390    fn agent_card_with_capabilities() {
391        let card = AgentCard::new("agent", "desc", "http://localhost")
392            .with_capability("text-generation")
393            .with_capability("tool-use");
394        assert_eq!(card.capabilities.len(), 2);
395        assert_eq!(card.capabilities[0], "text-generation");
396        assert_eq!(card.capabilities[1], "tool-use");
397    }
398
399    #[test]
400    fn agent_card_serialization() {
401        let card =
402            AgentCard::new("agent", "desc", "http://localhost").with_capability("text-generation");
403        let json = serde_json::to_string(&card).unwrap();
404        assert!(json.contains("\"name\":\"agent\""));
405        assert!(json.contains("\"capabilities\""));
406        assert!(json.contains("\"text-generation\""));
407    }
408
409    #[test]
410    fn agent_card_deserialization() {
411        let json = r#"{"name":"agent","description":"desc","url":"http://localhost","capabilities":[],"version":"0.1.0"}"#;
412        let card: AgentCard = serde_json::from_str(json).unwrap();
413        assert_eq!(card.name, "agent");
414        assert_eq!(card.version, "0.1.0");
415    }
416
417    #[test]
418    fn task_status_serialization() {
419        let statuses = vec![
420            TaskStatus::Submitted,
421            TaskStatus::Working,
422            TaskStatus::Completed,
423            TaskStatus::Failed,
424            TaskStatus::Cancelled,
425        ];
426        let json = serde_json::to_string(&statuses).unwrap();
427        assert!(json.contains("\"submitted\""));
428        assert!(json.contains("\"working\""));
429        assert!(json.contains("\"completed\""));
430        assert!(json.contains("\"failed\""));
431        assert!(json.contains("\"cancelled\""));
432    }
433
434    #[test]
435    fn task_status_display() {
436        assert_eq!(TaskStatus::Submitted.to_string(), "submitted");
437        assert_eq!(TaskStatus::Working.to_string(), "working");
438        assert_eq!(TaskStatus::Completed.to_string(), "completed");
439        assert_eq!(TaskStatus::Failed.to_string(), "failed");
440        assert_eq!(TaskStatus::Cancelled.to_string(), "cancelled");
441    }
442
443    #[test]
444    fn a2a_message_user() {
445        let msg = A2AMessage::user("hello");
446        assert_eq!(msg.role, "user");
447        assert_eq!(msg.content, "hello");
448    }
449
450    #[test]
451    fn a2a_message_agent() {
452        let msg = A2AMessage::agent("response");
453        assert_eq!(msg.role, "agent");
454        assert_eq!(msg.content, "response");
455    }
456
457    #[test]
458    fn a2a_task_new() {
459        let task = A2ATask::new("task-1", A2AMessage::user("hello"));
460        assert_eq!(task.id, "task-1");
461        assert_eq!(task.status, TaskStatus::Submitted);
462        assert_eq!(task.message.content, "hello");
463    }
464
465    #[test]
466    fn a2a_task_with_status() {
467        let task =
468            A2ATask::new("task-1", A2AMessage::user("hello")).with_status(TaskStatus::Completed);
469        assert_eq!(task.status, TaskStatus::Completed);
470    }
471
472    #[test]
473    fn a2a_task_result() {
474        let result = A2ATaskResult::new("output text");
475        assert_eq!(result.output, "output text");
476    }
477
478    #[test]
479    fn a2a_request_new() {
480        let req = A2ARequest::new(1, "tasks/send", None);
481        assert_eq!(req.jsonrpc, "2.0");
482        assert_eq!(req.id, 1);
483        assert_eq!(req.method, "tasks/send");
484        assert!(req.params.is_none());
485    }
486
487    #[test]
488    fn a2a_request_send_task() {
489        let msg = A2AMessage::user("hello");
490        let req = A2ARequest::send_task(1, &msg);
491        assert_eq!(req.method, "tasks/send");
492        assert!(req.params.is_some());
493        let params = req.params.unwrap();
494        assert!(params.get("message").is_some());
495    }
496
497    #[test]
498    fn a2a_request_get_task() {
499        let req = A2ARequest::get_task(2, "task-123");
500        assert_eq!(req.method, "tasks/get");
501        let params = req.params.unwrap();
502        assert_eq!(params["taskId"], "task-123");
503    }
504
505    #[test]
506    fn a2a_request_cancel_task() {
507        let req = A2ARequest::cancel_task(3, "task-456");
508        assert_eq!(req.method, "tasks/cancel");
509        let params = req.params.unwrap();
510        assert_eq!(params["taskId"], "task-456");
511    }
512
513    #[test]
514    fn a2a_request_serialization_skips_none_params() {
515        let req = A2ARequest::new(1, "tasks/send", None);
516        let json = serde_json::to_string(&req).unwrap();
517        assert!(!json.contains("params"));
518    }
519
520    #[test]
521    fn a2a_response_ok() {
522        let resp = A2AResponse::ok(1, serde_json::json!({"status": "completed"}));
523        assert!(!resp.is_error());
524        assert!(resp.result.is_some());
525        assert!(resp.error.is_none());
526    }
527
528    #[test]
529    fn a2a_response_error() {
530        let resp = A2AResponse::error(1, -32601, "Method not found");
531        assert!(resp.is_error());
532        assert!(resp.result.is_none());
533        let err = resp.error.unwrap();
534        assert_eq!(err.code, -32601);
535    }
536
537    #[test]
538    fn a2a_response_into_result_ok() {
539        let resp = A2AResponse::ok(1, serde_json::json!({"output": "done"}));
540        let result = resp.into_result();
541        assert!(result.is_ok());
542        assert_eq!(result.unwrap()["output"], "done");
543    }
544
545    #[test]
546    fn a2a_response_into_result_err() {
547        let resp = A2AResponse::error(1, -32601, "Method not found");
548        let result = resp.into_result();
549        assert!(result.is_err());
550        assert_eq!(result.unwrap_err().code, -32601);
551    }
552
553    #[test]
554    fn a2a_response_serialization() {
555        let resp = A2AResponse::ok(1, serde_json::json!({"status": "completed"}));
556        let json = serde_json::to_string(&resp).unwrap();
557        assert!(json.contains("\"jsonrpc\":\"2.0\""));
558        assert!(json.contains("\"id\":1"));
559        assert!(json.contains("\"result\""));
560        assert!(!json.contains("\"error\""));
561    }
562
563    #[test]
564    fn a2a_error_data_display() {
565        let err = A2AErrorData::new(-1, "boom");
566        assert_eq!(format!("{}", err), "A2A Error [-1]: boom");
567    }
568
569    #[test]
570    fn a2a_error_data_standard_errors() {
571        let err = A2AErrorData::method_not_found();
572        assert_eq!(err.code, -32601);
573
574        let err = A2AErrorData::invalid_params("bad input");
575        assert_eq!(err.code, -32602);
576        assert!(err.message.contains("bad input"));
577
578        let err = A2AErrorData::internal_error("oops");
579        assert_eq!(err.code, -32603);
580    }
581
582    #[test]
583    fn roundtrip_request_json() {
584        let req = A2ARequest::send_task(42, &A2AMessage::user("test"));
585        let json = serde_json::to_string(&req).unwrap();
586        let parsed: A2ARequest = serde_json::from_str(&json).unwrap();
587        assert_eq!(parsed.id, 42);
588        assert_eq!(parsed.method, "tasks/send");
589    }
590
591    #[test]
592    fn roundtrip_response_json() {
593        let resp = A2AResponse::ok(7, serde_json::json!({"task": {"id": "t1"}}));
594        let json = serde_json::to_string(&resp).unwrap();
595        let parsed: A2AResponse = serde_json::from_str(&json).unwrap();
596        assert_eq!(parsed.id, 7);
597        assert!(!parsed.is_error());
598    }
599}