Skip to main content

systemprompt_models/artifacts/
metadata.rs

1//! Execution provenance carried on every artifact.
2//!
3//! [`ExecutionMetadata`] captures the full identity of the run that produced an
4//! artifact — context, trace, session, user, agent, and the optional tool/skill
5//! that emitted it — and is derived from a [`RequestContext`] via
6//! [`ExecutionMetadataBuilder`]. [`ToolResponse`] wraps an artifact with this
7//! metadata and its persisted ids; it is the storage envelope for
8//! `mcp_artifacts.data` rows and never appears on the wire, where provenance
9//! travels under the [`EXECUTION_META_KEY`] `_meta` key instead.
10//!
11//! Copyright (c) systemprompt.io — Business Source License 1.1.
12//! See <https://systemprompt.io> for licensing details.
13
14use chrono::{DateTime, Utc};
15use schemars::JsonSchema;
16use serde::{Deserialize, Serialize};
17use serde_json::Value as JsonValue;
18use systemprompt_identifiers::{
19    AgentName, ArtifactId, ContextId, McpExecutionId, McpToolName, SessionId, SkillId, SkillName,
20    TaskId, TraceId, UserId,
21};
22
23use crate::execution::context::RequestContext;
24
25pub const EXECUTION_META_KEY: &str = "io.systemprompt/execution";
26
27#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
28pub struct ExecutionMetadata {
29    #[schemars(with = "String")]
30    pub context_id: ContextId,
31
32    #[schemars(with = "String")]
33    pub trace_id: TraceId,
34
35    #[schemars(with = "String")]
36    pub session_id: SessionId,
37
38    #[schemars(with = "String")]
39    pub user_id: UserId,
40
41    #[schemars(with = "String")]
42    pub agent_name: AgentName,
43
44    #[schemars(with = "String")]
45    pub timestamp: DateTime<Utc>,
46
47    #[serde(skip_serializing_if = "Option::is_none")]
48    #[schemars(with = "Option<String>")]
49    pub task_id: Option<TaskId>,
50
51    #[serde(skip_serializing_if = "Option::is_none")]
52    #[schemars(with = "Option<String>")]
53    pub tool_name: Option<McpToolName>,
54
55    #[serde(skip_serializing_if = "Option::is_none")]
56    #[schemars(with = "Option<String>")]
57    pub skill_id: Option<SkillId>,
58
59    #[serde(skip_serializing_if = "Option::is_none")]
60    #[schemars(with = "Option<String>")]
61    pub skill_name: Option<SkillName>,
62
63    #[serde(skip_serializing_if = "Option::is_none")]
64    pub execution_id: Option<McpExecutionId>,
65}
66
67#[derive(Debug)]
68pub struct ExecutionMetadataBuilder {
69    context_id: ContextId,
70    trace_id: TraceId,
71    session_id: SessionId,
72    user_id: UserId,
73    agent_name: AgentName,
74    timestamp: DateTime<Utc>,
75    task_id: Option<TaskId>,
76    tool_name: Option<McpToolName>,
77    skill_id: Option<SkillId>,
78    skill_name: Option<SkillName>,
79    execution_id: Option<McpExecutionId>,
80}
81
82impl ExecutionMetadataBuilder {
83    pub fn new(ctx: &RequestContext) -> Self {
84        Self {
85            context_id: ctx.context_id().clone(),
86            trace_id: ctx.trace_id().clone(),
87            session_id: ctx.session_id().clone(),
88            user_id: ctx.user_id().clone(),
89            agent_name: ctx.agent_name().clone(),
90            timestamp: Utc::now(),
91            task_id: ctx.task_id().cloned(),
92            tool_name: None,
93            skill_id: None,
94            skill_name: None,
95            execution_id: None,
96        }
97    }
98
99    pub fn with_tool(mut self, name: McpToolName) -> Self {
100        self.tool_name = Some(name);
101        self
102    }
103
104    pub fn with_skill(mut self, id: SkillId, name: SkillName) -> Self {
105        self.skill_id = Some(id);
106        self.skill_name = Some(name);
107        self
108    }
109
110    pub fn with_execution(mut self, id: McpExecutionId) -> Self {
111        self.execution_id = Some(id);
112        self
113    }
114
115    pub fn build(self) -> ExecutionMetadata {
116        ExecutionMetadata {
117            context_id: self.context_id,
118            trace_id: self.trace_id,
119            session_id: self.session_id,
120            user_id: self.user_id,
121            agent_name: self.agent_name,
122            timestamp: self.timestamp,
123            task_id: self.task_id,
124            tool_name: self.tool_name,
125            skill_id: self.skill_id,
126            skill_name: self.skill_name,
127            execution_id: self.execution_id,
128        }
129    }
130}
131
132impl ExecutionMetadata {
133    pub fn builder(ctx: &RequestContext) -> ExecutionMetadataBuilder {
134        ExecutionMetadataBuilder::new(ctx)
135    }
136
137    pub fn with_request(ctx: &RequestContext) -> Self {
138        Self::builder(ctx).build()
139    }
140
141    pub fn with_tool(mut self, name: McpToolName) -> Self {
142        self.tool_name = Some(name);
143        self
144    }
145
146    pub fn with_skill(mut self, id: SkillId, name: SkillName) -> Self {
147        self.skill_id = Some(id);
148        self.skill_name = Some(name);
149        self
150    }
151
152    pub fn with_execution(mut self, id: McpExecutionId) -> Self {
153        self.execution_id = Some(id);
154        self
155    }
156
157    // JSON: JSON Schema document describing the metadata for the model.
158    pub fn schema() -> JsonValue {
159        match serde_json::to_value(schemars::schema_for!(Self)) {
160            Ok(v) => v,
161            Err(e) => {
162                tracing::error!(error = %e, "ExecutionMetadata schema serialization failed");
163                JsonValue::Null
164            },
165        }
166    }
167
168    // JSON: A2A `Artifact.metadata` map.
169    pub fn to_object(&self) -> Result<serde_json::Map<String, JsonValue>, serde_json::Error> {
170        serde_json::from_value(serde_json::to_value(self)?)
171    }
172}
173
174/// Provenance an artifact accumulates while it is built: the request it ran
175/// under (absent until `with_request` is called) and the execution and skill
176/// that produced it.
177#[derive(Debug, Clone, Default)]
178pub struct ArtifactProvenance {
179    request: Option<ExecutionMetadata>,
180    execution_id: Option<McpExecutionId>,
181    skill: Option<(SkillId, SkillName)>,
182}
183
184impl ArtifactProvenance {
185    pub fn set_request(&mut self, ctx: &RequestContext) {
186        self.request = Some(ExecutionMetadata::with_request(ctx));
187    }
188
189    pub fn set_metadata(&mut self, metadata: ExecutionMetadata) {
190        self.request = Some(metadata);
191    }
192
193    pub fn set_execution_id(&mut self, id: McpExecutionId) {
194        self.execution_id = Some(id);
195    }
196
197    pub fn set_skill(&mut self, id: SkillId, name: SkillName) {
198        self.skill = Some((id, name));
199    }
200
201    pub fn execution_id(&self) -> Option<&McpExecutionId> {
202        self.execution_id.as_ref().or_else(|| {
203            self.request
204                .as_ref()
205                .and_then(|metadata| metadata.execution_id.as_ref())
206        })
207    }
208
209    pub fn metadata(&self) -> Option<ExecutionMetadata> {
210        let mut metadata = self.request.clone()?;
211        if let Some(id) = &self.execution_id {
212            metadata.execution_id = Some(id.clone());
213        }
214        if let Some((id, name)) = &self.skill {
215            metadata.skill_id = Some(id.clone());
216            metadata.skill_name = Some(name.clone());
217        }
218        Some(metadata)
219    }
220}
221
222#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
223pub struct ToolResponse<T> {
224    pub artifact_id: ArtifactId,
225    pub mcp_execution_id: McpExecutionId,
226    pub artifact: T,
227    #[serde(rename = "_metadata")]
228    pub metadata: ExecutionMetadata,
229}
230
231impl<T: Serialize + JsonSchema> ToolResponse<T> {
232    pub const fn new(
233        artifact_id: ArtifactId,
234        mcp_execution_id: McpExecutionId,
235        artifact: T,
236        metadata: ExecutionMetadata,
237    ) -> Self {
238        Self {
239            artifact_id,
240            mcp_execution_id,
241            artifact,
242            metadata,
243        }
244    }
245
246    // JSON: A2A `Artifact.metadata` map.
247    pub fn to_json(&self) -> Result<JsonValue, serde_json::Error> {
248        serde_json::to_value(self)
249    }
250}