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/api-report/apps/api/src/modules/loop-engine/port.d.ts
T
ChaiHaoandClaude Opus 5 94f31f267b 调优:并发推进的真根因 + loop 的三处行为修正
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
2026-07-27 11:31:41 +08:00

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;
}>;
};