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
111 lines
3.2 KiB
TypeScript
111 lines
3.2 KiB
TypeScript
/**
|
|
* 模拟主工程的“实例/任务”台账。
|
|
*
|
|
* 主工程语义:回边 reopen = 回到 A4 **重开一个新实例**(新 instance 号、新任务 ID,
|
|
* 旧实例留痕不动)。这里用一个进程内表来记账,用来验证「哪条路线能自然地拿到新实例号」。
|
|
*/
|
|
|
|
export type NodeId = 'A4' | 'A5' | 'A6';
|
|
|
|
export type InstanceRow = {
|
|
/** 实例号:同一节点上单调递增,A4#1 / A4#2 ... */
|
|
instanceNo: number;
|
|
nodeId: NodeId;
|
|
/** 任务 ID:每个实例一个,全局唯一 */
|
|
taskId: string;
|
|
/** 哪个 run 起的它(路线 B 会出现多个 runId) */
|
|
runId: string;
|
|
/** 哪一轮循环起的它(路线 A 才有意义) */
|
|
iteration: number;
|
|
/** 由哪条边重开(E10 / E12);首开为 null */
|
|
reopenedBy: string | null;
|
|
status: 'open' | 'closed';
|
|
outcome?: string;
|
|
openedAt: number;
|
|
closedAt?: number;
|
|
};
|
|
|
|
export class Ledger {
|
|
readonly rows: InstanceRow[] = [];
|
|
private seq = 0;
|
|
|
|
openInstance(args: {
|
|
nodeId: NodeId;
|
|
runId: string;
|
|
iteration: number;
|
|
reopenedBy?: string | null;
|
|
}): InstanceRow {
|
|
const instanceNo = this.rows.filter((r) => r.nodeId === args.nodeId).length + 1;
|
|
const row: InstanceRow = {
|
|
instanceNo,
|
|
nodeId: args.nodeId,
|
|
taskId: `T-${args.nodeId}-${String(++this.seq).padStart(3, '0')}`,
|
|
runId: args.runId,
|
|
iteration: args.iteration,
|
|
reopenedBy: args.reopenedBy ?? null,
|
|
status: 'open',
|
|
openedAt: Date.now(),
|
|
};
|
|
this.rows.push(row);
|
|
return row;
|
|
}
|
|
|
|
closeTask(taskId: string, outcome: string): InstanceRow {
|
|
const row = this.rows.find((r) => r.taskId === taskId);
|
|
if (!row) throw new Error(`ledger: unknown taskId ${taskId}`);
|
|
row.status = 'closed';
|
|
row.outcome = outcome;
|
|
row.closedAt = Date.now();
|
|
return row;
|
|
}
|
|
|
|
dump(title: string): void {
|
|
console.log(`\n ── 台账 ${title} ──`);
|
|
console.log(
|
|
' ' +
|
|
['节点', '实例', '任务ID', '轮次', '重开自', '状态', '结论', 'runId'].join('\t'),
|
|
);
|
|
for (const r of this.rows) {
|
|
console.log(
|
|
' ' +
|
|
[
|
|
r.nodeId,
|
|
`#${r.instanceNo}`,
|
|
r.taskId,
|
|
`it=${r.iteration}`,
|
|
r.reopenedBy ?? '-',
|
|
r.status,
|
|
r.outcome ?? '-',
|
|
r.runId.slice(0, 8),
|
|
].join('\t'),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
/** 外部注入的“决策脚本”:模拟人在另一个调用链里点了按钮 */
|
|
export type Decision =
|
|
| { node: 'A4'; price: number }
|
|
| { node: 'A5'; verdict: 'pass' | 'reject' }
|
|
| { node: 'A6'; outcome: 'won' | 'revise' };
|
|
|
|
export class DecisionScript {
|
|
private i = 0;
|
|
constructor(private readonly script: Decision[]) {}
|
|
next(expectNode: NodeId): Decision {
|
|
const d = this.script[this.i++];
|
|
if (!d) throw new Error(`decision script exhausted at ${expectNode}`);
|
|
if (d.node !== expectNode) {
|
|
throw new Error(`decision script mismatch: expect ${expectNode}, got ${d.node}`);
|
|
}
|
|
return d;
|
|
}
|
|
get consumed(): number {
|
|
return this.i;
|
|
}
|
|
}
|
|
|
|
export const log = (...args: unknown[]): void => console.log(...args);
|
|
export const banner = (s: string): void =>
|
|
console.log(`\n${'='.repeat(78)}\n${s}\n${'='.repeat(78)}`);
|