turn_context y composición del prompt
Responsabilidad
Antes de cada llamada a la API, ensambla «historial + entrada del usuario de este turno (turn) + contexto (context) adicional del gateway + marcas de compresión (compression)» en los api_messages que se envían al provider. TurnContext es el contenedor del producto de este turno; build_turn_context es la entrada de ensamblaje. También inyecta las notas que el gateway recogió dentro del contenido multimodal.
Archivos clave
class TurnContext:242-268— dataclass del contexto del turnodef build_turn_context:268-400— entrada de ensamblajecompose_user_api_content:44-79— composición del contenido del mensaje de usuariosubstitute_api_content:79-101— sustitución de placeholdersconsume_gateway_turn_context_notes:124-164— consume notas añadidas por el gateway y las inyecta en contenido multimodalreanchor_current_turn_user_idx:164-210— recoloca el índice del mensaje de usuario del turno (corrección tras compresión)estimación de progreso de compresión:190-242—_compression_made_progress/_should_run_preflight_estimateprompt_builder— composición del system prompt (referenciado en esta página)
Flujo de datos
- El bucle principal llama cada turno a
build_turn_context:268, obteniendo unTurnContext. compose_user_api_content(agent/turn_context.py:44) ensambla la entrada cruda del usuario en contenido de API.- Si el gateway ha acumulado notas (de adjuntos, indicaciones de contexto),
consume_gateway_turn_context_notes(agent/turn_context.py:124) las extrae e inyecta víaappend_notes_to_multimodal_content. substitute_api_content(agent/turn_context.py:79) sustituye placeholders;drop_stale_api_contentlimpia contenido caducado.- Si el turno anterior sufrió compresión,
reanchor_current_turn_user_idx(agent/turn_context.py:164) corrige la posición del mensaje de usuario actual en el historial. - Los
api_messagesensamblados entran alsubbucle de reintentos de API:1227.
TurnContext es un dataclass común que agrupa en un solo objeto lo calculado por el prologue (campos clave en agent/turn_context.py:242-268): user_message es el mensaje del turno ya sanitizado; original_user_message conserva el texto crudo para consultas de memoria y logs (los nudge no se inyectan ahí); conversation_history puede quedar a None si el preflight comprimió — la compresión abre una sesión (session) nueva y el history viejo ya no sirve; active_system_prompt es el system prompt cacheado para este turno, que la compresción también puede reconstruir.
La lógica de consumo de las notas del gateway está en 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 ""Tras leerlas, se pone a cero — es un pop de un solo uso, no un peek. Aunque el agent se cachee y se reutilice, las notas no consumidas en un turno no se filtran al siguiente.
Motivo de diseño
¿Por qué se extrajo el prologue a build_turn_context? Antes, todo el trabajo «una vez por turno» (guard de stdio, reset de contadores de retry, sanitización del user message, inyección de todo/nudge, construcción del system prompt, preflight de compresión, hook pre_llm_call, prefetch de memoria externa, persistencia para recovery de crash) estaba inline al principio de run_conversation, casi 130 líneas. Al extraerlo a una función, el bucle principal se queda con «bucle + retry»; leer el bucle ya no te interrumpe con lógica de preparación. El prologue se puede testear por separado y la ruta del subagent lo puede reutilizar.
¿Por qué las notas van por el sidecar del user message y no dentro del system prompt? El system prompt debe ser byte-stable entre turnos — en cuanto cambia, el prefijo cacheado se invalida y el prompt caching del provider se va al garete. El gateway saca del system prompt el contenido volátil (primera autopresentación, cambio de canal de voz, reset automático con prompts temporales) y lo entrega como sidecar del api_content del user message. Así, cada turno puede llevar hechos nuevos sin romper el hit rate de caché.
_compression_made_progress usa 5% como umbral material para evitar que la compresión dé vueltas en vacío durante varios turnos: un turno comprime 220 mensajes a 220 mensajes pero baja los tokens de 288k a 183k; si se juzgara por número de líneas, se reportaría «no progresa» y dispararía un auto-reset.
Límites y fallos
- Mensajes multimodales pierden las notas:
compose_user_api_contentdevuelveNonepara contenido no-string; si el usuario envía una imagen, las notas en sidecar se pierden sin ruido.append_notes_to_multimodal_contentlas añade como text part directamente, a costa de que ese contenido acaba persistido en el transcript — el gateway solo debe poner ahí lo que de verdad necesite quedar en disco. - Deriva del índice del user message tras compresión: la compresión sintetiza los mensajes viejos en un summary; la posición del mensaje del usuario actual dentro de
messagescambia.reanchor_current_turn_user_idxrecorre la lista de nuevo para encontrar el índice; sin ese reanclaje, el rellenado del resultado de la herramienta acabaría en el lugar equivocado. - Pocos mensajes pero enormes: la versión vieja de
_should_run_preflight_estimatesolo miraba el número de mensajes, así que unos cuantos base64 gigantes nunca disparaban la compresión y se iba al hard overflow (#27405). La nueva versión añade una rama de estimación por caracteres para que los mensajes grandes también disparen el umbral. - Notas stale que sobreviven:
consume_gateway_turn_context_notesenvuelve la escritura en try/except — incluso si el atributo del agent es una property read-only, el fallo de puesta a cero no rompe el ensamblaje.
Resumen
turn_context es la «fábrica de instantáneas por turno»: solidifica el contexto dinámico en una estructura enviable. Coordina con la compresión de contexto: tras reescribir el historial, turn_context recoloca los punteros para que vuelvan a apuntar correctamente.