Archived
用户定: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
118 lines
5.6 KiB
TypeScript
118 lines
5.6 KiB
TypeScript
/**
|
|
* 路线 A 的坑(每个坑一个全新的 run,避免互相污染):
|
|
* 1. dowhile 循环体的嵌套快照存在哪?循环第 2 轮会不会覆盖第 1 轮?
|
|
* 2. forEachIndex 能不能定位 dowhile 的第 N 轮?
|
|
* 3. resume 打错 step(或任务回调重复到达)会怎样?
|
|
*
|
|
* 运行:SPIKE_NO_AUTORUN=1 tsx src/probe-02-route-a-pitfalls.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 } from './trace.js';
|
|
|
|
_internals.setLedger(new Ledger());
|
|
_internals.setScript(new DecisionScript([]));
|
|
resetTrace();
|
|
|
|
const wf = mastraA.getWorkflow('routeA');
|
|
const snapOf = async (workflowName: string, runId: string) =>
|
|
(await (storeA as any).stores.workflows.loadWorkflowSnapshot({ workflowName, runId })) as
|
|
| Record<string, any>
|
|
| null;
|
|
|
|
/** 开一个 run 并推进到「第 2 轮 A4 挂起」 */
|
|
async function toRound2() {
|
|
const run = await wf.createRun();
|
|
await run.start({ inputData: { quoteId: 'Q-pit' } });
|
|
await run.resume({ step: ['round-body', 'A4'], resumeData: { price: 100 } });
|
|
await run.resume({ step: ['round-body', 'A5'], resumeData: { verdict: 'reject' } });
|
|
return run;
|
|
}
|
|
|
|
banner('坑 1:循环体(嵌套 workflow)的快照存在哪?第 2 轮会不会盖掉第 1 轮?');
|
|
{
|
|
const run = await toRound2();
|
|
const outer = await snapOf('route-a', run.runId);
|
|
const inner = await snapOf('round-body', run.runId);
|
|
console.log('外层快照 context 键 =', JSON.stringify(Object.keys(outer?.context ?? {})));
|
|
console.log('外层 context["round-body"].metadata =', JSON.stringify(outer?.context?.['round-body']?.metadata));
|
|
console.log('\n内层(workflowName="round-body", 同一个 runId)快照存在吗?', !!inner);
|
|
console.log('内层 context 键 =', JSON.stringify(Object.keys(inner?.context ?? {})));
|
|
console.log('内层 A4.suspendPayload =', JSON.stringify(inner?.context?.['A4']?.suspendPayload));
|
|
console.log('内层 A4.output =', JSON.stringify(inner?.context?.['A4']?.output));
|
|
console.log('内层 A5.output =', JSON.stringify(inner?.context?.['A5']?.output));
|
|
console.log(
|
|
'\n判定:第 1 轮的 A4(taskId=T-A4-001 / price=100)还在内层快照里吗?→',
|
|
JSON.stringify(inner?.context?.['A4']).includes('T-A4-001') ? '在' : '不在(已被第 2 轮覆盖)',
|
|
);
|
|
const runs = await wf.listWorkflowRuns();
|
|
console.log('listWorkflowRuns() 里能看到几条?', runs.total, JSON.stringify(runs.runs.map((r: any) => `${r.workflowName}/${r.runId.slice(0, 8)}`)));
|
|
}
|
|
|
|
banner('坑 2:forEachIndex 能不能定位 dowhile 的第 N 轮?(此刻第 2 轮 A4 正挂起)');
|
|
{
|
|
const run = await toRound2();
|
|
for (const idx of [0, 1]) {
|
|
const fresh = await wf.createRun({ runId: `${run.runId}-x${idx}` });
|
|
void fresh; // 只是为了不复用同一个 run
|
|
}
|
|
try {
|
|
const r = await run.resume({
|
|
step: ['round-body', 'A4'],
|
|
resumeData: { price: 999 },
|
|
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 inner = await snapOf('round-body', run.runId);
|
|
console.log(' 内层 A4.output.price =', JSON.stringify(inner?.context?.['A4']?.output?.price));
|
|
console.log(' → forEachIndex 被忽略了(打进的是「当前正挂起的那一轮」)');
|
|
} catch (e) {
|
|
console.log('抛异常:', (e as Error).message);
|
|
}
|
|
}
|
|
|
|
banner('坑 3:resume 打错 step(= 旧任务的回调迟到)会怎样?');
|
|
{
|
|
const run = await toRound2();
|
|
const st = await wf.getWorkflowRunById(run.runId);
|
|
console.log('resume 之前 status =', st?.status, '挂起的是',
|
|
JSON.stringify(createWorkflowStateReader(st!).getSuspendedStep()?.path));
|
|
let r;
|
|
try {
|
|
r = await run.resume({ step: ['round-body', 'A5'], resumeData: { verdict: 'pass' } });
|
|
console.log('resume(错的 step=A5) → status =', r.status);
|
|
console.log(' error =', JSON.stringify((r as any).error ?? null));
|
|
} catch (e) {
|
|
console.log('resume(错的 step=A5) 抛异常:', (e as Error).message);
|
|
}
|
|
const after = await wf.getWorkflowRunById(run.runId);
|
|
console.log(' 之后整个 run 的 status =', after?.status, ' ← 注意:run 被打成 failed 了');
|
|
try {
|
|
const r2 = await run.resume({ step: ['round-body', 'A4'], resumeData: { price: 1 } });
|
|
console.log(' 再补一次正确的 resume → status =', r2.status);
|
|
} catch (e) {
|
|
console.log(' 再补一次正确的 resume 抛异常:', (e as Error).message, ' ← 这一单救不回来了');
|
|
}
|
|
}
|
|
|
|
banner('坑 4:同一个任务回调重复到达(重复 resume 同一个 step)');
|
|
{
|
|
const run = await toRound2();
|
|
const r1 = await run.resume({ step: ['round-body', 'A4'], resumeData: { price: 50 } });
|
|
console.log('第一次 resume A4 → status =', r1.status, '(此时应轮到 A5 挂起)');
|
|
try {
|
|
const r2 = await run.resume({ step: ['round-body', 'A4'], resumeData: { price: 50 } });
|
|
console.log('第二次 resume A4 → status =', r2.status, 'error =', JSON.stringify((r2 as any).error ?? null));
|
|
} catch (e) {
|
|
console.log('第二次 resume A4 抛异常:', (e as Error).message);
|
|
}
|
|
const after = await wf.getWorkflowRunById(run.runId);
|
|
console.log('之后 run 的 status =', after?.status);
|
|
}
|
|
|
|
banner('坑 5:dowhile 的循环体输入/输出必须同构吗?');
|
|
console.log('见 route-a-dowhile.ts:循环体 round-body 的 inputSchema 与 outputSchema 都是 RoundState。');
|
|
console.log('把 outputSchema 改成别的形状时,TS 报错(dowhile 的 TStepInputSchema extends TPrevSchema 约束)。');
|