09 · Provider 系统与多实例
五种 Agent 如何共享一个窄接口,多账号、能力探测和 session 绑定又如何分层。
统一的不是 Provider,而是控制语义
五个内置 Provider 的技术路径不同:
| Provider | 底层接入 | 典型差异 |
|---|---|---|
| Codex | codex app-server 双向 JSON-RPC | thread/turn/item notification,审批与协作事件丰富 |
| Claude | Claude Agent SDK | async SDK stream、permission callback、Claude home/skills |
| Cursor | ACP over stdio | ACP session update + Cursor 扩展 |
| Grok | ACP over stdio | ACP 核心 + xAI 扩展 |
| OpenCode | OpenCode SDK/本地 runtime | server/session/event API 与 CLI 配置 |
若 UI 直接依赖这些差异,每新增一个 Provider 都要修改 Web、Desktop、Mobile、持久化和远程协议。
T3 Code 用 ProviderAdapter 建立窄腰:上游保持差异,下游只消费稳定控制语义。
ProviderAdapter 契约
Services/ProviderAdapter.ts
定义的能力大致分成四组:
会话生命周期
startSessionlistSessions/hasSession/readSessionstopSession/stopAll- 可选 rollback/恢复能力
Turn 控制
sendTurninterruptTurn- 响应 approval
- 响应 Provider 发起的 user input
事件面
streamEvents:输出 canonical provider events,而不是原生 SDK event。
能力与错误
- Provider availability/capabilities 由 snapshot 和 driver 描述;
- 启动失败、session 不存在、协议失败等被映射为统一 error 类别。
窄接口不意味着所有 Provider 功能只能取交集。T3 Code 仍可用 capability flags、provider-specific config 和扩展 event 保留高级能力;关键是核心 session/turn 生命周期不分叉。
Driver、Provider、Adapter、Instance:四层不要混
Driver:工厂与配置 Schema
ProviderDriver.ts
描述 provider type 如何从实例配置创建 adapter、snapshot 和辅助服务。每种内置 driver 在 Drivers/ 下实现,
builtInDrivers.ts
确定稳定顺序:Codex、Claude、Cursor、Grok、OpenCode。
Provider 实现:协议到产品语义
Layers/CodexProvider.ts 等负责某个协议 runtime 与生命周期,Adapter 再满足统一接口。分层允许同一协议连接被多个产品能力复用,
也便于单独测试启动参数、事件转换和 session directory。
Instance:一个已配置账号/环境
同一个 Provider 可以有多个实例,例如两个 Codex home、不同 Claude account 或不同可执行文件。实例拥有稳定 ID、配置与展示 snapshot。
Thread 绑定的是 provider instance ID,不只是 codex 这个类型。
Registry:静态目录、活实例与 session 绑定
ProviderRegistry:有哪些 driver/provider type;ProviderInstanceRegistry:用户配置了哪些实例,它们当前 snapshot 如何;ProviderAdapterRegistry:哪些实例已有可用 live adapter;ProviderSessionDirectory:session 与实例/运行信息的关联。
把它们拆开,是因为“配置存在”“Provider 可用”“adapter 已启动”“某 session 属于它”是四个不同事实。
多实例为何必须从数据模型开始
如果早期只在 UI 增加“账号下拉框”,却仍把 session 只关联 provider type,会产生不可修复歧义:Server 重启后不知道该用哪个 home 恢复、审批应该发给哪个 adapter、旧 thread 的模型选项属于哪个账号。
T3 Code 的迁移 027/028 分别给 provider session runtime 与 projection thread session 加 instanceId,说明多实例最终需要贯穿:
instance config → live adapter → thread binding → session runtime → projection → client snapshot配置隔离
ProviderInstanceEnvironment.ts 负责为实例构造环境变量和 home 语义。Codex/Claude 都有专门的 home layout 解析与测试,避免:
- 一个实例读取另一个实例的认证;
- 启动 child process 时污染 Server 全局
process.env; - 自定义 executable 与默认 CLI 探测互相覆盖;
- 展示状态与实际启动配置不一致。
正确做法是为每次 process/SDK 初始化构造显式 env,不在运行时全局 mutate 环境。
Snapshot 与 capability probe
客户端需要在未启动 session 时就知道:Provider 是否安装、版本、登录状态、模型选项、可用权限模式和维护状态。
因此 Provider 暴露 snapshot,而不是要求 UI 通过试错调用 startSession。
能力探测可能昂贵或失败,代码使用 status cache/maintenance 服务控制刷新。不可用 snapshot 也是合法产品状态,应该带原因与修复提示, 而非抛出导致整个 Provider 列表消失。
Session reaper 与资源所有权
外部 Agent session 会留下进程和内存。ProviderSessionReaper 负责回收过期/失去绑定的 session,adapter 的 stopAll 在 shutdown
清理其余资源。Reaper 必须尊重正在运行 turn 和恢复窗口;只按“长时间没有 UI 连接”杀进程会破坏远程/后台工作。
新增 Provider 的最小工作面
- 定义 driver ID、config Schema 与 catalog metadata;
- 实现 ProviderAdapter 的 session/turn 控制;
- 将原生事件映射成 canonical event stream;
- 构造 availability/capability snapshot;
- 注册 built-in driver 和 instance factory;
- 覆盖启动失败、interrupt、approval、session 恢复、重复/乱序事件测试;
- 只在确有不可归一功能时扩展 contracts/UI。
新增工作的多数时间应花在事件与生命周期一致性,而不是加一个 logo。
源码锚点
可迁移经验
- 多供应商系统要统一生命周期和错误边界,不要强行统一所有功能。
- 类型、配置实例、live adapter、session binding 分层,才能支持多账号与按需启动。
- “不可用”是一等 snapshot,不应成为全局异常。
- Instance identity 必须进入持久化和 session 关联;UI 下拉框不是多账号架构。