Skip to content

Failure Taxonomy

本课交付结果

你将交付 classifyFailure(value),支持 permission、validation、tool、timeout、cancelled、test、typecheck、dependency、budget、unknown 十类,返回安全消息、retryable 和 SHA-256 指纹。

岗位问题

没有分类的 Agent 只能“失败就再试”。这会反复请求已拒绝权限、无视取消信号,或用相同补丁撞同一测试。恢复决策需要稳定类别与指纹,而不是依赖自由文本。

前置检查

前置知识快照

先理解第 14 课测试输出分类:

bash
pnpm --filter @learn-traeai/coding-agent-bootcamp verify:lesson -- 14

本课扩大到 Runtime 全域。结构化 kind 优先于消息推断,以免“permission: request timed out”被误判 timeout。

失败分类不是为了生成漂亮标签,而是决定下一步控制流。permission需要用户改变授权,cancelled代表用户已要求停止,validation说明相同输入不会自愈;这些类别若原样重试会形成死循环。tool、test或typecheck可能在新参数、新补丁或暂时故障后恢复,但仍受次数与时间预算。

输入是unknown,因为错误可能来自Error、工具结果、插件JSON、字符串甚至null。TypeScript类型无法保护运行时边界,分类器先安全收窄对象、message、kind、code和name,再按优先级推断。它本身绝不能因奇怪错误再次崩溃。

原理拆解

先读取已知结构化 kind;否则按错误 code、name、具体模式到通用模式推断。permission、validation、cancelled、budget 和 unknown 默认不可重试;tool、timeout、test、typecheck、dependency 可在策略预算内重试。指纹由 kind 和去除路径、数字等波动值的消息生成。

mermaid
flowchart TD
  A[unknown failure] --> B[安全提取 record/message/code/name]
  B --> C{已知结构化 kind?}
  C -- 是 --> D[保留 kind]
  C -- 否 --> E[code/name 具体映射]
  E --> F[消息具体模式]
  F --> G[unknown 兜底]
  D --> H[类别可重试映射]
  G --> H
  H --> I[路径/数字/空白归一]
  I --> J[SHA-256 fingerprint]
  J --> K[kind + safe message + retryable + fingerprint]

十类覆盖不同层:permission是策略拒绝,validation是输入或不变量,tool是外部能力暂时故障,timeout是预算截止,cancelled是主动终止,test与typecheck是代码验证,dependency是环境或依赖缺失,budget是资源硬上限,unknown是无法可靠解释。分类尽量互斥,复杂失败可在details保留次要信号。

结构化kind优先体现“离事实最近的组件最懂原因”。一个权限服务可能消息写“approval timed out”,若文本规则先运行会分类timeout并自动重试,重复弹审批;保留permission才能停止并等待用户。只有缺少可信kind时才从code/name/message推断。

可重试不是“马上用相同输入再来一次”。tool或timeout可能退避重试,test/typecheck需要产生新补丁,dependency需要审批环境动作。retryable:true只是允许恢复策略考虑下一轮;具体策略、次数和差异证明由第20课控制。

permission、validation、cancelled、budget和unknown默认不可重试体现保守原则。权限不改变就不会成功,验证错误需修输入,取消必须尊重,预算需调整计划,unknown无法证明重试安全。上层若有新外部事实,可以创建新操作,而不是在同一恢复循环盲试。

指纹用于识别“本质相同失败”。直接hash原消息会把 /tmp/run-123 line 42 与下一轮临时路径当不同;先小写、替换绝对路径、数字为占位符、压缩空白,再与kind一起hash。两类相同文本不会碰撞,因为kind加入输入。

归一化也可能过度:把所有数字替换为#,期望1与2和期望1与3可能合并。课程选择简单可解释基线,生产fingerprint应按类别提取稳定字段,例如测试名、相对文件、错误代码和断言结构,同时移除端口、时间与临时路径。

message要安全。参考实现返回原message便于Lab,正式系统对外只返回脱敏摘要,完整stack进入受控Trace。错误对象可能含token、绝对路径和源码行,分类不应把所有可枚举字段序列化给模型。

代码实验

失败实现

危险实现只看文本并把所有错误设为可重试:

