/** * 契约七件(编码规范 §8.1;语义出处 OS 4.1,AM 01)。 * * 契约是活动对调用方暴露的全部真相,也是对 Agent 机制的全部授权; * 两种范式同一契约格式 —— 这是「统一套执行契约」的锚点。 * 契约必须在活动启动前冻结;运行中不得变更 —— 变更即新版本。 * * ADR-003:**声明层 = JSON Schema 2020-12 子集**(活动声明是运行时数据,可 diff 可版本), * 代码层 = zod。本文件用 zod 描述「声明的形状」,声明里的 schema 字段本身是 JSON Schema。 */ import { z } from 'zod'; import { acceptanceLevelSchema } from './terminal.ts'; import { activityIdSchema, skillIdSchema } from './id.ts'; /** JSON Schema 2020-12 子集的运行时形态 —— 由 ajv 校验,不在编译期展开 */ export const jsonSchemaSchema = z.record(z.string(), z.unknown()); export type JsonSchema = z.infer; /* ── ① 输入口:单一入口,data + message ──────────────────────────────── */ export const contractInputSchema = z.object({ /** data:结构化载荷,按 schema 校验,缺项不得启动 */ data: jsonSchemaSchema, /** message:委托消息(自然语言),可为空;**不得携带扩大授权的效力** */ messageAllowed: z.boolean().default(true), messageHint: z.string().optional(), /** * **这一单是被一封信叫起来的** —— 声明入参里哪几个字段合起来是那封来件。 * * 声明了,基座在起单那一刻就把它归进任务的**沟通记录**(工作台那一屏), * 原件与附件一并存下:谈过什么、什么时候谈的,回头查得到。 * * 为什么是**声明**而不是活动跑起来之后再做一遍:这件事没有临场判断的成分 —— * 「入参的 subject 就是来件主题」是活动作者写清单时就知道的事实。 * 交给模型每单认一遍,是拿一整拍的 token 去换一个常量。 * 值填的是**入参里的字段名**,不是字面内容。 */ inboundComm: z .object({ channel: z.string().default('mail'), /** 对方是谁 —— 填入参里那个字段的名字(如 `customerEmail`) */ partyField: z.string().min(1), subjectField: z.string().optional(), bodyField: z.string().optional(), attachmentsField: z.string().optional(), }) .optional(), }); export type ContractInput = z.infer; /* ── ② 输出口:必发(AND)与选择组(XOR) ──────────────────────────── */ export const outputPortSchema = z.object({ name: z.string().min(1), /** 交付物 schema —— 调用方仅凭口名与 schema 即可衔接下游 */ payload: jsonSchemaSchema, description: z.string().optional(), /** * **这一口什么时候走** —— 对任务视图求值的 S-FEEL 谓词(选择组内**按序求值,首个命中即出**)。 * * 组内每一口都写了 `when`,去向就是**算出来的**:放行时基座自己判, * 卡上只如实告诉人走哪一口、凭哪一条,不摆成选择题。 * 「这一单往哪走」多半是台账里那几个数的函数(有没有 C 类、有没有 B 类)—— * 让人凭印象点一下,等于把一个可回放的判定换成一次不可回放的手感; * 让模型选,又多一处它可以说了算的地方。 * * 有一口没写,整组就算不出来,退回「人选一口」那条老路(不是所有分支都算得出来: * 议价的 成交/有异议/拒绝 就得人裁)。 */ when: z.string().optional(), }); export type OutputPort = z.infer; export const outputPortsSchema = z.object({ /** 必发口:每次「放行完成」必发;可多口,同时发射 */ mandatory: z.array(outputPortSchema).default([]), /** 选择组:组内恰发一口;选择由环内决断,在放行(验收工单)上一并确认 */ selective: z .array( z.object({ group: z.string().min(1), ports: z.array(outputPortSchema).min(2), }), ) .default([]), }); export type OutputPorts = z.infer; /* ── ③ 必交付物 ─────────────────────────────────────────────────────── */ export const requiredDeliverableSchema = z.object({ key: z.string().min(1), label: z.string(), /** 缺一不得提交放行 —— 验收工单「成果清单」的来源 */ schema: jsonSchemaSchema.optional(), /** * 「有没有产出」的**可机检依据**:台账地址 `域.对象`,该地址在任务视图里有行即算有。 * * 不给依据的话,「缺一不得提交放行」就只能靠活动自报 —— 自报等于没有门。 * 声明了地址,预检就能自己数行数:活动说交付了、台账里一行没有,当场拒收。 */ ledger: z.string().optional(), /** * **这份成果由哪几张表承载**(2026-08-02 用户当场指出:「料号分类表和受理结论 * 不都是包含在询价受理记录的结构里的吗,为啥单独陈列?」)。 * * 一份成果常常不止一张表:A1 交出的是**一份询价受理记录**,而它由受理记录、 * 料号分类表(判据来源)、受理结论(结论栏)三张表共同承载。 * 从前三张表各占一条 `requiredDeliverables`,人在成果面上就看到三样东西 —— * 而在业务上他只拿到一份。 * * 声明了它,落地那一屏按**交付物**点名,三张表的行数并成一行; * 不声明就退回 `ledger` 那一张 —— 老清单不必改一个字。 */ ledgers: z.array(z.string()).optional(), /** * **验收页上让人就地改的那几列** —— 台账列名,顺序即列序。 * * 验收是这一单最重的一道门:人得能在**成果本身**上改,而不只是在旁边几个整单级字段上改。 * 给了这几列,基座在放行卡上铺一张绑着 `ledger` 的可编辑表(整份行都在), * 人改完由基座写回草稿层、随放行合并 —— AI 不经手。 * 不给就没有这张表:哪些列该让人动,是写清单时就该定的事,不是临场发挥。 */ editableAtRelease: z.array(z.string()).optional(), }); export type RequiredDeliverable = z.infer; /* ── ④ 验收判据:逐条标注真实等级 L1–L5 ───────────────────────────── */ export const acceptanceCriterionSchema = z.object({ key: z.string().min(1), label: z.string(), level: acceptanceLevelSchema, /** L1/L2 的机检表达式(S-FEEL);L3–L5 由环内实证 / 独立评审 / 人放行承担 */ predicate: z.string().optional(), /** loop 可引用业务模型的完成判据或 DMN 决策 */ modelRef: z.string().optional(), }); export type AcceptanceCriterion = z.infer; /* ── ⑤ 禁区 ─────────────────────────────────────────────────────────── */ export const prohibitionSchema = z.object({ key: z.string().min(1), /** 应短而硬;负面清单优于正面枚举 */ statement: z.string().min(1), }); export type Prohibition = z.infer; /* ── ⑥ 授权边界 ─────────────────────────────────────────────────────── */ export const authorizationBoundarySchema = z.object({ /** AI 可自决的范围:金额 / 折扣 / 措辞等,以 S-FEEL 谓词或叙述给出 */ discretion: z.array(z.object({ key: z.string(), statement: z.string() })).default([]), /** 预授权动作清单 —— 必须逐项列举,方可在过程中即时执行写类动作(OS 7.3) */ preauthorizedActions: z .array(z.object({ tool: z.string(), operation: z.string(), limit: z.string().optional() })) .default([]), /** 调人预算(打扰上限) */ humanCallBudget: z.number().int().min(0), }); export type AuthorizationBoundary = z.infer; /* ── ⑦ 资源声明 ─────────────────────────────────────────────────────── */ /** 预算四类上限 —— 由基座在环外强制,不得由活动代码自查自守(AM 10.1) */ export const budgetCapsSchema = z.object({ steps: z.number().int().min(1), tokens: z.number().int().min(1), /** 活跃时长(分钟);挂起、监听等待、待卡回传不计入 */ activeMinutes: z.number().int().min(1), humanCalls: z.number().int().min(0), }); export type BudgetCaps = z.infer; export const resourceDeclarationSchema = z.object({ /** 工具调用契约引用 + 版本约束 */ tools: z .array(z.object({ name: z.string().min(1), version: z.string().optional() })) .default([]), skills: z.array(skillIdSchema).default([]), /** 活动使用的 LLM,以自定义模型名(逻辑名)声明;活动不触厂商与密钥 */ models: z.array(z.string().min(1)).default([]), /** 岗位:调人时卡投递到哪个工位 */ roles: z.array(z.string().min(1)).default([]), budget: budgetCapsSchema, }); export type ResourceDeclaration = z.infer; /* ── 七件合体 ───────────────────────────────────────────────────────── */ export const sevenPartContractSchema = z.object({ activityId: activityIdSchema, version: z.string().min(1), input: contractInputSchema, output: outputPortsSchema, requiredDeliverables: z.array(requiredDeliverableSchema).default([]), acceptanceCriteria: z.array(acceptanceCriterionSchema).default([]), /** * **放行时人可当场改的值** —— 验收报告「AI 已填好、可编辑」改的就是这几个。 * * 它是**设计期的事实**,和验收判据一样:哪几个值该让负责人有机会覆盖、 * 改完落回哪个台账地址,由做这个活动的人一次定死,过安装审。 * 让 Agent 每单临场重述一遍是不可靠的 —— 实测它会漏(说了「可直接编辑」而没给字段), * 而漏掉的后果是验收报告只能看不能改,人工修订形同虚设。 * * `ai` 不在这儿:拟值是**运行期**的事实 —— 放行前由基座从台账现读活动写下的那个值, * 不让模型重述一遍(重述就会和台账里的值不一致,而以哪个为准说不清)。 */ revisableAtRelease: z .array( z.object({ key: z.string().min(1), label: z.string(), /** 改完落回哪儿:`域.对象`;只改本单在该对象的那一行 */ ledger: z.string().min(1), }), ) .default([]), prohibitions: z.array(prohibitionSchema).default([]), authorization: authorizationBoundarySchema, resources: resourceDeclarationSchema, }); export type SevenPartContract = z.infer; /** * 完成时的发射集合 = 全部必发口 + 每个选择组各一口(AM 1.2)。 * 纯函数,前后端同源:前端预演、后端裁决。 */ export function emissionPortNames( ports: OutputPorts, selected: Readonly>, ): string[] { const out = ports.mandatory.map((p) => p.name); for (const group of ports.selective) { const choice = selected[group.group]; if (choice !== undefined) out.push(choice); } return out; } /** 选择组必须恰选一口 —— 未选或选了组外的口即非法 */ export function validateSelection( ports: OutputPorts, selected: Readonly>, ): string[] { const errors: string[] = []; for (const group of ports.selective) { const choice = selected[group.group]; if (choice === undefined) { errors.push(`选择组「${group.group}」未选出口`); continue; } if (!group.ports.some((p) => p.name === choice)) { errors.push(`选择组「${group.group}」的出口「${choice}」不在组内`); } } return errors; } /** 验收等级不得谎报:L4 不得标注为 L1–L3;生成者不得自评(AM 12.1) */ export function machineCheckable(criterion: AcceptanceCriterion): boolean { return criterion.level === 'L1' || criterion.level === 'L2'; }