From 275aba1450e461c71b06a1470d76b0f045b0dd6b Mon Sep 17 00:00:00 2001 From: lyon Date: Thu, 25 Jun 2026 06:45:13 +0800 Subject: [PATCH] fix: seal canceled workbench final response --- .../cloud/workbench-projection-writer.test.ts | 68 +++++++++++++++++++ internal/cloud/workbench-turn-projection.ts | 17 ++++- 2 files changed, 83 insertions(+), 2 deletions(-) diff --git a/internal/cloud/workbench-projection-writer.test.ts b/internal/cloud/workbench-projection-writer.test.ts index d04a3698..4a727fc8 100644 --- a/internal/cloud/workbench-projection-writer.test.ts +++ b/internal/cloud/workbench-projection-writer.test.ts @@ -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 = { diff --git a/internal/cloud/workbench-turn-projection.ts b/internal/cloud/workbench-turn-projection.ts index 9d581738..118ed3f4 100644 --- a/internal/cloud/workbench-turn-projection.ts +++ b/internal/cloud/workbench-turn-projection.ts @@ -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;