事件流 Event Stream
昨天(Day 02)我们认识了裸的 Action/Observation——但它们还要被包成带元数据的"事件"才能在系统里流动。一次会话 = 一条按时间排列的事件流。今天看事件怎么包装、动作和观察靠什么配对、还有哪些辅助事件;理解了"事件流",明天(Day 04)才能看懂运行时里数据到底在传什么。
会话 = 一条事件流
OpenHands 里,一次对话(conversation)就是一条按时间排列的事件(Event)序列。你发的消息、Agent 的动作、环境返回的结果,全都是流里的一个个事件:
cat app.py(附带 thought)因为它带来三个超能力:① 能存能回放——整个任务过程是一串数据,存下来随时能重现、审计"AI 到底干了啥";② 前端只要订阅这条流就能实时展示——来一个事件画一个卡片;③ 能压缩——对话太长时把老事件折叠成摘要(L07)。事件流是 OpenHands 一切能力的载体。
BaseEvent:所有事件的共同三字段
每个事件都继承 BaseEvent(frontend/src/types/v1/core/base/event.ts:10):
export interface BaseEvent {
id: EventID; // 唯一 id(用于配对、去重)
timestamp: string; // ISO 时间字符串(用于排序)
source: SourceType; // 事件来源:agent / user / environment / hook
}
id 让事件能互相引用(观察靠它找到对应动作,L05);timestamp 让事件能排成时间流;source 告诉你"这事件是谁产生的"。⚠️ 小白常误以为 id 是"第几条"的顺序号,其实它是全局唯一标识(像微信每条消息的内部消息号),负责被别人引用和去重;排时间顺序是
timestamp 的活,两者分工不同。事件的四种来源(source)
base/common.ts:56 定义了 4 种来源:
发消息、暂停
动作、思考、系统提示
观察结果、状态、报错
PreToolUse 等
source 快速分流:user 画成你的气泡、agent 画成 AI 的卡片、environment 画成执行结果面板。观察类事件的 source 永远是 environment(因为它来自沙箱运行时),这个约定后面判断事件类型时会用到。ActionEvent:给动作套上"信封"
cat app.py 丢进流里——过一会你回看历史,根本不知道 AI 当时为什么要读这个文件、这是它第几次工具调用、这个动作危不危险。就像收到一张没写寄件人、没写日期的快递单。Day 02 的 Action 只是"要做的事"本身。真正在流里流动的是 ActionEvent——把 Action 连同一堆元数据包起来(events/action-event.ts:11):
export interface ActionEvent<T extends Action = Action> extends BaseEvent {
thought: TextContent[]; // Agent 采取此动作前的"思考"
action: T; // ★ 真正的动作(Day 02 那些)
tool_name: string; // 被调用的工具名
tool_call_id: ToolCallID; // 大模型返回的 tool call id
llm_response_id: EventID; // 同一次 LLM 响应里的并行动作归组
security_risk: SecurityRisk; // ★ 大模型对这个动作的风险评估
}
thought(把 AI 的心理活动和动作绑在一起,前端能显示"AI 在想什么才决定这么做")、tool_call_id(对应大模型的函数调用)、以及关键的 security_risk。rm -rf"或"往数据库写"是高危。系统可以配置成:高危动作执行前先暂停、弹出来问你"确定吗?"(confirmation mode,Day 17 细讲)。这个字段就是这套机制的数据基础。让 AI 自己评估风险 + 人工确认高危动作 = 敢把执行权交给 AI 的底气。ObservationEvent 与 action_id 配对
观察事件(events/observation-event.ts:6)同样包装,且带一个关键字段 action_id:
export interface ObservationEvent<T extends Observation = Observation> extends ObservationBaseEvent {
source: "environment"; // 观察永远来自环境
observation: T; // 真正的观察结果(Day 02)
action_id: EventID; // ★ 这个观察,回应的是哪个 ActionEvent
}
ActionEvent{ id: "abc", action: ExecuteBash("pytest") } → 沙箱跑完 pytest → 环境产出 ObservationEvent{ action_id: "abc", observation: "5 passed" }。前端凭 action_id === "abc" 就知道这条输出属于刚才那条命令。action_id 指向"我是哪个动作的结果"——这是动作与观察配对的钥匙。Agent 发出 id=abc 的 ActionEvent(跑命令),命令执行完,环境产出一个 ObservationEvent,它的 action_id=abc,表示"这是 abc 那个命令的输出"。utils/handle-event-for-ui.ts)有条核心规则:观察到达时,用 action_id 找到之前那张"动作卡片",原地替换它。所以你在界面上看到的是:先出现一张卡片"⏳ 要运行命令 pytest",命令跑完,同一张卡片变成"✅ 命令 pytest 的输出:..."。不是两张卡片,是一张卡片的"状态更新"。action_id 就是让"意图"和"结果"能在 UI 上合体的黏合剂。事件全家族(openhands-event.ts)
所有事件汇成一个联合类型 OpenHandsEvent(openhands-event.ts:25)。三个"主角" + 一堆"配角":
- 主角:
ActionEvent(动作)、ObservationEvent(观察)、MessageEvent(纯聊天消息,user 提问 / assistant 回复)。 - 系统:
SystemPromptEvent(会话开头发一次,携带系统提示 + 所有可用工具的定义)。 - 控制/状态:
ConversationStateUpdateEvent(执行状态:idle/running/paused/finished…)、PauseEvent(用户暂停)。 - 错误:
AgentErrorEvent、ConversationErrorEvent、ServerErrorEvent。 - 进阶:
StreamingDeltaEvent(流式 token)、Condensation*Event(历史压缩)、HookExecutionEvent(钩子)、ACPToolCallEvent(第三方 agent)。
SystemPromptEvent 里的 tools 字段(OpenAI 格式的工具定义)就是"Agent 能用哪些工具"的正式声明——它对应 Day 02 的动作全家福,也对应配置里的 enable_* 开关。会话一开始发这一条,等于告诉大模型"你有这些能力"。两个重要的进阶事件
① StreamingDeltaEvent(流式增量)(streaming-delta-event.ts):大模型是一个字一个字生成的。为了让你看到"打字机效果",每生成一小段就推一个 delta 事件。关键:它不持久化——只用于实时渲染。中途重连的客户端拿不到这些碎片,只会拿到最终完整的 MessageEvent。
② CondensationEvent(历史压缩)(condensation-event.ts):对话越来越长,迟早超出大模型的上下文窗口。压缩事件负责"把一批老事件折叠成一段摘要"——forgotten_event_ids 记录哪些事件被移出上下文、summary 是它们的替代摘要。
大模型能"记住"的内容有上限(上下文窗口,比如 20 万 token)。一个复杂任务可能产生几百个事件,早就超了。如果不处理,要么报错,要么把最早的直接砍掉(丢失重要信息)。压缩 = 把"跑了 50 步的详细过程"浓缩成"前面我做了 A、B、C,结论是 D"的摘要,既省 token 又保留关键信息。配置里
[condenser] 段能选压缩策略(Day 04/14 提)。这是长任务 Agent 的必备能力。今日小结 + 动手
🧠 今天你应该能回答
- "一次会话是一条事件流"意味着什么?带来哪三个超能力?
- BaseEvent 的三个共同字段?事件的四种 source?
- ActionEvent 比裸 Action 多了什么?security_risk 干嘛?
- action_id 怎么把观察和动作配对?前端如何用它?
- StreamingDelta 为什么不持久化?历史压缩解决什么?
✋ 动手:读事件类型
# 1. 所有事件的联合类型
sed -n '25,47p' frontend/src/types/v1/core/openhands-event.ts
# 2. BaseEvent + source
sed -n '10,25p' frontend/src/types/v1/core/base/event.ts
# 3. ActionEvent / ObservationEvent 包装
sed -n '11,72p' frontend/src/types/v1/core/events/action-event.ts
sed -n '6,49p' frontend/src/types/v1/core/events/observation-event.ts
config.template.toml 的关键配置(runtime/agent 工具开关/sandbox 镜像/安全),以及配置如何决定 Agent 的能力。