This repository has been archived on 2026-09-13. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
BPOS-MVP/spike/mastra-reopen/src/probe-01-route-a-snapshot.ts
T
ChaiHaoandClaude Fable 5 f96eff906a 工具总线 + Agent 开环:声明式工具、人类工具与 LLM 五件系统内化(Mastra tool 形状)
用户定:Agent 调用工具一律**声明式**;人类工具与 LLM 工具要系统内化;
工具总线基于 Mastra tool 设计。

**声明面**(`toolbox/internal/domain/tool-bus.ts`,纯逻辑零框架):
`compileToolBus(声明 × 目录) → 一张清单` —— 编译期定死这一单能调什么,
清单之外结构上调不到(白名单在缝里再把一次关);缺件/停用如实报不静默。
三路同一形状:
  · `function` 功能工具:按活动白名单查目录,带出效应(read/write)
  · `human` **人类工具**(系统内化、零声明即得):调人 = 发工单 + 立监听听人 ——
    这是 Harness 给的能力,不进目录、不需白名单,每个活动天然可以调人
  · `llm` **LLM 五件**(系统内化):extract/classify/summarize/judge/rewrite,
    各有入参出参契约、经模型网关一缝;**仅当活动声明了模型用途名才上总线**
    (没声明却会问模型 = 越权,用量也无从归因);提示词归系统,活动改不了

**装配面**(`app/tool-bus.ts`,Mastra `createTool`):清单 → 可调用工具,
JSON Schema 现译 zod(模型照它填参,填错当场被拦);三路各穿各的缝:
写类工具**过程中一律不落地**,只登记动作意向(骗 Agent 说已执行是最坏选项);
调人把卡号记进 trace,引擎据此把这一拍挂起。

**Agent 开环**(`app/mastra-agent-runner.ts`,M5-2 第二实现,用户选的路线):
Mastra `Agent` + 工具总线,拍内自主连环调工具,拍末交回结构化结论。
放开的是工具循环不是规矩:写类不落地、终局唯放行、调人即挂起(新增
`AgentAction.asked` 型 —— 卡已由总线发出,引擎只收尾,不重复发卡)、
用量按 usage 计入预算。`JCAI_AGENT_OPEN_LOOP=1` 开启,缺省仍是单拍版。
`ModelGateway.resolve` 新增:名 → 四元(密钥当场解出,只给 app 层装配用)。

代价如实记 debt M5-2:开环下模型**逐轮**调用不经录制回放(tool-calling 往返
`generate` 的 text-in/text-out 承载不了)—— 凭证隔离与工具调用一缝完整。

测试:tool-bus 六例(白名单/缺件/停用/人类工具恒在/LLM 按声明/五件契约);
四闸(typecheck/lint/depcruise/test)全过;spike 目录排除出 lint(验证性原型)。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KeDBx2PHLavbURXB8LJJ7m
2026-07-28 23:46:12 +08:00

132 lines
6.2 KiB
TypeScript