ts
function unsafe(error: Error) {
  const kind = error.message.includes("timeout") ? "timeout" : "unknown";
  return { kind, retryable: true, fingerprint: hash(error.message) };
}

正确实现先尊重结构化kind,再使用code/name与具体模式:

ts
const known = new Set<FailureKind>([
  "permission", "validation", "tool", "timeout", "cancelled",
  "test", "typecheck", "dependency", "budget", "unknown",
]);
const retryableKinds = new Set<FailureKind>([
  "tool", "timeout", "test", "typecheck", "dependency",
]);

if (typeof record.kind === "string" && known.has(record.kind as FailureKind)) {
  kind = record.kind as FailureKind;
} else if (["EACCES", "EPERM", "PERMISSION_DENIED"].includes(code)) {
  kind = "permission";
} else if (name === "AbortError" || code === "ABORT_ERR") {
  kind = "cancelled";
}

最终fingerprint使用NUL分隔kind与规范消息,避免简单字符串拼接歧义:

ts
const stable = message.toLowerCase()
  .replace(/\/[\w./-]+/g, "<path>")
  .replace(/\b\d+\b/g, "#")
  .replace(/\s+/g, " ").trim();
const fingerprint = createHash("sha256")
  .update(`${kind}\0${stable}`).digest("hex");

打开本课 Lab

bash
pnpm --dir bootcamps/coding-agent/labs/19-failure-taxonomy/starter test
pnpm --dir bootcamps/coding-agent/labs/19-failure-taxonomy/solution test

Starter 应声明 实现失败可重试映射;Solution 应通过 5 项测试,并在一项表驱动断言中覆盖十类。

优先级测试刻意让message含超时词:

ts
it("结构化权限优先于超时文本", () => {
  const result = classifyFailure({
    kind: "permission",
    message: "approval request timed out",
  });
  expect(result.kind).toBe("permission");
  expect(result.retryable).toBe(false);
});

关键实现讲解

指纹不能直接 hash 原消息,否则临时目录和行号每轮变化会逃过重复检测。先把绝对路径替换为 <path>、数字替换为 #、空白归一,再和 kind 一起 hash。不要把原始 stack 放进模型消息。

错误code比message稳定,例如EACCES明确权限,ABORT_ERR明确取消。name次之,消息模式最后。规则从具体到通用,unknown兜底;永远不要用“包含error”这类宽模式抢占所有类别。每条规则配正例、反例和复合信号。

分类结果应创建新对象,不返回或修改原错误。插件可能复用Error,给它附加kind会污染其他捕获路径;不可变ClassifiedFailure便于历史冻结与Trace。fingerprint固定六十四位小写十六进制,可作为索引,不等同安全签名。

运行与验证

真实运行输出

bash
pnpm --filter @learn-traeai/coding-agent-bootcamp verify:lesson -- 19
pnpm --filter @coding-agent/agent-core test

失败产物检查:failed at /tmp/run-123 line 42 与另一临时路径/行号必须生成同一 64 位指纹;结构化 permission 即使消息含 timed out 仍不可重试。

真实Solution五项应全绿:

text
solution: 5 passed, 0 failed
覆盖: 十类结构化kind / 可恢复映射 / 结构优先 / Error name与code / 稳定fingerprint

再用null、数字、字符串、带getter的奇怪对象和超长消息做边界测试,确保分类器始终有界返回;不同kind相同message得到不同fingerprint,相同本质路径/行号变化得到相同fingerprint。

常见失败与排查

故障案例 1

权限拒绝被当超时反复请求。

症状:用户拒绝后审批窗口再次弹出,直到最大次数。

根因:message中的timed out覆盖了结构化permission,或所有类别统一retryable。

定位:构造kind permission且消息含timeout的复合对象,检查分类与重试标志。

修复:可信结构化kind最高优先,permission固定不可重试;新授权属于外部新操作。

故障案例 2

相同测试每轮都看似新失败。

症状:临时目录、端口或行号变化让恢复循环无法触发重复停止。

根因:fingerprint直接hash完整message或stack,没有归一波动字段。

定位:只改变临时路径和数字生成两条错误,比较fingerprint。

修复:按类别提取稳定语义,替换波动值,kind与规范消息共同hash。

故障案例 3

未知插件错误无限消耗预算。

