Skip to main content

codetether_agent/a2a/
types.rs

1//! A2A Protocol Types
2//!
3//! Types aligned with the A2A gRPC specification (a2a.proto) and JSON-RPC schema.
4//! This module provides full protocol parity with the proto definition including
5//! streaming events, security schemes, push notification config CRUD, and
6//! agent card extensions.
7
8use serde::{Deserialize, Serialize};
9use std::collections::HashMap;
10
11/// A2A Task States
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
13#[serde(rename_all = "kebab-case")]
14pub enum TaskState {
15    Submitted,
16    Working,
17    Completed,
18    Failed,
19    Cancelled,
20    InputRequired,
21    Rejected,
22    AuthRequired,
23}
24
25impl TaskState {
26    /// Check if this is a terminal state
27    pub fn is_terminal(&self) -> bool {
28        matches!(
29            self,
30            TaskState::Completed | TaskState::Failed | TaskState::Cancelled | TaskState::Rejected
31        )
32    }
33
34    /// Check if this is an active state
35    pub fn is_active(&self) -> bool {
36        !self.is_terminal()
37    }
38}
39
40/// An A2A Task
41#[derive(Debug, Clone, Serialize, Deserialize)]
42#[serde(rename_all = "camelCase")]
43pub struct Task {
44    pub id: String,
45    pub context_id: Option<String>,
46    pub status: TaskStatus,
47    #[serde(default)]
48    pub artifacts: Vec<Artifact>,
49    #[serde(default)]
50    pub history: Vec<Message>,
51    #[serde(default)]
52    pub metadata: HashMap<String, serde_json::Value>,
53}
54
55/// Task status information
56#[derive(Debug, Clone, Serialize, Deserialize)]
57#[serde(rename_all = "camelCase")]
58pub struct TaskStatus {
59    pub state: TaskState,
60    #[serde(default)]
61    pub message: Option<Message>,
62    pub timestamp: Option<String>,
63}
64
65/// An A2A Message
66#[derive(Debug, Clone, Serialize, Deserialize)]
67#[serde(rename_all = "camelCase")]
68pub struct Message {
69    pub message_id: String,
70    pub role: MessageRole,
71    pub parts: Vec<Part>,
72    #[serde(default)]
73    pub context_id: Option<String>,
74    #[serde(default)]
75    pub task_id: Option<String>,
76    #[serde(default)]
77    pub metadata: HashMap<String, serde_json::Value>,
78    /// URIs of extensions present in or contributing to this Message
79    #[serde(default, skip_serializing_if = "Vec::is_empty")]
80    pub extensions: Vec<String>,
81}
82
83#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
84#[serde(rename_all = "lowercase")]
85pub enum MessageRole {
86    User,
87    Agent,
88}
89
90/// A part of a message
91#[derive(Debug, Clone, Serialize, Deserialize)]
92#[serde(tag = "kind", rename_all = "camelCase")]
93pub enum Part {
94    #[serde(rename = "text")]
95    Text { text: String },
96    #[serde(rename = "file")]
97    File { file: FileContent },
98    #[serde(rename = "data")]
99    Data { data: serde_json::Value },
100}
101
102/// File content (bytes or URI)
103#[derive(Debug, Clone, Serialize, Deserialize)]
104#[serde(rename_all = "camelCase")]
105pub struct FileContent {
106    #[serde(skip_serializing_if = "Option::is_none")]
107    pub bytes: Option<String>, // Base64 encoded
108    #[serde(skip_serializing_if = "Option::is_none")]
109    pub uri: Option<String>,
110    #[serde(skip_serializing_if = "Option::is_none")]
111    pub mime_type: Option<String>,
112    #[serde(skip_serializing_if = "Option::is_none")]
113    pub name: Option<String>,
114}
115
116/// An artifact produced by a task
117#[derive(Debug, Clone, Serialize, Deserialize)]
118#[serde(rename_all = "camelCase")]
119pub struct Artifact {
120    pub artifact_id: String,
121    pub parts: Vec<Part>,
122    #[serde(skip_serializing_if = "Option::is_none")]
123    pub name: Option<String>,
124    #[serde(skip_serializing_if = "Option::is_none")]
125    pub description: Option<String>,
126    #[serde(default)]
127    pub metadata: HashMap<String, serde_json::Value>,
128    /// URIs of extensions present in or contributing to this Artifact
129    #[serde(default, skip_serializing_if = "Vec::is_empty")]
130    pub extensions: Vec<String>,
131}
132
133/// Agent Card - self-describing manifest (full proto parity)
134#[derive(Debug, Clone, Serialize, Deserialize)]
135#[serde(rename_all = "camelCase")]
136pub struct AgentCard {
137    pub name: String,
138    pub description: String,
139    pub url: String,
140    pub version: String,
141    #[serde(default = "default_protocol_version")]
142    pub protocol_version: String,
143    /// Transport of the preferred endpoint. Defaults to "JSONRPC".
144    #[serde(default, skip_serializing_if = "Option::is_none")]
145    pub preferred_transport: Option<String>,
146    /// Additional supported transports
147    #[serde(default, skip_serializing_if = "Vec::is_empty")]
148    pub additional_interfaces: Vec<AgentInterface>,
149    pub capabilities: AgentCapabilities,
150    pub skills: Vec<AgentSkill>,
151    #[serde(default)]
152    pub default_input_modes: Vec<String>,
153    #[serde(default)]
154    pub default_output_modes: Vec<String>,
155    #[serde(skip_serializing_if = "Option::is_none")]
156    pub provider: Option<AgentProvider>,
157    #[serde(skip_serializing_if = "Option::is_none")]
158    pub icon_url: Option<String>,
159    #[serde(skip_serializing_if = "Option::is_none")]
160    pub documentation_url: Option<String>,
161    /// Security scheme definitions
162    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
163    pub security_schemes: HashMap<String, SecurityScheme>,
164    /// Security requirements (OR of ANDs)
165    #[serde(default, skip_serializing_if = "Vec::is_empty")]
166    pub security: Vec<SecurityRequirement>,
167    /// Whether the agent supports an extended card when authenticated
168    #[serde(default)]
169    pub supports_authenticated_extended_card: bool,
170    /// JWS signatures for this agent card
171    #[serde(default, skip_serializing_if = "Vec::is_empty")]
172    pub signatures: Vec<AgentCardSignature>,
173}
174
175fn default_protocol_version() -> String {
176    "0.3.0".to_string()
177}
178
179#[derive(Debug, Clone, Default, Serialize, Deserialize)]
180#[serde(rename_all = "camelCase")]
181pub struct AgentCapabilities {
182    #[serde(default)]
183    pub streaming: bool,
184    #[serde(default)]
185    pub push_notifications: bool,
186    #[serde(default)]
187    pub state_transition_history: bool,
188    /// Extensions supported by this agent
189    #[serde(default, skip_serializing_if = "Vec::is_empty")]
190    pub extensions: Vec<AgentExtension>,
191}
192
193#[derive(Debug, Clone, Serialize, Deserialize)]
194#[serde(rename_all = "camelCase")]
195pub struct AgentSkill {
196    pub id: String,
197    pub name: String,
198    pub description: String,
199    #[serde(default)]
200    pub tags: Vec<String>,
201    #[serde(default)]
202    pub examples: Vec<String>,
203    #[serde(default)]
204    pub input_modes: Vec<String>,
205    #[serde(default)]
206    pub output_modes: Vec<String>,
207}
208
209#[derive(Debug, Clone, Serialize, Deserialize)]
210pub struct AgentProvider {
211    pub organization: String,
212    pub url: String,
213}
214
215/// JSON-RPC 2.0 Request
216#[derive(Debug, Clone, Serialize, Deserialize)]
217pub struct JsonRpcRequest {
218    pub jsonrpc: String,
219    pub id: serde_json::Value,
220    pub method: String,
221    #[serde(default)]
222    pub params: serde_json::Value,
223}
224
225/// JSON-RPC 2.0 Response
226#[derive(Debug, Clone, Serialize, Deserialize)]
227pub struct JsonRpcResponse {
228    pub jsonrpc: String,
229    pub id: serde_json::Value,
230    #[serde(skip_serializing_if = "Option::is_none")]
231    pub result: Option<serde_json::Value>,
232    #[serde(skip_serializing_if = "Option::is_none")]
233    pub error: Option<JsonRpcError>,
234}
235
236#[derive(Debug, Clone, Serialize, Deserialize)]
237pub struct JsonRpcError {
238    pub code: i32,
239    pub message: String,
240    #[serde(skip_serializing_if = "Option::is_none")]
241    pub data: Option<serde_json::Value>,
242}
243
244#[allow(dead_code)]
245impl JsonRpcError {
246    /// Create a parse error (-32700)
247    pub fn parse_error(msg: impl Into<String>) -> Self {
248        Self {
249            code: PARSE_ERROR,
250            message: msg.into(),
251            data: None,
252        }
253    }
254
255    /// Create an invalid request error (-32600)
256    pub fn invalid_request(msg: impl Into<String>) -> Self {
257        Self {
258            code: INVALID_REQUEST,
259            message: msg.into(),
260            data: None,
261        }
262    }
263
264    /// Create a method not found error (-32601)
265    pub fn method_not_found(method: &str) -> Self {
266        Self {
267            code: METHOD_NOT_FOUND,
268            message: format!("Method not found: {}", method),
269            data: None,
270        }
271    }
272
273    /// Create an invalid params error (-32602)
274    pub fn invalid_params(msg: impl Into<String>) -> Self {
275        Self {
276            code: INVALID_PARAMS,
277            message: msg.into(),
278            data: None,
279        }
280    }
281
282    /// Create an internal error (-32603)
283    pub fn internal_error(msg: impl Into<String>) -> Self {
284        Self {
285            code: INTERNAL_ERROR,
286            message: msg.into(),
287            data: None,
288        }
289    }
290
291    /// Create an unsupported operation error (-32004)
292    pub fn unsupported_operation(msg: impl Into<String>) -> Self {
293        Self {
294            code: UNSUPPORTED_OPERATION,
295            message: msg.into(),
296            data: None,
297        }
298    }
299}
300
301// Standard JSON-RPC error codes
302#[allow(dead_code)]
303pub const PARSE_ERROR: i32 = -32700;
304#[allow(dead_code)]
305pub const INVALID_REQUEST: i32 = -32600;
306pub const METHOD_NOT_FOUND: i32 = -32601;
307pub const INVALID_PARAMS: i32 = -32602;
308pub const INTERNAL_ERROR: i32 = -32603;
309
310// A2A-specific error codes
311pub const TASK_NOT_FOUND: i32 = -32001;
312pub const TASK_NOT_CANCELABLE: i32 = -32002;
313#[allow(dead_code)]
314pub const PUSH_NOT_SUPPORTED: i32 = -32003;
315pub const UNSUPPORTED_OPERATION: i32 = -32004;
316#[allow(dead_code)]
317pub const CONTENT_TYPE_NOT_SUPPORTED: i32 = -32005;
318
319/// Message send parameters
320#[derive(Debug, Clone, Serialize, Deserialize)]
321#[serde(rename_all = "camelCase")]
322pub struct MessageSendParams {
323    pub message: Message,
324    #[serde(skip_serializing_if = "Option::is_none")]
325    pub configuration: Option<MessageSendConfiguration>,
326}
327
328#[derive(Debug, Clone, Serialize, Deserialize)]
329#[serde(rename_all = "camelCase")]
330pub struct MessageSendConfiguration {
331    #[serde(default)]
332    pub accepted_output_modes: Vec<String>,
333    #[serde(skip_serializing_if = "Option::is_none")]
334    pub blocking: Option<bool>,
335    #[serde(skip_serializing_if = "Option::is_none")]
336    pub history_length: Option<usize>,
337    #[serde(skip_serializing_if = "Option::is_none")]
338    pub push_notification_config: Option<PushNotificationConfig>,
339}
340
341#[derive(Debug, Clone, Serialize, Deserialize)]
342#[serde(rename_all = "camelCase")]
343pub struct PushNotificationConfig {
344    pub url: String,
345    #[serde(skip_serializing_if = "Option::is_none")]
346    pub token: Option<String>,
347    #[serde(skip_serializing_if = "Option::is_none")]
348    pub id: Option<String>,
349}
350
351/// Task query parameters
352#[derive(Debug, Clone, Serialize, Deserialize)]
353#[serde(rename_all = "camelCase")]
354pub struct TaskQueryParams {
355    pub id: String,
356    #[serde(skip_serializing_if = "Option::is_none")]
357    pub history_length: Option<usize>,
358}
359
360// ─── Streaming & Response Event Types ────────────────────────────────────
361
362/// A task status update pushed via SSE / streaming
363#[derive(Debug, Clone, Serialize, Deserialize)]
364#[serde(rename_all = "camelCase")]
365pub struct TaskStatusUpdateEvent {
366    pub id: String,
367    pub status: TaskStatus,
368    #[serde(default, rename = "final")]
369    pub is_final: bool,
370    #[serde(default)]
371    pub metadata: HashMap<String, serde_json::Value>,
372}
373
374/// A task artifact update pushed via SSE / streaming
375#[derive(Debug, Clone, Serialize, Deserialize)]
376#[serde(rename_all = "camelCase")]
377pub struct TaskArtifactUpdateEvent {
378    pub id: String,
379    pub artifact: Artifact,
380    #[serde(default)]
381    pub metadata: HashMap<String, serde_json::Value>,
382}
383
384/// A single frame on a streaming response (SSE `data:` payload).
385#[derive(Debug, Clone, Serialize, Deserialize)]
386#[serde(untagged)]
387pub enum StreamEvent {
388    StatusUpdate(TaskStatusUpdateEvent),
389    ArtifactUpdate(TaskArtifactUpdateEvent),
390}
391
392/// The response union for `message/send` — either a full Task or a Message
393#[derive(Debug, Clone, Serialize, Deserialize)]
394#[serde(untagged)]
395pub enum SendMessageResponse {
396    Task(Task),
397    Message(Message),
398}
399
400// ─── Security Types (OAS 3.1 / proto parity) ────────────────────────────
401
402/// Security scheme definition (union), matches proto `SecurityScheme`
403#[derive(Debug, Clone, Serialize, Deserialize)]
404#[serde(tag = "type", rename_all = "camelCase")]
405pub enum SecurityScheme {
406    #[serde(rename = "apiKey")]
407    ApiKey {
408        #[serde(skip_serializing_if = "Option::is_none")]
409        description: Option<String>,
410        name: String,
411        #[serde(rename = "in")]
412        location: String, // "query" | "header" | "cookie"
413    },
414    #[serde(rename = "http")]
415    Http {
416        #[serde(skip_serializing_if = "Option::is_none")]
417        description: Option<String>,
418        scheme: String,
419        #[serde(skip_serializing_if = "Option::is_none")]
420        bearer_format: Option<String>,
421    },
422    #[serde(rename = "oauth2")]
423    OAuth2 {
424        #[serde(skip_serializing_if = "Option::is_none")]
425        description: Option<String>,
426        flows: OAuthFlows,
427    },
428    #[serde(rename = "openIdConnect")]
429    OpenIdConnect {
430        #[serde(skip_serializing_if = "Option::is_none")]
431        description: Option<String>,
432        open_id_connect_url: String,
433    },
434    #[serde(rename = "mutualTLS")]
435    MutualTls {
436        #[serde(skip_serializing_if = "Option::is_none")]
437        description: Option<String>,
438    },
439}
440
441/// OAuth 2.0 flows (matches proto `OAuthFlows`)
442#[derive(Debug, Clone, Default, Serialize, Deserialize)]
443#[serde(rename_all = "camelCase")]
444pub struct OAuthFlows {
445    #[serde(skip_serializing_if = "Option::is_none")]
446    pub implicit: Option<OAuthFlowImplicit>,
447    #[serde(skip_serializing_if = "Option::is_none")]
448    pub authorization_code: Option<OAuthFlowAuthorizationCode>,
449    #[serde(skip_serializing_if = "Option::is_none")]
450    pub client_credentials: Option<OAuthFlowClientCredentials>,
451    #[serde(skip_serializing_if = "Option::is_none")]
452    pub device_code: Option<OAuthFlowDeviceCode>,
453}
454
455#[derive(Debug, Clone, Serialize, Deserialize)]
456#[serde(rename_all = "camelCase")]
457pub struct OAuthFlowImplicit {
458    pub authorization_url: String,
459    #[serde(skip_serializing_if = "Option::is_none")]
460    pub refresh_url: Option<String>,
461    #[serde(default)]
462    pub scopes: HashMap<String, String>,
463}
464
465#[derive(Debug, Clone, Serialize, Deserialize)]
466#[serde(rename_all = "camelCase")]
467pub struct OAuthFlowAuthorizationCode {
468    pub authorization_url: String,
469    pub token_url: String,
470    #[serde(skip_serializing_if = "Option::is_none")]
471    pub refresh_url: Option<String>,
472    #[serde(default)]
473    pub scopes: HashMap<String, String>,
474}
475
476#[derive(Debug, Clone, Serialize, Deserialize)]
477#[serde(rename_all = "camelCase")]
478pub struct OAuthFlowClientCredentials {
479    pub token_url: String,
480    #[serde(skip_serializing_if = "Option::is_none")]
481    pub refresh_url: Option<String>,
482    #[serde(default)]
483    pub scopes: HashMap<String, String>,
484}
485
486#[derive(Debug, Clone, Serialize, Deserialize)]
487#[serde(rename_all = "camelCase")]
488pub struct OAuthFlowDeviceCode {
489    pub device_authorization_url: String,
490    pub token_url: String,
491    #[serde(skip_serializing_if = "Option::is_none")]
492    pub refresh_url: Option<String>,
493    #[serde(default)]
494    pub scopes: HashMap<String, String>,
495}
496
497/// A security requirement entry: name → list of required scopes
498pub type SecurityRequirement = HashMap<String, Vec<String>>;
499
500// ─── Agent Card Extension Types ──────────────────────────────────────────
501
502/// An agent-level extension declaration
503#[derive(Debug, Clone, Serialize, Deserialize)]
504#[serde(rename_all = "camelCase")]
505pub struct AgentExtension {
506    /// URI identifying the extension
507    pub uri: String,
508    #[serde(skip_serializing_if = "Option::is_none")]
509    pub description: Option<String>,
510    /// Whether this extension is required to interact with the agent
511    #[serde(default)]
512    pub required: bool,
513    /// Extension-specific configuration parameters
514    #[serde(default, skip_serializing_if = "Option::is_none")]
515    pub params: Option<serde_json::Value>,
516}
517
518/// JWS signature attached to an agent card
519#[derive(Debug, Clone, Serialize, Deserialize)]
520#[serde(rename_all = "camelCase")]
521pub struct AgentCardSignature {
522    /// The JWS compact serialization payload
523    pub signature: String,
524    /// The algorithm used (e.g. "ES256", "RS256")
525    #[serde(skip_serializing_if = "Option::is_none")]
526    pub algorithm: Option<String>,
527    /// Key ID referencing the signing key
528    #[serde(skip_serializing_if = "Option::is_none")]
529    pub key_id: Option<String>,
530}
531
532/// An additional transport interface for an agent
533#[derive(Debug, Clone, Serialize, Deserialize)]
534#[serde(rename_all = "camelCase")]
535pub struct AgentInterface {
536    /// Transport name (e.g. "GRPC", "JSONRPC", "WEBSOCKET")
537    pub transport: String,
538    /// Endpoint URL for this interface
539    pub url: String,
540    /// Content-types accepted at this interface
541    #[serde(default, skip_serializing_if = "Vec::is_empty")]
542    pub content_types: Vec<String>,
543}
544
545// ─── Push Notification CRUD Types ────────────────────────────────────────
546
547/// Request to set push notification config for a task
548#[derive(Debug, Clone, Serialize, Deserialize)]
549#[serde(rename_all = "camelCase")]
550pub struct TaskPushNotificationConfig {
551    pub id: String, // task id
552    pub push_notification_config: PushNotificationConfig,
553}
554
555/// Authentication info for connecting to an agent (used by clients)
556#[derive(Debug, Clone, Serialize, Deserialize)]
557#[serde(rename_all = "camelCase")]
558pub struct AuthenticationInfo {
559    #[serde(default, skip_serializing_if = "Vec::is_empty")]
560    pub schemes: Vec<String>,
561    #[serde(skip_serializing_if = "Option::is_none")]
562    pub credentials: Option<String>,
563}