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-TMP-REPO/apps/api/src/modules/task/internal/usecase/task-service.ts
T
ChaiHaoandClaude Opus 5 61fe181678 度量八指标全部接真:DMN 覆盖率与单位成本不再恒为 0
两处度量是空的,查下来都是「没人记」而不是「算错了」:

① **DMN 覆盖率恒 0**:判据只认标题里的「求值:」,把跑得最多的**纯工具步**整个漏掉。
   DMN 决策表与机检正是落在纯工具/工具步上。改为按步类型分:
   确定性求值(pure-tool / tool)÷ 判断总数(再加 LLM 步)——「临场搬进确定」的进度表。
   顺带把**步类型显式写进留痕**:判据该由引擎给出,不靠解析标题后缀。
   现读数 66%。

② **单位成本恒 0**:任务消耗只记步数,token / 活跃时长 / 调人次数**从来没人累加** ——
   于是八指标里的单位成本永远是 0,预算熔断与打扰上限也形同虚设。
   · tick 计这一拍真正花掉的时间(挂起等人的不算活跃,那是世界等待);
   · flow 的 LLM 步与 loop 的每一回合都把 token 计入;
   · 每发一张卡计一次调人。
   现读数:A2 单轮 12k token、A3 3.4k;单位成本 73 token 每放行单。

另修:执行卡的 ask 优先取 `card.ask`(此前只认 `reason`,新卡面格式下会退化成标题)。

第四轮端到端复跑:整条链闭环,消耗数据真实。

gates: tsc / eslint / depcruise(336 模块)/ api-report / 429 测试 全绿

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

308 lines
12 KiB
TypeScript

/**
* 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,
LandingGateway,
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 landing: LandingGateway;
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',
data: input.data,
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 startedAt = deps.clock.now().getTime();
const outcome = await engineOf(task).advance(taskId);
// 步计量:loop 的一次决策循环 / flow 的一个声明步。
// 活跃时长按**这一拍真正花掉的时间**计 —— 挂起等人的时间不算活跃,那是世界等待。
const used = await deps.store.addConsumption(taskId, {
steps: 1,
activeMs: Math.max(0, deps.clock.now().getTime() - startedAt),
});
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 };
}
/*
* 谱走完了。**完成只出自放行通过**(OS 05):
* · 放行通过 → 落地收敛(草稿合并 + 动作执行 → 收敛才发射输出口)→ released;
* · 未放行(还没裁 / 驳回)→ 不是完成,也**不再空推** —— 挂起等人,
* 否则推进器会一拍一拍把预算烧到 exhausted,把「没人裁」误报成「跑爆了」。
*/
if (outcome.status === 'finished') {
if (outcome.released !== true) {
const waiting = await deps.store.update(taskId, { runtime: 'awaiting-human' });
await deps.interaction.setPhase(taskId, 'awaiting-event');
return { task: waiting, verdict, progressed: false };
}
const landed = await deps.landing.land({
taskId,
ownerId: task.ownerId,
humanCallCap: task.caps.humanCalls,
selectedPorts: outcome.selectedPorts ?? {},
});
if (!landed.converged) {
// 落地未收敛:输出口不得发射,任务受阻待人(落地失败卡已发)
const blocked = await this.finish(taskId, { kind: 'blocked', reason: '落地未收敛,输出口未发射' });
return { task: blocked, verdict, progressed: true };
}
await deps.memory.append({
taskId,
kind: 'engine-transition',
title: `落地收敛:合并 ${landed.ledgerRowsMerged} 行,发射输出口 ${landed.emittedPorts.join('、') || '—'}`,
});
const finished = await this.finish(taskId, { kind: 'released' }, landed.facts);
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: 'control',
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: 'control', 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: 'control',
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, facts) {
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, facts ?? { flat: {}, rows: [] });
await deps.interaction.setPhase(taskId, 'terminal');
// 终态封卷 → 归档 → 蒸馏钩子产出活动记忆候选条目
await deps.memory.seal(taskId);
try {
await deps.memory.distill({
taskId,
activityId: task.activityId,
trigger: 'terminal',
prompt: `任务「${task.title}」以 ${terminal} 收场(${reason})。归纳可复用的事实、模式、裁决与例外。`,
});
} catch {
/*
* 蒸馏是**进化管线**的入口,不是终态的一部分:它产出的是待回放待放行的候选增量。
* 它失败(模型不可达等)绝不能把已经落定的终态掀翻 —— 那会让任务卡在
* 「实际已终、状态却没落」的中间态,治理台也看不到。
*
* 也不能把失败记进任务记忆:此刻记忆**已封卷**,追加会被只追加不改的守卫拒掉
* (那正是它该做的)。蒸馏的成败属于进化管线的账,不属于这一单的过程留痕。
*/
}
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 };