From dcb09b5493f66f5013bdc2bbc3225ffd1e551020 Mon Sep 17 00:00:00 2001 From: Kyle Mistele Date: Fri, 4 Sep 2026 13:56:00 -0700 Subject: [PATCH] Add line numbers to read tool text output Match agentlayer's format: right-aligned 1-indexed line numbers with arrow separator on every text line. Updates tool description, unit tests, and integration test expectations. Co-Authored-By: Claude Opus 4.6 (1M context) HumanLayer-Session: https://app.dev.codelayer.gg/sessions/01a06e1f-c013-7ca0-a97e-2c566e117dd2 --- packages/fold-agent/src/Tools/ReadTool.ts | 15 ++++++++--- .../fold-agent/test/Tools/ReadTool.vi.test.ts | 25 +++++++++++++++---- ...ToolResultFormattingIntegration.vi.test.ts | 4 +-- packages/fold-core/src/Tools/Contracts.ts | 3 ++- 4 files changed, 36 insertions(+), 11 deletions(-) diff --git a/packages/fold-agent/src/Tools/ReadTool.ts b/packages/fold-agent/src/Tools/ReadTool.ts index 024c350..eed563b 100644 --- a/packages/fold-agent/src/Tools/ReadTool.ts +++ b/packages/fold-agent/src/Tools/ReadTool.ts @@ -1,6 +1,6 @@ /** * This file implements the read tool handler (D18, pi port) over the FileSystem seam: text files are - * head-truncated raw lines (no line-number prefixes) with pi's verbatim continuation notices and + * head-truncated lines with right-aligned line-number prefixes and pi's continuation notices and * 1-indexed offset/limit; images (the ticket's hard requirement) are magic-byte sniffed, normalized, * auto-resized, and returned as an image content block that RequestBuilder delivers as a native user * file part (D3). Errors are typed model-visible failures. @@ -96,7 +96,16 @@ export const readTool = (options?: { readonly cwd?: string }): FoldTool => ), }) -/** Read the text path: offset/limit selection, head truncation, and pi's verbatim notices. */ +/** Prefix text lines with right-aligned, 1-indexed line numbers and an arrow separator. */ +const numberTextLines = (content: string, startLine: number, outputLines: number): string => { + const width = String(startLine + outputLines - 1).length + return content + .split('\n') + .map((line, index) => `${String(startLine + index).padStart(width, ' ')}→${line}`) + .join('\n') +} + +/** Read the text path: offset/limit selection, head truncation, line numbering, and continuation notices. */ const readTextContent = ( bytes: Uint8Array, params: { readonly path: string; readonly offset?: number | undefined; readonly limit?: number | undefined }, @@ -129,7 +138,7 @@ const readTextContent = ( }) } - let outputText = truncation.content + let outputText = numberTextLines(truncation.content, startLineDisplay, truncation.outputLines) const endLineDisplay = startLineDisplay + truncation.outputLines - 1 const nextOffset = endLineDisplay + 1 const totalFileLines = allLines.length diff --git a/packages/fold-agent/test/Tools/ReadTool.vi.test.ts b/packages/fold-agent/test/Tools/ReadTool.vi.test.ts index ef6621d..483a385 100644 --- a/packages/fold-agent/test/Tools/ReadTool.vi.test.ts +++ b/packages/fold-agent/test/Tools/ReadTool.vi.test.ts @@ -46,14 +46,29 @@ const firstText = (result: unknown): string => { return block.text } -it.effect('reads raw text with no line-number prefixes', () => +it.effect('reads text with arrow-separated line numbers', () => Effect.gen(function* () { const dir = yield* tempDir writeFileSync(join(dir, 'plain.txt'), 'first line\nsecond line\n') const result = yield* runHandler(handlerOf(readTool({ cwd: dir }))({ path: 'plain.txt' })) - expect(firstText(result)).toBe('first line\nsecond line\n') + expect(firstText(result)).toBe('1→first line\n2→second line\n3→') + }), +) + +it.effect('right-aligns line numbers to the largest displayed line number', () => + Effect.gen(function* () { + const dir = yield* tempDir + writeFileSync( + join(dir, 'aligned.txt'), + Array.from({ length: 10 }, (_, index) => `line ${index + 1}`).join('\n'), + ) + + const result = yield* runHandler(handlerOf(readTool({ cwd: dir }))({ path: 'aligned.txt' })) + + expect(firstText(result)).toContain(' 1→line 1') + expect(firstText(result)).toContain('10→line 10') }), ) @@ -64,7 +79,7 @@ it.effect('applies 1-indexed offset and limit with the more-lines notice', () => const result = yield* runHandler(handlerOf(readTool({ cwd: dir }))({ path: 'lines.txt', offset: 3, limit: 2 })) - expect(firstText(result)).toBe('line-3\nline-4\n\n[6 more lines in file. Use offset=5 to continue.]') + expect(firstText(result)).toBe('3→line-3\n4→line-4\n\n[6 more lines in file. Use offset=5 to continue.]') }), ) @@ -78,7 +93,7 @@ it.effect('head-truncates large files with pi verbatim notice', () => const text = firstText(result) expect(text.endsWith('[Showing lines 1-2000 of 2500. Use offset=2001 to continue.]')).toBe(true) - expect(text.startsWith('l1\n')).toBe(true) + expect(text.startsWith(' 1→l1\n')).toBe(true) }), ) @@ -159,6 +174,6 @@ it.effect('resolves macOS filename variants (curly apostrophe)', () => const result = yield* runHandler(handlerOf(readTool({ cwd: dir }))({ path: "it's a file.txt" })) - expect(firstText(result)).toBe('variant content\n') + expect(firstText(result)).toBe('1→variant content\n2→') }), ) diff --git a/packages/fold-codex/test/ToolResultFormattingIntegration.vi.test.ts b/packages/fold-codex/test/ToolResultFormattingIntegration.vi.test.ts index 64769ed..cb3f60e 100644 --- a/packages/fold-codex/test/ToolResultFormattingIntegration.vi.test.ts +++ b/packages/fold-codex/test/ToolResultFormattingIntegration.vi.test.ts @@ -54,7 +54,7 @@ const outputStringsFrom = (body: string): ReadonlyArray => { } const expectedModelText = [ - 'read alpha\nread beta\n', + '1→read alpha\n2→read beta\n3→', 'bash success\n', 'bash failure\n\n\nCommand exited with code 7', 'Successfully wrote 16 bytes to nested/written.txt', @@ -62,7 +62,7 @@ const expectedModelText = [ ] const expectedDurableResults: ReadonlyArray = [ - { _tag: 'text', text: 'read alpha\nread beta\n' }, + { _tag: 'text', text: '1→read alpha\n2→read beta\n3→' }, { _tag: 'text', text: 'bash success\n' }, { _tag: 'failure', text: 'bash failure\n\n\nCommand exited with code 7' }, { _tag: 'text', text: 'Successfully wrote 16 bytes to nested/written.txt' }, diff --git a/packages/fold-core/src/Tools/Contracts.ts b/packages/fold-core/src/Tools/Contracts.ts index c4d9cae..7561d73 100644 --- a/packages/fold-core/src/Tools/Contracts.ts +++ b/packages/fold-core/src/Tools/Contracts.ts @@ -41,7 +41,8 @@ const ReadParameters = Schema.Struct({ export const readToolContract = { name: 'read', description: - `Read the contents of a file. Supports text files and images (jpeg, png, gif, webp, bmp). ` + + `Read the contents of a file. Text lines include line numbers. Supports text files and images ` + + `(jpeg, png, gif, webp, bmp). ` + `Text output is limited to ${defaultMaxLines} lines or ${formatSize(defaultMaxBytes)}; ` + `use offset and limit to read further sections of large files.`, parameters: ReadParameters,