症状:无法解析的对象被标为unknown,但循环仍原样重试直到费用耗尽。

根因:默认认为“也许暂时故障”,把unknown设为retryable。

定位:输入随机对象与字符串,检查默认分支和恢复调用次数。

修复:unknown默认不可重试,保留安全摘要和Trace,要求新证据或人工处理。

课后作业

为每类失败定义用户动作、内部诊断字段和最大重试次数;加入密钥脱敏测试,并评估数字归一化可能合并不同错误的权衡。

进阶要求是用版本化规则表声明优先级、默认策略与恢复建议,建立真实错误语料的混淆矩阵。为fingerprint增加类别专用提取器和schema版本,让规则升级不会把新旧指纹误当同一空间。

验收 Rubric

维度通过标准常见扣分
覆盖十类均可稳定产生大量退化为 unknown
优先级结构化信息优先仅靠文本猜测
重试性保守且类别明确全部可重试
指纹去除路径、数字等波动直接 hash 原日志

总项目增量

总项目包:@coding-agent/agent-core

总项目路径:packages/agent-core/src/failure-taxonomy.ts

总项目验证命令:pnpm --filter @coding-agent/agent-core test

失败分类回答“能否再试”;下一课实现“何时停止”和“保留什么历史”。

延伸阅读

方案对比与工程取舍

自由文本让每个组件实现快,却把理解成本推给所有调用方;统一错误类提供强类型,但跨进程、插件与语言时难保持instanceof;结构化plain object最易序列化,需运行时验证。分类器作为适配层,允许内部多种来源,输出统一可辨识事实。

用HTTP式单一code层级简单,却难表达测试、预算和策略等正交来源;多标签能保留丰富信号,却让恢复规则组合爆炸;一个primary kind加details是常见折中。primary驱动控制流,details用于诊断与专用策略。

retryable最好不是错误对象作者随意声明的真值。低层可以提供建议,中央策略按kind、幂等性、尝试历史与预算最终决定。课程在分类器中给默认值,生产可同时返回 retryHintdefaultRetryable,Runtime仍可收紧,不能被插件放宽permission。

timeout可重试也有前提。相同命令、相同预算和相同负载原样重试常无效;恢复应缩小任务、增加经批准预算或等待暂时资源。retryable表示有可能通过策略变化恢复,不等于立即无条件执行。

dependency可重试更敏感。缺模块可能经批准安装恢复,也可能是代码import错误;自动安装会触发供应链与网络。分类器只允许进入恢复候选,策略先检查lockfile、manifest与用户授权。类别不能直接等价具体副作用。

validation不可重试指相同输入。模型重新读取并生成不同补丁属于新操作,可以再次分类;循环不应对同一失败对象原样调用。这个区别让不可重试并不等于任务永远失败,而是要求改变事实来源。

budget失败也类似。上下文或输出超限需要压缩、分步或用户调整配置,当前操作不能自行突破硬上限。将budget设retryable会让相同大输入重复撞墙。规划层创建更小的新请求才是有效恢复。

cancelled必须具有最高停止语义。即使取消发生时底层还返回test错误,主动取消信号应成为primary,避免Agent在用户点击停止后继续修复。结构化Context比日志最后一行更可信,组合层决定优先级。

tool类别要区分暂时与永久,例如服务限流、网络抖动、可执行文件缺失、协议不兼容。details可带retryAfter与code,默认tool可重试但最大次数小;明确永久code降为validation或unknown不可重试。十类是第一层,不妨碍细分。

fingerprint版本必须显式。归一规则改变后,同一错误hash变化,历史重复计数失效;报告带fingerprintVersion,恢复只比较同版本。迁移可同时计算新旧一段时间,但不能无说明混合。

路径正则要跨平台。参考只替换POSIX绝对路径,Windows盘符、UNC和URL可能保留波动或被误删。生产归一器根据受信workspace把内部绝对路径转相对,外部路径占位,URL与版本号按类别处理。全局粗正则容易吞掉有意义消息。

数字不是都波动。错误TS2322中的2322是稳定代码,应保留;行号、端口、耗时可替换;断言expected数字可能决定是否同一失败。类别专用parser比统一替换所有数字更准确。课程基线故意简单,作业应量化误合并与漏合并。

