use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use crate::{ActivityOutcome, HostFacts, TurnLifecycleEffect, TurnPlan, TurnState, plan_next_turn};
#[derive(Debug, Clone)]
pub struct ExecutionTransition {
pub plan: TurnPlan,
pub effects: Vec<TurnLifecycleEffect>,
}
pub trait Execution {
fn state(&self) -> &TurnState;
fn advance(
&mut self,
outcome: ActivityOutcome,
pending_user_message_count: usize,
now: DateTime<Utc>,
facts: HostFacts,
) -> ExecutionTransition;
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TurnExecution {
state: TurnState,
}
impl TurnExecution {
pub fn new(state: TurnState) -> Self {
Self { state }
}
pub fn into_state(self) -> TurnState {
self.state
}
fn apply_plan(&mut self, plan: &TurnPlan) {
match plan {
TurnPlan::ScheduleReason(next) => self.state = next.clone(),
TurnPlan::ScheduleAct(plan) => {
let mut next = (*plan.resume_state).clone();
next.previous_response_id = plan.previous_response_id.clone();
next.iteration = plan.iteration;
next.request_id = plan.request_id.clone();
self.state = next;
}
TurnPlan::WaitForToolResults { resume } => self.state = resume.clone(),
TurnPlan::Complete { .. } => {}
}
}
}
impl Execution for TurnExecution {
fn state(&self) -> &TurnState {
&self.state
}
fn advance(
&mut self,
outcome: ActivityOutcome,
pending_user_message_count: usize,
now: DateTime<Utc>,
facts: HostFacts,
) -> ExecutionTransition {
let terminal_reason_state = match &outcome {
ActivityOutcome::Reason(reason) => Some(self.state.with_reason_summary(reason)),
_ => None,
};
let (plan, effects) =
plan_next_turn(&self.state, outcome, pending_user_message_count, now, facts);
self.apply_plan(&plan);
if matches!(plan, TurnPlan::Complete { .. })
&& let Some(terminal_reason_state) = terminal_reason_state
{
self.state = terminal_reason_state;
}
ExecutionTransition { plan, effects }
}
}
#[cfg(test)]
mod tests {
use everruns_provider::typed_id::{HarnessId, MessageId, SessionId, TurnId};
use super::*;
fn state() -> TurnState {
TurnState {
org_id: 1,
session_id: SessionId::new(),
harness_id: HarnessId::new(),
agent_id: None,
input_message_id: MessageId::new(),
turn_id: None,
previous_response_id: None,
iteration: 1,
request_id: None,
started_at: None,
cumulative_usage: None,
tool_call_count: 0,
llm_call_count: 0,
time_to_first_token_ms: None,
final_message_id: None,
final_answer_preview: None,
}
}
#[test]
fn process_input_advances_the_owned_state() {
let turn_id = TurnId::new();
let mut execution = TurnExecution::new(state());
let transition = execution.advance(
ActivityOutcome::ProcessInput {
turn_id: Some(turn_id),
},
0,
Utc::now(),
HostFacts::default(),
);
assert!(matches!(transition.plan, TurnPlan::ScheduleReason(_)));
assert_eq!(execution.state().turn_id, Some(turn_id));
}
#[test]
fn execution_checkpoint_round_trips() {
let execution = TurnExecution::new(state());
let bytes = serde_json::to_vec(&execution).expect("serialize execution");
let restored: TurnExecution =
serde_json::from_slice(&bytes).expect("restore execution checkpoint");
assert_eq!(restored.state().session_id, execution.state().session_id);
assert_eq!(
restored.state().input_message_id,
execution.state().input_message_id
);
}
}