Merge pull request #942 from pikasTech/fix/issue934-trace-final-response

fix: preserve code agent final response text
This commit is contained in:
Lyon
2026-06-05 22:31:45 +08:00
committed by GitHub
3 changed files with 237 additions and 4 deletions
+196
View File
@@ -1664,6 +1664,202 @@ test("access controller dedupes duplicate refreshed assistant messages by id and
assert.equal(conversation.messages[1].text, "最终回复");
});
test("access controller keeps final response separate from progress assistant text (#934)", async () => {
const projectId = "prj_hwpod_workbench";
const ownerUserId = "usr_issue934_owner_merge";
const conversationId = "cnv_issue934_owner_merge";
const sessionId = "ses_issue934_owner_merge";
const traceId = "trc_issue934_owner_merge";
const threadId = "thread-issue-934-owner-merge";
const nowValue = "2026-06-05T13:39:48.000Z";
const userText = "看看有几个hwpod可用?";
const progressText = "Let me check the available HWPODs using the `hwpod` CLI tool.";
const middleText = "当前工作区还没有 `.hwlab/hwpod-spec.yaml` 文件,所以 `hwpod inspect` 无法获取可用 HWPOD 信息。";
const finalText = "当前工作区没有 `.hwlab/hwpod-spec.yaml`,也没有 `.hwlab` 目录或现成的 hwpod-spec 文件。这意味着这个 AgentRun 会话还没有绑定具体的 HWPOD。\n\n目前可用的 HWPOD**0 个**(需要先创建 spec 并绑定)。";
const accessController = createAccessController({ now: () => nowValue });
await accessController.recordAgentSessionOwner({
ownerUserId,
sessionId,
projectId,
agentId: "hwlab-code-agent",
status: "active",
conversationId,
threadId,
traceId,
session: {
source: "workbench-status-sync",
sessionStatus: "running",
lastTraceId: traceId,
messages: [
{ id: "msg_issue934_user", role: "user", title: "用户", text: userText, status: "sent", traceId, conversationId, sessionId, threadId, createdAt: nowValue },
{ id: "msg_issue934_agent_pending", role: "agent", title: "Code Agent 处理中", text: `${progressText}${middleText}`, status: "running", traceId, conversationId, sessionId, threadId, createdAt: nowValue }
],
messageCount: 2,
firstUserMessagePreview: userText,
valuesRedacted: true,
secretMaterialStored: false
}
});
await accessController.recordAgentSessionOwner({
ownerUserId,
sessionId,
projectId,
agentId: "hwlab-code-agent",
status: "completed",
conversationId,
threadId,
traceId,
session: {
source: "code-agent-result",
status: "completed",
sessionStatus: "completed",
lastTraceId: traceId,
finalResponse: {
text: finalText,
textChars: finalText.length,
role: "assistant",
status: "completed",
traceId,
valuesPrinted: false
},
messages: [
{ id: "msg_issue934_agent_result", role: "agent", title: "Code Agent 回复", text: finalText, status: "completed", traceId, conversationId, sessionId, threadId, createdAt: nowValue }
],
messageCount: 1,
firstUserMessagePreview: userText,
valuesRedacted: true,
secretMaterialStored: false
}
});
await accessController.recordAgentSessionOwner({
ownerUserId,
sessionId,
projectId,
agentId: "hwlab-code-agent",
status: "completed",
conversationId,
threadId,
traceId,
session: {
source: "cloud-web-react-account-sync",
sessionStatus: "completed",
lastTraceId: traceId,
messages: [
{ id: "msg_issue934_user", role: "user", title: "用户", text: userText, status: "sent", traceId, conversationId, sessionId, threadId, createdAt: nowValue },
{ id: "msg_issue934_agent_pending", role: "agent", title: "Code Agent 回复", text: `${progressText}${middleText}${finalText}`, status: "completed", traceId, conversationId, sessionId, threadId, createdAt: nowValue }
],
messageCount: 2,
firstUserMessagePreview: userText,
valuesRedacted: true,
secretMaterialStored: false
}
});
const session = await accessController.getAgentSession(sessionId);
const conversation = await accessController.visibleConversationForActor({ id: ownerUserId, role: "user" }, conversationId, projectId, { includeArchived: true });
assert.deepEqual(session.session.messages.map((message) => message.role), ["user", "agent"]);
assert.deepEqual(conversation.messages.map((message) => message.role), ["user", "agent"]);
assert.equal(session.session.finalResponse.text, finalText);
assert.equal(session.session.messages[1].text, finalText);
assert.equal(conversation.messages[1].text, finalText);
assert.doesNotMatch(conversation.messages[1].text, /Let me check the available HWPODs/u);
assert.doesNotMatch(conversation.messages[1].text, //u);
assert.equal(conversation.messageCount, 2);
assert.equal(conversation.firstUserMessagePreview, userText);
});
test("cloud api exposes final response for legacy progress-polluted snapshots (#934)", async () => {
const nowValue = "2026-06-05T13:39:48.000Z";
const projectId = "prj_hwpod_workbench";
const conversationId = "cnv_issue934_legacy_snapshot";
const sessionId = "ses_issue934_legacy_snapshot";
const traceId = "trc_issue934_legacy_final";
const staleLastTraceId = "trc_issue934_legacy_greeting";
const threadId = "thread-issue-934-legacy";
const userText = "看看有几个hwpod可用?";
const progressText = "Let me check the available HWPODs using the hwpod CLI tool.";
const middleText = "当前工作区还没有 .hwlab/hwpod-spec.yaml 文件,所以 hwpod inspect 无法获取可用 HWPOD 信息。";
const finalText = "当前工作区没有 .hwlab/hwpod-spec.yaml,也没有 .hwlab 目录或现成的 hwpod-spec 文件。\n\n目前可用的 HWPOD0 个。";
const accessController = createAccessController({
env: { HWLAB_ACCESS_CONTROL_REQUIRED: "1", HWLAB_BOOTSTRAP_ADMIN_PASSWORD: "admin-pass" },
now: () => nowValue
});
const server = createCloudApiServer({ env: { HWLAB_ACCESS_CONTROL_REQUIRED: "1" }, accessController, now: () => nowValue });
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
try {
const { port } = server.address();
const login = await postJson(port, "/auth/login", { username: "admin", password: "admin-pass" });
assert.equal(login.status, 200);
const ownerUserId = login.body.actor.id;
accessController.store.agentSessions.set(sessionId, {
id: sessionId,
projectId,
agentId: "hwlab-code-agent",
status: "completed",
startedAt: nowValue,
endedAt: null,
ownerUserId,
conversationId,
threadId,
lastTraceId: staleLastTraceId,
updatedAt: nowValue,
session: {
source: "legacy-progress-polluted-snapshot",
sessionStatus: "completed",
lastTraceId: staleLastTraceId,
finalResponse: {
text: finalText,
textChars: finalText.length,
role: "assistant",
status: "completed",
traceId,
valuesPrinted: false
},
messages: [
{ id: "msg_issue934_legacy_user", role: "user", title: "用户", text: userText, status: "sent", traceId, conversationId, sessionId, threadId, createdAt: nowValue },
{ id: "msg_issue934_legacy_agent", role: "agent", title: "Code Agent 回复", text: `${progressText}${middleText}${finalText}`, status: "completed", traceId, conversationId, sessionId, threadId, createdAt: nowValue }
],
messageCount: 2,
firstUserMessagePreview: userText,
valuesRedacted: true,
secretMaterialStored: false
}
});
const list = await getJson(port, `/v1/agent/conversations?projectId=${projectId}`, login.cookie);
assert.equal(list.status, 200);
const listed = list.body.conversations.find((conversation) => conversation.conversationId === conversationId);
assert.ok(listed, "expected legacy conversation to be listed");
assert.equal(listed.messages[1].text, finalText);
assert.doesNotMatch(listed.messages[1].text, /Let me check|/u);
const direct = await getJson(port, `/v1/agent/conversations/${conversationId}?projectId=${projectId}`, login.cookie);
assert.equal(direct.status, 200);
assert.equal(direct.body.conversation.messages[1].text, finalText);
assert.doesNotMatch(direct.body.conversation.messages[1].text, /Let me check|/u);
const workspace = await getJson(port, `/v1/workbench/workspace?projectId=${projectId}`, login.cookie);
assert.equal(workspace.status, 200);
const selected = await postJson(port, `/v1/workbench/workspace/${workspace.body.workspace.workspaceId}/select-conversation`, {
projectId,
conversationId,
sessionId,
updatedByClient: "test-suite"
}, login.cookie);
assert.equal(selected.status, 200);
assert.equal(selected.body.workspace.selectedConversation.messages[1].text, finalText);
assert.doesNotMatch(selected.body.workspace.selectedConversation.messages[1].text, /Let me check|/u);
} finally {
await new Promise((resolve, reject) => {
server.close((error) => (error ? reject(error) : resolve()));
});
}
});
test("access controller restores AgentRun mapping by Code Agent traceId", async () => {
const accessController = createAccessController({ now: () => "2026-06-01T00:00:00.000Z" });
await accessController.recordAgentSessionOwner({
+20 -4
View File
@@ -1866,6 +1866,7 @@ function mergeAgentSessionOwnerEvidence(nextValue, existingValue) {
} else if (existingMessages?.length) {
merged.messages = existingMessages;
}
merged.messages = normalizeFinalResponseConversationMessages(merged.messages, merged.finalResponse);
const existingChatMessages = Array.isArray(existing.chatMessages) ? existing.chatMessages : null;
const nextChatMessages = Array.isArray(next.chatMessages) ? next.chatMessages : null;
if (existingChatMessages?.length && (!nextChatMessages || nextChatMessages.length === 0)) merged.chatMessages = existingChatMessages;
@@ -1884,6 +1885,20 @@ function mergeAgentSessionOwnerEvidence(nextValue, existingValue) {
}
return merged;
}
function normalizeFinalResponseConversationMessages(messages, finalResponse) {
const finalText = textOr(finalResponse?.text, "");
const traceId = safeTraceIdLocal(finalResponse?.traceId);
if (!Array.isArray(messages) || !finalText || !traceId) return messages;
return messages.map((message) => {
if (message?.role !== "agent" || safeTraceIdLocal(message.traceId) !== traceId) return message;
return redactConversationMessage({
...message,
text: finalText,
status: textOr(finalResponse.status, message.status),
updatedAt: textOr(finalResponse.updatedAt ?? finalResponse.createdAt, message.updatedAt)
});
}).filter(Boolean);
}
function mergeAgentSessionOwnerMessages(nextMessages = [], existingMessages = [], context = {}) {
const next = dedupeConversationMessages(nextMessages);
if (next.some((message) => message.role === "user")) return ensureTerminalUserMessage(next, context).slice(-50);
@@ -2353,8 +2368,9 @@ function publicAgentConversation(session) {
now: session.updatedAt,
firstUserPreview: snapshot.firstUserMessagePreview
}));
const status = resolvedConversationStatus(session.status, snapshot, messages);
const lastTraceId = resolvedConversationLastTraceId(session.lastTraceId, status, messages, snapshot);
const displayMessages = normalizeFinalResponseConversationMessages(messages, snapshot.finalResponse);
const status = resolvedConversationStatus(session.status, snapshot, displayMessages);
const lastTraceId = resolvedConversationLastTraceId(session.lastTraceId, status, displayMessages, snapshot);
return {
conversationId: session.conversationId,
sessionId: session.id,
@@ -2368,8 +2384,8 @@ function publicAgentConversation(session) {
startedAt: session.startedAt,
endedAt: session.endedAt,
session: pruneEmpty({ sessionId: session.id, threadId: session.threadId, status }),
messages,
messageCount: messages.length,
messages: displayMessages,
messageCount: displayMessages.length,
firstUserMessagePreview: textOr(snapshot.firstUserMessagePreview, null),
snapshot: pruneEmpty({
sessionStatus: snapshot.sessionStatus,
@@ -45,6 +45,27 @@ test("web trace rows keep every assistant message and mark only the last one ter
assert.doesNotMatch(JSON.stringify(rows), /backend-turn-finished|short-connection polling/u);
});
test("web trace rows do not fold progress assistant text into the final response (#934)", () => {
const events: Record<string, unknown>[] = [
{ seq: 1, label: "agentrun:request:accepted", status: "accepted", createdAt: "2026-06-05T13:39:36.000Z" },
{ seq: 10, label: "agentrun:assistant:message", type: "assistant", status: "running", messageIndex: 1, messageCount: 3, message: "Let me check the available HWPODs using the hwpod CLI tool.", createdAt: "2026-06-05T13:39:38.000Z" },
{ seq: 20, label: "agentrun:assistant:message", type: "assistant", status: "running", messageIndex: 2, messageCount: 3, message: "当前工作区还没有 .hwlab/hwpod-spec.yaml 文件,所以 hwpod inspect 无法获取可用 HWPOD 信息。", createdAt: "2026-06-05T13:39:40.000Z" },
{ seq: 30, label: "agentrun:assistant:message", type: "assistant", status: "completed", messageIndex: 3, messageCount: 3, replyAuthority: true, final: true, terminal: true, message: "当前工作区没有 .hwlab/hwpod-spec.yaml,也没有 .hwlab 目录或现成的 hwpod-spec 文件。\n\n目前可用的 HWPOD0 个。", createdAt: "2026-06-05T13:39:44.000Z" },
{ seq: 34, label: "agentrun:result:completed", type: "result", status: "completed", terminal: true, message: "AgentRun result is ready for HWLAB short-connection polling.", createdAt: "2026-06-05T13:39:44.280Z" }
];
const rows = traceDisplayRows({ startedAt: "2026-06-05T13:39:36.000Z" }, events);
const assistantRows = rows.filter((row) => row.bodyFormat === "markdown");
const terminalRows = assistantRows.filter((row) => row.terminal === true);
assert.equal(assistantRows.length, 3);
assert.equal(terminalRows.length, 1);
assert.match(assistantRows[0]?.body ?? "", /Let me check/u);
assert.match(assistantRows[1]?.body ?? "", //u);
assert.match(terminalRows[0]?.body ?? "", / HWPOD0 /u);
assert.doesNotMatch(terminalRows[0]?.body ?? "", /Let me check|/u);
});
test("web trace rows collapse tool start/completed and clean shell command escaping", () => {
const events: Record<string, unknown>[] = [
{ seq: 1, label: "agentrun:request:accepted", status: "accepted", createdAt: "2026-06-04T12:13:58.212Z" },