140 lines
4.4 KiB
TypeScript
140 lines
4.4 KiB
TypeScript
import type { Message, ToolCallEvent } from "@/lib/api";
|
|||
|
|
|
||
|
|
export const STREAMING_ASSISTANT_METADATA_KIND = "streaming_assistant";
|
||
|
|
|
||
|
|
export type StreamingAssistantMetadata = {
|
||
|
|
kind: typeof STREAMING_ASSISTANT_METADATA_KIND;
|
||
|
|
ephemeralTrace: string | null;
|
||
|
|
traceRevision: number;
|
||
|
|
};
|
||
|
|
|
||
|
|
export type StreamingAttemptState = {
|
||
|
|
segmentText: string;
|
||
|
|
initiatedToolCallIds: string[];
|
||
|
|
};
|
||
|
|
|
||
|
|
export type StreamingToolCallTransition = {
|
||
|
|
attempt: StreamingAttemptState;
|
||
|
|
startedNewToolCall: boolean;
|
||
|
|
traceText: string | null;
|
||
|
|
};
|
||
|
|
|
||
|
|
export function createStreamingAssistantMetadata(): StreamingAssistantMetadata {
|
||
|
|
return {
|
||
|
|
kind: STREAMING_ASSISTANT_METADATA_KIND,
|
||
|
|
ephemeralTrace: null,
|
||
|
|
traceRevision: 0,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
export function asStreamingAssistantMetadata(value: unknown): StreamingAssistantMetadata | null {
|
||
|
|
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
||
|
|
const record = value as Record<string, unknown>;
|
||
|
|
if (record.kind !== STREAMING_ASSISTANT_METADATA_KIND) return null;
|
||
|
|
|
||
|
|
return {
|
||
|
|
kind: STREAMING_ASSISTANT_METADATA_KIND,
|
||
|
|
ephemeralTrace: typeof record.ephemeralTrace === "string" ? record.ephemeralTrace : null,
|
||
|
|
traceRevision:
|
||
|
|
typeof record.traceRevision === "number" && Number.isFinite(record.traceRevision)
|
||
|
|
? Math.max(0, Math.floor(record.traceRevision))
|
||
|
|
: 0,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
export function createStreamingAttemptState(): StreamingAttemptState {
|
||
|
|
return {
|
||
|
|
segmentText: "",
|
||
|
|
initiatedToolCallIds: [],
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
export function appendStreamingDelta(attempt: StreamingAttemptState, delta: string): StreamingAttemptState {
|
||
|
|
if (!delta) return attempt;
|
||
|
|
return {
|
||
|
|
...attempt,
|
||
|
|
segmentText: attempt.segmentText + delta,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
export function transitionStreamingToolCall(
|
||
|
|
attempt: StreamingAttemptState,
|
||
|
|
event: Pick<ToolCallEvent, "toolCallId" | "status">
|
||
|
|
): StreamingToolCallTransition {
|
||
|
|
if (event.status !== "initiated" || attempt.initiatedToolCallIds.includes(event.toolCallId)) {
|
||
|
|
return { attempt, startedNewToolCall: false, traceText: null };
|
||
|
|
}
|
||
|
|
|
||
|
|
const traceText = attempt.segmentText.trim();
|
||
|
|
return {
|
||
|
|
attempt: {
|
||
|
|
segmentText: "",
|
||
|
|
initiatedToolCallIds: attempt.initiatedToolCallIds.concat(event.toolCallId),
|
||
|
|
},
|
||
|
|
startedNewToolCall: true,
|
||
|
|
traceText: traceText || null,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
function updateStreamingAssistant(
|
||
|
|
messages: Message[],
|
||
|
|
assistantMessagePrefix: string,
|
||
|
|
update: (message: Message, metadata: StreamingAssistantMetadata) => Message
|
||
|
|
) {
|
||
|
|
let didUpdate = false;
|
||
|
|
const nextMessages = messages.map((message, index, all) => {
|
||
|
|
const isTarget = index === all.length - 1 && message.id.startsWith(assistantMessagePrefix);
|
||
|
|
if (!isTarget) return message;
|
||
|
|
didUpdate = true;
|
||
|
|
return update(message, asStreamingAssistantMetadata(message.metadata) ?? createStreamingAssistantMetadata());
|
||
|
|
});
|
||
|
|
return didUpdate ? nextMessages : messages;
|
||
|
|
}
|
||
|
|
|
||
|
|
export function setStreamingAssistantSegment(messages: Message[], assistantMessagePrefix: string, segmentText: string) {
|
||
|
|
return updateStreamingAssistant(messages, assistantMessagePrefix, (message, metadata) => ({
|
||
|
|
...message,
|
||
|
|
content: segmentText,
|
||
|
|
metadata: {
|
||
|
|
...metadata,
|
||
|
|
ephemeralTrace: null,
|
||
|
|
} satisfies StreamingAssistantMetadata,
|
||
|
|
}));
|
||
|
|
}
|
||
|
|
|
||
|
|
export function promoteStreamingAssistantTrace(messages: Message[], assistantMessagePrefix: string, traceText: string) {
|
||
|
|
const normalizedTrace = traceText.trim();
|
||
|
|
return updateStreamingAssistant(messages, assistantMessagePrefix, (message, metadata) => ({
|
||
|
|
...message,
|
||
|
|
content: "",
|
||
|
|
metadata: normalizedTrace
|
||
|
|
? ({
|
||
|
|
...metadata,
|
||
|
|
ephemeralTrace: normalizedTrace,
|
||
|
|
traceRevision: metadata.traceRevision + 1,
|
||
|
|
} satisfies StreamingAssistantMetadata)
|
||
|
|
: metadata,
|
||
|
|
}));
|
||
|
|
}
|
||
|
|
|
||
|
|
export function finalizeStreamingAssistant(messages: Message[], assistantMessagePrefix: string, finalText: string) {
|
||
|
|
return updateStreamingAssistant(messages, assistantMessagePrefix, (message, metadata) => ({
|
||
|
|
...message,
|
||
|
|
content: finalText,
|
||
|
|
metadata: {
|
||
|
|
...metadata,
|
||
|
|
ephemeralTrace: null,
|
||
|
|
} satisfies StreamingAssistantMetadata,
|
||
|
|
}));
|
||
|
|
}
|
||
|
|
|
||
|
|
export function clearStreamingAssistantTrace(messages: Message[], assistantMessagePrefix: string) {
|
||
|
|
return updateStreamingAssistant(messages, assistantMessagePrefix, (message, metadata) => ({
|
||
|
|
...message,
|
||
|
|
metadata: {
|
||
|
|
...metadata,
|
||
|
|
ephemeralTrace: null,
|
||
|
|
} satisfies StreamingAssistantMetadata,
|
||
|
|
}));
|
||
|
|
}
|