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