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, SessionId, SkillId, TaskId, TraceId, UserId,
20};
21
22use crate::execution::context::RequestContext;
23
24/// Reverse-DNS `_meta` key holding execution provenance on the wire; MCP
25/// reserves unprefixed `_meta` keys, so the fields must never appear bare.
26pub const EXECUTION_META_KEY: &str = "io.systemprompt/execution";
27
28#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
29pub struct ExecutionMetadata {
30    #[schemars(with = "String")]
31    pub context_id: ContextId,
32
33    #[schemars(with = "String")]
34    pub trace_id: TraceId,
35
36    #[schemars(with = "String")]
37    pub session_id: SessionId,
38
39    #[schemars(with = "String")]
40    pub user_id: UserId,
41
42    #[schemars(with = "String")]
43    pub agent_name: AgentName,
44
45    #[schemars(with = "String")]
46    pub timestamp: DateTime<Utc>,
47
48    #[serde(skip_serializing_if = "Option::is_none")]
49    #[schemars(with = "Option<String>")]
50    pub task_id: Option<TaskId>,
51
52    #[serde(skip_serializing_if = "Option::is_none")]
53    pub tool_name: Option<String>,
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    pub skill_name: Option<String>,
61
62    #[serde(skip_serializing_if = "Option::is_none")]
63    pub execution_id: Option<String>,
64}
65
66impl Default for ExecutionMetadata {
67    fn default() -> Self {
68        Self {
69            context_id: ContextId::legacy(),
70            trace_id: TraceId::new("unset"),
71            session_id: SessionId::new("unset"),
72            user_id: UserId::new("unset"),
73            agent_name: AgentName::new("unset"),
74            timestamp: Utc::now(),
75            task_id: None,
76            tool_name: None,
77            skill_id: None,
78            skill_name: None,
79            execution_id: None,
80        }
81    }
82}
83
84#[derive(Debug)]
85pub struct ExecutionMetadataBuilder {
86    context_id: ContextId,
87    trace_id: TraceId,
88    session_id: SessionId,
89    user_id: UserId,
90    agent_name: AgentName,
91    timestamp: DateTime<Utc>,
92    task_id: Option<TaskId>,
93    tool_name: Option<String>,
94    skill_id: Option<SkillId>,
95    skill_name: Option<String>,
96    execution_id: Option<String>,
97}
98
99impl ExecutionMetadataBuilder {
100    pub fn new(ctx: &RequestContext) -> Self {
101        Self {
102            context_id: ctx.context_id().clone(),
103            trace_id: ctx.trace_id().clone(),
104            session_id: ctx.session_id().clone(),
105            user_id: ctx.user_id().clone(),
106            agent_name: ctx.agent_name().clone(),
107            timestamp: Utc::now(),
108            task_id: ctx.task_id().cloned(),
109            tool_name: None,
110            skill_id: None,
111            skill_name: None,
112            execution_id: None,
113        }
114    }
115
116    pub fn with_tool(mut self, name: impl Into<String>) -> Self {
117        self.tool_name = Some(name.into());
118        self
119    }
120
121    pub fn with_skill(mut self, id: impl Into<SkillId>, name: impl Into<String>) -> Self {
122        self.skill_id = Some(id.into());
123        self.skill_name = Some(name.into());
124        self
125    }
126
127    pub fn with_execution(mut self, id: impl Into<String>) -> Self {
128        self.execution_id = Some(id.into());
129        self
130    }
131
132    pub fn build(self) -> ExecutionMetadata {
133        ExecutionMetadata {
134            context_id: self.context_id,
135            trace_id: self.trace_id,
136            session_id: self.session_id,
137            user_id: self.user_id,
138            agent_name: self.agent_name,
139            timestamp: self.timestamp,
140            task_id: self.task_id,
141            tool_name: self.tool_name,
142            skill_id: self.skill_id,
143            skill_name: self.skill_name,
144            execution_id: self.execution_id,
145        }
146    }
147}
148
149impl ExecutionMetadata {
150    pub fn builder(ctx: &RequestContext) -> ExecutionMetadataBuilder {
151        ExecutionMetadataBuilder::new(ctx)
152    }
153
154    pub fn with_request(ctx: &RequestContext) -> Self {
155        Self::builder(ctx).build()
156    }
157
158    pub fn with_tool(mut self, name: impl Into<String>) -> Self {
159        self.tool_name = Some(name.into());
160        self
161    }
162
163    pub fn with_skill(mut self, id: impl Into<SkillId>, name: impl Into<String>) -> Self {
164        self.skill_id = Some(id.into());
165        self.skill_name = Some(name.into());
166        self
167    }
168
169    pub fn with_execution(mut self, id: impl Into<String>) -> Self {
170        self.execution_id = Some(id.into());
171        self
172    }
173
174    pub fn schema() -> JsonValue {
175        match serde_json::to_value(schemars::schema_for!(Self)) {
176            Ok(v) => v,
177            Err(e) => {
178                tracing::error!(error = %e, "ExecutionMetadata schema serialization failed");
179                JsonValue::Null
180            },
181        }
182    }
183
184    pub fn to_object(&self) -> Option<serde_json::Map<String, JsonValue>> {
185        serde_json::to_value(self)
186            .map_err(|e| {
187                tracing::warn!(error = %e, "ExecutionMetadata serialization failed");
188                e
189            })
190            .ok()
191            .and_then(|v| v.as_object().cloned())
192    }
193}
194
195#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
196pub struct ToolResponse<T> {
197    pub artifact_id: ArtifactId,
198    pub mcp_execution_id: McpExecutionId,
199    pub artifact: T,
200    #[serde(rename = "_metadata")]
201    pub metadata: ExecutionMetadata,
202}
203
204impl<T: Serialize + JsonSchema> ToolResponse<T> {
205    pub const fn new(
206        artifact_id: ArtifactId,
207        mcp_execution_id: McpExecutionId,
208        artifact: T,
209        metadata: ExecutionMetadata,
210    ) -> Self {
211        Self {
212            artifact_id,
213            mcp_execution_id,
214            artifact,
215            metadata,
216        }
217    }
218
219    pub fn to_json(&self) -> Result<JsonValue, serde_json::Error> {
220        serde_json::to_value(self)
221    }
222}