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, TerminalRequirementFailure};
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 TerminalRequirementFailed {
47 failure: TerminalRequirementFailure,
49 attempts: u32,
51 },
52}
53
54#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
56pub struct DurableConversationCheckpoint {
57 pub conversation_id: ConversationId,
59 pub namespace: MemoryNamespace,
61 pub expected_version: ConversationVersion,
63 pub persisted_prefix_len: u64,
65}
66
67#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
69pub struct AgentCheckpointState {
70 pub execution_id: String,
72 pub agent: String,
74 pub model: ModelRef,
76 pub transcript: Vec<Message>,
78 pub turns: u32,
80 pub tool_calls: u32,
82 pub delegations: u32,
84 pub usage: Usage,
86 pub phase: AgentCheckpointPhase,
88 #[serde(default, skip_serializing_if = "Option::is_none")]
90 pub durable_conversation: Option<DurableConversationCheckpoint>,
91}
92
93impl AgentCheckpointState {
94 pub(crate) fn outcome(&self) -> Option<AgentOutcome> {
95 match &self.phase {
96 AgentCheckpointPhase::Completed { response } => Some(AgentOutcome {
97 response: response.as_ref().clone(),
98 transcript: self.transcript.clone(),
99 turns: self.turns,
100 tool_calls: self.tool_calls,
101 delegations: self.delegations,
102 usage: self.usage,
103 }),
104 _ => None,
105 }
106 }
107
108 pub(crate) fn terminal_failure(&self) -> Option<AgentError> {
109 match &self.phase {
110 AgentCheckpointPhase::TerminalRequirementFailed {
111 failure, attempts, ..
112 } => Some(super::agent::completion::failure_error(failure, *attempts)),
113 _ => None,
114 }
115 }
116}
117
118#[derive(Clone)]
120pub struct AgentCheckpoint {
121 id: CheckpointId,
122 store: Arc<dyn CheckpointStore>,
123}
124
125impl AgentCheckpoint {
126 pub fn new(store: Arc<dyn CheckpointStore>) -> Self {
128 Self {
129 id: CheckpointId::new(),
130 store,
131 }
132 }
133
134 pub fn existing(id: CheckpointId, store: Arc<dyn CheckpointStore>) -> Self {
136 Self { id, store }
137 }
138
139 pub const fn id(&self) -> CheckpointId {
141 self.id
142 }
143
144 pub fn load(&self) -> Result<(Checkpoint, AgentCheckpointState), CheckpointError> {
150 let checkpoint = self.store.load(self.id)?;
151 if checkpoint.kind != CHECKPOINT_KIND
152 || checkpoint.schema_version != CHECKPOINT_SCHEMA_VERSION
153 {
154 return Err(CheckpointError::new(
155 CheckpointErrorKind::InvalidPayload,
156 "checkpoint kind or schema version is not supported",
157 ));
158 }
159 let state = serde_json::from_value(checkpoint.payload.clone()).map_err(|error| {
160 CheckpointError::new(CheckpointErrorKind::InvalidPayload, error.to_string())
161 })?;
162 Ok((checkpoint, state))
163 }
164}
165
166impl fmt::Debug for AgentCheckpoint {
167 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
168 formatter
169 .debug_struct("AgentCheckpoint")
170 .field("id", &self.id)
171 .finish_non_exhaustive()
172 }
173}
174
175pub(crate) struct CheckpointCursor {
176 handle: AgentCheckpoint,
177 envelope: Checkpoint,
178}
179
180impl CheckpointCursor {
181 pub(crate) fn create(
182 handle: &AgentCheckpoint,
183 run: &RunContext,
184 state: &AgentCheckpointState,
185 ) -> Result<Self, AgentError> {
186 let payload = serialize(state)?;
187 let envelope = Checkpoint::initial(
188 handle.id,
189 run.run_id(),
190 CHECKPOINT_KIND,
191 CHECKPOINT_SCHEMA_VERSION,
192 payload,
193 );
194 handle.store.compare_and_swap(&envelope, None)?;
195 Ok(Self {
196 handle: handle.clone(),
197 envelope,
198 })
199 }
200
201 pub(crate) fn loaded(handle: &AgentCheckpoint, envelope: Checkpoint) -> Self {
202 Self {
203 handle: handle.clone(),
204 envelope,
205 }
206 }
207
208 pub(crate) fn save(&mut self, state: &AgentCheckpointState) -> Result<(), AgentError> {
209 let next = self.envelope.next(serialize(state)?)?;
210 self.handle
211 .store
212 .compare_and_swap(&next, Some(self.envelope.revision))?;
213 self.envelope = next;
214 Ok(())
215 }
216
217 pub(crate) fn next(&self, state: &AgentCheckpointState) -> Result<Checkpoint, AgentError> {
218 self.envelope.next(serialize(state)?).map_err(Into::into)
219 }
220
221 pub(crate) const fn revision(&self) -> u64 {
222 self.envelope.revision
223 }
224
225 pub(crate) const fn id(&self) -> CheckpointId {
226 self.envelope.id
227 }
228}
229
230fn serialize(state: &AgentCheckpointState) -> Result<serde_json::Value, AgentError> {
231 serde_json::to_value(state).map_err(|error| {
232 CheckpointError::new(CheckpointErrorKind::InvalidPayload, error.to_string()).into()
233 })
234}