import { getCourtlistenerCases, searchCourtlistenerCaseLaw, verifyCourtlistenerCitations, } from "../../courtlistener"; import { COURTLISTENER_TOOL_NAMES, type CaseCitationEvent, type CourtlistenerToolEvent, } from "./courtlistenerTools"; import { executeMcpToolCall, type McpToolEvent, } from "../../mcpConnectors"; import { createServerSupabase } from "../../supabase"; import { type DocStore, type DocIndex, type TabularCellStore, type WorkflowStore, type ToolCall, type AskInputItem, type AskInputOption, type AskInputsEvent, devLog, resolveDocLabel, } from "../types"; import { downloadFile, storageKey, uploadFile, } from "../../storage"; import { convertedPdfKey } from "../../convert"; import { contentTypeForDocumentType } from "../../documentTypes"; import { buildDownloadUrl } from "../../downloadTokens"; import { loadActiveVersion } from "../../documentVersions"; import { type EditInput } from "../../docxTrackedChanges"; import { citationReminder, generateDocx, generateExcel, generatePpt, getTurnReadIdentity, duplicateReadDocumentResult, clearTurnReadsForDocument, readDocumentContent, findInDocumentContent, findTextMatches, runEditDocument, safeGeneratedFilename, type DocEditedResult, type TurnEditState, type TurnReadState, type DocCreatedResult, type DocReplicatedResult, type TextMatch, } from "./documentOps"; type CourtlistenerCaseRecord = { clusterId: number; caseName: string | null; citations: string[]; url: string | null; pdfUrl: string | null; dateFiled: string | null; opinions?: unknown[]; }; type CourtlistenerCaseInput = { clusterId?: number | null; caseName?: string | null; citation?: string | null; citations?: string[]; url?: string | null; pdfUrl?: string | null; dateFiled?: string | null; opinions?: unknown[]; }; export type CourtlistenerTurnState = { casesByClusterId: Map; }; function nonEmpty(value: unknown): string | null { return typeof value === "string" && value.trim() ? value.trim() : null; } function cleanAskInputString(value: unknown, fallback = ""): string { const text = typeof value === "string" ? value.trim() : ""; return text || fallback; } function normalizeAskInputsEvent(args: Record): AskInputsEvent { const rawItems = Array.isArray(args.items) ? args.items : []; const items = rawItems .map((item, index): AskInputItem | null => { if (!item || typeof item !== "object" || Array.isArray(item)) return null; const row = item as Record; const id = cleanAskInputString(row.id) || `${row.kind === "documents" ? "documents" : "choice"}-${index + 1}`; const responsePrefix = cleanAskInputString(row.response_prefix); if (row.kind === "documents") { const rawDocumentTypes = Array.isArray(row.document_types) ? row.document_types : []; const documentTypes = rawDocumentTypes .filter((type): type is string => typeof type === "string") .map((type) => type.trim()) .filter(Boolean) .map((type) => type.slice(0, 300)) .slice(0, 8); return { id: id.slice(0, 80), kind: "documents", document_types: documentTypes, ...(responsePrefix ? { response_prefix: responsePrefix.slice(0, 200) } : {}), }; } const question = cleanAskInputString( row.question, "Please choose an option.", ); const rawOptions = Array.isArray(row.options) ? row.options : []; const options = rawOptions .map((option): AskInputOption | null => { if (!option || typeof option !== "object") return null; const optionRow = option as Record; const value = cleanAskInputString(optionRow.value) || cleanAskInputString(optionRow.label); if (!value) return null; return { value: value.slice(0, 500), }; }) .filter((option): option is AskInputOption => !!option) .slice(0, 8); const normalizedOptions = options.length > 0 ? options : [{ value: "Continue" }]; const otherLabel = cleanAskInputString(row.other_label, "Other"); return { id: id.slice(0, 80), kind: "choice", question: question.slice(0, 500), options: normalizedOptions, allow_other: row.allow_other !== false, other_label: otherLabel.slice(0, 80), ...(responsePrefix ? { response_prefix: responsePrefix.slice(0, 200) } : {}), }; }) .filter((item): item is AskInputItem => !!item) .slice(0, 12); return { type: "ask_inputs", items }; } function upsertCourtlistenerCases( state: CourtlistenerTurnState, inputs: CourtlistenerCaseInput[], ): CourtlistenerCaseRecord[] { const records: CourtlistenerCaseRecord[] = []; for (const input of inputs) { if (typeof input.clusterId !== "number" || !Number.isFinite(input.clusterId)) { continue; } const clusterId = Math.floor(input.clusterId); const current = state.casesByClusterId.get(clusterId) ?? { clusterId, caseName: null, citations: [], url: null, pdfUrl: null, dateFiled: null, }; const nextCitations = [ ...current.citations, ...(input.citation ? [input.citation] : []), ...(input.citations ?? []), ] .map(nonEmpty) .filter((value): value is string => !!value); const record: CourtlistenerCaseRecord = { ...current, caseName: current.caseName ?? nonEmpty(input.caseName), citations: Array.from(new Set(nextCitations)), url: current.url ?? nonEmpty(input.url), pdfUrl: current.pdfUrl ?? nonEmpty(input.pdfUrl), dateFiled: current.dateFiled ?? nonEmpty(input.dateFiled), opinions: current.opinions ?? input.opinions, }; state.casesByClusterId.set(clusterId, record); records.push(record); } return records; } function caseCitationEventFromRecord( record: CourtlistenerCaseRecord, ): CaseCitationEvent | null { if (!record.url) return null; return { type: "case_citation", cluster_id: record.clusterId, case_name: record.caseName, citation: record.citations[0] ?? null, url: record.url, pdfUrl: record.pdfUrl, dateFiled: record.dateFiled, }; } function recordFromUnknown(value: unknown): Record | null { return value && typeof value === "object" && !Array.isArray(value) ? (value as Record) : null; } function stringField( record: Record | null, key: string, ): string | null { const value = record?.[key]; return typeof value === "string" ? value : null; } function numberField( record: Record | null, key: string, ): number | null { const value = record?.[key]; return typeof value === "number" && Number.isFinite(value) ? Math.floor(value) : null; } function stringArrayField( record: Record | null, key: string, ): string[] { const value = record?.[key]; return Array.isArray(value) ? value.filter((item): item is string => typeof item === "string") : []; } function courtlistenerCaseInputFromFetchedCase( fallbackClusterId: number, fetchedCase: unknown, ): CourtlistenerCaseInput { const record = recordFromUnknown(fetchedCase); const clusterId = numberField(record, "clusterId") ?? numberField(record, "id") ?? fallbackClusterId; return { clusterId, caseName: stringField(record, "caseName"), citations: stringArrayField(record, "citations"), url: stringField(record, "url"), pdfUrl: stringField(record, "pdfUrl"), dateFiled: stringField(record, "dateFiled"), opinions: Array.isArray(record?.opinions) ? record.opinions : undefined, }; } function courtlistenerOpinionCount(fetchedCase: unknown): number { const record = recordFromUnknown(fetchedCase); return Array.isArray(record?.opinions) ? record.opinions.length : 0; } function courtlistenerOpinionMetadata(raw: unknown) { const opinion = recordFromUnknown(raw); if (!opinion) return null; const text = stringField(opinion, "text") ?? (stringField(opinion, "html") ? stripCaseOpinionHtml(stringField(opinion, "html")!) : null); return { opinion_id: numberField(opinion, "opinionId") ?? numberField(opinion, "id"), type: stringField(opinion, "type"), author: stringField(opinion, "author"), per_curiam: stringField(opinion, "per_curiam"), joined_by_str: stringField(opinion, "joined_by_str"), url: stringField(opinion, "url"), char_count: text?.length ?? 0, }; } function courtlistenerFetchedCaseMetadata( record: CourtlistenerCaseRecord, opinionCount: number, ) { return { cluster_id: record.clusterId, case_name: record.caseName, citation: record.citations[0] ?? null, citations: record.citations, dateFiled: record.dateFiled, url: record.url, pdfUrl: record.pdfUrl, opinion_count: opinionCount, opinions: (record.opinions ?? []) .map(courtlistenerOpinionMetadata) .filter((opinion): opinion is NonNullable => !!opinion), }; } function stripCaseOpinionHtml(value: string): string { return value .replace(//gi, " ") .replace(//gi, " ") .replace(/<[^>]+>/g, " ") .replace(/ /g, " ") .replace(/&/g, "&") .replace(/</g, "<") .replace(/>/g, ">") .replace(/"/g, '"') .replace(/'/g, "'") .replace(/\s+/g, " ") .trim(); } type CachedCaseOpinionText = { opinion_id: number | null; type: string | null; author: string | null; url: string | null; text: string; }; function cachedCaseOpinionTexts( record: CourtlistenerCaseRecord, ): CachedCaseOpinionText[] { return (record.opinions ?? []) .map((raw) => { const opinion = recordFromUnknown(raw); if (!opinion) return null; const text = stringField(opinion, "text") ?? (stringField(opinion, "html") ? stripCaseOpinionHtml(stringField(opinion, "html")!) : null); if (!text) return null; return { opinion_id: numberField(opinion, "opinionId") ?? numberField(opinion, "id"), type: stringField(opinion, "type"), author: stringField(opinion, "author"), url: stringField(opinion, "url"), text, }; }) .filter((opinion): opinion is CachedCaseOpinionText => !!opinion); } function requestedCourtlistenerOpinionIds(args: Record) { const rawIds = Array.isArray(args.opinionIds) ? args.opinionIds : Array.isArray(args.opinion_ids) ? args.opinion_ids : typeof args.opinionId === "number" ? [args.opinionId] : typeof args.opinion_id === "number" ? [args.opinion_id] : []; return Array.from( new Set( rawIds .filter((value): value is number => typeof value === "number") .filter((value) => Number.isFinite(value) && value > 0) .map((value) => Math.floor(value)), ), ); } type FindInCaseArgs = { clusterId: number | null; query: string; maxResults: number; contextChars: number; }; function parseFindInCaseArgs(args: Record): FindInCaseArgs { return { clusterId: typeof args.clusterId === "number" && Number.isFinite(args.clusterId) ? Math.floor(args.clusterId) : typeof args.cluster_id === "number" && Number.isFinite(args.cluster_id) ? Math.floor(args.cluster_id) : null, query: typeof args.query === "string" ? args.query : "", maxResults: typeof args.max_results === "number" ? Math.max(0, Math.floor(args.max_results)) : 20, contextChars: typeof args.context_chars === "number" ? Math.max(0, Math.floor(args.context_chars)) : 160, }; } function findInCaseSearchSummary( event: Extract, ) { return { cluster_id: event.cluster_id, query: event.query, total_matches: event.total_matches, case_name: event.case_name, citation: event.citation, error: event.error, }; } function cachedCaseNotFetchedResult(clusterId: number | null) { return { ok: false, cluster_id: clusterId, error: "Case has not been fetched in this turn. Call courtlistener_get_cases first.", }; } export async function runToolCalls( toolCalls: ToolCall[], docStore: DocStore, userId: string, db: ReturnType, write: (s: string) => void, workflowStore?: WorkflowStore, tabularStore?: TabularCellStore, docIndex?: DocIndex, turnEditState?: TurnEditState, turnReadState?: TurnReadState, projectId?: string | null, courtlistenerState?: CourtlistenerTurnState, apiKeys?: import("../../llm").UserApiKeys, ): 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[]; askInputsEvents: AskInputsEvent[]; courtlistenerEvents: CourtlistenerToolEvent[]; caseCitationEvents: CaseCitationEvent[]; mcpEvents: McpToolEvent[]; }> { 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 askInputsEvents: AskInputsEvent[] = []; const courtlistenerEvents: CourtlistenerToolEvent[] = []; const caseCitationEvents: CaseCitationEvent[] = []; const mcpEvents: McpToolEvent[] = []; const courtState: CourtlistenerTurnState = courtlistenerState ?? { casesByClusterId: new Map(), }; const groupedFindInCaseSearches = toolCalls .filter((tc) => tc.function.name === COURTLISTENER_TOOL_NAMES.findInCase) .map((tc) => { let rawArgs: Record = {}; try { rawArgs = JSON.parse(tc.function.arguments || "{}"); } catch { /* ignore */ } const parsed = parseFindInCaseArgs(rawArgs); return { cluster_id: parsed.clusterId, query: parsed.query, total_matches: 0, }; }); const shouldGroupFindInCase = groupedFindInCaseSearches.length > 1; let groupedFindInCaseStarted = false; const groupedFindInCaseEvents: Extract< CourtlistenerToolEvent, { type: "courtlistener_find_in_case" } >[] = []; const registerGeneratedDocument = ( tc: ToolCall, result: Record, previewFilename: string, fileType: string, ) => { 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; 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: fileType, 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`, ); } const { download_url, storage_path, ...safeToolResult } = result; const toolResultPayload = newDocLabel ? { ...safeToolResult, doc_id: newDocLabel, next_required_action: [ `Before writing your final response, call read_document with doc_id "${newDocLabel}".`, `Base your description on the generated document's actual returned text, not on memory of what you intended to generate.`, `Do not include download links, URLs, or markdown links to the document in your prose response; the document card is shown automatically by the UI.`, `Give a concise description of the generated document and, if you make factual claims about its contents, cite it with [N] markers and a final block using doc_id "${newDocLabel}", not any source/template document.`, ].join(" "), } : safeToolResult; toolResults.push({ role: "tool", tool_call_id: tc.id, content: JSON.stringify(toolResultPayload), }); }; for (const tc of toolCalls) { let args: Record = {}; try { args = JSON.parse(tc.function.arguments || "{}"); } catch { /* ignore */ } if (tc.function.name.startsWith("mcp_")) { write( `data: ${JSON.stringify({ type: "mcp_tool_start", name: tc.function.name, })}\n\n`, ); const { content, event } = await executeMcpToolCall( userId, tc.function.name, args, db, ); toolResults.push({ role: "tool", tool_call_id: tc.id, content, }); mcpEvents.push(event); write( `data: ${JSON.stringify({ type: "mcp_tool_result", name: tc.function.name, connector_name: event.connector_name, tool_name: event.tool_name, status: event.status, error: event.error, })}\n\n`, ); continue; } if (tc.function.name === "ask_inputs") { const event = normalizeAskInputsEvent(args); if (event.items.length > 0) askInputsEvents.push(event); continue; } if (tc.function.name === "read_document") { const rawDocId = args.doc_id as string; const docId = resolveDocLabel(rawDocId, docStore, docIndex) ?? rawDocId; const readIdentity = await getTurnReadIdentity({ docLabel: docId, docStore, docIndex, db, }); if (readIdentity && turnReadState?.has(readIdentity.key)) { toolResults.push({ role: "tool", tool_call_id: tc.id, content: duplicateReadDocumentResult(readIdentity), }); continue; } const content = await readDocumentContent( docId, docStore, write, docIndex, db, ); const filename = docStore.get(docId)?.filename; const documentId = docIndex?.[docId]?.document_id; if (readIdentity && turnReadState) { turnReadState.set(readIdentity.key, readIdentity); } if (filename) docsRead.push({ filename, document_id: documentId }); toolResults.push({ role: "tool", tool_call_id: tc.id, content: filename ? `${citationReminder(docId, filename)}\n\n${content}` : 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 content = await findInDocumentContent({ docLabel: docId, query, maxResults, contextChars, docStore, write, docIndex, db, }); 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 === "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 readIdentity = await getTurnReadIdentity({ docLabel: docId, docStore, docIndex, db, }); if (readIdentity && turnReadState?.has(readIdentity.key)) { const filename = docStore.get(docId)?.filename ?? docId; parts.push( `--- ${filename} (${docId}) ---\n${duplicateReadDocumentResult( readIdentity, )}`, ); continue; } const content = await readDocumentContent( docId, docStore, write, docIndex, db, ); const filename = docStore.get(docId)?.filename ?? docId; if (readIdentity && turnReadState) { turnReadState.set(readIdentity.key, readIdentity); } parts.push( `--- ${filename} (${docId}) ---\n${citationReminder(docId, filename)}\n\n${content}`, ); if (docStore.get(docId)) { 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 }); } toolResults.push({ role: "tool", tool_call_id: tc.id, content: wf ? wf.skill_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 === COURTLISTENER_TOOL_NAMES.searchCaseLaw) { const query = typeof args.query === "string" ? args.query : ""; write( `data: ${JSON.stringify({ type: "courtlistener_search_case_law_start", query })}\n\n`, ); try { const result = await searchCourtlistenerCaseLaw({ query: query || undefined, court: typeof args.court === "string" ? args.court : undefined, filedAfter: typeof args.filedAfter === "string" ? args.filedAfter : undefined, filedBefore: typeof args.filedBefore === "string" ? args.filedBefore : undefined, limit: typeof args.limit === "number" ? args.limit : undefined, apiToken: apiKeys?.courtlistener, }); const resultCount = result && typeof result === "object" && Array.isArray((result as { results?: unknown }).results) ? (result as { results: unknown[] }).results.length : 0; const error = result && typeof result === "object" && typeof (result as { error?: unknown }).error === "string" ? (result as { error: string }).error : undefined; const event: CourtlistenerToolEvent = { type: "courtlistener_search_case_law", query, result_count: resultCount, ...(error ? { error } : {}), }; write(`data: ${JSON.stringify(event)}\n\n`); courtlistenerEvents.push(event); toolResults.push({ role: "tool", tool_call_id: tc.id, content: JSON.stringify(result), }); } catch (err) { const event: CourtlistenerToolEvent = { type: "courtlistener_search_case_law", query, result_count: 0, error: err instanceof Error ? err.message : "CourtListener search failed.", }; write(`data: ${JSON.stringify(event)}\n\n`); courtlistenerEvents.push(event); toolResults.push({ role: "tool", tool_call_id: tc.id, content: JSON.stringify({ error: err instanceof Error ? err.message : "CourtListener search failed.", }), }); } } else if (tc.function.name === COURTLISTENER_TOOL_NAMES.getCases) { const rawClusterIds = Array.isArray(args.clusterIds) ? args.clusterIds : Array.isArray(args.cluster_ids) ? args.cluster_ids : typeof args.clusterId === "number" ? [args.clusterId] : []; const clusterIds = Array.from( new Set( rawClusterIds .filter((value): value is number => typeof value === "number") .filter((value) => Number.isFinite(value) && value > 0) .map((value) => Math.floor(value)), ), ); write( `data: ${JSON.stringify({ type: "courtlistener_get_cases_start", cluster_ids: clusterIds })}\n\n`, ); try { const result = await getCourtlistenerCases({ clusterIds, db, apiToken: apiKeys?.courtlistener, }); const fetchedCases = result && typeof result === "object" && Array.isArray((result as { cases?: unknown }).cases) ? (result as { cases: unknown[] }).cases : []; fetchedCases.forEach((fetchedCase, index) => { const clusterId = courtlistenerCaseInputFromFetchedCase( clusterIds[index] ?? 0, fetchedCase, ).clusterId ?? 0; if (clusterId) { write( `data: ${JSON.stringify({ type: "case_opinions", cluster_id: clusterId, case: fetchedCase })}\n\n`, ); } }); const caseRecords = upsertCourtlistenerCases( courtState, fetchedCases.map((fetchedCase, index) => courtlistenerCaseInputFromFetchedCase( clusterIds[index] ?? 0, fetchedCase, ), ), ); const opinionCount = fetchedCases.reduce( (sum, fetchedCase) => sum + courtlistenerOpinionCount(fetchedCase), 0, ); const caseOpinionCountByClusterId = new Map(); fetchedCases.forEach((fetchedCase, index) => { const clusterId = courtlistenerCaseInputFromFetchedCase( clusterIds[index] ?? 0, fetchedCase, ).clusterId ?? 0; if (clusterId) { caseOpinionCountByClusterId.set( clusterId, courtlistenerOpinionCount(fetchedCase), ); } }); const errors = fetchedCases .map((fetchedCase) => stringField(recordFromUnknown(fetchedCase), "error"), ) .filter((error): error is string => !!error); const resultError = result && typeof result === "object" && typeof (result as { error?: unknown }).error === "string" ? (result as { error: string }).error : undefined; const hasMultipleOpinionCase = caseRecords.some( (record) => (caseOpinionCountByClusterId.get(record.clusterId) ?? 0) > 1, ); const event: CourtlistenerToolEvent = { type: "courtlistener_get_cases", cluster_ids: clusterIds, case_count: fetchedCases.length, opinion_count: opinionCount, cases: caseRecords.map((record) => ({ cluster_id: record.clusterId, case_name: record.caseName, citation: record.citations[0] ?? null, dateFiled: record.dateFiled, url: record.url, })), ...(resultError || errors.length ? { error: resultError ?? errors.join("; ") } : {}), }; write(`data: ${JSON.stringify(event)}\n\n`); courtlistenerEvents.push(event); toolResults.push({ role: "tool", tool_call_id: tc.id, content: JSON.stringify({ ok: !resultError && errors.length === 0, cluster_ids: clusterIds, case_count: fetchedCases.length, opinion_count: opinionCount, cases: caseRecords.map((record) => courtlistenerFetchedCaseMetadata( record, caseOpinionCountByClusterId.get(record.clusterId) ?? 0, ), ), ...(resultError || errors.length ? { error: resultError ?? errors.join("; ") } : {}), next_required_action: hasMultipleOpinionCase ? "Opinion text is cached server-side only. Use courtlistener_find_in_case with short 1-3 word keyword probes for relevant passages. At least one fetched case has multiple opinions; if snippets are insufficient, choose the needed opinion_id(s) from the text-free opinion metadata and call courtlistener_read_case with only those IDs. Do not read all opinions unless the question requires it." : "Opinion text is cached server-side only. Use courtlistener_find_in_case with short 1-3 word keyword probes for relevant passages, or courtlistener_read_case if snippets are insufficient.", }), }); } catch (err) { const event: CourtlistenerToolEvent = { type: "courtlistener_get_cases", cluster_ids: clusterIds, case_count: 0, opinion_count: 0, error: err instanceof Error ? err.message : "CourtListener case fetch failed.", }; write(`data: ${JSON.stringify(event)}\n\n`); courtlistenerEvents.push(event); toolResults.push({ role: "tool", tool_call_id: tc.id, content: JSON.stringify({ error: err instanceof Error ? err.message : "CourtListener case fetch failed.", }), }); } } else if (tc.function.name === COURTLISTENER_TOOL_NAMES.findInCase) { const { clusterId, query, maxResults, contextChars } = parseFindInCaseArgs(args); if (shouldGroupFindInCase) { if (!groupedFindInCaseStarted) { write( `data: ${JSON.stringify({ type: "courtlistener_find_in_case_start", cluster_id: null, query: "", searches: groupedFindInCaseSearches, })}\n\n`, ); groupedFindInCaseStarted = true; } } else { write( `data: ${JSON.stringify({ type: "courtlistener_find_in_case_start", cluster_id: clusterId, query })}\n\n`, ); } const record = typeof clusterId === "number" ? courtState.casesByClusterId.get(clusterId) : undefined; if (!record) { const payload = cachedCaseNotFetchedResult(clusterId); const event: CourtlistenerToolEvent = { type: "courtlistener_find_in_case", cluster_id: clusterId, query, total_matches: 0, error: payload.error, }; if (shouldGroupFindInCase) { groupedFindInCaseEvents.push(event); } else { write(`data: ${JSON.stringify(event)}\n\n`); courtlistenerEvents.push(event); } toolResults.push({ role: "tool", tool_call_id: tc.id, content: JSON.stringify(payload), }); continue; } const opinions = cachedCaseOpinionTexts(record); const hits: Array< TextMatch & { opinion_id: number | null; type: string | null; author: string | null; url: string | null; } > = []; let totalMatches = 0; for (const opinion of opinions) { const remaining = Math.max(0, maxResults - hits.length); const result = findTextMatches({ text: opinion.text, query, maxResults: remaining, contextChars, startIndex: hits.length, }); totalMatches += result.totalMatches; hits.push( ...result.hits.map((hit) => ({ ...hit, opinion_id: opinion.opinion_id, type: opinion.type, author: opinion.author, url: opinion.url, })), ); } const event: CourtlistenerToolEvent = { type: "courtlistener_find_in_case", cluster_id: record.clusterId, query, total_matches: totalMatches, case_name: record.caseName, citation: record.citations[0] ?? null, }; if (shouldGroupFindInCase) { groupedFindInCaseEvents.push(event); } else { write(`data: ${JSON.stringify(event)}\n\n`); courtlistenerEvents.push(event); } toolResults.push({ role: "tool", tool_call_id: tc.id, content: JSON.stringify({ ok: true, cluster_id: record.clusterId, case_name: record.caseName, citation: record.citations[0] ?? null, query, total_matches: totalMatches, returned: hits.length, truncated: totalMatches > hits.length, hits, }), }); } else if (tc.function.name === COURTLISTENER_TOOL_NAMES.readCase) { const clusterId = typeof args.clusterId === "number" && Number.isFinite(args.clusterId) ? Math.floor(args.clusterId) : typeof args.cluster_id === "number" && Number.isFinite(args.cluster_id) ? Math.floor(args.cluster_id) : null; write( `data: ${JSON.stringify({ type: "courtlistener_read_case_start", cluster_id: clusterId })}\n\n`, ); const record = typeof clusterId === "number" ? courtState.casesByClusterId.get(clusterId) : undefined; if (!record) { const payload = cachedCaseNotFetchedResult(clusterId); const event: CourtlistenerToolEvent = { type: "courtlistener_read_case", cluster_id: clusterId, opinion_count: 0, error: payload.error, }; write(`data: ${JSON.stringify(event)}\n\n`); courtlistenerEvents.push(event); toolResults.push({ role: "tool", tool_call_id: tc.id, content: JSON.stringify(payload), }); continue; } const opinions = cachedCaseOpinionTexts(record); const requestedOpinionIds = requestedCourtlistenerOpinionIds(args); const selectedOpinions = requestedOpinionIds.length > 0 ? opinions.filter( (opinion) => typeof opinion.opinion_id === "number" && requestedOpinionIds.includes(opinion.opinion_id), ) : opinions.length === 1 ? opinions : []; if (!selectedOpinions.length) { const multipleOpinions = opinions.length > 1; const payload = { ok: false, cluster_id: record.clusterId, case_name: record.caseName, citations: record.citations, url: record.url, dateFiled: record.dateFiled, opinion_count: opinions.length, opinions: (record.opinions ?? []) .map(courtlistenerOpinionMetadata) .filter( (opinion): opinion is NonNullable => !!opinion, ), error: multipleOpinions ? "Multiple opinions are available. Call courtlistener_read_case again with the opinionId or opinionIds needed." : "No matching opinion_id was found for this fetched case.", }; const event: CourtlistenerToolEvent = { type: "courtlistener_read_case", cluster_id: record.clusterId, case_name: record.caseName, citation: record.citations[0] ?? null, opinion_count: 0, error: payload.error, }; write(`data: ${JSON.stringify(event)}\n\n`); courtlistenerEvents.push(event); toolResults.push({ role: "tool", tool_call_id: tc.id, content: JSON.stringify(payload), }); continue; } const event: CourtlistenerToolEvent = { type: "courtlistener_read_case", cluster_id: record.clusterId, case_name: record.caseName, citation: record.citations[0] ?? null, opinion_count: selectedOpinions.length, }; write(`data: ${JSON.stringify(event)}\n\n`); courtlistenerEvents.push(event); toolResults.push({ role: "tool", tool_call_id: tc.id, content: JSON.stringify({ ok: true, cluster_id: record.clusterId, case_name: record.caseName, citations: record.citations, url: record.url, dateFiled: record.dateFiled, opinion_count: opinions.length, returned_opinion_count: selectedOpinions.length, opinions: selectedOpinions, }), }); } else if (tc.function.name === COURTLISTENER_TOOL_NAMES.verifyCitations) { const citations = Array.isArray(args.citations) ? args.citations.filter( (value): value is string => typeof value === "string", ) : []; const citationCount = citations.length; write( `data: ${JSON.stringify({ type: "courtlistener_verify_citations_start", citation_count: citationCount })}\n\n`, ); try { const result = (await verifyCourtlistenerCitations({ citations, db, apiToken: apiKeys?.courtlistener, })) as { citationLinks?: { clusterId?: number | null; citation?: string | null; caseName?: string | null; dateFiled?: string | null; pdfUrl?: string | null; url?: string | null; markdown?: string; }[]; results?: unknown[]; error?: string; source?: string; [key: string]: unknown; }; if (Array.isArray(result.citationLinks)) { const caseRecords = upsertCourtlistenerCases( courtState, result.citationLinks.map((link) => ({ clusterId: link.clusterId, caseName: link.caseName, citation: link.citation, url: link.url, pdfUrl: link.pdfUrl, dateFiled: link.dateFiled, })), ); const recordsByClusterId = new Map( caseRecords.map((record) => [record.clusterId, record]), ); result.citationLinks = result.citationLinks.map((link) => { if (!link.url) return link; const href = typeof link.clusterId === "number" ? `us-case-${link.clusterId}` : link.url; const label = [link.caseName, link.citation] .filter(Boolean) .join(", "); const record = typeof link.clusterId === "number" ? recordsByClusterId.get(link.clusterId) : undefined; if (record) { const event = caseCitationEventFromRecord(record); if (event) { caseCitationEvents.push(event); write(`data: ${JSON.stringify(event)}\n\n`); } } return { ...link, markdown: `[${label || link.url}](${href})`, }; }); } const rows = result && typeof result === "object" && Array.isArray((result as { results?: unknown }).results) ? (result as { results: unknown[] }).results : []; const matchCount = rows.reduce((count, row) => { if (!row || typeof row !== "object") return count; const clusters = (row as { clusters?: unknown }).clusters; return count + (Array.isArray(clusters) ? clusters.length : 0); }, 0); const error = result && typeof result === "object" && typeof (result as { error?: unknown }).error === "string" ? (result as { error: string }).error : undefined; const event: CourtlistenerToolEvent = { type: "courtlistener_verify_citations", citation_count: citationCount, match_count: matchCount, ...(error ? { error } : {}), }; write(`data: ${JSON.stringify(event)}\n\n`); courtlistenerEvents.push(event); toolResults.push({ role: "tool", tool_call_id: tc.id, content: JSON.stringify(result), }); } catch (err) { const event: CourtlistenerToolEvent = { type: "courtlistener_verify_citations", citation_count: citationCount, match_count: 0, error: err instanceof Error ? err.message : "CourtListener citation lookup failed.", }; write(`data: ${JSON.stringify(event)}\n\n`); courtlistenerEvents.push(event); toolResults.push({ role: "tool", tool_call_id: tc.id, content: JSON.stringify({ error: err instanceof Error ? err.message : "CourtListener citation lookup failed.", }), }); } } else if (tc.function.name === "edit_document" && docIndex) { const rawDocId = args.doc_id as string; const editsRaw = args.edits as unknown[] | undefined; const docId = resolveDocLabel(rawDocId, docStore, docIndex) ?? rawDocId; const docInfo = docStore.get(docId); const indexed = docIndex?.[docId]; const emitEditError = ( filename: string, documentId: string, error: string, ) => { // Surface the failure as a failed "Edited" block in the UI // (start → done-with-error) so it matches the shape the // success/late-failure paths already use. write( `data: ${JSON.stringify({ type: "doc_edited_start", filename, })}\n\n`, ); write( `data: ${JSON.stringify({ type: "doc_edited", filename, document_id: documentId, version_id: "", download_url: "", annotations: [], error, })}\n\n`, ); }; if (!docInfo || !indexed) { const err = `Document '${docId}' not found in this chat's attachments.`; emitEditError(docId, indexed?.document_id ?? "", err); toolResults.push({ role: "tool", tool_call_id: tc.id, content: JSON.stringify({ error: err }), }); } else if (!Array.isArray(editsRaw) || editsRaw.length === 0) { const err = "edits array is required and must not be empty."; emitEditError(docInfo.filename, indexed.document_id, err); toolResults.push({ role: "tool", tool_call_id: tc.id, content: JSON.stringify({ error: err }), }); } else if (docInfo.file_type !== "docx") { const err = "edit_document only supports .docx files."; emitEditError(docInfo.filename, indexed.document_id, err); toolResults.push({ role: "tool", tool_call_id: tc.id, content: JSON.stringify({ error: err }), }); } else { write( `data: ${JSON.stringify({ type: "doc_edited_start", filename: docInfo.filename, })}\n\n`, ); const edits: EditInput[] = (editsRaw as Record[]).map( (e) => ({ find: String(e.find ?? ""), replace: String(e.replace ?? ""), context_before: String(e.context_before ?? ""), context_after: String(e.context_after ?? ""), reason: e.reason ? String(e.reason) : undefined, }), ); const reuseVersion = turnEditState?.get(indexed.document_id); const result = await runEditDocument({ documentId: indexed.document_id, userId, edits, db, reuseVersion, }); if (result.ok) { turnEditState?.set(indexed.document_id, { versionId: result.version_id, versionNumber: result.version_number, storagePath: result.storage_path, }); clearTurnReadsForDocument(turnReadState, indexed.document_id); // Keep the chat-local doc label pointed at the latest // edited version so any follow-up read_document call in // the same assistant turn reads and cites the same bytes. if (docIndex[docId]) { docIndex[docId] = { ...docIndex[docId], version_id: result.version_id, version_number: result.version_number, }; } const currentDocStore = docStore.get(docId); if (currentDocStore) { docStore.set(docId, { ...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, download_url: result.download_url, annotations: result.annotations, }; docsEdited.push(payload); write( `data: ${JSON.stringify({ type: "doc_edited", ...payload, })}\n\n`, ); toolResults.push({ role: "tool", tool_call_id: tc.id, content: JSON.stringify({ ok: true, doc_id: docId, document_id: indexed.document_id, version_id: result.version_id, version_number: result.version_number, applied: result.annotations.length, errors: result.errors, next_required_action: [ `The edited document remains available as doc_id "${docId}".`, `Before making factual claims about the edited document's final contents, call read_document with doc_id "${docId}" and base the response on that returned text.`, `Do not include download links or URLs in your prose response; the edited document card is shown automatically by the UI.`, `If you describe specific content from the edited document, cite it with [N] markers and a final block using doc_id "${docId}".`, ].join(" "), }), }); } else { write( `data: ${JSON.stringify({ type: "doc_edited", filename: docInfo.filename, document_id: indexed.document_id, version_id: "", download_url: "", annotations: [], error: result.error, })}\n\n`, ); toolResults.push({ role: "tool", tool_call_id: tc.id, content: JSON.stringify({ ok: false, error: result.error, }), }); } } } 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, status: "ready", })); const { data: insertedDocs, error: docErr } = await db .from("documents") .insert(docRows) .select("id"); 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 }[]).map( (doc, idx) => ({ ...doc, filename: filenames[idx] ?? "Untitled document.docx", }), ); const contentType = contentTypeForDocumentType( sourceInfo.file_type, ); // Parallel uploads: the doc bytes (and PDF // rendition if any) for every new copy. const uploadJobs: Promise[] = []; const newKeys: string[] = []; const newPdfKeys: (string | null)[] = []; for (const d of newDocs) { const key = storageKey(userId, d.id, d.filename); newKeys.push(key); uploadJobs.push(uploadFile(key, raw, contentType)); if (pdfBytes) { const pdfKey = convertedPdfKey(userId, d.id); newPdfKeys.push(pdfKey); uploadJobs.push( uploadFile(pdfKey, pdfBytes, "application/pdf"), ); } else { newPdfKeys.push(null); } } await Promise.all(uploadJobs); // Bulk insert N versions in one round-trip. const versionRows = newDocs.map((d, idx) => ({ document_id: d.id, storage_path: newKeys[idx], pdf_storage_path: newPdfKeys[idx], source: "upload", version_number: 1, filename: d.filename, file_type: active?.file_type ?? sourceInfo.file_type, size_bytes: active?.size_bytes ?? raw.byteLength, page_count: active?.page_count ?? null, })); const { data: insertedVersions, error: verErr } = await db .from("document_versions") .insert(versionRows) .select("id, document_id"); if ( verErr || !insertedVersions || insertedVersions.length !== newDocs.length ) { fail( `Failed to record replicated document versions: ${verErr?.message ?? "unknown"}`, ); } else { const versionByDocId = new Map(); for (const v of insertedVersions as { id: string; document_id: string; }[]) { versionByDocId.set(v.document_id, v.id); } // current_version_id has to be a per-row // value, so a single UPDATE statement // can't cover all N. Fan out in parallel // instead of sequential awaits. await Promise.all( newDocs.map((d) => db .from("documents") .update({ current_version_id: versionByDocId.get(d.id), }) .eq("id", d.id), ), ); // Register every copy under a fresh doc-N // slug so the model can edit/read any of // them in the same turn. const existingLabels = new Set(Object.keys(docIndex)); let nextLabelIdx = 0; const copies: { new_filename: string; document_id: string; version_id: string; }[] = []; const toolPayloadCopies: { doc_id: string; document_id: string; version_id: string; filename: string; download_url: string; }[] = []; for (let idx = 0; idx < newDocs.length; idx++) { const d = newDocs[idx]; const newKey = newKeys[idx]; const versionId = versionByDocId.get(d.id); if (!versionId) continue; while (existingLabels.has(`doc-${nextLabelIdx}`)) nextLabelIdx++; const slug = `doc-${nextLabelIdx}`; existingLabels.add(slug); docIndex[slug] = { document_id: d.id, filename: d.filename, }; docStore.set(slug, { storage_path: newKey, file_type: sourceInfo.file_type, filename: d.filename, }); copies.push({ new_filename: d.filename, document_id: d.id, version_id: versionId, }); toolPayloadCopies.push({ doc_id: slug, document_id: d.id, version_id: versionId, filename: d.filename, download_url: buildDownloadUrl(newKey, d.filename), }); } write( `data: ${JSON.stringify({ type: "doc_replicated", filename: sourceFilename, count: copies.length, copies, })}\n\n`, ); docsReplicated.push({ filename: sourceFilename, count: copies.length, copies, }); toolResults.push({ role: "tool", tool_call_id: tc.id, content: JSON.stringify({ ok: true, count: copies.length, copies: toolPayloadCopies, }), }); } } } } catch (e) { fail(`replicate_document failed: ${String(e)}`); } } } else if (tc.function.name === "generate_docx") { const title = args.title as string; const landscape = !!args.landscape; devLog( `[generate_docx] title="${title}" landscape=${landscape} args.landscape=${args.landscape}`, ); const previewFilename = safeGeneratedFilename(title, "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 }, ); registerGeneratedDocument( tc, result as Record, previewFilename, "docx", ); } else if (tc.function.name === "generate_excel") { const title = args.title as string; devLog(`[generate_excel] title="${title}"`); const previewFilename = safeGeneratedFilename(title, "xlsx"); write( `data: ${JSON.stringify({ type: "doc_created_start", filename: previewFilename })}\n\n`, ); const result = await generateExcel( title, args.sheets as unknown[], userId, db, { projectId: projectId ?? null }, ); registerGeneratedDocument( tc, result as Record, previewFilename, "xlsx", ); } else if (tc.function.name === "generate_ppt") { const title = args.title as string; devLog(`[generate_ppt] title="${title}"`); const previewFilename = safeGeneratedFilename(title, "pptx"); write( `data: ${JSON.stringify({ type: "doc_created_start", filename: previewFilename })}\n\n`, ); const result = await generatePpt( title, args.slides as unknown[], userId, db, { projectId: projectId ?? null }, ); registerGeneratedDocument( tc, result as Record, previewFilename, "pptx", ); } } if (shouldGroupFindInCase && groupedFindInCaseEvents.length > 0) { const errors = groupedFindInCaseEvents .map((event) => event.error) .filter((error): error is string => !!error); const groupEvent: CourtlistenerToolEvent = { type: "courtlistener_find_in_case", cluster_id: null, query: "", total_matches: groupedFindInCaseEvents.reduce( (sum, event) => sum + event.total_matches, 0, ), searches: groupedFindInCaseEvents.map(findInCaseSearchSummary), ...(errors.length ? { error: errors.join("; ") } : {}), }; write(`data: ${JSON.stringify(groupEvent)}\n\n`); courtlistenerEvents.push(groupEvent); } return { toolResults, docsRead, docsFound, docsCreated, docsReplicated, workflowsApplied, docsEdited, askInputsEvents, courtlistenerEvents, caseCitationEvents, mcpEvents, }; }