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,37 @@
|
||||
# module: task
|
||||
|
||||
职责: 任务运行时:生命周期与**终态五名裁定**、Harness 预算执行器 / 停滞熔断、
|
||||
控制三语义、改派、断点续跑的就绪判定、封卷与蒸馏钩子的触发
|
||||
不负责: 范式的推进逻辑(见 loop-engine / flow-engine,经 `ParadigmEngine` 端口)、
|
||||
放行与落地(见 release)、工作流的编排与聚合(见 process)
|
||||
|
||||
公开出口: index.ts
|
||||
- 类型:`TaskService` `Task` `StartTaskInput` `TickOutcome` `Consumption` `HarnessVerdict`
|
||||
`ControlOutcome` `TerminalCause` `ResumeReadiness`
|
||||
- 错误:`TaskNotFoundError` `TaskAlreadyTerminalError`
|
||||
- 端口类型:`TaskStore` `ActivityLoader` `SchemaValidator` `ParadigmEngine` `MemoryRecorder`
|
||||
`ListenerProbe` `PersistenceProbe` `TaskEventPublisher` `ReassignNotifier`
|
||||
`InteractionPhaseSetter`
|
||||
- 用例工厂:`createTaskService(deps): TaskService`
|
||||
- 纯函数:`judge` `budgetBreaches` `isStalled` `terminalOf` `durationMs`
|
||||
`applyControl` `decideTerminal` `isSuccess` `triggersFailureDisposition`
|
||||
`resumeReadiness` `isPassiveOpen` `landingBlocked` `rejectionKeepsRunning`
|
||||
- adapter:`createTaskStore`;Drizzle 表:`taskTable`
|
||||
|
||||
层级: L6(编码规范 §4)
|
||||
依赖: @contracts;@shared/db;loop-engine / flow-engine / activity-registry(L5/L4);
|
||||
port 注入:memory / event / message / interaction / ledger
|
||||
被依赖: process, org(停用前置检查的在途任务), report, app
|
||||
|
||||
不变量(OS 04 / 4.3,AM 10 / 12.1):
|
||||
- **恰一负责人**:任务必绑定恰一个 `ownerId`;改派**留痕并通知双方**
|
||||
- **输入必过 schema**:`data` 缺项**不得启动**;`message` 计入初始观察但不得扩大授权
|
||||
- **预算环外强制**:每一拍先 `judge` 再交引擎 —— 活动代码无从自查自守;
|
||||
任一项触顶即 `exhausted`,空转即 `stalled`
|
||||
- **监听中是健康态**:有监听在等或等人回传一律**不算停滞**
|
||||
- **终态五名封闭**:`decideTerminal` 是唯一命名口;`released` 是唯一成功终态
|
||||
- **已终结不得再推进**:`tick` / `deliver` 对终态任务抛 `TaskAlreadyTerminalError` ——
|
||||
流级重做一律重开新实例,不复活旧任务
|
||||
- **叫停不是终态**:转挂起待纠偏,并记**最重档纠正信号**
|
||||
- **封卷触发蒸馏**:终态后 `seal` + `distill`,产出活动记忆候选条目
|
||||
- **断点续跑靠三份持久状态**:任务记忆 / 草稿层 / 运行时实例状态,缺一即不可续
|
||||
@@ -0,0 +1,158 @@
|
||||
/** Harness 与终态五名裁定 —— domain 测试零 mock(OS 04 / 4.3,AM 10) */
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import type { BudgetCaps } from '@contracts';
|
||||
import { budgetBreaches, durationMs, isStalled, judge, terminalOf, ZERO_CONSUMPTION } from '../internal/domain/harness.ts';
|
||||
import {
|
||||
applyControl,
|
||||
decideTerminal,
|
||||
isPassiveOpen,
|
||||
isSuccess,
|
||||
isTerminalControl,
|
||||
landingBlocked,
|
||||
rejectionKeepsRunning,
|
||||
resumeReadiness,
|
||||
triggersFailureDisposition,
|
||||
} from '../internal/domain/lifecycle.ts';
|
||||
|
||||
const caps: BudgetCaps = { steps: 50, tokens: 100_000, activeMinutes: 120, humanCalls: 5 };
|
||||
|
||||
describe('预算执行器 —— 环外强制,活动不自查自守', () => {
|
||||
it('未越限即无 breach', () => {
|
||||
expect(budgetBreaches({ steps: 10, tokens: 100, activeMs: 1000, humanCalls: 1 }, caps)).toEqual([]);
|
||||
});
|
||||
|
||||
it('任一项触顶即熔断', () => {
|
||||
expect(budgetBreaches({ ...ZERO_CONSUMPTION, steps: 50 }, caps).map((b) => b.item)).toEqual(['steps']);
|
||||
expect(budgetBreaches({ ...ZERO_CONSUMPTION, tokens: 100_000 }, caps).map((b) => b.item)).toEqual(['tokens']);
|
||||
expect(budgetBreaches({ ...ZERO_CONSUMPTION, humanCalls: 5 }, caps).map((b) => b.item)).toEqual(['humanCalls']);
|
||||
});
|
||||
|
||||
it('活跃时长按分钟折算', () => {
|
||||
expect(budgetBreaches({ ...ZERO_CONSUMPTION, activeMs: 120 * 60_000 }, caps).map((b) => b.item)).toEqual(['activeMs']);
|
||||
expect(budgetBreaches({ ...ZERO_CONSUMPTION, activeMs: 119 * 60_000 }, caps)).toEqual([]);
|
||||
});
|
||||
|
||||
it('多项同时越限全部报出', () => {
|
||||
expect(budgetBreaches({ steps: 99, tokens: 999_999, activeMs: 0, humanCalls: 0 }, caps)).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('停滞检测 —— 监听中是健康态', () => {
|
||||
const base = { sinceLastProgressMs: 60 * 60_000, stallAfterMs: 30 * 60_000, hasArmedListener: false, awaitingHumanReply: false };
|
||||
|
||||
it('该动未动、超限仍无推进即停滞', () => {
|
||||
expect(isStalled(base)).toBe(true);
|
||||
});
|
||||
|
||||
it('有监听在等一律不算停滞 —— 与停滞分别对待', () => {
|
||||
expect(isStalled({ ...base, hasArmedListener: true })).toBe(false);
|
||||
});
|
||||
|
||||
it('等人回传也不算停滞', () => {
|
||||
expect(isStalled({ ...base, awaitingHumanReply: true })).toBe(false);
|
||||
});
|
||||
|
||||
it('未到阈值不算停滞', () => {
|
||||
expect(isStalled({ ...base, sinceLastProgressMs: 10 * 60_000 })).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('judge —— 预算优先于停滞', () => {
|
||||
const stall = { sinceLastProgressMs: 999_999_999, stallAfterMs: 1000, hasArmedListener: false, awaitingHumanReply: false };
|
||||
|
||||
it('越限即 exhausted', () => {
|
||||
const verdict = judge({ ...ZERO_CONSUMPTION, steps: 50 }, caps, stall);
|
||||
expect(verdict.kind).toBe('exhausted');
|
||||
expect(terminalOf(verdict)).toBe('exhausted');
|
||||
});
|
||||
|
||||
it('未越限但空转即 stalled', () => {
|
||||
const verdict = judge(ZERO_CONSUMPTION, caps, stall);
|
||||
expect(verdict.kind).toBe('stalled');
|
||||
expect(terminalOf(verdict)).toBe('stalled');
|
||||
});
|
||||
|
||||
it('都没事即 ok,不产生终态', () => {
|
||||
const verdict = judge(ZERO_CONSUMPTION, caps, { ...stall, hasArmedListener: true });
|
||||
expect(verdict).toEqual({ kind: 'ok' });
|
||||
expect(terminalOf(verdict)).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('控制三语义', () => {
|
||||
it('暂停 = 可恢复冻结', () => {
|
||||
expect(applyControl('pause')).toEqual({ kind: 'suspended', resumable: true });
|
||||
});
|
||||
|
||||
it('叫停 = 挂起 + 最重档纠正信号,恢复归负责人', () => {
|
||||
expect(applyControl('halt')).toEqual({ kind: 'halted', resumable: true, correctionSignal: 'heaviest' });
|
||||
});
|
||||
|
||||
it('终止 = 不可恢复关闭', () => {
|
||||
expect(applyControl('terminate')).toEqual({ kind: 'terminated', resumable: false, terminal: 'terminated' });
|
||||
});
|
||||
|
||||
it('只有终止是终态 —— 叫停不是', () => {
|
||||
expect(isTerminalControl('halt')).toBe(false);
|
||||
expect(isTerminalControl('pause')).toBe(false);
|
||||
expect(isTerminalControl('terminate')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('终态五名 —— 系统命名,封闭枚举', () => {
|
||||
it('放行完成是唯一成功终态', () => {
|
||||
expect(decideTerminal({ kind: 'released' }).terminal).toBe('released');
|
||||
expect(isSuccess('released')).toBe(true);
|
||||
for (const t of ['blocked', 'stalled', 'exhausted', 'terminated'] as const) expect(isSuccess(t)).toBe(false);
|
||||
});
|
||||
|
||||
it('五名各有其因', () => {
|
||||
expect(decideTerminal({ kind: 'blocked', reason: '监听时限走完' }).terminal).toBe('blocked');
|
||||
expect(decideTerminal({ kind: 'stalled', evidence: '空转' }).terminal).toBe('stalled');
|
||||
expect(decideTerminal({ kind: 'exhausted', detail: 'token' }).terminal).toBe('exhausted');
|
||||
expect(decideTerminal({ kind: 'terminated', reason: '治理台终止' }).terminal).toBe('terminated');
|
||||
});
|
||||
|
||||
it('非完成终态触发流程层失败走向', () => {
|
||||
expect(triggersFailureDisposition('released')).toBe(false);
|
||||
expect(triggersFailureDisposition('blocked')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('「不是终态」的三样', () => {
|
||||
it('驳回是环内回转,任务继续', () => {
|
||||
expect(rejectionKeepsRunning()).toEqual({ runtime: 'running' });
|
||||
});
|
||||
it('落地受阻是运行态', () => {
|
||||
expect(landingBlocked('连接超时')).toEqual({ runtime: 'landing-blocked', reason: '连接超时' });
|
||||
});
|
||||
it('打开任务只是打开呈现', () => {
|
||||
expect(isPassiveOpen('running')).toBe(false);
|
||||
expect(isPassiveOpen('awaiting-event')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('断点续跑 —— 三份持久状态共同保证', () => {
|
||||
it('三样齐备才算可续', () => {
|
||||
expect(resumeReadiness({ 'task-memory': true, 'draft-layer': true, 'runtime-state': true })).toEqual({
|
||||
ready: true,
|
||||
missing: [],
|
||||
});
|
||||
});
|
||||
it('缺一即不可续,并指出缺谁', () => {
|
||||
expect(resumeReadiness({ 'task-memory': true, 'draft-layer': false, 'runtime-state': true })).toEqual({
|
||||
ready: false,
|
||||
missing: ['draft-layer'],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('durationMs', () => {
|
||||
it('解析 ISO-8601', () => {
|
||||
expect(durationMs('PT30M')).toBe(30 * 60_000);
|
||||
expect(durationMs('P1D')).toBe(86_400_000);
|
||||
});
|
||||
it('非法即报错', () => {
|
||||
expect(() => durationMs('30分钟')).toThrow(/非法时长/);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,104 @@
|
||||
/**
|
||||
* module: task —— 对外类型(编码规范 §10.1;语义出处 OS 04,AM 01/10)。
|
||||
*
|
||||
* 任务运行时:生命周期与**终态五名裁定**、Harness 预算执行器 / 停滞熔断、
|
||||
* 控制三语义、改派、断点续跑。范式(loop / flow)在此汇流 —— 对流程层与交互层不可区分。
|
||||
*/
|
||||
import type { BudgetCaps, ControlSemantic, Paradigm, StaffId, TaskId, TaskRuntimeState, TerminalState } from '@contracts';
|
||||
import type { BudgetBreach, Consumption, HarnessVerdict } from './internal/domain/harness.ts';
|
||||
import type { ControlOutcome, ResumeReadiness, TaskSnapshot, TerminalCause } from './internal/domain/lifecycle.ts';
|
||||
|
||||
export type {
|
||||
BudgetBreach,
|
||||
BudgetCaps,
|
||||
Consumption,
|
||||
ControlOutcome,
|
||||
ControlSemantic,
|
||||
HarnessVerdict,
|
||||
Paradigm,
|
||||
ResumeReadiness,
|
||||
TaskRuntimeState,
|
||||
TaskSnapshot,
|
||||
TerminalCause,
|
||||
TerminalState,
|
||||
};
|
||||
|
||||
export class TaskNotFoundError extends Error {
|
||||
constructor(readonly taskId: TaskId) {
|
||||
super(`任务不存在:${taskId}`);
|
||||
this.name = 'TaskNotFoundError';
|
||||
}
|
||||
}
|
||||
|
||||
export class TaskAlreadyTerminalError extends Error {
|
||||
constructor(
|
||||
readonly taskId: TaskId,
|
||||
readonly terminal: TerminalState,
|
||||
) {
|
||||
super(`任务已终结(${terminal}),不得再推进:${taskId} —— 流级重做一律重开新实例`);
|
||||
this.name = 'TaskAlreadyTerminalError';
|
||||
}
|
||||
}
|
||||
|
||||
/** 一单任务 —— 活动的运行时实例;**恰有一名负责人** */
|
||||
export type Task = {
|
||||
readonly id: TaskId;
|
||||
readonly activityId: string;
|
||||
readonly paradigm: Paradigm;
|
||||
/** 起单来源:工作流节点;登记触发的直起单为 undefined */
|
||||
readonly workflowId?: string;
|
||||
readonly nodeId?: string;
|
||||
readonly ownerId: StaffId;
|
||||
readonly title: string;
|
||||
readonly runtime: TaskRuntimeState;
|
||||
readonly terminal?: TerminalState;
|
||||
readonly reason?: string;
|
||||
/** 装载信封:包版本 + 各资产生效版 —— 行为可归因 */
|
||||
readonly envelope: Readonly<Record<string, unknown>>;
|
||||
readonly caps: BudgetCaps;
|
||||
readonly used: Consumption;
|
||||
readonly startedAt: string;
|
||||
readonly finishedAt?: string;
|
||||
};
|
||||
|
||||
export type StartTaskInput = {
|
||||
readonly activityId: string;
|
||||
readonly ownerId: StaffId;
|
||||
readonly title: string;
|
||||
readonly workflowId?: string;
|
||||
readonly nodeId?: string;
|
||||
/** 输入:data(按 schema 校验,缺项不得启动)+ message(委托消息,可为空) */
|
||||
readonly data: Record<string, unknown>;
|
||||
readonly message?: string;
|
||||
};
|
||||
|
||||
export type TickOutcome = {
|
||||
readonly task: Task;
|
||||
readonly verdict: HarnessVerdict;
|
||||
readonly progressed: boolean;
|
||||
};
|
||||
|
||||
export type TaskService = {
|
||||
/** 起单:装载 → 校验输入 → 建任务记忆 → 首个回合 */
|
||||
start(input: StartTaskInput): Promise<Task>;
|
||||
get(taskId: TaskId): Promise<Task>;
|
||||
listByOwner(ownerId: StaffId, openOnly?: boolean): Promise<Task[]>;
|
||||
/** org 停用前置检查要问的「在途任务」 */
|
||||
openTasksOf(ownerId: StaffId): Promise<Array<{ taskId: string; title: string }>>;
|
||||
|
||||
/** 推进一拍:**预算在环外先扣先判**,再交范式引擎 */
|
||||
tick(taskId: TaskId): Promise<TickOutcome>;
|
||||
/** 事件驱动续跑 —— 挂起期间事件不丢,恢复即补投 */
|
||||
deliver(taskId: TaskId, event: { kind: 'data' | 'time' | 'human' | 'control'; ref: string; payload: Record<string, unknown> }): Promise<TickOutcome>;
|
||||
|
||||
/** 控制三语义:暂停 / 叫停 / 终止 */
|
||||
control(taskId: TaskId, semantic: ControlSemantic, by: StaffId, reason?: string): Promise<ControlOutcome>;
|
||||
resume(taskId: TaskId, by: StaffId): Promise<Task>;
|
||||
/** 改派:在途任务的负责人变更,**留痕并通知双方** */
|
||||
reassign(taskId: TaskId, to: StaffId, by: StaffId): Promise<Task>;
|
||||
|
||||
/** 终态裁定 —— 完成由框架裁定,终态由系统命名 */
|
||||
finish(taskId: TaskId, cause: TerminalCause): Promise<Task>;
|
||||
/** 断点续跑的三份持久状态是否齐备 */
|
||||
resumeReadiness(taskId: TaskId): Promise<ResumeReadiness>;
|
||||
};
|
||||
@@ -0,0 +1,64 @@
|
||||
/**
|
||||
* module: task —— 唯一出口。只做 re-export,不写逻辑(编码规范 §10.1)。
|
||||
*/
|
||||
export type {
|
||||
BudgetBreach,
|
||||
BudgetCaps,
|
||||
Consumption,
|
||||
ControlOutcome,
|
||||
ControlSemantic,
|
||||
HarnessVerdict,
|
||||
Paradigm,
|
||||
ResumeReadiness,
|
||||
StartTaskInput,
|
||||
Task,
|
||||
TaskRuntimeState,
|
||||
TaskService,
|
||||
TaskSnapshot,
|
||||
TerminalCause,
|
||||
TerminalState,
|
||||
TickOutcome,
|
||||
} from './contract.ts';
|
||||
export { TaskAlreadyTerminalError, TaskNotFoundError } from './contract.ts';
|
||||
|
||||
export type {
|
||||
ActivityLoader,
|
||||
Clock,
|
||||
IdGenerator,
|
||||
InteractionPhaseSetter,
|
||||
ListenerProbe,
|
||||
MemoryRecorder,
|
||||
ParadigmEngine,
|
||||
PersistenceProbe,
|
||||
ReassignNotifier,
|
||||
SchemaValidator,
|
||||
TaskEventPublisher,
|
||||
TaskStore,
|
||||
} from './port.ts';
|
||||
|
||||
export type { TaskServiceDeps } from './internal/usecase/task-service.ts';
|
||||
export { createTaskService } from './internal/usecase/task-service.ts';
|
||||
|
||||
export {
|
||||
budgetBreaches,
|
||||
durationMs,
|
||||
isStalled,
|
||||
judge,
|
||||
terminalOf,
|
||||
ZERO_CONSUMPTION,
|
||||
} from './internal/domain/harness.ts';
|
||||
export {
|
||||
applyControl,
|
||||
decideTerminal,
|
||||
isPassiveOpen,
|
||||
isSuccess,
|
||||
isTerminalControl,
|
||||
landingBlocked,
|
||||
RESUME_SOURCES,
|
||||
rejectionKeepsRunning,
|
||||
resumeReadiness,
|
||||
triggersFailureDisposition,
|
||||
} from './internal/domain/lifecycle.ts';
|
||||
|
||||
export { createTaskStore } from './internal/adapter/task-store.ts';
|
||||
export { taskTable } from './internal/adapter/task-schema.ts';
|
||||
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* task 的 sys schema 表(编码规范 §7 / ADR-002)。
|
||||
*/
|
||||
import { index, integer, jsonb, text, timestamp } from 'drizzle-orm/pg-core';
|
||||
import { sys } from '@shared/db.ts';
|
||||
|
||||
export const taskTable = sys.table(
|
||||
'task',
|
||||
{
|
||||
id: text('id').primaryKey(),
|
||||
activityId: text('activity_id').notNull(),
|
||||
paradigm: text('paradigm').notNull(),
|
||||
workflowId: text('workflow_id'),
|
||||
nodeId: text('node_id'),
|
||||
/** 恰有一名负责人 */
|
||||
ownerId: text('owner_id').notNull(),
|
||||
title: text('title').notNull(),
|
||||
runtime: text('runtime').notNull().default('running'),
|
||||
/** 终态五名 —— 封闭枚举 */
|
||||
terminal: text('terminal'),
|
||||
reason: text('reason'),
|
||||
/** 装载信封:包版本 + 各资产生效版 —— 行为可归因 */
|
||||
envelope: jsonb('envelope').notNull(),
|
||||
caps: jsonb('caps').notNull(),
|
||||
usedSteps: integer('used_steps').notNull().default(0),
|
||||
usedTokens: integer('used_tokens').notNull().default(0),
|
||||
usedActiveMs: integer('used_active_ms').notNull().default(0),
|
||||
usedHumanCalls: integer('used_human_calls').notNull().default(0),
|
||||
lastProgressAt: timestamp('last_progress_at', { withTimezone: true }),
|
||||
startedAt: timestamp('started_at', { withTimezone: true }).notNull().defaultNow(),
|
||||
finishedAt: timestamp('finished_at', { withTimezone: true }),
|
||||
},
|
||||
(t) => [
|
||||
index('task_owner_idx').on(t.ownerId, t.runtime),
|
||||
index('task_workflow_idx').on(t.workflowId),
|
||||
index('task_terminal_idx').on(t.terminal),
|
||||
],
|
||||
);
|
||||
@@ -0,0 +1,124 @@
|
||||
/**
|
||||
* task 存储 adapter —— Drizzle 只准出现在 adapter(编码规范 §5.3)。
|
||||
*/
|
||||
import { and, asc, eq, isNull, sql } from 'drizzle-orm';
|
||||
import type { BudgetCaps, Paradigm, StaffId, TaskId, TaskRuntimeState, TerminalState } from '@contracts';
|
||||
import type { Db } from '@shared/db.ts';
|
||||
import type { TaskStore } from '../../port.ts';
|
||||
import type { Task } from '../../contract.ts';
|
||||
import type { Consumption } from '../domain/harness.ts';
|
||||
import { taskTable } from './task-schema.ts';
|
||||
|
||||
type Row = typeof taskTable.$inferSelect;
|
||||
|
||||
function toTask(row: Row): Task {
|
||||
return {
|
||||
id: row.id as TaskId,
|
||||
activityId: row.activityId,
|
||||
paradigm: row.paradigm as Paradigm,
|
||||
...(row.workflowId === null ? {} : { workflowId: row.workflowId }),
|
||||
...(row.nodeId === null ? {} : { nodeId: row.nodeId }),
|
||||
ownerId: row.ownerId as StaffId,
|
||||
title: row.title,
|
||||
runtime: row.runtime as TaskRuntimeState,
|
||||
...(row.terminal === null ? {} : { terminal: row.terminal as TerminalState }),
|
||||
...(row.reason === null ? {} : { reason: row.reason }),
|
||||
envelope: row.envelope as Record<string, unknown>,
|
||||
caps: row.caps as BudgetCaps,
|
||||
used: {
|
||||
steps: row.usedSteps,
|
||||
tokens: row.usedTokens,
|
||||
activeMs: row.usedActiveMs,
|
||||
humanCalls: row.usedHumanCalls,
|
||||
},
|
||||
startedAt: row.startedAt.toISOString(),
|
||||
...(row.finishedAt === null ? {} : { finishedAt: row.finishedAt.toISOString() }),
|
||||
};
|
||||
}
|
||||
|
||||
export function createTaskStore(db: Db): TaskStore {
|
||||
return {
|
||||
async insert(task) {
|
||||
await db.insert(taskTable).values({
|
||||
id: task.id,
|
||||
activityId: task.activityId,
|
||||
paradigm: task.paradigm,
|
||||
workflowId: task.workflowId ?? null,
|
||||
nodeId: task.nodeId ?? null,
|
||||
ownerId: task.ownerId,
|
||||
title: task.title,
|
||||
runtime: task.runtime,
|
||||
envelope: task.envelope,
|
||||
caps: task.caps,
|
||||
startedAt: new Date(task.startedAt),
|
||||
});
|
||||
},
|
||||
|
||||
async get(taskId: TaskId) {
|
||||
const rows = await db.select().from(taskTable).where(eq(taskTable.id, taskId)).limit(1);
|
||||
return rows[0] === undefined ? undefined : toTask(rows[0]);
|
||||
},
|
||||
|
||||
async listByOwner(ownerId: StaffId, openOnly) {
|
||||
const conditions = [eq(taskTable.ownerId, ownerId)];
|
||||
if (openOnly) conditions.push(isNull(taskTable.terminal));
|
||||
const rows = await db
|
||||
.select()
|
||||
.from(taskTable)
|
||||
.where(and(...conditions))
|
||||
.orderBy(asc(taskTable.startedAt));
|
||||
return rows.map(toTask);
|
||||
},
|
||||
|
||||
async update(taskId: TaskId, patch) {
|
||||
const values: Record<string, unknown> = {};
|
||||
if (patch.runtime !== undefined) values['runtime'] = patch.runtime;
|
||||
if (patch.terminal !== undefined) values['terminal'] = patch.terminal;
|
||||
if (patch.reason !== undefined) values['reason'] = patch.reason;
|
||||
if (patch.ownerId !== undefined) values['ownerId'] = patch.ownerId;
|
||||
if (patch.finishedAt !== undefined) values['finishedAt'] = new Date(patch.finishedAt);
|
||||
if (patch.used !== undefined) {
|
||||
values['usedSteps'] = patch.used.steps;
|
||||
values['usedTokens'] = patch.used.tokens;
|
||||
values['usedActiveMs'] = patch.used.activeMs;
|
||||
values['usedHumanCalls'] = patch.used.humanCalls;
|
||||
}
|
||||
const [row] = await db.update(taskTable).set(values).where(eq(taskTable.id, taskId)).returning();
|
||||
if (row === undefined) throw new Error(`任务不存在:${taskId}`);
|
||||
return toTask(row);
|
||||
},
|
||||
|
||||
async addConsumption(taskId: TaskId, delta): Promise<Consumption> {
|
||||
const [row] = await db
|
||||
.update(taskTable)
|
||||
.set({
|
||||
usedSteps: sql`${taskTable.usedSteps} + ${delta.steps ?? 0}`,
|
||||
usedTokens: sql`${taskTable.usedTokens} + ${delta.tokens ?? 0}`,
|
||||
usedActiveMs: sql`${taskTable.usedActiveMs} + ${delta.activeMs ?? 0}`,
|
||||
usedHumanCalls: sql`${taskTable.usedHumanCalls} + ${delta.humanCalls ?? 0}`,
|
||||
})
|
||||
.where(eq(taskTable.id, taskId))
|
||||
.returning();
|
||||
if (row === undefined) throw new Error(`任务不存在:${taskId}`);
|
||||
return {
|
||||
steps: row.usedSteps,
|
||||
tokens: row.usedTokens,
|
||||
activeMs: row.usedActiveMs,
|
||||
humanCalls: row.usedHumanCalls,
|
||||
};
|
||||
},
|
||||
|
||||
async lastProgressAt(taskId: TaskId) {
|
||||
const rows = await db
|
||||
.select({ at: taskTable.lastProgressAt })
|
||||
.from(taskTable)
|
||||
.where(eq(taskTable.id, taskId))
|
||||
.limit(1);
|
||||
return rows[0]?.at ?? undefined;
|
||||
},
|
||||
|
||||
async markProgress(taskId: TaskId, at: Date) {
|
||||
await db.update(taskTable).set({ lastProgressAt: at }).where(eq(taskTable.id, taskId));
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
/**
|
||||
* Harness:预算执行器与停滞检测 —— 纯判定,零 IO(编码规范 §5.3;语义出处 OS 04,AM 10.1)。
|
||||
*
|
||||
* 「预算与熔断由活动基座**在环外强制执行** —— 活动仅在 Harness 配置中申报参数,
|
||||
* **不自查自守**。任一项越限,基座抛控制事件并挂起任务。」
|
||||
*/
|
||||
import type { BudgetCaps, TerminalState } from '@contracts';
|
||||
|
||||
/** 计量口径(OS 04):步 / token / 活跃时长 / 调人次数 */
|
||||
export type Consumption = {
|
||||
/** loop 的一次决策循环 / flow 的一个声明步 */
|
||||
readonly steps: number;
|
||||
/** LLM 调用出入 token 累计 */
|
||||
readonly tokens: number;
|
||||
/** **活跃时长**:Agent 机制运行与工具执行的时间;挂起、监听等待、待卡回传**不计入** */
|
||||
readonly activeMs: number;
|
||||
/** 调人次数按卡计 */
|
||||
readonly humanCalls: number;
|
||||
};
|
||||
|
||||
export const ZERO_CONSUMPTION: Consumption = { steps: 0, tokens: 0, activeMs: 0, humanCalls: 0 };
|
||||
|
||||
export type BudgetBreach = { readonly item: keyof Consumption; readonly used: number; readonly cap: number };
|
||||
|
||||
/** 任一项越限即熔断 —— 预算耗尽 */
|
||||
export function budgetBreaches(used: Consumption, caps: BudgetCaps): BudgetBreach[] {
|
||||
const out: BudgetBreach[] = [];
|
||||
if (used.steps >= caps.steps) out.push({ item: 'steps', used: used.steps, cap: caps.steps });
|
||||
if (used.tokens >= caps.tokens) out.push({ item: 'tokens', used: used.tokens, cap: caps.tokens });
|
||||
if (used.activeMs >= caps.activeMinutes * 60_000) {
|
||||
out.push({ item: 'activeMs', used: used.activeMs, cap: caps.activeMinutes * 60_000 });
|
||||
}
|
||||
if (used.humanCalls >= caps.humanCalls) out.push({ item: 'humanCalls', used: used.humanCalls, cap: caps.humanCalls });
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* 停滞检测:loop = Enabled 集与任务数据皆无变化、且**无监听在等**的空转;
|
||||
* flow = 当前步长期无进展且无监听在等。
|
||||
* 「**监听中是健康态**,与停滞分别对待」—— 有监听在等一律不算停滞。
|
||||
*/
|
||||
export type StallInput = {
|
||||
readonly sinceLastProgressMs: number;
|
||||
readonly stallAfterMs: number;
|
||||
readonly hasArmedListener: boolean;
|
||||
readonly awaitingHumanReply: boolean;
|
||||
};
|
||||
|
||||
export function isStalled(input: StallInput): boolean {
|
||||
if (input.hasArmedListener || input.awaitingHumanReply) return false;
|
||||
return input.sinceLastProgressMs >= input.stallAfterMs;
|
||||
}
|
||||
|
||||
/** Harness 的裁定:什么都不做 / 熔断 / 停滞 */
|
||||
export type HarnessVerdict =
|
||||
| { readonly kind: 'ok' }
|
||||
| { readonly kind: 'exhausted'; readonly breaches: readonly BudgetBreach[] }
|
||||
| { readonly kind: 'stalled' };
|
||||
|
||||
export function judge(used: Consumption, caps: BudgetCaps, stall: StallInput): HarnessVerdict {
|
||||
const breaches = budgetBreaches(used, caps);
|
||||
if (breaches.length > 0) return { kind: 'exhausted', breaches };
|
||||
if (isStalled(stall)) return { kind: 'stalled' };
|
||||
return { kind: 'ok' };
|
||||
}
|
||||
|
||||
/** 「错误与耗尽不得计为成功」—— Harness 裁定直接映射终态 */
|
||||
export function terminalOf(verdict: HarnessVerdict): TerminalState | undefined {
|
||||
if (verdict.kind === 'exhausted') return 'exhausted';
|
||||
if (verdict.kind === 'stalled') return 'stalled';
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/** ISO-8601 duration → 毫秒(与 event 的口径同一子集) */
|
||||
export function durationMs(iso: string): number {
|
||||
const m = /^P(?:(\d+)D)?(?:T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?)?$/.exec(iso);
|
||||
if (m === null) throw new Error(`非法时长:${iso}`);
|
||||
const [, d, h, min, s] = m;
|
||||
return Number(d ?? 0) * 86_400_000 + Number(h ?? 0) * 3_600_000 + Number(min ?? 0) * 60_000 + Number(s ?? 0) * 1_000;
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
/**
|
||||
* 任务生命周期与终态五名裁定 —— 纯逻辑,零 IO(编码规范 §5.3;语义出处 OS 4.3,AM 01/10)。
|
||||
*
|
||||
* 「**完成由框架裁定,终态由系统命名**」—— 终态是封闭枚举,活动不得增删。
|
||||
*/
|
||||
import type { ControlSemantic, TaskRuntimeState, TerminalState } from '@contracts';
|
||||
|
||||
export type TaskSnapshot = {
|
||||
readonly runtime: TaskRuntimeState;
|
||||
readonly terminal?: TerminalState;
|
||||
readonly reason?: string;
|
||||
};
|
||||
|
||||
export const RUNNING: TaskSnapshot = { runtime: 'running' };
|
||||
|
||||
/* ── 控制三语义(OS 06) ────────────────────────────────────────────── */
|
||||
|
||||
export type ControlOutcome =
|
||||
/** 暂停 = 可恢复冻结(监听与时限停摆) */
|
||||
| { readonly kind: 'suspended'; readonly resumable: true }
|
||||
/** 叫停 = 负责人紧急中断:挂起并**记最重档纠正**,恢复归负责人 */
|
||||
| { readonly kind: 'halted'; readonly resumable: true; readonly correctionSignal: 'heaviest' }
|
||||
/** 终止 = 不可恢复关闭 */
|
||||
| { readonly kind: 'terminated'; readonly resumable: false; readonly terminal: TerminalState };
|
||||
|
||||
export function applyControl(semantic: ControlSemantic): ControlOutcome {
|
||||
switch (semantic) {
|
||||
case 'pause':
|
||||
return { kind: 'suspended', resumable: true };
|
||||
case 'halt':
|
||||
return { kind: 'halted', resumable: true, correctionSignal: 'heaviest' };
|
||||
case 'terminate':
|
||||
return { kind: 'terminated', resumable: false, terminal: 'terminated' };
|
||||
}
|
||||
}
|
||||
|
||||
/** 叫停**不是终态** —— 转挂起待纠偏,处置后恢复,或由负责人终止 */
|
||||
export function isTerminalControl(semantic: ControlSemantic): boolean {
|
||||
return semantic === 'terminate';
|
||||
}
|
||||
|
||||
/* ── 终态裁定 ───────────────────────────────────────────────────────── */
|
||||
|
||||
export type TerminalCause =
|
||||
/** 放行通过且落地收敛,输出口已发射 —— **唯一成功终态** */
|
||||
| { readonly kind: 'released' }
|
||||
/** 无法推进,封卷待治理处置 */
|
||||
| { readonly kind: 'blocked'; readonly reason: string }
|
||||
| { readonly kind: 'stalled'; readonly evidence: string }
|
||||
| { readonly kind: 'exhausted'; readonly detail: string }
|
||||
| { readonly kind: 'terminated'; readonly reason: string };
|
||||
|
||||
export function decideTerminal(cause: TerminalCause): { terminal: TerminalState; reason: string } {
|
||||
switch (cause.kind) {
|
||||
case 'released':
|
||||
return { terminal: 'released', reason: '放行通过且落地收敛,输出口已发射' };
|
||||
case 'blocked':
|
||||
return { terminal: 'blocked', reason: cause.reason };
|
||||
case 'stalled':
|
||||
return { terminal: 'stalled', reason: cause.evidence };
|
||||
case 'exhausted':
|
||||
return { terminal: 'exhausted', reason: cause.detail };
|
||||
case 'terminated':
|
||||
return { terminal: 'terminated', reason: cause.reason };
|
||||
}
|
||||
}
|
||||
|
||||
export function isSuccess(terminal: TerminalState): boolean {
|
||||
return terminal === 'released';
|
||||
}
|
||||
|
||||
/** 非完成终态触发流程层的**失败走向**(OS 03) */
|
||||
export function triggersFailureDisposition(terminal: TerminalState): boolean {
|
||||
return terminal !== 'released';
|
||||
}
|
||||
|
||||
/* ── 「不是终态」的三样(OS 4.3) ──────────────────────────────────── */
|
||||
|
||||
/** 驳回是**环内回转**,任务继续 —— 不出活动 */
|
||||
export function rejectionKeepsRunning(): TaskSnapshot {
|
||||
return { runtime: 'running' };
|
||||
}
|
||||
|
||||
/** 落地受阻是**运行态**,处置后仍收敛于终态名录 */
|
||||
export function landingBlocked(reason: string): TaskSnapshot {
|
||||
return { runtime: 'landing-blocked', reason };
|
||||
}
|
||||
|
||||
/* ── 断点续跑:三份持久状态(OS 09) ──────────────────────────────── */
|
||||
|
||||
export const RESUME_SOURCES = ['task-memory', 'draft-layer', 'runtime-state'] as const;
|
||||
export type ResumeSource = (typeof RESUME_SOURCES)[number];
|
||||
|
||||
export type ResumeReadiness = {
|
||||
readonly ready: boolean;
|
||||
readonly missing: readonly ResumeSource[];
|
||||
};
|
||||
|
||||
/**
|
||||
* 「任务做一半的续跑由三份持久状态**共同**保证:
|
||||
* 任务记忆(按窗口重建上下文)、草稿层(截至断点的过程产物与动作意向)、
|
||||
* 运行时实例状态(loop 的模型定位与就绪集 / flow 的步序位置)。」
|
||||
*/
|
||||
export function resumeReadiness(present: Readonly<Record<ResumeSource, boolean>>): ResumeReadiness {
|
||||
const missing = RESUME_SOURCES.filter((s) => !present[s]);
|
||||
return { ready: missing.length === 0, missing };
|
||||
}
|
||||
|
||||
/** 「打开任务只是打开呈现,不是启动过程」—— 过程要么在跑,要么挂起在等 */
|
||||
export function isPassiveOpen(state: TaskRuntimeState): boolean {
|
||||
return state !== 'running';
|
||||
}
|
||||
@@ -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 };
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"name": "@modules/task",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"exports": { ".": "./index.ts" }
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
/**
|
||||
* module: task —— 本模块要外界提供的能力(编码规范 §10.1 / §5.3)。
|
||||
*/
|
||||
import type { BudgetCaps, Paradigm, StaffId, TaskId, TerminalState } from '@contracts';
|
||||
import type { Consumption } from './internal/domain/harness.ts';
|
||||
import type { Task } from './contract.ts';
|
||||
|
||||
export type Clock = { now(): Date };
|
||||
export type IdGenerator = { next(): string };
|
||||
|
||||
export type TaskStore = {
|
||||
insert(task: Task): Promise<void>;
|
||||
get(taskId: TaskId): Promise<Task | undefined>;
|
||||
listByOwner(ownerId: StaffId, openOnly: boolean): Promise<Task[]>;
|
||||
update(taskId: TaskId, patch: Partial<Task>): Promise<Task>;
|
||||
addConsumption(taskId: TaskId, delta: Partial<Consumption>): Promise<Consumption>;
|
||||
lastProgressAt(taskId: TaskId): Promise<Date | undefined>;
|
||||
markProgress(taskId: TaskId, at: Date): Promise<void>;
|
||||
};
|
||||
|
||||
/** 活动装载 —— app 注入 `activity-registry`(L4) */
|
||||
export type ActivityLoader = {
|
||||
load(activityId: string): Promise<{
|
||||
paradigm: Paradigm;
|
||||
caps: BudgetCaps;
|
||||
envelope: Record<string, unknown>;
|
||||
/** 输入 data 的 JSON Schema —— 缺项不得启动 */
|
||||
inputSchema: Record<string, unknown>;
|
||||
businessModel: unknown;
|
||||
stallAfter: string;
|
||||
}>;
|
||||
};
|
||||
|
||||
export type SchemaValidator = {
|
||||
validate(schema: Record<string, unknown>, value: unknown): { ok: boolean; errors: string[] };
|
||||
};
|
||||
|
||||
/** 范式引擎 —— loop / flow 在此汇流;对上层不可区分(§9 双层边界) */
|
||||
export type ParadigmEngine = {
|
||||
load(taskId: TaskId, model: unknown): Promise<void>;
|
||||
advance(taskId: TaskId): Promise<{ status: 'running' | 'awaiting' | 'submitted' | 'finished' | 'blocked'; reason?: string }>;
|
||||
onEvent(
|
||||
taskId: TaskId,
|
||||
event: { kind: 'data' | 'time' | 'human' | 'control'; ref: string; payload: Record<string, unknown> },
|
||||
): Promise<{ status: 'running' | 'awaiting' | 'submitted' | 'finished' | 'blocked'; reason?: string }>;
|
||||
};
|
||||
|
||||
export type MemoryRecorder = {
|
||||
append(input: {
|
||||
taskId: TaskId;
|
||||
kind: 'engine-transition' | 'terminal' | 'observation';
|
||||
title: string;
|
||||
detail?: Record<string, unknown>;
|
||||
}): Promise<void>;
|
||||
seal(taskId: TaskId): Promise<void>;
|
||||
/** 终态封卷触发蒸馏钩子 —— 产出活动记忆候选条目 */
|
||||
distill(input: { taskId: TaskId; activityId: string; trigger: 'terminal'; prompt: string }): Promise<unknown>;
|
||||
hasHistory(taskId: TaskId): Promise<boolean>;
|
||||
};
|
||||
|
||||
/** 监听在等?—— app 注入 `event`(L1):监听中是健康态,与停滞分别对待 */
|
||||
export type ListenerProbe = {
|
||||
hasArmedListener(taskId: TaskId): Promise<boolean>;
|
||||
};
|
||||
|
||||
/** 草稿层与运行时状态是否在 —— 断点续跑的三份持久状态 */
|
||||
export type PersistenceProbe = {
|
||||
hasDrafts(taskId: TaskId): Promise<boolean>;
|
||||
hasRuntimeState(taskId: TaskId): Promise<boolean>;
|
||||
};
|
||||
|
||||
/** 控制事件与终态事件 —— app 注入 `event`(L1) */
|
||||
export type TaskEventPublisher = {
|
||||
publishControl(taskId: TaskId, semantic: string, by: StaffId, reason?: string): Promise<void>;
|
||||
publishTerminal(taskId: TaskId, terminal: TerminalState, reason: string): Promise<void>;
|
||||
};
|
||||
|
||||
/** 改派通知 —— app 注入 `message`(L2):改派留痕并**通知双方** */
|
||||
export type ReassignNotifier = {
|
||||
notify(input: { from: StaffId; to: StaffId; taskId: TaskId; title: string; by: StaffId }): Promise<void>;
|
||||
};
|
||||
|
||||
/** 交互态 —— app 注入 `interaction`(L3):控制权按态转交 */
|
||||
export type InteractionPhaseSetter = {
|
||||
setPhase(taskId: TaskId, phase: 'activating' | 'active' | 'awaiting-human' | 'awaiting-event' | 'transitioning' | 'terminal'): Promise<void>;
|
||||
};
|
||||
Reference in New Issue
Block a user