1use aidens_agency_kit::{
4 AgencyPolicyEngineV1, AgencyPolicyInputV1, AgencyPolicyOutcomeV1, AgencyPolicyReportV1,
5 NudgeLedgerV1,
6};
7use aidens_boundary_kit::{parse_json_boundary, BoundaryRepairPolicyV1};
8use aidens_budget_kit::BudgetV1;
9use aidens_contracts::{
10 display_only_unstable_id, AgentMemoryModeV1, AgentProviderModeV1, AgentSpecSupportLabelV1,
11 AgentSpecV1, AgentVerificationCheckV1, AiDENsRunBudgetDeadlineV1, AiDENsRunBundleV3,
12 AiDENsRunEventLogDigestV1, AiDENsRunFailureTaxonomyV1, AiDENsRunReplayNormalizationV1,
13 AiDENsRunSupportTierEvidenceV1, AidensRunContextV1, ArtifactId, BoundaryRepairReportV1,
14 BudgetExhaustionReportDraftV1, BudgetExhaustionReportV1, DegradationEventV1, DisplayDigestV1,
15 JsonBoundaryRepairDisplayReportV1, ProjectionDigestV1, ProviderRouteKindV1,
16 ProviderRouteReportV1, QueryWideningReportV1, ReportLevelV1, RunReportV1, RuntimeViewRequestV1,
17 StackAttemptId, StackTrialId, StopRuleReportV1, StopRuleV1, ToolCallRequestV1,
18 ToolCallResultV1, ToolCallSourceV1, ToolExposureSetV1, ToolInvocationReportV1,
19 TurnExecutionPlanV1, TurnFinalStateV1, TurnModeV1, TurnReportV1, ViewDisclosureReportV1,
20};
21use aidens_governance_kit::{
22 canonical_stack as governance_stack, CanonicalGovernanceAdapter, GovernanceContext,
23};
24use aidens_kernel_kit::CanonicalKernelAdapter;
25use aidens_memory_kit::{
26 canonical_stack, memory_config_for_root, runtime_config_for_namespace, CanonicalMemoryAdapter,
27 MemoryGroundingEvidenceV1,
28};
29use aidens_permit_kit::PermitPolicyV1;
30use aidens_provider_kit::{
31 build_provider, route_receipt, AiDENsChatMessageV1, AiDENsCompletionRequestV1,
32 AiDENsCompletionResponseV1, AiDENsProvider, ProviderSpecV1,
33};
34use aidens_receipts::{CanonicalEventLog, CanonicalEventLogConfig, CanonicalEventLogEntry};
35use aidens_tool_kit::{
36 safe_coding_registry_for_current_dir, ToolDispatcher, ToolExposurePolicyV1,
37 ToolInvocationError, ToolRegistryV1,
38};
39use anyhow::anyhow;
40use std::collections::BTreeSet;
41use std::sync::{Arc, Mutex, MutexGuard};
42use std::time::Instant;
43use std::time::{SystemTime, UNIX_EPOCH};
44
45mod execution;
46mod finalization;
47mod provider_tool;
48mod receipts;
49mod replay;
50
51use execution::*;
52use finalization::*;
53use provider_tool::*;
54pub use receipts::*;
55use replay::*;
56
57pub struct PlanActVerifyLoopV1 {
58 app_id: String,
59 spec: AgentSpecV1,
60 tools: ToolRegistryV1,
61 permit_policy: PermitPolicyV1,
62 provider_mock_response: Option<String>,
63 provider_model: Option<String>,
64 provider_base_url: Option<String>,
65 provider_api_key: Option<String>,
66 sandbox_root: Option<String>,
67 canonical_receipt_log_config: Option<CanonicalEventLogConfig>,
68 budget_retries: u32,
69 run_reports: RunReportLedger,
70 receipt_level: ReportLevelV1,
71 agency_policy: AgencyPolicyEngineV1,
72 agency_nudges: Arc<Mutex<NudgeLedgerV1>>,
73 memory: Option<std::sync::Arc<aidens_memory_kit::CanonicalMemoryAdapter>>,
74 governance: Option<aidens_governance_kit::GovernanceContext>,
75 kernel: Option<aidens_kernel_kit::CanonicalKernelAdapter>,
76}
77
78#[derive(Debug, Clone)]
79pub struct AiDENsRunInput {
80 pub prompt: String,
81}
82
83impl AiDENsRunInput {
84 pub fn new(prompt: impl Into<String>) -> Self {
85 Self {
86 prompt: prompt.into(),
87 }
88 }
89}
90
91impl PlanActVerifyLoopV1 {
92 pub fn new(spec: AgentSpecV1) -> Self {
93 Self {
94 app_id: "aidens-plan-act-loop".into(),
95 spec,
96 tools: safe_coding_registry_for_current_dir(),
97 permit_policy: PermitPolicyV1::default(),
98 provider_mock_response: None,
99 provider_model: None,
100 provider_base_url: None,
101 provider_api_key: None,
102 sandbox_root: None,
103 canonical_receipt_log_config: Some(default_plan_act_verify_receipt_log_config()),
104 budget_retries: 2,
105 run_reports: RunReportLedger::default(),
106 receipt_level: ReportLevelV1::Full,
107 agency_policy: AgencyPolicyEngineV1::default(),
108 agency_nudges: Arc::new(Mutex::new(NudgeLedgerV1::default())),
109 memory: None,
110 governance: None,
111 kernel: None,
112 }
113 }
114
115 pub fn app_id(mut self, app_id: impl Into<String>) -> Self {
116 self.app_id = app_id.into();
117 self
118 }
119
120 pub fn tools(mut self, tools: ToolRegistryV1) -> Self {
121 self.tools = tools;
122 self
123 }
124
125 pub fn permit_policy(mut self, permit_policy: PermitPolicyV1) -> Self {
126 self.permit_policy = permit_policy;
127 self
128 }
129
130 pub fn provider_mock_response(mut self, response: impl Into<String>) -> Self {
131 self.provider_mock_response = Some(response.into());
132 self
133 }
134
135 pub fn provider_model(mut self, model: impl Into<String>) -> Self {
136 self.provider_model = Some(model.into());
137 self
138 }
139
140 pub fn provider_base_url(mut self, base_url: impl Into<String>) -> Self {
141 self.provider_base_url = Some(base_url.into());
142 self
143 }
144
145 pub fn api_key(mut self, key: impl Into<String>) -> Self {
146 self.provider_api_key = Some(key.into());
147 self
148 }
149
150 pub fn sandbox_root(mut self, sandbox_root: impl Into<String>) -> Self {
151 self.sandbox_root = Some(sandbox_root.into());
152 self
153 }
154
155 pub fn canonical_receipt_log_config(mut self, config: CanonicalEventLogConfig) -> Self {
156 self.canonical_receipt_log_config = Some(config);
157 self
158 }
159
160 pub fn with_agency_policy(mut self, policy: AgencyPolicyEngineV1) -> Self {
161 self.agency_policy = policy;
162 self
163 }
164
165 pub fn with_run_reports(mut self, reports: RunReportLedger) -> Self {
166 self.run_reports = reports;
167 self
168 }
169
170 pub fn with_memory(
171 mut self,
172 memory: std::sync::Arc<aidens_memory_kit::CanonicalMemoryAdapter>,
173 ) -> Self {
174 self.memory = Some(memory);
175 self
176 }
177
178 pub fn with_governance(mut self, governance: aidens_governance_kit::GovernanceContext) -> Self {
179 self.governance = Some(governance);
180 self
181 }
182
183 pub fn with_kernel_reasoning(
184 mut self,
185 kernel: aidens_kernel_kit::CanonicalKernelAdapter,
186 ) -> Self {
187 self.kernel = Some(kernel);
188 self
189 }
190
191 pub fn has_memory(&self) -> bool {
192 self.memory.is_some()
193 }
194
195 pub fn has_governance(&self) -> bool {
196 self.governance.is_some()
197 }
198
199 pub fn has_kernel(&self) -> bool {
200 self.kernel.is_some()
201 }
202
203 pub fn max_retries(mut self, retries: u32) -> Self {
204 self.budget_retries = retries;
205 self
206 }
207
208 pub async fn execute(
209 &self,
210 input: impl Into<String>,
211 ) -> anyhow::Result<PlanActVerifyLoopV1Output> {
212 let prompt = input.into();
213 if let Err(reason_codes) = self.spec.validate() {
214 let mut output = PlanActVerifyLoopV1Output::abstention(
215 &self.spec,
216 1,
217 "agent-spec-validation-failed",
218 );
219 output.verification_receipts.push(VerificationReceiptV1 {
220 receipt_id: display_only_unstable_id("agent-verification"),
221 step: 1,
222 check: "agent-spec".into(),
223 passed: false,
224 reason_codes,
225 });
226 return Ok(output);
227 }
228
229 let mut memory_grounding_receipts = Vec::new();
230 if self.spec.memory_policy.enabled
231 && self.spec.memory_policy.mode == AgentMemoryModeV1::CanonicalSeam
232 {
233 match run_canonical_memory_grounding(&self.spec, &prompt).await {
234 Ok(receipts) => {
235 memory_grounding_receipts = receipts;
236 }
237 Err(error) => {
238 let mut output = PlanActVerifyLoopV1Output::abstention(
239 &self.spec,
240 1,
241 "memory-grounding-failed",
242 );
243 output.memory_grounding_receipts = vec![error.to_string()];
244 output.abstention_receipt = Some(AbstentionReceiptV1 {
245 receipt_id: display_only_unstable_id("agent-abstention"),
246 step: 1,
247 reason_code: "memory-grounding-failed".into(),
248 blocked_action: "memory-grounding-query".into(),
249 evidence: vec![error.to_string()],
250 required_permits: Vec::new(),
251 can_resume: true,
252 support_impact: "degraded".into(),
253 });
254 output.finalization = Some(FinalizationReceiptV1 {
255 receipt_id: display_only_unstable_id("agent-finalization"),
256 step: 1,
257 outcome: "abstained-memory-grounding".into(),
258 final_state: "memory-grounding-error".into(),
259 blocked: true,
260 support_label: self.spec.support_label.to_string(),
261 reason_codes: vec!["memory-grounding-failed".into()],
262 });
263 output.repair_plan = Some(RepairPlanDisplayReceiptV1 {
264 repair_id: display_only_unstable_id("agent-repair"),
265 source_run_id: None,
266 failure_kind: "memory-grounding".into(),
267 candidate_repair_actions: vec![
268 "re-check memory seam fixture availability".into(),
269 "adjust query for grounded scope".into(),
270 ],
271 required_verification: vec!["memory-grounding".into()],
272 required_permits: Vec::new(),
273 risk_level: "moderate".into(),
274 canonical_owner: Some("knowledge-runtime".into()),
275 });
276 return Ok(output);
277 }
278 }
279 }
280
281 let mapped_tools = map_agent_tools_to_ids(&self.spec.tool_policy.allowed_tools);
282 if !mapped_tools.unsupported.is_empty() {
283 let mut output = PlanActVerifyLoopV1Output::abstention(
284 &self.spec,
285 1,
286 "tool-policy-contains-unsupported-alias",
287 );
288 output.memory_grounding_receipts = memory_grounding_receipts;
289 output.verification_receipts.push(VerificationReceiptV1 {
290 receipt_id: display_only_unstable_id("agent-verification"),
291 step: 1,
292 check: "tool-policy".into(),
293 passed: false,
294 reason_codes: mapped_tools
295 .unsupported
296 .iter()
297 .map(|alias| format!("unsupported-tool-alias:{alias}"))
298 .collect(),
299 });
300 output.repair_plan = Some(RepairPlanDisplayReceiptV1 {
301 repair_id: display_only_unstable_id("agent-repair"),
302 source_run_id: None,
303 failure_kind: "unsupported-tool-alias".into(),
304 candidate_repair_actions: vec![
305 "remove unsupported tool aliases from allowed tool list".into(),
306 "replace run.replay requests with a supported alias".into(),
307 ],
308 required_verification: vec!["tool-policy".into()],
309 required_permits: Vec::new(),
310 risk_level: "low".into(),
311 canonical_owner: Some("aidens-tool-kit".into()),
312 });
313 output.finalization = Some(FinalizationReceiptV1 {
314 receipt_id: display_only_unstable_id("agent-finalization"),
315 step: 1,
316 outcome: "abstained-unsupported-tools".into(),
317 final_state: "tool-policy-check".into(),
318 blocked: true,
319 support_label: self.spec.support_label.to_string(),
320 reason_codes: vec!["tool-policy-contains-unsupported-alias".into()],
321 });
322 return Ok(output);
323 }
324
325 let plan = PlanReceiptV1 {
326 receipt_id: display_only_unstable_id("agent-plan"),
327 plan_id: display_only_unstable_id("agent-plan"),
328 step: 1,
329 action: "bounded plan-act-verify".into(),
330 reason_codes: vec!["agent-spec-valid".into(), "tool-policy-authorized".into()],
331 };
332 let local_policy_uses_mock_fixture =
333 self.spec.provider_policy.provider == AgentProviderModeV1::Local;
334 let route_receipt = ToolRouteReceiptV1 {
335 receipt_id: display_only_unstable_id("tool-route"),
336 plan_id: plan.plan_id.clone(),
337 step: 1,
338 requested_tool_ids: mapped_tools.canonical.clone(),
339 exposed_tool_ids: mapped_tools.canonical.clone(),
340 reason_codes: if local_policy_uses_mock_fixture {
341 vec!["provider-policy-local-routed-to-explicit-mock-fixture".into()]
342 } else {
343 Vec::new()
344 },
345 };
346
347 let provider_kind = match self.spec.provider_policy.provider {
348 AgentProviderModeV1::Mock => "mock".to_string(),
349 AgentProviderModeV1::Local => "mock".to_string(),
350 AgentProviderModeV1::Ollama => {
351 if let Some(ref url) = self.provider_base_url {
353 if url.contains("opencode") || url.contains("/v1") {
354 "openai-compatible".to_string()
355 } else {
356 "ollama".to_string()
357 }
358 } else {
359 "ollama".to_string()
360 }
361 }
362 };
363 let provider_spec = ProviderSpecV1 {
364 kind: provider_kind,
365 model: self.provider_model.clone(),
366 api_key: self.provider_api_key.clone(),
367 base_url: self.provider_base_url.clone(),
368 mock_response: self.provider_mock_response.clone(),
369 };
370 if provider_spec.kind == "mock" && provider_spec.mock_response.as_deref().is_none() {
371 let mut output = PlanActVerifyLoopV1Output::abstention(
372 &self.spec,
373 1,
374 "provider-mock-response-missing",
375 );
376 output.memory_grounding_receipts = memory_grounding_receipts;
377 output.repair_plan = Some(RepairPlanDisplayReceiptV1 {
378 repair_id: display_only_unstable_id("agent-repair"),
379 source_run_id: None,
380 failure_kind: "provider-config".into(),
381 candidate_repair_actions: vec![
382 "configure a local provider mock response before execution".into(),
383 "validate local provider route and retry".into(),
384 ],
385 required_verification: vec!["provider-routing".into()],
386 required_permits: Vec::new(),
387 risk_level: "high".into(),
388 canonical_owner: Some("aidens-provider-kit".into()),
389 });
390 output.finalization = Some(FinalizationReceiptV1 {
391 receipt_id: display_only_unstable_id("agent-finalization"),
392 step: 1,
393 outcome: "abstained-setup".into(),
394 final_state: "provider-mock-missing".into(),
395 blocked: true,
396 support_label: self.spec.support_label.to_string(),
397 reason_codes: vec!["provider-mock-response-missing".into()],
398 });
399 return Ok(output);
400 }
401
402 let runner = AiDENsRunner::builder()
403 .app_id(self.app_id.clone())
404 .provider_spec(provider_spec)
405 .tools(self.tools.clone())
406 .permit_policy(self.permit_policy.clone())
407 .budget(BudgetV1 {
408 max_tool_calls: self.spec.budget_policy.max_tool_calls,
409 max_retries: self.budget_retries,
410 max_turn_millis: self
411 .spec
412 .budget_policy
413 .deadline_seconds
414 .saturating_mul(1000),
415 })
416 .run_reports(self.run_reports.clone())
417 .receipt_level(self.receipt_level.clone())
418 .agency_policy(self.agency_policy.clone())
419 .agency_nudge_ledger(self.agency_nudges.clone())
420 .governance(self.governance.clone())
421 .kernel(self.kernel)
422 .canonical_receipt_log_config(
423 self.canonical_receipt_log_config
424 .clone()
425 .unwrap_or_else(default_plan_act_verify_receipt_log_config),
426 )
427 .build()?;
428 let mut policy = ToolExposurePolicyV1::coding_agent_default();
429 if let Some(root) = self.tools.sandbox_root() {
430 policy = policy.with_sandbox_root(root.to_string_lossy().to_string());
431 } else if let Some(ref root) = self.sandbox_root {
432 policy = policy.with_sandbox_root(root.clone());
433 }
434 policy.allowed_tool_ids = Some(mapped_tools.canonical.iter().cloned().collect());
435
436 let mut output = PlanActVerifyLoopV1Output {
437 agent_id: self.spec.agent_id.clone(),
438 app_id: self.app_id.clone(),
439 profile: self.spec.profile.clone(),
440 support_label: self.spec.support_label.to_string(),
441 outcome: PlanActVerifyOutcomeV1::Failed,
442 turns_used: 0,
443 max_turns: self.spec.budget_policy.max_turns,
444 plan_receipts: vec![plan],
445 tool_route_receipts: vec![route_receipt],
446 tool_call_receipts: Vec::new(),
447 verification_receipts: Vec::new(),
448 memory_grounding_receipts,
449 abstention_receipt: None,
450 repair_plan: None,
451 finalization: None,
452 run_output: None,
453 };
454
455 let max_turns = self.spec.budget_policy.max_turns.max(1);
456 let run_input = AiDENsRunInput::new(prompt.clone());
457 for step in 1..=max_turns {
458 output.turns_used = step;
459 let turn_output = match runner
460 .run_with_tool_policy(run_input.clone(), policy.clone())
461 .await
462 {
463 Ok(run_output) => run_output,
464 Err(error) => {
465 output.outcome = PlanActVerifyOutcomeV1::Abstained;
466 output.abstention_receipt = Some(AbstentionReceiptV1 {
467 receipt_id: display_only_unstable_id("agent-abstention"),
468 step,
469 reason_code: format!("runner-execution-error:{error}"),
470 blocked_action: "provider/completion".into(),
471 evidence: vec![error.to_string()],
472 required_permits: Vec::new(),
473 can_resume: false,
474 support_impact: "degraded".into(),
475 });
476 output.repair_plan = Some(RepairPlanDisplayReceiptV1 {
477 repair_id: display_only_unstable_id("agent-repair"),
478 source_run_id: None,
479 failure_kind: "runner-execution".into(),
480 candidate_repair_actions: vec![
481 "collect provider and tool-chain evidence for the error".into(),
482 "retry with corrected local provider configuration".into(),
483 ],
484 required_verification: vec!["runner".into()],
485 required_permits: Vec::new(),
486 risk_level: "high".into(),
487 canonical_owner: Some("aidens-provider-kit".into()),
488 });
489 output.finalization = Some(FinalizationReceiptV1 {
490 receipt_id: display_only_unstable_id("agent-finalization"),
491 step,
492 outcome: "failed-runner-error".into(),
493 final_state: "failed".into(),
494 blocked: true,
495 support_label: self.spec.support_label.to_string(),
496 reason_codes: vec![error.to_string()],
497 });
498 return Ok(output);
499 }
500 };
501 let plan_id = output
502 .plan_receipts
503 .first()
504 .map(|p| p.plan_id.clone())
505 .unwrap_or_else(|| display_only_unstable_id("agent-plan"));
506 if let Some(ref memory) = self.memory {
507 match memory.search(&prompt, None, Some(5)).await {
508 Ok(results) => {
509 let grounding = MemoryGroundingEvidenceV1::canonical_seam(
510 "grounded-chat",
511 &prompt,
512 results.len(),
513 0,
514 "memory-grounding",
515 Vec::new(),
516 Vec::new(),
517 );
518 output.memory_grounding_receipts.push(
519 grounding.to_receipt_line().map_err(|error| {
520 anyhow!("memory-grounding-receipt-json:{error}")
521 })?,
522 );
523 }
524 Err(error) => {
525 let grounding = MemoryGroundingEvidenceV1::canonical_seam(
526 "grounded-chat",
527 &prompt,
528 0,
529 0,
530 "memory-grounding",
531 vec![format!("memory-search-failed:{error}")],
532 Vec::new(),
533 );
534 output.memory_grounding_receipts.push(
535 grounding.to_receipt_line().map_err(|error| {
536 anyhow!("memory-grounding-receipt-json:{error}")
537 })?,
538 );
539 }
540 }
541 }
542 output.run_output = Some(turn_output.clone());
543 output.tool_call_receipts.push(ToolCallReceiptV1 {
544 receipt_id: display_only_unstable_id("agent-tool-call"),
545 plan_id,
546 step,
547 run_id: turn_output.receipt.context.run_id.to_string(),
548 permitted_tool_calls: turn_output.receipt.permit_use_receipts.len(),
549 blocked_tool_calls: turn_output.receipt.approval_requests.len(),
550 succeeded_tool_calls: turn_output
551 .receipt
552 .tool_invocation_receipts
553 .iter()
554 .filter(|receipt| receipt.succeeded)
555 .count(),
556 failed_tool_calls: turn_output
557 .receipt
558 .tool_invocation_receipts
559 .iter()
560 .filter(|receipt| !receipt.succeeded)
561 .count(),
562 reason_codes: turn_output
563 .receipt
564 .tool_invocation_receipts
565 .iter()
566 .flat_map(|receipt| receipt.reason_codes.iter().cloned())
567 .collect(),
568 });
569
570 output
571 .verification_receipts
572 .extend(verification_checks_for_loop(
573 &self.spec,
574 &turn_output,
575 &mapped_tools.canonical,
576 ));
577 let failed_checks = output
578 .verification_receipts
579 .iter()
580 .any(|receipt| !receipt.passed);
581
582 if failed_checks && self.spec.verification_policy.fail_closed {
583 output.outcome = PlanActVerifyOutcomeV1::Abstained;
584 let failed_checks = failed_verification_checks(&output.verification_receipts);
585 output.abstention_receipt = Some(AbstentionReceiptV1 {
586 receipt_id: display_only_unstable_id("agent-abstention"),
587 step,
588 reason_code: "verification-failed".into(),
589 blocked_action: "execution".into(),
590 evidence: vec![turn_output.receipt.receipt_id.to_string()],
591 required_permits: Vec::new(),
592 can_resume: false,
593 support_impact: "degraded".into(),
594 });
595 output.repair_plan = Some(RepairPlanDisplayReceiptV1 {
596 repair_id: display_only_unstable_id("agent-repair"),
597 source_run_id: Some(turn_output.receipt.context.run_id.to_string()),
598 failure_kind: "verification-failed".into(),
599 candidate_repair_actions: vec![
600 "re-run with fixed verification-policy inputs".into(),
601 "remove or correct unsupported tool-call evidence".into(),
602 ],
603 required_verification: failed_checks,
604 required_permits: Vec::new(),
605 risk_level: "moderate".into(),
606 canonical_owner: Some("aidens-verification-kit".into()),
607 });
608 output.finalization = Some(FinalizationReceiptV1 {
609 receipt_id: display_only_unstable_id("agent-finalization"),
610 step,
611 outcome: "abstained-verification".into(),
612 final_state: format!("{:?}", turn_output.turn_receipt.final_state),
613 blocked: true,
614 support_label: self.spec.support_label.to_string(),
615 reason_codes: vec!["verification-failed".into()],
616 });
617 return Ok(output);
618 }
619
620 if turn_output.turn_receipt.final_state == TurnFinalStateV1::FinalOutput {
621 output.outcome = PlanActVerifyOutcomeV1::Success;
622 output.finalization = Some(FinalizationReceiptV1 {
623 receipt_id: display_only_unstable_id("agent-finalization"),
624 step,
625 outcome: "success".into(),
626 final_state: "FinalOutput".into(),
627 blocked: false,
628 support_label: self.spec.support_label.to_string(),
629 reason_codes: if local_policy_uses_mock_fixture {
630 vec!["provider-policy-local-routed-to-explicit-mock-fixture".into()]
631 } else {
632 Vec::new()
633 },
634 });
635 return Ok(output);
636 }
637
638 if turn_output.turn_receipt.blocked || turn_output.turn_receipt.degraded {
639 output.outcome = PlanActVerifyOutcomeV1::Abstained;
640 let stop_reasons = turn_output_block_reason_codes(&turn_output);
641 let blocked_action = blocked_turn_action(turn_output.turn_receipt.final_state);
642 output.abstention_receipt = Some(AbstentionReceiptV1 {
643 receipt_id: display_only_unstable_id("agent-abstention"),
644 step,
645 reason_code: format!("turn-blocked:{}", stop_reasons.join("|")),
646 blocked_action: blocked_action.into(),
647 evidence: {
648 let mut evidence = vec![turn_output.receipt.receipt_id.to_string()];
649 evidence.extend(stop_reasons);
650 evidence
651 },
652 required_permits: turn_output
653 .receipt
654 .approval_requests
655 .iter()
656 .map(|request| request.tool_id.clone())
657 .collect(),
658 can_resume: !turn_output.receipt.approval_requests.is_empty(),
659 support_impact: "deferred".into(),
660 });
661 output.repair_plan = Some(RepairPlanDisplayReceiptV1 {
662 repair_id: display_only_unstable_id("agent-repair"),
663 source_run_id: Some(turn_output.receipt.context.run_id.to_string()),
664 failure_kind: "blocked".into(),
665 candidate_repair_actions: vec![
666 "request permits for blocked tools".into(),
667 "inspect stop rule codes and adjust tool authority".into(),
668 "tighten tool policy to explicit writable tools".into(),
669 ],
670 required_verification: turn_output
671 .receipt
672 .stop_rule_receipts
673 .iter()
674 .flat_map(|receipt| receipt.reason_codes.iter().cloned())
675 .collect(),
676 required_permits: turn_output
677 .receipt
678 .approval_requests
679 .iter()
680 .map(|request| request.tool_id.clone())
681 .collect(),
682 risk_level: "moderate".into(),
683 canonical_owner: Some("aidens-tool-kit".into()),
684 });
685 output.finalization = Some(FinalizationReceiptV1 {
686 receipt_id: display_only_unstable_id("agent-finalization"),
687 step,
688 outcome: "abstained-blocked".into(),
689 final_state: format!("{:?}", turn_output.turn_receipt.final_state),
690 blocked: true,
691 support_label: self.spec.support_label.to_string(),
692 reason_codes: vec!["turn-blocked".into()],
693 });
694 return Ok(output);
695 }
696
697 if step >= max_turns {
698 output.outcome = PlanActVerifyOutcomeV1::Abstained;
699 output.abstention_receipt = Some(AbstentionReceiptV1 {
700 receipt_id: display_only_unstable_id("agent-abstention"),
701 step,
702 reason_code: "max-turns-exhausted".into(),
703 blocked_action: "repeat".into(),
704 evidence: vec![turn_output.receipt.receipt_id.to_string()],
705 required_permits: Vec::new(),
706 can_resume: true,
707 support_impact: "degraded".into(),
708 });
709 output.finalization = Some(FinalizationReceiptV1 {
710 receipt_id: display_only_unstable_id("agent-finalization"),
711 step,
712 outcome: "abstained-bounds-exhausted".into(),
713 final_state: format!("{:?}", turn_output.turn_receipt.final_state),
714 blocked: true,
715 support_label: self.spec.support_label.to_string(),
716 reason_codes: vec!["max-turns-exhausted".into()],
717 });
718 return Ok(output);
719 }
720 }
721
722 output.outcome = PlanActVerifyOutcomeV1::RepairNeeded;
723 output.finalization = Some(FinalizationReceiptV1 {
724 receipt_id: display_only_unstable_id("agent-finalization"),
725 step: output.turns_used,
726 outcome: "repair-needed".into(),
727 final_state: "repair".into(),
728 blocked: true,
729 support_label: self.spec.support_label.to_string(),
730 reason_codes: vec!["loop-exited-without-success".into()],
731 });
732 Ok(output)
733 }
734}
735
736#[derive(Debug, Clone)]
737pub struct AiDENsRunOutput {
738 pub text: String,
739 pub receipt: RunReportV1,
740 pub turn_receipt: TurnReportV1,
741 pub tool_exposure: ToolExposureSetV1,
742 pub agency_policy_reports: Vec<AgencyPolicyReportV1>,
743 pub memory_grounding_receipts: Vec<String>,
744 pub durable_receipt_records: Vec<CanonicalEventLogEntry>,
745}
746
747#[derive(Debug, Clone, Default)]
748pub struct RunReportLedger {
749 reports: Arc<Mutex<Vec<RunReportV1>>>,
750}
751
752impl RunReportLedger {
753 pub fn append(&self, report: RunReportV1) -> usize {
754 let mut reports = self.lock_reports();
755 reports.push(report);
756 reports.len()
757 }
758
759 pub fn list(&self) -> Vec<RunReportV1> {
760 self.lock_reports().clone()
761 }
762
763 pub fn len(&self) -> usize {
764 self.lock_reports().len()
765 }
766
767 pub fn is_empty(&self) -> bool {
768 self.len() == 0
769 }
770
771 fn lock_reports(&self) -> MutexGuard<'_, Vec<RunReportV1>> {
772 self.reports
773 .lock()
774 .unwrap_or_else(|poisoned| poisoned.into_inner())
775 }
776}
777
778pub fn disclose_runtime_view(
779 request: &RuntimeViewRequestV1,
780 projection_digest: ProjectionDigestV1,
781 matched_claim_ids: Vec<ArtifactId>,
782 query_widenings: &[QueryWideningReportV1],
783 degradations: &[DegradationEventV1],
784) -> ViewDisclosureReportV1 {
785 ViewDisclosureReportV1::new(
786 request,
787 projection_digest,
788 matched_claim_ids,
789 query_widenings
790 .iter()
791 .map(|receipt| receipt.receipt_id.clone())
792 .collect(),
793 degradations
794 .iter()
795 .map(|event| event.event_id.clone())
796 .collect(),
797 )
798}
799
800#[derive(Clone)]
801pub struct AiDENsRunner {
802 app_id: String,
803 provider: Arc<dyn AiDENsProvider>,
804 tools: ToolRegistryV1,
805 permit_policy: PermitPolicyV1,
806 budget: BudgetV1,
807 run_reports: RunReportLedger,
808 receipt_level: ReportLevelV1,
809 canonical_receipts: Option<CanonicalEventLog>,
810 agency_policy: AgencyPolicyEngineV1,
811 agency_nudges: Arc<Mutex<NudgeLedgerV1>>,
812 governance: Option<GovernanceContext>,
813 kernel: Option<CanonicalKernelAdapter>,
814}
815
816impl std::fmt::Debug for AiDENsRunner {
817 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
818 f.debug_struct("AiDENsRunner")
819 .field("app_id", &self.app_id)
820 .field("provider_kind", &self.provider.provider_kind())
821 .field("model", &self.provider.model())
822 .field("tool_count", &self.tools.len())
823 .field("permit_policy", &"scoped")
824 .field("budget", &self.budget)
825 .field("receipt_level", &self.receipt_level)
826 .field("agency_policy", &"enabled")
827 .field("governance", &self.governance.is_some())
828 .field("kernel", &self.kernel.is_some())
829 .field(
830 "durable_receipts",
831 &self
832 .canonical_receipts
833 .as_ref()
834 .map(|store| store.config().root_path.display().to_string()),
835 )
836 .finish()
837 }
838}
839
840impl AiDENsRunner {
841 pub fn builder() -> AiDENsRunnerBuilder {
842 AiDENsRunnerBuilder::default()
843 }
844
845 pub async fn run(&self, input: AiDENsRunInput) -> anyhow::Result<AiDENsRunOutput> {
846 self.run_with_tool_policy(input, ToolExposurePolicyV1::coding_agent_default())
847 .await
848 }
849
850 pub async fn run_with_tool_policy(
851 &self,
852 input: AiDENsRunInput,
853 tool_exposure_policy: ToolExposurePolicyV1,
854 ) -> anyhow::Result<AiDENsRunOutput> {
855 TurnExecutorV1::new(TurnExecutorConfigV1 {
856 app_id: self.app_id.clone(),
857 provider: self.provider.clone(),
858 tools: self.tools.clone(),
859 permit_policy: self.permit_policy.clone(),
860 budget: self.budget.clone(),
861 run_reports: self.run_reports.clone(),
862 receipt_level: self.receipt_level.clone(),
863 canonical_receipts: self.canonical_receipts.clone(),
864 agency_policy: self.agency_policy.clone(),
865 agency_nudges: self.agency_nudges.clone(),
866 governance: self.governance.clone(),
867 kernel: self.kernel,
868 })
869 .execute_with_tool_policy(input, tool_exposure_policy)
870 .await
871 }
872
873 pub fn run_reports(&self) -> RunReportLedger {
874 self.run_reports.clone()
875 }
876
877 pub fn receipt_level(&self) -> &ReportLevelV1 {
878 &self.receipt_level
879 }
880
881 pub fn canonical_receipt_log_config(&self) -> Option<&CanonicalEventLogConfig> {
882 self.canonical_receipts
883 .as_ref()
884 .map(CanonicalEventLog::config)
885 }
886
887 pub fn provider_route(&self) -> ProviderRouteReportV1 {
888 route_receipt(
889 self.provider.provider_kind(),
890 self.provider.model().map(str::to_string),
891 &self.provider.capabilities(),
892 )
893 }
894
895 pub fn tool_ids(&self) -> Vec<String> {
896 self.tools.tool_ids()
897 }
898
899 pub fn executable_tool_ids(&self) -> Vec<String> {
900 self.tools.executable_tool_ids()
901 }
902
903 pub fn tool_exposure(&self) -> ToolExposureSetV1 {
904 let provider_route = self.provider_route();
905 let tool_policy = ToolExposurePolicyV1::coding_agent_default()
906 .with_permit_policy(self.permit_policy.clone())
907 .for_provider_route(&provider_route);
908 self.tools.plan_exposure(&tool_policy)
909 }
910}
911
912#[derive(Clone)]
913pub struct TurnExecutorV1 {
914 app_id: String,
915 provider: Arc<dyn AiDENsProvider>,
916 tools: ToolRegistryV1,
917 permit_policy: PermitPolicyV1,
918 budget: BudgetV1,
919 run_reports: RunReportLedger,
920 receipt_level: ReportLevelV1,
921 canonical_receipts: Option<CanonicalEventLog>,
922 agency_policy: AgencyPolicyEngineV1,
923 agency_nudges: Arc<Mutex<NudgeLedgerV1>>,
924 governance: Option<GovernanceContext>,
925 kernel: Option<CanonicalKernelAdapter>,
926}
927
928#[derive(Clone)]
929pub struct TurnExecutorConfigV1 {
930 pub app_id: String,
931 pub provider: Arc<dyn AiDENsProvider>,
932 pub tools: ToolRegistryV1,
933 pub permit_policy: PermitPolicyV1,
934 pub budget: BudgetV1,
935 pub run_reports: RunReportLedger,
936 pub receipt_level: ReportLevelV1,
937 pub canonical_receipts: Option<CanonicalEventLog>,
938 pub agency_policy: AgencyPolicyEngineV1,
939 pub agency_nudges: Arc<Mutex<NudgeLedgerV1>>,
940 pub governance: Option<GovernanceContext>,
941 pub kernel: Option<CanonicalKernelAdapter>,
942}
943
944impl std::fmt::Debug for TurnExecutorV1 {
945 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
946 f.debug_struct("TurnExecutorV1")
947 .field("app_id", &self.app_id)
948 .field("provider_kind", &self.provider.provider_kind())
949 .field("model", &self.provider.model())
950 .field("tool_count", &self.tools.len())
951 .field("permit_policy", &"scoped")
952 .field("budget", &self.budget)
953 .field("receipt_level", &self.receipt_level)
954 .field("governance", &self.governance.is_some())
955 .field("kernel", &self.kernel.is_some())
956 .finish()
957 }
958}
959
960impl TurnExecutorV1 {
961 pub fn new(config: TurnExecutorConfigV1) -> Self {
962 Self {
963 app_id: config.app_id,
964 provider: config.provider,
965 tools: config.tools,
966 permit_policy: config.permit_policy,
967 budget: config.budget,
968 run_reports: config.run_reports,
969 receipt_level: config.receipt_level,
970 canonical_receipts: config.canonical_receipts,
971 agency_policy: config.agency_policy,
972 agency_nudges: config.agency_nudges,
973 governance: config.governance,
974 kernel: config.kernel,
975 }
976 }
977
978 pub async fn execute(&self, input: AiDENsRunInput) -> anyhow::Result<AiDENsRunOutput> {
979 self.execute_with_tool_policy(input, ToolExposurePolicyV1::coding_agent_default())
980 .await
981 }
982
983 pub async fn execute_with_tool_policy(
984 &self,
985 input: AiDENsRunInput,
986 mut tool_policy: ToolExposurePolicyV1,
987 ) -> anyhow::Result<AiDENsRunOutput> {
988 let ctx = AidensRunContextV1::new(&self.app_id);
989 let provider_route = route_receipt(
990 self.provider.provider_kind(),
991 self.provider.model().map(str::to_string),
992 &self.provider.capabilities(),
993 );
994 tool_policy = tool_policy
995 .with_permit_policy(self.permit_policy.clone())
996 .for_provider_route(&provider_route);
997 let tool_exposure = self.tools.plan_exposure(&tool_policy);
998 let mode = turn_mode_for(&provider_route, &tool_exposure);
999 let plan = TurnExecutionPlanV1::new(
1000 mode,
1001 provider_route.clone(),
1002 &tool_exposure,
1003 self.budget.max_tool_calls,
1004 self.budget.max_retries,
1005 self.budget.max_turn_millis,
1006 turn_plan_reasons(mode),
1007 );
1008
1009 let mut run_receipt = RunReportV1::started(ctx.clone());
1010 run_receipt.provider_route = Some(provider_route.clone());
1011 run_receipt
1012 .tool_exposure_ids
1013 .push(tool_exposure.exposure_id.clone());
1014 run_receipt
1015 .approval_requests
1016 .extend(tool_exposure.approval_requests.clone());
1017 run_receipt
1018 .permit_use_receipts
1019 .extend(tool_exposure.permit_use_receipts.clone());
1020 if provider_route.degraded {
1021 run_receipt.warnings.push(format!(
1022 "provider-route-degraded:{}",
1023 provider_route.route_label
1024 ));
1025 }
1026 for reason in &provider_route.reason_codes {
1027 if reason.contains("fallback") || reason.contains("unknown-native-provider") {
1028 run_receipt
1029 .warnings
1030 .push(format!("provider-route:{reason}"));
1031 }
1032 }
1033 for reason in &tool_exposure.reason_codes {
1034 run_receipt.warnings.push(format!("tool-exposure:{reason}"));
1035 }
1036
1037 let mut turn_receipt = TurnReportV1::started(&ctx, &plan);
1038 if mode == TurnModeV1::ProviderUnavailable {
1039 let stop = StopRuleReportV1::triggered(
1040 &ctx,
1041 StopRuleV1::ProviderUnavailable,
1042 provider_route.reason_codes.clone(),
1043 );
1044 turn_receipt.record_stop_rule(&stop);
1045 let completed_turn = turn_receipt.complete(TurnFinalStateV1::ProviderUnavailable);
1046 run_receipt.stop_rule_receipts.push(stop);
1047 run_receipt.turn_receipts.push(completed_turn);
1048 run_receipt.warnings.push("provider-unavailable".into());
1049 let completed = run_receipt.complete();
1050 let control_records = self.failure_control_records(
1051 &ctx,
1052 "provider-route",
1053 &provider_route.reason_codes,
1054 serde_json::json!({
1055 "provider_kind": provider_route.provider_kind.clone(),
1056 "route_label": provider_route.route_label.clone(),
1057 "route": provider_route.route,
1058 "degraded": provider_route.degraded,
1059 }),
1060 )?;
1061 self.persist_completed_with_records(completed, &tool_exposure, control_records)?;
1062 return Err(anyhow!(
1063 "provider unavailable: {}",
1064 provider_route.reason_codes.join(",")
1065 ));
1066 }
1067
1068 let mut tool_results = Vec::<ToolCallResultV1>::new();
1069 let mut current_turn_tool_calls = Vec::<ToolCallRequestV1>::new();
1070 let mut agency_policy_reports = Vec::<AgencyPolicyReportV1>::new();
1071 let mut tool_calls_so_far = 0u32;
1072 let mut retries = 0u32;
1073 let mut seen_tool_calls = BTreeSet::<String>::new();
1074 let started_at = Instant::now();
1075 let exposed_tool_ids = tool_exposure
1076 .exposed_tool_ids
1077 .iter()
1078 .cloned()
1079 .collect::<BTreeSet<_>>();
1080
1081 loop {
1082 seen_tool_calls.clear();
1085 let elapsed = elapsed_millis(started_at);
1086 if self.budget.deadline_exceeded(elapsed) {
1087 return self.finish_budget_exhausted(BudgetStopInput {
1088 ctx,
1089 run_receipt,
1090 turn_receipt,
1091 tool_exposure,
1092 attempted_tool_calls: tool_calls_so_far,
1093 retries,
1094 elapsed_millis: elapsed,
1095 reason: "turn-deadline-exceeded".into(),
1096 agency_policy_reports,
1097 });
1098 }
1099
1100 let request = match completion_request(
1101 input.prompt.clone(),
1102 &tool_exposure,
1103 &tool_results,
1104 ¤t_turn_tool_calls,
1105 ) {
1106 Ok(request) => request,
1107 Err(error) => {
1108 let reason_codes = vec!["tool-result-serialization-failed".into()];
1109 run_receipt
1110 .warnings
1111 .push(format!("provider-request-error:{error}"));
1112 let stop = StopRuleReportV1::triggered(
1113 &ctx,
1114 StopRuleV1::ToolInvocationFailed,
1115 reason_codes.clone(),
1116 );
1117 turn_receipt.record_stop_rule(&stop);
1118 turn_receipt.degraded = true;
1119 turn_receipt.blocked = true;
1120 run_receipt.stop_rule_receipts.push(stop);
1121 let completed_turn = turn_receipt.complete(TurnFinalStateV1::ToolFailed);
1122 run_receipt.turn_receipts.push(completed_turn);
1123 let completed = run_receipt.complete();
1124 let control_records = self.failure_control_records(
1125 &ctx,
1126 "provider-request",
1127 &reason_codes,
1128 serde_json::json!({
1129 "provider_kind": provider_route.provider_kind.clone(),
1130 "route_label": provider_route.route_label.clone(),
1131 "error": error.to_string(),
1132 }),
1133 )?;
1134 self.persist_completed_with_records(
1135 completed,
1136 &tool_exposure,
1137 control_records,
1138 )?;
1139 return Err(error);
1140 }
1141 };
1142 let completion = match self.provider.complete(request).await {
1143 Ok(completion) => completion,
1144 Err(error) if self.budget.allows_retry(retries) => {
1145 retries += 1;
1146 run_receipt.warnings.push(format!("provider-retry:{error}"));
1147 continue;
1148 }
1149 Err(error) => {
1150 run_receipt.warnings.push(format!("provider-error:{error}"));
1151 let stop = StopRuleReportV1::triggered(
1152 &ctx,
1153 StopRuleV1::MaxRetries,
1154 vec!["provider-error-max-retries".into()],
1155 );
1156 turn_receipt.record_stop_rule(&stop);
1157 let completed_turn = turn_receipt.complete(TurnFinalStateV1::StopRuleTriggered);
1158 run_receipt.stop_rule_receipts.push(stop);
1159 run_receipt.turn_receipts.push(completed_turn);
1160 let completed = run_receipt.complete();
1161 let control_records = self.failure_control_records(
1162 &ctx,
1163 "provider-error",
1164 &["provider-error-max-retries".into()],
1165 serde_json::json!({
1166 "provider_kind": provider_route.provider_kind.clone(),
1167 "route_label": provider_route.route_label.clone(),
1168 "error": error.to_string(),
1169 }),
1170 )?;
1171 self.persist_completed_with_records(
1172 completed,
1173 &tool_exposure,
1174 control_records,
1175 )?;
1176 return Err(error);
1177 }
1178 };
1179
1180 let completion_tool_calls = tool_calls_for_completion(&completion, mode);
1181 run_receipt
1182 .boundary_repair_receipts
1183 .extend(completion_tool_calls.boundary_repair_receipts);
1184 run_receipt
1185 .json_repair_receipts
1186 .extend(completion_tool_calls.json_repair_receipts);
1187 if !completion_tool_calls.degradation_reason_codes.is_empty() {
1188 let reason_codes = completion_tool_calls.degradation_reason_codes;
1189 let stop = StopRuleReportV1::triggered(
1190 &ctx,
1191 StopRuleV1::ToolInvocationFailed,
1192 reason_codes.clone(),
1193 );
1194 turn_receipt.record_stop_rule(&stop);
1195 turn_receipt.degraded = true;
1196 turn_receipt.blocked = true;
1197 run_receipt.stop_rule_receipts.push(stop);
1198 run_receipt.warnings.extend(
1199 reason_codes
1200 .iter()
1201 .map(|reason| format!("tool-call-degraded:{reason}")),
1202 );
1203 let completed_turn = turn_receipt.complete(TurnFinalStateV1::ToolFailed);
1204 run_receipt.turn_receipts.push(completed_turn.clone());
1205 let completed = run_receipt.complete();
1206 let control_records = self.failure_control_records(
1207 &ctx,
1208 "parser-fallback-tool-call",
1209 &reason_codes,
1210 serde_json::json!({
1211 "provider_kind": provider_route.provider_kind.clone(),
1212 "route_label": provider_route.route_label.clone(),
1213 "provider_text": completion.text.clone(),
1214 }),
1215 )?;
1216 let (completed, durable_receipt_records) = self.persist_completed_with_records(
1217 completed,
1218 &tool_exposure,
1219 control_records,
1220 )?;
1221 return Ok(AiDENsRunOutput {
1222 text: "Turn stopped: malformed parser-fallback tool call.".into(),
1223 receipt: completed,
1224 turn_receipt: completed_turn,
1225 tool_exposure,
1226 agency_policy_reports,
1227 memory_grounding_receipts: Vec::new(),
1228 durable_receipt_records,
1229 });
1230 }
1231 let tool_calls = completion_tool_calls.calls;
1232 current_turn_tool_calls.clear();
1234 current_turn_tool_calls.extend(tool_calls.iter().cloned());
1235
1236 if tool_calls.is_empty() {
1237 let tool_result_texts = tool_results
1238 .iter()
1239 .map(ToolCallResultV1::output_text)
1240 .filter(|text| !text.trim().is_empty())
1241 .collect::<Vec<_>>();
1242 let agency_input = AgencyPolicyInputV1::for_runner_final_output(
1243 input.prompt.clone(),
1244 completion.text.clone(),
1245 &tool_result_texts,
1246 );
1247 let agency_report = self.evaluate_agency_policy(&agency_input);
1248 run_receipt
1249 .agency_receipt_ids
1250 .extend(agency_report.receipt_ids());
1251 run_receipt.warnings.extend(
1252 agency_report
1253 .reason_codes()
1254 .iter()
1255 .map(|reason| format!("agency-policy:{reason}")),
1256 );
1257 let final_text = final_text_after_agency_gate(&completion.text, &agency_report);
1258 let agency_outcome = agency_report.outcome;
1259 agency_policy_reports.push(agency_report);
1260
1261 if !agency_outcome.allows_direct_output() {
1262 let reason_codes = vec![format!(
1263 "agency-policy:{}",
1264 agency_outcome.as_policy_label()
1265 )];
1266 let stop = StopRuleReportV1::triggered(
1267 &ctx,
1268 StopRuleV1::AgencyPolicy,
1269 reason_codes.clone(),
1270 );
1271 turn_receipt.record_stop_rule(&stop);
1272 turn_receipt.degraded = true;
1273 turn_receipt.blocked = true;
1274 run_receipt.stop_rule_receipts.push(stop);
1275 run_receipt.warnings.extend(reason_codes.clone());
1276 let completed_turn = turn_receipt.complete(TurnFinalStateV1::StopRuleTriggered);
1277 run_receipt.turn_receipts.push(completed_turn.clone());
1278 let completed = run_receipt.complete();
1279 let mut control_records = self.turn_control_records(
1280 &ctx,
1281 "agency-policy",
1282 &reason_codes,
1283 serde_json::json!({
1284 "control_decision": agency_outcome.as_policy_label(),
1285 "agency_reports": &agency_policy_reports,
1286 "provider_kind": provider_route.provider_kind.clone(),
1287 "route_label": provider_route.route_label.clone(),
1288 "turn_receipt_id": completed_turn.receipt_id.clone(),
1289 "turn_final_state": completed_turn.final_state,
1290 "tool_exposure_id": tool_exposure.exposure_id.clone(),
1291 }),
1292 true,
1293 governance_stack::VerificationAttemptState::Blocked,
1294 )?;
1295 control_records.extend(self.agency_policy_records(&agency_policy_reports)?);
1296 let (completed, durable_receipt_records) = self
1297 .persist_completed_with_records(
1298 completed,
1299 &tool_exposure,
1300 control_records,
1301 )?;
1302 return Ok(AiDENsRunOutput {
1303 text: final_text,
1304 receipt: completed,
1305 turn_receipt: completed_turn,
1306 tool_exposure,
1307 agency_policy_reports,
1308 memory_grounding_receipts: Vec::new(),
1309 durable_receipt_records,
1310 });
1311 }
1312
1313 let reason_codes = vec!["final-output-produced".into()];
1314 let stop = StopRuleReportV1::triggered(
1315 &ctx,
1316 StopRuleV1::FinalOutput,
1317 reason_codes.clone(),
1318 );
1319 turn_receipt.record_stop_rule(&stop);
1320 let completed_turn = turn_receipt.complete(TurnFinalStateV1::FinalOutput);
1321 run_receipt.stop_rule_receipts.push(stop);
1322 run_receipt.turn_receipts.push(completed_turn.clone());
1323 let completed = run_receipt.complete();
1324 let mut control_records = self.turn_control_records(
1325 &ctx,
1326 "final-output",
1327 &reason_codes,
1328 serde_json::json!({
1329 "control_decision": "final-output-produced",
1330 "provider_kind": provider_route.provider_kind.clone(),
1331 "route_label": provider_route.route_label.clone(),
1332 "turn_receipt_id": completed_turn.receipt_id.clone(),
1333 "turn_final_state": completed_turn.final_state,
1334 "tool_exposure_id": tool_exposure.exposure_id.clone(),
1335 "exposed_tool_ids": tool_exposure.exposed_tool_ids.clone(),
1336 }),
1337 completed_turn.degraded,
1338 governance_stack::VerificationAttemptState::AdvisoryOnly,
1339 )?;
1340 control_records.extend(self.agency_policy_records(&agency_policy_reports)?);
1341 let (completed, durable_receipt_records) = self.persist_completed_with_records(
1342 completed,
1343 &tool_exposure,
1344 control_records,
1345 )?;
1346 return Ok(AiDENsRunOutput {
1347 text: final_text,
1348 receipt: completed,
1349 turn_receipt: completed_turn,
1350 tool_exposure,
1351 agency_policy_reports,
1352 memory_grounding_receipts: Vec::new(),
1353 durable_receipt_records,
1354 });
1355 }
1356
1357 let requested_tool_calls = tool_calls.len() as u32;
1358 if !self
1359 .budget
1360 .allows_tool_call(tool_calls_so_far, requested_tool_calls)
1361 {
1362 for tool_call in tool_calls {
1363 let invocation = ToolInvocationReportV1::started(
1364 tool_call.tool_id.clone(),
1365 tool_call.input.clone(),
1366 )
1367 .with_execution_context(&ctx)
1368 .complete_failure("budget-exhausted-before-dispatch");
1369 let result = ToolCallResultV1::from_invocation(&tool_call, &invocation);
1370 turn_receipt.record_tool_call(&tool_call, &invocation);
1371 run_receipt.tool_call_requests.push(tool_call);
1372 run_receipt.tool_call_results.push(result);
1373 run_receipt.tool_invocation_receipts.push(invocation);
1374 }
1375 return self.finish_budget_exhausted(BudgetStopInput {
1376 ctx,
1377 run_receipt,
1378 turn_receipt,
1379 tool_exposure,
1380 attempted_tool_calls: tool_calls_so_far.saturating_add(requested_tool_calls),
1381 retries,
1382 elapsed_millis: elapsed_millis(started_at),
1383 reason: "max-tool-calls-exhausted".into(),
1384 agency_policy_reports,
1385 });
1386 }
1387
1388 let dispatcher = ToolDispatcher::new(self.tools.clone())
1391 .with_permit_policy(self.permit_policy.clone());
1392
1393 for tool_call in tool_calls {
1394 let call_signature = format!("{}:{}", tool_call.tool_id, tool_call.input_digest);
1395 if !seen_tool_calls.insert(call_signature) {
1396 let invocation = ToolInvocationReportV1::started(
1397 tool_call.tool_id.clone(),
1398 tool_call.input.clone(),
1399 )
1400 .with_execution_context(&ctx)
1401 .complete_failure("recursive-tool-call");
1402 let result = ToolCallResultV1::from_invocation(&tool_call, &invocation);
1403 turn_receipt.record_tool_call(&tool_call, &invocation);
1404 let stop = StopRuleReportV1::triggered(
1405 &ctx,
1406 StopRuleV1::RecursiveToolCall,
1407 vec!["recursive-tool-call".into()],
1408 );
1409 turn_receipt.record_stop_rule(&stop);
1410 return self.finish_blocked_turn(BlockedTurnInput {
1411 run_receipt,
1412 turn_receipt,
1413 tool_exposure,
1414 tool_call,
1415 invocation,
1416 result,
1417 stop_rule: stop,
1418 text: "Turn stopped: recursive tool call was blocked.".into(),
1419 final_state: TurnFinalStateV1::ToolBlocked,
1420 agency_policy_reports,
1421 durable_receipt_records: Vec::new(),
1422 });
1423 }
1424
1425 if !exposed_tool_ids.contains(&tool_call.tool_id) {
1426 let invocation = ToolInvocationReportV1::started(
1427 tool_call.tool_id.clone(),
1428 tool_call.input.clone(),
1429 )
1430 .with_execution_context(&ctx)
1431 .complete_failure("tool-not-exposed-this-turn");
1432 let result = ToolCallResultV1::from_invocation(&tool_call, &invocation);
1433 turn_receipt.record_tool_call(&tool_call, &invocation);
1434 let stop = StopRuleReportV1::triggered(
1435 &ctx,
1436 StopRuleV1::ToolNotExposed,
1437 vec!["tool-not-exposed-this-turn".into()],
1438 );
1439 turn_receipt.record_stop_rule(&stop);
1440 return self.finish_blocked_turn(BlockedTurnInput {
1441 run_receipt,
1442 turn_receipt,
1443 tool_exposure,
1444 tool_call,
1445 invocation,
1446 result,
1447 stop_rule: stop,
1448 text: "Turn stopped: provider requested a tool that was not exposed."
1449 .into(),
1450 final_state: TurnFinalStateV1::ToolBlocked,
1451 agency_policy_reports,
1452 durable_receipt_records: Vec::new(),
1453 });
1454 }
1455
1456 let governance_case_plan = self.governance_tool_case_plan(&ctx, &tool_call)?;
1457 if let Some((case, plan)) = &governance_case_plan {
1458 if let Some(governance) = &self.governance {
1459 if let Err(error) = governance.check_permit(case, plan) {
1460 let reason_codes = vec![format!("governance-permit-denied:{error}")];
1461 let invocation = ToolInvocationReportV1::started(
1462 tool_call.tool_id.clone(),
1463 tool_call.input.clone(),
1464 )
1465 .with_execution_context(&ctx)
1466 .complete_failure(reason_codes[0].clone());
1467 let result = ToolCallResultV1::from_invocation(&tool_call, &invocation);
1468 turn_receipt.record_tool_call(&tool_call, &invocation);
1469 let stop = StopRuleReportV1::triggered(
1470 &ctx,
1471 StopRuleV1::ToolInvocationFailed,
1472 reason_codes.clone(),
1473 );
1474 turn_receipt.record_stop_rule(&stop);
1475 let control_records = self.governance_tool_control_records(
1476 case,
1477 plan,
1478 governance_stack::VerificationAttemptState::Blocked,
1479 &reason_codes.join(","),
1480 serde_json::json!({
1481 "tool_call": &tool_call,
1482 "permit_error": error.to_string(),
1483 }),
1484 false,
1485 )?;
1486 return self.finish_blocked_turn(BlockedTurnInput {
1487 run_receipt,
1488 turn_receipt,
1489 tool_exposure,
1490 tool_call,
1491 invocation,
1492 result,
1493 stop_rule: stop,
1494 text: "Turn stopped: governance permit denied tool dispatch."
1495 .into(),
1496 final_state: TurnFinalStateV1::ToolBlocked,
1497 agency_policy_reports,
1498 durable_receipt_records: control_records,
1499 });
1500 }
1501 }
1502 }
1503
1504 let invocation = match dispatcher
1505 .invoke(&tool_call.tool_id, tool_call.input.clone())
1506 .await
1507 {
1508 Ok(outcome) => {
1509 if let Some(mut permit_use_receipt) = outcome.permit_use_receipt {
1510 permit_use_receipt.run_id = Some(ctx.run_id.clone());
1511 permit_use_receipt.attempt_id = Some(ctx.attempt_id.clone());
1512 run_receipt.permit_use_receipts.push(permit_use_receipt);
1513 }
1514 outcome.receipt.with_execution_context(&ctx)
1515 }
1516 Err(error) => {
1517 if let Some(invocation_error) = error.downcast_ref::<ToolInvocationError>()
1518 {
1519 if let Some(approval_request) = invocation_error.approval_request() {
1520 run_receipt.approval_requests.push(approval_request.clone());
1521 }
1522 if let Some(schema_validation) =
1523 invocation_error.schema_validation_receipt()
1524 {
1525 run_receipt
1526 .schema_validation_receipts
1527 .push(schema_validation.clone().with_execution_context(&ctx));
1528 }
1529 }
1530 tool_invocation_receipt_from_error(
1531 &tool_call.tool_id,
1532 tool_call.input.clone(),
1533 &error,
1534 &ctx,
1535 )
1536 }
1537 };
1538 let result = ToolCallResultV1::from_invocation(&tool_call, &invocation);
1539 if let Some((case, plan)) = &governance_case_plan {
1540 let state = if invocation.succeeded {
1541 governance_stack::VerificationAttemptState::Succeeded
1542 } else {
1543 governance_stack::VerificationAttemptState::Failed
1544 };
1545 let control_records = self.governance_tool_control_records(
1546 case,
1547 plan,
1548 state,
1549 if invocation.succeeded {
1550 "tool-dispatch-completed"
1551 } else {
1552 "tool-dispatch-failed"
1553 },
1554 serde_json::json!({
1555 "tool_invocation": &invocation,
1556 "tool_result": &result,
1557 }),
1558 false,
1559 )?;
1560 run_receipt.warnings.extend(
1561 control_records
1562 .iter()
1563 .map(|record| format!("governance-control:{}", record.receipt_id)),
1564 );
1565 }
1566 turn_receipt.record_tool_call(&tool_call, &invocation);
1567 run_receipt.tool_call_requests.push(tool_call.clone());
1568 run_receipt.tool_call_results.push(result.clone());
1569 run_receipt
1570 .tool_invocation_receipts
1571 .push(invocation.clone());
1572 tool_results.push(result.clone());
1573 tool_calls_so_far += 1;
1574
1575 if result.succeeded {
1576 let tool_output = result.output_text();
1577 if !tool_output.trim().is_empty() {
1578 let agency_input = AgencyPolicyInputV1::for_runner_tool_output(
1579 input.prompt.clone(),
1580 tool_output,
1581 );
1582 let agency_report = self.evaluate_agency_policy(&agency_input);
1583 let agency_outcome = agency_report.outcome;
1584 run_receipt
1585 .agency_receipt_ids
1586 .extend(agency_report.receipt_ids());
1587 run_receipt.warnings.extend(
1588 agency_report
1589 .reason_codes()
1590 .iter()
1591 .map(|reason| format!("agency-policy:{reason}")),
1592 );
1593 agency_policy_reports.push(agency_report);
1594 if !agency_outcome.allows_direct_output() {
1595 let stop = StopRuleReportV1::triggered(
1596 &ctx,
1597 StopRuleV1::AgencyPolicy,
1598 vec![format!(
1599 "agency-policy:{}",
1600 agency_outcome.as_policy_label()
1601 )],
1602 );
1603 turn_receipt.record_stop_rule(&stop);
1604 run_receipt.stop_rule_receipts.push(stop);
1605 turn_receipt.degraded = true;
1606 turn_receipt.blocked = true;
1607 let completed_turn =
1608 turn_receipt.complete(TurnFinalStateV1::StopRuleTriggered);
1609 run_receipt.turn_receipts.push(completed_turn.clone());
1610 let completed = run_receipt.complete();
1611 let mut control_records = self.failure_control_records(
1612 &ctx,
1613 "agency-tool-output-policy",
1614 &[format!(
1615 "agency-policy:{}",
1616 agency_outcome.as_policy_label()
1617 )],
1618 serde_json::json!({
1619 "agency_reports": &agency_policy_reports,
1620 "tool_result": result,
1621 }),
1622 )?;
1623 control_records
1624 .extend(self.agency_policy_records(&agency_policy_reports)?);
1625 let (completed, durable_receipt_records) = self
1626 .persist_completed_with_records(
1627 completed,
1628 &tool_exposure,
1629 control_records,
1630 )?;
1631 return Ok(AiDENsRunOutput {
1632 text: final_text_for_agency_block(agency_outcome),
1633 receipt: completed,
1634 turn_receipt: completed_turn,
1635 tool_exposure,
1636 agency_policy_reports,
1637 memory_grounding_receipts: Vec::new(),
1638 durable_receipt_records,
1639 });
1640 }
1641 }
1642 }
1643
1644 if !invocation.succeeded {
1645 let stop = StopRuleReportV1::triggered(
1646 &ctx,
1647 StopRuleV1::ToolInvocationFailed,
1648 invocation.reason_codes.clone(),
1649 );
1650 turn_receipt.record_stop_rule(&stop);
1651 run_receipt.stop_rule_receipts.push(stop);
1652 turn_receipt.degraded = true;
1653 let completed_turn = turn_receipt.complete(TurnFinalStateV1::ToolFailed);
1654 run_receipt.turn_receipts.push(completed_turn.clone());
1655 run_receipt.warnings.push("tool-invocation-failed".into());
1656 let completed = run_receipt.complete();
1657 let control_records = self.failure_control_records(
1658 &ctx,
1659 &invocation.tool_id,
1660 &invocation.reason_codes,
1661 serde_json::json!({
1662 "tool_invocation": invocation,
1663 "tool_result": result,
1664 }),
1665 )?;
1666 let (completed, durable_receipt_records) = self
1667 .persist_completed_with_records(
1668 completed,
1669 &tool_exposure,
1670 control_records,
1671 )?;
1672 return Ok(AiDENsRunOutput {
1673 text: "Turn stopped: tool invocation failed.".into(),
1674 receipt: completed,
1675 turn_receipt: completed_turn,
1676 tool_exposure,
1677 agency_policy_reports,
1678 memory_grounding_receipts: Vec::new(),
1679 durable_receipt_records,
1680 });
1681 }
1682 }
1683 }
1684 }
1685
1686 fn governance_tool_case_plan(
1687 &self,
1688 context: &AidensRunContextV1,
1689 tool_call: &ToolCallRequestV1,
1690 ) -> anyhow::Result<
1691 Option<(
1692 governance_stack::VerificationCase,
1693 governance_stack::CheckPlan,
1694 )>,
1695 > {
1696 let Some(governance) = &self.governance else {
1697 return Ok(None);
1698 };
1699 let case = governance.open_case(
1700 governance_stack::VerificationCaseClass::QueryTurn,
1701 self.app_id.clone(),
1702 format!("tool-dispatch:{}", tool_call.tool_id),
1703 context.stack_trace_ctx(),
1704 context.stack_attempt_id(),
1705 );
1706 let plan = CanonicalGovernanceAdapter.check_plan(
1707 &case,
1708 governance_stack::CheckMethod::CausalRefuter,
1709 vec!["tool-dispatch-permit".into()],
1710 governance_stack::PromotionClass::P3,
1711 governance_stack::ReversibilityClass::ReversibleLocal,
1712 true,
1713 false,
1714 false,
1715 "runner governance pre-dispatch permit check",
1716 serde_json::json!({
1717 "control_surface": "runner-tool-dispatch",
1718 "tool_id": &tool_call.tool_id,
1719 "input_digest": &tool_call.input_digest,
1720 }),
1721 );
1722 Ok(Some((case, plan)))
1723 }
1724
1725 fn governance_tool_control_records(
1726 &self,
1727 case: &governance_stack::VerificationCase,
1728 plan: &governance_stack::CheckPlan,
1729 state: governance_stack::VerificationAttemptState,
1730 outcome_signature: &str,
1731 details: serde_json::Value,
1732 promotable: bool,
1733 ) -> anyhow::Result<Vec<CanonicalEventLogEntry>> {
1734 let recorded_at = current_recorded_at_label()?;
1735 let adapter = CanonicalGovernanceAdapter;
1736 let attempt = adapter.completed_attempt(
1737 case,
1738 plan,
1739 state,
1740 recorded_at.clone(),
1741 recorded_at.clone(),
1742 Some(outcome_signature.to_string()),
1743 );
1744 let receipt =
1745 adapter.control_receipt_for_attempt(case, plan, &attempt, promotable, details);
1746 if let Some(store) = &self.canonical_receipts {
1747 return Ok(vec![store.append_control_receipt(&receipt)?]);
1748 }
1749 Ok(vec![CanonicalEventLogEntry::new(
1750 "verification-control",
1751 "in-memory-control-receipt",
1752 receipt.receipt_id.to_string(),
1753 serde_json::to_value(&receipt)?,
1754 )?])
1755 }
1756
1757 fn evaluate_agency_policy(&self, input: &AgencyPolicyInputV1) -> AgencyPolicyReportV1 {
1758 let (mut ledger, recovered_from_poison) = match self.agency_nudges.lock() {
1759 Ok(ledger) => (ledger, false),
1760 Err(poisoned) => (poisoned.into_inner(), true),
1761 };
1762 let mut report = self.agency_policy.evaluate(input, &mut ledger);
1763 if recovered_from_poison {
1764 report
1765 .decision
1766 .reason_codes
1767 .push("agency-nudge-ledger-poison-recovered".into());
1768 report.decision.reason_codes.sort();
1769 report.decision.reason_codes.dedup();
1770 report.receipts.decision.reason_codes = report.decision.reason_codes.clone();
1771 }
1772 report
1773 }
1774
1775 fn agency_policy_records(
1776 &self,
1777 reports: &[AgencyPolicyReportV1],
1778 ) -> anyhow::Result<Vec<CanonicalEventLogEntry>> {
1779 let Some(store) = &self.canonical_receipts else {
1780 return Ok(Vec::new());
1781 };
1782 let mut records = Vec::new();
1783 for report in reports {
1784 records.push(store.append_json(
1785 "aidens-agency-kit",
1786 "agency-policy-report-v1",
1787 report.report_id.clone(),
1788 serde_json::to_value(report)?,
1789 )?);
1790 }
1791 Ok(records)
1792 }
1793
1794 fn persist_completed_with_records(
1795 &self,
1796 completed: RunReportV1,
1797 tool_exposure: &ToolExposureSetV1,
1798 mut durable_receipt_records: Vec<CanonicalEventLogEntry>,
1799 ) -> anyhow::Result<(RunReportV1, Vec<CanonicalEventLogEntry>)> {
1800 self.run_reports.append(completed.clone());
1801 if let Some(store) = &self.canonical_receipts {
1802 let tool_exposure_record_id =
1803 format!("{}:{}", tool_exposure.exposure_id, completed.receipt_id);
1804 durable_receipt_records.push(store.append_orchestration_report(
1805 "tool-exposure-plan-v1",
1806 tool_exposure_record_id,
1807 serde_json::to_value(tool_exposure)?,
1808 )?);
1809 durable_receipt_records.push(store.append_orchestration_report(
1810 "run-report-v1",
1811 completed.receipt_id.to_string(),
1812 serde_json::to_value(&completed)?,
1813 )?);
1814 }
1815 Ok((completed, durable_receipt_records))
1816 }
1817
1818 fn turn_control_records(
1819 &self,
1820 context: &AidensRunContextV1,
1821 target_key: &str,
1822 reason_codes: &[String],
1823 details: serde_json::Value,
1824 degraded: bool,
1825 state: governance_stack::VerificationAttemptState,
1826 ) -> anyhow::Result<Vec<CanonicalEventLogEntry>> {
1827 let Some(store) = &self.canonical_receipts else {
1828 return Ok(Vec::new());
1829 };
1830 let adapter = CanonicalGovernanceAdapter;
1831 let recorded_at = context.started_at.to_rfc3339();
1832 let reason_summary = if reason_codes.is_empty() {
1833 "turn-control-decision".to_string()
1834 } else {
1835 reason_codes.join(",")
1836 };
1837 let receipt_details = serde_json::json!({
1838 "control_surface": "runner-turn",
1839 "reason_codes": reason_codes,
1840 "verification_attempt_state": state,
1841 "details": details,
1842 });
1843 let case = adapter.verification_case(
1844 governance_stack::VerificationCaseClass::QueryTurn,
1845 self.app_id.clone(),
1846 target_key.to_string(),
1847 context.stack_trace_ctx(),
1848 context.stack_attempt_id(),
1849 recorded_at.clone(),
1850 degraded,
1851 true,
1852 );
1853 let plan = adapter.check_plan(
1854 &case,
1855 governance_stack::CheckMethod::AdvisoryOnly,
1856 reason_codes.to_vec(),
1857 governance_stack::PromotionClass::P3,
1858 governance_stack::ReversibilityClass::ReversibleLocal,
1859 false,
1860 true,
1861 degraded,
1862 format!("runner turn control decision: {reason_summary}"),
1863 receipt_details.clone(),
1864 );
1865 let attempt = adapter.completed_attempt(
1866 &case,
1867 &plan,
1868 state,
1869 recorded_at.clone(),
1870 recorded_at,
1871 Some(reason_summary),
1872 );
1873 let receipt =
1874 adapter.control_receipt_for_attempt(&case, &plan, &attempt, false, receipt_details);
1875 Ok(vec![store.append_control_receipt(&receipt)?])
1876 }
1877
1878 fn failure_control_records(
1879 &self,
1880 context: &AidensRunContextV1,
1881 target_key: &str,
1882 reason_codes: &[String],
1883 details: serde_json::Value,
1884 ) -> anyhow::Result<Vec<CanonicalEventLogEntry>> {
1885 let Some(store) = &self.canonical_receipts else {
1886 let recorded_at = context.started_at.to_rfc3339();
1887 let reason_summary = if reason_codes.is_empty() {
1888 "failure-honesty-degraded".to_string()
1889 } else {
1890 reason_codes.join(",")
1891 };
1892 let non_durable = serde_json::json!({
1893 "durable": false,
1894 "phase": "04-failure-honesty",
1895 "reason_summary": reason_summary,
1896 "reason_codes": reason_codes,
1897 "details": details,
1898 "target_key": target_key,
1899 "app_id": self.app_id.clone(),
1900 });
1901 let receipt = CanonicalEventLogEntry::new(
1902 "aidens-runner",
1903 "in-memory-control-receipt",
1904 format!("failure-control:{target_key}:{recorded_at}"),
1905 non_durable,
1906 )?;
1907 return Ok(vec![receipt]);
1908 };
1909 let adapter = CanonicalGovernanceAdapter;
1910 let recorded_at = context.started_at.to_rfc3339();
1911 let reason_summary = if reason_codes.is_empty() {
1912 "failure-honesty-degraded".to_string()
1913 } else {
1914 reason_codes.join(",")
1915 };
1916 let receipt_details = serde_json::json!({
1917 "phase": "04-failure-honesty",
1918 "reason_codes": reason_codes,
1919 "details": details,
1920 });
1921 let case = adapter.verification_case(
1922 governance_stack::VerificationCaseClass::QueryTurn,
1923 self.app_id.clone(),
1924 target_key.to_string(),
1925 context.stack_trace_ctx(),
1926 context.stack_attempt_id(),
1927 recorded_at.clone(),
1928 true,
1929 true,
1930 );
1931 let plan = adapter.check_plan(
1932 &case,
1933 governance_stack::CheckMethod::AdvisoryOnly,
1934 reason_codes.to_vec(),
1935 governance_stack::PromotionClass::P3,
1936 governance_stack::ReversibilityClass::ReversibleLocal,
1937 false,
1938 true,
1939 true,
1940 format!("failure honesty degradation: {reason_summary}"),
1941 receipt_details.clone(),
1942 );
1943 let attempt = adapter.completed_attempt(
1944 &case,
1945 &plan,
1946 governance_stack::VerificationAttemptState::Blocked,
1947 recorded_at.clone(),
1948 recorded_at,
1949 Some(reason_summary),
1950 );
1951 let receipt =
1952 adapter.control_receipt_for_attempt(&case, &plan, &attempt, false, receipt_details);
1953 Ok(vec![store.append_control_receipt(&receipt)?])
1954 }
1955
1956 fn finish_budget_exhausted(&self, input: BudgetStopInput) -> anyhow::Result<AiDENsRunOutput> {
1957 let BudgetStopInput {
1958 ctx,
1959 mut run_receipt,
1960 mut turn_receipt,
1961 tool_exposure,
1962 attempted_tool_calls,
1963 retries,
1964 elapsed_millis,
1965 reason,
1966 agency_policy_reports,
1967 } = input;
1968 let budget_receipt = BudgetExhaustionReportV1::new(BudgetExhaustionReportDraftV1 {
1969 run_id: ctx.run_id.clone(),
1970 attempt_id: ctx.attempt_id.clone(),
1971 max_tool_calls: self.budget.max_tool_calls,
1972 attempted_tool_calls,
1973 max_retries: self.budget.max_retries,
1974 retries,
1975 max_turn_millis: self.budget.max_turn_millis,
1976 elapsed_millis,
1977 reason_codes: vec![reason.clone()],
1978 });
1979 let stop_rule = StopRuleReportV1::triggered(
1980 &ctx,
1981 if reason == "turn-deadline-exceeded" {
1982 StopRuleV1::DeadlineExceeded
1983 } else {
1984 StopRuleV1::MaxToolCalls
1985 },
1986 vec![reason.clone()],
1987 );
1988 turn_receipt.record_budget_exhaustion(&budget_receipt);
1989 turn_receipt.record_stop_rule(&stop_rule);
1990 let completed_turn = turn_receipt.complete(TurnFinalStateV1::BudgetExhausted);
1991 let control_records = self.failure_control_records(
1992 &ctx,
1993 "budget-exhaustion",
1994 std::slice::from_ref(&reason),
1995 serde_json::json!({
1996 "budget_exhaustion": budget_receipt.clone(),
1997 }),
1998 )?;
1999 run_receipt.budget_exhaustion_receipts.push(budget_receipt);
2000 run_receipt.stop_rule_receipts.push(stop_rule);
2001 run_receipt.turn_receipts.push(completed_turn.clone());
2002 run_receipt
2003 .warnings
2004 .push(format!("budget-exhausted:{reason}"));
2005 let completed = run_receipt.complete();
2006 let (completed, durable_receipt_records) =
2007 self.persist_completed_with_records(completed, &tool_exposure, control_records)?;
2008 Ok(AiDENsRunOutput {
2009 text: format!("Turn stopped: budget exhausted ({reason})."),
2010 receipt: completed,
2011 turn_receipt: completed_turn,
2012 tool_exposure,
2013 agency_policy_reports,
2014 memory_grounding_receipts: Vec::new(),
2015 durable_receipt_records,
2016 })
2017 }
2018
2019 fn finish_blocked_turn(&self, input: BlockedTurnInput) -> anyhow::Result<AiDENsRunOutput> {
2020 let BlockedTurnInput {
2021 mut run_receipt,
2022 mut turn_receipt,
2023 tool_exposure,
2024 tool_call,
2025 invocation,
2026 result,
2027 stop_rule,
2028 text,
2029 final_state,
2030 agency_policy_reports,
2031 durable_receipt_records,
2032 } = input;
2033 run_receipt.tool_call_requests.push(tool_call);
2034 run_receipt.tool_call_results.push(result);
2035 run_receipt.tool_invocation_receipts.push(invocation);
2036 run_receipt.stop_rule_receipts.push(stop_rule);
2037 turn_receipt.degraded = true;
2038 turn_receipt.blocked = true;
2039 let completed_turn = turn_receipt.complete(final_state);
2040 run_receipt.turn_receipts.push(completed_turn.clone());
2041 run_receipt.warnings.push("turn-blocked".into());
2042 let completed = run_receipt.complete();
2043 let (completed, durable_receipt_records) = self.persist_completed_with_records(
2044 completed,
2045 &tool_exposure,
2046 durable_receipt_records,
2047 )?;
2048 Ok(AiDENsRunOutput {
2049 text,
2050 receipt: completed,
2051 turn_receipt: completed_turn,
2052 tool_exposure,
2053 agency_policy_reports,
2054 memory_grounding_receipts: Vec::new(),
2055 durable_receipt_records,
2056 })
2057 }
2058}
2059
2060struct BudgetStopInput {
2061 ctx: AidensRunContextV1,
2062 run_receipt: RunReportV1,
2063 turn_receipt: TurnReportV1,
2064 tool_exposure: ToolExposureSetV1,
2065 attempted_tool_calls: u32,
2066 retries: u32,
2067 elapsed_millis: u64,
2068 reason: String,
2069 agency_policy_reports: Vec<AgencyPolicyReportV1>,
2070}
2071
2072struct BlockedTurnInput {
2073 run_receipt: RunReportV1,
2074 turn_receipt: TurnReportV1,
2075 tool_exposure: ToolExposureSetV1,
2076 tool_call: ToolCallRequestV1,
2077 invocation: ToolInvocationReportV1,
2078 result: ToolCallResultV1,
2079 stop_rule: StopRuleReportV1,
2080 text: String,
2081 final_state: TurnFinalStateV1,
2082 agency_policy_reports: Vec<AgencyPolicyReportV1>,
2083 durable_receipt_records: Vec<CanonicalEventLogEntry>,
2084}
2085
2086fn current_recorded_at_label() -> anyhow::Result<String> {
2087 let nanos = SystemTime::now()
2088 .duration_since(UNIX_EPOCH)
2089 .map_err(|error| anyhow!("recorded-at-clock-error:{error}"))?
2090 .as_nanos();
2091 Ok(format!("unix-nanos:{nanos}"))
2092}
2093
2094#[derive(Clone)]
2095pub struct AiDENsRunnerBuilder {
2096 app_id: String,
2097 provider: Option<Arc<dyn AiDENsProvider>>,
2098 provider_spec: ProviderSpecV1,
2099 tools: ToolRegistryV1,
2100 permit_policy: PermitPolicyV1,
2101 budget: BudgetV1,
2102 run_reports: RunReportLedger,
2103 receipt_level: ReportLevelV1,
2104 canonical_receipt_log_config: Option<CanonicalEventLogConfig>,
2105 agency_policy: AgencyPolicyEngineV1,
2106 agency_nudges: Arc<Mutex<NudgeLedgerV1>>,
2107 governance: Option<GovernanceContext>,
2108 kernel: Option<CanonicalKernelAdapter>,
2109}
2110
2111impl Default for AiDENsRunnerBuilder {
2112 fn default() -> Self {
2113 Self {
2114 app_id: "aidens-app".into(),
2115 provider: None,
2116 provider_spec: ProviderSpecV1::new("disabled"),
2117 tools: safe_coding_registry_for_current_dir(),
2118 permit_policy: PermitPolicyV1::default(),
2119 budget: BudgetV1::default(),
2120 run_reports: RunReportLedger::default(),
2121 receipt_level: ReportLevelV1::Full,
2122 canonical_receipt_log_config: None,
2123 agency_policy: AgencyPolicyEngineV1::default(),
2124 agency_nudges: Arc::new(Mutex::new(NudgeLedgerV1::default())),
2125 governance: None,
2126 kernel: None,
2127 }
2128 }
2129}
2130
2131impl AiDENsRunnerBuilder {
2132 pub fn app_id(mut self, app_id: impl Into<String>) -> Self {
2133 self.app_id = app_id.into();
2134 self
2135 }
2136
2137 pub fn provider(mut self, provider: Arc<dyn AiDENsProvider>) -> Self {
2138 self.provider = Some(provider);
2139 self
2140 }
2141
2142 pub fn provider_spec(mut self, spec: ProviderSpecV1) -> Self {
2143 self.provider_spec = spec;
2144 self.provider = None;
2145 self
2146 }
2147
2148 pub fn provider_kind(mut self, kind: impl Into<String>) -> Self {
2149 self.provider_spec.kind = kind.into();
2150 self.provider = None;
2151 self
2152 }
2153
2154 pub fn mock_provider(mut self, response: impl Into<String>) -> Self {
2155 self.provider_spec.kind = "mock".into();
2156 self.provider_spec.mock_response = Some(response.into());
2157 self.provider = None;
2158 self
2159 }
2160
2161 pub fn model(mut self, model: impl Into<String>) -> Self {
2162 self.provider_spec.model = Some(model.into());
2163 self.provider = None;
2164 self
2165 }
2166
2167 pub fn base_url(mut self, base_url: impl Into<String>) -> Self {
2168 self.provider_spec.base_url = Some(base_url.into());
2169 self.provider = None;
2170 self
2171 }
2172
2173 pub fn api_key(mut self, api_key: impl Into<String>) -> Self {
2174 self.provider_spec.api_key = Some(api_key.into());
2175 self.provider = None;
2176 self
2177 }
2178
2179 pub fn tools(mut self, tools: ToolRegistryV1) -> Self {
2180 self.tools = tools;
2181 self
2182 }
2183
2184 pub fn permit_policy(mut self, permit_policy: PermitPolicyV1) -> Self {
2185 self.permit_policy = permit_policy;
2186 self
2187 }
2188
2189 pub fn budget(mut self, budget: BudgetV1) -> Self {
2190 self.budget = budget;
2191 self
2192 }
2193
2194 pub fn run_reports(mut self, reports: RunReportLedger) -> Self {
2195 self.run_reports = reports;
2196 self
2197 }
2198
2199 pub fn receipt_level(mut self, receipt_level: ReportLevelV1) -> Self {
2200 self.receipt_level = receipt_level;
2201 self
2202 }
2203
2204 pub fn canonical_receipt_log_config(mut self, config: CanonicalEventLogConfig) -> Self {
2205 self.canonical_receipt_log_config = Some(config);
2206 self
2207 }
2208
2209 pub fn agency_policy(mut self, policy: AgencyPolicyEngineV1) -> Self {
2210 self.agency_policy = policy;
2211 self
2212 }
2213
2214 pub fn agency_nudge_ledger(mut self, ledger: Arc<Mutex<NudgeLedgerV1>>) -> Self {
2215 self.agency_nudges = ledger;
2216 self
2217 }
2218
2219 pub fn governance(mut self, governance: Option<GovernanceContext>) -> Self {
2220 self.governance = governance;
2221 self
2222 }
2223
2224 pub fn kernel(mut self, kernel: Option<CanonicalKernelAdapter>) -> Self {
2225 self.kernel = kernel;
2226 self
2227 }
2228
2229 pub fn build(self) -> anyhow::Result<AiDENsRunner> {
2230 let provider = match self.provider {
2231 Some(provider) => provider,
2232 None => build_provider(self.provider_spec)?,
2233 };
2234 let canonical_receipt_log_config = self
2235 .canonical_receipt_log_config
2236 .unwrap_or_else(|| default_runner_receipt_log_config(&self.app_id));
2237 let canonical_receipts = Some(CanonicalEventLog::open(canonical_receipt_log_config)?);
2238 Ok(AiDENsRunner {
2239 app_id: self.app_id,
2240 provider,
2241 tools: self.tools,
2242 permit_policy: self.permit_policy,
2243 budget: self.budget,
2244 run_reports: self.run_reports,
2245 receipt_level: self.receipt_level,
2246 canonical_receipts,
2247 agency_policy: self.agency_policy,
2248 agency_nudges: self.agency_nudges,
2249 governance: self.governance,
2250 kernel: self.kernel,
2251 })
2252 }
2253}
2254
2255#[cfg(test)]
2256mod tests;