Merge pull request #1631 from pikasTech/fix-1629-workbench-final-response

修复 Workbench terminal assistant finalResponse 封存
This commit is contained in:
Lyon
2026-06-20 00:12:27 +08:00
committed by GitHub
6 changed files with 316 additions and 17 deletions
+31 -10
View File
@@ -1670,8 +1670,8 @@ function agentRunLongLivedSessionGate(base = {}) {
};
}
function agentRunToolCalls(result = {}, status = "completed") {
const replyText = agentRunReplyText(result);
function agentRunToolCalls(result = {}, status = "completed", replyTextOverride = null) {
const replyText = messageAuthorityTextValue(replyTextOverride) || agentRunReplyText(result);
return [{
name: "agentrun.v01.command.turn",
status,
@@ -1701,8 +1701,10 @@ function agentRunResultToCodeAgentPayload({ base, result, traceStore, traceId, a
const terminalEventCreatedAt = agentRunResultTraceCreatedAt(runnerTrace, now);
const providerTrace = agentRunProviderTrace({ base, result, terminalStatus });
const replyText = agentRunReplyText(result);
if (terminalStatus === "completed" && replyText) {
const finalResponse = agentRunCompletedFinalResponse({ base, result, traceId, now, replyText });
if (terminalStatus === "completed") {
const traceReplyText = agentRunTraceTerminalAssistantText(runnerTrace);
const completedReplyText = replyText || traceReplyText;
const finalResponse = completedReplyText ? agentRunCompletedFinalResponse({ base, result, traceId, now, replyText: completedReplyText }) : null;
const traceSummary = agentRunCompletedTraceSummary({ base, runnerTrace, finalResponse, traceId });
if (appendResultEvent) {
traceStore.append(traceId, agentRunTraceEvent({
@@ -1727,18 +1729,18 @@ function agentRunResultToCodeAgentPayload({ base, result, traceStore, traceId, a
sessionReuse: agentRunSessionReuseSummary(base, base.agentRun.reused === true),
runner: agentRunRunnerSummary(base.agentRun),
runnerTrace: traceStore.snapshot(traceId, agentRunTraceMeta({}, {})),
toolCalls: agentRunToolCalls(result, "completed"),
toolCalls: agentRunToolCalls(result, "completed", completedReplyText),
skills: { status: "delegated", provider: ADAPTER_ID, count: 0, items: [], valuesPrinted: false },
longLivedSessionGate: agentRunLongLivedSessionGate(base),
providerTrace,
finalResponse,
finalResponse: finalResponse ?? null,
traceSummary,
reply: {
reply: finalResponse ? {
messageId: finalResponse.messageId,
role: "assistant",
content: finalResponse.text,
createdAt: finalResponse.createdAt
},
} : null,
usage: null,
agentRun: { ...base.agentRun, terminalStatus, completed: true, reuseEligible: true, providerTrace, valuesPrinted: false },
valuesPrinted: false
@@ -1831,6 +1833,25 @@ function agentRunCompletedFinalResponse({ base, result, traceId, now, replyText
};
}
function agentRunTraceTerminalAssistantText(runnerTrace = {}) {
const events = Array.isArray(runnerTrace?.events) ? runnerTrace.events : [];
for (let index = events.length - 1; index >= 0; index -= 1) {
const event = events[index];
if (!event || typeof event !== "object") continue;
if (!isAgentRunAssistantTraceEvent(event)) continue;
if (!(event.terminal === true || event.final === true || event.replyAuthority === true)) continue;
const textValue = messageAuthorityTextValue(event.finalResponse ?? event.text ?? event.content ?? event.message ?? event.summary ?? event.payload?.text ?? event.payload?.content ?? event.payload?.message);
if (textValue) return textValue;
}
return "";
}
function isAgentRunAssistantTraceEvent(event = {}) {
const type = String(event.type ?? event.eventType ?? "").trim().toLowerCase();
if (type === "assistant" || type === "assistant_message") return true;
return /assistant:message|assistant_message/u.test(String(event.label ?? "").toLowerCase());
}
function agentRunTerminalFailureFinalResponse({ base, traceId, now, status, text }) {
const textValue = String(text ?? "").trim();
return {
@@ -1880,14 +1901,14 @@ function agentRunCompletedTraceSummary({ base, runnerTrace, finalResponse, trace
source: "agentrun-command-result",
sourceEventCount: Number(runnerTrace?.eventCount ?? events.length ?? 0),
terminalStatus: "completed",
finalAssistantRow: {
finalAssistantRow: finalResponse ? {
role: finalResponse.role,
status: finalResponse.status,
textChars: finalResponse.textChars,
textPreview: finalResponse.text.slice(0, 240),
messageId: finalResponse.messageId,
valuesPrinted: false
},
} : null,
agentRun: {
runId: base.agentRun?.runId ?? null,
commandId: base.agentRun?.commandId ?? null,
+110
View File
@@ -1054,6 +1054,116 @@ test("AgentRun sync converts terminal command result even when run remains claim
}
});
test("AgentRun sync seals completed final response from authoritative terminal assistant trace event (#1629)", async () => {
const calls = [];
const traceId = "trc_issue1629_terminal_assistant_final";
const runId = "run_issue1629_trace_final";
const commandId = "cmd_issue1629_trace_final";
const finalText = [
"全部六份数据到手!下面是完整的六语言终极性能对比:",
"",
"| language | runtime | status |",
"|---|---|---|",
"| Lua | LuaJIT | pass |"
].join("\n");
const agentRunServer = createHttpServer(async (request, response) => {
const url = new URL(request.url || "/", "http://127.0.0.1");
calls.push({ method: request.method, path: url.pathname, search: url.search });
const send = (data) => {
response.writeHead(200, { "content-type": "application/json" });
response.end(`${JSON.stringify({ ok: true, data, traceId: "trc_fake_issue1629" })}\n`);
};
if (request.method === "GET" && url.pathname === `/api/v1/runs/${runId}/commands`) {
return send({ items: [
{ id: commandId, runId, state: "completed", type: "turn", seq: 1, idempotencyKey: traceId, payload: { traceId, conversationId: "cnv_issue1629", hwlabSessionId: "ses_issue1629", threadId: "thread_issue1629" } }
] });
}
if (request.method === "GET" && url.pathname === `/api/v1/runs/${runId}/events`) {
return send({ items: [
{ id: "evt_issue1629_progress", runId, seq: 21, type: "assistant_message", payload: { commandId, text: "正在补 Lua 基准测试。" }, createdAt: "2026-06-19T15:47:13.000Z" },
{ id: "evt_issue1629_final", runId, seq: 28, type: "assistant_message", payload: { commandId, text: finalText, final: true, replyAuthority: true }, createdAt: "2026-06-19T15:47:28.000Z" },
{ id: "evt_issue1629_terminal", runId, seq: 29, type: "terminal_status", payload: { commandId, terminalStatus: "completed" }, createdAt: "2026-06-19T15:47:29.000Z" }
] });
}
if (request.method === "GET" && url.pathname === `/api/v1/runs/${runId}/commands/${commandId}/result`) {
return send({
runId,
commandId,
attemptId: "attempt_issue1629",
runnerId: "runner_issue1629",
jobName: "agentrun-v01-runner-issue1629",
namespace: "agentrun-v01",
status: "completed",
runStatus: "completed",
commandState: "completed",
terminalStatus: "completed",
completed: true,
reply: null,
finalResponse: null,
lastSeq: 29,
eventCount: 29,
sessionRef: { sessionId: "ses_issue1629", conversationId: "cnv_issue1629", threadId: "thread_issue1629" }
});
}
response.writeHead(404, { "content-type": "application/json" });
response.end(`${JSON.stringify({ ok: false, message: `unexpected ${request.method} ${url.pathname}` })}\n`);
});
await new Promise((resolve) => agentRunServer.listen(0, "127.0.0.1", resolve));
const agentRunPort = agentRunServer.address().port;
const traceStore = createCodeAgentTraceStore();
try {
const currentResult = {
ok: true,
accepted: true,
shortConnection: true,
status: "running",
traceId,
conversationId: "cnv_issue1629",
sessionId: "ses_issue1629",
threadId: "thread_issue1629",
agentRun: {
adapter: "agentrun-v01",
managerUrl: `http://127.0.0.1:${agentRunPort}`,
runId,
commandId,
status: "running",
runStatus: "running",
commandState: "running",
terminalStatus: null,
lastSeq: 0,
valuesPrinted: false
},
valuesPrinted: false
};
const synced = await syncAgentRunChatResult({
traceId,
currentResult,
traceStore,
options: {
env: {
AGENTRUN_MGR_URL: `http://127.0.0.1:${agentRunPort}`,
HWLAB_CODE_AGENT_AGENTRUN_ALLOW_NON_K3S_URL: "1"
}
}
});
assert.equal(synced.resultSynced, true);
assert.equal(synced.result.status, "completed");
assert.equal(synced.result.finalResponse.text, finalText);
assert.equal(synced.result.assistantText ?? synced.result.finalResponse.text, finalText);
assert.equal(synced.result.reply.content, finalText);
assert.equal(synced.result.traceSummary.finalAssistantRow.textChars, finalText.length);
const assistantEvent = synced.result.runnerTrace.events.find((event) => event.label === "agentrun:assistant:message" && event.message === finalText);
assert.equal(assistantEvent?.message, finalText);
assert.deepEqual(calls.map((call) => call.path), [
`/api/v1/runs/${runId}/commands`,
`/api/v1/runs/${runId}/events`,
`/api/v1/runs/${runId}/commands/${commandId}/result`
]);
} finally {
await new Promise((resolve, reject) => agentRunServer.close((error) => (error ? reject(error) : resolve())));
}
});
test("cloud api AgentRun adapter reports persistent thread resume when a completed run needs a new runner", async () => {
const calls = [];
const hwlabSessionId = "ses_server-test-thread-resume";
@@ -58,6 +58,51 @@ test("workbench projection diagnostics keeps projecting health distinct from cau
assert.equal(diagnostics.projectionHealth, "projecting");
});
test("workbench turn projection seals final response from authoritative terminal assistant trace event (#1629)", () => {
const traceId = "trc_workbench_terminal_assistant_final";
const finalText = [
"全部六份数据到手!下面是完整的六语言终极性能对比:",
"",
"| language | runtime | status |",
"|---|---|---|",
"| Lua | LuaJIT | pass |"
].join("\n");
const trace = {
traceId,
status: "completed",
events: [
{ seq: 1, type: "assistant_message", status: "running", message: "正在补 Lua 基准测试。" },
{ seq: 2, type: "assistant_message", status: "completed", replyAuthority: true, final: true, message: finalText },
{ seq: 3, type: "result", status: "completed", terminal: true, label: "agentrun:terminal:completed" }
],
eventCount: 3
};
const projection = createWorkbenchTurnProjection({ traceId, result: { traceId, status: "completed" }, trace });
assert.equal(projection.status, "completed");
assert.equal(projection.terminal, true);
assert.equal(projection.finalResponse.text, finalText);
assert.equal(projection.assistantText, finalText);
assert.equal(projection.terminalEvidence.finalResponse.text, finalText);
});
test("workbench turn projection keeps progress-only assistant trace text out of final response (#1629)", () => {
const traceId = "trc_workbench_terminal_without_authoritative_final";
const trace = {
traceId,
status: "completed",
events: [
{ seq: 1, type: "assistant_message", status: "running", message: "这只是运行中的进度。" },
{ seq: 2, type: "result", status: "completed", terminal: true, label: "agentrun:terminal:completed" }
],
eventCount: 2
};
const projection = createWorkbenchTurnProjection({ traceId, result: { traceId, status: "completed" }, trace });
assert.equal(projection.status, "completed");
assert.equal(projection.terminal, true);
assert.equal(projection.finalResponse, null);
assert.equal(projection.assistantText, null);
});
test("workbench trace event page exposes monotonic cursor range for restored mixed events", async () => {
const traceId = "trc_workbench_trace_mixed_cursor";
const session = {
+39 -7
View File
@@ -9,12 +9,13 @@ export const RUNNING_STATUSES = new Set(["running", "pending", "queued", "accept
export function createWorkbenchTurnProjection({ turnId = null, traceId = null, result = null, session = null, trace = null } = {}) {
const projectionTraceId = textValue(traceId ?? trace?.traceId ?? result?.traceId ?? session?.lastTraceId) || null;
const projectionTurnId = textValue(turnId) || projectionTraceId;
const terminalEvidence = terminalTurnEvidence({ result, trace });
const traceTerminal = traceTerminalEvidence(trace);
const terminalEvidence = terminalTurnEvidence({ result, traceTerminal });
const activeEvidence = activeTurnEvidence({ result, session, trace });
const status = terminalEvidence?.status ?? activeEvidence?.status ?? "unknown";
const running = RUNNING_STATUSES.has(status);
const terminal = Boolean(terminalEvidence && TERMINAL_STATUSES.has(status) && !running);
const finalText = terminal ? projectionText(result?.finalResponse, result?.assistantText, result?.reply, result?.text, result?.summary, trace?.finalResponse, trace?.terminalEvidence?.finalResponse) : null;
const finalText = terminal ? projectionText(result?.finalResponse, result?.assistantText, result?.reply, result?.text, result?.summary, trace?.finalResponse, trace?.terminalEvidence?.finalResponse, terminalEvidence?.finalResponse) : null;
const agentRun = objectValue(result?.agentRun ?? trace?.agentRun);
const lastEvent = traceLastEvent(trace);
return {
@@ -88,10 +89,10 @@ export function normalizeWorkbenchStatus(value) {
return text || "unknown";
}
function terminalTurnEvidence({ result = null, trace = null } = {}) {
function terminalTurnEvidence({ result = null, traceTerminal = null } = {}) {
const resultStatus = terminalStatusFromValue(result?.status ?? result?.terminalStatus ?? result?.agentRun?.terminalStatus);
if (resultStatus) return { source: "result", status: resultStatus, valuesRedacted: true };
return traceTerminalEvidence(trace);
if (resultStatus) return { source: "result", status: resultStatus, finalResponse: traceTerminal?.finalResponse ?? null, valuesRedacted: true };
return traceTerminal;
}
function activeTurnEvidence({ result = null, session = null, trace = null } = {}) {
@@ -128,7 +129,9 @@ function normalizeActiveStatus(status) {
}
function terminalTraceEventEvidence(events = []) {
for (const event of [...events].reverse()) {
const finalResponse = terminalAssistantEventFinalResponse(events);
for (let index = events.length - 1; index >= 0; index -= 1) {
const event = events[index];
if (!event || typeof event !== "object") continue;
const terminal = event.terminal === true || event.final === true || event.replyAuthority === true;
if (!terminal) continue;
@@ -136,14 +139,43 @@ function terminalTraceEventEvidence(events = []) {
return {
source: "trace-terminal-event",
status,
seq: eventSeq(event, events.indexOf(event)),
seq: eventSeq(event, index),
eventType: textValue(event.type ?? event.label) || null,
finalResponse,
evidence: finalResponse ? { textPresent: true, source: "trace-terminal-assistant-event", valuesRedacted: true } : null,
valuesRedacted: true
};
}
return null;
}
function terminalAssistantEventFinalResponse(events = []) {
for (let index = events.length - 1; index >= 0; index -= 1) {
const event = events[index];
if (!event || typeof event !== "object") continue;
if (!isAssistantTraceEvent(event)) continue;
if (!(event.terminal === true || event.final === true || event.replyAuthority === true)) continue;
const text = projectionText(event.finalResponse, event.text, event.content, event.message, event.summary, event.payload?.text, event.payload?.content, event.payload?.message);
if (!text) continue;
const status = terminalStatusFromValue(event.status ?? event.terminalStatus ?? event.payload?.terminalStatus) ?? "completed";
return {
text,
status,
traceId: textValue(event.traceId) || null,
seq: eventSeq(event, index),
eventType: textValue(event.type ?? event.label) || null,
valuesPrinted: false
};
}
return null;
}
function isAssistantTraceEvent(event = {}) {
const type = String(event.type ?? event.eventType ?? "").trim().toLowerCase();
if (type === "assistant" || type === "assistant_message") return true;
return /assistant:message|assistant_message/u.test(String(event.label ?? "").toLowerCase());
}
function activeTraceEventStatus(events = []) {
for (const event of [...events].reverse()) {
const status = normalizeWorkbenchStatus(event?.status ?? event?.type);
@@ -78,6 +78,15 @@ const markdownFinalText = [
"|---|---|",
"| local | pass |"
].join("\n");
const terminalAssistantFinalText = [
"全部六份数据到手!下面是完整的六语言终极性能对比:",
"",
"| language | runtime | status |",
"|---|---|---|",
"| Lua | LuaJIT | pass |",
"| Python | CPython | pass |",
"| Rust | native | pass |"
].join("\n");
let state = createScenarioState("baseline");
const sseClients = new Set<ServerResponse>();
@@ -383,6 +392,10 @@ function createScenarioState(scenarioId: string): ScenarioState {
sessions.unshift(markdownFinalSession());
traces.trc_markdown_final = markdownFinalTrace();
}
if (id === "terminal-assistant-event-final-response") {
sessions.unshift(terminalAssistantFinalSession());
traces.trc_terminal_assistant_final = terminalAssistantFinalTrace();
}
if (id === "progress-only-final-response") markRunningProgressOnly(sessions, traces);
if (id === "terminal-completed-no-final-response") markRunningNoFinalResponse(sessions, traces);
if (id === "tool-completed-projection-running") markToolCompletedProjectionRunning(sessions, traces);
@@ -431,6 +444,8 @@ function createScenarioState(scenarioId: string): ScenarioState {
? "ses_terminal_empty"
: id === "markdown-final-response"
? "ses_markdown_final"
: id === "terminal-assistant-event-final-response"
? "ses_terminal_assistant_final"
: id === "progress-only-final-response" || id === "terminal-completed-no-final-response" || id === "tool-completed-projection-running"
? "ses_running"
: id === "projector-resume-from-checkpoint"
@@ -972,6 +987,47 @@ function markdownFinalTrace(): JsonRecord {
};
}
function terminalAssistantFinalSession(): SessionRecord {
const now = new Date().toISOString();
const trace = terminalAssistantFinalTrace();
return {
sessionId: "ses_terminal_assistant_final",
conversationId: "cnv_terminal_assistant_final",
threadId: "thr_terminal_assistant_final",
status: "completed",
lastTraceId: "trc_terminal_assistant_final",
startedAt: now,
updatedAt: now,
messageCount: 2,
firstUserMessagePreview: "再把lua加入对比测试",
messages: [
{ id: "msg_terminal_assistant_user", messageId: "msg_terminal_assistant_user", role: "user", title: "用户", text: "再把lua加入对比测试", status: "sent", createdAt: now, sessionId: "ses_terminal_assistant_final", threadId: "thr_terminal_assistant_final", traceId: "trc_terminal_assistant_final", turnId: "trc_terminal_assistant_final" },
{ id: "msg_terminal_assistant_agent", messageId: "msg_terminal_assistant_agent", role: "agent", title: "Code Agent", text: terminalAssistantFinalText, parts: [{ type: "text", text: terminalAssistantFinalText, status: "completed" }], status: "completed", createdAt: now, updatedAt: now, sessionId: "ses_terminal_assistant_final", threadId: "thr_terminal_assistant_final", traceId: "trc_terminal_assistant_final", turnId: "trc_terminal_assistant_final", runnerTrace: trace, finalResponse: { text: terminalAssistantFinalText, status: "completed" } }
]
};
}
function terminalAssistantFinalTrace(): JsonRecord {
const createdAt = new Date().toISOString();
const events = [
{ seq: 1, sourceSeq: 21, createdAt, label: "agentrun:assistant:message", type: "assistant_message", status: "running", replyAuthority: false, final: false, terminal: false, message: "现在写 Lua 基准测试脚本。" },
{ seq: 2, sourceSeq: 28, createdAt, label: "agentrun:assistant:message", type: "assistant_message", status: "completed", replyAuthority: true, final: true, terminal: true, message: terminalAssistantFinalText },
{ seq: 3, sourceSeq: 29, createdAt, label: "agentrun:terminal:completed", type: "result", status: "completed", terminal: true, message: "AgentRun command completed." }
];
return {
traceId: "trc_terminal_assistant_final",
status: "completed",
sessionId: "ses_terminal_assistant_final",
threadId: "thr_terminal_assistant_final",
events,
eventCount: events.length,
fullTraceLoaded: true,
hasMore: false,
assistantText: terminalAssistantFinalText,
finalResponse: { text: terminalAssistantFinalText, status: "completed" }
};
}
function manyRailSession(index: number): SessionRecord {
const id = String(index).padStart(2, "0");
const now = new Date(Date.now() - (index + 1) * 1000).toISOString();
@@ -43,6 +43,41 @@ test.describe("terminal completed without final response", () => {
});
});
test.describe("terminal assistant event final response", () => {
test.use({ scenarioId: "terminal-assistant-event-final-response" });
test("renders the sealed Workbench final response from the authoritative terminal assistant event", async ({ page }, testInfo) => {
const sessionId = "ses_terminal_assistant_final";
const traceId = "trc_terminal_assistant_final";
const messagesResponse = await page.request.get(`/v1/workbench/sessions/${sessionId}/messages?limit=100`);
expect(messagesResponse.status()).toBe(200);
const messagesPayload = await messagesResponse.json();
const agentMessage = messagesPayload.messages.find((message: { role?: string }) => message.role === "agent");
expect(agentMessage?.text).toContain("全部六份数据到手");
expect(agentMessage?.finalResponse?.text).toBe(agentMessage?.text);
const turnResponse = await page.request.get(`/v1/workbench/turns/${traceId}`);
expect(turnResponse.status()).toBe(200);
const turnPayload = await turnResponse.json();
expect(turnPayload.turn.finalResponse.text).toBe(agentMessage?.text);
const traceResponse = await page.request.get(`/v1/workbench/traces/${traceId}/events?limit=20`);
expect(traceResponse.status()).toBe(200);
const tracePayload = await traceResponse.json();
const finalEvent = tracePayload.events.find((event: { label?: string; final?: boolean }) => event.label === "agentrun:assistant:message" && event.final === true);
expect(finalEvent?.replyAuthority).toBe(true);
expect(finalEvent?.message).toBe(agentMessage?.text);
await gotoWorkbench(page, `/workbench/sessions/${sessionId}`);
const card = page.locator(`${selectors.messageCard}[data-role="agent"][data-status="completed"]`).last();
await expect(card.locator(".message-markdown.message-text")).toContainText("全部六份数据到手");
await expect(card.locator(".message-markdown.message-text table")).toHaveCount(1);
await card.locator("summary.trace-disclosure-summary").click();
await expect(card.locator(".trace-row-markdown table")).toHaveCount(1);
await saveScreenshot(page, testInfo, "terminal-assistant-event-final-response");
});
});
test.describe("non-terminal tool completed event", () => {
test.use({ scenarioId: "tool-completed-projection-running" });