feat: 支持运行中 steer command

This commit is contained in:
Codex
2026-06-02 10:04:36 +08:00
parent 1d45d272f1
commit d90e01a91c
15 changed files with 206 additions and 13 deletions
+18 -1
View File
@@ -131,6 +131,23 @@ console.log(JSON.stringify({ apiVersion: manifest.apiVersion, kind: manifest.kin
assert.equal(secondEnvelope.terminalStatus, "completed");
assert.equal(secondEnvelope.reply, "fake codex stdio reply");
const steerRun = await createHwlabRun(client, context, bundle, "hwlab-session-steer", "start a turn that waits for steer", "hwlab-command-steer-turn", 10_000);
const steerRunner = runOnce({ managerUrl: server.baseUrl, runId: steerRun.runId, commandId: steerRun.commandId, codexCommand: context.fakeCodexCommand, codexArgs: context.fakeCodexArgs, codexHome: context.codexHome, env: { CODEX_HOME: context.codexHome, AGENTRUN_FAKE_CODEX_MODE: "steer-waits", AGENTRUN_WORKSPACE_ROOT: path.join(context.tmp, "workspaces-steer") }, oneShot: true, pollIntervalMs: 50 });
await waitForCommandState(client, steerRun.runId, steerRun.commandId, "acknowledged");
const steerCommand = await client.post(`/api/v1/runs/${steerRun.runId}/commands`, { type: "steer", payload: { prompt: "STEER_MARK_SELFTEST", traceId: "hwlab-command-steer" }, idempotencyKey: "hwlab-command-steer" }) as { id: string };
await waitForCommandState(client, steerRun.runId, steerCommand.id, "completed");
const steerRunnerResult = await steerRunner as JsonRecord;
assert.equal(steerRunnerResult.terminalStatus, "completed");
const steerTurnEnvelope = await client.get(`/api/v1/runs/${steerRun.runId}/commands/${steerRun.commandId}/result`) as JsonRecord;
assert.equal(steerTurnEnvelope.terminalStatus, "completed");
assert.match(String(steerTurnEnvelope.reply), /steered:STEER_MARK_SELFTEST/u);
const steerCommandEnvelope = await client.get(`/api/v1/runs/${steerRun.runId}/commands/${steerCommand.id}/result`) as JsonRecord;
assert.equal(steerCommandEnvelope.terminalStatus, "completed");
const steerEventsResponse = await client.get(`/api/v1/runs/${steerRun.runId}/events?afterSeq=0&limit=200`) as { items?: Array<{ type?: string; payload?: JsonRecord }> };
const steerEvents = steerEventsResponse.items ?? [];
assert.ok(steerEvents.some((event) => event.type === "backend_status" && event.payload?.phase === "steer-command-acknowledged" && event.payload?.commandId === steerCommand.id && event.payload?.targetCommandId === steerRun.commandId));
assert.ok(steerEvents.some((event) => event.type === "backend_status" && event.payload?.phase === "turn/steer:completed" && event.payload?.commandId === steerCommand.id && event.payload?.targetCommandId === steerRun.commandId));
const runningCancel = await createHwlabRun(client, context, bundle, "hwlab-session-cancel-running", "cancel running", "hwlab-command-cancel-running", 10_000);
const running = runOnce({ managerUrl: server.baseUrl, runId: runningCancel.runId, codexCommand: context.fakeCodexCommand, codexArgs: context.fakeCodexArgs, codexHome: context.codexHome, env: { CODEX_HOME: context.codexHome, AGENTRUN_FAKE_CODEX_MODE: "missing-terminal", AGENTRUN_WORKSPACE_ROOT: path.join(context.tmp, "workspaces-running-cancel") }, oneShot: true });
await waitForCommandState(client, runningCancel.runId, runningCancel.commandId, "acknowledged");
@@ -138,7 +155,7 @@ console.log(JSON.stringify({ apiVersion: manifest.apiVersion, kind: manifest.kin
const runningResult = await running;
assert.equal(runningResult.terminalStatus, "cancelled");
return { name: "hwlab-manual-dispatch", tests: ["runner-job-idempotency", "pending-cancel", "result-envelope", "session-ref-resume", "resource-bundle-materialization", "resource-bundle-tool-alias", "same-run-runner-multiturn", "running-cancel"] };
return { name: "hwlab-manual-dispatch", tests: ["runner-job-idempotency", "pending-cancel", "result-envelope", "session-ref-resume", "resource-bundle-materialization", "resource-bundle-tool-alias", "same-run-runner-multiturn", "running-steer", "running-cancel"] };
} finally {
await new Promise<void>((resolve) => server.server.close(() => resolve()));
}
+42
View File
@@ -7,6 +7,7 @@ if (process.env.AGENTRUN_FAKE_CODEX_START_FILE) appendFileSync(process.env.AGENT
let threadCounter = 0;
let turnCounter = 0;
let observedThreadModel = false;
let activeSteerTurn: { id: string; completed: boolean; timer: NodeJS.Timeout | null } | null = null;
for await (const line of rl) {
const trimmed = String(line).trim();
@@ -148,6 +149,18 @@ for await (const line of rl) {
}, 50);
continue;
}
if (mode === "steer-waits") {
turnCounter += 1;
const turn = { id: `turn_selftest_${turnCounter}`, status: "running" };
notify("turn/started", { turn });
activeSteerTurn = {
id: turn.id,
completed: false,
timer: setTimeout(() => completeActiveSteerTurn("timeout-no-steer"), 2_000),
};
respond(message.id, { turn });
continue;
}
if (mode === "noisy-reasoning-events") {
turnCounter += 1;
const turn = { id: `turn_selftest_${turnCounter}`, status: "completed" };
@@ -176,6 +189,18 @@ for await (const line of rl) {
respond(message.id, { turn });
continue;
}
if (message.method === "turn/steer") {
if (mode !== "steer-waits" || !activeSteerTurn) {
respond(message.id, null, { code: -32000, message: "no active fake turn for steer" });
continue;
}
const text = steerText(message.params?.input);
notify("item/agentMessage/delta", { itemId: "msg_steer", delta: `steered:${text}` });
notify("item/completed", { item: { id: "msg_steer", type: "agentMessage", text: `steered:${text}` } });
respond(message.id, { accepted: true });
setTimeout(() => completeActiveSteerTurn("steer-applied"), 20);
continue;
}
respond(message.id, null, { code: -32601, message: `unsupported fake method ${message.method ?? "unknown"}` });
}
@@ -187,3 +212,20 @@ function respond(id: number | undefined, result: unknown, error?: unknown): void
function notify(method: string, params: unknown): void {
process.stdout.write(`${JSON.stringify({ method, params })}\n`);
}
function completeActiveSteerTurn(reason: string): void {
if (!activeSteerTurn || activeSteerTurn.completed) return;
activeSteerTurn.completed = true;
if (activeSteerTurn.timer) clearTimeout(activeSteerTurn.timer);
const turn = { id: activeSteerTurn.id, status: "completed", reason };
notify("turn/completed", { turn });
}
function steerText(input: unknown): string {
if (!Array.isArray(input)) return "";
return input.flatMap((item) => {
if (typeof item !== "object" || item === null || Array.isArray(item)) return [];
const text = (item as Record<string, unknown>).text;
return typeof text === "string" ? [text] : [];
}).join("");
}