1use serde::{Deserialize, Serialize};
13use thiserror::Error;
14
15use super::{AgentState, AgentStateManager, AgentStateStatus, Checkpoint, StateManagerError};
16
17pub type ResumerResult<T> = Result<T, ResumerError>;
19
20#[derive(Debug, Error)]
22pub enum ResumerError {
23 #[error("Agent not found: {0}")]
25 AgentNotFound(String),
26
27 #[error("Agent cannot be resumed: {0}")]
29 CannotResume(String),
30
31 #[error("Checkpoint not found: {0}")]
33 CheckpointNotFound(String),
34
35 #[error("State manager error: {0}")]
37 StateManager(#[from] StateManagerError),
38
39 #[error("Invalid resume point: {0}")]
41 InvalidResumePoint(String),
42}
43
44#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
46#[serde(rename_all = "snake_case")]
47pub enum ResumePoint {
48 #[default]
50 Last,
51 Checkpoint(String),
53 Beginning,
55}
56
57#[derive(Debug, Clone, Serialize, Deserialize)]
59#[serde(rename_all = "camelCase")]
60pub struct ResumeOptions {
61 pub agent_id: String,
63 #[serde(default)]
65 pub continue_from: ResumePoint,
66 #[serde(default)]
68 pub reset_errors: bool,
69 pub additional_context: Option<String>,
71}
72
73impl ResumeOptions {
74 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 pub fn from_point(mut self, point: ResumePoint) -> Self {
86 self.continue_from = point;
87 self
88 }
89
90 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 pub fn with_reset_errors(mut self, reset: bool) -> Self {
98 self.reset_errors = reset;
99 self
100 }
101
102 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#[derive(Debug, Clone, Serialize, Deserialize)]
111#[serde(rename_all = "camelCase")]
112pub struct ResumePointInfo {
113 pub can_resume: bool,
115 pub agent_id: String,
117 pub status: AgentStateStatus,
119 pub step: usize,
121 pub total_steps: Option<usize>,
123 pub checkpoint_available: bool,
125 pub last_checkpoint: Option<Checkpoint>,
127 pub error_count: usize,
129 pub suggestions: Option<Vec<String>>,
131}
132
133impl ResumePointInfo {
134 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 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#[derive(Debug)]
205pub struct AgentResumer {
206 state_manager: AgentStateManager,
208}
209
210impl AgentResumer {
211 pub fn new(state_manager: AgentStateManager) -> Self {
213 Self { state_manager }
214 }
215
216 pub fn state_manager(&self) -> &AgentStateManager {
218 &self.state_manager
219 }
220
221 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 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 pub async fn resume(&self, options: ResumeOptions) -> ResumerResult<AgentState> {
244 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 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 match &options.continue_from {
259 ResumePoint::Last => {
260 }
262 ResumePoint::Checkpoint(checkpoint_id) => {
263 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 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 if options.reset_errors {
284 state.reset_errors();
285 }
286
287 if let Some(context) = &options.additional_context {
289 state.set_metadata("additional_context", serde_json::json!(context));
290 }
291
292 if state.status == AgentStateStatus::Paused || state.status == AgentStateStatus::Failed {
294 state.status = AgentStateStatus::Running;
295 }
296
297 self.state_manager.save_state(&state).await?;
299
300 Ok(state)
301 }
302
303 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 summary.push_str(&format!("# Resume Summary for Agent: {}\n\n", state.id));
315
316 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 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 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 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 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 summary.push_str("## Original Prompt\n");
371 let prompt_preview = if state.prompt.len() > 200 {
372 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 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 let mut state = create_test_state("agent-1");
600 state.current_step = 3;
601 let checkpoint = state.create_checkpoint(Some("cp-1"));
602
603 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 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 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}