fix: preserve native ripgrep capabilities for direct models
Replace the restricted workspace_rg parameter mapping with native args and optional cwd. Preserve rg output and exit codes, and reuse process cancellation and output_read pagination so large search results remain accessible. Ask remains read-only within the workspace; Execute uses current-account permissions. Validation: focused regression 169 passed, 1 skipped; typecheck and lint passed. Real DeepSeek Ask and Execute pagination scenarios passed with six counted requests in the successful run; an earlier failed fixture run has no retained request count. Full-suite validation is not green: the recorded run had unrelated failures and the latest run timed out without a summary. The remote Agent does not use this local execution path. Release note: 直连模型可使用 ripgrep 原生参数搜索文件并保留原生结果,大输出可通过 output_read 完整续读。Ask 保持工作区只读,Execute 使用当前账号权限。
This commit is contained in:
@@ -480,6 +480,68 @@ describe.runIf(enabled)('runtime end-to-end', () => {
|
||||
180_000
|
||||
)
|
||||
|
||||
it.each(['ask', 'execute'] as const)(
|
||||
'searches native rg arguments and reads hidden output through real model in %s',
|
||||
async (workMode) => {
|
||||
const marker = `RG_TAIL_${crypto.randomUUID()}`
|
||||
await writeFile(join(workspace, 'native-rg.txt'), `target ${' '.repeat(110 * 1024)}${marker}\n`)
|
||||
const upstreamUrl = protocol === 'anthropic-messages'
|
||||
? createAnthropicMessagesUrl(baseUrl)
|
||||
: protocol === 'openai-responses'
|
||||
? createOpenAIResponsesUrl(baseUrl)
|
||||
: createOpenAIChatCompletionsUrl(baseUrl)
|
||||
const probe = await createModelRequestProbe({ upstreamUrl, headerName: 'x-goodbuddy-rg-test' })
|
||||
const provider = new ModelToolProvider(workspace, [], undefined, undefined, false, {
|
||||
ripgrepExecutablePath: join(process.cwd(), '.runtime-resources', process.arch, process.platform === 'win32' ? 'rg.exe' : 'rg')
|
||||
})
|
||||
const listTools = provider.listTools.bind(provider)
|
||||
vi.spyOn(provider, 'listTools').mockImplementation(async (context, signal) =>
|
||||
(await listTools(context, signal)).filter((tool) => ['workspace_rg', 'output_read'].includes(tool.name)))
|
||||
const callTool = provider.callTool.bind(provider)
|
||||
const observed: Array<{ name: string; result: ProcessExecuteResult | PagedOutputPage }> = []
|
||||
vi.spyOn(provider, 'callTool').mockImplementation(async (name, args, signal, context) => {
|
||||
if (observed.length >= 4) throw new Error('Real rg test exceeded four tool calls')
|
||||
const result = await callTool(name, args, signal, context)
|
||||
const part = result.parts[0]
|
||||
if (part?.type === 'text') observed.push({ name, result: JSON.parse(part.text) })
|
||||
return result
|
||||
})
|
||||
const runtime = new ModelAgentRuntime({
|
||||
apiKey, baseUrl: probe.baseUrl, model: modelName, protocol,
|
||||
authentication: 'api-key', defaultWorkspace: workspace,
|
||||
maximumOutputTokens: 1024, toolProvider: provider
|
||||
})
|
||||
try {
|
||||
const events = await collectEvents(runtime.run({
|
||||
requestId: crypto.randomUUID(), conversationId: crypto.randomUUID(), workMode,
|
||||
prompt: [
|
||||
'Call workspace_rg exactly once with args ["--no-line-number", "-F", "target", "native-rg.txt"].',
|
||||
'The result has a hidden RG_TAIL_ marker after a long line of spaces.',
|
||||
'Read the remaining output using output_read with stdoutReference.handle and stdoutReference.nextCursor.',
|
||||
'Continue until eof. Reply only with the exact RG_TAIL_ marker. Do not guess or repeat the search.'
|
||||
].join('\n')
|
||||
}, AbortSignal.timeout(180000), async () => 'once'))
|
||||
expect(observed.filter((item) => item.name === 'workspace_rg')).toHaveLength(1)
|
||||
const search = observed[0]!.result as ProcessExecuteResult
|
||||
expect(search.exitCode).toBe(0)
|
||||
expect(search.stdoutTruncated).toBe(true)
|
||||
expect(search.stdout).not.toContain(marker)
|
||||
const pages = observed.filter((item) => item.name === 'output_read')
|
||||
expect(pages.length).toBeGreaterThan(0)
|
||||
expect(pages.at(-1)!.result).toMatchObject({ eof: true })
|
||||
expect(events.filter((event) => event.type === 'text').map((event) => event.delta).join('')).toContain(marker)
|
||||
expect(events.some((event) => event.type === 'tool' && event.state === 'completed')).toBe(true)
|
||||
expect(events.at(-1)).toMatchObject({ type: 'done' })
|
||||
} finally {
|
||||
console.info(JSON.stringify({ boundary: 'native-rg-pagination', workMode,
|
||||
realModelCalls: probe.observations.length, toolCalls: observed.map((item) => item.name) }))
|
||||
await runtime.dispose()
|
||||
await probe.close()
|
||||
}
|
||||
},
|
||||
200000
|
||||
)
|
||||
|
||||
it(
|
||||
'recovers hidden process output markers through real-model output_read pagination',
|
||||
async () => {
|
||||
|
||||
Reference in New Issue
Block a user