open-multi-agent/src/tool/built-in/file-read.ts

106 lines
3.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Built-in file-read tool.
*
* Reads a file from disk and returns its contents with 1-based line numbers.
* Supports reading a slice of lines via `offset` and `limit` for large files.
*/
import { readFile } from 'fs/promises'
import { z } from 'zod'
import { defineTool } from '../framework.js'
// ---------------------------------------------------------------------------
// Tool definition
// ---------------------------------------------------------------------------
export const fileReadTool = defineTool({
name: 'file_read',
description:
'Read the contents of a file from disk. ' +
'Returns the file contents with line numbers prefixed in the format "N\\t<line>". ' +
'Use `offset` and `limit` to read large files in chunks without loading the ' +
'entire file into the context window.',
inputSchema: z.object({
path: z.string().describe('Absolute path to the file to read.'),
offset: z
.number()
.int()
.nonnegative()
.optional()
.describe(
'1-based line number to start reading from. ' +
'When omitted the file is read from the beginning.',
),
limit: z
.number()
.int()
.positive()
.optional()
.describe(
'Maximum number of lines to return. ' +
'When omitted all lines from `offset` to the end of the file are returned.',
),
}),
execute: async (input) => {
let raw: string
try {
const buffer = await readFile(input.path)
raw = buffer.toString('utf8')
} catch (err) {
const message =
err instanceof Error ? err.message : 'Unknown error reading file.'
return {
data: `Could not read file "${input.path}": ${message}`,
isError: true,
}
}
// Split preserving trailing newlines correctly
const lines = raw.split('\n')
// Remove the last empty string produced by a trailing newline
if (lines.length > 0 && lines[lines.length - 1] === '') {
lines.pop()
}
const totalLines = lines.length
// Apply offset (convert from 1-based to 0-based)
const startIndex =
input.offset !== undefined ? Math.max(0, input.offset - 1) : 0
if (startIndex >= totalLines && totalLines > 0) {
return {
data:
`File "${input.path}" has ${totalLines} line${totalLines === 1 ? '' : 's'} ` +
`but offset ${input.offset} is beyond the end.`,
isError: true,
}
}
const endIndex =
input.limit !== undefined
? Math.min(startIndex + input.limit, totalLines)
: totalLines
const slice = lines.slice(startIndex, endIndex)
// Build line-numbered output (1-based line numbers matching file positions)
const numbered = slice
.map((line, i) => `${startIndex + i + 1}\t${line}`)
.join('\n')
const meta =
endIndex < totalLines
? `\n\n(showing lines ${startIndex + 1}${endIndex} of ${totalLines})`
: ''
return {
data: numbered + meta,
isError: false,
}
},
})