Skip to main content

aidens_runner/
lib.rs

1//! Reusable run/turn executor. Coordinates but does not own domain truth.
2
3use 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 base_url points to an opencode/compatible endpoint, use openai-compatible transport
352                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            // Clear seen_tool_calls at the start of each turn so legitimate
1083            // repeated calls across turns aren't blocked as "recursive".
1084            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                &current_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            // Track only this turn's tool calls for the next completion_request
1233            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            // Hoist dispatcher construction outside the per-tool-call loop
1389            // to avoid cloning the entire ToolRegistryV1 on every tool invocation.
1390            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;