diff --git a/packages/coding-agent/src/core/tools/truncate.ts b/packages/coding-agent/src/core/tools/truncate.ts index c638ee480..05cded0ef 100644 --- a/packages/coding-agent/src/core/tools/truncate.ts +++ b/packages/coding-agent/src/core/tools/truncate.ts @@ -9,7 +9,13 @@ */ export const DEFAULT_MAX_LINES = 2000; -export const DEFAULT_MAX_BYTES = 50 * 1024; // 50KB +// 20KB per tool result: bounds how much any single read/find/grep can drag +// into billed model context (each result rides along with every subsequent +// call in the session). Offset/limit pagination remains available for more — +// the truncation notices tell the model how to continue. Lowered from the +// upstream 50KB after a production session accumulated ~450KB of context via +// nine max-size reads of a legal document (CD-1284 follow-up). +export const DEFAULT_MAX_BYTES = 20 * 1024; // 20KB export const GREP_MAX_LINE_LENGTH = 500; // Max chars per grep match line export interface TruncationResult { diff --git a/packages/coding-agent/src/linc/casedev-vault-api.ts b/packages/coding-agent/src/linc/casedev-vault-api.ts index 6ae9e487f..9a592a5a6 100644 --- a/packages/coding-agent/src/linc/casedev-vault-api.ts +++ b/packages/coding-agent/src/linc/casedev-vault-api.ts @@ -48,10 +48,6 @@ export interface CaseDevVaultReadTextParams { filename?: string; } -export const CASEDEV_VAULT_TEXT_MAX_CHARS = 10_000; -export const CASEDEV_VAULT_TEXT_TRUNCATION_WARNING = - "this document is too long to view in full. Use targeted vault searches to retrieve more context"; - export interface CaseDevVaultSearchParams { query: string; method?: "hybrid" | "global" | "entity" | "fast" | "vector" | "graph" | "local"; @@ -287,24 +283,25 @@ export async function readCaseDevVaultObjectText( const filename = baseName.endsWith(".txt") ? baseName : `${baseName}.txt`; await mkdir(params.outDir, { recursive: true }); const outputPath = safeOutputPath(params.outDir, filename); - const truncated = text.length > CASEDEV_VAULT_TEXT_MAX_CHARS; - const visibleTextLimit = truncated - ? CASEDEV_VAULT_TEXT_MAX_CHARS - CASEDEV_VAULT_TEXT_TRUNCATION_WARNING.length - 2 - : CASEDEV_VAULT_TEXT_MAX_CHARS; - const visibleText = text.slice(0, visibleTextLimit); - const output = truncated ? `${CASEDEV_VAULT_TEXT_TRUNCATION_WARNING}\n\n${visibleText}` : visibleText; - await writeFile(outputPath, output, "utf8"); - const pageMarkers = (visibleText.match(/^--- Page \d+ ---$/gm) ?? []).length; + // The full text is written to disk deliberately: files on disk are outside + // model context and cost nothing, and grep over the complete text is what + // makes comprehensive large-document analysis possible. Context safety is + // enforced where the cost is — per-result output caps on read/grep/bash — + // not by truncating the file (which silently breaks any search past the + // cut and does nothing for other large files in the workspace). + await writeFile(outputPath, text, "utf8"); + const pageMarkers = (text.match(/^--- Page \d+ ---$/gm) ?? []).length; + const usage = + 'Locate content with `grep -n "pattern" ` and read only narrow line ranges around matches. Never page through the full text — it is too large for the conversation.'; return { objectId: params.objectId, path: outputPath, chars: text.length, pageMarkers, - note: truncated - ? CASEDEV_VAULT_TEXT_TRUNCATION_WARNING - : pageMarkers > 0 - ? "Text contains --- Page N --- markers; cite findings by those page numbers." - : "This document has no page markers; cite findings by section or heading.", + note: + (pageMarkers > 0 + ? "Text contains --- Page N --- markers; cite findings by those page numbers. " + : "This document has no page markers; cite findings by section or heading. ") + usage, }; } diff --git a/packages/coding-agent/src/linc/casedev-vault-tools.ts b/packages/coding-agent/src/linc/casedev-vault-tools.ts index 667390a30..89e01ff9a 100644 --- a/packages/coding-agent/src/linc/casedev-vault-tools.ts +++ b/packages/coding-agent/src/linc/casedev-vault-tools.ts @@ -351,7 +351,7 @@ export function createCaseDevVaultTools(): ToolDefinition[] { name: "casedev_vault_read_text", label: "case.dev vault read text", description: - "Write up to the first 10,000 characters of an ingested Case.dev vault object's extracted text into the workspace as a .txt file. Use targeted vault searches for additional context.", + "Write the full extracted text of an ingested Case.dev vault object into the workspace as a .txt file, page-numbered when the source is paginated. Locate content with grep -n and read only narrow line ranges around matches; never page the full text into the conversation.", promptSnippet: "Read a bounded excerpt of a Case.dev vault object's extracted text into the workspace.", parameters: vaultReadTextSchema, async execute(_toolCallId, params: VaultReadTextInput, signal, _onUpdate, ctx) { @@ -439,7 +439,7 @@ export function createCaseDevVaultTools(): ToolDefinition[] { name: "vault_read_text", label: "vault_read_text", description: - "Write up to the first 10,000 characters of an ingested matter document's extracted text into the workspace as a .txt file. Use targeted vault searches for additional context.", + "Write the full extracted text of an ingested matter document into the workspace as a .txt file, page-numbered when the source is paginated. The right tool for comprehensively analyzing document content at any size: locate content with grep -n and read only narrow line ranges around matches; never page the full text into the conversation.", promptSnippet: "Read a bounded excerpt of a matter document's extracted text into the workspace", promptGuidelines: [ "Originals from vault_download are raw bytes and often scanned images; use vault_read_text for a bounded excerpt and targeted vault searches for additional context.", diff --git a/packages/coding-agent/test/casedev-vault-api.test.ts b/packages/coding-agent/test/casedev-vault-api.test.ts index 35a9688df..a332fef06 100644 --- a/packages/coding-agent/test/casedev-vault-api.test.ts +++ b/packages/coding-agent/test/casedev-vault-api.test.ts @@ -4,8 +4,6 @@ import { join } from "node:path"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import type { ExtensionContext } from "../src/core/extensions/types.ts"; import { - CASEDEV_VAULT_TEXT_MAX_CHARS, - CASEDEV_VAULT_TEXT_TRUNCATION_WARNING, downloadCaseDevVaultObject, listCaseDevVaults, readCaseDevVaultObjectText, @@ -232,8 +230,8 @@ describe("Case.dev vault REST API helper", () => { }); }); - it("limits extracted object text and prepends targeted-search guidance", async () => { - const text = "x".repeat(CASEDEV_VAULT_TEXT_MAX_CHARS + 1); + it("writes large extracted text to disk in full with grep-first guidance", async () => { + const text = "x".repeat(50_000); vi.stubGlobal( "fetch", vi.fn(async () => jsonResponse({ text, metadata: { filename: "long.pdf" } })), @@ -248,15 +246,13 @@ describe("Case.dev vault REST API helper", () => { expect(result).toMatchObject({ objectId: "obj-long", chars: text.length, - note: CASEDEV_VAULT_TEXT_TRUNCATION_WARNING, }); + expect(result.note).toContain("grep -n"); + expect(result.note).toContain("Never page through the full text"); + // The on-disk file is complete: disk is outside model context and free, + // and grep over the whole text is what enables large-document analysis. const written = await readFile(join(cwd, "long.pdf.txt"), "utf-8"); - expect(written).toHaveLength(CASEDEV_VAULT_TEXT_MAX_CHARS); - expect(written).toBe( - `${CASEDEV_VAULT_TEXT_TRUNCATION_WARNING}\n\n${"x".repeat( - CASEDEV_VAULT_TEXT_MAX_CHARS - CASEDEV_VAULT_TEXT_TRUNCATION_WARNING.length - 2, - )}`, - ); + expect(written).toBe(text); }); it("rejects text reads for objects with no extracted text", async () => { diff --git a/packages/coding-agent/test/tools.test.ts b/packages/coding-agent/test/tools.test.ts index c5e8fa6fb..970b95e92 100644 --- a/packages/coding-agent/test/tools.test.ts +++ b/packages/coding-agent/test/tools.test.ts @@ -84,7 +84,7 @@ describe("Coding Agent Tools", () => { it("should truncate when byte limit exceeded", async () => { const testFile = join(testDir, "large-bytes.txt"); - // Create file that exceeds 50KB byte limit but has fewer than 2000 lines + // Create file that exceeds the byte limit but has fewer than 2000 lines const lines = Array.from({ length: 500 }, (_, i) => `Line ${i + 1}: ${"x".repeat(200)}`); writeFileSync(testFile, lines.join("\n"));