# Adapter Extension 设计

本文是 Provider Kit 动态扩展的公共契约。架构取舍记录在 [ADR 0005](./adr/0005-reload-discovered-adapter-extensions.md)；本文描述使用方式、生命周期和故障边界。

## 目标

用户可以通过以下方式共享 Provider Kit 能力，而无需修改 Provider Kit 的 `index.ts`：

1. 在已安装包的 capability 目录中添加或删除 TypeScript 文件；
2. 安装另一个本地、npm 或 git Pi 包；
3. 执行 `/reload`。

新增的 Provider、Status、Preflight 和 Tuner 在 reload 后进入同一个 Provider Kit Host。本文不承诺文件变化自动生效，也不支持会话内热插拔。

## 包布局

Provider Kit Host 包和独立 Adapter 包都使用对应的 capability 目录：

```text
package-root/
  index.ts                 # Provider Kit Host；每个运行时只应有一个
  providers/*.ts           # Provider Adapter Extension
  status/*.ts              # Status Adapter Extension
  preflight/*.ts           # Preflight Adapter Extension
  tuners/*.ts              # Tuner Adapter Extension
```

`package.json` 使用 Pi 官方 manifest：

```json
{
  "pi": {
    "extensions": [
      "./index.ts",
      "./providers/*.ts",
      "./status/*.ts",
      "./preflight/*.ts",
      "./tuners/*.ts"
    ]
  }
}
```

被 glob 命中的文件必须 default-export Pi extension factory。它们不是可以直接被 Pi 解释的原始 Adapter 对象。辅助文件不能放在这些顶层 glob 会匹配的位置。

独立 Adapter 包不要复制或再次加载 Host 的 `index.ts`；用户应启用一个 Provider Kit Host，再启用任意数量的 Adapter 包。Pi 包和松散 TypeScript 扩展都拥有完整系统权限，只使用可信来源。

## 文件契约

一个顶层文件只贡献一个 Adapter。使用对应 capability 的专用 helper：

```ts
// providers/example.ts
import { defineProviderExtension } from "pi-provider-kit";

export default defineProviderExtension({
  id: "example",
  create: ({ fetch, now, modelDiscoveryTimeoutMs }) => {
    // factory 阶段只创建同步可用的 fallback/缓存目录；不要等待网络。
    return createExampleProvider(fetch, modelDiscoveryTimeoutMs, now);
  },
});
```

其他目录分别使用 `defineStatusExtension`、`definePreflightExtension` 和 `defineTunerExtension`。身份元数据必须静态提供：

- Provider：`id`；
- Status：`id`、`providerId`；
- Preflight：`id`、`providerId`；
- Tuner：`id`。

helper 会在创建前验证静态身份，并在创建后要求返回 Adapter 的身份完全一致。Adapter ID 必须是非空、无空白的稳定标识。一个文件中的 named export 可以继续供程序化调用，但 default export 是 Pi 加载契约。

## 生命周期

### Extension factory 阶段

Pi 会在启动和 reload 时重新执行所有 factory。Adapter helper 会：

1. 校验静态 descriptor；
2. 创建一个 Adapter；
3. 对 Provider 通过 Provider Kit 启动桥接路径调用 Pi `registerProvider()`；
4. 通过 `pi.events` 发布 `version: 2`、包含 factory 和启动依赖版本的注册信封；
5. 注册一个同步的 `session_start` 重放处理器。

Provider 必须在 factory 返回前完成 Pi 注册，因为启动模型选择和 `pi --list-models` 不会等待 `session_start`。动态 Provider 的 factory 应提供同步 fallback 或已有进程内快照，不应等待模型目录网络请求；Provider 的 `refreshModels` 负责读取 Provider-scoped `store`，遵守 `allowNetwork`/`force` 和调用方 `signal`。Status、Preflight 和 Tuner 不在此阶段创建自己的 Manager，也不发起诊断网络请求。

