Skip to content

Commit a6e226c

Browse files
fix(tools): make insert_edit_into_file more robust for patch markers (olimorris#1653)
LLMs sometimes miss the BEGIN and END wrapping for the patches. This fails the merge. Making the algorithm lax to allow and handle these misses confidently.
1 parent b1682f9 commit a6e226c

File tree

5 files changed

+56
-13
lines changed

5 files changed

+56
-13
lines changed

lua/codecompanion/strategies/chat/agents/tools/helpers/patch.lua

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ local function parse_changes_from_patch(patch)
8686
elseif line == "" and lines[i + 1] and lines[i + 1]:match("^@@") then
8787
-- empty lines can be part of pre/post context
8888
-- we treat empty lines as new change block and not as post context
89-
-- only when the the next line uses @@ identifier
89+
-- only when the next line uses @@ identifier
9090
table.insert(changes, change)
9191
change = get_new_change()
9292
elseif line:sub(1, 1) == "-" then
@@ -115,14 +115,20 @@ end
115115

116116
---Parse the full raw string from LLM for all patches, returning all Change objects parsed.
117117
---@param raw string Raw text containing patch blocks
118-
---@return Change[] All parsed Change objects
118+
---@return Change[], boolean All parsed Change objects, and whether the patch was properly parsed
119119
function M.parse_changes(raw)
120120
local patches = {}
121121
for patch in raw:gmatch("%*%*%* Begin Patch%s+(.-)%s+%*%*%* End Patch") do
122122
table.insert(patches, patch)
123123
end
124+
125+
local had_begin_end_markers = true
124126
if #patches == 0 then
125-
error("Invalid patch format: missing Begin/End markers")
127+
--- LLMs miss the begin / end markers sometimes
128+
--- let's assume the raw content was correctly wrapped in these cases
129+
--- setting a `markers_error` so that we can show this error in case the patch fails to apply
130+
had_begin_end_markers = false
131+
table.insert(patches, raw)
126132
end
127133

128134
local all_changes = {}
@@ -132,7 +138,7 @@ function M.parse_changes(raw)
132138
table.insert(all_changes, change)
133139
end
134140
end
135-
return all_changes
141+
return all_changes, had_begin_end_markers
136142
end
137143

138144
---Score how many lines from needle match haystack lines.
@@ -190,7 +196,7 @@ end
190196
---Determine best insertion spot for a Change and its match score.
191197
---@param lines string[] File lines
192198
---@param change Change Patch block
193-
---@return integer,number location (1-based), Score (0-1)
199+
---@return integer, number location (1-based), Score (0-1)
194200
local function get_best_location(lines, change)
195201
-- try applying patch in flexible spaces mode
196202
-- there is no standardised way to of spaces in diffs

lua/codecompanion/strategies/chat/agents/tools/insert_edit_into_file.lua

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ local function edit_file(action)
3636

3737
-- 1. extract list of changes from the code
3838
local raw = action.code or ""
39-
local changes = patch.parse_changes(raw)
39+
local changes, had_begin_end_markers = patch.parse_changes(raw)
4040

4141
-- 2. read file into lines
4242
local content = p:read()
@@ -46,7 +46,11 @@ local function edit_file(action)
4646
for _, change in ipairs(changes) do
4747
local new_lines = patch.apply_change(lines, change)
4848
if new_lines == nil then
49-
error(fmt("Bad/Incorrect diff:\n\n%s\n\nNo changes were applied", patch.get_change_string(change)))
49+
if had_begin_end_markers then
50+
error(fmt("Bad/Incorrect diff:\n\n%s\n\nNo changes were applied", patch.get_change_string(change)))
51+
else
52+
error("Invalid patch format: missing Begin/End markers")
53+
end
5054
else
5155
lines = new_lines
5256
end
@@ -82,7 +86,7 @@ local function edit_buffer(bufnr, chat_bufnr, action, output_handler, opts)
8286

8387
-- Parse and apply patches to buffer
8488
local raw = action.code or ""
85-
local changes = patch.parse_changes(raw)
89+
local changes, had_begin_end_markers = patch.parse_changes(raw)
8690

8791
-- Get current buffer content as lines
8892
local lines = api.nvim_buf_get_lines(bufnr, 0, -1, false)
@@ -92,7 +96,11 @@ local function edit_buffer(bufnr, chat_bufnr, action, output_handler, opts)
9296
for _, change in ipairs(changes) do
9397
local new_lines = patch.apply_change(lines, change)
9498
if new_lines == nil then
95-
error(fmt("Bad/Incorrect diff:\n\n%s\n\nNo changes were applied", patch.get_change_string(change)))
99+
if had_begin_end_markers then
100+
error(fmt("Bad/Incorrect diff:\n\n%s\n\nNo changes were applied", patch.get_change_string(change)))
101+
else
102+
error("Invalid patch format: missing Begin/End markers")
103+
end
96104
else
97105
if not start_line then
98106
start_line = patch.get_change_location(lines, change)
Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1-
*** Begin Patch
21
@@ two
32
three
43
four
5-
five
64
-five
75
+5
86
six
97
seven
10-
*** End Patch
8+
9+
@@
10+
seven
11+
eight
12+
-nine

tests/fixtures/files-output-1.5.html

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,4 @@
66
six
77
seven
88
eight
9-
nine
109
ten

tests/strategies/chat/agents/tools/test_insert_edit_into_file.lua

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,34 @@ T["File"]["insert_edit_into_file tool multiple patches"] = function()
214214
h.eq_info(output, expected, child.lua_get("chat.messages[#chat.messages].content"))
215215
end
216216

217+
T["File"]["insert_edit_into_file tool multiple patches"] = function()
218+
child.lua([[
219+
-- read initial file from fixture
220+
local initial = vim.fn.readfile("tests/fixtures/files-input-1.html")
221+
local ok = vim.fn.writefile(initial, _G.TEST_TMPFILE_ABSOLUTE)
222+
assert(ok == 0)
223+
224+
-- read contents for the tool from fixtures
225+
local content = table.concat(vim.fn.readfile("tests/fixtures/files-diff-1.5.patch"), "\n")
226+
local arguments = vim.json.encode({ filepath = _G.TEST_TMPFILE, explanation = "...", code = content })
227+
local tool = {
228+
{
229+
["function"] = {
230+
name = "insert_edit_into_file",
231+
arguments = arguments
232+
},
233+
},
234+
}
235+
agent:execute(chat, tool)
236+
vim.wait(200)
237+
]])
238+
239+
-- Test that the file was updated as per the output fixture
240+
local output = child.lua_get("vim.fn.readfile(_G.TEST_TMPFILE_ABSOLUTE)")
241+
local expected = child.lua_get("vim.fn.readfile('tests/fixtures/files-output-1.5.html')")
242+
h.eq_info(output, expected, child.lua_get("chat.messages[#chat.messages].content"))
243+
end
244+
217245
T["File"]["insert_edit_into_file tool multiple continuation"] = function()
218246
child.lua([[
219247
-- read initial file from fixture

0 commit comments

Comments
 (0)