Archived
E2E 跑 loop 时 A3 一口气发了八张一模一样的「请处理」卡。逐层查下去,四处: ① **并发推进的真根因**:`pg_try_advisory_lock` 是**会话级**的,而我们经连接池取连接 —— 加锁与解锁多半落在不同连接上,解锁解不掉自己那把;同一连接被复用时又因可重入而直接放行。 两头都不对,于是同一任务被并发推进(现象:同一步跑两遍、同一张卡发两张、 同一行草稿写出两条互相打架的版本)。前一轮「共用同一把锁」的修法方向对、器械错。 改用**进程内按目标互斥**(MVP 单进程,确定且够用),推进器与派发器共用一把; 多进程部署时再叠事务级 `pg_try_advisory_xact_lock`(随事务释放,不依赖连接归属)。 实测:同一单从 8 张卡降到 1 张,2 回合后落到等待。 ② **非法行动会把回合打死**:AI 提了不合法的生命周期迁移(Available → Disabled), `applyAction` 抛出、运行时没落笔、推进器一遍遍重来。引擎拒绝它是对的 ——「语义忠实:引擎不得私改迁移拓扑」——但不该因此崩掉。 改为:拒绝 + 留痕 + 把理由喂回下一回合的上下文。AI 提出非法行动是预期内的事。 ③ **副作用抢在校验之前**:卡在迁移合法性判定**之前**就发出去了,于是每个被拒的回合 都白发一张卡 —— 卡收不回,负责人工作台上堆起一摞没人知道怎么答的「请处理」。 human / tool 两个分支改为**先验迁移再动手**。 ④ **裸调用**:模型只回一个「请处理」。按「调用必附上下文,裸调用不得发出」(AM 06) 把卡面结构写进出参 schema(title / ask / basis / fields 必填),提示里讲明要求, 归一时用当下局面兜底;卡片装配也修了 —— 结构化 basis 优先,嵌套对象转 JSON, 不再出现一行 `[object Object]`。 实测卡面:「请确认 NE555P 历史报价并判定采用策略」+ 三个回填字段。 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
144 lines
4.8 KiB
TypeScript
144 lines
4.8 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 { AgentAction } from './contract.ts';
|
|
export type Clock = {
|
|
now(): Date;
|
|
};
|
|
/** 运行时实例状态 —— loop 的模型定位与就绪集(OS 09 三份持久状态之一) */
|
|
export type LoopStateStore = {
|
|
save(taskId: TaskId, model: LoopModel, parsed: CaseModel, runtime: CaseRuntime): Promise<void>;
|
|
load(taskId: TaskId): Promise<{
|
|
model: LoopModel;
|
|
parsed: CaseModel;
|
|
runtime: CaseRuntime;
|
|
} | 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>>;
|
|
draftWrite(input: {
|
|
taskId: TaskId;
|
|
domain: string;
|
|
object: string;
|
|
key: Record<string, string | number>;
|
|
patch: Record<string, unknown>;
|
|
}): Promise<void>;
|
|
};
|
|
/**
|
|
* 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>;
|
|
}): Promise<AgentAction>;
|
|
};
|
|
/** 工具 / 人类工具 / 监听 —— 三型行动的落点 */
|
|
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;
|
|
};
|
|
}>;
|
|
isWriteEffect(tool: string): Promise<boolean>;
|
|
registerIntent(input: {
|
|
taskId: TaskId;
|
|
tool: string;
|
|
input: Record<string, unknown>;
|
|
label: string;
|
|
}): Promise<void>;
|
|
};
|
|
export type CardGateway = {
|
|
issue(input: {
|
|
taskId: TaskId;
|
|
ownerId: StaffId;
|
|
humanCallCap: number;
|
|
workOrder: 'execution' | 'acceptance';
|
|
card: Record<string, unknown>;
|
|
}): Promise<{
|
|
id: string;
|
|
}>;
|
|
};
|
|
export type ListenerRegistrar = {
|
|
register(input: {
|
|
taskId: TaskId;
|
|
eventType: 'data' | 'time' | 'human';
|
|
keyFilter: Record<string, string>;
|
|
deadline: string;
|
|
}): Promise<{
|
|
id: string;
|
|
}>;
|
|
};
|
|
/**
|
|
* 视图流控 —— app 注入 `interaction`(L3)。
|
|
* 「loop:AI 按定位与局面临场组织视图流」(AM 07)。MVP 每回合推一次**案例全景**:
|
|
* 阶段与计划项的生命周期态、当下可为、目标缺口 —— 这些是语义引擎算出来的确定事实,
|
|
* 零 LLM 即可成屏;模型只决定「下一步做什么」,不负责画面。
|
|
*/
|
|
export type ViewGateway = {
|
|
push(input: {
|
|
taskId: TaskId;
|
|
activityId: string;
|
|
template: string;
|
|
scope: Record<string, unknown>;
|
|
}): Promise<void>;
|
|
};
|
|
/**
|
|
* 技能 —— 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';
|
|
title: string;
|
|
detail?: Record<string, unknown>;
|
|
}): Promise<void>;
|
|
/** 装载:按定位选摘活动记忆入 Agent 上下文(渐进披露) */
|
|
loadActivityMemory(activityId: string, limit: number): Promise<Array<{
|
|
layer: string;
|
|
statement: string;
|
|
}>>;
|
|
};
|
|
export type TaskContext = {
|
|
of(taskId: TaskId): Promise<{
|
|
activityId: string;
|
|
ownerId: StaffId;
|
|
humanCallCap: number;
|
|
}>;
|
|
};
|