fix: seal canceled workbench final response

This commit is contained in:
lyon
2026-06-25 06:45:13 +08:00
parent a693e40b1e
commit 275aba1450
2 changed files with 83 additions and 2 deletions
@@ -90,6 +90,74 @@ test("workbench projection writer commits terminal owner evidence as sealed dura
assert.equal(facts.checkpoints[0].timing.finishedAt, "2026-06-20T11:00:00.000Z");
});
test("workbench projection writer seals canceled AgentRun turns with canonical cancel final response", async () => {
const factWrites = [];
const runtimeStore = {
async writeWorkbenchFacts(params, requestMeta) {
factWrites.push({ params, requestMeta });
return { written: true, facts: params.facts };
}
};
const accessController = {
async recordAgentSessionOwner(input) {
return {
id: input.sessionId,
projectId: input.projectId,
ownerUserId: input.ownerUserId,
conversationId: input.conversationId,
threadId: input.threadId,
lastTraceId: input.traceId,
status: input.status,
session: input.session,
updatedAt: "2026-06-20T11:02:00.000Z"
};
}
};
await writeWorkbenchProjectionSession({
accessController,
runtimeStore,
traceId: "trc_writer_canceled",
ownerUserId: "usr_writer",
ownerRole: "user",
sessionId: "ses_writer_canceled",
projectId: "prj_writer",
conversationId: "cnv_writer_canceled",
threadId: "thread-writer-canceled",
status: "canceled",
payload: {
traceId: "trc_writer_canceled",
status: "canceled",
userMessage: "当前 AgentRun 请求已取消;traceId/runId/commandId 已保留,可重试上一条消息。",
runnerTrace: {
traceId: "trc_writer_canceled",
status: "canceled",
events: [{ type: "cancel", status: "canceled", terminal: true, createdAt: "2026-06-20T11:02:00.000Z" }],
updatedAt: "2026-06-20T11:02:00.000Z"
},
agentRun: { runId: "run_writer_canceled", commandId: "cmd_writer_canceled", status: "cancelled", commandState: "cancelled", terminalStatus: "cancelled", lastSeq: 12 },
updatedAt: "2026-06-20T11:02:00.000Z"
},
session: {
sessionStatus: "canceled",
messages: [
{ messageId: "msg_writer_canceled_user", role: "user", text: "cancel me", status: "sent", turnId: "trc_writer_canceled", traceId: "trc_writer_canceled" },
{ messageId: "msg_writer_canceled_agent", role: "agent", text: "", status: "canceled", turnId: "trc_writer_canceled", traceId: "trc_writer_canceled" }
]
}
});
assert.equal(factWrites.length, 1);
const facts = factWrites[0].params.facts;
const agentMessage = facts.messages.find((message) => message.messageId === "msg_writer_canceled_agent");
assert.equal(facts.sessions[0].status, "canceled");
assert.equal(facts.turns[0].terminal, true);
assert.equal(facts.turns[0].finalResponse.text, "hwlab-user-cancel");
assert.equal(facts.turns[0].assistantText, "hwlab-user-cancel");
assert.equal(agentMessage.text, "hwlab-user-cancel");
assert.equal(facts.parts.some((part) => part.messageId === "msg_writer_canceled_agent" && part.partType === "final_response" && part.text === "hwlab-user-cancel" && part.sealed === true), true);
});
test("workbench projection writer does not seal running assistant text as final response", async () => {
const factWrites = [];
const runtimeStore = {
+15 -2
View File
@@ -5,6 +5,7 @@
export const TERMINAL_STATUSES = new Set(["completed", "failed", "blocked", "timeout", "cancelled", "canceled", "idle"]);
export const RUNNING_STATUSES = new Set(["running", "retrying", "pending", "queued", "accepted", "dispatching", "streaming", "active", "processing", "busy", "creating"]);
const CANCEL_FINAL_RESPONSE_TEXT = "hwlab-user-cancel";
export function createWorkbenchTurnProjection({ turnId = null, traceId = null, result = null, session = null, trace = null } = {}) {
const projectionTraceId = textValue(traceId ?? trace?.traceId ?? result?.traceId ?? session?.lastTraceId) || null;
@@ -175,17 +176,29 @@ function terminalTurnEvidence({ result = null, traceTerminal = null } = {}) {
);
if (resultStatus) {
if (resultStatus !== "completed" && retryableProviderInterruptionEvidence(result, result?.agentRun, result?.providerTrace, traceTerminal?.evidence)) return null;
return { source: "result", status: resultStatus, finalResponse: traceTerminal?.finalResponse ?? null, valuesRedacted: true };
return { source: "result", status: resultStatus, finalResponse: terminalFinalResponse(resultStatus, result, traceTerminal), valuesRedacted: true };
}
const statusOnly = terminalStatusFromValue(result?.status);
if (statusOnly && resultHasTerminalAuthority(result, traceTerminal)) {
if (statusOnly !== "completed" && retryableProviderInterruptionEvidence(result, result?.agentRun, result?.providerTrace, traceTerminal?.evidence)) return null;
return { source: "result", status: statusOnly, finalResponse: traceTerminal?.finalResponse ?? null, valuesRedacted: true };
return { source: "result", status: statusOnly, finalResponse: terminalFinalResponse(statusOnly, result, traceTerminal), valuesRedacted: true };
}
if (traceTerminal && retryableProviderInterruptionEvidence(traceTerminal.evidence)) return null;
return traceTerminal;
}
function terminalFinalResponse(status, result = null, traceTerminal = null) {
const direct = traceTerminal?.finalResponse ?? result?.finalResponse;
if (direct) return direct;
if (normalizeWorkbenchStatus(status) !== "canceled") return null;
return {
text: CANCEL_FINAL_RESPONSE_TEXT,
status: "canceled",
traceId: textValue(result?.traceId ?? traceTerminal?.finalResponse?.traceId ?? traceTerminal?.evidence?.traceId) || null,
valuesPrinted: false
};
}
function resultHasTerminalAuthority(result = null, traceTerminal = null) {
if (!result || typeof result !== "object") return false;
if (traceTerminal) return true;