From 36872809f17d60dbe7a0249d0ea9e88164a96aac Mon Sep 17 00:00:00 2001 From: Osamaali313 Date: Mon, 6 Jul 2026 00:02:03 +0300 Subject: [PATCH] fix(cast): delete pruned tool-call parts in reverse to avoid index shift 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. --- backend/pkg/cast/chain_ast.go | 6 ++++- backend/pkg/cast/newbodypair_reg_test.go | 28 ++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) create mode 100644 backend/pkg/cast/newbodypair_reg_test.go diff --git a/backend/pkg/cast/chain_ast.go b/backend/pkg/cast/chain_ast.go index 5a84763a2..33fa6bf09 100644 --- a/backend/pkg/cast/chain_ast.go +++ b/backend/pkg/cast/chain_ast.go @@ -453,7 +453,11 @@ func NewBodyPair(aiMsg *llms.MessageContent, toolMsgs []*llms.MessageContent) *B break } } - for _, id := range partsToDelete { + // Delete in reverse so each removal does not shift the indices of the + // entries still pending deletion (a forward loop with the pre-collected + // ascending indices would delete the wrong elements once there are 2+). + for i := len(partsToDelete) - 1; i >= 0; i-- { + id := partsToDelete[i] aiMsg.Parts = append(aiMsg.Parts[:id], aiMsg.Parts[id+1:]...) } } diff --git a/backend/pkg/cast/newbodypair_reg_test.go b/backend/pkg/cast/newbodypair_reg_test.go new file mode 100644 index 000000000..d1ebe5645 --- /dev/null +++ b/backend/pkg/cast/newbodypair_reg_test.go @@ -0,0 +1,28 @@ +package cast + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/vxcontrol/langchaingo/llms" +) + +func TestNewBodyPair_DropsMultipleNilFunctionCallToolCalls(t *testing.T) { + aiMsg := &llms.MessageContent{ + Role: llms.ChatMessageTypeAI, + Parts: []llms.ContentPart{ + llms.ToolCall{ID: "a", Type: "function", FunctionCall: nil}, + llms.ToolCall{ID: "b", Type: "function", FunctionCall: nil}, + llms.TextContent{Text: "keep me"}, + }, + } + + NewBodyPair(aiMsg, nil) + + assert.Len(t, aiMsg.Parts, 1, "both nil-FunctionCall tool calls should be removed, text kept") + if assert.Len(t, aiMsg.Parts, 1) { + txt, ok := aiMsg.Parts[0].(llms.TextContent) + assert.True(t, ok, "surviving part should be the TextContent, got %T", aiMsg.Parts[0]) + assert.Equal(t, "keep me", txt.Text) + } +}