feat(agent): automation-first sessions with step plan, option choice and inline approval
- agent.plan emits plan_updated; system prompt asks for a plan first and updates while executing - agent.propose_options registers a decision and suspends the run (waitForFact, no worker slot); the answer or timeout (recommended/stop) resumes it as the tool result - console-session runs get inline approvals for plan-bound capabilities: approve replays the call with the approval, reject fails it with the reason and records it on the decision - proposeFirst blocks execute capabilities until an option is chosen; E12 covers all paths Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
74c2ed45aa
commit
cf2c4ff0e6
14 files changed
+1360
-26
No files matched your search
@@ -26,6 +26,7 @@ import { type AgentRunRow, type DownloaderInstanceRow, sql } from "@umt/db";
|
||||
import { type RunUpstream, resolveRunUpstream } from "../pi/index.ts";
|
||||
import type { ServerContext } from "../types.ts";
|
||||
import { DEMO_SCRIPTS, parseFakeScripts } from "./fake-scripts.ts";
|
||||
import { canAskUser, canRequestApproval, sessionOptions } from "./interaction.ts";
|
||||
import { buildGoalReport } from "./report.ts";
|
||||
import { type AwaitSuspension, attachLiveRun, type LiveRun, subagentSettings, wakeParent } from "./subagents.ts";
|
||||
|
||||
@@ -72,9 +73,12 @@ export interface RunScope {
|
||||
capabilities: string[];
|
||||
allowedKinds: CapabilityKind[] | null;
|
||||
decisionId?: string | null;
|
||||
/** 会话选项(见 interaction.ts RunSessionOptions) */
|
||||
interactive?: boolean;
|
||||
proposeFirst?: boolean;
|
||||
}
|
||||
|
||||
/** 按模式确定能力白名单与类别上限;plan_only 下给了 execute 类能力直接报错 */
|
||||
/** 按内部能力上限确定白名单与类别上限;plan_only(后台只读调查)下给了 execute 类能力直接报错 */
|
||||
export function scopeFor(mode: "execute" | "plan_only", allowlist?: readonly string[]): RunScope {
|
||||
const all = Object.values(CAPABILITY_CATALOG);
|
||||
if (mode === "plan_only") {
|
||||
@@ -251,7 +255,7 @@ function systemPromptFor(
|
||||
`可用能力:${scope.capabilities.join(", ")}`,
|
||||
"有副作用的能力(提交下载、执行整理、发布策略、重试/取消任务)需要服务端授权;被拒时说明缺少哪项授权。",
|
||||
];
|
||||
if (run.mode === "plan_only") constraints.push("本次只给方案:只调查与规划,不提交下载、不改文件、不发布策略。");
|
||||
if (run.mode === "plan_only") constraints.push("本次只读:只调查与规划,不提交下载、不改文件、不发布策略。");
|
||||
if (sub.canSpawn)
|
||||
constraints.push(
|
||||
`可以用 agent.spawn 把彼此独立的有界子目标交给子会话(同时最多 ${sub.maxConcurrent} 个,派生层数上限 ${sub.maxDepth});` +
|
||||
@@ -265,10 +269,22 @@ function systemPromptFor(
|
||||
);
|
||||
else if (run.trigger === "event")
|
||||
constraints.push("本任务由后台事件触发,只做只读调查:收集证据,给出结论与建议,不要反复重试同一操作。");
|
||||
const opts = sessionOptions(run);
|
||||
return buildSystemPrompt({
|
||||
role: run.trigger === "event" ? "媒体库异常调查员" : "媒体库自动化 Agent",
|
||||
goal: run.goal,
|
||||
constraints,
|
||||
// 自动化优先:用户会话默认执行,先出步骤清单;有取舍时给方案;"先看方案再执行"是会话选项
|
||||
...(canAskUser(run)
|
||||
? {
|
||||
interaction: {
|
||||
plan: scope.capabilities.includes("agent.plan"),
|
||||
options: scope.capabilities.includes("agent.propose_options"),
|
||||
proposeFirst: opts.proposeFirst === true,
|
||||
approvals: canRequestApproval(run),
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -389,9 +405,20 @@ export async function drive(
|
||||
.select("id")
|
||||
.where("parent_run_id", "=", runId)
|
||||
.executeTakeFirst());
|
||||
// 方案选择只在用户发起的顶层会话里提供(子会话 / 后台会话没有人可问)
|
||||
const askUser = canAskUser(run);
|
||||
const capabilities = (scope.capabilities?.length ? scope.capabilities : Object.keys(CAPABILITY_CATALOG)).filter(
|
||||
(id) => (id === "agent.spawn" ? canSpawn : id === "agent.await" ? canSpawn || hasChildren : true),
|
||||
(id) =>
|
||||
id === "agent.spawn"
|
||||
? canSpawn
|
||||
: id === "agent.await"
|
||||
? canSpawn || hasChildren
|
||||
: id === "agent.propose_options"
|
||||
? askUser
|
||||
: true,
|
||||
);
|
||||
// 会话内审批:挂起的审批调用恢复后要重放(只放行本次恢复针对的那个调用)
|
||||
const resumed = new Set(opts.resumedAwaits ?? []);
|
||||
let external: LiveRun | null = null;
|
||||
let handle: AgentRunHandle;
|
||||
try {
|
||||
@@ -405,6 +432,7 @@ export async function drive(
|
||||
actor,
|
||||
firstSeq: maxSeq?.m === null || maxSeq?.m === undefined ? 0 : Number(maxSeq.m) + 1,
|
||||
externalUsage: () => external?.externalUsage() ?? { toolCalls: 0, totalTokens: 0, costUsd: 0 },
|
||||
...(canRequestApproval(run) ? { replayGate: (_cap: string, inv: string) => resumed.has(inv) } : {}),
|
||||
});
|
||||
} catch (err) {
|
||||
return { status: "failed", reason: "error", budget: null, message: (err as Error).message, finalText: "" };
|
||||
|
||||
Reference in new issue
Block a user