DeepSeek Harness Sitzungsprotokoll und Turn-Lebenszyklus

Haben Sie sich schon einmal gefragt: Wo genau wird der Gesprächsverlauf gespeichert, den das Modell sieht?

dshs Antwort ist recht speziell:Es gibt keinen dedizierten Speicherort; vielmehr wird er aus einem Sitzungsprotokoll abgeleitet.。

In diesem Abschnitt behandeln wir Sitzungsprotokolle und den Turn-Lebenszyklus: wie das Protokoll als einzige Quelle der Wahrheit dient, wie Turns und Schritte unterteilt werden und die Invariante, dass "was das Modell sieht, ist das, was aufgezeichnet wurde."

In einem Satz: Eine Sitzung ist ein Append-only-Ereignisprotokoll, die Modellhistorie wird aus dem Protokoll abgeleitet; Turns und Schritte sind Ausführungsgrenzen im Protokoll.


Konversationsprotokoll: die einzige Quelle der Wahrheit

Sessionist ein typisiertesSessionEventdas resultierende Append-only-Protokoll.

Es ist die einzige Quelle der Wahrheit für die vollständige Interaktionshistorie des Agenten; die LLM-Nachrichtenhistorie wird aus dem Protokoll abgeleitet und nie separat gespeichert.

Erneutes Abspielen bedeutet, die Historie aus derselben Ereignismenge erneut abzuleiten.

Jedes Ereignis im Protokoll hat eine monoton steigendeseq(seq = log.length) und Epoch-Millisekunden vontime。

Ereignisse basieren auftypeum eine echte diskriminierte Union zu bilden, daherswitch (event.type)Lässt sich direkt eingrenzenevent.data, ohne Typzusicherungen.

Alleevent.dataund müssen alle verlustfrei nach JSON serialisierbar sein,Session.appendwird dies an der Quelle erzwingen.

Was für das Modell sichtbar ist, wird bereits aufgezeichnet.Alles, was die Modellanfrage erreicht, muss aus dem Log rekonstruierbar sein, und eine Laufzeit-Invariante stellt dies sicher.

Daher erfordert das Hinzufügen einer modellsichtbaren Eingabe das Hinzufügen eines Session-Events: SessionEventMap erweitern und es aus dem Log rendern.

SDK-Nutzer, die wiedergabefähige Transkriptdaten benötigen, sollten konsumierensession/eventEreignisfluss.


deriveMessages: projiziert den Modellverlauf aus dem Log

Session.deriveMessages()Projiziere das Ereignisprotokoll in das, was das Modell siehtMessage[]。

Es wird zwischengespeichert: Jeder Oberflächenknoten wird einmal projiziert, wenn er zum ersten Mal erscheint, und neu aufgebaut, wenn die Oberfläche neu geschrieben wird.

Es gibt ein eingefrorenes Array von Nachrichten zurück; das Ändern der aufgezeichneten Historie durch Projektion ist auf Typebene nicht ausdrückbar.

Die Projektionsregeln sind einfach:

