# Proactive Layer 接入代码 — 设计

> Proactive 本身的设计在 [`../proactive/`](../proactive/README.zh.md)。本文只讲那套设计
> 与现有 OpenProgram 代码的交界：复用了哪些机制、事件从哪里发出、gate 挂在哪里、
> 它的强制力实际覆盖到什么程度。有冲突以设计文档为准。

## 1. 代码落点

事件层是 `openprogram/events/bus.py` 的就地升级：`Event`、按事件类型订阅、
进程级单例。taps 加在各个本来就知道「刚才发生了什么」的源文件里，而不是集中到一个
收集器。`openprogram/proactive/` 包装规则层，等第一个规则消费者出现时才建；在那之前
事件层本身已经有用。

事件模型是核心三样加一个开放的 `metadata` 口袋（设计 `event-layer.md` §1）。
`turn` 和 `session` 走 metadata 而不是固定字段，这样没有 turn 概念的源不必硬造一个。

## 2. 复用而非重建的机制

设计里描述的每个角色，代码库里都已经有一份能用的实现：

| 设计中的角色 | 现有机制 | 位置 |
|---|---|---|
| 进程内事件扇出 | `EventBus`——已实现但闲置，dispatcher 和 agent_loop 用直接回调绕过了它 | `openprogram/events/bus.py` |
| gate 的 `ask` 路径 | `ApprovalRegistry` + `_wrap_with_approval`：发起请求、阻塞等待、批准或拒绝，拒绝时回一个 is_error 的 tool result | `openprogram/agent/permissions/approval.py` |
| observer 的 `Prepare` 后台 task | `JobRunner.spawn_job`——ThreadPoolExecutor、状态机、job_status 广播 | `openprogram/agent/job/runner.py` |
| `Inject` 的落地槽位 | 注入 system prompt 的 memory prefetch，以及 steering messages | `openprogram/agent/agent_loop.py` |
| 事件因果、rewind、分支 | session git DAG，节点带 parent_id / caller | `openprogram/context/git/` |
| gate 的强制点 | 所有 chat tool 调用都要过的那一个点 | `agent_loop.py` `_execute_tool_calls` |

## 3. 事件 tap

事件从本来就能察觉到相应事实的位置发出。多数 tap 是把现有回调或内部事件转成规范事件，
而不是新增一套探测：

| Event | 来源 | tap 的性质 |
|---|---|---|
| `user.prompt_submitted` | `dispatcher/__init__.py`，持久化用户消息处 | 加在已有的 chat_ack / chat_response 广播旁；放在持久化分支之外，两条路径都会发 |
| `model.response_started` | `agent_loop.py` 的 AgentEventMessageStart | 转换现有事件 |
| `model.response_completed` | `agent_loop.py` 的 AgentEventMessageEnd | 转换现有事件 |
| `tool.before` | `agent_loop.py`，每次 `tool.execute()` 之前 | 一份事件同时喂 notify emit 与 gate 问询 |
| `tool.after` | `agent_loop.py`，每次工具调用结束之后 | notify emit，附结果文本通道 |
| `subagent.started` / `completed` | `task/runner.py` 的 job_status 广播 | 转换，经 `_broadcast_job_status` 汇总 |
| `permission.requested` | `permissions/approval.py` 的 approval_request 信封 | 新增 tap |
| `artifact.file.changed` | `file_backup.backup_before_edit` 与 `project_commit` | 写成功后新增发送 |

## 4. gate 的挂载点与如实声明的覆盖率

gate 挂在两个地方，两处的保证强度确实不同，这个差别是明说的，不掩盖。

**chat 路径——强制。** gate 串在 `agent_loop.py` 的 `_execute_tool_calls` 里、
`tool.execute` 之前。所有 chat tool 调用都过这一个点，绕不过去。

**agentic 嵌套路径——可选挂载。** `function.py` 的 `_pre_invocation_hooks`，
cancel 检查已经挂在这里。它是一个挂载点而非咽喉点；覆盖率如实声明，不对嵌套调用
宣称全覆盖。

