1use super::*;
6
7#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
8pub struct PoisonReportEntryV1 {
9 pub poison_id: ArtifactId,
10 pub source_path: String,
11 pub line_number: u64,
12 pub raw_digest: String,
13 pub raw_line: String,
14 #[serde(default, skip_serializing_if = "Vec::is_empty")]
15 pub reason_codes: Vec<String>,
16 pub recorded_at: DateTime<Utc>,
17}
18
19impl PoisonReportEntryV1 {
20 pub fn new(
21 source_path: impl Into<String>,
22 line_number: u64,
23 raw_line: impl Into<String>,
24 reason: impl Into<String>,
25 ) -> Self {
26 let raw_line = raw_line.into();
27 Self {
28 poison_id: display_only_unstable_id("poison-receipt"),
29 source_path: source_path.into(),
30 line_number,
31 raw_digest: non_authoritative_json_display_digest(&serde_json::Value::String(
32 raw_line.clone(),
33 )),
34 raw_line,
35 reason_codes: vec![reason.into()],
36 recorded_at: Utc::now(),
37 }
38 }
39}
40
41#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
42pub struct ExecutionLineageGraphV1 {
43 pub graph_id: ArtifactId,
44 #[serde(default, skip_serializing_if = "Option::is_none")]
45 pub run_id: Option<ArtifactId>,
46 pub nodes: Vec<ExecutionLineageNodeV1>,
47 pub edges: Vec<ExecutionLineageEdgeV1>,
48 pub generated_at: DateTime<Utc>,
49}
50
51impl ExecutionLineageGraphV1 {
52 pub fn new(
53 run_id: Option<ArtifactId>,
54 nodes: Vec<ExecutionLineageNodeV1>,
55 edges: Vec<ExecutionLineageEdgeV1>,
56 ) -> Self {
57 Self {
58 graph_id: display_only_unstable_id("execution-lineage-graph"),
59 run_id,
60 nodes,
61 edges,
62 generated_at: Utc::now(),
63 }
64 }
65}
66
67#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
68pub struct ExecutionLineageNodeV1 {
69 pub receipt_id: ArtifactId,
70 pub kind: ArtifactKindV1,
71 pub content_digest: String,
72}
73
74#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
75pub struct ExecutionLineageEdgeV1 {
76 pub parent_receipt_id: ArtifactId,
77 pub child_receipt_id: ArtifactId,
78 pub relation: String,
79}
80
81#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
82#[serde(rename_all = "kebab-case")]
83pub enum ToolLifecycleStateV1 {
84 Declared,
85 Registered,
86 Executable,
87 Exposed,
88 ExposedThisTurn,
89 Invoked,
90 Succeeded,
91 Failed,
92 Hidden,
93 Blocked,
94}
95
96#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
97#[serde(rename_all = "kebab-case")]
98pub enum CapabilityGateOutcomeV1 {
99 Exposed,
100 Hidden,
101 Blocked,
102}
103
104#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
105pub struct CapabilityGateDecisionV1 {
106 pub decision_id: ArtifactId,
107 pub kind: ArtifactKindV1,
108 pub capability_id: String,
109 pub outcome: CapabilityGateOutcomeV1,
110 pub lifecycle: Vec<ToolLifecycleStateV1>,
111 #[serde(default, skip_serializing_if = "Option::is_none")]
112 pub risk_class: Option<CanonicalToolSideEffectClass>,
113 #[serde(default)]
114 pub permit_required: bool,
115 pub executable_this_turn: bool,
116 #[serde(default, skip_serializing_if = "Option::is_none")]
117 pub sandbox_root: Option<String>,
118 #[serde(default, skip_serializing_if = "Option::is_none")]
119 pub approval_request: Option<ApprovalRequestV1>,
120 #[serde(default, skip_serializing_if = "Option::is_none")]
121 pub permit_grant_id: Option<ArtifactId>,
122 #[serde(default, skip_serializing_if = "Option::is_none")]
123 pub permit_use_receipt_id: Option<ArtifactId>,
124 #[serde(default, skip_serializing_if = "Vec::is_empty")]
125 pub reason_codes: Vec<String>,
126}
127
128impl CapabilityGateDecisionV1 {
129 pub fn new(
130 capability_id: impl Into<String>,
131 outcome: CapabilityGateOutcomeV1,
132 lifecycle: Vec<ToolLifecycleStateV1>,
133 executable_this_turn: bool,
134 reason_codes: Vec<String>,
135 ) -> Self {
136 Self {
137 decision_id: display_only_unstable_id("capability-gate"),
138 kind: ArtifactKindV1::ToolExposure,
139 capability_id: capability_id.into(),
140 outcome,
141 lifecycle,
142 risk_class: None,
143 permit_required: false,
144 executable_this_turn,
145 sandbox_root: None,
146 approval_request: None,
147 permit_grant_id: None,
148 permit_use_receipt_id: None,
149 reason_codes,
150 }
151 }
152
153 pub fn for_tool(draft: CapabilityGateDecisionDraftV1) -> Self {
154 Self {
155 decision_id: display_only_unstable_id("capability-gate"),
156 kind: ArtifactKindV1::ToolExposure,
157 capability_id: draft.tool_id,
158 outcome: draft.outcome,
159 lifecycle: draft.lifecycle,
160 risk_class: Some(draft.risk_class),
161 permit_required: draft.permit_required,
162 executable_this_turn: draft.executable_this_turn,
163 sandbox_root: draft.sandbox_root,
164 approval_request: draft.approval_request,
165 permit_grant_id: draft.permit_grant_id,
166 permit_use_receipt_id: draft.permit_use_receipt_id,
167 reason_codes: draft.reason_codes,
168 }
169 }
170}
171
172#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
173pub struct CapabilityGateDecisionDraftV1 {
174 pub tool_id: String,
175 pub outcome: CapabilityGateOutcomeV1,
176 pub lifecycle: Vec<ToolLifecycleStateV1>,
177 pub risk_class: CanonicalToolSideEffectClass,
178 pub permit_required: bool,
179 pub executable_this_turn: bool,
180 #[serde(default, skip_serializing_if = "Option::is_none")]
181 pub sandbox_root: Option<String>,
182 #[serde(default, skip_serializing_if = "Option::is_none")]
183 pub approval_request: Option<ApprovalRequestV1>,
184 #[serde(default, skip_serializing_if = "Option::is_none")]
185 pub permit_grant_id: Option<ArtifactId>,
186 #[serde(default, skip_serializing_if = "Option::is_none")]
187 pub permit_use_receipt_id: Option<ArtifactId>,
188 #[serde(default, skip_serializing_if = "Vec::is_empty")]
189 pub reason_codes: Vec<String>,
190}
191
192#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
193pub struct PermitGrantV1 {
194 pub permit_id: ArtifactId,
195 pub risk_class: CanonicalToolSideEffectClass,
196 pub tool_id: String,
197 pub sandbox_root: String,
198 pub scope: String,
199 pub granted_by: String,
200 pub granted_at: DateTime<Utc>,
201 pub expires_at: Option<DateTime<Utc>>,
202 #[serde(default, skip_serializing_if = "Option::is_none")]
203 pub run_id: Option<ArtifactId>,
204 #[serde(default, skip_serializing_if = "Option::is_none")]
205 pub attempt_id: Option<ArtifactId>,
206 #[serde(default, skip_serializing_if = "Vec::is_empty")]
207 pub reason_codes: Vec<String>,
208}
209
210impl PermitGrantV1 {
211 pub fn new(
212 risk_class: CanonicalToolSideEffectClass,
213 scope: impl Into<String>,
214 granted_by: impl Into<String>,
215 ) -> Self {
216 let scope = scope.into();
217 Self {
218 permit_id: display_only_unstable_id("permit"),
219 risk_class,
220 tool_id: "*".into(),
221 sandbox_root: scope.clone(),
222 scope,
223 granted_by: granted_by.into(),
224 granted_at: Utc::now(),
225 expires_at: None,
226 run_id: None,
227 attempt_id: None,
228 reason_codes: Vec::new(),
229 }
230 }
231
232 pub fn scoped(
233 risk_class: CanonicalToolSideEffectClass,
234 tool_id: impl Into<String>,
235 sandbox_root: impl Into<String>,
236 granted_by: impl Into<String>,
237 ) -> Self {
238 let tool_id = tool_id.into();
239 let sandbox_root = sandbox_root.into();
240 Self {
241 permit_id: display_only_unstable_id("permit"),
242 risk_class,
243 tool_id: tool_id.clone(),
244 sandbox_root: sandbox_root.clone(),
245 scope: format!("tool={tool_id};sandbox={sandbox_root}"),
246 granted_by: granted_by.into(),
247 granted_at: Utc::now(),
248 expires_at: None,
249 run_id: None,
250 attempt_id: None,
251 reason_codes: Vec::new(),
252 }
253 }
254
255 pub fn for_execution_context(mut self, context: &AidensRunContextV1) -> Self {
256 self.run_id = Some(context.run_id.clone());
257 self.attempt_id = Some(context.attempt_id.clone());
258 self.scope = format!(
259 "{};run={};attempt={}",
260 self.scope, context.run_id.0, context.attempt_id.0
261 );
262 self
263 }
264
265 pub fn matches_scope(
266 &self,
267 risk_class: &CanonicalToolSideEffectClass,
268 tool_id: &str,
269 sandbox_root: &str,
270 run_id: Option<&ArtifactId>,
271 attempt_id: Option<&ArtifactId>,
272 ) -> bool {
273 if &self.risk_class != risk_class {
274 return false;
275 }
276 if self.tool_id != tool_id {
277 return false;
278 }
279 if self.sandbox_root != sandbox_root {
280 return false;
281 }
282 if self
283 .run_id
284 .as_ref()
285 .is_some_and(|grant_run_id| Some(grant_run_id) != run_id)
286 {
287 return false;
288 }
289 if self
290 .attempt_id
291 .as_ref()
292 .is_some_and(|grant_attempt_id| Some(grant_attempt_id) != attempt_id)
293 {
294 return false;
295 }
296 self.expires_at
297 .map(|expires_at| expires_at > Utc::now())
298 .unwrap_or(true)
299 }
300}
301
302#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
303pub struct ApprovalRequestV1 {
304 pub request_id: ArtifactId,
305 pub tool_id: String,
306 pub risk_class: CanonicalToolSideEffectClass,
307 pub scope: String,
308 pub sandbox_root: String,
309 #[serde(default, skip_serializing_if = "Option::is_none")]
310 pub run_id: Option<ArtifactId>,
311 #[serde(default, skip_serializing_if = "Option::is_none")]
312 pub attempt_id: Option<ArtifactId>,
313 pub reason: String,
314 #[serde(default, skip_serializing_if = "Vec::is_empty")]
315 pub reason_codes: Vec<String>,
316 pub requested_at: DateTime<Utc>,
317}
318
319impl ApprovalRequestV1 {
320 pub fn new(
321 tool_id: impl Into<String>,
322 risk_class: CanonicalToolSideEffectClass,
323 scope: impl Into<String>,
324 reason: impl Into<String>,
325 ) -> Self {
326 let scope = scope.into();
327 Self {
328 request_id: display_only_unstable_id("approval-request"),
329 tool_id: tool_id.into(),
330 risk_class,
331 sandbox_root: scope.clone(),
332 scope,
333 run_id: None,
334 attempt_id: None,
335 reason: reason.into(),
336 reason_codes: vec!["approval-required".into()],
337 requested_at: Utc::now(),
338 }
339 }
340
341 pub fn scoped(
342 tool_id: impl Into<String>,
343 risk_class: CanonicalToolSideEffectClass,
344 sandbox_root: impl Into<String>,
345 reason: impl Into<String>,
346 ) -> Self {
347 let tool_id = tool_id.into();
348 let sandbox_root = sandbox_root.into();
349 Self {
350 request_id: display_only_unstable_id("approval-request"),
351 tool_id: tool_id.clone(),
352 risk_class,
353 scope: format!("tool={tool_id};sandbox={sandbox_root}"),
354 sandbox_root,
355 run_id: None,
356 attempt_id: None,
357 reason: reason.into(),
358 reason_codes: vec!["approval-required".into()],
359 requested_at: Utc::now(),
360 }
361 }
362
363 pub fn for_execution_context(mut self, context: &AidensRunContextV1) -> Self {
364 self.run_id = Some(context.run_id.clone());
365 self.attempt_id = Some(context.attempt_id.clone());
366 self.scope = format!(
367 "{};run={};attempt={}",
368 self.scope, context.run_id.0, context.attempt_id.0
369 );
370 self
371 }
372}
373
374#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
375pub struct ApprovalDecisionV1 {
376 pub decision_id: ArtifactId,
377 pub request_id: ArtifactId,
378 pub approved: bool,
379 pub permit_grant: Option<PermitGrantV1>,
380 pub decided_by: String,
381 pub decided_at: DateTime<Utc>,
382 #[serde(default, skip_serializing_if = "Vec::is_empty")]
383 pub reason_codes: Vec<String>,
384}
385
386impl ApprovalDecisionV1 {
387 pub fn denied(
388 request_id: ArtifactId,
389 decided_by: impl Into<String>,
390 reason: impl Into<String>,
391 ) -> Self {
392 Self {
393 decision_id: display_only_unstable_id("approval-decision"),
394 request_id,
395 approved: false,
396 permit_grant: None,
397 decided_by: decided_by.into(),
398 decided_at: Utc::now(),
399 reason_codes: vec![reason.into()],
400 }
401 }
402
403 pub fn approved(
404 request_id: ArtifactId,
405 permit_grant: PermitGrantV1,
406 decided_by: impl Into<String>,
407 ) -> Self {
408 Self {
409 decision_id: display_only_unstable_id("approval-decision"),
410 request_id,
411 approved: true,
412 permit_grant: Some(permit_grant),
413 decided_by: decided_by.into(),
414 decided_at: Utc::now(),
415 reason_codes: Vec::new(),
416 }
417 }
418}
419
420#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
421pub struct PermitUseReportV1 {
422 pub receipt_id: ArtifactId,
423 pub kind: ArtifactKindV1,
424 pub permit_id: ArtifactId,
425 pub tool_id: String,
426 pub risk_class: CanonicalToolSideEffectClass,
427 pub sandbox_root: String,
428 #[serde(default, skip_serializing_if = "Option::is_none")]
429 pub run_id: Option<ArtifactId>,
430 #[serde(default, skip_serializing_if = "Option::is_none")]
431 pub attempt_id: Option<ArtifactId>,
432 pub allowed: bool,
433 #[serde(default, skip_serializing_if = "Vec::is_empty")]
434 pub reason_codes: Vec<String>,
435 pub used_at: DateTime<Utc>,
436}
437
438impl PermitUseReportV1 {
439 pub fn allowed(
440 grant: &PermitGrantV1,
441 tool_id: impl Into<String>,
442 sandbox_root: impl Into<String>,
443 run_id: Option<ArtifactId>,
444 attempt_id: Option<ArtifactId>,
445 ) -> Self {
446 Self {
447 receipt_id: display_only_unstable_id("permit-use"),
448 kind: ArtifactKindV1::PermitUse,
449 permit_id: grant.permit_id.clone(),
450 tool_id: tool_id.into(),
451 risk_class: grant.risk_class.clone(),
452 sandbox_root: sandbox_root.into(),
453 run_id,
454 attempt_id,
455 allowed: true,
456 reason_codes: vec!["permit-scope-matched".into()],
457 used_at: Utc::now(),
458 }
459 }
460
461 pub fn denied(
462 permit_id: ArtifactId,
463 tool_id: impl Into<String>,
464 risk_class: CanonicalToolSideEffectClass,
465 sandbox_root: impl Into<String>,
466 reason: impl Into<String>,
467 ) -> Self {
468 Self {
469 receipt_id: display_only_unstable_id("permit-use"),
470 kind: ArtifactKindV1::PermitUse,
471 permit_id,
472 tool_id: tool_id.into(),
473 risk_class,
474 sandbox_root: sandbox_root.into(),
475 run_id: None,
476 attempt_id: None,
477 allowed: false,
478 reason_codes: vec![reason.into()],
479 used_at: Utc::now(),
480 }
481 }
482}
483
484#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
485#[serde(rename_all = "kebab-case")]
486pub enum TurnModeV1 {
487 NoTools,
488 NativeToolLoop,
489 ParserFallback,
490 ProviderUnavailable,
491 BudgetExhausted,
492}
493
494impl fmt::Display for TurnModeV1 {
495 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
496 f.write_str(match self {
497 Self::NoTools => "no-tools",
498 Self::NativeToolLoop => "native-tool-loop",
499 Self::ParserFallback => "parser-fallback",
500 Self::ProviderUnavailable => "provider-unavailable",
501 Self::BudgetExhausted => "budget-exhausted",
502 })
503 }
504}
505
506#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
507#[serde(rename_all = "kebab-case")]
508pub enum ToolCallSourceV1 {
509 NativeProvider,
510 ParserFallback,
511}
512
513#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
514pub struct ToolCallRequestV1 {
515 pub call_id: ArtifactId,
516 pub source: ToolCallSourceV1,
517 pub tool_id: String,
518 pub input: serde_json::Value,
519 pub input_digest: String,
520 #[serde(default)]
521 pub degraded: bool,
522 pub raw_provider_text: Option<String>,
523 #[serde(default, skip_serializing_if = "Vec::is_empty")]
524 pub reason_codes: Vec<String>,
525 #[serde(default, skip_serializing_if = "Vec::is_empty")]
526 pub canonical_backpointers: Vec<CanonicalBackpointerV1>,
527 pub requested_at: DateTime<Utc>,
528}
529
530impl ToolCallRequestV1 {
531 pub fn new(
532 source: ToolCallSourceV1,
533 tool_id: impl Into<String>,
534 input: serde_json::Value,
535 raw_provider_text: Option<String>,
536 reason_codes: Vec<String>,
537 ) -> Self {
538 Self {
539 call_id: display_only_unstable_id("tool-call"),
540 source,
541 tool_id: tool_id.into(),
542 input_digest: json_digest(&input),
543 input,
544 degraded: source == ToolCallSourceV1::ParserFallback,
545 raw_provider_text,
546 reason_codes,
547 canonical_backpointers: canonical_owner_backpointer(
548 "llm-tool-runtime",
549 "ToolCall",
550 "canonical-tool-call-owner",
551 ),
552 requested_at: Utc::now(),
553 }
554 }
555}
556
557#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
558pub struct ToolCallResultV1 {
559 pub result_id: ArtifactId,
560 pub call_id: ArtifactId,
561 pub tool_id: String,
562 pub input_digest: String,
563 pub output: Option<serde_json::Value>,
564 pub output_digest: Option<String>,
565 pub succeeded: bool,
566 pub invocation_receipt_id: ArtifactId,
567 #[serde(default, skip_serializing_if = "Vec::is_empty")]
568 pub reason_codes: Vec<String>,
569 #[serde(default, skip_serializing_if = "Vec::is_empty")]
570 pub canonical_backpointers: Vec<CanonicalBackpointerV1>,
571 pub completed_at: DateTime<Utc>,
572}
573
574impl ToolCallResultV1 {
575 pub fn from_invocation(
576 request: &ToolCallRequestV1,
577 invocation_receipt: &ToolInvocationReportV1,
578 ) -> Self {
579 Self {
580 result_id: display_only_unstable_id("tool-call-result"),
581 call_id: request.call_id.clone(),
582 tool_id: request.tool_id.clone(),
583 input_digest: request.input_digest.clone(),
584 output: invocation_receipt.output.clone(),
585 output_digest: invocation_receipt.output_digest.clone(),
586 succeeded: invocation_receipt.succeeded,
587 invocation_receipt_id: invocation_receipt.receipt_id.clone(),
588 reason_codes: invocation_receipt.reason_codes.clone(),
589 canonical_backpointers: canonical_owner_backpointer(
590 "llm-tool-runtime",
591 "ToolResult",
592 "canonical-tool-result-owner",
593 ),
594 completed_at: Utc::now(),
595 }
596 }
597
598 pub fn output_text(&self) -> String {
599 self.output
600 .as_ref()
601 .and_then(|output| output.get("content"))
602 .and_then(serde_json::Value::as_str)
603 .map(str::to_string)
604 .or_else(|| self.output.as_ref().map(serde_json::Value::to_string))
605 .unwrap_or_default()
606 }
607}
608
609#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
610pub struct TurnExecutionPlanV1 {
611 pub plan_id: ArtifactId,
612 pub mode: TurnModeV1,
613 pub provider_route: ProviderRouteReportV1,
614 pub tool_exposure_id: ArtifactId,
615 pub exposed_tool_ids: Vec<String>,
616 pub max_tool_calls: u32,
617 pub max_retries: u32,
618 pub max_turn_millis: u64,
619 #[serde(default, skip_serializing_if = "Vec::is_empty")]
620 pub reason_codes: Vec<String>,
621 pub created_at: DateTime<Utc>,
622}
623
624impl TurnExecutionPlanV1 {
625 pub fn new(
626 mode: TurnModeV1,
627 provider_route: ProviderRouteReportV1,
628 tool_exposure: &ToolExposureSetV1,
629 max_tool_calls: u32,
630 max_retries: u32,
631 max_turn_millis: u64,
632 reason_codes: Vec<String>,
633 ) -> Self {
634 Self {
635 plan_id: display_only_unstable_id("turn-plan"),
636 mode,
637 provider_route,
638 tool_exposure_id: tool_exposure.exposure_id.clone(),
639 exposed_tool_ids: tool_exposure.exposed_tool_ids.clone(),
640 max_tool_calls,
641 max_retries,
642 max_turn_millis,
643 reason_codes,
644 created_at: Utc::now(),
645 }
646 }
647}
648
649#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
650#[serde(rename_all = "kebab-case")]
651pub enum TurnFinalStateV1 {
652 FinalOutput,
653 ProviderUnavailable,
654 ToolBlocked,
655 ToolFailed,
656 BudgetExhausted,
657 StopRuleTriggered,
658}
659
660#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
661pub struct TurnReportV1 {
662 pub receipt_id: ArtifactId,
663 pub kind: ArtifactKindV1,
664 pub run_id: ArtifactId,
665 pub attempt_id: ArtifactId,
666 pub plan_id: ArtifactId,
667 pub mode: TurnModeV1,
668 pub final_state: TurnFinalStateV1,
669 #[serde(default)]
670 pub degraded: bool,
671 #[serde(default)]
672 pub blocked: bool,
673 #[serde(default, skip_serializing_if = "Vec::is_empty")]
674 pub tool_call_ids: Vec<ArtifactId>,
675 #[serde(default, skip_serializing_if = "Vec::is_empty")]
676 pub tool_invocation_receipt_ids: Vec<ArtifactId>,
677 #[serde(default, skip_serializing_if = "Vec::is_empty")]
678 pub stop_rule_receipt_ids: Vec<ArtifactId>,
679 pub budget_exhaustion_receipt_id: Option<ArtifactId>,
680 #[serde(default, skip_serializing_if = "Vec::is_empty")]
681 pub reason_codes: Vec<String>,
682 pub started_at: DateTime<Utc>,
683 pub completed_at: Option<DateTime<Utc>>,
684}
685
686impl TurnReportV1 {
687 pub fn started(context: &AidensRunContextV1, plan: &TurnExecutionPlanV1) -> Self {
688 Self {
689 receipt_id: display_only_unstable_id("turn"),
690 kind: ArtifactKindV1::Turn,
691 run_id: context.run_id.clone(),
692 attempt_id: context.attempt_id.clone(),
693 plan_id: plan.plan_id.clone(),
694 mode: plan.mode,
695 final_state: TurnFinalStateV1::StopRuleTriggered,
696 degraded: false,
697 blocked: false,
698 tool_call_ids: Vec::new(),
699 tool_invocation_receipt_ids: Vec::new(),
700 stop_rule_receipt_ids: Vec::new(),
701 budget_exhaustion_receipt_id: None,
702 reason_codes: plan.reason_codes.clone(),
703 started_at: Utc::now(),
704 completed_at: None,
705 }
706 }
707
708 pub fn record_tool_call(
709 &mut self,
710 request: &ToolCallRequestV1,
711 invocation_receipt: &ToolInvocationReportV1,
712 ) {
713 self.tool_call_ids.push(request.call_id.clone());
714 self.tool_invocation_receipt_ids
715 .push(invocation_receipt.receipt_id.clone());
716 if request.degraded {
717 self.degraded = true;
718 }
719 }
720
721 pub fn record_stop_rule(&mut self, stop_rule: &StopRuleReportV1) {
722 self.stop_rule_receipt_ids
723 .push(stop_rule.receipt_id.clone());
724 }
725
726 pub fn record_budget_exhaustion(&mut self, budget: &BudgetExhaustionReportV1) {
727 self.budget_exhaustion_receipt_id = Some(budget.receipt_id.clone());
728 self.degraded = true;
729 self.blocked = true;
730 }
731
732 pub fn complete(mut self, final_state: TurnFinalStateV1) -> Self {
733 self.final_state = final_state;
734 self.completed_at = Some(Utc::now());
735 self
736 }
737}
738
739#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
740#[serde(rename_all = "kebab-case")]
741pub enum StopRuleV1 {
742 FinalOutput,
743 ProviderUnavailable,
744 AgencyPolicy,
745 ToolNotExposed,
746 ToolInvocationFailed,
747 RecursiveToolCall,
748 MaxToolCalls,
749 MaxRetries,
750 DeadlineExceeded,
751}
752
753#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
754pub struct StopRuleReportV1 {
755 pub receipt_id: ArtifactId,
756 pub kind: ArtifactKindV1,
757 pub run_id: ArtifactId,
758 pub attempt_id: ArtifactId,
759 pub rule: StopRuleV1,
760 pub triggered: bool,
761 #[serde(default, skip_serializing_if = "Vec::is_empty")]
762 pub reason_codes: Vec<String>,
763 pub checked_at: DateTime<Utc>,
764}
765
766impl StopRuleReportV1 {
767 pub fn triggered(
768 context: &AidensRunContextV1,
769 rule: StopRuleV1,
770 reason_codes: Vec<String>,
771 ) -> Self {
772 Self {
773 receipt_id: display_only_unstable_id("stop-rule"),
774 kind: ArtifactKindV1::StopRule,
775 run_id: context.run_id.clone(),
776 attempt_id: context.attempt_id.clone(),
777 rule,
778 triggered: true,
779 reason_codes,
780 checked_at: Utc::now(),
781 }
782 }
783}
784
785#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
786pub struct BudgetExhaustionReportV1 {
787 pub receipt_id: ArtifactId,
788 pub kind: ArtifactKindV1,
789 pub run_id: ArtifactId,
790 pub attempt_id: ArtifactId,
791 pub max_tool_calls: u32,
792 pub attempted_tool_calls: u32,
793 pub max_retries: u32,
794 pub retries: u32,
795 pub max_turn_millis: u64,
796 pub elapsed_millis: u64,
797 #[serde(default, skip_serializing_if = "Vec::is_empty")]
798 pub reason_codes: Vec<String>,
799 pub exhausted_at: DateTime<Utc>,
800}
801
802impl BudgetExhaustionReportV1 {
803 pub fn new(draft: BudgetExhaustionReportDraftV1) -> Self {
804 Self {
805 receipt_id: display_only_unstable_id("budget-exhaustion"),
806 kind: ArtifactKindV1::BudgetExhaustion,
807 run_id: draft.run_id,
808 attempt_id: draft.attempt_id,
809 max_tool_calls: draft.max_tool_calls,
810 attempted_tool_calls: draft.attempted_tool_calls,
811 max_retries: draft.max_retries,
812 retries: draft.retries,
813 max_turn_millis: draft.max_turn_millis,
814 elapsed_millis: draft.elapsed_millis,
815 reason_codes: draft.reason_codes,
816 exhausted_at: Utc::now(),
817 }
818 }
819}
820
821#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
822pub struct BudgetExhaustionReportDraftV1 {
823 pub run_id: ArtifactId,
824 pub attempt_id: ArtifactId,
825 pub max_tool_calls: u32,
826 pub attempted_tool_calls: u32,
827 pub max_retries: u32,
828 pub retries: u32,
829 pub max_turn_millis: u64,
830 pub elapsed_millis: u64,
831 #[serde(default, skip_serializing_if = "Vec::is_empty")]
832 pub reason_codes: Vec<String>,
833}
834
835#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
836#[serde(rename_all = "kebab-case")]
837pub enum ToolInvocationOutcomeV1 {
838 Started,
839 Succeeded,
840 Failed,
841}
842
843#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
844pub struct ToolInvocationReportV1 {
845 pub receipt_id: ArtifactId,
846 pub kind: ArtifactKindV1,
847 pub run_id: Option<ArtifactId>,
848 pub attempt_id: Option<ArtifactId>,
849 pub tool_id: String,
850 pub input: serde_json::Value,
851 pub input_digest: String,
852 pub output: Option<serde_json::Value>,
853 pub output_digest: Option<String>,
854 pub succeeded: bool,
855 pub outcome: ToolInvocationOutcomeV1,
856 pub permit_grant_id: Option<ArtifactId>,
857 pub permit_use_receipt_id: Option<ArtifactId>,
858 pub approval_decision_id: Option<ArtifactId>,
859 pub approval_request_id: Option<ArtifactId>,
860 #[serde(default, skip_serializing_if = "Vec::is_empty")]
861 pub schema_validation_receipt_ids: Vec<ArtifactId>,
862 #[serde(default, skip_serializing_if = "Vec::is_empty")]
863 pub canonical_backpointers: Vec<CanonicalBackpointerV1>,
864 pub lifecycle: Vec<ToolLifecycleStateV1>,
865 #[serde(default, skip_serializing_if = "Vec::is_empty")]
866 pub reason_codes: Vec<String>,
867 pub started_at: DateTime<Utc>,
868 pub completed_at: Option<DateTime<Utc>>,
869}
870
871pub type ToolAttemptReportV1 = ToolInvocationReportV1;
872
873impl ToolInvocationReportV1 {
874 pub fn started(tool_id: impl Into<String>, input: serde_json::Value) -> Self {
875 Self {
876 receipt_id: display_only_unstable_id("tool-invocation"),
877 kind: ArtifactKindV1::ToolInvocation,
878 run_id: None,
879 attempt_id: None,
880 tool_id: tool_id.into(),
881 input_digest: json_digest(&input),
882 input,
883 output: None,
884 output_digest: None,
885 succeeded: false,
886 outcome: ToolInvocationOutcomeV1::Started,
887 permit_grant_id: None,
888 permit_use_receipt_id: None,
889 approval_decision_id: None,
890 approval_request_id: None,
891 schema_validation_receipt_ids: Vec::new(),
892 canonical_backpointers: canonical_owner_backpointer(
893 "llm-tool-runtime",
894 "ToolReceipt",
895 "canonical-tool-receipt-owner",
896 ),
897 lifecycle: vec![ToolLifecycleStateV1::Invoked],
898 reason_codes: Vec::new(),
899 started_at: Utc::now(),
900 completed_at: None,
901 }
902 }
903
904 pub fn with_execution_context(mut self, context: &AidensRunContextV1) -> Self {
905 self.run_id = Some(context.run_id.clone());
906 self.attempt_id = Some(context.attempt_id.clone());
907 self
908 }
909
910 pub fn with_permit_grant(mut self, permit_grant_id: ArtifactId) -> Self {
911 self.permit_grant_id = Some(permit_grant_id);
912 self
913 }
914
915 pub fn with_permit_use(mut self, permit_use_receipt_id: ArtifactId) -> Self {
916 self.permit_use_receipt_id = Some(permit_use_receipt_id);
917 self
918 }
919
920 pub fn with_approval_request(mut self, approval_request_id: ArtifactId) -> Self {
921 self.approval_request_id = Some(approval_request_id);
922 self
923 }
924
925 pub fn with_approval_decision(mut self, approval_decision_id: ArtifactId) -> Self {
926 self.approval_decision_id = Some(approval_decision_id);
927 self
928 }
929
930 pub fn with_schema_validation(mut self, schema_validation_receipt_id: ArtifactId) -> Self {
931 self.schema_validation_receipt_ids
932 .push(schema_validation_receipt_id);
933 self
934 }
935
936 pub fn with_canonical_tool_receipt(mut self, receipt_id: impl Into<String>) -> Self {
937 self.canonical_backpointers
938 .push(CanonicalBackpointerV1::external(
939 "llm-tool-runtime",
940 "ToolReceipt",
941 "canonical-tool-receipt-id",
942 receipt_id,
943 ));
944 self
945 }
946
947 pub fn complete_success(mut self, output: serde_json::Value) -> Self {
948 self.output_digest = Some(json_digest(&output));
949 self.output = Some(output);
950 self.succeeded = true;
951 self.outcome = ToolInvocationOutcomeV1::Succeeded;
952 self.lifecycle.push(ToolLifecycleStateV1::Succeeded);
953 self.completed_at = Some(Utc::now());
954 self
955 }
956
957 pub fn complete_failure(mut self, reason: impl Into<String>) -> Self {
958 self.reason_codes.push(reason.into());
959 self.succeeded = false;
960 self.outcome = ToolInvocationOutcomeV1::Failed;
961 self.lifecycle.push(ToolLifecycleStateV1::Failed);
962 self.completed_at = Some(Utc::now());
963 self
964 }
965
966 pub fn complete_failure_with_output(
967 mut self,
968 reason: impl Into<String>,
969 output: serde_json::Value,
970 ) -> Self {
971 self.output_digest = Some(json_digest(&output));
972 self.output = Some(output);
973 self.complete_failure(reason)
974 }
975}
976
977#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
978pub struct RunReportV1 {
979 pub receipt_id: ArtifactId,
980 pub kind: ArtifactKindV1,
981 pub context: AidensRunContextV1,
982 pub provider_route: Option<ProviderRouteReportV1>,
983 #[serde(default, skip_serializing_if = "Vec::is_empty")]
984 pub tool_exposure_ids: Vec<ArtifactId>,
985 #[serde(default, skip_serializing_if = "Vec::is_empty")]
986 pub turn_receipts: Vec<TurnReportV1>,
987 #[serde(default, skip_serializing_if = "Vec::is_empty")]
988 pub tool_call_requests: Vec<ToolCallRequestV1>,
989 #[serde(default, skip_serializing_if = "Vec::is_empty")]
990 pub tool_call_results: Vec<ToolCallResultV1>,
991 #[serde(default, skip_serializing_if = "Vec::is_empty")]
992 pub tool_invocation_receipts: Vec<ToolInvocationReportV1>,
993 #[serde(default, skip_serializing_if = "Vec::is_empty")]
994 pub approval_requests: Vec<ApprovalRequestV1>,
995 #[serde(default, skip_serializing_if = "Vec::is_empty")]
996 pub approval_decisions: Vec<ApprovalDecisionV1>,
997 #[serde(default, skip_serializing_if = "Vec::is_empty")]
998 pub permit_use_receipts: Vec<PermitUseReportV1>,
999 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1000 pub boundary_repair_receipts: Vec<BoundaryRepairReportV1>,
1001 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1002 pub json_repair_receipts: Vec<JsonBoundaryRepairDisplayReportV1>,
1003 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1004 pub schema_validation_receipts: Vec<SchemaValidationReportV1>,
1005 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1006 pub stop_rule_receipts: Vec<StopRuleReportV1>,
1007 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1008 pub budget_exhaustion_receipts: Vec<BudgetExhaustionReportV1>,
1009 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1010 pub agency_receipt_ids: Vec<String>,
1011 pub warnings: Vec<String>,
1012 pub completed_at: Option<DateTime<Utc>>,
1013}
1014
1015impl RunReportV1 {
1016 pub fn started(context: AidensRunContextV1) -> Self {
1017 Self {
1018 receipt_id: display_only_unstable_id("receipt"),
1019 kind: ArtifactKindV1::Run,
1020 context,
1021 provider_route: None,
1022 tool_exposure_ids: Vec::new(),
1023 turn_receipts: Vec::new(),
1024 tool_call_requests: Vec::new(),
1025 tool_call_results: Vec::new(),
1026 tool_invocation_receipts: Vec::new(),
1027 approval_requests: Vec::new(),
1028 approval_decisions: Vec::new(),
1029 permit_use_receipts: Vec::new(),
1030 boundary_repair_receipts: Vec::new(),
1031 json_repair_receipts: Vec::new(),
1032 schema_validation_receipts: Vec::new(),
1033 stop_rule_receipts: Vec::new(),
1034 budget_exhaustion_receipts: Vec::new(),
1035 agency_receipt_ids: Vec::new(),
1036 warnings: Vec::new(),
1037 completed_at: None,
1038 }
1039 }
1040
1041 pub fn complete(mut self) -> Self {
1042 self.completed_at = Some(Utc::now());
1043 self
1044 }
1045}
1046
1047#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
1048#[serde(rename_all = "kebab-case")]
1049pub enum AiDENsRunFailureClassV1 {
1050 None,
1051 ProviderUnavailable,
1052 ToolBlocked,
1053 ToolFailed,
1054 BudgetExhausted,
1055 VerificationUnavailable,
1056 ReplayMismatch,
1057 OperatorAbstained,
1058}
1059
1060#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
1061pub struct AiDENsRunEventLogDigestV1 {
1062 pub event_log_path: String,
1063 pub digest: StackContentDigest,
1064 pub replay_normalized_digest: StackContentDigest,
1065 pub canonical_record_count: usize,
1066 pub event_count: usize,
1067 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1068 pub reason_codes: Vec<String>,
1069}
1070
1071#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
1072pub struct AiDENsRunBudgetDeadlineV1 {
1073 pub max_steps: u32,
1074 pub max_tool_calls: u32,
1075 pub max_retries: u32,
1076 pub max_turn_millis: u64,
1077 pub elapsed_ms: i64,
1078 pub deadline: Option<String>,
1079 pub cost_budget_units: Option<u64>,
1080 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1081 pub degradation_markers: Vec<String>,
1082}
1083
1084#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
1085pub struct AiDENsRunReplayNormalizationV1 {
1086 pub replay_command: String,
1087 pub fixture_path: Option<String>,
1088 pub normalized_fields: Vec<String>,
1089 pub deterministic_compare: bool,
1090 pub normalized_digest: StackContentDigest,
1091 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1092 pub reason_codes: Vec<String>,
1093}
1094
1095#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
1096pub struct AiDENsRunFailureTaxonomyV1 {
1097 pub class: AiDENsRunFailureClassV1,
1098 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1099 pub reason_codes: Vec<String>,
1100 pub degraded: bool,
1101 pub blocked: bool,
1102}