Skip to content

Commit ff60949

Browse files
committed
implement batched writes and benchify call for supported file types
1 parent dfe1723 commit ff60949

File tree

3 files changed

+440
-18
lines changed

3 files changed

+440
-18
lines changed
Lines changed: 359 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,359 @@
1+
import { handleStrReplace } from './handlers/tool/str-replace'
2+
import { getFileProcessingValues } from './handlers/tool/write-file'
3+
import { logger } from '../util/logger'
4+
import { Benchify } from 'benchify'
5+
import type { CodebuffToolCall } from '@codebuff/common/tools/list'
6+
import type { ToolResultPart } from '@codebuff/common/types/messages/content-part'
7+
import type { PrintModeEvent } from '@codebuff/common/types/print-mode'
8+
import type { AgentTemplate } from '../templates/types'
9+
import type { ProjectFileContext } from '@codebuff/common/util/file'
10+
import type { WebSocket } from 'ws'
11+
import { file } from 'bun'
12+
13+
export type DeferredStrReplace = {
14+
toolCall: CodebuffToolCall<'str_replace'>
15+
}
16+
17+
export type BatchStrReplaceState = {
18+
deferredStrReplaces: DeferredStrReplace[]
19+
otherToolsQueue: any[]
20+
strReplacePhaseComplete: boolean
21+
failures: any[]
22+
}
23+
24+
const BENCHIFY_FILE_TYPES = ['tsx', 'ts', 'jsx', 'js']
25+
26+
export async function executeBatchStrReplaces({
27+
deferredStrReplaces,
28+
toolCalls,
29+
toolResults,
30+
ws,
31+
agentTemplate,
32+
fileContext,
33+
agentStepId,
34+
clientSessionId,
35+
userInputId,
36+
fullResponse,
37+
onResponseChunk,
38+
state,
39+
userId,
40+
}: {
41+
deferredStrReplaces: DeferredStrReplace[]
42+
toolCalls: (CodebuffToolCall | any)[]
43+
toolResults: ToolResultPart[]
44+
ws: WebSocket
45+
agentTemplate: AgentTemplate
46+
fileContext: ProjectFileContext
47+
agentStepId: string
48+
clientSessionId: string
49+
userInputId: string
50+
fullResponse: string
51+
onResponseChunk: (chunk: string | PrintModeEvent) => void
52+
state: Record<string, any>
53+
userId: string | undefined
54+
}) {
55+
if (deferredStrReplaces.length === 0) {
56+
return
57+
}
58+
59+
logger.debug(
60+
{ count: deferredStrReplaces.length },
61+
`Executing batch of ${deferredStrReplaces.length} str_replace calls`,
62+
)
63+
64+
const batchPromises: Promise<void>[] = []
65+
let previousPromise = Promise.resolve()
66+
67+
// Track successfully edited files for benchify call
68+
const editedFiles: { path: string; contents: string }[] = []
69+
70+
// Execute all str_replace calls in sequence to maintain file consistency
71+
for (let i = 0; i < deferredStrReplaces.length; i++) {
72+
const { toolCall } = deferredStrReplaces[i]
73+
74+
// Chain each str_replace to the previous one to ensure proper ordering
75+
const strReplacePromise = previousPromise.then(async () => {
76+
try {
77+
const { result } = handleStrReplace({
78+
previousToolCallFinished: Promise.resolve(),
79+
toolCall,
80+
requestClientToolCall: async () => {
81+
throw new Error('Client tool calls not supported in batch mode')
82+
},
83+
writeToClient: onResponseChunk,
84+
getLatestState: () => getFileProcessingValues(state),
85+
state: { ...state, ws },
86+
})
87+
88+
const toolResult = await result
89+
90+
if (toolResult) {
91+
const toolResultPart: ToolResultPart = {
92+
type: 'tool-result',
93+
toolName: 'str_replace',
94+
toolCallId: toolCall.toolCallId,
95+
output: toolResult,
96+
}
97+
98+
toolResults.push(toolResultPart)
99+
100+
onResponseChunk({
101+
type: 'tool_result',
102+
toolCallId: toolCall.toolCallId,
103+
output: toolResult,
104+
})
105+
106+
// Add to message history
107+
state.messages.push({
108+
role: 'tool' as const,
109+
content: toolResultPart,
110+
})
111+
112+
// Track successfully edited files
113+
if (
114+
Array.isArray(toolResult) &&
115+
toolResult.length > 0 &&
116+
benchifyCanFixLanguage(toolCall.input.path)
117+
) {
118+
const result = toolResult[0]
119+
if (
120+
result.type === 'json' &&
121+
result.value &&
122+
'content' in result.value
123+
) {
124+
const existingFileIndex = editedFiles.findIndex(
125+
(f) => f.path === toolCall.input.path,
126+
)
127+
const fileContent = result.value.content as string
128+
129+
if (existingFileIndex >= 0) {
130+
// Update existing file with latest content
131+
editedFiles[existingFileIndex].contents = fileContent
132+
} else {
133+
// Add new file to tracking
134+
editedFiles.push({
135+
path: toolCall.input.path,
136+
contents: fileContent,
137+
})
138+
}
139+
140+
logger.debug(
141+
{
142+
path: toolCall.input.path,
143+
contentLength: fileContent.length,
144+
},
145+
'Tracked edited file for benchify',
146+
)
147+
}
148+
}
149+
}
150+
151+
logger.debug(
152+
{ toolCallId: toolCall.toolCallId },
153+
`Completed str_replace ${i + 1}/${deferredStrReplaces.length}`,
154+
)
155+
} catch (error) {
156+
logger.error(
157+
{ error, toolCallId: toolCall.toolCallId },
158+
`Error executing batched str_replace ${i + 1}/${deferredStrReplaces.length}`,
159+
)
160+
161+
// Create error result
162+
const errorResult: ToolResultPart = {
163+
type: 'tool-result',
164+
toolName: 'str_replace',
165+
toolCallId: toolCall.toolCallId,
166+
output: [
167+
{
168+
type: 'json',
169+
value: {
170+
errorMessage: `Batched str_replace failed: ${error instanceof Error ? error.message : String(error)}`,
171+
},
172+
},
173+
],
174+
}
175+
176+
toolResults.push(errorResult)
177+
onResponseChunk({
178+
type: 'tool_result',
179+
toolCallId: toolCall.toolCallId,
180+
output: errorResult.output,
181+
})
182+
}
183+
})
184+
185+
// Add to toolCalls array
186+
toolCalls.push(toolCall)
187+
batchPromises.push(strReplacePromise)
188+
previousPromise = strReplacePromise
189+
}
190+
191+
// Wait for all batched operations to complete
192+
await Promise.all(batchPromises)
193+
194+
logger.debug(
195+
{ count: deferredStrReplaces.length, editedFileCount: editedFiles.length },
196+
`Completed batch execution of ${deferredStrReplaces.length} str_replace calls`,
197+
)
198+
199+
// Call benchify if we have edited files
200+
if (editedFiles.length > 0) {
201+
try {
202+
const benchifyResult = await callBenchify(editedFiles, {
203+
agentStepId,
204+
clientSessionId,
205+
userInputId,
206+
userId,
207+
})
208+
209+
if (benchifyResult && benchifyResult.length > 0) {
210+
// Apply benchify results back to files
211+
await applyBenchifyResults(benchifyResult, {
212+
ws,
213+
onResponseChunk,
214+
state,
215+
toolResults,
216+
toolCalls: deferredStrReplaces.map((d) => d.toolCall),
217+
})
218+
}
219+
} catch (error) {
220+
logger.error(
221+
{ error, editedFiles: editedFiles.map((f) => f.path) },
222+
'Failed to call benchify after str_replace batch',
223+
)
224+
}
225+
}
226+
}
227+
228+
/**
229+
* Calls benchify API with the list of edited files
230+
*/
231+
async function callBenchify(
232+
editedFiles: { path: string; contents: string }[],
233+
context: {
234+
agentStepId: string
235+
clientSessionId: string
236+
userInputId: string
237+
userId: string | undefined
238+
},
239+
): Promise<{ path: string; contents: string }[] | null> {
240+
logger.info(
241+
{
242+
fileCount: editedFiles.length,
243+
files: editedFiles.map((f) => f.path),
244+
...context,
245+
},
246+
'Calling benchify after str_replace batch completion',
247+
)
248+
249+
const client = new Benchify({
250+
apiKey: process.env['BENCHIFY_API_KEY'], // This is the default and can be omitted
251+
})
252+
253+
const response = await client.runFixer(editedFiles, {
254+
fix_types: ['string_literals'],
255+
})
256+
257+
return response
258+
}
259+
260+
/**
261+
* Applies benchify results back to the file system and updates tool results
262+
*/
263+
async function applyBenchifyResults(
264+
benchifyFiles: { path: string; contents: string }[],
265+
context: {
266+
ws: WebSocket
267+
onResponseChunk: (chunk: string | PrintModeEvent) => void
268+
state: Record<string, any>
269+
toolResults: ToolResultPart[]
270+
toolCalls: CodebuffToolCall<'str_replace'>[]
271+
},
272+
) {
273+
logger.info(
274+
{
275+
fileCount: benchifyFiles.length,
276+
files: benchifyFiles.map((f) => f.path),
277+
},
278+
'Applying benchify results to files',
279+
)
280+
281+
for (const benchifyFile of benchifyFiles) {
282+
try {
283+
// Find the corresponding tool call for this file
284+
const relatedToolCall = context.toolCalls.find(
285+
(tc) => tc.input.path === benchifyFile.path,
286+
)
287+
288+
if (!relatedToolCall) {
289+
logger.warn(
290+
{ fileName: benchifyFile.path },
291+
'No matching tool call found for benchify result',
292+
)
293+
continue
294+
}
295+
296+
// TODO: Apply the benchify content to the actual file
297+
// This would typically involve writing the content to the file system
298+
// You might want to use your existing file writing infrastructure
299+
300+
// Create a new tool result indicating benchify updated the file
301+
const benchifyToolResult: ToolResultPart = {
302+
type: 'tool-result',
303+
toolName: 'str_replace',
304+
toolCallId: relatedToolCall.toolCallId,
305+
output: [
306+
{
307+
type: 'json',
308+
value: {
309+
tool: 'str_replace',
310+
path: benchifyFile.path,
311+
content: benchifyFile.contents,
312+
patch: '(Updated by benchify)',
313+
messages: [
314+
'File updated by benchify after batch str_replace completion',
315+
],
316+
},
317+
},
318+
],
319+
}
320+
321+
// Update the existing tool result or add new one
322+
const existingResultIndex = context.toolResults.findIndex(
323+
(tr) => tr.toolCallId === relatedToolCall.toolCallId,
324+
)
325+
326+
if (existingResultIndex >= 0) {
327+
context.toolResults[existingResultIndex] = benchifyToolResult
328+
} else {
329+
context.toolResults.push(benchifyToolResult)
330+
}
331+
332+
// Notify client about the benchify update
333+
context.onResponseChunk({
334+
type: 'tool_result',
335+
toolCallId: relatedToolCall.toolCallId,
336+
output: benchifyToolResult.output,
337+
})
338+
339+
logger.debug(
340+
{ fileName: benchifyFile.path, toolCallId: relatedToolCall.toolCallId },
341+
'Applied benchify result to file',
342+
)
343+
} catch (error) {
344+
logger.error(
345+
{ error, fileName: benchifyFile.path },
346+
'Failed to apply benchify result to file',
347+
)
348+
}
349+
}
350+
}
351+
352+
function benchifyCanFixLanguage(path: string): boolean {
353+
for (const file_extension in BENCHIFY_FILE_TYPES) {
354+
if (path.endsWith(file_extension)) {
355+
return true
356+
}
357+
}
358+
return false
359+
}

0 commit comments

Comments
 (0)