一条统一的事件流,
给整个框架用。
框架里"某件事发生了"的信号,现在散在六套互不相通的机制里。这层把它们统一成一条总线:源往里 emit,消费者从里 subscribe。proactive 只是它的第一个消费者。
✅ 已落地(迁移步 1·2):总线 · A 类 taps · file.changed · tool gate | B 类桥接 = 步 3 待做 | 观察:OPENPROGRAM_EVENT_LOG=1
架构总览
所有组件都在同一个 worker 进程里(各是 daemon 线程),所以总线就是一个进程级单例。看事件怎么流:
Event 模型:核心三样 + 开放口袋
核心(是什么事 + 内容 + 时间)固定;关联信息放进开放的 metadata 口袋,不写死成字段。
它们不是事件的内在属性,是外加的关联——对一半事件(auth、channel)根本没意义,做成字段就得靠"可空"打补丁。成熟事件系统(DOM / 日志 / MQ / 追踪)都这个形状:核心固定,关联进 labels / headers。
框架里没有 Turn 对象,它就是 assistant 消息的 id,靠 ContextVar(
_current_turn_id)传着。agent 事件 emit 时它有值就顺手塞进口袋;auth / channel 事件 emit 时它是空的,口袋里自然没有 turn。两类事件源
容易只盯着 A 类,但 agent loop 之外的 B 类对主动性常常更重要。两类进同一条总线,metadata 口袋天然装得下。
agent 活动事件
系统状态事件
事件类型(第一版)
精挑的"有消费者想响应"的时机,不是框架所有动作的清单。以后加新类型是纯加法(见 07)。
| TYPE | 何时 | 现有代码来源 | |
|---|---|---|---|
| A | user.prompt_submitted | 用户发消息 | dispatcher 入口 |
| A | model.response_started / .completed | 模型开始 / 说完回复 | AgentEventMessageStart/End |
| A | tool.before | 工具即将执行(可拦截) | AgentEventToolStart |
| A | tool.after | 工具执行完 | AgentEventToolEnd |
| A | file.changed新增 | 文件被改 | 挂 backup_for_current_turn |
| A | turn.ended | 一轮结束 | AgentEventTurnEnd |
| A | subagent.started / .ended | 子任务起止 | TaskRunner 广播 |
| B | credential.cooldown / .exhausted / .rotated | 凭据限流 / 池耗尽 / 轮换 | AuthStore._emit |
| B | context.compaction_recommended / .compacted | 上下文到阈值 / 已压缩 | context/engine.py |
| B | channel.message_inbound | 外部消息进来 | channels/_broadcast.py |
| B | skills.changed / plugins.update_available | 技能改 / 插件有新版 | webui watcher |
总线:一个进程级单例
webui、agent loop、channels、memory、auth、task runner 全在同一个 worker 进程里。总线照框架已有的 get_store() / get_runner() 双检锁先例做单例,复用闲置的 agent/event_bus.py。不需要跨进程桥接。
def get_event_bus() -> EventBus: …
class EventBus:
def emit(self, event: Event): …
# 广播,fire-and-forget,不阻塞调用方
def subscribe(self, handler, *, types=None): …
# 按事件类型订阅,只收关心的那几类
观察 vs 拦截
观察型
默认 · 异步emit 出去,订阅者异步收到,事件源不等。
- 所有事件都走这条
- 订阅者再慢也不拖慢框架
拦截型
仅 tool.before · 同步工具执行前,下游可以说"别执行"。挂在工具单一入口 _execute_tool_calls 的 tool.execute() 之前。
- 必须快——不许调 LLM
- 多方表态取最严:拦下 > 确认 > 放行,"确认"复用现有 _approval
- 对 subagent 也生效——独立于 permission_mode=bypass,否则危险动作塞进子任务就溜了
两条要记住的原则
有消费者想响应,才是事件
不是所有调用都是事件。类型表是精挑的:工具要执行、凭据被限流——是事件;agent 内部一次列表拼接——不是。事件流变成什么都往里倒的垃圾场,是这类系统最常见的腐烂方式。
演进只加不改
源和消费者互不认识,所以加事件只动 emit 那一处:框架内函数加一行,一类动作在公共入口加一次。加类型、给 payload 加字段都零风险;改老结构才会伤老订阅者——这也是 payload / metadata 用开放 dict 的理由。