Skip to main content

tiny_agent/core/
failure.rs

1use 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
12/// 退避时长上限,避免指数爆炸把单次重试拖到分钟级。
13const MAX_BACKOFF: Duration = Duration::from_secs(30);
14
15/// 失败的结构化类别。
16#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
17#[serde(rename_all = "snake_case")]
18pub enum FailureKind {
19    /// 达到最大迭代步数。
20    MaxIter,
21    /// 调用 provider 建立流失败(网络 / 传输层)。
22    Provider,
23    /// provider 明确返回确定性失败,例如鉴权失败、请求格式错误、模型不存在。
24    ProviderPermanent,
25    /// provider 限流。
26    ProviderRateLimited,
27    /// 流式响应过程中出错,或未收到正常收尾。
28    ProviderStream,
29    /// 模型输出被长度上限截断。
30    Truncated,
31    /// 模型输出被内容过滤拦截。
32    ContentFilter,
33    /// 中间件自身报错。
34    Middleware,
35    /// 工具执行失败(模型可据错误结果调整后继续)。
36    Tool,
37    /// 打开沙箱失败。
38    SandboxOpen,
39    /// 保存沙箱状态失败。
40    SandboxSave,
41    /// transcript/checkpoint 等持久化层失败。
42    Storage,
43}
44
45impl FailureKind {
46    /// 该类失败是否值得用同样的输入重试。
47    ///
48    /// 可重试:瞬时性的 provider / 流 / 沙箱故障,以及工具失败(交回模型自愈)。
49    /// 不可重试:`MaxIter` / `Truncated` / `ContentFilter` —— 重试必然复现,
50    /// 应直接进终态。`Middleware` 视为确定性错误(护栏 / 逻辑 bug),同样终态。
51    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    /// 稳定的 snake_case 字符串,用于可观测属性和错误展示。
65    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            // 不可重试的类别:重试只会用相同输入复现,直接进终态,不消耗预算、不退避。
143            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            // 工具失败的 `tool_result` 已由 execute_pending_tools 写入 transcript
153            // (那里负责维持"每个 tool_use 必有 tool_result"的不变量),这里不再重复回应。
154            // 非工具失败(provider / stream / sandbox 等)没有对应的 tool_use,
155            // 补一条 system 面包屑让模型知道发生了什么并据此重试。
156            //
157            // Storage 失败时不要再写 transcript:持久化层已经在抖,继续写只会把
158            // 可重试失败变成外层 abort。
159            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            // 重试前退避:指数 + 等量 jitter,打散同时失败的并发会话。
177            self.retry_backoff(ctx.consecutive_fail_count).await;
178            return Ok(Agent::Ready(ctx));
179        }
180    }
181
182    /// 第 `attempt` 次连续失败后的退避等待:`base * 2^(attempt-1)`,封顶 `MAX_BACKOFF`,
183    /// 再叠加 [delay/2, delay] 区间的 jitter。`base` 为 0 时直接返回(测试用)。
184    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); // 2^6 后由 MAX_BACKOFF 封顶
190        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}