mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-10-15 04:18:44 +00:00
fix(agent): correct within-turn duplicate tool call ID pairing in sanitizeHistory
Replace single-value idRemap with queue-based idQueue so multiple tool results sharing the same original ID pair correctly in encounter order. Previously the map overwrote on duplicates, causing the second result to be dropped and a synthetic placeholder to be emitted instead.
This commit is contained in:
1 parent
55bc752773
commit
cc43f78b4b
2 files changed
+60
-5
No files matched your search
@@ -408,9 +408,10 @@ func sanitizeHistory(msgs []providers.Message) ([]providers.Message, int) {
|
||||
msg.ToolCalls = make([]providers.ToolCall, len(oldCalls))
|
||||
copy(msg.ToolCalls, oldCalls)
|
||||
|
||||
// Dedup: rewrite any ID that was already seen in an earlier turn.
|
||||
// Maps original ID → rewritten ID for pairing with tool results below.
|
||||
idRemap := make(map[string]string, len(msg.ToolCalls))
|
||||
// Dedup: rewrite any ID that was already seen in an earlier turn or
|
||||
// within the same turn. Uses a queue per original ID so multiple tool
|
||||
// results with the same raw ID pair correctly in encounter order.
|
||||
idQueue := make(map[string][]string, len(msg.ToolCalls)) // origID → []newID
|
||||
expectedIDs := make(map[string]bool, len(msg.ToolCalls))
|
||||
for j := range msg.ToolCalls {
|
||||
origID := msg.ToolCalls[j].ID
|
||||
@@ -421,7 +422,7 @@ func sanitizeHistory(msgs []providers.Message) ([]providers.Message, int) {
|
||||
}
|
||||
msg.ToolCalls[j].ID = newID
|
||||
globalSeen[newID] = true
|
||||
idRemap[origID] = newID
|
||||
idQueue[origID] = append(idQueue[origID], newID)
|
||||
expectedIDs[newID] = true
|
||||
}
|
||||
|
||||
@@ -431,7 +432,9 @@ func sanitizeHistory(msgs []providers.Message) ([]providers.Message, int) {
|
||||
for i+1 < len(msgs) && msgs[i+1].Role == "tool" {
|
||||
i++
|
||||
toolMsg := msgs[i]
|
||||
if newID, ok := idRemap[toolMsg.ToolCallID]; ok && expectedIDs[newID] {
|
||||
if queue, ok := idQueue[toolMsg.ToolCallID]; ok && len(queue) > 0 {
|
||||
newID := queue[0]
|
||||
idQueue[toolMsg.ToolCallID] = queue[1:]
|
||||
toolMsg.ToolCallID = newID
|
||||
result = append(result, toolMsg)
|
||||
delete(expectedIDs, newID)
|
||||
|
||||
@@ -243,6 +243,58 @@ func TestSanitizeHistory_DedupsDuplicateIDsAcrossTurns(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestSanitizeHistory_DedupsDuplicateIDsWithinTurn(t *testing.T) {
|
||||
msgs := []providers.Message{
|
||||
{Role: "user", Content: "do two things"},
|
||||
{Role: "assistant", Content: "", ToolCalls: []providers.ToolCall{
|
||||
{ID: "call_abc", Name: "read_file"},
|
||||
{ID: "call_abc", Name: "write_file"}, // same ID within turn
|
||||
}},
|
||||
{Role: "tool", Content: "result1", ToolCallID: "call_abc"},
|
||||
{Role: "tool", Content: "result2", ToolCallID: "call_abc"},
|
||||
{Role: "assistant", Content: "done"},
|
||||
}
|
||||
got, dropped := sanitizeHistory(msgs)
|
||||
if dropped != 0 {
|
||||
t.Errorf("expected 0 dropped, got %d", dropped)
|
||||
}
|
||||
|
||||
// Both tool results must be present and paired correctly
|
||||
toolResults := 0
|
||||
for _, m := range got {
|
||||
if m.Role == "tool" {
|
||||
toolResults++
|
||||
}
|
||||
}
|
||||
if toolResults != 2 {
|
||||
t.Errorf("expected 2 tool results, got %d", toolResults)
|
||||
}
|
||||
|
||||
// All tool call IDs must be unique
|
||||
seen := make(map[string]bool)
|
||||
for _, m := range got {
|
||||
for _, tc := range m.ToolCalls {
|
||||
if seen[tc.ID] {
|
||||
t.Errorf("duplicate tool call ID: %s", tc.ID)
|
||||
}
|
||||
seen[tc.ID] = true
|
||||
}
|
||||
}
|
||||
|
||||
// Each tool result ID must match a tool call ID
|
||||
callIDs := make(map[string]bool)
|
||||
for _, m := range got {
|
||||
for _, tc := range m.ToolCalls {
|
||||
callIDs[tc.ID] = true
|
||||
}
|
||||
}
|
||||
for _, m := range got {
|
||||
if m.Role == "tool" && !callIDs[m.ToolCallID] {
|
||||
t.Errorf("tool result ID %s has no matching tool call", m.ToolCallID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSanitizeHistory_NoDedupWhenIDsUnique(t *testing.T) {
|
||||
msgs := []providers.Message{
|
||||
{Role: "user", Content: "hello"},
|
||||
|
||||
Reference in new issue
Block a user