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::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    // JSON: JSON Schema document describing the metadata for the model.
173    pub fn schema() -> JsonValue {
174        match serde_json::to_value(schemars::schema_for!(Self)) {
175            Ok(v) => v,
176            Err(e) => {
177                tracing::error!(error = %e, "ExecutionMetadata schema serialization failed");
178                JsonValue::Null
179            },
180        }
181    }
182
183    // JSON: A2A `Artifact.metadata` map.
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    // JSON: A2A `Artifact.metadata` map.
220    pub fn to_json(&self) -> Result<JsonValue, serde_json::Error> {
221        serde_json::to_value(self)
222    }
223}