Skip to content

Commit b717bec

Browse files
cvick32brandonkachen
authored andcommitted
Switch to using DIFF response format and apply patches directly (#320)
1 parent 0969d7f commit b717bec

File tree

3 files changed

+52
-122
lines changed

3 files changed

+52
-122
lines changed

backend/src/tools/batch-str-replace.ts

Lines changed: 46 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { logger } from '../util/logger'
44
import { Benchify } from 'benchify'
55
import { env } from '@codebuff/internal/env'
66
import { requestToolCall } from '../websockets/websocket-action'
7-
import { createPatch } from 'diff'
7+
import { ParsedDiff, parsePatch } from 'diff'
88
import { withRetry, withTimeout } from '@codebuff/common/util/promise'
99
import { match, P } from 'ts-pattern'
1010
import type {
@@ -454,8 +454,7 @@ async function applyBenchifyIfNeeded(
454454
},
455455
) {
456456
// Early exit conditions - fail gracefully without blocking user edits
457-
const client = getBenchifyClient()
458-
if (!client || batchContext.intendedChanges.size === 0) {
457+
if (batchContext.intendedChanges.size === 0) {
459458
return
460459
}
461460

@@ -499,15 +498,15 @@ async function applyBenchifyIfNeeded(
499498
logger.info(
500499
{
501500
benchifyResultCount: benchifyResult.length,
502-
resultFiles: benchifyResult.map((r) => r.path),
501+
diffResults: benchifyResult.length,
503502
agentStepId: options.agentStepId,
504503
userInputId: options.userInputId,
505504
},
506-
`executeBatchStrReplaces: Benchify returned ${benchifyResult.length} results, applying them`,
505+
`executeBatchStrReplaces: Benchify returned ${benchifyResult.length} diff results, applying them`,
507506
)
508507

509508
// Apply results with individual error handling to prevent one failure from blocking others
510-
await applyBenchifyResultsGracefully(benchifyResult, {
509+
await applyBenchifyResultsGracefully(filteredChanges, benchifyResult, {
511510
ws: batchContext.ws,
512511
onResponseChunk: batchContext.onResponseChunk,
513512
state: {
@@ -585,32 +584,34 @@ async function callBenchifyWithResilience(
585584
userInputId: string
586585
userId: string | undefined
587586
},
588-
): Promise<{ path: string; contents: string }[] | null> {
587+
): Promise<ParsedDiff[]> {
589588
const client = getBenchifyClient()
590589
if (!client) {
591-
return null
590+
return []
592591
}
593592

594593
return await withRetry(
595594
async () => {
596-
const response = await withTimeout(
595+
const diff_response = await withTimeout(
597596
client.runFixer(editedFiles, {
598-
fix_types: ['string_literals'],
597+
fixes: ['parsing'],
598+
mode: 'files',
599+
response_format: 'DIFF',
599600
}),
600601
BENCHIFY_TIMEOUT_MS,
601602
`Benchify call timed out after ${BENCHIFY_TIMEOUT_MS}ms`,
602603
)
603604

604605
// Validate response
605-
if (response && Array.isArray(response)) {
606+
if (diff_response) {
606607
return validateBenchifyResponse(
607-
response,
608+
diff_response,
608609
editedFiles,
609610
context.agentStepId,
610611
)
611612
}
612613

613-
return null
614+
return []
614615
},
615616
{
616617
maxRetries: 2,
@@ -634,42 +635,34 @@ async function callBenchifyWithResilience(
634635
* Validates Benchify API response using pattern matching
635636
*/
636637
function validateBenchifyResponse(
637-
response: any[],
638+
response: string,
638639
originalFiles: { path: string; contents: string }[],
639640
agentStepId: string,
640-
): { path: string; contents: string }[] {
641+
): ParsedDiff[] {
641642
const originalPaths = new Set(originalFiles.map((f) => f.path))
642643

643-
return response.flatMap((result) =>
644-
match(result)
645-
.with({ path: P.string, contents: P.string }, (res) => {
646-
if (!originalPaths.has(res.path)) {
644+
const patches = parsePatch(response)
645+
return patches.flatMap((patch) =>
646+
match(patch)
647+
.with({ oldFileName: P.string }, (res) => {
648+
// drop prefix a/ adding by diff patch
649+
const actualFileName = res.oldFileName.replace('a/', '')
650+
if (!originalPaths.has(actualFileName)) {
647651
logger.warn(
648-
{ path: res.path, agentStepId },
652+
{ path: actualFileName, agentStepId },
649653
'Benchify returned result for unexpected path',
650654
)
651655
return []
652656
}
653-
if (res.contents.length > BENCHIFY_MAX_FILE_SIZE * 2) {
654-
logger.warn(
655-
{
656-
path: res.path,
657-
size: res.contents.length,
658-
agentStepId,
659-
},
660-
'Benchify result exceeds size limit',
661-
)
662-
return []
663-
}
664-
return [{ path: res.path, contents: res.contents }]
657+
return [patch]
665658
})
666659
.otherwise(() => {
667660
logger.warn(
668661
{
669-
result: JSON.stringify(result).substring(0, 100),
662+
result: JSON.stringify(patch).substring(0, 100),
670663
agentStepId,
671664
},
672-
'Invalid Benchify result structure',
665+
'Invalid Benchify patch',
673666
)
674667
return []
675668
}),
@@ -707,7 +700,8 @@ function shouldRetryBenchifyError(error: Error): boolean {
707700
* Applies benchify results back to the file system with individual error handling
708701
*/
709702
async function applyBenchifyResultsGracefully(
710-
benchifyFiles: { path: string; contents: string }[],
703+
editedFiles: { path: string; contents: string }[],
704+
benchifyDiffs: ParsedDiff[],
711705
context: {
712706
ws: WebSocket
713707
onResponseChunk: (chunk: string | PrintModeEvent) => void
@@ -719,9 +713,20 @@ async function applyBenchifyResultsGracefully(
719713
},
720714
) {
721715
const results = await Promise.allSettled(
722-
benchifyFiles.map((benchifyFile) =>
723-
applyBenchifyResultSafely(benchifyFile, context),
724-
),
716+
editedFiles.map((editedFile) => {
717+
// again, we have to replace the a/ that the ParsedDiff introduced
718+
const diff = benchifyDiffs.find(
719+
(v) => v.oldFileName?.replace('a/', '') == editedFile.path,
720+
)
721+
if (diff) {
722+
applyBenchifyResultSafely(editedFile, diff, context)
723+
} else {
724+
logger.warn(
725+
{ file: editedFile.path },
726+
'No Benchify diff found for file.',
727+
)
728+
}
729+
}),
725730
)
726731

727732
// Log any failures but don't throw - individual file failures shouldn't block the batch
@@ -730,7 +735,7 @@ async function applyBenchifyResultsGracefully(
730735
logger.warn(
731736
{
732737
failureCount: failures.length,
733-
totalFiles: benchifyFiles.length,
738+
totalFiles: editedFiles.length,
734739
agentStepId: context.agentStepId,
735740
},
736741
'Some Benchify results failed to apply',
@@ -743,6 +748,7 @@ async function applyBenchifyResultsGracefully(
743748
*/
744749
async function applyBenchifyResultSafely(
745750
benchifyFile: { path: string; contents: string },
751+
benchifyDiff: ParsedDiff,
746752
context: {
747753
ws: WebSocket
748754
onResponseChunk: (chunk: string | PrintModeEvent) => void
@@ -798,30 +804,12 @@ async function applyBenchifyResultSafely(
798804
return
799805
}
800806

801-
// Skip if content is unchanged
802-
if (baseContent === benchifyFile.contents) {
803-
logger.debug(
804-
{ path: benchifyFile.path, agentStepId: context.agentStepId },
805-
'Benchify result identical to current content, skipping',
806-
)
807-
return
808-
}
809-
810-
// Generate a proper unified diff patch
811-
const patch = createPatch(
812-
benchifyFile.path,
813-
baseContent,
814-
benchifyFile.contents,
815-
'',
816-
'',
817-
)
818-
819807
// Apply with timeout to prevent hanging
820808
const toolCallResult = await withTimeout(
821809
requestToolCall(context.ws, context.userInputId, 'str_replace', {
822810
type: 'patch',
823811
path: benchifyFile.path,
824-
content: patch,
812+
content: benchifyDiff,
825813
}),
826814
5000,
827815
'Benchify patch application timed out',

0 commit comments

Comments
 (0)