Skip to main content

aidens_contracts/
execution.rs

1//! Execution context and invocation receipt envelopes for material operations.
2//!
3//! The owner plane is AiDENs orchestration evidence. Tool/runtime authority
4//! remains delegated to the canonical runtime and tool crates.
5
6use 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}