Separate live text and tool stream parts
This commit is contained in:
@@ -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,
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user