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
15 changes: 12 additions & 3 deletions packages/fold-agent/src/Tools/ReadTool.ts
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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 },
Expand Down Expand Up @@ -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
Expand Down
25 changes: 20 additions & 5 deletions packages/fold-agent/test/Tools/ReadTool.vi.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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')
}),
)

Expand All @@ -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.]')
}),
)

Expand All @@ -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)
}),
)

Expand Down Expand Up @@ -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→')
}),
)
Original file line number Diff line number Diff line change
Expand Up @@ -54,15 +54,15 @@ const outputStringsFrom = (body: string): ReadonlyArray<string> => {
}

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',
'Successfully replaced 1 block(s) in editable.txt.',
]

const expectedDurableResults: ReadonlyArray<typeof Schema.Json.Type> = [
{ _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' },
Expand Down
3 changes: 2 additions & 1 deletion packages/fold-core/src/Tools/Contracts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Loading