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

+43 -19
View File
@@ -27,7 +27,7 @@ import { type RunUpstream, resolveRunUpstream } from "../pi/index.ts";
import type { ServerContext } from "../types.ts";
import { DEMO_SCRIPTS, parseFakeScripts } from "./fake-scripts.ts";
import { buildGoalReport } from "./report.ts";
import { attachLiveRun, type LiveRun, subagentSettings } from "./subagents.ts";
import { type AwaitSuspension, attachLiveRun, type LiveRun, subagentSettings } from "./subagents.ts";
// runAgent 任务的执行体:从设置装配 Pi(或 fake)运行时 → 事件先写 agent_run_event 再推 SSE →
// 崩溃恢复(pendingOperations>0 时先按幂等键对账未知结果再 resume)→ 消费追加消息 → 汇报以业务表为准。
@@ -108,11 +108,13 @@ export function sessionDirOf(env: AgentEnv, configured?: string | null): string
}
export interface DriveResult {
status: "completed" | "failed" | "cancelled";
/** suspended:agent.await 挂起等子会话(会话已关闭,runAgent waitForFact 后以 resumedAwaits 重开) */
status: "completed" | "failed" | "cancelled" | "suspended";
reason: "budget" | "aborted" | "error" | null;
budget: string | null;
message: string | null;
finalText: string;
suspension?: AwaitSuspension;
}
/**
@@ -305,6 +307,7 @@ export async function drive(
registry: CapabilityRegistry,
runId: string,
signal: AbortSignal,
opts: { resumedAwaits?: readonly string[] } = {},
): Promise<DriveResult> {
const { ctx } = env;
const run = await ctx.db.selectFrom("agent_run").selectAll().where("id", "=", runId).executeTakeFirstOrThrow();
@@ -406,7 +409,7 @@ export async function drive(
} catch (err) {
return { status: "failed", reason: "error", budget: null, message: (err as Error).message, finalText: "" };
}
const liveRun = await attachLiveRun(env, runId, handle);
const liveRun = await attachLiveRun(env, runId, handle, opts);
external = liveRun.run;
// 取消传播:任务取消信号或 agent_run.cancel_requested_at → 中止会话
@@ -428,8 +431,42 @@ export async function drive(
void liveRun.run.pump();
}, 1000);
const suspended = (): DriveResult | null => {
const sus = liveRun.run.suspend?.requested;
return sus
? { status: "suspended", reason: null, budget: null, message: null, finalText: "", suspension: sus }
: null;
};
try {
let outcome: RunOutcome | null = null;
let outcome: RunOutcome;
try {
outcome = await runOutcome();
} catch (err) {
// agent.await 挂起时会话被主动关闭,prompt / resume 以 HarnessClosed 结束
const sus = suspended();
if (sus) return sus;
throw err;
}
const sus = suspended();
if (sus) return sus;
if (outcome.status === "completed")
return { status: "completed", reason: null, budget: null, message: null, finalText: outcome.finalText };
return {
status: outcome.reason === "aborted" ? "cancelled" : "failed",
reason: outcome.reason ?? "error",
budget: outcome.budget ?? null,
message: outcome.message ?? null,
finalText: outcome.finalText,
};
} finally {
clearInterval(watch);
signal.removeEventListener("abort", abortOnce);
await liveRun.stop();
await handle.close();
}
async function runOutcome(): Promise<RunOutcome> {
let outcome: RunOutcome;
if (handle.pendingOperations > 0) {
await reconcilePending(env, registry, run, signal);
outcome = await handle.resume();
@@ -452,25 +489,12 @@ export async function drive(
};
}
}
while (outcome.status === "completed" && !aborting) {
while (outcome.status === "completed" && !aborting && !liveRun.run.suspend?.requested) {
const text = await claimMessage(ctx, runId);
if (text === null) break;
outcome = await handle.prompt(text);
}
if (outcome.status === "completed")
return { status: "completed", reason: null, budget: null, message: null, finalText: outcome.finalText };
return {
status: outcome.reason === "aborted" ? "cancelled" : "failed",
reason: outcome.reason ?? "error",
budget: outcome.budget ?? null,
message: outcome.message ?? null,
finalText: outcome.finalText,
};
} finally {
clearInterval(watch);
signal.removeEventListener("abort", abortOnce);
await liveRun.stop();
await handle.close();
return outcome;
}
}