import type { ChatMessage } from "./types"; export interface BootstrapInputResult { text: string; includedCount: number; omittedCount: number; truncated: boolean; } const BOOTSTRAP_PREAMBLE = "Continue this conversation using the transcript context below. The final section is the latest user request to answer now."; const TRANSCRIPT_HEADER = "Transcript context:"; const LATEST_PROMPT_HEADER = "Latest user request (answer this now):"; const OMITTED_SUMMARY = (count: number) => `[${count} earlier message(s) omitted to stay within input limits.]`; function messageRoleLabel(message: ChatMessage): "USER" | "ASSISTANT" { return message.role === "assistant" ? "ASSISTANT" : "USER"; } function attachmentSummary(message: ChatMessage): string | null { const imageAttachments = message.attachments?.filter((attachment) => attachment.type === "image"); const count = imageAttachments?.length ?? 0; if (count === 0) { return null; } const names = imageAttachments?.slice(0, 3).map((image) => image.name) ?? []; const namesSummary = names.join(", "); const extraCount = count - names.length; const extraSummary = extraCount > 0 ? ` (+${extraCount} more)` : ""; return `[Attached image${count === 1 ? "" : "s"}: ${namesSummary}${extraSummary}]`; } function buildMessageBlock(message: ChatMessage): string { const text = message.text; const attachments = attachmentSummary(message); if (text && attachments) { return `${messageRoleLabel(message)}:\n${text}\n${attachments}`; } if (text) { return `${messageRoleLabel(message)}:\n${text}`; } if (attachments) { return `${messageRoleLabel(message)}:\n${attachments}`; } return `${messageRoleLabel(message)}:\n(empty message)`; } function finalizeWithPrompt( transcriptBody: string, latestPrompt: string, maxChars: number, ): string | null { const text = `${BOOTSTRAP_PREAMBLE}\n\n${TRANSCRIPT_HEADER}\n${transcriptBody}\n\n${LATEST_PROMPT_HEADER}\n${latestPrompt}`; return text.length <= maxChars ? text : null; } export function buildBootstrapInput( previousMessages: ChatMessage[], latestPrompt: string, maxChars: number, ): BootstrapInputResult { const budget = Number.isFinite(maxChars) ? Math.max(1, Math.floor(maxChars)) : 1; const promptOnly = latestPrompt.length <= budget ? latestPrompt : latestPrompt.slice(0, budget); if (previousMessages.length === 0) { return { text: promptOnly, includedCount: 0, omittedCount: 0, truncated: promptOnly.length !== latestPrompt.length, }; } const newestFirstBlocks: string[] = []; for (let index = previousMessages.length - 1; index >= 0; index -= 1) { const message = previousMessages[index]; if (!message) continue; newestFirstBlocks.push(buildMessageBlock(message)); } if (newestFirstBlocks.length === 0) { return { text: promptOnly, includedCount: 0, omittedCount: previousMessages.length, truncated: true, }; } // Include a contiguous suffix from newest to oldest, then reverse to chronological. let includedNewestFirst: string[] = []; for (const block of newestFirstBlocks) { const nextNewestFirst = [...includedNewestFirst, block]; const nextChronological = nextNewestFirst.toReversed(); const omittedCount = newestFirstBlocks.length - nextChronological.length; const transcriptBody = omittedCount > 0 ? `${OMITTED_SUMMARY(omittedCount)}\n\n${nextChronological.join("\n\n")}` : nextChronological.join("\n\n"); if (!finalizeWithPrompt(transcriptBody, latestPrompt, budget)) { break; } includedNewestFirst = nextNewestFirst; } let includedChronological = includedNewestFirst.toReversed(); while (true) { const omittedCount = newestFirstBlocks.length - includedChronological.length; const transcriptBody = omittedCount > 0 ? includedChronological.length > 0 ? `${OMITTED_SUMMARY(omittedCount)}\n\n${includedChronological.join("\n\n")}` : OMITTED_SUMMARY(omittedCount) : includedChronological.join("\n\n"); const finalized = finalizeWithPrompt(transcriptBody, latestPrompt, budget); if (finalized) { return { text: finalized, includedCount: includedChronological.length, omittedCount, truncated: omittedCount > 0 || latestPrompt.length !== promptOnly.length, }; } if (includedChronological.length === 0) { return { text: promptOnly, includedCount: 0, omittedCount: previousMessages.length, truncated: true, }; } includedChronological = includedChronological.slice(1); } }