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)
|
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
|
return final
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1291,10 +1291,7 @@ func (al *AgentLoop) runLLMIteration(
|
||||||
streamer.Cancel(ctx)
|
streamer.Cancel(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
normalizedToolCalls := make([]providers.ToolCall, 0, len(response.ToolCalls))
|
normalizedToolCalls := providers.NormalizeToolCall(response.ToolCalls)
|
||||||
for _, tc := range response.ToolCalls {
|
|
||||||
normalizedToolCalls = append(normalizedToolCalls, providers.NormalizeToolCall(tc))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Log tool calls
|
// Log tool calls
|
||||||
toolNames := make([]string, 0, len(normalizedToolCalls))
|
toolNames := make([]string, 0, len(normalizedToolCalls))
|
||||||
|
|
|
||||||
|
|
@ -46,45 +46,68 @@ func buildCLIToolsPrompt(tools []ToolDefinition) string {
|
||||||
// NormalizeToolCall normalizes a ToolCall to ensure all fields are properly populated.
|
// 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)
|
// It handles cases where Name/Arguments might be in different locations (top-level vs Function)
|
||||||
// and ensures both are populated consistently.
|
// 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.)
|
||||||
normalized := tc
|
func NormalizeToolCall(calls []ToolCall) []ToolCall {
|
||||||
|
if len(calls) == 0 {
|
||||||
// Ensure Name is populated from Function if not set
|
return calls
|
||||||
if normalized.Name == "" && normalized.Function != nil {
|
|
||||||
normalized.Name = normalized.Function.Name
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure Arguments is not nil
|
used := make(map[string]bool)
|
||||||
if normalized.Arguments == nil {
|
sanitized := make([]ToolCall, 0, len(calls))
|
||||||
normalized.Arguments = map[string]any{}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse Arguments from Function.Arguments if not already set
|
for i, tc := range calls {
|
||||||
if len(normalized.Arguments) == 0 && normalized.Function != nil && normalized.Function.Arguments != "" {
|
normalized := tc
|
||||||
var parsed map[string]any
|
|
||||||
if err := json.Unmarshal([]byte(normalized.Function.Arguments), &parsed); err == nil && parsed != nil {
|
|
||||||
normalized.Arguments = parsed
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure Function is populated with consistent values
|
// Ensure Name is populated from Function if not set
|
||||||
argsJSON, _ := json.Marshal(normalized.Arguments)
|
if normalized.Name == "" && normalized.Function != nil {
|
||||||
if normalized.Function == nil {
|
|
||||||
normalized.Function = &FunctionCall{
|
|
||||||
Name: normalized.Name,
|
|
||||||
Arguments: string(argsJSON),
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if normalized.Function.Name == "" {
|
|
||||||
normalized.Function.Name = normalized.Name
|
|
||||||
}
|
|
||||||
if normalized.Name == "" {
|
|
||||||
normalized.Name = normalized.Function.Name
|
normalized.Name = normalized.Function.Name
|
||||||
}
|
}
|
||||||
if normalized.Function.Arguments == "" {
|
|
||||||
normalized.Function.Arguments = string(argsJSON)
|
// Ensure Arguments is not nil
|
||||||
|
if normalized.Arguments == nil {
|
||||||
|
normalized.Arguments = map[string]any{}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Parse Arguments from Function.Arguments if not already set
|
||||||
|
if len(normalized.Arguments) == 0 && normalized.Function != nil && normalized.Function.Arguments != "" {
|
||||||
|
var parsed map[string]any
|
||||||
|
if err := json.Unmarshal([]byte(normalized.Function.Arguments), &parsed); err == nil && parsed != nil {
|
||||||
|
normalized.Arguments = parsed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure Function is populated with consistent values
|
||||||
|
argsJSON, _ := json.Marshal(normalized.Arguments)
|
||||||
|
if normalized.Function == nil {
|
||||||
|
normalized.Function = &FunctionCall{
|
||||||
|
Name: normalized.Name,
|
||||||
|
Arguments: string(argsJSON),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if normalized.Function.Name == "" {
|
||||||
|
normalized.Function.Name = normalized.Name
|
||||||
|
}
|
||||||
|
if normalized.Name == "" {
|
||||||
|
normalized.Name = normalized.Function.Name
|
||||||
|
}
|
||||||
|
if normalized.Function.Arguments == "" {
|
||||||
|
normalized.Function.Arguments = string(argsJSON)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 normalized
|
return sanitized
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -85,10 +85,7 @@ func RunToolLoop(
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
normalizedToolCalls := make([]providers.ToolCall, 0, len(response.ToolCalls))
|
normalizedToolCalls := providers.NormalizeToolCall(response.ToolCalls)
|
||||||
for _, tc := range response.ToolCalls {
|
|
||||||
normalizedToolCalls = append(normalizedToolCalls, providers.NormalizeToolCall(tc))
|
|
||||||
}
|
|
||||||
|
|
||||||
// 5. Log tool calls
|
// 5. Log tool calls
|
||||||
toolNames := make([]string, 0, len(normalizedToolCalls))
|
toolNames := make([]string, 0, len(normalizedToolCalls))
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue