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

+18 -2
View File
@@ -1,4 +1,5 @@
import { join, resolve } from "node:path";
import type { DownloaderClient } from "@umt/adapters";
import {
type AgentRunHandle,
type AgentSettings,
@@ -21,7 +22,7 @@ import {
type GlobalSettings,
KnownCapabilityId,
} from "@umt/contracts";
import { type AgentRunRow, sql } from "@umt/db";
import { type AgentRunRow, type DownloaderInstanceRow, sql } from "@umt/db";
import type { ServerContext } from "../types.ts";
import { DEMO_SCRIPTS, parseFakeScripts } from "./fake-scripts.ts";
import { buildGoalReport } from "./report.ts";
@@ -36,6 +37,8 @@ export interface AgentModuleOptions {
fakeScripts?: FakeScript[];
/** 覆盖 settings.agent.goalPollSec(测试用小值) */
goalPollSec?: number;
/** download.recheck 用的下载器客户端;缺省按实例配置 + secret 现场创建 qB 客户端 */
downloader?: (inst: DownloaderInstanceRow) => Promise<DownloaderClient>;
}
export interface AgentEnv {
@@ -54,6 +57,14 @@ export const INVESTIGATE_CAPABILITIES = [
"task.inspect",
"identity.resolve",
] as const;
/**
* 授权内恢复(settings.agent.autoInvestigateMode = recover)的能力白名单:只读调查 + 恢复类
* (重试失败任务、强制重新校验 / 恢复暂停)。换源(download.plan / download.submit)、删除 / 清理、
* 取消任务、整理执行、策略发布一律不在白名单内。实际能否执行仍由 authorize 按持续授权判定。
*/
export const RECOVER_CAPABILITIES = [...INVESTIGATE_CAPABILITIES, "task.retry", "download.recheck"] as const;
/** 被视为"恢复动作"的能力(结果需系统核实) */
export const RECOVERY_ACTIONS = ["task.retry", "download.recheck", "identity.resolve"] as const;
export interface RunScope {
capabilities: string[];
@@ -243,7 +254,12 @@ function systemPromptFor(run: AgentRunRow, scope: RunScope): string {
];
if (run.mode === "plan_only")
constraints.push("本次为“先给方案”:只调查与规划,不提交下载、不做文件变更、不发布策略。");
if (run.trigger === "event")
if (run.trigger === "event" && run.mode === "execute")
constraints.push(
"本任务由后台事件触发,是有界的授权内恢复:先查证据,再只在授权范围内执行恢复(task.retry / download.recheck / identity.resolve)," +
"每种恢复动作最多一次;不换源、不删除、不清理;恢复是否成功由系统按业务状态核实,不要自行宣称已恢复。",
);
else if (run.trigger === "event")
constraints.push("本任务由后台事件触发,是有界的只读调查:收集证据、给出结论与建议,不要反复重试同一操作。");
return buildSystemPrompt({
role: run.trigger === "event" ? "媒体库异常调查员" : "媒体库自动化助手",