@bpos/activity-contract (6.2.0)
Installation
@bpos:registry=https://gitea.akhr-ai.com/api/packages/ChaiHao/npm/npm install @bpos/activity-contract@6.2.0"@bpos/activity-contract": "6.2.0"About this package
@bpos/activity-contract —— 脚手架契约
系统给活动的只有一样东西:这份契约。 契约以内是系统;契约以外、活动包以内的一切归活动。 那就是黑盒线的准确位置。
形状全部是 zod,不是 TS 接口:每次跨界调用双向 safeParse —— 系统不信任活动交来的,
活动侧的 SDK 也校验系统答的,装不装 SDK 都拦得住。
规范在《操作系统系统设计》:§6.1(清单)· §3(操作表单)· §6.2(活动进程)· §6.4(能力面的名词表)· §6.5(事件表)· §4.2(工单那一组)· §2.8(业务数据)· §7.3(交出的两条路)· §12(令牌只对本任务节点有效)。
三样东西
| 在哪 | 是什么 | |
|---|---|---|
| 线材 | src/index.ts |
递入信封 · 事件表 · 名词表 · 交出与错误信封 |
| 清单与操作表单 | src/manifest.ts |
bpos-activity.json 的形状(manifestSchema,含 declares.tables[] 的表声明)与工单定义那三段,加机检用的几把尺子(manifestGaps / outputIssues / orderIssues / tableIssues / deliverableIssues / chartIssues / archiveIssues),以及件上 ref 那一格的值形状(refValueSchema / refIsEmpty / refKeyOf)与主键串的编解码(encodeRefKey / decodeRefKey) |
| 不在这里的 | — | 实践的图(graphSchema 那一组)——图归实践、在系统侧编排,活动不知道有边 |
清单在这里而不在工作台契约里,是因为它是活动交付物的形状:包一搬出这个仓,
手写 bpos-activity.json 的人得拿得到那把尺子。构建时 manifestSchema.parse(自己的清单),
写坏了停在人改得动的地方,不必等装包那一刻由服务端说。
引用方向只许一边:本包不引 web/api/contracts;工作台契约单向引本包的清单形状,
再原样导出(接缝只有 web/api/contracts/plugin/manifest.ts 一处)。两向各有一条用例钉着:
本包的 tests/boundary.test.ts 钉禁止的那一向,server 的 tests/boundaries.test.ts R8 钉许可的那一向。
两个方向,两条线材
| 方向 | 线材 | 形状 |
|---|---|---|
| 系统 → 活动 | POST http://127.0.0.1:<BPOS_EVENT_PORT>/events |
eventEnvelopeSchema;进程答 eventAckSchema 即确认 |
| 活动 → 系统 | POST <BPOS_CAPABILITY_BASE>/call |
capabilityCallSchema → capabilityResultSchema |
| 活动 → 系统 | POST <BPOS_CAPABILITY_BASE>/handoff |
handoffSchema → handoffOutcomeSchema |
信封(环境变量)见 ACTIVITY_ENV:taskId · nodeRunId · nodeId · token · base · eventPort。
一个任务节点一个进程:进程知道自己是哪一单的哪一步,不知道图长什么样、别的步是谁。
事件
enter · work-order-reply · card-reply · human-say · timer · control · handoff-refused
(现算一份在 SYSTEM_EVENTS)。
每条带 id、按本任务节点内的先后顺序投、至少送到一次——活动用自己的实例状态去重,
系统不重放(§6.2)。每种事件的来处见 SYSTEM_EVENT_SOURCE:发出去的每一个口对着一条回来的路。
名词
工单 4(create · read · write · void)· 对话 3(say · card · input)· 模型 1 ·
业务数据 5(table.read · ledger.append · ledger.reverse · object.upsert · artifact.archive)·
工作持久 4(state · memory 各两个)· 文件 4(get · text · put · info)· 定时 1 · 本步读数 1
(现算一份在 CAPABILITIES)。
卡只有两种(决策 / 补格):制品卡 6.0 撤了——制品从工单来(产品设计 §1.5),
要人认一版草稿的派验收工单,要人做出一份文件的派协同工单,件上标了 archive 的文件格
在放行成立的那个事务里归档。artifact.archive 因此只剩资料一档。
审批不在里面:活动不声明审批、不知道审批、收不到裁决。 邮箱也不在里面:信箱只服务系统的事件规则(监听一个信箱、命中条件就起一单), 起单之后活动与那封信再无关系——活动既不发信、也不收信。 对象新建卡也不在里面:那是系统触发活动时的副作用,人答完系统自己建对象。
什么改动要升大版本
往 CAPABILITIES / SYSTEM_EVENTS / CAPABILITY_ERRORS 这几个封闭集里减一个值,
对已经打好的活动包是破坏性的——那些包把旧的枚举内联进了 dist/activity.mjs。加一个值
破不破,看这个值从哪一头来:系统答出去的(一种事件、一个拒绝码)加了就破——老包拒收并报
「系统答的形状不对」,而毛病在系统;活动发出来的(一个能力名词)加了不破——老包不认识它,
也永远不会收到它。破坏性的走大版本,其余走小版本。
往事件载荷里加一个字段同样不破(zod 对象默认非严格,多的字段被忽略),走小版本。
分界线不是「改没改契约」,是「老包会不会收到一个它解不出来的东西」——看的是活动包内联的那一份。
清单那一侧收窄不按这条走:清单不在活动包内联校验的那条线上(manifestSchema 是系统在装包那一刻用的尺子),
所以判据换成一条:收窄之后从前跑得起来的包会不会跑不起来。会 → 大版本;
只是把一个本来就用不成的值提前拦下来(拦点从下游挪到机检,并说得更清楚)→ 小版本。
icon 收进图标调色板走的就是后者:调色板外的值从前装得进、机检还全绿,可它建不出活动类型来(建类型 422),
从来没有一个包靠它跑起来过。
5.5.0:enter 载荷加 extraContext(恒有,没填是空串)——往开放的对象里加字段,按上面的判据走小版本;活动清单的 ^5.x 范围都满足。
5.6.0:能力名词加 ledger.reverse(冲销一行台账:打废弃标记、记理由与本步,数字一个不动)、table.read 多一格
retired(不给就是在用行);清单的表声明与列声明多几样注解(表的 text / time / refs,列的 text / measure)。
名词是活动发出来的那一头、注解是清单那一侧只放宽不收窄,两样都不破老包,走小版本。
5.7.0:工单封闭集加第四种 collaborate(协同工单:人带着自己的 AI 做出一份交付物,
执行工单偏人独立完成;同一副表单、同一枚「提交」,分界在谁来干);工单定义加一格可选的
deliverable(只协同有、且必有,指数据结构里一个 file 字段——放行时那一格非空是系统那一道
的固定规则,机检第七查拦漏写 / 错写)。清单那一侧是放宽:从前跑得起来的三种单一个字都不用改;
work-order-reply 的结果集一个值都没加(协同与执行同一枚 submitted),所以老包也收不到
任何它解不出来的东西。两条判据都指向小版本。
5.8.0:表单字段的封闭集加第九种 chart(件上一格只读的展示),字段多一格 chart
({ kind, unit?, from? }:画法封闭五种 bar / hbar / line / pie / donut)。两条数据驱动的路:
显式值由活动写(order.create 的初值、order.write 的补丁),值是图表数据
{ labels, series: [{ name?, values }] }(每条系列与 labels 等长);派生图写 chart.from,
值从本件那张表格此刻的行现算、数据对象里不存这一格。图表恒为只读(accessOf 对它恒答 read),
不进 rows、不作 decision / deliverable,机检第七查逐条拦(chartIssues 一处判两侧用)。
表的列类型 COLUMN_TYPES 与产出字段 CONTRACT_FIELD_TYPES 一个字都没加——图表不是列、也不是产出。
清单那一侧是放宽:从前跑得起来的八种字段一个字都不用改;活动发出来的那一头没减东西,
老包也收不到任何它解不出来的东西。两条判据都指向小版本。
6.0.0:制品从工单来 + 六格补齐。六条改动,头两条各自就够得上大版本——
- 撤制品卡(产品设计 §1.5、附录 B 31)。
chat.card的卡族去artifact(只剩decision·elicitation),card-reply.kind跟着去一个值,artifactArchivePayload去kind与cardId(只归资料;那一份收严成strictObject,老包照旧递kind: 'artifact'时当场被指名, 而不是把那一格默默丢掉、照type归出一份资料来)。两个封闭集各减一个值:卡族是活动 发出来的那一头(减一个破),card-reply.kind是系统答出去的那一头(加减都破)。 要人认一版草稿的改派验收工单,要人做出一份文件的改派协同工单。 ref值的形状进契约(收窄)。refValueSchema/refIsEmpty/refKeyOf从工作台契约 搬进这里(那一侧经plugin/manifest.ts那一处接缝再导出),checkValue多一支ref: 主键串或{ key, label? },老写法{ 列: 值 }当场拒并指名。从前它落在default分支上, 活动经order.create/order.write写老写法一路无声——从前跑得起来的包会跑不起来。needs.tables分读写(收窄)。一条可以写成{ address, write?: true };字符串写法恒为 只读。机检第五查核「write只许标在master.*上」(标在自己声明的表或别的域上都 fail), 能力面对没标write的外域表拒写并指名。从前靠约定,一次手滑就能让方案侧的包改掉全域 共用的那一行——清单侧的判据同样指向大版本。- 文件格的
archive(加格)。工单定义里file字段多一格archive,指本清单declares.documents[]里一个文档类型的代号:放行成立的那个事务里按那个类型归档, 种类随类型。第七查三条(archiveIssues一处判两侧用):代号要在本清单声明里、 协同单的deliverable那一格必须标了archive且那一类是制品、非文件格不许标。CAPABILITY_ERRORS加suspended(挂起中调能力面收它,与「这一步已收场」的node-closed分开)——系统答出去的封闭集加一个值,本身就是大版本。 measure.unitFrom(加格):度量四选一,单位可按行取自另一列(数量列的单位随料走)。- 列
default(加格):declares.tables[].columns[].default,与主数据那一层的列规格同一格, 落到库上是DEFAULT、落到屏上是新建弹窗的预填;形不合这一列的由机检第五查当场指名。
活动包要跟的:清单 contract 范围改 ^6.0.0;制品卡那一段改成验收单 / 协同单加一格标了
archive 的文件格;写 master.* 的登记类活动把那张表标上 write: true;ref 那几格改成
主键串或 { key, label }。
6.1.0:协同工单 = 制品卡的替代。6.0 落的是「制品从工单来」的前一半(文件格标 archive、
放行即归档),这一版补另一半——复杂制品不再由活动自己做完请人认,而是派一张协同单、
让执行人带着自己的 MCP host 做出来(产品设计 §1.4 / §1.5 / §3.5、系统设计 §3.1 / §3.4、附录 B 43 / 44)。
活动要把这一步交代清楚,所以多两格、多两条机检;加格与加机检,一个封闭集都没减——小版本:
text.prompt(加格):给 AI 的提示词——执行人的 host 做这一步时喂给它的模型的那段话。 协同单该有(缺了机检警示,不拦激活);别的类型写了也警示(它不上屏、不进取单答复)。text.references[](加格):制品参照与模版{ field, role: 'example' | 'template' }[]。field必须是数据结构里access: read的file字段(机检拦:不是文件格的话 host 拿不到可读的字节,人能改的话活动给的那一版会被当成待填的格改掉);值由活动在order.create时给(文件库里的一份引用)。- 交付物那一格不许写
multiple(加机检):只有一个槽,再交即覆盖(件上换一份、 host 里desk.deliver_artifact再交一次都是覆盖,旧的那份留一行工作日志、字节不删)。 多份的话「这一张单的产出是哪一份」在屏上、在 host 里、在放行那一道都答不出来。 - 交付物归到的那个文档类型,
formats要落在DELIVERABLE_FORMATS之内(加机检):html/markdown/pdf——工作区那一列里渲得开的那三种(产品设计 §3.5「交付物的规格」、 系统设计 §13)。不写formats(不限)也拦:不限意味着 Word / Excel 也收得下。 这个常量住契约包,机检、服务端(件上传与desk.deliver_artifact同一把尺)、前端三处同源。 - 活动可在
order.create时预填交付物槽(自己起的一版草稿当起点):checkValue对file值照旧判形状,没有新的口子。
活动包要跟的:清单 contract 范围改 ^6.1.0(^6.0.0 照样满足,不改也跑得起来);
协同单补 text.prompt,要给参照 / 模版的补 text.references[] 并把那几格标成 access: read;
交付物那一格去掉 multiple、它归到的那个文档类型的 formats 收进三种之内。
6.2.0:引用进输入与表格列 · datetime · 主键串的编解码。第六阶段第一个活动
端到端跑通之后撞出来的几格;全是放宽,一个封闭集都没减——小版本:
- 输入 / 产出字段收
ref(CONTRACT_FIELD_TYPES加一个值):一步的输入常常就是 「哪一条对象」。写成text的时候起单弹窗摆的是一个空输入框,人得自己把主键串背出来, 抄错一个字照样收得下——活动拿它读不到行,屏上是「这一步空跑了一趟」而没有一处说得出为什么。 值的形状与件上那一格同一份(refValueSchema);字段上写ref指定对象表地址, 那个地址要在needs.tables里且是对象表(机检第五查拦)。 - 表格的内联列收
ref(新封闭集FORM_COLUMN_TYPES= 列类型 +ref):一张报价行里的 「哪一颗料」就是这一格。从前只能摊成两列文本自己拼回去,拼不回去一个字也不报错。 人保存与放行按refValueSchema判(tableRowGaps一把尺);列上那个地址归机检第七查。 - 列类型加
datetime(COLUMN_TYPES加一个值):落库timestamptz,契约里的值是 ISO 8601 带时区的串(checkValue拒光一个YYYY-MM-DD、也拒不带时区的时刻)。date只写得出哪一天;只有它的时候,一件「几点发生的」只剩两条路——截成一天, 或塞进文本列(从此排不了序、比不了大小、数仓认不出它是时间),两条路都不报错。 台账的业务时间time指得到它(一刻本来就含着一天);屏上按业务时区渲。 encodeRefKey/decodeRefKey(加两个函数):主键串col=val;col=val的编解码搬进契约包。 从前它只在工作台契约里、活动够不着,于是每个要写引用格的活动自己拼一遍——值里带了;=%的那一条拼出来拆回去就不是原来那一条,而两头都不报错。工作台契约的encodeRecordKey/decodeRecordKey改成从这里单向再导出(接缝只此一处,用例钉)。
活动包要跟的:清单 contract 范围改 ^6.2.0(^6.0.0 / ^6.1.0 照样满足,不改也跑得起来);
把“哪一条对象”那几格输入从 text 改成 ref 并补上地址;摸成几列文本的引用列合回一格 ref;
要记「哪一刻」的列从 text 改成 datetime;自己拼主键串的改调 encodeRefKey。
拒绝
业务上的拒绝用 HTTP 200 承载(ok: false + error + message + hint)——它不是传输错误,
是系统对这次调用的回答,活动要读得到才知道下一步做什么。真正的 4xx 只有两种:
令牌不对 401、这一步已交出或这一单已挂起 409。码是封闭集,见 CAPABILITY_ERRORS。
Dependencies
Dependencies
| ID | Version |
|---|---|
| zod | 4.4.3 |
Development Dependencies
| ID | Version |
|---|---|
| @types/node | ^24.0.0 |
| tsx | ^4.19.0 |
| typescript | ^5.6.0 |