10 · API、事件与无竞态等待¶
Herdr 的 UI 是一个客户端,CLI、脚本和插件则通过本地 JSON API 操作同一状态。API 的难点不是 JSON 解析,而是:短请求要串行改变权威状态,长等待又不能卡住这个唯一写者;订阅过程中还不能漏掉刚好发生的事件。
10.1 API 与 client protocol 是两条路¶
| 维度 | 本地 API | client wire protocol |
|---|---|---|
| 编码 | newline-delimited JSON | 长度前缀二进制消息 |
| 主要消费者 | CLI、脚本、integrations、plugins | app/terminal/observer client |
| 流量 | 命令、查询、等待、事件 | 高频输入与帧 |
| schema | typed Method + JSON Schema |
Rust wire enums + protocol version |
| socket | session API socket | session client socket |
两者分离避免大 frame 和长事件订阅挤占自动化请求,也让 API 可以用通用工具调试。
10.2 请求形状¶
Request 的外部形状是:
{
"id": "req-42",
"method": "pane.send_text",
"params": {
"pane_id": "7",
"text": "cargo test\n"
}
}
method 通过 serde tagged enum 映射到带类型的 params。固定快照的 Method 有 92 个外部名字。
响应保持请求 id,返回 typed result 或结构化 error code/message。稳定 error code 对脚本比自然语言更重要;CLI 可以把它翻译成友好提示,自动化则按 code 分支。
10.3 方法版图¶
| 领域 | 代表方法 |
|---|---|
| server/session | ping, server.stop, server.live_handoff, session.snapshot |
| workspace | create/list/get/focus/rename/move/close/metadata |
| worktree | list/create/open/remove |
| tab | create/list/get/focus/rename/move/close |
| agent | list/get/read/explain/start/prompt/wait/send_keys |
| pane/layout | split/move/swap/resize/read/input/graphics/layout export/apply |
| events/waits | events.subscribe, events.wait, pane.wait_for_output |
| integration | install/uninstall |
| plugin | link/list/enable/action/log/pane lifecycle |
这说明 API 不是旁路 debug 接口,而是 Herdr 控制平面的正式表面。CLI 大部分只是构造 typed request、连接当前 session API socket、打印结果。
10.4 传输与安全边界¶
src/api/server.rs 监听本地 socket。关键防线包括:
- Unix socket 权限限制为当前用户(0600 语义);
- 初始请求大小上限约 1 MiB;
- 每行解析一个 JSON request;
- app 响应有超时;
- client 断开时取消长 wait/stream;
- session namespace 隔离端点;
- graphics stream 使用单独限制和 file transport 校验。
“只监听本地”不等于无需限制。任何同用户进程或被利用的插件都可能发巨型 JSON、建立永不读取的 stream 或请求危险 filesystem 动作。
10.5 从 API 线程到唯一写者¶
普通请求流程:
sequenceDiagram
participant C as CLI/plugin
participant AS as API connection thread
participant Q as request channel
participant App as App/HeadlessServer loop
participant R as oneshot/std mpsc response
C->>AS: one JSON line
AS->>AS: size + JSON + typed validation
AS->>Q: Request + response sender
App->>Q: drain request
App->>App: resolve IDs + mutate state/runtime
App->>R: typed response
R-->>AS: response
AS-->>C: JSON line
API 连接线程不直接锁住 AppState 修改。它把请求送回协调循环,保持 UI action、PTY event、client input 和 API mutation 的相对顺序清楚。
10.6 为什么长等待不能留在 App 循环¶
agent.wait 可能等待数小时。如果 App handler 同步阻塞:
- PTY output 无人处理;
- Agent 状态永远不会更新到完成条件;
- 形成自我死锁;
- 所有 client 和其他 API 都冻结。
因此 src/api/wait.rs 在连接侧编排等待,只用短 probe 请求查询 App,并从共享 EventHub 观察变化。
10.7 经典 check-then-subscribe 竞态¶
错误实现:
若 Agent 在步骤 1 与 2 之间变 Idle,订阅永远收不到那次事件,调用会错误超时。
Herdr 的 EventHub 为每个事件分配单调 sequence,并保留有界 history。等待者记录“probe 周围的序列边界”,之后读取 events_after(sequence)。
sequenceDiagram
participant W as Wait worker
participant H as EventHub
participant A as App probe
W->>H: current_sequence() = 100
W->>A: get current agent
Note over A,H: 状态变化发布 event 101 也不会丢
A-->>W: Working / identity
W->>H: events_after(100)
H-->>W: event 101
W->>A: re-probe current state
A-->>W: Idle
W-->>W: condition matched
有界 history 让 sequence 不只是通知,还成为“建立订阅时的短期重放日志”。
10.8 为什么事件后还要 re-probe¶
事件 payload 可能:
- 被后续事件超越;
- 只表示“可能相关的东西变了”;
- 来自旧 pane/terminal identity;
- 在 filter 层裁掉部分字段。
因此 wait 把 EventHub 当唤醒/序列证据,把 App probe 当当前事实。相关 event 到达后重新查询,再判断 condition。
这种模式可以概括为:
事件告诉你何时值得看,权威查询告诉你现在是什么。
10.9 Identity 防止 pane 复用误命中¶
等待 pane 7 完成时,用户可能关闭 pane 7,随后创建的新 pane 又获得相似外部引用。等待建立时会捕获更强身份,如 terminal/session identity;后续 probe 用 agent_wait_identity_matches 验证仍是同一对象。
若目标消失或 identity 改变,返回 “not running/target changed”,而不是悄悄开始等待新 Agent。
10.10 agent.wait 的状态条件¶
wait 参数可以表达目标状态集合和 timeout。实现还跟踪 state-change sequence,避免一开始已经是 Idle 时把“等待这次 prompt 完成”立即判成功。
典型 prompt flow:
flowchart TD
P["agent.prompt"] --> Baseline["记录 identity + state change baseline"]
Baseline --> Send["发送 prompt/Enter"]
Send --> Start{"观察到开始工作/状态推进?"}
Start -->|否且超时| Stalled["stalled / start timeout"]
Start -->|是| Wait["等待 until: idle/blocked/..."]
Wait --> Done["返回当前 Agent snapshot"]
这比“发送字符串,然后等当前状态等于 Idle”可靠:Agent 发送前本来就可能 Idle,必须先证明新工作确实开始或 sequence 推进。
10.11 pane.wait_for_output¶
等待输出需要选择:
- source:可见、近期等;
- match:substring/regex 等;
- 是否 strip ANSI;
- 从当前已有内容立即匹配,还是等待未来变化;
- timeout/cancel;
- pane identity 与 content sequence。
PTY output 高频,不能为每个字节发布完整文本 event。实现用 output/content 相关事件作为 probe 提示,再通过短 pane.read 获取权威文本并匹配。
10.12 events.subscribe 与 events.wait¶
固定快照的 EventKind 有 26 种,覆盖:
- workspace 创建/更新/聚焦/关闭;
- worktree 创建/打开/移除;
- tab 创建/移动/聚焦/关闭;
- pane 创建/更新/移动/退出/Agent 变化;
- layout 等其他状态变化。
订阅可参数化过滤特定 pane/status 等。setup 本身也要处理 race:记录 sequence、probe 初始状态、回看期间事件,并避免把 probe 已反映的同一变化重复发给客户端。
10.13 EventHub 的边界¶
EventHub 是有界内存历史,不是 durable event log:
- server 重启后 sequence/history 不承诺续接;
- 慢订阅者不能要求无限回放;
- 事件 payload 不是恢复全部 state 的来源;
- client 应能在 gap/重连时重新 snapshot/probe。
这与 terminal frame 的“最新状态优先”相似。若业务需要审计日志,应另建持久 log,而不是无限扩大 EventHub。
10.14 Plugin event hooks 为什么排除高频事件¶
Plugin event hook 会启动外部命令。若允许每次 scroll/output/update 都 spawn:
- 一个活跃 Agent 可制造进程风暴;
- event loop 被日志和 child 管理拖垮;
- 插件本身形成反馈循环。
因此插件只允许适合自动化的较低频事件集合,高频细粒度事件仍可供受控 API stream/subscribe 使用。第 11 章继续说明 plugin command 的隔离。
10.15 JSON Schema 与协议守卫¶
API types 通过 schemars 生成/校验 schema,固定产物在 docs/next/api/herdr-api.schema.json。维护测试用于发现:
- Rust enum 新增方法但 schema 未更新;
- CLI surface 与 Method 不一致;
- serde rename 漂移;
- 参数 default/optional 改变;
- error/result shape 非兼容变化。
checked-in schema 既是文档输入,也是跨语言插件/客户端可消费的契约。
10.16 失败语义¶
| 情况 | 正确响应 |
|---|---|
| JSON/方法/参数无效 | 带 request id(若可得)的结构化 parse/validation error |
| App 响应超时 | 明确 server timeout,不让连接永久挂起 |
| wait client 断开 | 取消 worker,释放订阅和 stream |
| 目标 pane 消失 | target not running/identity changed |
| wait timeout | 返回最后观察 snapshot 和 timeout kind |
| EventHub history gap | re-probe/snapshot,而非假设无事件 |
| API socket 权限错误 | 保留 session/path 诊断,不降级到错误 daemon |
10.17 本章结论¶
- API 连接线程做解析和长等待,权威 mutation 回到单写者循环;
- EventHub sequence + 短历史消除 probe/subscribe 之间的丢事件窗口;
- 事件用于唤醒,re-probe 用于确认当前事实;
- wait 必须绑定目标 identity 和状态变化 baseline;
- schema、CLI、serde surface 应由自动测试共同锁定。
下一章解释两种经常混淆的扩展:Integration 修改外部 Agent 配置来提高观测质量,Plugin 则通过 Herdr API/command/pane 扩展工作流;它们拥有不同信任和生命周期边界。