EreignisseProjiziert alsBeschreibung
user/messageeine BenutzernachrichtTrägt den exakten Inhalt; die optionale Hülle dient nur als Metadaten für die Protokollanzeige.
assistant/messageeine AssistentennachrichtEnthält Anbieter, Modell und optionalen Replay-Zustand.
assistant/chunkskipGehört zu Replay-/UI-Daten; die zusammengesetzte Nachricht ist maßgeblich.
tool/resultEine Benutzernachricht mit einem Tool-Result-BlockTool-Ergebnisse kehren mit der Benutzerrolle zum Modell zurück
turn/*、step/*skipstrukturelle Informationen, nicht als Nachrichten projiziert

Ein Detail: Inhalt, der leer istassistant/messagewird ebenfalls übersprungen.

Durch max-tokens abgeschnittene Schritte ohne Inhalt zeichnen weiterhin einen assistant/message-Eintrag auf, um Nutzung, Anbieter und Modell zu erhalten, aber inhaltslose Assistant-Turns dürfen nicht in das Provider-Transkript gelangen.


Drei Ereignisdomänen

Die Wahl der richtigen Ereignisdomäne ist die erste Entscheidung für die meisten Änderungen.

Die offizielle Dokumentation unterteilt Ereignisse in drei Domänen, jede mit ihrem eigenen Zweck:

EreignisdomäneRepräsentatives EreignisFunktionenWann zu verwenden
Sitzungsereignisturn/start、step/start、user/message、assistant/*、tool/call、tool/resultan das Log angehängt und gebroadcastet, eine persistente TatsacheEine Tatsache, die nach einem Reload noch existieren muss
Agent-Ereignisagent/pre-step、agent/request、agent/status、agent/turn-stoppingTrägt den Live-Agent, Echtzeitsteuerung und Zustand.laufende Arbeit beobachten oder abfangen
Capability-Ereignistools/*、fs/*、llm/streamRichtlinien ohne Importzyklen an Nahtstellen anhängen.Richtlinien und Adapter an Capability-Nahtstellen anhängen.

wobeiagent/pre-step、agent/request、llm/streamund dreitools/*Ereignisse sind ein Wasserfall; Listener müssen aufrufennext()erst dann kann es weiter delegiert werden.

agent/turn-stoppingist ein serielles Ereignis, ohnenext()。


die Definition von Turns und Schritten

aSchrittIst eine Modellanfrage plus die Tools, die er aufruft.

aTurnEnthält null oder mehr Schritte: Er öffnet, bevor die erste Eingabe beansprucht wird, und schließt, wenn keine Arbeit mehr aussteht.

Hinweis: Ein Turn umfasst eine Modellschleifen-Ausführung, nicht das gesamte Sitzungsprotokoll.

Das offizielle Sequenzdiagramm stellt den vollständigen Ablauf wie folgt dar:turn/start → agent/pre-step → step/start → llm/stream → 工具 → step/end → turn/end。

轮次时序图

Eingaben erreichen den Treiber über einen einzigen Posteingang.

Einige Nachrichten wecken den Treiber sofort; injizierter Kontext bleibt im Posteingang, bis eine andere Nachricht ihn weckt.

agent/pre-stepBestimmt, was das Modell sieht: Listener können beanspruchte Nachrichten umschreiben oder sie direkt ablehnen.

Wenn die erste Beanspruchung abgelehnt oder auf leer überschrieben wird, wird ein persistenter Turn ohne Schritte dennoch geschlossen, sodass das Protokoll diesen Versuch aufzeichnet.


Schlüsselereignisse für Turns und Schritte

Jede Grenze im Protokoll hat ein entsprechendes Ereignis; unten stehen die am häufigsten verwendeten.

EreignisseMitgeführte DatenBeschreibung
turn/start{ turn }Öffne einen Turn, bevor die Schleife wartende Eingaben beansprucht oder Vor-Schritte ausführt
turn/end{ turn, reason }mit TurnEndReason Close轮times(completed / aborted / blocked / error / max-tokens / interrupted)
step/start{ turn, step }Einen Schritt innerhalb eines Turns öffnen
step/end{ turn, step }Diesen Schritt schließen
user/messageUserMessageEin getaggter Wert, der von direkten Prompts, injiziertem Kontext, Steuerung und Live-Inbox-Ereignissen gemeinsam genutzt wird.
assistant/chunk{ turn, step, chunk }Rohe Streaming-Chunks, Wiedergabetreue auf Token-Ebene.
assistant/message{ turn, step, message, usage? }Die zusammengesetzte Assistenten-Nachricht (von der abgeleiteten Historie verwendet).
tool/call{ turn, step, callId, name, arguments }Ein Tool-Aufruf aus einer Modellanfrage; arguments ist der vom Modell erzeugte rohe JSON-String.
tool/result{ turn, step, message, error?, meta? }das für das Modell sichtbare Ergebnis eines abgeschlossenen Tool-Aufrufs

Eine nützliche Semantik:assistant/messageDas Ereignis protokolliert jeden erfolgreichen Provider-Aufruf, einschließlich Aufrufen, die leeren Inhalt zurückgeben oder mit max-tokens enden.

Leerer Inhalt geht nicht in die abgeleitete Historie ein, aber das persistierte Ereignis speichert die Nutzung weiterhin.

Es wird durchgereichtsourceEventSeqsdie entsprechenden genau auflistenassistant/chunkEreignisse, einschließlich expliziter leerer Listen.


Praxisbeispiel: Parsen eines JSONL-Sitzungsprotokolls

Nachfolgend ein kurzes Sitzungsprotokoll (Standard-JSONL-Zeilenpackungs-Layout) aus einer Aufgabe, um "einen Tippfehler im Beispiel-Repository zu beheben."

Jede Zeile ist ein SessionEvent, das type, seq, time und data enthält.

{"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 example 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 example 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 example 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 example README.md\"}"}}
{"type":"tool/result","seq":7,"time":1755000000070,"data":{"turn":1,"step":1,"message":{"role":"tool","toolName":"bash","content":"example","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"}}}

Hinweisuser/message、assistant/message、tool/resultDrei Oberflächenereignisse tragensurfaceOpeine Markierung, die angibt, wie sie zur abgeleiteten Oberfläche hinzugefügt werden.

turn/start、step/startGrenzereignisse wie diese tragen kein surfaceOp und werden nicht als Modellnachrichten projiziert.

Nachfolgend spielen wir dieses Protokoll in Python erneut ab, bauen die Konversation neu auf und markieren die Turn-/Schrittgrenzen.

Beispiel

# Dateipfad: examples/parse_session_log.py
# JSONL-Konversationsprotokoll parsen, den modellsichtbaren Dialog rekonstruieren und Turn-/Schrittgrenzen markieren.
# Dies ist Session.deriveMessages() ofa简transformTeach学Model;truerealImplementationYesCachingofandreturn冻结message.
import json
import sys

def derive_messages(events):
    """Nur Oberflächenereignisse projizieren, wobei die Projektionsregeln von deriveMessages simuliert werden.

user/message -> Benutzernachricht
assistant/message -> Assistentennachricht
tool/result -> Benutzernachricht, die einen tool-result-Block enthält
turn/*, step/*, assistant/chunk, tool/call werden nicht als Nachrichten projiziert
    """

    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()]

    # Erster Durchlauf: Ausführungsgrenzen ausgeben, um die verschachtelte Beziehung zwischen Turn und Step zu verstehen.
    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']}")

    # Zweiter Durchlauf: die abgeleitete, vom Modell sichtbare Historie neu aufbauen.
    print("\nVom Modell sichtbare abgeleitete Nachrichten:")
    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])

Wenn man es auf diesem Log ausführt, ist die Ausgabe ungefähr:

[turn/start] turn=1
  [step/start] turn=1 step=1
    chunk: text-delta
    chunk: tool-call-delta
    tool/call: bash args={"command":"grep example README.md"}
  [step/end]   turn=1 step=1
[turn/end]   turn=1 reason={'kind': 'completed'}

模型可见的派生消息:
  [user] [{'type': 'text', 'text': 'Fix the typo in the example README.'}]
  [assistant] [{'type': 'text', 'text': 'I'll search'}, {'type': 'tool_use', ...}]
  [tool] bash: example

originalassistant/chunkwährend der Ableitung übersprungen, die zusammengesetzteassistant/messageist die maßgebliche Instanz.

Das ist genau "Was das Modell sieht, ist das, was aufgezeichnet wurde": Jede Nachricht, die das Modell sieht, kann wörtlich aus diesem Log rekonstruiert werden.


Zusammenfassung Selbsttest

Das Sitzungslog ist die einzige Quelle für den Kontext, den das Modell sieht; Turns und Steps sind Ausführungsgrenzen im Log; die drei Ereignisdomänen helfen dir, den richtigen Erweiterungspunkt zu wählen.

Selbsttestfragen:

ProblemReferenz
Welche Ereignisdomäne solltest du verwenden, um Fakten zu persistieren, die einen Reload überleben müssen?Session-Ereignisse (persistent, an das Log angehängt)
Woher stammt die Historie, die das Modell sieht?Session.deriveMessages()Aus dem Log abgeleitet, niemals separat gespeichert.
Wie viele Schritte kann ein Zug enthalten?Null oder mehr; öffnet, bevor die erste Eingabe beansprucht wird, und schließt, wenn keine Arbeit mehr aussteht.
Weitere Erweiterungen