Skip to content

Commit 408fe2c

Browse files
slg95Spencer Gray
authored andcommitted
fix(tools): Ensure tool result message follows tool_call.
It's possible to send a message during a tool call, which causes errors with all major LLM providers.
1 parent 61f56e8 commit 408fe2c

File tree

1 file changed

+40
-15
lines changed
  • lua/codecompanion/interactions/chat

1 file changed

+40
-15
lines changed

lua/codecompanion/interactions/chat/init.lua

Lines changed: 40 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -187,17 +187,22 @@ local function get_client(adapter)
187187
end
188188
end
189189

190-
---Find a message in the table that has a specific tool call ID
190+
---Find the index of the parent tool call message that contains a specific tool call ID
191191
---@param id string
192192
---@param messages CodeCompanion.Chat.Messages
193-
---@return table|nil
194-
local function find_tool_call(id, messages)
195-
for _, msg in ipairs(messages) do
196-
if msg.tools and msg.tools.call_id and msg.tools.call_id == id then
197-
return msg
193+
---@return number|nil parent_index The index of the parent tool call message
194+
---@return number|nil call_position The position of this tool call within tools.calls (1-based)
195+
local function find_tool_call_index(id, messages)
196+
for i, msg in ipairs(messages) do
197+
if msg.tools and msg.tools.calls then
198+
for j, call in ipairs(msg.tools.calls) do
199+
if call.id == id then
200+
return i, j
201+
end
202+
end
198203
end
199204
end
200-
return nil
205+
return nil, nil
201206
end
202207

203208
---Increment the cycle count in the chat buffer
@@ -1561,16 +1566,36 @@ function Chat:add_tool_output(tool, for_llm, for_user)
15611566
visible = true,
15621567
})
15631568

1564-
-- Ensure that tool output is merged if it has the same tool call ID
1565-
local existing = find_tool_call(tool_call.id, self.messages)
1566-
if existing then
1567-
if existing.content ~= "" then
1568-
existing.content = existing.content .. "\n\n" .. output.content
1569+
-- Find the parent tool call message and insert result right after it
1570+
-- call_position ensures correct ordering when multiple tool calls exist in same message
1571+
local parent_index, call_position = find_tool_call_index(tool_call.id, self.messages)
1572+
if not parent_index then
1573+
-- Fallback: append to end if parent not found (e.g., direct execute calls in tests)
1574+
log:warn("Could not find parent tool call for ID: %s, appending to end", tool_call.id)
1575+
table.insert(self.messages, output)
1576+
else
1577+
-- Calculate insert position, clamping to valid range to avoid sparse arrays
1578+
local insert_index = math.min(parent_index + call_position, #self.messages + 1)
1579+
1580+
-- Search from after parent to insert position for existing tool result to merge with
1581+
local existing_msg = nil
1582+
for i = parent_index + 1, insert_index do
1583+
local msg = self.messages[i]
1584+
if msg and msg.tools and msg.tools.call_id == tool_call.id then
1585+
existing_msg = msg
1586+
break
1587+
end
1588+
end
1589+
1590+
if existing_msg then
1591+
if existing_msg.content ~= "" then
1592+
existing_msg.content = existing_msg.content .. "\n\n" .. output.content
1593+
else
1594+
existing_msg.content = output.content
1595+
end
15691596
else
1570-
existing.content = output.content
1597+
table.insert(self.messages, insert_index, output)
15711598
end
1572-
else
1573-
table.insert(self.messages, output)
15741599
end
15751600

15761601
-- Allow tools to pass in an empty string to not write any output to the buffer

0 commit comments

Comments
 (0)