M4: 范式引擎与任务运行时

skill-engine(L4):载方法不载权力(白名单外工具即拒)、结晶片段经 ToolInvoker 留痕、
  装配入信封、技能目录渐进披露

flow-engine(L5):四种步型、确定性谓词跳步、重试与声明给定的兜底、
  异步步即监听(时限必配)、写类工具登记动作意向、步序位置持久

loop-engine(L5):自研 CMMN 子集解析器(表外构件直接报错,子集封顶可机器证明)、
  两副标准生命周期与迁移拓扑(引擎不得私改)、Enabled ≠ Active、
  语义性前提标待裁量评估(引擎不代裁量)、DMN 确定性求值 + 命中行留痕、
  DRD 缺口演绎与拓扑序、双向驱动与交集导航(断点即人环)、三型行动、
  终局唯放行与 Harness

task(L6):终态五名裁定(released 唯一成功)、Harness 预算环外强制与停滞熔断
  (监听中是健康态)、控制三语义(叫停不是终态,记最重档纠正)、改派留痕通知双方、
  断点续跑三份持久状态、封卷触发蒸馏钩子

301 个测试通过,门禁五道全绿

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KeDBx2PHLavbURXB8LJJ7m
This commit is contained in:
JCAI Main Agent
2026-07-27 01:51:22 +08:00
co-authored by Claude Opus 5
parent 2850e6d4ef
commit 3c974d2860
55 changed files with 7113 additions and 1 deletions
@@ -0,0 +1,27 @@
# module: skill-engine
职责: skill 范式:技能目录的渐进披露、装配前的扩权检查、结晶片段脚本(确定性动作链)的运行、
装配信封的戳
不负责: 技能的存放与版本(见 activity-registry,技能是活动的资产之一)、
工具执行(见 toolbox 的 ToolInvoker)、何时装配哪些技能(loop 临场 / flow 步引用)
公开出口: index.ts
- 类型:`SkillEngineService` `RunScriptInput` `ScriptOutcome` `SkillIndexEntry` `SkillStamp`
- 错误:`SkillPowerEscalationError`
- 端口类型:`SkillCatalog` `ScriptToolInvoker` `MemoryRecorder` `Clock`
- 用例工厂:`createSkillEngineService(deps): SkillEngineService`
- 纯函数:`checkSkillPower` `checkCatalog` `skillIndex` `stampOf` `crystallizedChain` `interpolate`
层级: L4(编码规范 §4;置于 L4 因 loop/flow 两范式依赖它 —— 临场装配 / 步引用,
import 恒向下、免 port)
依赖: @contracts;port:`SkillCatalog`(app 注入 activity-registry)、
`ScriptToolInvoker`(app 注入 toolbox)、`MemoryRecorder`(app 注入 memory)
被依赖: loop-engine, flow-engine, task
不变量(AM 08):
- **载方法,不载权力**:结晶片段引用白名单外的工具即 `SkillPowerEscalationError`;
技能不得扩大白名单、授权边界与禁区
- **脚本按纯工具规矩**:每步经 `ToolInvoker` 调用而留痕 —— 不得内联绕过,不得成为绕过留痕的暗道
- **装配入信封**:`stamp()` 给出技能 ID × 版本,记入任务记忆 —— 回放与归因依赖之
- **渐进披露**:常驻上下文只有 `index()` 的目录;正文按事 `load()`
- 技能**不是独立范式**:无自己的约束体系,一切继承本规范
@@ -0,0 +1,50 @@
/**
* module: skill-engine —— 对外类型(编码规范 §10.1;语义出处 AM 08)。
*
* 技能不是独立范式:没有自己的约束体系 —— 留痕、治理、权力边界与生命周期全部继承本规范。
* **内容软**(方法正文自由,用不用由 AI 决断),**边框硬**(装配入信封 · 脚本注册留痕 ·
* 不得扩权 · 版本过进化关口)。
*/
import type { Skill, TaskId } from '@contracts';
import type {
AssemblyContext,
CrystallizedStep,
SkillIndexEntry,
SkillStamp,
SkillViolation,
} from './internal/domain/skill-assembly.ts';
export type { AssemblyContext, CrystallizedStep, Skill, SkillIndexEntry, SkillStamp, SkillViolation };
export class SkillPowerEscalationError extends Error {
constructor(readonly violations: readonly SkillViolation[]) {
super(`技能试图扩权:${violations.map((v) => v.message).join('; ')}`);
this.name = 'SkillPowerEscalationError';
}
}
export type RunScriptInput = {
readonly taskId: TaskId;
readonly activityId: string;
readonly skillId: string;
/** 任务数据 —— 结晶片段的入参插值面 */
readonly scope: Readonly<Record<string, unknown>>;
readonly stepNo?: number;
};
export type ScriptOutcome = {
readonly ok: boolean;
readonly steps: ReadonlyArray<{ tool: string; ok: boolean; output?: Record<string, unknown>; error?: string }>;
readonly stamp: readonly SkillStamp[];
};
export type SkillEngineService = {
/** 常驻上下文只有目录 —— 渐进披露 */
index(activityId: string): Promise<SkillIndexEntry[]>;
/** 按事加载正文;装配前过扩权检查 */
load(activityId: string, skillIds: readonly string[]): Promise<Skill[]>;
/** 结晶片段脚本 = 确定性动作链;经 ToolInvoker 调用而留痕,不得内联绕过 */
runScript(input: RunScriptInput): Promise<ScriptOutcome>;
/** 装配入信封 —— 供任务记忆盖戳 */
stamp(activityId: string, skillIds: readonly string[]): Promise<SkillStamp[]>;
};
@@ -0,0 +1,29 @@
/**
* module: skill-engine —— 唯一出口。只做 re-export,不写逻辑(编码规范 §10.1)。
*/
export type {
AssemblyContext,
CrystallizedStep,
RunScriptInput,
ScriptOutcome,
Skill,
SkillEngineService,
SkillIndexEntry,
SkillStamp,
SkillViolation,
} from './contract.ts';
export { SkillPowerEscalationError } from './contract.ts';
export type { Clock, MemoryRecorder, ScriptToolInvoker, SkillCatalog } from './port.ts';
export type { SkillEngineDeps } from './internal/usecase/skill-engine-service.ts';
export { createSkillEngineService } from './internal/usecase/skill-engine-service.ts';
export {
checkCatalog,
checkSkillPower,
crystallizedChain,
interpolate,
skillIndex,
stampOf,
} from './internal/domain/skill-assembly.ts';
@@ -0,0 +1,91 @@
/**
* 技能装配 —— 纯逻辑,零 IO(编码规范 §5.3;语义出处 AM 08)。
*
* 「白名单回答『能用什么』(权力),技能回答『怎么用好』(方法)—— 棋子确定,棋谱是资产。」
*
* 三层软硬:
* 工具白名单 = 硬约束(权力,契约定)—— 不在名单调不动
* 技能清单 = 软资源(方法,包声明)—— AI 可用可不用,**可以越谱,不可越权**
* 临场路径 = 自由(由事决定,loop)—— 受上两层约束
*/
import type { Skill } from '@contracts';
export type AssemblyContext = {
/** 本活动的工具白名单 —— 硬约束 */
readonly allowlist: readonly string[];
/** 本活动可装配的技能集合 —— 软资源 */
readonly catalog: readonly Skill[];
};
export type SkillViolation = { readonly skillId: string; readonly tool: string; readonly message: string };
/**
* 载方法,不载权力(必须)——
* 「技能不得扩大白名单、授权边界与禁区;技能提及的工具不在本活动白名单者,**不可调**。」
*/
export function checkSkillPower(skill: Skill, allowlist: readonly string[]): SkillViolation[] {
return skill.crystallized
.filter((step) => !allowlist.includes(step.tool))
.map((step) => ({
skillId: skill.id,
tool: step.tool,
message: `技能 ${skill.label} 的结晶片段引用了白名单外的工具 ${step.tool} —— 载方法不载权力`,
}));
}
export function checkCatalog(context: AssemblyContext): SkillViolation[] {
return context.catalog.flatMap((skill) => checkSkillPower(skill, context.allowlist));
}
/** 技能目录:常驻上下文里只有它 —— 正文按事加载,引用按需下钻(渐进披露) */
export type SkillIndexEntry = { readonly id: string; readonly label: string; readonly applicability: string };
export function skillIndex(catalog: readonly Skill[]): SkillIndexEntry[] {
return catalog.map((s) => ({ id: s.id, label: s.label, applicability: s.applicability }));
}
/** 装配入信封:每步装配了哪些技能及其版本,记入任务记忆 —— 回放与归因依赖之 */
export type SkillStamp = { readonly id: string; readonly version: string };
export function stampOf(skills: readonly Skill[]): SkillStamp[] {
return skills.map((s) => ({ id: s.id, version: s.version }));
}
/**
* 结晶片段脚本 = 确定性动作链。
* 「脚本必须**注册进工具表、经运行时调用而留痕**、无环境能力;不得成为绕过留痕的暗道。」
*/
export type CrystallizedStep = { readonly tool: string; readonly input: Readonly<Record<string, string>> };
export function crystallizedChain(skill: Skill): CrystallizedStep[] {
return skill.crystallized.map((s) => ({ tool: s.tool, input: s.input }));
}
/** 入参插值:`{{path}}` 从任务数据取值 —— 纯字符串替换,零求值 */
export function interpolate(
input: Readonly<Record<string, string>>,
scope: Readonly<Record<string, unknown>>,
): Record<string, unknown> {
const out: Record<string, unknown> = {};
for (const [key, template] of Object.entries(input)) {
const exact = /^\{\{\s*([\w.]+)\s*\}\}$/.exec(template);
if (exact !== null) {
out[key] = resolvePath(scope, exact[1]!);
continue;
}
out[key] = template.replace(/\{\{\s*([\w.]+)\s*\}\}/g, (_, path: string) => {
const value = resolvePath(scope, path);
return value === null || value === undefined ? '' : String(value);
});
}
return out;
}
function resolvePath(scope: Readonly<Record<string, unknown>>, path: string): unknown {
let current: unknown = scope;
for (const segment of path.split('.')) {
if (current === null || current === undefined || typeof current !== 'object') return undefined;
current = (current as Record<string, unknown>)[segment];
}
return current;
}
@@ -0,0 +1,90 @@
/**
* skill-engine 用例 —— 编排 domain,依赖 port.ts(编码规范 §5.3)。
*/
import type { Skill } from '@contracts';
import type { RunScriptInput, ScriptOutcome, SkillEngineService } from '../../contract.ts';
import { SkillPowerEscalationError } from '../../contract.ts';
import type { Clock, MemoryRecorder, ScriptToolInvoker, SkillCatalog } from '../../port.ts';
import { checkSkillPower, crystallizedChain, interpolate, skillIndex, stampOf } from '../domain/skill-assembly.ts';
export type SkillEngineDeps = {
readonly catalog: SkillCatalog;
readonly tools: ScriptToolInvoker;
readonly memory: MemoryRecorder;
readonly clock: Clock;
};
export function createSkillEngineService(deps: SkillEngineDeps): SkillEngineService {
async function assemble(activityId: string, skillIds: readonly string[]): Promise<Skill[]> {
const [all, allowlist] = await Promise.all([
deps.catalog.skillsOf(activityId),
deps.catalog.allowlistOf(activityId),
]);
const picked = all.filter((s) => skillIds.includes(s.id));
// 载方法不载权力:装配前先查扩权
const violations = picked.flatMap((s) => checkSkillPower(s, allowlist));
if (violations.length > 0) throw new SkillPowerEscalationError(violations);
return picked;
}
return {
async index(activityId) {
return skillIndex(await deps.catalog.skillsOf(activityId));
},
async load(activityId, skillIds) {
return assemble(activityId, skillIds);
},
async stamp(activityId, skillIds) {
return stampOf(await assemble(activityId, skillIds));
},
/** 结晶片段脚本照谱执行 —— 每步经 ToolInvoker,留痕不可绕过 */
async runScript(input: RunScriptInput): Promise<ScriptOutcome> {
const [skill] = await assemble(input.activityId, [input.skillId]);
if (skill === undefined) {
return { ok: false, steps: [], stamp: [] };
}
const chain = crystallizedChain(skill);
const steps: Array<{ tool: string; ok: boolean; output?: Record<string, unknown>; error?: string }> = [];
let ok = true;
for (const [index, step] of chain.entries()) {
const payload = interpolate(step.input, input.scope);
await deps.memory.append({
taskId: input.taskId,
kind: 'action',
title: `技能 ${skill.label} · 结晶片段 ${index + 1}:${step.tool}`,
detail: { skillId: skill.id, version: skill.version, input: payload },
});
const result = await deps.tools.invoke({
tool: step.tool,
input: payload,
activityId: input.activityId,
taskId: input.taskId,
...(input.stepNo === undefined ? {} : { stepNo: input.stepNo * 100 + index }),
});
steps.push({
tool: step.tool,
ok: result.ok,
...(result.output === undefined ? {} : { output: result.output }),
...(result.error === undefined ? {} : { error: result.error.message }),
});
await deps.memory.append({
taskId: input.taskId,
kind: 'result',
title: `${step.tool} ${result.ok ? '成功' : '失败'}`,
detail: { output: result.output, error: result.error?.message },
});
if (!result.ok) {
ok = false;
break;
}
}
return { ok, steps, stamp: stampOf([skill]) };
},
};
}
@@ -0,0 +1,7 @@
{
"name": "@modules/skill-engine",
"version": "0.0.0",
"private": true,
"type": "module",
"exports": { ".": "./index.ts" }
}
+30
View File
@@ -0,0 +1,30 @@
/**
* module: skill-engine —— 本模块要外界提供的能力(编码规范 §10.1 / §5.3)。
*/
import type { Skill, TaskId } from '@contracts';
export type Clock = { now(): Date };
/** 技能目录 —— app 注入 `activity-registry`(L4 同层):技能是活动的资产之一 */
export type SkillCatalog = {
skillsOf(activityId: string): Promise<Skill[]>;
allowlistOf(activityId: string): Promise<string[]>;
};
/**
* 工具调用 —— app 注入 `toolbox`(L2)的 `ToolInvoker`。
* 脚本引用的工具**仍过本活动白名单与调用契约** —— 载方法不载权力。
*/
export type ScriptToolInvoker = {
invoke(input: {
tool: string;
input: Record<string, unknown>;
activityId: string;
taskId: TaskId;
stepNo?: number;
}): Promise<{ ok: boolean; output?: Record<string, unknown>; error?: { message: string } }>;
};
export type MemoryRecorder = {
append(input: { taskId: TaskId; kind: 'action' | 'result'; title: string; detail?: Record<string, unknown> }): Promise<void>;
};