fix: 支持长工具调用 interrupt 与硬超时回收

This commit is contained in:
Codex
2026-06-10 00:25:54 +08:00
parent da218772ea
commit ff2b5dce34
4 changed files with 213 additions and 38 deletions
+33 -9
View File
@@ -199,7 +199,7 @@ async function executeCommand(api: RunnerManagerApi, options: RunnerOnceOptions,
await appendBestEffort(api, options.runId, annotateCommandEvent(event, command.id, attemptId, runner.id));
},
onActiveTurn: (control: BackendActiveTurnControl) => {
stopSteerWatch = startSteerWatch(api, options.runId, command.id, attemptId, runner.id, control, options.pollIntervalMs);
stopSteerWatch = startActiveTurnCommandWatch(api, options.runId, command.id, attemptId, runner.id, control, options.pollIntervalMs);
return () => {
stopSteerWatch?.();
stopSteerWatch = undefined;
@@ -224,7 +224,7 @@ async function executeCommand(api: RunnerManagerApi, options: RunnerOnceOptions,
}
}
function startSteerWatch(api: RunnerManagerApi, runId: string, targetCommandId: string, attemptId: string, runnerId: string, control: BackendActiveTurnControl, pollIntervalMs: number | undefined): () => void {
function startActiveTurnCommandWatch(api: RunnerManagerApi, runId: string, targetCommandId: string, attemptId: string, runnerId: string, control: BackendActiveTurnControl, pollIntervalMs: number | undefined): () => void {
let stopped = false;
let polling = false;
const seen = new Set<string>();
@@ -234,13 +234,14 @@ function startSteerWatch(api: RunnerManagerApi, runId: string, targetCommandId:
polling = true;
try {
const commands = await api.listCommands(runId, { afterSeq: 0, limit: 100 });
const pendingSteer = commands.filter((item) => item.type === "steer" && item.state === "pending" && !seen.has(item.id));
for (const steerCommand of pendingSteer) {
seen.add(steerCommand.id);
await handleSteerCommand(api, runId, steerCommand, targetCommandId, attemptId, runnerId, control);
const pendingControlCommands = commands.filter((item) => (item.type === "steer" || item.type === "interrupt") && item.state === "pending" && !seen.has(item.id));
for (const controlCommand of pendingControlCommands) {
seen.add(controlCommand.id);
if (controlCommand.type === "interrupt") await handleInterruptCommand(api, runId, controlCommand, targetCommandId, attemptId, runnerId, control);
else await handleSteerCommand(api, runId, controlCommand, targetCommandId, attemptId, runnerId, control);
}
} catch {
// The active backend turn remains authoritative; missed steer commands stay pending for the next poll.
// The active backend turn remains authoritative; missed control commands stay pending for the next poll.
} finally {
polling = false;
}
@@ -275,11 +276,29 @@ async function handleSteerCommand(api: RunnerManagerApi, runId: string, command:
}
}
async function handleInterruptCommand(api: RunnerManagerApi, runId: string, command: CommandRecord, targetCommandId: string, attemptId: string, runnerId: string, control: BackendActiveTurnControl): Promise<void> {
const acked = await api.ackCommand(command.id);
if (acked.state === "cancelled") {
await api.reportCommandStatus(command.id, { terminalStatus: "cancelled", failureKind: "cancelled", failureMessage: "interrupt command cancelled before delivery", threadId: control.threadId, turnId: control.turnId });
return;
}
const reason = interruptReason(command.payload);
await appendBestEffort(api, runId, { type: "backend_status", payload: { phase: "interrupt-command-acknowledged", commandId: command.id, commandType: "interrupt", targetCommandId, attemptId, runnerId, threadId: control.threadId, turnId: control.turnId, reason } });
try {
await control.interrupt();
await appendBestEffort(api, runId, { type: "backend_status", payload: { phase: "turn/interrupt:completed", commandId: command.id, commandType: "interrupt", targetCommandId, attemptId, runnerId, threadId: control.threadId, turnId: control.turnId, reason } });
await api.reportCommandStatus(command.id, { terminalStatus: "completed", failureKind: null, failureMessage: null, threadId: control.threadId, turnId: control.turnId });
} catch (error) {
const failureKind = failureKindFromError(error);
await reportNonTerminalCommandFailure(api, runId, command.id, attemptId, runnerId, { terminalStatus: terminalStatusForFailure(failureKind), failureKind, message: errorMessage(error) }, "runner:interrupt", control);
}
}
async function failPendingSteerWithoutActiveTurn(api: RunnerManagerApi, runId: string, commands: CommandRecord[], runnerId: string, attemptId: string): Promise<void> {
for (const command of commands.filter((item) => item.type === "steer" && item.state === "pending")) {
for (const command of commands.filter((item) => (item.type === "steer" || item.type === "interrupt") && item.state === "pending")) {
const acked = await api.ackCommand(command.id);
if (acked.state === "cancelled") continue;
await reportNonTerminalCommandFailure(api, runId, command.id, attemptId, runnerId, { terminalStatus: "blocked", failureKind: "schema-invalid", message: "steer command requires an active turn" }, "runner:steer:no-active-turn");
await reportNonTerminalCommandFailure(api, runId, command.id, attemptId, runnerId, { terminalStatus: "blocked", failureKind: "schema-invalid", message: `${command.type} command requires an active turn` }, `runner:${command.type}:no-active-turn`);
}
}
@@ -296,6 +315,11 @@ function steerPrompt(payload: JsonRecord): string | null {
return null;
}
function interruptReason(payload: JsonRecord): string | null {
const value = payload.reason ?? payload.message ?? payload.prompt ?? payload.text;
return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
}
function isTerminalRun(run: RunRecord): boolean {
return run.status === "completed" || run.status === "failed" || run.status === "blocked" || run.status === "cancelled";
}