forked from ChaiHao/BPOS-MVP
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,279 @@
|
||||
/**
|
||||
* flow 运行器 —— 照谱执行,零临场调度(编码规范 §5.3;语义出处 AM 05)。
|
||||
*/
|
||||
import type { FlowStep, TaskId } from '@contracts';
|
||||
import type { AdvanceOutcome, FlowEngineService } from '../../contract.ts';
|
||||
import { FlowDeclarationInvalidError } from '../../contract.ts';
|
||||
import type {
|
||||
CardGateway,
|
||||
Clock,
|
||||
FlowStateStore,
|
||||
ListenerRegistrar,
|
||||
LlmStepGateway,
|
||||
MemoryRecorder,
|
||||
PredicateEvaluator,
|
||||
TaskContext,
|
||||
TaskDataGateway,
|
||||
ToolStepInvoker,
|
||||
} from '../../port.ts';
|
||||
import type { FlowPosition } from '../domain/step-sequence.ts';
|
||||
import {
|
||||
checkDeclaration,
|
||||
failureAction,
|
||||
initialPosition,
|
||||
markStep,
|
||||
nextAction,
|
||||
} from '../domain/step-sequence.ts';
|
||||
|
||||
export type FlowRunnerDeps = {
|
||||
readonly state: FlowStateStore;
|
||||
readonly evaluate: PredicateEvaluator;
|
||||
readonly data: TaskDataGateway;
|
||||
readonly tools: ToolStepInvoker;
|
||||
readonly llm: LlmStepGateway;
|
||||
readonly cards: CardGateway;
|
||||
readonly listeners: ListenerRegistrar;
|
||||
readonly memory: MemoryRecorder;
|
||||
readonly tasks: TaskContext;
|
||||
readonly clock: Clock;
|
||||
};
|
||||
|
||||
export function createFlowRunner(deps: FlowRunnerDeps): FlowEngineService {
|
||||
async function loaded(taskId: TaskId) {
|
||||
const found = await deps.state.load(taskId);
|
||||
if (found === undefined) throw new Error(`flow 未装载:${taskId}`);
|
||||
return found;
|
||||
}
|
||||
|
||||
/** 出参写入落**草稿层**(草稿态);放行前不落事实 */
|
||||
async function writeOutput(taskId: TaskId, step: FlowStep, output: Record<string, unknown>): Promise<void> {
|
||||
if (step.output === undefined) return;
|
||||
const patch: Record<string, unknown> = {};
|
||||
for (const [field, path] of Object.entries(step.output.fields)) {
|
||||
patch[field] = output[path] ?? output[field];
|
||||
}
|
||||
await deps.data.draftWrite({
|
||||
taskId,
|
||||
domain: step.output.domain,
|
||||
object: step.output.object,
|
||||
key: {},
|
||||
patch,
|
||||
});
|
||||
}
|
||||
|
||||
function inputOf(step: FlowStep, scope: Record<string, unknown>): Record<string, unknown> {
|
||||
const out: Record<string, unknown> = {};
|
||||
for (const [key, path] of Object.entries(step.input)) {
|
||||
out[key] = path.split('.').reduce<unknown>((acc, seg) => {
|
||||
if (acc === null || acc === undefined || typeof acc !== 'object') return undefined;
|
||||
return (acc as Record<string, unknown>)[seg];
|
||||
}, scope);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
async function runOne(
|
||||
taskId: TaskId,
|
||||
step: FlowStep,
|
||||
index: number,
|
||||
position: FlowPosition,
|
||||
): Promise<AdvanceOutcome> {
|
||||
const context = await deps.tasks.of(taskId);
|
||||
const scope = await deps.data.view(taskId);
|
||||
const payload = inputOf(step, scope);
|
||||
const attempt = (position.steps[index]?.attempt ?? 0) + 1;
|
||||
|
||||
await deps.memory.append({
|
||||
taskId,
|
||||
kind: 'action',
|
||||
title: `步 ${index + 1} · ${step.label}(${step.kind})`,
|
||||
detail: { step: step.name, attempt, input: payload },
|
||||
});
|
||||
|
||||
switch (step.kind) {
|
||||
case 'tool':
|
||||
case 'pure-tool': {
|
||||
// 写类工具在 flow 里同样登记为动作意向 —— 触达世界的动作集中在放行时落地
|
||||
if (step.kind === 'tool' && (await deps.tools.isWriteEffect(step.tool))) {
|
||||
await deps.tools.registerIntent({ taskId, tool: step.tool, input: payload, label: step.label });
|
||||
const next = markStep(position, index, { state: 'done', attempt });
|
||||
await deps.state.save(taskId, (await loaded(taskId)).model, next);
|
||||
return { position: next, status: 'running' };
|
||||
}
|
||||
const result = await deps.tools.invoke({
|
||||
tool: step.tool,
|
||||
input: payload,
|
||||
activityId: context.activityId,
|
||||
taskId,
|
||||
stepNo: index,
|
||||
});
|
||||
if (!result.ok) return handleFailure(taskId, step, index, position, attempt, result.error?.message ?? '工具失败');
|
||||
if (result.output !== undefined) await writeOutput(taskId, step, result.output);
|
||||
const next = markStep(position, index, { state: 'done', attempt, ...(result.output === undefined ? {} : { output: result.output }) });
|
||||
await deps.state.save(taskId, (await loaded(taskId)).model, next);
|
||||
return { position: next, status: 'running' };
|
||||
}
|
||||
|
||||
case 'llm': {
|
||||
// LLM 只以「被调用的工具」出现,不是调度者;模板与出参 schema 随声明版本冻结
|
||||
const prompt = step.template.replace(/\{\{\s*([\w.]+)\s*\}\}/g, (_, path: string) => String(payload[path] ?? ''));
|
||||
const result = await deps.llm.generate({
|
||||
model: step.model,
|
||||
prompt,
|
||||
outputSchema: step.outputSchema,
|
||||
taskId,
|
||||
stepNo: index,
|
||||
});
|
||||
if (!result.ok) return handleFailure(taskId, step, index, position, attempt, result.error ?? 'LLM 步失败');
|
||||
const output = (result.object ?? {}) as Record<string, unknown>;
|
||||
await writeOutput(taskId, step, output);
|
||||
const next = markStep(position, index, { state: 'done', attempt, output });
|
||||
await deps.state.save(taskId, (await loaded(taskId)).model, next);
|
||||
return { position: next, status: 'running' };
|
||||
}
|
||||
|
||||
case 'human': {
|
||||
// 异步步:完成条件 = 人事件;时限必配
|
||||
const card = await deps.cards.issue({
|
||||
taskId,
|
||||
ownerId: context.ownerId,
|
||||
humanCallCap: context.humanCallCap,
|
||||
workOrder: step.workOrder,
|
||||
card: step.card,
|
||||
});
|
||||
await deps.listeners.register({
|
||||
taskId,
|
||||
eventType: 'human',
|
||||
keyFilter: { cardId: card.id },
|
||||
deadline: step.deadline,
|
||||
});
|
||||
const next = markStep(position, index, { state: 'awaiting', attempt });
|
||||
await deps.state.save(taskId, (await loaded(taskId)).model, next);
|
||||
return {
|
||||
position: next,
|
||||
status: 'awaiting',
|
||||
waitingFor: { kind: 'human', ref: card.id, deadline: step.deadline },
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 失败路径**声明给定**,不临场发明 */
|
||||
async function handleFailure(
|
||||
taskId: TaskId,
|
||||
step: FlowStep,
|
||||
index: number,
|
||||
position: FlowPosition,
|
||||
attempt: number,
|
||||
error: string,
|
||||
): Promise<AdvanceOutcome> {
|
||||
const action = failureAction(step, attempt);
|
||||
await deps.memory.append({
|
||||
taskId,
|
||||
kind: 'result',
|
||||
title: `步 ${index + 1} 失败:${error} → ${action.kind}`,
|
||||
detail: { step: step.name, attempt, action },
|
||||
});
|
||||
|
||||
if (action.kind === 'retry') {
|
||||
const next = markStep(position, index, { state: 'pending', attempt, error });
|
||||
await deps.state.save(taskId, (await loaded(taskId)).model, next);
|
||||
return { position: next, status: 'running' };
|
||||
}
|
||||
|
||||
if (action.kind === 'human') {
|
||||
const context = await deps.tasks.of(taskId);
|
||||
const card = await deps.cards.issue({
|
||||
taskId,
|
||||
ownerId: context.ownerId,
|
||||
humanCallCap: context.humanCallCap,
|
||||
workOrder: action.workOrder,
|
||||
card: { title: `步「${step.label}」重试仍败,转人兜底`, reason: error },
|
||||
});
|
||||
await deps.listeners.register({
|
||||
taskId,
|
||||
eventType: 'human',
|
||||
keyFilter: { cardId: card.id },
|
||||
deadline: 'P3D',
|
||||
});
|
||||
const next = markStep(position, index, { state: 'awaiting', attempt, error });
|
||||
await deps.state.save(taskId, (await loaded(taskId)).model, next);
|
||||
return { position: next, status: 'awaiting', waitingFor: { kind: 'human', ref: card.id, deadline: 'P3D' } };
|
||||
}
|
||||
|
||||
const next = markStep(position, index, { state: 'failed', attempt, error });
|
||||
await deps.state.save(taskId, (await loaded(taskId)).model, next);
|
||||
return { position: next, status: 'blocked', blockedReason: error };
|
||||
}
|
||||
|
||||
return {
|
||||
async load(taskId, model) {
|
||||
const issues = checkDeclaration(model.steps);
|
||||
if (issues.length > 0) throw new FlowDeclarationInvalidError(issues);
|
||||
const position = initialPosition(model.steps);
|
||||
await deps.state.save(taskId, model, position);
|
||||
await deps.memory.append({
|
||||
taskId,
|
||||
kind: 'engine-transition',
|
||||
title: `装载步序声明:${model.steps.length} 步`,
|
||||
});
|
||||
return position;
|
||||
},
|
||||
|
||||
async position(taskId) {
|
||||
return (await deps.state.load(taskId))?.position;
|
||||
},
|
||||
|
||||
async advance(taskId) {
|
||||
const { model, position } = await loaded(taskId);
|
||||
const scope = await deps.data.view(taskId);
|
||||
const action = nextAction(model.steps, position, scope, deps.evaluate);
|
||||
|
||||
switch (action.kind) {
|
||||
case 'finished': {
|
||||
// 末步是验收工单时,终态由其裁决给出;此处只报「谱走完」
|
||||
return { position, status: 'finished', released: false };
|
||||
}
|
||||
case 'skip': {
|
||||
await deps.memory.append({
|
||||
taskId,
|
||||
kind: 'engine-transition',
|
||||
title: `跳步:${action.step.label} —— ${action.reason}`,
|
||||
});
|
||||
const next = markStep(position, action.index, { state: 'skipped' });
|
||||
await deps.state.save(taskId, model, next);
|
||||
return { position: next, status: 'running' };
|
||||
}
|
||||
case 'await':
|
||||
return {
|
||||
position,
|
||||
status: 'awaiting',
|
||||
waitingFor: { kind: 'event', ref: action.step.name, deadline: 'P3D' },
|
||||
};
|
||||
case 'run':
|
||||
return runOne(taskId, action.step, action.index, position);
|
||||
}
|
||||
},
|
||||
|
||||
/** 事件即步的完成条件 —— 到达即续跑,挂起期间事件不丢 */
|
||||
async resume(taskId, event) {
|
||||
const { model, position } = await loaded(taskId);
|
||||
const index = position.steps.findIndex((s) => s.state === 'awaiting');
|
||||
if (index < 0) return { position, status: 'running' };
|
||||
|
||||
const step = model.steps[index];
|
||||
await deps.memory.append({
|
||||
taskId,
|
||||
kind: 'engine-transition',
|
||||
title: `事件到达,步 ${index + 1} 完成条件满足(${event.kind})`,
|
||||
detail: { ref: event.ref },
|
||||
});
|
||||
if (step !== undefined && event.kind === 'human') {
|
||||
await writeOutput(taskId, step, event.payload);
|
||||
}
|
||||
const next = markStep(position, index, { state: 'done' });
|
||||
await deps.state.save(taskId, model, next);
|
||||
return { position: next, status: 'running' };
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user