Spaces:
Sleeping
Sleeping
Download backend/src/lib/chatTools.ts from bardd/mike-backend: direct link, hf CLI and curl.
- Browser
- Download file 397 kB
-
https://huggingface.co/spaces/bardd/mike-backend/resolve/main/backend/src/lib/chatTools.ts
- Command line
-
hf download hf://spaces/bardd/mike-backend/backend/src/lib/chatTools.ts
-
curl -L -o chatTools.ts https://huggingface.co/spaces/bardd/mike-backend/resolve/main/backend/src/lib/chatTools.ts
397 kB
| 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<T>( | |
| fn: (signal: AbortSignal) => Promise<T>, | |
| externalSignal?: AbortSignal, | |
| ): Promise<T> { | |
| 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<void> { | |
| 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<string, { title: string; prompt_md: string; edit_mode_prompt_md?: string }>; | |
| 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<string, { summary: string; flag?: string; reasoning?: string } | null>; | |
| }; | |
| export type ToolCall = { | |
| id: string; | |
| function: { name: string; arguments: string }; | |
| }; | |
| // Canonical anchor map cache: surface_id -> block_id -> CanonicalAnchor | |
| const surfaceAnchorCache = new Map<string, Map<string, CanonicalAnchor>>(); | |
| type AnchorManifest = { | |
| docLabel: string; | |
| documentId?: string; | |
| docRevisionId: string; | |
| surfaceId: string; | |
| readSerial: number; | |
| anchors: Map<string, CanonicalAnchor>; | |
| }; | |
| type AnchorManifestState = { | |
| byDocLabel: Map<string, AnchorManifest>; | |
| 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<ReturnType<typeof buildDocxEditSurface>>, | |
| 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<ReturnType<typeof buildDocxEditSurface>>, | |
| 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<string, TurnReadRecord>; | |
| 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<ReturnType<typeof buildDocxEditSurface>>, | |
| ): AnchorManifest | null { | |
| const anchorMap = new Map<string, CanonicalAnchor>(); | |
| 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<AssistantTurnMode, ReadonlySet<string>> = { | |
| answer: new Set<string>([...READ_ONLY_TOOLS]), | |
| web_search: new Set<string>([...READ_ONLY_TOOLS]), | |
| comments: new Set<string>([...READ_ONLY_TOOLS, ...COMMENT_TOOLS]), | |
| workflow_review: new Set<string>([ | |
| ...READ_ONLY_TOOLS, | |
| ...COMMENT_TOOLS, | |
| ...GENERATION_TOOLS, | |
| ]), | |
| document_generation: new Set<string>([ | |
| ...READ_ONLY_TOOLS, | |
| ...GENERATION_TOOLS, | |
| ]), | |
| document_direct_edit: new Set<string>([ | |
| ...READ_ONLY_TOOLS, | |
| ...DIRECT_EDIT_TOOLS, | |
| ]), | |
| document_edit_with_web_search: new Set<string>([ | |
| ...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<string>([ | |
| ...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<string, unknown>; | |
| 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<typeof createServerSupabase>, | |
| docIndex: DocIndex, | |
| ): Promise<ChatMessage[]> { | |
| 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<string, string>(); | |
| 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<string, unknown>[]) { | |
| 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<string, string>(); | |
| 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<PdfVirtualSurface> { | |
| 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<string, CanonicalAnchor>(); | |
| 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<string> { | |
| try { | |
| const surface = await buildPdfVirtualSurface(buf); | |
| return surface.text; | |
| } catch { | |
| return ""; | |
| } | |
| } | |
| export async function generateDocx( | |
| title: string, | |
| sections: unknown[], | |
| userId: string, | |
| db: ReturnType<typeof createServerSupabase>, | |
| 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<typeof Paragraph> | InstanceType<typeof Table>; | |
| 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<typeof TableRow>[] = []; | |
| // 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<typeof createServerSupabase>, | |
| ): 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<typeof createServerSupabase>; | |
| /** | |
| * 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<EditVerification> { | |
| // 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<CommentBatchVerification> { | |
| 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<string, unknown>): Record<string, unknown> { | |
| 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<string, unknown>, | |
| docStore: DocStore, | |
| ): CommentArgValidation { | |
| const errors: string[] = []; | |
| const validated: Partial<CommentArgValidation["validated"]> = {}; | |
| // 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<typeof createServerSupabase>; | |
| 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<typeof createServerSupabase>, | |
| opts?: { emitEvents?: boolean }, | |
| anchorManifestState?: AnchorManifestState, | |
| trace?: RunTree | null, | |
| ): Promise<string> { | |
| 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<typeof createServerSupabase>; | |
| opts?: { emitEvents?: boolean }; | |
| anchorManifestState?: AnchorManifestState; | |
| trace?: RunTree | null; | |
| turnDocumentState: TurnDocumentState; | |
| }): Promise<string> { | |
| 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<typeof createServerSupabase>; | |
| anchorManifestState?: AnchorManifestState; | |
| }): Promise<string> { | |
| 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<ReturnType<DocStore["get"]>>; | |
| indexed: DocIndex[string]; | |
| current: NonNullable<Awaited<ReturnType<typeof loadCurrentVersionBytes>>>; | |
| currentSurface: Awaited<ReturnType<typeof buildDocxEditSurface>>; | |
| manifest: AnchorManifest | undefined; | |
| valid: PlannedCommentItem[]; | |
| needsCorrection: PlannedCommentItem[]; | |
| failedPermanently: PlannedCommentItem[]; | |
| ignoredPassReports: Record<string, any>[]; | |
| }; | |
| export type PendingCommentPlanState = Map<string, PendingCommentPlan>; | |
| // 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<string, unknown>; | |
| 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<string, CommentVerificationRow>()) | |
| .values(), | |
| ); | |
| } | |
| type ValidatedCommentArgs = CommentArgValidation["validated"] & { | |
| comment_key: string; | |
| }; | |
| type PreparedCommentCall = { | |
| tc: ToolCall; | |
| docLabel: string; | |
| docInfo: NonNullable<ReturnType<DocStore["get"]>>; | |
| 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<string, unknown>; | |
| 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<string, unknown> { | |
| 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<string, unknown> { | |
| 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<string, any>; | |
| 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<string, any>, 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<string, any>()) | |
| .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<string, any>; | |
| 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<ReturnType<typeof buildDocxEditSurface>>; | |
| commentAttemptState?: CommentAttemptState; | |
| }): { ok: true; anchor: CanonicalAnchor; readSerial: number } | { ok: false; report: Record<string, unknown>; 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<ReturnType<typeof buildDocxEditSurface>>; | |
| 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<ReturnType<typeof buildDocxEditSurface>>, | |
| 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<ReturnType<typeof buildDocxEditSurface>>; | |
| }): 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<ReturnType<typeof buildDocxEditSurface>>; | |
| 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<ReturnType<typeof buildDocxEditSurface>>; | |
| 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<ReturnType<typeof buildDocxEditSurface>>; | |
| }): | |
| | { 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<ReturnType<typeof buildDocxEditSurface>>; | |
| 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<CommentPlanIntent["reasoning"]> { | |
| 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<ReturnType<typeof buildDocxEditSurface>>; | |
| 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<ReturnType<typeof buildDocxEditSurface>>; | |
| 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<ReturnType<typeof buildDocxEditSurface>>; | |
| }): Record<string, unknown> { | |
| 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<string, unknown> { | |
| 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<ReturnType<DocStore["get"]>>; | |
| current: NonNullable<Awaited<ReturnType<typeof loadCurrentVersionBytes>>>; | |
| comments: WriterCommentItem[]; | |
| db: ReturnType<typeof createServerSupabase>; | |
| 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<string, any>(); | |
| 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<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; source?: CommentPlanSource; amended_wording?: string }>(); | |
| 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<string, unknown>; | |
| 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<string, unknown>).clause_section ?? "")), | |
| violated_standard: sanitizeWorkflowDisplayText(String((r.reasoning as Record<string, unknown>).violated_standard ?? "")), | |
| zamil_exposure: sanitizeWorkflowDisplayText(String((r.reasoning as Record<string, unknown>).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<string, unknown>; | |
| docStore: DocStore; | |
| docIndex: DocIndex; | |
| userId: string; | |
| db: ReturnType<typeof createServerSupabase>; | |
| 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<string, unknown>; | |
| 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<string, unknown>).clause_section ?? "").trim(), violated_standard: String((report.reasoning as Record<string, unknown>).violated_standard ?? "").trim(), zamil_exposure: String((report.reasoning as Record<string, unknown>).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<string, unknown>).clause_section ?? "").trim(), violated_standard: String((report.reasoning as Record<string, unknown>).violated_standard ?? "").trim(), zamil_exposure: String((report.reasoning as Record<string, unknown>).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<string, unknown>; | |
| docStore: DocStore; | |
| docIndex: DocIndex; | |
| userId: string; | |
| db: ReturnType<typeof createServerSupabase>; | |
| 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<string, unknown> => !!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<string, unknown>).clause_section ?? "").trim(), violated_standard: String((report.reasoning as Record<string, unknown>).violated_standard ?? "").trim(), zamil_exposure: String((report.reasoning as Record<string, unknown>).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<string, unknown>[] { | |
| 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<ReturnType<DocStore["get"]>>; | |
| current: NonNullable<Awaited<ReturnType<typeof loadCurrentVersionBytes>>>; | |
| comments: Array<WriterCommentItem>; | |
| db: ReturnType<typeof createServerSupabase>; | |
| 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<string, unknown> { | |
| 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<ReturnType<typeof buildDocxEditSurface>>, | |
| ): 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<ReturnType<typeof buildDocxEditSurface>>; | |
| 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<number>(); | |
| 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<string, unknown>; | |
| docStore: DocStore; | |
| docIndex: DocIndex; | |
| db: ReturnType<typeof createServerSupabase>; | |
| 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<string, unknown>; | |
| docStore: DocStore; | |
| docIndex: DocIndex; | |
| db: ReturnType<typeof createServerSupabase>; | |
| 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<string, unknown>; | |
| docStore: DocStore; | |
| docIndex: DocIndex; | |
| userId: string; | |
| db: ReturnType<typeof createServerSupabase>; | |
| 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<string, unknown>) => ({ | |
| 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<typeof createServerSupabase>; | |
| write: (s: string) => void; | |
| trace?: RunTree | null; | |
| commentAttemptState?: CommentAttemptState; | |
| anchorManifestState?: AnchorManifestState; | |
| toolResults: unknown[]; | |
| docsEdited: DocEditedResult[]; | |
| commentVerificationRows: CommentVerificationRow[]; | |
| }): Promise<void> { | |
| const { | |
| commentCalls, | |
| docStore, | |
| docIndex, | |
| userId, | |
| db, | |
| write, | |
| trace, | |
| commentAttemptState, | |
| anchorManifestState, | |
| toolResults, | |
| docsEdited, | |
| commentVerificationRows, | |
| } = params; | |
| const preparedByDoc = new Map<string, PreparedCommentCall[]>(); | |
| const resultByToolCallId = new Map<string, string>(); | |
| const addReportResult = ( | |
| tc: ToolCall, | |
| report: Record<string, any>, | |
| 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<string, unknown> = {}; | |
| 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<PreparedCommentCall & { anchor: CanonicalAnchor; readSerial: number }> = []; | |
| const prevalidationFailures: Record<string, any>[] = []; | |
| 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<string, any>; | |
| 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<string, any>(); | |
| 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<string, unknown> : {}; | |
| 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<typeof createServerSupabase>, | |
| 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<string>(); | |
| const effectiveTurnDocumentState = turnDocumentState ?? createTurnDocumentState(); | |
| for (const tc of toolCalls) { | |
| let args: Record<string, unknown> = {}; | |
| 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<unknown>[] = []; | |
| 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<string, string>(); | |
| 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<string, unknown>), 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 = /<CITATIONS>\s*([\s\S]*?)\s*<\/CITATIONS>/; | |
| const CITATIONS_OPEN_TAG = "<CITATIONS>"; | |
| 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<WorkflowApplyStatus, number> = { | |
| 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<string, WorkflowFindingRow>(); | |
| 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<string, any> { | |
| 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<string, string> = { | |
| COMMENT: "Comment", | |
| REWRITE_PARAGRAPH: "Rewrite", | |
| INSERT_PARAGRAPH: "Insert", | |
| DELETE_PARAGRAPH: "Delete", | |
| }; | |
| const STATUS_LABELS: Record<string, string> = { | |
| FAIL: "FAIL", | |
| RISK: "RISK", | |
| REVIEW: "REVIEW", | |
| ESCALATE: "ESCALATE", | |
| MISSING: "MISSING", | |
| }; | |
| const SEVERITY_ORDER: Record<string, number> = { | |
| FAIL: 1, | |
| MISSING: 2, | |
| RISK: 3, | |
| ESCALATE: 4, | |
| REVIEW: 5, | |
| }; | |
| const ACTION_ORDER: Record<string, number> = { | |
| 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<string, number>; | |
| }; | |
| 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<typeof createServerSupabase>; | |
| 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<string, string>(); | |
| 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 <CITATIONS> 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<string, unknown>[] | 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<typeof createServerSupabase>, | |
| chatId?: string | null, | |
| ): Promise<{ docIndex: DocIndex; docStore: DocStore }> { | |
| const docIndex: DocIndex = {}; | |
| const docStore: DocStore = new Map(); | |
| const documentIds = new Set<string>(); | |
| 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<string, unknown>[]) { | |
| 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<typeof createServerSupabase>, | |
| ): Promise<{ docIndex: DocIndex; docStore: DocStore; folderPaths: Map<string, string> }> { | |
| 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<string, { name: string; parent_folder_id: string | null }>(); | |
| 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<string, string>(); // 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<typeof createServerSupabase>, | |
| ): Promise<WorkflowStore> { | |
| 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; | |
| } | |