1use std::{fmt, sync::Arc};
2
3use runifold_core::{
4 Checkpoint, CheckpointError, CheckpointErrorKind, CheckpointId, CheckpointStore, RunContext,
5 Usage,
6};
7use runifold_model::{Message, ModelRef, ModelResponse};
8use serde::{Deserialize, Serialize};
9
10use crate::{AgentError, AgentOutcome};
11
12const CHECKPOINT_KIND: &str = "runifold.agent";
13const CHECKPOINT_SCHEMA_VERSION: u32 = 1;
14
15#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
17#[non_exhaustive]
18pub enum ResumePolicy {
19 #[default]
21 RejectAmbiguous,
22 RetryInterruptedTurn,
24}
25
26#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
28#[serde(tag = "state", rename_all = "snake_case")]
29#[non_exhaustive]
30pub enum AgentCheckpointPhase {
31 ReadyForTurn,
33 TurnInFlight {
35 turn: u32,
37 },
38 Completed {
40 response: Box<ModelResponse>,
42 },
43}
44
45#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
47pub struct AgentCheckpointState {
48 pub execution_id: String,
50 pub agent: String,
52 pub model: ModelRef,
54 pub transcript: Vec<Message>,
56 pub turns: u32,
58 pub tool_calls: u32,
60 pub delegations: u32,
62 pub usage: Usage,
64 pub phase: AgentCheckpointPhase,
66}
67
68impl AgentCheckpointState {
69 pub(crate) fn outcome(&self) -> Option<AgentOutcome> {
70 match &self.phase {
71 AgentCheckpointPhase::Completed { response } => Some(AgentOutcome {
72 response: response.as_ref().clone(),
73 transcript: self.transcript.clone(),
74 turns: self.turns,
75 tool_calls: self.tool_calls,
76 delegations: self.delegations,
77 usage: self.usage,
78 }),
79 _ => None,
80 }
81 }
82}
83
84#[derive(Clone)]
86pub struct AgentCheckpoint {
87 id: CheckpointId,
88 store: Arc<dyn CheckpointStore>,
89}
90
91impl AgentCheckpoint {
92 pub fn new(store: Arc<dyn CheckpointStore>) -> Self {
94 Self {
95 id: CheckpointId::new(),
96 store,
97 }
98 }
99
100 pub fn existing(id: CheckpointId, store: Arc<dyn CheckpointStore>) -> Self {
102 Self { id, store }
103 }
104
105 pub const fn id(&self) -> CheckpointId {
107 self.id
108 }
109
110 pub fn load(&self) -> Result<(Checkpoint, AgentCheckpointState), CheckpointError> {
116 let checkpoint = self.store.load(self.id)?;
117 if checkpoint.kind != CHECKPOINT_KIND
118 || checkpoint.schema_version != CHECKPOINT_SCHEMA_VERSION
119 {
120 return Err(CheckpointError::new(
121 CheckpointErrorKind::InvalidPayload,
122 "checkpoint kind or schema version is not supported",
123 ));
124 }
125 let state = serde_json::from_value(checkpoint.payload.clone()).map_err(|error| {
126 CheckpointError::new(CheckpointErrorKind::InvalidPayload, error.to_string())
127 })?;
128 Ok((checkpoint, state))
129 }
130}
131
132impl fmt::Debug for AgentCheckpoint {
133 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
134 formatter
135 .debug_struct("AgentCheckpoint")
136 .field("id", &self.id)
137 .finish_non_exhaustive()
138 }
139}
140
141pub(crate) struct CheckpointCursor {
142 handle: AgentCheckpoint,
143 envelope: Checkpoint,
144}
145
146impl CheckpointCursor {
147 pub(crate) fn create(
148 handle: &AgentCheckpoint,
149 run: &RunContext,
150 state: &AgentCheckpointState,
151 ) -> Result<Self, AgentError> {
152 let payload = serialize(state)?;
153 let envelope = Checkpoint::initial(
154 handle.id,
155 run.run_id(),
156 CHECKPOINT_KIND,
157 CHECKPOINT_SCHEMA_VERSION,
158 payload,
159 );
160 handle.store.compare_and_swap(&envelope, None)?;
161 Ok(Self {
162 handle: handle.clone(),
163 envelope,
164 })
165 }
166
167 pub(crate) fn loaded(handle: &AgentCheckpoint, envelope: Checkpoint) -> Self {
168 Self {
169 handle: handle.clone(),
170 envelope,
171 }
172 }
173
174 pub(crate) fn save(&mut self, state: &AgentCheckpointState) -> Result<(), AgentError> {
175 let next = self.envelope.next(serialize(state)?)?;
176 self.handle
177 .store
178 .compare_and_swap(&next, Some(self.envelope.revision))?;
179 self.envelope = next;
180 Ok(())
181 }
182}
183
184fn serialize(state: &AgentCheckpointState) -> Result<serde_json::Value, AgentError> {
185 serde_json::to_value(state).map_err(|error| {
186 CheckpointError::new(CheckpointErrorKind::InvalidPayload, error.to_string()).into()
187 })
188}