Skip to content

fix(cast): delete pruned tool-call parts in reverse to avoid index shift#364

Open
Osamaali313 wants to merge 1 commit into
vxcontrol:mainfrom
Osamaali313:fix/newbodypair-reverse-delete
Open

fix(cast): delete pruned tool-call parts in reverse to avoid index shift#364
Osamaali313 wants to merge 1 commit into
vxcontrol:mainfrom
Osamaali313:fix/newbodypair-reverse-delete

Conversation

@Osamaali313

Copy link
Copy Markdown

Problem

NewBodyPair (backend/pkg/cast/chain_ast.go) prunes ToolCall parts that have a nil FunctionCall from an AI message. It first collects their indices into partsToDelete (ascending), then deletes them in a forward loop:

for _, id := range partsToDelete {
    aiMsg.Parts = append(aiMsg.Parts[:id], aiMsg.Parts[id+1:]...)
}

Each append-based deletion shifts every later element left by one, but the loop keeps using the original, now-stale indices. With two or more collected indices, the second (and later) deletions remove the wrong elements.

Concrete failure

Input parts [ToolCall{FC:nil}, ToolCall{FC:nil}, TextContent{"keep me"}] (two leading invalid tool calls, then real text):

  • Expected after pruning: ["keep me"]
  • Actual: [ToolCall{FC:nil}] — the legitimate text is dropped and an invalid nil-FunctionCall tool call survives into the message chain, feeding malformed messages to downstream summarization / LLM code.

(The scan continues over nil-FC tool calls and only breaks on a valid tool call, so 2+ leading nil-FC tool calls are collected — the trigger.)

Fix

Delete the collected indices in reverse, so removing a later element never invalidates the indices still pending:

for i := len(partsToDelete) - 1; i >= 0; i-- {
    id := partsToDelete[i]
    aiMsg.Parts = append(aiMsg.Parts[:id], aiMsg.Parts[id+1:]...)
}

Tests

Added TestNewBodyPair_DropsMultipleNilFunctionCallToolCalls in backend/pkg/cast/newbodypair_reg_test.go.

  • Before the fix it fails (surviving part is a ToolCall, text lost).
  • After the fix it passes.

go test ./pkg/cast/ ./pkg/csum/ passes (no regressions) and go vet ./pkg/cast/ is clean.

NewBodyPair collects the indices of ToolCall parts with a nil
FunctionCall into partsToDelete, then deletes them in a forward loop
using those pre-collected ascending indices. Each deletion shifts every
later element left by one, so with two or more collected indices the
second and subsequent deletions remove the wrong elements: a valid
TextContent can be dropped while an invalid nil-FunctionCall tool call
survives into the message chain (feeding malformed messages downstream).

Iterate the collected indices in reverse so earlier indices stay valid
during deletion. Adds a regression test covering two leading
nil-FunctionCall tool calls followed by a text part.
Copilot AI review requested due to automatic review settings July 5, 2026 21:02

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants