This repository has been archived on 2026-09-13. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
BPOS-MVP/apps/api/src/app/wiring/engines.ts
T
ChaiHaoandClaude Opus 5 691c61b6b8 选择组的出口选择要走到落地:恰发一口,随放行一并确认
A6 的输出是**选择组**(negotiation: won / revise / lost),回传协议里本来就有
`selectedPorts`,human-tool 也接住了 —— 但落地那一步硬写 `selectedPorts: {}`,
选择从未抵达。结果:放行了、台账写了,却「发射输出口:(无)」,
G4 异或网关拿不到 outcome,整条流悄无声息地停住。

按 verdict 同样的路子把它串起来:人事件载荷 → 末步运行时 → 谱走完时报上去 →
落地收敛时照它发射。

gates: tsc / eslint / depcruise / 399 测试 全绿

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KeDBx2PHLavbURXB8LJJ7m
2026-07-27 05:34:13 +08:00

686 lines
27 KiB
TypeScript

/**
* 范式引擎与任务运行时的装配 —— app 是唯一组装层(编码规范 §5.2)。
*
* 这里把 M4/M5 的模块用**端口**接起来:跨层协作一律经端口,模块之间不互相认识。
*/
import type { Db } from '@shared/db.ts';
import type { LandedFacts } from '@modules/task';
import type { EventService } from '@modules/event';
import type { LedgerService } from '@modules/ledger';
import type { MemoryService } from '@modules/memory';
import type { ToolboxService, ToolInvoker } from '@modules/toolbox';
import type { HumanToolService } from '@modules/human-tool';
import type { InteractionService } from '@modules/interaction';
import type { MessageService } from '@modules/message';
import type { OrgService } from '@modules/org';
import type { LlmService, ModelGateway } from '@modules/llm';
import type { ActivityRegistryService } from '@modules/activity-registry';
import type { ReleaseService } from '@modules/release';
import { createFlowRunner, createFlowStateStore } from '@modules/flow-engine';
import { createLoopStateStore, createTurnRunner } from '@modules/loop-engine';
import { createSkillEngineService } from '@modules/skill-engine';
import { createTaskService, createTaskStore } from '@modules/task';
import type { TaskService } from '@modules/task';
import { createProcessService, createProcessStore } from '@modules/process';
import type { ProcessService } from '@modules/process';
import { createTriggerService, createTriggerStore } from '@modules/trigger';
import type { TriggerService } from '@modules/trigger';
import { evaluate as evalSfeel, evaluateCondition, evaluateUnaryTest } from '@sfeel';
import type { StaffId, TaskId } from '@contracts';
import { createAgentRunner } from '../agent-runner.ts';
import type { Clock, IdGenerator } from '../runtime.ts';
export type EngineWiringDeps = {
readonly db: Db;
readonly clock: Clock;
readonly ids: IdGenerator;
readonly event: EventService;
readonly ledger: LedgerService;
readonly memory: MemoryService;
readonly toolbox: ToolboxService;
readonly toolInvoker: ToolInvoker;
readonly humanTool: HumanToolService;
readonly interaction: InteractionService;
readonly message: MessageService;
readonly org: OrgService;
readonly llm: LlmService;
readonly modelGateway: ModelGateway;
readonly registry: ActivityRegistryService;
readonly release: ReleaseService;
readonly schemas: { validate(schema: Record<string, unknown>, value: unknown): { ok: boolean; errors: string[] } };
};
export type EngineWiring = {
readonly task: TaskService;
readonly process: ProcessService;
readonly trigger: TriggerService;
};
function writeLog(line: string): void {
globalThis.process.stderr.write(`${line}\n`);
}
/** S-FEEL 三件套 —— 求值异常按「不成立」处置,不让流转停摆 */
const sfeel = {
condition: (expression: string, scope: Readonly<Record<string, unknown>>): boolean => {
try {
return evaluateCondition(expression, scope);
} catch {
return false;
}
},
expression: (expression: string, scope: Readonly<Record<string, unknown>>): unknown => {
try {
return evalSfeel(expression, scope);
} catch {
return undefined;
}
},
unaryTest: (cell: string, value: unknown, scope: Readonly<Record<string, unknown>>): boolean => {
try {
return evaluateUnaryTest(cell, value, scope);
} catch {
return false;
}
},
};
/** 延后绑定的流程层引用 —— 只用到「任务终态上报」这一件 */
function createDeferredProcess(): {
onTaskTerminal: ProcessService['onTaskTerminal'];
bind(real: ProcessService): void;
} {
let real: ProcessService | undefined;
return {
onTaskTerminal: async (taskId, terminal, ports) => real?.onTaskTerminal(taskId, terminal, ports),
bind(next) {
real = next;
},
};
}
export function wireEngines(deps: EngineWiringDeps): EngineWiring {
const { db, clock, ids } = deps;
/* ── 任务上下文:活动 ID / 负责人 / 调人预算 —— 引擎共用 ─────────── */
const taskStore = createTaskStore(db);
const taskContext = {
async of(taskId: TaskId) {
const task = await taskStore.get(taskId);
if (task === undefined) throw new Error(`任务不存在:${taskId}`);
return { activityId: task.activityId, ownerId: task.ownerId, humanCallCap: task.caps.humanCalls };
},
};
/* ── 任务数据:引擎读**任务视图** = 正式版 + 本任务草稿层 ───────── */
const taskData = {
/**
* 任务视图 = 正式版 + 本任务草稿层(AM 4.2),再叠上起单入参。
*
* 作用域的**形状要对得上声明的写法**:步声明里写的是裸字段名
* (`input: { pn: 'pn' }`),不是 `域.对象.行号.字段`。所以这里三层铺开:
* ① 起单入参在底 —— 契约的 data 是这单的出发事实;
* ② 各对象**最新一行**的字段名铺到顶层 —— 过程工作区的数据形态即草稿层;
* ③ `域.对象` 仍保留整表,要整表的照样取得到。
* 顶层同名以「后写的赢」为准:草稿层是这一单最新的过程事实,盖过起单入参。
*/
async view(taskId: TaskId) {
const task = await taskStore.get(taskId);
if (task === undefined) throw new Error(`任务不存在:${taskId}`);
const objects = await deps.ledger.listObjects();
const scope: Record<string, unknown> = { ...task.data };
for (const address of objects) {
const page = await deps.ledger.taskView(taskId, {
address,
where: {},
orderBy: [],
limit: 200,
offset: 0,
});
scope[`${address.domain}.${address.object}`] = page.rows;
}
/*
* 铺到顶层的只能是**这一单自己写下的**事实(草稿层 = 读己之写)。
* 不能拿「该对象最新一行」来铺:那一行很可能是别的任务放行后的正式版记录,
* 一铺就把别人的结论当成了自己的输入 —— 打标会照着上一单的情报出结果。
* 别人的事实仍在 `域.对象` 整表里,要用得显式去取。
*/
for (const draft of await deps.ledger.taskDrafts(taskId)) {
for (const [field, value] of Object.entries({ ...draft.key, ...draft.patch })) {
if (value !== null && value !== undefined) scope[field] = value;
}
}
return scope;
},
async draftWrite(input: {
taskId: TaskId;
domain: string;
object: string;
key: Record<string, string | number>;
patch: Record<string, unknown>;
}) {
// 一切过程写入落草稿层 —— 放行前不落事实
await deps.ledger.draftWrite({
taskId: input.taskId,
address: { domain: input.domain, object: input.object },
key: input.key,
patch: input.patch,
});
},
};
const memoryRecorder = {
append: async (input: { taskId: TaskId; kind: string; title: string; detail?: Record<string, unknown> }) => {
await deps.memory.append({
taskId: input.taskId,
kind: input.kind as never,
title: input.title,
...(input.detail === undefined ? {} : { detail: input.detail }),
stamp: { contractVersion: '1', businessModelVersion: '1', skillVersions: [] },
});
},
};
const listeners = {
async register(input: { taskId: TaskId; eventType: 'data' | 'time' | 'human'; keyFilter: Record<string, string>; deadline: string }) {
const listener = await deps.event.registerListener(input.taskId, {
origin: 'dynamic',
eventType: input.eventType,
keyFilter: input.keyFilter,
semantic: 'complete-step',
deadline: input.deadline,
escalation: ['nudge', 'escalate'],
});
return { id: listener.id };
},
};
const cards = {
async issue(input: {
taskId: TaskId;
ownerId: StaffId;
humanCallCap: number;
workOrder: 'execution' | 'acceptance';
card: Record<string, unknown>;
}) {
const context = { taskId: input.taskId, ownerId: input.ownerId, humanCallCap: input.humanCallCap };
const title = String(input.card['title'] ?? '请处理');
const basis = Object.entries(input.card)
.filter(([k]) => k !== 'title' && k !== 'fields')
.map(([k, v]) => ({ k, v: String(v) }));
if (input.workOrder === 'acceptance') {
const issued = await deps.humanTool.issueAcceptance(context, {
title,
basis: basis.length > 0 ? basis : [{ k: '来源', v: '步序声明' }],
deliverable: String(input.card['deliverable'] ?? 'deliverable') as never,
criteria: [{ label: '成果齐备', level: 'L5', pass: false }],
fields: [],
});
return { id: issued.order.id };
}
const fields = Array.isArray(input.card['fields'])
? (input.card['fields'] as string[]).map((key) => ({ key, label: key, kind: 'text' as const, required: true }))
: [{ key: 'result', label: '结果', kind: 'textarea' as const, required: true }];
const issued = await deps.humanTool.issueExecution(context, {
title,
basis: basis.length > 0 ? basis : [{ k: '来源', v: '步序声明' }],
ask: String(input.card['reason'] ?? title),
fields,
});
return { id: issued.order.id };
},
};
const toolGateway = {
invoke: async (input: { tool: string; input: Record<string, unknown>; activityId: string; taskId: TaskId; stepNo?: number }) => {
const result = await deps.toolInvoker.invoke({
tool: input.tool,
input: input.input,
activityId: input.activityId,
taskId: input.taskId,
...(input.stepNo === undefined ? {} : { stepNo: input.stepNo }),
});
return {
ok: result.ok,
...(result.output === undefined ? {} : { output: result.output }),
...(result.error === undefined ? {} : { error: { message: result.error.message, retryable: result.error.retryable } }),
};
},
isWriteEffect: async (tool: string) => (await deps.toolbox.get(tool))?.effect === 'write',
registerIntent: async (input: { taskId: TaskId; tool: string; input: Record<string, unknown>; label: string }) => {
// 写类动作登记为动作意向:入参冻结,放行后由系统执行落地
await deps.release.registerIntent(input.taskId, {
id: ids.next(),
tool: input.tool,
input: input.input,
idempotencyKey: `${input.taskId}:${input.tool}:${ids.next()}`,
label: input.label,
});
},
};
/* ── skill-engine ─────────────────────────────────────────────── */
const skills = createSkillEngineService({
catalog: {
skillsOf: async (activityId) => (await deps.registry.manifestOf(activityId)).assets.skills,
allowlistOf: (activityId) => deps.toolbox.allowlistOf(activityId),
},
tools: {
invoke: (input) =>
toolGateway.invoke({ ...input, ...(input.stepNo === undefined ? {} : { stepNo: input.stepNo }) }),
},
memory: memoryRecorder as never,
clock,
});
void skills;
/* ── flow-engine ──────────────────────────────────────────────── */
const flow = createFlowRunner({
state: createFlowStateStore(db),
evaluate: sfeel.condition,
data: taskData,
tools: { ...toolGateway, invoke: (i) => toolGateway.invoke(i) },
llm: {
async generate(input) {
// LLM 在 flow 里只以「被调用的工具」出现,不是调度者
try {
const result = await deps.modelGateway.generate({
model: input.model,
prompt: input.prompt,
outputSchema: input.outputSchema,
taskId: input.taskId,
stepNo: input.stepNo,
label: 'flow-step',
});
return { ok: result.object !== undefined, ...(result.object === undefined ? { error: '出参不合 schema' } : { object: result.object }) };
} catch (error) {
return { ok: false, error: error instanceof Error ? error.message : String(error) };
}
},
},
cards,
listeners,
memory: memoryRecorder as never,
tasks: taskContext,
clock,
});
/* ── loop-engine ──────────────────────────────────────────────── */
const loop = createTurnRunner({
state: createLoopStateStore(db),
sfeel,
data: taskData,
agent: createAgentRunner({
gateway: deps.modelGateway,
modelOf: async (taskId) => {
const { activityId } = await taskContext.of(taskId);
const manifest = await deps.registry.manifestOf(activityId);
return manifest.declarations.models[0] ?? 'default';
},
}),
tools: toolGateway,
cards,
listeners,
memory: {
append: memoryRecorder.append as never,
loadActivityMemory: async (activityId: string, limit: number) =>
(await deps.memory.loadActivityMemory(activityId, { limit })).map((m) => ({ layer: m.layer, statement: m.statement })),
},
tasks: taskContext,
clock,
});
/**
* process 晚于 task 构造 —— 延后绑定(与 container 里 `TriggerMatcher` 同一手法)。
* 未绑定时静默不报:装配尚未完成的那一瞬没有任务会终态。
*/
const processRef = createDeferredProcess();
/**
* 任务的输出口载荷。
*
* 载荷来自**落地收敛的事实**(`facts`),不是从作用域里再捞一遍:
* 放行那一刻草稿层已合并并清空,此时再捞只剩起单入参,下游节点会因缺字段起不来。
* 输出口声明的属性名与台账列名可能不同形(驼峰 / 下划线),两种写法都认。
*/
function outputPortsOf(
manifest: { contract: { output: { mandatory: ReadonlyArray<{ name: string; payload: unknown }> } } },
landed: LandedFacts,
): Record<string, Record<string, unknown>> {
const snake = (name: string): string => name.replace(/[A-Z]/g, (c) => `_${c.toLowerCase()}`);
const ports: Record<string, Record<string, unknown>> = {};
for (const port of manifest.contract.output.mandatory) {
const payload = port.payload as { properties?: Record<string, { type?: string }> };
const declared = Object.entries(payload.properties ?? {});
const picked: Record<string, unknown> = {};
for (const [field, spec] of declared) {
// 数组属性 = 这一单落地的那些行(逐行的键 + 值);标量属性 = 铺平后的事实
if (spec.type === 'array') {
picked[field] = landed.rows.map((r) => ({ ...r.key, ...r.values }));
continue;
}
const value = landed.flat[field] ?? landed.flat[snake(field)];
if (value !== undefined) picked[field] = value;
}
// 声明之外的落地事实一并带上:边映射也可能直接引用台账列名
ports[port.name] = { ...landed.flat, ...picked };
}
return ports;
}
/* ── task(L6):范式在此汇流,对上层不可区分 ──────────────────── */
const task = createTaskService({
store: taskStore,
activities: {
async load(activityId) {
const manifest = await deps.registry.manifestOf(activityId);
const envelope = await deps.registry.loadEnvelope(activityId);
return {
paradigm: manifest.identity.paradigm,
caps: manifest.contract.resources.budget,
envelope: { ...envelope },
inputSchema: manifest.contract.input.data,
businessModel: manifest.assets.businessModel,
stallAfter: manifest.declarations.harness.stallAfter,
};
},
},
schemas: deps.schemas,
// 范式在此汇流:两个引擎按统一的 ParadigmEngine 契约暴露给 task —— 对上层不可区分(§9)
engines: {
loop: {
load: (taskId, model) => loop.load(taskId, model as never).then(() => undefined),
advance: async (taskId) => {
const outcome = await loop.runTurn(taskId);
return { status: outcome.status, ...(outcome.blockedReason === undefined ? {} : { reason: outcome.blockedReason }) };
},
onEvent: async (taskId, event) => {
const outcome = await loop.onEvent(taskId, event);
return { status: outcome.status, ...(outcome.blockedReason === undefined ? {} : { reason: outcome.blockedReason }) };
},
},
flow: {
load: (taskId, model) => flow.load(taskId, model as never).then(() => undefined),
advance: async (taskId) => {
const outcome = await flow.advance(taskId);
return {
status: outcome.status,
...(outcome.blockedReason === undefined ? {} : { reason: outcome.blockedReason }),
// 谱走完时把放行裁决带上来 —— 完成只出自放行通过
...(outcome.released === undefined ? {} : { released: outcome.released }),
...(outcome.selectedPorts === undefined ? {} : { selectedPorts: outcome.selectedPorts }),
};
},
onEvent: async (taskId, event) => {
const outcome = await flow.resume(taskId, {
kind: event.kind === 'control' ? 'time' : event.kind,
ref: event.ref,
payload: event.payload,
});
return { status: outcome.status, ...(outcome.blockedReason === undefined ? {} : { reason: outcome.blockedReason }) };
},
},
},
landing: {
/** 放行通过即刻落地:草稿合并 + 动作执行(附幂等键)→ 收敛才发射输出口(OS 7.3) */
async land(input) {
const outcome = await deps.release.land({
taskId: input.taskId,
ownerId: input.ownerId,
humanCallCap: input.humanCallCap,
selectedPorts: input.selectedPorts,
});
return {
converged: outcome.verdict.kind === 'converged',
emittedPorts: outcome.emittedPorts,
ledgerRowsMerged: outcome.ledgerRowsMerged,
// 输出口载荷 = 已落地的事实:铺平供标量取值,逐行供数组取值
facts: {
flat: outcome.merged.reduce<Record<string, unknown>>(
(acc, row) => ({ ...acc, ...row.key, ...row.values }),
{},
),
rows: outcome.merged.map((r) => ({ key: r.key, values: r.values })),
},
};
},
},
memory: {
append: memoryRecorder.append as never,
seal: (taskId) => deps.memory.seal(taskId),
distill: (input) => deps.memory.distill(input),
hasHistory: async (taskId) => (await deps.memory.history(taskId)).length > 0,
},
listeners: {
hasArmedListener: async (taskId) =>
(await deps.event.listListeners(taskId)).some((l) => l.state === 'armed'),
},
persistence: {
hasDrafts: async (taskId) => (await deps.ledger.checkConflicts(taskId)) !== undefined,
hasRuntimeState: async (taskId) =>
(await flow.position(taskId)) !== undefined || (await loop.runtime(taskId)) !== undefined,
},
events: {
publishControl: async (taskId, semantic, by, reason) => {
await deps.event.publish({
type: 'control',
correlation: { target: { kind: 'task', id: taskId }, cause: { kind: 'governance', ref: by }, keys: {} },
payload: { data: { semantic, issuedBy: by, reason } },
occurredAt: clock.now().toISOString(),
source: { kind: 'governance' },
});
},
publishTerminal: async (taskId, terminal, reason, facts) => {
await deps.event.publish({
type: 'data',
correlation: { target: { kind: 'task', id: taskId }, cause: { kind: 'engine', ref: 'terminal' }, keys: { terminal } },
payload: { data: { terminal, reason } },
occurredAt: clock.now().toISOString(),
source: { kind: 'engine', detail: 'task' },
});
/*
* 任务终态到达 → 通知流程层(L7)。
*
* task 在 L6、process 在 L7,task 不得反向依赖 process —— 这一跳由 app 这层缝合。
* 「工作流状态是聚合投影」的前提是节点实例的终态**报得上去**:不报,
* 流就停在那个节点上,既不流转也不走失败走向。
* 用延后绑定是因为 process 在本函数里晚于 task 构造。
*/
const task = await taskStore.get(taskId);
const manifest = task === undefined ? undefined : await deps.registry.manifestOf(task.activityId);
const ports = manifest === undefined ? {} : outputPortsOf(manifest, facts);
await processRef.onTaskTerminal(taskId, terminal, ports);
},
},
notifier: {
notify: async (input) => {
// 改派留痕并**通知双方**
for (const to of [input.from, input.to]) {
await deps.message.notify({
staffId: to,
title: `任务改派:${input.title}`,
body: `任务 ${input.taskId} 的负责人由 ${input.from} 改为 ${input.to}(操作人 ${input.by})`,
});
}
},
},
interaction: { setPhase: async (taskId, phase) => void (await deps.interaction.setPhase(taskId, phase)) },
clock,
ids,
});
/* ── process(L7) ─────────────────────────────────────────────── */
const process: ProcessService = createProcessService({
store: createProcessStore(db),
activities: {
async faces() {
const cards = await deps.registry.list();
const map = new Map<string, Awaited<ReturnType<ProcessService['validate']>> extends never ? never : never>();
void map;
const faces = new Map();
for (const card of cards) {
const manifest = await deps.registry.manifestOf(card.activityId);
const input = manifest.contract.input.data as { required?: string[]; properties?: Record<string, { type?: string }> };
const outputs: Record<string, Record<string, string>> = {};
for (const port of manifest.contract.output.mandatory) {
const payload = port.payload as { properties?: Record<string, { type?: string }> };
outputs[port.name] = Object.fromEntries(
Object.entries(payload.properties ?? {}).map(([k, v]) => [k, v.type ?? 'string']),
);
}
faces.set(card.activityId, {
activityId: card.activityId,
requiredInputs: input.required ?? [],
inputTypes: Object.fromEntries(
Object.entries(input.properties ?? {}).map(([k, v]) => [k, v.type ?? 'string']),
),
outputs,
});
}
return faces;
},
},
org: {
positions: async () => new Set((await deps.org.listStaff()).positions.map((p) => p.code)),
candidates: async (position) => {
const staff = await deps.org.staffOfPosition(position);
const withLoad = await Promise.all(
staff.map(async (s) => ({
staffId: s.id,
name: s.name,
openTasks: (await task.openTasksOf(s.id)).length,
})),
);
return withLoad;
},
},
tasks: {
start: async (input) =>
task.start({
activityId: input.activityId,
ownerId: input.ownerId,
title: input.title,
workflowId: input.workflowId,
nodeId: input.nodeId,
data: input.data,
...(input.message === undefined ? {} : { message: input.message }),
}),
},
approvals: {
async raise(input) {
// 审批点:卡到审批人工位 —— 与活动级的放行不同层
const issued = await deps.humanTool.issueExecution(
{ taskId: input.workflowId as never, ownerId: input.approverId, humanCallCap: 99 },
{
title: `审批:${input.subject}`,
basis: input.basis.length > 0 ? [...input.basis] : [{ k: '节点', v: input.nodeId }],
ask: '请裁决:通过 / 驳回(驳回必附意见)',
fields: [
{
key: 'verdict',
label: '裁决',
kind: 'radio',
required: true,
options: [
{ value: 'pass', label: '通过' },
{ value: 'reject', label: '驳回', negative: true },
],
},
{ key: 'comment', label: '依据 / 意见', kind: 'textarea' },
],
},
);
return { id: issued.order.id };
},
},
sfeel,
schemas: deps.schemas,
events: {
publish: async (input) => {
await deps.event.publish({
type: 'data',
correlation: {
target: { kind: 'workflow', id: input.workflowId as never },
cause: { kind: 'engine', ref: input.kind },
keys: {},
},
payload: { data: input.detail },
occurredAt: clock.now().toISOString(),
source: { kind: 'engine', detail: 'process' },
});
},
},
receipts: {
notify: async (input) => {
await deps.message.notify({
staffId: input.to,
title: `工作流回执:${input.subject}`,
body: `工作流 ${input.workflowId} 状态:${input.state}`,
});
},
},
clock,
ids,
});
/* ── trigger(L7) ────────────────────────────────────────────── */
const trigger = createTriggerService({
store: createTriggerStore(db),
process: {
start: async (input) => process.start(input),
requiredInputs: async (packageId) => {
const pkg = await process.getPackage(packageId);
const input = pkg?.callInterface.input as { required?: string[] } | undefined;
return input?.required ?? [];
},
},
sfeel,
deadLetters: {
// 起单失败入死信 —— 落日志 + 送治理台的消息要素,不静默丢弃
report: async (input) => {
writeLog(`[trigger] 起单失败入死信:${input.triggerId} —— ${input.reason}`);
const { staff } = await deps.org.listStaff();
for (const person of staff) {
const governs = await deps.org.authorize(person.id, { kind: 'governance', function: 'trigger' }, 'govern');
if (!governs) continue;
await deps.message.notify({
staffId: person.id,
title: `触发器起单失败:${input.triggerId}`,
body: input.reason,
});
}
},
},
ledger: {
scan: async (input) => {
const page = await deps.ledger.query({
address: { domain: input.domain, object: input.object },
where: {},
orderBy: [],
limit: 200,
offset: 0,
});
return page.rows.flatMap((row) => {
const date = row[input.dateField];
if (typeof date !== 'string') return [];
return [{ key: { id: String(row['id'] ?? '') }, date, row }];
});
},
},
clock,
ids,
});
// 延后绑定落定:此后任务终态才报得到流程层
processRef.bind(process);
return { task, process, trigger };
}