Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion packages/coding-agent/src/core/tools/truncate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
31 changes: 14 additions & 17 deletions packages/coding-agent/src/linc/casedev-vault-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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" <path>` 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,
};
}

Expand Down
4 changes: 2 additions & 2 deletions packages/coding-agent/src/linc/casedev-vault-tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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.",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale promptSnippet/promptGuidelines strings for the vault read-text tools contradict the updated full-text/grep-first tool descriptions, injecting conflicting guidance into the system prompt.

Fix on Vercel

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.",
Expand Down
18 changes: 7 additions & 11 deletions packages/coding-agent/test/casedev-vault-api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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" } })),
Expand All @@ -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 () => {
Expand Down
2 changes: 1 addition & 1 deletion packages/coding-agent/test/tools.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"));

Expand Down
Loading