/** * flow 步序推进 —— 纯逻辑,零 IO(编码规范 §5.3;语义出处 AM 05)。 * * 「flow 是活动内的**确定性范式**:业务模型 = 步序声明 —— 一条谱; * 引擎照谱执行,**零临场调度**,调度权归系统。 * 差异只有一处:下一步由**声明给定**,不由 AI 决断。 * LLM 在 flow 里可以出现,但只以『被调用的工具』出现,不是调度者。」 */ import type { FlowStep } from '@contracts'; export const STEP_STATES = ['pending', 'running', 'awaiting', 'done', 'skipped', 'failed'] as const; export type StepState = (typeof STEP_STATES)[number]; export type StepRuntime = { readonly name: string; readonly state: StepState; readonly attempt: number; readonly output?: Record; readonly error?: string; /** 验收工单的放行裁决:pass / 驳回类别 —— 末步据此定终局 */ readonly verdict?: string; /** 选择组的出口选择:恰发一口,随放行一并确认(AM 1.2) */ readonly selectedPorts?: Record; }; export type FlowPosition = { /** 步序位置 —— 三份持久状态之一(OS 09 断点续跑) */ readonly index: number; readonly steps: readonly StepRuntime[]; }; export function initialPosition(steps: readonly FlowStep[]): FlowPosition { return { index: 0, steps: steps.map((s) => ({ name: s.name, state: 'pending', attempt: 0 })) }; } /** 条件跳步:条件必须是任务数据上的**确定性谓词**,由引擎求值,零 LLM(AM 5.2) */ export type PredicateEvaluator = (expression: string, scope: Readonly>) => boolean; export type NextAction = | { readonly kind: 'run'; readonly step: FlowStep; readonly index: number } | { readonly kind: 'skip'; readonly step: FlowStep; readonly index: number; readonly reason: string } | { readonly kind: 'await'; readonly step: FlowStep; readonly index: number } | { readonly kind: 'finished' }; /** * 下一步由声明给定。异步步(人类工具步 / 等事件的步)进入 `await` —— 完成条件 = 监听。 */ export function nextAction( declaration: readonly FlowStep[], position: FlowPosition, scope: Readonly>, evaluate: PredicateEvaluator, ): NextAction { for (let i = position.index; i < declaration.length; i += 1) { const step = declaration[i]!; const runtime = position.steps[i]; if (runtime?.state === 'done' || runtime?.state === 'skipped') continue; if (runtime?.state === 'awaiting') return { kind: 'await', step, index: i }; if (step.when !== undefined && !evaluate(step.when, scope)) { return { kind: 'skip', step, index: i, reason: `条件不成立:${step.when}` }; } return { kind: 'run', step, index: i }; } return { kind: 'finished' }; } export function markStep(position: FlowPosition, index: number, patch: Partial): FlowPosition { const steps = position.steps.map((s, i) => (i === index ? { ...s, ...patch } : s)); const advanced = patch.state === 'done' || patch.state === 'skipped'; return { index: advanced ? index + 1 : index, steps }; } /* ── 失败与缺口:声明给定,不临场(AM 5.3) ────────────────────────── */ export type FailureAction = | { readonly kind: 'retry'; readonly attempt: number; readonly backoff: string } /** 转人兜底:改发人类工具卡,卡内容随声明 */ | { readonly kind: 'human'; readonly workOrder: 'execution' | 'acceptance' } /** 受阻待人:挂起,携阻因 */ | { readonly kind: 'blocked' }; /** * 「每步声明重试次数与间隔;重试仍败,按声明走下一步。 * **flow 内不得出现『由 AI 决定怎么办』的失败处理** —— 需要临场,即说明该活动不该是 flow。」 */ export function failureAction(step: FlowStep, attempt: number): FailureAction { if (attempt < step.failure.retry.times) { return { kind: 'retry', attempt: attempt + 1, backoff: step.failure.retry.backoff }; } return step.failure.fallback.kind === 'human' ? { kind: 'human', workOrder: step.failure.fallback.workOrder } : { kind: 'blocked' }; } /* ── 静态检查:谱必须全程可静态检查(AM 12.3) ────────────────────── */ export type FlowIssue = { readonly step: string; readonly message: string }; export function checkDeclaration(steps: readonly FlowStep[]): FlowIssue[] { const out: FlowIssue[] = []; const names = new Set(); for (const step of steps) { if (names.has(step.name)) out.push({ step: step.name, message: '步名重复' }); names.add(step.name); // 步序声明完整:步型 / 入参映射 / 出参写入 / 完成条件 / 失败策略齐备 if (step.failure.retry.times < 0) out.push({ step: step.name, message: '重试次数不得为负' }); if (step.kind === 'human' && step.deadline.trim() === '') { out.push({ step: step.name, message: '人类工具步必配时限 —— 无死等' }); } } return out; } /** 终态映射(AM 5.3):放行完成 = 末步验收工单「放行通过」 */ export function lastStepIsAcceptance(steps: readonly FlowStep[]): boolean { const last = steps.at(-1); return last !== undefined && last.kind === 'human' && last.workOrder === 'acceptance'; } /** 并行组:组内步无依赖、**全完成再继续** */ export function parallelGroupComplete(position: FlowPosition, members: readonly string[]): boolean { return members.every((name) => { const runtime = position.steps.find((s) => s.name === name); return runtime?.state === 'done' || runtime?.state === 'skipped'; }); } /** * 人兜底时要回填的字段 —— 取自这一步**声明的出参**。 * * 出参契约属于**步**,不属于执行者:同一步无论由 LLM、工具还是人做完, * 落进作用域的形状必须一致。若兜底卡只给一个笼统的「结果」文本框, * 人填了也落不进去 —— 下游按 `output.fields` 取值,取不到就在下一步崩。 * * 返回键名数组,与 human 步的 `card.fields` 同形:卡的渲染只有一套。 */ export function fallbackFields(step: FlowStep, scope: Readonly> = {}): readonly string[] { if (step.output === undefined) return []; // 出参的值要问,**行标识**也要问 —— 主键取不到就写不进台账(非空约束会在合并时挡下)。 // 作用域里已经有的键不必再问一遍:上游给过就是给过了。 const keys = Object.values(step.output.key).filter((path) => scope[path] === undefined); return [...new Set([...keys, ...Object.values(step.output.fields)])]; } /** * 末步的放行裁决是否「通过」—— **完成只出自放行通过**(OS 05)。 * 驳回(重做 / 迭代)不是完成:谱虽走完,任务不得进 released。 */ export function lastVerdictPassed(position: FlowPosition): boolean { const last = position.steps[position.steps.length - 1]; return last?.verdict === 'pass'; } /** 末步随放行确认的出口选择 —— 落地收敛时按它发射 */ export function lastSelectedPorts(position: FlowPosition): Record { return position.steps[position.steps.length - 1]?.selectedPorts ?? {}; } /** * 取值路径:点号分段下行,数组下标按数字段走(`records.0.pn`)。 * * **入参与出参共用这一份**:声明里写的路径是同一种写法,不该一边能走点号、 * 另一边只认扁平键 —— 那会让「模型按 schema 回了嵌套结构」这种最常见的情形 * 悄悄写出一条空记录。 */ export function resolvePath(source: unknown, path: string): unknown { /* * 整键优先:作用域里有些键**本身带点号**(台账地址 `域.对象`)。 * 先按字面整键找,找不到再按点号下行 —— 反过来会把地址键拆成两级、永远找不到。 */ if (typeof source === 'object' && source !== null && !Array.isArray(source)) { const direct = (source as Record)[path]; if (direct !== undefined) return direct; } return path.split('.').reduce((acc, seg) => { if (acc === null || acc === undefined || typeof acc !== 'object') return undefined; if (Array.isArray(acc)) { const index = Number(seg); return Number.isInteger(index) ? acc[index] : undefined; } return (acc as Record)[seg]; }, source); }