fix: 支持长工具调用 interrupt 与硬超时回收
This commit is contained in:
+33
-9
@@ -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";
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user