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::conversation::{ConversationId, ConversationVersion, MemoryNamespace};
11use crate::{AgentError, AgentOutcome};
12
13const CHECKPOINT_KIND: &str = "runifold.agent";
14const CHECKPOINT_SCHEMA_VERSION: u32 = 1;
15
16#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
18#[non_exhaustive]
19pub enum ResumePolicy {
20 #[default]
22 RejectAmbiguous,
23 RetryInterruptedTurn,
25}
26
27#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
29#[serde(tag = "state", rename_all = "snake_case")]
30#[non_exhaustive]
31pub enum AgentCheckpointPhase {
32 ReadyForTurn,
34 TurnInFlight {
36 turn: u32,
38 },
39 Completed {
41 response: Box<ModelResponse>,
43 },
44}
45
46#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
48pub struct DurableConversationCheckpoint {
49 pub conversation_id: ConversationId,
51 pub namespace: MemoryNamespace,
53 pub expected_version: ConversationVersion,
55 pub persisted_prefix_len: u64,
57}
58
59#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
61pub struct AgentCheckpointState {
62 pub execution_id: String,
64 pub agent: String,
66 pub model: ModelRef,
68 pub transcript: Vec<Message>,
70 pub turns: u32,
72 pub tool_calls: u32,
74 pub delegations: u32,
76 pub usage: Usage,
78 pub phase: AgentCheckpointPhase,
80 #[serde(default, skip_serializing_if = "Option::is_none")]
82 pub durable_conversation: Option<DurableConversationCheckpoint>,
83}
84
85impl AgentCheckpointState {
86 pub(crate) fn outcome(&self) -> Option<AgentOutcome> {
87 match &self.phase {
88 AgentCheckpointPhase::Completed { response } => Some(AgentOutcome {
89 response: response.as_ref().clone(),
90 transcript: self.transcript.clone(),
91 turns: self.turns,
92 tool_calls: self.tool_calls,
93 delegations: self.delegations,
94 usage: self.usage,
95 }),
96 _ => None,
97 }
98 }
99}
100
101#[derive(Clone)]
103pub struct AgentCheckpoint {
104 id: CheckpointId,
105 store: Arc<dyn CheckpointStore>,
106}
107
108impl AgentCheckpoint {
109 pub fn new(store: Arc<dyn CheckpointStore>) -> Self {
111 Self {
112 id: CheckpointId::new(),
113 store,
114 }
115 }
116
117 pub fn existing(id: CheckpointId, store: Arc<dyn CheckpointStore>) -> Self {
119 Self { id, store }
120 }
121
122 pub const fn id(&self) -> CheckpointId {
124 self.id
125 }
126
127 pub fn load(&self) -> Result<(Checkpoint, AgentCheckpointState), CheckpointError> {
133 let checkpoint = self.store.load(self.id)?;
134 if checkpoint.kind != CHECKPOINT_KIND
135 || checkpoint.schema_version != CHECKPOINT_SCHEMA_VERSION
136 {
137 return Err(CheckpointError::new(
138 CheckpointErrorKind::InvalidPayload,
139 "checkpoint kind or schema version is not supported",
140 ));
141 }
142 let state = serde_json::from_value(checkpoint.payload.clone()).map_err(|error| {
143 CheckpointError::new(CheckpointErrorKind::InvalidPayload, error.to_string())
144 })?;
145 Ok((checkpoint, state))
146 }
147}
148
149impl fmt::Debug for AgentCheckpoint {
150 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
151 formatter
152 .debug_struct("AgentCheckpoint")
153 .field("id", &self.id)
154 .finish_non_exhaustive()
155 }
156}
157
158pub(crate) struct CheckpointCursor {
159 handle: AgentCheckpoint,
160 envelope: Checkpoint,
161}
162
163impl CheckpointCursor {
164 pub(crate) fn create(
165 handle: &AgentCheckpoint,
166 run: &RunContext,
167 state: &AgentCheckpointState,
168 ) -> Result<Self, AgentError> {
169 let payload = serialize(state)?;
170 let envelope = Checkpoint::initial(
171 handle.id,
172 run.run_id(),
173 CHECKPOINT_KIND,
174 CHECKPOINT_SCHEMA_VERSION,
175 payload,
176 );
177 handle.store.compare_and_swap(&envelope, None)?;
178 Ok(Self {
179 handle: handle.clone(),
180 envelope,
181 })
182 }
183
184 pub(crate) fn loaded(handle: &AgentCheckpoint, envelope: Checkpoint) -> Self {
185 Self {
186 handle: handle.clone(),
187 envelope,
188 }
189 }
190
191 pub(crate) fn save(&mut self, state: &AgentCheckpointState) -> Result<(), AgentError> {
192 let next = self.envelope.next(serialize(state)?)?;
193 self.handle
194 .store
195 .compare_and_swap(&next, Some(self.envelope.revision))?;
196 self.envelope = next;
197 Ok(())
198 }
199
200 pub(crate) fn next(&self, state: &AgentCheckpointState) -> Result<Checkpoint, AgentError> {
201 self.envelope.next(serialize(state)?).map_err(Into::into)
202 }
203
204 pub(crate) const fn revision(&self) -> u64 {
205 self.envelope.revision
206 }
207
208 pub(crate) const fn id(&self) -> CheckpointId {
209 self.envelope.id
210 }
211}
212
213fn serialize(state: &AgentCheckpointState) -> Result<serde_json::Value, AgentError> {
214 serde_json::to_value(state).map_err(|error| {
215 CheckpointError::new(CheckpointErrorKind::InvalidPayload, error.to_string()).into()
216 })
217}