1use super::{Agent, AgentRunTime};
2use crate::{
3 error::AgentError,
4 shared::{Message, Role, ToolCall},
5 state::AgentState,
6 trajectory::TrajectoryEventKind,
7};
8use serde::{Deserialize, Serialize};
9use std::fmt;
10use std::time::{Duration, SystemTime, UNIX_EPOCH};
11
12const MAX_BACKOFF: Duration = Duration::from_secs(30);
14
15#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
17#[serde(rename_all = "snake_case")]
18pub enum FailureKind {
19 MaxIter,
21 Provider,
23 ProviderPermanent,
25 ProviderRateLimited,
27 ProviderStream,
29 Truncated,
31 ContentFilter,
33 Middleware,
35 Tool,
37 SandboxOpen,
39 SandboxSave,
41 Storage,
43}
44
45impl FailureKind {
46 pub fn is_retryable(&self) -> bool {
52 matches!(
53 self,
54 FailureKind::Provider
55 | FailureKind::ProviderRateLimited
56 | FailureKind::ProviderStream
57 | FailureKind::Tool
58 | FailureKind::SandboxOpen
59 | FailureKind::SandboxSave
60 | FailureKind::Storage
61 )
62 }
63
64 pub fn as_str(&self) -> &'static str {
66 match self {
67 FailureKind::MaxIter => "max_iter",
68 FailureKind::Provider => "provider",
69 FailureKind::ProviderPermanent => "provider_permanent",
70 FailureKind::ProviderRateLimited => "provider_rate_limited",
71 FailureKind::ProviderStream => "provider_stream",
72 FailureKind::Truncated => "truncated",
73 FailureKind::ContentFilter => "content_filter",
74 FailureKind::Middleware => "middleware",
75 FailureKind::Tool => "tool",
76 FailureKind::SandboxOpen => "sandbox_open",
77 FailureKind::SandboxSave => "sandbox_save",
78 FailureKind::Storage => "storage",
79 }
80 }
81}
82
83impl fmt::Display for FailureKind {
84 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
85 f.write_str(self.as_str())
86 }
87}
88
89#[derive(thiserror::Error, Debug, Serialize, Deserialize, Clone)]
90#[error("{kind}: {message}")]
91pub struct AgentFailure {
92 pub kind: FailureKind,
93 pub message: String,
94 pub tool_call: Option<ToolCall>,
95}
96
97impl AgentFailure {
98 pub(crate) fn new(kind: FailureKind, message: impl Into<String>) -> Self {
99 Self {
100 kind,
101 message: message.into(),
102 tool_call: None,
103 }
104 }
105
106 pub(crate) fn tool(tool_call: ToolCall, message: impl Into<String>) -> Self {
107 Self {
108 kind: FailureKind::Tool,
109 message: message.into(),
110 tool_call: Some(tool_call),
111 }
112 }
113}
114
115impl AgentRunTime {
116 pub(crate) fn fail_with(
117 &self,
118 ctx: AgentState,
119 kind: FailureKind,
120 message: impl Into<String>,
121 ) -> Agent {
122 Agent::Fail(ctx, AgentFailure::new(kind, message))
123 }
124
125 pub(crate) fn fail_tool(
126 &self,
127 ctx: AgentState,
128 tool_call: ToolCall,
129 message: impl Into<String>,
130 ) -> Agent {
131 Agent::Fail(ctx, AgentFailure::tool(tool_call, message))
132 }
133
134 pub(crate) async fn handle_fail(
135 &self,
136 mut ctx: AgentState,
137 mut failure: AgentFailure,
138 ) -> Result<Agent, AgentError> {
139 loop {
140 self.emit(
141 &ctx.session_id,
142 TrajectoryEventKind::Failed {
143 kind: failure.kind.as_str().to_string(),
144 message: failure.message.clone(),
145 },
146 )
147 .await;
148
149 if !failure.kind.is_retryable() {
151 return Ok(Agent::Fail(ctx, failure));
152 }
153
154 ctx.consecutive_fail_count = ctx.consecutive_fail_count.saturating_add(1);
155 if ctx.consecutive_fail_count > self.max_consecutive_fail_count {
156 return Ok(Agent::Fail(ctx, failure));
157 }
158
159 if failure.tool_call.is_none() && failure.kind != FailureKind::Storage {
167 if let Err(e) = self
168 .transcript_storage
169 .append_message(
170 &ctx.session_id,
171 Message::text(
172 Role::System,
173 format!("{} failed: {}", failure.kind, failure.message),
174 ),
175 )
176 .await
177 {
178 failure = AgentFailure::new(FailureKind::Storage, e.to_string());
179 continue;
180 }
181 }
182
183 self.retry_backoff(ctx.consecutive_fail_count).await;
185 return Ok(Agent::Ready(ctx));
186 }
187 }
188
189 async fn retry_backoff(&self, attempt: u32) {
192 let base = self.retry_backoff;
193 if base.is_zero() {
194 return;
195 }
196 let exponent = attempt.saturating_sub(1).min(6); let delay = base.saturating_mul(1u32 << exponent).min(MAX_BACKOFF);
198 let half = delay / 2;
199 let half_nanos = (half.as_nanos() as u64).max(1);
200 let jitter_source = SystemTime::now()
201 .duration_since(UNIX_EPOCH)
202 .map(|d| d.subsec_nanos() as u64)
203 .unwrap_or(0);
204 let jitter = Duration::from_nanos(jitter_source % half_nanos);
205 tokio::time::sleep(half + jitter).await;
206 }
207}