feat(agent): agent.await suspends the session instead of holding a worker slot

When children are still running, agent.await records the wait, closes
the session (the tool call stays pending in the JSONL) and drive returns
suspended. runAgent waits with waitForFact for the children to settle,
the deadline, or cancellation, then reopens the session; agent.await is
replay-safe, so resume replays it and the model gets the real result.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
rootandClaude Opus 5.5 committed 2026-09-28 13:45:40 +08:00
1 parent 3e37ac8ae7
commit d64f735810
4 files changed
+129 -24

No files matched your search

+52 -3
View File
@@ -31,7 +31,9 @@ import type { AgentEnv, RunScope } from "./runner.ts";
// 能力白名单 ⊆ 父、模式不高于父、预算从父剩余预算划拨;settings.pi.subagents{enabled,maxConcurrent,maxDepth}。
// - 并发:子会话在 runAgent 里先占名额(父会话下同时 running 的子会话 < maxConcurrent,咨询锁内原子判定),
// 没名额就 waitForFact 挂起(不占 worker、不占模型)。
// - agent.await:等子会话结束(轮询业务表,不调用模型),返回脱敏后的汇报摘要。
// - agent.await:等子会话结束,返回脱敏后的汇报摘要。runAgent 驱动的会话里,子会话还没结束时挂起:
// 关闭本会话(工具调用留在 JSONL 里未完成),runAgent 用 waitForFact 等子会话落定(不占 worker、不占模型),
// 再打开会话 resume —— agent.await 是只读能力(replay=safe),恢复时重放一次拿到结果。见 AwaitSuspension。
// - 事件:父会话运行进程把子会话事件按父 seq 镜像进父事件流(subagent_spawned / subagent_event / subagent_completed);
// 父会话已不在运行时,由直接写入器在咨询锁内补记 subagent_completed。
// - 取消:父会话取消递归传播到全部子孙会话;预算:父会话运行时把子孙会话用量计入自己的预算。
@@ -64,8 +66,20 @@ const safe = (s: string | null | undefined, n = 1500) => clip(redactString(s ??
// ── 进程内活跃会话(镜像泵 + 用量缓存) ──
/** agent.await 挂起请求:runAgent 据此 waitForFact,落定后以 resumedAwaits 重开会话 */
export interface AwaitSuspension {
/** 挂起的工具调用(恢复时重放,同一 invocationId 不再挂起) */
invocationId: string;
/** 等待的子会话;null = 本会话全部子会话 */
runIds: string[] | null;
/** 超过这个时间(ISO)不再等,恢复后返回当前状态(未结束的标 pending) */
deadline: string;
}
export interface LiveRun {
handle: AgentRunHandle;
/** runAgent 的 drive 打开的会话才可挂起;已挂起过的 invocationId 恢复时不再挂起 */
suspend?: { resumed: ReadonlySet<string>; requested: AwaitSuspension | null };
/** 把子会话的新事件 / 新派生 / 结束镜像进本会话事件流(串行) */
pump(): Promise<void>;
/** 子孙会话已用量(同步读缓存,pump 刷新) */
@@ -80,6 +94,7 @@ export async function attachLiveRun(
env: AgentEnv,
runId: string,
handle: AgentRunHandle,
opts: { resumedAwaits?: readonly string[] } = {},
): Promise<{ run: LiveRun; stop(): Promise<void> }> {
const db = env.ctx.db;
const prior = await db
@@ -140,7 +155,12 @@ export async function attachLiveRun(
chain = next;
return next;
};
const run: LiveRun = { handle, pump, externalUsage: () => usage };
const run: LiveRun = {
handle,
pump,
externalUsage: () => usage,
suspend: { resumed: new Set(opts.resumedAwaits ?? []), requested: null },
};
live.set(runId, run);
return {
run,
@@ -553,6 +573,21 @@ export async function awaitChildren(
}
}
/** runAgent 挂起期间的唤醒条件:等待的子会话全部落定 / 超时 / 本会话被取消 */
export async function awaitReady(ctx: ServerContext, parentId: string, sus: AwaitSuspension): Promise<true | null> {
if (Date.now() >= Date.parse(sus.deadline)) return true;
const parent = await ctx.db
.selectFrom("agent_run")
.select("cancel_requested_at")
.where("id", "=", parentId)
.executeTakeFirst();
if (!parent || parent.cancel_requested_at) return true;
let q = ctx.db.selectFrom("agent_run").select("status").where("parent_run_id", "=", parentId);
if (sus.runIds?.length) q = q.where("id", "in", sus.runIds);
const rows = await q.execute();
return rows.every((r) => SETTLED.includes(r.status)) ? true : null;
}
// ── 能力处理器 ──
export interface SpawnDeps {
@@ -636,7 +671,21 @@ export function registerSubagentCapabilities(registry: CapabilityRegistry, ctx:
registry.register(capabilitySpec("agent.await"), async (input, c) => {
const parent = await parentOf(ctx, c);
return awaitChildren(ctx, parent.id, input.runIds, input.timeoutSec, c.signal);
const live = liveRun(parent.id);
const inv = c.invocationId;
if (!live?.suspend || !inv) return awaitChildren(ctx, parent.id, input.runIds, input.timeoutSec, c.signal);
// 恢复时重放:已挂起等过,直接返回当前状态
const snap = await awaitChildren(ctx, parent.id, input.runIds, 0, c.signal);
if (snap.allDone || live.suspend.resumed.has(inv) || live.suspend.requested) return snap;
// 挂起:记下等待条件,关闭会话(本调用在 JSONL 里保持未完成);drive 看到 requested 后返回 suspended
live.suspend.requested = {
invocationId: inv,
runIds: input.runIds?.length ? [...input.runIds] : null,
deadline: new Date(Date.now() + input.timeoutSec * 1000).toISOString(),
};
await new Promise((r) => setTimeout(r, 0));
await live.handle.close();
return snap;
});
}