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:
rootandClaude Opus 5.5 committed 2026-09-28 21:07:01 +08:00
1 parent 74c2ed45aa
commit cf2c4ff0e6
14 files changed
+1360 -26

No files matched your search

+40 -5
View File
@@ -9,6 +9,7 @@ import {
} from "@umt/agent";
import {
type AgentBudget,
type AgentPlanStep,
type AgentRunEvent,
type AgentRunMode,
type AgentRunStatus,
@@ -84,14 +85,22 @@ export interface AwaitWake {
fact: string;
}
/** agent.await / agent.spawn{await:true} 挂起请求:runAgent 据此 waitForFact,落定后以 resumedAwaits 重开会话 */
/**
* 会话挂起请求:runAgent 据此 waitForFact,条件满足后以 resumedAwaits 重开会话并重放挂起的调用。
* - subagents(缺省):agent.await / agent.spawn{await:true} 等子会话落定;
* - options:agent.propose_options 等用户选方案(decisionId);
* - approval:需审批的能力等管理员批准 / 拒绝(approvalId)。
*/
export interface AwaitSuspension {
kind?: "subagents" | "options" | "approval";
/** 挂起的工具调用(恢复时重放,同一 invocationId 不再挂起) */
invocationId: string;
/** 等待的子会话;null = 本会话全部子会话 */
runIds: string[] | null;
/** 超过这个时间(ISO)不再等,恢复后返回当前状态(未结束的标 pending) */
/** 超过这个时间(ISO)不再等,恢复后返回当前状态(未结束的标 pending / 按超时处理) */
deadline: string;
decisionId?: string;
approvalId?: string;
}
export interface LiveRun {
@@ -102,6 +111,8 @@ export interface LiveRun {
pump(): Promise<void>;
/** 子孙会话已用量(同步读缓存,pump 刷新) */
externalUsage(): ExternalUsage;
/** agent.plan 的当前步骤清单(首次用到时从事件流载入) */
plan?: { steps: AgentPlanStep[]; revision: number } | null;
}
const live = new Map<string, LiveRun>();
@@ -638,17 +649,41 @@ export async function wakeParent(ctx: ServerContext, childRunId: string): Promis
}
}
/** 唤醒挂起中的会话自身(用户选了方案 / 审批已决定):按 await_wake 发引擎信号;没挂起时什么也不做 */
export async function wakeRun(
ctx: ServerContext,
runId: string,
payload: Record<string, string | boolean>,
): Promise<boolean> {
try {
const r = await ctx.db.selectFrom("agent_run").select("await_wake").where("id", "=", runId).executeTakeFirst();
const w = r?.await_wake as AwaitWake | null | undefined;
if (!w?.engineRunId || !w.fact) return false;
await ctx.engine.signal(w.engineRunId, w.fact, payload);
return true;
} catch (err) {
ctx.logger.warn({ runId, err: { message: (err as Error).message } }, "唤醒会话失败(会话会兜底轮询)");
return false;
}
}
/**
* 子会话还没落定时请求挂起:记下等待条件并关闭会话(本调用在 JSONL 里保持未完成),drive 看到 requested 后返回
* suspended。同一批里已有别的调用在挂起时不再重复登记。
*/
async function requestSuspension(live: LiveRun, invocationId: string, runIds: string[] | null, timeoutSec: number) {
if (!live.suspend || live.suspend.requested) return;
live.suspend.requested = {
await suspendRun(live, {
kind: "subagents",
invocationId,
runIds,
deadline: new Date(Date.now() + timeoutSec * 1000).toISOString(),
};
});
}
/** 登记挂起条件并关闭会话(挂起的调用在 JSONL 里保持未完成);同一批里已有挂起时不再重复登记 */
export async function suspendRun(live: LiveRun, sus: AwaitSuspension): Promise<void> {
if (!live.suspend || live.suspend.requested) return;
live.suspend.requested = sus;
await new Promise((r) => setTimeout(r, 0));
await live.handle.close();
}