架构
一个极小的内核、其余一切都是插件,以及一个由构建来断言的依赖方向。
一句话
一个极小的内核——会话 actor、有序的事件日志、回合状态机、权限闸门、插件宿主——其余 一切都是躲在稳定 trait 后面的插件 crate,而每一个界面都是一个提交入口和一个订阅的 客户端。
分层
bingo (bin) composes Vec<Box<dyn Plugin>>, picks a Surface
↑
plugins / surfaces depend on bingo-sdk only
↑
bingo-core depends on bingo-sdk only
↑
bingo-sdk serde, schemars, thiserror, async-trait, tokio(sync),
futures, ulid, jiff — nothing heavier
依赖方向严格向下。有五条边被彻底禁止,而 scripts/check_discipline.sh 在
cargo metadata 上断言它们(ADR-0001):
- 一个插件或界面 crate 依赖
bingo-core; bingo-core依赖一个插件或界面 crate;bingo-surface-tui以外的任何东西依赖ratatui或crossterm;bingo-core或bingo-sdk在它们的常规依赖树的任何地方解析出reqwest、rmcp、ratatui、crossterm、image或syntect;- 一个插件依赖另一个插件。 跨插件的需求走一个经 sdk 注册的服务 trait。
后果才是重点。内核是在进程内、对着 sdk 自己的假实现测试的,没有子进程。动一个工具 crate 绝不会重新链接终端界面。sdk——外部插件作者下载的东西——拉不进 ratatui 或 reqwest。而终端界面够不到引擎,因为根本没有一条边让它沿着够过去。
这些 crate
内核与契约
| crate | 它装着什么 |
|---|---|
bingo-sdk | id、Message/ContentPart、Frame/Event/Item、SessionState 与 apply、每一个 trait、HostApi、服务注册表、测试用的假实现 |
bingo-core | 会话 actor、日志与广播、回合状态机、权限闸门、工具执行器、插件宿主、上下文标尺、ContextView::fold |
插件
| 种类 | crate |
|---|---|
| 提供方 | bingo-provider-anthropic、bingo-provider-openai(openai 与 codex)、bingo-provider-fake |
| 工具 | bingo-tool-fs、bingo-tool-bash、bingo-tool-web、bingo-mcp、bingo-agents |
| 策略 | bingo-permissions、bingo-hooks-shell |
| 会话 | bingo-store-jsonl、bingo-context |
| 功能 | bingo-skills、bingo-rooms、bingo-tasks、bingo-experience、bingo-schedule |
| 扩展 | bingo-plugin-rpc —— 跨进程桥 |
| 界面 | bingo-surface-print、bingo-surface-rpc、bingo-surface-tui、bingo-channels |
| demo | bingo-demo-ui —— 除非 --demo-ui 否则关着,也是插件作者最先该读的那个完整例子 |
库
一个声明了 [package.metadata.bingo] tier = "library" 的 crate 什么都不注册,只依赖
bingo-sdk 和外部 crate,而任何插件都可以依赖它。bingo-auth-oauth 是第一个、目前
也是唯一一个:PKCE loopback、设备码、auth.json、单飞刷新(ADR-0012)。
二进制
bingo 显式地组装插件列表——在插件必须不由 bin 点名就能加载之前,不会有一个自注册
crate——并根据这次运行是什么来挑界面:管道两端都是人时是终端界面,否则是 print 界面,
serve 时是 RPC 界面,channels 和 gateway run 时是频道界面。
一个回合
客户端调用 SessionHandle::submit(intent, input)——同步,且什么也不返回。会话 actor
追加一个 user Item,生成一个 seq,把这个回合交给状态机。循环向贡献者要上下文,
把提供方流式取回,把提供方的事件折进一个个 Item,让每一次工具调用过钩子和策略
(需要人回答时是一个 Interaction),执行这些工具,在屏障处吸收排队的转向输入,并
以恰好一个 TurnCompleted 收尾。每一帧都先进日志再广播到每个订阅者的有界通道。每个
客户端都用 SessionState::apply 折叠这些帧。
会话是唯一的对话名词
子代理是带一条 parent 链接的会话。房间是没有模型的会话。两者都经由同一个 reducer
和同一套绘制代码渲染,而内核两个名词都不拥有:room、team、hire、task、
experience 和 schedule 在 bingo-sdk 或 bingo-core 里一处也不出现。它们是插件
的词,装在内核并不去枚举的 Event::Extension 载荷上。
接下来读什么
- 事件流 —— 每个界面都在消费的那一套词汇。
- 用 Rust 写插件 ——
Plugintrait 以及它可以 注册什么。 - 跨进程插件 —— 同样这些贡献,用任何语言。
- ADR 索引 —— 每一个边界决定,各一行。
在仓库自身里:ARCHITECTURE.md 是地图,docs/adr/ 是那些决定,docs/plans/ 是眼下
在建的东西,docs/design/ 是那些提案和库选型背后的调研。