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,254 @@
|
||||
/**
|
||||
* task 用例 —— 任务运行时(编码规范 §5.3;语义出处 OS 04 / 4.3,AM 10)。
|
||||
*
|
||||
* 预算在**环外**强制:每一拍先扣先判,再交范式引擎 —— 活动不自查自守。
|
||||
*/
|
||||
import type { StaffId, TaskId } from '@contracts';
|
||||
import { taskIdSchema } from '@contracts';
|
||||
import type { StartTaskInput, Task, TaskService, TickOutcome } from '../../contract.ts';
|
||||
import { TaskAlreadyTerminalError, TaskNotFoundError } from '../../contract.ts';
|
||||
import type {
|
||||
ActivityLoader,
|
||||
Clock,
|
||||
IdGenerator,
|
||||
InteractionPhaseSetter,
|
||||
ListenerProbe,
|
||||
MemoryRecorder,
|
||||
ParadigmEngine,
|
||||
PersistenceProbe,
|
||||
ReassignNotifier,
|
||||
SchemaValidator,
|
||||
TaskEventPublisher,
|
||||
TaskStore,
|
||||
} from '../../port.ts';
|
||||
import { durationMs, judge, terminalOf, ZERO_CONSUMPTION } from '../domain/harness.ts';
|
||||
import { applyControl, decideTerminal, resumeReadiness } from '../domain/lifecycle.ts';
|
||||
|
||||
export type TaskServiceDeps = {
|
||||
readonly store: TaskStore;
|
||||
readonly activities: ActivityLoader;
|
||||
readonly schemas: SchemaValidator;
|
||||
/** 范式引擎按 paradigm 分派 —— 对上层不可区分 */
|
||||
readonly engines: Readonly<Record<'loop' | 'flow', ParadigmEngine>>;
|
||||
readonly memory: MemoryRecorder;
|
||||
readonly listeners: ListenerProbe;
|
||||
readonly persistence: PersistenceProbe;
|
||||
readonly events: TaskEventPublisher;
|
||||
readonly notifier: ReassignNotifier;
|
||||
readonly interaction: InteractionPhaseSetter;
|
||||
readonly clock: Clock;
|
||||
readonly ids: IdGenerator;
|
||||
};
|
||||
|
||||
export function createTaskService(deps: TaskServiceDeps): TaskService {
|
||||
async function require(taskId: TaskId): Promise<Task> {
|
||||
const task = await deps.store.get(taskId);
|
||||
if (task === undefined) throw new TaskNotFoundError(taskId);
|
||||
return task;
|
||||
}
|
||||
|
||||
function engineOf(task: Task): ParadigmEngine {
|
||||
return deps.engines[task.paradigm];
|
||||
}
|
||||
|
||||
/** Harness 在环外先判:越限即熔断并挂起,处置归治理台 */
|
||||
async function harnessCheck(task: Task, stallAfter: string) {
|
||||
const lastProgress = (await deps.store.lastProgressAt(task.id)) ?? new Date(task.startedAt);
|
||||
const stall = {
|
||||
sinceLastProgressMs: deps.clock.now().getTime() - lastProgress.getTime(),
|
||||
stallAfterMs: durationMs(stallAfter),
|
||||
hasArmedListener: await deps.listeners.hasArmedListener(task.id),
|
||||
awaitingHumanReply: task.runtime === 'awaiting-human',
|
||||
};
|
||||
return judge(task.used, task.caps, stall);
|
||||
}
|
||||
|
||||
return {
|
||||
async start(input: StartTaskInput): Promise<Task> {
|
||||
const loaded = await deps.activities.load(input.activityId);
|
||||
// 输入 data 必过 schema 校验,**缺项不得启动**(AM 12.1)
|
||||
const check = deps.schemas.validate(loaded.inputSchema, input.data);
|
||||
if (!check.ok) throw new Error(`输入不合契约,不得启动:${check.errors.join('; ')}`);
|
||||
|
||||
const now = deps.clock.now();
|
||||
const task: Task = {
|
||||
id: taskIdSchema.parse(deps.ids.next()),
|
||||
activityId: input.activityId,
|
||||
paradigm: loaded.paradigm,
|
||||
...(input.workflowId === undefined ? {} : { workflowId: input.workflowId }),
|
||||
...(input.nodeId === undefined ? {} : { nodeId: input.nodeId }),
|
||||
ownerId: input.ownerId,
|
||||
title: input.title,
|
||||
runtime: 'running',
|
||||
envelope: loaded.envelope,
|
||||
caps: loaded.caps,
|
||||
used: ZERO_CONSUMPTION,
|
||||
startedAt: now.toISOString(),
|
||||
};
|
||||
await deps.store.insert(task);
|
||||
await deps.memory.append({
|
||||
taskId: task.id,
|
||||
kind: 'engine-transition',
|
||||
title: `起单:${task.title}`,
|
||||
// message 计入初始观察、全程留痕;**不得扩大授权边界**
|
||||
detail: { data: input.data, message: input.message, envelope: loaded.envelope },
|
||||
});
|
||||
await engineOf(task).load(task.id, loaded.businessModel);
|
||||
await deps.interaction.setPhase(task.id, 'active');
|
||||
await deps.store.markProgress(task.id, now);
|
||||
return task;
|
||||
},
|
||||
|
||||
async get(taskId) {
|
||||
return require(taskId);
|
||||
},
|
||||
|
||||
async listByOwner(ownerId, openOnly = false) {
|
||||
return deps.store.listByOwner(ownerId, openOnly);
|
||||
},
|
||||
|
||||
async openTasksOf(ownerId) {
|
||||
const tasks = await deps.store.listByOwner(ownerId, true);
|
||||
return tasks.map((t) => ({ taskId: t.id, title: t.title }));
|
||||
},
|
||||
|
||||
async tick(taskId): Promise<TickOutcome> {
|
||||
const task = await require(taskId);
|
||||
if (task.terminal !== undefined) throw new TaskAlreadyTerminalError(taskId, task.terminal);
|
||||
|
||||
const loaded = await deps.activities.load(task.activityId);
|
||||
const verdict = await harnessCheck(task, loaded.stallAfter);
|
||||
const terminal = terminalOf(verdict);
|
||||
if (terminal !== undefined) {
|
||||
const finished = await this.finish(
|
||||
taskId,
|
||||
terminal === 'exhausted'
|
||||
? { kind: 'exhausted', detail: JSON.stringify(verdict) }
|
||||
: { kind: 'stalled', evidence: '该动未动、超限仍无推进' },
|
||||
);
|
||||
return { task: finished, verdict, progressed: false };
|
||||
}
|
||||
|
||||
const outcome = await engineOf(task).advance(taskId);
|
||||
// 步计量:loop 的一次决策循环 / flow 的一个声明步
|
||||
const used = await deps.store.addConsumption(taskId, { steps: 1 });
|
||||
await deps.store.markProgress(taskId, deps.clock.now());
|
||||
|
||||
const runtime =
|
||||
outcome.status === 'awaiting'
|
||||
? 'awaiting-event'
|
||||
: outcome.status === 'blocked'
|
||||
? 'suspended'
|
||||
: 'running';
|
||||
const updated = await deps.store.update(taskId, { runtime, used });
|
||||
await deps.interaction.setPhase(taskId, runtime === 'running' ? 'active' : 'awaiting-event');
|
||||
|
||||
if (outcome.status === 'blocked') {
|
||||
const finished = await this.finish(taskId, { kind: 'blocked', reason: outcome.reason ?? '无法推进' });
|
||||
return { task: finished, verdict, progressed: true };
|
||||
}
|
||||
return { task: updated, verdict, progressed: true };
|
||||
},
|
||||
|
||||
async deliver(taskId, event): Promise<TickOutcome> {
|
||||
const task = await require(taskId);
|
||||
if (task.terminal !== undefined) throw new TaskAlreadyTerminalError(taskId, task.terminal);
|
||||
|
||||
await deps.memory.append({
|
||||
taskId,
|
||||
kind: 'observation',
|
||||
title: `事件到达:${event.kind}/${event.ref}`,
|
||||
detail: event.payload,
|
||||
});
|
||||
await engineOf(task).onEvent(taskId, event);
|
||||
await deps.store.markProgress(taskId, deps.clock.now());
|
||||
return this.tick(taskId);
|
||||
},
|
||||
|
||||
/** 控制三语义 —— 叫停留痕为最重档纠正信号 */
|
||||
async control(taskId, semantic, by, reason) {
|
||||
const task = await require(taskId);
|
||||
const outcome = applyControl(semantic);
|
||||
await deps.events.publishControl(taskId, semantic, by, reason);
|
||||
await deps.memory.append({
|
||||
taskId,
|
||||
kind: 'engine-transition',
|
||||
title: `控制事件:${semantic}`,
|
||||
detail: { by, reason, correctionSignal: outcome.kind === 'halted' ? 'heaviest' : undefined },
|
||||
});
|
||||
|
||||
if (outcome.kind === 'terminated') {
|
||||
await this.finish(taskId, { kind: 'terminated', reason: reason ?? '治理台终止' });
|
||||
return outcome;
|
||||
}
|
||||
await deps.store.update(taskId, { runtime: 'suspended', ...(reason === undefined ? {} : { reason }) });
|
||||
await deps.interaction.setPhase(taskId, 'awaiting-event');
|
||||
void task;
|
||||
return outcome;
|
||||
},
|
||||
|
||||
async resume(taskId, by) {
|
||||
const task = await require(taskId);
|
||||
if (task.terminal !== undefined) throw new TaskAlreadyTerminalError(taskId, task.terminal);
|
||||
await deps.memory.append({ taskId, kind: 'engine-transition', title: '恢复', detail: { by } });
|
||||
const updated = await deps.store.update(taskId, { runtime: 'running' });
|
||||
await deps.interaction.setPhase(taskId, 'active');
|
||||
return updated;
|
||||
},
|
||||
|
||||
/** 改派:留痕并通知双方(OS 02) */
|
||||
async reassign(taskId, to, by) {
|
||||
const task = await require(taskId);
|
||||
const updated = await deps.store.update(taskId, { ownerId: to });
|
||||
await deps.memory.append({
|
||||
taskId,
|
||||
kind: 'engine-transition',
|
||||
title: `改派:${task.ownerId} → ${to}`,
|
||||
detail: { by },
|
||||
});
|
||||
await deps.notifier.notify({ from: task.ownerId, to, taskId, title: task.title, by });
|
||||
return updated;
|
||||
},
|
||||
|
||||
/** 终态由系统命名;封卷触发蒸馏钩子 */
|
||||
async finish(taskId, cause) {
|
||||
const task = await require(taskId);
|
||||
if (task.terminal !== undefined) return task;
|
||||
|
||||
const { terminal, reason } = decideTerminal(cause);
|
||||
const now = deps.clock.now();
|
||||
const updated = await deps.store.update(taskId, {
|
||||
runtime: 'terminal',
|
||||
terminal,
|
||||
reason,
|
||||
finishedAt: now.toISOString(),
|
||||
});
|
||||
|
||||
await deps.memory.append({ taskId, kind: 'terminal', title: `终态:${terminal}`, detail: { reason } });
|
||||
await deps.events.publishTerminal(taskId, terminal, reason);
|
||||
await deps.interaction.setPhase(taskId, 'terminal');
|
||||
|
||||
// 终态封卷 → 归档 → 蒸馏钩子产出活动记忆候选条目
|
||||
await deps.memory.seal(taskId);
|
||||
await deps.memory.distill({
|
||||
taskId,
|
||||
activityId: task.activityId,
|
||||
trigger: 'terminal',
|
||||
prompt: `任务「${task.title}」以 ${terminal} 收场(${reason})。归纳可复用的事实、模式、裁决与例外。`,
|
||||
});
|
||||
|
||||
return updated;
|
||||
},
|
||||
|
||||
async resumeReadiness(taskId) {
|
||||
const task = await require(taskId);
|
||||
return resumeReadiness({
|
||||
'task-memory': await deps.memory.hasHistory(task.id),
|
||||
'draft-layer': await deps.persistence.hasDrafts(task.id),
|
||||
'runtime-state': await deps.persistence.hasRuntimeState(task.id),
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export type { StaffId };
|
||||
Reference in New Issue
Block a user