Skip to main content

systemprompt_models/a2a/
artifact_metadata.rs

1//! Provenance metadata attached to A2A artifacts.
2//!
3//! [`ArtifactMetadata`] records where an artifact came from — the originating
4//! context and task, the MCP tool or skill that produced it, and rendering
5//! hints — and carries the [`Validate`]/[`MetadataValidation`] contract that
6//! rejects artifacts missing their required identity fields.
7
8use chrono::Utc;
9use serde::{Deserialize, Serialize};
10use systemprompt_identifiers::{ContextId, SkillId, TaskId};
11use systemprompt_traits::validation::{
12    MetadataValidation, Validate, ValidationError, ValidationResult,
13};
14
15#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
16pub struct ArtifactMetadata {
17    pub artifact_type: String,
18    pub context_id: ContextId,
19    pub created_at: String,
20    pub task_id: TaskId,
21    #[serde(skip_serializing_if = "Option::is_none")]
22    pub rendering_hints: Option<serde_json::Value>,
23    #[serde(skip_serializing_if = "Option::is_none")]
24    pub source: Option<String>,
25    #[serde(skip_serializing_if = "Option::is_none")]
26    pub mcp_execution_id: Option<String>,
27    #[serde(skip_serializing_if = "Option::is_none")]
28    pub mcp_schema: Option<serde_json::Value>,
29    #[serde(skip_serializing_if = "Option::is_none")]
30    pub is_internal: Option<bool>,
31    #[serde(skip_serializing_if = "Option::is_none")]
32    pub fingerprint: Option<String>,
33    #[serde(skip_serializing_if = "Option::is_none")]
34    pub tool_name: Option<String>,
35    #[serde(skip_serializing_if = "Option::is_none")]
36    pub execution_index: Option<usize>,
37    #[serde(skip_serializing_if = "Option::is_none")]
38    pub skill_id: Option<SkillId>,
39    #[serde(skip_serializing_if = "Option::is_none")]
40    pub skill_name: Option<String>,
41}
42
43impl ArtifactMetadata {
44    pub fn new(artifact_type: String, context_id: ContextId, task_id: TaskId) -> Self {
45        Self {
46            artifact_type,
47            context_id,
48            task_id,
49            created_at: Utc::now().to_rfc3339(),
50            rendering_hints: None,
51            source: Some("mcp_tool".to_owned()),
52            mcp_execution_id: None,
53            mcp_schema: None,
54            is_internal: None,
55            fingerprint: None,
56            tool_name: None,
57            execution_index: None,
58            skill_id: None,
59            skill_name: None,
60        }
61    }
62
63    pub fn with_rendering_hints(mut self, hints: serde_json::Value) -> Self {
64        self.rendering_hints = Some(hints);
65        self
66    }
67
68    pub fn with_source(mut self, source: String) -> Self {
69        self.source = Some(source);
70        self
71    }
72
73    pub fn with_mcp_execution_id(mut self, id: String) -> Self {
74        self.mcp_execution_id = Some(id);
75        self
76    }
77
78    pub fn with_mcp_schema(mut self, schema: serde_json::Value) -> Self {
79        self.mcp_schema = Some(schema);
80        self
81    }
82
83    pub const fn with_is_internal(mut self, is_internal: bool) -> Self {
84        self.is_internal = Some(is_internal);
85        self
86    }
87
88    pub fn with_fingerprint(mut self, fingerprint: String) -> Self {
89        self.fingerprint = Some(fingerprint);
90        self
91    }
92
93    pub fn with_tool_name(mut self, tool_name: String) -> Self {
94        self.tool_name = Some(tool_name);
95        self
96    }
97
98    pub const fn with_execution_index(mut self, index: usize) -> Self {
99        self.execution_index = Some(index);
100        self
101    }
102
103    pub fn with_skill_id(mut self, skill_id: SkillId) -> Self {
104        self.skill_id = Some(skill_id);
105        self
106    }
107
108    pub fn with_skill_name(mut self, skill_name: String) -> Self {
109        self.skill_name = Some(skill_name);
110        self
111    }
112
113    pub fn with_skill(mut self, skill_id: SkillId, skill_name: String) -> Self {
114        self.skill_id = Some(skill_id);
115        self.skill_name = Some(skill_name);
116        self
117    }
118
119    pub fn new_validated(
120        artifact_type: String,
121        context_id: ContextId,
122        task_id: TaskId,
123    ) -> ValidationResult<Self> {
124        if artifact_type.is_empty() {
125            return Err(ValidationError::new(
126                "artifact_type",
127                "Cannot create ArtifactMetadata: artifact_type is empty",
128            )
129            .with_context(format!(
130                "artifact_type={artifact_type:?}, context_id={context_id:?}, task_id={task_id:?}"
131            )));
132        }
133
134        let metadata = Self {
135            artifact_type,
136            context_id,
137            task_id,
138            created_at: Utc::now().to_rfc3339(),
139            rendering_hints: None,
140            source: Some("mcp_tool".to_owned()),
141            mcp_execution_id: None,
142            mcp_schema: None,
143            is_internal: None,
144            fingerprint: None,
145            tool_name: None,
146            execution_index: None,
147            skill_id: None,
148            skill_name: None,
149        };
150
151        metadata.validate()?;
152        Ok(metadata)
153    }
154}
155
156impl Validate for ArtifactMetadata {
157    fn validate(&self) -> ValidationResult<()> {
158        self.validate_required_fields()?;
159        Ok(())
160    }
161}
162
163impl MetadataValidation for ArtifactMetadata {
164    fn required_string_fields(&self) -> Vec<(&'static str, &str)> {
165        vec![
166            ("artifact_type", &self.artifact_type),
167            ("task_id", self.task_id.as_str()),
168            ("created_at", &self.created_at),
169        ]
170    }
171}