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