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

+28 -2
View File
@@ -17,12 +17,15 @@ import {
import { buildGoalReport } from "./report.ts";
import { type AgentEnv, type DriveResult, drive, goalSettled } from "./runner.ts";
import { finalize } from "./service.ts";
import { catchUpChildren, claimSubagentSlot } from "./subagents.ts";
import { awaitReady, catchUpChildren, claimSubagentSlot } from "./subagents.ts";
// 持久任务:runAgent(驱动一次有界 Agent 会话 → 等目标关联的下载/入库落定 → 以业务表汇报)
// 与 resolveDecision(decision.created → 自动调查;decision.updated(resolved)→ 执行所选处理)。
const id = z.string().uuid();
/** agent.await 挂起时的唤醒轮询间隔与单个会话最多挂起次数 */
const AWAIT_POLL_SEC = 2;
const MAX_AWAIT_SUSPENDS = 100;
const DB_RETRY = { maximumAttempts: 3, initialInterval: "1s", backoffCoefficient: 2, maximumInterval: "30s" } as const;
export function registerAgentTasks(registry: TaskRegistry, env: () => AgentEnv) {
@@ -72,7 +75,30 @@ export function registerAgentTasks(registry: TaskRegistry, env: () => AgentEnv)
: "claimed";
if (slot === null)
res = { status: "failed", reason: "error", budget: null, message: "等待子会话名额超时", finalText: "" };
else res = await t.step.run("drive", () => drive(e, e.ctx.capabilities, runId, t.abortSignal));
else {
res = await t.step.run("drive", () => drive(e, e.ctx.capabilities, runId, t.abortSignal));
// agent.await 挂起:会话已关闭,waitForFact 等子会话落定 / 超时 / 取消(不占 worker、不占模型),
// 再重开会话 resume(挂起的 agent.await 重放一次拿到结果)
const resumedAwaits: string[] = [];
for (let n = 1; res.status === "suspended" && res.suspension; n++) {
const sus = res.suspension;
if (n > MAX_AWAIT_SUSPENDS) {
res = { status: "failed", reason: "error", budget: null, message: "等待子会话次数过多", finalText: "" };
break;
}
const waitMs = Math.max(0, Date.parse(sus.deadline) - Date.now());
await t.step.waitForFact<true>(`subagent-await-${n}`, {
check: () => awaitReady(e.ctx, runId, sus),
timeout: `${AWAIT_POLL_SEC}s`,
maxRounds: Math.max(1, Math.ceil(waitMs / 1000 / AWAIT_POLL_SEC) + 1),
});
resumedAwaits.push(sus.invocationId);
const resumed = [...resumedAwaits];
res = await t.step.run(`drive-${n}`, () =>
drive(e, e.ctx.capabilities, runId, t.abortSignal, { resumedAwaits: resumed }),
);
}
}
let settled: AgentGoalReport | null = null;
const run = await e.ctx.db
.selectFrom("agent_run")