fix: preserva campos propietarios del proveedor en el loop de tool-calling

Gemini (modelos "thinking" como 2.5) exige que el tool_call se reenvíe con
su thought_signature intacto en la ronda siguiente, o rechaza con 400
"Function call is missing a thought_signature". Nuestro parseo a
agentMessage/agentToolCall no conocía ese campo y lo descartaba en el
unmarshal, así que cualquier tool call con Gemini fallaba apenas el modelo
pedía usar una herramienta (buscar_conocimiento incluida).

agentMessage ahora guarda los bytes JSON originales cuando se parsea de una
respuesta (UnmarshalJSON) y los reenvía tal cual al volver a serializar
(MarshalJSON) — preserva thought_signature u otro campo propietario que
llegue, sin que el código necesite conocer su nombre/forma exacta. Los
mensajes que armamos nosotros (user/tool/system) no llevan Raw y siguen
serializando normal. Afecta tanto a uMind como al agente de Telegram, que
comparten este mismo motor.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Lizandro Guarnizo
2026-08-12 22:39:09 -05:00
co-authored by Claude Sonnet 5
parent e0c2f77f8c
commit 8eba3ab97f
2 changed files with 74 additions and 0 deletions
+32
View File
@@ -47,6 +47,38 @@ type agentMessage struct {
// Adjunto opcional para que el modelo lea el archivo real (solo Anthropic).
AttachmentBase64 string `json:"-"`
AttachmentMime string `json:"-"`
// Raw guarda el JSON tal cual vino del proveedor cuando este mensaje se
// parseó de una respuesta (no cuando lo armamos nosotros). Algunos
// proveedores mandan campos propios fuera de nuestra struct — ej. Gemini
// exige que el tool_call de un modelo "thinking" se reenvíe con su
// thought_signature intacto, si no rechaza la siguiente ronda con 400.
// Reenviar los bytes originales en vez de reserializar desde los campos
// tipados preserva cualquier campo así sin que tengamos que conocerlo.
Raw json.RawMessage `json:"-"`
}
// UnmarshalJSON guarda una copia de los bytes originales en Raw además de
// poblar los campos normales — así un mensaje que vino de la respuesta del
// proveedor se puede reenviar exactamente como llegó.
func (m *agentMessage) UnmarshalJSON(data []byte) error {
type alias agentMessage
var a alias
if err := json.Unmarshal(data, &a); err != nil {
return err
}
*m = agentMessage(a)
m.Raw = append(json.RawMessage(nil), data...)
return nil
}
// MarshalJSON reenvía Raw tal cual si el mensaje vino de un unmarshal
// previo; si lo armamos nosotros (Raw vacío), serializa los campos normales.
func (m agentMessage) MarshalJSON() ([]byte, error) {
if len(m.Raw) > 0 {
return m.Raw, nil
}
type alias agentMessage
return json.Marshal(alias(m))
}
type agentToolCall struct {