feat(agent): W4-E2 in-grant auto recovery for decisions with verified outcome

- settings.agent.autoInvestigateMode investigate|recover, investigateMaxAttempts, recoverVerifySec
- new capability download.recheck (execute, replay never): qB force recheck / resume, no source swap/delete
- autoInvestigate moves to agent/investigate.ts: recover mode runs execute with a recovery allowlist
  only when an active grant (published policy if bound) covers the recovery actions; otherwise degrades
  to read-only investigation and leaves suggestion + recheck option on the decision
- runAgent verifies recovery against business facts (download_task/task/source_release) before finalize;
  verified -> decision resolved by system:auto-recover, run summary '已恢复(系统核实)'; otherwise stays
  awaiting_user; per-root-cause attempt cap
- e2e E6: missingFiles -> download_failed -> recheck -> verified; no grant degrades; swap rejected;
  dedupe; attempts exhausted; budget exceeded keeps attention

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
rootandClaude Opus 5.5 committed 2026-09-27 22:35:03 +08:00
1 parent 23309042dc
commit 86af723b2a
13 files changed
+1072 -15

No files matched your search

+47 -2
View File
@@ -6,7 +6,7 @@
## 能力
启动时装配 14 个能力(`catalog.lookup`、`release.search`、`release.inspect`、`identity.resolve`、`download.plan`、`download.submit`、`organize.plan`、`organize.execute`、`task.inspect`、`task.retry`、`task.cancel`、`policy.preview`、`policy.apply`、`inventory.query`)。注册数不等于 14 时进程启动失败。Web(`POST /capabilities/{id}/invoke`)、Pi 工具和后台任务都走 `ctx.capabilities.invoke` 这一条路径。
启动时装配 15 个能力(`catalog.lookup`、`release.search`、`release.inspect`、`identity.resolve`、`download.plan`、`download.submit`、`download.recheck`、`organize.plan`、`organize.execute`、`task.inspect`、`task.retry`、`task.cancel`、`policy.preview`、`policy.apply`、`inventory.query`)。注册数不等于 `CAPABILITY_IDS.length` 时进程启动失败。Web(`POST /capabilities/{id}/invoke`)、Pi 工具和后台任务都走 `ctx.capabilities.invoke` 这一条路径。
- **授权**(`agent/authorize.ts`):propose / approve / execute 类必须满足以下之一:
- 单次审批 `approval`:状态为 approved,绑定计划类型 + 计划 ID + payload hash + revision,且未过期。执行成功后审批标记为 consumed。
@@ -57,7 +57,7 @@ worker 崩溃后,新 worker 在引擎租约到期后(约 30s)接手,`drive` 步
## 后台触发与 resolveDecision
- `decision.created` 路由到 `resolveDecision(trigger=created)`。满足以下全部条件时,会发起一次有界调查 run:`agent.autoInvestigate=true`、kind 为 `identity_ambiguous` / `download_failed` / `download_stalled`、事项仍为 open、事项不是由 Agent 自己引起的。这个 run 的参数:`trigger=event`、`mode=plan_only`,白名单为只读能力加 `identity.resolve`,工具调用上限为 `agent.investigateMaxToolCalls`,`dedupe_key=investigate:<decisionId>`。同一事项只会有一个 run,重复事件或重新打开都不会再建。调查结束后,事项转为 `awaiting_user`,`evidence.agentInvestigation` 里带上结论。
- `decision.created` 路由到 `resolveDecision(trigger=created)`,由 `agent/investigate.ts` 的 `autoInvestigate` 处理。满足以下全部条件时,会发起一次有界 run:`agent.autoInvestigate=true`、kind 为 `identity_ambiguous` / `download_failed` / `download_stalled`、事项仍为 open、事项不是由 Agent 自己引起的。`trigger=event`,工具调用上限为 `agent.investigateMaxToolCalls`,时长上限 `min(600, agent.maxDurationSec)` 秒,`dedupe_key=investigate:<decisionId>`。同一事项只会有一个 run,重复事件或重新打开都不会再建。模式见下一节。调查结束后,没有被系统核实恢复的事项转为 `awaiting_user`,`evidence.agentInvestigation` 里带上结论。
- `decision.updated(status=resolved)` 路由到 `resolveDecision(trigger=updated)`,执行用户选中的处理:
- 选项带 `capabilityId` 时经注册中心调用(照样校验授权);
- `retry` 重试关联任务;
@@ -66,6 +66,48 @@ worker 崩溃后,新 worker 在引擎租约到期后(约 30s)接手,`drive` 步
执行结果写进 `decision.resolution.execution`。身份歧义类由 sources-identity 在处理端点里直接执行。
## 自动调查与恢复
依据 REQ §8.4.2(下载无进展默认不换源)、§10.7(有界任务、同根因去重、有限重试)、AT68 / AT71。
**模式**(`agent.autoInvestigateMode`):
| 模式 | run 参数 | 能做什么 |
|---|---|---|
| `investigate`(缺省) | `mode=plan_only`,白名单 = 只读能力 + `identity.resolve` | 查证据、给结论与建议,事项交还人工 |
| `recover` | `mode=execute`,白名单 = 上面 + `task.retry` + `download.recheck`,`created_by=system:auto-recover` | 在持续授权范围内执行恢复动作,系统核实后自动关闭事项 |
**恢复范围**:
- `download.recheck`(execute,replay=never):对已纳管、未移除的下载任务调 qB 强制重新校验(`action=recheck`,起点 missing / failed / stalled / paused),或恢复暂停(`action=resume`)。缓存阈值暂停和人工暂停不自动恢复。只动这一个种子,不换源、不删除、不改路径 / 分类 / 标签。
- `task.retry`:重试失败 / 死信的业务任务,副作用由各处理器的操作账本去重。
- `identity.resolve`:按已有证据等级自动接受;证据不够时仍进待处理。
**明确排除**:换源(`download.plan` / `download.submit`)、删除与缓存清理、`task.cancel`、`organize.execute`、`policy.apply`。这些能力不在恢复白名单里,模型调用会得到 `tool_call_rejected`。
**授权**:每次恢复调用仍经 `authorize` 校验持续授权,模型声明无效。可用的 grant:
- `action=download.recheck`,或发布 kind=automation 的策略时自动生成的 `policy.automation`;
- `action=task.retry` / `task.*`;`identity.resolve` / `policy.identity`;或 `*`。
grant 必须 active、未过期,绑定策略版本时该版本必须仍是 published。`scope.mediaKinds` / `scope.mediaEntityIds` 由 `download.recheck` 处理器按下载任务 → 计划 → Slot → 作品核对。注意:authorize 对非计划类能力拿不到目标作品,所以带 `mediaEntityIds` 的 grant 目前一律不覆盖 `download.recheck`,是保守拒绝。建 run 前先查有没有覆盖该类事项恢复动作的 grant(下载类看 `download.recheck` / `task.retry`,身份类看 `identity.resolve`)。没有就**降级为 investigate**,并在事项上写 `evidence.autoRecover.degraded` 和 `suggestion`,再追加一个可直接点选的"强制重新校验"选项(`capabilityId=download.recheck`,人工点选同样经授权、审计、账本)。
授权示例(把恢复限定在某个已发布策略下):
```sh
curl -X POST localhost:4380/api/v1/automation-grants -H "Authorization: Bearer <admin>" -H 'content-type: application/json' \
-d '{"action":"download.recheck","policyRevisionId":"<published automation 策略版本>","reason":"下载异常自动重新校验"}'
```
**核实与收尾**(`runAgent` 里的 `recover-*` 步骤):
1. 从账本(`capability_invocation`)取出本 run 成功的恢复动作,从审计取出被拒绝的调用。
2. 按业务事实轮询核实,间隔 `min(goalPollSec, 30)` 秒,最长 `agent.recoverVerifySec`:
- `download_task` 进入 downloading / queued / completed / importing / imported 算恢复,removed 算失败;
- 重试的 `task` 为 succeeded 算恢复,failed / dead / cancelled 算失败;
- `source_release` 已 resolved 算恢复。
3. 核实已恢复:事项转 `resolved`,`resolved_by=system:auto-recover`,`resolution.execution={kind:"agent_recover", verified:true, facts}`,发 `decision.updated`(executeResolution 看到 execution 已存在,不会重复执行)。run 汇总以"已恢复(系统核实)"开头,模型原话附在后面,标注"未经核查"。
4. 失败、超时(unverified)、预算超限、没有执行恢复动作:只写 `evidence.autoRecover.outcome` 和建议,事项由 finalize 转 `awaiting_user`,不会自动再试。run 汇总以"未恢复:…"开头。
**有界尝试**:`evidence.autoRecover.attempt` 记录该根因的第几次自动恢复。同一去重键(如 `download_failed:<downloadTaskId>`)下,历史上 `recoverAttempted=true` 的事项数达到 `agent.investigateMaxAttempts`(缺省 2)后,同根因的新事项不再唤醒模型,直接 `awaiting_user`,并写 `skipped=attempts_exhausted`。
## 配置上游
在设置页或 `PATCH /settings` 里配置:
@@ -85,6 +127,8 @@ worker 崩溃后,新 worker 在引擎租约到期后(约 30s)接手,`drive` 步
"maxToolCalls": 40, "maxDurationSec": 900, // 单次运行预算
"maxTokensPerRun": null, "maxCostUsdPerRun": null,
"autoInvestigate": false, "investigateMaxToolCalls": 8,
"autoInvestigateMode": "investigate", // 或 "recover"(授权内恢复,见上一节)
"investigateMaxAttempts": 2, "recoverVerifySec": 600,
"goalPollSec": 60, "goalWaitHours": 72
},
"budget": { "agentMaxRunsPerDay": 50, "agentMaxCostUsdPerDay": 5 }
@@ -137,6 +181,7 @@ curl -X POST localhost:4380/api/v1/agent/runs -H "Authorization: Bearer umt-dev-
| `e3-background-continues` | 关闭聊天后台照样推进 |
| `e4-crash-recovery` | worker 子进程在 download.submit 时 kill -9,新 worker resume,qB 只收到一次 add |
| `e5-auto-investigate` | 识别歧义 → 有界调查,去重 |
| `e6-auto-recover` | qB missingFiles → download_failed → 授权内恢复(task.inspect → download.recheck → 系统核实关闭事项);无授权时降级只调查;换源被拒;去重;同根因尝试上限;预算超限保持待处理 |
## 已知限制