工具总线装配面测试:三路各穿各的缝
守开环 Agent 最容易出事的三条: - 写类工具**过程中不落地**(只登记意向,并如实告诉 Agent 世界还没变)—— 骗它说已执行,它会据此往下推而世界一步没动 - 调人 = 发工单 + 立监听,卡号记进 trace(引擎据此挂起,不重复发第二张) - LLM 五件经模型网关、用活动声明的用途名;提示词归系统 六例全绿;顺带钉住 Mastra 的 tool id 规矩(点号 → 双下划线)。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KeDBx2PHLavbURXB8LJJ7m
This commit is contained in:
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* 反例:dowhile 的循环体「输出形状 ≠ 输入形状」时会怎样(只做类型检查,不运行)。
|
||||
* 用 tsc 单独检查这个文件:npx tsc --noEmit --strict --module esnext --moduleResolution bundler \
|
||||
* --target es2022 --skipLibCheck src/negative/dowhile-shape-mismatch.ts
|
||||
*/
|
||||
import { createStep, createWorkflow } from '@mastra/core/workflows';
|
||||
import { z } from 'zod';
|
||||
|
||||
const In = z.object({ round: z.number(), reopen: z.boolean() });
|
||||
const Out = z.object({ somethingElse: z.string() });
|
||||
|
||||
const body = createStep({
|
||||
id: 'body',
|
||||
inputSchema: In,
|
||||
outputSchema: Out,
|
||||
execute: async () => ({ somethingElse: 'x' }),
|
||||
});
|
||||
|
||||
const start = createStep({
|
||||
id: 'start',
|
||||
inputSchema: z.object({}),
|
||||
outputSchema: In,
|
||||
execute: async () => ({ round: 1, reopen: false }),
|
||||
});
|
||||
|
||||
export const wf = createWorkflow({ id: 'neg', inputSchema: z.object({}), outputSchema: Out })
|
||||
.then(start)
|
||||
// 循环体输出 Out,下一轮却要拿它当 In 用
|
||||
.dowhile(body, async ({ inputData }) => inputData.reopen === true)
|
||||
.commit();
|
||||
@@ -0,0 +1,75 @@
|
||||
/**
|
||||
* 关键差异确认:误 resume(迟到的/重复的任务回调)会不会把整条 run 打死?
|
||||
* - 顶层 step 打错 → ?
|
||||
* - 嵌套 step(nested workflow 里的 step)打错 → ?
|
||||
* 路线 A 里所有节点都在嵌套的循环体里,所以这条差异对选型很关键。
|
||||
*
|
||||
* 运行:SPIKE_NO_AUTORUN=1 tsx src/probe-05-misresume-blast-radius.ts
|
||||
*/
|
||||
import { DecisionScript, Ledger, banner } from './ledger.js';
|
||||
import { mastraB, _internals as bInternals } from './route-b-external.js';
|
||||
import { mastraA, _internals as aInternals } from './route-a-dowhile.js';
|
||||
import { resetTrace } from './trace.js';
|
||||
|
||||
aInternals.setLedger(new Ledger());
|
||||
aInternals.setScript(new DecisionScript([]));
|
||||
bInternals.setLedger(new Ledger());
|
||||
bInternals.setScript(new DecisionScript([]));
|
||||
resetTrace();
|
||||
|
||||
const wfB = mastraB.getWorkflow('routeB');
|
||||
const wfA = mastraA.getWorkflow('routeA');
|
||||
|
||||
banner('B-1:顶层 step 打错(A4 挂起时去 resume A5)');
|
||||
{
|
||||
const run = await wfB.createRun({ runId: 'blast-top' });
|
||||
await run.start({ inputData: { quoteId: 'QB', instanceSeq: 1, reopenedBy: null, prevRunId: null } });
|
||||
try {
|
||||
const r = await run.resume({ step: ['A5'], resumeData: { verdict: 'pass' } });
|
||||
console.log('返回 status =', r.status, 'error =', JSON.stringify((r as any).error ?? null));
|
||||
} catch (e) {
|
||||
console.log('抛异常 =', (e as Error).message);
|
||||
}
|
||||
console.log('run 之后的 status =', (await wfB.getWorkflowRunById('blast-top'))?.status);
|
||||
const ok = await run.resume({ step: ['A4'], resumeData: { price: 1 } });
|
||||
console.log('再补正确的 resume A4 → status =', ok.status, ' ← run 没被打坏,还能继续');
|
||||
}
|
||||
|
||||
banner('B-2:嵌套 step 打错(A4 挂起时去 resume A6-sub.A6)');
|
||||
{
|
||||
const run = await wfB.createRun({ runId: 'blast-nested' });
|
||||
await run.start({ inputData: { quoteId: 'QB2', instanceSeq: 1, reopenedBy: null, prevRunId: null } });
|
||||
try {
|
||||
const r = await run.resume({ step: ['A6-sub', 'A6'], resumeData: { outcome: 'won' } });
|
||||
console.log('返回 status =', r.status, 'error =', JSON.stringify((r as any).error ?? null));
|
||||
} catch (e) {
|
||||
console.log('抛异常 =', (e as Error).message);
|
||||
}
|
||||
const st = await wfB.getWorkflowRunById('blast-nested');
|
||||
console.log('run 之后的 status =', st?.status);
|
||||
try {
|
||||
const ok = await run.resume({ step: ['A4'], resumeData: { price: 1 } });
|
||||
console.log('再补正确的 resume A4 → status =', ok.status);
|
||||
} catch (e) {
|
||||
console.log('再补正确的 resume A4 抛异常 =', (e as Error).message, ' ← 这一单救不回来了');
|
||||
}
|
||||
}
|
||||
|
||||
banner('A-1:路线 A 里所有节点都在嵌套循环体里,打错一次会怎样');
|
||||
{
|
||||
const run = await wfA.createRun({ runId: 'blast-loop' });
|
||||
await run.start({ inputData: { quoteId: 'QA' } });
|
||||
try {
|
||||
const r = await run.resume({ step: ['round-body', 'A5'], resumeData: { verdict: 'pass' } });
|
||||
console.log('返回 status =', r.status, 'error =', JSON.stringify((r as any).error ?? null));
|
||||
} catch (e) {
|
||||
console.log('抛异常 =', (e as Error).message);
|
||||
}
|
||||
console.log('run 之后的 status =', (await wfA.getWorkflowRunById('blast-loop'))?.status);
|
||||
try {
|
||||
const ok = await run.resume({ step: ['round-body', 'A4'], resumeData: { price: 1 } });
|
||||
console.log('再补正确的 resume A4 → status =', ok.status);
|
||||
} catch (e) {
|
||||
console.log('再补正确的 resume A4 抛异常 =', (e as Error).message, ' ← 这一单救不回来了');
|
||||
}
|
||||
}
|
||||
@@ -262,11 +262,13 @@ export const routeA = createWorkflow({
|
||||
outputSchema: z.object({ rounds: z.number(), finalOutcome: z.string().nullable() }),
|
||||
})
|
||||
.then(start)
|
||||
.dowhile(roundBody, async ({ inputData, iterationCount }) => {
|
||||
.dowhile(roundBody, async ({ inputData, iterationCount, requestContext, getInitData }) => {
|
||||
t('dowhile.condition', 'evaluated', {
|
||||
iterationCount,
|
||||
reopen: inputData.reopen,
|
||||
round: inputData.round,
|
||||
'requestContext.tenant': requestContext?.get?.('tenant') ?? null,
|
||||
'getInitData()': getInitData(),
|
||||
'继续循环?': inputData.reopen === true,
|
||||
});
|
||||
return inputData.reopen === true;
|
||||
|
||||
Reference in New Issue
Block a user