/**
* 路线 A 深挖:
* a) 快照(WorkflowRunState)里循环轮次到底怎么表示?
* b) resume 能不能定位「第几轮的那个 step」?(label / forEachIndex / path)
* c) 重复 resume、跨轮 resume、run 结束后 resume 会怎样?
*
* 运行:SPIKE_NO_AUTORUN=1 tsx src/probe-01-route-a-snapshot.ts
*/
import { createWorkflowStateReader } from '@mastra/core/workflows';
import { DecisionScript, Ledger, banner } from './ledger.js';
import { mastraA, storeA, _internals } from './route-a-dowhile.js';
import { resetTrace, t } from './trace.js';
_internals.setLedger(new Ledger());
_internals.setScript(new DecisionScript([]));
resetTrace();
const wf = mastraA.getWorkflow('routeA');
const rawSnap = async (runId: string) =>
(await (storeA as any).stores.workflows.loadWorkflowSnapshot({
workflowName: 'route-a',
runId,
})) as Record<string, any> | null;
banner('探针 A-1:跑到「第 2 轮 A4 挂起」,看快照');
const run = await wf.createRun();
console.log('runId =', run.runId);
let res = await run.start({ inputData: { quoteId: 'Q-probe' } });
console.log('after start: status =', res.status);
// 第 1 轮 A4
res = await run.resume({ step: ['round-body', 'A4'], resumeData: { price: 100 } });
// 第 1 轮 A5 → reject → 触发回边 E10 → 第 2 轮开始,A4 再次挂起
res = await run.resume({ step: ['round-body', 'A5'], resumeData: { verdict: 'reject' } });
console.log('after A5(reject): status =', res.status, 'suspended =', JSON.stringify((res as any).suspended));
const s2 = await rawSnap(run.runId);
console.log('\n── 原始快照 WorkflowRunState 的顶层键 ──');
console.log(Object.keys(s2 ?? {}));
console.log('\nvalue =', JSON.stringify(s2?.value));
console.log('activePaths =', JSON.stringify(s2?.activePaths));
console.log('activeStepsPath=', JSON.stringify(s2?.activeStepsPath));
console.log('suspendedPaths =', JSON.stringify(s2?.suspendedPaths));
console.log('resumeLabels =', JSON.stringify(s2?.resumeLabels));
console.log('stepExecutionPath =', JSON.stringify(s2?.stepExecutionPath));
console.log('\ncontext 的键(= 快照里保存了哪些 step 的结果):');
console.log(JSON.stringify(Object.keys(s2?.context ?? {}), null, 0));
console.log('\ncontext["round-body.A4"] =');
console.log(JSON.stringify(s2?.context?.['round-body.A4'], null, 2));
console.log('\ncontext["round-body.A5"] =');
console.log(JSON.stringify(s2?.context?.['round-body.A5'], null, 2));
console.log('\ncontext["round-body"] (循环体本身) =');
console.log(JSON.stringify(s2?.context?.['round-body'], null, 2));
banner('探针 A-2:resume 能不能指名「第 1 轮的 A4」?');
// 现在挂起的是「第 2 轮的 A4」。第 1 轮 A4 的 resumeLabel 是 A4-round-1。
const reader = createWorkflowStateReader((await wf.getWorkflowRunById(run.runId))!);
console.log('当前 resumeLabels =', JSON.stringify(reader.getResumeLabels()));
console.log('getResumeLabel("A4-round-1") =', JSON.stringify(reader.getResumeLabel('A4-round-1')));
console.log('getResumeLabel("A4-round-2") =', JSON.stringify(reader.getResumeLabel('A4-round-2')));
try {
const r = await run.resume({ label: 'A4-round-1', resumeData: { price: 111 } });
console.log('resume(label=A4-round-1) → status =', r.status);
console.log(' 报错?', JSON.stringify((r as any).error ?? null));
} catch (e) {
console.log('resume(label=A4-round-1) 抛异常:', (e as Error).message);
}
banner('探针 A-3:forEachIndex 对 dowhile 有用吗?');
try {
const r = await run.resume({
step: ['round-body', 'A4'],
resumeData: { price: 222 },
forEachIndex: 0,
});
console.log('resume(step=round-body.A4, forEachIndex=0) → status =', r.status);
console.log(' error =', JSON.stringify((r as any).error ?? null));
const snap = await rawSnap(run.runId);
console.log(
' 快照里 A4 的 output.price =',
JSON.stringify(snap?.context?.['round-body.A4']?.output?.price),
);
} catch (e) {
console.log('resume(forEachIndex=0) 抛异常:', (e as Error).message);
}
banner('探针 A-4:对「已经推进过的 step」重复 resume');
try {
const r = await run.resume({ step: ['round-body', 'A4'], resumeData: { price: 333 } });
console.log('重复 resume A4 → status =', r.status, 'error =', JSON.stringify((r as any).error ?? null));
} catch (e) {
console.log('重复 resume A4 抛异常:', (e as Error).message);
}
banner('探针 A-5:resume 一个「没有挂起」的 step');
try {
const r = await run.resume({ step: ['round-body', 'A6-sub', 'A6'], resumeData: { outcome: 'won' } });
console.log('resume 未挂起的 A6 → status =', r.status, 'error =', JSON.stringify((r as any).error ?? null));
} catch (e) {
console.log('resume 未挂起的 A6 抛异常:', (e as Error).message);
}
banner('探针 A-6:把这一单跑完,再看终态快照能否回溯轮次');
let guard = 0;
let cur = await wf.getWorkflowRunById(run.runId);
while (cur?.status === 'suspended' && guard++ < 8) {
const rd = createWorkflowStateReader(cur);
const sus = rd.getSuspendedStep()!;
const nodeId = (sus.suspendPayload as { nodeId: string }).nodeId;
const data =
nodeId === 'A4' ? { price: 77 } : nodeId === 'A5' ? { verdict: 'pass' } : { outcome: 'won' };
t('[probe]', `resume ${nodeId} @ ${JSON.stringify(sus.path)}`, data);
await run.resume({ step: sus.path, resumeData: data });
cur = await wf.getWorkflowRunById(run.runId);
}
console.log('最终 status =', cur?.status, 'result =', JSON.stringify(cur?.result));
const fin = await rawSnap(run.runId);
console.log('\n终态快照 context 的键:', JSON.stringify(Object.keys(fin?.context ?? {})));
console.log('终态 context["round-body.A4"].suspendPayload =', JSON.stringify(fin?.context?.['round-body.A4']?.suspendPayload));
console.log('终态 context["round-body.A4"].output =', JSON.stringify(fin?.context?.['round-body.A4']?.output));
console.log('终态 context["round-body.A4"] 是数组吗? ', Array.isArray(fin?.context?.['round-body.A4']));
console.log('终态 stepExecutionPath =', JSON.stringify(fin?.stepExecutionPath));
console.log('\n第 1 轮的 A4(taskId=T-A4-001, price=100)还在快照里吗?→',
JSON.stringify(fin?.context?.['round-body.A4']).includes('T-A4-001') ? '在' : '不在(被覆盖)');
console.log('\n台账(我们自己记的)里第 1 轮还在吗?');
_internals.getLedger().dump('probe');