diff --git a/guardrails/gate-flow.yaml b/guardrails/gate-flow.yaml new file mode 100644 index 0000000..1c4af8b --- /dev/null +++ b/guardrails/gate-flow.yaml @@ -0,0 +1,41 @@ +# guardrails/gate-flow.yaml +# GATE 流程门控铁律 — 最高频纠正(33+ 次),违反后果最严重 +name: gate-flow +severity: high +trigger: "非平凡任务启动、bug 修复、新功能设计、代码改动" +rule: | + 需求不清不动手 — 列出假设让用户确认 + 根因不明不修复(修bug时)— 先查清再改 + 方案未定不实现(新功能/L3时)— 先出方案等确认 + 评估过影响范围才动手 + 涉及代码改动或调研分析的非平凡任务,使用 plan-act-verify skill + L1 小改动(单文件 <50 行,做错代价低)可跳过 GATE +evidence: + - "批次1卡片14:33 次纠正都涉及流程问题,是最频繁的纠正类别" + - "批次1卡片1:用户多次说"先不要改",Agent 必须立即停下" + - "批次1卡片8:6 次因"先确认再改"被纠正" + - "批次2卡片1:评审必须验证最终代码,不能只审方案(5+次)" + - "批次2卡片4:设计文档-代码一致性审查不可省(4次)" +anti-patterns: + - "找到根因后直接给出改动点,不等评审" + - "一次性提多个修复方案不等评审通过" + - "收到评审意见后直接改,不先确认" + - "bug 还没查清就开始写修复代码" + - "需求还不清楚就开始编码" + - "改了一个文件但没有检查关联影响" +--- + +## 典型违规场景 + +### 场景1:跳过根因确认 +Agent 看到问题后立刻写修复代码,没有先查清根因。 +- 用户:"先不要改,你去检查下什么原因" +- 正确:先查清根因 → 汇报发现 → 等确认 → 再改 + +### 场景2:跳过方案评审 +Agent 找到根因后直接给出了 4 个改动点,但应该先发给评审再改。 +- 正确:汇报根因 → 提出方案 → 等评审通过 → 再改代码 + +### 场景3:跳过影响范围评估 +Agent 改了状态枚举值但没有检查所有引用点。 +- 正确:grep 所有引用点 → 评估影响 → 提出方案 → 确认后改 diff --git a/guardrails/no-circle-jerking.yaml b/guardrails/no-circle-jerking.yaml new file mode 100644 index 0000000..d03809b --- /dev/null +++ b/guardrails/no-circle-jerking.yaml @@ -0,0 +1,39 @@ +# guardrails/no-circle-jerking.yaml +# 不绕圈子铁律 — 第二高频(17+3=20 次),用户极度沮丧 +name: no-circle-jerking +severity: high +trigger: "用户已明确方向或给出前提假设后,Agent 准备回复时" +rule: | + 用户已明确方向后,不再质疑前提条件 + 用户给出条件假设时("哪怕1%"),按最坏情况设计,不讨论概率 + 用户说"别绕了"时,意味着之前已经讨论过至少一次,直接给方案 + "发生概率低"不是不处理某个场景的理由 + 先直接回答问题(哪怕答案是"不能/不知道"),再补充说明 +evidence: + - "批次1卡片2:反复讨论已确认的问题,17 次被纠正,用户极度沮丧" + - "批次2卡片13:回答用户问题要直接不要绕弯子,3 次被纠正" + - "批次1卡片3:被动跟随用户 lost 上下文,4 次被纠正" + - "批次1卡片13:Memory 噪音导致 Agent 跳到不相关话题(27 次,系统层面)" +anti-patterns: + - "用户要求聚焦方案,Agent 却在分析前提条件的概率" + - "用户说"别绕了"后仍然在重复之前的论点" + - "用"发生概率低"作为不设计方案的理由" + - "回避核心问题,用相关信息绕弯子" +--- + +## 典型违规场景 + +### 场景1:质疑已确认的前提 +用户:"集中在 compact 发生之后如何处理" +Agent:"compact 发生的概率很低,建议先考虑..." +❌ 错误:用户已经明确要处理 compact 后的情况 + +### 场景2:重复讨论已决定的事项 +用户:"别绕了,我都说过了" +Agent:继续讨论之前的前提条件... +❌ 错误:"别绕了" = 之前已讨论过,直接给方案 + +### 场景3:回避核心问题 +用户:"能不能区分超时原因?" +Agent:"超时有很多种,v1 和 v2 的区别是..." +❌ 正确:先说"不能区分",再说"但不影响处理方案设计" diff --git a/skills/trial-and-error-patterns.md b/skills/trial-and-error-patterns.md new file mode 100644 index 0000000..6247ee6 --- /dev/null +++ b/skills/trial-and-error-patterns.md @@ -0,0 +1,161 @@ +--- +name: trial-and-error-patterns +description: >- + 从任务失败和重试中提炼的可复用模式:Counter/锁生命周期、进程管理、续杯机制、广播路径、JSON解析验证、中断恢复。 + 在涉及并发控制、任务重启、系统资源管理的复杂任务时触发。 +--- + +# Trial and Error Patterns + +> 来源:从三国团队对话历史中的 226 次试错和 200 次成功中蒸馏(批次1-2) + +## 适用场景 + +- 并发控制任务(counter/锁) +- 任务重启/续杯(retry)机制 +- 进程管理(启动/退出/资源释放) +- 多路径调度(广播 vs 单播) +- 外部命令输出解析 +- Agent 中断恢复与自检 + +## 模式清单 + +### 模式 1:Counter/锁的生命周期必须贯穿完整调用链(severity: high) + +**场景**:Agent 并发控制需要 counter/锁机制。如果 acquire-release 不成对,会导致永久占满或双重释放。 + +**错误做法**: +retry 路径提前调 `on_complete("retry_release")` 释放 counter,但续杯 spawn 时 counter 已释放,导致同一 Agent 被并发 spawn 多次。另一条路径中 `_do_retry` 的 on_complete 设为 None,counter 仍被占用但无法释放,续杯永远无法 acquire。 [批次2卡片6] + +**正确做法**: +1. Counter 贯穿 retry 链:acquire 在 spawn 前,release 在最终退出(done/failed)后 +2. Retry 不释放 counter,而是复用同一个 counter slot +3. 所有 `release` 调用都要带 session_id +4. release 后短期 cooldown,防止立即 re-acquire 导致抖动 +5. 广播 spawn 路径也要 catch AgentBusyError + +**关键细节**: +- 司马懿建议的四层控制:cooldown → global limit → per-agent → per-session +- counter 提前释放曾导致 zhipu API 429(rate limit 被打爆),Gateway 假死 + +--- + +### 模式 2:进程退出 ≠ 资源释放(severity: high) + +**场景**:异步系统中,子进程退出和回调执行不是原子操作。假设进程退出时资源已释放,会出现竞态条件。 + +**错误做法**: +spawner 在 retry 时:进程退出 → on_complete 未调用 → counter 仍占用 → can_acquire 返回 False → AgentBusyError → 续杯永远不会成功。 [批次2卡片7] + +**正确做法**: +1. 资源释放应该有明确的触发点,不能依赖"进程退出自然释放" +2. 在 retry 入口手动释放 counter,并将 on_complete 设为 None 防止 double release +3. 审查每条退出路径(正常退出、异常退出、超时退出、retry 退出)是否都有对应的资源释放 +4. on_complete=None 后需要替代兜底机制(如 ticker 超时),并记录为已知 trade-off + +**关键细节**: +- 这类竞态条件在单元测试中难以复现,只有 E2E 真实环境才能暴露 + +--- + +### 模式 3:续杯(retry)机制是最大假死来源(severity: high) + +**场景**:任务执行超时后需要 retry("续杯")。retry 机制本身有 Bug 时,会导致无限重试或永久卡住。 + +**错误做法**: +多个 Bug 叠加导致续杯死循环: +1. retry_count 在续杯中永远不变(retry_count=1/3),续杯永不停 +2. Mail 续杯用 RETRY_PROMPT(含 review 标记指令),Agent 标了 review,但 auto_complete 只认 done +3. 续杯创建新 session 而非复用 main session,每次重试丢失之前工作 [批次2卡片8] + +**正确做法**: +1. 续杯模板必须专用(如 MAIL_RETRY_PROMPT),明确禁止状态转换命令 +2. retry_count 必须在每次续杯时递增,max_retries 硬性兜底 +3. 续杯应复用 main session 保持上下文连续性,不创建新 session +4. use_main_session 在所有路径(主 dispatch、legacy broadcast、retry)必须统一 + +**关键细节**: +- 续杯模板的"不要执行状态转换"指令是关键——Agent 容易自作主张标 done +- 续杯死循环会阻塞 tick,导致整个调度停滞 + +--- + +### 模式 4:广播路径与单播路径行为必须一致(severity: medium) + +**场景**:任务调度有两条路径:指定 Agent 的单播和广播认领。如果两条路径的行为不一致,某些任务类型会系统性地出问题。 + +**错误做法**: +广播路径 `ticker._broadcast_claim` 调 `spawn_full_agent` 时没传 `task_id` 和 `db_path`,导致续杯时无法查询任务状态,retry_count 永远不变。 [批次2卡片9] + +**正确做法**: +1. 所有 spawn 路径(单播、广播、retry)必须传递完整的上下文参数 +2. 新增功能时逐条检查是否覆盖了所有 spawn 路径,不能只改单播忘了广播 +3. 统一 TickResult 返回值 + 方法注册,避免路径分歧 + +**关键细节**: +- 状态机枚举值不一致是同类问题——SQL 查询用 `'executing'` 但实际枚举是 `'working'`,重启恢复时 working 节点被遗漏 [批次2卡片2] +- 安装目录同步(auto-sync)可能静默修改配置,需要与设计文档对照 + +--- + +### 模式 5:stdout JSON 解析路径必须实测验证(severity: medium) + +**场景**:外部命令的输出结构可能随版本变化。如果解析路径是猜的而非实测的,会导致静默失败。 + +**错误做法**: +spawner 代码取 `data.response.meta.transport`,但实际 `openclaw agent --json` 的输出是 `{ response: { meta: { transport, ... } } }`,路径不对。解析不到数据时走了 fallback,掩盖了问题。这个 Bug 从第一天就存在。 [批次2卡片14] + +**正确做法**: +1. 解析外部命令输出前,先用实际命令验证输出格式 +2. 评审时要求看到实测输出样例,不接受"应该是这样" +3. 解析失败时显式报错或日志警告,不要静默 fallback + +**关键细节**: +- 静默失败比报错更危险——fallback 掩盖了问题,导致所有依赖 stdout 判定的功能(如幻觉门控)静默失效 +- 外部 CLI 的 `--json` 输出格式属于外部接口,需要像对待 API 一样验证 + +--- + +### 模式 6:中断恢复后必须自检(severity: medium) + +**场景**:Agent 在执行过程中出现中断(进程挂起、网络断开、超时等),恢复后继续工作,但未检查中断期间发生了什么。 + +**错误做法**: +Agent 在设计评审完成后开始编码时"死掉"了 40 分钟,恢复后直接继续编码,没有自查中断原因、没有检查中断期间是否有新的指令或状态变化。 [批次1卡片10] + +**正确做法**: +1. 发现自己有中断时,主动检查中断原因和时长 +2. 检查中断期间是否错过了重要事件(新邮件、状态变更、用户指令) +3. 向用户汇报中断情况和恢复状态 +4. 如果中断导致上下文丢失,回退到最近已确认的状态重新开始 + +**关键细节**: +- Agent 不一定能感知自己的中断,但可以通过时间戳、日志断点来推断 +- 中断可能影响依赖链上的其他 agent——任务遗漏或重复执行 + +--- + +## 检查清单(快速参考) + +并发控制: +- [ ] Counter 的 acquire 和 release 是否成对? +- [ ] 所有 release 调用是否带 session_id? +- [ ] 续杯路径是否复用 counter slot 而非释放再获取? + +Retry/续杯: +- [ ] retry_count 是否每次递增?max_retries 是否兜底? +- [ ] 续杯模板是否明确禁止状态转换? +- [ ] 是否复用 main session 而非创建新 session? +- [ ] 进程退出后资源是否有明确的释放触发点? + +路径一致性: +- [ ] 广播路径和单播路径是否传递相同的完整参数? +- [ ] 新增功能是否覆盖了所有 spawn 路径? + +外部接口: +- [ ] stdout JSON 解析路径是否用实际命令验证过? +- [ ] 解析失败时是否显式报错而非静默 fallback? + +中断恢复: +- [ ] 中断恢复后是否检查了中断期间的事件? +- [ ] 是否向用户汇报了中断情况?