Skip to content

turn_context et assemblage du prompt ​

源码版本v2026.7.20

Responsabilité ​

Avant chaque appel API, assemble « messages d'historique + entrée utilisateur du tour (turn) + contexte (context) additionnel côté passerelle (gateway) + marqueurs de compression (compression) » en api_messages envoyé au provider. TurnContext est le conteneur produit pour ce tour, build_turn_context en est l'entrée d'assemblage. Il injecte aussi dans le contenu multimodal les notes collectées par la passerelle.

Fichiers clés ​

Flux de données ​

  1. La boucle principale appelle build_turn_context:268 à chaque tour et obtient un TurnContext.
  2. compose_user_api_content (agent/turn_context.py:44) assemble l'entrée utilisateur brute en contenu API.
  3. Si la passerelle a accumulé des notes (pièces jointes, indices de contexte), consume_gateway_turn_context_notes (agent/turn_context.py:124) les récupère et les injecte via append_notes_to_multimodal_content.
  4. substitute_api_content (agent/turn_context.py:79) remplace les placeholders ; drop_stale_api_content nettoie le contenu périmé.
  5. Si le tour précédent a déclenché une compression, reanchor_current_turn_user_idx (agent/turn_context.py:164) corrige la position du message utilisateur du tour dans l'historique.
  6. Les api_messages assemblés entrent dans sous-boucle de retry API:1227.

TurnContext est une dataclass ordinaire, qui regroupe les valeurs calculées par le prologue (champs clés dans agent/turn_context.py:242-268) : user_message est le message du tour après sanitization ; original_user_message conserve le texte brut pour la mémoire et la recherche dans les logs (les nudge ne s'y injectent pas) ; conversation_history peut être mise à None par une compression preflight — la compression ouvre une nouvelle session (session), l'ancien history n'est plus réutilisable ; active_system_prompt est le prompt système caché pour ce tour, potentiellement reconstruit par la compression.

La logique de consommation des notes passerelle est dans agent/turn_context.py:124-145 :

python
def consume_gateway_turn_context_notes(agent: Any) -> str:
    """Pop the gateway's per-turn must-deliver notes off the agent (one-shot).
    ... so the composed system prompt stays byte-stable turn-over-turn.
    ... this consumes them so a cached agent can never replay a stale note."""
    notes = getattr(agent, "_gateway_turn_context_notes", "") or ""
    if hasattr(agent, "_gateway_turn_context_notes"):
        try:
            agent._gateway_turn_context_notes = ""
        except Exception:
            pass
    return notes if isinstance(notes, str) else ""

Une fois lues, on remet à zéro — c'est un pop one-shot, pas un peek. Même si l'agent est mis en cache et réutilisé, les notes non consommées du tour précédent ne fuient pas dans le suivant.

Mot de conception ​

Pourquoi extraire le prologue en build_turn_context ? À l'origine, toute la préparation « une fois par tour » (guard stdio, reset du compteur de retry, sanitization du user message, injection todo/nudge, construction du prompt système, compression preflight, hook pre_llm_call, préfetch mémoire externe, persistance crash-récup) était inline en tête de run_conversation, près de 130 lignes. Une fois extrait en fonction, la boucle principale ne contient plus que « boucle + retry », lisible sans être interrompue par la logique de préparation ; le prologue devient testable isolément et réutilisable par le chemin sous-agent.

Pourquoi les notes passent-elles par un sidecar du message utilisateur plutôt que par le system prompt ? Le prompt système doit rester byte-stable d'un tour à l'autre — dès qu'il change, le prefix caché précédent est invalidé, et le prompt caching côté provider saute. La passerelle déplace donc le contenu volatile (première auto-présentation, bascule de canal vocal, hint temporaire d'auto-reset) hors du system prompt, et le délivre via le sidecar api_content du message utilisateur — chaque tour peut emporter de nouveaux faits sans casser le hit rate du cache.

_compression_made_progress utilise 5 % comme seuil matériel pour empêcher la rotation à vide : un tour qui compacte 220 messages en 220 mais abaisse les tokens de 288k à 183k serait faussement signalé « ne progresse plus » si l'on ne juge que sur le nombre de lignes, et déclencherait un auto-reset.

Limites et échecs ​

  • Messages multimodal qui perdent les notes : compose_user_api_content renvoie None pour les contenus non-chaîne ; si l'utilisateur envoie une image, le sidecar notes est silencieusement perdu. append_notes_to_multimodal_content rattrape en appendant les notes comme text part directement — au prix d'une persistance dans le transcript ; la passerelle ne doit y mettre que du contenu vraiment destiné à être persisté.
  • Dérive de l'index du message utilisateur après compression : la compacte fusionne les anciens messages en summary, la position du message utilisateur du tour dans messages bouge. reanchor_current_turn_user_idx rescanne la liste pour retrouver l'indice ; sans repositionnement, le résultat d'un appel d'outil (tool) serait réinjecté au mauvais endroit.
  • Peu de messages mais très gros : l'ancienne version de _should_run_preflight_estimate ne regardait que le nombre de messages, et quelques énormes images base64 ne déclenchaient jamais la compression, partant direct au hard overflow (#27405). La nouvelle version ajoute une branche d'estimation char-based, pour que les gros messages tombent aussi dans le seuil.
  • Notes stale résiduelles : consume_gateway_turn_context_notes attrape l'échec d'écriture par try/except — même si l'attribut agent est une property en lecture seule, l'échec de remise à zéro n'interrompt pas l'assemblage.

Résumé ​

turn_context est la « fabrique d'instantanés par tour » : il fige un contexte dynamique en une structure envoyable. Il travaille de pair avec la compression de contexte — une fois la compression effectuée, turn_context est chargé de réaligner les pointeurs.