Archived
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:
@@ -11,5 +11,7 @@ export const loopStateTable = sys.table('loop_state', {
|
||||
/** 解析后的 CMMN 子集结构 —— 免去每回合重解析 */
|
||||
parsed: jsonb('parsed').notNull(),
|
||||
runtime: jsonb('runtime').notNull(),
|
||||
/** 在途验收:提交放行后发出的验收卡与(到达后的)裁决 —— 防重复发卡、恢复时定终局 */
|
||||
pending: jsonb('pending'),
|
||||
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
|
||||
});
|
||||
|
||||
@@ -5,21 +5,22 @@ import { eq } from 'drizzle-orm';
|
||||
import type { LoopModel, TaskId } from '@contracts';
|
||||
import { loopModelSchema } from '@contracts';
|
||||
import type { Db } from '@shared/db.ts';
|
||||
import type { LoopStateStore } from '../../port.ts';
|
||||
import type { LoopStateStore, PendingAcceptance } from '../../port.ts';
|
||||
import type { CaseModel } from '../domain/cmmn-parse.ts';
|
||||
import type { CaseRuntime } from '../domain/lifecycle.ts';
|
||||
import { loopStateTable } from './loop-state-schema.ts';
|
||||
|
||||
export function createLoopStateStore(db: Db): LoopStateStore {
|
||||
return {
|
||||
async save(taskId: TaskId, model: LoopModel, parsed: CaseModel, runtime: CaseRuntime) {
|
||||
const values = { taskId, model, parsed, runtime, updatedAt: new Date() };
|
||||
async save(taskId: TaskId, model: LoopModel, parsed: CaseModel, runtime: CaseRuntime, pending?: PendingAcceptance | null) {
|
||||
const values = { taskId, model, parsed, runtime, pending: pending ?? null, updatedAt: new Date() };
|
||||
await db
|
||||
.insert(loopStateTable)
|
||||
.values(values)
|
||||
.onConflictDoUpdate({
|
||||
target: loopStateTable.taskId,
|
||||
set: { model, parsed, runtime, updatedAt: values.updatedAt },
|
||||
// pending 省参 = 清空(在途验收是每次落笔都要如实交代的状态,不做“不传就保留”的暗默)
|
||||
set: { model, parsed, runtime, pending: values.pending, updatedAt: values.updatedAt },
|
||||
});
|
||||
},
|
||||
async load(taskId: TaskId) {
|
||||
@@ -30,6 +31,7 @@ export function createLoopStateStore(db: Db): LoopStateStore {
|
||||
model: loopModelSchema.parse(row.model),
|
||||
parsed: row.parsed as CaseModel,
|
||||
runtime: row.runtime as CaseRuntime,
|
||||
...(row.pending === null ? {} : { pending: row.pending as PendingAcceptance }),
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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:输入齐备的决策表确定性求值,命中行留痕 */
|
||||
|
||||
Reference in New Issue
Block a user