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.written).toHaveLength(1);
|
||||||
expect(h.decided).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('|');
|
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): {
|
export function createAutoRunner(deps: AutoRunnerDeps): {
|
||||||
run(recipe: AutoRecipe, ctx: AutoContext): Promise<AutoOutcome>;
|
run(recipe: AutoRecipe, ctx: AutoContext): Promise<AutoOutcome>;
|
||||||
} {
|
} {
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ import type {
|
|||||||
import { parseCmmn } from '../domain/cmmn-parse.ts';
|
import { parseCmmn } from '../domain/cmmn-parse.ts';
|
||||||
import type { CaseModel } from '../domain/cmmn-parse.ts';
|
import type { CaseModel } from '../domain/cmmn-parse.ts';
|
||||||
import type { CaseRuntime } from '../domain/lifecycle.ts';
|
import type { CaseRuntime } from '../domain/lifecycle.ts';
|
||||||
import { createAutoRunner } from './auto-runner.ts';
|
import { createAutoRunner, recipeIssuesCard } from './auto-runner.ts';
|
||||||
import {
|
import {
|
||||||
achieveMilestones,
|
achieveMilestones,
|
||||||
autoCompleteReady,
|
autoCompleteReady,
|
||||||
@@ -2016,6 +2016,30 @@ export function createTurnRunner(deps: TurnRunnerDeps): LoopEngineService {
|
|||||||
return { runtime: reswept.runtime, enabled: reswept.enabled, navigation: { kind: 'converged' }, status: 'running' };
|
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() === '') {
|
if (current.guide.trim() === '') {
|
||||||
/* 没有 guide、没发卡、doneWhen 也不成立 → 配方没做到位,是包的 bug,交人 */
|
/* 没有 guide、没发卡、doneWhen 也不成立 → 配方没做到位,是包的 bug,交人 */
|
||||||
await deps.memory.append({
|
await deps.memory.append({
|
||||||
|
|||||||
Reference in New Issue
Block a user