gate 对 subagent turn 生效，且**独立于 `permission_mode`**。特别地，它不会被
`sub_agent_run.py` 里设的 `permission_mode="bypass"` 关掉——那个 bypass 是本设计
要堵的现有漏洞（设计 `invariants.md` 与 `execution-model.md` §2）。

## 5. Prepare 的执行

`Prepare` 复用 `JobRunner.spawn_job`，但注入一个不含 bash、write、network 的受限
tool allowlist。它跑在一个独立小池里，并发 1–2，可被用户任务抢占，遇 429 让路
（设计 `execution-model.md` §3）。

## 6. 一个已知缺口，但不阻塞

`@function` 的 tool 执行不写 DAG 节点，只有 `Agent` method 写，因此 DAG 树作为
因果记录是不完整的。如果审计必须靠 DAG 做因果回溯，就得先补上这一块。本设计改为在
`events.jsonl` 里独立记录全量事件，于是 DAG 的缺口成了一个已知项，而不是前置条件。

## 7. 验证方式

每一处接线改动都按同一套查：`py_compile`、相关单测、仓库规定的本地刷新流程、
`/healthz` 正常，以及经 web UI 发一条真实消息（前端改动要先 `npm --prefix apps/web run build`）。

事件顺序的验证方式是跑一个带工具调用的 turn，读该会话的 `events.jsonl`
（事件日志常开）。日志必须依序出现 `user.prompt_submitted → model.response_started →
tool.before → tool.after → model.response_completed`，且每条的
metadata 里带 session 和 turn。

## 附录：实现状态

总线、源头 tap、外部源桥接、订阅式 web UI，以及 `openprogram/proactive/` 包都已在当前
源码中存在。规则层入口是 `openprogram/proactive/engine.py::install_proactive`，Policy
位于 `openprogram/proactive/policies/`。包和 Policy 已实现，但当前
`openprogram/worker/runner.py` 没有在 worker 启动时调用 `install_proactive()`；worker
启动时安装的是另一个独立的 event bridge。因此 worker 生命周期接线仍是明确的未实现边界。
原五步顺序作为设计历史保留；当前状态以源码路径和测试为准。

已落地的各部分位置：

| 件 | 位置 |
|---|---|
| `Event` / `make_event` / `emit_safe` / `subscribe(types=)` / `get_event_bus` / 事件日志订阅者 | `openprogram/events/bus.py` |
| 同步问询点：`register_tool_gate` / `decide_tool_gate` / `ToolGateDenied` | `openprogram/events/tool_gate.py` |
| `tool.before` 观察与问询、`tool.after`、`model.*` taps | `openprogram/agent/agent_loop.py` |
| `user.prompt_submitted` | `openprogram/agent/dispatcher/__init__.py` |
| `subagent.started` / `ended` | `openprogram/agent/job/runner.py` `_broadcast_job_status` |
| `file.changed`，写成功后经懒 import 发出 | write / edit / apply_patch 三个工具中的五处 |
| 外部源桥，worker 启动时幂等安装 | `openprogram/events/bridges.py` + `worker/runner.py` |
| 外部源头 taps | `context/engine.py`（compaction ×2）、`channels/_conversation.py`、`memory/session_watcher.py`（×2）、`webui/server.py`（skills / plugins） |
| `emit_ws_frame` 透传信封 + `_subscribe_event_bus` 订阅转发 | `openprogram/events/bus.py`、`webui/server.py` |
| 外部源不再 import web UI | `task/runner.py`、`sub_agent_run.py`、`worktree/manager.py`、`functions/watcher.py`、`channels/_broadcast.py` |
| 单测（30 个） | `tests/agent/test_event_bus.py`、`test_tool_gate.py`、`test_event_bridges.py` |

单测覆盖事件总线、tool gate、事件桥和 proactive policies。Installed-App 与实时
WebSocket 验收属于独立检查，本设计记录不宣称已经完成。有一条环境注意事项要带下去：
worker 的工作目录是 home，因此项目 skills 目录解析成 `~/skills`。
