1use std::{
5 any::Any,
6 cell::RefCell,
7 collections::HashMap,
8 fmt,
9 sync::{Arc, Condvar, Mutex, OnceLock},
10 time::Instant,
11};
12
13tokio::task_local! {
14 static DEFERRED_HEATMAP: RefCell<Vec<HeatmapObservation>>;
15}
16
17thread_local! {
18 static SYNC_DEFERRED_HEATMAP: RefCell<Option<Vec<HeatmapObservation>>> = const {
19 RefCell::new(None)
20 };
21}
22
23use lean_ctx_protocol::{AcceptanceState, AcceptedOutcomeV1, TaskEnvelopeV1};
24
25use crate::core::{
26 outcome::{
27 contracts::{OutcomeContractV1, TaskClass},
28 evaluator::OutcomeEvaluator,
29 signals::{LocalSignalAdapters, OutcomeSignal as CanonicalSignal},
30 },
31 task_spine::TaskSpine,
32 triage::{TriageEngine, profile::TaskProfileLocal},
33 value_gate::{
34 ExecutionCost, OutcomeSignal, TaskOutcome, ValueAssessment, ValueGate,
35 cost_tracker::calculate_cost,
36 },
37};
38
39#[derive(Debug, Clone, Copy, PartialEq, Eq)]
41pub enum LifecycleStage {
42 IdentifyCaller,
43 ResolveWorkspace,
44 ResolveEntitlements,
45 ApplySecurityBoundaries,
46 GatherContextStrategy,
47 AskAutopilot,
48 DispatchPrimitive,
49 ReversiblePostProcess,
50 RecordContextIr,
51 RecordLedger,
52 RecordEvidence,
53 RecordOutcome,
54 UpdateLearning,
55 UpdateHeatmap,
56 UpdateBreakEven,
57 Checkpoint,
58 ScheduleTelemetry,
59 FlushState,
60}
61
62pub const LIFECYCLE_STAGE_ORDER: [LifecycleStage; 18] = [
63 LifecycleStage::IdentifyCaller,
64 LifecycleStage::ResolveWorkspace,
65 LifecycleStage::ResolveEntitlements,
66 LifecycleStage::ApplySecurityBoundaries,
67 LifecycleStage::GatherContextStrategy,
68 LifecycleStage::AskAutopilot,
69 LifecycleStage::DispatchPrimitive,
70 LifecycleStage::ReversiblePostProcess,
71 LifecycleStage::RecordContextIr,
72 LifecycleStage::RecordLedger,
73 LifecycleStage::RecordEvidence,
74 LifecycleStage::RecordOutcome,
75 LifecycleStage::UpdateLearning,
76 LifecycleStage::UpdateHeatmap,
77 LifecycleStage::UpdateBreakEven,
78 LifecycleStage::Checkpoint,
79 LifecycleStage::ScheduleTelemetry,
80 LifecycleStage::FlushState,
81];
82
83#[derive(Debug, Clone, Copy, PartialEq, Eq)]
84pub enum StageDisposition {
85 Applied,
86 Skipped(&'static str),
87}
88
89#[derive(Debug, Clone, Copy, PartialEq, Eq)]
90pub struct StageExecution {
91 pub stage: LifecycleStage,
92 pub disposition: StageDisposition,
93}
94
95#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
96pub enum ToolSurface {
97 Mcp,
98 Cli,
99 Daemon,
100 Hook,
101 Proxy,
102}
103
104#[derive(Debug, Clone)]
105pub struct ToolRequest {
106 pub tool_name: String,
107 pub query: Option<String>,
108 pub session_id: String,
109 pub agent_id: String,
110 pub surface: ToolSurface,
111 pub idempotency_key: Option<String>,
112}
113
114#[derive(Debug, Clone, Default)]
115pub struct RuntimeContext {
116 pub client_name: Option<String>,
117 pub project_root: Option<String>,
118}
119
120#[derive(Debug, Clone, Copy, Default)]
121pub struct ProductEntitlements {
122 pub autopilot: bool,
123 pub personalized_learning: bool,
124}
125
126#[derive(Debug, Clone)]
127pub struct CompletionObservation {
128 pub input_tokens: u64,
129 pub output_tokens: u64,
130 pub model: String,
131 pub provider: String,
132 pub success: bool,
133 pub outcome_signals: Vec<OutcomeSignal>,
135 pub proxy_economics: Option<ProxyEconomicsObservation>,
136 pub heatmap: Option<HeatmapObservation>,
137}
138
139#[derive(Debug, Clone)]
140pub struct ProxyEconomicsObservation {
141 pub tokens_pruned: usize,
142 pub original_tokens: usize,
143 pub task_class: String,
144}
145
146#[derive(Debug, Clone)]
147pub struct HeatmapObservation {
148 pub path: String,
149 pub original_tokens: usize,
150 pub saved_tokens: usize,
151}
152
153pub fn record_heatmap_access(path: &str, original_tokens: usize, saved_tokens: usize) {
155 let observation = HeatmapObservation {
156 path: path.to_owned(),
157 original_tokens,
158 saved_tokens,
159 };
160 if DEFERRED_HEATMAP
161 .try_with(|pending| pending.borrow_mut().push(observation.clone()))
162 .is_ok()
163 {
164 return;
165 }
166 let deferred = SYNC_DEFERRED_HEATMAP.with(|pending| {
167 let mut pending = pending.borrow_mut();
168 pending.as_mut().is_some_and(|items| {
169 items.push(observation);
170 true
171 })
172 });
173 if !deferred {
174 crate::core::heatmap::record_file_access(path, original_tokens, saved_tokens);
175 }
176}
177
178pub fn begin_sync_heatmap_capture() {
179 SYNC_DEFERRED_HEATMAP.with(|pending| *pending.borrow_mut() = Some(Vec::new()));
180}
181
182fn take_deferred_heatmap() -> Vec<HeatmapObservation> {
183 DEFERRED_HEATMAP
184 .try_with(|pending| std::mem::take(&mut *pending.borrow_mut()))
185 .unwrap_or_else(|_| {
186 SYNC_DEFERRED_HEATMAP.with(|pending| pending.borrow_mut().take().unwrap_or_default())
187 })
188}
189
190pub struct CancellationCompletion {
192 pub observation: CompletionObservation,
193 pub ledger: StageDisposition,
194}
195
196pub type CancellationHandler = Box<dyn FnOnce() -> CancellationCompletion + Send>;
197
198#[async_trait::async_trait]
199pub trait ExecutionDriver: Send {
200 type Primitive: Send;
201 type Processed: Send;
202 type Output: Clone + Send + Sync + 'static;
203 type Error: Clone + Send + Sync + 'static;
204
205 fn cancellation_handler(&self) -> Option<CancellationHandler> {
208 None
209 }
210
211 async fn identify_caller(
212 &mut self,
213 _context: &TaskContext,
214 ) -> Result<StageDisposition, Self::Error> {
215 Ok(StageDisposition::Skipped("caller identity unavailable"))
216 }
217
218 async fn resolve_workspace(
219 &mut self,
220 _context: &TaskContext,
221 ) -> Result<StageDisposition, Self::Error> {
222 Ok(StageDisposition::Skipped("workspace unavailable"))
223 }
224
225 async fn resolve_entitlements(
226 &mut self,
227 _context: &TaskContext,
228 ) -> Result<StageDisposition, Self::Error> {
229 Ok(StageDisposition::Skipped("no product entitlement source"))
230 }
231
232 async fn apply_security_boundaries(
233 &mut self,
234 _context: &TaskContext,
235 ) -> Result<StageDisposition, Self::Error> {
236 Ok(StageDisposition::Skipped("no surface security adapter"))
237 }
238
239 async fn gather_context_strategy(
240 &mut self,
241 _context: &TaskContext,
242 ) -> Result<StageDisposition, Self::Error> {
243 Ok(StageDisposition::Skipped("no context strategy"))
244 }
245
246 async fn ask_autopilot(
247 &mut self,
248 context: &TaskContext,
249 ) -> Result<StageDisposition, Self::Error> {
250 Ok(if context.entitlements.autopilot {
251 StageDisposition::Skipped("no canonical autopilot decision")
253 } else {
254 StageDisposition::Skipped("not entitled")
255 })
256 }
257
258 async fn validate_cached_replay(
260 &mut self,
261 _context: &TaskContext,
262 ) -> Result<Option<Self::Output>, Self::Error> {
263 Ok(None)
264 }
265
266 async fn dispatch_primitive(
267 &mut self,
268 context: &TaskContext,
269 ) -> Result<(Self::Primitive, StageDisposition), Self::Error>;
270
271 async fn reversible_post_process(
272 &mut self,
273 context: &TaskContext,
274 primitive: Self::Primitive,
275 ) -> Result<(Self::Processed, StageDisposition), Self::Error>;
276
277 async fn record_context_ir(
278 &mut self,
279 context: &TaskContext,
280 processed: &Self::Processed,
281 ) -> Result<StageDisposition, Self::Error>;
282
283 async fn record_ledger(
284 &mut self,
285 context: &TaskContext,
286 processed: &Self::Processed,
287 ) -> Result<StageDisposition, Self::Error>;
288
289 async fn checkpoint(
290 &mut self,
291 _context: &TaskContext,
292 _output: &mut Self::Output,
293 ) -> Result<StageDisposition, Self::Error> {
294 Ok(StageDisposition::Skipped("checkpoint not configured"))
295 }
296
297 fn output_from_processed(&mut self, processed: Self::Processed) -> Self::Output;
298
299 fn observe(
300 &self,
301 result: &Result<Self::Output, LifecycleRunError<Self::Error>>,
302 ) -> CompletionObservation;
303}
304
305impl CompletionObservation {
306 pub fn tool_result(input_tokens: u64, output_tokens: u64, model: &str, success: bool) -> Self {
307 Self {
308 input_tokens,
309 output_tokens,
310 model: model.to_owned(),
311 provider: "mcp".to_owned(),
312 success,
313 outcome_signals: Vec::new(),
315 proxy_economics: None,
316 heatmap: None,
317 }
318 }
319}
320
321#[derive(Debug, Clone)]
322pub struct LifecycleOutcome {
323 pub accepted_outcome: AcceptedOutcomeV1,
324 pub assessment: Option<ValueAssessment>,
325}
326
327#[derive(Debug, Clone)]
328enum RunState {
329 Open,
330 Completing,
331 Completed(Box<LifecycleOutcome>),
332}
333
334#[cfg(test)]
335type FinalizationHook = Arc<dyn Fn(LifecycleStage, Option<&LifecycleOutcome>) + Send + Sync>;
336
337#[cfg(test)]
338fn test_finalization_point(
339 context: &TaskContext,
340 stage: LifecycleStage,
341 outcome: Option<&LifecycleOutcome>,
342) {
343 let hook = context
344 .shared
345 .finalization_hook
346 .lock()
347 .unwrap_or_else(std::sync::PoisonError::into_inner)
348 .clone();
349 if let Some(hook) = hook {
350 hook(stage, outcome);
351 }
352}
353
354struct SharedRun {
355 state: Mutex<RunState>,
356 completed: Condvar,
357 dispatch: Mutex<DispatchState>,
358 dispatch_completed: tokio::sync::Notify,
359 stages: Mutex<Vec<StageExecution>>,
360 computed_outcome: OnceLock<LifecycleOutcome>,
361 telemetry_started: std::sync::atomic::AtomicBool,
362 flush_started: std::sync::atomic::AtomicBool,
363 #[cfg(test)]
364 flush_hook: Mutex<Option<Arc<dyn Fn() + Send + Sync>>>,
365 #[cfg(test)]
366 finalization_hook: Mutex<Option<FinalizationHook>>,
367 autopilot: OnceLock<Arc<crate::core::context_kernel::bridge::runtime::PreparedKernelContext>>,
368 #[cfg(any(test, feature = "test-support"))]
369 autopilot_planning_attempts: std::sync::atomic::AtomicUsize,
370}
371
372enum DispatchState {
373 Idle,
374 Running,
375 Finished(Arc<dyn Any + Send + Sync>),
376 Aborted(Box<LifecycleOutcome>),
377}
378
379#[derive(Debug, Clone)]
380pub enum LifecycleRunError<E> {
381 Dispatch(E),
382 Aborted(Box<LifecycleOutcome>),
383 ReplayTypeMismatch,
385}
386
387impl fmt::Debug for SharedRun {
388 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
389 formatter.debug_struct("SharedRun").finish_non_exhaustive()
390 }
391}
392
393impl Default for SharedRun {
394 fn default() -> Self {
395 Self {
396 state: Mutex::new(RunState::Open),
397 completed: Condvar::new(),
398 dispatch: Mutex::new(DispatchState::Idle),
399 dispatch_completed: tokio::sync::Notify::new(),
400 stages: Mutex::new(Vec::new()),
401 computed_outcome: OnceLock::new(),
402 telemetry_started: std::sync::atomic::AtomicBool::new(false),
403 flush_started: std::sync::atomic::AtomicBool::new(false),
404 #[cfg(test)]
405 flush_hook: Mutex::new(None),
406 #[cfg(test)]
407 finalization_hook: Mutex::new(None),
408 autopilot: OnceLock::new(),
409 #[cfg(any(test, feature = "test-support"))]
410 autopilot_planning_attempts: std::sync::atomic::AtomicUsize::new(0),
411 }
412 }
413}
414
415#[derive(Debug, Clone)]
417pub struct TaskContext {
418 pub task_id: String,
419 pub session_id: String,
420 pub triage_class: String,
421 pub profile_intent: String,
422 pub profile_complexity: String,
423 pub filtered_lines: usize,
424 pub start_time: Instant,
425 pub envelope: TaskEnvelopeV1,
426 pub profile: TaskProfileLocal,
427 pub surface: ToolSurface,
428 pub runtime: RuntimeContext,
429 pub entitlements: ProductEntitlements,
430 planning_query: Option<String>,
431 shared: Arc<SharedRun>,
432}
433
434#[path = "execution_lifecycle_state.rs"]
435mod task_state;
436
437#[derive(Debug)]
438struct DecisionSpine {
439 triage: TriageEngine,
440 value_gate: ValueGate,
441 task_profiles: Mutex<HashMap<String, TaskProfileLocal>>,
442 runs: Mutex<HashMap<task_state::TaskReplayKey, TaskContext>>,
443 assessments: Mutex<HashMap<String, ValueAssessment>>,
444 outcomes: Mutex<HashMap<String, LifecycleOutcome>>,
445}
446
447#[derive(Debug, Clone)]
449pub struct ExecutionLifecycle {
450 spine: Arc<DecisionSpine>,
451}
452
453impl Default for ExecutionLifecycle {
454 fn default() -> Self {
455 Self::with_triage(TriageEngine::default())
456 }
457}
458
459impl ExecutionLifecycle {
460 pub fn global() -> &'static Self {
461 static LIFECYCLE: std::sync::OnceLock<ExecutionLifecycle> = std::sync::OnceLock::new();
462 LIFECYCLE.get_or_init(Self::default)
463 }
464
465 pub fn with_triage(triage: TriageEngine) -> Self {
466 Self::with_components(triage, ValueGate)
467 }
468
469 pub fn with_components(triage: TriageEngine, value_gate: ValueGate) -> Self {
470 Self {
471 spine: Arc::new(DecisionSpine::new(triage, value_gate)),
472 }
473 }
474
475 pub fn begin(
476 &self,
477 request: ToolRequest,
478 runtime: RuntimeContext,
479 entitlements: ProductEntitlements,
480 ) -> TaskContext {
481 self.spine.begin(request, runtime, entitlements, None)
482 }
483
484 fn prepare(&self, context: &TaskContext) {
485 let _ = context.advance(LifecycleStage::IdentifyCaller);
486 let _ = context.advance(LifecycleStage::ResolveWorkspace);
487 let _ = context.advance(LifecycleStage::ResolveEntitlements);
488 let _ = context.advance(LifecycleStage::ApplySecurityBoundaries);
489 let _ = context.advance(LifecycleStage::GatherContextStrategy);
490 if context.entitlements.autopilot {
491 let _ = context.skip(
492 LifecycleStage::AskAutopilot,
493 "no canonical autopilot decision",
494 );
495 } else {
496 let _ = context.skip(LifecycleStage::AskAutopilot, "not entitled");
497 }
498 }
499
500 pub async fn run<D>(
501 &self,
502 request: ToolRequest,
503 runtime: RuntimeContext,
504 entitlements: ProductEntitlements,
505 driver: D,
506 ) -> Result<D::Output, LifecycleRunError<D::Error>>
507 where
508 D: ExecutionDriver,
509 {
510 self.run_with_task_identity(request, runtime, entitlements, driver, None)
511 .await
512 }
513
514 pub async fn run_with_task_identity<D>(
516 &self,
517 request: ToolRequest,
518 runtime: RuntimeContext,
519 entitlements: ProductEntitlements,
520 driver: D,
521 identity: Option<crate::core::task_spine::AdmittedTaskIdentity>,
522 ) -> Result<D::Output, LifecycleRunError<D::Error>>
523 where
524 D: ExecutionDriver,
525 {
526 TaskSpine::scope(
528 None,
529 DEFERRED_HEATMAP.scope(
530 RefCell::new(Vec::new()),
531 self.run_scoped(request, runtime, entitlements, driver, identity),
532 ),
533 )
534 .await
535 }
536
537 async fn run_scoped<D>(
538 &self,
539 request: ToolRequest,
540 runtime: RuntimeContext,
541 entitlements: ProductEntitlements,
542 driver: D,
543 identity: Option<crate::core::task_spine::AdmittedTaskIdentity>,
544 ) -> Result<D::Output, LifecycleRunError<D::Error>>
545 where
546 D: ExecutionDriver,
547 {
548 let context = self
549 .spine
550 .begin(request, runtime, entitlements, identity.as_ref());
551 TaskSpine::scope(
552 Some(context.envelope.clone()),
553 Box::pin(self.run_context(context, driver)),
555 )
556 .await
557 }
558
559 async fn run_context<D>(
560 &self,
561 context: TaskContext,
562 mut driver: D,
563 ) -> Result<D::Output, LifecycleRunError<D::Error>>
564 where
565 D: ExecutionDriver,
566 {
567 if let Some(cached) = context
568 .cached_or_claim_dispatch::<D::Output, D::Error>()
569 .await
570 {
571 if matches!(cached, Err(LifecycleRunError::ReplayTypeMismatch)) {
572 return cached;
573 }
574 return match driver.validate_cached_replay(&context).await {
575 Ok(Some(current_denial)) => Ok(current_denial),
576 Ok(None) => cached,
577 Err(error) => Err(LifecycleRunError::Dispatch(error)),
578 };
579 }
580 let mut permit = DispatchPermit::new(context.shared.clone());
581 let guard = LifecycleGuard::for_run(self, context.clone(), driver.cancellation_handler());
582 *context
583 .shared
584 .state
585 .lock()
586 .unwrap_or_else(std::sync::PoisonError::into_inner) = RunState::Completing;
587 macro_rules! run_admission_stage {
588 ($method:ident, $stage:expr) => {
589 match driver.$method(&context).await {
590 Ok(disposition) => {
591 let _ = context.record_stage($stage, disposition);
592 }
593 Err(error) => {
594 let _ = context.advance($stage);
595 context.skip_through(LifecycleStage::RecordLedger, "admission failed");
596 let result = Err(LifecycleRunError::Dispatch(error));
597 let _ = guard.complete(driver.observe(&result));
598 permit.publish(result.clone());
599 return result;
600 }
601 }
602 };
603 }
604
605 run_admission_stage!(identify_caller, LifecycleStage::IdentifyCaller);
606 run_admission_stage!(resolve_workspace, LifecycleStage::ResolveWorkspace);
607 run_admission_stage!(resolve_entitlements, LifecycleStage::ResolveEntitlements);
608 run_admission_stage!(
609 apply_security_boundaries,
610 LifecycleStage::ApplySecurityBoundaries
611 );
612 run_admission_stage!(
613 gather_context_strategy,
614 LifecycleStage::GatherContextStrategy
615 );
616 run_admission_stage!(ask_autopilot, LifecycleStage::AskAutopilot);
617
618 let primitive = match driver.dispatch_primitive(&context).await {
619 Ok((primitive, disposition)) => {
620 let _ = context.record_stage(LifecycleStage::DispatchPrimitive, disposition);
621 primitive
622 }
623 Err(error) => {
624 let _ = context.advance(LifecycleStage::DispatchPrimitive);
625 let _ = context.skip(LifecycleStage::ReversiblePostProcess, "dispatch failed");
626 let _ = context.skip(LifecycleStage::RecordContextIr, "dispatch failed");
627 let _ = context.skip(LifecycleStage::RecordLedger, "dispatch failed");
628 let result = Err(LifecycleRunError::Dispatch(error));
629 let _ = guard.complete(driver.observe(&result));
630 permit.publish(result.clone());
631 return result;
632 }
633 };
634 let processed = match driver.reversible_post_process(&context, primitive).await {
635 Ok((processed, disposition)) => {
636 let _ = context.record_stage(LifecycleStage::ReversiblePostProcess, disposition);
637 processed
638 }
639 Err(error) => {
640 let _ = context.advance(LifecycleStage::ReversiblePostProcess);
641 let _ = context.skip(LifecycleStage::RecordContextIr, "post-processing failed");
642 let _ = context.skip(LifecycleStage::RecordLedger, "post-processing failed");
643 let result = Err(LifecycleRunError::Dispatch(error));
644 let _ = guard.complete(driver.observe(&result));
645 permit.publish(result.clone());
646 return result;
647 }
648 };
649 let ir = driver.record_context_ir(&context, &processed).await;
650 match ir {
651 Ok(disposition) => {
652 let _ = context.record_stage(LifecycleStage::RecordContextIr, disposition);
653 }
654 Err(error) => {
655 let _ = context.advance(LifecycleStage::RecordContextIr);
656 let _ = context.skip(LifecycleStage::RecordLedger, "Context IR failed");
657 let result = Err(LifecycleRunError::Dispatch(error));
658 let _ = guard.complete(driver.observe(&result));
659 permit.publish(result.clone());
660 return result;
661 }
662 }
663 match driver.record_ledger(&context, &processed).await {
664 Ok(disposition) => {
665 let _ = context.record_stage(LifecycleStage::RecordLedger, disposition);
666 }
667 Err(error) => {
668 let _ = context.advance(LifecycleStage::RecordLedger);
669 let result = Err(LifecycleRunError::Dispatch(error));
670 let _ = guard.complete(driver.observe(&result));
671 permit.publish(result.clone());
672 return result;
673 }
674 }
675 let mut output = driver.output_from_processed(processed);
676 let provisional = Ok(output.clone());
677 let observation = driver.observe(&provisional);
678 *guard
679 .observation
680 .lock()
681 .unwrap_or_else(std::sync::PoisonError::into_inner) = Some(observation.clone());
682 let fallback_observation = observation.clone();
683 let outcome = match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
684 self.finalize_through_economics(&context, observation)
685 })) {
686 Ok(outcome) => {
687 *guard
688 .outcome
689 .lock()
690 .unwrap_or_else(std::sync::PoisonError::into_inner) = Some(outcome.clone());
691 match driver.checkpoint(&context, &mut output).await {
692 Ok(disposition) => {
693 let _ = context.record_stage(LifecycleStage::Checkpoint, disposition);
694 }
695 Err(_) => {
696 let _ = context.skip(LifecycleStage::Checkpoint, "checkpoint failed");
697 }
698 }
699 let telemetry_observation = driver.observe(&Ok(output.clone()));
700 self.finish_cleanup(&context, &telemetry_observation);
701 outcome
702 }
703 Err(_) => self.finalize_after_panic(&context, fallback_observation),
704 };
705 let mut state = context
706 .shared
707 .state
708 .lock()
709 .unwrap_or_else(std::sync::PoisonError::into_inner);
710 *state = RunState::Completed(Box::new(outcome));
711 context.shared.completed.notify_all();
712 drop(state);
713 let result = Ok(output);
714 permit.publish(result.clone());
715 result
716 }
717
718 pub fn complete(
719 &self,
720 context: &TaskContext,
721 observation: CompletionObservation,
722 ) -> LifecycleOutcome {
723 self.prepare(context);
724 self.complete_ordered(context, observation).0
725 }
726
727 pub fn complete_once(
733 &self,
734 context: &TaskContext,
735 observation: CompletionObservation,
736 ) -> Option<LifecycleOutcome> {
737 self.prepare(context);
738 let (outcome, completed_now) = self.complete_ordered(context, observation);
739 completed_now.then_some(outcome)
740 }
741
742 fn complete_ordered(
743 &self,
744 context: &TaskContext,
745 observation: CompletionObservation,
746 ) -> (LifecycleOutcome, bool) {
747 let mut state = context
748 .shared
749 .state
750 .lock()
751 .unwrap_or_else(std::sync::PoisonError::into_inner);
752 loop {
753 match &*state {
754 RunState::Completed(outcome) => return (outcome.as_ref().clone(), false),
755 RunState::Completing => {
756 state = context
757 .shared
758 .completed
759 .wait(state)
760 .unwrap_or_else(std::sync::PoisonError::into_inner);
761 }
762 RunState::Open => {
763 *state = RunState::Completing;
764 break;
765 }
766 }
767 }
768 drop(state);
769 context.skip_through(
770 LifecycleStage::RecordLedger,
771 "surface ended before dispatch",
772 );
773
774 let fallback_observation = observation.clone();
775 let outcome = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
776 self.finalize(context, observation)
777 }))
778 .unwrap_or_else(|_| {
779 tracing::error!(
780 task_id = context.task_id,
781 "execution lifecycle finalizer panicked"
782 );
783 self.finalize_after_panic(context, fallback_observation)
784 });
785 let mut state = context
786 .shared
787 .state
788 .lock()
789 .unwrap_or_else(std::sync::PoisonError::into_inner);
790 *state = RunState::Completed(Box::new(outcome.clone()));
791 context.shared.completed.notify_all();
792 (outcome, true)
793 }
794
795 fn complete_cancelled(
796 &self,
797 context: &TaskContext,
798 observation: &CompletionObservation,
799 completed_outcome: Option<LifecycleOutcome>,
800 ) -> LifecycleOutcome {
801 let outcome = if let Some(outcome) = completed_outcome {
804 outcome
805 } else {
806 std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
809 self.finalize_through_economics(context, observation.clone())
810 }))
811 .unwrap_or_else(|_| self.finalize_after_panic(context, observation.clone()))
812 };
813 context.skip_through(LifecycleStage::Checkpoint, "execution cancelled");
814 self.finish_cleanup(context, observation);
815 self.spine
816 .outcomes
817 .lock()
818 .unwrap_or_else(std::sync::PoisonError::into_inner)
819 .entry(context.task_id.clone())
820 .or_insert_with(|| outcome.clone());
821 *context
822 .shared
823 .state
824 .lock()
825 .unwrap_or_else(std::sync::PoisonError::into_inner) =
826 RunState::Completed(Box::new(outcome.clone()));
827 context.shared.completed.notify_all();
828 outcome
829 }
830
831 fn complete_owned(
832 &self,
833 context: &TaskContext,
834 observation: CompletionObservation,
835 ) -> LifecycleOutcome {
836 context.skip_through(
837 LifecycleStage::RecordLedger,
838 "surface ended before dispatch",
839 );
840 let state = context
841 .shared
842 .state
843 .lock()
844 .unwrap_or_else(std::sync::PoisonError::into_inner);
845 if let RunState::Completed(outcome) = &*state {
846 return outcome.as_ref().clone();
847 }
848 drop(state);
849 let fallback_observation = observation.clone();
850 let outcome = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
851 self.finalize(context, observation)
852 }))
853 .unwrap_or_else(|_| self.finalize_after_panic(context, fallback_observation));
854 let mut state = context
855 .shared
856 .state
857 .lock()
858 .unwrap_or_else(std::sync::PoisonError::into_inner);
859 *state = RunState::Completed(Box::new(outcome.clone()));
860 context.shared.completed.notify_all();
861 outcome
862 }
863
864 fn finalize(
865 &self,
866 context: &TaskContext,
867 observation: CompletionObservation,
868 ) -> LifecycleOutcome {
869 let telemetry = observation.clone();
870 let outcome = if context.completed_stages().len() >= 15 {
871 self.outcome_for(&context.task_id)
872 .unwrap_or_else(|| self.finalize_through_economics(context, observation))
873 } else {
874 self.finalize_through_economics(context, observation)
875 };
876 let _ = context.skip(LifecycleStage::Checkpoint, "checkpoint not configured");
877 self.finish_cleanup(context, &telemetry);
878 outcome
879 }
880
881 fn finalize_through_economics(
882 &self,
883 context: &TaskContext,
884 observation: CompletionObservation,
885 ) -> LifecycleOutcome {
886 TaskSpine::set(context.envelope.clone());
887 if let Some(outcome) = context.shared.computed_outcome.get() {
888 context.skip_through(
890 LifecycleStage::UpdateBreakEven,
891 "finalization already attempted",
892 );
893 return outcome.clone();
894 }
895
896 #[cfg(test)]
897 test_finalization_point(context, LifecycleStage::RecordEvidence, None);
898
899 let CompletionObservation {
900 input_tokens,
901 output_tokens,
902 model,
903 provider,
904 success: _,
905 outcome_signals,
906 proxy_economics,
907 heatmap,
908 } = observation;
909 #[cfg(test)]
910 let _ = (output_tokens, &outcome_signals);
911
912 let accepted_outcome =
914 evaluate_outcome_signals(&context.task_id, &context.profile_intent, &outcome_signals);
915
916 let assessment = (accepted_outcome.accepted != AcceptanceState::Unknown
917 && !outcome_signals.is_empty())
918 .then(|| {
919 let cost = ExecutionCost {
920 input_tokens,
921 output_tokens,
922 cache_read_tokens: 0,
923 model: model.clone(),
924 provider,
925 estimated_cost_micros: calculate_cost(input_tokens, output_tokens, 0, &model),
926 };
927 let outcome = TaskOutcome {
928 task_id: context.task_id.clone(),
929 completed: true,
930 signals: outcome_signals.clone(),
931 };
932 self.spine
933 .value_gate
934 .assess(&context.task_id, &cost, &outcome)
935 });
936
937 let outcome = LifecycleOutcome {
938 accepted_outcome,
939 assessment,
940 };
941 let _ = context.shared.computed_outcome.set(outcome.clone());
944
945 if outcome.assessment.is_some() {
948 let _ = context.advance(LifecycleStage::RecordEvidence);
949 } else {
950 let _ = context.skip(
951 LifecycleStage::RecordEvidence,
952 "outcome has no accepted evidence",
953 );
954 }
955 if let Some(assessment) = outcome.assessment.as_ref() {
956 self.spine
957 .assessments
958 .lock()
959 .unwrap_or_else(std::sync::PoisonError::into_inner)
960 .insert(context.task_id.clone(), assessment.clone());
961 }
962 self.spine
963 .outcomes
964 .lock()
965 .unwrap_or_else(std::sync::PoisonError::into_inner)
966 .insert(context.task_id.clone(), outcome.clone());
967 let _ = context.advance(LifecycleStage::RecordOutcome);
968 if let Some(assessment) = outcome.assessment.as_ref()
969 && context.entitlements.personalized_learning
970 {
971 #[cfg(not(test))]
972 crate::core::value_gate::record_assessment(assessment);
973 #[cfg(test)]
974 let _ = assessment;
975 let _ = context.advance(LifecycleStage::UpdateLearning);
976 } else {
977 let _ = context.skip(
978 LifecycleStage::UpdateLearning,
979 "not entitled or outcome unknown",
980 );
981 }
982 let mut heatmaps = take_deferred_heatmap();
983 if let Some(heatmap) = heatmap {
984 heatmaps.push(heatmap);
985 }
986 if heatmaps.is_empty() {
987 let _ = context.skip(LifecycleStage::UpdateHeatmap, "no file access recorded");
988 } else {
989 for heatmap in heatmaps {
990 crate::core::heatmap::record_file_access(
991 &heatmap.path,
992 heatmap.original_tokens,
993 heatmap.saved_tokens,
994 );
995 }
996 let _ = context.advance(LifecycleStage::UpdateHeatmap);
997 }
998 if context.surface == ToolSurface::Proxy {
999 #[cfg(test)]
1000 test_finalization_point(context, LifecycleStage::UpdateBreakEven, Some(&outcome));
1001 let economics = proxy_economics.unwrap_or_else(|| ProxyEconomicsObservation {
1002 tokens_pruned: 0,
1003 original_tokens: usize::try_from(input_tokens).unwrap_or(usize::MAX),
1004 task_class: context.triage_class.clone(),
1005 });
1006 #[cfg(not(test))]
1007 crate::core::proxy_value_metrics::record_completion(
1008 economics.tokens_pruned,
1009 economics.original_tokens,
1010 &economics.task_class,
1011 outcome.accepted_outcome.accepted,
1012 );
1013 #[cfg(test)]
1014 let _ = economics;
1015 let _ = context.advance(LifecycleStage::UpdateBreakEven);
1016 } else {
1017 let _ = context.skip(LifecycleStage::UpdateBreakEven, "no provider economics");
1018 }
1019 outcome
1020 }
1021
1022 fn finish_cleanup(&self, context: &TaskContext, observation: &CompletionObservation) {
1023 if !context
1024 .completed_stages()
1025 .contains(&LifecycleStage::ScheduleTelemetry)
1026 && std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1027 self.record_telemetry(context, observation);
1028 }))
1029 .is_err()
1030 {
1031 context.skip(LifecycleStage::ScheduleTelemetry, "telemetry panicked");
1032 }
1033 let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1036 self.flush_state(context);
1037 }));
1038 }
1039
1040 fn record_telemetry(&self, context: &TaskContext, observation: &CompletionObservation) {
1041 if context
1042 .shared
1043 .telemetry_started
1044 .swap(true, std::sync::atomic::Ordering::AcqRel)
1045 {
1046 context.skip(
1047 LifecycleStage::ScheduleTelemetry,
1048 "telemetry already attempted",
1049 );
1050 return;
1051 }
1052 #[cfg(test)]
1053 test_finalization_point(context, LifecycleStage::ScheduleTelemetry, None);
1054 #[cfg(not(test))]
1055 {
1056 let metrics = crate::core::telemetry::global_metrics();
1060 metrics.record_tokens(
1061 observation.input_tokens,
1062 observation.output_tokens,
1063 observation
1064 .input_tokens
1065 .saturating_sub(observation.output_tokens),
1066 );
1067 }
1068 #[cfg(test)]
1069 let _ = (context, observation);
1070 let _ = context.advance(LifecycleStage::ScheduleTelemetry);
1071 }
1072
1073 fn flush_state(&self, context: &TaskContext) {
1074 if context
1077 .shared
1078 .flush_started
1079 .swap(true, std::sync::atomic::Ordering::AcqRel)
1080 {
1081 return;
1082 }
1083 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1084 #[cfg(test)]
1085 {
1086 let hook = context
1087 .shared
1088 .flush_hook
1089 .lock()
1090 .unwrap_or_else(std::sync::PoisonError::into_inner)
1091 .clone();
1092 if let Some(hook) = hook {
1093 hook();
1094 return;
1095 }
1096 }
1097 crate::core::tool_lifecycle::flush_all();
1098 }));
1099 match result {
1100 Ok(()) => {
1101 let _ = context.advance(LifecycleStage::FlushState);
1102 }
1103 Err(payload) => {
1104 let _ = context.skip(LifecycleStage::FlushState, "state flush panicked");
1105 std::panic::resume_unwind(payload);
1106 }
1107 }
1108 }
1109
1110 fn finalize_after_panic(
1111 &self,
1112 context: &TaskContext,
1113 mut observation: CompletionObservation,
1114 ) -> LifecycleOutcome {
1115 observation.success = false;
1116 let outcome = context
1119 .shared
1120 .computed_outcome
1121 .get()
1122 .cloned()
1123 .unwrap_or_else(|| {
1124 let contract =
1125 OutcomeContractV1::for_task_class(task_class(&context.profile_intent));
1126 LifecycleOutcome {
1127 accepted_outcome: OutcomeEvaluator::new()
1128 .evaluate_with_context(
1129 &contract,
1130 &[],
1131 crate::core::outcome::evaluator::EvaluationContext::new(
1132 &context.task_id,
1133 &contract.contract_version,
1134 crate::core::outcome::evaluator::EVALUATOR_VERSION,
1135 ),
1136 )
1137 .outcome,
1138 assessment: None,
1139 }
1140 });
1141 context.skip_through(
1142 LifecycleStage::UpdateBreakEven,
1143 "lifecycle finalizer failed",
1144 );
1145 let _ = context.skip(LifecycleStage::Checkpoint, "lifecycle finalizer failed");
1146 self.finish_cleanup(context, &observation);
1147 if let Some(assessment) = outcome.assessment.as_ref() {
1148 self.spine
1149 .assessments
1150 .lock()
1151 .unwrap_or_else(std::sync::PoisonError::into_inner)
1152 .insert(context.task_id.clone(), assessment.clone());
1153 }
1154 self.spine
1155 .outcomes
1156 .lock()
1157 .unwrap_or_else(std::sync::PoisonError::into_inner)
1158 .insert(context.task_id.clone(), outcome.clone());
1159 outcome
1160 }
1161
1162 pub fn profile_for_session(&self, session_id: &str) -> Option<TaskProfileLocal> {
1163 self.spine.profile_for_session(session_id)
1164 }
1165
1166 pub fn assessment_for(&self, task_id: &str) -> Option<ValueAssessment> {
1167 self.spine.assessment_for(task_id)
1168 }
1169
1170 pub fn outcome_for(&self, task_id: &str) -> Option<LifecycleOutcome> {
1171 self.spine
1172 .outcomes
1173 .lock()
1174 .unwrap_or_else(std::sync::PoisonError::into_inner)
1175 .get(task_id)
1176 .cloned()
1177 }
1178}
1179
1180pub struct LifecycleGuard<'a> {
1182 lifecycle: &'a ExecutionLifecycle,
1183 context: TaskContext,
1184 prepare_missing: bool,
1185 cancellation: Option<CancellationHandler>,
1186 observation: Mutex<Option<CompletionObservation>>,
1187 outcome: Mutex<Option<LifecycleOutcome>>,
1188}
1189
1190struct DispatchPermit {
1191 shared: Arc<SharedRun>,
1192 published: bool,
1193}
1194
1195impl DispatchPermit {
1196 fn new(shared: Arc<SharedRun>) -> Self {
1197 Self {
1198 shared,
1199 published: false,
1200 }
1201 }
1202
1203 fn publish<T: Send + Sync + 'static>(&mut self, result: T) {
1204 *self
1205 .shared
1206 .dispatch
1207 .lock()
1208 .unwrap_or_else(std::sync::PoisonError::into_inner) =
1209 DispatchState::Finished(Arc::new(result));
1210 self.published = true;
1211 self.shared.dispatch_completed.notify_waiters();
1212 }
1213}
1214
1215impl Drop for DispatchPermit {
1216 fn drop(&mut self) {
1217 if self.published {
1218 return;
1219 }
1220 let mut dispatch = self
1221 .shared
1222 .dispatch
1223 .lock()
1224 .unwrap_or_else(std::sync::PoisonError::into_inner);
1225 let terminal = match &*self
1226 .shared
1227 .state
1228 .lock()
1229 .unwrap_or_else(std::sync::PoisonError::into_inner)
1230 {
1231 RunState::Completed(outcome) => Some(outcome.clone()),
1232 RunState::Open | RunState::Completing => None,
1233 };
1234 *dispatch = terminal.map_or(DispatchState::Idle, DispatchState::Aborted);
1235 self.shared.dispatch_completed.notify_waiters();
1236 }
1237}
1238
1239impl<'a> LifecycleGuard<'a> {
1240 pub fn new(lifecycle: &'a ExecutionLifecycle, context: TaskContext) -> Self {
1241 lifecycle.prepare(&context);
1242 Self {
1243 lifecycle,
1244 context,
1245 prepare_missing: true,
1246 cancellation: None,
1247 observation: Mutex::new(None),
1248 outcome: Mutex::new(None),
1249 }
1250 }
1251
1252 fn for_run(
1253 lifecycle: &'a ExecutionLifecycle,
1254 context: TaskContext,
1255 cancellation: Option<CancellationHandler>,
1256 ) -> Self {
1257 Self {
1258 lifecycle,
1259 context,
1260 prepare_missing: false,
1261 cancellation,
1262 observation: Mutex::new(None),
1263 outcome: Mutex::new(None),
1264 }
1265 }
1266
1267 pub const fn context(&self) -> &TaskContext {
1268 &self.context
1269 }
1270
1271 pub fn complete(&self, observation: CompletionObservation) -> LifecycleOutcome {
1272 if self.prepare_missing {
1273 self.lifecycle.complete(&self.context, observation)
1274 } else {
1275 self.lifecycle.complete_owned(&self.context, observation)
1276 }
1277 }
1278}
1279
1280impl Drop for LifecycleGuard<'_> {
1281 fn drop(&mut self) {
1282 if self.context.outcome().is_some() {
1283 return;
1284 }
1285 let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1286 let mut observation = CompletionObservation::tool_result(0, 0, "unknown", false);
1287 observation.outcome_signals.clear();
1289 if !self
1290 .context
1291 .completed_stages()
1292 .contains(&LifecycleStage::RecordLedger)
1293 {
1294 self.context.skip_through(
1295 LifecycleStage::RecordContextIr,
1296 "execution cancelled before stage completed",
1297 );
1298 let mut ledger = StageDisposition::Skipped("execution cancelled before ledger");
1299 if let Some(handler) = self.cancellation.take() {
1300 match std::panic::catch_unwind(std::panic::AssertUnwindSafe(handler)) {
1301 Ok(completion) => {
1302 observation = completion.observation;
1303 ledger = completion.ledger;
1304 }
1305 Err(_) => {
1306 ledger = StageDisposition::Skipped("cancellation ledger panicked");
1307 }
1308 }
1309 }
1310 self.context
1311 .record_stage(LifecycleStage::RecordLedger, ledger);
1312 }
1313 if let Some(saved) = self
1314 .observation
1315 .lock()
1316 .unwrap_or_else(std::sync::PoisonError::into_inner)
1317 .take()
1318 {
1319 observation = saved;
1320 }
1321 let completed_outcome = self
1322 .outcome
1323 .lock()
1324 .unwrap_or_else(std::sync::PoisonError::into_inner)
1325 .take();
1326 let _ = if self.prepare_missing {
1327 self.lifecycle.complete(&self.context, observation)
1328 } else {
1329 self.lifecycle
1330 .complete_cancelled(&self.context, &observation, completed_outcome)
1331 };
1332 }));
1333 }
1334}
1335
1336pub(crate) fn evaluate_outcome_signals(
1339 task_id: &str,
1340 profile_intent: &str,
1341 signals: &[OutcomeSignal],
1342) -> AcceptedOutcomeV1 {
1343 let signals = canonical_signals(signals, task_id);
1344 let contract = OutcomeContractV1::for_task_class(task_class(profile_intent));
1345 let context = crate::core::outcome::evaluator::EvaluationContext::new(
1346 task_id,
1347 &contract.contract_version,
1348 crate::core::outcome::evaluator::EVALUATOR_VERSION,
1349 );
1350 OutcomeEvaluator::new()
1351 .evaluate_with_context(&contract, &signals, context)
1352 .outcome
1353}
1354
1355fn task_class(value: &str) -> TaskClass {
1356 match value {
1357 "coding_fix" | "bug_fix" | "debugging" => TaskClass::BugFix,
1358 "refactor" | "coding_refactor" => TaskClass::Refactor,
1359 "test_addition" => TaskClass::TestAddition,
1360 "documentation" | "docs" => TaskClass::Documentation,
1361 _ => TaskClass::Investigation,
1362 }
1363}
1364
1365fn canonical_signals(signals: &[OutcomeSignal], task_id: &str) -> Vec<CanonicalSignal> {
1366 signals
1367 .iter()
1368 .map(|signal| {
1369 let signal = match signal {
1370 OutcomeSignal::BuildSucceeded => LocalSignalAdapters::build_success(true),
1371 OutcomeSignal::TestsPassed => LocalSignalAdapters::tests_passing(true),
1372 OutcomeSignal::LintClean => LocalSignalAdapters::lint_clean(true),
1373 OutcomeSignal::UserAccepted => LocalSignalAdapters::human_acceptance(true),
1374 OutcomeSignal::UserRejected => LocalSignalAdapters::human_acceptance(false),
1375 OutcomeSignal::CompileError => LocalSignalAdapters::build_success(false),
1376 OutcomeSignal::TestFailed => LocalSignalAdapters::tests_passing(false),
1377 };
1378 signal.with_evidence_ref(format!("task:{task_id}"))
1379 })
1380 .collect()
1381}
1382
1383#[cfg(test)]
1384#[path = "execution_lifecycle_tests.rs"]
1385mod tests;