1use super::*;
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
9#[serde(rename_all = "kebab-case")]
10pub enum ExecutionCompletionStateV1 {
11 Started,
12 Succeeded,
13 Failed,
14 TimedOut,
15 Cancelled,
16 Partial,
17}
18
19#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
20pub struct ExecutionContextRefV1 {
21 pub execution_id: ArtifactId,
22 pub trace_id: ArtifactId,
23 pub operation_id: String,
24}
25
26#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
27pub struct ExecutionContextEnvelopeV1 {
28 pub execution_id: ArtifactId,
29 pub trace_id: ArtifactId,
30 pub span_id: String,
31 pub operation_id: String,
32 pub attempt_family_id: ArtifactId,
33 pub retry_family_id: ArtifactId,
34 #[serde(default, skip_serializing_if = "Vec::is_empty")]
35 pub queue_lineage: Vec<String>,
36 pub provider_route: String,
37 pub tool_route: String,
38 pub environment_fingerprint: String,
39 pub started_at: DateTime<Utc>,
40 #[serde(default, skip_serializing_if = "Option::is_none")]
41 pub completed_at: Option<DateTime<Utc>>,
42 pub budget_millis_allocated: u64,
43 pub budget_millis_consumed: u64,
44 pub deadline_status: String,
45 pub completion_state: ExecutionCompletionStateV1,
46 pub replay_handle: String,
47 #[serde(default, skip_serializing_if = "Option::is_none")]
48 pub non_replayability_reason: Option<String>,
49 pub redaction_state: String,
50 #[serde(default, skip_serializing_if = "Vec::is_empty")]
51 pub degradation_refs: Vec<ArtifactId>,
52 #[serde(default, skip_serializing_if = "Vec::is_empty")]
53 pub reason_codes: Vec<String>,
54}
55
56impl ExecutionContextEnvelopeV1 {
57 pub fn local_started(
58 operation_id: impl Into<String>,
59 attempt_family_id: ArtifactId,
60 provider_route: impl Into<String>,
61 tool_route: impl Into<String>,
62 ) -> Self {
63 let operation_id = operation_id.into();
64 let started_at = Utc::now();
65 let environment_fingerprint = format!(
66 "crate:{}@{};os:{};arch:{};family:{}",
67 env!("CARGO_PKG_NAME"),
68 env!("CARGO_PKG_VERSION"),
69 std::env::consts::OS,
70 std::env::consts::ARCH,
71 std::env::consts::FAMILY
72 );
73 let material = format!(
74 "{}|{}|{}",
75 operation_id,
76 attempt_family_id.0,
77 started_at.to_rfc3339_opts(SecondsFormat::Nanos, true)
78 );
79 Self {
80 execution_id: generated_artifact_id_from_material("execution-context", &material),
81 trace_id: generated_artifact_id_from_material("trace", &material),
82 span_id: generated_artifact_id_from_material("span", &material).0,
83 operation_id: operation_id.clone(),
84 attempt_family_id: attempt_family_id.clone(),
85 retry_family_id: generated_artifact_id_from_material(
86 "retry-family",
87 &format!("retry|{}|{}", operation_id, attempt_family_id.0),
88 ),
89 queue_lineage: Vec::new(),
90 provider_route: provider_route.into(),
91 tool_route: tool_route.into(),
92 environment_fingerprint,
93 started_at,
94 completed_at: None,
95 budget_millis_allocated: 0,
96 budget_millis_consumed: 0,
97 deadline_status: "not-declared".into(),
98 completion_state: ExecutionCompletionStateV1::Started,
99 replay_handle: "local-replay-handle-deferred".into(),
100 non_replayability_reason: None,
101 redaction_state: "unredacted-local-fixture".into(),
102 degradation_refs: Vec::new(),
103 reason_codes: vec!["execution-context-started".into()],
104 }
105 }
106
107 pub fn as_ref(&self) -> ExecutionContextRefV1 {
108 ExecutionContextRefV1 {
109 execution_id: self.execution_id.clone(),
110 trace_id: self.trace_id.clone(),
111 operation_id: self.operation_id.clone(),
112 }
113 }
114
115 pub fn complete(mut self, state: ExecutionCompletionStateV1, consumed_millis: u64) -> Self {
116 self.completed_at = Some(Utc::now());
117 self.completion_state = state;
118 self.budget_millis_consumed = consumed_millis;
119 if matches!(
120 state,
121 ExecutionCompletionStateV1::TimedOut | ExecutionCompletionStateV1::Partial
122 ) {
123 self.deadline_status = "partial-or-timeout".into();
124 self.reason_codes.push("execution-output-partial".into());
125 } else {
126 self.deadline_status = "completed".into();
127 }
128 self.reason_codes.push("execution-context-completed".into());
129 self.reason_codes.sort();
130 self.reason_codes.dedup();
131 self
132 }
133
134 pub fn terminal_budget_is_enforced(&self) -> bool {
135 if self.completed_at.is_none()
136 || matches!(self.completion_state, ExecutionCompletionStateV1::Started)
137 {
138 return false;
139 }
140 if self.budget_millis_allocated == 0
141 || self.budget_millis_consumed <= self.budget_millis_allocated
142 {
143 return true;
144 }
145 matches!(
146 self.completion_state,
147 ExecutionCompletionStateV1::TimedOut | ExecutionCompletionStateV1::Partial
148 )
149 }
150}
151
152#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
153pub struct ToolCallReceiptV1 {
154 pub receipt_id: ArtifactId,
155 pub execution_context_ref: ExecutionContextRefV1,
156 pub tool_id: String,
157 pub input_digest: DisplayDigestV1,
158 pub output_digest: DisplayDigestV1,
159 pub completion_state: ExecutionCompletionStateV1,
160 pub started_at: DateTime<Utc>,
161 pub completed_at: DateTime<Utc>,
162 pub partial_output: bool,
163 #[serde(default, skip_serializing_if = "Vec::is_empty")]
164 pub degradation_refs: Vec<ArtifactId>,
165 #[serde(default, skip_serializing_if = "Vec::is_empty")]
166 pub reason_codes: Vec<String>,
167}
168
169impl ToolCallReceiptV1 {
170 pub fn new(
171 context: &ExecutionContextEnvelopeV1,
172 tool_id: impl Into<String>,
173 input: &serde_json::Value,
174 output: &serde_json::Value,
175 completion_state: ExecutionCompletionStateV1,
176 ) -> Self {
177 let tool_id = tool_id.into();
178 let input_digest = DisplayDigestV1::for_json_value(input);
179 let output_digest = DisplayDigestV1::for_json_value(output);
180 let completed_at = Utc::now();
181 let material = format!(
182 "{}|{}|{}|{}|{}",
183 context.execution_id.0,
184 tool_id,
185 input_digest.digest,
186 output_digest.digest,
187 completed_at.to_rfc3339_opts(SecondsFormat::Nanos, true)
188 );
189 let partial_output = matches!(
190 completion_state,
191 ExecutionCompletionStateV1::TimedOut | ExecutionCompletionStateV1::Partial
192 );
193 Self {
194 receipt_id: generated_artifact_id_from_material("tool-call-receipt", &material),
195 execution_context_ref: context.as_ref(),
196 tool_id,
197 input_digest,
198 output_digest,
199 completion_state,
200 started_at: context.started_at,
201 completed_at,
202 partial_output,
203 degradation_refs: Vec::new(),
204 reason_codes: if partial_output {
205 vec!["tool-output-partial-or-timeout".into()]
206 } else {
207 vec!["tool-call-receipted".into()]
208 },
209 }
210 }
211}
212
213#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
214pub struct OperatorInvocationReceiptV1 {
215 pub receipt_id: ArtifactId,
216 pub operator_id: String,
217 pub execution_context_ref: ExecutionContextRefV1,
218 pub input_manifest: ArtifactManifestV1,
219 pub output_manifest: ArtifactManifestV1,
220 #[serde(default, skip_serializing_if = "Vec::is_empty")]
221 pub tool_call_receipt_refs: Vec<ArtifactId>,
222 pub completion_state: ExecutionCompletionStateV1,
223 pub material_done: bool,
224 #[serde(default, skip_serializing_if = "Vec::is_empty")]
225 pub proof_debt_refs: Vec<ArtifactId>,
226 #[serde(default, skip_serializing_if = "Vec::is_empty")]
227 pub degradation_refs: Vec<ArtifactId>,
228 #[serde(default, skip_serializing_if = "Vec::is_empty")]
229 pub reason_codes: Vec<String>,
230 pub recorded_at: DateTime<Utc>,
231}
232
233impl OperatorInvocationReceiptV1 {
234 pub fn material_done(
235 operator_id: impl Into<String>,
236 context: &ExecutionContextEnvelopeV1,
237 input_manifest: ArtifactManifestV1,
238 output_manifest: ArtifactManifestV1,
239 tool_call_receipt_refs: Vec<ArtifactId>,
240 ) -> Result<Self, String> {
241 let operator_id = operator_id.into();
242 if tool_call_receipt_refs.is_empty() {
243 return Err("material operation done state requires at least one receipt ref".into());
244 }
245 if !input_manifest.complete() || !output_manifest.complete() {
246 return Err(
247 "material operation done state requires complete input/output manifests".into(),
248 );
249 }
250 let material = format!(
251 "{}|{}|{}|{}",
252 operator_id,
253 context.execution_id.0,
254 input_manifest.manifest_id.0,
255 output_manifest.manifest_id.0
256 );
257 Ok(Self {
258 receipt_id: generated_artifact_id_from_material("operator-invocation", &material),
259 operator_id,
260 execution_context_ref: context.as_ref(),
261 input_manifest,
262 output_manifest,
263 tool_call_receipt_refs,
264 completion_state: ExecutionCompletionStateV1::Succeeded,
265 material_done: true,
266 proof_debt_refs: Vec::new(),
267 degradation_refs: Vec::new(),
268 reason_codes: vec!["material-operation-done-with-receipts".into()],
269 recorded_at: Utc::now(),
270 })
271 }
272}