Mastra 化前置:loop 三修 —— 提交放行发真验收卡、事件只记不跑、轨迹喂回叙事面

勘察(两路 code-explorer)坐实 loop 范式三处真缺口,本轮先修(用户拍板),
与 Mastra 化解耦、按外科手术做(活动内 MVP 期高内聚不过度抽象):

① 提交放行走不到 released:submit/判齐只返回 submitted,task 层当 running
  处理,任务原地重跑到预算耗尽(exhausted 而非 released)。
  修:提交 = 发**验收卡**挂起等裁决(raiseAcceptance 唯一出口:发卡、立监听、
  在途验收入状态信封 sys.loop_state.pending,迁移 0015);判齐重入不发第二张卡;
  裁决 pass → finished released(选口随放行带回),reject → finished 不放行。
  TurnOutcome 增 finished/released/selectedPorts,engines 接线透传。

② onEvent 双拍:自跑一整拍后 deliver 的 tick 再跑一拍 —— 每次人卡回传多耗
  一次不计 steps/activeMs 预算的模型决策。修:onEvent **只记不跑**
  (登记 occur / 收验收裁决入信封,存快照即返),推进统一归 tick,预算一个口径。

③ Agent 每拍失忆:memory.window 定义了但零调用,任务自身轨迹从不喂回 prompt。
  修:叙事面新增【本任务最近轨迹】段,窗口大小取活动声明的
  manifest.declarations.harness.memoryWindow(活动侧唯一记忆配置,不再硬编码)。