hash碰撞在SHA-256下概率极低,但fingerprint不是身份认证。攻击者可以控制message制造语义碰撞,系统不应用它授权或去重安全事件;它只帮助恢复和日志聚合。审批仍绑定完整规范请求hash与策略事实。

安全message需分三层:模型获得可行动且脱敏摘要,用户获得本地化说明,开发者受控Trace看到stack与内部code。把一个字符串复用三层,要么泄密要么信息不足。ClassifiedFailure可带publicMessage和diagnosticId,而非原stack。

错误对象的getter可能抛异常,Proxy也可能做副作用。读取unknown record字段应使用安全访问或复制受控属性,捕获提取异常并归unknown。不要JSON.stringify任意错误,它可能循环引用、巨大或执行toJSON。分类器本身必须资源有界。

超长message先设字节上限再归一与hash,避免恶意插件让正则消耗大量CPU。保留头尾或稳定摘要,并标记truncated;fingerprint基于有界语义字段。正则避免灾难性回溯,语料加入大输入性能测试。

规则评测用真实脱敏错误,计算各kind precision、recall和unknown率。与自动副作用关联的类别以precision优先,宁可unknown也不误导;诊断类可提高recall。每次规则变更比较混淆矩阵,而不是只新增一个正例。

Trace记录原始来源组件、分类规则id、kind、retryable、fingerprint版本与diagnosticId。规则命中证据让误分类可追踪;没有rule id时,团队只能猜是哪条正则抢先。规范报告不含原秘密。

变异测试把结构优先移到文本后、所有kind设retryable、删除路径归一、unknown设true、fingerprint去掉kind。每项都应被测试杀死。再用性质测试覆盖十类全集和六十四位hash格式,新增kind时编译与测试同时提醒更新策略。

逐类走一遍恢复语义能避免只记枚举。permission如写入未批准,当前请求停止并显示审批;validation如补丁before不匹配,必须重新读取;tool如临时服务错误,可退避;timeout需缩小任务或新预算;cancelled立即尊重用户;test与typecheck要求新代码策略;dependency可能需批准安装;budget要压缩;unknown停止并保留诊断。每类都对应不同“改变什么事实”。

结构化输入的十类表驱动测试不仅看kind,还应检查retryable映射。新增第十一类时Set、类型、策略和文档必须一起更新,否则默认unknown可能掩盖。可用TypeScript satisfies Record<FailureKind,...> 做穷尽映射,比两个Set更容易在编译期发现漏项。

Error的message提取也有顺序。普通Error通过record.message与value.message都能得到字符串;字符串输入用String(value);null变成null。正式实现不要把Symbol直接String失败或对象默认成[object Object]后当有意义错误,可以提供稳定“无法读取错误消息”并归unknown。

code与name可能位于不可枚举属性,直接展开对象会丢失;安全按字段读取。跨realm Error未必通过instanceof,因此对象message/name仍要工作。插件JSON没有Error原型,也应按plain record处理。适配层的价值正是消除来源差异。

permission code包括EACCES、EPERM与领域PERMISSION_DENIED,name AbortError或ABORT_ERR归cancelled。TimeoutError name比message稳定,先判;TS编号、module not found、assertion和budget模式随后。顺序要用复合夹具验证,不能只凭直觉。

消息“permission request timed out”同时匹配超时文本,但结构kind permission优先;若没有结构kind只有文本,可能归timeout。这说明上游越早结构化越好,文本推断永远有歧义。团队应逐步让所有核心组件返回code/kind,把正则限制在第三方边界。

安全消息不等于完全模糊。对模型说“validation failed”不足以修复,可以带相对路径、错误码和受限原因;对permission说明资源与需用户动作;对test带首个失败摘要。脱敏后仍要保留行动信息,安全与可用不是二选一。

fingerprint例子中两条消息路径和行号不同,归一后都成为类似 failed at <path> line #,加unknown kind后hash相同。若其中一条结构kind为tool,hash会不同。测试同时断言格式与相等,证明波动被移除而类别被保留。

连续重复判断只用hash会失去可解释性,因此历史仍保留kind与message摘要。停止时UI显示“连续两次相同typecheck失败”以及对应文件,不只展示hex。fingerprint服务机器比较,不能取代人类证据。

