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