Skip to main content

zeph_a2a/
types.rs

1// SPDX-FileCopyrightText: 2026 Andrei G <bug-ops>
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4//! Wire-format types for the A2A protocol.
5//!
6//! All types in this module are serialized using `camelCase` JSON field names to comply with
7//! the A2A specification. They are re-exported from the crate root via `pub use types::*`.
8
9use serde::{Deserialize, Serialize};
10
11/// Lifecycle state of an A2A task.
12///
13/// The state machine progresses roughly as:
14/// `Submitted` → `Working` → `Completed` (success) or `Failed` (error).
15/// `InputRequired` pauses processing until the caller sends more data.
16/// Terminal states (`Completed`, `Failed`, `Canceled`, `Rejected`) cannot be resumed.
17#[non_exhaustive]
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
19pub enum TaskState {
20    /// Task has been received and queued but processing has not started.
21    #[serde(rename = "submitted")]
22    Submitted,
23    /// The agent is actively processing the task.
24    #[serde(rename = "working")]
25    Working,
26    /// Processing is paused; the agent needs more input from the caller.
27    #[serde(rename = "input-required")]
28    InputRequired,
29    /// Task finished successfully. Terminal state.
30    #[serde(rename = "completed")]
31    Completed,
32    /// Task encountered an unrecoverable error. Terminal state.
33    #[serde(rename = "failed")]
34    Failed,
35    /// Task was canceled by the caller. Terminal state.
36    #[serde(rename = "canceled")]
37    Canceled,
38    /// Task was rejected by the agent (e.g., policy violation). Terminal state.
39    #[serde(rename = "rejected")]
40    Rejected,
41    /// The agent requires authentication before proceeding.
42    #[serde(rename = "auth-required")]
43    AuthRequired,
44    /// State could not be determined (e.g., deserialization of a future protocol version).
45    #[serde(rename = "unknown")]
46    Unknown,
47}
48
49/// A unit of work dispatched to or created by an A2A agent.
50///
51/// Tasks are the central concept in the A2A protocol. A caller creates a task by sending
52/// a [`Message`] via `message/send`. The agent processes it and returns the completed
53/// [`Task`] with [`artifacts`](Task::artifacts) and final [`status`](Task::status).
54#[derive(Debug, Clone, Serialize, Deserialize)]
55#[serde(rename_all = "camelCase")]
56pub struct Task {
57    /// Unique task identifier, assigned by the server on creation.
58    pub id: String,
59    /// Optional session/conversation context shared across multiple tasks.
60    #[serde(default, skip_serializing_if = "Option::is_none")]
61    pub context_id: Option<String>,
62    /// Current lifecycle state plus timestamp.
63    pub status: TaskStatus,
64    /// Output artifacts produced by the agent for this task.
65    #[serde(default, skip_serializing_if = "Vec::is_empty")]
66    pub artifacts: Vec<Artifact>,
67    /// Conversation history for this task (may be limited by `historyLength` on retrieval).
68    #[serde(default, skip_serializing_if = "Vec::is_empty")]
69    pub history: Vec<Message>,
70    /// Arbitrary key-value metadata for extension without schema changes.
71    #[serde(default, skip_serializing_if = "Option::is_none")]
72    pub metadata: Option<serde_json::Value>,
73}
74
75/// Current lifecycle state of a task, including when the state was last updated.
76#[derive(Debug, Clone, Serialize, Deserialize)]
77#[serde(rename_all = "camelCase")]
78pub struct TaskStatus {
79    /// The task's current lifecycle state.
80    pub state: TaskState,
81    /// RFC 3339 timestamp of the last state transition.
82    pub timestamp: String,
83    /// Optional agent message accompanying the state transition (e.g., an error description).
84    #[serde(default, skip_serializing_if = "Option::is_none")]
85    pub message: Option<Message>,
86}
87
88/// Participant role in a conversation message.
89#[non_exhaustive]
90#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
91#[serde(rename_all = "lowercase")]
92pub enum Role {
93    /// Message originated from the human user or calling system.
94    User,
95    /// Message originated from the AI agent.
96    Agent,
97}
98
99/// A single message in the A2A conversation, consisting of one or more [`Part`]s.
100///
101/// Messages carry content between the caller and the agent. Use [`Message::user_text`]
102/// to construct a simple single-part text message from the user side.
103///
104/// # Examples
105///
106/// ```rust
107/// use zeph_a2a::{Message, Part, Role};
108///
109/// let msg = Message::user_text("Summarize this document.");
110/// assert_eq!(msg.role, Role::User);
111/// assert_eq!(msg.text_content(), Some("Summarize this document."));
112/// ```
113#[derive(Debug, Clone, Serialize, Deserialize)]
114#[serde(rename_all = "camelCase")]
115pub struct Message {
116    /// Who sent this message.
117    pub role: Role,
118    /// Content parts; at least one is expected for meaningful messages.
119    pub parts: Vec<Part>,
120    /// Optional stable identifier for this specific message.
121    #[serde(default, skip_serializing_if = "Option::is_none")]
122    pub message_id: Option<String>,
123    /// Task this message belongs to (set by the server on responses).
124    #[serde(default, skip_serializing_if = "Option::is_none")]
125    pub task_id: Option<String>,
126    /// Conversation context shared with other tasks in the same session.
127    #[serde(default, skip_serializing_if = "Option::is_none")]
128    pub context_id: Option<String>,
129    /// Arbitrary extension metadata.
130    #[serde(default, skip_serializing_if = "Option::is_none")]
131    pub metadata: Option<serde_json::Value>,
132}
133
134/// A typed content part within a [`Message`] or [`Artifact`].
135///
136/// The A2A spec uses a tagged union (`"kind"` discriminant) so that clients and agents
137/// can safely ignore part types they do not understand. Use [`Part::text`] to construct
138/// the most common variant without boilerplate.
139///
140/// # Examples
141///
142/// ```rust
143/// use zeph_a2a::{Part};
144///
145/// let text_part = Part::text("Hello!");
146/// assert!(matches!(text_part, Part::Text { .. }));
147/// ```
148#[non_exhaustive]
149#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
150#[serde(tag = "kind", rename_all = "lowercase")]
151pub enum Part {
152    /// Plain or markdown text content.
153    Text {
154        text: String,
155        #[serde(default, skip_serializing_if = "Option::is_none")]
156        metadata: Option<serde_json::Value>,
157    },
158    /// Binary or URI-referenced file attachment.
159    File {
160        file: FileContent,
161        #[serde(default, skip_serializing_if = "Option::is_none")]
162        metadata: Option<serde_json::Value>,
163    },
164    /// Arbitrary structured JSON data (e.g., tool call results, structured output).
165    Data {
166        data: serde_json::Value,
167        #[serde(default, skip_serializing_if = "Option::is_none")]
168        metadata: Option<serde_json::Value>,
169    },
170}
171
172/// File attachment within a [`Part::File`], specified either as inline base64 bytes or a URI.
173///
174/// Exactly one of `file_with_bytes` or `file_with_uri` should be set. If both are present,
175/// the server's behavior is unspecified by the protocol — prefer one field per message.
176#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
177#[serde(rename_all = "camelCase")]
178pub struct FileContent {
179    /// Human-readable filename (e.g., `"report.pdf"`).
180    #[serde(default, skip_serializing_if = "Option::is_none")]
181    pub name: Option<String>,
182    /// MIME type of the file (e.g., `"application/pdf"`).
183    #[serde(default, skip_serializing_if = "Option::is_none")]
184    pub media_type: Option<String>,
185    /// Standard base64-encoded file content for inline transfer.
186    #[serde(default, skip_serializing_if = "Option::is_none")]
187    pub file_with_bytes: Option<String>,
188    /// URL referencing the file for out-of-band retrieval.
189    #[serde(default, skip_serializing_if = "Option::is_none")]
190    pub file_with_uri: Option<String>,
191}
192
193/// A named output produced by an agent during task processing.
194///
195/// Artifacts are the primary mechanism for agents to return results. They can contain
196/// text, files, or structured data, and are accumulated on the [`Task`] as the agent runs.
197#[derive(Debug, Clone, Serialize, Deserialize)]
198#[serde(rename_all = "camelCase")]
199pub struct Artifact {
200    /// Unique artifact identifier within the task.
201    pub artifact_id: String,
202    /// Optional human-readable label for the artifact (e.g., `"generated_report"`).
203    #[serde(default, skip_serializing_if = "Option::is_none")]
204    pub name: Option<String>,
205    /// Content parts composing the artifact.
206    pub parts: Vec<Part>,
207    /// Arbitrary extension metadata.
208    #[serde(default, skip_serializing_if = "Option::is_none")]
209    pub metadata: Option<serde_json::Value>,
210}
211
212/// Capability advertisement document served at `/.well-known/agent.json`.
213///
214/// [`AgentCard`] describes an agent's identity, endpoint, skills, and protocol capabilities.
215/// It is the primary discovery mechanism — callers fetch the card before sending messages.
216///
217/// Prefer constructing cards via [`AgentCardBuilder`](crate::AgentCardBuilder) to get correct
218/// defaults (including the current [`A2A_PROTOCOL_VERSION`](crate::A2A_PROTOCOL_VERSION)).
219///
220/// # Examples
221///
222/// ```rust
223/// use zeph_a2a::AgentCardBuilder;
224///
225/// let card = AgentCardBuilder::new("my-agent", "http://localhost:8080", "0.1.0")
226///     .description("An AI assistant")
227///     .streaming(true)
228///     .build();
229///
230/// assert_eq!(card.name, "my-agent");
231/// assert!(card.capabilities.streaming);
232/// ```
233#[derive(Debug, Clone, Serialize, Deserialize)]
234#[serde(rename_all = "camelCase")]
235pub struct AgentCard {
236    /// Human-readable agent name.
237    pub name: String,
238    /// Short description of the agent's purpose.
239    pub description: String,
240    /// Base URL of the A2A endpoint (without path suffix).
241    pub url: String,
242    /// Agent software version string (semver recommended).
243    pub version: String,
244    /// A2A protocol version the agent implements (see [`A2A_PROTOCOL_VERSION`](crate::A2A_PROTOCOL_VERSION)).
245    pub protocol_version: String,
246    /// Optional organization that built or operates the agent.
247    #[serde(default, skip_serializing_if = "Option::is_none")]
248    pub provider: Option<AgentProvider>,
249    /// Flags indicating which A2A capabilities the agent supports.
250    pub capabilities: AgentCapabilities,
251    /// MIME types or mode identifiers the agent accepts as input (e.g., `"text/plain"`).
252    #[serde(default, skip_serializing_if = "Vec::is_empty")]
253    pub default_input_modes: Vec<String>,
254    /// MIME types or mode identifiers the agent can produce as output.
255    #[serde(default, skip_serializing_if = "Vec::is_empty")]
256    pub default_output_modes: Vec<String>,
257    /// Discrete skills the agent exposes, each with its own examples and mode overrides.
258    #[serde(default, skip_serializing_if = "Vec::is_empty")]
259    pub skills: Vec<AgentSkill>,
260    /// JWS signatures over this card's content, per A2A 1.0.0 §8.4.2.
261    ///
262    /// Empty for unsigned cards (all 0.2.x peers and most 1.0.0 peers today). See
263    /// [`crate::card_signing`] for verification. `#[serde(default)]` makes this field
264    /// backward compatible: legacy cards without a `signatures` key deserialize to `[]`.
265    #[serde(default, skip_serializing_if = "Vec::is_empty")]
266    pub signatures: Vec<AgentCardSignature>,
267}
268
269/// A single JWS signature over an [`AgentCard`], per A2A 1.0.0 §8.4.2.
270///
271/// The signed payload is the RFC 8785 JCS canonicalization of the card's JSON
272/// representation with the `signatures` field itself removed. See
273/// [`crate::card_signing`] for the verification algorithm and its known limitations.
274#[derive(Debug, Clone, Serialize, Deserialize)]
275#[serde(rename_all = "camelCase")]
276pub struct AgentCardSignature {
277    /// Base64url-encoded (unpadded) JWS protected header, e.g. `{"alg":"ES256","kid":"key-1"}`.
278    pub protected: String,
279    /// Base64url-encoded (unpadded) signature bytes.
280    pub signature: String,
281    /// Optional unprotected JWS header (A2A spec §8.4.2 `header` field).
282    #[serde(default, skip_serializing_if = "Option::is_none")]
283    pub header: Option<serde_json::Value>,
284}
285
286/// Organization that built or operates an agent.
287#[derive(Debug, Clone, Serialize, Deserialize)]
288#[serde(rename_all = "camelCase")]
289pub struct AgentProvider {
290    /// Name of the organization (e.g., `"Acme Corp"`).
291    pub organization: String,
292    /// Optional URL for the organization's public homepage.
293    #[serde(default, skip_serializing_if = "Option::is_none")]
294    pub url: Option<String>,
295}
296
297/// Boolean flags advertising which A2A protocol extensions an agent supports.
298///
299/// The three protocol-defined fields (`streaming`, `push_notifications`,
300/// `state_transition_history`) are part of the A2A specification. The modality fields
301/// (`images`, `audio`, `files`) are Zeph forward-compatible extensions — they default to
302/// `false` so that peers that do not understand them can safely ignore the fields via
303/// `#[serde(default)]`. If the A2A spec standardises different names for these capabilities
304/// in a future revision, a follow-up PR can add the canonical names without breaking
305/// existing serialised cards.
306#[derive(Debug, Clone, Default, Serialize, Deserialize)]
307#[serde(rename_all = "camelCase")]
308#[allow(clippy::struct_excessive_bools)] // independent boolean flags; bitflags or enum would obscure semantics without reducing complexity
309pub struct AgentCapabilities {
310    /// Agent supports `message/stream` for real-time SSE output.
311    #[serde(default)]
312    pub streaming: bool,
313    /// Agent supports server-initiated push notifications.
314    #[serde(default)]
315    pub push_notifications: bool,
316    /// Agent includes full state-transition history in task responses.
317    #[serde(default)]
318    pub state_transition_history: bool,
319    /// Agent can receive and send `Part::File` entries with `image/*` media types (#3326).
320    ///
321    /// Defaults to `false`. Set via [`AgentCardBuilder::images`](crate::AgentCardBuilder::images).
322    #[serde(default)]
323    pub images: bool,
324    /// Agent can receive and send `Part::File` entries with `audio/*` media types (#3326).
325    ///
326    /// Defaults to `false`. Set via [`AgentCardBuilder::audio`](crate::AgentCardBuilder::audio).
327    #[serde(default)]
328    pub audio: bool,
329    /// Agent can receive and send non-media file attachments via `Part::File` (#3326).
330    ///
331    /// Defaults to `false`. Set via [`AgentCardBuilder::files`](crate::AgentCardBuilder::files).
332    #[serde(default)]
333    pub files: bool,
334}
335
336/// A discrete skill or capability advertised by an agent in its [`AgentCard`].
337///
338/// Skills allow callers to discover what a specific agent is good at before sending a task,
339/// enabling smarter agent routing and delegation decisions.
340#[derive(Debug, Clone, Serialize, Deserialize)]
341#[serde(rename_all = "camelCase")]
342pub struct AgentSkill {
343    /// Machine-readable skill identifier (e.g., `"code-review"`).
344    pub id: String,
345    /// Human-readable skill name.
346    pub name: String,
347    /// Explanation of what this skill does and when to use it.
348    pub description: String,
349    /// Searchable labels for capability-based routing (e.g., `["rust", "security"]`).
350    #[serde(default, skip_serializing_if = "Vec::is_empty")]
351    pub tags: Vec<String>,
352    /// Example prompts or queries that invoke this skill well.
353    #[serde(default, skip_serializing_if = "Vec::is_empty")]
354    pub examples: Vec<String>,
355    /// Input mode overrides for this skill (falls back to card-level defaults).
356    #[serde(default, skip_serializing_if = "Vec::is_empty")]
357    pub input_modes: Vec<String>,
358    /// Output mode overrides for this skill (falls back to card-level defaults).
359    #[serde(default, skip_serializing_if = "Vec::is_empty")]
360    pub output_modes: Vec<String>,
361}
362
363/// SSE event emitted by the server when a task's [`TaskStatus`] changes.
364///
365/// Delivered over the `POST /a2a/stream` SSE channel. The `is_final` flag signals
366/// that the stream will not emit further events after this one.
367#[derive(Debug, Clone, Serialize, Deserialize)]
368#[serde(rename_all = "camelCase")]
369pub struct TaskStatusUpdateEvent {
370    /// Always `"status-update"` — used by clients to distinguish event types.
371    #[serde(default = "kind_status_update")]
372    pub kind: String,
373    /// The task whose status changed.
374    pub task_id: String,
375    /// Conversation context for the task, if any.
376    #[serde(default, skip_serializing_if = "Option::is_none")]
377    pub context_id: Option<String>,
378    /// New status value including state and timestamp.
379    pub status: TaskStatus,
380    /// If `true`, this is the last event in the stream.
381    #[serde(rename = "final", default)]
382    pub is_final: bool,
383}
384
385fn kind_status_update() -> String {
386    "status-update".into()
387}
388
389/// SSE event emitted by the server when a new [`Artifact`] is produced or updated.
390///
391/// Delivered over the `POST /a2a/stream` SSE channel alongside [`TaskStatusUpdateEvent`]s.
392/// The `is_final` flag on the artifact event indicates that the artifact is complete.
393#[derive(Debug, Clone, Serialize, Deserialize)]
394#[serde(rename_all = "camelCase")]
395pub struct TaskArtifactUpdateEvent {
396    /// Always `"artifact-update"` — used by clients to distinguish event types.
397    #[serde(default = "kind_artifact_update")]
398    pub kind: String,
399    /// The task that produced this artifact.
400    pub task_id: String,
401    /// Conversation context for the task, if any.
402    #[serde(default, skip_serializing_if = "Option::is_none")]
403    pub context_id: Option<String>,
404    /// The artifact content (may be a partial chunk if `is_final` is `false`).
405    pub artifact: Artifact,
406    /// If `true`, the artifact is fully produced and no further chunks will follow.
407    #[serde(rename = "final", default)]
408    pub is_final: bool,
409}
410
411fn kind_artifact_update() -> String {
412    "artifact-update".into()
413}
414
415impl Part {
416    /// Construct a plain-text [`Part`] with no metadata.
417    ///
418    /// # Examples
419    ///
420    /// ```rust
421    /// use zeph_a2a::Part;
422    ///
423    /// let p = Part::text("Hello, world!");
424    /// assert!(matches!(p, Part::Text { ref text, .. } if text == "Hello, world!"));
425    /// ```
426    #[must_use]
427    pub fn text(s: impl Into<String>) -> Self {
428        Self::Text {
429            text: s.into(),
430            metadata: None,
431        }
432    }
433}
434
435impl Message {
436    /// Construct a single-part user text message.
437    ///
438    /// This is the most common way to build an outgoing message when calling a peer agent.
439    ///
440    /// # Examples
441    ///
442    /// ```rust
443    /// use zeph_a2a::{Message, Role};
444    ///
445    /// let msg = Message::user_text("Please summarize this.");
446    /// assert_eq!(msg.role, Role::User);
447    /// assert_eq!(msg.text_content(), Some("Please summarize this."));
448    /// ```
449    #[must_use]
450    pub fn user_text(s: impl Into<String>) -> Self {
451        Self {
452            role: Role::User,
453            parts: vec![Part::text(s)],
454            message_id: None,
455            task_id: None,
456            context_id: None,
457            metadata: None,
458        }
459    }
460
461    /// Return the text of the first [`Part::Text`] in this message, if any.
462    ///
463    /// For messages that may contain multiple text parts, prefer [`all_text_content`](Self::all_text_content).
464    ///
465    /// # Examples
466    ///
467    /// ```rust
468    /// use zeph_a2a::Message;
469    ///
470    /// let msg = Message::user_text("hello");
471    /// assert_eq!(msg.text_content(), Some("hello"));
472    /// ```
473    #[must_use]
474    pub fn text_content(&self) -> Option<&str> {
475        self.parts.iter().find_map(|p| match p {
476            Part::Text { text, .. } => Some(text.as_str()),
477            _ => None,
478        })
479    }
480
481    /// Collect and concatenate all `Part::Text` entries in order.
482    ///
483    /// Unlike `text_content` which returns only the first text part, this method
484    /// preserves the full message when an agent sends multiple text parts.
485    /// Returns an empty string if the message contains no text parts.
486    #[must_use]
487    pub fn all_text_content(&self) -> String {
488        let parts: Vec<&str> = self
489            .parts
490            .iter()
491            .filter_map(|p| match p {
492                Part::Text { text, .. } => Some(text.as_str()),
493                _ => None,
494            })
495            .collect();
496        parts.join("\n\n")
497    }
498}
499
500#[cfg(test)]
501mod tests {
502    use super::*;
503
504    #[test]
505    fn task_state_serde() {
506        let states = [
507            (TaskState::Submitted, "\"submitted\""),
508            (TaskState::Working, "\"working\""),
509            (TaskState::InputRequired, "\"input-required\""),
510            (TaskState::Completed, "\"completed\""),
511            (TaskState::Failed, "\"failed\""),
512            (TaskState::Canceled, "\"canceled\""),
513            (TaskState::Rejected, "\"rejected\""),
514            (TaskState::AuthRequired, "\"auth-required\""),
515            (TaskState::Unknown, "\"unknown\""),
516        ];
517        for (state, expected) in states {
518            let json = serde_json::to_string(&state).unwrap();
519            assert_eq!(json, expected, "serialization mismatch for {state:?}");
520            let back: TaskState = serde_json::from_str(&json).unwrap();
521            assert_eq!(back, state);
522        }
523    }
524
525    #[test]
526    fn role_serde_lowercase() {
527        assert_eq!(serde_json::to_string(&Role::User).unwrap(), "\"user\"");
528        assert_eq!(serde_json::to_string(&Role::Agent).unwrap(), "\"agent\"");
529    }
530
531    #[test]
532    fn part_text_constructor() {
533        let part = Part::text("hello");
534        assert_eq!(
535            part,
536            Part::Text {
537                text: "hello".into(),
538                metadata: None
539            }
540        );
541    }
542
543    #[test]
544    fn part_kind_serde() {
545        let text_part = Part::text("hello");
546        let json = serde_json::to_string(&text_part).unwrap();
547        assert!(json.contains("\"kind\":\"text\""));
548        assert!(json.contains("\"text\":\"hello\""));
549        let back: Part = serde_json::from_str(&json).unwrap();
550        assert_eq!(back, text_part);
551
552        let file_part = Part::File {
553            file: FileContent {
554                name: Some("doc.pdf".into()),
555                media_type: None,
556                file_with_bytes: None,
557                file_with_uri: Some("https://example.com/doc.pdf".into()),
558            },
559            metadata: None,
560        };
561        let json = serde_json::to_string(&file_part).unwrap();
562        assert!(json.contains("\"kind\":\"file\""));
563        let back: Part = serde_json::from_str(&json).unwrap();
564        assert_eq!(back, file_part);
565
566        let data_part = Part::Data {
567            data: serde_json::json!({"key": "value"}),
568            metadata: None,
569        };
570        let json = serde_json::to_string(&data_part).unwrap();
571        assert!(json.contains("\"kind\":\"data\""));
572        let back: Part = serde_json::from_str(&json).unwrap();
573        assert_eq!(back, data_part);
574    }
575
576    #[test]
577    fn message_user_text_constructor() {
578        let msg = Message::user_text("test input");
579        assert_eq!(msg.role, Role::User);
580        assert_eq!(msg.text_content(), Some("test input"));
581    }
582
583    #[test]
584    fn message_serde_round_trip() {
585        let msg = Message::user_text("hello agent");
586        let json = serde_json::to_string(&msg).unwrap();
587        let back: Message = serde_json::from_str(&json).unwrap();
588        assert_eq!(back.role, Role::User);
589        assert_eq!(back.text_content(), Some("hello agent"));
590    }
591
592    #[test]
593    fn task_serde_round_trip() {
594        let task = Task {
595            id: "task-1".into(),
596            context_id: None,
597            status: TaskStatus {
598                state: TaskState::Working,
599                timestamp: "2025-01-01T00:00:00Z".into(),
600                message: None,
601            },
602            artifacts: vec![],
603            history: vec![Message::user_text("do something")],
604            metadata: None,
605        };
606        let json = serde_json::to_string(&task).unwrap();
607        assert!(json.contains("\"contextId\"").not());
608        let back: Task = serde_json::from_str(&json).unwrap();
609        assert_eq!(back.id, "task-1");
610        assert_eq!(back.status.state, TaskState::Working);
611        assert_eq!(back.history.len(), 1);
612    }
613
614    #[test]
615    fn task_skips_empty_vecs_and_none() {
616        let task = Task {
617            id: "t".into(),
618            context_id: None,
619            status: TaskStatus {
620                state: TaskState::Submitted,
621                timestamp: "ts".into(),
622                message: None,
623            },
624            artifacts: vec![],
625            history: vec![],
626            metadata: None,
627        };
628        let json = serde_json::to_string(&task).unwrap();
629        assert!(!json.contains("artifacts"));
630        assert!(!json.contains("history"));
631        assert!(!json.contains("metadata"));
632        assert!(!json.contains("contextId"));
633    }
634
635    #[test]
636    fn artifact_serde_round_trip() {
637        let artifact = Artifact {
638            artifact_id: "art-1".into(),
639            name: Some("result.txt".into()),
640            parts: vec![Part::text("file content")],
641            metadata: None,
642        };
643        let json = serde_json::to_string(&artifact).unwrap();
644        assert!(json.contains("\"artifactId\""));
645        let back: Artifact = serde_json::from_str(&json).unwrap();
646        assert_eq!(back.artifact_id, "art-1");
647    }
648
649    #[test]
650    fn agent_card_serde_round_trip() {
651        let card = AgentCard {
652            name: "test-agent".into(),
653            description: "A test agent".into(),
654            url: "http://localhost:8080".into(),
655            version: "0.1.0".into(),
656            protocol_version: "0.2.1".into(),
657            provider: Some(AgentProvider {
658                organization: "TestOrg".into(),
659                url: Some("https://test.org".into()),
660            }),
661            capabilities: AgentCapabilities {
662                streaming: true,
663                push_notifications: false,
664                state_transition_history: false,
665                images: false,
666                audio: false,
667                files: false,
668            },
669            default_input_modes: vec!["text".into()],
670            default_output_modes: vec!["text".into()],
671            skills: vec![AgentSkill {
672                id: "skill-1".into(),
673                name: "Test Skill".into(),
674                description: "Does testing".into(),
675                tags: vec!["test".into()],
676                examples: vec![],
677                input_modes: vec![],
678                output_modes: vec![],
679            }],
680            signatures: vec![],
681        };
682        let json = serde_json::to_string_pretty(&card).unwrap();
683        let back: AgentCard = serde_json::from_str(&json).unwrap();
684        assert_eq!(back.name, "test-agent");
685        assert!(back.capabilities.streaming);
686        assert_eq!(back.skills.len(), 1);
687    }
688
689    #[test]
690    fn agent_card_signatures_default_empty_and_skipped() {
691        let card = minimal_card();
692        let json = serde_json::to_string(&card).unwrap();
693        assert!(!json.contains("signatures"));
694        let back: AgentCard = serde_json::from_str(&json).unwrap();
695        assert!(back.signatures.is_empty());
696    }
697
698    #[test]
699    fn agent_card_deserializes_legacy_card_without_signatures_key() {
700        // A pre-#5928 card JSON with no `signatures` key at all must still deserialize.
701        let json = r#"{"name":"old","description":"","url":"http://x","version":"1","protocolVersion":"0.2.1","capabilities":{"streaming":false}}"#;
702        let card: AgentCard = serde_json::from_str(json).unwrap();
703        assert!(card.signatures.is_empty());
704    }
705
706    #[test]
707    fn agent_card_signature_round_trips() {
708        let sig = AgentCardSignature {
709            protected: "eyJhbGciOiJFUzI1NiJ9".into(),
710            signature: "c2lnbmF0dXJlLWJ5dGVz".into(),
711            header: Some(serde_json::json!({"kid": "key-1"})),
712        };
713        let json = serde_json::to_string(&sig).unwrap();
714        let back: AgentCardSignature = serde_json::from_str(&json).unwrap();
715        assert_eq!(back.protected, sig.protected);
716        assert_eq!(back.signature, sig.signature);
717        assert_eq!(back.header, sig.header);
718    }
719
720    fn minimal_card() -> AgentCard {
721        AgentCard {
722            name: "test-agent".into(),
723            description: "A test agent".into(),
724            url: "http://localhost:8080".into(),
725            version: "0.1.0".into(),
726            protocol_version: "0.2.1".into(),
727            provider: None,
728            capabilities: AgentCapabilities::default(),
729            default_input_modes: vec![],
730            default_output_modes: vec![],
731            skills: vec![],
732            signatures: vec![],
733        }
734    }
735
736    #[test]
737    fn task_status_update_event_serde() {
738        let event = TaskStatusUpdateEvent {
739            kind: "status-update".into(),
740            task_id: "t-1".into(),
741            context_id: None,
742            status: TaskStatus {
743                state: TaskState::Completed,
744                timestamp: "ts".into(),
745                message: None,
746            },
747            is_final: true,
748        };
749        let json = serde_json::to_string(&event).unwrap();
750        assert!(json.contains("\"final\":true"));
751        assert!(!json.contains("isFinal"));
752        assert!(json.contains("\"kind\":\"status-update\""));
753        let back: TaskStatusUpdateEvent = serde_json::from_str(&json).unwrap();
754        assert!(back.is_final);
755        assert_eq!(back.kind, "status-update");
756    }
757
758    #[test]
759    fn task_artifact_update_event_serde() {
760        let event = TaskArtifactUpdateEvent {
761            kind: "artifact-update".into(),
762            task_id: "t-1".into(),
763            context_id: None,
764            artifact: Artifact {
765                artifact_id: "a-1".into(),
766                name: None,
767                parts: vec![Part::text("data")],
768                metadata: None,
769            },
770            is_final: false,
771        };
772        let json = serde_json::to_string(&event).unwrap();
773        assert!(json.contains("\"final\":false"));
774        assert!(json.contains("\"kind\":\"artifact-update\""));
775        let back: TaskArtifactUpdateEvent = serde_json::from_str(&json).unwrap();
776        assert!(!back.is_final);
777        assert_eq!(back.kind, "artifact-update");
778    }
779
780    #[test]
781    fn file_content_serde() {
782        let fc = FileContent {
783            name: Some("doc.pdf".into()),
784            media_type: Some("application/pdf".into()),
785            file_with_bytes: Some("base64data==".into()),
786            file_with_uri: None,
787        };
788        let json = serde_json::to_string(&fc).unwrap();
789        assert!(json.contains("\"mediaType\""));
790        assert!(json.contains("\"fileWithBytes\""));
791        assert!(!json.contains("fileWithUri"));
792        let back: FileContent = serde_json::from_str(&json).unwrap();
793        assert_eq!(back.name.as_deref(), Some("doc.pdf"));
794    }
795
796    #[test]
797    fn all_text_content_single_part() {
798        let msg = Message::user_text("hello world");
799        assert_eq!(msg.all_text_content(), "hello world");
800    }
801
802    #[test]
803    fn all_text_content_multiple_parts_joined() {
804        let msg = Message {
805            role: Role::User,
806            parts: vec![
807                Part::text("first"),
808                Part::text("second"),
809                Part::text("third"),
810            ],
811            message_id: None,
812            task_id: None,
813            context_id: None,
814            metadata: None,
815        };
816        assert_eq!(msg.all_text_content(), "first\n\nsecond\n\nthird");
817    }
818
819    #[test]
820    fn all_text_content_no_text_parts_returns_empty() {
821        let msg = Message {
822            role: Role::User,
823            parts: vec![],
824            message_id: None,
825            task_id: None,
826            context_id: None,
827            metadata: None,
828        };
829        assert_eq!(msg.all_text_content(), "");
830    }
831
832    #[test]
833    fn all_text_content_skips_non_text_parts() {
834        let msg = Message {
835            role: Role::User,
836            parts: vec![
837                Part::text("text-only"),
838                Part::Data {
839                    data: serde_json::json!({"key": "val"}),
840                    metadata: None,
841                },
842            ],
843            message_id: None,
844            task_id: None,
845            context_id: None,
846            metadata: None,
847        };
848        assert_eq!(msg.all_text_content(), "text-only");
849    }
850
851    #[test]
852    fn agent_capabilities_default_has_no_modalities() {
853        let caps = AgentCapabilities::default();
854        assert!(!caps.images);
855        assert!(!caps.audio);
856        assert!(!caps.files);
857    }
858
859    #[test]
860    fn agent_capabilities_modality_fields_serialize() {
861        let caps = AgentCapabilities {
862            streaming: false,
863            push_notifications: false,
864            state_transition_history: false,
865            images: false,
866            audio: false,
867            files: false,
868        };
869        let json = serde_json::to_string(&caps).unwrap();
870        assert!(json.contains("\"images\":false"));
871        assert!(json.contains("\"audio\":false"));
872        assert!(json.contains("\"files\":false"));
873    }
874
875    #[test]
876    fn deserialize_legacy_capabilities_uses_modality_defaults() {
877        // Old-format JSON with only the core A2A fields — modality fields must default to false.
878        let json = r#"{"streaming": true}"#;
879        let caps: AgentCapabilities = serde_json::from_str(json).unwrap();
880        assert!(caps.streaming);
881        assert!(!caps.images);
882        assert!(!caps.audio);
883        assert!(!caps.files);
884    }
885
886    trait Not {
887        fn not(&self) -> bool;
888    }
889    impl Not for bool {
890        fn not(&self) -> bool {
891            !*self
892        }
893    }
894}