T3 Code 源码拆解
Provider 与执行

09 · Provider 系统与多实例

五种 Agent 如何共享一个窄接口,多账号、能力探测和 session 绑定又如何分层。

统一的不是 Provider,而是控制语义

五个内置 Provider 的技术路径不同:

Provider底层接入典型差异
Codexcodex app-server 双向 JSON-RPCthread/turn/item notification,审批与协作事件丰富
ClaudeClaude Agent SDKasync SDK stream、permission callback、Claude home/skills
CursorACP over stdioACP session update + Cursor 扩展
GrokACP over stdioACP 核心 + xAI 扩展
OpenCodeOpenCode SDK/本地 runtimeserver/session/event API 与 CLI 配置

若 UI 直接依赖这些差异,每新增一个 Provider 都要修改 Web、Desktop、Mobile、持久化和远程协议。 T3 Code 用 ProviderAdapter 建立窄腰:上游保持差异,下游只消费稳定控制语义。

ProviderAdapter 契约

Services/ProviderAdapter.ts 定义的能力大致分成四组:

会话生命周期

  • startSession
  • listSessions / hasSession / readSession
  • stopSession / stopAll
  • 可选 rollback/恢复能力

Turn 控制

  • sendTurn
  • interruptTurn
  • 响应 approval
  • 响应 Provider 发起的 user input

事件面

  • streamEvents:输出 canonical provider events,而不是原生 SDK event。

能力与错误

  • Provider availability/capabilities 由 snapshot 和 driver 描述;
  • 启动失败、session 不存在、协议失败等被映射为统一 error 类别。
Provider 窄腰
Orchestration + Reactors ProviderAdapter Codex Adapter Claude Adapter Cursor ACP Adapter Grok ACP Adapter OpenCode Adapter Canonical event stream Runtime Ingestion

窄接口不意味着所有 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 的最小工作面

  1. 定义 driver ID、config Schema 与 catalog metadata;
  2. 实现 ProviderAdapter 的 session/turn 控制;
  3. 将原生事件映射成 canonical event stream;
  4. 构造 availability/capability snapshot;
  5. 注册 built-in driver 和 instance factory;
  6. 覆盖启动失败、interrupt、approval、session 恢复、重复/乱序事件测试;
  7. 只在确有不可归一功能时扩展 contracts/UI。

新增工作的多数时间应花在事件与生命周期一致性,而不是加一个 logo。

源码锚点

可迁移经验

  • 多供应商系统要统一生命周期和错误边界,不要强行统一所有功能。
  • 类型、配置实例、live adapter、session binding 分层,才能支持多账号与按需启动。
  • “不可用”是一等 snapshot,不应成为全局异常。
  • Instance identity 必须进入持久化和 session 关联;UI 下拉框不是多账号架构。

On this page