/** * 范式引擎与任务运行时的装配 —— 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 { renderView } from '@modules/interaction'; 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, 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>): boolean => { try { return evaluateCondition(expression, scope); } catch { return false; } }, expression: (expression: string, scope: Readonly>): unknown => { try { return evalSfeel(expression, scope); } catch { return undefined; } }, unaryTest: (cell: string, value: unknown, scope: Readonly>): 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 = { ...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; patch: Record; }) { // 一切过程写入落草稿层 —— 放行前不落事实 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 }) => { 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; 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; }) { 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; 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; 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) }, schemas: deps.schemas, views: { /* * 按名取模版 → 填数 → 推给前端。 * 模版取自**该任务装载时的资产版本**(经 registry),不是「当前最新」—— * 在途任务按其启动时版本跑完,呈现也该跟着那一版。 */ async push({ taskId, activityId, template, scope }) { const manifest = await deps.registry.manifestOf(activityId); const found = manifest.assets.viewTemplates.find((t) => t.name === template); if (found === undefined) return; /* * 视图作用域 = 任务视图 + **本单自己写下的行**。 * 两者要分得开:任务视图里的 `域.对象` 是正式版 + 我的草稿的合并结果, * 拿它画「本单交付」会把别单放行过的记录一并列出来。 * 本单的行放在 `mine:域.对象` 下 —— 整键,取值不歧义。 */ const own: Record = {}; for (const draft of await deps.ledger.taskDrafts(taskId)) { const key = `mine:${draft.domain}.${draft.object}`; (own[key] ??= []).push({ ...draft.key, ...draft.patch }); } await deps.interaction.pushViewHtml(taskId, renderView(found.html, { ...scope, ...own })); }, }, 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: true, ...(result.object === undefined ? {} : { object: result.object }), text: result.text, }; } 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 }>; selective?: ReadonlyArray<{ group: string; ports: ReadonlyArray<{ name: string; payload: unknown }> }>; }; }; }, landed: LandedFacts, ): Record> { const snake = (name: string): string => name.replace(/[A-Z]/g, (c) => `_${c.toLowerCase()}`); const ports: Record> = {}; // 必交付口与选择组的口都要装配:活动可以只有选择组(如 A6 的成交/有异议/拒绝), // 只看必交付口会得到一张空表,下游连分支判据都读不到。 const declaredPorts = [ ...manifest.contract.output.mandatory, ...(manifest.contract.output.selective ?? []).flatMap((g) => g.ports), ]; for (const port of declaredPorts) { const payload = port.payload as { properties?: Record }; const declared = Object.entries(payload.properties ?? {}); const picked: Record = {}; 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>( (acc, row) => ({ ...acc, ...row.key, ...row.values }), {}, ), /* * **选择组发射的那一口就是这次运行的 outcome**。 * 「恰发一口,随放行一并确认」——那一口本身就是这一单的分支结论, * 下游异或网关的判据读的就是它(如 G4:成交 / 有异议 / 拒绝)。 * 只有选择组才产生 outcome;必交付口是「都要交」,不构成选择。 */ ...(Object.values(input.selectedPorts).length > 0 ? { outcome: Object.values(input.selectedPorts)[0] } : {}), }, 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> 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 }; const outputs: Record> = {}; for (const port of manifest.contract.output.mandatory) { const payload = port.payload as { properties?: Record }; 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 }; }