turn_context und Prompt-Zusammenbau
Verantwortung
Vor jedem API-Aufruf werden «Historien-Nachrichten + Nutzereingabe des aktuellen Turns + vom Gateway (gateway) angehängter Kontext (context) + Kompressions-Marker (compression marker)» zu den api_messages für den Provider (provider) zusammengebaut. TurnContext ist der Container dieses Turn-Produkts, build_turn_context der Assembly-Einstieg. Es injiziert außerdem die vom Gateway gesammelten Notes in multimodale Inhalte.
Schlüsseldateien
class TurnContext:242-268— Datenklasse des aktuellen Turn-Kontextsdef build_turn_context:268-400— Assembly-Einstiegcompose_user_api_content:44-79— Zusammenbau der Nutzernachrichten-Inhaltesubstitute_api_content:79-101— Platzhalter-Ersetzungconsume_gateway_turn_context_notes:124-164— Gateway-seitige Notes konsumieren und in multimodale Inhalte injizierenreanchor_current_turn_user_idx:164-210— Index der User-Nachricht im aktuellen Turn neu verankern (Korrektur nach Kompression)Kompressions-Fortschritts-Schätzung:190-242—_compression_made_progress/_should_run_preflight_estimateprompt_builder— System-Prompt-Zusammenbau (auf dieser Seite referenziert)
Datenfluss
- Die Hauptschleife ruft jeden Turn (turn)
build_turn_context:268auf und erhält einTurnContext. compose_user_api_content(agent/turn_context.py:44) baut die rohe Nutzereingabe zu API-Inhalten zusammen.- Hat das Gateway Notes gesammelt (aus Anhängen, Kontext-Hinweisen), holt
consume_gateway_turn_context_notes(agent/turn_context.py:124) sie heraus und injiziert sie überappend_notes_to_multimodal_content. substitute_api_content(agent/turn_context.py:79) ersetzt Platzhalter;drop_stale_api_contentbereinigt veraltete Inhalte.- Hat im vorherigen Turn eine Kompression stattgefunden, korrigiert
reanchor_current_turn_user_idx(agent/turn_context.py:164) die Position der aktuellen User-Nachricht in der Historie. - Die fertigen
api_messagestreten in dieAPI-Aufruf-Wiederholungs-Teilschleife:1227ein.
TurnContext ist eine ganz gewöhnliche Dataclass; sie bündelt die Werte aus dem Prolog in einer einzigen Instanz (Schlüsselfelder siehe agent/turn_context.py:242-268): user_message ist die desinfizierte Turn-Nachricht; original_user_message bewahrt den rohen Text für Memory und Log-Anfragen (Nudge wird hier nicht injiziert); conversation_history kann von der Preflight-Kompression auf None gesetzt werden — Kompression öffnet eine neue Sitzung (session), die alte History ist nicht mehr verwendbar; active_system_prompt ist der zwischengespeicherte System-Prompt (system prompt) des Turns, der ebenfalls von der Kompression neu gebaut werden kann.
Die Konsum-Logik der Gateway-Notes siehe agent/turn_context.py:124-145:
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 ""Nach dem Holen sofort nullen — das ist ein einmaliger Pop, kein Peek. Auch wenn der Agent zwischengespeichert und wiederverwendet wird, können nicht verbrauchte Notes eines vorherigen Turn nicht in den nächsten Turn lecken.
Designmotiv
Warum den Prolog als build_turn_context auslagern? Ursprünglich steckte alles «einmal pro Turn» (stdio-Guard, Retry-Zähler-Reset, User-Message-Desinfektion, Todo-/Nudge-Injektion, System-Prompt-Aufbau, Preflight-Kompression, pre_llm_call-Hooks, externes Memory-Vorabholen, Crash-Recovery-Persistenz) inline am Anfang von run_conversation, rund 130 Zeilen. Als Funktion ausgelagert, bleibt in der Hauptschleife nur «Schleife + Wiederholung»; wer die Hauptschleife liest, wird nicht von Vorbereitungslogik unterbrochen, der Prolog lässt sich separat testen und der Subagent-Pfad kann ihn wiederverwenden.
Warum Notes über einen Seitenpfad der User-Nachricht statt eingespeist in den System-Prompt? Der System-Prompt muss turn-übergreifend byte-stabil sein — ändert er sich, verfällt das zuvor gecachte Prefix und das Prompt-Caching des Providers bricht. Das Gateway zieht veränderliche Inhalte (erstmalige Selbstvorstellung, Sprachkanal-Wechsel, temporärer Hinweis bei Auto-Reset) aus dem System-Prompt heraus und liefert sie über einen api_content-Sidecar der User-Nachricht; so kann jeder Turn neue Fakten tragen, ohne die Cache-Trefferquote zu gefährden.
Dass _compression_made_progress 5 % als Materialitätsschwelle setzt, verhindert endloses Kompressions-Kreisen: Ein Turn, der 220 Nachrichten auf 220 Nachrichten komprimiert, aber die Token von 288k auf 183k senkt, würde nach Zeilenzahl fälschlich als «komprimiert nicht weiter» gewertet und Auto-Reset auslösen.
Grenzen und Fehler
- Multimodale Nachrichten verlieren Notes:
compose_user_api_contentgibt für nicht-stringige InhalteNonezurück; sendet der User in diesem Turn ein Bild, gehen sidecar-Notes lautlos verloren.append_notes_to_multimodal_contentfängt auf, indem es die Notes als Text-Part direkt anhängt — der Preis: Dieser Inhalt wird persistiert und im Transcript gespeichert; das Gateway darf also nur wirklich ablagbare Inhalte hineinlegen. - User-Nachrichten-Index driftet nach Kompression: Kompression fasst alte Nachrichten zu einer Summary zusammen, und die Position der User-Nachricht des aktuellen Turns in
messagesverschiebt sich.reanchor_current_turn_user_idxscannt die Liste neu und findet den Index; ohne Reanchor würde das Zurückfüllen des Werkzeugaufruf-Ergebnisses an eine falsche Stelle geschrieben. - Wenige, aber riesige Nachrichten: Die alte Version von
_should_run_preflight_estimatesah nur auf die Nachrichtenzahl; ein paar gigantische Base64-Bilder lösten nie eine Kompression aus und knallten gegen den Hard-Overflow (#27405). Die neue Version hat zusätzlich einen char-basierten Schätz-Zweig, der auch große Nachrichten trifft. - Stale-Notes-Rest:
consume_gateway_turn_context_notesfängt Schreibfehler mit try/except ab — selbst wenn die Agent-Eigenschaft ein read-only Property ist, bricht ein gescheitertes Nullen nicht die Assembly.
Zusammenfassung
turn_context ist die «Snapshot-Fabrik pro Turn»: Es friert dynamischen Kontext zu einer verschickbaren Struktur ein. Es arbeitet mit der Kontext-Kompression zusammen – sobald die Kompression die Historie umgeschrieben hat, richtet turn_context den Zeiger wieder aus.