跳至正文
浏览章节

Schema 与协议

两份已提交的 JSON Schema、它们所带的版本,以及 bingo 会说的三种连线格式。

两份 schema,都是生成的

bingo 在仓库根部提交了两份 JSON Schema 文档。两份都由 schemars 从 sdk 自己的类型生成, 所以 schema 就是代码,而不是对代码的一段描述。

文件标题协议它描述什么
schema/rpc.jsonbingo rpc1bingo serve 所说的那个 JSON-RPC 界面
schema/plugin.jsonbingo plugin5一个跨进程插件所说的那条线

每一份都为每个连线类型持有 $defs——rpc.json 里 81 个,plugin.json 里 110 个——外加 一张 methods 表和一张 notifications 表,它们的条目是指向那些定义的 $ref。 plugin.json 还带着一个 manifest 引用和一个 hostService 块。

有两个测试让它们保持诚实。一个重新生成文档,一有差异就失败,并说出更新它的那条命令:

BINGO_UPDATE_SCHEMA=1 cargo test -p bingo-surface-rpc
BINGO_UPDATE_SCHEMA=1 cargo test -p bingo-plugin-rpc

另一个断言每个属性名都是 camelCase,而这正是日志、RPC 界面和插件连线三者一致同意的 写法。

当你要对着这两条线中的任何一条写东西时,读 schema,不要读这一页。下面这些是给你定位 方向用的。

RPC 界面

JSON-RPC 2.0,一行一条消息,UTF-8,走 stdin 和 stdout(ADR-0007)。

方法表与内核的宿主 API 一一对应,另加一次握手。initialize 必须最先来——别的都会得到 NOT_INITIALIZED。

方法用途
initialize握手:进去的是客户端身份和协议,出来的是名字、版本和能力
shutdown结束这个服务器
session/list匹配某个过滤条件的会话
session/open附着:返回这个会话和一份快照
session/close解除附着;会话继续跑
session/delete删掉它
session/history给一段长对话记录翻页
session/events重新同步:某个 since 之后的帧会被重发
session/submit一次输入
session/interrupt停下一个回合
session/answer回答一个打开着的交互
session/deliver投递到另一个会话的队列里
session/extend发布持久的插件状态
session/signal发布瞬时的插件状态
catalog/readmodels、providers、tools、commands、skills、plugins 之一
gateway/subscribe网关层面的事件

两个通知:event,原样携带 sdk 序列化出来的一个 Frame;以及 gateway/event。

写客户端之前有三条性质值得知道:

  • 事件是原样的。 客户端用 SessionState::apply 折叠它们——就是内核所跑的那个 reducer——并导出自己的视图。session/open 的响应是 seq 为 N 的一份快照,它会写在该 会话任何 seq 大于 N 的 event 之前;同一个会话的帧按 seq 顺序到达;而 Lagged { from, to } 的意思是“带着你的 seq 调用 session/events”。
  • 写操作返回 {}。 submit、interrupt 和 answer 在邮箱收下它们的那一刻就作答。 结果作为 IntentAck 事件到达,它的 intent 就是客户端生成的那个 ULID,那也是幂等键。
  • 错误分两层。 协议层面的毛病用 JSON-RPC 自己的码——-32700 解析、-32600 无效 请求、-32601 未知方法、-32602 无效参数——而每一个内核错误都是 -32000,它那个 稳定的字符串走 error.data.code,它的文字是 error.message:
{"jsonrpc":"2.0","id":3,"error":{"code":-32000,"message":"no such session",
 "data":{"code":"SESSION_NOT_FOUND"}}}

一个 stdio 服务器服务一个客户端。同一个会话上的两个进程会被存储的锁拒绝。一个服务器 的并发客户端会随 WebSocket 传输一起到来,那会承载同样的字节。

插件连线

同样是 NDJSON 上的 JSON-RPC 2.0,同样是 sdk 自己的类型作为 JSON——桥只加信封,从不加 形状。

九个方法:initialize、tool/call、command/run、command/complete、 context/contribute、compactor/compact、provider/stream、hook/decide,以及 service/call——那个双向旅行的。五个通知:tool/progress、tool/cancel、 provider/delta、provider/cancel、hook/observe。

PROTOCOL 版本是 5,在握手里发出并被回显。宿主不会说的大版本会被拒绝并给出一条 通知,而不是去猜。方法的数目不再是一个字面量了——它是从已提交的 schema 推出来的一个钉 子,所以开一项新能力意味着重新生成那份文档并抬高协议版本。

schema 里的 hostService 点明桥所保留的那一个服务 bingo.host,以及它背后的两个方法。 见连线服务。

stream-json 信封

--print --output-format stream-json 是一个有损的兼容编码器,不是第三套协议 (ADR-0007 §8)。它把同一批帧投影到 Claude Code 的方言上,好让一个已经会说那套方言的 宿主不用插件就能驱动 bingo。内核不知道它存在。

行的种类:先是 subtype 为 init 的 system,然后是回合运行期间的 assistant 和 user 行,最后是一行 result。

{"type":"system","subtype":"init","session_id":"…","cwd":"…","tools":[…],
 "model":"…","permissionMode":"default","apiKeySource":"none"}
{"type":"result","subtype":"success","is_error":false,"duration_ms":8412,
 "duration_api_ms":0,"num_turns":3,"result":"…","session_id":"…",
 "total_cost_usd":0.0,"usage":{…}}

result 只存在于成功那一支;出错的那几支在它的位置上带着 errors。bingo 填不了的 字段是略去而不是编造——uuid、mcp_servers、slash_commands、modelUsage、 permission_denials,以及 result 行的 stop_reason。写出来但恒定不变的东西写得 诚实:apiKeySource 是 none,total_cost_usd 是 0.0,因为这里没有任何东西给一个 回合定价;duration_api_ms 是 0,因为只有整个回合被计时;而一条消息的 usage 是零, 因为 bingo 按轮计 token,并在 result 行里把总数报一次。

与之对应的输入方向 --input-format stream-json,文档在 无头运行。

要的东西不止是兼容的话,--output-format json 给你帧本身。

日志格式

磁盘上的一个会话就是它的日志被写了下来(ADR-0005)。第 1 行是一个头部:

{"format":"bingo-journal","version":1,"session":"<id>"}

之后的每一行都是一个持久的 Frame,就是它序列化出来的样子,按 seq 顺序,逐帧追加并 刷盘,绝不重写。瞬时的帧绝不写下。

由此得出三条规则。撕裂的最后一行——写到一半崩了——会被丢掉,回放止于最后一个完整的帧。 别处任何一行读不出来,都是一个点名那一行的 STORAGE 错误:损坏是要报出来的,不是跳过 去的。而一个比读取方更新的头部版本会被拒绝,因为版本 1 绝不就地编辑。格式变更是一个 新版本和一个重新折叠的迁移器。

它旁边的两个文件是 .lock——唯一的所有权声明——以及 summary.json,后者是推导出来的: 缺了一份会从日志重建,而把它们全删掉,除了 list 的速度以外什么也不会失去。