fix(agent): occurrence-aware tool call id sanitization
This commit is contained in:
parent
6148ccc529
commit
f390b6686e
4 changed files with 107 additions and 40 deletions
|
|
@ -708,6 +708,56 @@ func sanitizeHistoryForProvider(history []providers.Message) []providers.Message
|
|||
final = append(final, msg)
|
||||
}
|
||||
|
||||
// Third pass (occurrence-aware): ensure all tool_call_id values are unique
|
||||
// across the entire transcript. This prevents 400 errors from strict
|
||||
// non-OpenAI providers when the same ID appears in different turns or
|
||||
// is duplicated within a single assistant message.
|
||||
type callResolver struct {
|
||||
rewritten []string
|
||||
usedCount int
|
||||
}
|
||||
resolver := make(map[string]*callResolver)
|
||||
globalUsed := make(map[string]bool)
|
||||
|
||||
for i := range final {
|
||||
msg := &final[i]
|
||||
if msg.Role == "assistant" && len(msg.ToolCalls) > 0 {
|
||||
for j := range msg.ToolCalls {
|
||||
originalID := msg.ToolCalls[j].ID
|
||||
if _, ok := resolver[originalID]; !ok {
|
||||
resolver[originalID] = &callResolver{}
|
||||
}
|
||||
res := resolver[originalID]
|
||||
occ := len(res.rewritten)
|
||||
|
||||
rewrittenID := originalID
|
||||
// If ID was used globally or this is an occurrence repeat, make it unique.
|
||||
if occ > 0 || globalUsed[rewrittenID] {
|
||||
rewrittenID = fmt.Sprintf("%s:%d", originalID, occ)
|
||||
for globalUsed[rewrittenID] {
|
||||
// Extremely unlikely collision with another ID's suffix
|
||||
rewrittenID += "_x"
|
||||
}
|
||||
}
|
||||
|
||||
msg.ToolCalls[j].ID = rewrittenID
|
||||
res.rewritten = append(res.rewritten, rewrittenID)
|
||||
globalUsed[rewrittenID] = true
|
||||
}
|
||||
} else if msg.Role == "tool" {
|
||||
originalID := msg.ToolCallID
|
||||
if res, ok := resolver[originalID]; ok && res.usedCount < len(res.rewritten) {
|
||||
msg.ToolCallID = res.rewritten[res.usedCount]
|
||||
res.usedCount++
|
||||
} else {
|
||||
// Orphan result: assign unique fallback ID but preserve original prefix for context.
|
||||
fallback := fmt.Sprintf("orphan_%s_%d", originalID, i)
|
||||
msg.ToolCallID = fallback
|
||||
globalUsed[fallback] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return final
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1291,10 +1291,7 @@ func (al *AgentLoop) runLLMIteration(
|
|||
streamer.Cancel(ctx)
|
||||
}
|
||||
|
||||
normalizedToolCalls := make([]providers.ToolCall, 0, len(response.ToolCalls))
|
||||
for _, tc := range response.ToolCalls {
|
||||
normalizedToolCalls = append(normalizedToolCalls, providers.NormalizeToolCall(tc))
|
||||
}
|
||||
normalizedToolCalls := providers.NormalizeToolCall(response.ToolCalls)
|
||||
|
||||
// Log tool calls
|
||||
toolNames := make([]string, 0, len(normalizedToolCalls))
|
||||
|
|
|
|||
|
|
@ -46,7 +46,16 @@ func buildCLIToolsPrompt(tools []ToolDefinition) string {
|
|||
// NormalizeToolCall normalizes a ToolCall to ensure all fields are properly populated.
|
||||
// It handles cases where Name/Arguments might be in different locations (top-level vs Function)
|
||||
// and ensures both are populated consistently.
|
||||
func NormalizeToolCall(tc ToolCall) ToolCall {
|
||||
// (Addition: It also ensures that each tool call ID is unique across the set for strict providers.)
|
||||
func NormalizeToolCall(calls []ToolCall) []ToolCall {
|
||||
if len(calls) == 0 {
|
||||
return calls
|
||||
}
|
||||
|
||||
used := make(map[string]bool)
|
||||
sanitized := make([]ToolCall, 0, len(calls))
|
||||
|
||||
for i, tc := range calls {
|
||||
normalized := tc
|
||||
|
||||
// Ensure Name is populated from Function if not set
|
||||
|
|
@ -86,5 +95,19 @@ func NormalizeToolCall(tc ToolCall) ToolCall {
|
|||
}
|
||||
}
|
||||
|
||||
return normalized
|
||||
// ID uniqueness normalization fix
|
||||
id := strings.TrimSpace(normalized.ID)
|
||||
if id == "" || used[id] {
|
||||
id = fmt.Sprintf("call_auto_%d", i)
|
||||
for used[id] {
|
||||
id += "_x"
|
||||
}
|
||||
}
|
||||
used[id] = true
|
||||
normalized.ID = id
|
||||
|
||||
sanitized = append(sanitized, normalized)
|
||||
}
|
||||
|
||||
return sanitized
|
||||
}
|
||||
|
|
|
|||
|
|
@ -85,10 +85,7 @@ func RunToolLoop(
|
|||
break
|
||||
}
|
||||
|
||||
normalizedToolCalls := make([]providers.ToolCall, 0, len(response.ToolCalls))
|
||||
for _, tc := range response.ToolCalls {
|
||||
normalizedToolCalls = append(normalizedToolCalls, providers.NormalizeToolCall(tc))
|
||||
}
|
||||
normalizedToolCalls := providers.NormalizeToolCall(response.ToolCalls)
|
||||
|
||||
// 5. Log tool calls
|
||||
toolNames := make([]string, 0, len(normalizedToolCalls))
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue