Skip to main content

everruns_engine/
machine.rs

1//! Stateful execution over the shared turn planner.
2//!
3//! [`TurnExecution`] is the abstract execution kernel. Hosts decide how an
4//! execution is driven and stored, while this type owns the current
5//! [`TurnState`] and the rules for advancing it after every phase.
6
7use chrono::{DateTime, Utc};
8use serde::{Deserialize, Serialize};
9
10use crate::{ActivityOutcome, HostFacts, TurnLifecycleEffect, TurnPlan, TurnState, plan_next_turn};
11
12/// One engine decision and the ordered lifecycle effects it requires.
13#[derive(Debug, Clone)]
14pub struct ExecutionTransition {
15    /// The next semantic step for the execution driver.
16    pub plan: TurnPlan,
17    /// Effects the driver must apply in order before scheduling the plan.
18    pub effects: Vec<TurnLifecycleEffect>,
19}
20
21/// Common contract implemented by immediate and durable turn executions.
22///
23/// The contract is deliberately synchronous and sans I/O. An implementation
24/// may keep the state in process, checkpoint it between durable activities, or
25/// project it into another scheduler. Stores, queues, clocks, and effect
26/// application remain execution-driver concerns.
27pub trait Execution {
28    /// Current engine-owned state.
29    fn state(&self) -> &TurnState;
30
31    /// Advance after one completed Input/Reason/Act phase.
32    fn advance(
33        &mut self,
34        outcome: ActivityOutcome,
35        pending_user_message_count: usize,
36        now: DateTime<Utc>,
37        facts: HostFacts,
38    ) -> ExecutionTransition;
39}
40
41/// Default stateful implementation of the shared turn execution contract.
42///
43/// Immediate hosts keep this value in memory for the turn lifetime. Durable
44/// hosts serialize its state between activities and restore it before the next
45/// transition.
46#[derive(Debug, Clone, Serialize, Deserialize)]
47pub struct TurnExecution {
48    state: TurnState,
49}
50
51impl TurnExecution {
52    /// Start or restore an execution from its engine state.
53    pub fn new(state: TurnState) -> Self {
54        Self { state }
55    }
56
57    /// Consume the execution and return its checkpoint value.
58    pub fn into_state(self) -> TurnState {
59        self.state
60    }
61
62    fn apply_plan(&mut self, plan: &TurnPlan) {
63        match plan {
64            TurnPlan::ScheduleReason(next) => self.state = next.clone(),
65            TurnPlan::ScheduleAct(plan) => {
66                let mut next = (*plan.resume_state).clone();
67                next.previous_response_id = plan.previous_response_id.clone();
68                next.iteration = plan.iteration;
69                next.request_id = plan.request_id.clone();
70                self.state = next;
71            }
72            TurnPlan::WaitForToolResults { resume } => self.state = resume.clone(),
73            TurnPlan::Complete { .. } => {}
74        }
75    }
76}
77
78impl Execution for TurnExecution {
79    fn state(&self) -> &TurnState {
80        &self.state
81    }
82
83    fn advance(
84        &mut self,
85        outcome: ActivityOutcome,
86        pending_user_message_count: usize,
87        now: DateTime<Utc>,
88        facts: HostFacts,
89    ) -> ExecutionTransition {
90        // A terminal reason plan carries no resume state because the driver has
91        // nothing left to schedule. Preserve the reason summary in the owned
92        // execution nevertheless, so in-memory inspection and durable terminal
93        // checkpoints observe the same final counters and output metadata.
94        let terminal_reason_state = match &outcome {
95            ActivityOutcome::Reason(reason) => Some(self.state.with_reason_summary(reason)),
96            _ => None,
97        };
98        let (plan, effects) =
99            plan_next_turn(&self.state, outcome, pending_user_message_count, now, facts);
100        self.apply_plan(&plan);
101        if matches!(plan, TurnPlan::Complete { .. })
102            && let Some(terminal_reason_state) = terminal_reason_state
103        {
104            self.state = terminal_reason_state;
105        }
106        ExecutionTransition { plan, effects }
107    }
108}
109
110#[cfg(test)]
111mod tests {
112    use everruns_provider::typed_id::{HarnessId, MessageId, SessionId, TurnId};
113
114    use super::*;
115
116    fn state() -> TurnState {
117        TurnState {
118            org_id: 1,
119            session_id: SessionId::new(),
120            harness_id: HarnessId::new(),
121            agent_id: None,
122            input_message_id: MessageId::new(),
123            turn_id: None,
124            previous_response_id: None,
125            iteration: 1,
126            request_id: None,
127            started_at: None,
128            cumulative_usage: None,
129            tool_call_count: 0,
130            llm_call_count: 0,
131            time_to_first_token_ms: None,
132            final_message_id: None,
133            final_answer_preview: None,
134        }
135    }
136
137    #[test]
138    fn process_input_advances_the_owned_state() {
139        let turn_id = TurnId::new();
140        let mut execution = TurnExecution::new(state());
141
142        let transition = execution.advance(
143            ActivityOutcome::ProcessInput {
144                turn_id: Some(turn_id),
145            },
146            0,
147            Utc::now(),
148            HostFacts::default(),
149        );
150
151        assert!(matches!(transition.plan, TurnPlan::ScheduleReason(_)));
152        assert_eq!(execution.state().turn_id, Some(turn_id));
153    }
154
155    #[test]
156    fn execution_checkpoint_round_trips() {
157        let execution = TurnExecution::new(state());
158        let bytes = serde_json::to_vec(&execution).expect("serialize execution");
159        let restored: TurnExecution =
160            serde_json::from_slice(&bytes).expect("restore execution checkpoint");
161
162        assert_eq!(restored.state().session_id, execution.state().session_id);
163        assert_eq!(
164            restored.state().input_message_id,
165            execution.state().input_message_id
166        );
167    }
168}