lean_ctx/core/
decision_loop_runtime.rs1use std::{
4 panic::{AssertUnwindSafe, catch_unwind},
5 sync::OnceLock,
6};
7
8use crate::core::execution_lifecycle::{
9 CompletionObservation, ExecutionLifecycle, ProductEntitlements, RuntimeContext, ToolRequest,
10 ToolSurface,
11};
12
13#[cfg(any(test, feature = "test-support"))]
14use crate::core::triage::TriageEngine;
15
16pub use crate::core::execution_lifecycle::TaskContext;
17
18#[derive(Debug)]
19pub struct DecisionLoopRuntime {
21 lifecycle: ExecutionLifecycle,
22}
23
24impl DecisionLoopRuntime {
25 pub fn get_or_init() -> &'static Self {
26 static RUNTIME: OnceLock<DecisionLoopRuntime> = OnceLock::new();
27 RUNTIME.get_or_init(|| Self {
28 lifecycle: ExecutionLifecycle::global().clone(),
29 })
30 }
31
32 pub fn profile_for_session(
34 &self,
35 session_id: &str,
36 ) -> Option<crate::core::triage::profile::TaskProfileLocal> {
37 self.lifecycle.profile_for_session(session_id)
38 }
39
40 pub fn on_tool_start(
41 &self,
42 tool_name: &str,
43 query: Option<&str>,
44 session_id: &str,
45 agent_id: &str,
46 ) -> TaskContext {
47 catch_unwind(AssertUnwindSafe(|| {
48 self.lifecycle.begin(
49 ToolRequest {
50 tool_name: tool_name.to_owned(),
51 query: query.map(str::to_owned),
52 session_id: session_id.to_owned(),
53 agent_id: agent_id.to_owned(),
54 surface: ToolSurface::Mcp,
55 idempotency_key: None,
56 },
57 RuntimeContext::default(),
58 ProductEntitlements::default(),
59 )
60 }))
61 .unwrap_or_else(|_| {
62 self.lifecycle.begin(
63 ToolRequest {
64 tool_name: tool_name.to_owned(),
65 query: None,
66 session_id: session_id.to_owned(),
67 agent_id: agent_id.to_owned(),
68 surface: ToolSurface::Mcp,
69 idempotency_key: None,
70 },
71 RuntimeContext::default(),
72 ProductEntitlements::default(),
73 )
74 })
75 }
76
77 pub fn on_tool_end(
78 &self,
79 ctx: &TaskContext,
80 input_tokens: u64,
81 output_tokens: u64,
82 model: &str,
83 success: bool,
84 ) -> Option<crate::core::value_gate::ValueAssessment> {
85 if ctx.outcome().is_some() {
86 return None;
87 }
88 catch_unwind(AssertUnwindSafe(|| {
89 self.lifecycle
90 .complete_once(
91 ctx,
92 CompletionObservation::tool_result(input_tokens, output_tokens, model, success),
93 )
94 .and_then(|outcome| outcome.assessment)
95 }))
96 .ok()
97 .flatten()
98 }
99
100 #[cfg(any(test, feature = "test-support"))]
101 pub fn with_triage(triage: TriageEngine) -> Self {
102 Self {
103 lifecycle: ExecutionLifecycle::with_triage(triage),
104 }
105 }
106
107 #[cfg(any(test, feature = "test-support"))]
108 pub fn assessment_for(
109 &self,
110 task_id: &str,
111 ) -> Option<crate::core::value_gate::ValueAssessment> {
112 self.lifecycle.assessment_for(task_id)
113 }
114
115 #[cfg(any(test, feature = "test-support"))]
116 pub fn outcome_for(
117 &self,
118 task_id: &str,
119 ) -> Option<crate::core::execution_lifecycle::LifecycleOutcome> {
120 self.lifecycle.outcome_for(task_id)
121 }
122}