Skip to main content

algocline_core/execution/
state.rs

1//! Execution state types (v2) for the `ExecutionService` layer.
2//!
3//! This module defines [`ExecutionState`] v2, [`ExecutionStateTag`], and [`ExecutionResult`].
4//! These types are **distinct** from the legacy `state::ExecutionState` in
5//! `crates/algocline-core/src/state.rs`; both coexist while the migration to the new
6//! `ExecutionService` API is in progress.
7
8use serde::{Deserialize, Serialize};
9use serde_json::Value as JsonValue;
10
11use super::cancel::{CancelInfo, FailureInfo};
12use super::pause::PauseInfo;
13use crate::TokenUsage;
14
15/// Rich execution state used by [`crate::execution::ExecutionService`].
16///
17/// This is the v2 variant; the legacy `crate::ExecutionState` from `state.rs` remains
18/// untouched for backward compatibility with existing engine consumers.
19#[derive(Debug, Clone, Serialize, Deserialize)]
20#[serde(tag = "state", rename_all = "snake_case")]
21pub enum ExecutionState {
22    /// The session is actively executing.
23    Running,
24    /// The session is paused, waiting for one or more LLM responses.
25    Paused(PauseInfo),
26    /// The session completed successfully.
27    Done(ExecutionResult),
28    /// The session was cancelled cooperatively.
29    Cancelled(CancelInfo),
30    /// The session ended with an error.
31    Failed(FailureInfo),
32}
33
34impl ExecutionState {
35    /// Returns the lightweight tag for this state variant.
36    pub fn tag(&self) -> ExecutionStateTag {
37        ExecutionStateTag::from(self)
38    }
39
40    /// Returns `true` if this state is a terminal (non-resumable) state.
41    pub fn is_terminal(&self) -> bool {
42        matches!(self, Self::Done(_) | Self::Cancelled(_) | Self::Failed(_))
43    }
44}
45
46/// Lightweight discriminant for [`ExecutionState`].
47///
48/// Used in [`crate::execution::ProgressEvent::StateTransition`] to avoid carrying
49/// the full state payload in every event.
50#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
51#[serde(rename_all = "snake_case")]
52pub enum ExecutionStateTag {
53    /// Corresponds to [`ExecutionState::Running`].
54    Running,
55    /// Corresponds to [`ExecutionState::Paused`].
56    Paused,
57    /// Corresponds to [`ExecutionState::Done`].
58    Done,
59    /// Corresponds to [`ExecutionState::Cancelled`].
60    Cancelled,
61    /// Corresponds to [`ExecutionState::Failed`].
62    Failed,
63}
64
65impl From<&ExecutionState> for ExecutionStateTag {
66    /// Extracts the discriminant tag from a state reference without cloning the payload.
67    fn from(state: &ExecutionState) -> Self {
68        match state {
69            ExecutionState::Running => Self::Running,
70            ExecutionState::Paused(_) => Self::Paused,
71            ExecutionState::Done(_) => Self::Done,
72            ExecutionState::Cancelled(_) => Self::Cancelled,
73            ExecutionState::Failed(_) => Self::Failed,
74        }
75    }
76}
77
78/// Payload carried by [`ExecutionState::Done`].
79///
80/// `finished_at` is stored as a Unix timestamp in milliseconds to ensure
81/// lossless serde without additional crate dependencies (the standard library
82/// `SystemTime` has no built-in serde support).
83#[derive(Debug, Clone, Serialize, Deserialize)]
84pub struct ExecutionResult {
85    /// The JSON value returned by the Lua execution.
86    pub value: JsonValue,
87    /// Aggregate token usage for the session, if available.
88    pub usage: Option<TokenUsage>,
89    /// Unix timestamp (milliseconds) when the session finished.
90    pub finished_at: i64,
91}
92
93#[cfg(test)]
94mod tests {
95    use super::*;
96    use crate::execution::cancel::{CancelCode, CancelInfo, CancelReason};
97    use crate::execution::pause::{PauseInfo, PauseKind};
98
99    fn make_pause_info() -> PauseInfo {
100        PauseInfo {
101            kind: PauseKind::Single,
102            prompts: vec![],
103            paused_at: 0,
104        }
105    }
106
107    fn make_cancel_info() -> CancelInfo {
108        CancelInfo {
109            reason: CancelReason {
110                code: CancelCode::User,
111                detail: None,
112                requested_at: 0,
113            },
114            observed_at: 0,
115            state_before: Box::new(ExecutionState::Running),
116        }
117    }
118
119    fn make_failure_info() -> FailureInfo {
120        crate::execution::cancel::FailureInfo {
121            message: "test".into(),
122            kind: crate::execution::cancel::FailureKind::Other,
123            occurred_at: 0,
124        }
125    }
126
127    #[test]
128    fn execution_state_tag_from_state() {
129        assert_eq!(
130            ExecutionStateTag::from(&ExecutionState::Running),
131            ExecutionStateTag::Running
132        );
133        assert_eq!(
134            ExecutionStateTag::from(&ExecutionState::Paused(make_pause_info())),
135            ExecutionStateTag::Paused
136        );
137        assert_eq!(
138            ExecutionStateTag::from(&ExecutionState::Done(ExecutionResult {
139                value: serde_json::Value::Null,
140                usage: None,
141                finished_at: 0,
142            })),
143            ExecutionStateTag::Done
144        );
145        assert_eq!(
146            ExecutionStateTag::from(&ExecutionState::Cancelled(make_cancel_info())),
147            ExecutionStateTag::Cancelled
148        );
149        assert_eq!(
150            ExecutionStateTag::from(&ExecutionState::Failed(make_failure_info())),
151            ExecutionStateTag::Failed
152        );
153    }
154
155    #[test]
156    fn execution_state_serde_roundtrip() {
157        let states: Vec<ExecutionState> = vec![
158            ExecutionState::Running,
159            ExecutionState::Paused(make_pause_info()),
160            ExecutionState::Done(ExecutionResult {
161                value: serde_json::json!({"ok": true}),
162                usage: None,
163                finished_at: 1_700_000_000_000,
164            }),
165            ExecutionState::Cancelled(make_cancel_info()),
166            ExecutionState::Failed(make_failure_info()),
167        ];
168
169        for state in states {
170            let json = serde_json::to_string(&state).expect("serialize");
171            let _: ExecutionState = serde_json::from_str(&json).expect("deserialize");
172        }
173    }
174}