everruns_engine/
machine.rs1use chrono::{DateTime, Utc};
8use serde::{Deserialize, Serialize};
9
10use crate::{ActivityOutcome, HostFacts, TurnLifecycleEffect, TurnPlan, TurnState, plan_next_turn};
11
12#[derive(Debug, Clone)]
14pub struct ExecutionTransition {
15 pub plan: TurnPlan,
17 pub effects: Vec<TurnLifecycleEffect>,
19}
20
21pub trait Execution {
28 fn state(&self) -> &TurnState;
30
31 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#[derive(Debug, Clone, Serialize, Deserialize)]
47pub struct TurnExecution {
48 state: TurnState,
49}
50
51impl TurnExecution {
52 pub fn new(state: TurnState) -> Self {
54 Self { state }
55 }
56
57 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 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}