Archived
human-tool / ledger / loop-engine / release / task / interaction 六模块与 app 层(desk 路由、engines、consumers、 data-order-landing、case-stamp、reset-runs、seed-activities、容器与 server 装配),web 的 desk-task (ViewRegion / chat-bubbles / review-face)与 app.css、ui-kit 图标;契约 card / manifest; 迁移 0034_task_seq 与迁移日志;api-report 快照;CLAUDE.md 快照、debt、活动开发规范、HTML 报告规范同步。 逐条口径见 CLAUDE.md 快照(2026-08-27 下午至晚)。另一会话做的,一直留在工作树没入库;与同批另两条提交互相依赖。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0167qWjeyBwvfdGovd7FWbCu
392 lines
16 KiB
TypeScript
392 lines
16 KiB
TypeScript
/**
|
|
* module: loop-engine —— 本模块要外界提供的能力(编码规范 §10.1 / §5.3)。
|
|
*/
|
|
import type { LoopModel, StaffId, TaskId } from '@contracts';
|
|
import type { CaseModel } from './internal/domain/cmmn-parse.ts';
|
|
import type { CaseRuntime, EnabledSet } from './internal/domain/lifecycle.ts';
|
|
import type { AgentTurn } from './contract.ts';
|
|
export type Clock = {
|
|
now(): Date;
|
|
};
|
|
/** 运行时实例状态 —— loop 的模型定位与就绪集(OS 09 三份持久状态之一) */
|
|
export type LoopStateStore = {
|
|
save(taskId: TaskId, model: LoopModel, parsed: CaseModel, runtime: CaseRuntime, pending?: PendingAcceptance | null): Promise<void>;
|
|
load(taskId: TaskId): Promise<{
|
|
model: LoopModel;
|
|
parsed: CaseModel;
|
|
runtime: CaseRuntime;
|
|
pending?: PendingAcceptance;
|
|
} | undefined>;
|
|
};
|
|
/** S-FEEL —— app 注入 `@sfeel`;引擎零 LLM */
|
|
export type Sfeel = {
|
|
condition(expression: string, scope: Readonly<Record<string, unknown>>): boolean;
|
|
expression(expression: string, scope: Readonly<Record<string, unknown>>): unknown;
|
|
unaryTest(cell: string, value: unknown, scope: Readonly<Record<string, unknown>>): boolean;
|
|
};
|
|
/** 任务数据 —— app 注入 `ledger`(L1):引擎对**任务视图**求值,不依赖事件 */
|
|
export type TaskDataGateway = {
|
|
view(taskId: TaskId): Promise<Record<string, unknown>>;
|
|
/**
|
|
* 本活动**声明过**的台账对象(域.对象 → 列名与中文标)。
|
|
*
|
|
* 两用:喂给 Agent(不知道能写哪儿就只会瞎猜列名),以及校验 `write` 行动 ——
|
|
* 没声明过的地址一律拒写。越域写与越权调工具同罪,都得在结构上拦住。
|
|
*/
|
|
writable(taskId: TaskId): Promise<Array<{
|
|
domain: string;
|
|
object: string;
|
|
label: string;
|
|
keys: string[];
|
|
fields: Array<{
|
|
name: string;
|
|
label: string;
|
|
type?: string;
|
|
}>;
|
|
}>>;
|
|
/**
|
|
* 写草稿,并回报**这一笔到底有没有改变什么**。
|
|
*
|
|
* 值和上次一模一样的写是空转。不回报,Agent 就看不出自己在原地打转 ——
|
|
* 实测:同一行 verdict 被反复重写十几拍,每拍烧一次模型,预算就这么淌完。
|
|
*/
|
|
draftWrite(input: {
|
|
taskId: TaskId;
|
|
domain: string;
|
|
object: string;
|
|
key: Record<string, string | number>;
|
|
patch: Record<string, unknown>;
|
|
}): Promise<{
|
|
changed: boolean;
|
|
}>;
|
|
/**
|
|
* **撤掉本单草稿层的一行** —— 写错了得有退路。
|
|
*
|
|
* 回 `false` = 草稿层里根本没有这一行(多半是主键写法对不上,
|
|
* `DAG…` 与 `DAG…-R0` 是两行)。如实说比静默成功有用:静默成功会让模型
|
|
* 以为撤掉了,下一拍判据照旧不过,它又去撤一次。
|
|
*/
|
|
draftDrop(input: {
|
|
taskId: TaskId;
|
|
domain: string;
|
|
object: string;
|
|
key: Record<string, string | number>;
|
|
}): Promise<{
|
|
dropped: boolean;
|
|
}>;
|
|
};
|
|
/**
|
|
* Agent 回路 —— app 注入(Mastra Agent 在 adapter,§6.1)。
|
|
* **回合由 usecase 驱动,不放开自由 maxSteps,预算在环外由 Harness 强制。**
|
|
*/
|
|
export type AgentPort = {
|
|
decide(input: {
|
|
taskId: TaskId;
|
|
/** 叙事面:说明书 + 活动记忆选摘 + 任务记忆窗口 */
|
|
narrative: string;
|
|
enabled: EnabledSet;
|
|
scope: Record<string, unknown>;
|
|
/**
|
|
* 这一单能调什么 —— **声明 × 目录**编译出来的工具清单(`compileToolBus`)。
|
|
*
|
|
* 不给它这份清单,它就只知道说明书里提过的工具名,连入参长什么样都得猜 ——
|
|
* 实测的后果是:该调工具的那一步改成了发一张卡问人(「请为以下三个PN查询历史价格」),
|
|
* 明明系统里就有这件工具。工具是声明式的,清单得摆在它眼前。
|
|
*/
|
|
tools: Array<{
|
|
id: string;
|
|
description: string;
|
|
input: Record<string, unknown>;
|
|
effect: 'read' | 'write';
|
|
}>;
|
|
/** 本活动可写的台账地址与列名 —— 不给它字典,它只会照想象编列名 */
|
|
writable: Array<{
|
|
domain: string;
|
|
object: string;
|
|
label: string;
|
|
keys: string[];
|
|
fields: Array<{
|
|
name: string;
|
|
label: string;
|
|
type?: string;
|
|
}>;
|
|
}>;
|
|
}): Promise<AgentTurn>;
|
|
};
|
|
/** 工具 / 人类工具 / 监听 —— 三型行动的落点 */
|
|
export type ToolGateway = {
|
|
invoke(input: {
|
|
tool: string;
|
|
input: Record<string, unknown>;
|
|
activityId: string;
|
|
taskId: TaskId;
|
|
}): Promise<{
|
|
ok: boolean;
|
|
output?: Record<string, unknown>;
|
|
error?: {
|
|
message: string;
|
|
};
|
|
/**
|
|
* 已转人代办,等人回填(§6.2)——「人是工具的一种实现」。
|
|
*
|
|
* 系统里没有这件工具时,不是失败:基座已开了一张执行卡并立了监听。
|
|
* 这一拍该**挂起等人**,不该把计划项判 Failed —— 判 Failed 等于告诉 Agent
|
|
* 「这条路走不通,换一条」,可这条路明明正在走,只是走的人换成了人。
|
|
*/
|
|
awaitingHuman?: {
|
|
readonly workOrderId: string;
|
|
};
|
|
}>;
|
|
isWriteEffect(tool: string): Promise<boolean>;
|
|
registerIntent(input: {
|
|
taskId: TaskId;
|
|
tool: string;
|
|
input: Record<string, unknown>;
|
|
label: string;
|
|
}): Promise<void>;
|
|
/** 这一单能调什么(声明 × 目录)—— 单拍路径与开环路径吃同一份清单 */
|
|
busOf(activityId: string): Promise<Array<{
|
|
id: string;
|
|
description: string;
|
|
input: Record<string, unknown>;
|
|
effect: 'read' | 'write';
|
|
}>>;
|
|
};
|
|
/**
|
|
* 在途验收(提交放行的持久痕):卡发出即记,裁决到达再记 verdict ——
|
|
* 判齐重入不再发第二张卡。恢复的那一拍按 verdict 分岔:
|
|
* `pass` 才是终局(released);**非 pass = 打回重做,不是终局** ——
|
|
* 意见(comment)用人话进轨迹、在途验收清掉,同一拍照常问模型,
|
|
* 模型改完重新提交,新发的验收卡才是下一次裁决的对象(2026-08-22,
|
|
* 从前非 pass 只把任务重新挂成 awaiting-human,人说了「重做」却再没有
|
|
* 任何事件叫醒它,一单就此卡死 —— 柴浩实测)。
|
|
*/
|
|
export type PendingAcceptance = {
|
|
readonly acceptanceCardId: string;
|
|
readonly verdict?: 'pass' | 'reject';
|
|
/** 打回重做时人写的意见 —— 空着就是点了固定按钮没写话 */
|
|
readonly comment?: string;
|
|
readonly selectedPorts?: Record<string, string>;
|
|
};
|
|
export type CardGateway = {
|
|
/** 数据录入卡 —— 人确认那一下由基座直接写草稿层,不回到 Agent 手里 */
|
|
entry(input: {
|
|
taskId: TaskId;
|
|
ownerId: StaffId;
|
|
humanCallCap: number;
|
|
ask: string;
|
|
ledger: string;
|
|
key: Readonly<Record<string, string>>;
|
|
fields: ReadonlyArray<{
|
|
key: string;
|
|
label: string;
|
|
value?: string;
|
|
required?: boolean;
|
|
}>;
|
|
/** 同一次确认还要落的另几张表 —— 建一位新客户 = 公司档 + 联系人档 */
|
|
also?: ReadonlyArray<{
|
|
title: string;
|
|
ledger: string;
|
|
key: Readonly<Record<string, string>>;
|
|
fields: ReadonlyArray<{
|
|
key: string;
|
|
label: string;
|
|
value?: string;
|
|
required?: boolean;
|
|
}>;
|
|
}>;
|
|
basis?: ReadonlyArray<{
|
|
k: string;
|
|
v: string;
|
|
}>;
|
|
}): Promise<{
|
|
id: string;
|
|
}>;
|
|
issue(input: {
|
|
taskId: TaskId;
|
|
ownerId: StaffId;
|
|
humanCallCap: number;
|
|
workOrder: 'execution' | 'acceptance';
|
|
/** 这张卡挂在哪个计划项上 —— 引擎按它查剧本声明(如 `cardVariant`),不靠模型抄卡骨架;引擎自己发的卡(验收/兜底)没有项 */
|
|
itemId?: string | undefined;
|
|
card: Record<string, unknown>;
|
|
}): Promise<{
|
|
id: string;
|
|
}>;
|
|
/**
|
|
* 发**补全卡**(对话卡族)—— 缺口清单摆在对话区、带一张表单,人就地填齐。
|
|
*
|
|
* 与执行工单分开是因为它们是两件事:工单是「派一件活给你办」,
|
|
* 派一张工单去问几个字段,收卡的人得进待处理、办完再回来,重得不成比例。
|
|
* SOP 上画着「补全卡」的那一步(A1-S2),要的就是这一件。
|
|
*/
|
|
ask(input: {
|
|
taskId: TaskId;
|
|
ownerId: StaffId;
|
|
humanCallCap: number;
|
|
ask: string;
|
|
fields: ReadonlyArray<{
|
|
key: string;
|
|
label: string;
|
|
hint?: string;
|
|
}>;
|
|
basis?: ReadonlyArray<{
|
|
k: string;
|
|
v: string;
|
|
}>;
|
|
}): Promise<{
|
|
id: string;
|
|
}>;
|
|
/**
|
|
* 发**决策卡**(对话卡族)—— 岔路口摆给人**点一个**,不是让人敲字。
|
|
*
|
|
* 与补全卡的分工:补全卡问事实(缺哪几个字段,你填);决策卡问处置(往哪走,你选)。
|
|
* 两条路都合法、选哪条是业务判断的时候,该发的是这一件。
|
|
*/
|
|
decide(input: {
|
|
taskId: TaskId;
|
|
ownerId: StaffId;
|
|
humanCallCap: number;
|
|
ask: string;
|
|
options: ReadonlyArray<{
|
|
value: string;
|
|
label: string;
|
|
detail?: string;
|
|
consequence?: string;
|
|
recommended?: boolean;
|
|
}>;
|
|
basis?: ReadonlyArray<{
|
|
k: string;
|
|
v: string;
|
|
}>;
|
|
allowCustom?: boolean;
|
|
/** 这张卡挂在哪个计划项上 —— 组装层按它查剧本声明(`cardVariant: 'review'` = 摆成验收工单) */
|
|
itemId?: string | undefined;
|
|
/**
|
|
* **这张卡穿哪副面孔,引擎已经按剧本定好**(2026-08-27):模型没给 itemId、或给的项挂不上时,
|
|
* 组装层按 itemId 查剧本就查不到,卡就退成「需要你定」—— 而这一步的剧本明明写着 `cardVariant: 'review'`。
|
|
* 引擎知道当下在办哪一步,由它把面孔定下来递过去;组装层只在这里没给时才自己按 itemId 查。
|
|
*/
|
|
variant?: 'review' | undefined;
|
|
/** 人工验收清单(剧本 `acceptanceChecklist`)—— 审核卡上一条一枚「达标 / 不达标」开关 */
|
|
checklist?: ReadonlyArray<string> | undefined;
|
|
}): Promise<{
|
|
id: string;
|
|
}>;
|
|
/**
|
|
* **这一单还有哪几张卡在等人交回**(2026-08-27,用户:「你叫了人我都没填你就直接推进了」)。
|
|
* 每拍开头问一次:有卡没答,这一拍不问模型 —— 叫了人就等人;
|
|
* 任何多余的唤醒(迟到的回传事件、另一台后端的推进器)都推不动它。
|
|
*/
|
|
unanswered(taskId: TaskId): Promise<ReadonlyArray<{
|
|
id: string;
|
|
title: string;
|
|
}>>;
|
|
};
|
|
export type ListenerRegistrar = {
|
|
register(input: {
|
|
taskId: TaskId;
|
|
eventType: 'data' | 'time' | 'human';
|
|
keyFilter: Record<string, string>;
|
|
deadline: string;
|
|
}): Promise<{
|
|
id: string;
|
|
}>;
|
|
/**
|
|
* **这个活动声明过哪几种监听** —— 立监听的授权面(2026-08-23 加)。
|
|
*
|
|
* 与「写的授权面是台账声明」「调工具的授权面是白名单」同一条:没声明过的监听立不得。
|
|
* 起因是实测的一次永久挂起(E04):模型在依赖断点上临场立了个 data 监听等回执,
|
|
* 而那个活动 `declarations.listeners` 是空的、这一单也不会有任何 data 事件进来 ——
|
|
* 单子就此挂在 `awaiting-event` 一动不动。**立了没人叫醒的监听不是等待,是失联。**
|
|
*/
|
|
declared(taskId: TaskId): Promise<ReadonlyArray<{
|
|
eventType: string;
|
|
keyFilter: Record<string, string>;
|
|
}>>;
|
|
};
|
|
/**
|
|
* 视图流控 —— app 注入 `interaction`(L3)。
|
|
* 「loop:AI 按定位与局面临场组织视图流」(AM 07)。MVP 每回合推一次**案例全景**:
|
|
* 阶段与计划项的生命周期态、当下可为、目标缺口 —— 这些是语义引擎算出来的确定事实,
|
|
* 零 LLM 即可成屏;模型只决定「下一步做什么」,不负责画面。
|
|
*/
|
|
export type ViewGateway = {
|
|
/**
|
|
* 推一帧画面。**回报推没推上** —— 从前是 `Promise<void>` 且模版查不到就静默 return:
|
|
* 引擎每拍推一个硬编码的 `case-overview`,而 A1 声明的四张模版一个都不叫这个名字,
|
|
* 于是**每一帧都被悄悄丢掉**,工作区从头到尾空白,日志里也没有一个字。
|
|
* 静默失败比报错坏得多:它让「没画面」看起来像「活动还没产出」。
|
|
*/
|
|
push(input: {
|
|
taskId: TaskId;
|
|
activityId: string;
|
|
template: string;
|
|
scope: Record<string, unknown>;
|
|
}): Promise<{
|
|
pushed: boolean;
|
|
templates: string[];
|
|
}>;
|
|
};
|
|
/**
|
|
* 技能 —— app 注入 `skill-engine`(L4)。
|
|
* 「渐进披露:常驻上下文**仅有技能目录**,按事加载正文、按需下钻引用」(AM 08)。
|
|
*/
|
|
export type SkillGateway = {
|
|
/** 目录:id + 何时适用 —— 只给这些,正文等 AI 说要才加载 */
|
|
index(activityId: string): Promise<ReadonlyArray<{
|
|
id: string;
|
|
label: string;
|
|
applicability: string;
|
|
}>>;
|
|
/** 装配入信封:这一回合装配了哪些技能及其版本,记入任务记忆 */
|
|
stamp(activityId: string, skillIds: readonly string[]): Promise<ReadonlyArray<{
|
|
id: string;
|
|
version: string;
|
|
}>>;
|
|
};
|
|
export type MemoryRecorder = {
|
|
append(input: {
|
|
taskId: TaskId;
|
|
kind: 'engine-transition' | 'evaluation' | 'thought' | 'action' | 'result' | 'observation' | 'narration';
|
|
title: string;
|
|
detail?: Record<string, unknown>;
|
|
}): Promise<void>;
|
|
/** 装载:按定位选摘活动记忆入 Agent 上下文(渐进披露) */
|
|
loadActivityMemory(activityId: string, limit: number): Promise<Array<{
|
|
layer: string;
|
|
statement: string;
|
|
}>>;
|
|
/**
|
|
* 本任务自己的最近轨迹 —— 喂回叙事面,Agent 每拍不失忆。
|
|
*
|
|
* 不给 `size` 就用活动声明的 `memoryWindow`(叙事面按它给;窗口越大越贵)。
|
|
* **熔断检测要另开一个更大的窗**:它数的是「同一堵墙撞了几次」,
|
|
* 而一次提交被拒会连带写下交代、评估、推画面好几条 —— 20 条的叙事窗里
|
|
* 至多装得下三四次撞墙,于是撞十次也数不到 5(实测:同一条判据拒回 10 次没熔断,
|
|
* 另一单拒回 33 次,40 万 token 烧到 exhausted 才停)。
|
|
*/
|
|
window(taskId: TaskId, size?: number): Promise<Array<{
|
|
kind: string;
|
|
title: string;
|
|
detail?: Record<string, unknown> | undefined;
|
|
}>>;
|
|
};
|
|
export type TaskContext = {
|
|
of(taskId: TaskId): Promise<{
|
|
activityId: string;
|
|
ownerId: StaffId;
|
|
humanCallCap: number;
|
|
}>;
|
|
/**
|
|
* 这一单的**机检判据名**(归一化后的 key 与 label 都在里头)。
|
|
*
|
|
* 给 `judge` 行动当门用:L1/L2 是拿 S-FEEL 对台账求值的,Agent 裁不动它 ——
|
|
* 判据投影压根不读 Agent 对机检项的裁定。可从前引擎照收不误,还往轨迹里
|
|
* 写一句「语义性前提评估:XXX → 满足」:模型于是以为这条已经过了,
|
|
* 转头就 submit,被门以同一条判据拒回;再裁、再拒,连拒五拍熔断(实测 A2 死于此)。
|
|
* 轨迹里那句「满足」是**假话** —— 界面不撒谎,对 Agent 也一样。
|
|
*/
|
|
machineCriteria(taskId: TaskId): Promise<ReadonlySet<string>>;
|
|
};
|