Skip to content

Appariement tool_calls/tool : un stop en pleine série peut bloquer la conversation - #171

Merged
nathaninline merged 1 commit into
nathaninline:mainfrom
oh-codes:fix/tool-call-pairing
Oct 24, 2026
Merged

nathaninline merged 1 commit into
nathaninline:mainfrom
oh-codes:fix/tool-call-pairing

Conversation

@oh-codes

Copy link
Copy Markdown
Contributor

Contexte

Après un arrêt (bouton stop) pendant une série d'appels d'outils, tous les tours suivants échouent avec :

An assistant message with 'tool_calls' must be followed by tool messages responding to each 'tool_call_id'. (insufficient tool messages following tool_calls message)

La conversation est définitivement bloquée : « Vider le contexte » ne sert à rien si la demi-paire est recréée ensuite.

Cause

  1. llm_client.go (~1586-1600) ajoute le message assistant porteur de tous les tool_calls, puis sort par if ctx.Err() != nil { return extra, nil } entre deux appels.
  2. chat_conversation.go (~921-930) persiste extra avec un garde sur epoch mais aucun garde sur l'annulation — le client CLI, lui, vérifie (cli_chat.go:464-472).
  3. La demi-paire part dans bbolt et est renvoyée à chaque tour → 400 permanent.

Preuve

Relevé en usage réel (2026-10-22) : 308 ids d'appels côté assistant contre 306 réponses tool ; call_00_h80I… (write {}) sans réponse et dernier message de la conversation active, call_01_CWX… (bash) sans réponse dans une session archivée.

Correctif

repairToolPairs (chat_tool_pairs.go) garantit qu'après chaque assistant porteur d'appels suivent immédiatement une réponse tool par tool_call_id, dans l'ordre :

  • réponses existantes conservées, manquantes marquées [interrompu] ;
  • id vide synthétisé (call_repair_<i>_<k>) ;
  • messages média de see_image / see_video replacés après le bloc (ils créaient assistant(a,b) → tool(a) → user(image) → tool(b), forme que le validateur strict refuse) ;
  • réponses réellement orphelines supprimées ;
  • idempotente, ne modifie pas l'entrée.

Appliquée à l'envoi, à la sortie sur stop et dans persist() : les conversations déjà empoisonnées se réparent au tour suivant, sans action manuelle.

Tests

go test ./internal/ajean -run RepairToolPairs — les deux formes exactes relevées en base servent de cas de test (paire complète, appel unique sans réponse, série partielle, orphelin, id vide, média intercalé, idempotence, non-mutation de l'entrée).

gofmt, go vet et staticcheck propres. go test ./... conserve trois échecs préexistants sur main (vérifiés sur un checkout propre) : TestMoeMigrate (privilège symlink Windows), TestShrinkToFit, TestSystemPromptStaysLean.

Vérification en conditions réelles

Binaire reconstruit et relancé : la conversation bloquée répond de nouveau, la vue modèle ne contient plus aucune paire orpheline.

🤖 Generated with Claude Code

…la conversation

Un arrêt entre deux appels d'outil laissait dans la vue modèle l'assistant
porteur de TOUS ses tool_calls et une partie seulement des réponses `tool`.
Persistée, cette demi-paire faisait refuser tous les tours suivants en 400
(« An assistant message with 'tool_calls' must be followed by tool messages
responding to each 'tool_call_id' ») : la conversation devenait définitivement
inutilisable, « Vider le contexte » étant le seul recours.

Relevé en base le 2026-10-22 : deux ids d'appel sans réponse, dont un en dernier
message de la conversation active (308 appels contre 306 réponses).

repairToolPairs garantit qu'après chaque assistant porteur d'appels suivent
immédiatement une réponse par tool_call_id, dans l'ordre : les réponses
présentes sont conservées, les manquantes reçoivent un marqueur « interrompu »,
un id vide est synthétisé, les messages média injectés par see_image/see_video
sont replacés après le bloc et les réponses réellement orphelines sont retirées.

Appliquée à l'envoi, à la sortie sur stop et dans persist() : les conversations
déjà empoisonnées se réparent seules au tour suivant, et le blob reste sain.

Co-Authored-By: Claude Code <noreply@anthropic.com>
@oh-codes oh-codes changed the title Appariement tool_calls/tool : un stop en pleine série ne bloque plus la conversation Appariement tool_calls/tool : un stop en pleine sériepeut bloquer la conversation Oct 22, 2026
@oh-codes oh-codes changed the title Appariement tool_calls/tool : un stop en pleine sériepeut bloquer la conversation Appariement tool_calls/tool : un stop en pleine série peut bloquer la conversation Oct 22, 2026
@nathaninline
nathaninline merged commit e12ac56 into nathaninline:main Oct 24, 2026
2 checks passed
@nathaninline

Copy link
Copy Markdown
Owner

Merci, diagnostic juste : j'ai vérifié dans le code, le stop sortait de la boucle avec l'assistant porteur de tous les appels et seulement une partie des réponses. J'ai aussi confirmé le cas de l'image de see_image glissée entre deux réponses d'outil. Fusionné sur main, ce sera dans la prochaine version.

@oh-codes
oh-codes deleted the fix/tool-call-pairing branch October 25, 2026 17:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants