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}
261
262/// Organization that built or operates an agent.
263#[derive(Debug, Clone, Serialize, Deserialize)]
264#[serde(rename_all = "camelCase")]
265pub struct AgentProvider {
266    /// Name of the organization (e.g., `"Acme Corp"`).
267    pub organization: String,
268    /// Optional URL for the organization's public homepage.
269    #[serde(default, skip_serializing_if = "Option::is_none")]
270    pub url: Option<String>,
271}
272
273/// Boolean flags advertising which A2A protocol extensions an agent supports.
274///
275/// The three protocol-defined fields (`streaming`, `push_notifications`,
276/// `state_transition_history`) are part of the A2A specification. The modality fields
277/// (`images`, `audio`, `files`) are Zeph forward-compatible extensions — they default to
278/// `false` so that peers that do not understand them can safely ignore the fields via
279/// `#[serde(default)]`. If the A2A spec standardises different names for these capabilities
280/// in a future revision, a follow-up PR can add the canonical names without breaking
281/// existing serialised cards.
282#[derive(Debug, Clone, Default, Serialize, Deserialize)]
283#[serde(rename_all = "camelCase")]
284#[allow(clippy::struct_excessive_bools)] // independent boolean flags; bitflags or enum would obscure semantics without reducing complexity
285pub struct AgentCapabilities {
286    /// Agent supports `message/stream` for real-time SSE output.
287    #[serde(default)]
288    pub streaming: bool,
289    /// Agent supports server-initiated push notifications.
290    #[serde(default)]
291    pub push_notifications: bool,
292    /// Agent includes full state-transition history in task responses.
293    #[serde(default)]
294    pub state_transition_history: bool,
295    /// Agent can receive and send `Part::File` entries with `image/*` media types (#3326).
296    ///
297    /// Defaults to `false`. Set via [`AgentCardBuilder::images`](crate::AgentCardBuilder::images).
298    #[serde(default)]
299    pub images: bool,
300    /// Agent can receive and send `Part::File` entries with `audio/*` media types (#3326).
301    ///
302    /// Defaults to `false`. Set via [`AgentCardBuilder::audio`](crate::AgentCardBuilder::audio).
303    #[serde(default)]
304    pub audio: bool,
305    /// Agent can receive and send non-media file attachments via `Part::File` (#3326).
306    ///
307    /// Defaults to `false`. Set via [`AgentCardBuilder::files`](crate::AgentCardBuilder::files).
308    #[serde(default)]
309    pub files: bool,
310}
311
312/// A discrete skill or capability advertised by an agent in its [`AgentCard`].
313///
314/// Skills allow callers to discover what a specific agent is good at before sending a task,
315/// enabling smarter agent routing and delegation decisions.
316#[derive(Debug, Clone, Serialize, Deserialize)]
317#[serde(rename_all = "camelCase")]
318pub struct AgentSkill {
319    /// Machine-readable skill identifier (e.g., `"code-review"`).
320    pub id: String,
321    /// Human-readable skill name.
322    pub name: String,
323    /// Explanation of what this skill does and when to use it.
324    pub description: String,
325    /// Searchable labels for capability-based routing (e.g., `["rust", "security"]`).
326    #[serde(default, skip_serializing_if = "Vec::is_empty")]
327    pub tags: Vec<String>,
328    /// Example prompts or queries that invoke this skill well.
329    #[serde(default, skip_serializing_if = "Vec::is_empty")]
330    pub examples: Vec<String>,
331    /// Input mode overrides for this skill (falls back to card-level defaults).
332    #[serde(default, skip_serializing_if = "Vec::is_empty")]
333    pub input_modes: Vec<String>,
334    /// Output mode overrides for this skill (falls back to card-level defaults).
335    #[serde(default, skip_serializing_if = "Vec::is_empty")]
336    pub output_modes: Vec<String>,
337}
338
339/// SSE event emitted by the server when a task's [`TaskStatus`] changes.
340///
341/// Delivered over the `POST /a2a/stream` SSE channel. The `is_final` flag signals
342/// that the stream will not emit further events after this one.
343#[derive(Debug, Clone, Serialize, Deserialize)]
344#[serde(rename_all = "camelCase")]
345pub struct TaskStatusUpdateEvent {
346    /// Always `"status-update"` — used by clients to distinguish event types.
347    #[serde(default = "kind_status_update")]
348    pub kind: String,
349    /// The task whose status changed.
350    pub task_id: String,
351    /// Conversation context for the task, if any.
352    #[serde(default, skip_serializing_if = "Option::is_none")]
353    pub context_id: Option<String>,
354    /// New status value including state and timestamp.
355    pub status: TaskStatus,
356    /// If `true`, this is the last event in the stream.
357    #[serde(rename = "final", default)]
358    pub is_final: bool,
359}
360
361fn kind_status_update() -> String {
362    "status-update".into()
363}
364
365/// SSE event emitted by the server when a new [`Artifact`] is produced or updated.
366///
367/// Delivered over the `POST /a2a/stream` SSE channel alongside [`TaskStatusUpdateEvent`]s.
368/// The `is_final` flag on the artifact event indicates that the artifact is complete.
369#[derive(Debug, Clone, Serialize, Deserialize)]
370#[serde(rename_all = "camelCase")]
371pub struct TaskArtifactUpdateEvent {
372    /// Always `"artifact-update"` — used by clients to distinguish event types.
373    #[serde(default = "kind_artifact_update")]
374    pub kind: String,
375    /// The task that produced this artifact.
376    pub task_id: String,
377    /// Conversation context for the task, if any.
378    #[serde(default, skip_serializing_if = "Option::is_none")]
379    pub context_id: Option<String>,
380    /// The artifact content (may be a partial chunk if `is_final` is `false`).
381    pub artifact: Artifact,
382    /// If `true`, the artifact is fully produced and no further chunks will follow.
383    #[serde(rename = "final", default)]
384    pub is_final: bool,
385}
386
387fn kind_artifact_update() -> String {
388    "artifact-update".into()
389}
390
391impl Part {
392    /// Construct a plain-text [`Part`] with no metadata.
393    ///
394    /// # Examples
395    ///
396    /// ```rust
397    /// use zeph_a2a::Part;
398    ///
399    /// let p = Part::text("Hello, world!");
400    /// assert!(matches!(p, Part::Text { ref text, .. } if text == "Hello, world!"));
401    /// ```
402    #[must_use]
403    pub fn text(s: impl Into<String>) -> Self {
404        Self::Text {
405            text: s.into(),
406            metadata: None,
407        }
408    }
409}
410
411impl Message {
412    /// Construct a single-part user text message.
413    ///
414    /// This is the most common way to build an outgoing message when calling a peer agent.
415    ///
416    /// # Examples
417    ///
418    /// ```rust
419    /// use zeph_a2a::{Message, Role};
420    ///
421    /// let msg = Message::user_text("Please summarize this.");
422    /// assert_eq!(msg.role, Role::User);
423    /// assert_eq!(msg.text_content(), Some("Please summarize this."));
424    /// ```
425    #[must_use]
426    pub fn user_text(s: impl Into<String>) -> Self {
427        Self {
428            role: Role::User,
429            parts: vec![Part::text(s)],
430            message_id: None,
431            task_id: None,
432            context_id: None,
433            metadata: None,
434        }
435    }
436
437    /// Return the text of the first [`Part::Text`] in this message, if any.
438    ///
439    /// For messages that may contain multiple text parts, prefer [`all_text_content`](Self::all_text_content).
440    ///
441    /// # Examples
442    ///
443    /// ```rust
444    /// use zeph_a2a::Message;
445    ///
446    /// let msg = Message::user_text("hello");
447    /// assert_eq!(msg.text_content(), Some("hello"));
448    /// ```
449    #[must_use]
450    pub fn text_content(&self) -> Option<&str> {
451        self.parts.iter().find_map(|p| match p {
452            Part::Text { text, .. } => Some(text.as_str()),
453            _ => None,
454        })
455    }
456
457    /// Collect and concatenate all `Part::Text` entries in order.
458    ///
459    /// Unlike `text_content` which returns only the first text part, this method
460    /// preserves the full message when an agent sends multiple text parts.
461    /// Returns an empty string if the message contains no text parts.
462    #[must_use]
463    pub fn all_text_content(&self) -> String {
464        let parts: Vec<&str> = self
465            .parts
466            .iter()
467            .filter_map(|p| match p {
468                Part::Text { text, .. } => Some(text.as_str()),
469                _ => None,
470            })
471            .collect();
472        parts.join("\n\n")
473    }
474}
475
476#[cfg(test)]
477mod tests {
478    use super::*;
479
480    #[test]
481    fn task_state_serde() {
482        let states = [
483            (TaskState::Submitted, "\"submitted\""),
484            (TaskState::Working, "\"working\""),
485            (TaskState::InputRequired, "\"input-required\""),
486            (TaskState::Completed, "\"completed\""),
487            (TaskState::Failed, "\"failed\""),
488            (TaskState::Canceled, "\"canceled\""),
489            (TaskState::Rejected, "\"rejected\""),
490            (TaskState::AuthRequired, "\"auth-required\""),
491            (TaskState::Unknown, "\"unknown\""),
492        ];
493        for (state, expected) in states {
494            let json = serde_json::to_string(&state).unwrap();
495            assert_eq!(json, expected, "serialization mismatch for {state:?}");
496            let back: TaskState = serde_json::from_str(&json).unwrap();
497            assert_eq!(back, state);
498        }
499    }
500
501    #[test]
502    fn role_serde_lowercase() {
503        assert_eq!(serde_json::to_string(&Role::User).unwrap(), "\"user\"");
504        assert_eq!(serde_json::to_string(&Role::Agent).unwrap(), "\"agent\"");
505    }
506
507    #[test]
508    fn part_text_constructor() {
509        let part = Part::text("hello");
510        assert_eq!(
511            part,
512            Part::Text {
513                text: "hello".into(),
514                metadata: None
515            }
516        );
517    }
518
519    #[test]
520    fn part_kind_serde() {
521        let text_part = Part::text("hello");
522        let json = serde_json::to_string(&text_part).unwrap();
523        assert!(json.contains("\"kind\":\"text\""));
524        assert!(json.contains("\"text\":\"hello\""));
525        let back: Part = serde_json::from_str(&json).unwrap();
526        assert_eq!(back, text_part);
527
528        let file_part = Part::File {
529            file: FileContent {
530                name: Some("doc.pdf".into()),
531                media_type: None,
532                file_with_bytes: None,
533                file_with_uri: Some("https://example.com/doc.pdf".into()),
534            },
535            metadata: None,
536        };
537        let json = serde_json::to_string(&file_part).unwrap();
538        assert!(json.contains("\"kind\":\"file\""));
539        let back: Part = serde_json::from_str(&json).unwrap();
540        assert_eq!(back, file_part);
541
542        let data_part = Part::Data {
543            data: serde_json::json!({"key": "value"}),
544            metadata: None,
545        };
546        let json = serde_json::to_string(&data_part).unwrap();
547        assert!(json.contains("\"kind\":\"data\""));
548        let back: Part = serde_json::from_str(&json).unwrap();
549        assert_eq!(back, data_part);
550    }
551
552    #[test]
553    fn message_user_text_constructor() {
554        let msg = Message::user_text("test input");
555        assert_eq!(msg.role, Role::User);
556        assert_eq!(msg.text_content(), Some("test input"));
557    }
558
559    #[test]
560    fn message_serde_round_trip() {
561        let msg = Message::user_text("hello agent");
562        let json = serde_json::to_string(&msg).unwrap();
563        let back: Message = serde_json::from_str(&json).unwrap();
564        assert_eq!(back.role, Role::User);
565        assert_eq!(back.text_content(), Some("hello agent"));
566    }
567
568    #[test]
569    fn task_serde_round_trip() {
570        let task = Task {
571            id: "task-1".into(),
572            context_id: None,
573            status: TaskStatus {
574                state: TaskState::Working,
575                timestamp: "2025-01-01T00:00:00Z".into(),
576                message: None,
577            },
578            artifacts: vec![],
579            history: vec![Message::user_text("do something")],
580            metadata: None,
581        };
582        let json = serde_json::to_string(&task).unwrap();
583        assert!(json.contains("\"contextId\"").not());
584        let back: Task = serde_json::from_str(&json).unwrap();
585        assert_eq!(back.id, "task-1");
586        assert_eq!(back.status.state, TaskState::Working);
587        assert_eq!(back.history.len(), 1);
588    }
589
590    #[test]
591    fn task_skips_empty_vecs_and_none() {
592        let task = Task {
593            id: "t".into(),
594            context_id: None,
595            status: TaskStatus {
596                state: TaskState::Submitted,
597                timestamp: "ts".into(),
598                message: None,
599            },
600            artifacts: vec![],
601            history: vec![],
602            metadata: None,
603        };
604        let json = serde_json::to_string(&task).unwrap();
605        assert!(!json.contains("artifacts"));
606        assert!(!json.contains("history"));
607        assert!(!json.contains("metadata"));
608        assert!(!json.contains("contextId"));
609    }
610
611    #[test]
612    fn artifact_serde_round_trip() {
613        let artifact = Artifact {
614            artifact_id: "art-1".into(),
615            name: Some("result.txt".into()),
616            parts: vec![Part::text("file content")],
617            metadata: None,
618        };
619        let json = serde_json::to_string(&artifact).unwrap();
620        assert!(json.contains("\"artifactId\""));
621        let back: Artifact = serde_json::from_str(&json).unwrap();
622        assert_eq!(back.artifact_id, "art-1");
623    }
624
625    #[test]
626    fn agent_card_serde_round_trip() {
627        let card = AgentCard {
628            name: "test-agent".into(),
629            description: "A test agent".into(),
630            url: "http://localhost:8080".into(),
631            version: "0.1.0".into(),
632            protocol_version: "0.2.1".into(),
633            provider: Some(AgentProvider {
634                organization: "TestOrg".into(),
635                url: Some("https://test.org".into()),
636            }),
637            capabilities: AgentCapabilities {
638                streaming: true,
639                push_notifications: false,
640                state_transition_history: false,
641                images: false,
642                audio: false,
643                files: false,
644            },
645            default_input_modes: vec!["text".into()],
646            default_output_modes: vec!["text".into()],
647            skills: vec![AgentSkill {
648                id: "skill-1".into(),
649                name: "Test Skill".into(),
650                description: "Does testing".into(),
651                tags: vec!["test".into()],
652                examples: vec![],
653                input_modes: vec![],
654                output_modes: vec![],
655            }],
656        };
657        let json = serde_json::to_string_pretty(&card).unwrap();
658        let back: AgentCard = serde_json::from_str(&json).unwrap();
659        assert_eq!(back.name, "test-agent");
660        assert!(back.capabilities.streaming);
661        assert_eq!(back.skills.len(), 1);
662    }
663
664    #[test]
665    fn task_status_update_event_serde() {
666        let event = TaskStatusUpdateEvent {
667            kind: "status-update".into(),
668            task_id: "t-1".into(),
669            context_id: None,
670            status: TaskStatus {
671                state: TaskState::Completed,
672                timestamp: "ts".into(),
673                message: None,
674            },
675            is_final: true,
676        };
677        let json = serde_json::to_string(&event).unwrap();
678        assert!(json.contains("\"final\":true"));
679        assert!(!json.contains("isFinal"));
680        assert!(json.contains("\"kind\":\"status-update\""));
681        let back: TaskStatusUpdateEvent = serde_json::from_str(&json).unwrap();
682        assert!(back.is_final);
683        assert_eq!(back.kind, "status-update");
684    }
685
686    #[test]
687    fn task_artifact_update_event_serde() {
688        let event = TaskArtifactUpdateEvent {
689            kind: "artifact-update".into(),
690            task_id: "t-1".into(),
691            context_id: None,
692            artifact: Artifact {
693                artifact_id: "a-1".into(),
694                name: None,
695                parts: vec![Part::text("data")],
696                metadata: None,
697            },
698            is_final: false,
699        };
700        let json = serde_json::to_string(&event).unwrap();
701        assert!(json.contains("\"final\":false"));
702        assert!(json.contains("\"kind\":\"artifact-update\""));
703        let back: TaskArtifactUpdateEvent = serde_json::from_str(&json).unwrap();
704        assert!(!back.is_final);
705        assert_eq!(back.kind, "artifact-update");
706    }
707
708    #[test]
709    fn file_content_serde() {
710        let fc = FileContent {
711            name: Some("doc.pdf".into()),
712            media_type: Some("application/pdf".into()),
713            file_with_bytes: Some("base64data==".into()),
714            file_with_uri: None,
715        };
716        let json = serde_json::to_string(&fc).unwrap();
717        assert!(json.contains("\"mediaType\""));
718        assert!(json.contains("\"fileWithBytes\""));
719        assert!(!json.contains("fileWithUri"));
720        let back: FileContent = serde_json::from_str(&json).unwrap();
721        assert_eq!(back.name.as_deref(), Some("doc.pdf"));
722    }
723
724    #[test]
725    fn all_text_content_single_part() {
726        let msg = Message::user_text("hello world");
727        assert_eq!(msg.all_text_content(), "hello world");
728    }
729
730    #[test]
731    fn all_text_content_multiple_parts_joined() {
732        let msg = Message {
733            role: Role::User,
734            parts: vec![
735                Part::text("first"),
736                Part::text("second"),
737                Part::text("third"),
738            ],
739            message_id: None,
740            task_id: None,
741            context_id: None,
742            metadata: None,
743        };
744        assert_eq!(msg.all_text_content(), "first\n\nsecond\n\nthird");
745    }
746
747    #[test]
748    fn all_text_content_no_text_parts_returns_empty() {
749        let msg = Message {
750            role: Role::User,
751            parts: vec![],
752            message_id: None,
753            task_id: None,
754            context_id: None,
755            metadata: None,
756        };
757        assert_eq!(msg.all_text_content(), "");
758    }
759
760    #[test]
761    fn all_text_content_skips_non_text_parts() {
762        let msg = Message {
763            role: Role::User,
764            parts: vec![
765                Part::text("text-only"),
766                Part::Data {
767                    data: serde_json::json!({"key": "val"}),
768                    metadata: None,
769                },
770            ],
771            message_id: None,
772            task_id: None,
773            context_id: None,
774            metadata: None,
775        };
776        assert_eq!(msg.all_text_content(), "text-only");
777    }
778
779    #[test]
780    fn agent_capabilities_default_has_no_modalities() {
781        let caps = AgentCapabilities::default();
782        assert!(!caps.images);
783        assert!(!caps.audio);
784        assert!(!caps.files);
785    }
786
787    #[test]
788    fn agent_capabilities_modality_fields_serialize() {
789        let caps = AgentCapabilities {
790            streaming: false,
791            push_notifications: false,
792            state_transition_history: false,
793            images: false,
794            audio: false,
795            files: false,
796        };
797        let json = serde_json::to_string(&caps).unwrap();
798        assert!(json.contains("\"images\":false"));
799        assert!(json.contains("\"audio\":false"));
800        assert!(json.contains("\"files\":false"));
801    }
802
803    #[test]
804    fn deserialize_legacy_capabilities_uses_modality_defaults() {
805        // Old-format JSON with only the core A2A fields — modality fields must default to false.
806        let json = r#"{"streaming": true}"#;
807        let caps: AgentCapabilities = serde_json::from_str(json).unwrap();
808        assert!(caps.streaming);
809        assert!(!caps.images);
810        assert!(!caps.audio);
811        assert!(!caps.files);
812    }
813
814    trait Not {
815        fn not(&self) -> bool;
816    }
817    impl Not for bool {
818        fn not(&self) -> bool {
819            !*self
820        }
821    }
822}