DeepSeek Harness 会话日志与轮次生命周期

2026-09-08 00:00    #AI   #工具   #DeepSeek  

会话是一份仅追加的事件日志,模型历史从日志派生;轮次与步骤是日志上的执行边界。

会话日志:唯一真源

Session 由类型化 SessionEvent 组成仅追加日志,是 agent 完整交互历史的唯一真源;LLM 消息历史从日志派生,从不单独存储,回放就是重新从同一组事件派生历史。

deriveMessages:从日志投影模型历史

Session.deriveMessages() 把事件日志投影成模型看到的 Message[]。缓存式:每个 surface 节点首次出现时投影一次,surface 重写时重建。

事件投影为说明
user/message一条 user 消息携带确切 content,可选 envelope 只作日志展示元数据
assistant/message一条 assistant 消息包含提供方、模型与可选回放状态
assistant/chunk跳过属于回放/UI 数据,组装后的消息才是权威
tool/result一条带 tool-result 块的 user 消息工具结果以 user 角色回到模型
turn/*step/*跳过结构信息,不投影为消息

内容为空的 assistant/message 也会被跳过:因 max-tokens 截断且无内容的步骤仍记录一条 assistant/message 保存用量/提供方/模型,但无内容的 assistant 轮次不得进入提供方 transcript。

事件三域

事件域代表事件特性什么时候用
会话事件turn/startstep/startuser/messageassistant/*tool/calltool/result追加进日志并广播,持久事实某事实必须在重新加载后仍然存在
Agent 事件agent/pre-stepagent/requestagent/statusagent/turn-stopping携带活跃 Agent,实时控制与状态观察或拦截进行中的工作
能力事件tools/*fs/*llm/stream无导入循环地向 seam 附加策略给能力 seam 挂策略与适配器

轮次与步骤的定义

turn 与 step 的关键事件

事件携带的数据说明
turn/start{ turn }在 loop 认领排队输入或运行 pre-step 之前打开轮次
turn/end{ turn, reason }以 TurnEndReason 关闭轮次(completed / aborted / blocked / error / max-tokens / interrupted)
step/start{ turn, step }打开某轮次里的一个步骤
step/end{ turn, step }关闭该步骤
user/messageUserMessage直接提示词、注入上下文、steering 与实时收件箱事件共享的带标识值
assistant/chunk{ turn, step, chunk }原始流式分片,token 级回放保真
assistant/message{ turn, step, message, usage? }组装后的 assistant 消息(派生历史用它)
tool/call{ turn, step, callId, name, arguments }模型请求的工具调用
tool/result{ turn, step, message, error?, meta? }一次完成的工具调用的模型可见结果

assistant/message 记录每次成功的提供方调用(含空内容或 max-tokens 结束);空内容不进派生历史但保留用量。它通过 sourceEventSeqs 精确列出对应的 assistant/chunk 事件。

动手示例:解析一份 JSONL 会话日志

日志(“修复 runoob 仓库 typo"任务):

 1{"type":"turn/start","seq":0,"time":1755000000000,"data":{"turn":1}}
 2{"type":"step/start","seq":1,"time":1755000000010,"data":{"turn":1,"step":1}}
 3{"type":"user/message","seq":2,"time":1755000000020,"data":{"role":"user","content":[{"type":"text","text":"Fix the typo in the runoob README."}]},"surfaceOp":"append","sourceEventSeqs":[0]}
 4{"type":"assistant/chunk","seq":3,"time":1755000000030,"data":{"turn":1,"step":1,"chunk":{"type":"text-delta","text":"I'll "}}}
 5{"type":"assistant/chunk","seq":4,"time":1755000000040,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","name":"bash","arguments":"{\"command\":\"grep runoob README.md\"}"}}}
 6{"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 runoob README.md"}}]},"usage":{"inputTokens":12,"outputTokens":4}},"surfaceOp":"append","sourceEventSeqs":[3,4]}
 7{"type":"tool/call","seq":6,"time":1755000000060,"data":{"turn":1,"step":1,"callId":"call_1","name":"bash","arguments":"{\"command\":\"grep runoob README.md\"}"}}
 8{"type":"tool/result","seq":7,"time":1755000000070,"data":{"turn":1,"step":1,"message":{"role":"tool","toolName":"bash","content":"runoob","isError":false}},"surfaceOp":"append","sourceEventSeqs":[6]}
 9{"type":"step/end","seq":8,"time":1755000000080,"data":{"turn":1,"step":1}}
10{"type":"turn/end","seq":9,"time":1755000000090,"data":{"turn":1,"reason":{"kind":"completed"}}}

Python 重放脚本 examples/parse_session_log.py

 1import json
 2import sys
 3
 4def derive_messages(events):
 5    """只投影 surface 事件,模拟 deriveMessages 的投影规则。"""
 6    messages = []
 7    for ev in events:
 8        t = ev["type"]
 9        d = ev["data"]
10        if t == "user/message":
11            messages.append({"role": "user", "content": d["content"]})
12        elif t == "assistant/message":
13            messages.append({"role": "assistant", "content": d["message"]["content"]})
14        elif t == "tool/result":
15            messages.append({"role": "tool", "name": d["message"]["toolName"], "content": d["message"]["content"]})
16    return messages
17
18def main(path):
19    with open(path, encoding="utf-8") as f:
20        events = [json.loads(line) for line in f if line.strip()]
21
22    # 第一遍:打印执行边界,理解 turn 与 step 的嵌套关系。
23    for ev in events:
24        d = ev["data"]
25        if ev["type"] == "turn/start":
26            print(f"[turn/start] turn={d['turn']}")
27        elif ev["type"] == "turn/end":
28            print(f"[turn/end] turn={d['turn']} reason={d['reason']}")
29        elif ev["type"] == "step/start":
30            print(f" [step/start] turn={d['turn']} step={d['step']}")
31        elif ev["type"] == "step/end":
32            print(f" [step/end] turn={d['turn']} step={d['step']}")
33        elif ev["type"] == "assistant/chunk":
34            print(f" chunk: {d['chunk']['type']}")
35        elif ev["type"] == "tool/call":
36            print(f" tool/call: {d['name']} args={d['arguments']}")
37
38    # 第二遍:重建模型可见的派生历史。
39    print("\n模型可见的派生消息:")
40    for m in derive_messages(events):
41        if m["role"] == "tool":
42            print(f" [tool] {m['name']}: {m['content']}")
43        else:
44            print(f" [{m['role']}] {m['content']}")
45
46if __name__ == "__main__":
47    main(sys.argv[1])

要点

  1. 重新加载后仍存在的事实用会话事件(持久、追加进日志)。
  2. 模型历史来自 Session.deriveMessages() 从日志派生,从不单独存储。
  3. 一个轮次可含零个或多个步骤。