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:
1 parent
23309042dc
commit
86af723b2a
13 files changed
+1072
-15
No files matched your search
@@ -4,9 +4,19 @@ import { defineTask, type OutboxRouteFn, type TaskRegistry } from "@umt/tasks";
|
||||
import { z } from "zod";
|
||||
import type { ServerContext } from "../types.ts";
|
||||
import { retryTask } from "./capabilities.ts";
|
||||
import {
|
||||
annotateRunSummary,
|
||||
applyRecoveryOutcome,
|
||||
autoInvestigate,
|
||||
checkRecovery,
|
||||
isInvestigationRun,
|
||||
type RecoveryCheck,
|
||||
type RecoveryOutcome,
|
||||
recoveryActions,
|
||||
} from "./investigate.ts";
|
||||
import { buildGoalReport } from "./report.ts";
|
||||
import { type AgentEnv, type DriveResult, drive, goalSettled } from "./runner.ts";
|
||||
import { autoInvestigate, finalize } from "./service.ts";
|
||||
import { finalize } from "./service.ts";
|
||||
|
||||
// 持久任务:runAgent(驱动一次有界 Agent 会话 → 等目标关联的下载/入库落定 → 以业务表汇报)
|
||||
// 与 resolveDecision(decision.created → 自动调查;decision.updated(resolved)→ 执行所选处理)。
|
||||
@@ -37,10 +47,31 @@ export function registerAgentTasks(registry: TaskRegistry, env: () => AgentEnv)
|
||||
let settled: AgentGoalReport | null = null;
|
||||
const run = await e.ctx.db
|
||||
.selectFrom("agent_run")
|
||||
.select(["mode"])
|
||||
.select(["mode", "dedupe_key"])
|
||||
.where("id", "=", runId)
|
||||
.executeTakeFirstOrThrow();
|
||||
if (res.status === "completed" && run.mode === "execute") {
|
||||
// 后台授权内恢复:核实业务事实(下载 / 任务 / 身份状态)后再收尾;不等"目标入库"
|
||||
const recovering = run.mode === "execute" && isInvestigationRun(run);
|
||||
let recovery: RecoveryOutcome | null = null;
|
||||
if (recovering) {
|
||||
const acts = await t.step.run("recover-actions", () => recoveryActions(e.ctx, runId));
|
||||
let check: RecoveryCheck | null = null;
|
||||
if (res.status === "completed" && acts.actions.some((a) => a.status === "succeeded")) {
|
||||
const s = await e.ctx.settings.get("agent");
|
||||
const pollSec = Math.min(e.opts.goalPollSec ?? s.goalPollSec, 30);
|
||||
check = await t.step.waitForFact<RecoveryCheck>("recover-verified", {
|
||||
check: async () => {
|
||||
const c = await checkRecovery(e.ctx, acts.actions);
|
||||
return c.state === "pending" ? null : c;
|
||||
},
|
||||
timeout: `${pollSec}s`,
|
||||
maxRounds: Math.max(1, Math.min(900, Math.ceil(s.recoverVerifySec / pollSec))),
|
||||
});
|
||||
// 超时:用最后一次观测作为证据(state 仍是 pending → unverified)
|
||||
check ??= await t.step.run("recover-last-check", () => checkRecovery(e.ctx, acts.actions));
|
||||
}
|
||||
recovery = await t.step.run("recover-outcome", () => applyRecoveryOutcome(e.ctx, runId, res, acts, check));
|
||||
} else if (res.status === "completed" && run.mode === "execute") {
|
||||
const s = await e.ctx.settings.get("agent");
|
||||
const pollSec = e.opts.goalPollSec ?? s.goalPollSec;
|
||||
const rounds = Math.max(1, Math.min(900, Math.ceil((s.goalWaitHours * 3600) / pollSec)));
|
||||
@@ -59,7 +90,12 @@ export function registerAgentTasks(registry: TaskRegistry, env: () => AgentEnv)
|
||||
maxRounds: rounds,
|
||||
});
|
||||
}
|
||||
return t.step.run("finalize", () => finalize(e, runId, res, settled));
|
||||
const out = await t.step.run("finalize", () => finalize(e, runId, res, settled));
|
||||
if (recovery) {
|
||||
const rec = recovery;
|
||||
return t.step.run("recover-summary", () => annotateRunSummary(e.ctx, runId, rec));
|
||||
}
|
||||
return out;
|
||||
},
|
||||
}),
|
||||
defineTask({
|
||||
|
||||
Reference in new issue
Block a user