Skip to main content

mur_core/a2a/
protocol.rs

1//! A2A Protocol types — JSON-RPC 2.0 messages for agent-to-agent communication.
2//!
3//! Based on the Google A2A spec: JSON-RPC over HTTP with typed task lifecycle.
4
5use chrono::{DateTime, Utc};
6use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8
9// ---------------------------------------------------------------------------
10// JSON-RPC 2.0 Envelope
11// ---------------------------------------------------------------------------
12
13/// JSON-RPC 2.0 request envelope.
14#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct JsonRpcRequest {
16    pub jsonrpc: String,
17    pub id: serde_json::Value,
18    pub method: String,
19    #[serde(default, skip_serializing_if = "Option::is_none")]
20    pub params: Option<serde_json::Value>,
21}
22
23/// JSON-RPC 2.0 response envelope.
24#[derive(Debug, Clone, Serialize, Deserialize)]
25pub struct JsonRpcResponse {
26    pub jsonrpc: String,
27    pub id: serde_json::Value,
28    #[serde(skip_serializing_if = "Option::is_none")]
29    pub result: Option<serde_json::Value>,
30    #[serde(skip_serializing_if = "Option::is_none")]
31    pub error: Option<JsonRpcError>,
32}
33
34/// JSON-RPC 2.0 error object.
35#[derive(Debug, Clone, Serialize, Deserialize)]
36pub struct JsonRpcError {
37    pub code: i32,
38    pub message: String,
39    #[serde(skip_serializing_if = "Option::is_none")]
40    pub data: Option<serde_json::Value>,
41}
42
43impl JsonRpcRequest {
44    pub fn new(method: &str, params: Option<serde_json::Value>, id: serde_json::Value) -> Self {
45        Self {
46            jsonrpc: "2.0".into(),
47            id,
48            method: method.into(),
49            params,
50        }
51    }
52}
53
54impl JsonRpcResponse {
55    pub fn success(id: serde_json::Value, result: serde_json::Value) -> Self {
56        Self {
57            jsonrpc: "2.0".into(),
58            id,
59            result: Some(result),
60            error: None,
61        }
62    }
63
64    pub fn error(id: serde_json::Value, code: i32, message: &str) -> Self {
65        Self {
66            jsonrpc: "2.0".into(),
67            id,
68            result: None,
69            error: Some(JsonRpcError {
70                code,
71                message: message.into(),
72                data: None,
73            }),
74        }
75    }
76}
77
78// ---------------------------------------------------------------------------
79// Agent Card (/.well-known/agent.json)
80// ---------------------------------------------------------------------------
81
82/// Agent card published at `/.well-known/agent.json`.
83#[derive(Debug, Clone, Serialize, Deserialize)]
84pub struct AgentCard {
85    /// Human-readable agent name.
86    pub name: String,
87    /// Description of the agent's capabilities.
88    pub description: String,
89    /// Base URL of the agent's A2A endpoint.
90    pub url: String,
91    /// Agent version.
92    pub version: String,
93    /// Supported protocol version.
94    pub protocol_version: String,
95    /// Capabilities this agent provides.
96    pub capabilities: AgentCapabilities,
97    /// Skills/tasks this agent can perform.
98    #[serde(default)]
99    pub skills: Vec<AgentSkill>,
100    /// Authentication requirements.
101    #[serde(default, skip_serializing_if = "Option::is_none")]
102    pub authentication: Option<AgentAuthentication>,
103}
104
105/// Capabilities advertised by the agent.
106#[derive(Debug, Clone, Serialize, Deserialize)]
107pub struct AgentCapabilities {
108    /// Whether the agent supports streaming responses.
109    #[serde(default)]
110    pub streaming: bool,
111    /// Whether the agent supports push notifications.
112    #[serde(default)]
113    pub push_notifications: bool,
114    /// Whether the agent can store conversation state.
115    #[serde(default)]
116    pub state_management: bool,
117}
118
119/// A skill the agent can perform.
120#[derive(Debug, Clone, Serialize, Deserialize)]
121pub struct AgentSkill {
122    /// Unique skill identifier.
123    pub id: String,
124    /// Human-readable name.
125    pub name: String,
126    /// Description of what this skill does.
127    pub description: String,
128    /// Tags for categorization.
129    #[serde(default)]
130    pub tags: Vec<String>,
131    /// Input schema (JSON Schema).
132    #[serde(default, skip_serializing_if = "Option::is_none")]
133    pub input_schema: Option<serde_json::Value>,
134    /// Output schema (JSON Schema).
135    #[serde(default, skip_serializing_if = "Option::is_none")]
136    pub output_schema: Option<serde_json::Value>,
137}
138
139/// Authentication requirements for the agent.
140#[derive(Debug, Clone, Serialize, Deserialize)]
141pub struct AgentAuthentication {
142    /// Authentication schemes supported (e.g., "bearer", "api_key").
143    pub schemes: Vec<String>,
144}
145
146// ---------------------------------------------------------------------------
147// Task Lifecycle
148// ---------------------------------------------------------------------------
149
150/// Request to create or send a task to an agent.
151#[derive(Debug, Clone, Serialize, Deserialize)]
152pub struct TaskRequest {
153    /// Unique task identifier (client-generated).
154    pub id: String,
155    /// The skill/capability to invoke.
156    #[serde(default, skip_serializing_if = "Option::is_none")]
157    pub skill_id: Option<String>,
158    /// Messages forming the task conversation.
159    pub messages: Vec<TaskMessage>,
160    /// Optional metadata for the task.
161    #[serde(default)]
162    pub metadata: HashMap<String, serde_json::Value>,
163}
164
165/// Response to a task request.
166#[derive(Debug, Clone, Serialize, Deserialize)]
167pub struct TaskResponse {
168    /// The task identifier.
169    pub id: String,
170    /// Current status of the task.
171    pub status: TaskStatus,
172    /// Result artifacts produced by the task.
173    #[serde(default)]
174    pub artifacts: Vec<TaskArtifact>,
175    /// History of messages exchanged.
176    #[serde(default)]
177    pub history: Vec<TaskMessage>,
178    /// Metadata about the task.
179    #[serde(default)]
180    pub metadata: HashMap<String, serde_json::Value>,
181}
182
183/// Status update for a running task (used in streaming / push).
184#[derive(Debug, Clone, Serialize, Deserialize)]
185pub struct TaskStatusUpdate {
186    /// The task identifier.
187    pub id: String,
188    /// Updated status.
189    pub status: TaskStatus,
190    /// Whether this is the final update.
191    #[serde(default)]
192    pub final_update: bool,
193    /// Timestamp of this update.
194    pub timestamp: DateTime<Utc>,
195}
196
197/// Task status with associated state.
198#[derive(Debug, Clone, Serialize, Deserialize)]
199pub struct TaskStatus {
200    /// The current state of the task.
201    pub state: TaskState,
202    /// Human-readable status message.
203    #[serde(default, skip_serializing_if = "Option::is_none")]
204    pub message: Option<TaskMessage>,
205}
206
207/// Task lifecycle states.
208#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
209#[serde(rename_all = "snake_case")]
210pub enum TaskState {
211    /// Task has been submitted and is queued.
212    Submitted,
213    /// Task is currently being processed.
214    Working,
215    /// Task requires additional input from the client.
216    InputRequired,
217    /// Task completed successfully.
218    Completed,
219    /// Task failed.
220    Failed,
221    /// Task was cancelled.
222    Canceled,
223}
224
225/// Role of a message sender in a task conversation.
226#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
227#[serde(rename_all = "snake_case")]
228pub enum MessageRole {
229    /// Message from the user / client.
230    User,
231    /// Message from the agent.
232    Agent,
233}
234
235/// A message in a task conversation.
236#[derive(Debug, Clone, Serialize, Deserialize)]
237pub struct TaskMessage {
238    /// Who sent this message.
239    pub role: MessageRole,
240    /// Content parts of the message.
241    pub parts: Vec<MessagePart>,
242}
243
244/// A part of a message (text, file, data, etc.).
245#[derive(Debug, Clone, Serialize, Deserialize)]
246#[serde(tag = "type", rename_all = "snake_case")]
247pub enum MessagePart {
248    /// Plain text content.
249    Text { text: String },
250    /// File content (inline or reference).
251    File {
252        #[serde(skip_serializing_if = "Option::is_none")]
253        name: Option<String>,
254        #[serde(skip_serializing_if = "Option::is_none")]
255        mime_type: Option<String>,
256        #[serde(skip_serializing_if = "Option::is_none")]
257        data: Option<String>,
258        #[serde(skip_serializing_if = "Option::is_none")]
259        uri: Option<String>,
260    },
261    /// Structured data.
262    Data { data: serde_json::Value },
263}
264
265/// An artifact produced by a task.
266#[derive(Debug, Clone, Serialize, Deserialize)]
267pub struct TaskArtifact {
268    /// Artifact name.
269    pub name: String,
270    /// Content parts.
271    pub parts: Vec<MessagePart>,
272    /// Artifact index (for ordering).
273    #[serde(default)]
274    pub index: u32,
275}
276
277// ---------------------------------------------------------------------------
278// A2A Methods (JSON-RPC method names)
279// ---------------------------------------------------------------------------
280
281/// Well-known A2A JSON-RPC method names.
282pub mod methods {
283    /// Send a task to the agent.
284    pub const TASKS_SEND: &str = "tasks/send";
285    /// Get the status of a task.
286    pub const TASKS_GET: &str = "tasks/get";
287    /// Cancel a running task.
288    pub const TASKS_CANCEL: &str = "tasks/cancel";
289    /// Subscribe to task updates (streaming).
290    pub const TASKS_SEND_SUBSCRIBE: &str = "tasks/sendSubscribe";
291}
292
293/// Standard JSON-RPC error codes.
294pub mod error_codes {
295    pub const PARSE_ERROR: i32 = -32700;
296    pub const INVALID_REQUEST: i32 = -32600;
297    pub const METHOD_NOT_FOUND: i32 = -32601;
298    pub const INVALID_PARAMS: i32 = -32602;
299    pub const INTERNAL_ERROR: i32 = -32603;
300    pub const TASK_NOT_FOUND: i32 = -32000;
301    pub const SKILL_NOT_FOUND: i32 = -32001;
302}
303
304#[cfg(test)]
305mod tests {
306    use super::*;
307
308    #[test]
309    fn test_json_rpc_request_serialization() {
310        let req = JsonRpcRequest::new(
311            methods::TASKS_SEND,
312            Some(serde_json::json!({"id": "task-1"})),
313            serde_json::json!(1),
314        );
315        let json = serde_json::to_string(&req).unwrap();
316        assert!(json.contains("tasks/send"));
317        assert!(json.contains("\"jsonrpc\":\"2.0\""));
318    }
319
320    #[test]
321    fn test_json_rpc_response_success() {
322        let resp = JsonRpcResponse::success(
323            serde_json::json!(1),
324            serde_json::json!({"status": "ok"}),
325        );
326        assert!(resp.error.is_none());
327        assert!(resp.result.is_some());
328    }
329
330    #[test]
331    fn test_json_rpc_response_error() {
332        let resp = JsonRpcResponse::error(
333            serde_json::json!(1),
334            error_codes::TASK_NOT_FOUND,
335            "Task not found",
336        );
337        assert!(resp.result.is_none());
338        assert_eq!(resp.error.as_ref().unwrap().code, -32000);
339    }
340
341    #[test]
342    fn test_agent_card_serialization() {
343        let card = AgentCard {
344            name: "MUR Commander".into(),
345            description: "Autonomous workflow agent".into(),
346            url: "http://localhost:3939/a2a".into(),
347            version: "0.1.0".into(),
348            protocol_version: "0.1".into(),
349            capabilities: AgentCapabilities {
350                streaming: true,
351                push_notifications: false,
352                state_management: true,
353            },
354            skills: vec![AgentSkill {
355                id: "run-workflow".into(),
356                name: "Run Workflow".into(),
357                description: "Execute a named workflow".into(),
358                tags: vec!["automation".into()],
359                input_schema: None,
360                output_schema: None,
361            }],
362            authentication: None,
363        };
364        let json = serde_json::to_string_pretty(&card).unwrap();
365        assert!(json.contains("MUR Commander"));
366        assert!(json.contains("run-workflow"));
367    }
368
369    #[test]
370    fn test_task_request_response() {
371        let req = TaskRequest {
372            id: "task-1".into(),
373            skill_id: Some("run-workflow".into()),
374            messages: vec![TaskMessage {
375                role: MessageRole::User,
376                parts: vec![MessagePart::Text {
377                    text: "Run the deploy workflow".into(),
378                }],
379            }],
380            metadata: HashMap::new(),
381        };
382        let json = serde_json::to_string(&req).unwrap();
383        assert!(json.contains("task-1"));
384
385        let resp = TaskResponse {
386            id: "task-1".into(),
387            status: TaskStatus {
388                state: TaskState::Completed,
389                message: None,
390            },
391            artifacts: vec![TaskArtifact {
392                name: "result".into(),
393                parts: vec![MessagePart::Text {
394                    text: "Deployment successful".into(),
395                }],
396                index: 0,
397            }],
398            history: vec![],
399            metadata: HashMap::new(),
400        };
401        let json = serde_json::to_string(&resp).unwrap();
402        assert!(json.contains("completed"));
403    }
404
405    #[test]
406    fn test_task_status_update() {
407        let update = TaskStatusUpdate {
408            id: "task-1".into(),
409            status: TaskStatus {
410                state: TaskState::Working,
411                message: Some(TaskMessage {
412                    role: MessageRole::Agent,
413                    parts: vec![MessagePart::Text {
414                        text: "Processing step 2/5".into(),
415                    }],
416                }),
417            },
418            final_update: false,
419            timestamp: Utc::now(),
420        };
421        let json = serde_json::to_string(&update).unwrap();
422        assert!(json.contains("working"));
423        assert!(!update.final_update);
424    }
425
426    #[test]
427    fn test_task_states() {
428        let states = vec![
429            TaskState::Submitted,
430            TaskState::Working,
431            TaskState::InputRequired,
432            TaskState::Completed,
433            TaskState::Failed,
434            TaskState::Canceled,
435        ];
436        for state in states {
437            let json = serde_json::to_string(&state).unwrap();
438            let back: TaskState = serde_json::from_str(&json).unwrap();
439            assert_eq!(state, back);
440        }
441    }
442}