Skip to main content

a2a_protocol_types/
artifact.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 Tom F.
3
4//! Artifact types for the A2A protocol.
5//!
6//! An [`Artifact`] represents a discrete output produced by an agent — for
7//! example a generated file, a code snippet, or a structured result. Artifacts
8//! are carried in [`crate::task::Task::artifacts`] and in
9//! [`crate::events::TaskArtifactUpdateEvent`].
10
11use serde::{Deserialize, Serialize};
12
13use crate::message::Part;
14
15// ── ArtifactId ────────────────────────────────────────────────────────────────
16
17/// Opaque unique identifier for an [`Artifact`].
18///
19/// Wraps a `String` for compile-time type safety.
20#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
21pub struct ArtifactId(pub String);
22
23impl ArtifactId {
24    /// Creates a new [`ArtifactId`] from any string-like value.
25    #[must_use]
26    pub fn new(s: impl Into<String>) -> Self {
27        Self(s.into())
28    }
29}
30
31impl std::fmt::Display for ArtifactId {
32    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
33        f.write_str(&self.0)
34    }
35}
36
37impl From<String> for ArtifactId {
38    fn from(s: String) -> Self {
39        Self(s)
40    }
41}
42
43impl From<&str> for ArtifactId {
44    fn from(s: &str) -> Self {
45        Self(s.to_owned())
46    }
47}
48
49impl AsRef<str> for ArtifactId {
50    fn as_ref(&self) -> &str {
51        &self.0
52    }
53}
54
55// ── Artifact ──────────────────────────────────────────────────────────────────
56
57/// An output artifact produced by an agent.
58///
59/// Each artifact has a unique [`ArtifactId`] and carries its content as a
60/// non-empty list of [`Part`] values.
61#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
62#[serde(rename_all = "camelCase")]
63pub struct Artifact {
64    /// Unique artifact identifier.
65    #[serde(rename = "artifactId")]
66    pub id: ArtifactId,
67
68    /// Optional human-readable name.
69    #[serde(skip_serializing_if = "Option::is_none")]
70    pub name: Option<String>,
71
72    /// Optional human-readable description.
73    #[serde(skip_serializing_if = "Option::is_none")]
74    pub description: Option<String>,
75
76    /// Content parts.
77    ///
78    /// **Spec requirement:** Must contain at least one element. The A2A
79    /// protocol does not define behavior for empty parts lists.
80    pub parts: Vec<Part>,
81
82    /// URIs of extensions used in this artifact.
83    #[serde(skip_serializing_if = "Option::is_none")]
84    pub extensions: Option<Vec<String>>,
85
86    /// Arbitrary metadata.
87    #[serde(skip_serializing_if = "Option::is_none")]
88    pub metadata: Option<serde_json::Value>,
89}
90
91impl Artifact {
92    /// Creates a minimal [`Artifact`] with an ID and a single part.
93    #[must_use]
94    pub fn new(id: impl Into<ArtifactId>, parts: Vec<Part>) -> Self {
95        Self {
96            id: id.into(),
97            name: None,
98            description: None,
99            parts,
100            extensions: None,
101            metadata: None,
102        }
103    }
104}
105
106// ── Tests ─────────────────────────────────────────────────────────────────────
107
108#[cfg(test)]
109mod tests {
110    use super::*;
111    use crate::message::Part;
112
113    #[test]
114    fn artifact_roundtrip() {
115        let artifact = Artifact::new("art-1", vec![Part::text("result content")]);
116        let json = serde_json::to_string(&artifact).expect("serialize");
117        assert!(json.contains("\"artifactId\":\"art-1\""));
118        assert!(json.contains("\"text\":\"result content\""));
119
120        let back: Artifact = serde_json::from_str(&json).expect("deserialize");
121        assert_eq!(back.id, ArtifactId::new("art-1"));
122        assert_eq!(back.parts.len(), 1);
123    }
124
125    #[test]
126    fn optional_fields_omitted() {
127        let artifact = Artifact::new("art-2", vec![Part::text("x")]);
128        let json = serde_json::to_string(&artifact).expect("serialize");
129        assert!(!json.contains("\"name\""), "name should be omitted");
130        assert!(
131            !json.contains("\"description\""),
132            "description should be omitted"
133        );
134        assert!(!json.contains("\"metadata\""), "metadata should be omitted");
135    }
136
137    #[test]
138    fn artifact_id_display() {
139        let id = ArtifactId::new("my-artifact");
140        assert_eq!(id.to_string(), "my-artifact");
141    }
142}