Archived
human-tool / ledger / loop-engine / release / task / interaction 六模块与 app 层(desk 路由、engines、consumers、 data-order-landing、case-stamp、reset-runs、seed-activities、容器与 server 装配),web 的 desk-task (ViewRegion / chat-bubbles / review-face)与 app.css、ui-kit 图标;契约 card / manifest; 迁移 0034_task_seq 与迁移日志;api-report 快照;CLAUDE.md 快照、debt、活动开发规范、HTML 报告规范同步。 逐条口径见 CLAUDE.md 快照(2026-08-27 下午至晚)。另一会话做的,一直留在工作树没入库;与同批另两条提交互相依赖。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0167qWjeyBwvfdGovd7FWbCu
372 lines
19 KiB
TypeScript
372 lines
19 KiB
TypeScript
/**
|
|
* 活动包清单六区与资产形状(编码规范 §8.1;语义出处 OS 11.1,AM 11)。
|
|
*
|
|
* 清单机器可读,系统**不执行代码即知包为何物** —— 导入登记、安装静态审、编排装配读的都是清单。
|
|
* 资产与包分离版本化(OS 11.3):包版本管代码 / 契约 / 交互模版 / Harness;
|
|
* 资产版本管业务模型 / DMN / 技能 —— 包只携初始版,此后由进化管线在系统内产生。
|
|
*/
|
|
import { z } from 'zod';
|
|
import { activityIdSchema, skillIdSchema } from './id.ts';
|
|
import { jsonSchemaSchema, sevenPartContractSchema } from './contract7.ts';
|
|
import { listenerSpecSchema } from './envelope.ts';
|
|
import { objectDeclarationSchema } from './ledger.ts';
|
|
|
|
/* ── ① 身份 ─────────────────────────────────────────────────────────── */
|
|
|
|
export const manifestIdentitySchema = z.object({
|
|
activityId: activityIdSchema,
|
|
name: z.string().min(1),
|
|
packageVersion: z.string().min(1),
|
|
/**
|
|
* 业务分类(一级,组织自定)—— 活动库唯一的一维。
|
|
* 「活动类型(标准/案例)」那一维 2026-07-29 撤销:活动不再分范式,**一律 loop**。
|
|
*/
|
|
category: z.string().min(1),
|
|
sdkVersion: z.string().min(1),
|
|
checksum: z.string().min(1),
|
|
signature: z.string().optional(),
|
|
});
|
|
export type ManifestIdentity = z.infer<typeof manifestIdentitySchema>;
|
|
|
|
/* ── ③ 声明 ─────────────────────────────────────────────────────────── */
|
|
|
|
export const harnessConfigSchema = z.object({
|
|
/** 记忆窗口:每次 LLM 调用加载多少轮会话历史 —— 活动侧唯一记忆接口(AM 09) */
|
|
memoryWindow: z.number().int().min(1).max(200).default(20),
|
|
/** 停滞检测:多久无推进算空转(ISO-8601 duration) */
|
|
stallAfter: z.string().min(2).default('PT30M'),
|
|
/** 时限阶梯 */
|
|
nudge: z.array(z.object({ after: z.string().min(2), action: z.enum(['nudge', 'escalate']) })).default([]),
|
|
/** 步失败的默认重试(flow 每步可覆盖) */
|
|
retry: z.object({ times: z.number().int().min(0).max(10).default(2), backoff: z.string().default('PT30S') }).default({
|
|
times: 2,
|
|
backoff: 'PT30S',
|
|
}),
|
|
});
|
|
export type HarnessConfig = z.infer<typeof harnessConfigSchema>;
|
|
|
|
export const manifestDeclarationsSchema = z.object({
|
|
/** 模型声明:自定义模型名(逻辑名);安装静态审核对名称已绑定 */
|
|
models: z.array(z.string().min(1)).default([]),
|
|
/** 工具声明:调用契约引用 + 版本约束 */
|
|
tools: z.array(z.object({ name: z.string().min(1), version: z.string().optional() })).default([]),
|
|
/** 台账与共享域使用声明(域.对象.字段) */
|
|
ledger: z.array(objectDeclarationSchema).default([]),
|
|
/** 静态监听声明 —— 时限必配,悬空监听安装时拒绝 */
|
|
listeners: z.array(listenerSpecSchema).default([]),
|
|
harness: harnessConfigSchema,
|
|
});
|
|
export type ManifestDeclarations = z.infer<typeof manifestDeclarationsSchema>;
|
|
|
|
/* ── ④ 资产初始版 ───────────────────────────────────────────────────── */
|
|
|
|
/* loop 的业务模型:CMMN XML + 案例说明书 + DMN(AM 03) */
|
|
|
|
export const dmnDecisionTableSchema = z.object({
|
|
id: z.string().min(1),
|
|
label: z.string(),
|
|
hitPolicy: z.enum(['UNIQUE', 'FIRST', 'PRIORITY', 'COLLECT']).default('FIRST'),
|
|
inputs: z.array(z.object({ name: z.string().min(1), expression: z.string().min(1) })).min(1),
|
|
outputs: z.array(z.object({ name: z.string().min(1) })).min(1),
|
|
rules: z
|
|
.array(
|
|
z.object({
|
|
id: z.string().min(1),
|
|
/** 单元格谓词:S-FEEL(空串 = 「-」任意匹配) */
|
|
when: z.array(z.string()),
|
|
then: z.array(z.unknown()),
|
|
annotation: z.string().optional(),
|
|
}),
|
|
)
|
|
.min(1),
|
|
});
|
|
export type DmnDecisionTable = z.infer<typeof dmnDecisionTableSchema>;
|
|
|
|
export const dmnModelSchema = z.object({
|
|
/** DRD:声明「定什么需要知道什么」—— 决策 → 依赖的决策 / 输入项 */
|
|
drd: z.array(
|
|
z.object({
|
|
decision: z.string().min(1),
|
|
requires: z.array(z.string()).default([]),
|
|
inputs: z.array(z.string()).default([]),
|
|
}),
|
|
),
|
|
tables: z.array(dmnDecisionTableSchema),
|
|
});
|
|
export type DmnModel = z.infer<typeof dmnModelSchema>;
|
|
|
|
/**
|
|
* **步骤剧本** —— 一个计划项一条,把「这一步是什么状态」写成机器读得懂的东西。
|
|
*
|
|
* 为什么要有它(2026-07-31 加,起因是 A1 反复空转):
|
|
* 从前每一拍喂给模型的是**整本说明书 + 全部工具 + 全部可写台账**,
|
|
* 模型得自己从六步里认出「我现在在第几步」。认对了还好,认岔了就开始转圈 ——
|
|
* 实测 S1 已经把八字段写齐了,而说明书里 S3 写着「把八字段补到齐」,
|
|
* 模型于是把同样三行又写一遍,被「写了等于没写」拒回,下一拍再写一遍,如此往复。
|
|
*
|
|
* 治法不是把说明书写得更啰嗦,是**把状态挪进引擎**:
|
|
* ① `doneWhen` 让引擎自己算得出「这一步的成果已经在那儿了」—— 是就直接收掉,
|
|
* **不问模型**(能算的不问 LLM:同输入必同结论、零 token);
|
|
* ② `guide` / `writes` / `tools` 让每一拍的上下文**只装当前这一步要用的东西** ——
|
|
* 上下文小了,模型跑偏的余地也就小了。
|
|
*
|
|
* 不写剧本的活动照旧跑(整本说明书 + 全部工具),只是没有这两样好处。
|
|
*/
|
|
export const planStepSchema = z.object({
|
|
/** 对应 CMMN 里的计划项 id —— 对不上的条目安装期即拒 */
|
|
item: z.string().min(1),
|
|
/** 一句话:这一步交出什么 */
|
|
intent: z.string().min(1),
|
|
/** 这一步的做法(从说明书里搬过来的那一段)—— 只在轮到它时进上下文 */
|
|
guide: z.string().min(1),
|
|
/** 这一步可写哪几个台账地址「域.对象」;空 = 这一步不写台账 */
|
|
writes: z.array(z.string()).default([]),
|
|
/**
|
|
* 这一步**要读哪几个台账地址** —— 与 `writes` 对称,同样是「域.对象」。
|
|
*
|
|
* 为什么非声明不可:`域.对象` 整表那一段进提示词前会**按字符截断**
|
|
* (整表里躺着历史所有已放行单的行,只会越来越长,不截会把上下文吃光)。
|
|
* 截断是盲的 —— 砍掉的可能恰是这一步要用的那张表。实测:A1 第一步要拿发件邮箱
|
|
* 去 \`customer.contact\` 认客户,而这一单的来函正文与 BOM 附件就占了近 2000 字,
|
|
* 客户联系人表整个落在刀口外;模型看不见,只能报「主数据里没找到」,
|
|
* 于是老客户被当成新客户 —— **而库里那一行一直好端端在**。
|
|
*
|
|
* 声明了的表**排在最前且不参与截断**:这一步该看见的,一定看得见。
|
|
*/
|
|
reads: z.array(z.string()).default([]),
|
|
/**
|
|
* 这一步的**工作区画面**(视图模版名)—— 人办这一步时看的那份件。
|
|
*
|
|
* 不写就由模型临场挑。而「这一步该看哪张」是**写清单时就定死的事**:
|
|
* 阶段二派执行单,人要看的是那份询价受理需求;模型挑了「缺口清单」,
|
|
* 人拿到的就不是那份件了(实测:两份目标件都在包里,推出来的却是旧的那张)。
|
|
* 画面归活动定,不归模型临场发挥 —— 模型仍可用 `say.view` 换画面,
|
|
* 但**没换的时候按这里推**,而不是不推或推上一张。
|
|
*/
|
|
view: z.string().optional(),
|
|
/** 这一步可调哪几件工具;空 = 这一步不调工具 */
|
|
tools: z.array(z.string()).default([]),
|
|
/**
|
|
* **这一步算干完了没有** —— 对任务视图求值的 S-FEEL 谓词。
|
|
*
|
|
* 真了引擎就自己把这一项收掉,不问模型。所以它必须是「成果已存在」的判据,
|
|
* 不是「该不该做」的判据:写成 `missing_key_total = 0` 这种**开局就为真**的式子,
|
|
* 会让那一步在还没干之前就被收掉。
|
|
*
|
|
* **`X != null` 是个坑**:S-FEEL 里未绑定的名字 `!= null` 求值为 **true**,
|
|
* 写它等于让那一步开局即收。要判「这个键写过没有」,用正向式子(`X >= 0`);
|
|
* 布尔列更要绕开 —— 它合法值含 false,拿它分不清「没写」与「写了 false」,
|
|
* 该改判同一步写下的计数列。
|
|
*/
|
|
doneWhen: z.string().optional(),
|
|
/**
|
|
* **这一步派的卡穿哪副面孔**(2026-08-27)—— 由剧本声明、引擎决定,不靠模型抄卡骨架。
|
|
* `data` = 执行工单摆成数据工单;`review` = 决策卡摆成验收工单(中间审核,可循环;终局验收仍是 submit)。
|
|
*
|
|
* 起因:说明书的卡骨架里写着 `"variant":"data"`,模型抄卡时把它丢了(看着像元数据),
|
|
* 对话区于是照旧摆「执行工单」。与 `view` 同一条道理:这一步派的是什么单,是写剧本时就定死的事,
|
|
* 归活动声明、不归模型临场发挥;模型写不写都一样。
|
|
*/
|
|
cardVariant: z.enum(['data', 'review']).optional(),
|
|
/**
|
|
* **人工验收清单**(2026-08-27 用户定):这一步派出的验收卡(终局 `submit` 的验收工单,或
|
|
* `cardVariant:'review'` 的审核卡)上,人要走的核对步骤 —— 一条一句可核对的动作,对每份验收工单**固定**。
|
|
* 不是系统判的判据(那是 `acceptanceCriteria`,把提交门);是人拿着这份工单要亲自核什么。
|
|
* 卡上一条一枚「达标 / 不达标」开关,全部达标才能「确认通过」。
|
|
*/
|
|
acceptanceChecklist: z.array(z.string().min(1)).optional(),
|
|
});
|
|
export type PlanStep = z.infer<typeof planStepSchema>;
|
|
|
|
/**
|
|
* 阶段 —— **给人看的进度刻度**,引擎一眼都不看。
|
|
*
|
|
* 计划项是给引擎的:一件事拆到能判「干完没有」的粒度,A1 有六项。
|
|
* 可六格进度条对人没有意义 —— 「查历史价 / 打标 / 定去向」在业务上是**一件事**
|
|
* (判这单该怎么走),人关心的是「到哪一步了」,不是引擎拆了几项。
|
|
* 所以另立一层:几个计划项合成一个阶段,进度条按阶段走。
|
|
*
|
|
* **只声明激活态的阶段**。结束态不在这里 —— 每个活动都以放行收口(铁律③),
|
|
* 那是系统的规矩不是活动的阶段;进度条自己在末尾加这一格。
|
|
*/
|
|
export const casePhaseSchema = z.object({
|
|
/** 阶段名 —— 进度条上显示的就是它 */
|
|
name: z.string().min(1),
|
|
/** 这一阶段含哪几个计划项(CMMN item id);**全收完** = 这一阶段过了 */
|
|
items: z.array(z.string().min(1)).min(1),
|
|
});
|
|
export type CasePhase = z.infer<typeof casePhaseSchema>;
|
|
|
|
export const loopModelSchema = z.object({
|
|
/** 案例模型 · 机器面:CMMN XML,语义引擎以它为运行结构 */
|
|
cmmnXml: z.string().min(1),
|
|
/** 案例模型 · 叙事面:说明书,与 XML **同版本发布,不允许分叉**(AM 12.2) */
|
|
narrative: z.string().min(1),
|
|
dmn: dmnModelSchema,
|
|
/** 步骤剧本:一个计划项一条(见 `planStepSchema`);**不写照旧跑**(整本说明书 + 全部工具) */
|
|
steps: z.array(planStepSchema).optional(),
|
|
/** 阶段:进度条的刻度(见 `casePhaseSchema`);**不写就没有进度条**,不编 */
|
|
phases: z.array(casePhaseSchema).optional(),
|
|
});
|
|
export type LoopModel = z.infer<typeof loopModelSchema>;
|
|
|
|
/**
|
|
* 业务模型只有一种(2026-07-29 用户定:活动不再分 flow 和 loop,内部实现都是 Loop)——
|
|
* 案例模型 CMMN + 说明书 + DMN。步序声明(flow)连同它的引擎一并退役:
|
|
* 一件事该怎么办,由 Agent 依案例模型临场决定,不再有「照谱执行」这一路。
|
|
*/
|
|
export const businessModelSchema = loopModelSchema;
|
|
export type BusinessModel = z.infer<typeof businessModelSchema>;
|
|
|
|
/* 技能(AM 08):载方法不载权力 */
|
|
|
|
export const skillSchema = z.object({
|
|
id: skillIdSchema,
|
|
label: z.string().min(1),
|
|
/** 何时适用 —— 常驻上下文只有目录,按事加载正文 */
|
|
applicability: z.string().min(1),
|
|
/** 怎么判断、怎么编排工具 */
|
|
method: z.string().min(1),
|
|
/** 结晶片段脚本:反复出现的确定性子序列;按纯工具规矩注册留痕 */
|
|
crystallized: z
|
|
.array(z.object({ tool: z.string().min(1), input: z.record(z.string(), z.string()) }))
|
|
.default([]),
|
|
knowledgeRefs: z.array(z.string()).default([]),
|
|
version: z.string().min(1),
|
|
});
|
|
export type Skill = z.infer<typeof skillSchema>;
|
|
|
|
/** `域.对象` 两段地址(与 `ledgerAddressSchema` 同一套标识符规则,只是写成一串) */
|
|
const viewLedgerAddress = z.string().regex(/^[a-z][a-z0-9_]*\.[a-z][a-z0-9_]*$/, '台账地址写成 域.对象');
|
|
const viewColumn = z.string().regex(/^[a-z][a-z0-9_]*$/, '列名只准小写字母开头、字母数字下划线');
|
|
/** `case`(案号本身)或 `<as>.<列>`(前面某个集合那一列的值) */
|
|
const viewDataRef = z.string().min(1);
|
|
|
|
/**
|
|
* 运营中心沙箱看板的**取数声明**(2026-08-27):模版归活动,「哪几行是这一单的」也归活动 —— 案号前缀、
|
|
* 哪张表按哪一列圈,平台不该写死;基座只照声明逐表查**正式表**、把行集摊成扁平作用域交渲染器。
|
|
*
|
|
* 求值:先定案号 `case.prefix + 实例开案时刻 YYYYMMDDHHmmss`(与引擎注给模型铸主键的 `case:openedAt` 同一串);
|
|
* `rows` 按序求值,每条 = `ledger` 里 `by.column ∈ values(by.from)` 的行(`from` 只能引用 `case` 或前面已求出的
|
|
* 集合),可再用 `not` 排除一批、`orderBy` 排序。作用域里 `<as>` = 行数组、`<as>.<列>` = 首行标量、
|
|
* `<as 去符号>_rows` = 行数、`<as 去符号>_<列>_<值>` = tally 计数。案号那一行查无 → 这一单没落账,看板如实说。
|
|
*/
|
|
export const viewDataRowsSchema = z.object({
|
|
/** 作用域键(模版 `data-rows` 用它);与台账地址同名最省事,同一张表圈两批时另起名 */
|
|
as: z.string().min(1),
|
|
/** 必须在 `declarations.ledger` 里 */
|
|
ledger: viewLedgerAddress,
|
|
by: z.object({ column: viewColumn, from: viewDataRef }),
|
|
not: z.object({ column: viewColumn, from: viewDataRef }).optional(),
|
|
orderBy: z.array(z.object({ field: viewColumn, dir: z.enum(['asc', 'desc']) })).optional(),
|
|
});
|
|
export type ViewDataRows = z.infer<typeof viewDataRowsSchema>;
|
|
export const viewDataSchema = z.object({
|
|
case: z.object({ ledger: viewLedgerAddress, key: viewColumn, prefix: z.string() }),
|
|
rows: z.array(viewDataRowsSchema),
|
|
});
|
|
export type ViewData = z.infer<typeof viewDataSchema>;
|
|
/** `by.from` / `not.from` 里指案号本身的那个字 */
|
|
export const VIEW_DATA_CASE_REF = 'case';
|
|
|
|
export const viewTemplateSchema = z.object({
|
|
name: z.string().min(1),
|
|
html: z.string(),
|
|
/** 只有运营中心沙箱看板这类**按实例圈行**的模版才带;工单件不带(它们的行由引擎按草稿层给) */
|
|
data: viewDataSchema.optional(),
|
|
});
|
|
export type ViewTemplate = z.infer<typeof viewTemplateSchema>;
|
|
|
|
export const manifestAssetsSchema = z.object({
|
|
businessModel: businessModelSchema,
|
|
skills: z.array(skillSchema).default([]),
|
|
/** 视图 HTML 模版 —— 模版是内容不是代码,容器净化渲染、无脚本执行权 */
|
|
viewTemplates: z.array(viewTemplateSchema).default([]),
|
|
/** 卡内容 schema */
|
|
cardSchemas: z.record(z.string(), jsonSchemaSchema).default({}),
|
|
/** 蒸馏钩子:终态封卷 / 里程碑达成时从任务记忆归纳 */
|
|
distillHooks: z
|
|
.array(z.object({ on: z.enum(['terminal', 'milestone']), milestone: z.string().optional(), prompt: z.string() }))
|
|
.default([]),
|
|
});
|
|
export type ManifestAssets = z.infer<typeof manifestAssetsSchema>;
|
|
|
|
/* ── ⑤ 样例集 ───────────────────────────────────────────────────────── */
|
|
|
|
export const sampleCaseSchema = z.object({
|
|
name: z.string().min(1),
|
|
input: z.record(z.string(), z.unknown()),
|
|
message: z.string().optional(),
|
|
/** 期望输出口与载荷形状 —— 首装审与回归的依据 */
|
|
expect: z.object({
|
|
ports: z.array(z.string()).min(1),
|
|
payloadShape: jsonSchemaSchema.optional(),
|
|
}),
|
|
});
|
|
export type SampleCase = z.infer<typeof sampleCaseSchema>;
|
|
|
|
/* ── 清单六区合体 ───────────────────────────────────────────────────── */
|
|
|
|
export const activityManifestSchema = z.object({
|
|
identity: manifestIdentitySchema,
|
|
contract: sevenPartContractSchema,
|
|
declarations: manifestDeclarationsSchema,
|
|
assets: manifestAssetsSchema,
|
|
samples: z.array(sampleCaseSchema).min(1),
|
|
/** ⑥ 代码入口:.JS 产物的入口与校验和 */
|
|
entry: z.object({ file: z.string().min(1), checksum: z.string().min(1) }),
|
|
});
|
|
export type ActivityManifest = z.infer<typeof activityManifestSchema>;
|
|
|
|
/* ── 安装两审的报告形状(OS 11.2) ─────────────────────────────────── */
|
|
|
|
export const staticAuditFindingSchema = z.object({
|
|
code: z.enum([
|
|
'signature-invalid',
|
|
'sdk-incompatible',
|
|
'schema-invalid',
|
|
'tool-contract-missing',
|
|
'model-name-unbound',
|
|
'dangling-listener',
|
|
'template-has-script',
|
|
'environment-capability',
|
|
'dynamic-eval',
|
|
'ledger-type-conflict',
|
|
'acceptance-level-misreported',
|
|
'acceptance-predicate-unbound',
|
|
'deliverable-address-unbound',
|
|
'ledger-kind-invalid',
|
|
// refersTo 指向没声明的表/列 —— 引用检查会成「永远查无此值」的死墙(2026-08-24)
|
|
'reference-target-unbound',
|
|
]),
|
|
severity: z.enum(['error', 'warn']),
|
|
message: z.string(),
|
|
at: z.string().optional(),
|
|
});
|
|
export type StaticAuditFinding = z.infer<typeof staticAuditFindingSchema>;
|
|
|
|
export const replayAuditReportSchema = z.object({
|
|
/** 「旧成不坏」是生效硬门;「旧败能成」为报告参考项,不作门槛 */
|
|
oldPassStillPass: z.boolean(),
|
|
checked: z.number().int().min(0),
|
|
regressions: z.array(z.object({ taskId: z.string(), detail: z.string() })).default([]),
|
|
newlyBlocked: z.array(z.object({ taskId: z.string(), detail: z.string() })).default([]),
|
|
/** 首次安装无历史:免回放审,以静态审 + 样例集替代 */
|
|
skippedNoHistory: z.boolean().default(false),
|
|
});
|
|
export type ReplayAuditReport = z.infer<typeof replayAuditReportSchema>;
|
|
|
|
export const installVerdictSchema = z.object({
|
|
accepted: z.boolean(),
|
|
staticFindings: z.array(staticAuditFindingSchema),
|
|
replay: replayAuditReportSchema,
|
|
/**
|
|
* **这一版真的存进库了吗**(2026-08-27)—— 安装恒新增、同版重装是 no-op:两审过了、版本已在库,
|
|
* 什么都没写。从前这一维不回,安装脚本照样打印「活动包已装」,改了内容忘升版本的人以为装上了
|
|
* (同一天两个会话共用一个库,各自升到同一个版本号,后装的那份就这么静默丢了两次)。
|
|
* `false` = 库里已有同版,本次未重装;老调用方不读它也不受影响。
|
|
*/
|
|
stored: z.boolean().default(true),
|
|
});
|
|
export type InstallVerdict = z.infer<typeof installVerdictSchema>;
|