会话是一份仅追加的事件日志,模型历史从日志派生;轮次与步骤是日志上的执行边界。
会话日志:唯一真源
Session 由类型化 SessionEvent 组成仅追加日志,是 agent 完整交互历史的唯一真源;LLM 消息历史从日志派生,从不单独存储,回放就是重新从同一组事件派生历史。
- 每个事件有单调递增的
seq(seq = log.length)和 epoch 毫秒的time。 - 事件基于
type做真正可辨识联合,switch (event.type)直接收窄event.data。 - 所有
event.data必须能无损序列化为 JSON,Session.append在源头强制。 - 模型可见即已记录:抵达模型请求的一切都必须能从日志重建,由运行时不变量断言。新增一项模型可见输入 → 新增一个会话事件。
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/start、step/start、user/message、assistant/*、tool/call、tool/result | 追加进日志并广播,持久事实 | 某事实必须在重新加载后仍然存在 |
| Agent 事件 | agent/pre-step、agent/request、agent/status、agent/turn-stopping | 携带活跃 Agent,实时控制与状态 | 观察或拦截进行中的工作 |
| 能力事件 | tools/*、fs/*、llm/stream | 无导入循环地向 seam 附加策略 | 给能力 seam 挂策略与适配器 |
agent/pre-step、agent/request、llm/stream和三个tools/*事件是 waterfall,必须调用next()委托;agent/turn-stopping是 serial 事件,没有next()。
轮次与步骤的定义
- 步骤(step):一次模型请求 + 它调用的工具。
- 轮次(turn):包含零个或多个步骤;在领取首条输入之前打开,在不再欠下任何工作时关闭。轮次包围一次模型循环执行,而不是整个会话日志。
- 完整时序:
turn/start → agent/pre-step → step/start → llm/stream → 工具 → step/end → turn/end。 - 输入通过同一个 inbox 到达驱动器;有些消息立即唤醒驱动器,注入的上下文留在 inbox 直到另一条消息唤醒。
agent/pre-step决定模型看到什么:可改写已领取消息或直接拒绝。首次领取被拒绝或被改写为空时,仍关闭一个不含步骤的持久轮次,日志会记录这次尝试。
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/message | UserMessage | 直接提示词、注入上下文、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"}}}
user/message、assistant/message、tool/result三种 surface 事件带surfaceOp标记,说明如何加入派生 surface;turn/step 边界事件不携带 surfaceOp 也不投影为模型消息。
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])
要点
- 重新加载后仍存在的事实用会话事件(持久、追加进日志)。
- 模型历史来自
Session.deriveMessages()从日志派生,从不单独存储。 - 一个轮次可含零个或多个步骤。