误合并的代价是恢复过早停止,误拆分的代价是无效重试。对昂贵Agent通常宁可稍早停并请求用户,也不无限循环;因此简单数字归一基线偏向合并。不同产品可根据任务价值和自动化风险调整,但必须通过评测量化。

retryable映射也可细分最大次数。tool两次、timeout一次、test三次、typecheck三次、dependency一次;实际值由Recovery Policy配置,不写死分类器。分类器给默认上限建议,循环统一执行总预算。单一布尔无法表达所有策略,但足以完成本课接口。

退避只适合可能随时间变化的tool与部分timeout,对test/typecheck等待没有意义,必须先生成不同补丁。Recovery Loop可要求strategyTag变化,否则即使retryable也停止。第20课作业会把“同一fingerprint不能重复同策略”加入。

dependency错误若由缺包造成,安装后fingerprint可能消失;若由拼错模块名造成,重复安装无效。恢复策略先搜索manifest与源码,形成新证据。分类器不应尝试判定业务根因,否则职责膨胀并引入副作用。

预算错误包含上下文token、工具输出、时间和成本,使用details区分。某些可以降级,某些是用户硬限额不可突破。统一kind让默认停止,规划器根据budgetType提供新方案。不要自动提高用户成本上限。

unknown事件应形成产品反馈闭环。按来源组件和fingerprint聚合频次,高频样本人工脱敏标注后新增结构化适配器;低频保留停止。不要把原始未知日志自动上传或直接训练,其中可能含私有代码与攻击文本。

分类器版本变更会影响线上恢复指标。shadow计算新kind与fingerprint,不驱动行为,比较误分类与重复停止率;确认后切换。紧急安全收紧如unknown改不可重试可以立即发布,但仍记录版本便于解释任务行为变化。

最终验收不仅跑五项Lab,还把Week3执行结果、Week4编辑冲突、Diff风险和用户取消都送入分类器,检查跨模块code映射。局部正则全绿不证明真实系统会产生预期字段,集成夹具必须覆盖来源协议。

做一次攻击性输入测试:百万级重复字符、循环对象、抛异常getter、包含ANSI与疑似密钥的message。函数在严格时间和内存内返回unknown或安全类别,public message脱敏,Trace有diagnosticId。分类器位于所有失败都会经过的路径,自身韧性尤其重要。

团队还要定义类别所有权。Sandbox维护permission/cancelled,工具层维护tool/validation,Runner维护timeout/test/typecheck/dependency,预算器维护budget,分类器只负责统一。上游新增code时必须更新契约测试,避免多个组件用同一字符串表达不同含义。清晰所有权比继续添加全局正则更能长期降低unknown。

审计报告按kind、来源、retryable和fingerprint版本聚合,观察某版本是否突然增加timeout或unknown。指标异常先检查基础设施与分类规则,不要立即让模型多重试。错误taxonomy也是系统健康观测层,能把个别失败转成可行动趋势。

发布前让恢复策略在录制Trace上离线回放:旧分类与新分类分别产生何种停止原因、尝试次数和预计成本。任何permission从不可重试变可重试、unknown被宽泛归tool等权限扩张都要求人工审查。分类变化会改变真实副作用,评审标准应接近安全策略而非普通文案。

最后检查每个ClassifiedFailure都能被稳定序列化,不包含Error实例、函数、循环引用或可变共享对象。冻结或复制结果后交给历史与Trace,确保后续UI渲染不会改变机器判断。分类事实一旦生成,就应像事件记录一样不可篡改。

当同一夹具在不同平台、路径和时间运行时,kind、retryable与规范指纹仍应稳定;只有真正根因变化才改变身份。这是发布门禁最后要证明的性质。

稳定分类才能驱动可靠恢复。

下一课衔接

分类器已经回答“这次失败是否值得考虑恢复”并提供稳定身份,但尚未限制实际尝试。下一课Recovery Loop按attempt执行,记录冻结历史;不可重试一次即停,连续相同fingerprint达到阈值停止,变化失败也受maxAttempts硬上限。分类与控制分离,才能给恢复提供终止证明。

  • Fault taxonomy 与 retry storm。
  • Error fingerprinting 和日志去重。
  • 用户错误、系统错误与策略错误的边界。

从零实现 Mini Code Agent Runtime