fix(chat): only expose final agent response
Generic Container CI/CD / test-build-publish (push) Successful in 1m4s
Agent CI/CD v2 / build-test-publish-and-deploy (push) Successful in 1m4s

This commit is contained in:
2026-08-24 18:30:24 +08:00
parent 11ebf428bb
commit c6efccb88a
4 changed files with 300 additions and 130 deletions
+56 -70
View File
@@ -111,24 +111,40 @@ const toRuntimeModel = (model?: SupportedModel) => {
};
};
const emitFallbackMessage = async (
const emitFinalMessage = async (
runtime: OpencodeRuntimeAdapter,
sessionId: string,
clientSessionId: string,
currentAssistantMessageIds: Set<string>,
assistantTextParts: Map<string, Map<string, string>>,
write: (event: string, data: Record<string, unknown>) => void,
) => {
const messages = await runtime.messages(sessionId);
const assistantMessage = [...messages]
let text = [...currentAssistantMessageIds]
.reverse()
.find((message) => message.info.role === "assistant");
const parts = assistantMessage?.parts ?? [];
const text = collectTextContent(parts);
.map((messageId) => [...(assistantTextParts.get(messageId)?.values() ?? [])].join(""))
.find((content) => content.length > 0) ?? "";
if (!text) {
const messages = await runtime.messages(sessionId);
const assistantMessage = [...messages]
.reverse()
.find(
(message) =>
message.info.role === "assistant" &&
(currentAssistantMessageIds.size === 0 ||
currentAssistantMessageIds.has(message.info.id)),
);
text = collectTextContent(assistantMessage?.parts ?? []);
}
if (text) {
write("token", {
session_id: clientSessionId,
content: text,
});
return true;
}
return false;
};
export const streamPromptResponse = async ({
@@ -156,15 +172,14 @@ export const streamPromptResponse = async ({
const emittedToolParts = new Set<string>();
const emittedQuestionToolParts = new Set<string>();
const emittedQuestionRequestIds = new Set<string>();
const currentAssistantMessageIds = new Set<string>();
const assistantTextParts = new Map<string, Map<string, string>>();
const partTypes = new Map<string, Part["type"]>();
const pendingPartTextDeltas = new Map<string, string[]>();
const reasoningDeltas = new Map<string, string[]>();
const pendingTextDeltas = new Map<string, string[]>();
const reasoningStatuses = new Map<string, "running" | "completed">();
const toolStatuses = new Map<string, string>();
let firstSessionEventLogged = false;
let firstNonStatusEventLogged = false;
let firstTokenLogged = false;
let firstReasoningLogged = false;
let firstToolEventLogged = false;
let lastSessionStatus: string | null = null;
let lastSessionStatusMessage: string | null = null;
@@ -626,45 +641,26 @@ export const streamPromptResponse = async ({
if (event.type === "message.updated") {
if (event.properties.info.role === "assistant") {
sawResponseActivity = true;
currentAssistantMessageIds.add(event.properties.info.id);
}
continue;
}
if (event.type === "message.part.delta" && event.properties.field === "text") {
sawResponseActivity = true;
currentAssistantMessageIds.add(event.properties.messageID);
const partType = partTypes.get(event.properties.partID);
if (partType === "text") {
if (!firstTokenLogged) {
firstTokenLogged = true;
logDevelopmentDebug("first response token emitted", {
...debugContext,
partId: event.properties.partID,
elapsedMs: Math.max(0, Date.now() - requestStartedAt),
sincePromptDispatchMs: Math.max(0, Date.now() - promptStartedAt),
});
}
emittedText = true;
write("token", {
session_id: clientSessionId,
content: event.properties.delta,
});
} else if (partType === "reasoning") {
if (!firstReasoningLogged) {
firstReasoningLogged = true;
logDevelopmentDebug("first reasoning delta received", {
...debugContext,
partId: event.properties.partID,
elapsedMs: Math.max(0, Date.now() - requestStartedAt),
sincePromptDispatchMs: Math.max(0, Date.now() - promptStartedAt),
});
}
const pending = reasoningDeltas.get(event.properties.partID) ?? [];
pending.push(event.properties.delta);
reasoningDeltas.set(event.properties.partID, pending);
const messageParts = assistantTextParts.get(event.properties.messageID) ?? new Map();
messageParts.set(
event.properties.partID,
`${messageParts.get(event.properties.partID) ?? ""}${event.properties.delta}`,
);
assistantTextParts.set(event.properties.messageID, messageParts);
} else if (!partType) {
const pending = pendingPartTextDeltas.get(event.properties.partID) ?? [];
const pending = pendingTextDeltas.get(event.properties.partID) ?? [];
pending.push(event.properties.delta);
pendingPartTextDeltas.set(event.properties.partID, pending);
pendingTextDeltas.set(event.properties.partID, pending);
}
continue;
}
@@ -673,23 +669,19 @@ export const streamPromptResponse = async ({
sawResponseActivity = true;
const part = event.properties.part;
partTypes.set(part.id, part.type);
if (part.type === "text" || part.type === "reasoning" || part.type === "tool") {
currentAssistantMessageIds.add(part.messageID);
}
if (part.type === "text") {
const pending = pendingPartTextDeltas.get(part.id) ?? [];
pendingPartTextDeltas.delete(part.id);
for (const content of pending) {
emittedText = true;
write("token", {
session_id: clientSessionId,
content,
});
}
} else if (part.type === "reasoning") {
const pending = pendingPartTextDeltas.get(part.id) ?? [];
if (pending.length > 0) {
const existing = reasoningDeltas.get(part.id) ?? [];
reasoningDeltas.set(part.id, existing.concat(pending));
}
pendingPartTextDeltas.delete(part.id);
const pendingText = (pendingTextDeltas.get(part.id) ?? []).join("");
pendingTextDeltas.delete(part.id);
const messageParts = assistantTextParts.get(part.messageID) ?? new Map();
messageParts.set(part.id, part.text || pendingText);
assistantTextParts.set(part.messageID, messageParts);
} else {
pendingTextDeltas.delete(part.id);
}
if (part.type === "reasoning") {
const reasoningStatus = part.time.end ? "completed" : "running";
if (reasoningStatuses.get(part.id) !== reasoningStatus) {
reasoningStatuses.set(part.id, reasoningStatus);
@@ -697,14 +689,10 @@ export const streamPromptResponse = async ({
...debugContext,
partId: part.id,
status: reasoningStatus,
chunkCount: (reasoningDeltas.get(part.id) ?? []).length,
elapsedMs: Math.max(0, Date.now() - requestStartedAt),
});
}
const reasoningDetail = buildReasoningProgressDetail(
reasoningDeltas.get(part.id) ?? [],
part.time.end,
);
const reasoningDetail = buildReasoningProgressDetail(part.time.end);
emitProgress({
id: part.id,
phase: "planning",
@@ -784,9 +772,6 @@ export const streamPromptResponse = async ({
detail: buildToolProgressDetail(
part.tool,
part.state.status,
toolParams,
reason,
part.state.status === "error" ? part.state.error : undefined,
),
});
if (
@@ -932,13 +917,14 @@ export const streamPromptResponse = async ({
}
await promptPromise;
if (!emittedText) {
logDevelopmentDebug("no streamed text emitted, falling back to messages()", {
...debugContext,
elapsedMs: Math.max(0, Date.now() - requestStartedAt),
});
await emitFallbackMessage(runtime, sessionId, clientSessionId, write);
}
emittedText = await emitFinalMessage(
runtime,
sessionId,
clientSessionId,
currentAssistantMessageIds,
assistantTextParts,
write,
);
emitProgress({
id: "request-received",
phase: "start",