import path from "path"; import crypto from "node:crypto"; import type { RunTree } from "langsmith"; import { downloadFile, generatedDocKey, storageKey, uploadFile, } from "./storage"; import { convertedPdfKey } from "./convert"; import { createServerSupabase } from "./supabase"; import { applyTrackedEdits, applyTrackedOperations, buildDocxEditSurface, extractDocxBodyText, findInDocxEditSurface, renderDocxEditSurfaceForModel, extractTrackedChangeIds, type EditOperation, type EditInput, type AppliedChange, type CanonicalAnchor, } from "./docxTrackedChanges"; import { buildDownloadUrl } from "./downloadTokens"; import { attachActiveVersionPaths, loadActiveVersion } from "./documentVersions"; import { DOCX_AUTHOR_DEFAULT } from "./brand"; import { streamChatWithTools, resolveModel, extractWebAnnotationsFromEvents, type LlmMessage, type OpenAIToolSchema, type NormalizedToolCall, type NormalizedToolResult, } from "./llm"; import { getDefaultModelForFeature } from "./inference/config"; import { createTraceChild, endTrace, traceHeaders, buildDocumentSurfaceTracePayload, sha256, } from "./langsmith"; import { ControlledUserFacingError } from "./assistantErrors"; import { decodeXmlEntities } from "./docxTrackedChanges"; import type { AssistantTurnMode } from "./documentMode"; import { buildChatSystemPrompt } from "../prompts/chat/builders/buildChatSystemPrompt"; import { appendModeBanner } from "../prompts/chat/fragments/mode-banners"; import { buildSaudiLawWebHintsBanner } from "../prompts/chat/fragments/saudi-law-web-hints"; import type { SaudiLawRoutingResult } from "./saudiLawRouting"; import { buildSaudiLawSourcePacket, renderSaudiLawSourcePacketHintsBanner, summarizeSaudiLawSourcePacket, type SaudiLawSourcePacket, } from "./saudiLawSourceControl"; import { buildSaudiLawEvidencePacket, hasGrounding, renderSaudiLawEvidencePacketBanner, summarizeSaudiLawEvidencePacket, type SaudiLawEvidencePacket, } from "./saudiLawResearch"; import { mergeGrounding } from "./llm/grounding"; import type { SearchGrounding } from "./llm/types"; import { buildWorkflowUserPrefix } from "../prompts/chat/workflow-user-prefix"; import { DOCX_EDIT_EMPTY_TURN_RETRY } from "../prompts/chat/retry/empty-turn-docx-edit"; import { WORKFLOW_REVIEW_EMPTY_TURN_RETRY } from "../prompts/chat/retry/empty-turn-workflow-review"; import { WEB_SEARCH_EMPTY_TURN_RETRY } from "../prompts/chat/retry/empty-turn-web-search"; const STANDARD_FONT_DATA_URL = (() => { try { const pkgPath = require.resolve("pdfjs-dist/package.json"); return path.join(path.dirname(pkgPath), "standard_fonts") + path.sep; } catch { return undefined; } })(); // --------------------------------------------------------------------------- // Types // --------------------------------------------------------------------------- const FREE_PROXY_CONCURRENCY = Number(process.env.MIKE_FREE_PROXY_CONCURRENCY) || 4; const freeProxySlotTimeoutMs = () => Number(process.env.MIKE_FREE_PROXY_SLOT_TIMEOUT_MS) || 240_000; const freeProxyAcquireTimeoutMs = () => Number(process.env.MIKE_FREE_PROXY_SLOT_ACQUIRE_TIMEOUT_MS) || 30_000; interface FreeProxySlotWaiter { resolve: (release: () => void) => void; reject: (err: Error) => void; } let freeProxyPermits = FREE_PROXY_CONCURRENCY; const freeProxyWaiters: FreeProxySlotWaiter[] = []; function acquireFreeProxySlot(): Promise<() => void> { return new Promise((resolve, reject) => { const waiter: FreeProxySlotWaiter = { resolve, reject }; const timer = setTimeout(() => { const idx = freeProxyWaiters.indexOf(waiter); if (idx >= 0) freeProxyWaiters.splice(idx, 1); reject(new Error("Timed out waiting for a free proxy slot")); }, freeProxyAcquireTimeoutMs()); const release = () => { clearTimeout(timer); freeProxyPermits++; const next = freeProxyWaiters.shift(); if (next) { freeProxyPermits--; next.resolve(release); } }; if (freeProxyPermits > 0) { freeProxyPermits--; resolve(release); } else { freeProxyWaiters.push(waiter); } }); } export async function withFreeProxySlot( fn: (signal: AbortSignal) => Promise, externalSignal?: AbortSignal, ): Promise { const ac = new AbortController(); const slotTimeout = setTimeout(() => { console.error( "[withFreeProxySlot] slot timeout; aborting request and releasing slot", ); ac.abort(); }, freeProxySlotTimeoutMs()); // Merge the pool's own abort (slot timeout) with any external abort (e.g. // client disconnect from the route) so all sources can cancel the in-flight // request and free the slot. const merged = new AbortController(); const link = (sig: AbortSignal) => { if (sig.aborted) merged.abort(sig.reason); else sig.addEventListener("abort", () => merged.abort(sig.reason), { once: true }); }; link(ac.signal); if (externalSignal) link(externalSignal); let release: (() => void) | undefined; try { release = await acquireFreeProxySlot(); return await fn(merged.signal); } catch (e) { if (ac.signal.aborted) { throw new Error("proxy_timeout: free proxy slot timed out"); } throw e; } finally { clearTimeout(slotTimeout); release?.(); } } async function delayBetweenSaudiEvalProxyCalls(label: string): Promise { const raw = process.env.SAUDI_EVAL_PROXY_DELAY_MS ?? process.env.PROXY_DELAY_MS ?? "0"; const delayMs = Number(raw); if (!Number.isFinite(delayMs) || delayMs <= 0) return; console.log( `[runLLMStream] waiting ${delayMs}ms before proxy request (${label})`, ); await new Promise((resolve) => setTimeout(resolve, delayMs)); } export type DocStore = Map< string, { storage_path: string; file_type: string; filename: string } >; export type WorkflowStore = Map; export type DocIndex = Record< string, { document_id: string; filename: string; file_type?: string; version_id?: string | null; version_number?: number | null; } >; export type TabularCellStore = { columns: { index: number; name: string }[]; documents: { id: string; filename: string }[]; /** key: `${colIndex}:${docId}` */ cells: Map; }; export type ToolCall = { id: string; function: { name: string; arguments: string }; }; // Canonical anchor map cache: surface_id -> block_id -> CanonicalAnchor const surfaceAnchorCache = new Map>(); type AnchorManifest = { docLabel: string; documentId?: string; docRevisionId: string; surfaceId: string; readSerial: number; anchors: Map; }; type AnchorManifestState = { byDocLabel: Map; nextReadSerial: number; }; const SECTION_HEADING_RE = /^(?:Section|Clause|Article)\s+\d/i; const INTERNAL_BLOCK_ID_RE = /\b(?:BMK_[PT]_[A-Za-z0-9_-]+|P_\d{4,})\b/g; function sanitizeWorkflowDisplayText(value: string | undefined | null): string { return (value ?? "") .replace(INTERNAL_BLOCK_ID_RE, "") .replace(/\[\s*\]/g, "") .replace(/\(\s*\)/g, "") .replace(/\s+([,.;:])/g, "$1") .replace(/\s{2,}/g, " ") .trim(); } function isMeaningfulSectionLabel(value: string | undefined | null): value is string { const sanitized = sanitizeWorkflowDisplayText(value); return Boolean(sanitized && !/^(?:section|clause|article)$/i.test(sanitized)); } function findSectionHeading( surface: Awaited>, blockId: string, ): string | null { const blockIdx = surface.blocks.findIndex((b) => b.id === blockId); if (blockIdx < 0) return null; // Include the selected block because MISSING findings are intentionally // anchored on the nearest eligible section heading. for (let i = blockIdx; i >= 0; i--) { if (surface.anchors[i]?.anchor_eligible === false) continue; const text = surface.blocks[i].text.trim(); if (SECTION_HEADING_RE.test(text)) { const heading = sanitizeWorkflowDisplayText(decodeXmlEntities(text)); if (heading) return heading; } } return null; } function normalizeWorkflowAction(opType: string | undefined): WorkflowAction { switch (opType) { case "REWRITE_PARAGRAPH": case "REWRITE": return "REWRITE"; case "INSERT": case "INSERT_PARAGRAPH": return "INSERT"; case "DELETE_PARAGRAPH": case "DELETE": return "DELETE"; case "COMMENT": default: return "COMMENT"; } } function parseCommentFields(comment: string): { issue: string; zamilImpact: string; recommendedAction: string; } { let issue = comment; let zamilImpact = ""; let recommendedAction = ""; const zamilMatch = comment.match(/Zamil Impact:\s*(.*?)(?:\s*\|\s*Recommended Action:|$)/i); if (zamilMatch) zamilImpact = zamilMatch[1].trim(); const recMatch = comment.match(/Recommended Action:\s*(.*?)$/i); if (recMatch) recommendedAction = recMatch[1].trim(); const issueMatch = comment.match(/(?:\[[\w]+\]\s*)?Issue:\s*(.*?)(?:\s*\|\s*Zamil Impact:|$)/i); if (issueMatch) { issue = issueMatch[1].trim(); } else { const stripped = comment.replace(/^\[[\w]+\]\s*/, ""); const pipeIdx = stripped.indexOf("| Zamil Impact:"); issue = pipeIdx >= 0 ? stripped.slice(0, pipeIdx).trim() : stripped.trim(); } return { issue, zamilImpact, recommendedAction }; } function normalizeModelBlockId(blockId: string): string { return blockId .replace(/^_(BMK_[PT]_[a-z0-9]+)$/, "$1") .replace(/^_(BMK_P_page\d+)$/, "$1"); } function isValidDocxModelBlockId(blockId: string): boolean { return /^P_\d+$/.test(blockId) || /^BMK_[PT]_[a-z0-9]+$/.test(blockId); } function getDocumentOrder( surface: Awaited>, blockId?: string | null, bookmarkName?: string | null, ): number | undefined { const normalizedBlockId = blockId ? normalizeModelBlockId(blockId) : null; const normalizedBookmarkName = bookmarkName ? normalizeModelBlockId(bookmarkName) : null; if (!normalizedBlockId && !normalizedBookmarkName) return undefined; const idx = surface.anchors.findIndex((a: any) => (!!normalizedBlockId && a.block_id === normalizedBlockId) || (!!normalizedBookmarkName && a.bookmark_name === normalizedBookmarkName) ); return idx >= 0 ? idx : undefined; } function createAnchorManifestState(): AnchorManifestState { return { byDocLabel: new Map(), nextReadSerial: 0 }; } type TurnReadRecord = { docLabel: string; filename: string; content: string; charCount: number; readSerial: number; surfaceId?: string; }; type TurnDocumentState = { readsByDocLabel: Map; duplicateReadCount: number; }; function createTurnDocumentState(): TurnDocumentState { return { readsByDocLabel: new Map(), duplicateReadCount: 0, }; } const MAX_TOOL_BATCHES_PER_TURN = 6; const MAX_TOOL_BATCHES_WORKFLOW_REVIEW = 12; const MAX_FULL_DOC_READS_PER_TURN = 5; const MAX_DUPLICATE_READS_PER_TURN = 2; const WORKFLOW_WRITE_TOOL_NAMES = new Set([ "plan_comments_on_document", "correct_comment_anchors", "fix_comment_findings", ]); function assertToolBudget(params: { toolBatchCount: number; turnDocumentState: TurnDocumentState; workflowReview?: boolean; }): void { const maxBatches = params.workflowReview ? MAX_TOOL_BATCHES_WORKFLOW_REVIEW : MAX_TOOL_BATCHES_PER_TURN; if (params.toolBatchCount > maxBatches) { throw new ControlledUserFacingError({ code: "tool_loop_detected", message: "The assistant attempted too many tool rounds. I stopped the turn to prevent a runaway loop.", safeDetails: { tool_batch_count: params.toolBatchCount, max_tool_batches_per_turn: maxBatches, }, }); } if (params.turnDocumentState.readsByDocLabel.size > MAX_FULL_DOC_READS_PER_TURN) { throw new ControlledUserFacingError({ code: "too_many_document_reads", message: "The assistant attempted to read too many full documents in one turn. Please narrow the request or use targeted search.", safeDetails: { full_document_reads: params.turnDocumentState.readsByDocLabel.size, max_full_doc_reads_per_turn: MAX_FULL_DOC_READS_PER_TURN, }, }); } if (params.turnDocumentState.duplicateReadCount > MAX_DUPLICATE_READS_PER_TURN) { throw new ControlledUserFacingError({ code: "duplicate_document_read_loop", message: "The assistant repeatedly tried to read the same document. I stopped the loop to protect the session.", safeDetails: { duplicate_read_count: params.turnDocumentState.duplicateReadCount, max_duplicate_reads_per_turn: MAX_DUPLICATE_READS_PER_TURN, }, }); } } function isAlreadyReadToolResult(content: string): boolean { try { const parsed = JSON.parse(content) as { already_read?: unknown }; return parsed.already_read === true; } catch { return false; } } function clampInteger(value: number | undefined, params: { min: number; max: number; fallback: number }): number { if (typeof value !== "number" || !Number.isFinite(value)) return params.fallback; return Math.max(params.min, Math.min(params.max, Math.floor(value))); } function rememberAnchorManifest( state: AnchorManifestState | undefined, docLabel: string, documentId: string | undefined, surface: Awaited>, ): AnchorManifest | null { const anchorMap = new Map(); for (const a of surface.anchors) anchorMap.set(a.block_id, a); surfaceAnchorCache.set(surface.surfaceId, anchorMap); if (!state) return null; const manifest: AnchorManifest = { docLabel, documentId, docRevisionId: surface.surfaceId, surfaceId: surface.surfaceId, readSerial: ++state.nextReadSerial, anchors: anchorMap, }; state.byDocLabel.set(docLabel, manifest); return manifest; } export type ChatMessage = { role: string; content: string | null; files?: { filename: string; document_id?: string }[]; workflow?: { id: string; title: string }; }; // --------------------------------------------------------------------------- // Prompt constants (migrated to backend/src/prompts/ — re-export shims) // --------------------------------------------------------------------------- import { DIRECT_DOCX_EDIT_TOOLS } from "../prompts/tools/direct-docx-edit"; import { WORKFLOW_TOOLS } from "../prompts/tools/workflow"; import { ASSISTANT_CORE_TOOLS as TOOLS } from "../prompts/tools/assistant-core"; export { SYSTEM_PROMPT } from "../prompts/chat/system-main"; export { DIRECT_DOCX_EDIT_SYSTEM_PROMPT } from "../prompts/chat/system-direct-docx-edit"; export { PROJECT_EXTRA_TOOLS } from "../prompts/tools/project"; export { TABULAR_TOOLS } from "../prompts/tools/tabular"; export { WORKFLOW_TOOLS } from "../prompts/tools/workflow"; export { DIRECT_DOCX_EDIT_TOOLS } from "../prompts/tools/direct-docx-edit"; export { ASSISTANT_CORE_TOOLS as TOOLS } from "../prompts/tools/assistant-core"; // --------------------------------------------------------------------------- // Server-side capability matrix // --------------------------------------------------------------------------- // // Tool availability is enforced HERE, in server code, not in prompts. Each // assistant turn mode exposes only a fixed, audited set of tools. This is the // single source of truth used both to filter the tool schema sent to the model // AND to reject any forbidden tool call the model attempts anyway. // Read-only tools never mutate document bytes or storage. Safe in every mode. const READ_ONLY_TOOLS = [ "read_document", "find_in_document", "list_documents", "fetch_documents", "list_workflows", "read_workflow", "read_table_cells", "inspect_document", "search_document", ] as const; // Comment tools annotate documents (Word comments / planned findings). They do // not perform tracked-byte mutations on their own in comments mode. const COMMENT_TOOLS = [ "comment_on_document", "plan_comments_on_document", "correct_comment_anchors", "fix_comment_findings", ] as const; // Tracked-change byte mutation tool for direct document edits. const DIRECT_EDIT_TOOLS = ["edit_document"] as const; // Document creation / replication tools. const GENERATION_TOOLS = ["generate_docx", "replicate_document"] as const; const MODE_TOOL_CAPABILITIES: Record> = { answer: new Set([...READ_ONLY_TOOLS]), web_search: new Set([...READ_ONLY_TOOLS]), comments: new Set([...READ_ONLY_TOOLS, ...COMMENT_TOOLS]), workflow_review: new Set([ ...READ_ONLY_TOOLS, ...COMMENT_TOOLS, ...GENERATION_TOOLS, ]), document_generation: new Set([ ...READ_ONLY_TOOLS, ...GENERATION_TOOLS, ]), document_direct_edit: new Set([ ...READ_ONLY_TOOLS, ...DIRECT_EDIT_TOOLS, ]), document_edit_with_web_search: new Set([ ...READ_ONLY_TOOLS, ...DIRECT_EDIT_TOOLS, ]), // project_chat is the auto fallback for project routes: full comment + // generation surface, but never the direct tracked-edit tool. project_chat: new Set([ ...READ_ONLY_TOOLS, ...COMMENT_TOOLS, ...GENERATION_TOOLS, ]), }; /** * Single source of truth for "may this tool run in this turn mode?". Used to * filter the exposed tool schema and to reject forbidden tool calls the model * attempts despite the filtered schema. */ export function isToolAllowedForMode( toolName: string, turnMode: AssistantTurnMode, ): boolean { return MODE_TOOL_CAPABILITIES[turnMode]?.has(toolName) ?? false; } /** Human-readable rejection returned to the model for a forbidden tool call. */ export function forbiddenToolMessage( toolName: string, turnMode: AssistantTurnMode, ): string { return `Tool '${toolName}' is not permitted in the current mode (${turnMode}).`; } type ParsedCitation = { ref: number; doc_id: string; page: number | string; quote: string; }; function normalizeCitation(raw: unknown): ParsedCitation | null { if (!raw || typeof raw !== "object") return null; const c = raw as Record; if (typeof c.ref !== "number" || typeof c.doc_id !== "string") return null; if (typeof c.quote !== "string" || !c.quote) return null; let page: number | string; if (typeof c.page === "number") { page = c.page; } else if (typeof c.page === "string" && /^\d+\s*-\s*\d+$/.test(c.page)) { page = c.page; } else { const n = parseInt(String(c.page ?? ""), 10); if (!Number.isFinite(n)) return null; page = n; } return { ref: c.ref, doc_id: c.doc_id, page, quote: c.quote }; } // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- export function resolveDoc(rawId: string, docIndex: DocIndex) { return docIndex[rawId]; } /** * Resolve whatever identifier the model passed (`doc-N` slug, filename, or * document UUID) back to a chat-local doc label. Generated docs surface in * tool results with both `doc_id` (slug) and `document_id` (UUID), so the * model often picks the wrong one — without this fallback `read_document` * silently returns "not found" and the model gives up and re-generates. */ export function resolveDocLabel( rawId: string, docStore: DocStore, docIndex?: DocIndex, ): string | null { if (docStore.has(rawId)) return rawId; for (const [label, info] of docStore.entries()) { if (info.filename === rawId) return label; } if (docIndex) { for (const [label, info] of Object.entries(docIndex)) { if (info.document_id === rawId) return label; } } return null; } /** * Append a tool-activity summary to the most recent assistant message so * the model can see what it just did (read / create / edit / workflow * applied) in the prior turn — otherwise it only sees its own prose and * forgets which docs it touched, which leads to e.g. re-generating a doc * that already exists. * * Doc references use the *current-turn* `doc_id` slug (looked up by * matching the event's stored `document_id` against this turn's freshly * built `docIndex`), since slugs are reassigned every turn and the old * slug from the prior turn would be meaningless. Falls back to filename * only if the doc is no longer in the index (deleted, scope changed). */ export async function enrichWithPriorEvents( messages: ChatMessage[], chatId: string | null | undefined, db: ReturnType, docIndex: DocIndex, ): Promise { if (!chatId) return messages; const { data: rows } = await db .from("chat_messages") .select("content, created_at") .eq("chat_id", chatId) .eq("role", "assistant") .order("created_at", { ascending: false }) .limit(1); const lastRow = rows?.[0] as { content?: unknown } | undefined; const content = lastRow?.content; if (!Array.isArray(content)) return messages; const slugByDocumentId = new Map(); for (const [slug, info] of Object.entries(docIndex)) { if (info.document_id) slugByDocumentId.set(info.document_id, slug); } const refFor = (documentId: unknown, filename: unknown) => { const slug = typeof documentId === "string" ? slugByDocumentId.get(documentId) : undefined; return slug ? `${slug} ("${filename}")` : `"${filename}"`; }; const lines: string[] = []; for (const ev of content as Record[]) { if (ev?.type === "doc_created") { lines.push( `- generate_docx → ${refFor(ev.document_id, ev.filename)}`, ); } else if (ev?.type === "doc_edited") { lines.push( `- plan_comments_on_document (edit) → ${refFor(ev.document_id, ev.filename)}`, ); } else if (ev?.type === "doc_read") { lines.push( `- read_document → ${refFor(ev.document_id, ev.filename)}`, ); } else if (ev?.type === "doc_replicated") { // The model needs to know what each copy resolved to so it // can call plan_comments_on_document / read_document on them. Emit one // line per copy, all attributed back to the same source. const srcLabel = typeof ev.filename === "string" ? `"${ev.filename}"` : ""; const copies = Array.isArray(ev.copies) ? (ev.copies as { new_filename?: unknown; document_id?: unknown; }[]) : []; for (const c of copies) { const ref = refFor(c.document_id, c.new_filename); lines.push( srcLabel ? `- replicate_document → ${ref} (copy of ${srcLabel})` : `- replicate_document → ${ref}`, ); } } else if (ev?.type === "workflow_applied") { lines.push(`- applied workflow: "${ev.title}"`); } } if (lines.length === 0) return messages; const summary = `\n\n[Tool activity in your previous turn]\n${lines.join("\n")}`; // Find the index of the last assistant message and attach the // summary there only. let lastAssistantIdx = -1; for (let i = messages.length - 1; i >= 0; i--) { if (messages[i].role === "assistant") { lastAssistantIdx = i; break; } } if (lastAssistantIdx < 0) return messages; const enriched = messages.slice(); const target = enriched[lastAssistantIdx]; enriched[lastAssistantIdx] = { ...target, content: (target.content ?? "") + summary, }; return enriched; } export function buildMessages( messages: ChatMessage[], docAvailability: { doc_id: string; filename: string; folder_path?: string }[], systemPromptExtra?: string, docIndex?: DocIndex, turnMode: AssistantTurnMode = "answer", ) { const formatted: unknown[] = []; const isDirectDocxEdit = turnMode === "document_direct_edit" || turnMode === "document_edit_with_web_search"; const systemContent = buildChatSystemPrompt({ turnMode, docAvailability, systemPromptExtra, }); formatted.push({ role: "system", content: systemContent }); // Map document_id (UUID) → current-turn doc_id slug, so when we // inline a user attachment we hand the model the same handle it // would use to call read_document / fetch_documents. const slugByDocumentId = new Map(); if (docIndex) { for (const [slug, info] of Object.entries(docIndex)) { if (info.document_id) slugByDocumentId.set(info.document_id, slug); } } for (const msg of messages) { let content = msg.content ?? ""; if (msg.role === "user" && msg.workflow) { content = `${buildWorkflowUserPrefix(msg.workflow.title, msg.workflow.id)}\n\n${content}`; } if (msg.role === "user" && msg.files?.length) { const lines = msg.files.map((f) => { const slug = f.document_id ? slugByDocumentId.get(f.document_id) : undefined; return slug ? `- ${slug}: ${f.filename}` : `- ${f.filename}`; }); if (isDirectDocxEdit) { content = `[The user has attached the following document(s). In direct DOCX edit mode, use inspect_document/search_document if content or anchors are needed, then use edit_document to apply the requested mutation:\n${lines.join("\n")}]\n\n${content}`; } else { content = `[The user has attached the following document(s). You MUST use the read_document tool to read their contents before you can answer or edit:\n${lines.join("\n")}]\n\n${content}`; } } formatted.push({ role: msg.role, content }); } return formatted; } // =========================================================================== // PDF Read-Only virtual surface and quote resolution types & helpers // =========================================================================== export type PdfVirtualPage = { page: number; block_id: string; text: string; text_hash: string; }; export type PdfVirtualSurface = { surfaceId: string; pages: PdfVirtualPage[]; text: string; }; export function sha256Text(input: string): string { return crypto.createHash("sha256").update(input, "utf8").digest("hex"); } export function normalizePdfPageText(input: string): string { return input .replace(/\u00A0/g, " ") .replace(/[ \t\r\f\v]+/g, " ") .replace(/\n[ \t]+/g, "\n") .replace(/[ \t]+\n/g, "\n") .replace(/\n{3,}/g, "\n\n") .trim(); } export function pdfPageBlockId(page: number): string { return `BMK_P_page${page}`; } export function pageFromPdfBlockId(blockId: string | null | undefined): number | null { if (!blockId) return null; const normalizedBlockId = normalizeModelBlockId(blockId); const match = normalizedBlockId.match(/^BMK_P_page(\d+)$/); if (!match) return null; const page = Number.parseInt(match[1], 10); if (!Number.isFinite(page) || page < 1) return null; return page; } export async function buildPdfVirtualSurface( buf: ArrayBuffer, opts?: { documentId?: string; versionId?: string | null }, ): Promise { const pdfjs = await import("pdfjs-dist/legacy/build/pdf.mjs" as string) as any; const loadingTask = pdfjs.getDocument({ data: new Uint8Array(buf), useWorkerFetch: false, isEvalSupported: false, disableFontFace: true, standardFontDataUrl: STANDARD_FONT_DATA_URL, }); const pdf = await loadingTask.promise; const pages: PdfVirtualPage[] = []; for (let pageNumber = 1; pageNumber <= pdf.numPages; pageNumber += 1) { const page = await pdf.getPage(pageNumber); const textContent = await page.getTextContent(); const rawText = textContent.items .map((item: any) => { if ( item && typeof item === "object" && "str" in item && typeof item.str === "string" ) { return item.str; } return ""; }) .filter((part: string) => part.length > 0) .join(" "); const text = normalizePdfPageText(rawText); const block_id = pdfPageBlockId(pageNumber); pages.push({ page: pageNumber, block_id, text, text_hash: sha256Text(text), }); } const allExtractedText = pages.map((page) => page.text).join("\n\n"); const textHash = sha256Text(allExtractedText); const surfaceId = sha256Text( [ "pdf", opts?.documentId ?? "unknown-document", opts?.versionId ?? "unknown-version", String(pdf.numPages), textHash, ].join(":"), ); const text = pages .map((page) => { return [ `[[${page.block_id}]]`, `[Page ${page.page}]`, page.text || "[No extractable text on this page]", ].join("\n"); }) .join("\n\n"); return { surfaceId, pages, text, }; } export function rememberPdfAnchorManifest( anchorManifestState: AnchorManifestState | undefined, docLabel: string, documentId: string | undefined, surface: PdfVirtualSurface, ): void { if (!anchorManifestState) return; const anchors = surface.pages.map((page, index) => { const previousPage = index > 0 ? surface.pages[index - 1] : null; const nextPage = index < surface.pages.length - 1 ? surface.pages[index + 1] : null; return { block_id: page.block_id, model_block_id: page.block_id, doc_revision_id: surface.surfaceId, native_para_id: null, stable_id: `pdf-page-${page.page}`, xml_path: `pdf/page/${page.page}`, text_hash: page.text_hash, prev_text_hash: previousPage ? previousPage.text_hash : null, next_text_hash: nextPage ? nextPage.text_hash : null, visible_text_length: page.text.length, commentable: page.text.trim().length > 0, text_preview: page.text.slice(0, 240), kind: "paragraph", container: "body", bookmark_name: page.block_id, }; }); const anchorMap = new Map(); for (const a of anchors) { anchorMap.set(a.block_id, a as unknown as CanonicalAnchor); } const manifest: AnchorManifest = { docLabel, documentId, docRevisionId: surface.surfaceId, surfaceId: surface.surfaceId, readSerial: ++anchorManifestState.nextReadSerial, anchors: anchorMap, }; anchorManifestState.byDocLabel.set(docLabel, manifest); } export type PdfTargetResolution = { text: string; confidence: number; strategy: string; }; export type PdfTargetResolutionInput = { pageText: string; targetText: string; beforeAnchor?: string; afterAnchor?: string; }; export function normalizeForPdfQuoteMatch(input: string): string { return input .replace(/\u00A0/g, " ") .replace(/\s+/g, " ") .trim(); } function escapeRegExp(input: string): string { return input.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); } export function findWhitespaceNormalizedOriginalSlice( originalText: string, targetText: string, caseInsensitive: boolean, ): string | null { const normalizedTarget = normalizeForPdfQuoteMatch(targetText); if (!normalizedTarget) return null; const escapedParts = normalizedTarget .split(" ") .filter(Boolean) .map(escapeRegExp); if (escapedParts.length === 0) return null; const pattern = escapedParts.join("\\s+"); const flags = caseInsensitive ? "i" : ""; const regex = new RegExp(pattern, flags); const match = originalText.match(regex); return match?.[0] ?? null; } export function extractSentenceWindowContaining( pageText: string, targetText: string, ): string | null { const normalizedTarget = normalizeForPdfQuoteMatch(targetText); if (!normalizedTarget) return null; const sentences = pageText .split(/(?<=[.!?;:])\s+/) .map((part) => part.trim()) .filter(Boolean); const lowerTarget = normalizedTarget.toLowerCase(); const matchingSentences = sentences.filter((sentence) => { const normalizedSentence = normalizeForPdfQuoteMatch(sentence).toLowerCase(); return normalizedSentence.includes(lowerTarget) || lowerTarget.includes(normalizedSentence); }); if (matchingSentences.length === 1) { return matchingSentences[0]; } if (matchingSentences.length > 1) { return matchingSentences .sort((a, b) => Math.abs(a.length - normalizedTarget.length) - Math.abs(b.length - normalizedTarget.length))[0]; } const targetWords = lowerTarget.split(" ").filter((word) => word.length >= 4); if (targetWords.length === 0) return null; const scored = sentences .map((sentence) => { const lowerSentence = normalizeForPdfQuoteMatch(sentence).toLowerCase(); const score = targetWords.reduce((count, word) => { return lowerSentence.includes(word) ? count + 1 : count; }, 0); return { sentence, score }; }) .filter((item) => item.score > 0) .sort((a, b) => b.score - a.score); if (scored.length === 0) return null; const best = scored[0]; if (best.score >= Math.max(2, Math.ceil(targetWords.length * 0.5))) { return best.sentence; } return null; } export function resolvePdfTargetText(input: PdfTargetResolutionInput): PdfTargetResolution { const pageText = input.pageText ?? ""; const targetText = input.targetText ?? ""; const beforeAnchor = input.beforeAnchor ?? ""; const afterAnchor = input.afterAnchor ?? ""; if (!pageText.trim() && targetText.trim()) { return { text: targetText.trim(), confidence: 0.1, strategy: "target_text_on_empty_page", }; } if (!targetText.trim()) { return { text: pageText.slice(0, 240), confidence: 0.15, strategy: "missing_target_text_page_preview", }; } if (pageText.includes(targetText)) { return { text: targetText, confidence: 1, strategy: "exact", }; } const whitespaceMatch = findWhitespaceNormalizedOriginalSlice(pageText, targetText, false); if (whitespaceMatch) { return { text: whitespaceMatch, confidence: 0.92, strategy: "whitespace_normalized", }; } const caseInsensitiveMatch = findWhitespaceNormalizedOriginalSlice(pageText, targetText, true); if (caseInsensitiveMatch) { return { text: caseInsensitiveMatch, confidence: 0.85, strategy: "case_insensitive_whitespace_normalized", }; } const normalizedBefore = normalizeForPdfQuoteMatch(beforeAnchor); const normalizedAfter = normalizeForPdfQuoteMatch(afterAnchor); if (normalizedBefore || normalizedAfter) { const normalizedPageText = normalizeForPdfQuoteMatch(pageText); const normalizedTarget = normalizeForPdfQuoteMatch(targetText); const beforeIndex = normalizedBefore ? normalizedPageText.indexOf(normalizedBefore) : -1; const afterIndex = normalizedAfter ? normalizedPageText.indexOf(normalizedAfter) : -1; const targetIndex = normalizedTarget ? normalizedPageText.indexOf(normalizedTarget) : -1; if (targetIndex >= 0) { const beforeOk = beforeIndex < 0 || beforeIndex <= targetIndex; const afterOk = afterIndex < 0 || afterIndex >= targetIndex; if (beforeOk && afterOk) { const anchorAwareMatch = findWhitespaceNormalizedOriginalSlice(pageText, targetText, true); if (anchorAwareMatch) { return { text: anchorAwareMatch, confidence: 0.88, strategy: "anchor_aware", }; } } } } const sentenceWindow = extractSentenceWindowContaining(pageText, targetText); if (sentenceWindow) { return { text: sentenceWindow, confidence: 0.6, strategy: "sentence_window", }; } return { text: targetText.trim() || pageText.slice(0, 240), confidence: 0.25, strategy: "fallback_original_target_text", }; } export async function extractPdfText(buf: ArrayBuffer): Promise { try { const surface = await buildPdfVirtualSurface(buf); return surface.text; } catch { return ""; } } export async function generateDocx( title: string, sections: unknown[], userId: string, db: ReturnType, options?: { landscape?: boolean; projectId?: string | null }, ) { try { const { Document, Paragraph, HeadingLevel, Packer, Table, TableRow, TableCell, WidthType, BorderStyle, TextRun, AlignmentType, PageOrientation, PageBreak, } = await import("docx"); const FONT = "Times New Roman"; const SIZE = 22; // 11pt in half-points type DocChild = InstanceType | InstanceType; const children: DocChild[] = []; children.push( new Paragraph({ heading: HeadingLevel.TITLE, spacing: { after: 200 }, alignment: AlignmentType.CENTER, children: [new TextRun({ text: title.toUpperCase(), color: "000000", font: FONT, size: SIZE, bold: true })], }), ); const cellBorder = { top: { style: BorderStyle.SINGLE, size: 1, color: "CCCCCC" }, bottom: { style: BorderStyle.SINGLE, size: 1, color: "CCCCCC" }, left: { style: BorderStyle.SINGLE, size: 1, color: "CCCCCC" }, right: { style: BorderStyle.SINGLE, size: 1, color: "CCCCCC" }, }; const headingLevels = [ HeadingLevel.HEADING_1, HeadingLevel.HEADING_2, HeadingLevel.HEADING_3, HeadingLevel.HEADING_4, ]; const counters = [0, 0, 0, 0]; for (const section of sections as { heading?: string; content?: string; level?: number; pageBreak?: boolean; table?: { headers: string[]; rows: string[][] }; }[]) { if (section.pageBreak) { children.push( new Paragraph({ children: [new PageBreak()] }), ); } if (section.heading) { const idx = Math.min((section.level ?? 1) - 1, 3); counters[idx]++; for (let i = idx + 1; i < 4; i++) counters[i] = 0; const prefix = counters.slice(0, idx + 1).join("."); const headingText = `${prefix}. ${idx === 0 ? section.heading.toUpperCase() : section.heading}`; children.push( new Paragraph({ heading: headingLevels[idx], spacing: { after: 160 }, children: [new TextRun({ text: headingText, color: "000000", font: FONT, size: SIZE, bold: true })], }), ); } if (section.table) { const { headers, rows } = section.table; const colCount = headers.length; const tableRows: InstanceType[] = []; // Header row tableRows.push( new TableRow({ tableHeader: true, children: headers.map( (h) => new TableCell({ borders: cellBorder, shading: { fill: "F2F2F2" }, children: [ new Paragraph({ children: [new TextRun({ text: h, bold: true, font: FONT, size: SIZE })], alignment: AlignmentType.LEFT, }), ], }), ), }), ); // Data rows — normalize each row to exactly colCount cells. // LLMs occasionally emit malformed rows (extra fragments from // stray delimiters, or short rows); padding/truncating here // keeps the rendered table aligned to the headers. for (const rawRow of rows) { const row = Array.isArray(rawRow) ? rawRow : []; const normalized: string[] = []; for (let i = 0; i < colCount; i++) { normalized.push( typeof row[i] === "string" ? row[i] : "", ); } if (row.length !== colCount) { console.warn( `[generate_docx] row length ${row.length} != headers ${colCount}; normalized`, ); } tableRows.push( new TableRow({ children: normalized.map( (cell) => new TableCell({ borders: cellBorder, children: [ new Paragraph({ children: [new TextRun({ text: cell, font: FONT, size: SIZE })], }), ], }), ), }), ); } children.push( new Table({ width: { size: 100, type: WidthType.PERCENTAGE }, rows: tableRows, }), ); children.push(new Paragraph({ text: "" })); } if (section.content) { for (const line of section.content.split("\n")) { const trimmed = line.trim(); if (!trimmed) continue; const bulletMatch = trimmed.match(/^[-•*]\s+(.+)/); if (bulletMatch) { children.push( new Paragraph({ bullet: { level: 0 }, spacing: { after: 120 }, children: [new TextRun({ text: bulletMatch[1], font: FONT, size: SIZE })], }), ); } else { children.push( new Paragraph({ spacing: { after: 120 }, children: [new TextRun({ text: trimmed, font: FONT, size: SIZE })], }), ); } } } } const pageSetup = options?.landscape ? { page: { size: { orientation: PageOrientation.LANDSCAPE } } } : {}; const doc = new Document({ sections: [{ properties: pageSetup, children }] }); const buf = await Packer.toBuffer(doc); const docId = crypto.randomUUID().replace(/-/g, ""); const safeTitle = title .replace(/[^a-zA-Z0-9 -]/g, "") .trim() .slice(0, 64) || "document"; const filename = `${safeTitle}.docx`; const key = generatedDocKey(userId, docId, filename); await uploadFile( key, buf.buffer as ArrayBuffer, "application/vnd.openxmlformats-officedocument.wordprocessingml.document", ); const downloadUrl = buildDownloadUrl(key, filename); // Persist to DB so generated docs are first-class documents: // openable in the DocPanel and editable via plan_comments_on_document. In // project chats we attach to the project so it appears in the // sidebar; in the general chat we leave project_id null and it // stays a standalone document. const { data: docRow, error: docErr } = await db .from("documents") .insert({ project_id: options?.projectId ?? null, user_id: userId, filename, file_type: "docx", size_bytes: buf.byteLength, status: "ready", }) .select("id") .single(); if (docErr || !docRow) { return { error: `Failed to record generated document: ${docErr?.message ?? "unknown"}`, }; } const documentId = docRow.id as string; const { data: versionRow, error: verErr } = await db .from("document_versions") .insert({ document_id: documentId, storage_path: key, source: "generated", version_number: 1, display_name: filename, }) .select("id") .single(); if (verErr || !versionRow) { return { error: `Failed to record generated document version: ${verErr?.message ?? "unknown"}`, }; } const versionId = versionRow.id as string; await db .from("documents") .update({ current_version_id: versionId }) .eq("id", documentId); return { filename, download_url: downloadUrl, document_id: documentId, version_id: versionId, version_number: 1, storage_path: key, message: `Document '${filename}' has been generated successfully.`, }; } catch (e) { return { error: String(e) }; } } // --------------------------------------------------------------------------- // Document version helpers (DOCX tracked-change editing) // --------------------------------------------------------------------------- /** * Resolve the current .docx bytes for a document, preferring the active * tracked-changes version if one exists, else the original upload. */ export async function loadCurrentVersionBytes( documentId: string, db: ReturnType, ): Promise<{ bytes: Buffer; storage_path: string; version_id: string; version_number: number | null; } | null> { const active = await loadActiveVersion(documentId, db); if (!active) return null; const raw = await downloadFile(active.storage_path); if (!raw) return null; return { bytes: Buffer.from(raw), storage_path: active.storage_path, version_id: active.id, version_number: active.version_number, }; } /** * Ensure the document has a document_versions row for the current upload. * Called before writing the first 'assistant_edit' row so the history is * complete. Idempotent. */ export async function runEditDocument(params: { documentId: string; userId: string; edits?: EditInput[]; operations?: EditOperation[]; expectedSurfaceId?: string; db: ReturnType; /** * If provided, append these edits to the existing turn-scoped version * (overwrites the file at storagePath and reuses the document_versions * row) instead of creating a new version. Used to collapse multiple * plan_comments_on_document tool calls within a single assistant turn into one * version. */ reuseVersion?: { versionId: string; versionNumber: number; storagePath: string; }; baseBytesOverride?: Buffer; extraApplyReport?: any[]; extraApplySummary?: { total: number; applied: number; failed: number; failures: { id: string; target_text: string; message: string; paragraph_id: string | null; }[]; }; }): Promise< | { ok: true; version_id: string; version_number: number; storage_path: string; download_url: string; annotations: EditAnnotation[]; errors: { index: number; reason: string }[]; edit_verification?: EditVerification; apply_report?: any[]; apply_summary?: { total: number; applied: number; failed: number; failures: { id: string; target_text: string; message: string; paragraph_id: string | null; }[]; }; } | { ok: false; error: string } > { const { documentId, userId, edits, operations, expectedSurfaceId, db, reuseVersion, baseBytesOverride, extraApplyReport, extraApplySummary, } = params; const { data: doc } = await db .from("documents") .select("id, filename") .eq("id", documentId) .single(); if (!doc) return { ok: false, error: "Document not found." }; const current = await loadCurrentVersionBytes(documentId, db); if (!current) return { ok: false, error: "Could not load document bytes." }; const hasOperations = Array.isArray(operations) && operations.length > 0; const hasLegacyEdits = Array.isArray(edits) && edits.length > 0; if (!hasOperations && !hasLegacyEdits) { return { ok: false, error: "No edit payload provided. Pass operations or edits." }; } const baseBytes = baseBytesOverride ?? current.bytes; const { bytes: editedBytes, changes, errors } = hasOperations ? await applyTrackedOperations(baseBytes, operations!, { author: DOCX_AUTHOR_DEFAULT, expectedSurfaceId, }) : await applyTrackedEdits(baseBytes, edits!, { author: DOCX_AUTHOR_DEFAULT }); if (changes.length === 0) { return { ok: false, error: errors[0]?.reason ?? "No edits could be applied. Re-read the document and retry with a precise anchor target.", }; } const ab = editedBytes.buffer.slice( editedBytes.byteOffset, editedBytes.byteOffset + editedBytes.byteLength, ) as ArrayBuffer; let versionRowId: string; let newPath: string; let nextVersionNumber: number; if (reuseVersion) { // Overwrite the existing turn version's file in place. The version // row, version_number, and current_version_id all already point here. newPath = reuseVersion.storagePath; versionRowId = reuseVersion.versionId; nextVersionNumber = reuseVersion.versionNumber; await uploadFile( newPath, ab, "application/vnd.openxmlformats-officedocument.wordprocessingml.document", ); } else { const versionId = crypto.randomUUID().replace(/-/g, ""); newPath = `documents/${userId}/${documentId}/edits/${versionId}.docx`; await uploadFile( newPath, ab, "application/vnd.openxmlformats-officedocument.wordprocessingml.document", ); // Per-document sequential number for the new assistant_edit // version. The counter spans upload + user_upload + assistant_edit // so the original upload is V1 and the first assistant edit is V2. const { data: maxRow } = await db .from("document_versions") .select("version_number") .eq("document_id", documentId) .in("source", ["upload", "user_upload", "assistant_edit"]) .order("version_number", { ascending: false, nullsFirst: false }) .limit(1) .maybeSingle(); nextVersionNumber = ((maxRow?.version_number as number | null) ?? 1) + 1; // Inherit the display name from the most recent prior version so // user-applied renames carry forward through further edits. Falls // back to the parent document's filename when no prior version has // a display name (e.g. the first assistant edit of a pre-existing // doc). We intentionally do NOT append "[Edited Vn]" — the version // number is surfaced separately as a tag in the UI. const { data: prevRow } = await db .from("document_versions") .select("display_name, created_at") .eq("document_id", documentId) .order("created_at", { ascending: false }) .limit(1) .maybeSingle(); const inheritedDisplayName = (prevRow?.display_name as string | null) ?? (doc.filename as string | null) ?? null; const { data: versionRow, error: verErr } = await db .from("document_versions") .insert({ document_id: documentId, storage_path: newPath, source: "assistant_edit", version_number: nextVersionNumber, display_name: inheritedDisplayName, }) .select("id") .single(); if (verErr || !versionRow) { return { ok: false, error: "Failed to record document version." }; } versionRowId = versionRow.id as string; } // Insert one row per change const editRows = changes.map((c) => ({ document_id: documentId, version_id: versionRowId, change_id: c.id, del_w_id: c.delId ?? null, ins_w_id: c.insId ?? null, deleted_text: c.deletedText, inserted_text: c.insertedText, context_before: c.contextBefore ?? "", context_after: c.contextAfter ?? "", operation: c.operation ?? null, target_block_id: c.targetBlockId ?? null, target_start: c.targetStart ?? null, target_end: c.targetEnd ?? null, base_surface_id: c.baseSurfaceId ?? expectedSurfaceId ?? null, target_text_hash: c.targetTextHash ?? null, status: "pending" as const, })); const { data: insertedEdits, error: editsErr } = await db .from("document_edits") .insert(editRows) .select("id, change_id, del_w_id, ins_w_id, deleted_text, inserted_text, context_before, context_after, operation, target_block_id, target_start, target_end, base_surface_id, target_text_hash"); if (editsErr || !insertedEdits) { return { ok: false, error: "Failed to record edits." }; } await db .from("documents") .update({ current_version_id: versionRowId }) .eq("id", documentId); const annotations: EditAnnotation[] = insertedEdits.map((r: { id: string; change_id: string; deleted_text: string; inserted_text: string; context_before: string | null; context_after: string | null; operation: string | null; target_block_id: string | null; target_start: number | null; target_end: number | null; base_surface_id: string | null; target_text_hash: string | null; }) => { const src = changes.find((c) => c.id === r.change_id); return { kind: "edit", edit_id: r.id, document_id: documentId, version_id: versionRowId, version_number: nextVersionNumber, change_id: r.change_id, del_w_id: src?.delId, ins_w_id: src?.insId, deleted_text: r.deleted_text ?? "", inserted_text: r.inserted_text ?? "", context_before: r.context_before ?? "", context_after: r.context_after ?? "", operation: r.operation ?? src?.operation ?? null, target_block_id: r.target_block_id ?? src?.targetBlockId ?? null, target_start: r.target_start ?? src?.targetStart ?? null, target_end: r.target_end ?? src?.targetEnd ?? null, base_surface_id: r.base_surface_id ?? src?.baseSurfaceId ?? expectedSurfaceId ?? null, target_text_hash: r.target_text_hash ?? src?.targetTextHash ?? null, reason: src?.reason, status: "pending", }; }); // --- Net-new tracked-change verification --- let editVerification: EditVerification | undefined; try { editVerification = await verifyDocxEdits(baseBytes, editedBytes, changes); } catch (err) { console.warn("[runEditDocument] edit verification failed:", err); } // Persistent, non-expiring permalink. The backend streams fresh bytes // on each request, so this URL stays valid as long as the file exists. const permalink = buildDownloadUrl(newPath, doc.filename as string); return { ok: true, version_id: versionRowId, version_number: nextVersionNumber, storage_path: newPath, download_url: permalink, annotations, errors, edit_verification: editVerification, apply_report: extraApplyReport, apply_summary: extraApplySummary, }; } // --------------------------------------------------------------------------- // Net-new tracked-change verification // --------------------------------------------------------------------------- interface EditVerification { claimed: number; net_new_inserted: number; matched: { text: string }[]; unmatched: { text: string; reason: string }[]; } /** * Compares the original and edited document bytes to find net-new w:ins/w:del * tags (ignoring pre-existing ones), then cross-references with the list of * changes the model claimed to insert. Returns a verification summary. */ async function verifyDocxEdits( originalBytes: Buffer, editedBytes: Buffer, claimedChanges: AppliedChange[], ): Promise { // Extract all tracked change IDs from both documents const originalIds = await extractTrackedChangeIds(originalBytes); const editedIds = await extractTrackedChangeIds(editedBytes); // Build set of pre-existing IDs const preExisting = new Set(originalIds.map((i) => i.w_id)); // Net-new = IDs in edited that weren't in original const netNew = editedIds.filter((i) => !preExisting.has(i.w_id)); const netNewIdSet = new Set(netNew.map((i) => i.w_id)); const matched: { text: string }[] = []; const unmatched: { text: string; reason: string }[] = []; for (const change of claimedChanges) { // A change is "matched" if any of its IDs (insId, delId) appear in net-new const changeIds = [change.insId, change.delId].filter( (id): id is string => typeof id === "string" && id.length > 0, ); const isPresent = changeIds.some((id) => netNewIdSet.has(id)); const label = change.insertedText?.slice(0, 60) || change.deletedText?.slice(0, 60) || change.id; if (isPresent) { matched.push({ text: label }); } else if (changeIds.length === 0) { // No IDs to check — likely a formatting-only change matched.push({ text: label }); } else { unmatched.push({ text: label, reason: "Tracked change IDs not found in net-new changes of the output document", }); } } return { claimed: claimedChanges.length, net_new_inserted: netNew.length, matched, unmatched, }; } // --------------------------------------------------------------------------- // Post-write verification for comment-only batches // --------------------------------------------------------------------------- export interface CommentBatchVerification { ok: boolean; original_block_count: number; commented_block_count: number; body_text_unchanged: boolean; comments_in_xml: number; expected_applied: number; missing_from_xml: number; paragraph_mismatches: string[]; } export async function verifyCommentBatchPostWrite(params: { originalBytes: Buffer; commentedBytes: Buffer; expectedAppliedCount: number; appliedReports: any[]; }): Promise { const { originalBytes, commentedBytes, expectedAppliedCount, appliedReports } = params; try { const originalSurface = await buildDocxEditSurface(originalBytes); const commentedSurface = await buildDocxEditSurface(commentedBytes); const bodyTextUnchanged = originalSurface.blocks.map((b) => b.text).join("\n") === commentedSurface.blocks.map((b) => b.text).join("\n"); // Count comments in word/comments.xml let commentsInXml = 0; try { const JSZip = (await import("jszip")).default; const zip = await JSZip.loadAsync(commentedBytes); const commentsXml = zip.file("word/comments.xml"); if (commentsXml) { const xml = await commentsXml.async("string"); const parser = new (await import("fast-xml-parser")).XMLParser({ ignoreAttributes: false, attributeNamePrefix: "@_", }); const parsed = parser.parse(xml); const comments = parsed?.["w:comments"]?.["w:comment"]; if (Array.isArray(comments)) { commentsInXml = comments.length; } else if (comments) { commentsInXml = 1; } } } catch { // If we can't parse comments.xml, fall through } const appliedIds = new Set( appliedReports .filter((r) => r.status === "applied") .map((r) => r.model_block_id ?? r.paragraph_id) .filter(Boolean), ); const paragraphMismatches: string[] = []; for (const blockId of appliedIds) { const origAnchor = originalSurface.anchors.find((a) => a.block_id === blockId); const newAnchor = commentedSurface.anchors.find((a) => a.block_id === blockId); if (origAnchor && newAnchor) { if (origAnchor.text_hash !== newAnchor.text_hash) { paragraphMismatches.push(`${blockId}: text_hash changed`); } } } const missingFromXml = Math.max(0, expectedAppliedCount - commentsInXml); return { ok: bodyTextUnchanged && missingFromXml === 0 && paragraphMismatches.length === 0, original_block_count: originalSurface.blocks.length, commented_block_count: commentedSurface.blocks.length, body_text_unchanged: bodyTextUnchanged, comments_in_xml: commentsInXml, expected_applied: expectedAppliedCount, missing_from_xml: missingFromXml, paragraph_mismatches: paragraphMismatches, }; } catch (err) { console.warn("[verifyCommentBatchPostWrite] verification failed:", err); return { ok: false, original_block_count: -1, commented_block_count: -1, body_text_unchanged: false, comments_in_xml: -1, expected_applied: expectedAppliedCount, missing_from_xml: -1, paragraph_mismatches: [String(err)], }; } } // --------------------------------------------------------------------------- // Pydantic-style validation for comment_on_document arguments // --------------------------------------------------------------------------- interface CommentArgValidation { valid: boolean; errors: string[]; validated: { doc_id: string; block_id: string; target_text: string; anchor_mode: "paragraph" | "exact"; before_anchor?: string; after_anchor?: string; comment: string; comment_key?: string; }; } function sanitizeCommentArgs(args: Record): Record { const sanitized = { ...args }; const rawDocId = String(sanitized.doc_id ?? "").trim(); const docMatch = rawDocId.match(/^(doc-\d+)/); if (docMatch && rawDocId !== docMatch[1]) { sanitized.doc_id = docMatch[1]; console.warn( `[comment_on_document] sanitized malformed doc_id "${rawDocId}" -> "${docMatch[1]}"` ); } const rawBlockId = String(sanitized.block_id ?? "").trim(); const blockMatch = rawBlockId.match(/^((?:P_\d+|_?BMK_[PT]_[a-z0-9]+))/); if (blockMatch) { const cleanedBlockId = normalizeModelBlockId(blockMatch[1]); if (rawBlockId !== cleanedBlockId) { sanitized.block_id = cleanedBlockId; console.warn( `[comment_on_document] sanitized malformed block_id "${rawBlockId}" -> "${cleanedBlockId}"` ); } } return sanitized; } function validateCommentArgs( args: Record, docStore: DocStore, ): CommentArgValidation { const errors: string[] = []; const validated: Partial = {}; // 1. doc_id validation const rawDocId = args.doc_id; if (rawDocId === undefined || rawDocId === null || rawDocId === "") { errors.push(`doc_id: Field is required.`); } else { const docIdStr = String(rawDocId).trim(); const availableLabels = Array.from(docStore.keys()); if (!docStore.has(docIdStr)) { errors.push( `doc_id: Value "${docIdStr}" is invalid. ` + `Must be one of the available document labels: ${JSON.stringify(availableLabels)}.` ); } else { validated.doc_id = docIdStr; } } // 2. block_id validation const rawBlockId = args.block_id; if (rawBlockId === undefined || rawBlockId === null || rawBlockId === "") { errors.push(`block_id: Field is required.`); } else { const blockIdStr = normalizeModelBlockId(String(rawBlockId).trim()); if (!isValidDocxModelBlockId(blockIdStr)) { errors.push( `block_id: Value "${String(rawBlockId).trim()}" is invalid. ` + `Must match P_NNNN or BMK_P_/BMK_T_ bookmark format (e.g., "P_0001", "BMK_P_a3x7k").` ); } else { validated.block_id = blockIdStr; } } // 3. anchor_mode validation const rawAnchorMode = args.anchor_mode; const anchorMode = rawAnchorMode === undefined || rawAnchorMode === null || rawAnchorMode === "" ? "paragraph" : String(rawAnchorMode).trim().toLowerCase(); if (anchorMode !== "paragraph" && anchorMode !== "exact") { errors.push(`anchor_mode: Value "${anchorMode}" is invalid. Must be "paragraph" or "exact".`); } else { validated.anchor_mode = anchorMode; } // 4. target_text validation const rawTargetText = args.target_text; if (rawTargetText === undefined || rawTargetText === null || rawTargetText === "") { if (anchorMode === "exact") { errors.push(`target_text: Field is required when anchor_mode is "exact".`); } else { validated.target_text = ""; } } else { const targetTextStr = String(rawTargetText).trim(); if (targetTextStr.length === 0 && anchorMode === "exact") { errors.push(`target_text: Cannot be an empty string.`); } else { validated.target_text = targetTextStr; } } // 5. comment validation const rawComment = args.comment; if (rawComment === undefined || rawComment === null || rawComment === "") { errors.push(`comment: Field is required.`); } else { const commentStr = String(rawComment).trim(); if (commentStr.length === 0) { errors.push(`comment: Cannot be an empty string.`); } else { validated.comment = commentStr; } } // 6. optional anchor fields validated.before_anchor = args.before_anchor ? String(args.before_anchor).trim() : ""; validated.after_anchor = args.after_anchor ? String(args.after_anchor).trim() : ""; // 7. optional comment_key const rawCommentKey = args.comment_key; if (rawCommentKey !== undefined && rawCommentKey !== null && rawCommentKey !== "") { validated.comment_key = String(rawCommentKey).trim(); } return { valid: errors.length === 0, errors, validated: validated as CommentArgValidation["validated"], }; } export async function runCommentDocument(params: { userId: string; documentId: string; args: { block_id: string; target_text: string; anchor_mode: "paragraph" | "exact"; before_anchor: string; after_anchor: string; comment: string; comment_key?: string }; db: ReturnType; docIndex: DocIndex; trace?: RunTree | null; }): Promise<{ ok: boolean; version_id?: string; version_number?: number; download_url?: string; apply_report?: any[]; error?: string; }> { const { userId, documentId, args, db, docIndex, trace } = params; const docInfo = Object.values(docIndex).find(d => d.document_id === documentId); if (!docInfo) return { ok: false, error: "Document not found." }; if (docInfo.file_type !== "docx") return { ok: false, error: "comment_on_document only supports .docx files." }; const current = await loadCurrentVersionBytes(documentId, db); if (!current) return { ok: false, error: "Could not load document bytes." }; // Build canonical anchor map for the current document bytes. const currentSurface = await buildDocxEditSurface(current.bytes, { versionId: current.version_id, }); const anchorMap = new Map(currentSurface.anchors.map((a) => [a.block_id, a])); const anchor = anchorMap.get(args.block_id); if (!anchor) { return { ok: false, error: `Unknown or stale block_id ${args.block_id} for the current document surface. Call read_document again and retry with a block_id from the latest surface.`, }; } try { const pythonUrl = process.env.PYTHON_SERVICE_URL || "http://localhost:8000"; const response = await fetch(`${pythonUrl}/api/apply-comments`, { method: "POST", headers: { "Content-Type": "application/json", "x-user-id": userId, ...traceHeaders(trace), }, body: JSON.stringify({ document_bytes_b64: Buffer.from(current.bytes).toString("base64"), document_name: docInfo.filename, findings: [ { id: `chat_comment_${Date.now()}`, model_block_id: args.block_id, comment_key: args.comment_key, risk_level: "info", clause_type: "Comment", issue: "Assistant Feedback", comment: args.comment, review_reasoning: { clause_section: "N/A", violated_standard: "N/A", zamil_exposure: "Manual chat comment not from playbook review" }, target_text: args.target_text, anchor_mode: args.anchor_mode, start_paragraph_id: args.block_id, end_paragraph_id: args.block_id, start_anchor: args.target_text ? args.target_text.slice(0, 220) : "", end_anchor: args.target_text ? args.target_text.slice(-220) : "", before_anchor: args.before_anchor || "", after_anchor: args.after_anchor || "", confidence: 1.0, stable_id: anchor.stable_id, native_para_id: anchor.native_para_id, text_hash: anchor.text_hash, xml_path: anchor.xml_path, bookmark_name: anchor.bookmark_name ?? null, }, ], }), }); if (!response.ok) { const errData = await response.json().catch(() => ({})); return { ok: false, error: errData.detail || `Python service error: ${response.statusText}` }; } const data = await response.json(); const commentedBytesB64 = typeof data.commented_bytes_b64 === "string" ? data.commented_bytes_b64 : typeof data.document_bytes_b64 === "string" ? data.document_bytes_b64 : ""; if (!commentedBytesB64) { return { ok: false, error: "Python service did not return commented document bytes.", apply_report: Array.isArray(data.apply_report) ? data.apply_report : undefined, }; } const commentedBytes = Buffer.from(commentedBytesB64, "base64"); // Save as a new version const versionId = crypto.randomUUID().replace(/-/g, ""); const newPath = `documents/${userId}/${documentId}/comments/${versionId}.docx`; const exact = commentedBytes.buffer.slice( commentedBytes.byteOffset, commentedBytes.byteOffset + commentedBytes.byteLength, ) as ArrayBuffer; await uploadFile(newPath, exact, "application/vnd.openxmlformats-officedocument.wordprocessingml.document"); const { data: maxRow } = await db .from("document_versions") .select("version_number") .eq("document_id", documentId) .order("version_number", { ascending: false }) .limit(1) .maybeSingle(); const nextVersionNumber = ((maxRow?.version_number as number | null) ?? 1) + 1; const { data: versionRow, error: verErr } = await db .from("document_versions") .insert({ document_id: documentId, storage_path: newPath, source: "assistant_edit", // Treat as an edit for UI purposes version_number: nextVersionNumber, display_name: docInfo.filename, }) .select("id") .single(); if (verErr || !versionRow) { return { ok: false, error: "Failed to record document version." }; } await db .from("documents") .update({ current_version_id: versionRow.id }) .eq("id", documentId); return { ok: true, version_id: versionRow.id, version_number: nextVersionNumber, download_url: buildDownloadUrl(newPath, docInfo.filename), apply_report: data.apply_report, }; } catch (err) { console.error("[runCommentDocument] error:", err); return { ok: false, error: err instanceof Error ? err.message : String(err) }; } } // --------------------------------------------------------------------------- // Tool dispatch // --------------------------------------------------------------------------- async function readDocumentContent( docLabel: string, docStore: DocStore, write: (s: string) => void, docIndex?: DocIndex, db?: ReturnType, opts?: { emitEvents?: boolean }, anchorManifestState?: AnchorManifestState, trace?: RunTree | null, ): Promise { const emitEvents = opts?.emitEvents ?? true; console.log(`[read_document] called with docLabel="${docLabel}"`); const docInfo = docStore.get(docLabel); if (!docInfo) { console.log( `[read_document] MISS — docLabel "${docLabel}" not in docStore. Known labels:`, Array.from(docStore.keys()), ); return "Document not found."; } console.log( `[read_document] docInfo: filename="${docInfo.filename}", file_type="${docInfo.file_type}", storage_path="${docInfo.storage_path}"`, ); const documentId = docIndex?.[docLabel]?.document_id; const emitDocRead = () => { if (!emitEvents) return; write( `data: ${JSON.stringify({ type: "doc_read", filename: docInfo.filename, document_id: documentId, })}\n\n`, ); }; if (emitEvents) write( `data: ${JSON.stringify({ type: "doc_read_start", filename: docInfo.filename, document_id: documentId, })}\n\n`, ); try { // Prefer the current tracked-changes version (if any) so read_document // reflects accepted/pending edits rather than the original upload. let raw: ArrayBuffer | null = null; let sourcePath = docInfo.storage_path; if (documentId && db) { const current = await loadCurrentVersionBytes(documentId, db); if (current) { raw = current.bytes.buffer.slice( current.bytes.byteOffset, current.bytes.byteOffset + current.bytes.byteLength, ) as ArrayBuffer; sourcePath = current.storage_path; console.log( `[read_document] using current version path="${sourcePath}" (bytes=${raw.byteLength})`, ); } else { console.log( `[read_document] loadCurrentVersionBytes returned null for documentId="${documentId}", falling back to original storage_path`, ); } } if (!raw) { raw = await downloadFile(docInfo.storage_path); if (raw) { console.log( `[read_document] fallback download from storage_path="${docInfo.storage_path}" (bytes=${raw.byteLength})`, ); } } if (!raw) { console.log( `[read_document] FAILED to download any bytes for docLabel="${docLabel}" (tried path="${sourcePath}")`, ); emitDocRead(); return "Document could not be read."; } // Log the first 8 bytes so we can identify real file format regardless // of the declared file_type. Valid .docx starts with "PK\x03\x04" // (zip). Legacy .doc starts with "\xD0\xCF\x11\xE0" (OLE/CFB). // %PDF-1 is a PDF even if mislabeled. Truncated uploads show as all-zero. { const head = Buffer.from(raw).subarray(0, 8); const hex = head.toString("hex"); const ascii = head .toString("binary") .replace(/[^\x20-\x7e]/g, "."); console.log( `[read_document] magic bytes hex=${hex} ascii="${ascii}" for filename="${docInfo.filename}"`, ); } let text: string; if (docInfo.file_type === "pdf") { const surface = await buildPdfVirtualSurface(raw, { documentId, versionId: docIndex?.[docLabel]?.version_id ?? null, }); rememberPdfAnchorManifest( anchorManifestState, docLabel, documentId, surface, ); text = surface.text; console.log( `[read_document] pdf virtual surface constructed length=${text.length} pages=${surface.pages.length} for filename="${docInfo.filename}"`, ); } else if (docInfo.file_type === "docx") { // For DOCX we expose an anchor-aware edit surface so the model can // target edits deterministically via block IDs. const surface = await buildDocxEditSurface(Buffer.from(raw), { versionId: docIndex?.[docLabel]?.version_id ?? null, }); // Cache canonical anchors so comment_on_document can validate // block_id against the exact surface shown to the model. rememberAnchorManifest( anchorManifestState, docLabel, documentId, surface, ); text = renderDocxEditSurfaceForModel(surface); console.log( `[read_document] docx rendered edit surface length=${text.length} blocks=${surface.blocks.length} ` + `navigation_regions=${surface.navigationRegions.length} ` + `navigation_blocks=${surface.anchors.filter((anchor) => !anchor.anchor_eligible).length} ` + `for filename="${docInfo.filename}"`, ); if (!text) { console.log( `[read_document] docx surface renderer returned empty, falling back to extractDocxBodyText for filename="${docInfo.filename}"`, ); text = await extractDocxBodyText(Buffer.from(raw)); console.log( `[read_document] docx extractDocxBodyText fallback length=${text.length} for filename="${docInfo.filename}"`, ); } // Log exact model-visible surface to LangSmith for debugging anchor issues if (trace) { const surfaceTrace = buildDocumentSurfaceTracePayload({ docLabel, documentId, filename: docInfo.filename, surfaceId: surface.surfaceId, text, }); const surfTrace = createTraceChild(trace, { name: "read_document.surface", run_type: "chain", inputs: surfaceTrace, tags: ["document_surface_for_model"], }); void endTrace(surfTrace, { outputs: { char_count: text.length, block_count: surface.anchors.length, navigation_region_count: surface.navigationRegions.length, navigation_block_count: surface.anchors.filter( (anchor) => !anchor.anchor_eligible, ).length, surface_id: surface.surfaceId, }, }); } } else { console.log( `[read_document] unknown file_type="${docInfo.file_type}" for filename="${docInfo.filename}", trying mammoth`, ); const mammoth = await import("mammoth"); const result = await mammoth.extractRawText({ buffer: Buffer.from(raw), }); text = result.value; console.log( `[read_document] mammoth length=${text.length} for filename="${docInfo.filename}"`, ); } console.log( `[read_document] DONE filename="${docInfo.filename}" finalTextLength=${text.length}`, ); emitDocRead(); return text; } catch (err) { console.log( `[read_document] THREW for docLabel="${docLabel}" filename="${docInfo.filename}":`, err, ); if (emitEvents) write(`data: ${JSON.stringify({ type: "doc_read", filename: docInfo.filename })}\n\n`); return "Document could not be read."; } } async function readDocumentContentOncePerTurn(params: { docLabel: string; docStore: DocStore; write: (s: string) => void; docIndex?: DocIndex; db?: ReturnType; opts?: { emitEvents?: boolean }; anchorManifestState?: AnchorManifestState; trace?: RunTree | null; turnDocumentState: TurnDocumentState; }): Promise { const { docLabel, docStore, write, docIndex, db, opts, anchorManifestState, trace, turnDocumentState, } = params; const existing = turnDocumentState.readsByDocLabel.get(docLabel); if (existing) { turnDocumentState.duplicateReadCount += 1; return JSON.stringify({ ok: true, already_read: true, doc_id: docLabel, filename: existing.filename, char_count: existing.charCount, read_serial: existing.readSerial, surface_id: existing.surfaceId ?? null, message: "This document has already been read in this assistant turn. Use the prior read_document/fetch_documents result already in context. For exact phrase lookup, call find_in_document instead of reading the full document again.", }); } const content = await readDocumentContent( docLabel, docStore, write, docIndex, db, opts, anchorManifestState, trace, ); const docInfo = docStore.get(docLabel); if (docInfo) { const manifest = anchorManifestState?.byDocLabel.get(docLabel); turnDocumentState.readsByDocLabel.set(docLabel, { docLabel, filename: docInfo.filename, content, charCount: content.length, readSerial: manifest?.readSerial ?? turnDocumentState.readsByDocLabel.size + 1, surfaceId: manifest?.surfaceId, }); } return content; } /** * Build a whitespace-collapsed, lowercased copy of `text`, plus a map from * each character index in the normalized form back to the corresponding * index in the original text. Used by `findInDocumentContent` so matches * are tolerant of case + whitespace variance but can still return the * exact original excerpt. */ function normalizeWithMap(text: string): { norm: string; origIdx: number[] } { const norm: string[] = []; const origIdx: number[] = []; let prevSpace = false; for (let i = 0; i < text.length; i++) { const ch = text[i]; if (/\s/.test(ch)) { if (!prevSpace) { norm.push(" "); origIdx.push(i); prevSpace = true; } } else { norm.push(ch.toLowerCase()); origIdx.push(i); prevSpace = false; } } return { norm: norm.join(""), origIdx }; } function normalizeQuery(q: string): string { return q.trim().replace(/\s+/g, " ").toLowerCase(); } /** * Ctrl+F helper. Returns a JSON-serializable result with up to `maxResults` * hits, each containing the original-text excerpt plus surrounding context. */ async function findInDocumentContent(params: { docLabel: string; query: string; maxResults?: number; contextChars?: number; anchorEligibleOnly?: boolean; docStore: DocStore; write: (s: string) => void; docIndex?: DocIndex; db?: ReturnType; anchorManifestState?: AnchorManifestState; }): Promise { const { docLabel, query, maxResults: requestedMaxResults, contextChars: requestedContextChars, anchorEligibleOnly = true, docStore, write, docIndex, db, anchorManifestState, } = params; const maxResults = clampInteger(requestedMaxResults, { min: 1, max: 20, fallback: 10, }); const contextChars = clampInteger(requestedContextChars, { min: 20, max: 300, fallback: 120, }); if (!query || !query.trim()) { return JSON.stringify({ ok: false, error: "Empty query." }); } const docInfo = docStore.get(docLabel); if (!docInfo) { return JSON.stringify({ ok: false, error: `Document '${docLabel}' not found.`, }); } // Announce the search to the UI, then reuse readDocumentContent for its // fallbacks — but suppress its own doc_read events so the user only sees // the doc_find block (not a competing doc_read block for the same op). write( `data: ${JSON.stringify({ type: "doc_find_start", filename: docInfo.filename, query, })}\n\n`, ); const documentId = docIndex?.[docLabel]?.document_id; let raw: ArrayBuffer | null = null; if (documentId && db) { const current = await loadCurrentVersionBytes(documentId, db); if (current) { raw = current.bytes.buffer.slice( current.bytes.byteOffset, current.bytes.byteOffset + current.bytes.byteLength, ) as ArrayBuffer; } } if (!raw) { raw = await downloadFile(docInfo.storage_path); } if (!raw) { write( `data: ${JSON.stringify({ type: "doc_find", filename: docInfo.filename, query, total_matches: 0, })}\n\n`, ); return JSON.stringify({ ok: false, filename: docInfo.filename, error: "Document could not be read.", }); } if (docInfo.file_type === "docx") { const surface = await buildDocxEditSurface(Buffer.from(raw), { versionId: docIndex?.[docLabel]?.version_id ?? null, }); rememberAnchorManifest( anchorManifestState, docLabel, documentId, surface, ); const search = findInDocxEditSurface({ surface, query, maxResults, contextChars, anchorEligibleOnly, }); write( `data: ${JSON.stringify({ type: "doc_find", filename: docInfo.filename, query, total_matches: search.totalMatches, excluded_navigation_matches: search.excludedNavigationMatches, })}\n\n`, ); return JSON.stringify({ ok: true, filename: docInfo.filename, query, surface_id: surface.surfaceId, total_matches: search.totalMatches, excluded_navigation_matches: search.excludedNavigationMatches, returned: search.hits.length, truncated: search.totalMatches > search.hits.length, hits: search.hits, }); } const text = await readDocumentContent( docLabel, docStore, write, docIndex, db, { emitEvents: false }, anchorManifestState, ); if (!text || text === "Document could not be read.") { write( `data: ${JSON.stringify({ type: "doc_find", filename: docInfo.filename, query, total_matches: 0, })}\n\n`, ); return JSON.stringify({ ok: false, filename: docInfo.filename, error: "Document could not be read.", }); } const { norm, origIdx } = normalizeWithMap(text); const needle = normalizeQuery(query); if (!needle) { return JSON.stringify({ ok: false, error: "Empty query after normalization." }); } type Hit = { index: number; excerpt: string; context: string; }; const hits: Hit[] = []; let from = 0; while (from <= norm.length - needle.length && hits.length < maxResults) { const pos = norm.indexOf(needle, from); if (pos < 0) break; const endNormPos = pos + needle.length; const origStart = origIdx[pos] ?? 0; const origEnd = endNormPos - 1 < origIdx.length ? origIdx[endNormPos - 1] + 1 : text.length; const ctxStart = Math.max(0, origStart - contextChars); const ctxEnd = Math.min(text.length, origEnd + contextChars); hits.push({ index: hits.length, excerpt: text.slice(origStart, origEnd), context: (ctxStart > 0 ? "…" : "") + text.slice(ctxStart, ctxEnd).replace(/\s+/g, " ").trim() + (ctxEnd < text.length ? "…" : ""), }); from = pos + Math.max(1, needle.length); } // Count total occurrences beyond the cap so the model knows whether to narrow the query. let totalMatches = hits.length; if (hits.length >= maxResults) { let probe = from; while (probe <= norm.length - needle.length) { const pos = norm.indexOf(needle, probe); if (pos < 0) break; totalMatches++; probe = pos + Math.max(1, needle.length); } } write( `data: ${JSON.stringify({ type: "doc_find", filename: docInfo.filename, query, total_matches: totalMatches, })}\n\n`, ); return JSON.stringify({ ok: true, filename: docInfo.filename, query, total_matches: totalMatches, returned: hits.length, truncated: totalMatches > hits.length, hits, }); } export type DocEditedResult = { filename: string; document_id: string; version_id: string; version_number: number | null; download_url: string; annotations: EditAnnotation[]; apply_report?: any[]; apply_summary?: { total: number; applied: number; failed: number; failures: { id: string; target_text: string; message: string; paragraph_id: string | null; }[]; }; edit_verification?: unknown; }; export type TurnEditState = Map< string, { versionId: string; versionNumber: number; storagePath: string } >; type CommentAttemptState = Map< string, { attempts: number; applied: boolean; staleFailures: number; latestFailureCode?: string; latestMessage?: string; latestFailureReadSerial?: number; toolCallIds: string[]; } >; // --------------------------------------------------------------------------- // Best-of-both-worlds comment planning pipeline types // --------------------------------------------------------------------------- export type CommentPlanIntent = { block_id: string; comment: string; status?: string; reasoning?: { clause_section: string; violated_standard: string; zamil_exposure: string }; target_text?: string; anchor_mode?: "paragraph" | "exact"; before_anchor?: string; after_anchor?: string; comment_key?: string; type?: "COMMENT" | "REWRITE_PARAGRAPH" | "INSERT" | "DELETE_PARAGRAPH"; new_text?: string; insert_text?: string; position?: string; rewrite_mode?: string; old_span?: string; new_span?: string; }; type AnchorCorrectionCandidate = { block_id: string; text: string; score: number; match_reason: string; }; type CommentPlanSource = | "plan_comments_on_document" | "correct_comment_anchors" | "fix_comment_findings"; type RepairKind = | "anchor" | "evidence" | "operation_schema" | "base_schema" | "red_zone" | "unknown"; type TerminalFallback = "COMMENT"; type PlannedCommentItem = CommentPlanIntent & { anchor?: CanonicalAnchor; failureCode?: string; relatedFailureCodes?: string[]; message?: string; repairKind?: RepairKind; repairable?: boolean; repairAttempts?: number; terminalFallback?: TerminalFallback; rawIntent?: unknown; downgradeToComment?: boolean; fallbackApplied?: boolean; anchorCandidates?: AnchorCorrectionCandidate[]; anchorRejectionReason?: string; excludedNavigationRegion?: { start_block_id: string; end_block_id: string; reason: string; }; anchorRedirectedFrom?: string; anchorRedirectedTo?: string; anchorRedirectReason?: string; }; type FindingCorrection = { comment_key: string; status: "corrected" | "unresolved" | "downgrade_to_comment"; reason?: string; new_block_id?: string; corrected_comment?: string; corrected_status?: "FAIL" | "MISSING" | "RISK" | "REVIEW" | "ESCALATE"; corrected_reasoning?: { clause_section: string; violated_standard: string; zamil_exposure: string; }; corrected_target_text?: string; corrected_type?: "COMMENT" | "REWRITE_PARAGRAPH" | "INSERT" | "DELETE_PARAGRAPH"; corrected_new_text?: string; corrected_insert_text?: string; corrected_position?: "before" | "after"; corrected_rewrite_mode?: "paragraph" | "span"; corrected_old_span?: string; corrected_new_span?: string; }; type PlanValidationResult = | { ok: true; item: PlannedCommentItem } | { ok: false; item: PlannedCommentItem; failureCode: string; relatedFailureCodes?: string[]; message: string; repairable: boolean; repairKind: RepairKind; terminalFallback?: TerminalFallback; }; const MAX_FINDING_REPAIR_ATTEMPTS = 1; type PendingCommentPlan = { docLabel: string; documentId: string; docInfo: NonNullable>; indexed: DocIndex[string]; current: NonNullable>>; currentSurface: Awaited>; manifest: AnchorManifest | undefined; valid: PlannedCommentItem[]; needsCorrection: PlannedCommentItem[]; failedPermanently: PlannedCommentItem[]; ignoredPassReports: Record[]; }; export type PendingCommentPlanState = Map; // Generic writer input so both legacy batch and planned batch can share runCommentDocumentBatch type WriterCommentItem = { id: string; args: { doc_id: string; block_id: string; comment: string; target_text: string; anchor_mode: "paragraph" | "exact"; before_anchor?: string; after_anchor?: string; comment_key: string; type?: string; new_text?: string; insert_text?: string; position?: string; rewrite_mode?: string; old_span?: string; new_span?: string; }; anchor: CanonicalAnchor; reasoning?: { clause_section: string; violated_standard: string; zamil_exposure: string }; anchor_text?: string; section_heading?: string | null; finding_status?: WorkflowFindingStatus; document_order?: number; source?: CommentPlanSource; }; type CommentVerificationRow = { tool_call_id: string; ok: boolean; doc_id?: string; document_id?: string; block_id?: string; comment_key?: string; status?: string; failure_code?: string; message?: string; model_block_id?: string; writer_paragraph_id?: string; comment?: string; reasoning?: { clause_section: string; violated_standard: string; zamil_exposure: string }; anchor_text?: string; section_heading?: string | null; op_type?: string; bookmark_name?: string | null; finding_status?: WorkflowFindingStatus; document_order?: number; apply_status?: WorkflowApplyStatus; source?: CommentPlanSource; amended_wording?: string; repair_kind?: RepairKind; repair_attempts?: number; fallback_applied?: boolean; downgrade_to_comment?: boolean; original_type?: string; final_type?: string; }; function stringOrUndefined(value: unknown): string | undefined { return typeof value === "string" && value.trim() ? value : undefined; } function stringOrNull(value: unknown): string | null { return typeof value === "string" && value.trim() ? value : null; } function numberOrUndefined(value: unknown): number | undefined { return typeof value === "number" && Number.isFinite(value) ? value : undefined; } function workflowReasoningOrUndefined( value: unknown, ): CommentVerificationRow["reasoning"] | undefined { if (!value || typeof value !== "object") return undefined; const raw = value as Record; if (!("clause_section" in raw)) return undefined; return { clause_section: String(raw.clause_section ?? "").trim(), violated_standard: String(raw.violated_standard ?? "").trim(), zamil_exposure: String(raw.zamil_exposure ?? "").trim(), }; } function workflowFindingStatusOrUndefined( value: unknown, ): WorkflowFindingStatus | undefined { return value === "FAIL" || value === "MISSING" || value === "RISK" || value === "REVIEW" || value === "ESCALATE" ? value : undefined; } function workflowSourceOrUndefined( value: unknown, ): CommentVerificationRow["source"] | undefined { return value === "plan_comments_on_document" || value === "correct_comment_anchors" || value === "fix_comment_findings" ? value : undefined; } function applyStatusFromVerificationRow( row: CommentVerificationRow, ): WorkflowApplyStatus { return row.apply_status ?? (row.status === "ignored_pass" ? "ignored_pass" : row.status === "applied" ? "applied" : row.failure_code ? "writer_failed" : "not_applied"); } function mergeVerificationMetadata( existing: CommentVerificationRow, incoming: CommentVerificationRow, ): CommentVerificationRow { return { ...incoming, status: incoming.status ?? existing.status, failure_code: incoming.failure_code ?? existing.failure_code, message: incoming.message ?? existing.message, comment: incoming.comment ?? existing.comment, reasoning: incoming.reasoning ?? existing.reasoning, anchor_text: incoming.anchor_text ?? existing.anchor_text, section_heading: incoming.section_heading ?? existing.section_heading, op_type: incoming.op_type ?? existing.op_type, bookmark_name: incoming.bookmark_name ?? existing.bookmark_name, finding_status: incoming.finding_status ?? existing.finding_status, document_order: incoming.document_order ?? existing.document_order, apply_status: incoming.apply_status ?? existing.apply_status, source: incoming.source ?? existing.source, amended_wording: incoming.amended_wording ?? existing.amended_wording, }; } function dedupeCommentVerificationRows( rows: CommentVerificationRow[], ): CommentVerificationRow[] { return Array.from( rows.reduce((acc, row) => { const key = row.comment_key || `${row.model_block_id ?? row.block_id ?? "unknown"}:${row.tool_call_id}`; const existing = acc.get(key); if (!existing) { acc.set(key, row); return acc; } const existingStatus = applyStatusFromVerificationRow(existing); const incomingStatus = applyStatusFromVerificationRow(row); if (shouldReplace(existingStatus, incomingStatus)) { acc.set(key, mergeVerificationMetadata(existing, row)); } else if (existingStatus === incomingStatus) { acc.set(key, mergeVerificationMetadata(row, existing)); } return acc; }, new Map()) .values(), ); } type ValidatedCommentArgs = CommentArgValidation["validated"] & { comment_key: string; }; type PreparedCommentCall = { tc: ToolCall; docLabel: string; docInfo: NonNullable>; indexed: DocIndex[string]; args: ValidatedCommentArgs; op_type?: string; }; export type DocCreatedResult = { filename: string; download_url: string; document_id?: string; version_id?: string; version_number?: number | null; }; export type DocReplicatedResult = { /** Filename of the source document being copied. */ filename: string; /** How many copies were produced in this single tool call. */ count: number; /** One entry per new copy. */ copies: { new_filename: string; document_id: string; version_id: string; }[]; }; type ToolResultRow = { tool_call_id: string; content?: unknown; }; type ToolFailureSummary = { tool_call_id: string; tool_name: string; error: string; doc_id?: string; document_id?: string; }; function parseToolResultContent(content: unknown): unknown { if (typeof content !== "string") return content; try { return JSON.parse(content); } catch { return content; } } function summarizeToolFailure( row: ToolResultRow, call: ToolCall | undefined, ): ToolFailureSummary | null { const parsed = parseToolResultContent(row.content); if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) { return null; } const obj = parsed as Record; const error = typeof obj.error === "string" && obj.error.trim() ? obj.error.trim() : obj.ok === false ? "Tool returned ok=false without an error message." : null; if (!error) return null; return { tool_call_id: row.tool_call_id, tool_name: call?.function.name ?? "unknown", error, doc_id: typeof obj.doc_id === "string" ? obj.doc_id : undefined, document_id: typeof obj.document_id === "string" ? obj.document_id : undefined, }; } function summarizeMissingToolResult(call: ToolCall): ToolFailureSummary { return { tool_call_id: call.id, tool_name: call.function.name, error: `Tool '${call.function.name}' did not produce a tool_result.`, }; } const STALE_ANCHOR_FAILURE_CODES = new Set([ "anchor_stale", "anchor_stale_requires_document_refresh", ]); export function makeCommentKey(args: CommentArgValidation["validated"]): string { if (args.comment_key) { return `${args.doc_id}:${args.comment_key}`; } return `${args.doc_id}:${args.block_id}:${args.comment .slice(0, 120) .replace(/\s+/g, " ") .trim()}`; } const ACTIONABLE_STATUSES = new Set(["FAIL", "MISSING", "RISK", "REVIEW", "ESCALATE"]); export function isPassItem(intent: CommentPlanIntent): boolean { const status = intent.status?.trim().toUpperCase(); if (status === "PASS") return true; const commentPrefix = intent.comment?.trim().toUpperCase(); if (commentPrefix?.startsWith("[PASS]")) return true; return false; } export function isActionableStatus(intent: CommentPlanIntent): boolean { const status = intent.status?.trim().toUpperCase(); if (!status) return false; return ACTIONABLE_STATUSES.has(status); } export function buildIgnoredPassReport(intent: CommentPlanIntent, index: number): Record { return { id: `ignored-pass-${index}`, status: "ignored_pass", failure_code: null, ignored_reason: "pass_status", doc_id: intent.block_id ? undefined : null, block_id: intent.block_id ?? null, comment_key: intent.comment_key ?? null, comment: intent.comment ?? null, reasoning: intent.reasoning ?? null, }; } function makeCommentFailureReport(params: { id: string; commentKey?: string; blockId?: string; status?: string; failureCode: string; message: string; anchor?: CanonicalAnchor | null; opType?: string; }): Record { const { id, commentKey, blockId, status = "not_applied", failureCode, message, anchor, opType } = params; return { id, status, message, paragraph_id: blockId ?? null, match_count: 0, failure_code: failureCode, model_block_id: blockId ?? null, writer_paragraph_id: null, native_para_id: anchor?.native_para_id ?? null, stable_id: anchor?.stable_id ?? null, xml_path: anchor?.xml_path ?? null, comment_key: commentKey ?? null, op_type: opType ?? null, bookmark_name: anchor?.bookmark_name ?? null, }; } function verificationRowFromReport(params: { toolCallId: string; docId?: string; documentId?: string; blockId?: string; commentKey?: string; report: Record; comment?: string; reasoning?: { clause_section: string; violated_standard: string; zamil_exposure: string }; anchorText?: string; sectionHeading?: string | null; opType?: string; bookmarkName?: string | null; }): CommentVerificationRow { const { toolCallId, docId, documentId, blockId, commentKey, report, comment, reasoning, anchorText, sectionHeading, opType, bookmarkName } = params; const status = typeof report.status === "string" ? report.status : "not_applied"; return { tool_call_id: toolCallId, ok: status === "applied", doc_id: docId, document_id: documentId, block_id: blockId, comment_key: typeof report.comment_key === "string" && report.comment_key ? report.comment_key : commentKey, status, failure_code: typeof report.failure_code === "string" ? report.failure_code : undefined, message: typeof report.message === "string" ? report.message : undefined, model_block_id: typeof report.model_block_id === "string" ? report.model_block_id : undefined, writer_paragraph_id: typeof report.writer_paragraph_id === "string" ? report.writer_paragraph_id : undefined, comment, reasoning, anchor_text: anchorText, section_heading: sectionHeading, op_type: opType ?? (typeof report.op_type === "string" ? report.op_type : undefined), bookmark_name: bookmarkName ?? (typeof report.bookmark_name === "string" ? report.bookmark_name : null) ?? null, }; } export function buildApplySummary(applyReport: any[]): DocEditedResult["apply_summary"] { const uniqueReports = Array.from( applyReport .reduce((acc: Map, report: any) => { const key = report?.comment_key || `${report?.model_block_id ?? report?.paragraph_id ?? "unknown"}:${report?.id ?? "unknown"}`; const existing = acc.get(key); if (!existing || existing.status !== "applied") { acc.set(key, report); } return acc; }, new Map()) .values(), ); const actionableReports = uniqueReports.filter((r: any) => r.status !== "ignored_pass"); const applied = actionableReports.filter((r: any) => r.status === "applied").length; const failed = actionableReports.length - applied; return { total: actionableReports.length, applied, failed, failures: actionableReports .filter((r: any) => r.status !== "applied") .map((r: any) => ({ id: String(r.id ?? ""), target_text: String(r.message ?? "").slice(0, 80), message: String(r.message || "Comment was not inserted."), paragraph_id: r.model_block_id ?? r.paragraph_id ?? null, })), }; } function recordCommentAttempt(params: { state?: CommentAttemptState; commentKey?: string; toolCallId: string; report: Record; readSerial?: number; }): void { const { state, commentKey, toolCallId, report, readSerial } = params; if (!state || !commentKey) return; const existing = state.get(commentKey) ?? { attempts: 0, applied: false, staleFailures: 0, toolCallIds: [], }; existing.attempts += 1; existing.toolCallIds.push(toolCallId); if (report.status === "applied") { existing.applied = true; existing.latestFailureCode = undefined; existing.latestMessage = String(report.message ?? ""); } else { const failureCode = String(report.failure_code ?? "not_applied"); existing.latestFailureCode = failureCode; existing.latestMessage = String(report.message ?? ""); if (STALE_ANCHOR_FAILURE_CODES.has(failureCode)) { existing.staleFailures += 1; existing.latestFailureReadSerial = readSerial; } } state.set(commentKey, existing); } function validatePreparedCommentAnchor(params: { prepared: PreparedCommentCall; manifest: AnchorManifest | undefined; currentSurface: Awaited>; commentAttemptState?: CommentAttemptState; }): { ok: true; anchor: CanonicalAnchor; readSerial: number } | { ok: false; report: Record; readSerial?: number } { const { prepared, manifest, currentSurface, commentAttemptState } = params; const commentKey = prepared.args.comment_key; const opType = prepared.op_type; const previousAttempt = commentAttemptState?.get(commentKey); if ( previousAttempt && !previousAttempt.applied && STALE_ANCHOR_FAILURE_CODES.has(previousAttempt.latestFailureCode ?? "") && previousAttempt.staleFailures >= 2 ) { return { ok: false, readSerial: manifest?.readSerial, report: makeCommentFailureReport({ id: prepared.tc.id, commentKey, blockId: prepared.args.block_id, failureCode: "DUPLICATE_COMMENT_RETRY_BLOCKED", message: `This same comment has already failed twice with anchor_stale at ${prepared.args.block_id}. ` + "It will not be retried again in this turn.", anchor: manifest?.anchors.get(prepared.args.block_id), opType, }), }; } if ( previousAttempt && !previousAttempt.applied && STALE_ANCHOR_FAILURE_CODES.has(previousAttempt.latestFailureCode ?? "") && previousAttempt.staleFailures >= 1 && (!manifest || (previousAttempt.latestFailureReadSerial !== undefined && manifest.readSerial <= previousAttempt.latestFailureReadSerial)) ) { return { ok: false, readSerial: manifest?.readSerial, report: makeCommentFailureReport({ id: prepared.tc.id, commentKey, blockId: prepared.args.block_id, failureCode: "anchor_stale_requires_document_refresh", message: `This same comment already failed with anchor_stale at ${prepared.args.block_id}. ` + "Call read_document and choose a fresh block_id before retrying.", anchor: manifest?.anchors.get(prepared.args.block_id), opType, }), }; } if (!manifest) { return { ok: false, report: makeCommentFailureReport({ id: prepared.tc.id, commentKey, blockId: prepared.args.block_id, failureCode: "anchor_revision_stale", message: "No current anchor manifest exists for this document in the turn. " + "Call read_document before inserting comments.", opType, }), }; } if (manifest.docRevisionId !== currentSurface.surfaceId) { return { ok: false, readSerial: manifest.readSerial, report: makeCommentFailureReport({ id: prepared.tc.id, commentKey, blockId: prepared.args.block_id, failureCode: "anchor_revision_stale", message: "The document revision has changed since the model read it. " + "Call read_document again and retry with a block_id from the latest surface.", anchor: manifest.anchors.get(prepared.args.block_id), opType, }), }; } const manifestAnchor = manifest.anchors.get(prepared.args.block_id); if (!manifestAnchor) { return { ok: false, readSerial: manifest.readSerial, report: makeCommentFailureReport({ id: prepared.tc.id, commentKey, blockId: prepared.args.block_id, failureCode: "anchor_not_found", message: `Block ${prepared.args.block_id} is not in the latest document surface shown to the model. ` + "Call read_document and choose a valid block_id.", opType, }), }; } if (!manifestAnchor.commentable) { return { ok: false, readSerial: manifest.readSerial, report: makeCommentFailureReport({ id: prepared.tc.id, commentKey, blockId: prepared.args.block_id, failureCode: "anchor_uncommentable", message: `Block ${prepared.args.block_id} has no visible non-deleted text and cannot be safely commented.`, anchor: manifestAnchor, opType, }), }; } const currentAnchor = currentSurface.anchors.find((a) => a.block_id === prepared.args.block_id); if (!currentAnchor) { return { ok: false, readSerial: manifest.readSerial, report: makeCommentFailureReport({ id: prepared.tc.id, commentKey, blockId: prepared.args.block_id, failureCode: "anchor_stale_requires_document_refresh", message: `Block ${prepared.args.block_id} no longer exists in the current document. ` + "Call read_document and retry with a fresh block_id.", anchor: manifestAnchor, opType, }), }; } if (!manifestAnchor.anchor_eligible || !currentAnchor.anchor_eligible) { const region = navigationRegionForBlock( currentSurface, prepared.args.block_id, ); const eligibleCandidates = buildEligibleAnchorCandidates({ intent: { block_id: prepared.args.block_id, comment: prepared.args.comment, target_text: prepared.args.target_text, }, surface: currentSurface, rejectedAnchor: currentAnchor, }); return { ok: false, readSerial: manifest.readSerial, report: { ...makeCommentFailureReport({ id: prepared.tc.id, commentKey, blockId: prepared.args.block_id, failureCode: "ANCHOR_NOT_ELIGIBLE", message: `Block ${prepared.args.block_id} is in a navigation region and cannot be used for comments or edits.`, anchor: currentAnchor, opType, }), anchor_rejection_reason: currentAnchor.anchor_rejection_reason ?? manifestAnchor.anchor_rejection_reason ?? "navigation", excluded_navigation_region: region, eligible_candidates: eligibleCandidates, }, }; } const identityMismatch = (manifestAnchor.native_para_id || currentAnchor.native_para_id) && manifestAnchor.native_para_id !== currentAnchor.native_para_id ? "native_para_id" : manifestAnchor.stable_id !== currentAnchor.stable_id ? "stable_id" : manifestAnchor.xml_path !== currentAnchor.xml_path ? "xml_path" : ""; if (identityMismatch) { return { ok: false, readSerial: manifest.readSerial, report: makeCommentFailureReport({ id: prepared.tc.id, commentKey, blockId: prepared.args.block_id, failureCode: "anchor_identity_mismatch", message: `Block ${prepared.args.block_id} has a ${identityMismatch} mismatch between the read surface and current document. ` + "Call read_document and retry with a fresh block_id.", anchor: currentAnchor, opType, }), }; } const textMismatch = manifestAnchor.text_hash !== currentAnchor.text_hash || manifestAnchor.prev_text_hash !== currentAnchor.prev_text_hash || manifestAnchor.next_text_hash !== currentAnchor.next_text_hash; if (textMismatch) { return { ok: false, readSerial: manifest.readSerial, report: { ...makeCommentFailureReport({ id: prepared.tc.id, commentKey, blockId: prepared.args.block_id, failureCode: "anchor_stale_requires_document_refresh", message: `Block ${prepared.args.block_id} text or neighbor hash changed since the model read it. ` + "Call read_document and retry with a fresh block_id.", anchor: currentAnchor, opType, }), expected_text_hash: manifestAnchor.text_hash, actual_text_hash: currentAnchor.text_hash, }, }; } if (!currentAnchor.commentable) { return { ok: false, readSerial: manifest.readSerial, report: makeCommentFailureReport({ id: prepared.tc.id, commentKey, blockId: prepared.args.block_id, failureCode: "anchor_uncommentable", message: `Block ${prepared.args.block_id} has no visible non-deleted text in the current document.`, anchor: currentAnchor, opType, }), }; } return { ok: true, anchor: currentAnchor, readSerial: manifest.readSerial }; } // --------------------------------------------------------------------------- // Plan / correct comment pipeline (best-of-both-worlds) // --------------------------------------------------------------------------- function normalizeAnchorCandidateText(value: string): string { return decodeXmlEntities(value) .toLowerCase() .replace(/\u00a0/g, " ") .replace(/[^\p{L}\p{N}]+/gu, " ") .replace(/\s+/g, " ") .trim(); } function buildEligibleAnchorCandidates(params: { intent: CommentPlanIntent; surface: Awaited>; rejectedAnchor: CanonicalAnchor; limit?: number; }): AnchorCorrectionCandidate[] { const { intent, surface, rejectedAnchor, limit = 3 } = params; const rejectedIndex = surface.anchors.findIndex( (anchor) => anchor.block_id === rejectedAnchor.block_id, ); const targetText = normalizeAnchorCandidateText(intent.target_text ?? ""); const navigationLabel = normalizeAnchorCandidateText( rejectedAnchor.navigation_label ?? rejectedAnchor.text_preview, ); const clauseSection = normalizeAnchorCandidateText( intent.reasoning?.clause_section ?? "", ); const scored = surface.anchors .map((anchor, index) => { if (!anchor.anchor_eligible || !anchor.commentable) return null; const fullText = surface.blocks[index]?.text ?? anchor.text_preview; const text = normalizeAnchorCandidateText(fullText); let score = 0; const reasons: string[] = []; if (targetText.length >= 8 && text.includes(targetText)) { score += 120; reasons.push("supporting quote match"); } if (navigationLabel.length >= 3) { if (text === navigationLabel) { score += 100; reasons.push("exact normalized title match"); } else if ( text.startsWith(navigationLabel) || navigationLabel.startsWith(text) ) { score += 70; reasons.push("title prefix match"); } else if ( text.includes(navigationLabel) || navigationLabel.includes(text) ) { score += 45; reasons.push("partial title match"); } } if ( clauseSection.length >= 5 && (text.includes(clauseSection) || clauseSection.includes(text)) ) { score += 35; reasons.push("reasoning section match"); } if (rejectedIndex >= 0) { const distance = Math.abs(index - rejectedIndex); score += Math.max(0, 12 - Math.min(12, distance)); if (index > rejectedIndex && distance <= 12) { reasons.push("near rejected navigation entry"); } } return { block_id: anchor.block_id, text: anchor.text_preview.slice(0, 200), score, match_reason: reasons.join(", ") || "nearest eligible body block", index, }; }) .filter( ( candidate, ): candidate is AnchorCorrectionCandidate & { index: number } => candidate !== null, ) .sort((a, b) => b.score - a.score || a.index - b.index); const matched = scored.filter((candidate) => candidate.score >= 20); const candidates = matched.length > 0 ? matched : scored; return candidates.slice(0, limit).map(({ index: _index, ...candidate }) => candidate); } function navigationRegionForBlock( surface: Awaited>, blockId: string, ): { start_block_id: string; end_block_id: string; reason: string; } | undefined { const blockIndex = surface.anchors.findIndex( (anchor) => anchor.block_id === blockId, ); if (blockIndex < 0) return undefined; const region = surface.navigationRegions.find( (candidate) => blockIndex >= candidate.startIndex && blockIndex <= candidate.endIndex, ); if (!region) return undefined; return { start_block_id: region.startBlockId, end_block_id: region.endBlockId, reason: region.reason, }; } type AnchorRedirect = { from: string; to: string; reason: string; }; function tryHighConfidenceNavigationRedirect(params: { intent: CommentPlanIntent; surface: Awaited>; }): AnchorRedirect | null { const { intent, surface } = params; const rejectedIndex = surface.anchors.findIndex( (anchor) => anchor.block_id === intent.block_id, ); if (rejectedIndex < 0) return null; const rejectedAnchor = surface.anchors[rejectedIndex]; if (rejectedAnchor.anchor_eligible) return null; const title = normalizeAnchorCandidateText( rejectedAnchor.navigation_label ?? rejectedAnchor.text_preview, ); const targetText = normalizeAnchorCandidateText(intent.target_text ?? ""); if (title.length < 3 || targetText.length < 12) return null; const matches = surface.anchors .map((anchor, index) => { if (!anchor.anchor_eligible || !anchor.commentable) return null; const blockText = normalizeAnchorCandidateText( surface.blocks[index]?.text ?? anchor.text_preview, ); const titleMatches = blockText === title || blockText.startsWith(`${title} `); const quoteMatches = blockText.includes(targetText); return titleMatches && quoteMatches ? anchor : null; }) .filter((anchor): anchor is CanonicalAnchor => anchor !== null); if (matches.length !== 1) return null; return { from: rejectedAnchor.block_id, to: matches[0].block_id, reason: "unique eligible title and supporting-quote match", }; } function isLikelyHeadingBlock(params: { surface: Awaited>; blockIndex: number; normalizedBlockText: string; }): boolean { const { surface, blockIndex, normalizedBlockText } = params; const anchor = surface.anchors[blockIndex]; const style = `${anchor?.style_id ?? ""} ${anchor?.style_name ?? ""}`.toLowerCase(); const rawText = surface.blocks[blockIndex]?.text.trim() ?? ""; const headingShape = /^(section|clause|article|schedule|exhibit|annex|appendix)\s+\d+/i.test( rawText, ) || /^\d+(?:\.\d+)*\s+\S+/.test(rawText); const styledHeading = style.includes("heading") || style.includes("title") || style.includes("subtitle"); if (!headingShape && !styledHeading) return false; const nextSubstantive = surface.anchors .slice(blockIndex + 1) .findIndex((candidate) => candidate.anchor_eligible && candidate.commentable); if (nextSubstantive < 0) return false; const actualNextIndex = blockIndex + 1 + nextSubstantive; const nextText = normalizeAnchorCandidateText( surface.blocks[actualNextIndex]?.text ?? "", ); return normalizedBlockText.length <= 180 && nextText.length >= 80; } export function validateFindingTargetEvidence(params: { intent: CommentPlanIntent; surface: Awaited>; anchor: CanonicalAnchor; }): | { ok: true } | { ok: false; failureCode: string; message: string } { const { intent, surface, anchor } = params; if (intent.status?.trim().toUpperCase() === "MISSING") return { ok: true }; const targetText = normalizeAnchorCandidateText(intent.target_text ?? ""); if (targetText.length < 12) { return { ok: false, failureCode: intent.target_text?.trim() ? "target_text_too_short" : "schema_missing_target_text", message: "Non-MISSING findings require target_text containing at least 12 meaningful characters quoted from the selected block.", }; } const blockIndex = surface.anchors.findIndex( (candidate) => candidate.block_id === anchor.block_id, ); const blockText = normalizeAnchorCandidateText( blockIndex >= 0 ? surface.blocks[blockIndex]?.text ?? "" : "", ); if (!blockText.includes(targetText)) { return { ok: false, failureCode: "target_text_not_in_block", message: `target_text does not occur in block ${anchor.block_id}. ` + "Provide corrected_target_text from that block or choose a different eligible block_id.", }; } if ( blockText === targetText && isLikelyHeadingBlock({ surface, blockIndex, normalizedBlockText: blockText, }) ) { return { ok: false, failureCode: "target_text_heading_only", message: `target_text for block ${anchor.block_id} contains only a section heading. ` + "Anchor the finding on the substantive clause paragraph, or use MISSING when the clause is absent.", }; } return { ok: true }; } export function validateCommentPlanIntent(params: { intent: CommentPlanIntent; manifest: AnchorManifest | undefined; currentSurface: Awaited>; }): | { ok: true; anchor: CanonicalAnchor; readSerial: number } | { ok: false; failureCode: string; message: string; readSerial?: number; candidates?: AnchorCorrectionCandidate[]; rejectionReason?: string; excludedNavigationRegion?: { start_block_id: string; end_block_id: string; reason: string; }; } { const { intent, manifest, currentSurface } = params; const blockId = intent.block_id; if (!manifest) { return { ok: false, failureCode: "anchor_revision_stale", message: "No current anchor manifest exists for this document in the turn. Call read_document before inserting comments.", }; } if (manifest.docRevisionId !== currentSurface.surfaceId) { return { ok: false, readSerial: manifest.readSerial, failureCode: "anchor_revision_stale", message: "The document revision has changed since the model read it. Call read_document again and retry with a block_id from the latest surface.", }; } const manifestAnchor = manifest.anchors.get(blockId); if (!manifestAnchor) { return { ok: false, readSerial: manifest.readSerial, failureCode: "anchor_not_found", message: `Block ${blockId} is not in the latest document surface shown to the model. Call read_document and choose a valid block_id.`, }; } if (!manifestAnchor.commentable) { return { ok: false, readSerial: manifest.readSerial, failureCode: "anchor_uncommentable", message: `Block ${blockId} has no visible non-deleted text and cannot be safely commented.`, }; } const currentAnchor = currentSurface.anchors.find((a) => a.block_id === blockId); if (!currentAnchor) { return { ok: false, readSerial: manifest.readSerial, failureCode: "anchor_stale_requires_document_refresh", message: `Block ${blockId} no longer exists in the current document. Call read_document and retry with a fresh block_id.`, }; } if (!manifestAnchor.anchor_eligible || !currentAnchor.anchor_eligible) { const candidates = buildEligibleAnchorCandidates({ intent, surface: currentSurface, rejectedAnchor: currentAnchor, }); const candidateIds = candidates.map((candidate) => candidate.block_id).join(", "); const excludedNavigationRegion = navigationRegionForBlock( currentSurface, blockId, ); return { ok: false, readSerial: manifest.readSerial, failureCode: "ANCHOR_NOT_ELIGIBLE", message: `Block ${blockId} is in a navigation region` + `${currentAnchor.anchor_rejection_reason ? ` (${currentAnchor.anchor_rejection_reason})` : ""} ` + "and cannot be used for comments or edits. " + (candidateIds ? `Choose an eligible body block, such as: ${candidateIds}.` : "Use find_in_document to choose an eligible body block."), candidates, rejectionReason: currentAnchor.anchor_rejection_reason ?? manifestAnchor.anchor_rejection_reason ?? "navigation", excludedNavigationRegion, }; } const identityMismatch = (manifestAnchor.native_para_id || currentAnchor.native_para_id) && manifestAnchor.native_para_id !== currentAnchor.native_para_id ? "native_para_id" : manifestAnchor.stable_id !== currentAnchor.stable_id ? "stable_id" : manifestAnchor.xml_path !== currentAnchor.xml_path ? "xml_path" : ""; if (identityMismatch) { return { ok: false, readSerial: manifest.readSerial, failureCode: "anchor_identity_mismatch", message: `Block ${blockId} has a ${identityMismatch} mismatch between the read surface and current document. Call read_document and retry with a fresh block_id.`, }; } const textMismatch = manifestAnchor.text_hash !== currentAnchor.text_hash || manifestAnchor.prev_text_hash !== currentAnchor.prev_text_hash || manifestAnchor.next_text_hash !== currentAnchor.next_text_hash; if (textMismatch) { return { ok: false, readSerial: manifest.readSerial, failureCode: "anchor_stale_requires_document_refresh", message: `Block ${blockId} text or neighbor hash changed since the model read it. Call read_document and retry with a fresh block_id.`, }; } if (!currentAnchor.commentable) { return { ok: false, readSerial: manifest.readSerial, failureCode: "anchor_uncommentable", message: `Block ${blockId} has no visible non-deleted text in the current document.`, }; } return { ok: true, anchor: currentAnchor, readSerial: manifest.readSerial }; } export function buildCandidateContext(params: { surface: Awaited>; targetBlockId: string; windowSize?: number; }): Array<{ block_id: string; text: string }> { const { surface, targetBlockId, windowSize = 5 } = params; const idx = surface.anchors.findIndex((a) => a.block_id === targetBlockId); if (idx < 0) return []; const start = Math.max(0, idx - windowSize); const end = Math.min(surface.anchors.length, idx + windowSize + 1); return surface.anchors .slice(start, end) .filter((anchor) => anchor.anchor_eligible && anchor.commentable) .map((anchor) => ({ block_id: anchor.block_id, text: anchor.text_preview.slice(0, 200), })); } function normalizeFindingType(value: unknown): CommentPlanIntent["type"] { const raw = typeof value === "string" ? value.trim() : ""; if ( raw === "COMMENT" || raw === "REWRITE_PARAGRAPH" || raw === "INSERT" || raw === "DELETE_PARAGRAPH" ) { return raw; } return undefined; } function isTrackedChangeOperation(intent: CommentPlanIntent | PlannedCommentItem): boolean { return intent.type === "REWRITE_PARAGRAPH" || intent.type === "INSERT" || intent.type === "DELETE_PARAGRAPH"; } function downgradeEditIntentToComment(intent: PlannedCommentItem, reason: string): PlannedCommentItem { const existingComment = intent.comment?.trim() || "[REVIEW] Issue preserved as a comment."; const systemNote = `[System note: The model attempted an edit operation, but the document was reviewed in comments-only mode. ` + `Preserved as a comment instead. Reason: ${reason}]`; const next: PlannedCommentItem = { ...intent, type: "COMMENT", comment: existingComment.includes("[System note:") ? existingComment : `${existingComment}\n\n${systemNote}`, downgradeToComment: true, fallbackApplied: true, }; delete (next as any).new_text; delete (next as any).insert_text; delete (next as any).position; delete (next as any).rewrite_mode; delete (next as any).old_span; delete (next as any).new_span; return next; } function isRepairableAnchorFailure(failureCode: string): boolean { return failureCode === "ANCHOR_NOT_ELIGIBLE" || failureCode === "anchor_not_found" || failureCode === "anchor_invalid_format" || failureCode === "anchor_uncommentable" || failureCode === "anchor_revision_stale" || failureCode === "anchor_identity_mismatch" || failureCode === "anchor_stale_requires_document_refresh"; } function hasValidReasoning( reasoning: CommentPlanIntent["reasoning"], ): reasoning is NonNullable { return !!reasoning && typeof reasoning === "object" && !!reasoning.clause_section?.trim() && !!reasoning.violated_standard?.trim() && !!reasoning.zamil_exposure?.trim(); } function terminalFailure( intent: CommentPlanIntent, failureCode: string, message: string, repairKind: RepairKind = "base_schema", ): PlanValidationResult { return { ok: false, item: { ...intent, failureCode, message, repairKind, repairable: false, }, failureCode, message, repairable: false, repairKind, }; } function repairableFailure(params: { intent: CommentPlanIntent; failureCode: string; relatedFailureCodes?: string[]; message: string; repairKind: RepairKind; terminalFallback?: TerminalFallback; anchor?: CanonicalAnchor; anchorCandidates?: AnchorCorrectionCandidate[]; anchorRejectionReason?: string; excludedNavigationRegion?: { start_block_id: string; end_block_id: string; reason: string; }; anchorRedirect?: AnchorRedirect; }): PlanValidationResult { const item: PlannedCommentItem = { ...params.intent, anchor: params.anchor, failureCode: params.failureCode, relatedFailureCodes: params.relatedFailureCodes, message: params.message, repairKind: params.repairKind, repairable: true, repairAttempts: 0, terminalFallback: params.terminalFallback, anchorCandidates: params.anchorCandidates, anchorRejectionReason: params.anchorRejectionReason, excludedNavigationRegion: params.excludedNavigationRegion, anchorRedirectedFrom: params.anchorRedirect?.from, anchorRedirectedTo: params.anchorRedirect?.to, anchorRedirectReason: params.anchorRedirect?.reason, }; return { ok: false, item, failureCode: params.failureCode, relatedFailureCodes: params.relatedFailureCodes, message: params.message, repairable: true, repairKind: params.repairKind, terminalFallback: params.terminalFallback, }; } function validateBaseFindingFields(intent: CommentPlanIntent): PlanValidationResult | null { const missingFields: string[] = []; if (!intent.comment_key?.trim()) missingFields.push("comment_key"); if (!intent.block_id?.trim()) missingFields.push("block_id"); if (!intent.comment?.trim()) missingFields.push("comment"); if (!intent.status || !isActionableStatus(intent)) missingFields.push("status"); if (!hasValidReasoning(intent.reasoning)) missingFields.push("reasoning"); if (missingFields.length === 0) return null; if (!intent.comment_key?.trim()) { return terminalFailure( intent, "schema_missing_comment_key", `Missing required field(s): ${missingFields.join(", ")}. Cannot repair without comment_key because corrections are matched by comment_key.`, "base_schema", ); } const hasEnoughIdentityForRepair = !!intent.comment_key?.trim() && (!!intent.block_id?.trim() || !!intent.comment?.trim()); if (!hasEnoughIdentityForRepair) { return terminalFailure( intent, "schema_insufficient_identity", `Missing required field(s): ${missingFields.join(", ")}. The item does not have enough identity to repair safely.`, "base_schema", ); } const primaryFailure = missingFields.includes("block_id") ? "schema_missing_block_id" : missingFields.includes("comment") ? "schema_missing_comment" : missingFields.includes("status") ? "schema_missing_or_invalid_status" : "schema_missing_or_invalid_reasoning"; const repairKind: RepairKind = missingFields.includes("block_id") ? "anchor" : "base_schema"; return repairableFailure({ intent, failureCode: primaryFailure, relatedFailureCodes: missingFields.map((field) => `schema_missing_${field}`), message: `Missing or invalid required field(s): ${missingFields.join(", ")}. ` + "Provide corrected_comment, corrected_status, corrected_reasoning, and/or new_block_id as applicable.", repairKind, }); } function validateOperationFields(intent: CommentPlanIntent): PlanValidationResult | null { const opType = intent.type ?? "COMMENT"; if ( opType !== "COMMENT" && opType !== "REWRITE_PARAGRAPH" && opType !== "INSERT" && opType !== "DELETE_PARAGRAPH" ) { return repairableFailure({ intent, failureCode: "schema_invalid_operation_type", message: `Unsupported operation type '${opType}'. ` + "Provide corrected_type as COMMENT, REWRITE_PARAGRAPH, INSERT, or DELETE_PARAGRAPH.", repairKind: "operation_schema", terminalFallback: "COMMENT", }); } if (opType === "REWRITE_PARAGRAPH") { if (intent.rewrite_mode === "span") { if (!intent.old_span?.trim() || !intent.new_span?.trim()) { return repairableFailure({ intent, failureCode: "schema_missing_rewrite_span", relatedFailureCodes: [ !intent.old_span?.trim() ? "schema_missing_old_span" : "", !intent.new_span?.trim() ? "schema_missing_new_span" : "", ].filter(Boolean), message: "REWRITE_PARAGRAPH with rewrite_mode=span requires old_span and new_span.", repairKind: "operation_schema", terminalFallback: "COMMENT", }); } } else if (!intent.new_text?.trim()) { return repairableFailure({ intent, failureCode: "schema_missing_new_text", message: "REWRITE_PARAGRAPH requires new_text.", repairKind: "operation_schema", terminalFallback: "COMMENT", }); } } if (opType === "INSERT" && !intent.insert_text?.trim()) { return repairableFailure({ intent, failureCode: "schema_missing_insert_text", message: "INSERT requires insert_text.", repairKind: "operation_schema", terminalFallback: "COMMENT", }); } return null; } function isRedZoneEditOperation(params: { intent: CommentPlanIntent; anchor?: CanonicalAnchor; }): { blocked: true; failureCode: string; message: string } | { blocked: false } { const opType = params.intent.type ?? "COMMENT"; if (opType === "COMMENT") return { blocked: false }; const anchor = params.anchor; if (!anchor) return { blocked: false }; const redZoneStyles = ["TOC", "TOC1", "TOC2", "TOC3", "TOC4", "TOCHeading", "TOCEntry"]; const redZoneKeywords = ["table of contents", "cover page", "signature page", "execution page"]; const styleId = `${String((anchor as any).style_id ?? "")} ${String((anchor as any).style_name ?? "")}`.toLowerCase(); const textPreview = String((anchor as any).text_preview ?? "").toLowerCase(); if (redZoneStyles.some((style) => styleId.includes(style.toLowerCase()))) { return { blocked: true, failureCode: "red_zone_toc", message: "Tracked edit operations on Table of Contents blocks are not allowed.", }; } if (redZoneKeywords.some((keyword) => textPreview.includes(keyword))) { return { blocked: true, failureCode: "red_zone_signature_cover", message: "Tracked edit operations on cover/signature/execution page sections are not allowed.", }; } return { blocked: false }; } export function validatePlannedFinding(params: { intent: CommentPlanIntent; manifest: AnchorManifest | undefined; currentSurface: Awaited>; allowCommentFallback: boolean; }): PlanValidationResult { const { intent, manifest, currentSurface, allowCommentFallback } = params; const baseFailure = validateBaseFindingFields(intent); if (baseFailure) return baseFailure; const operationFailure = validateOperationFields(intent); let anchorFailure: | { failureCode: string; message: string; candidates?: AnchorCorrectionCandidate[]; rejectionReason?: string; excludedNavigationRegion?: { start_block_id: string; end_block_id: string; reason: string; }; } | null = null; let anchor: CanonicalAnchor | undefined; let anchorRedirect: AnchorRedirect | null = null; if (!intent.block_id?.trim()) { anchorFailure = { failureCode: "schema_missing_block_id", message: "Missing block_id. Provide new_block_id from the latest document surface.", }; } else { intent.block_id = normalizeModelBlockId(intent.block_id); if (!isValidDocxModelBlockId(intent.block_id)) { anchorFailure = { failureCode: "anchor_invalid_format", message: `Block id '${intent.block_id}' is not a valid P_NNNN or BMK_P_/BMK_T_ bookmark format.`, }; } else { anchorRedirect = tryHighConfidenceNavigationRedirect({ intent, surface: currentSurface, }); if (anchorRedirect) { intent.block_id = anchorRedirect.to; } const anchorValidation = validateCommentPlanIntent({ intent, manifest, currentSurface, }); if (anchorValidation.ok) { anchor = anchorValidation.anchor; } else { anchorFailure = { failureCode: anchorValidation.failureCode, message: anchorValidation.message, candidates: anchorValidation.candidates, rejectionReason: anchorValidation.rejectionReason, excludedNavigationRegion: anchorValidation.excludedNavigationRegion, }; } } } if (operationFailure && !operationFailure.ok && anchorFailure) { const relatedFailureCodes = [ operationFailure.failureCode, ...(operationFailure.relatedFailureCodes ?? []), anchorFailure.failureCode, ]; return repairableFailure({ intent, failureCode: operationFailure.failureCode, relatedFailureCodes, message: `${operationFailure.message} Also fix anchor issue: ${anchorFailure.message}`, repairKind: "operation_schema", terminalFallback: operationFailure.terminalFallback, anchor, anchorCandidates: anchorFailure.candidates, anchorRejectionReason: anchorFailure.rejectionReason, excludedNavigationRegion: anchorFailure.excludedNavigationRegion, anchorRedirect: anchorRedirect ?? undefined, }); } if (operationFailure && !operationFailure.ok) { return repairableFailure({ intent, failureCode: operationFailure.failureCode, relatedFailureCodes: operationFailure.relatedFailureCodes, message: operationFailure.message, repairKind: operationFailure.repairKind, terminalFallback: operationFailure.terminalFallback, anchor, anchorRedirect: anchorRedirect ?? undefined, }); } if (anchorFailure) { return repairableFailure({ intent, failureCode: anchorFailure.failureCode, message: anchorFailure.message, repairKind: "anchor", terminalFallback: undefined, anchor, anchorCandidates: anchorFailure.candidates, anchorRejectionReason: anchorFailure.rejectionReason, excludedNavigationRegion: anchorFailure.excludedNavigationRegion, anchorRedirect: anchorRedirect ?? undefined, }); } const evidenceValidation = validateFindingTargetEvidence({ intent, surface: currentSurface, anchor: anchor!, }); if (!evidenceValidation.ok) { return repairableFailure({ intent, failureCode: evidenceValidation.failureCode, message: evidenceValidation.message, repairKind: "evidence", anchor, anchorRedirect: anchorRedirect ?? undefined, }); } const redZone = isRedZoneEditOperation({ intent, anchor }); if (redZone.blocked) { return repairableFailure({ intent, failureCode: redZone.failureCode, message: redZone.message, repairKind: "red_zone", terminalFallback: allowCommentFallback ? "COMMENT" : undefined, anchor, anchorRedirect: anchorRedirect ?? undefined, }); } return { ok: true, item: { ...intent, anchor, repairable: false, anchorRedirectedFrom: anchorRedirect?.from, anchorRedirectedTo: anchorRedirect?.to, anchorRedirectReason: anchorRedirect?.reason, }, }; } function downgradeMalformedEditToComment( intent: PlannedCommentItem, reason: string, ): PlannedCommentItem { const existingComment = intent.comment?.trim() || "[REVIEW] Issue preserved as a comment."; const systemNote = `[System note: The model attempted an edit operation but the edit payload could not be safely applied. ` + `Preserved as a comment instead. Original validation error: ${reason}]`; const next: PlannedCommentItem = { ...intent, type: "COMMENT", comment: existingComment.includes("[System note:") ? existingComment : `${existingComment}\n\n${systemNote}`, new_text: undefined, insert_text: undefined, position: undefined, rewrite_mode: undefined, old_span: undefined, new_span: undefined, failureCode: undefined, relatedFailureCodes: undefined, message: undefined, repairKind: intent.repairKind, repairable: false, terminalFallback: undefined, downgradeToComment: true, fallbackApplied: true, }; delete (next as any).new_text; delete (next as any).insert_text; delete (next as any).position; delete (next as any).rewrite_mode; delete (next as any).old_span; delete (next as any).new_span; return next; } function applyFindingCorrection( original: PlannedCommentItem, correction: FindingCorrection, ): PlannedCommentItem { const next: PlannedCommentItem = { ...original, repairAttempts: (original.repairAttempts ?? 0) + 1, }; if (correction.new_block_id) next.block_id = correction.new_block_id; if (correction.corrected_comment) next.comment = correction.corrected_comment; if (correction.corrected_status) next.status = correction.corrected_status; if (correction.corrected_reasoning) next.reasoning = correction.corrected_reasoning; if (correction.corrected_target_text) { next.target_text = correction.corrected_target_text; } if (correction.status === "downgrade_to_comment") { return downgradeMalformedEditToComment( next, correction.reason || next.message || "Model requested safe downgrade to COMMENT.", ); } if (correction.corrected_type) next.type = correction.corrected_type; if (correction.corrected_new_text) next.new_text = correction.corrected_new_text; if (correction.corrected_insert_text) next.insert_text = correction.corrected_insert_text; if (correction.corrected_position) next.position = correction.corrected_position; if (correction.corrected_rewrite_mode) next.rewrite_mode = correction.corrected_rewrite_mode; if (correction.corrected_old_span) next.old_span = correction.corrected_old_span; if (correction.corrected_new_span) next.new_span = correction.corrected_new_span; return next; } function buildFallbackCandidateContext(params: { surface: Awaited>; targetBlockId: string; }): Array<{ block_id: string; text: string }> { const direct = buildCandidateContext({ surface: params.surface, targetBlockId: params.targetBlockId, windowSize: 5, }); if (direct.length > 0) return direct; return params.surface.anchors .filter((anchor) => anchor.anchor_eligible && anchor.commentable) .slice(0, 20) .map((anchor) => ({ block_id: anchor.block_id, text: anchor.text_preview.slice(0, 200), })); } function buildFindingCorrectionRequest(params: { item: PlannedCommentItem; currentSurface: Awaited>; }): Record { const { item, currentSurface } = params; const failureCodes = new Set([ item.failureCode, ...(item.relatedFailureCodes ?? []), ].filter(Boolean) as string[]); const allowedCorrections: string[] = []; if ( failureCodes.has("schema_missing_block_id") || failureCodes.has("ANCHOR_NOT_ELIGIBLE") || failureCodes.has("anchor_invalid_format") || failureCodes.has("anchor_not_found") || failureCodes.has("anchor_uncommentable") || failureCodes.has("anchor_revision_stale") || failureCodes.has("anchor_identity_mismatch") || failureCodes.has("anchor_stale_requires_document_refresh") ) { allowedCorrections.push("new_block_id"); } if ( failureCodes.has("schema_missing_comment") || failureCodes.has("schema_missing_comment_field") ) { allowedCorrections.push("corrected_comment"); } if ( failureCodes.has("schema_missing_or_invalid_status") || failureCodes.has("schema_missing_status") ) { allowedCorrections.push("corrected_status"); } if ( failureCodes.has("schema_missing_or_invalid_reasoning") || failureCodes.has("schema_missing_reasoning") ) { allowedCorrections.push("corrected_reasoning"); } if ( failureCodes.has("schema_missing_target_text") || failureCodes.has("target_text_too_short") || failureCodes.has("target_text_not_in_block") || failureCodes.has("target_text_heading_only") ) { allowedCorrections.push("corrected_target_text"); allowedCorrections.push("new_block_id + corrected_target_text"); } if (failureCodes.has("schema_invalid_operation_type")) { allowedCorrections.push("corrected_type"); } if (failureCodes.has("schema_missing_new_text")) { allowedCorrections.push("corrected_new_text"); allowedCorrections.push("corrected_rewrite_mode + corrected_old_span + corrected_new_span"); allowedCorrections.push("downgrade_to_comment"); } if ( failureCodes.has("schema_missing_rewrite_span") || failureCodes.has("schema_missing_old_span") || failureCodes.has("schema_missing_new_span") ) { allowedCorrections.push("corrected_old_span + corrected_new_span"); allowedCorrections.push("corrected_new_text"); allowedCorrections.push("downgrade_to_comment"); } if (failureCodes.has("schema_missing_insert_text")) { allowedCorrections.push("corrected_insert_text"); allowedCorrections.push("downgrade_to_comment"); } if (item.repairKind === "red_zone") { allowedCorrections.push("new_block_id"); allowedCorrections.push("downgrade_to_comment"); } if (allowedCorrections.length === 0 && item.terminalFallback === "COMMENT") { allowedCorrections.push("downgrade_to_comment"); } const requiredFix = item.repairKind === "operation_schema" ? "Provide the missing corrected operation payload, or set status='downgrade_to_comment' if you cannot draft the edit safely." : item.repairKind === "anchor" ? "Provide new_block_id from the latest document surface, or set status='unresolved' if no safe anchor exists." : item.repairKind === "evidence" ? "Provide corrected_target_text quoted from the selected substantive block, and change new_block_id too if the finding points to a different clause." : item.repairKind === "base_schema" ? "Provide corrected_comment, corrected_status, and/or corrected_reasoning for the missing finding fields." : item.repairKind === "red_zone" ? "Provide a safe non-red-zone new_block_id, or set status='downgrade_to_comment' to preserve the issue as a comment." : "Repair the finding using the allowed correction fields."; return { comment_key: item.comment_key, failure_code: item.failureCode, related_failure_codes: item.relatedFailureCodes ?? [], repair_kind: item.repairKind ?? "unknown", repair_attempts: item.repairAttempts ?? 0, message: item.message, original_type: item.type ?? "COMMENT", original_block_id: item.block_id, bad_block_id: item.block_id, original_comment: item.comment, original_status: item.status, original_reasoning: item.reasoning, target_text: item.target_text, anchor_rejection_reason: item.anchorRejectionReason, excluded_navigation_region: item.excludedNavigationRegion, required_fix: requiredFix, allowed_corrections: Array.from(new Set(allowedCorrections)), eligible_candidates: item.anchorCandidates && item.anchorCandidates.length > 0 ? item.anchorCandidates : buildFallbackCandidateContext({ surface: currentSurface, targetBlockId: item.block_id, }), }; } function buildFindingDiagnostics(params: { item: PlannedCommentItem; finalItem?: PlannedCommentItem; applyStatus: string; }): Record { const { item, finalItem, applyStatus } = params; return { comment_key: item.comment_key, original_type: item.type ?? "COMMENT", final_type: finalItem?.type ?? item.type ?? "COMMENT", repair_kind: item.repairKind ?? "unknown", repair_attempts: item.repairAttempts ?? 0, failure_code: item.failureCode ?? null, fallback_applied: !!(finalItem?.fallbackApplied ?? item.fallbackApplied), downgrade_to_comment: !!(finalItem?.downgradeToComment ?? item.downgradeToComment), anchor_redirected_from: finalItem?.anchorRedirectedFrom ?? item.anchorRedirectedFrom ?? null, anchor_redirected_to: finalItem?.anchorRedirectedTo ?? item.anchorRedirectedTo ?? null, anchor_redirect_reason: finalItem?.anchorRedirectReason ?? item.anchorRedirectReason ?? null, apply_status: applyStatus, }; } async function applyCommentWriterBatch(params: { userId: string; documentId: string; docLabel: string; docInfo: NonNullable>; current: NonNullable>>; comments: WriterCommentItem[]; db: ReturnType; write: (s: string) => void; trace?: RunTree | null; docIndex?: DocIndex; docStore?: DocStore; anchorManifestState?: AnchorManifestState; docsEdited: DocEditedResult[]; prevalidationFailures?: any[]; toolCallId: string; verificationRows: CommentVerificationRow[]; trackChanges?: boolean; }): Promise<{ ok: boolean; version_id?: string; version_number?: number; download_url?: string; apply_report?: any[]; apply_summary?: DocEditedResult["apply_summary"]; error?: string; }> { const { userId, documentId, docLabel, docInfo, current, comments, db, write, trace, docIndex, docStore, anchorManifestState, docsEdited, prevalidationFailures = [], toolCallId, verificationRows, trackChanges } = params; if (comments.length === 0) { return { ok: false, error: "No validated comments to apply." }; } write(`data: ${JSON.stringify({ type: "doc_edited_start", filename: docInfo.filename, })} `); const batchResult = await runCommentDocumentBatch({ userId, documentId, docLabel, docInfo, current, comments, db, trackChanges, trace, }); const writerReport = batchResult.apply_report ?? []; const reportById = new Map(); for (const report of writerReport) { if (typeof report?.id === "string") reportById.set(report.id, report); } const combinedApplyReport = [...prevalidationFailures, ...writerReport]; // Build lookup from comment_key to original intent data for enriching verification rows const commentMetaByKey = new Map(); for (const c of comments) { commentMetaByKey.set(c.args.comment_key, { comment: c.args.comment, reasoning: c.reasoning, anchor_text: c.anchor_text, section_heading: c.section_heading, op_type: c.args.type, bookmark_name: c.anchor?.bookmark_name ?? null, finding_status: c.finding_status, document_order: c.document_order, source: c.source, amended_wording: c.args.new_span || c.args.new_text || c.args.insert_text || undefined, }); } // Push verification rows for every item in this batch so insertion_verification traces correctly for (const report of combinedApplyReport) { const meta = commentMetaByKey.get(report.comment_key ?? "") ?? {}; const fallbackOpType = stringOrUndefined(report.op_type) ?? "COMMENT"; verificationRows.push({ tool_call_id: toolCallId, ok: report.status === "applied", doc_id: docLabel, document_id: documentId, block_id: report.model_block_id ?? report.paragraph_id ?? null, comment_key: report.comment_key ?? null, status: report.status ?? "not_applied", failure_code: report.failure_code ?? undefined, message: report.message ?? undefined, model_block_id: report.model_block_id ?? undefined, writer_paragraph_id: report.writer_paragraph_id ?? undefined, comment: meta.comment ?? stringOrUndefined(report.comment), reasoning: meta.reasoning ?? workflowReasoningOrUndefined(report.reasoning), anchor_text: meta.anchor_text ?? stringOrUndefined(report.anchor_text), section_heading: meta.section_heading ?? stringOrNull(report.section_heading), op_type: meta.op_type ?? fallbackOpType, bookmark_name: meta.bookmark_name ?? stringOrNull(report.bookmark_name), finding_status: meta.finding_status ?? workflowFindingStatusOrUndefined(report.finding_status), document_order: meta.document_order ?? numberOrUndefined(report.document_order), apply_status: report.apply_status ?? (report.status === "applied" ? "applied" : "writer_failed"), source: meta.source ?? workflowSourceOrUndefined(report.source), amended_wording: meta.amended_wording, }); } if (batchResult.ok && batchResult.download_url && batchResult.version_id) { if (docIndex?.[docLabel]) { docIndex[docLabel] = { ...docIndex[docLabel], version_id: batchResult.version_id, version_number: batchResult.version_number ?? null, }; } if (docStore && batchResult.storage_path) { const currentDocStore = docStore.get(docLabel); if (currentDocStore) { docStore.set(docLabel, { ...currentDocStore, storage_path: batchResult.storage_path, }); } } const appliedCount = combinedApplyReport.filter((r: any) => r.status === "applied").length; let commentVerification: CommentBatchVerification | undefined; if (batchResult.commented_bytes) { commentVerification = await verifyCommentBatchPostWrite({ originalBytes: current.bytes, commentedBytes: batchResult.commented_bytes, expectedAppliedCount: appliedCount, appliedReports: combinedApplyReport, }); } if (batchResult.commented_bytes && commentVerification?.body_text_unchanged) { const newSurface = await buildDocxEditSurface(batchResult.commented_bytes, { versionId: batchResult.version_id, }); rememberAnchorManifest(anchorManifestState, docLabel, documentId, newSurface); const oldSurfaceId = (await buildDocxEditSurface(current.bytes, { versionId: current.version_id })).surfaceId; console.log( `[applyCommentWriterBatch] manifest refreshed for ${docLabel} after comment-only write. ` + `old_revision=${oldSurfaceId} new_revision=${newSurface.surfaceId} ` + `entries=${newSurface.anchors.length} applied=${appliedCount}` ); } else { anchorManifestState?.byDocLabel.delete(docLabel); } const payload: DocEditedResult = { filename: docInfo.filename, document_id: documentId, version_id: batchResult.version_id, version_number: batchResult.version_number ?? null, download_url: batchResult.download_url, annotations: [], apply_report: combinedApplyReport, apply_summary: buildApplySummary(combinedApplyReport), }; docsEdited.push(payload); write(`data: ${JSON.stringify({ type: "doc_edited", ...payload, })} `); return { ok: true, version_id: batchResult.version_id, version_number: batchResult.version_number, download_url: batchResult.download_url, apply_report: combinedApplyReport, apply_summary: buildApplySummary(combinedApplyReport), }; } else { const payload = { type: "doc_edited", filename: docInfo.filename, document_id: documentId, version_id: "", version_number: null, download_url: "", annotations: [], apply_report: combinedApplyReport, apply_summary: buildApplySummary(combinedApplyReport), error: batchResult.error ?? "No comments were inserted.", }; write(`data: ${JSON.stringify(payload)} `); return { ok: false, error: batchResult.error ?? "No comments were inserted.", apply_report: combinedApplyReport, apply_summary: buildApplySummary(combinedApplyReport), }; } } function buildFailedPdfValidationRow(input: { tc: ToolCall; docLabel: string; documentId: string; intent: any; error: string; message: string; }): any { return { tool_call_id: input.tc.id, ok: false, doc_id: input.docLabel, document_id: input.documentId, block_id: input.intent?.block_id ?? null, comment_key: input.intent?.comment_key ?? null, status: "failed", comment: input.intent?.comment ?? input.message, reasoning: input.intent?.reasoning ?? null, anchor_text: input.intent?.target_text ?? "", section_heading: input.intent?.block_id ?? "PDF", op_type: input.intent?.type ?? "COMMENT", bookmark_name: input.intent?.block_id ?? null, finding_status: input.intent?.status ?? "REVIEW", document_order: Number.MAX_SAFE_INTEGER, apply_status: "failed", error: input.error, message: input.message, source: "plan_comments_on_document", }; } function parseStructuredCommentParts(comment: string | null | undefined): { issue: string; zamil_impact: string; recommended_action: string; } { const raw = comment ?? ""; const issueMatch = raw.match(/Issue:\s*(.*?)(?:\s*\|\s*Zamil Impact:|\s*\|\s*Recommended Action:|$)/i); const impactMatch = raw.match(/Zamil Impact:\s*(.*?)(?:\s*\|\s*Recommended Action:|$)/i); const actionMatch = raw.match(/Recommended Action:\s*(.*)$/i); return { issue: issueMatch?.[1]?.trim() || raw.trim(), zamil_impact: impactMatch?.[1]?.trim() || "", recommended_action: actionMatch?.[1]?.trim() || "", }; } function parseCommentPlanIntents(commentsRaw: unknown): CommentPlanIntent[] { const rawList = Array.isArray(commentsRaw) ? commentsRaw : []; const intents: CommentPlanIntent[] = []; for (const raw of rawList) { if (!raw || typeof raw !== "object") continue; const r = raw as Record; const blockId = normalizeModelBlockId(String(r.block_id ?? "").trim()); const comment = sanitizeWorkflowDisplayText(String(r.comment ?? "")); const commentKey = String(r.comment_key ?? "").trim(); intents.push({ block_id: blockId, comment, status: r.status ? String(r.status).trim() : undefined, reasoning: r.reasoning && typeof r.reasoning === "object" && "clause_section" in r.reasoning ? { clause_section: sanitizeWorkflowDisplayText(String((r.reasoning as Record).clause_section ?? "")), violated_standard: sanitizeWorkflowDisplayText(String((r.reasoning as Record).violated_standard ?? "")), zamil_exposure: sanitizeWorkflowDisplayText(String((r.reasoning as Record).zamil_exposure ?? "")), } : undefined, target_text: r.target_text ? sanitizeWorkflowDisplayText(String(r.target_text)) : undefined, anchor_mode: r.anchor_mode === "exact" ? "exact" : "paragraph", before_anchor: r.before_anchor ? sanitizeWorkflowDisplayText(String(r.before_anchor)) : undefined, after_anchor: r.after_anchor ? sanitizeWorkflowDisplayText(String(r.after_anchor)) : undefined, comment_key: commentKey, type: normalizeFindingType(r.type), new_text: r.new_text ? sanitizeWorkflowDisplayText(String(r.new_text)) : undefined, insert_text: r.insert_text ? sanitizeWorkflowDisplayText(String(r.insert_text)) : undefined, position: r.position ? String(r.position).trim() : undefined, rewrite_mode: r.rewrite_mode ? String(r.rewrite_mode).trim() : undefined, old_span: r.old_span ? sanitizeWorkflowDisplayText(String(r.old_span)) : undefined, new_span: r.new_span ? sanitizeWorkflowDisplayText(String(r.new_span)) : undefined, }); } return intents; } async function handlePlanCommentsOnPdf(params: { args: any; tc: ToolCall; docLabel: string; docInfo: any; indexed: any; db: any; verificationRows: any[]; }): Promise<{ role: "tool"; tool_call_id: string; content: string }> { const { args, tc, docLabel, indexed, db, verificationRows, } = params; const documentId = indexed.document_id; const current = await loadCurrentVersionBytes(documentId, db); if (!current) { return { role: "tool", tool_call_id: tc.id, content: JSON.stringify({ ok: false, error: "DOCUMENT_BYTES_UNAVAILABLE", message: "Could not load current PDF document bytes.", }), }; } const currentBytes = (current.bytes instanceof ArrayBuffer ? current.bytes : current.bytes.buffer.slice( current.bytes.byteOffset, current.bytes.byteOffset + current.bytes.byteLength, )) as ArrayBuffer; const surface = await buildPdfVirtualSurface(currentBytes, { documentId, versionId: current.version_id ?? indexed.version_id ?? null, }); const comments = parseCommentPlanIntents(args.comments); const rows: any[] = []; for (const intent of comments) { const pageNumber = pageFromPdfBlockId(intent.block_id); const page = pageNumber ? surface.pages[pageNumber - 1] : null; if (!pageNumber || !page) { rows.push(buildFailedPdfValidationRow({ tc, docLabel, documentId, intent, error: "PDF_PAGE_NOT_FOUND", message: `Invalid PDF page anchor "${intent.block_id}". Expected a block_id like BMK_P_page1, BMK_P_page2, etc.`, })); continue; } if (intent.type && intent.type !== "COMMENT") { rows.push(buildFailedPdfValidationRow({ tc, docLabel, documentId, intent, error: "PDF_READ_ONLY", message: "PDF documents are read-only. Only COMMENT findings are supported for PDFs.", })); continue; } if (!intent.status || !intent.reasoning || !intent.comment_key || !intent.comment) { rows.push(buildFailedPdfValidationRow({ tc, docLabel, documentId, intent, error: "VALIDATION_FAILED", message: "PDF finding is missing one or more required fields: status, reasoning, comment_key, comment.", })); continue; } const resolvedQuote = resolvePdfTargetText({ pageText: page.text, targetText: intent.target_text ?? "", beforeAnchor: intent.before_anchor ?? "", afterAnchor: intent.after_anchor ?? "", }); const parts = parseStructuredCommentParts(intent.comment); rows.push({ tool_call_id: tc.id, ok: true, row_id: `${docLabel}:${intent.comment_key}`, doc_id: docLabel, document_id: documentId, version_id: current.version_id ?? indexed.version_id ?? null, block_id: intent.block_id, comment_key: intent.comment_key, status: "applied", comment: intent.comment, reasoning: intent.reasoning, anchor_text: resolvedQuote.text || intent.target_text || page.text.slice(0, 240), original_wording: resolvedQuote.text || intent.target_text || page.text.slice(0, 240), section_heading: `Page ${pageNumber}`, op_type: "COMMENT", action: "COMMENT", bookmark_name: intent.block_id, finding_status: intent.status, document_order: pageNumber * 1000, apply_status: "applied", issue: parts.issue, zamil_impact: parts.zamil_impact, recommended_action: parts.recommended_action, quote_resolution: { confidence: resolvedQuote.confidence, strategy: resolvedQuote.strategy, }, source: "plan_comments_on_document", read_only_pdf: true, }); } verificationRows.push(...rows); const applied = rows.filter((row) => row.ok).length; const failed = rows.filter((row) => !row.ok).length; const extractedTextLength = surface.pages.reduce((sum, page) => sum + page.text.trim().length, 0); return { role: "tool", tool_call_id: tc.id, content: JSON.stringify({ ok: true, doc_id: docLabel, document_id: documentId, read_only_pdf: true, no_extractable_text: extractedTextLength === 0, total: comments.length, applied, failed, message: extractedTextLength === 0 ? "The PDF appears to have no extractable text. No PDF file was modified." : "PDF review findings were recorded as read-only workflow rows. No PDF file was modified.", }), }; } export async function handlePlanCommentsOnDocument(params: { tc: ToolCall; args: Record; docStore: DocStore; docIndex: DocIndex; userId: string; db: ReturnType; write: (s: string) => void; trace?: RunTree | null; anchorManifestState?: AnchorManifestState; pendingCommentPlanState: PendingCommentPlanState; docsEdited: DocEditedResult[]; verificationRows?: CommentVerificationRow[]; trackChanges?: boolean; }): Promise<{ role: "tool"; tool_call_id: string; content: string }> { const { tc, args, docStore, docIndex, userId, db, write, trace, anchorManifestState, pendingCommentPlanState, docsEdited, verificationRows = [], trackChanges } = params; const rawDocId = String(args.doc_id ?? "").trim(); const docLabel = resolveDocLabel(rawDocId, docStore, docIndex) ?? rawDocId; const docInfo = docStore.get(docLabel); const indexed = docIndex?.[docLabel]; if (!docInfo || !indexed) { return { role: "tool", tool_call_id: tc.id, content: JSON.stringify({ ok: false, error: "DOCUMENT_NOT_FOUND", message: `Document '${rawDocId}' is not available.`, }), }; } if (docInfo.file_type === "pdf") { return handlePlanCommentsOnPdf({ args, tc, docLabel, docInfo, indexed, db, verificationRows, }); } if (docInfo.file_type !== "docx") { return { role: "tool", tool_call_id: tc.id, content: JSON.stringify({ ok: false, error: "UNSUPPORTED_DOCUMENT_TYPE", message: "comment_on_document only supports .docx files.", }), }; } const current = await loadCurrentVersionBytes(indexed.document_id, db); if (!current) { return { role: "tool", tool_call_id: tc.id, content: JSON.stringify({ ok: false, error: "DOCUMENT_BYTES_UNAVAILABLE", message: "Could not load current document bytes.", }), }; } const currentSurface = await buildDocxEditSurface(current.bytes, { versionId: current.version_id, }); const manifest = anchorManifestState?.byDocLabel.get(docLabel); const commentsRaw = Array.isArray(args.comments) ? args.comments : []; const actionableIntents: PlannedCommentItem[] = []; const ignoredPassIntents: CommentPlanIntent[] = []; const failedPermanently: PlannedCommentItem[] = []; // 1) Parse raw input list and partition into actionable, pass, and malformed for (const [index, raw] of commentsRaw.entries()) { if (!raw || typeof raw !== "object") { failedPermanently.push({ block_id: "UNKNOWN", comment: "Comment intent is not an object.", comment_key: `malformed_${index}`, failureCode: "VALIDATION_FAILED", message: "Comment intent is not an object.", rawIntent: raw, }); continue; } const r = raw as Record; const intent: CommentPlanIntent = { block_id: normalizeModelBlockId(String(r.block_id ?? "").trim()), comment: sanitizeWorkflowDisplayText(String(r.comment ?? "")), status: r.status ? String(r.status).trim() : undefined, reasoning: r.reasoning && typeof r.reasoning === "object" && "clause_section" in r.reasoning ? { clause_section: sanitizeWorkflowDisplayText(String((r.reasoning as any).clause_section ?? "")), violated_standard: sanitizeWorkflowDisplayText(String((r.reasoning as any).violated_standard ?? "")), zamil_exposure: sanitizeWorkflowDisplayText(String((r.reasoning as any).zamil_exposure ?? "")), } : undefined, target_text: r.target_text ? sanitizeWorkflowDisplayText(String(r.target_text)) : undefined, anchor_mode: r.anchor_mode === "exact" ? "exact" : "paragraph", before_anchor: r.before_anchor ? sanitizeWorkflowDisplayText(String(r.before_anchor)) : undefined, after_anchor: r.after_anchor ? sanitizeWorkflowDisplayText(String(r.after_anchor)) : undefined, comment_key: String(r.comment_key ?? "").trim(), type: normalizeFindingType(r.type), new_text: r.new_text ? sanitizeWorkflowDisplayText(String(r.new_text)) : undefined, insert_text: r.insert_text ? sanitizeWorkflowDisplayText(String(r.insert_text)) : undefined, position: r.position ? String(r.position).trim() : undefined, rewrite_mode: r.rewrite_mode ? String(r.rewrite_mode).trim() : undefined, old_span: r.old_span ? sanitizeWorkflowDisplayText(String(r.old_span)) : undefined, new_span: r.new_span ? sanitizeWorkflowDisplayText(String(r.new_span)) : undefined, }; if (isPassItem(intent)) { ignoredPassIntents.push(intent); } else { let item: PlannedCommentItem = { ...intent, rawIntent: r, }; if (trackChanges !== true && isTrackedChangeOperation(item)) { item = downgradeEditIntentToComment(item, "Document reviewed in comments-only mode"); } actionableIntents.push(item); } } const ignoredPassReports = ignoredPassIntents.map((intent, i) => buildIgnoredPassReport(intent, i)); const validationTrace = createTraceChild(trace, { name: "comment_plan_validation", run_type: "chain", inputs: { doc_id: docLabel, document_id: indexed.document_id, comments_attempted: commentsRaw.length, comments_actionable: actionableIntents.length, comments_ignored_pass: ignoredPassIntents.length, manifest_surface_id: manifest?.surfaceId ?? null, current_surface_id: currentSurface.surfaceId, navigation_region_count: currentSurface.navigationRegions.length, navigation_block_count: currentSurface.anchors.filter( (anchor) => !anchor.anchor_eligible, ).length, track_changes: trackChanges ?? false, }, tags: ["verification", "comments", "plan"], }); // Log ignored PASS diagnostics to LangSmith if (ignoredPassReports.length > 0 && validationTrace) { validationTrace.addEvent({ name: "ignored_pass_items", kwargs: { count: ignoredPassReports.length, items: ignoredPassReports }, }); } const valid: PlannedCommentItem[] = []; const needsCorrection: PlannedCommentItem[] = []; // 2) Run validatePlannedFinding on all actionable items with allowCommentFallback=true for (const intent of actionableIntents) { const result = validatePlannedFinding({ intent, manifest, currentSurface, allowCommentFallback: true, }); if (result.ok) { valid.push(result.item); } else { if (result.repairable) { needsCorrection.push(result.item); } else { failedPermanently.push(result.item); } } } await endTrace(validationTrace, { outputs: { commentsAttempted: commentsRaw.length, commentsActionable: actionableIntents.length, commentsIgnoredPass: ignoredPassIntents.length, commentsValid: valid.length, commentsNeedCorrection: needsCorrection.length, commentsFailedPermanently: failedPermanently.length, ignoredPassReports, validItems: valid.map((v) => ({ comment_key: v.comment_key, status: v.status, block_id: v.block_id, comment: v.comment, reasoning: v.reasoning, anchor_text: v.anchor?.text_preview ?? null, anchor_text_hash: v.anchor?.text_hash ?? null, section_heading: findSectionHeading(currentSurface, v.block_id), native_para_id: v.anchor?.native_para_id ?? null, stable_id: v.anchor?.stable_id ?? null, xml_path: v.anchor?.xml_path ?? null, container: v.anchor?.container ?? null, commentable: v.anchor?.commentable ?? null, bookmark_name: v.anchor?.bookmark_name ?? null, op_type: v.type ?? "COMMENT", validation_status: "valid", anchor_redirected_from: v.anchorRedirectedFrom ?? null, anchor_redirected_to: v.anchorRedirectedTo ?? null, anchor_redirect_reason: v.anchorRedirectReason ?? null, })), needCorrectionItems: needsCorrection.map((c) => ({ comment_key: c.comment_key, status: c.status, block_id: c.block_id, comment: c.comment, reasoning: c.reasoning, anchor_text: c.anchor?.text_preview ?? null, section_heading: findSectionHeading(currentSurface, c.block_id), bookmark_name: c.anchor?.bookmark_name ?? null, op_type: c.type ?? "COMMENT", failure_code: c.failureCode, message: c.message, validation_status: "failed", })), failedPermanentlyItems: failedPermanently.map((f) => ({ comment_key: f.comment_key, status: f.status, block_id: f.block_id, comment: f.comment, reasoning: f.reasoning, anchor_text: f.anchor?.text_preview ?? null, section_heading: findSectionHeading(currentSurface, f.block_id), bookmark_name: f.anchor?.bookmark_name ?? null, op_type: f.type ?? "COMMENT", failure_code: f.failureCode, message: f.message, validation_status: "failed", })), }, error: needsCorrection.length > 0 || failedPermanently.length > 0 ? `${needsCorrection.length + failedPermanently.length} comment anchor(s) failed prevalidation` : null, }); // Build correction requests using the unified helper const correctionRequests = needsCorrection.map((item) => buildFindingCorrectionRequest({ item, currentSurface }) ); // Store the pending plan so correct_comment_anchors can retrieve it pendingCommentPlanState.set(docLabel, { docLabel, documentId: indexed.document_id, docInfo, indexed, current, currentSurface, manifest, valid, needsCorrection, failedPermanently, ignoredPassReports, }); if (needsCorrection.length > 0) { return { role: "tool", tool_call_id: tc.id, content: JSON.stringify({ ok: true, doc_id: docLabel, total: commentsRaw.length, valid: valid.length, ignored_pass: ignoredPassIntents.length, needs_correction: needsCorrection.length, failed_permanently: failedPermanently.length, ready_to_apply: false, valid_comments: valid.map((v) => ({ comment_key: v.comment_key, block_id: v.block_id, status: "pending_valid", anchor_redirected_from: v.anchorRedirectedFrom ?? null, anchor_redirected_to: v.anchorRedirectedTo ?? null, anchor_redirect_reason: v.anchorRedirectReason ?? null, })), correction_requests: correctionRequests, failed_permanently_items: failedPermanently.map((f) => ({ comment_key: f.comment_key, block_id: f.block_id, failure_code: f.failureCode, message: f.message, })), instructions: `Call correct_comment_anchors for doc_id '${docLabel}' with corrected block_ids for the ${needsCorrection.length} item(s) that need correction. Valid comments are preserved and will be merged after correction.`, }), }; } if (valid.length > 0) { const writerItems: WriterCommentItem[] = valid.map((v, i) => ({ id: `plan-comment-${i}`, args: { doc_id: docLabel, block_id: v.block_id, comment: v.comment, target_text: v.target_text ?? "", anchor_mode: v.anchor_mode ?? "paragraph", before_anchor: v.before_anchor ?? "", after_anchor: v.after_anchor ?? "", comment_key: v.comment_key ?? makeCommentKey({ doc_id: docLabel, block_id: v.block_id, target_text: v.target_text ?? "", anchor_mode: v.anchor_mode ?? "paragraph", comment: v.comment, }), type: (v as any).type, new_text: (v as any).new_text, insert_text: (v as any).insert_text, position: (v as any).position, rewrite_mode: (v as any).rewrite_mode, old_span: (v as any).old_span, new_span: (v as any).new_span, }, anchor: v.anchor!, reasoning: v.reasoning, anchor_text: v.anchor?.text_preview ?? undefined, section_heading: findSectionHeading(currentSurface, v.block_id), finding_status: v.status as WorkflowFindingStatus, document_order: getDocumentOrder(currentSurface, v.block_id, v.anchor?.bookmark_name ?? null), source: "plan_comments_on_document", })); const prevalidationFailures = [ ...failedPermanently.map((item) => ({ status: "not_applied", apply_status: "failed_validation" as const, comment_key: item.comment_key, model_block_id: item.block_id, message: item.message, failure_code: item.failureCode ?? "VALIDATION_FAILED", op_type: item.type ?? "COMMENT", comment: item.comment, reasoning: item.reasoning, finding_status: item.status, section_heading: findSectionHeading(currentSurface, item.block_id), document_order: getDocumentOrder(currentSurface, item.block_id, item.anchor?.bookmark_name ?? null), bookmark_name: item.anchor?.bookmark_name ?? null, source: "plan_comments_on_document" as const, ...buildFindingDiagnostics({ item, applyStatus: "failed_validation" }), })), ...ignoredPassReports.map((report) => ({ ...report, source: "plan_comments_on_document" as const, })), ]; const applyResult = await applyCommentWriterBatch({ userId, documentId: indexed.document_id, docLabel, docInfo, current, comments: writerItems, db, write, trace, docIndex, docStore, anchorManifestState, docsEdited, prevalidationFailures, toolCallId: tc.id, verificationRows, trackChanges, }); pendingCommentPlanState.delete(docLabel); return { role: "tool", tool_call_id: tc.id, content: JSON.stringify({ ok: applyResult.ok, doc_id: docLabel, total: commentsRaw.length, valid: valid.length, ignored_pass: ignoredPassIntents.length, needs_correction: 0, failed_permanently: failedPermanently.length, ready_to_apply: true, applied: applyResult.ok, version_id: applyResult.version_id, version_number: applyResult.version_number, download_url: applyResult.download_url, apply_summary: applyResult.apply_summary, anchor_redirects: valid .filter((item) => item.anchorRedirectedFrom) .map((item) => ({ comment_key: item.comment_key, from: item.anchorRedirectedFrom, to: item.anchorRedirectedTo, reason: item.anchorRedirectReason, })), }), }; } if (failedPermanently.length > 0) { for (const item of failedPermanently) { verificationRows.push({ tool_call_id: tc.id, ok: false, doc_id: docLabel, document_id: indexed.document_id, block_id: item.block_id, comment_key: item.comment_key, status: "not_applied", failure_code: item.failureCode ?? "VALIDATION_FAILED", message: item.message ?? "Validation failed.", comment: item.comment, reasoning: item.reasoning, anchor_text: item.anchor?.text_preview ?? undefined, section_heading: findSectionHeading(currentSurface, item.block_id), op_type: item.type ?? "COMMENT", bookmark_name: item.anchor?.bookmark_name ?? null, finding_status: item.status as WorkflowFindingStatus | undefined, document_order: getDocumentOrder(currentSurface, item.block_id, item.anchor?.bookmark_name ?? null), apply_status: "failed_validation", source: "plan_comments_on_document", }); } for (const report of ignoredPassReports) { verificationRows.push({ tool_call_id: tc.id, ok: true, doc_id: docLabel, document_id: indexed.document_id, block_id: String(report.block_id ?? ""), comment_key: String(report.comment_key ?? ""), status: "ignored_pass", failure_code: undefined, message: String(report.ignored_reason ?? "pass_status"), comment: String(report.comment ?? ""), reasoning: (report.reasoning && typeof report.reasoning === "object" && "clause_section" in report.reasoning) ? { clause_section: String((report.reasoning as Record).clause_section ?? "").trim(), violated_standard: String((report.reasoning as Record).violated_standard ?? "").trim(), zamil_exposure: String((report.reasoning as Record).zamil_exposure ?? "").trim() } : undefined, op_type: (typeof report.op_type === "string" ? report.op_type : undefined), bookmark_name: (typeof report.bookmark_name === "string" ? report.bookmark_name : null) ?? null, finding_status: undefined, apply_status: "ignored_pass", source: "plan_comments_on_document", }); } pendingCommentPlanState.delete(docLabel); return { role: "tool", tool_call_id: tc.id, content: JSON.stringify({ ok: false, doc_id: docLabel, total: commentsRaw.length, valid: 0, ignored_pass: ignoredPassIntents.length, needs_correction: 0, failed_permanently: failedPermanently.length, ready_to_apply: true, applied: false, }), }; } // Only ignored PASS or empty input remains for (const report of ignoredPassReports) { verificationRows.push({ tool_call_id: tc.id, ok: true, doc_id: docLabel, document_id: indexed.document_id, block_id: String(report.block_id ?? ""), comment_key: String(report.comment_key ?? ""), status: "ignored_pass", failure_code: undefined, message: String(report.ignored_reason ?? "pass_status"), comment: String(report.comment ?? ""), reasoning: (report.reasoning && typeof report.reasoning === "object" && "clause_section" in report.reasoning) ? { clause_section: String((report.reasoning as Record).clause_section ?? "").trim(), violated_standard: String((report.reasoning as Record).violated_standard ?? "").trim(), zamil_exposure: String((report.reasoning as Record).zamil_exposure ?? "").trim() } : undefined, op_type: (typeof report.op_type === "string" ? report.op_type : undefined), bookmark_name: (typeof report.bookmark_name === "string" ? report.bookmark_name : null) ?? null, finding_status: undefined, apply_status: "ignored_pass", source: "plan_comments_on_document", }); } pendingCommentPlanState.delete(docLabel); return { role: "tool", tool_call_id: tc.id, content: JSON.stringify({ ok: true, doc_id: docLabel, total: commentsRaw.length, valid: 0, ignored_pass: ignoredPassIntents.length, needs_correction: 0, failed_permanently: 0, ready_to_apply: true, applied: false, no_action: true, }), }; } export async function handleCorrectCommentAnchors(params: { tc: ToolCall; args: Record; docStore: DocStore; docIndex: DocIndex; userId: string; db: ReturnType; write: (s: string) => void; trace?: RunTree | null; anchorManifestState?: AnchorManifestState; pendingCommentPlanState: PendingCommentPlanState; docsEdited: DocEditedResult[]; verificationRows?: CommentVerificationRow[]; trackChanges?: boolean; }): Promise<{ role: "tool"; tool_call_id: string; content: string }> { const { tc, args, docStore, docIndex, userId, db, write, trace, anchorManifestState, pendingCommentPlanState, docsEdited, verificationRows = [], trackChanges } = params; const rawDocId = String(args.doc_id ?? "").trim(); const docLabel = resolveDocLabel(rawDocId, docStore, docIndex) ?? rawDocId; const plan = pendingCommentPlanState.get(docLabel); if (!plan) { return { role: "tool", tool_call_id: tc.id, content: JSON.stringify({ ok: false, error: "NO_PENDING_PLAN", message: `No pending comment plan found for '${docLabel}'. Call plan_comments_on_document first.`, }), }; } const sourceToolName: CommentPlanSource = tc.function.name === "fix_comment_findings" ? "fix_comment_findings" : "correct_comment_anchors"; const correctionsRaw = Array.isArray(args.corrections) ? args.corrections : []; const corrections = correctionsRaw .filter((r): r is Record => !!r && typeof r === "object") .map((r) => ({ comment_key: String(r.comment_key ?? "").trim(), new_block_id: r.new_block_id ? String(r.new_block_id).trim() : undefined, status: String(r.status ?? "").trim() as FindingCorrection["status"], reason: r.reason ? String(r.reason).trim() : undefined, corrected_comment: r.corrected_comment ? String(r.corrected_comment).trim() : undefined, corrected_status: r.corrected_status ? String(r.corrected_status).trim() as FindingCorrection["corrected_status"] : undefined, corrected_reasoning: r.corrected_reasoning && typeof r.corrected_reasoning === "object" && "clause_section" in r.corrected_reasoning ? { clause_section: String((r.corrected_reasoning as any).clause_section ?? "").trim(), violated_standard: String((r.corrected_reasoning as any).violated_standard ?? "").trim(), zamil_exposure: String((r.corrected_reasoning as any).zamil_exposure ?? "").trim(), } : undefined, corrected_target_text: r.corrected_target_text ? String(r.corrected_target_text).trim() : undefined, corrected_type: normalizeFindingType(r.corrected_type) as FindingCorrection["corrected_type"], corrected_new_text: r.corrected_new_text ? String(r.corrected_new_text).trim() : undefined, corrected_insert_text: r.corrected_insert_text ? String(r.corrected_insert_text).trim() : undefined, corrected_position: r.corrected_position ? String(r.corrected_position).trim() as FindingCorrection["corrected_position"] : undefined, corrected_rewrite_mode: r.corrected_rewrite_mode ? String(r.corrected_rewrite_mode).trim() as FindingCorrection["corrected_rewrite_mode"] : undefined, corrected_old_span: r.corrected_old_span ? String(r.corrected_old_span).trim() : undefined, corrected_new_span: r.corrected_new_span ? String(r.corrected_new_span).trim() : undefined, })); const stillNeedsCorrection: PlannedCommentItem[] = []; const newlyValid: PlannedCommentItem[] = []; const unresolved: PlannedCommentItem[] = []; const revalidationTrace = createTraceChild(trace, { name: "comment_anchor_correction", run_type: "chain", inputs: { doc_id: docLabel, document_id: plan.documentId, corrections_attempted: corrections.length, source_tool: sourceToolName, }, tags: ["verification", "comments", "correction"], }); for (const correction of corrections) { const originalItem = plan.needsCorrection.find((n) => n.comment_key === correction.comment_key); if (!originalItem) continue; // Ensure at most 1 repair attempt is allowed (MAX_FINDING_REPAIR_ATTEMPTS = 1) const currentAttempts = originalItem.repairAttempts ?? 0; if (currentAttempts >= MAX_FINDING_REPAIR_ATTEMPTS) { // Apply safe downgrade to COMMENT if defined, else fail permanently if (originalItem.terminalFallback === "COMMENT") { const downgraded = downgradeMalformedEditToComment( originalItem, "Max repair attempts exceeded. Falling back to COMMENT." ); const result = validatePlannedFinding({ intent: downgraded, manifest: plan.manifest, currentSurface: plan.currentSurface, allowCommentFallback: false, }); if (result.ok) { newlyValid.push(result.item); } else { unresolved.push({ ...result.item, failureCode: result.failureCode ?? "max_repairs_exceeded_downgrade_failed", message: `Max repair attempts exceeded. Fallback to COMMENT failed: ${result.message}`, }); } } else { unresolved.push({ ...originalItem, failureCode: "max_repairs_exceeded", message: "Maximum repair attempts exceeded. Finding could not be repaired.", }); } continue; } if (correction.status === "unresolved") { if (originalItem.terminalFallback === "COMMENT") { const downgraded = downgradeMalformedEditToComment( originalItem, correction.reason || "Model marked unresolved. Applying safe fallback COMMENT." ); const result = validatePlannedFinding({ intent: downgraded, manifest: plan.manifest, currentSurface: plan.currentSurface, allowCommentFallback: false, }); if (result.ok) { newlyValid.push(result.item); } else { unresolved.push({ ...result.item, failureCode: result.failureCode ?? "unresolved_downgrade_failed", message: `Model marked unresolved. Fallback to COMMENT failed: ${result.message}`, }); } } else { unresolved.push({ ...originalItem, failureCode: "unresolved", message: correction.reason || "Could not confidently identify the correct block or repair finding.", }); } continue; } // Apply LLM corrected edits to the original item let correctedItem = applyFindingCorrection(originalItem, correction); if (trackChanges !== true && isTrackedChangeOperation(correctedItem)) { correctedItem = downgradeEditIntentToComment(correctedItem, "Document reviewed in comments-only mode"); } // Run validatePlannedFinding on the corrected item const result = validatePlannedFinding({ intent: correctedItem, manifest: plan.manifest, currentSurface: plan.currentSurface, allowCommentFallback: false, // Do not trigger automatic fallback }); if (result.ok) { newlyValid.push(result.item); } else { if (result.item.terminalFallback === "COMMENT") { const downgraded = downgradeMalformedEditToComment( result.item, `Repair attempt failed: ${result.message}. Applying safe fallback COMMENT.` ); const secondResult = validatePlannedFinding({ intent: downgraded, manifest: plan.manifest, currentSurface: plan.currentSurface, allowCommentFallback: false, }); if (secondResult.ok) { newlyValid.push(secondResult.item); } else { unresolved.push({ ...secondResult.item, failureCode: secondResult.failureCode ?? "repair_failed_downgrade_failed", message: `Repair failed: ${result.message}. Downgrade fallback also failed: ${secondResult.message}`, }); } } else { stillNeedsCorrection.push(result.item); } } } const addressedKeys = new Set(corrections.map((c) => c.comment_key)); for (const unaddressed of plan.needsCorrection) { if (!addressedKeys.has(unaddressed.comment_key!)) { if (unaddressed.terminalFallback === "COMMENT") { const downgraded = downgradeMalformedEditToComment( unaddressed, "No correction was provided. Falling back to COMMENT." ); const result = validatePlannedFinding({ intent: downgraded, manifest: plan.manifest, currentSurface: plan.currentSurface, allowCommentFallback: false, }); if (result.ok) { newlyValid.push(result.item); } else { unresolved.push({ ...result.item, failureCode: result.failureCode ?? "unaddressed_downgrade_failed", message: `Unaddressed fallback to COMMENT failed: ${result.message}`, }); } } else { unresolved.push({ ...unaddressed, failureCode: "unresolved", message: "No correction was provided for this item.", }); } } } // Merge: original valid + newly corrected valid const allValid = [...plan.valid, ...newlyValid]; const allFailed = [...plan.failedPermanently, ...stillNeedsCorrection, ...unresolved]; await endTrace(revalidationTrace, { outputs: { originallyValid: plan.valid.length, newlyValid: newlyValid.length, stillNeedsCorrection: stillNeedsCorrection.length, unresolved: unresolved.length, failedPermanently: plan.failedPermanently.length, newlyValidItems: newlyValid.map((v) => ({ comment_key: v.comment_key, block_id: v.block_id, op_type: v.type ?? "COMMENT", bookmark_name: v.anchor?.bookmark_name ?? null, })), stillNeedsCorrectionItems: stillNeedsCorrection.map((c) => ({ comment_key: c.comment_key, block_id: c.block_id, op_type: c.type ?? "COMMENT", failure_code: c.failureCode, message: c.message, })), unresolvedItems: unresolved.map((u) => ({ comment_key: u.comment_key, block_id: u.block_id, op_type: u.type ?? "COMMENT", failure_code: u.failureCode, message: u.message, })), }, error: allFailed.length > 0 ? `${allFailed.length} comment(s) still failed after correction` : null, }); // Update the pending plan with merged state pendingCommentPlanState.set(docLabel, { ...plan, valid: allValid, needsCorrection: stillNeedsCorrection, failedPermanently: allFailed, }); if (stillNeedsCorrection.length > 0) { const newCorrectionRequests = stillNeedsCorrection.map((item) => buildFindingCorrectionRequest({ item, currentSurface: plan.currentSurface }) ); return { role: "tool", tool_call_id: tc.id, content: JSON.stringify({ ok: true, doc_id: docLabel, total: plan.valid.length + plan.needsCorrection.length + plan.failedPermanently.length, valid: allValid.length, ignored_pass: plan.ignoredPassReports?.length ?? 0, needs_correction: stillNeedsCorrection.length, failed_permanently: allFailed.length - stillNeedsCorrection.length, ready_to_apply: false, valid_comments: allValid.map((v) => ({ comment_key: v.comment_key, block_id: v.block_id, status: "pending_valid", })), correction_requests: newCorrectionRequests, failed_permanently_items: allFailed.filter(f => !stillNeedsCorrection.some(s => s.comment_key === f.comment_key)).map((f) => ({ comment_key: f.comment_key, block_id: f.block_id, failure_code: f.failureCode, message: f.message, })), instructions: `Call ${sourceToolName} for doc_id '${docLabel}' with corrected block_ids/payloads for the ${stillNeedsCorrection.length} item(s) that need correction.`, }), }; } if (allValid.length === 0) { for (const item of allFailed) { verificationRows.push({ tool_call_id: tc.id, ok: false, doc_id: docLabel, document_id: plan.documentId, block_id: item.block_id, comment_key: item.comment_key, status: "not_applied", failure_code: item.failureCode ?? "VALIDATION_FAILED", message: item.message ?? "Validation failed after correction.", comment: item.comment, reasoning: item.reasoning, anchor_text: item.anchor?.text_preview ?? undefined, section_heading: findSectionHeading(plan.currentSurface, item.block_id), op_type: item.type ?? "COMMENT", bookmark_name: item.anchor?.bookmark_name ?? null, finding_status: item.status as WorkflowFindingStatus | undefined, document_order: getDocumentOrder(plan.currentSurface, item.block_id, item.anchor?.bookmark_name ?? null), apply_status: "failed_validation", source: sourceToolName, ...buildFindingDiagnostics({ item, applyStatus: "failed_validation" }), }); } for (const report of plan.ignoredPassReports ?? []) { verificationRows.push({ tool_call_id: tc.id, ok: true, doc_id: docLabel, document_id: plan.documentId, block_id: String(report.block_id ?? ""), comment_key: String(report.comment_key ?? ""), status: "ignored_pass", failure_code: undefined, message: String(report.ignored_reason ?? "pass_status"), comment: String(report.comment ?? ""), reasoning: (report.reasoning && typeof report.reasoning === "object" && "clause_section" in report.reasoning) ? { clause_section: String((report.reasoning as Record).clause_section ?? "").trim(), violated_standard: String((report.reasoning as Record).violated_standard ?? "").trim(), zamil_exposure: String((report.reasoning as Record).zamil_exposure ?? "").trim() } : undefined, op_type: (typeof report.op_type === "string" ? report.op_type : undefined), bookmark_name: (typeof report.bookmark_name === "string" ? report.bookmark_name : null) ?? null, finding_status: undefined, apply_status: "ignored_pass", source: sourceToolName, }); } pendingCommentPlanState.delete(docLabel); return { role: "tool", tool_call_id: tc.id, content: JSON.stringify({ ok: false, doc_id: docLabel, total: plan.valid.length + plan.needsCorrection.length + plan.failedPermanently.length, valid: 0, applied: 0, failed: allFailed.length, ready_to_apply: true, message: `No comments could be applied after correction. Re-read the document and create a new plan if needed.`, }), }; } // Apply merged batch const writerItems: WriterCommentItem[] = allValid.map((v, i) => ({ id: `plan-comment-${i}`, args: { doc_id: docLabel, block_id: v.block_id, comment: v.comment, target_text: v.target_text ?? "", anchor_mode: v.anchor_mode ?? "paragraph", before_anchor: v.before_anchor ?? "", after_anchor: v.after_anchor ?? "", comment_key: v.comment_key ?? makeCommentKey({ doc_id: docLabel, block_id: v.block_id, target_text: v.target_text ?? "", anchor_mode: v.anchor_mode ?? "paragraph", comment: v.comment, }), type: v.type, new_text: v.new_text, insert_text: v.insert_text, position: v.position, rewrite_mode: v.rewrite_mode, old_span: v.old_span, new_span: v.new_span, }, anchor: v.anchor!, reasoning: v.reasoning, anchor_text: v.anchor?.text_preview ?? undefined, section_heading: findSectionHeading(plan.currentSurface, v.block_id), finding_status: v.status as WorkflowFindingStatus, document_order: getDocumentOrder(plan.currentSurface, v.block_id, v.anchor?.bookmark_name ?? null), source: sourceToolName, })); const prevalidationFailures = [ ...allFailed.map((f) => ({ status: "not_applied", apply_status: "failed_validation" as const, comment_key: f.comment_key, model_block_id: f.block_id, message: f.message, failure_code: f.failureCode ?? "VALIDATION_FAILED", op_type: f.type ?? "COMMENT", comment: f.comment, reasoning: f.reasoning, finding_status: f.status, section_heading: findSectionHeading(plan.currentSurface, f.block_id), document_order: getDocumentOrder(plan.currentSurface, f.block_id, f.anchor?.bookmark_name ?? null), bookmark_name: f.anchor?.bookmark_name ?? null, source: sourceToolName, ...buildFindingDiagnostics({ item: f, applyStatus: "failed_validation" }), })), ...(plan.ignoredPassReports ?? []).map((report) => ({ ...report, source: sourceToolName, })), ]; const applyResult = await applyCommentWriterBatch({ userId, documentId: plan.documentId, docLabel, docInfo: plan.docInfo, current: plan.current, comments: writerItems, db, write, trace, docIndex, docStore, anchorManifestState, docsEdited, prevalidationFailures, toolCallId: tc.id, verificationRows, trackChanges, }); pendingCommentPlanState.delete(docLabel); return { role: "tool", tool_call_id: tc.id, content: JSON.stringify({ ok: applyResult.ok, doc_id: docLabel, total: plan.valid.length + plan.needsCorrection.length + plan.failedPermanently.length, valid: allValid.length, applied: applyResult.ok ? (applyResult.apply_summary?.applied ?? 0) : 0, failed: allFailed.length, ready_to_apply: true, version_id: applyResult.version_id, version_number: applyResult.version_number, download_url: applyResult.download_url, apply_summary: applyResult.apply_summary, anchor_redirects: allValid .filter((item) => item.anchorRedirectedFrom) .map((item) => ({ comment_key: item.comment_key, from: item.anchorRedirectedFrom, to: item.anchorRedirectedTo, reason: item.anchorRedirectReason, })), unresolved: unresolved.map((u) => ({ comment_key: u.comment_key, block_id: u.block_id, failure_code: u.failureCode, message: u.message, })), }), }; } export type WriterAnchorEligibilityInput = { id: string; args: { block_id: string; comment_key: string; type?: string; }; anchor: CanonicalAnchor; }; export function buildWriterAnchorEligibilityFailures( comments: WriterAnchorEligibilityInput[], ): Record[] { return comments .filter((item) => !item.anchor.anchor_eligible) .map((item) => ({ ...makeCommentFailureReport({ id: item.id, commentKey: item.args.comment_key, blockId: item.args.block_id, failureCode: "ANCHOR_NOT_ELIGIBLE", message: `Block ${item.args.block_id} is not eligible for comments or edits.`, anchor: item.anchor, opType: item.args.type, }), anchor_rejection_reason: item.anchor.anchor_rejection_reason ?? "navigation", })); } async function runCommentDocumentBatch(params: { userId: string; documentId: string; docLabel: string; docInfo: NonNullable>; current: NonNullable>>; comments: Array; db: ReturnType; trace?: RunTree | null; trackChanges?: boolean; }): Promise<{ ok: boolean; version_id?: string; version_number?: number; storage_path?: string; download_url?: string; apply_report?: any[]; commented_bytes?: Buffer; error?: string; }> { const { userId, documentId, docInfo, current, comments, db, trace, trackChanges } = params; if (comments.length === 0) return { ok: false, error: "No validated comments to apply." }; const eligibilityFailures = buildWriterAnchorEligibilityFailures(comments); if (eligibilityFailures.length > 0) { return { ok: false, error: "Writer boundary rejected one or more navigation-region anchors.", apply_report: eligibilityFailures, }; } try { const pythonUrl = process.env.PYTHON_SERVICE_URL || "http://localhost:8000"; const response = await fetch(`${pythonUrl}/api/apply-comments`, { method: "POST", headers: { "Content-Type": "application/json", "x-user-id": userId, ...traceHeaders(trace), }, body: JSON.stringify({ document_bytes_b64: Buffer.from(current.bytes).toString("base64"), document_name: docInfo.filename, track_changes: trackChanges === true, findings: comments.map((item) => ({ id: item.id, model_block_id: item.args.block_id, comment_key: item.args.comment_key, risk_level: "info", clause_type: "Comment", issue: "Assistant Feedback", comment: item.args.comment, review_reasoning: { clause_section: "N/A", violated_standard: "N/A", zamil_exposure: "Manual chat comment not from playbook review" }, target_text: item.args.target_text, anchor_mode: item.args.anchor_mode, start_paragraph_id: item.args.block_id, end_paragraph_id: item.args.block_id, start_anchor: item.args.target_text ? item.args.target_text.slice(0, 220) : "", end_anchor: item.args.target_text ? item.args.target_text.slice(-220) : "", before_anchor: item.args.before_anchor || "", after_anchor: item.args.after_anchor || "", confidence: 1.0, stable_id: item.anchor.stable_id, native_para_id: item.anchor.native_para_id, text_hash: item.anchor.text_hash, xml_path: item.anchor.xml_path, op_type: item.args.type || "COMMENT", new_text: item.args.new_text ?? null, insert_text: item.args.insert_text ?? null, position: item.args.position ?? null, rewrite_mode: item.args.rewrite_mode ?? null, old_span: item.args.old_span ?? null, new_span: item.args.new_span ?? null, bookmark_name: item.anchor?.bookmark_name ?? null, })), }), }); if (!response.ok) { const errData = await response.json().catch(() => ({})); return { ok: false, error: errData.detail || `Python service error: ${response.statusText}` }; } const data = await response.json(); const applyReport = Array.isArray(data.apply_report) ? data.apply_report : []; const appliedCount = applyReport.filter((r: any) => r.status === "applied").length; const commentedBytesB64 = typeof data.commented_bytes_b64 === "string" ? data.commented_bytes_b64 : typeof data.document_bytes_b64 === "string" ? data.document_bytes_b64 : ""; if (appliedCount === 0) { return { ok: false, error: "No comments were inserted.", apply_report: applyReport, }; } if (!commentedBytesB64) { return { ok: false, error: "Python service did not return commented document bytes.", apply_report: applyReport, }; } const commentedBytes = Buffer.from(commentedBytesB64, "base64"); const versionId = crypto.randomUUID().replace(/-/g, ""); const newPath = `documents/${userId}/${documentId}/comments/${versionId}.docx`; const exact = commentedBytes.buffer.slice( commentedBytes.byteOffset, commentedBytes.byteOffset + commentedBytes.byteLength, ) as ArrayBuffer; await uploadFile( newPath, exact, "application/vnd.openxmlformats-officedocument.wordprocessingml.document", ); const { data: maxRow } = await db .from("document_versions") .select("version_number") .eq("document_id", documentId) .order("version_number", { ascending: false }) .limit(1) .maybeSingle(); const nextVersionNumber = ((maxRow?.version_number as number | null) ?? 1) + 1; const { data: versionRow, error: verErr } = await db .from("document_versions") .insert({ document_id: documentId, storage_path: newPath, source: "assistant_edit", version_number: nextVersionNumber, display_name: docInfo.filename, }) .select("id") .single(); if (verErr || !versionRow) { return { ok: false, error: "Failed to record document version.", apply_report: applyReport }; } await db .from("documents") .update({ current_version_id: versionRow.id }) .eq("id", documentId); return { ok: true, version_id: versionRow.id, version_number: nextVersionNumber, storage_path: newPath, download_url: buildDownloadUrl(newPath, docInfo.filename), apply_report: applyReport, commented_bytes: commentedBytes, }; } catch (err) { console.error("[runCommentDocumentBatch] error:", err); return { ok: false, error: err instanceof Error ? err.message : String(err) }; } } type DirectInsertTextOperation = { op: "insert_text"; location?: | { type: "document_end" } | { type: "before_block"; block_id?: string } | { type: "after_block"; block_id?: string } | { type: "at_offset"; block_id?: string; offset?: number }; text?: string; reason?: string; }; type DirectReplaceTextOperation = { op: "replace_text"; block_id?: string; find?: string; replacement?: string; occurrence?: number; reason?: string; }; type DirectRewriteBlockOperation = { op: "rewrite_block"; block_id?: string; new_text?: string; reason?: string; }; type DirectDeleteTextOperation = { op: "delete_text"; block_id?: string; start_offset?: number; end_offset?: number; find?: string; occurrence?: number; reason?: string; }; type DirectAddCommentOperation = { op: "add_comment"; block_id?: string; comment?: string; target_text?: string; anchor_mode?: "paragraph" | "exact"; reason?: string; }; type DirectDocumentOperation = | DirectInsertTextOperation | DirectReplaceTextOperation | DirectRewriteBlockOperation | DirectDeleteTextOperation | DirectAddCommentOperation; type DirectNormalizedComment = { operationIndex: number; block_id: string; comment: string; target_text: string; anchor_mode: "paragraph" | "exact"; reason?: string; anchor: CanonicalAnchor; }; type DirectOperationNormalizationResult = { trackedOperations: EditOperation[]; comments: DirectNormalizedComment[]; errors: { index: number; reason: string }[]; }; function isObjectRecord(value: unknown): value is Record { return !!value && typeof value === "object" && !Array.isArray(value); } function asString(value: unknown): string | undefined { return typeof value === "string" ? value : undefined; } function asTrimmedString(value: unknown): string | undefined { const s = asString(value); if (!s) return undefined; const trimmed = s.trim(); return trimmed ? trimmed : undefined; } function asInteger(value: unknown): number | undefined { return typeof value === "number" && Number.isFinite(value) ? Math.floor(value) : undefined; } function findNthDirectOccurrence(text: string, needle: string, occurrence: number): number | null { if (!needle) return null; let from = 0; let seen = 0; while (from <= text.length - needle.length) { const pos = text.indexOf(needle, from); if (pos < 0) return null; if (seen === occurrence) return pos; seen += 1; from = pos + Math.max(1, needle.length); } return null; } function isDirectEditRedZone(anchor: CanonicalAnchor | undefined): { blocked: true; reason: string } | { blocked: false } { if (!anchor) return { blocked: false }; if (!anchor.anchor_eligible) { return { blocked: true, reason: `Block '${anchor.block_id}' is in a navigation region and is not eligible for edits.`, }; } const styleId = `${String((anchor as any).style_id ?? "")} ${String((anchor as any).style_name ?? "")}`.toLowerCase(); const preview = String((anchor as any).text_preview ?? "").toLowerCase(); const redZoneStyleFragments = [ "toc", "toc1", "toc2", "toc3", "toc4", "tocheading", "tocentry", ]; const redZoneTextFragments = [ "table of contents", "signature page", "execution page", "cover page", ]; if (redZoneStyleFragments.some((fragment) => styleId.includes(fragment))) { return { blocked: true, reason: "Direct tracked edits to table-of-contents blocks are not allowed.", }; } if (redZoneTextFragments.some((fragment) => preview.includes(fragment))) { return { blocked: true, reason: "Direct tracked edits to cover, TOC, signature, or execution page blocks are not allowed.", }; } return { blocked: false }; } function normalizeDirectSemanticOperations( rawOperations: unknown, surface: Awaited>, ): DirectOperationNormalizationResult { const trackedOperations: EditOperation[] = []; const comments: DirectNormalizedComment[] = []; const errors: { index: number; reason: string }[] = []; if (!Array.isArray(rawOperations)) { return { trackedOperations, comments, errors: [{ index: 0, reason: "operations must be an array." }], }; } if (rawOperations.length === 0) { return { trackedOperations, comments, errors: [{ index: 0, reason: "operations must contain at least one operation." }], }; } const blockById = new Map(surface.blocks.map((block) => [block.id, block])); const anchorByBlockId = new Map(surface.anchors.map((anchor) => [anchor.block_id, anchor])); for (let i = 0; i < rawOperations.length; i++) { const raw = rawOperations[i]; if (!isObjectRecord(raw)) { errors.push({ index: i, reason: "Operation must be an object." }); continue; } const op = asString(raw.op); if (op === "insert_text") { const text = asTrimmedString(raw.text); const location = raw.location; if (!text) { errors.push({ index: i, reason: "insert_text requires non-empty text." }); continue; } if (!isObjectRecord(location)) { errors.push({ index: i, reason: "insert_text requires a location object." }); continue; } const locationType = asString(location.type); const reason = asString(raw.reason); if (locationType === "document_end") { trackedOperations.push({ op: "insert_at_document_end", text, reason, }); continue; } const blockId = asTrimmedString(location.block_id); if (!blockId) { errors.push({ index: i, reason: `insert_text location '${locationType ?? "unknown"}' requires block_id.`, }); continue; } const block = blockById.get(blockId); const anchor = anchorByBlockId.get(blockId); if (!block) { errors.push({ index: i, reason: `Unknown block_id '${blockId}'. Call inspect_document or search_document and retry with a current block_id.`, }); continue; } const redZone = isDirectEditRedZone(anchor); if (redZone.blocked) { errors.push({ index: i, reason: redZone.reason }); continue; } if (locationType === "before_block") { trackedOperations.push({ op: "insert_before_block", block_id: blockId, text, reason, }); continue; } if (locationType === "after_block") { trackedOperations.push({ op: "insert_after_block", block_id: blockId, text, reason, }); continue; } if (locationType === "at_offset") { const offset = asInteger(location.offset); if (offset == null || offset < 0 || offset > block.length) { errors.push({ index: i, reason: `insert_text at_offset requires offset between 0 and ${block.length}.`, }); continue; } trackedOperations.push({ op: "insert_at_offset", block_id: blockId, offset, text, reason, }); continue; } errors.push({ index: i, reason: `Unsupported insert_text location type '${locationType ?? "missing"}'.`, }); continue; } if (op === "replace_text") { const blockId = asTrimmedString(raw.block_id); const find = asString(raw.find); const replacement = asString(raw.replacement); const occurrence = asInteger(raw.occurrence); const reason = asString(raw.reason); if (!blockId || !find || replacement == null) { errors.push({ index: i, reason: "replace_text requires block_id, find, and replacement.", }); continue; } const block = blockById.get(blockId); const anchor = anchorByBlockId.get(blockId); if (!block) { errors.push({ index: i, reason: `Unknown block_id '${blockId}'. Call inspect_document or search_document and retry with a current block_id.`, }); continue; } const redZone = isDirectEditRedZone(anchor); if (redZone.blocked) { errors.push({ index: i, reason: redZone.reason }); continue; } trackedOperations.push({ op: "replace_text_in_block", block_id: blockId, find, occurrence: occurrence ?? 0, text: replacement, reason, }); continue; } if (op === "rewrite_block") { const blockId = asTrimmedString(raw.block_id); const newText = asString(raw.new_text); const reason = asString(raw.reason); if (!blockId || newText == null) { errors.push({ index: i, reason: "rewrite_block requires block_id and new_text.", }); continue; } const block = blockById.get(blockId); const anchor = anchorByBlockId.get(blockId); if (!block) { errors.push({ index: i, reason: `Unknown block_id '${blockId}'. Call inspect_document or search_document and retry with a current block_id.`, }); continue; } const redZone = isDirectEditRedZone(anchor); if (redZone.blocked) { errors.push({ index: i, reason: redZone.reason }); continue; } trackedOperations.push({ op: "replace_range", block_id: blockId, start_offset: 0, end_offset: block.length, text: newText, reason, }); continue; } if (op === "delete_text") { const blockId = asTrimmedString(raw.block_id); const reason = asString(raw.reason); if (!blockId) { errors.push({ index: i, reason: "delete_text requires block_id.", }); continue; } const block = blockById.get(blockId); const anchor = anchorByBlockId.get(blockId); if (!block) { errors.push({ index: i, reason: `Unknown block_id '${blockId}'. Call inspect_document or search_document and retry with a current block_id.`, }); continue; } const redZone = isDirectEditRedZone(anchor); if (redZone.blocked) { errors.push({ index: i, reason: redZone.reason }); continue; } const find = asString(raw.find); if (find) { const occurrence = Math.max(0, asInteger(raw.occurrence) ?? 0); const start = findNthDirectOccurrence(block.text, find, occurrence); if (start == null) { errors.push({ index: i, reason: `Could not find occurrence ${occurrence} of '${find.slice(0, 80)}' in block ${blockId}.`, }); continue; } trackedOperations.push({ op: "delete_range", block_id: blockId, start_offset: start, end_offset: start + find.length, reason, }); continue; } const startOffset = asInteger(raw.start_offset); const endOffset = asInteger(raw.end_offset); if ( startOffset == null || endOffset == null || startOffset < 0 || endOffset < startOffset || endOffset > block.length ) { errors.push({ index: i, reason: `delete_text requires either find or a valid start_offset/end_offset range between 0 and ${block.length}.`, }); continue; } trackedOperations.push({ op: "delete_range", block_id: blockId, start_offset: startOffset, end_offset: endOffset, reason, }); continue; } if (op === "add_comment") { const blockId = asTrimmedString(raw.block_id); const comment = asTrimmedString(raw.comment); const targetText = asString(raw.target_text) ?? ""; const anchorModeRaw = asString(raw.anchor_mode); const anchorMode: "paragraph" | "exact" = anchorModeRaw === "exact" ? "exact" : "paragraph"; if (!blockId || !comment) { errors.push({ index: i, reason: "add_comment requires block_id and comment.", }); continue; } const block = blockById.get(blockId); const anchor = anchorByBlockId.get(blockId); if (!block || !anchor) { errors.push({ index: i, reason: `Unknown block_id '${blockId}'. Call inspect_document or search_document and retry with a current block_id.`, }); continue; } if (!anchor.commentable) { errors.push({ index: i, reason: `Block '${blockId}' is not safely commentable.`, }); continue; } if (!anchor.anchor_eligible) { errors.push({ index: i, reason: `Block '${blockId}' is in a navigation region and is not eligible for comments.`, }); continue; } if (anchorMode === "exact" && !targetText.trim()) { errors.push({ index: i, reason: "add_comment with anchor_mode='exact' requires target_text.", }); continue; } comments.push({ operationIndex: i, block_id: blockId, comment, target_text: targetText, anchor_mode: anchorMode, reason: asString(raw.reason), anchor, }); continue; } errors.push({ index: i, reason: `Unsupported operation '${op ?? "missing"}'. Valid operations are insert_text, replace_text, rewrite_block, delete_text, add_comment.`, }); } return { trackedOperations, comments, errors }; } function renderDirectInspectSurface(params: { surface: Awaited>; scope: string; query?: string; }): string { const { surface, scope, query } = params; const lines: string[] = []; const pushBlock = (index: number) => { const block = surface.blocks[index]; const anchor = surface.anchors[index]; if (!block) return; const container = anchor?.container === "table_cell" ? " container=table_cell" : ""; const eligibility = anchor?.anchor_eligible === false ? ` anchor_eligible=false reason=${anchor.anchor_rejection_reason ?? "navigation"}` : " anchor_eligible=true"; lines.push( `[[${block.id} type=${block.kind}${container} chars=${block.length} index=${index}${eligibility}]]`, ); lines.push(block.text); lines.push(""); }; lines.push(`[DOCX_EDIT_SURFACE surface_id=${surface.surfaceId}]`); lines.push(`[[DOC_END after=${surface.documentEnd.afterBlockId ?? "NONE"}]]`); lines.push(""); if (scope === "full") { for (let i = 0; i < surface.blocks.length; i++) pushBlock(i); return lines.join("\n"); } if (scope === "tail") { const start = Math.max(0, surface.blocks.length - 24); for (let i = start; i < surface.blocks.length; i++) pushBlock(i); return lines.join("\n"); } if (scope === "headings") { const headingRe = /^\s*((section|clause|article|schedule|exhibit|annex|appendix)\s+\d+|\d+(\.\d+)*\s+|[A-Z][A-Z0-9 ,/&()\-]{6,})/i; for (let i = 0; i < surface.blocks.length; i++) { if (surface.anchors[i]?.anchor_eligible === false) continue; const text = surface.blocks[i].text.trim(); if (text && text.length <= 180 && headingRe.test(text)) { pushBlock(i); } } if (lines.length <= 3) { lines.push("No likely headings detected. Use scope='summary' or search_document."); } return lines.join("\n"); } if (scope === "around_search_hit") { const q = query?.trim() ?? ""; if (!q) { lines.push("scope='around_search_hit' requires query."); return lines.join("\n"); } const search = findInDocxEditSurface({ surface, query: q, maxResults: 8, contextChars: 120, }); if (search.hits.length === 0) { lines.push(`No hits for query: ${q}`); return lines.join("\n"); } const indices = new Set(); for (const hit of search.hits) { const idx = surface.blocks.findIndex((b) => b.id === hit.block_id); if (idx >= 0) { indices.add(Math.max(0, idx - 1)); indices.add(idx); indices.add(Math.min(surface.blocks.length - 1, idx + 1)); } } for (const idx of Array.from(indices).sort((a, b) => a - b)) { pushBlock(idx); } lines.push(`[SEARCH_SUMMARY query=${JSON.stringify(q)} total_matches=${search.totalMatches} returned_hits=${search.hits.length}]`); return lines.join("\n"); } const headCount = Math.min(12, surface.blocks.length); for (let i = 0; i < headCount; i++) pushBlock(i); if (surface.blocks.length > headCount) { lines.push(`... ${surface.blocks.length - headCount} additional block(s) omitted. Use scope='full', scope='tail', scope='headings', or search_document to inspect more.`); } return lines.join("\n"); } function buildDirectWriterCommentItems(params: { docLabel: string; toolCallId: string; comments: DirectNormalizedComment[]; }): WriterCommentItem[] { return params.comments.map((comment, index) => ({ id: `${params.toolCallId}:direct-comment:${index}`, args: { doc_id: params.docLabel, block_id: comment.block_id, comment: comment.comment, target_text: comment.target_text, anchor_mode: comment.anchor_mode, before_anchor: "", after_anchor: "", comment_key: `direct-comment-${index + 1}`, type: "COMMENT", }, anchor: comment.anchor, anchor_text: comment.target_text || comment.anchor.text_preview, section_heading: null, source: "plan_comments_on_document", })); } async function applyDirectCommentsToDocxBytes(params: { userId: string; documentName: string; bytes: Buffer; comments: WriterCommentItem[]; trace?: RunTree | null; }): Promise<{ ok: true; bytes: Buffer; apply_report: any[]; } | { ok: false; error: string; apply_report?: any[]; }> { const { userId, documentName, bytes, comments, trace } = params; if (comments.length === 0) { return { ok: true, bytes, apply_report: [] }; } try { const pythonUrl = process.env.PYTHON_SERVICE_URL || "http://localhost:8000"; const response = await fetch(`${pythonUrl}/api/apply-comments`, { method: "POST", headers: { "Content-Type": "application/json", "x-user-id": userId, ...traceHeaders(trace), }, body: JSON.stringify({ document_bytes_b64: Buffer.from(bytes).toString("base64"), document_name: documentName, track_changes: false, findings: comments.map((item) => ({ id: item.id, model_block_id: item.args.block_id, comment_key: item.args.comment_key, risk_level: "info", clause_type: "DirectEditComment", issue: "Direct document edit comment", comment: item.args.comment, review_reasoning: { clause_section: "N/A", violated_standard: "N/A", zamil_exposure: "Direct user-requested comment", }, target_text: item.args.target_text, anchor_mode: item.args.anchor_mode, start_paragraph_id: item.args.block_id, end_paragraph_id: item.args.block_id, start_anchor: item.args.target_text ? item.args.target_text.slice(0, 220) : "", end_anchor: item.args.target_text ? item.args.target_text.slice(-220) : "", before_anchor: item.args.before_anchor || "", after_anchor: item.args.after_anchor || "", confidence: 1.0, stable_id: item.anchor.stable_id, native_para_id: item.anchor.native_para_id, text_hash: item.anchor.text_hash, xml_path: item.anchor.xml_path, op_type: "COMMENT", new_text: null, insert_text: null, position: null, rewrite_mode: null, old_span: null, new_span: null, bookmark_name: item.anchor?.bookmark_name ?? null, })), }), }); if (!response.ok) { const errData = await response.json().catch(() => ({})); return { ok: false, error: errData.detail || `Python service error: ${response.statusText}`, }; } const data = await response.json(); const applyReport = Array.isArray(data.apply_report) ? data.apply_report : []; const appliedCount = applyReport.filter((r: any) => r.status === "applied").length; const commentedBytesB64 = typeof data.commented_bytes_b64 === "string" ? data.commented_bytes_b64 : typeof data.document_bytes_b64 === "string" ? data.document_bytes_b64 : ""; if (appliedCount === 0) { return { ok: false, error: "No direct comments were inserted.", apply_report: applyReport, }; } if (!commentedBytesB64) { return { ok: false, error: "Python service did not return commented document bytes.", apply_report: applyReport, }; } return { ok: true, bytes: Buffer.from(commentedBytesB64, "base64"), apply_report: applyReport, }; } catch (err) { console.error("[applyDirectCommentsToDocxBytes] error:", err); return { ok: false, error: err instanceof Error ? err.message : String(err), }; } } async function handleInspectDocumentTool(params: { tc: ToolCall; args: Record; docStore: DocStore; docIndex: DocIndex; db: ReturnType; write: (s: string) => void; anchorManifestState?: AnchorManifestState; trace?: RunTree | null; }): Promise<{ toolResult: { role: "tool"; tool_call_id: string; content: string }; docRead?: { filename: string; document_id?: string }; }> { const { tc, args, docStore, docIndex, db, write, anchorManifestState, trace } = params; const rawDocId = asString(args.doc_id) ?? ""; const docLabel = resolveDocLabel(rawDocId, docStore, docIndex) ?? rawDocId; const scope = asString(args.scope) ?? "summary"; const query = asString(args.query); const docInfo = docStore.get(docLabel); const indexed = docIndex[docLabel]; if (!docInfo || !indexed) { return { toolResult: { role: "tool", tool_call_id: tc.id, content: JSON.stringify({ ok: false, error: "DOCUMENT_NOT_FOUND", message: `Document '${rawDocId}' was not found.`, }), }, }; } if (docInfo.file_type !== "docx") { return { toolResult: { role: "tool", tool_call_id: tc.id, content: JSON.stringify({ ok: false, error: "UNSUPPORTED_FILE_TYPE", message: "inspect_document only supports DOCX files in direct edit mode.", }), }, }; } write( `data: ${JSON.stringify({ type: "doc_read_start", filename: docInfo.filename, document_id: indexed.document_id, })}\n\n`, ); const current = await loadCurrentVersionBytes(indexed.document_id, db); if (!current) { write( `data: ${JSON.stringify({ type: "doc_read", filename: docInfo.filename, document_id: indexed.document_id, })}\n\n`, ); return { toolResult: { role: "tool", tool_call_id: tc.id, content: JSON.stringify({ ok: false, error: "DOCUMENT_BYTES_UNAVAILABLE", message: "Could not load current DOCX bytes.", }), }, }; } const surface = await buildDocxEditSurface(current.bytes, { versionId: current.version_id, }); rememberAnchorManifest( anchorManifestState, docLabel, indexed.document_id, surface, ); if (trace) { const renderedForTrace = renderDocxEditSurfaceForModel(surface); const surfaceTrace = buildDocumentSurfaceTracePayload({ docLabel, documentId: indexed.document_id, filename: docInfo.filename, surfaceId: surface.surfaceId, text: renderedForTrace, }); const surfTrace = createTraceChild(trace, { name: "inspect_document.surface", run_type: "chain", inputs: surfaceTrace, tags: ["document_surface_for_model", "direct_docx_edit"], }); void endTrace(surfTrace, { outputs: { char_count: renderedForTrace.length, block_count: surface.anchors.length, surface_id: surface.surfaceId, scope, }, }); } const text = renderDirectInspectSurface({ surface, scope, query, }); write( `data: ${JSON.stringify({ type: "doc_read", filename: docInfo.filename, document_id: indexed.document_id, })}\n\n`, ); return { docRead: { filename: docInfo.filename, document_id: indexed.document_id, }, toolResult: { role: "tool", tool_call_id: tc.id, content: JSON.stringify({ ok: true, doc_id: docLabel, document_id: indexed.document_id, filename: docInfo.filename, version_id: current.version_id, version_number: current.version_number, surface_id: surface.surfaceId, scope, block_count: surface.blocks.length, document_end: surface.documentEnd, content: text, }), }, }; } async function handleSearchDocumentTool(params: { tc: ToolCall; args: Record; docStore: DocStore; docIndex: DocIndex; db: ReturnType; write: (s: string) => void; anchorManifestState?: AnchorManifestState; }): Promise<{ toolResult: { role: "tool"; tool_call_id: string; content: string }; docFound?: { filename: string; query: string; total_matches: number }; }> { const { tc, args, docStore, docIndex, db, write, anchorManifestState } = params; const rawDocId = asString(args.doc_id) ?? ""; const docLabel = resolveDocLabel(rawDocId, docStore, docIndex) ?? rawDocId; const query = asString(args.query) ?? ""; const maxResults = asInteger(args.max_results); const docInfo = docStore.get(docLabel); const indexed = docIndex[docLabel]; if (!docInfo || !indexed) { return { toolResult: { role: "tool", tool_call_id: tc.id, content: JSON.stringify({ ok: false, error: "DOCUMENT_NOT_FOUND", message: `Document '${rawDocId}' was not found.`, }), }, }; } if (docInfo.file_type !== "docx") { return { toolResult: { role: "tool", tool_call_id: tc.id, content: JSON.stringify({ ok: false, error: "UNSUPPORTED_FILE_TYPE", message: "search_document only supports DOCX files in direct edit mode.", }), }, }; } const content = await findInDocumentContent({ docLabel, query, maxResults, contextChars: 140, docStore, write, docIndex, db, anchorManifestState, }); let totalMatches = 0; try { const parsed = JSON.parse(content) as { total_matches?: number }; totalMatches = parsed.total_matches ?? 0; } catch { totalMatches = 0; } return { docFound: { filename: docInfo.filename, query, total_matches: totalMatches, }, toolResult: { role: "tool", tool_call_id: tc.id, content, }, }; } async function handleEditDocumentTool(params: { tc: ToolCall; args: Record; docStore: DocStore; docIndex: DocIndex; userId: string; db: ReturnType; write: (s: string) => void; trace?: RunTree | null; anchorManifestState?: AnchorManifestState; docsEdited: DocEditedResult[]; }): Promise<{ role: "tool"; tool_call_id: string; content: string }> { const { tc, args, docStore, docIndex, userId, db, write, trace, anchorManifestState, docsEdited, } = params; const rawDocId = asString(args.doc_id) ?? ""; const docLabel = resolveDocLabel(rawDocId, docStore, docIndex) ?? rawDocId; const userVisibleSummary = asTrimmedString(args.user_visible_summary) ?? "Applied the requested document edit."; const docInfo = docStore.get(docLabel); const indexed = docIndex[docLabel]; const fail = (payload: Record) => ({ role: "tool" as const, tool_call_id: tc.id, content: JSON.stringify({ ok: false, doc_id: docLabel, ...payload, }), }); if (!docInfo || !indexed) { return fail({ error: "DOCUMENT_NOT_FOUND", message: `Document '${rawDocId}' was not found.`, }); } if (docInfo.file_type !== "docx") { return fail({ error: "UNSUPPORTED_FILE_TYPE", message: "edit_document only supports DOCX files.", }); } const current = await loadCurrentVersionBytes(indexed.document_id, db); if (!current) { return fail({ error: "DOCUMENT_BYTES_UNAVAILABLE", message: "Could not load current DOCX bytes.", }); } const surface = await buildDocxEditSurface(current.bytes, { versionId: current.version_id, }); rememberAnchorManifest( anchorManifestState, docLabel, indexed.document_id, surface, ); const normalized = normalizeDirectSemanticOperations(args.operations, surface); if (normalized.errors.length > 0) { return fail({ error: "VALIDATION_FAILED", needs_correction: true, errors: normalized.errors, hint: "Call inspect_document or search_document and retry with valid locations, block IDs, offsets, and text.", }); } if ( normalized.trackedOperations.length === 0 && normalized.comments.length === 0 ) { return fail({ error: "NO_OPERATIONS", needs_correction: true, message: "No valid operations were supplied.", }); } const writerComments = buildDirectWriterCommentItems({ docLabel, toolCallId: tc.id, comments: normalized.comments, }); write( `data: ${JSON.stringify({ type: "doc_edited_start", filename: docInfo.filename, })}\n\n`, ); if ( normalized.comments.length > 0 && normalized.trackedOperations.length === 0 ) { const batchResult = await runCommentDocumentBatch({ userId, documentId: indexed.document_id, docLabel, docInfo, current, comments: writerComments, db, trace, trackChanges: false, }); if (batchResult.ok && batchResult.download_url && batchResult.version_id) { if (docIndex[docLabel]) { docIndex[docLabel] = { ...docIndex[docLabel], version_id: batchResult.version_id, version_number: batchResult.version_number ?? null, }; } const currentDocStore = docStore.get(docLabel); if (currentDocStore && batchResult.storage_path) { docStore.set(docLabel, { ...currentDocStore, storage_path: batchResult.storage_path, }); } const applyReport = batchResult.apply_report ?? []; const payload: DocEditedResult = { filename: docInfo.filename, document_id: indexed.document_id, version_id: batchResult.version_id, version_number: batchResult.version_number ?? null, download_url: batchResult.download_url, annotations: [], apply_report: applyReport, apply_summary: buildApplySummary(applyReport), }; docsEdited.push(payload); write( `data: ${JSON.stringify({ type: "doc_edited", ...payload, })}\n\n`, ); return { role: "tool", tool_call_id: tc.id, content: JSON.stringify({ ok: true, doc_id: docLabel, document_id: indexed.document_id, version_id: batchResult.version_id, version_number: batchResult.version_number ?? null, applied_operations: normalized.comments.length, summary: userVisibleSummary, apply_report: applyReport, }), }; } const applyReport = batchResult.apply_report ?? []; write( `data: ${JSON.stringify({ type: "doc_edited", filename: docInfo.filename, document_id: indexed.document_id, version_id: "", version_number: null, download_url: "", annotations: [], apply_report: applyReport, apply_summary: buildApplySummary(applyReport), error: batchResult.error ?? "No comments were inserted.", })}\n\n`, ); return fail({ error: "COMMENT_WRITE_FAILED", message: batchResult.error ?? "No comments were inserted.", apply_report: applyReport, }); } let baseBytesOverride: Buffer | undefined; let extraApplyReport: any[] | undefined; let extraApplySummary: | { total: number; applied: number; failed: number; failures: { id: string; target_text: string; message: string; paragraph_id: string | null; }[]; } | undefined; if (writerComments.length > 0) { const commented = await applyDirectCommentsToDocxBytes({ userId, documentName: docInfo.filename, bytes: current.bytes, comments: writerComments, trace, }); if (!commented.ok) { const applyReport = commented.apply_report ?? []; write( `data: ${JSON.stringify({ type: "doc_edited", filename: docInfo.filename, document_id: indexed.document_id, version_id: "", version_number: null, download_url: "", annotations: [], apply_report: applyReport, apply_summary: buildApplySummary(applyReport), error: commented.error, })}\n\n`, ); return fail({ error: "COMMENT_WRITE_FAILED", message: commented.error, apply_report: applyReport, }); } baseBytesOverride = commented.bytes; extraApplyReport = commented.apply_report; extraApplySummary = buildApplySummary(commented.apply_report); } const result = await runEditDocument({ documentId: indexed.document_id, userId, operations: normalized.trackedOperations, expectedSurfaceId: surface.surfaceId, db, baseBytesOverride, extraApplyReport, extraApplySummary, }); if (!result.ok) { write( `data: ${JSON.stringify({ type: "doc_edited", filename: docInfo.filename, document_id: indexed.document_id, version_id: "", version_number: null, download_url: "", annotations: [], error: result.error, })}\n\n`, ); return fail({ error: "EDIT_WRITE_FAILED", message: result.error, }); } if (docIndex[docLabel]) { docIndex[docLabel] = { ...docIndex[docLabel], version_id: result.version_id, version_number: result.version_number ?? null, }; } const currentDocStore = docStore.get(docLabel); if (currentDocStore && result.storage_path) { docStore.set(docLabel, { ...currentDocStore, storage_path: result.storage_path, }); } const payload: DocEditedResult = { filename: docInfo.filename, document_id: indexed.document_id, version_id: result.version_id, version_number: result.version_number ?? null, download_url: result.download_url, annotations: result.annotations, apply_report: result.apply_report, apply_summary: result.apply_summary, edit_verification: result.edit_verification, }; docsEdited.push(payload); write( `data: ${JSON.stringify({ type: "doc_edited", ...payload, })}\n\n`, ); return { role: "tool", tool_call_id: tc.id, content: JSON.stringify({ ok: true, doc_id: docLabel, document_id: indexed.document_id, version_id: result.version_id, version_number: result.version_number ?? null, applied_operations: normalized.trackedOperations.length + normalized.comments.length, tracked_operations: normalized.trackedOperations.length, comment_operations: normalized.comments.length, summary: userVisibleSummary, errors: result.errors, apply_report: result.apply_report, edit_verification: result.edit_verification, }), }; } async function processCommentToolCallsBatch(params: { commentCalls: ToolCall[]; docStore: DocStore; docIndex: DocIndex; userId: string; db: ReturnType; write: (s: string) => void; trace?: RunTree | null; commentAttemptState?: CommentAttemptState; anchorManifestState?: AnchorManifestState; toolResults: unknown[]; docsEdited: DocEditedResult[]; commentVerificationRows: CommentVerificationRow[]; }): Promise { const { commentCalls, docStore, docIndex, userId, db, write, trace, commentAttemptState, anchorManifestState, toolResults, docsEdited, commentVerificationRows, } = params; const preparedByDoc = new Map(); const resultByToolCallId = new Map(); const addReportResult = ( tc: ToolCall, report: Record, opts: { docId?: string; documentId?: string; blockId?: string; commentKey?: string; readSerial?: number; error?: string; } = {}, ) => { const status = typeof report.status === "string" ? report.status : "not_applied"; const failureCode = typeof report.failure_code === "string" ? report.failure_code : undefined; const content = { ok: status === "applied", error: opts.error ?? (status === "applied" ? undefined : failureCode ?? "COMMENT_NOT_APPLIED"), message: report.message, doc_id: opts.docId, document_id: opts.documentId, block_id: opts.blockId, comment_key: opts.commentKey, apply_report: [report], }; resultByToolCallId.set(tc.id, JSON.stringify(content)); commentVerificationRows.push( verificationRowFromReport({ toolCallId: tc.id, docId: opts.docId, documentId: opts.documentId, blockId: opts.blockId, commentKey: opts.commentKey, report, }), ); recordCommentAttempt({ state: commentAttemptState, commentKey: opts.commentKey, toolCallId: tc.id, report, readSerial: opts.readSerial, }); }; for (const tc of commentCalls) { let args: Record = {}; try { args = JSON.parse(tc.function.arguments || "{}"); } catch { /* validation below will report the missing required fields */ } const sanitizedArgs = sanitizeCommentArgs(args); const validation = validateCommentArgs(sanitizedArgs, docStore); if (!validation.valid) { const report = makeCommentFailureReport({ id: tc.id, status: "validation_failed", failureCode: "VALIDATION_FAILED", message: validation.errors.join("; "), opType: typeof sanitizedArgs.type === "string" ? sanitizedArgs.type : undefined, }); addReportResult(tc, report, { error: "VALIDATION_FAILED", }); continue; } const docIdToUse = validation.validated.doc_id; const docLabel = resolveDocLabel(docIdToUse, docStore, docIndex) ?? docIdToUse; const docInfo = docStore.get(docLabel); const indexed = docIndex[docLabel]; const commentKey = makeCommentKey(validation.validated); const argsWithKey: ValidatedCommentArgs = { ...validation.validated, doc_id: docIdToUse, comment_key: commentKey, }; if (!docInfo || !indexed) { const message = `Document '${docIdToUse}' is registered in chat but could not be indexed.`; const report = makeCommentFailureReport({ id: tc.id, commentKey, blockId: validation.validated.block_id, status: "document_not_found", failureCode: "DOCUMENT_NOT_FOUND", message, opType: typeof sanitizedArgs.type === "string" ? sanitizedArgs.type : undefined, }); addReportResult(tc, report, { docId: docIdToUse, blockId: validation.validated.block_id, commentKey, error: "DOCUMENT_NOT_FOUND", }); continue; } if (docInfo.file_type !== "docx") { const report = makeCommentFailureReport({ id: tc.id, commentKey, blockId: validation.validated.block_id, failureCode: "UNSUPPORTED_DOCUMENT_TYPE", message: "comment_on_document only supports .docx files.", opType: typeof sanitizedArgs.type === "string" ? sanitizedArgs.type : undefined, }); addReportResult(tc, report, { docId: docIdToUse, documentId: indexed.document_id, blockId: validation.validated.block_id, commentKey, error: "UNSUPPORTED_DOCUMENT_TYPE", }); continue; } const bucket = preparedByDoc.get(docLabel) ?? []; bucket.push({ tc, docLabel, docInfo, indexed, args: argsWithKey, op_type: typeof sanitizedArgs.type === "string" ? sanitizedArgs.type : undefined, }); preparedByDoc.set(docLabel, bucket); } for (const [docLabel, preparedCalls] of preparedByDoc) { const first = preparedCalls[0]; const current = await loadCurrentVersionBytes(first.indexed.document_id, db); if (!current) { for (const prepared of preparedCalls) { const report = makeCommentFailureReport({ id: prepared.tc.id, commentKey: prepared.args.comment_key, blockId: prepared.args.block_id, failureCode: "DOCUMENT_BYTES_UNAVAILABLE", message: "Could not load current document bytes.", opType: prepared.op_type, }); addReportResult(prepared.tc, report, { docId: prepared.args.doc_id, documentId: prepared.indexed.document_id, blockId: prepared.args.block_id, commentKey: prepared.args.comment_key, error: "DOCUMENT_BYTES_UNAVAILABLE", }); } continue; } const currentSurface = await buildDocxEditSurface(current.bytes, { versionId: current.version_id, }); const manifest = anchorManifestState?.byDocLabel.get(docLabel); const validationTrace = createTraceChild(trace, { name: "comment_batch_validation", run_type: "chain", inputs: { doc_id: docLabel, document_id: first.indexed.document_id, comments_attempted: preparedCalls.length, manifest_surface_id: manifest?.surfaceId ?? null, current_surface_id: currentSurface.surfaceId, }, tags: ["verification", "comments"], }); const validForWriter: Array = []; const prevalidationFailures: Record[] = []; for (const prepared of preparedCalls) { const validated = validatePreparedCommentAnchor({ prepared, manifest, currentSurface, commentAttemptState, }); if (validated.ok) { validForWriter.push({ ...prepared, anchor: validated.anchor, readSerial: validated.readSerial, }); continue; } const report = validated.report as Record; prevalidationFailures.push(report); addReportResult(prepared.tc, report, { docId: prepared.args.doc_id, documentId: prepared.indexed.document_id, blockId: prepared.args.block_id, commentKey: prepared.args.comment_key, readSerial: validated.readSerial, error: String(report.failure_code ?? "COMMENT_PREVALIDATION_FAILED"), }); } await endTrace(validationTrace, { outputs: { commentsAttempted: preparedCalls.length, commentsValid: validForWriter.length, commentsRejected: prevalidationFailures.length, rejected: prevalidationFailures, }, error: prevalidationFailures.length > 0 ? `${prevalidationFailures.length} comment anchor(s) failed prevalidation` : null, }); if (validForWriter.length === 0) { continue; } write(`data: ${JSON.stringify({ type: "doc_edited_start", filename: first.docInfo.filename, })}\n\n`); const batchResult = await runCommentDocumentBatch({ userId, documentId: first.indexed.document_id, docLabel, docInfo: first.docInfo, current, comments: validForWriter.map((v) => ({ id: v.tc.id, args: v.args, anchor: v.anchor, })), db, trace, }); const writerReport = batchResult.apply_report ?? []; const reportById = new Map(); for (const report of writerReport) { if (typeof report?.id === "string") reportById.set(report.id, report); } for (const prepared of validForWriter) { const report = reportById.get(prepared.tc.id) ?? makeCommentFailureReport({ id: prepared.tc.id, commentKey: prepared.args.comment_key, blockId: prepared.args.block_id, failureCode: batchResult.error ? "backend_error" : "comment_result_missing", message: batchResult.error ?? "The comment writer did not return a result for this comment.", anchor: prepared.anchor, opType: prepared.op_type, }); addReportResult(prepared.tc, report, { docId: prepared.args.doc_id, documentId: prepared.indexed.document_id, blockId: prepared.args.block_id, commentKey: prepared.args.comment_key, readSerial: prepared.readSerial, error: report.status === "applied" ? undefined : String(report.failure_code ?? batchResult.error ?? "COMMENT_NOT_APPLIED"), }); } if (batchResult.ok && batchResult.download_url && batchResult.version_id) { const combinedApplyReport = [...prevalidationFailures, ...writerReport]; if (docIndex[docLabel]) { docIndex[docLabel] = { ...docIndex[docLabel], version_id: batchResult.version_id, version_number: batchResult.version_number ?? null, }; } const currentDocStore = docStore.get(docLabel); if (currentDocStore && batchResult.storage_path) { docStore.set(docLabel, { ...currentDocStore, storage_path: batchResult.storage_path, }); } // Phase 9: Post-write verification const appliedCount = combinedApplyReport.filter((r: any) => r.status === "applied").length; let commentVerification: CommentBatchVerification | undefined; if (batchResult.commented_bytes) { commentVerification = await verifyCommentBatchPostWrite({ originalBytes: current.bytes, commentedBytes: batchResult.commented_bytes, expectedAppliedCount: appliedCount, appliedReports: combinedApplyReport, }); } // Phase 10: Refresh manifest after comment-only write // For comment-only writes, body text should not change, so we can safely rebuild the manifest. if (batchResult.commented_bytes && commentVerification?.body_text_unchanged) { const newSurface = await buildDocxEditSurface(batchResult.commented_bytes, { versionId: batchResult.version_id, }); rememberAnchorManifest(anchorManifestState, docLabel, first.indexed.document_id, newSurface); console.log( `[processCommentToolCallsBatch] manifest refreshed for ${docLabel} after comment-only write. ` + `old_revision=${currentSurface.surfaceId} new_revision=${newSurface.surfaceId} ` + `entries=${newSurface.anchors.length} applied=${appliedCount}` ); } else { // If body text changed or we couldn't verify, delete the manifest to force a re-read anchorManifestState?.byDocLabel.delete(docLabel); } const payload: DocEditedResult = { filename: first.docInfo.filename, document_id: first.indexed.document_id, version_id: batchResult.version_id, version_number: batchResult.version_number ?? null, download_url: batchResult.download_url, annotations: [], apply_report: combinedApplyReport, apply_summary: buildApplySummary(combinedApplyReport), }; docsEdited.push(payload); write(`data: ${JSON.stringify({ type: "doc_edited", ...payload, })}\n\n`); for (const prepared of validForWriter) { const report = reportById.get(prepared.tc.id); if (!report) continue; const existing = resultByToolCallId.get(prepared.tc.id); const parsed = existing ? JSON.parse(existing) as Record : {}; resultByToolCallId.set(prepared.tc.id, JSON.stringify({ ...parsed, ok: report.status === "applied", version_id: batchResult.version_id, version_number: batchResult.version_number ?? null, download_url: batchResult.download_url, })); } } else { write(`data: ${JSON.stringify({ type: "doc_edited", filename: first.docInfo.filename, document_id: first.indexed.document_id, version_id: "", version_number: null, download_url: "", annotations: [], apply_report: [...prevalidationFailures, ...writerReport], apply_summary: buildApplySummary([...prevalidationFailures, ...writerReport]), error: batchResult.error ?? "No comments were inserted.", })}\n\n`); } } for (const tc of commentCalls) { toolResults.push({ role: "tool", tool_call_id: tc.id, content: resultByToolCallId.get(tc.id) ?? JSON.stringify({ ok: false, error: "COMMENT_TOOL_INTERNAL_ERROR", message: "comment_on_document did not produce a result.", }), }); } } export async function runToolCalls( toolCalls: ToolCall[], docStore: DocStore, userId: string, db: ReturnType, write: (s: string) => void, workflowStore?: WorkflowStore, tabularStore?: TabularCellStore, docIndex?: DocIndex, turnEditState?: TurnEditState, projectId?: string | null, trace?: RunTree | null, commentAttemptState?: CommentAttemptState, anchorManifestState?: AnchorManifestState, pendingCommentPlanState?: PendingCommentPlanState, trackChanges?: boolean, turnDocumentState?: TurnDocumentState, ): Promise<{ toolResults: unknown[]; docsRead: { filename: string; document_id?: string }[]; docsFound: { filename: string; query: string; total_matches: number }[]; docsCreated: DocCreatedResult[]; docsReplicated: DocReplicatedResult[]; workflowsApplied: { workflow_id: string; title: string }[]; docsEdited: DocEditedResult[]; workflowFindings: WorkflowFinding[]; workflowRows: WorkflowFindingRow[]; }> { const toolResults: unknown[] = []; const docsRead: { filename: string; document_id?: string }[] = []; const docsFound: { filename: string; query: string; total_matches: number; }[] = []; const docsCreated: DocCreatedResult[] = []; const docsReplicated: DocReplicatedResult[] = []; const workflowsApplied: { workflow_id: string; title: string }[] = []; const docsEdited: DocEditedResult[] = []; const commentVerificationRows: CommentVerificationRow[] = []; const processedCommentToolCallIds = new Set(); const effectiveTurnDocumentState = turnDocumentState ?? createTurnDocumentState(); for (const tc of toolCalls) { let args: Record = {}; try { args = JSON.parse(tc.function.arguments || "{}"); } catch { /* ignore */ } if (tc.function.name === "inspect_document" && docIndex) { const result = await handleInspectDocumentTool({ tc, args, docStore, docIndex, db, write, anchorManifestState, trace, }); if (result.docRead) { docsRead.push(result.docRead); } toolResults.push(result.toolResult); continue; } if (tc.function.name === "search_document" && docIndex) { const result = await handleSearchDocumentTool({ tc, args, docStore, docIndex, db, write, anchorManifestState, }); if (result.docFound) { docsFound.push(result.docFound); } toolResults.push(result.toolResult); continue; } if (tc.function.name === "edit_document" && docIndex) { const result = await handleEditDocumentTool({ tc, args, docStore, docIndex, userId, db, write, trace, anchorManifestState, docsEdited, }); toolResults.push(result); continue; } if (tc.function.name === "read_document") { const rawDocId = args.doc_id as string; const docId = resolveDocLabel(rawDocId, docStore, docIndex) ?? rawDocId; const content = await readDocumentContentOncePerTurn({ docLabel: docId, docStore, write, docIndex, db, anchorManifestState, trace, turnDocumentState: effectiveTurnDocumentState, }); const filename = docStore.get(docId)?.filename; const documentId = docIndex?.[docId]?.document_id; if (filename && !isAlreadyReadToolResult(content)) { docsRead.push({ filename, document_id: documentId }); } toolResults.push({ role: "tool", tool_call_id: tc.id, content }); } else if (tc.function.name === "find_in_document") { const rawDocId = args.doc_id as string; const docId = resolveDocLabel(rawDocId, docStore, docIndex) ?? rawDocId; const query = (args.query as string) ?? ""; const maxResults = typeof args.max_results === "number" ? args.max_results : undefined; const contextChars = typeof args.context_chars === "number" ? args.context_chars : undefined; const anchorEligibleOnly = typeof args.anchor_eligible_only === "boolean" ? args.anchor_eligible_only : undefined; const content = await findInDocumentContent({ docLabel: docId, query, maxResults, contextChars, anchorEligibleOnly, docStore, write, docIndex, db, anchorManifestState, }); const filename = docStore.get(docId)?.filename; if (filename) { let totalMatches = 0; try { const parsed = JSON.parse(content) as { total_matches?: number; }; totalMatches = parsed.total_matches ?? 0; } catch { /* ignore — still record the find attempt */ } docsFound.push({ filename, query, total_matches: totalMatches, }); } toolResults.push({ role: "tool", tool_call_id: tc.id, content }); } else if (tc.function.name === "comment_on_document" && docIndex) { if (!processedCommentToolCallIds.has(tc.id)) { const commentCalls = toolCalls.filter( (call) => call.function.name === "comment_on_document", ); for (const call of commentCalls) { processedCommentToolCallIds.add(call.id); } await processCommentToolCallsBatch({ commentCalls, docStore, docIndex, userId, db, write, trace, commentAttemptState, anchorManifestState, toolResults, docsEdited, commentVerificationRows, }); } } else if (tc.function.name === "plan_comments_on_document" && docIndex) { const planResult = await handlePlanCommentsOnDocument({ tc, args, docStore, docIndex, userId, db, write, trace, anchorManifestState, pendingCommentPlanState: pendingCommentPlanState ?? new Map(), docsEdited, verificationRows: commentVerificationRows, trackChanges, }); toolResults.push(planResult); } else if ((tc.function.name === "correct_comment_anchors" || tc.function.name === "fix_comment_findings") && docIndex) { const correctResult = await handleCorrectCommentAnchors({ tc, args, docStore, docIndex, userId, db, write, trace, anchorManifestState, pendingCommentPlanState: pendingCommentPlanState ?? new Map(), docsEdited, verificationRows: commentVerificationRows, trackChanges, }); toolResults.push(correctResult); } else if (tc.function.name === "list_documents") { const list = Array.from(docStore.entries()).map( ([doc_id, info]) => ({ doc_id, filename: info.filename, file_type: info.file_type, }), ); toolResults.push({ role: "tool", tool_call_id: tc.id, content: JSON.stringify(list), }); } else if (tc.function.name === "fetch_documents") { const rawDocIds = (args.doc_ids as string[]) ?? []; const docIds = rawDocIds.map( (id) => resolveDocLabel(id, docStore, docIndex) ?? id, ); const parts: string[] = []; for (const docId of docIds) { const content = await readDocumentContentOncePerTurn({ docLabel: docId, docStore, write, docIndex, db, anchorManifestState, trace, turnDocumentState: effectiveTurnDocumentState, }); const filename = docStore.get(docId)?.filename ?? docId; parts.push(`--- ${filename} (${docId}) ---\n${content}`); if (docStore.get(docId) && !isAlreadyReadToolResult(content)) { const documentId = docIndex?.[docId]?.document_id; docsRead.push({ filename, document_id: documentId }); } } toolResults.push({ role: "tool", tool_call_id: tc.id, content: parts.join("\n\n"), }); } else if (tc.function.name === "list_workflows") { const list = workflowStore ? Array.from(workflowStore.entries()).map(([id, w]) => ({ id, title: w.title })) : []; toolResults.push({ role: "tool", tool_call_id: tc.id, content: JSON.stringify(list) }); } else if (tc.function.name === "read_workflow") { const wfId = args.workflow_id as string; const wf = workflowStore?.get(wfId); if (wf) { write(`data: ${JSON.stringify({ type: "workflow_applied", workflow_id: wfId, title: wf.title })}\n\n`); workflowsApplied.push({ workflow_id: wfId, title: wf.title }); // Log workflow identity to LangSmith if (trace) { const wfIdTrace = createTraceChild(trace, { name: "workflow_prompt_identity", run_type: "chain", inputs: { workflow_id: wfId, workflow_name: wf.title, workflow_type: "assistant/playbook", prompt_sha256: sha256(wf.prompt_md), prompt_preview: wf.prompt_md.slice(0, 400), }, tags: ["workflow", "prompt_identity"], }); void endTrace(wfIdTrace, { outputs: { workflow_id: wfId, workflow_name: wf.title }, }); } } toolResults.push({ role: "tool", tool_call_id: tc.id, content: wf ? ((trackChanges && wf.edit_mode_prompt_md) ? wf.edit_mode_prompt_md : wf.prompt_md) : `Workflow '${wfId}' not found.`, }); } else if (tc.function.name === "read_table_cells" && tabularStore) { const colIndices = args.col_indices as number[] | undefined; const rowIndices = args.row_indices as number[] | undefined; const filteredCols = colIndices?.length ? tabularStore.columns.filter((_, i) => colIndices.includes(i)) : tabularStore.columns; const filteredDocs = rowIndices?.length ? tabularStore.documents.filter((_, i) => rowIndices.includes(i)) : tabularStore.documents; const label = `${filteredCols.length} ${filteredCols.length === 1 ? "column" : "columns"} × ${filteredDocs.length} ${filteredDocs.length === 1 ? "row" : "rows"}`; write(`data: ${JSON.stringify({ type: "doc_read_start", filename: label })}\n\n`); const lines: string[] = []; for (const col of filteredCols) { const colPos = tabularStore.columns.findIndex((c) => c.index === col.index); for (const doc of filteredDocs) { const rowPos = tabularStore.documents.findIndex((d) => d.id === doc.id); const cell = tabularStore.cells.get(`${col.index}:${doc.id}`); lines.push(`[COL:${colPos} "${col.name}" | ROW:${rowPos} "${doc.filename}"]`); if (cell?.summary) { lines.push(`Summary: ${cell.summary}`); if (cell.flag) lines.push(`Flag: ${cell.flag}`); if (cell.reasoning) lines.push(`Reasoning: ${cell.reasoning}`); } else { lines.push(`(not yet generated)`); } lines.push(""); } } write(`data: ${JSON.stringify({ type: "doc_read", filename: label })}\n\n`); docsRead.push({ filename: label }); toolResults.push({ role: "tool", tool_call_id: tc.id, content: lines.join("\n") || "No cells found.", }); } else if (tc.function.name === "replicate_document" && docIndex) { const rawDocId = args.doc_id as string; const requestedFilename = typeof args.new_filename === "string" && args.new_filename.trim() ? args.new_filename.trim() : null; const requestedCount = typeof args.count === "number" && Number.isFinite(args.count) ? Math.max(1, Math.min(20, Math.floor(args.count))) : 1; const sourceLabel = resolveDocLabel(rawDocId, docStore, docIndex) ?? rawDocId; const sourceInfo = docStore.get(sourceLabel); const sourceIndexed = docIndex[sourceLabel]; const sourceFilename = sourceInfo?.filename ?? rawDocId; write( `data: ${JSON.stringify({ type: "doc_replicate_start", filename: sourceFilename, count: requestedCount, })}\n\n`, ); const fail = (error: string) => { write( `data: ${JSON.stringify({ type: "doc_replicated", filename: sourceFilename, count: requestedCount, copies: [], error, })}\n\n`, ); toolResults.push({ role: "tool", tool_call_id: tc.id, content: JSON.stringify({ ok: false, error }), }); }; if (!sourceInfo || !sourceIndexed) { fail(`Document '${rawDocId}' not found in this project.`); } else if (!projectId) { fail("replicate_document is only available in project chats."); } else { try { // Pull the active version once — every copy gets the // same starting bytes (with any accepted tracked // changes rolled in), no point re-fetching per copy. const active = await loadActiveVersion( sourceIndexed.document_id, db, ); const sourcePath = active?.storage_path ?? sourceInfo.storage_path; const sourcePdfPath = active?.pdf_storage_path ?? null; const raw = await downloadFile(sourcePath); const pdfBytes = sourcePdfPath ? await downloadFile(sourcePdfPath) : null; if (!raw) { fail( "Could not read the source document's bytes from storage.", ); } else { // Build N filenames. With count=1 keep the // pre-existing "(copy)" suffix; with count>1 use // numbered "(1)", "(2)" suffixes. const srcExt = sourceInfo.filename.match(/\.[^./\\]+$/)?.[0] ?? ""; const baseStem = (() => { if (requestedFilename) { return requestedFilename.replace( /\.[^./\\]+$/, "", ); } return sourceInfo.filename.replace( /\.[^./\\]+$/, "", ); })(); const filenames: string[] = []; for (let n = 1; n <= requestedCount; n++) { const suffix = requestedCount === 1 ? requestedFilename ? "" : " (copy)" : ` (${n})`; filenames.push(`${baseStem}${suffix}${srcExt}`); } // Bulk insert N documents in one round-trip. const docRows = filenames.map((fn) => ({ project_id: projectId, user_id: userId, filename: fn, file_type: sourceInfo.file_type, size_bytes: raw.byteLength, status: "ready", })); const { data: insertedDocs, error: docErr } = await db .from("documents") .insert(docRows) .select("id, filename"); if (docErr || !insertedDocs || insertedDocs.length === 0) { fail( `Failed to record replicated documents: ${docErr?.message ?? "unknown"}`, ); } else { // Preserve the request order so each row pairs // with the right filename. Supabase returns // inserted rows in the same order as the // payload. const newDocs = insertedDocs as { id: string; filename: string; }[]; const contentType = sourceInfo.file_type === "pdf" ? "application/pdf" : "application/vnd.openxmlformats-officedocument.wordprocessingml.document"; // Parallel uploads: the doc bytes (and PDF // rendition if any) for every new copy. const uploadJobs: Promise[] = []; const newKeys: string[] = []; const newPdfKeys: (string | null)[] = []; for (const d of newDocs) { const key = storageKey( userId, d.id, d.filename, ); newKeys.push(key); uploadJobs.push( uploadFile(key, raw, contentType), ); if (pdfBytes) { const pdfKey = convertedPdfKey( userId, d.id, ); newPdfKeys.push(pdfKey); uploadJobs.push( uploadFile( pdfKey, pdfBytes, "application/pdf", ), ); } else { newPdfKeys.push(null); } } await Promise.all(uploadJobs); // Bulk insert N versions in one round-trip. const versionRows = newDocs.map((d, idx) => ({ document_id: d.id, storage_path: newKeys[idx], pdf_storage_path: newPdfKeys[idx], source: "upload", version_number: 1, display_name: d.filename, })); const { data: insertedVersions, error: verErr } = await db .from("document_versions") .insert(versionRows) .select("id, document_id"); if ( verErr || !insertedVersions || insertedVersions.length !== newDocs.length ) { fail( `Failed to record replicated document versions: ${verErr?.message ?? "unknown"}`, ); } else { const versionByDocId = new Map(); for (const v of insertedVersions as { id: string; document_id: string; }[]) { versionByDocId.set(v.document_id, v.id); } // current_version_id has to be a per-row // value, so a single UPDATE statement // can't cover all N. Fan out in parallel // instead of sequential awaits. await Promise.all( newDocs.map((d) => db .from("documents") .update({ current_version_id: versionByDocId.get(d.id), }) .eq("id", d.id), ), ); // Register every copy under a fresh doc-N // slug so the model can edit/read any of // them in the same turn. const existingLabels = new Set( Object.keys(docIndex), ); let nextLabelIdx = 0; const copies: { new_filename: string; document_id: string; version_id: string; }[] = []; const toolPayloadCopies: { doc_id: string; document_id: string; version_id: string; filename: string; download_url: string; }[] = []; for (let idx = 0; idx < newDocs.length; idx++) { const d = newDocs[idx]; const newKey = newKeys[idx]; const versionId = versionByDocId.get(d.id); if (!versionId) continue; while ( existingLabels.has( `doc-${nextLabelIdx}`, ) ) nextLabelIdx++; const slug = `doc-${nextLabelIdx}`; existingLabels.add(slug); docIndex[slug] = { document_id: d.id, filename: d.filename, }; docStore.set(slug, { storage_path: newKey, file_type: sourceInfo.file_type, filename: d.filename, }); copies.push({ new_filename: d.filename, document_id: d.id, version_id: versionId, }); toolPayloadCopies.push({ doc_id: slug, document_id: d.id, version_id: versionId, filename: d.filename, download_url: buildDownloadUrl( newKey, d.filename, ), }); } write( `data: ${JSON.stringify({ type: "doc_replicated", filename: sourceFilename, count: copies.length, copies, })}\n\n`, ); docsReplicated.push({ filename: sourceFilename, count: copies.length, copies, }); toolResults.push({ role: "tool", tool_call_id: tc.id, content: JSON.stringify({ ok: true, count: copies.length, copies: toolPayloadCopies, }), }); } } } } catch (e) { fail(`replicate_document failed: ${String(e)}`); } } } else if (tc.function.name === "generate_docx") { const title = args.title as string; const landscape = !!(args.landscape); console.log(`[generate_docx] title="${title}" landscape=${landscape} args.landscape=${args.landscape}`); const previewFilename = `${(title.replace(/[^a-zA-Z0-9 _-]/g, "").trim().slice(0, 64) || "document")}.docx`; write(`data: ${JSON.stringify({ type: "doc_created_start", filename: previewFilename })}\n\n`); const result = await generateDocx( title, args.sections as unknown[], userId, db, { landscape, projectId: projectId ?? null }, ); let newDocLabel: string | null = null; if ("filename" in result && "download_url" in result) { const dlFilename = result.filename as string; const dlUrl = result.download_url as string; const documentId = (result as { document_id?: string }).document_id; const versionId = (result as { version_id?: string }).version_id; const versionNumber = (result as { version_number?: number }).version_number ?? null; const storagePath = (result as { storage_path?: string }).storage_path; // Register the generated doc in the chat context so // edit_document (and read_document / find_in_document) // can act on it within the same assistant turn. New label // is the next free `doc-N` index. Subsequent turns pick // it up via the normal attachment/project doc query. if (documentId && storagePath && docIndex) { const existingLabels = new Set(Object.keys(docIndex)); let i = 0; while (existingLabels.has(`doc-${i}`)) i++; newDocLabel = `doc-${i}`; docIndex[newDocLabel] = { document_id: documentId, filename: dlFilename, }; docStore.set(newDocLabel, { storage_path: storagePath, file_type: "docx", filename: dlFilename, }); } write( `data: ${JSON.stringify({ type: "doc_created", filename: dlFilename, download_url: dlUrl, document_id: documentId, version_id: versionId, version_number: versionNumber, })}\n\n`, ); docsCreated.push({ filename: dlFilename, download_url: dlUrl, document_id: documentId, version_id: versionId, version_number: versionNumber, }); } else { write(`data: ${JSON.stringify({ type: "doc_created", filename: previewFilename, download_url: "" })}\n\n`); } // Surface the chat-local doc label in the tool result so the // model can pass it as `doc_id` to edit_document / read_document // / find_in_document in the same turn. Without this the model // only sees the DB UUID, which isn't valid as a doc_id anchor. const toolResultPayload = newDocLabel ? { ...(result as Record), doc_id: newDocLabel } : result; toolResults.push({ role: "tool", tool_call_id: tc.id, content: JSON.stringify(toolResultPayload), }); } } let dedupedReports: CommentVerificationRow[] = []; if (commentVerificationRows.length > 0) { const applied = commentVerificationRows.filter( (row) => row.status === "applied", ).length; const ignoredPassCount = commentVerificationRows.filter( (row) => row.status === "ignored_pass", ).length; const failed = commentVerificationRows.length - applied - ignoredPassCount; dedupedReports = dedupeCommentVerificationRows(commentVerificationRows); const dedupedApplied = dedupedReports.filter( (row) => row.status === "applied", ).length; const dedupedIgnoredPass = dedupedReports.filter( (row) => row.status === "ignored_pass", ).length; const dedupedFailed = dedupedReports.length - dedupedApplied - dedupedIgnoredPass; const actionableAttempted = commentVerificationRows.length - ignoredPassCount; const verificationTrace = createTraceChild(trace, { name: "insertion_verification", run_type: "chain", inputs: { comments_attempted: commentVerificationRows.length, comments_actionable_attempted: actionableAttempted, comments_ignored_pass: ignoredPassCount, documents_edited: docsEdited.map((doc) => ({ filename: doc.filename, document_id: doc.document_id, version_id: doc.version_id, })), }, tags: ["verification", "comments"], }); await endTrace(verificationTrace, { outputs: { commentsAttempted: commentVerificationRows.length, commentsActionableAttempted: actionableAttempted, commentsApplied: applied, commentsFailed: failed, commentsIgnoredPass: ignoredPassCount, uniqueCommentsAttempted: dedupedReports.length, uniqueCommentsApplied: dedupedApplied, uniqueCommentsFailed: dedupedFailed, uniqueCommentsIgnoredPass: dedupedIgnoredPass, reports: commentVerificationRows, deduplicatedReports: dedupedReports, }, error: failed > 0 ? `${failed} comment insertion attempt(s) failed` : null, }); } const workflowSourceRows = dedupedReports.length > 0 ? dedupedReports : commentVerificationRows; const workflowFindings: WorkflowFinding[] = workflowSourceRows .filter((row) => row.ok && row.status !== "ignored_pass") .map((row) => { const edit = docsEdited .flatMap((d) => d.annotations ?? []) .find((a) => a.target_block_id === row.block_id || a.change_id === row.comment_key); return { comment_key: row.comment_key, block_id: row.block_id, op_type: row.op_type ?? "COMMENT", status: row.status, section_heading: row.section_heading, bookmark_name: row.bookmark_name, comment: row.comment, target_text: edit?.deleted_text || row.anchor_text || undefined, new_text: edit?.inserted_text || undefined, reasoning: row.reasoning || null, applied: row.ok, }; }); const workflowRows: WorkflowFindingRow[] = workflowSourceRows.map((row) => { const action = normalizeWorkflowAction(row.op_type ?? "COMMENT"); const { issue, zamilImpact, recommendedAction } = parseCommentFields(row.comment ?? ""); const edit = docsEdited .flatMap((d) => d.annotations ?? []) .find((a) => a.target_block_id === row.block_id || a.change_id === row.comment_key); const isComment = action === "COMMENT"; const isInsert = action === "INSERT"; const isDelete = action === "DELETE"; const applyStatus = applyStatusFromVerificationRow(row); return { row_id: `${row.doc_id ?? ""}:${row.comment_key ?? row.block_id ?? crypto.randomUUID()}`, doc_id: row.doc_id ?? "", document_id: row.document_id, comment_key: row.comment_key ?? "", block_id: row.block_id ?? row.model_block_id ?? "", bookmark_name: row.bookmark_name ?? null, section_heading: row.section_heading ?? null, document_order: row.document_order, finding_status: row.finding_status ?? "REVIEW", apply_status: applyStatus, action, issue, zamil_impact: zamilImpact || undefined, recommended_action: recommendedAction || undefined, original_wording: isInsert ? undefined : edit?.deleted_text || row.anchor_text || undefined, amended_wording: isComment || isDelete ? undefined : row.amended_wording ?? edit?.inserted_text ?? undefined, reasoning: row.reasoning ?? null, failure_code: row.failure_code, failure_message: row.message, source: row.source ?? "plan_comments_on_document", }; }); return { toolResults, docsRead, docsFound, docsCreated, docsReplicated, workflowsApplied, docsEdited, workflowFindings, workflowRows, }; } // --------------------------------------------------------------------------- // Citation parsing // --------------------------------------------------------------------------- const CITATIONS_BLOCK_RE = /\s*([\s\S]*?)\s*<\/CITATIONS>/; const CITATIONS_OPEN_TAG = ""; function parseCitations(text: string): ParsedCitation[] { const match = text.match(CITATIONS_BLOCK_RE); if (!match) return []; try { const raw = JSON.parse(match[1]); if (!Array.isArray(raw)) return []; return raw .map(normalizeCitation) .filter((c): c is ParsedCitation => c !== null); } catch { return []; } } // --------------------------------------------------------------------------- // LLM streaming loop // --------------------------------------------------------------------------- export type EditAnnotation = { kind: "edit"; edit_id: string; document_id: string; version_id: string; version_number?: number | null; change_id: string; del_w_id?: string; ins_w_id?: string; deleted_text: string; inserted_text: string; context_before: string; context_after: string; operation?: string | null; target_block_id?: string | null; target_start?: number | null; target_end?: number | null; base_surface_id?: string | null; target_text_hash?: string | null; reason?: string; status: "pending" | "accepted" | "rejected"; }; export type WorkflowFindingStatus = | "FAIL" | "MISSING" | "RISK" | "REVIEW" | "ESCALATE"; export type WorkflowApplyStatus = | "applied" | "not_applied" | "ignored_pass" | "needs_correction" | "failed_validation" | "writer_failed"; export type WorkflowAction = | "COMMENT" | "REWRITE" | "INSERT" | "DELETE"; export type WorkflowFindingRow = { row_id: string; workflow_id?: string; workflow_title?: string; doc_id: string; document_id?: string; version_id?: string; download_url?: string; comment_key: string; block_id: string; bookmark_name?: string | null; section_heading?: string | null; document_order?: number; finding_status: WorkflowFindingStatus; apply_status: WorkflowApplyStatus; action: WorkflowAction; issue: string; zamil_impact?: string; recommended_action?: string; original_wording?: string; amended_wording?: string; reasoning?: { clause_section: string; violated_standard: string; zamil_exposure: string; } | null; failure_code?: string; failure_message?: string; repair_kind?: RepairKind; repair_attempts?: number; fallback_applied?: boolean; downgrade_to_comment?: boolean; original_type?: string; final_type?: string; source: CommentPlanSource; }; export type WorkflowFinding = { comment_key?: string; block_id?: string; op_type?: string; status?: string; section_heading?: string | null; bookmark_name?: string | null; comment?: string; target_text?: string; new_text?: string; reasoning?: { clause_section: string; violated_standard: string; zamil_exposure: string } | null; applied: boolean; }; const APPLY_STATUS_PRIORITY: Record = { applied: 6, writer_failed: 5, failed_validation: 4, needs_correction: 3, not_applied: 2, ignored_pass: 1, }; function shouldReplace( current: WorkflowApplyStatus, incoming: WorkflowApplyStatus, ): boolean { return APPLY_STATUS_PRIORITY[incoming] > APPLY_STATUS_PRIORITY[current]; } class WorkflowAccumulator { private rows = new Map(); addRows(rows: WorkflowFindingRow[]): void { for (const row of rows) { const existing = this.rows.get(row.row_id); if (!existing || shouldReplace(existing.apply_status, row.apply_status)) { this.rows.set(row.row_id, { ...row, finding_status: row.finding_status ?? existing?.finding_status ?? "REVIEW", issue: row.issue || existing?.issue || "", reasoning: row.reasoning ?? existing?.reasoning ?? null, section_heading: row.section_heading ?? existing?.section_heading ?? null, document_order: row.document_order ?? existing?.document_order, original_wording: row.original_wording ?? existing?.original_wording, amended_wording: row.amended_wording ?? existing?.amended_wording, }); } } } getRows(): WorkflowFindingRow[] { return Array.from(this.rows.values()); } size(): number { return this.rows.size; } } type WorkflowBatchDecision = | { terminal: true; reason: "applied" | "no_action" | "failed" } | { terminal: false; reason: "needs_anchor_correction" }; function parseToolJson(content: string): Record { try { const parsed = JSON.parse(content); return parsed && typeof parsed === "object" ? parsed : {}; } catch { return {}; } } export function isWorkflowTerminalBatch(input: { calls: NormalizedToolCall[]; toolResults: NormalizedToolResult[]; workflowRows: WorkflowFindingRow[]; }): WorkflowBatchDecision { const writeTools = new Set([ "plan_comments_on_document", "correct_comment_anchors", "fix_comment_findings", ]); const hasWriteTool = input.calls.some((c) => writeTools.has(c.name)); if (!hasWriteTool) { return { terminal: false, reason: "needs_anchor_correction" }; } let resultSaysApplied = false; let resultSaysFailed = false; let resultSaysNoAction = false; const writeCalls = input.calls.filter((c) => writeTools.has(c.name)); for (const call of writeCalls) { const result = input.toolResults.find((r) => r.tool_use_id === call.id); const parsed = result ? parseToolJson(result.content) : {}; const needsCorrection = Number(parsed.needs_correction ?? 0); const readyToApply = parsed.ready_to_apply === true; if (needsCorrection > 0 && !readyToApply) { return { terminal: false, reason: "needs_anchor_correction" }; } const appliedCount = Number(parsed.apply_summary?.applied ?? 0); const failedCount = Number( parsed.failed_permanently ?? parsed.failed ?? parsed.apply_summary?.failed ?? 0, ); const validCount = Number(parsed.valid ?? 0); const ignoredPassCount = Number(parsed.ignored_pass ?? 0); if (parsed.applied === true || appliedCount > 0) { resultSaysApplied = true; } if (failedCount > 0 || typeof parsed.error === "string") { resultSaysFailed = true; } if ( readyToApply && validCount === 0 && failedCount === 0 && ignoredPassCount > 0 ) { resultSaysNoAction = true; } } const applied = input.workflowRows.some((r) => r.apply_status === "applied"); const failed = input.workflowRows.some( (r) => r.apply_status !== "applied" && r.apply_status !== "ignored_pass", ); if (applied || resultSaysApplied) return { terminal: true, reason: "applied" }; if (failed || resultSaysFailed) return { terminal: true, reason: "failed" }; if (resultSaysNoAction) return { terminal: true, reason: "no_action" }; return { terminal: true, reason: "no_action" }; } const OP_TYPE_LABELS: Record = { COMMENT: "Comment", REWRITE_PARAGRAPH: "Rewrite", INSERT_PARAGRAPH: "Insert", DELETE_PARAGRAPH: "Delete", }; const STATUS_LABELS: Record = { FAIL: "FAIL", RISK: "RISK", REVIEW: "REVIEW", ESCALATE: "ESCALATE", MISSING: "MISSING", }; const SEVERITY_ORDER: Record = { FAIL: 1, MISSING: 2, RISK: 3, ESCALATE: 4, REVIEW: 5, }; const ACTION_ORDER: Record = { REWRITE: 1, INSERT: 2, DELETE: 3, COMMENT: 4, }; export type WorkflowSummaryMetrics = { total: number; actionable: number; applied: number; failed: number; ignoredPass: number; rewrites: number; inserts: number; deletes: number; comments: number; bySeverity: Record; }; export type WorkflowSummaryRender = { markdown: string; reasoningMarkdown: string; metrics: WorkflowSummaryMetrics; }; export function buildWorkflowSummaryFromRows(rows: WorkflowFindingRow[]): WorkflowSummaryRender { const applied = rows.filter((r) => r.apply_status === "applied"); const failed = rows.filter((r) => r.apply_status !== "applied" && r.apply_status !== "ignored_pass"); const ignoredPass = rows.filter((r) => r.apply_status === "ignored_pass"); const actionable = rows.filter((r) => r.apply_status !== "ignored_pass"); const metrics: WorkflowSummaryMetrics = { total: rows.length, actionable: actionable.length, applied: applied.length, failed: failed.length, ignoredPass: ignoredPass.length, rewrites: applied.filter((r) => r.action === "REWRITE").length, inserts: applied.filter((r) => r.action === "INSERT").length, deletes: applied.filter((r) => r.action === "DELETE").length, comments: applied.filter((r) => r.action === "COMMENT").length, bySeverity: {}, }; for (const r of applied) { const key = r.finding_status; metrics.bySeverity[key] = (metrics.bySeverity[key] ?? 0) + 1; } if (rows.length === 0 || (applied.length === 0 && failed.length === 0)) { return { markdown: "I completed the playbook review. No actionable changes were applied.", reasoningMarkdown: "", metrics, }; } const sortKey = (r: WorkflowFindingRow) => [ r.document_order ?? Infinity, SEVERITY_ORDER[r.finding_status] ?? 99, ACTION_ORDER[r.action] ?? 99, r.comment_key, ]; const sortedApplied = [...applied].sort((a, b) => { const ka = sortKey(a); const kb = sortKey(b); for (let i = 0; i < ka.length; i++) { if (ka[i] < kb[i]) return -1; if (ka[i] > kb[i]) return 1; } return 0; }); const esc = (s: string | undefined | null) => sanitizeWorkflowDisplayText(s) .replace(/\|/g, "\\|") .replace(/\n/g, " ") .slice(0, 120) || "—"; const formatWorkflowSection = (row: any): string => { const pageMatch = typeof row?.block_id === "string" ? row.block_id.match(/^BMK_P_page(\d+)$/) : null; if (pageMatch) return `Page ${pageMatch[1]}`; const sectionCandidates = [ row.section_heading, row.reasoning?.clause_section, ]; for (const candidate of sectionCandidates) { if (isMeaningfulSectionLabel(candidate)) { return decodeXmlEntities(esc(candidate)); } } return "Relevant section"; }; const isPdfWorkflowRow = (row: any): boolean => { return typeof row?.block_id === "string" && /^BMK_P_page\d+$/.test(row.block_id); }; const hasPdfRows = rows.some(isPdfWorkflowRow); const hasOnlyPdfRows = rows.length > 0 && rows.every(isPdfWorkflowRow); const appliedCount = applied.length; const actionParts = [ metrics.rewrites > 0 ? `${metrics.rewrites} rewrite${metrics.rewrites === 1 ? "" : "s"}` : "", metrics.inserts > 0 ? `${metrics.inserts} insertion${metrics.inserts === 1 ? "" : "s"}` : "", metrics.deletes > 0 ? `${metrics.deletes} deletion${metrics.deletes === 1 ? "" : "s"}` : "", metrics.comments > 0 ? `${metrics.comments} comment${metrics.comments === 1 ? "" : "s"}` : "", ].filter(Boolean); const severityParts = Object.entries(metrics.bySeverity) .map(([severity, count]) => `${count} ${severity}`) .join(", "); const existingIntro = applied.length > 0 ? failed.length > 0 ? `I completed the playbook review and applied ${applied.length} of ${actionable.length} actionable findings. ${failed.length} finding${failed.length === 1 ? " needs" : "s need"} manual review.` : `I completed the playbook review and applied ${applied.length} finding${applied.length === 1 ? "" : "s"}${actionParts.length ? `: ${actionParts.join(", ")}` : ""}${severityParts ? `. Severity: ${severityParts}.` : "."}` : `I completed the playbook review. No findings were applied. ${failed.length} finding${failed.length === 1 ? " needs" : "s need"} manual review.`; const intro = hasOnlyPdfRows ? `I completed the read-only PDF review and reported ${appliedCount} actionable finding${appliedCount === 1 ? "" : "s"}. No changes were made to the PDF.` : hasPdfRows ? `I completed the document review. PDF findings were reported read-only, and editable document comments were handled through the existing document workflow.` : existingIntro; const header = "| # | Section | Status | Action | Target Text | Amended Text | Issue / Reason | Zamil Impact | Recommended Action |"; const separator = "|---|---------|--------|--------|-------------|--------------|----------------|--------------|--------------------|"; const appliedRows = sortedApplied.map((r, index) => { const section = formatWorkflowSection(r); return `| ${index + 1} | ${section} | ${r.finding_status} | ${capitalize(r.action)} | ${r.action === "INSERT" ? "—" : esc(r.original_wording)} | ${r.action === "COMMENT" || r.action === "DELETE" ? "—" : esc(r.amended_wording)} | ${esc(r.issue)} | ${esc(r.zamil_impact)} | ${esc(r.recommended_action)} |`; }); let markdown = intro; if (appliedRows.length > 0) { markdown += `\n\n${header}\n${separator}\n${appliedRows.join("\n")}`; } if (failed.length > 0) { const sortedFailed = [...failed].sort((a, b) => { const ka = sortKey(a); const kb = sortKey(b); for (let i = 0; i < ka.length; i++) { if (ka[i] < kb[i]) return -1; if (ka[i] > kb[i]) return 1; } return 0; }); markdown += "\n\n### Not applied\n\n"; const failedHeader = "| # | Section | Status | Action | Reason | Next Step |"; const failedSeparator = "|---|---------|--------|--------|--------|-----------|"; const failedRows = sortedFailed.map((r, index) => { const section = formatWorkflowSection(r); const nextStep = r.failure_code === "anchor_not_found" ? "Re-read the document and rerun the finding, or apply manually." : "Review the failure reason and retry or apply manually."; return `| ${index + 1} | ${section} | ${r.finding_status} | ${capitalize(r.action)} | ${esc(r.failure_message ?? r.failure_code)} | ${nextStep} |`; }); markdown += `${failedHeader}\n${failedSeparator}\n${failedRows.join("\n")}`; } const reasoningLines: string[] = []; sortedApplied.forEach((r, index) => { const section = formatWorkflowSection(r); reasoningLines.push(`### ${index + 1}. ${section}`); if (r.reasoning) { reasoningLines.push(`- **Clause/Section:** ${esc(r.reasoning.clause_section)}`); reasoningLines.push(`- **Violated Standard:** ${esc(r.reasoning.violated_standard)}`); reasoningLines.push(`- **Zamil Exposure:** ${esc(r.reasoning.zamil_exposure)}`); } if (r.issue) reasoningLines.push(`- **Issue:** ${esc(r.issue)}`); if (r.zamil_impact) reasoningLines.push(`- **Zamil Impact:** ${esc(r.zamil_impact)}`); if (r.recommended_action) reasoningLines.push(`- **Recommended Action:** ${esc(r.recommended_action)}`); reasoningLines.push(""); }); return { markdown, reasoningMarkdown: reasoningLines.join("\n"), metrics, }; } function capitalize(s: string): string { return s.charAt(0).toUpperCase() + s.slice(1).toLowerCase(); } export function buildWorkflowSummary(findings: WorkflowFinding[]): string { const applied = findings.filter((f) => f.applied); const rewriteCount = applied.filter((f) => f.op_type === "REWRITE_PARAGRAPH").length; const insertCount = applied.filter((f) => f.op_type === "INSERT_PARAGRAPH").length; const deleteCount = applied.filter((f) => f.op_type === "DELETE_PARAGRAPH").length; const commentCount = applied.filter((f) => !f.op_type || f.op_type === "COMMENT").length; const flaggedCount = applied.filter((f) => f.status === "FAIL").length; const riskCount = applied.filter((f) => f.status === "RISK").length; const reviewCount = applied.filter((f) => f.status === "REVIEW").length; const parts: string[] = []; if (rewriteCount > 0) parts.push(`redlined ${rewriteCount} issue${rewriteCount > 1 ? "s" : ""}`); if (insertCount > 0) parts.push(`inserted ${insertCount} clause${insertCount > 1 ? "s" : ""}`); if (deleteCount > 0) parts.push(`deleted ${deleteCount} provision${deleteCount > 1 ? "s" : ""}`); if (commentCount > 0) parts.push(`flagged ${commentCount} concern${commentCount > 1 ? "s" : ""}`); const statusParts: string[] = []; if (flaggedCount > 0) statusParts.push(`${flaggedCount} flagged`); if (riskCount > 0) statusParts.push(`${riskCount} at risk`); if (reviewCount > 0) statusParts.push(`${reviewCount} for review`); let intro = "Here's my review"; if (parts.length > 0) { intro += ` — I ${parts.join(", ")}`; } intro += " in the document."; if (statusParts.length > 0) { intro += ` (${statusParts.join(", ")})`; } if (applied.length === 0) { return `${intro}\n\nNo changes were applied to the document.`; } const header = "| Section | Status | Change | Target Text | Amended Text | Reason |"; const separator = "|---------|--------|--------|-------------|--------------|--------|"; const rows = applied.map((f) => { const sectionCandidate = isMeaningfulSectionLabel(f.section_heading) ? f.section_heading : isMeaningfulSectionLabel(f.reasoning?.clause_section) ? f.reasoning?.clause_section : "Relevant section"; const section = decodeXmlEntities( sanitizeWorkflowDisplayText(sectionCandidate).replace(/\|/g, "\\|"), ); const statusLabel = STATUS_LABELS[f.status ?? ""] || f.status || "—"; const changeLabel = OP_TYPE_LABELS[f.op_type ?? ""] || f.op_type || "Comment"; const isInsert = f.op_type === "INSERT" || f.op_type === "INSERT_PARAGRAPH"; const isComment = !f.op_type || f.op_type === "COMMENT"; const isDelete = f.op_type === "DELETE_PARAGRAPH"; const target = isInsert ? "—" : (sanitizeWorkflowDisplayText(f.target_text) || "—").replace(/\|/g, "\\|").replace(/\n/g, " ").slice(0, 120); const amended = isComment || isDelete ? "—" : (sanitizeWorkflowDisplayText(f.new_text) || "—").replace(/\|/g, "\\|").replace(/\n/g, " ").slice(0, 120); const reason = (sanitizeWorkflowDisplayText(f.comment) || "—").replace(/\|/g, "\\|").replace(/\n/g, " ").slice(0, 200); return `| ${section} | ${statusLabel} | ${changeLabel} | ${target} | ${amended} | ${reason} |`; }); return `${intro}\n\n${header}\n${separator}\n${rows.join("\n")}`; } type AssistantEvent = | { type: "reasoning"; text: string } | { type: "doc_read"; filename: string; document_id?: string } | { type: "doc_find"; filename: string; query: string; total_matches: number; } | { type: "doc_created"; filename: string; download_url: string; document_id?: string; version_id?: string; version_number?: number | null; } | { type: "doc_download"; filename: string; download_url: string } | { type: "doc_replicated"; /** Source document being copied. */ filename: string; count: number; copies: { new_filename: string; document_id: string; version_id: string; }[]; } | { type: "workflow_applied"; workflow_id: string; title: string } | { type: "doc_edited"; filename: string; document_id: string; version_id: string; /** Per-document monotonic Vn; null if backend couldn't determine it. */ version_number: number | null; download_url: string; annotations: EditAnnotation[]; apply_report?: any[]; apply_summary?: DocEditedResult["apply_summary"]; edit_verification?: unknown; } | { type: "content"; text: string } | { type: "search_grounding"; grounding: import("./llm/types").SearchGrounding; } | { type: "workflow_summary"; summary: string; reasoning: string; metrics?: WorkflowSummaryRender["metrics"]; rows?: WorkflowFindingRow[]; }; function isClarifyingDocumentEditResponse(text: string): boolean { const normalized = text.trim(); if (!normalized) return false; return ( /\?/.test(normalized) && /\b(which|what|where|who|how|please confirm|clarify|document|docx|file|section|clause|paragraph|location|text|wording|should I)\b/i.test( normalized, ) ); } export async function runLLMStream(params: { apiMessages: unknown[]; docStore: DocStore; docIndex: DocIndex; userId: string; db: ReturnType; write: (s: string) => void; extraTools?: unknown[]; workflowStore?: WorkflowStore; tabularStore?: TabularCellStore; buildCitations?: (fullText: string) => unknown[]; model?: string; inferenceProvider?: string | null; apiKeys?: import("./llm").UserApiKeys; trace?: RunTree | null; /** * If set, generate_docx will attach created docs to this project so * they appear in the project sidebar. Leave null for general chats — * generated docs still get persisted, but as standalone documents. */ projectId?: string | null; trackChanges?: boolean; isExecutingWorkflow?: boolean; turnMode?: AssistantTurnMode; enableGoogleSearch?: boolean; saudiLawRouting?: SaudiLawRoutingResult; saudiLawSourcePacket?: SaudiLawSourcePacket; signal?: AbortSignal; }): Promise<{ fullText: string; events: AssistantEvent[]; saudiLawEvidence?: SaudiLawEvidencePacket | null; }> { const { apiMessages, docStore, docIndex, userId, db, write, extraTools, workflowStore, tabularStore, buildCitations, model, inferenceProvider, apiKeys, projectId, trace, trackChanges, isExecutingWorkflow, turnMode, enableGoogleSearch, saudiLawRouting, saudiLawSourcePacket, signal } = params; const anchorManifestState = createAnchorManifestState(); const isSaudiLawTurn = saudiLawRouting?.isSaudiLawQuestion === true; const baseTurnMode: AssistantTurnMode = turnMode ?? (isExecutingWorkflow ? "workflow_review" : trackChanges ? "document_direct_edit" : projectId ? "project_chat" : "answer"); const effectiveTurnMode: AssistantTurnMode = isSaudiLawTurn && baseTurnMode === "answer" ? "web_search" : isSaudiLawTurn && baseTurnMode === "document_direct_edit" ? "document_edit_with_web_search" : baseTurnMode; const isDocumentEditTurn = effectiveTurnMode === "document_direct_edit" || effectiveTurnMode === "document_edit_with_web_search"; const isWebSearchTurn = effectiveTurnMode === "web_search" || effectiveTurnMode === "document_edit_with_web_search"; const effectiveEnableGoogleSearch = isSaudiLawTurn || enableGoogleSearch === true || isWebSearchTurn; let activeTools: OpenAIToolSchema[]; if (isDocumentEditTurn) { activeTools = DIRECT_DOCX_EDIT_TOOLS; } else { activeTools = extraTools?.length ? ([...TOOLS, ...WORKFLOW_TOOLS, ...extraTools] as OpenAIToolSchema[]) : ([...TOOLS, ...WORKFLOW_TOOLS] as OpenAIToolSchema[]); } // Server-side capability enforcement: expose only the tools the resolved // turn mode is allowed to use. The model never sees a forbidden tool. activeTools = activeTools.filter((tool) => isToolAllowedForMode(tool.function.name, effectiveTurnMode), ); // Extract system prompt; pass remaining turns to the adapter as // plain user/assistant messages. const rawMsgs = apiMessages as { role: string; content: string | null }[]; let systemPrompt = rawMsgs[0]?.role === "system" ? (rawMsgs[0].content ?? "") : ""; const lastUserContent = rawMsgs .slice() .reverse() .find((m) => m.role === "user")?.content ?? ""; // Append mode-specific banner. Direct DOCX edit mode already uses // DIRECT_DOCX_EDIT_SYSTEM_PROMPT from buildMessages(), so never append // the legacy plan_comments_on_document edit banner there. systemPrompt = appendModeBanner(systemPrompt, { trackChanges, effectiveTurnMode, saudiLawRouting, }); let activeSaudiLawSourcePacket: SaudiLawSourcePacket | null = null; if (isSaudiLawTurn) { systemPrompt += `\n\n${buildSaudiLawWebHintsBanner(saudiLawRouting)}`; activeSaudiLawSourcePacket = saudiLawSourcePacket ?? buildSaudiLawSourcePacket({ routing: saudiLawRouting, userText: lastUserContent, }); const sourcePacketBanner = renderSaudiLawSourcePacketHintsBanner( activeSaudiLawSourcePacket, ); if (sourcePacketBanner) { systemPrompt += `\n\n${sourcePacketBanner}`; } } const chatMessages: LlmMessage[] = rawMsgs .filter((m) => m.role !== "system") .map((m) => ({ role: m.role === "assistant" ? "assistant" : "user", content: m.content ?? "", })); const chatFeatureId = projectId ? "project-chat" : "chat"; const activeInferenceFeatureId = isSaudiLawTurn ? "saudi-law-answer" : chatFeatureId; const defaultChatModel = isSaudiLawTurn ? (process.env.SAUDI_EVAL_ANSWER_MODEL?.trim() || getDefaultModelForFeature("saudi-law-answer")) : getDefaultModelForFeature(chatFeatureId); const selectedModel = isSaudiLawTurn ? resolveModel(undefined, defaultChatModel) : resolveModel(model, defaultChatModel); const assistantTrace = createTraceChild(trace, { name: "assistant.turn", run_type: "chain", inputs: { message_count: chatMessages.length, api_messages: apiMessages, doc_count: Object.keys(docIndex).length, workflow_count: workflowStore?.size ?? 0, tabular_columns: tabularStore?.columns.length ?? 0, model: selectedModel, track_changes: trackChanges ?? false, turn_mode: effectiveTurnMode, enable_google_search: effectiveEnableGoogleSearch, inference_feature_id: activeInferenceFeatureId, saudi_law_source_packet: summarizeSaudiLawSourcePacket(activeSaudiLawSourcePacket), }, tags: ["assistant", "llm"], metadata: { project_id: projectId ?? null, }, }); let activeSaudiLawEvidence: SaudiLawEvidencePacket | null = null; if (isSaudiLawTurn && activeSaudiLawSourcePacket) { activeSaudiLawEvidence = await buildSaudiLawEvidencePacket({ routing: saudiLawRouting, sourcePacket: activeSaudiLawSourcePacket, userText: lastUserContent, apiKeys, trace: assistantTrace, signal, }); systemPrompt += `\n\n${renderSaudiLawEvidencePacketBanner(activeSaudiLawEvidence)}`; assistantTrace?.addEvent({ name: "saudi_law_evidence_packet", kwargs: summarizeSaudiLawEvidencePacket(activeSaudiLawEvidence) ?? undefined, }); } // CRITICAL: Do not inline full document contents into the system prompt. // Document content must enter the model context only through read_document // or fetch_documents, and each full document may be returned at most once // per assistant turn. This prevents duplicate document bodies from // ballooning provider payloads and exhausting proxy credentials. console.log( "[runLLMStream] system prompt:\n" + "─".repeat(80) + "\n" + systemPrompt.slice(0, 1000) + "...\n[TRUNCATED IN LOGS]\n" + "─".repeat(80), ); const isSaudiLawContext = /Saudi|KSA|CTL|Civil Transactions Law|Royal Decree|Article \d+|regulation|Ministry|ZATCA|MHRSD|Qiwa|Companies Law|Labour Law|Arbitration Law|Enforcement Law|Bankruptcy Law/i.test(lastUserContent); const events: AssistantEvent[] = []; let emittedResearchGrounding: SearchGrounding | null = null; if (activeSaudiLawEvidence) { const researchGrounding = activeSaudiLawEvidence.research.grounding; if (researchGrounding && hasGrounding(researchGrounding)) { emittedResearchGrounding = researchGrounding; events.push({ type: "search_grounding", grounding: researchGrounding, }); write( `data: ${JSON.stringify({ type: "search_grounding", grounding: researchGrounding, })}\n\n`, ); } } const emitMergedGrounding = (grounding: SearchGrounding) => { const merged = mergeGrounding(emittedResearchGrounding, grounding); if (!merged) return; emittedResearchGrounding = merged; const existingIdx = events.findIndex( (event) => event.type === "search_grounding", ); const groundingEvent = { type: "search_grounding" as const, grounding: merged, }; if (existingIdx >= 0) { events[existingIdx] = groundingEvent; } else { events.push(groundingEvent); } write( `data: ${JSON.stringify({ type: "search_grounding", grounding: merged, })}\n\n`, ); }; // One assistant turn produces at most one document_versions row per // edited doc. `runToolCalls` fires once per tool-call batch; the model // may emit multiple batches in a single turn, so this map persists // across batches to let subsequent plan_comments_on_document edit calls overwrite the // turn's existing version instead of creating a new one. const turnEditState: TurnEditState = new Map(); let fullText = ""; let iterText = ""; let iterVisibleText = ""; let iterReasoning = ""; let visibleTailBuffer = ""; let citationsOpenSeen = false; const streamVisibleContent = (delta: string) => { if (!delta) return; if (citationsOpenSeen) return; const combined = visibleTailBuffer + delta; const markerIdx = combined.indexOf(CITATIONS_OPEN_TAG); if (markerIdx >= 0) { const visible = combined.slice(0, markerIdx); if (visible) { iterVisibleText += visible; write( `data: ${JSON.stringify({ type: "content_delta", text: visible })}\n\n`, ); assistantTrace?.addEvent({ name: "content_delta", kwargs: { text: visible }, }); } visibleTailBuffer = ""; citationsOpenSeen = true; return; } const keep = Math.min(CITATIONS_OPEN_TAG.length - 1, combined.length); const visible = combined.slice(0, combined.length - keep); visibleTailBuffer = combined.slice(combined.length - keep); if (visible) { iterVisibleText += visible; write( `data: ${JSON.stringify({ type: "content_delta", text: visible })}\n\n`, ); assistantTrace?.addEvent({ name: "content_delta", kwargs: { text: visible }, }); } }; // Keep the streaming connection active during long model turns. const keepAliveInterval = setInterval(() => { try { write(": keep-alive\n\n"); } catch (e) { // connection likely closed clearInterval(keepAliveInterval); } }, 5000); const flushVisibleTail = () => { if (citationsOpenSeen || !visibleTailBuffer) { visibleTailBuffer = ""; return; } iterVisibleText += visibleTailBuffer; write( `data: ${JSON.stringify({ type: "content_delta", text: visibleTailBuffer })}\n\n`, ); assistantTrace?.addEvent({ name: "content_delta", kwargs: { text: visibleTailBuffer }, }); visibleTailBuffer = ""; }; const flushText = () => { if (!iterText) return; fullText += iterText; flushVisibleTail(); if (iterVisibleText) { events.push({ type: "content", text: iterVisibleText }); } iterText = ""; iterVisibleText = ""; visibleTailBuffer = ""; citationsOpenSeen = false; }; const flushReasoning = () => { if (!iterReasoning) return; events.push({ type: "reasoning", text: iterReasoning }); write(`data: ${JSON.stringify({ type: "reasoning_block_end" })}\n\n`); iterReasoning = ""; }; const commentAttemptState: CommentAttemptState = new Map(); const pendingCommentPlanState: PendingCommentPlanState = new Map(); const turnDocumentState = createTurnDocumentState(); const allWorkflowFindings: WorkflowFinding[] = []; const workflowAccumulator = new WorkflowAccumulator(); const isWorkflowTurn = effectiveTurnMode === "workflow_review"; let toolBatchCount = 0; const emptyTurnRetryInstruction = isDocumentEditTurn ? DOCX_EDIT_EMPTY_TURN_RETRY : isWorkflowTurn ? WORKFLOW_REVIEW_EMPTY_TURN_RETRY : isWebSearchTurn ? WEB_SEARCH_EMPTY_TURN_RETRY : undefined; try { if (activeSaudiLawEvidence?.research.invoked) { await delayBetweenSaudiEvalProxyCalls( "Saudi-law final answer after research node", ); } const streamResult = await streamChatWithTools({ model: selectedModel, explicitRequestProvider: inferenceProvider, featureId: activeInferenceFeatureId, systemPrompt, messages: chatMessages, tools: activeTools as OpenAIToolSchema[], maxIterations: 10, apiKeys, enableThinking: true, trace: assistantTrace, workflowMode: isWorkflowTurn, enableGoogleSearch: effectiveEnableGoogleSearch, emptyTurnRetryInstruction, signal, ...(isSaudiLawContext ? { temperature: 0 } : {}), callbacks: { onContentDelta: (delta) => { iterText += delta; streamVisibleContent(delta); }, onReasoningDelta: (delta) => { iterReasoning += delta; write( `data: ${JSON.stringify({ type: "reasoning_delta", text: delta })}\n\n`, ); }, onReasoningBlockEnd: () => { if (!iterReasoning) return; events.push({ type: "reasoning", text: iterReasoning }); write( `data: ${JSON.stringify({ type: "reasoning_block_end" })}\n\n`, ); iterReasoning = ""; }, // Fires after Claude's turn ends with stop_reason=tool_use, before // the tool actually runs. Flushes any buffered assistant text so // it's emitted in chronological order, then signals the client so // it can open a fresh PreResponseWrapper (shows "Working…") while // the tool executes — avoids the dead gap between message_stop // and the first tool-specific event. onToolCallStart: (call) => { flushReasoning(); flushText(); write( `data: ${JSON.stringify({ type: "tool_call_start", name: call.name, })}\n\n`, ); }, onGrounding: (grounding) => { flushReasoning(); flushText(); emitMergedGrounding(grounding); }, }, runTools: async (calls) => { // Emit any text the model produced before this tool turn so the // UI sees it before the tool results stream in. flushReasoning(); flushText(); try { toolBatchCount += 1; assertToolBudget({ toolBatchCount, turnDocumentState, workflowReview: isWorkflowTurn, }); // After the document is loaded, workflow review must move to // plan_comments — block prep/search tools even if the proxy // hallucinates them. const workflowWritePhase = isWorkflowTurn && turnDocumentState.readsByDocLabel.size > 0; const allowedCalls = calls.filter((c) => { if ( workflowWritePhase && !WORKFLOW_WRITE_TOOL_NAMES.has(c.name) ) { return false; } return isToolAllowedForMode(c.name, effectiveTurnMode); }); const forbiddenCalls = calls.filter( (c) => !allowedCalls.includes(c), ); const toolCalls: ToolCall[] = allowedCalls.map((c) => ({ id: c.id, function: { name: c.name, arguments: JSON.stringify(c.input), }, })); const { toolResults, docsRead, docsFound, docsCreated, docsReplicated, workflowsApplied, docsEdited, workflowFindings, workflowRows, } = await runToolCalls( toolCalls, docStore, userId, db, write, workflowStore, tabularStore, docIndex, turnEditState, projectId, assistantTrace, commentAttemptState, anchorManifestState, pendingCommentPlanState, trackChanges, turnDocumentState, ); assertToolBudget({ toolBatchCount, turnDocumentState, workflowReview: isWorkflowTurn, }); allWorkflowFindings.push(...workflowFindings); workflowAccumulator.addRows(workflowRows); for (const r of docsRead) { events.push({ type: "doc_read", filename: r.filename, document_id: r.document_id, }); } for (const f of docsFound) { events.push({ type: "doc_find", filename: f.filename, query: f.query, total_matches: f.total_matches, }); } for (const dl of docsCreated) { events.push({ type: "doc_created", filename: dl.filename, download_url: dl.download_url, document_id: dl.document_id, version_id: dl.version_id, version_number: dl.version_number ?? null, }); } for (const r of docsReplicated) { events.push({ type: "doc_replicated", filename: r.filename, count: r.count, copies: r.copies, }); } for (const wf of workflowsApplied) { events.push({ type: "workflow_applied", workflow_id: wf.workflow_id, title: wf.title, }); } for (const e of docsEdited) { events.push({ type: "doc_edited", filename: e.filename, document_id: e.document_id, version_id: e.version_id, version_number: e.version_number, download_url: e.download_url, annotations: e.annotations, apply_report: e.apply_report, apply_summary: e.apply_summary, edit_verification: e.edit_verification, }); } // Index alignment would break if any tool branch skips its // push (unhandled tool name, disabled store, guard failure). // Each tool_result already carries its tool_call_id, so key off // that directly — and fall back to an error result for any // tool_use that didn't produce one, so Claude's next request // has a tool_result for every tool_use it sent. const resultByCallId = new Map(); const callById = new Map(toolCalls.map((call) => [call.id, call])); const toolFailures: ToolFailureSummary[] = []; for (const r of toolResults) { const row = r as ToolResultRow; resultByCallId.set(row.tool_call_id, String(row.content ?? "")); const failure = summarizeToolFailure( row, callById.get(row.tool_call_id), ); if (failure) toolFailures.push(failure); } for (const call of toolCalls) { if (!resultByCallId.has(call.id)) { toolFailures.push(summarizeMissingToolResult(call)); } } for (const call of forbiddenCalls) { resultByCallId.set( call.id, JSON.stringify({ error: forbiddenToolMessage( call.name, effectiveTurnMode, ), }), ); } const normalizedResults: NormalizedToolResult[] = calls.map((c) => ({ tool_use_id: c.id, content: resultByCallId.get(c.id) ?? JSON.stringify({ error: `Tool '${c.name}' is not available.`, }), })); if (isWorkflowTurn) { const decision = isWorkflowTerminalBatch({ calls, toolResults: normalizedResults, workflowRows: workflowAccumulator.getRows(), }); if (decision.terminal) { return { results: normalizedResults, control: { endTurn: true, suppressFurtherModelCalls: true, reason: decision.reason === "applied" ? "workflow_terminal" : decision.reason === "no_action" ? "workflow_no_action" : "workflow_failed", }, }; } } return normalizedResults; } catch (err) { throw err; } }, }); flushReasoning(); flushText(); if (effectiveTurnMode === "web_search") { const hasAnswerContent = events.some( (event) => event.type === "content" && event.text.trim().length > 0, ); if (!hasAnswerContent) { throw new ControlledUserFacingError({ code: "assistant_stream_error", message: "Web search completed but no answer was produced. Please retry.", safeDetails: { turn_mode: effectiveTurnMode, model: selectedModel, event_types: events.map((event) => event.type), has_answer_content: hasAnswerContent, }, }); } } if (isDocumentEditTurn) { const hasVisibleText = fullText.trim().length > 0 || events.some( (event) => event.type === "content" && event.text.trim().length > 0, ); const visibleText = [ fullText, ...events .filter((event) => event.type === "content") .map((event) => (event as { text: string }).text), ].join("\n"); const hasAppliedDocEdit = events.some( (event) => event.type === "doc_edited", ); const hasClarification = isClarifyingDocumentEditResponse(visibleText); if (!hasAppliedDocEdit && !hasClarification) { throw new ControlledUserFacingError({ code: "assistant_stream_error", message: hasVisibleText ? "The model responded without applying a document edit or asking a clarification question. No document changes were saved. Please retry, or switch to a non-proxy model if this repeats." : "The model stopped without applying a document edit. No document changes were saved. Please retry, or switch to a non-proxy model if this repeats.", safeDetails: { turn_mode: effectiveTurnMode, model: selectedModel, event_types: events.map((event) => event.type), has_visible_text: hasVisibleText, has_clarification: hasClarification, }, }); } } // Parse and emit citations from block plus web grounding const docCitations = buildCitations ? buildCitations(fullText) : parseCitations(fullText).map((c) => { const docInfo = resolveDoc(c.doc_id, docIndex); return { ref: c.ref, doc_id: c.doc_id, document_id: docInfo?.document_id, version_id: docInfo?.version_id ?? null, version_number: docInfo?.version_number ?? null, filename: docInfo?.filename ?? c.doc_id, page: c.page, quote: c.quote, }; }); const webCitations = extractWebAnnotationsFromEvents(events); const citations = [...docCitations, ...webCitations]; write(`data: ${JSON.stringify({ type: "citations", citations })}\n\n`); if (isWorkflowTurn) { const render = buildWorkflowSummaryFromRows(workflowAccumulator.getRows()); if (render.reasoningMarkdown.trim()) { events.push({ type: "reasoning", text: render.reasoningMarkdown, }); write( `data: ${JSON.stringify({ type: "reasoning_block_end", })}\n\n`, ); } events.push({ type: "workflow_summary", summary: render.markdown, reasoning: render.reasoningMarkdown, metrics: render.metrics, rows: workflowAccumulator.getRows(), }); write(`data: ${JSON.stringify({ type: "workflow_summary_replace", summary: render.markdown, reasoning: render.reasoningMarkdown, metrics: render.metrics, rows: workflowAccumulator.getRows(), })}\n\n`); } write("data: [DONE]\n\n"); await endTrace(assistantTrace, { outputs: { fullTextLength: fullText.length, event_count: events.length, citation_count: citations.length, ...(isWorkflowTurn ? { workflow_terminal_after_tool: streamResult.workflowTerminatedAfterTools ?? false, workflow_llm_iterations: streamResult.iterations ?? 0, workflow_prevented_final_call: streamResult.workflowTerminatedAfterTools ?? false, suppressed_workflow_text_chars: streamResult.suppressedWorkflowTextLength ?? 0, workflow_findings_total: workflowAccumulator.size(), workflow_findings_applied: workflowAccumulator.getRows().filter( (r) => r.apply_status === "applied", ).length, workflow_findings_failed: workflowAccumulator.getRows().filter( (r) => r.apply_status !== "applied" && r.apply_status !== "ignored_pass", ).length, workflow_findings_ignored_pass: workflowAccumulator.getRows().filter( (r) => r.apply_status === "ignored_pass", ).length, } : {}), }, }); clearInterval(keepAliveInterval); return { fullText, events, saudiLawEvidence: activeSaudiLawEvidence }; } catch (err) { clearInterval(keepAliveInterval); await endTrace(assistantTrace, { outputs: { fullTextLength: fullText.length, event_count: events.length, }, error: err instanceof Error ? err.message : String(err), }); throw err; } } // --------------------------------------------------------------------------- // Annotation extraction (for DB save) // --------------------------------------------------------------------------- export function extractAnnotations( fullText: string, docIndex: DocIndex, events?: { type: string } & Record[] | unknown[], ): unknown[] { const out: unknown[] = parseCitations(fullText).map((c) => { const docInfo = resolveDoc(c.doc_id, docIndex); return { type: "citation_data", ref: c.ref, doc_id: c.doc_id, document_id: docInfo?.document_id, version_id: docInfo?.version_id ?? null, version_number: docInfo?.version_number ?? null, filename: docInfo?.filename ?? c.doc_id, page: c.page, quote: c.quote, }; }); if (Array.isArray(events)) { for (const ev of events as { type?: string; annotations?: EditAnnotation[] }[]) { if (ev?.type === "doc_edited" && Array.isArray(ev.annotations)) { for (const a of ev.annotations) out.push({ ...a, type: "edit_data" }); } } out.push(...extractWebAnnotationsFromEvents(events)); } return out; } // --------------------------------------------------------------------------- // Document context builder (from message file attachments) // --------------------------------------------------------------------------- export async function buildDocContext( messages: ChatMessage[], userId: string, db: ReturnType, chatId?: string | null, ): Promise<{ docIndex: DocIndex; docStore: DocStore }> { const docIndex: DocIndex = {}; const docStore: DocStore = new Map(); const documentIds = new Set(); for (const m of messages) { for (const f of m.files ?? []) { if (f.document_id) documentIds.add(f.document_id); } } // Also pull in document_ids from prior assistant events in this chat — // generated docs (generate_docx) and tracked-change edits (plan_comments_on_document) // aren't attached to user messages as files, so they only live in the // assistant's `doc_created` / `doc_edited` events. Without this sweep // the model loses access to generated docs after the turn that created // them, and can't call plan_comments_on_document / read_document on them. if (chatId) { const { data: rows } = await db .from("chat_messages") .select("content") .eq("chat_id", chatId) .eq("role", "assistant"); for (const row of rows ?? []) { const content = (row as { content?: unknown }).content; if (!Array.isArray(content)) continue; for (const ev of content as Record[]) { if ( (ev?.type === "doc_created" || ev?.type === "doc_edited") && typeof ev.document_id === "string" ) { documentIds.add(ev.document_id); } } } } const ids = [...documentIds]; if (ids.length > 0) { const { data: docs } = await db .from("documents") .select("id, filename, file_type, current_version_id, status") .in("id", ids) .eq("user_id", userId) .eq("status", "ready"); const docList = (docs ?? []) as unknown as { id: string; filename: string; file_type: string; current_version_id?: string | null; active_version_number?: number | null; storage_path?: string | null; }[]; await attachActiveVersionPaths(db, docList); for (let i = 0; i < docList.length; i++) { const doc = docList[i]; if (!doc.storage_path) continue; const docLabel = `doc-${i}`; docIndex[docLabel] = { document_id: doc.id, filename: doc.filename, file_type: doc.file_type, version_id: doc.current_version_id ?? null, version_number: doc.active_version_number ?? null, }; docStore.set(docLabel, { storage_path: doc.storage_path, file_type: doc.file_type, filename: doc.filename, }); } } console.log( "[buildDocContext] available docs:", Object.entries(docIndex).map(([label, info]) => ({ label, filename: info.filename, document_id: info.document_id, })), ); return { docIndex, docStore }; } export async function buildProjectDocContext( projectId: string, _userId: string, db: ReturnType, ): Promise<{ docIndex: DocIndex; docStore: DocStore; folderPaths: Map }> { const docIndex: DocIndex = {}; const docStore: DocStore = new Map(); const [{ data: docs }, { data: folders }] = await Promise.all([ db.from("documents") .select("id, filename, file_type, current_version_id, status, folder_id") .eq("project_id", projectId) .eq("status", "ready") .order("created_at", { ascending: true }), db.from("project_subfolders") .select("id, name, parent_folder_id") .eq("project_id", projectId), ]); const docList = (docs ?? []) as unknown as { id: string; filename: string; file_type: string; current_version_id?: string | null; active_version_number?: number | null; folder_id?: string | null; storage_path?: string | null; }[]; await attachActiveVersionPaths(db, docList); // Build folder id → full path map const folderMap = new Map(); for (const f of folders ?? []) folderMap.set(f.id, { name: f.name, parent_folder_id: f.parent_folder_id }); function resolvePath(folderId: string | null): string { if (!folderId) return ""; const parts: string[] = []; let cur: string | null = folderId; while (cur) { const f = folderMap.get(cur); if (!f) break; parts.unshift(f.name); cur = f.parent_folder_id; } return parts.join(" / "); } const folderPaths = new Map(); // doc label → folder path for (let i = 0; i < docList.length; i++) { const doc = docList[i]; if (!doc.storage_path) continue; const docLabel = `doc-${i}`; docIndex[docLabel] = { document_id: doc.id, filename: doc.filename, file_type: doc.file_type, version_id: doc.current_version_id ?? null, version_number: doc.active_version_number ?? null, }; docStore.set(docLabel, { storage_path: doc.storage_path, file_type: doc.file_type, filename: doc.filename, }); const path = resolvePath(doc.folder_id ?? null); if (path) folderPaths.set(docLabel, path); } console.log( "[buildProjectDocContext] available docs:", Object.entries(docIndex).map(([label, info]) => ({ label, filename: info.filename, document_id: info.document_id, folder: folderPaths.get(label) ?? null, })), ); return { docIndex, docStore, folderPaths }; } export async function buildWorkflowStore( userId: string, userEmail: string | null | undefined, db: ReturnType, ): Promise { const { BUILTIN_WORKFLOWS } = await import("./builtinWorkflows"); const store: WorkflowStore = new Map(); const normalizedUserEmail = (userEmail ?? "").trim().toLowerCase(); // Seed built-ins first for (const wf of BUILTIN_WORKFLOWS) { store.set(wf.id, { title: wf.title, prompt_md: wf.prompt_md, edit_mode_prompt_md: (wf as any).edit_mode_prompt_md }); } // Then overlay user-owned assistant workflows. const { data: workflows } = await db .from("workflows") .select("id, title, prompt_md") .eq("user_id", userId) .eq("type", "assistant"); for (const wf of workflows ?? []) { if (wf.prompt_md) { store.set(wf.id, { title: wf.title, prompt_md: wf.prompt_md }); } } // Shared assistant workflows must also be readable by workflow tools. if (normalizedUserEmail) { const { data: shares } = await db .from("workflow_shares") .select("workflow_id") .eq("shared_with_email", normalizedUserEmail); const sharedIds = [...new Set((shares ?? []).map((share) => share.workflow_id))]; if (sharedIds.length > 0) { const { data: sharedWorkflows } = await db .from("workflows") .select("id, title, prompt_md") .in("id", sharedIds) .eq("type", "assistant"); for (const wf of sharedWorkflows ?? []) { if (wf.prompt_md) { store.set(wf.id, { title: wf.title, prompt_md: wf.prompt_md }); } } } } return store; }