Skip to main content

aster/agents/resume/
resumer.rs

1//! Agent Resumer
2//!
3//! Provides agent resume capabilities including
4//! resume point detection and state restoration.
5//!
6//! This module provides:
7//! - Resume capability checking
8//! - Resume point information retrieval
9//! - Agent state restoration from checkpoints
10//! - Resume summary generation
11
12use serde::{Deserialize, Serialize};
13use thiserror::Error;
14
15use super::{AgentState, AgentStateManager, AgentStateStatus, Checkpoint, StateManagerError};
16
17/// Result type alias for resumer operations
18pub type ResumerResult<T> = Result<T, ResumerError>;
19
20/// Error types for resumer operations
21#[derive(Debug, Error)]
22pub enum ResumerError {
23    /// Agent not found
24    #[error("Agent not found: {0}")]
25    AgentNotFound(String),
26
27    /// Agent cannot be resumed
28    #[error("Agent cannot be resumed: {0}")]
29    CannotResume(String),
30
31    /// Checkpoint not found
32    #[error("Checkpoint not found: {0}")]
33    CheckpointNotFound(String),
34
35    /// State manager error
36    #[error("State manager error: {0}")]
37    StateManager(#[from] StateManagerError),
38
39    /// Invalid resume point
40    #[error("Invalid resume point: {0}")]
41    InvalidResumePoint(String),
42}
43
44/// Resume point specification
45#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
46#[serde(rename_all = "snake_case")]
47pub enum ResumePoint {
48    /// Resume from the last saved state
49    #[default]
50    Last,
51    /// Resume from a specific checkpoint by ID
52    Checkpoint(String),
53    /// Resume from the beginning (restart)
54    Beginning,
55}
56
57/// Options for resuming an agent
58#[derive(Debug, Clone, Serialize, Deserialize)]
59#[serde(rename_all = "camelCase")]
60pub struct ResumeOptions {
61    /// Agent ID to resume
62    pub agent_id: String,
63    /// Where to resume from
64    #[serde(default)]
65    pub continue_from: ResumePoint,
66    /// Whether to reset error state
67    #[serde(default)]
68    pub reset_errors: bool,
69    /// Additional context to add on resume
70    pub additional_context: Option<String>,
71}
72
73impl ResumeOptions {
74    /// Create new resume options
75    pub fn new(agent_id: impl Into<String>) -> Self {
76        Self {
77            agent_id: agent_id.into(),
78            continue_from: ResumePoint::Last,
79            reset_errors: false,
80            additional_context: None,
81        }
82    }
83
84    /// Set resume point
85    pub fn from_point(mut self, point: ResumePoint) -> Self {
86        self.continue_from = point;
87        self
88    }
89
90    /// Set to resume from a specific checkpoint
91    pub fn from_checkpoint(mut self, checkpoint_id: impl Into<String>) -> Self {
92        self.continue_from = ResumePoint::Checkpoint(checkpoint_id.into());
93        self
94    }
95
96    /// Set to reset errors on resume
97    pub fn with_reset_errors(mut self, reset: bool) -> Self {
98        self.reset_errors = reset;
99        self
100    }
101
102    /// Set additional context
103    pub fn with_additional_context(mut self, context: impl Into<String>) -> Self {
104        self.additional_context = Some(context.into());
105        self
106    }
107}
108
109/// Information about a resume point
110#[derive(Debug, Clone, Serialize, Deserialize)]
111#[serde(rename_all = "camelCase")]
112pub struct ResumePointInfo {
113    /// Whether the agent can be resumed
114    pub can_resume: bool,
115    /// Agent ID
116    pub agent_id: String,
117    /// Current status
118    pub status: AgentStateStatus,
119    /// Current step number
120    pub step: usize,
121    /// Total steps (if known)
122    pub total_steps: Option<usize>,
123    /// Whether checkpoints are available
124    pub checkpoint_available: bool,
125    /// Last checkpoint (if any)
126    pub last_checkpoint: Option<Checkpoint>,
127    /// Number of errors encountered
128    pub error_count: usize,
129    /// Suggestions for resuming
130    pub suggestions: Option<Vec<String>>,
131}
132
133impl ResumePointInfo {
134    /// Create a new resume point info for a non-existent agent
135    pub fn not_found(agent_id: impl Into<String>) -> Self {
136        Self {
137            can_resume: false,
138            agent_id: agent_id.into(),
139            status: AgentStateStatus::default(),
140            step: 0,
141            total_steps: None,
142            checkpoint_available: false,
143            last_checkpoint: None,
144            error_count: 0,
145            suggestions: Some(vec![
146                "Agent state not found. Start a new agent instead.".to_string()
147            ]),
148        }
149    }
150
151    /// Create resume point info from an agent state
152    pub fn from_state(state: &AgentState) -> Self {
153        let can_resume = state.can_resume();
154        let checkpoint_available = !state.checkpoints.is_empty();
155        let last_checkpoint = state.latest_checkpoint().cloned();
156
157        let mut suggestions = Vec::new();
158
159        if can_resume {
160            if checkpoint_available {
161                suggestions
162                    .push("Resume from the last checkpoint for a clean restart.".to_string());
163            }
164            if state.error_count > 0 {
165                suggestions.push(format!(
166                    "Consider resetting errors ({} errors encountered).",
167                    state.error_count
168                ));
169            }
170            if state.status == AgentStateStatus::Failed {
171                suggestions.push("Agent failed. Review errors before resuming.".to_string());
172            }
173        } else {
174            match state.status {
175                AgentStateStatus::Completed => {
176                    suggestions.push("Agent completed successfully. No resume needed.".to_string());
177                }
178                AgentStateStatus::Cancelled => {
179                    suggestions.push("Agent was cancelled. Start a new agent instead.".to_string());
180                }
181                _ => {}
182            }
183        }
184
185        Self {
186            can_resume,
187            agent_id: state.id.clone(),
188            status: state.status,
189            step: state.current_step,
190            total_steps: state.total_steps,
191            checkpoint_available,
192            last_checkpoint,
193            error_count: state.error_count,
194            suggestions: if suggestions.is_empty() {
195                None
196            } else {
197                Some(suggestions)
198            },
199        }
200    }
201}
202
203/// Agent Resumer for resuming interrupted agents
204#[derive(Debug)]
205pub struct AgentResumer {
206    /// State manager for loading/saving states
207    state_manager: AgentStateManager,
208}
209
210impl AgentResumer {
211    /// Create a new AgentResumer
212    pub fn new(state_manager: AgentStateManager) -> Self {
213        Self { state_manager }
214    }
215
216    /// Get a reference to the state manager
217    pub fn state_manager(&self) -> &AgentStateManager {
218        &self.state_manager
219    }
220
221    /// Check if an agent can be resumed
222    pub async fn can_resume(&self, id: &str) -> bool {
223        match self.state_manager.load_state(id).await {
224            Ok(Some(state)) => state.can_resume(),
225            _ => false,
226        }
227    }
228
229    /// Get resume point information for an agent
230    pub async fn get_resume_point(&self, id: &str) -> ResumePointInfo {
231        match self.state_manager.load_state(id).await {
232            Ok(Some(state)) => ResumePointInfo::from_state(&state),
233            _ => ResumePointInfo::not_found(id),
234        }
235    }
236
237    /// Resume an agent from a saved state
238    ///
239    /// This method loads the agent state and optionally:
240    /// - Restores from a specific checkpoint
241    /// - Resets error state
242    /// - Adds additional context
243    pub async fn resume(&self, options: ResumeOptions) -> ResumerResult<AgentState> {
244        // Load the state
245        let state = self.state_manager.load_state(&options.agent_id).await?;
246        let mut state =
247            state.ok_or_else(|| ResumerError::AgentNotFound(options.agent_id.clone()))?;
248
249        // Check if resumable
250        if !state.can_resume() {
251            return Err(ResumerError::CannotResume(format!(
252                "Agent {} is in status {:?} and cannot be resumed",
253                options.agent_id, state.status
254            )));
255        }
256
257        // Handle resume point
258        match &options.continue_from {
259            ResumePoint::Last => {
260                // Resume from current state - no changes needed
261            }
262            ResumePoint::Checkpoint(checkpoint_id) => {
263                // Find and restore from checkpoint
264                let checkpoint = self
265                    .state_manager
266                    .load_checkpoint(&options.agent_id, checkpoint_id)
267                    .await?
268                    .ok_or_else(|| ResumerError::CheckpointNotFound(checkpoint_id.clone()))?;
269
270                state.restore_from_checkpoint(&checkpoint);
271            }
272            ResumePoint::Beginning => {
273                // Reset to beginning
274                state.current_step = 0;
275                state.messages.clear();
276                state.tool_calls.clear();
277                state.results.clear();
278                state.checkpoint = None;
279            }
280        }
281
282        // Reset errors if requested
283        if options.reset_errors {
284            state.reset_errors();
285        }
286
287        // Add additional context if provided
288        if let Some(context) = &options.additional_context {
289            state.set_metadata("additional_context", serde_json::json!(context));
290        }
291
292        // Update status to running if it was paused or failed
293        if state.status == AgentStateStatus::Paused || state.status == AgentStateStatus::Failed {
294            state.status = AgentStateStatus::Running;
295        }
296
297        // Save the updated state
298        self.state_manager.save_state(&state).await?;
299
300        Ok(state)
301    }
302
303    /// Create a summary of the resume point for an agent
304    ///
305    /// This generates a human-readable summary of the agent's current state
306    /// and what would happen if it were resumed.
307    pub async fn create_resume_summary(&self, id: &str) -> ResumerResult<String> {
308        let state = self.state_manager.load_state(id).await?;
309        let state = state.ok_or_else(|| ResumerError::AgentNotFound(id.to_string()))?;
310
311        let mut summary = String::new();
312
313        // Header
314        summary.push_str(&format!("# Resume Summary for Agent: {}\n\n", state.id));
315
316        // Status
317        summary.push_str("## Status\n");
318        summary.push_str(&format!("- Current Status: {:?}\n", state.status));
319        summary.push_str(&format!("- Can Resume: {}\n", state.can_resume()));
320        summary.push_str(&format!("- Agent Type: {}\n\n", state.agent_type));
321
322        // Progress
323        summary.push_str("## Progress\n");
324        summary.push_str(&format!("- Current Step: {}\n", state.current_step));
325        if let Some(total) = state.total_steps {
326            summary.push_str(&format!("- Total Steps: {}\n", total));
327            let progress = (state.current_step as f64 / total as f64 * 100.0).min(100.0);
328            summary.push_str(&format!("- Progress: {:.1}%\n", progress));
329        }
330        summary.push_str(&format!("- Messages: {}\n", state.messages.len()));
331        summary.push_str(&format!("- Tool Calls: {}\n", state.tool_calls.len()));
332        summary.push_str(&format!("- Results: {}\n\n", state.results.len()));
333
334        // Errors
335        if state.error_count > 0 || state.retry_count > 0 {
336            summary.push_str("## Errors\n");
337            summary.push_str(&format!("- Error Count: {}\n", state.error_count));
338            summary.push_str(&format!("- Retry Count: {}\n", state.retry_count));
339            summary.push_str(&format!("- Max Retries: {}\n\n", state.max_retries));
340        }
341
342        // Checkpoints
343        summary.push_str("## Checkpoints\n");
344        if state.checkpoints.is_empty() {
345            summary.push_str("- No checkpoints available\n\n");
346        } else {
347            summary.push_str(&format!(
348                "- Available Checkpoints: {}\n",
349                state.checkpoints.len()
350            ));
351            for (i, cp) in state.checkpoints.iter().enumerate() {
352                let name = cp.name.as_deref().unwrap_or("unnamed");
353                summary.push_str(&format!("  {}. {} (step {})\n", i + 1, name, cp.step));
354            }
355            summary.push('\n');
356        }
357
358        // Timestamps
359        summary.push_str("## Timestamps\n");
360        summary.push_str(&format!(
361            "- Created: {}\n",
362            state.created_at.format("%Y-%m-%d %H:%M:%S UTC")
363        ));
364        summary.push_str(&format!(
365            "- Last Updated: {}\n\n",
366            state.updated_at.format("%Y-%m-%d %H:%M:%S UTC")
367        ));
368
369        // Original prompt (truncated if too long)
370        summary.push_str("## Original Prompt\n");
371        let prompt_preview = if state.prompt.len() > 200 {
372            // Use char_indices to find a safe UTF-8 boundary
373            let truncate_at = state
374                .prompt
375                .char_indices()
376                .take_while(|(i, _)| *i < 200)
377                .last()
378                .map(|(i, c)| i + c.len_utf8())
379                .unwrap_or(0);
380            format!(
381                "{}...",
382                state.prompt.get(..truncate_at).unwrap_or(&state.prompt)
383            )
384        } else {
385            state.prompt.clone()
386        };
387        summary.push_str(&format!("{}\n\n", prompt_preview));
388
389        // Recommendations
390        summary.push_str("## Recommendations\n");
391        if !state.can_resume() {
392            match state.status {
393                AgentStateStatus::Completed => {
394                    summary.push_str("- Agent completed successfully. No resume needed.\n");
395                }
396                AgentStateStatus::Cancelled => {
397                    summary.push_str("- Agent was cancelled. Consider starting a new agent.\n");
398                }
399                _ => {}
400            }
401        } else {
402            if !state.checkpoints.is_empty() {
403                summary.push_str(
404                    "- Consider resuming from the latest checkpoint for a clean restart.\n",
405                );
406            }
407            if state.error_count > 0 {
408                summary.push_str(&format!(
409                    "- {} errors encountered. Consider using reset_errors option.\n",
410                    state.error_count
411                ));
412            }
413            if state.status == AgentStateStatus::Failed {
414                summary.push_str("- Agent failed. Review errors before resuming.\n");
415            }
416            if state.status == AgentStateStatus::Paused {
417                summary.push_str("- Agent is paused. Resume to continue execution.\n");
418            }
419        }
420
421        Ok(summary)
422    }
423}
424
425#[cfg(test)]
426mod tests {
427    use super::*;
428    use tempfile::TempDir;
429
430    fn create_test_state(id: &str) -> AgentState {
431        AgentState::new(id, "test_agent", "Test prompt")
432    }
433
434    #[test]
435    fn test_resume_point_default() {
436        let point = ResumePoint::default();
437        assert_eq!(point, ResumePoint::Last);
438    }
439
440    #[test]
441    fn test_resume_options_builder() {
442        let options = ResumeOptions::new("agent-1")
443            .from_checkpoint("cp-1")
444            .with_reset_errors(true)
445            .with_additional_context("Extra context");
446
447        assert_eq!(options.agent_id, "agent-1");
448        assert_eq!(
449            options.continue_from,
450            ResumePoint::Checkpoint("cp-1".to_string())
451        );
452        assert!(options.reset_errors);
453        assert_eq!(
454            options.additional_context,
455            Some("Extra context".to_string())
456        );
457    }
458
459    #[test]
460    fn test_resume_point_info_not_found() {
461        let info = ResumePointInfo::not_found("agent-1");
462
463        assert!(!info.can_resume);
464        assert_eq!(info.agent_id, "agent-1");
465        assert!(info.suggestions.is_some());
466    }
467
468    #[test]
469    fn test_resume_point_info_from_running_state() {
470        let state = create_test_state("agent-1");
471        let info = ResumePointInfo::from_state(&state);
472
473        assert!(info.can_resume);
474        assert_eq!(info.agent_id, "agent-1");
475        assert_eq!(info.status, AgentStateStatus::Running);
476        assert!(!info.checkpoint_available);
477    }
478
479    #[test]
480    fn test_resume_point_info_from_completed_state() {
481        let state = create_test_state("agent-1").with_status(AgentStateStatus::Completed);
482        let info = ResumePointInfo::from_state(&state);
483
484        assert!(!info.can_resume);
485        assert!(info.suggestions.is_some());
486        let suggestions = info.suggestions.unwrap();
487        assert!(suggestions.iter().any(|s| s.contains("completed")));
488    }
489
490    #[test]
491    fn test_resume_point_info_from_failed_state() {
492        let mut state = create_test_state("agent-1").with_status(AgentStateStatus::Failed);
493        state.error_count = 3;
494        let info = ResumePointInfo::from_state(&state);
495
496        assert!(info.can_resume);
497        assert_eq!(info.error_count, 3);
498        assert!(info.suggestions.is_some());
499    }
500
501    #[test]
502    fn test_resume_point_info_with_checkpoint() {
503        let mut state = create_test_state("agent-1");
504        state.create_checkpoint(Some("test-checkpoint"));
505        let info = ResumePointInfo::from_state(&state);
506
507        assert!(info.can_resume);
508        assert!(info.checkpoint_available);
509        assert!(info.last_checkpoint.is_some());
510    }
511
512    #[tokio::test]
513    async fn test_resumer_can_resume_nonexistent() {
514        let temp_dir = TempDir::new().unwrap();
515        let state_manager = AgentStateManager::new(Some(temp_dir.path().to_path_buf()));
516        let resumer = AgentResumer::new(state_manager);
517
518        assert!(!resumer.can_resume("nonexistent").await);
519    }
520
521    #[tokio::test]
522    async fn test_resumer_can_resume_running() {
523        let temp_dir = TempDir::new().unwrap();
524        let state_manager = AgentStateManager::new(Some(temp_dir.path().to_path_buf()));
525
526        let state = create_test_state("agent-1");
527        state_manager.save_state(&state).await.unwrap();
528
529        let resumer = AgentResumer::new(state_manager);
530        assert!(resumer.can_resume("agent-1").await);
531    }
532
533    #[tokio::test]
534    async fn test_resumer_can_resume_completed() {
535        let temp_dir = TempDir::new().unwrap();
536        let state_manager = AgentStateManager::new(Some(temp_dir.path().to_path_buf()));
537
538        let state = create_test_state("agent-1").with_status(AgentStateStatus::Completed);
539        state_manager.save_state(&state).await.unwrap();
540
541        let resumer = AgentResumer::new(state_manager);
542        assert!(!resumer.can_resume("agent-1").await);
543    }
544
545    #[tokio::test]
546    async fn test_resumer_get_resume_point_nonexistent() {
547        let temp_dir = TempDir::new().unwrap();
548        let state_manager = AgentStateManager::new(Some(temp_dir.path().to_path_buf()));
549        let resumer = AgentResumer::new(state_manager);
550
551        let info = resumer.get_resume_point("nonexistent").await;
552        assert!(!info.can_resume);
553        assert_eq!(info.agent_id, "nonexistent");
554    }
555
556    #[tokio::test]
557    async fn test_resumer_get_resume_point_existing() {
558        let temp_dir = TempDir::new().unwrap();
559        let state_manager = AgentStateManager::new(Some(temp_dir.path().to_path_buf()));
560
561        let mut state = create_test_state("agent-1");
562        state.current_step = 5;
563        state.total_steps = Some(10);
564        state_manager.save_state(&state).await.unwrap();
565
566        let resumer = AgentResumer::new(state_manager);
567        let info = resumer.get_resume_point("agent-1").await;
568
569        assert!(info.can_resume);
570        assert_eq!(info.agent_id, "agent-1");
571        assert_eq!(info.step, 5);
572        assert_eq!(info.total_steps, Some(10));
573    }
574
575    #[tokio::test]
576    async fn test_resumer_resume_from_last() {
577        let temp_dir = TempDir::new().unwrap();
578        let state_manager = AgentStateManager::new(Some(temp_dir.path().to_path_buf()));
579
580        let mut state = create_test_state("agent-1").with_status(AgentStateStatus::Paused);
581        state.current_step = 5;
582        state_manager.save_state(&state).await.unwrap();
583
584        let resumer = AgentResumer::new(state_manager);
585        let options = ResumeOptions::new("agent-1");
586
587        let resumed = resumer.resume(options).await.unwrap();
588
589        assert_eq!(resumed.current_step, 5);
590        assert_eq!(resumed.status, AgentStateStatus::Running);
591    }
592
593    #[tokio::test]
594    async fn test_resumer_resume_from_checkpoint() {
595        let temp_dir = TempDir::new().unwrap();
596        let state_manager = AgentStateManager::new(Some(temp_dir.path().to_path_buf()));
597
598        // Create state with checkpoint
599        let mut state = create_test_state("agent-1");
600        state.current_step = 3;
601        let checkpoint = state.create_checkpoint(Some("cp-1"));
602
603        // Advance state further
604        state.current_step = 10;
605        state.add_result(serde_json::json!({"result": "later"}));
606        state_manager.save_state(&state).await.unwrap();
607        state_manager.save_checkpoint(&checkpoint).await.unwrap();
608
609        let resumer = AgentResumer::new(state_manager);
610        let options = ResumeOptions::new("agent-1").from_checkpoint(&checkpoint.id);
611
612        let resumed = resumer.resume(options).await.unwrap();
613
614        // Should be restored to checkpoint state
615        assert_eq!(resumed.current_step, 3);
616    }
617
618    #[tokio::test]
619    async fn test_resumer_resume_from_beginning() {
620        let temp_dir = TempDir::new().unwrap();
621        let state_manager = AgentStateManager::new(Some(temp_dir.path().to_path_buf()));
622
623        let mut state = create_test_state("agent-1");
624        state.current_step = 10;
625        state.add_result(serde_json::json!({"result": "test"}));
626        state_manager.save_state(&state).await.unwrap();
627
628        let resumer = AgentResumer::new(state_manager);
629        let options = ResumeOptions::new("agent-1").from_point(ResumePoint::Beginning);
630
631        let resumed = resumer.resume(options).await.unwrap();
632
633        assert_eq!(resumed.current_step, 0);
634        assert!(resumed.messages.is_empty());
635        assert!(resumed.tool_calls.is_empty());
636        assert!(resumed.results.is_empty());
637    }
638
639    #[tokio::test]
640    async fn test_resumer_resume_with_reset_errors() {
641        let temp_dir = TempDir::new().unwrap();
642        let state_manager = AgentStateManager::new(Some(temp_dir.path().to_path_buf()));
643
644        let mut state = create_test_state("agent-1").with_status(AgentStateStatus::Failed);
645        state.error_count = 5;
646        state.retry_count = 3;
647        state_manager.save_state(&state).await.unwrap();
648
649        let resumer = AgentResumer::new(state_manager);
650        let options = ResumeOptions::new("agent-1").with_reset_errors(true);
651
652        let resumed = resumer.resume(options).await.unwrap();
653
654        assert_eq!(resumed.error_count, 0);
655        assert_eq!(resumed.retry_count, 0);
656        assert_eq!(resumed.status, AgentStateStatus::Running);
657    }
658
659    #[tokio::test]
660    async fn test_resumer_resume_with_additional_context() {
661        let temp_dir = TempDir::new().unwrap();
662        let state_manager = AgentStateManager::new(Some(temp_dir.path().to_path_buf()));
663
664        let state = create_test_state("agent-1");
665        state_manager.save_state(&state).await.unwrap();
666
667        let resumer = AgentResumer::new(state_manager);
668        let options =
669            ResumeOptions::new("agent-1").with_additional_context("Extra context for resume");
670
671        let resumed = resumer.resume(options).await.unwrap();
672
673        let context = resumed.metadata.get("additional_context");
674        assert!(context.is_some());
675        assert_eq!(
676            context.unwrap(),
677            &serde_json::json!("Extra context for resume")
678        );
679    }
680
681    #[tokio::test]
682    async fn test_resumer_resume_nonexistent_fails() {
683        let temp_dir = TempDir::new().unwrap();
684        let state_manager = AgentStateManager::new(Some(temp_dir.path().to_path_buf()));
685        let resumer = AgentResumer::new(state_manager);
686
687        let options = ResumeOptions::new("nonexistent");
688        let result = resumer.resume(options).await;
689
690        assert!(result.is_err());
691        assert!(matches!(
692            result.unwrap_err(),
693            ResumerError::AgentNotFound(_)
694        ));
695    }
696
697    #[tokio::test]
698    async fn test_resumer_resume_completed_fails() {
699        let temp_dir = TempDir::new().unwrap();
700        let state_manager = AgentStateManager::new(Some(temp_dir.path().to_path_buf()));
701
702        let state = create_test_state("agent-1").with_status(AgentStateStatus::Completed);
703        state_manager.save_state(&state).await.unwrap();
704
705        let resumer = AgentResumer::new(state_manager);
706        let options = ResumeOptions::new("agent-1");
707        let result = resumer.resume(options).await;
708
709        assert!(result.is_err());
710        assert!(matches!(result.unwrap_err(), ResumerError::CannotResume(_)));
711    }
712
713    #[tokio::test]
714    async fn test_resumer_resume_invalid_checkpoint_fails() {
715        let temp_dir = TempDir::new().unwrap();
716        let state_manager = AgentStateManager::new(Some(temp_dir.path().to_path_buf()));
717
718        let state = create_test_state("agent-1");
719        state_manager.save_state(&state).await.unwrap();
720
721        let resumer = AgentResumer::new(state_manager);
722        let options = ResumeOptions::new("agent-1").from_checkpoint("nonexistent-checkpoint");
723        let result = resumer.resume(options).await;
724
725        assert!(result.is_err());
726        assert!(matches!(
727            result.unwrap_err(),
728            ResumerError::CheckpointNotFound(_)
729        ));
730    }
731
732    #[tokio::test]
733    async fn test_resumer_create_resume_summary() {
734        let temp_dir = TempDir::new().unwrap();
735        let state_manager = AgentStateManager::new(Some(temp_dir.path().to_path_buf()));
736
737        let mut state = create_test_state("agent-1");
738        state.current_step = 5;
739        state.total_steps = Some(10);
740        state.error_count = 2;
741        state.create_checkpoint(Some("checkpoint-1"));
742        state_manager.save_state(&state).await.unwrap();
743
744        let resumer = AgentResumer::new(state_manager);
745        let summary = resumer.create_resume_summary("agent-1").await.unwrap();
746
747        // Verify summary contains expected sections
748        assert!(summary.contains("Resume Summary"));
749        assert!(summary.contains("agent-1"));
750        assert!(summary.contains("Status"));
751        assert!(summary.contains("Progress"));
752        assert!(summary.contains("Current Step: 5"));
753        assert!(summary.contains("Total Steps: 10"));
754        assert!(summary.contains("Checkpoints"));
755        assert!(summary.contains("checkpoint-1"));
756        assert!(summary.contains("Errors"));
757        assert!(summary.contains("Error Count: 2"));
758    }
759
760    #[tokio::test]
761    async fn test_resumer_create_resume_summary_nonexistent_fails() {
762        let temp_dir = TempDir::new().unwrap();
763        let state_manager = AgentStateManager::new(Some(temp_dir.path().to_path_buf()));
764        let resumer = AgentResumer::new(state_manager);
765
766        let result = resumer.create_resume_summary("nonexistent").await;
767
768        assert!(result.is_err());
769        assert!(matches!(
770            result.unwrap_err(),
771            ResumerError::AgentNotFound(_)
772        ));
773    }
774}