DeepSeek Harness: registros de sesión y ciclo de vida de turnos
¿Te has preguntado dónde se almacena exactamente el historial de conversación que ve el modelo?
La respuesta de dsh es muy especial:No hay un lugar dedicado; se deriva de un registro de sesión.。
En este capítulo hablaremos del registro de sesión y del ciclo de vida de turnos: cómo el registro actúa como única fuente de verdad, cómo se dividen turnos y pasos, y el invariante de que «todo lo visible para el modelo ya está registrado».
En una frase: la sesión es un registro de eventos de solo apéndice (append-only), el historial del modelo se deriva del registro; los turnos y pasos son límites de ejecución sobre el registro.
Registro de sesión: única fuente de verdad
Sessiones un registro de solo apéndice compuesto porSessionEventeventos tipados.
Es la única fuente de verdad del historial de interacción completo del agente; el historial de mensajes del LLM se deriva del registro y nunca se almacena por separado.
Replay consiste en volver a derivar el historial a partir del mismo conjunto de eventos.
Cada evento en el registro tiene un valor monótonamente creciente (seq(seq = log.length) y una marca de tiempo epoch en milisegundostime。
Los eventos se basan entypepara formar una unión discriminada real, por lo queswitch (event.type)se puede estrechar directamenteevent.data, sin necesidad de aserciones de tipo.
Todosevent.datadeben poder serializarse sin pérdida a JSON,Session.appendy esto se aplica en el origen.
Lo visible para el modelo ya está registrado.Todo lo que llega a la solicitud del modelo debe poder reconstruirse desde el registro, y un invariante en tiempo de ejecución lo garantiza.
Por lo tanto, añadir una entrada visible para el modelo requiere añadir un evento de sesión: extender SessionEventMap y renderizarlo desde el registro.
Los usuarios del SDK que necesiten datos de transcripción (transcript) reproducibles deben consumirsession/eventel flujo de eventos.
deriveMessages: proyección del historial del modelo desde el registro
Session.deriveMessages()Proyecta el registro de eventos en lo que el modelo veMessage[]。
Está cacheado: cada nodo de surface se proyecta una vez en su primera aparición y se reconstruye cuando la surface se reescribe.
Devuelve un array de mensajes congelado; modificar el historial ya registrado mediante proyección no es expresable a nivel de tipos.
Las reglas de proyección son directas:
| Evento | Se proyecta como | Descripción |
|---|---|---|
user/message | Un mensaje user | Lleva el content exacto; el envelope opcional solo sirve como metadatos de visualización del registro |
assistant/message | Un mensaje assistant | Incluye proveedor, modelo y estado de replay opcional |
assistant/chunk | Se omite | Pertenece a datos de replay/UI; el mensaje ensamblado es la autoridad |
tool/result | Un mensaje user con bloque tool-result | El resultado de la herramienta vuelve al modelo con rol user |
turn/*、step/* | Se omite | Información estructural, no se proyecta como mensaje |
Un detalle: los de contenido vacíoassistant/messagetambién se omiten.
Los pasos truncados por max-tokens y sin contenido aún registran un assistant/message para guardar uso, proveedor y modelo, pero los turnos assistant sin contenido no deben entrar en el transcript del proveedor.
Tres dominios de eventos
Elegir el dominio de eventos correcto es la primera decisión en la mayoría de los cambios.
La documentación oficial divide los eventos en tres dominios, cada uno con su propósito:
| Dominio de evento | Eventos representativos | Características | Cuándo usarlo |
|---|---|---|---|
| Evento de sesión | turn/start、step/start、user/message、assistant/*、tool/call、tool/result | Se agrega al registro y se transmite; hecho persistente | Cuando un hecho debe seguir existiendo después de recargar |
| Evento de agente | agent/pre-step、agent/request、agent/status、agent/turn-stopping | Lleva el agente activo, control y estado en tiempo real | Observar o interceptar trabajo en curso |
| Evento de capacidad | tools/*、fs/*、llm/stream | Adjuntar políticas al seam sin ciclos de importación | Colgar políticas y adaptadores en el seam de capacidad |
Entre ellosagent/pre-step、agent/request、llm/streamy trestools/*eventos son waterfall; los listeners deben llamarnext()para poder delegar.
agent/turn-stoppingson eventos serial, sinnext()。
Definición de turno y paso
Unpaso (step)es una solicitud al modelo más las herramientas que llama.
Unturno (turn)contiene cero o más pasos: se abre antes de reclamar la primera entrada y se cierra cuando no queda ningún trabajo pendiente.
Nota: el turno envuelve una ejecución del bucle del modelo, no todo el registro de sesión.
El diagrama de secuencia oficial dibuja el flujo completo así:turn/start → agent/pre-step → step/start → llm/stream → 工具 → step/end → turn/end。
Las entradas llegan al driver a través del mismo inbox.
Algunos mensajes despiertan inmediatamente al driver; el contexto inyectado permanece en el inbox hasta que otro mensaje lo despierte.
agent/pre-stepDecidir qué ve el modelo: los listeners pueden reescribir los mensajes reclamados o rechazarlos directamente.
Cuando la primera reclamación es rechazada o reescrita como vacía, se cierra un turno persistente sin pasos, por lo que el registro guarda el intento.
Eventos clave de turn y step
Cada límite en el registro tiene su evento correspondiente; a continuación se listan los más comunes.
| Evento | Datos que lleva | Descripción |
|---|---|---|
turn/start | { turn } | Abre el turno antes de que el loop reclame entradas en cola o ejecute pre-step |
turn/end | { turn, reason } | Cierra el turno con TurnEndReason (completed / aborted / blocked / error / max-tokens / interrupted) |
step/start | { turn, step } | Abre un paso dentro de un turno |
step/end | { turn, step } | Cierra ese paso |
user/message | UserMessage | Valor identificador compartido por prompt directo, contexto inyectado, steering y eventos de inbox en tiempo real |
assistant/chunk | { turn, step, chunk } | Fragmentos de streaming en bruto, fidelidad de replay a nivel de token |
assistant/message | { turn, step, message, usage? } | Mensaje assistant ensamblado (lo usa el historial derivado) |
tool/call | { turn, step, callId, name, arguments } | Una llamada a herramienta solicitada por el modelo; arguments es la cadena JSON en bruto producida por el modelo |
tool/result | { turn, step, message, error?, meta? } | Resultado visible para el modelo de una llamada a herramienta completada |
Una semántica útil:assistant/messageEl evento registra cada llamada exitosa al proveedor, incluidas las que devuelven contenido vacío o terminan por max-tokens.
El contenido vacío no entra en el historial derivado, pero el evento persistente aún conserva el uso.
A través desourceEventSeqsenumera con precisión los correspondientesassistant/chunkeventos, incluida la lista vacía explícita.
Ejemplo práctico: analizar un registro de sesión JSONL
A continuación se muestra un pequeño registro de sesión (disposición de líneas empaquetadas estándar de JSONL), de una tarea de «corregir un typo en el repositorio ejemplo».
Cada línea es un SessionEvent que contiene type, seq, time y data.
{"type":"turn/start","seq":0,"time":1755000000000,"data":{"turn":1}}
{"type":"step/start","seq":1,"time":1755000000010,"data":{"turn":1,"step":1}}
{"type":"user/message","seq":2,"time":1755000000020,"data":{"role":"user","content":[{"type":"text","text":"Fix the typo in the ejemplo README."}]},"surfaceOp":"append","sourceEventSeqs":[0]}
{"type":"assistant/chunk","seq":3,"time":1755000000030,"data":{"turn":1,"step":1,"chunk":{"type":"text-delta","text":"I'll "}}}
{"type":"assistant/chunk","seq":4,"time":1755000000040,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","name":"bash","arguments":"{\"command\":\"grep ejemplo README.md\"}"}}}
{"type":"assistant/message","seq":5,"time":1755000000050,"data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"I'll search"},{"type":"tool_use","id":"call_1","name":"bash","input":{"command":"grep ejemplo README.md"}}]},"usage":{"inputTokens":12,"outputTokens":4}},"surfaceOp":"append","sourceEventSeqs":[3,4]}
{"type":"tool/call","seq":6,"time":1755000000060,"data":{"turn":1,"step":1,"callId":"call_1","name":"bash","arguments":"{\"command\":\"grep ejemplo README.md\"}"}}
{"type":"tool/result","seq":7,"time":1755000000070,"data":{"turn":1,"step":1,"message":{"role":"tool","toolName":"bash","content":"ejemplo","isError":false}},"surfaceOp":"append","sourceEventSeqs":[6]}
{"type":"step/end","seq":8,"time":1755000000080,"data":{"turn":1,"step":1}}
{"type":"turn/end","seq":9,"time":1755000000090,"data":{"turn":1,"reason":{"kind":"completed"}}}
Observa queuser/message、assistant/message、tool/resultlos tres eventos de surface llevansurfaceOpmarcadores que indican cómo se incorporan a la surface derivada.
turn/start、step/startLos eventos de límite no llevan surfaceOp y no se proyectan como mensajes del modelo.
A continuación se reproduce este registro con Python, reconstruyendo la conversación y marcando los límites de turn/step.
Ejemplo
# Analiza un registro de sesión JSONL, reconstruye el diálogo visible para el modelo y marca los límites de turno/paso.
# Este es un modelo didáctico simplificado de Session.deriveMessages(); la implementación real está en caché y devuelve mensajes congelados.
import json
import sys
def derive_messages(events):
"""Solo proyecta eventos de surface, simulando las reglas de proyección de deriveMessages.
user/message -> mensaje de user
assistant/message -> mensaje de assistant
tool/result -> mensaje de user con bloque tool-result
turn/*, step/*, assistant/chunk, tool/call no se proyectan como mensajes
"""
messages = []
for ev in events:
t = ev["type"]
d = ev["data"]
if t == "user/message":
messages.append({"role": "user", "content": d["content"]})
elif t == "assistant/message":
messages.append({"role": "assistant", "content": d["message"]["content"]})
elif t == "tool/result":
messages.append({"role": "tool", "name": d["message"]["toolName"], "content": d["message"]["content"]})
return messages
def main(path):
with open(path, encoding="utf-8") as f:
events = [json.loads(line) for line in f if line.strip()]
# Primera pasada: imprimir los límites de ejecución, comprender la relación de anidación entre turn y step.
for ev in events:
d = ev["data"]
if ev["type"] == "turn/start":
print(f"[turn/start] turn={d['turn']}")
elif ev["type"] == "turn/end":
print(f"[turn/end] turn={d['turn']} reason={d['reason']}")
elif ev["type"] == "step/start":
print(f" [step/start] turn={d['turn']} step={d['step']}")
elif ev["type"] == "step/end":
print(f" [step/end] turn={d['turn']} step={d['step']}")
elif ev["type"] == "assistant/chunk":
print(f" chunk: {d['chunk']['type']}")
elif ev["type"] == "tool/call":
print(f" tool/call: {d['name']} args={d['arguments']}")
# Segunda pasada: reconstruir el historial derivado visible para el modelo.
print("\nMensajes derivados visibles para el modelo: ")
for m in derive_messages(events):
if m["role"] == "tool":
print(f" [tool] {m['name']}: {m['content']}")
else:
print(f" [{m['role']}] {m['content']}")
if __name__ == "__main__":
main(sys.argv[1])
Ejecuta esto con el registro, la salida es aproximadamente:
[turn/start] turn=1
[step/start] turn=1 step=1
chunk: text-delta
chunk: tool-call-delta
tool/call: bash args={"command":"grep ejemplo README.md"}
[step/end] turn=1 step=1
[turn/end] turn=1 reason={'kind': 'completed'}
模型可见的派生消息:
[user] [{'type': 'text', 'text': 'Fix the typo in the ejemplo README.'}]
[assistant] [{'type': 'text', 'text': 'I'll search'}, {'type': 'tool_use', ...}]
[tool] bash: ejemplo
Original
assistant/chunkSe omite en la derivación; el ensambladoassistant/messagees la autoridad.Esto es «lo que el modelo ve ya está registrado»: cada mensaje que el modelo ve puede reconstruirse tal cual a partir de este registro.
Resumen y autoevaluación
El registro de la sesión es la única fuente del contexto visto por el modelo; los turnos y pasos son los límites de ejecución en el registro; los tres dominios de eventos te ayudan a elegir el punto de extensión correcto.
Preguntas de autoevaluación:
| Pregunta | Referencia |
|---|---|
| ¿Qué tipo de evento se usa para guardar un hecho que «sigue existiendo después de recargar»? | Evento de sesión (persistente, se añade al registro) |
| ¿De dónde proviene el historial que ve el modelo? | Session.deriveMessages()Se deriva del registro, nunca se almacena por separado |
| ¿Cuántos pasos puede contener un turno? | Cero o más; se abre antes de tomar la primera entrada y se cierra cuando no hay trabajo pendiente. |