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,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]) };
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user