@sema-agent/core
Version:
Stateless, task-oriented AI agent core
100 lines (99 loc) • 7.31 kB
TypeScript
import { type MailboxStore } from "../mailbox-store.js";
import { type ContractAssertionRunner } from "./contract-harness.js";
/**
* design/159 S1 — the cross-backend {@link MailboxStore} contract, extracted VERBATIM from
* `test/mailbox-contract.ts` (design/157 A11's two-layer form) into a vitest-free kit (see
* `contract-harness.ts` for the runner seam and `contract-kit-version.ts` for version lockstep).
*
* 为什么这个家族有契约(结构性复发机制,两次实证):契约 harness 此前只喂 InMemory + File 两个
* 后端,于是没进 harness 的那个后端反复独立漂移 ——
* - RB-86:`stores/cc/mailbox-store.ts` 注释自陈 "the contract both bundled backends already
* honor, missing only here"(空箱 claimLease 不返回 null,还装上幽灵租约);
* - MB1:CC 后端的 `ack` 根本不查 lease 表,注释自陈 "worse than the other two
* backends' pre-fix bug"。
* 两次漏的都是同一个后端。修法不是再补一次点,而是把契约本身变成全后端共跑的资产 —— design/159
* 再进一步:第三方后端也拿得到同一份资产。
*
* 分层(与 test/mailbox-contract.ts 的挂载对应):
* - T1 核心不变量层 = {@link mailboxStoreContract} + {@link mailboxAckOwnershipContract}:每个后端
* 都必须过。任何后端在这一层变红 = 该后端违约,**不许通过放宽契约让它变绿**。核心层一律用
* `scope: "default"`({@link MAILBOX_CONTRACT_SCOPE}),这样租户轴受限的后端(CC inbox 文件)
* 也跑同一段代码。
* - T2 可选能力层 = {@link mailboxBundledOnlyContract}:多租户隔离 + 键注入性,只挂具备租户轴/
* 自有键空间的后端(core 的两个捆绑后端;CC 按 D-3 边界豁免,豁免明细在
* `src/stores/cc/mailbox-store.ts` 模块头)。
*
* `make()` must return a FRESH, empty store each call. Stores a contract case creates are closed
* (`close?.()`) when the case ends — the file backend's append fd is refcounted, so a harness that
* runs many cases would otherwise hold descriptors until process exit.
*/
/** 核心层统一用的 scope —— 每个后端都必须服务它(租户轴受限的后端只服务它)。 */
export declare const MAILBOX_CONTRACT_SCOPE = "default";
/**
* T1 核心不变量层 —— 每个后端都跑。用例出处见每条内联注释(`src/core/mailbox-store.ts` 的接口
* 契约段是权威文本)。
*/
export declare function mailboxStoreContract(mk: () => MailboxStore, runAssertion?: ContractAssertionRunner): Promise<void>;
/**
* T1 核心不变量层(MB1 分册)—— ack 的 owner 围栏,与 {@link mailboxStoreContract} 同层同权重,
* 单独成函数只是为了让 MB1 的出处叙述留在它自己的挂载文件里
* (`test/mb1-mailbox-ack-ownership.test.ts`)。
*/
export declare function mailboxAckOwnershipContract(mk: () => MailboxStore, runAssertion?: ContractAssertionRunner): Promise<void>;
/**
* T2 可选能力层 —— 只挂具备租户轴 + 自有键空间的后端(core 的两个捆绑后端)。CC 后端按其形态
* 明文豁免(豁免明细在 `src/stores/cc/mailbox-store.ts` 模块头,D-3 裁定),不是「没做到所以
* 放宽」:核心层没有一条为了任何后端而弱化。第三方后端若有租户轴,应当也挂这一层。
*/
export declare function mailboxBundledOnlyContract(mk: () => MailboxStore, runAssertion?: ContractAssertionRunner): Promise<void>;
export interface MailboxCrossProcessContractHooks {
/** Open ONE fresh, empty storage location TWICE: `local` in this process and `peer` served by ANOTHER
* OS process over the same storage (the peer is a real second process — an in-process second
* instance proves nothing here). `dispose` closes both and may remove the storage. The kit calls it
* once per case. */
openPair: () => Promise<{
local: MailboxStore;
peer: MailboxStore;
dispose: () => Promise<void>;
}>;
runAssertion?: ContractAssertionRunner;
}
/**
* T2 可选能力层(cross-process 分册,design/385 §1.2⑥ / G11)—— 声明 `crossProcessSafe: true` 的后端必须
* 过的全套判据。钉的是**整套**(跨进程 seq 单铸 + lease/ack 不交叉 + 一方的 housekeeping 不丢另一方的
* 已落盘 append + reap/drop 的 seq 语义跨进程成立),不是「有锁」:一把锁配一份陈旧缓存照样在锁内铸
* 重号、照样把别人的消息压缩掉——所以每一条都从**对方进程的视角**回读。
*
* 受众:文件后端(本仓捆绑)、SQL 后端(事务天然过)。进程内 Map 的参照实现与 CC inbox 适配器不声明,
* 也不挂——它们不是「没做到所以放宽」,而是根本不在跨进程车道上(挂载判据 `mailboxCrossProcessMountVerdict`
* 对未声明的后端响亮拒)。
*/
export declare function mailboxCrossProcessContract(hooks: MailboxCrossProcessContractHooks): Promise<void>;
export interface MailboxTombstonedRecipientContractHooks {
/** Put `(scope, handle)` into the deployment's PRE-DELETE state — whatever that is for this
* backend (a cascade marking the session row, a `deleting` column, a tombstone table). Core does
* not name the mechanism, only what `append` must then do. */
tombstone: (store: MailboxStore, scope: string, handle: string) => Promise<void>;
runAssertion?: ContractAssertionRunner;
}
/**
* T2 可选能力层(pre-delete 分册)—— 只挂**看得见收件人生命周期**的后端:它的 mailbox 行与某个
* 删除级联要清掉的 session 行同生共死(server 的留存删除级联是首个此形消费方)。三个捆绑后端都
* 没有这个视角,因此都不挂——**这不是「做不到所以放宽」**:没有预删除态的后端根本没有可拒的对象,
* 而有这个态却静默接受的后端,拿到的 seq 是一张级联马上就要撕掉的回执(`append` 是 durable-first,
* 回执即承诺),且没有任何其它面会说出这件事。
*
* 契约不规定态怎么置(那是部署自己的删除协议),只钉「拒」这件事的三条轴 —— **三条一起列在这里,
* 是因为逐条补的过程本身证明了单看一条会假绿**(每加一条,都有一个能过掉前面全部用例的错误实现被
* 抓出来):
* 1. **拒因轴**:必须拒,且每条路径上都带同一个码(空箱与非空箱都算路径);
* 2. **副作用轴**:被拒的 append 对箱零改动 —— 不入箱(幽灵投递)、不清箱(删除窗口内静默丢)、
* 也不动活租约(动了就撬开 X-1 的单消费者围栏,同一批消息会被二次投递);
* 3. **收件人边界轴**:态属于一个 `(scope, handle)` 整体,且这个整体是**单射**的 —— 兄弟 handle
* 不受牵连、跨租户同名不受牵连、拼键歧义不得把两个收件人折成一个。
* 新增用例请按这三条轴归位;要加第四条轴,先说清它是哪一类假绿。
*
* 受众前提与 {@link mailboxBundledOnlyContract} 同线:后端有自己的租户轴(生命周期可见的后端就是
* 服务端形态的 store),边界轴的两条用例要用两个 scope 构造。
*/
export declare function mailboxTombstonedRecipientContract(mk: () => MailboxStore, hooks: MailboxTombstonedRecipientContractHooks): Promise<void>;