契约约定
契约包(@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。见 动态查询。