Files
lam-agent-vicinae/src/lib/thread.ts
T

64 lines
2.1 KiB
TypeScript

import type { ChatTurn, ThreadMessage } from "./types";
/**
* Flattens stored turns into the flat message list the transcript renders.
*
* Storage keeps a question and its answer together as a turn, because that is
* what gets replayed to the model. The transcript needs a flat sequence, newest
* message first. This is the one place that translation happens.
*/
export interface LiveState {
isStreaming: boolean;
/** Partial answer for the turn currently in flight. */
response: string;
thinking: string;
/** Activity line, e.g. "Searching the web". Only meaningful mid-turn. */
status?: string;
}
export function buildThread(turns: ChatTurn[], live: LiveState): ThreadMessage[] {
const messages: ThreadMessage[] = [];
// Strictly newest message first. The search bar sits at the top of the window,
// so the most recent thing, an answer once one has been written, should be
// the first line under it. A consequence is that within an exchange the answer
// comes before its question, which is the intended reading order here.
for (let index = turns.length - 1; index >= 0; index--) {
const turn = turns[index];
// Only the newest turn can be mid-flight.
const isLive = index === turns.length - 1 && live.isStreaming;
const text = isLive ? live.response : turn.response;
messages.push({
id: `${turn.id}:assistant`,
role: "assistant",
text,
createdAt: turn.createdAt,
isStreaming: isLive,
thinking: isLive ? live.thinking : turn.thinking,
toolCalls: turn.toolCalls,
error: isLive ? undefined : turn.error,
// Only while there is nothing to read yet. Once tokens land, the
// placeholder it feeds is gone anyway.
status: isLive && text.length === 0 ? live.status : undefined,
// Only a question carries attachments, so the answer never has one.
attachments: [],
});
messages.push({
id: `${turn.id}:user`,
role: "user",
text: turn.prompt,
createdAt: turn.createdAt,
isStreaming: false,
thinking: "",
toolCalls: [],
attachments: turn.attachments,
});
}
return messages;
}