feat(chat): stream and persist local answer UI with final report references
This commit is contained in:
@@ -52,6 +52,7 @@ import {
|
||||
type TodoUpdatePayload,
|
||||
} from "./chatStreamEvents.js";
|
||||
import { createTokenSmoother } from "./chatTokenSmoother.js";
|
||||
import { parseAnswerUiInput } from "../chat/answer-ui.js";
|
||||
|
||||
export {
|
||||
createTokenSmoother,
|
||||
@@ -79,6 +80,7 @@ type StreamPromptOptions = {
|
||||
projectId?: string;
|
||||
signal?: AbortSignal;
|
||||
suppressLegacyFrontendActions?: boolean;
|
||||
suppressAnswerUi?: boolean;
|
||||
write: (event: string, data: Record<string, unknown>) => void;
|
||||
};
|
||||
|
||||
@@ -142,6 +144,7 @@ export const streamPromptResponse = async ({
|
||||
projectId,
|
||||
signal,
|
||||
suppressLegacyFrontendActions = false,
|
||||
suppressAnswerUi = false,
|
||||
write,
|
||||
}: StreamPromptOptions): Promise<{
|
||||
aborted: boolean;
|
||||
@@ -177,6 +180,8 @@ export const streamPromptResponse = async ({
|
||||
let aborted = signal?.aborted ?? false;
|
||||
let failed = false;
|
||||
let finalAnswer: string | undefined;
|
||||
const presentedAnswerIds = new Set<string>();
|
||||
let finalAnswerUiBlockIds: string[] = [];
|
||||
const trackBusinessStage = createBusinessStageTracker((event, data) => write(event, { ...data, session_id: clientSessionId }));
|
||||
const debugContext = {
|
||||
sessionId,
|
||||
@@ -722,11 +727,36 @@ export const streamPromptResponse = async ({
|
||||
});
|
||||
}
|
||||
if (part.type === "tool") {
|
||||
if (part.tool === "present_answer") {
|
||||
if (suppressAnswerUi) continue;
|
||||
if (part.state.status === "completed" && !emittedToolParts.has(part.id)) {
|
||||
emittedToolParts.add(part.id);
|
||||
const value = parseAnswerUiInput(part.state.input);
|
||||
if (value) {
|
||||
presentedAnswerIds.add(value.block_id);
|
||||
await tokenSmoother.flush();
|
||||
write("answer_ui", { ...value, id: value.block_id, session_id: clientSessionId });
|
||||
} else {
|
||||
write("progress", { id: part.id, phase: "visualization", status: "error", title: "回答界面未通过校验,保留文字说明" });
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (part.tool === "final_answer") {
|
||||
if (part.state.status === "completed") {
|
||||
const output = parseToolOutput(part.state.output);
|
||||
if (output && typeof output === "object" && typeof (output as Record<string, unknown>).answer === "string") {
|
||||
finalAnswer = (output as { answer: string }).answer;
|
||||
const refs = (output as { ui_block_ids?: unknown }).ui_block_ids;
|
||||
finalAnswerUiBlockIds = Array.isArray(refs) ? [...new Set(refs.filter((id): id is string => typeof id === "string" && presentedAnswerIds.has(id)))].slice(0, 48) : [];
|
||||
const hasPendingTool = [...toolStatuses.values()].some((status) => status !== "completed" && status !== "error");
|
||||
if (presentedAnswerIds.size > 0 && !suppressAnswerUi && !hasPendingTool) {
|
||||
// A final rich-chat answer closes this turn; native interruption stops
|
||||
// automatic revisions without marking a background task complete.
|
||||
await runtime.abortSession(sessionId);
|
||||
await runtime.waitForSessionIdle(sessionId);
|
||||
done = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
continue;
|
||||
@@ -985,7 +1015,7 @@ export const streamPromptResponse = async ({
|
||||
await emitFallbackMessage(runtime, sessionId, clientSessionId, tokenSmoother.writeToken);
|
||||
}
|
||||
await tokenSmoother.flush();
|
||||
if (finalAnswer) write("final_answer", { session_id: clientSessionId, content: finalAnswer });
|
||||
if (finalAnswer) write("final_answer", { session_id: clientSessionId, content: finalAnswer, ui_block_ids: finalAnswerUiBlockIds });
|
||||
emitProgress({
|
||||
id: "request-received",
|
||||
phase: "start",
|
||||
|
||||
Reference in New Issue
Block a user