Archived
feat(loop-engine): auto 步的卡答过了就收步
四个前提都成立才收:声明了 auto、卡发过、这一拍没有未答的卡、配方也没再发新卡。 卡还没发过就不收 —— 那才是替人签字。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LKMZDwMbyKzjw6Hembwvet
This commit is contained in:
@@ -2290,4 +2290,47 @@ describe('auto 步:配方在拍首跑', () => {
|
||||
expect(h.written).toHaveLength(1);
|
||||
expect(h.decided).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('卡答过了、这一步又没有 doneWhen → 收步(人已经签了,收步是照实记录)', async () => {
|
||||
const h = harness({
|
||||
parsed: AUTO_MODEL,
|
||||
runtime: { items: [{ id: 'S1', state: 'Active', worldTouched: true }] } as never,
|
||||
unanswered: [],
|
||||
sfeelCondition: () => false,
|
||||
steps: [
|
||||
{
|
||||
item: 'S1',
|
||||
guide: '',
|
||||
writes: [],
|
||||
reads: [],
|
||||
tools: [],
|
||||
auto: { do: [{ kind: 'card', as: 'decide', variant: 'review', title: '结论审核', options: [{ value: 'approve', label: '确认通过' }], basis: [{ k: 'a', v: 'b' }] }] },
|
||||
},
|
||||
],
|
||||
});
|
||||
const out = await h.runner.runTurn(TASK);
|
||||
expect(h.issued).toHaveLength(0);
|
||||
expect(out.runtime.items.find((i) => i.id === 'S1')?.state).toBe('Completed');
|
||||
});
|
||||
|
||||
it('卡还没发过就不收 —— 那才是替人签字', async () => {
|
||||
const h = harness({
|
||||
parsed: AUTO_MODEL,
|
||||
runtime: AUTO_RUNTIME,
|
||||
sfeelCondition: () => false,
|
||||
steps: [
|
||||
{
|
||||
item: 'S1',
|
||||
guide: '',
|
||||
writes: [],
|
||||
reads: [],
|
||||
tools: [],
|
||||
auto: { do: [{ kind: 'card', as: 'decide', variant: 'review', title: '结论审核', options: [{ value: 'approve', label: '确认通过' }], basis: [{ k: 'a', v: 'b' }] }] },
|
||||
},
|
||||
],
|
||||
});
|
||||
const out = await h.runner.runTurn(TASK);
|
||||
expect(h.issued).toHaveLength(1);
|
||||
expect(out.runtime.items.find((i) => i.id === 'S1')?.state).not.toBe('Completed');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -54,6 +54,11 @@ export type AutoOutcome = {
|
||||
|
||||
const keyText = (key: Readonly<Record<string, string>>): string => Object.values(key).join('|');
|
||||
|
||||
/** 这份配方会不会发卡 —— 「卡答过了就收步」那道门要问它 */
|
||||
export function recipeIssuesCard(recipe: AutoRecipe): boolean {
|
||||
return recipe.do.some((action) => action.kind === 'card');
|
||||
}
|
||||
|
||||
export function createAutoRunner(deps: AutoRunnerDeps): {
|
||||
run(recipe: AutoRecipe, ctx: AutoContext): Promise<AutoOutcome>;
|
||||
} {
|
||||
|
||||
@@ -25,7 +25,7 @@ import type {
|
||||
import { parseCmmn } from '../domain/cmmn-parse.ts';
|
||||
import type { CaseModel } from '../domain/cmmn-parse.ts';
|
||||
import type { CaseRuntime } from '../domain/lifecycle.ts';
|
||||
import { createAutoRunner } from './auto-runner.ts';
|
||||
import { createAutoRunner, recipeIssuesCard } from './auto-runner.ts';
|
||||
import {
|
||||
achieveMilestones,
|
||||
autoCompleteReady,
|
||||
@@ -2016,6 +2016,30 @@ export function createTurnRunner(deps: TurnRunnerDeps): LoopEngineService {
|
||||
return { runtime: reswept.runtime, enabled: reswept.enabled, navigation: { kind: 'converged' }, status: 'running' };
|
||||
}
|
||||
}
|
||||
/*
|
||||
* **卡答过了就收步**(spec §5.5)。走到这儿的四个前提都成立:
|
||||
* 这一步声明了 auto、卡发过(worldTouched)、这一拍没有未答的卡(前面 unanswered 把住)、
|
||||
* 配方这一拍也没再发新卡。**人已经签了字,收步是照实记录,不是替人签字** ——
|
||||
* 「人工激活项不给 doneWhen」那条纪律防的是引擎抢在人前面,这里人在前面。
|
||||
*/
|
||||
const noDoneWhen = current.doneWhen === undefined || current.doneWhen.trim() === '';
|
||||
const carded = stateRuntime.items.find((i) => i.id === current.item)?.worldTouched === true;
|
||||
if (noDoneWhen && carded && recipeIssuesCard(current.auto)) {
|
||||
const active = readyToWork(parsed, stateRuntime, current.item);
|
||||
const settled = transitionItem(active, current.item, 'Completed');
|
||||
await deps.memory.append({
|
||||
taskId,
|
||||
kind: 'engine-transition',
|
||||
/* 标题别以「完成」开头 —— `task-log.actorOf` 按 /^(完成|开始办)/ 认 AI,而这一步是引擎收的 */
|
||||
title: `自动收步:${current.item} —— 卡已交回,这一步到此为止`,
|
||||
detail: { internal: true, item: current.item, auto: current.item },
|
||||
});
|
||||
await deps.state.save(taskId, model, parsed, settled);
|
||||
/* `Navigation.kind` 没有 'progressed' 这一档(只有 proceed/breakpoint/converged,
|
||||
见上面 2014 行那条注释)—— 这一拍到此为止、下一拍重新导航,同一种写法 */
|
||||
return { runtime: settled, enabled: stateEnabled, navigation: { kind: 'converged' }, status: 'running' };
|
||||
}
|
||||
|
||||
if (current.guide.trim() === '') {
|
||||
/* 没有 guide、没发卡、doneWhen 也不成立 → 配方没做到位,是包的 bug,交人 */
|
||||
await deps.memory.append({
|
||||
|
||||
Reference in New Issue
Block a user