Merge pull request #942 from pikasTech/fix/issue934-trace-final-response
fix: preserve code agent final response text
This commit is contained in:
@@ -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目前可用的 HWPOD:0 个。";
|
||||
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({
|
||||
|
||||
@@ -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目前可用的 HWPOD:0 个。", 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 ?? "", /目前可用的 HWPOD:0 个/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" },
|
||||
|
||||
Reference in New Issue
Block a user