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