跳转至

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 竞态

错误实现:

1. 查询 Agent: Working
2. 建立事件订阅
3. 等到 Idle

若 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 本章结论

  1. API 连接线程做解析和长等待,权威 mutation 回到单写者循环;
  2. EventHub sequence + 短历史消除 probe/subscribe 之间的丢事件窗口;
  3. 事件用于唤醒,re-probe 用于确认当前事实;
  4. wait 必须绑定目标 identity 和状态变化 baseline;
  5. schema、CLI、serde surface 应由自动测试共同锁定。

下一章解释两种经常混淆的扩展:Integration 修改外部 Agent 配置来提高观测质量,Plugin 则通过 Herdr API/command/pane 扩展工作流;它们拥有不同信任和生命周期边界。