测试:turn-runner-acceptance 八例;全量绿,五闸退出码过;loop CONTRACT.md 同步。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KeDBx2PHLavbURXB8LJJ7m
This commit is contained in:
2026-07-28 23:02:41 +08:00
co-authored by Claude Fable 5
parent 967627d753
commit 1917320584
12 changed files with 340 additions and 28 deletions
@@ -191,18 +191,42 @@ export function createTurnRunner(deps: TurnRunnerDeps): LoopEngineService {
}
case 'submit': {
await deps.memory.append({
taskId,
kind: 'thought',
title: '判定成果已齐,提交放行',
detail: { rationale: action.rationale },
});
// 终局唯放行:引擎不把案计划置为 Completed —— 那是 release 的事
return { runtime, status: 'submitted' };
// 终局唯放行:提交 = 发验收卡挂起等人裁决,不是自结(引擎不把案计划置为 Completed)
return raiseAcceptance(taskId, runtime, action.rationale);
}
}
}
/**
* 提交放行的唯一出口:发**验收卡**、立监听、把在途验收记进状态信封。
* 信封里有在途验收即不再发第二张(判齐可能被无关事件反复唤起 ——
* 没有这道持久痕,每次唤起都会往负责人工作台堆一张新卡)。
*/
async function raiseAcceptance(
taskId: TaskId,
runtime: CaseRuntime,
rationale: string,
): Promise<{ runtime: CaseRuntime; status: TurnOutcome['status'] }> {
const context = await deps.tasks.of(taskId);
const card = await deps.cards.issue({
taskId,
ownerId: context.ownerId,
humanCallCap: context.humanCallCap,
workOrder: 'acceptance',
card: { title: '收口验收', basis: [{ k: '提交理由', v: rationale }] },
});
await deps.listeners.register({ taskId, eventType: 'human', keyFilter: { cardId: card.id }, deadline: 'P3D' });
const { model, parsed } = await loaded(taskId);
await deps.state.save(taskId, model, parsed, runtime, { acceptanceCardId: card.id });
await deps.memory.append({
taskId,
kind: 'thought',
title: '判定成果已齐,提交放行 —— 验收卡已发,等裁决',
detail: { rationale, cardId: card.id },
});
return { runtime, status: 'awaiting' };
}
return {
async load(taskId, model: LoopModel) {
const parsed = parseCmmn(model.cmmnXml);
@@ -227,10 +251,31 @@ export function createTurnRunner(deps: TurnRunnerDeps): LoopEngineService {
/** 一个回合:定位 → 择向 → 决断 → 行动 → 写数据 */
async runTurn(taskId): Promise<TurnOutcome> {
const { model, parsed, runtime, scope, enabled } = await publishFor(taskId);
const pending = (await loaded(taskId)).pending;
// 齐了即收:必做项完成且无进行中项 → 提交放行(仍必经放行卡)
/*
* 在途验收先于一切:裁决到了按它定终局(released 与否都是终局,不再问 Agent);
* 没到就继续等 —— 不重发卡、不空跑模型。
*/
if (pending !== undefined) {
if (pending.verdict === undefined) {
return { runtime, enabled, navigation: { kind: 'converged' }, status: 'awaiting' };
}
await deps.state.save(taskId, model, parsed, runtime, null);
return {
runtime,
enabled,
navigation: { kind: 'converged' },
status: 'finished',
released: pending.verdict === 'pass',
...(pending.selectedPorts === undefined ? {} : { selectedPorts: pending.selectedPorts }),
};
}
// 齐了即收:必做项完成且无进行中项 → 提交放行(发验收卡,终局唯放行)
if (autoCompleteReady(parsed, runtime)) {
return { runtime, enabled, navigation: { kind: 'converged' }, status: 'submitted' };
const raised = await raiseAcceptance(taskId, runtime, '必做项全部完成,无进行中项');
return { runtime: raised.runtime, enabled, navigation: { kind: 'converged' }, status: raised.status };
}
// 导航即交集:当下可为 ∩ 目标所需
@@ -254,9 +299,14 @@ export function createTurnRunner(deps: TurnRunnerDeps): LoopEngineService {
* 正文与知识引用等 AI 说要才加载 —— 一次把所有正文塞进去,
* 上下文会被方法论淹掉,真正的局面反而看不见。
*/
// 任务自身的最近轨迹(窗口大小 = 活动声明的 memoryWindow)—— 没有它 Agent 每拍失忆
const trail = await deps.memory.window(taskId);
const narrative = [
model.narrative,
...(await deps.memory.loadActivityMemory(activityId, 20)).map((m) => `[${m.layer}] ${m.statement}`),
trail.length === 0
? ''
: `【本任务最近轨迹】\n${trail.map((t) => `- [${t.kind}] ${t.title}`).join('\n')}`,
catalog.length === 0
? ''
: `【可装配的技能(目录)】\n${catalog.map((s) => `- ${s.id} ${s.label}:${s.applicability}`).join('\n')}`,
@@ -345,7 +395,7 @@ export function createTurnRunner(deps: TurnRunnerDeps): LoopEngineService {
/** 事件驱动续跑:挂起期间事件不丢,恢复即补投 */
async onEvent(taskId, event) {
const { model, parsed, runtime } = await loaded(taskId);
const { model, parsed, runtime, pending } = await loaded(taskId);
await deps.memory.append({
taskId,
kind: 'observation',
@@ -358,9 +408,22 @@ export function createTurnRunner(deps: TurnRunnerDeps): LoopEngineService {
const listener = parsed.items.find((i) => i.id === event.ref && i.type.endsWith('EventListener'));
if (listener !== undefined) next = occur(runtime, listener.id);
await deps.state.save(taskId, model, parsed, next);
// 前提前推:事件写数据 → 任务视图变化 → 前提求值 → 新项 Enabled
return this.runTurn(taskId);
/*
* 验收裁决到达:落进状态信封,终局由下一拍(deliver → tick → runTurn)裁定。
* 只记不跑 —— 推进(可能含一次模型决策)统一发生在 tick,预算才有一个口径。
*/
let nextPending = pending ?? null;
if (event.kind === 'human' && pending?.acceptanceCardId === event.ref) {
const verdict = event.payload['verdict'] === 'pass' ? ('pass' as const) : ('reject' as const);
const ports = event.payload['selectedPorts'];
nextPending = {
...pending,
verdict,
...(typeof ports === 'object' && ports !== null ? { selectedPorts: ports as Record<string, string> } : {}),
};
}
await deps.state.save(taskId, model, parsed, next, nextPending);
},
/** 能算的不问 LLM:输入齐备的决策表确定性求值,命中行留痕 */