1use std::{fmt, sync::Arc};
2
3use runifold_core::{
4 CapabilityId, Checkpoint, CheckpointError, CheckpointErrorKind, CheckpointId, CheckpointStore,
5 RunContext, Usage,
6};
7use runifold_model::{Message, ModelRef, ModelResponse};
8use serde::{Deserialize, Serialize};
9use serde_json::Value;
10
11use crate::conversation::{ConversationId, ConversationVersion, MemoryNamespace};
12use crate::{
13 AgentError, AgentOutcome, TerminalRequirementFailure, TerminalReviewPolicy,
14 TerminalReviewerDescriptor, TurnReviewPolicy,
15};
16
17const CHECKPOINT_KIND: &str = "runifold.agent";
18const CHECKPOINT_SCHEMA_VERSION: u32 = 1;
19
20#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
22#[non_exhaustive]
23pub enum ResumePolicy {
24 #[default]
26 RejectAmbiguous,
27 RetryInterruptedTurn,
31}
32
33#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
35#[serde(tag = "state", rename_all = "snake_case")]
36#[non_exhaustive]
37pub enum AgentCheckpointPhase {
38 ReadyForTurn,
40 TurnInFlight {
42 turn: u32,
44 },
45 Completed {
47 response: Box<ModelResponse>,
49 },
50 TerminalRequirementFailed {
53 failure: TerminalRequirementFailure,
55 attempts: u32,
57 },
58 TurnReviewReady {
61 response: Box<ModelResponse>,
63 turn: u32,
65 },
66 TurnReviewInFlight {
69 response: Box<ModelResponse>,
71 turn: u32,
73 },
74 TurnReviewApproved {
77 response: Box<ModelResponse>,
79 turn: u32,
81 },
82 TurnReviewRejected {
84 response: Box<ModelResponse>,
86 turn: u32,
88 reason: String,
90 attempts: u32,
92 },
93 TurnReviewExhausted {
95 response: Box<ModelResponse>,
97 turn: u32,
99 feedback: Value,
101 attempts: u32,
103 },
104 TerminalReviewReady {
106 response: Box<ModelResponse>,
108 attempt: u32,
110 },
111 TerminalReviewInFlight {
113 response: Box<ModelResponse>,
115 attempt: u32,
117 },
118 TerminalReviewRejected {
120 response: Box<ModelResponse>,
122 reason: String,
124 attempts: u32,
126 },
127 TerminalReviewExhausted {
129 response: Box<ModelResponse>,
131 feedback: Value,
133 attempts: u32,
135 },
136}
137
138#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
140pub struct DurableConversationCheckpoint {
141 pub conversation_id: ConversationId,
143 pub namespace: MemoryNamespace,
145 pub expected_version: ConversationVersion,
147 pub persisted_prefix_len: u64,
149}
150
151#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
156pub struct AgentRecoveryContract {
157 pub(crate) instructions: Vec<Message>,
158 pub(crate) context: Vec<Message>,
159 pub(crate) tools: Vec<runifold_tool::ToolDescriptor>,
160 pub(crate) agents: Vec<crate::AgentDescriptor>,
161 pub(crate) retrieval: Vec<(runifold_core::CapabilityDescriptor, usize)>,
162 pub(crate) generation: runifold_model::GenerationOptions,
163 pub(crate) output_format: runifold_model::OutputFormat,
164 pub(crate) response_mode: runifold_model::ResponseMode,
165 pub(crate) provider_tools: Vec<runifold_model::ProviderToolSpec>,
166 pub(crate) provider_options: std::collections::BTreeMap<String, Value>,
167 pub(crate) config: crate::AgentConfig,
168 pub(crate) tool_concurrency: std::num::NonZeroUsize,
169 pub(crate) min_successful_tool_calls: u32,
170 pub(crate) completion: crate::CompletionRequirement,
171 pub(crate) retry_safe_effects: bool,
172}
173
174#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
176pub struct AgentCheckpointState {
177 #[serde(default)]
180 pub recovery_contract: Option<AgentRecoveryContract>,
181 pub execution_id: String,
183 pub agent: String,
185 pub model: ModelRef,
187 pub transcript: Vec<Message>,
189 pub turns: u32,
191 pub tool_calls: u32,
193 pub delegations: u32,
195 pub usage: Usage,
197 #[serde(default, skip_serializing_if = "Option::is_none")]
199 pub turn_reviewer: Option<TerminalReviewerDescriptor>,
200 #[serde(default, skip_serializing_if = "Option::is_none")]
202 pub turn_review_policy: Option<TurnReviewPolicy>,
203 #[serde(default, skip_serializing_if = "Vec::is_empty")]
205 pub turn_reviewer_capabilities: Vec<CapabilityId>,
206 #[serde(default, skip_serializing_if = "Option::is_none")]
208 pub terminal_reviewer: Option<TerminalReviewerDescriptor>,
209 #[serde(default, skip_serializing_if = "Option::is_none")]
211 pub terminal_review_policy: Option<TerminalReviewPolicy>,
212 #[serde(default, skip_serializing_if = "Vec::is_empty")]
214 pub terminal_reviewer_capabilities: Vec<CapabilityId>,
215 pub phase: AgentCheckpointPhase,
217 #[serde(default, skip_serializing_if = "Option::is_none")]
219 pub durable_conversation: Option<DurableConversationCheckpoint>,
220}
221
222impl AgentCheckpointState {
223 pub(crate) fn outcome(&self) -> Option<AgentOutcome> {
224 match &self.phase {
225 AgentCheckpointPhase::Completed { response } => Some(AgentOutcome {
226 response: response.as_ref().clone(),
227 transcript: self.transcript.clone(),
228 turns: self.turns,
229 tool_calls: self.tool_calls,
230 delegations: self.delegations,
231 usage: self.usage,
232 }),
233 _ => None,
234 }
235 }
236
237 pub(crate) fn terminal_failure(&self) -> Option<AgentError> {
238 match &self.phase {
239 AgentCheckpointPhase::TerminalRequirementFailed {
240 failure, attempts, ..
241 } => Some(super::agent::completion::failure_error(failure, *attempts)),
242 AgentCheckpointPhase::TerminalReviewRejected { reason, .. } => {
243 Some(AgentError::TerminalReviewRejected {
244 reason: reason.clone(),
245 })
246 }
247 AgentCheckpointPhase::TerminalReviewExhausted { attempts, .. } => {
248 Some(AgentError::TerminalReviewExhausted {
249 attempts: *attempts,
250 })
251 }
252 AgentCheckpointPhase::TurnReviewRejected { reason, .. } => {
253 Some(AgentError::TurnReviewRejected {
254 reason: reason.clone(),
255 })
256 }
257 AgentCheckpointPhase::TurnReviewExhausted { attempts, .. } => {
258 Some(AgentError::TurnReviewExhausted {
259 attempts: *attempts,
260 })
261 }
262 _ => None,
263 }
264 }
265}
266
267#[derive(Clone)]
269pub struct AgentCheckpoint {
270 id: CheckpointId,
271 store: Arc<dyn CheckpointStore>,
272}
273
274impl AgentCheckpoint {
275 pub fn new(store: Arc<dyn CheckpointStore>) -> Self {
277 Self {
278 id: CheckpointId::new(),
279 store,
280 }
281 }
282
283 pub fn existing(id: CheckpointId, store: Arc<dyn CheckpointStore>) -> Self {
285 Self { id, store }
286 }
287
288 pub const fn id(&self) -> CheckpointId {
290 self.id
291 }
292
293 pub fn load(&self) -> Result<(Checkpoint, AgentCheckpointState), CheckpointError> {
299 let checkpoint = self.store.load(self.id)?;
300 if checkpoint.kind != CHECKPOINT_KIND
301 || checkpoint.schema_version != CHECKPOINT_SCHEMA_VERSION
302 {
303 return Err(CheckpointError::new(
304 CheckpointErrorKind::InvalidPayload,
305 "checkpoint kind or schema version is not supported",
306 ));
307 }
308 let state = serde_json::from_value(checkpoint.payload.clone()).map_err(|error| {
309 CheckpointError::new(CheckpointErrorKind::InvalidPayload, error.to_string())
310 })?;
311 Ok((checkpoint, state))
312 }
313}
314
315impl fmt::Debug for AgentCheckpoint {
316 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
317 formatter
318 .debug_struct("AgentCheckpoint")
319 .field("id", &self.id)
320 .finish_non_exhaustive()
321 }
322}
323
324pub(crate) struct CheckpointCursor {
325 handle: AgentCheckpoint,
326 envelope: Checkpoint,
327}
328
329impl CheckpointCursor {
330 pub(crate) fn create(
331 handle: &AgentCheckpoint,
332 run: &RunContext,
333 state: &AgentCheckpointState,
334 ) -> Result<Self, AgentError> {
335 let payload = serialize(state)?;
336 let envelope = Checkpoint::initial(
337 handle.id,
338 run.run_id(),
339 CHECKPOINT_KIND,
340 CHECKPOINT_SCHEMA_VERSION,
341 payload,
342 );
343 handle.store.compare_and_swap(&envelope, None)?;
344 Ok(Self {
345 handle: handle.clone(),
346 envelope,
347 })
348 }
349
350 pub(crate) fn loaded(handle: &AgentCheckpoint, envelope: Checkpoint) -> Self {
351 Self {
352 handle: handle.clone(),
353 envelope,
354 }
355 }
356
357 pub(crate) fn save(&mut self, state: &AgentCheckpointState) -> Result<(), AgentError> {
358 let next = self.envelope.next(serialize(state)?)?;
359 self.handle
360 .store
361 .compare_and_swap(&next, Some(self.envelope.revision))?;
362 self.envelope = next;
363 Ok(())
364 }
365
366 pub(crate) fn next(&self, state: &AgentCheckpointState) -> Result<Checkpoint, AgentError> {
367 self.envelope.next(serialize(state)?).map_err(Into::into)
368 }
369
370 pub(crate) const fn revision(&self) -> u64 {
371 self.envelope.revision
372 }
373
374 pub(crate) const fn id(&self) -> CheckpointId {
375 self.envelope.id
376 }
377}
378
379fn serialize(state: &AgentCheckpointState) -> Result<serde_json::Value, AgentError> {
380 serde_json::to_value(state).map_err(|error| {
381 CheckpointError::new(CheckpointErrorKind::InvalidPayload, error.to_string()).into()
382 })
383}