import type { ChatMessage, DocIndex } from "../chatTools"; import type { AssistantRoutingContextV2, AssistantSurface, FocusedDocumentExcerpt, } from "./types"; import type { RequestedAssistantMode } from "./requestedMode"; import { detectMutationIntentHint } from "./mutationHint"; export const CONTEXT_RECENT_MESSAGE_MAX = 2; export const CONTEXT_RECENT_MESSAGE_CHAR_LIMIT = 800; export const CONTEXT_LATEST_MESSAGE_CHAR_LIMIT = 12_000; export const CONTEXT_FOCUSED_EXCERPT_CHAR_LIMIT = 4_000; function truncate(value: string, max: number): string { if (value.length <= max) return value; return value.slice(0, max); } function fileTypeFor(filename: string, known?: string): string | null { if (known && typeof known === "string") return known; const lower = (filename || "").toLowerCase(); const ext = lower.slice(lower.lastIndexOf(".") + 1); if (!ext || ext === lower) return null; return ext; } function isEditable(fileType: string | null): boolean { return fileType === "docx"; } interface ContextDocInput { documentId: string; filename: string; } /** * Build the bounded, sanitized routing context. Contains only metadata safe to * send to a read-only router model: latest user message, at most two recent * conversation messages (truncated), filenames/file types/editable flags, and * optionally a focused excerpt the user pasted/quoted or selected in the UI. * * Never includes: signed URLs, storage paths, database rows, comments, full * document bodies, tool results, or secrets. */ export function buildAssistantRoutingContext(params: { surface: AssistantSurface; requestedMode: RequestedAssistantMode; messages: ChatMessage[]; docIndex: DocIndex; displayedDocument?: ContextDocInput | null; attachedDocuments?: ContextDocInput[]; projectDocumentCount?: number; selectedWorkflow?: { workflowId: string; title: string } | null; focusedDocumentExcerpt?: FocusedDocumentExcerpt | null; confirmationPresented?: boolean; }): AssistantRoutingContextV2 { const { surface, requestedMode, messages, docIndex, displayedDocument = null, attachedDocuments = [], projectDocumentCount = 0, selectedWorkflow = null, focusedDocumentExcerpt = null, confirmationPresented = false, } = params; // Latest user message only. const latestUser = [...messages].reverse().find((m) => m.role === "user") ?? null; const latestUserMessage = truncate( latestUser?.content ?? "", CONTEXT_LATEST_MESSAGE_CHAR_LIMIT, ); // At most two recent conversation messages for reference resolution. const recentConversation: AssistantRoutingContextV2["recentConversation"] = []; const trimmed = messages.slice(-CONTEXT_RECENT_MESSAGE_MAX - 1); for (const m of trimmed) { if (m.role !== "user" && m.role !== "assistant") continue; recentConversation.push({ role: m.role, content: truncate(m.content ?? "", CONTEXT_RECENT_MESSAGE_CHAR_LIMIT), }); if (recentConversation.length >= CONTEXT_RECENT_MESSAGE_MAX) break; } function resolveDoc(input: ContextDocInput) { const entry = docIndex[input.documentId]; const filename = entry?.filename ?? input.filename; const fileType = fileTypeFor(filename, entry?.file_type); return { documentId: input.documentId, filename, fileType, editable: isEditable(fileType), revisionId: entry?.version_id ?? null, }; } const displayedDocumentResolved = displayedDocument ? resolveDoc(displayedDocument) : null; const attachedDocumentsResolved = attachedDocuments.map(resolveDoc); // Focused excerpt only when pasted/quoted by the user or selected in UI, // capped, never scanned from a document body. let focused: FocusedDocumentExcerpt | null = null; if (focusedDocumentExcerpt && focusedDocumentExcerpt.text) { focused = { documentId: focusedDocumentExcerpt.documentId, text: truncate( focusedDocumentExcerpt.text, CONTEXT_FOCUSED_EXCERPT_CHAR_LIMIT, ), source: focusedDocumentExcerpt.source, }; } return { schemaVersion: 2, surface, requestedMode, currentDate: new Date().toISOString().slice(0, 10), latestUserMessage, recentConversation, displayedDocument: displayedDocumentResolved, attachedDocuments: attachedDocumentsResolved, projectDocumentCount, selectedWorkflow, focusedDocumentExcerpt: focused, mutationIntentHint: detectMutationIntentHint(latestUserMessage), confirmationPresented, }; }