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