Separate live text and tool stream parts

This commit is contained in:
wuyang6
2026-05-14 21:46:45 +08:00
parent 946cca1ac4
commit 57f4452508
+55 -29
View File
@@ -53,6 +53,8 @@ type ClawStreamItem =
type StreamState = { type StreamState = {
textStarted: boolean; textStarted: boolean;
textStreamed: boolean; textStreamed: boolean;
textPartIndex: number;
activeTextId?: string;
reasoningEnded: boolean; reasoningEnded: boolean;
phase: "waiting" | "queued" | "running" | "done"; phase: "waiting" | "queued" | "running" | "done";
runId?: string; runId?: string;
@@ -112,6 +114,7 @@ export async function POST(req: Request) {
const streamState: StreamState = { const streamState: StreamState = {
textStarted: false, textStarted: false,
textStreamed: false, textStreamed: false,
textPartIndex: 0,
reasoningEnded: false, reasoningEnded: false,
phase: "waiting", phase: "waiting",
announcedToolCallKeys: new Set<string>(), announcedToolCallKeys: new Set<string>(),
@@ -151,13 +154,9 @@ export async function POST(req: Request) {
} }
writeToolTrace(writer, payload.transcript, streamState); writeToolTrace(writer, payload.transcript, streamState);
if (!streamState.textStreamed) { if (!streamState.textStreamed) {
writer.write({ type: "text-start", id: "text-1" }); writeTextDelta(writer, text, streamState);
writer.write({ type: "text-delta", id: "text-1", delta: text });
writer.write({ type: "text-end", id: "text-1" });
} else if (streamState.textStarted) {
writer.write({ type: "text-end", id: "text-1" });
streamState.textStarted = false;
} }
endTextPart(writer, streamState);
writer.write({ type: "finish-step" }); writer.write({ type: "finish-step" });
writer.write({ writer.write({
type: "finish", type: "finish",
@@ -178,9 +177,9 @@ export async function POST(req: Request) {
? err.message ? err.message
: "请求失败。"; : "请求失败。";
writer.write({ type: "reasoning-end", id: "reasoning-1" }); writer.write({ type: "reasoning-end", id: "reasoning-1" });
writer.write({ type: "text-start", id: "text-1" }); endTextPart(writer, streamState);
writer.write({ type: "text-delta", id: "text-1", delta: text }); writeTextDelta(writer, text, streamState);
writer.write({ type: "text-end", id: "text-1" }); endTextPart(writer, streamState);
writer.write({ type: "finish-step" }); writer.write({ type: "finish-step" });
writer.write({ writer.write({
type: "finish", type: "finish",
@@ -209,6 +208,45 @@ function formatDuration(ms: number) {
return seconds > 0 ? `${minutes}min${seconds}s` : `${minutes}min`; return seconds > 0 ? `${minutes}min${seconds}s` : `${minutes}min`;
} }
function startTextPart(
writer: UIMessageStreamWriter<UIMessage>,
streamState: StreamState,
) {
if (streamState.textStarted && streamState.activeTextId) {
return streamState.activeTextId;
}
streamState.textPartIndex += 1;
const id = `text-${streamState.textPartIndex}`;
streamState.activeTextId = id;
streamState.textStarted = true;
writer.write({ type: "text-start", id });
return id;
}
function writeTextDelta(
writer: UIMessageStreamWriter<UIMessage>,
delta: string,
streamState?: StreamState,
) {
if (!streamState) {
writer.write({ type: "text-delta", id: "text-1", delta });
return;
}
const id = startTextPart(writer, streamState);
writer.write({ type: "text-delta", id, delta });
streamState.textStreamed = true;
}
function endTextPart(
writer: UIMessageStreamWriter<UIMessage>,
streamState?: StreamState,
) {
if (!streamState?.textStarted || !streamState.activeTextId) return;
writer.write({ type: "text-end", id: streamState.activeTextId });
streamState.textStarted = false;
streamState.activeTextId = undefined;
}
function writeToolTrace( function writeToolTrace(
writer: UIMessageStreamWriter<UIMessage>, writer: UIMessageStreamWriter<UIMessage>,
transcript?: ClawTranscriptEntry[], transcript?: ClawTranscriptEntry[],
@@ -232,6 +270,7 @@ function writeToolTrace(
if (!toolCallId || !toolName) continue; if (!toolCallId || !toolName) continue;
if (streamState?.streamedToolCallIds.has(toolCallId)) continue; if (streamState?.streamedToolCallIds.has(toolCallId)) continue;
endTextPart(writer, streamState);
writer.write({ writer.write({
type: "tool-input-available", type: "tool-input-available",
toolCallId, toolCallId,
@@ -524,12 +563,7 @@ function writeRuntimeEvent(
); );
} }
if (event.type === "content_delta" && event.delta) { if (event.type === "content_delta" && event.delta) {
if (!streamState?.textStarted) { writeTextDelta(writer, event.delta, streamState);
writer.write({ type: "text-start", id: "text-1" });
if (streamState) streamState.textStarted = true;
}
writer.write({ type: "text-delta", id: "text-1", delta: event.delta });
if (streamState) streamState.textStreamed = true;
} }
if (event.type === "tool_call_delta") { if (event.type === "tool_call_delta") {
const key = const key =
@@ -539,6 +573,7 @@ function writeRuntimeEvent(
: `index:${event.tool_call_index}`); : `index:${event.tool_call_index}`);
if (key && !streamState?.announcedToolCallKeys.has(key)) { if (key && !streamState?.announcedToolCallKeys.has(key)) {
streamState?.announcedToolCallKeys.add(key); streamState?.announcedToolCallKeys.add(key);
endTextPart(writer, streamState);
writeReasoningDelta( writeReasoningDelta(
writer, writer,
{ {
@@ -551,26 +586,16 @@ function writeRuntimeEvent(
} }
} }
if (event.type === "final_text_start") { if (event.type === "final_text_start") {
if (!streamState?.textStarted) { if (streamState) startTextPart(writer, streamState);
writer.write({ type: "text-start", id: "text-1" });
if (streamState) streamState.textStarted = true;
}
} }
if (event.type === "final_text_delta" && event.delta) { if (event.type === "final_text_delta" && event.delta) {
if (!streamState?.textStarted) { writeTextDelta(writer, event.delta, streamState);
writer.write({ type: "text-start", id: "text-1" });
if (streamState) streamState.textStarted = true;
}
writer.write({ type: "text-delta", id: "text-1", delta: event.delta });
if (streamState) streamState.textStreamed = true;
} }
if (event.type === "final_text_end") { if (event.type === "final_text_end") {
if (streamState?.textStarted) { endTextPart(writer, streamState);
writer.write({ type: "text-end", id: "text-1" });
streamState.textStarted = false;
}
} }
if (event.type === "tool_start" && event.tool_call_id && event.tool_name) { if (event.type === "tool_start" && event.tool_call_id && event.tool_name) {
endTextPart(writer, streamState);
streamState?.streamedToolCallIds.add(event.tool_call_id); streamState?.streamedToolCallIds.add(event.tool_call_id);
writer.write({ writer.write({
type: "tool-input-available", type: "tool-input-available",
@@ -580,6 +605,7 @@ function writeRuntimeEvent(
}); });
} }
if (event.type === "tool_delta" && event.tool_call_id && event.delta) { if (event.type === "tool_delta" && event.tool_call_id && event.delta) {
endTextPart(writer, streamState);
writeReasoningDelta( writeReasoningDelta(
writer, writer,
{ {