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 搜索项目、分页读取任意大小的文本,并通过多文件补丁修改代码;设置中的相关能力统一显示为工作区文件和开发工具。
This commit is contained in:
@@ -0,0 +1,109 @@
|
||||
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}`
|
||||
}
|
||||
Reference in New Issue
Block a user