Skip to main content

machi_runtime/
lifecycle.rs

1//! Turn lifecycle contributor port (W3.5).
2//!
3//! Maturity: **core** (port). Hooks product crates implement this later.
4
5use machi_types::{ErrorCode, MachiError, RunId};
6
7/// Why a turn aborted without a normal completion.
8#[derive(Debug, Clone, PartialEq, Eq)]
9pub enum TurnAbortReason {
10    /// Cancel token fired.
11    Cancelled,
12    /// Deadline exceeded.
13    Deadline,
14    /// Max steps exceeded.
15    MaxSteps,
16    /// Stationarity hard stop.
17    Stationarity,
18    /// Other typed error.
19    Error {
20        /// Error code.
21        code: ErrorCode,
22        /// Message.
23        message: String,
24    },
25}
26
27impl TurnAbortReason {
28    /// Build from a [`MachiError`].
29    #[must_use]
30    pub fn from_error(err: &MachiError) -> Self {
31        match err.code() {
32            ErrorCode::RuntimeCancelled | ErrorCode::LlmCancelled | ErrorCode::HostCancelled => {
33                Self::Cancelled
34            }
35            ErrorCode::RuntimeDeadline => Self::Deadline,
36            ErrorCode::RuntimeMaxSteps => Self::MaxSteps,
37            ErrorCode::RuntimeStationarity => Self::Stationarity,
38            code => Self::Error {
39                code,
40                message: err.message().to_owned(),
41            },
42        }
43    }
44}
45
46/// Lifecycle hooks for a single turn. Default methods are no-ops.
47pub trait TurnLifecycleContributor: Send + Sync {
48    /// Called when a turn begins (after run id is assigned).
49    fn on_turn_start(&self, _run_id: &RunId) {}
50    /// Called when a turn completes successfully.
51    fn on_turn_done(&self, _run_id: &RunId, _steps: usize) {}
52    /// Called when a turn aborts (cancel / deadline / max steps / stationarity / …).
53    fn on_turn_abort(&self, _run_id: &RunId, _reason: &TurnAbortReason) {}
54    /// Called when a turn ends with a returned error (non-cancelled path).
55    fn on_turn_error(&self, _run_id: &RunId, _err: &MachiError) {}
56}
57
58/// No-op contributor.
59#[derive(Debug, Default, Clone, Copy)]
60pub struct NoopLifecycle;
61
62impl TurnLifecycleContributor for NoopLifecycle {}
63
64/// Fan-out to a list of contributors.
65#[derive(Default)]
66pub struct LifecycleFanout {
67    contributors: Vec<std::sync::Arc<dyn TurnLifecycleContributor>>,
68}
69
70impl std::fmt::Debug for LifecycleFanout {
71    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
72        f.debug_struct("LifecycleFanout")
73            .field("contributors", &self.contributors.len())
74            .finish()
75    }
76}
77
78impl LifecycleFanout {
79    /// Empty fanout.
80    #[must_use]
81    pub fn new() -> Self {
82        Self::default()
83    }
84
85    /// Push a contributor.
86    #[must_use]
87    pub fn push(mut self, c: std::sync::Arc<dyn TurnLifecycleContributor>) -> Self {
88        self.contributors.push(c);
89        self
90    }
91}
92
93impl TurnLifecycleContributor for LifecycleFanout {
94    fn on_turn_start(&self, run_id: &RunId) {
95        for c in &self.contributors {
96            c.on_turn_start(run_id);
97        }
98    }
99
100    fn on_turn_done(&self, run_id: &RunId, steps: usize) {
101        for c in &self.contributors {
102            c.on_turn_done(run_id, steps);
103        }
104    }
105
106    fn on_turn_abort(&self, run_id: &RunId, reason: &TurnAbortReason) {
107        for c in &self.contributors {
108            c.on_turn_abort(run_id, reason);
109        }
110    }
111
112    fn on_turn_error(&self, run_id: &RunId, err: &MachiError) {
113        for c in &self.contributors {
114            c.on_turn_error(run_id, err);
115        }
116    }
117}