Archived
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:
co-authored by
Claude Opus 5
parent
2850e6d4ef
commit
3c974d2860
@@ -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" }
|
||||
}
|
||||
@@ -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>;
|
||||
};
|
||||
Reference in New Issue
Block a user