Skip to main content

vtcode_core/core/agent/runner/
retry.rs

1use super::AgentRunner;
2use crate::core::agent::task::{ContextItem, Task, TaskResults};
3use crate::error::{ErrorCode, Result as VtCodeResult, VtCodeError};
4use crate::retry::{RetryEvent, RetryPolicy, run_with_retry};
5use crate::utils::colors::style;
6use tracing::{info, warn};
7
8/// Per-retry-loop context for the agent runner. Held by `&mut` inside
9/// `run_with_retry` so the `on_event` and `operation` callbacks can
10/// share access to the runner's mutable state without splitting borrows.
11struct AgentRetryContext<'a> {
12    runner: &'a mut AgentRunner,
13    metrics: std::sync::Arc<crate::metrics::MetricsCollector>,
14    policy_max_attempts: u32,
15    task_id: String,
16}
17
18impl AgentRunner {
19    /// Execute a task with automatic retry on transient failures
20    ///
21    /// Wraps `execute_task` with retry logic using exponential backoff.
22    /// Retries only occur for transient errors (timeouts, network issues, 5xx errors).
23    /// Non-retryable errors (auth failures, invalid requests) fail immediately.
24    pub async fn execute_task_with_retry(
25        &mut self,
26        task: &Task,
27        contexts: &[ContextItem],
28        max_retries: u32,
29    ) -> VtCodeResult<TaskResults> {
30        use std::time::Duration;
31
32        let policy = RetryPolicy::from_retries(max_retries, Duration::from_secs(2), Duration::from_secs(30), 2.0);
33        let metrics = self.tool_registry.metrics_collector();
34        let task_id = task.id.clone();
35        let mut ctx = AgentRetryContext {
36            runner: self,
37            metrics,
38            policy_max_attempts: policy.max_attempts,
39            task_id: task_id.clone(),
40        };
41
42        run_with_retry(
43            &policy,
44            &mut ctx,
45            |ctx, event| match event {
46                RetryEvent::AttemptStart { attempt, max_attempts } => {
47                    info!(
48                        attempt = attempt + 1,
49                        max_attempts,
50                        task_id = %ctx.task_id,
51                        "agent task attempt starting"
52                    );
53                }
54                RetryEvent::Success { attempt } if attempt > 0 => {
55                    ctx.metrics.record_retry_success();
56                    ctx.runner.runner_println(format_args!(
57                        "{} Task succeeded after {} attempt(s)",
58                        style("[✓]").green().bold(),
59                        attempt + 1
60                    ));
61                    info!(
62                        attempt = attempt + 1,
63                        task_id = %ctx.task_id,
64                        "agent task succeeded after retry"
65                    );
66                }
67                RetryEvent::Success { .. } => {}
68                RetryEvent::GiveUp { attempt, error, decision, category_was_retryable } => {
69                    if category_was_retryable && attempt + 1 == ctx.policy_max_attempts {
70                        ctx.metrics.record_retry_exhausted();
71                    }
72                    warn!(
73                        attempt = attempt + 1,
74                        max_attempts = ctx.policy_max_attempts,
75                        task_id = %ctx.task_id,
76                        error = %error,
77                        category = ?decision.category,
78                        "agent task attempt failed (non-retryable)"
79                    );
80                }
81                RetryEvent::Backoff { attempt, error, decision, delay, .. } => {
82                    warn!(
83                        attempt = attempt + 1,
84                        max_attempts = ctx.policy_max_attempts,
85                        task_id = %ctx.task_id,
86                        error = %error,
87                        category = ?decision.category,
88                        "agent task attempt failed"
89                    );
90                    ctx.metrics.record_retry_attempt();
91                    ctx.runner.runner_println(format_args!(
92                        "{} Task failed (attempt {}/{}), retrying in {}s...",
93                        style("[Warning]").red().bold(),
94                        attempt + 1,
95                        ctx.policy_max_attempts,
96                        delay.as_secs()
97                    ));
98                    info!(
99                        delay_ms = delay.as_millis() as u64,
100                        next_attempt = attempt + 2,
101                        task_id = %ctx.task_id,
102                        category = ?decision.category,
103                        "backing off before retry"
104                    );
105                }
106                RetryEvent::Exhausted { .. } => {
107                    warn!(
108                        task_id = %ctx.task_id,
109                        attempts = ctx.policy_max_attempts,
110                        "agent task failed after all retries"
111                    );
112                    ctx.runner.runner_println(format_args!(
113                        "{} Task failed after {} attempts",
114                        style("[Error]").red().bold(),
115                        ctx.policy_max_attempts
116                    ));
117                }
118            },
119            |ctx| {
120                let task = task.clone();
121                let contexts = contexts.to_vec();
122                let runner = &mut *ctx.runner;
123                Box::pin(async move { Box::pin(runner.execute_task(&task, &contexts)).await })
124            },
125            move |_policy| {
126                VtCodeError::execution(
127                    ErrorCode::ToolExecutionFailed,
128                    format!("agent task '{task_id}' exhausted the retry loop without an error payload"),
129                )
130            },
131        )
132        .await
133    }
134}