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,322 @@
/**
* loop 回合驱动 —— 语义引擎 × Agent 回路(编码规范 §5.3 / §6.3;语义出处 AM 04)。
*
* 定位 → 择向 → 决断 → 行动 → 写数据 → 回到定位。
* **回合由 usecase 驱动,不放开自由 maxSteps;预算在环外由 Harness 强制。**
*/
import type { LoopModel, TaskId } from '@contracts';
import type { AgentAction, LoopEngineService, TurnOutcome } from '../../contract.ts';
import { ForbiddenTerminalError } from '../../contract.ts';
import type {
AgentPort,
CardGateway,
Clock,
ListenerRegistrar,
LoopStateStore,
MemoryRecorder,
Sfeel,
TaskContext,
TaskDataGateway,
ToolGateway,
} from '../../port.ts';
import { parseCmmn } from '../domain/cmmn-parse.ts';
import type { CaseRuntime } from '../domain/lifecycle.ts';
import {
autoCompleteReady,
initialRuntime,
navigate,
occur,
publishEnabled,
reenter as reenterItem,
transitionItem,
} from '../domain/lifecycle.ts';
import { evaluateTable, evaluationOrder } from '../domain/dmn.ts';
export type TurnRunnerDeps = {
readonly state: LoopStateStore;
readonly sfeel: Sfeel;
readonly data: TaskDataGateway;
readonly agent: AgentPort;
readonly tools: ToolGateway;
readonly cards: CardGateway;
readonly listeners: ListenerRegistrar;
readonly memory: MemoryRecorder;
readonly tasks: TaskContext;
readonly clock: Clock;
};
export function createTurnRunner(deps: TurnRunnerDeps): LoopEngineService {
async function loaded(taskId: TaskId) {
const found = await deps.state.load(taskId);
if (found === undefined) throw new Error(`loop 未装载:${taskId}`);
return found;
}
/** 变化面:任务视图里有值的键 —— onPart 的「件到」判据 */
function changedRefs(scope: Record<string, unknown>): Set<string> {
return new Set(Object.keys(scope).filter((k) => scope[k] !== undefined && scope[k] !== null));
}
async function publishFor(taskId: TaskId) {
const { model, parsed, runtime } = await loaded(taskId);
const scope = await deps.data.view(taskId);
const gaps = parsed.items
.filter((i) => !i.discretionary && runtime.items.find((r) => r.id === i.id)?.state !== 'Completed')
.map((i) => i.name);
return {
model,
parsed,
runtime,
scope,
enabled: publishEnabled(parsed, runtime, scope, changedRefs(scope), deps.sfeel.condition, gaps),
};
}
async function applyAction(taskId: TaskId, action: AgentAction, runtime: CaseRuntime): Promise<{ runtime: CaseRuntime; status: TurnOutcome['status']; reason?: string }> {
const context = await deps.tasks.of(taskId);
switch (action.kind) {
case 'start': {
// manualStart —— 进入 Active 永远出自 Agent 的裁量启动,留痕
await deps.memory.append({
taskId,
kind: 'engine-transition',
title: `manualStart:${action.itemId}`,
detail: { by: 'agent' },
});
return { runtime: transitionItem(runtime, action.itemId, 'Active'), status: 'running' };
}
case 'plan': {
await deps.memory.append({ taskId, kind: 'engine-transition', title: `规划裁量项入场:${action.itemId}` });
return { runtime: transitionItem(runtime, action.itemId, 'Enabled'), status: 'running' };
}
case 'disable': {
await deps.memory.append({
taskId,
kind: 'engine-transition',
title: `弃用计划项:${action.itemId}`,
detail: { reason: action.reason },
});
return { runtime: transitionItem(runtime, action.itemId, 'Disabled'), status: 'running' };
}
case 'judge': {
// 语义性前提的裁量评估 —— 评估留痕,可回放可归因
await deps.memory.append({
taskId,
kind: 'evaluation',
title: `语义性前提评估:${action.criterion} → ${action.satisfied ? '满足' : '未满足'}`,
detail: { rationale: action.rationale },
});
return { runtime, status: 'running' };
}
case 'tool': {
// 写类工具登记为动作意向 —— 触达世界的动作集中在放行时落地
if (await deps.tools.isWriteEffect(action.tool)) {
await deps.tools.registerIntent({
taskId,
tool: action.tool,
input: action.input,
label: `${action.itemId}:${action.tool}`,
});
return { runtime: transitionItem(runtime, action.itemId, 'Completed'), status: 'running' };
}
const result = await deps.tools.invoke({
tool: action.tool,
input: action.input,
activityId: context.activityId,
taskId,
});
await deps.memory.append({
taskId,
kind: 'result',
title: `${action.tool} ${result.ok ? '返回' : '失败'}`,
detail: { output: result.output, error: result.error?.message },
});
return {
runtime: transitionItem(runtime, action.itemId, result.ok ? 'Completed' : 'Failed'),
status: 'running',
};
}
case 'human': {
// 调人 = 立监听听人:调用即 Active(阻塞),回传驱动前提
const card = await deps.cards.issue({
taskId,
ownerId: context.ownerId,
humanCallCap: context.humanCallCap,
workOrder: action.workOrder,
card: action.card,
});
await deps.listeners.register({
taskId,
eventType: 'human',
keyFilter: { cardId: card.id },
deadline: 'P3D',
});
return { runtime: transitionItem(runtime, action.itemId, 'Active'), status: 'awaiting' };
}
case 'listen': {
// AI 不能调用世界:对世界只有两种合法姿势 —— 触达它(动作)与等它(监听)
await deps.listeners.register({
taskId,
eventType: action.eventType,
keyFilter: action.keyFilter,
deadline: action.deadline,
});
await deps.memory.append({
taskId,
kind: 'engine-transition',
title: `立监听:${action.eventType}(时限 ${action.deadline})`,
detail: action.keyFilter,
});
return { runtime, status: 'awaiting' };
}
case 'submit': {
await deps.memory.append({
taskId,
kind: 'thought',
title: '判定成果已齐,提交放行',
detail: { rationale: action.rationale },
});
// 终局唯放行:引擎不把案计划置为 Completed —— 那是 release 的事
return { runtime, status: 'submitted' };
}
}
}
return {
async load(taskId, model: LoopModel) {
const parsed = parseCmmn(model.cmmnXml);
const runtime = initialRuntime(parsed);
await deps.state.save(taskId, model, parsed, runtime);
await deps.memory.append({
taskId,
kind: 'engine-transition',
title: `装载案例模型:${parsed.name}(${parsed.items.length} 个计划项)`,
});
return runtime;
},
async runtime(taskId) {
return (await deps.state.load(taskId))?.runtime;
},
async publish(taskId) {
return (await publishFor(taskId)).enabled;
},
/** 一个回合:定位 → 择向 → 决断 → 行动 → 写数据 */
async runTurn(taskId): Promise<TurnOutcome> {
const { model, parsed, runtime, scope, enabled } = await publishFor(taskId);
// 齐了即收:必做项完成且无进行中项 → 提交放行(仍必经放行卡)
if (autoCompleteReady(parsed, runtime)) {
return { runtime, enabled, navigation: { kind: 'converged' }, status: 'submitted' };
}
// 导航即交集:当下可为 ∩ 目标所需
const navigation = navigate(enabled, new Set(enabled.entries.map((e) => e.id)));
if (navigation.kind === 'breakpoint') {
// 断点即人环 —— 由 Agent 定夺调人还是立监听,判定留痕
await deps.memory.append({
taskId,
kind: 'thought',
title: '依赖断点:当下可为 ∩ 目标所需 = 空',
detail: { gaps: navigation.gaps },
});
}
const narrative = [
model.narrative,
...(await deps.memory.loadActivityMemory((await deps.tasks.of(taskId)).activityId, 20)).map(
(m) => `[${m.layer}] ${m.statement}`,
),
].join('\n\n');
const action = await deps.agent.decide({ taskId, narrative, enabled, scope });
// AI 不得产生终局迁移
if (action.kind === 'start') {
const item = parsed.items.find((i) => i.id === action.itemId);
if (item === undefined) throw new ForbiddenTerminalError('owner');
}
const applied = await applyAction(taskId, action, runtime);
await deps.state.save(taskId, model, parsed, applied.runtime);
return {
runtime: applied.runtime,
enabled,
navigation,
action,
status: applied.status,
...(applied.reason === undefined ? {} : { blockedReason: applied.reason }),
};
},
/** 事件驱动续跑:挂起期间事件不丢,恢复即补投 */
async onEvent(taskId, event) {
const { model, parsed, runtime } = await loaded(taskId);
await deps.memory.append({
taskId,
kind: 'observation',
title: `事件到达(${event.kind}):${event.ref}`,
detail: event.payload,
});
let next = runtime;
// 里程碑与事件监听器只有一条主迁移:Available →(occur)→ Completed
const listener = parsed.items.find((i) => i.id === event.ref && i.type.endsWith('EventListener'));
if (listener !== undefined) next = occur(runtime, listener.id);
await deps.state.save(taskId, model, parsed, next);
// 前提前推:事件写数据 → 任务视图变化 → 前提求值 → 新项 Enabled
return this.runTurn(taskId);
},
/** 能算的不问 LLM:输入齐备的决策表确定性求值,命中行留痕 */
async decide(taskId, decision) {
const { model } = await loaded(taskId);
const scope = await deps.data.view(taskId);
const order = evaluationOrder(model.dmn, decision);
const enriched: Record<string, unknown> = { ...scope };
let last = undefined as ReturnType<typeof evaluateTable> | undefined;
for (const step of order) {
const table = model.dmn.tables.find((t) => t.id === step);
if (table === undefined) continue;
const result = evaluateTable(table, enriched, deps.sfeel.expression, deps.sfeel.unaryTest);
last = result;
if (result.kind !== 'decided') break;
Object.assign(enriched, result.trace.outputs);
await deps.memory.append({
taskId,
kind: 'evaluation',
title: `DMN 求值:${table.label} → 命中 ${result.trace.hitRules.join('、')}`,
detail: { inputs: result.trace.inputs, outputs: result.trace.outputs, table: table.id },
});
}
return last ?? { kind: 'no-match', inputs: {} };
},
/** 驳回:相关计划项以新实例重新入场(repetitionRule),记为纠正信号 */
async reenter(taskId, itemId) {
const { model, parsed, runtime } = await loaded(taskId);
const next = reenterItem(runtime, itemId);
await deps.state.save(taskId, model, parsed, next);
await deps.memory.append({
taskId,
kind: 'engine-transition',
title: `驳回重入:${itemId}(新实例)`,
});
return next;
},
};
}