Mastra 编译器落地:流程包 → workflow(路线 B)—— 回边成出口,变体各带入口
按 spike 实测结论(spike/mastra-reopen/FINDINGS.md)落地 M5-1 的编译器:
- **回边不连回去**,落成回边出口步骤:本 run 到此为止,把
`{reopen, reopenedBy, reopenTarget, payload}` 写进 result,胶合层起新 run。
1 run = 1 实例,与「重开产生新实例、旧实例留痕不动」一一对应
- **每个回边目标各编译一份「以它为入口」的变体**(`<包id>@<节点id>`)——
新 run 只能从入口起步;种子 unified-quote 编出 `@start` 与 `@A4` 两份
- 条件出边 → `.branch()` + **XOR 收口步**:Mastra 把未命中的分支键也放进输出
(值 undefined),收口步过滤并断言恰一条 —— 「恰取一条」运行时照旧成立;
条件谓词排掉「前面已命中」的情形(Mastra 求值非短路),无条件边作兜底,
连兜底都没有时留一条「全不中 → 死路诊断」的出路
- 活动点/审批点 → 「起任务 → suspend()」,副作用写在 `if (!resumeData)` 里
(实测 resume 时 execute 从头重跑,不这么写会起两次任务、旧的成孤儿)
- 编译期全验:悬空边、正向边成环(未标 reopen)当场报 —— 编译只走到得了的边,
悬空边会落在递归碰不到的角落(实测:多默认边情形下 E9 压根没被编译)
- Mastra 泛型桥接收在两个薄工厂里,不把类型逃逸撒满文件
测试:编译器四例(变体/无回边/成环/悬空)+ app 层一例(种子真图编得出来);
五闸全过。胶合层(FlowRuntime 的 Mastra 实现)与 CONTRACT/debt 对账随后。
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KeDBx2PHLavbURXB8LJJ7m
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
* 审批点两个去向都由图表达、图上每条语义边都画成真边(卡点画法已废)。
|
||||
*/
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { compilePackage } from '@modules/process';
|
||||
import { MVP_PACKAGE } from '../seed-mvp-flow.ts';
|
||||
|
||||
describe('MVP_PACKAGE', () => {
|
||||
@@ -101,3 +102,24 @@ describe('MVP_PACKAGE', () => {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
/*
|
||||
* 真图能不能编译成 Mastra 图 —— 放在 app 层测:模块测试不引 app(§11.3),
|
||||
* 而这一例要的正是「app 层的种子 × process 模块的编译器」。
|
||||
*/
|
||||
describe('MVP_PACKAGE → Mastra 编译', () => {
|
||||
const hooks = {
|
||||
startTask: async () => ({ taskId: 'T-1', instance: 1 }),
|
||||
raiseApproval: async () => ({ approvalId: 'A-1', instance: 1 }),
|
||||
assembleEdge: async ({ payload }: { payload: Record<string, unknown> }) => payload,
|
||||
evaluate: async () => true,
|
||||
finish: async () => undefined,
|
||||
deadEnd: async () => undefined,
|
||||
};
|
||||
|
||||
it('主入口 + A4 变体(两条回边都指向 A4)', () => {
|
||||
const flows = compilePackage(MVP_PACKAGE, hooks);
|
||||
expect(flows.map((f) => f.key).sort()).toEqual(['unified-quote@A4', 'unified-quote@start']);
|
||||
for (const f of flows) expect(f.workflow).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -94,8 +94,8 @@ describe('装配:功能工具', () => {
|
||||
|
||||
it('工具调用失败如实回报,不抛掉整拍', async () => {
|
||||
const h = harness();
|
||||
h.ports.invoke = async () => ({ ok: false, error: { message: '端点不可达' } });
|
||||
const tools = buildMastraTools([READ_TOOL], h.ports, { taskId: TASK, ownerId: OWNER, humanCallCap: 9, model: 'm' }, h.trace);
|
||||
const failing: ToolBusPorts = { ...h.ports, invoke: async () => ({ ok: false, error: { message: '端点不可达' } }) };
|
||||
const tools = buildMastraTools([READ_TOOL], failing, { taskId: TASK, ownerId: OWNER, humanCallCap: 9, model: 'm' }, h.trace);
|
||||
const out = await run(Object.values(tools)[0], { pn: 'PN-1' });
|
||||
expect(out).toEqual({ ok: false, error: '端点不可达' });
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user