Skip to content

契约约定 ​

契约包(@aalis/api-* 与第三方的契约包)导出服务描述符,定义服务的业务层。本文是写契约时遵守的约定。core 0.20 先写下约定,现有契约里与它不一致的地方另批收敛;新写的契约照本文写。

两层的形状与句柄的行为见 插件定义与能力,描述符与资源口见 服务,登记型能力的范本见 枢纽服务,本文不重复这些规则。

服务名 ​

  • 服务身份就是描述符的 name。第三方契约的服务名带包名前缀,如 @your-scope/api-weather 的服务取名 @your-scope/api-weather/forecast;不带前缀的短名留给第一方 @aalis/api-*。撞名的后果与 core 的告警见 契约包。
  • 一个服务名只对应一份描述符,由一个契约包导出。别的包要用这个服务就导入这份描述符,不另造同名的。

业务层 ​

  • 选择层(list() / use / of(id) / present / follow)由 core 定义,每个服务都一样,契约改不了。契约不在业务层上另写挑选哪一家的逻辑:固定某一家用 of(id),要全部提供者就遍历 list()。
  • 业务层是契约的接口,插件经活句柄调用(caps.x.use、of(id)、list() 条目的 use)。不给 bind 时业务层就是提供者接口;给 bind 时只包装业务方法或加登记方法:Bound* 接口 extends 提供者接口,实现写 Object.assign(Object.create(port.provider), { ... })。
  • 提供者接口不依赖句柄截下的那些行为:业务成员不叫 then、不用 symbol 键,不要求调用方迭代、展开或序列化句柄本身。句柄的全部边界见 句柄的边界。
  • 按领域键(模型、平台)区分的提供者,条目 id 就是领域键,调用方用 of(id) 取,或在 list() 上筛(如 caps.llm.of('<provider>/<model>'))。契约提供的筛选辅助函数收 Pick<ServiceSelection<P>, 'list'>,返回 ProviderEntry。

登记方法 ​

  • 往服务里登记条目(工具、命令、页面、预处理器、钩子、诊断检查、订阅)的方法放在业务层上,经资源口的 registrar 实现:登记归调用方这次激活,提供者换人时整体重挂,关闭时与原语同一拍撤回,关闭后拒收。不让插件拿提供者接口上的原始登记方法自己管撤回。
  • 登记方法返回退订。同键怎么处理由契约选键决定(按名唯一、每次独立、按激活分层),写进契约文档;对同一个键,registrar 只有一条规则:后调用者胜出,被取代的那条随之撤回。
  • 提供者接口上的登记方法收登记方的 port.id,只用于展示与记账;撤回靠 registrar 交回的退订,不靠 id。

在场 ​

  • 调用方用选择层的 present 判断此刻有没有提供者。契约不在业务层上另设表示「提供者在不在」的成员;业务上的可用性(例如沙箱在这台机器上能不能隔离)是另一回事,照常作为业务成员。
  • 选不到提供者时,访问业务句柄上转发到提供者的成员同步抛 ServiceUnavailableError(按 err.name 认),与成员本身是同步还是异步无关。bind 定义的成员按契约行事,缺席时的行为写进契约文档:经 registrar 的登记方法照收、上线后挂上,写成 async 函数的得到被拒的 Promise。契约文档写可选依赖的用法时,写明先看 present。

同步与异步 ​

  • 做 I/O 或可能等待的成员返回 Promise。
  • 只读提供者内存状态、在热路径上每次都要读的成员可以同步(例如每条消息都要读的工具定义)。同步与否要在契约里一次定好:改成异步会落到每一个调用点。
  • 转发到提供者的成员缺席时的错误是同步抛出的:caps.x.use.m().catch(...) 接不住它。bind 里写成 async 函数的成员缺席时得到被拒的 Promise。

尚未约定、留到收敛批再定的:AbortSignal 放在参数的哪里、流式结果的形状、错误类与命名。

本插件偏好 ​

  • 用户可以给任何插件的任何服务设本插件偏好(见 服务偏好),契约不需要也无法声明禁止。
  • 下面两类服务不宜按插件分开选,契约文档写明「只设全局偏好」:
    • 「谁生效」由当前选中者决定的服务,如 trigger;
    • 多个插件共用同一份数据的服务,如 memory、storage、vectorstore、embedding。
  • 插件一定要某一家提供某个服务时,core 不提供强制机制:在插件文档里写明,由用户设本插件偏好。插件自己的某次调用要固定某一家,用 of(id);需要的是那一家独有的能力,就把它做成单独的契约。

业务调用的入口 ​

  • 插件的业务调用只经 uses 声明的选择层:声明了才有激活闸、关停边与跟随。
  • services.use.get / all 只用来查某个服务在不在、列出运行期服务名(URL、配置里的名字)的提供者,以及取身份键;声明会成环或自依赖、只能动态查的,在调用处写明原因。展示与对外输出用 services.use.inspect。见 动态查询。

文档对应 dev@c4e046e41,core 0.21.0,构建于 2026-11-02
基于 MIT 许可发布