Adapter 不等待 Host 的确认。Pi 串行等待 extension factory；如果先加载的 Adapter 等待后加载的 Host，会阻塞 Host 本身。注册信封因此采用“即时发布 + `session_start` 重放”方式：Host 已经监听时立即收集，Host 尚未监听时由重放补齐。若 Adapter 先于 Host 加载，Host 会在组装 registry 时使用自身 runtime 重新调用该 factory，确保 Provider discovery、status/preflight timeout 和 fetch 不被加载顺序静默降级。

### `session_start` 阶段

Host 在收到所有 Adapter 重放后建立一次性会话 registry，并创建 Status、Preflight、Live Check Manager 和确定性的 tuner 列表。相互独立的 Adapter factory 会并行物化，但 Provider 注册和 Status/Preflight 绑定仍保持必要的先后顺序。Host 不依赖 manifest 或包的加载顺序；首次 `/status` 或 Provider 请求会再次确认 registry 已完成。Host 或显式 runtime 只在 `startup`/`reload` 触发一次非阻塞的 Pi model catalog refresh；普通 Pi 模型刷新由 Provider 的缓存/TTL 逻辑快速返回，强制刷新才访问网络。组装过程受 session generation 保护，reload/shutdown 期间完成的旧异步任务不会重新安装 runtime。

Host 对动态模块使用以下顺序：

- 各 capability 内按 Adapter ID 排序；
- Tuner 先按较低 `priority`，同 priority 按 Adapter ID 排序。

### `session_shutdown` 和 `/reload`

旧 Host 会取消请求、清空缓存和实时检查诊断，并移除事件监听。一个 Pi runtime 只允许一个 Host；后加载的 Host 会被忽略并输出诊断。`/reload` 随后重新发现 manifest 文件并建立全新的 Host 和 registry：

- 新文件在 reload 后出现；
- 删除的文件在 reload 后消失；
- 旧 Status、Preflight、Live Check 状态不会泄漏到新 runtime；
- 不运行后台 watcher，也不对动态 import 做会话内 cache-busting。

触发 `ctx.reload()` 的命令在 `await ctx.reload()` 后立即返回，不继续使用旧 extension 实例。

## 校验和故障隔离

- 空 ID、带空白 ID、返回身份不一致、错误 timing 配置和错误 Adapter 形状会使对应模块失效；
- 同一 capability namespace 的重复 Adapter ID、同一 Provider 的重复 Status/Preflight binding 会排除所有冲突项，避免产生依赖加载顺序的隐式赢家；
- Provider 冲突会清理 Pi 的动态 Provider 覆盖，若有同 ID 的 Pi 内置 Provider 则恢复内置实现；
- 一个模块的 default export 缺失、factory 抛错或注册信封损坏不会阻止其他有效模块；
- Status/Preflight 既可绑定 Provider Kit Provider，也可绑定 Pi 原生 Provider；无法解析的绑定只隔离该模块并显示诊断；跨 extension 的注册信封使用带版本的运行时契约，缺失 factory 或依赖信息的旧信封会被拒绝。

## 与程序化 API 的关系

`createProviderKitRuntime()` 和显式 `ProviderKitDefinition` 继续用于测试、SDK 集成或需要手工组合的调用方。发布包的默认 Pi 入口不再通过中央数组静态汇总 capability 文件；默认安装通过 manifest 和 Adapter Extension 契约发现它们。

## 测试边界

实现时必须覆盖：四个 capability 的单文件发现、确定性排序、静态及返回身份校验、重复 ID 和重复 binding、无效 default export、factory 异常、Host 先后加载时的依赖重建、重复 Host、shutdown/reload readiness race、reload 模拟下的增删、发布 tarball 边界，以及包含 mocked Adapter 的 Pi 入口 smoke test。测试不得包含私有 Provider 名称、端点、凭据、环境变量或模型 ID。
