tiny_agent/core/
failure.rs1use super::{Agent, AgentRunTime};
2use crate::{
3 error::AgentError,
4 observability,
5 shared::{Message, Role, ToolCall},
6 state::AgentState,
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 observability::failed(&ctx.session_id, failure.kind.as_str(), &failure.message);
141
142 if !failure.kind.is_retryable() {
144 return Ok(Agent::Fail(ctx, failure));
145 }
146
147 ctx.consecutive_fail_count = ctx.consecutive_fail_count.saturating_add(1);
148 if ctx.consecutive_fail_count > self.max_consecutive_fail_count {
149 return Ok(Agent::Fail(ctx, failure));
150 }
151
152 if failure.tool_call.is_none() && failure.kind != FailureKind::Storage {
160 if let Err(e) = self
161 .transcript_storage
162 .append_message(
163 &ctx.session_id,
164 Message::text(
165 Role::System,
166 format!("{} failed: {}", failure.kind, failure.message),
167 ),
168 )
169 .await
170 {
171 failure = AgentFailure::new(FailureKind::Storage, e.to_string());
172 continue;
173 }
174 }
175
176 self.retry_backoff(ctx.consecutive_fail_count).await;
178 return Ok(Agent::Ready(ctx));
179 }
180 }
181
182 async fn retry_backoff(&self, attempt: u32) {
185 let base = self.retry_backoff;
186 if base.is_zero() {
187 return;
188 }
189 let exponent = attempt.saturating_sub(1).min(6); let delay = base.saturating_mul(1u32 << exponent).min(MAX_BACKOFF);
191 let half = delay / 2;
192 let half_nanos = (half.as_nanos() as u64).max(1);
193 let jitter_source = SystemTime::now()
194 .duration_since(UNIX_EPOCH)
195 .map(|d| d.subsec_nanos() as u64)
196 .unwrap_or(0);
197 let jitter = Duration::from_nanos(jitter_source % half_nanos);
198 tokio::time::sleep(half + jitter).await;
199 }
200}