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
+10 -2
View File
@@ -76,8 +76,11 @@ export type TurnOutcome = {
readonly enabled: EnabledSet;
readonly navigation: Navigation;
readonly action?: AgentAction;
readonly status: 'running' | 'awaiting' | 'submitted' | 'blocked';
readonly status: 'running' | 'awaiting' | 'submitted' | 'finished' | 'blocked';
readonly blockedReason?: string;
/** 终局唯放行:finished 时带裁决 —— released=true 才算走完,选口随放行确认 */
readonly released?: boolean;
readonly selectedPorts?: Record<string, string>;
};
export type LoopEngineService = {
/** 装载:XML 进语义引擎,说明书进 Agent 上下文;版本全部入信封 */
@@ -88,11 +91,16 @@ export type LoopEngineService = {
/** 跑一个回合:定位 → 择向 → 决断 → 行动 → 写数据 */
runTurn(taskId: TaskId): Promise<TurnOutcome>;
/** 事件驱动续跑:数据 / 时间 / 人 / 控制 */
/**
* 事件到达:**只记不跑**(登记 occur / 收验收裁决,存快照即返)。
* 从前这里自跑一整拍、deliver 随后的 tick 又跑一拍 —— 每次人卡回传
* 多耗一次不计 steps/activeMs 预算的模型决策。推进统一归 tick。
*/
onEvent(taskId: TaskId, event: {
kind: 'data' | 'time' | 'human' | 'control';
ref: string;
payload: Record<string, unknown>;
}): Promise<TurnOutcome>;
}): Promise<void>;
/** DMN 确定性求值 —— 能算的不问 LLM */
decide(taskId: TaskId, decision: string): Promise<DecisionResult>;
/** 驳回:相关计划项以新实例重新入场(迁移不是状态) */
+16 -1
View File
@@ -10,11 +10,12 @@ export type Clock = {
};
/** 运行时实例状态 —— loop 的模型定位与就绪集(OS 09 三份持久状态之一) */
export type LoopStateStore = {
save(taskId: TaskId, model: LoopModel, parsed: CaseModel, runtime: CaseRuntime): Promise<void>;
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 */
@@ -69,6 +70,15 @@ export type ToolGateway = {
label: string;
}): Promise<void>;
};
/**
* 在途验收(提交放行的持久痕):卡发出即记,裁决到达再记 verdict ——
* 判齐重入不再发第二张卡;恢复的那一拍据 verdict 定终局(released 与否)。
*/
export type PendingAcceptance = {
readonly acceptanceCardId: string;
readonly verdict?: 'pass' | 'reject';
readonly selectedPorts?: Record<string, string>;
};
export type CardGateway = {
issue(input: {
taskId: TaskId;
@@ -133,6 +143,11 @@ export type MemoryRecorder = {
layer: string;
statement: string;
}>>;
/** 本任务自己的最近轨迹(窗口大小由活动声明的 memoryWindow 定)—— 喂回叙事面,Agent 每拍不失忆 */
window(taskId: TaskId): Promise<Array<{
kind: string;
title: string;
}>>;
};
export type TaskContext = {
of(taskId: TaskId): Promise<{
+16 -3
View File
@@ -425,6 +425,12 @@ export function wireEngines(deps: EngineWiringDeps): EngineWiring {
append: memoryRecorder.append as never,
loadActivityMemory: async (activityId: string, limit: number) =>
(await deps.memory.loadActivityMemory(activityId, { limit })).map((m) => ({ layer: m.layer, statement: m.statement })),
// 任务自身的最近轨迹:窗口大小是活动声明的唯一 Harness 记忆配置(manifest.declarations.harness.memoryWindow)
window: async (taskId) => {
const { activityId } = await taskContext.of(taskId);
const size = (await deps.registry.manifestOf(activityId)).declarations.harness.memoryWindow;
return (await deps.memory.window(taskId, size)).map((m) => ({ kind: m.kind, title: m.title }));
},
},
tasks: taskContext,
clock,
@@ -508,11 +514,18 @@ export function wireEngines(deps: EngineWiringDeps): EngineWiring {
load: (taskId, model) => loop.load(taskId, model as never).then(() => undefined),
advance: async (taskId) => {
const outcome = await loop.runTurn(taskId);
return { status: outcome.status, ...(outcome.blockedReason === undefined ? {} : { reason: outcome.blockedReason }) };
return {
status: outcome.status,
...(outcome.blockedReason === undefined ? {} : { reason: outcome.blockedReason }),
// 终局唯放行:runTurn 依在途验收的裁决给出 finished 时,released/选口一并带回
...(outcome.released === undefined ? {} : { released: outcome.released }),
...(outcome.selectedPorts === undefined ? {} : { selectedPorts: outcome.selectedPorts }),
};
},
// 只记不跑:登记事件/收裁决即返,推进统一归 deliver 随后的 tick —— 预算一个口径
onEvent: async (taskId, event) => {
const outcome = await loop.onEvent(taskId, event);
return { status: outcome.status, ...(outcome.blockedReason === undefined ? {} : { reason: outcome.blockedReason }) };
await loop.onEvent(taskId, event);
return { status: 'running' as const };
},
},
flow: {
@@ -38,4 +38,12 @@
调人 = 立监听听人;AI 对世界只有触达(动作,落在放行)与等待(监听)两种姿势
- **终局唯放行与 Harness**:引擎不把案计划置 Completed —— 那是 release 的事;
`mayCompleteCase` 只对 release 为真,`mayTerminateCase` 只对 harness / owner / governance 为真
- **提交放行 = 发验收卡**(2026-07-28 三修①):submit / 判齐(autoCompleteReady)
不再返回 `submitted` 空转 —— 发**验收卡**、立监听、在途验收入状态信封
(`loop_state.pending`,迁移 0015);判齐重入不发第二张卡;裁决到达后由下一拍
定终局:pass → `finished` + `released: true`(选口随放行带回),reject → 不放行
- **onEvent 只记不跑**(三修②):事件到达只登记 occur / 收裁决入信封,存快照即返;
推进(可能含一次模型决策)统一发生在 tick —— steps/activeMs/tokens 一个预算口径
- **任务轨迹喂回叙事面**(三修③):`memory.window(taskId)` 的最近轨迹进
【本任务最近轨迹】段,窗口大小 = 活动声明的 `harness.memoryWindow`,Agent 每拍不失忆
- **驳回是迁移不是状态**:`reenter` 以 repetitionRule 新实例重新入场
@@ -0,0 +1,163 @@
/**
* loop 的「提交放行」三修(2026-07-28,Mastra 化前置):
* ① 提交/判齐 = 发**验收卡**挂起等裁决 —— 从前只返回 submitted,任务原地重跑到预算耗尽;
* ② onEvent **只记不跑** —— 从前自跑一拍、tick 再跑一拍,每次回传多耗一次不计预算的模型决策;
* ③ 任务自身最近轨迹喂回叙事面 —— 从前 Agent 每拍失忆。
*/
import { describe, expect, it } from 'vitest';
import type { TaskId } from '@contracts';
import { createTurnRunner } from '../internal/usecase/turn-runner.ts';
import type { TurnRunnerDeps } from '../internal/usecase/turn-runner.ts';
import type { PendingAcceptance } from '../port.ts';
import type { CaseModel } from '../internal/domain/cmmn-parse.ts';
import type { CaseRuntime } from '../internal/domain/lifecycle.ts';
const TASK = 't-1' as TaskId;
/** 空案例模型:必做项全无 + autoComplete → 一进 runTurn 就「判齐」 */
const READY_MODEL: CaseModel = { name: '空案', autoComplete: true, items: [], sentries: [] } as never;
const READY_RUNTIME: CaseRuntime = { items: [] } as never;
/** 一个未完成必做项:走 Agent 决断路径(叙事面测试用) */
const OPEN_MODEL: CaseModel = {
name: '有活没干完',
autoComplete: true,
items: [{ id: 'i1', name: '干活', type: 'humanTask', discretionary: false, entryCriteria: [], exitCriteria: [] }],
sentries: [],
} as never;
const OPEN_RUNTIME: CaseRuntime = { items: [{ id: 'i1', state: 'Available' }] } as never;
type Harness = {
runner: ReturnType<typeof createTurnRunner>;
readonly issued: Array<{ workOrder: string; card: Record<string, unknown> }>;
readonly saves: Array<{ pending: PendingAcceptance | null | undefined }>;
readonly decided: string[];
envelope: { model: unknown; parsed: CaseModel; runtime: CaseRuntime; pending?: PendingAcceptance };
};
function harness(over: { parsed?: CaseModel; runtime?: CaseRuntime; pending?: PendingAcceptance; trail?: Array<{ kind: string; title: string }> } = {}): Harness {
const issued: Harness['issued'] = [];
const saves: Harness['saves'] = [];
const decided: string[] = [];
const h: Harness = { issued, saves, decided, runner: undefined as never, envelope: undefined as never };
h.envelope = {
model: { cmmnXml: '<xml/>', narrative: '说明书' },
parsed: over.parsed ?? READY_MODEL,
runtime: over.runtime ?? READY_RUNTIME,
...(over.pending === undefined ? {} : { pending: over.pending }),
};
const deps: TurnRunnerDeps = {
state: {
save: async (_t, model, parsed, runtime, pending) => {
saves.push({ pending });
h.envelope = { model, parsed, runtime, ...(pending == null ? {} : { pending }) };
},
load: async () => h.envelope as never,
},
sfeel: { condition: () => true, expression: () => undefined } as never,
data: { view: async () => ({}) } as never,
agent: {
decide: async (input: { narrative: string }) => {
decided.push(input.narrative);
return { kind: 'submit', rationale: '我判齐了' } as never;
},
} as never,
tools: { invoke: async () => ({ ok: true }), isWriteEffect: async () => false, registerIntent: async () => undefined } as never,
cards: {
issue: async (input: { workOrder: string; card: Record<string, unknown> }) => {
issued.push({ workOrder: input.workOrder, card: input.card });
return { id: `card-${issued.length}` };
},
} as never,
listeners: { register: async () => ({ id: 'lsn-1' }) } as never,
memory: {
append: async () => undefined,
loadActivityMemory: async () => [],
window: async () => over.trail ?? [],
} as never,
views: { push: async () => undefined } as never,
skills: { index: async () => [], stamp: async () => [] } as never,
tasks: { of: async () => ({ taskId: TASK, activityId: 'a-1', ownerId: 's-1', humanCallCap: 9 }) } as never,
clock: { now: () => new Date('2026-07-28T00:00:00Z') } as never,
};
h.runner = createTurnRunner(deps);
return h;
}
describe('提交放行 —— 终局唯放行,卡只发一张', () => {
it('判齐即发验收卡挂起(不再返回 submitted 空转),在途验收入信封', async () => {
const h = harness();
const outcome = await h.runner.runTurn(TASK);
expect(outcome.status).toBe('awaiting');
expect(h.issued).toEqual([
{ workOrder: 'acceptance', card: { title: '收口验收', basis: [{ k: '提交理由', v: '必做项全部完成,无进行中项' }] } },
]);
expect(h.envelope.pending?.acceptanceCardId).toBe('card-1');
expect(h.decided).toHaveLength(0); // 判齐不问 Agent
});
it('在途验收未裁决时再唤起:继续等,不发第二张卡', async () => {
const h = harness({ pending: { acceptanceCardId: 'card-9' } });
const outcome = await h.runner.runTurn(TASK);
expect(outcome.status).toBe('awaiting');
expect(h.issued).toHaveLength(0);
});
it('Agent 主动 submit 也走同一条路:发卡一张、状态 awaiting', async () => {
const h = harness({ parsed: OPEN_MODEL, runtime: OPEN_RUNTIME });
const outcome = await h.runner.runTurn(TASK);
expect(outcome.status).toBe('awaiting');
expect(h.issued).toHaveLength(1);
expect(h.issued[0]!.workOrder).toBe('acceptance');
});
it('裁决 pass → finished released,选口带回,信封清空', async () => {
const h = harness({ pending: { acceptanceCardId: 'card-9', verdict: 'pass', selectedPorts: { 议价结果: 'won' } } });
const outcome = await h.runner.runTurn(TASK);
expect(outcome.status).toBe('finished');
expect(outcome.released).toBe(true);
expect(outcome.selectedPorts).toEqual({ 议价结果: 'won' });
expect(h.saves.at(-1)?.pending).toBeNull();
});
it('裁决 reject → finished 不放行', async () => {
const h = harness({ pending: { acceptanceCardId: 'card-9', verdict: 'reject' } });
const outcome = await h.runner.runTurn(TASK);
expect(outcome.status).toBe('finished');
expect(outcome.released).toBe(false);
});
});
describe('onEvent —— 只记不跑', () => {
it('验收裁决到达:落进信封即返,不跑模型(推进归 tick,预算一个口径)', async () => {
const h = harness({ pending: { acceptanceCardId: 'card-9' } });
await h.runner.onEvent(TASK, { kind: 'human', ref: 'card-9', payload: { verdict: 'pass', selectedPorts: { 议价结果: 'won' } } });
expect(h.decided).toHaveLength(0);
expect(h.envelope.pending).toEqual({ acceptanceCardId: 'card-9', verdict: 'pass', selectedPorts: { 议价结果: 'won' } });
});
it('无关人卡回传不动在途验收', async () => {
const h = harness({ pending: { acceptanceCardId: 'card-9' } });
await h.runner.onEvent(TASK, { kind: 'human', ref: 'card-other', payload: { values: { x: 1 } } });
expect(h.envelope.pending).toEqual({ acceptanceCardId: 'card-9' });
expect(h.decided).toHaveLength(0);
});
});
describe('叙事面 —— 任务自身轨迹喂回', () => {
it('窗口有轨迹时,Agent 收到【本任务最近轨迹】段', async () => {
const h = harness({
parsed: OPEN_MODEL,
runtime: OPEN_RUNTIME,
trail: [
{ kind: 'thought', title: '上一拍我调了历史价' },
{ kind: 'result', title: '查得 3 条锚点' },
],
});
await h.runner.runTurn(TASK);
expect(h.decided).toHaveLength(1);
expect(h.decided[0]).toContain('【本任务最近轨迹】');
expect(h.decided[0]).toContain('- [thought] 上一拍我调了历史价');
expect(h.decided[0]).toContain('- [result] 查得 3 条锚点');
});
});
+10 -2
View File
@@ -77,8 +77,11 @@ export type TurnOutcome = {
readonly enabled: EnabledSet;
readonly navigation: Navigation;
readonly action?: AgentAction;
readonly status: 'running' | 'awaiting' | 'submitted' | 'blocked';
readonly status: 'running' | 'awaiting' | 'submitted' | 'finished' | 'blocked';
readonly blockedReason?: string;
/** 终局唯放行:finished 时带裁决 —— released=true 才算走完,选口随放行确认 */
readonly released?: boolean;
readonly selectedPorts?: Record<string, string>;
};
export type LoopEngineService = {
@@ -90,7 +93,12 @@ export type LoopEngineService = {
/** 跑一个回合:定位 → 择向 → 决断 → 行动 → 写数据 */
runTurn(taskId: TaskId): Promise<TurnOutcome>;
/** 事件驱动续跑:数据 / 时间 / 人 / 控制 */
onEvent(taskId: TaskId, event: { kind: 'data' | 'time' | 'human' | 'control'; ref: string; payload: Record<string, unknown> }): Promise<TurnOutcome>;
/**
* 事件到达:**只记不跑**(登记 occur / 收验收裁决,存快照即返)。
* 从前这里自跑一整拍、deliver 随后的 tick 又跑一拍 —— 每次人卡回传
* 多耗一次不计 steps/activeMs 预算的模型决策。推进统一归 tick。
*/
onEvent(taskId: TaskId, event: { kind: 'data' | 'time' | 'human' | 'control'; ref: string; payload: Record<string, unknown> }): Promise<void>;
/** DMN 确定性求值 —— 能算的不问 LLM */
decide(taskId: TaskId, decision: string): Promise<DecisionResult>;
/** 驳回:相关计划项以新实例重新入场(迁移不是状态) */
@@ -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,16 +191,40 @@ export function createTurnRunner(deps: TurnRunnerDeps): LoopEngineService {
}
case 'submit': {
// 终局唯放行:提交 = 发验收卡挂起等人裁决,不是自结(引擎不把案计划置为 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: action.rationale },
title: '判定成果已齐,提交放行 —— 验收卡已发,等裁决',
detail: { rationale, cardId: card.id },
});
// 终局唯放行:引擎不把案计划置为 Completed —— 那是 release 的事
return { runtime, status: 'submitted' };
}
}
return { runtime, status: 'awaiting' };
}
return {
@@ -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:输入齐备的决策表确定性求值,命中行留痕 */
+22 -2
View File
@@ -10,8 +10,16 @@ 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>;
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 */
@@ -58,6 +66,16 @@ export type ToolGateway = {
registerIntent(input: { taskId: TaskId; tool: string; input: Record<string, unknown>; label: string }): Promise<void>;
};
/**
* 在途验收(提交放行的持久痕):卡发出即记,裁决到达再记 verdict ——
* 判齐重入不再发第二张卡;恢复的那一拍据 verdict 定终局(released 与否)。
*/
export type PendingAcceptance = {
readonly acceptanceCardId: string;
readonly verdict?: 'pass' | 'reject';
readonly selectedPorts?: Record<string, string>;
};
export type CardGateway = {
issue(input: {
taskId: TaskId;
@@ -112,6 +130,8 @@ export type MemoryRecorder = {
}): Promise<void>;
/** 装载:按定位选摘活动记忆入 Agent 上下文(渐进披露) */
loadActivityMemory(activityId: string, limit: number): Promise<Array<{ layer: string; statement: string }>>;
/** 本任务自己的最近轨迹(窗口大小由活动声明的 memoryWindow 定)—— 喂回叙事面,Agent 每拍不失忆 */
window(taskId: TaskId): Promise<Array<{ kind: string; title: string }>>;
};
export type TaskContext = {
@@ -0,0 +1,3 @@
-- loop 的「提交放行」修成真机制:判齐后发验收卡挂起等裁决(从前只返回 submitted,
-- 任务原地重跑直到预算耗尽)。在途的验收卡与裁决结果落状态信封,防重复发卡、供恢复判定。
alter table sys.loop_state add column pending jsonb;
+7
View File
@@ -106,6 +106,13 @@
"when": 1785130113169,
"tag": "0014_instance_assignee",
"breakpoints": true
},
{
"idx": 15,
"version": "7",
"when": 1785130113170,
"tag": "0015_loop_pending_acceptance",
"breakpoints": true
}
]
}