Files
goodbuddy/src/main/agent/workspace-read-lines.ts
T
mesalogo ec100f8176 feat: improve direct model workspace tools
Direct models now use bundled ripgrep for workspace discovery, read files through resumable pages, and edit multiple files with apply_patch. Ask can search and read while Execute retains write access, and fixed total input limits no longer constrain these tools.

Release note: 直连模型现可使用随包 ripgrep 搜索项目、分页读取任意大小的文本,并通过多文件补丁修改代码;设置中的相关能力统一显示为工作区文件和开发工具。
2026-09-10 00:45:03 +08:00

110 lines
3.7 KiB
TypeScript

import type { WorkspaceAccess } from '../workspace'
const READ_CHUNK_BYTES = 128 * 1024
const MAXIMUM_RESULT_BYTES = 240 * 1024
const MAXIMUM_LINE_CHARACTERS = 8_000
export async function readWorkspaceLines(
workspace: WorkspaceAccess,
input: { path: string; offset: number; limit: number; offsetBytes?: number },
signal: AbortSignal
): Promise<string> {
if (input.offsetBytes !== undefined) {
const page = await workspace.readText({
path: input.path,
offsetBytes: input.offsetBytes,
maximumBytes: MAXIMUM_RESULT_BYTES,
allowTruncated: true,
invalidUtf8Message: '工作区读取目标不是有效 UTF-8 文本',
signal
})
const nextOffset = page.offsetBytes + page.bytesRead
const footer = page.truncated
? `[bytes ${page.offsetBytes}-${nextOffset}; continue with offsetBytes=${nextOffset}]`
: `[bytes ${page.offsetBytes}-${nextOffset}; end of file; ${page.size} bytes]`
return `${page.content}\n${footer}`
}
let offsetBytes = 0
let currentLine = 1
let pending = ''
let pendingTruncated = false
let totalBytes: number | undefined
let moreAvailable = false
let resultBytes = 0
const output: string[] = []
while (true) {
signal.throwIfAborted()
const page = await workspace.readText({
path: input.path,
offsetBytes,
maximumBytes: READ_CHUNK_BYTES,
allowTruncated: true,
invalidUtf8Message: '工作区读取目标不是有效 UTF-8 文本',
signal
})
totalBytes = page.size
offsetBytes += page.bytesRead
const carriedLineTruncated: boolean = pendingTruncated
const combined = pending + page.content
const lines = combined.split('\n')
pending = page.truncated ? lines.pop() ?? '' : ''
pendingTruncated =
page.truncated &&
(carriedLineTruncated || pending.length > MAXIMUM_LINE_CHARACTERS)
if (pending.length > MAXIMUM_LINE_CHARACTERS) {
pending = pending.slice(0, MAXIMUM_LINE_CHARACTERS)
}
if (!page.truncated && lines.at(-1) === '') lines.pop()
for (const [lineIndex, rawLine] of lines.entries()) {
if (currentLine >= input.offset) {
if (output.length >= input.limit) {
moreAvailable = true
break
}
const line = rawLine.endsWith('\r') ? rawLine.slice(0, -1) : rawLine
const lineTruncated =
line.length > MAXIMUM_LINE_CHARACTERS ||
(lineIndex === 0 && carriedLineTruncated)
const preview = `${line.slice(0, MAXIMUM_LINE_CHARACTERS)}${
lineTruncated ? '...[line truncated]' : ''
}`
const formatted = `${currentLine}: ${preview}`
const bytes = Buffer.byteLength(formatted) + 1
if (resultBytes + bytes > MAXIMUM_RESULT_BYTES) {
moreAvailable = true
break
}
output.push(formatted)
resultBytes += bytes
}
currentLine += 1
}
if (moreAvailable) break
if (!page.truncated) {
if (pending && currentLine >= input.offset && output.length < input.limit) {
const formatted = `${currentLine}: ${pending.slice(0, MAXIMUM_LINE_CHARACTERS)}`
if (resultBytes + Buffer.byteLength(formatted) + 1 <= MAXIMUM_RESULT_BYTES) {
output.push(formatted)
} else {
moreAvailable = true
}
}
break
}
if (page.bytesRead === 0) {
throw new Error('工作区读取分页无法继续')
}
}
if (output.length === 0) {
return `[no lines at offset ${input.offset}; ${totalBytes ?? 0} bytes]`
}
const lastLine = Number.parseInt(output.at(-1)!, 10)
const footer = moreAvailable
? `[lines ${input.offset}-${lastLine}; more available]`
: `[lines ${input.offset}-${lastLine}; end of file; ${totalBytes ?? 0} bytes]`
return `${output.join('\n')}\n${footer}`
}