Archived
工具总线 + 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
This commit is contained in:
@@ -9,6 +9,7 @@ import type { EventService } from '@modules/event';
|
||||
import type { LedgerService } from '@modules/ledger';
|
||||
import type { MemoryService } from '@modules/memory';
|
||||
import type { ToolboxService, ToolInvoker } from '@modules/toolbox';
|
||||
import { compileToolBus } from '@modules/toolbox';
|
||||
import type { HumanToolService } from '@modules/human-tool';
|
||||
import { renderView } from '@modules/interaction';
|
||||
import type { InteractionService } from '@modules/interaction';
|
||||
@@ -33,6 +34,7 @@ import { evaluate as evalSfeel, evaluateCondition, evaluateUnaryTest } from '@sf
|
||||
import type { StaffId, TaskId } from '@contracts';
|
||||
import { staffIdSchema } from '@contracts';
|
||||
import { createAgentRunner } from '../agent-runner.ts';
|
||||
import { createMastraAgentRunner } from '../mastra-agent-runner.ts';
|
||||
import type { Clock, IdGenerator } from '../runtime.ts';
|
||||
|
||||
export type EngineWiringDeps = {
|
||||
@@ -398,21 +400,93 @@ export function wireEngines(deps: EngineWiringDeps): EngineWiring {
|
||||
clock,
|
||||
});
|
||||
|
||||
/* ── Agent 回路:单拍 or 开环(工具总线)────────────────────────── */
|
||||
|
||||
/** 活动声明的自定义模型名 —— 活动只见用途名,不见厂商型号 */
|
||||
const modelOf = async (taskId: TaskId) => {
|
||||
const { activityId } = await taskContext.of(taskId);
|
||||
const manifest = await deps.registry.manifestOf(activityId);
|
||||
return manifest.declarations.models[0] ?? 'default';
|
||||
};
|
||||
const onAgentUsage = (taskId: TaskId, tokens: number) =>
|
||||
taskStore.addConsumption(taskId, { tokens }).then(() => undefined);
|
||||
|
||||
/**
|
||||
* 工具总线:**声明 × 目录**,编译期定死这一单能调什么(声明面在 toolbox/domain)。
|
||||
* 功能工具按活动白名单查目录;人类工具恒在(Harness 给的能力);
|
||||
* LLM 五件仅当活动声明了模型用途名才给 —— 没声明模型却会问模型,是越权也无从归因。
|
||||
*/
|
||||
const busOf = async (taskId: TaskId) => {
|
||||
const { activityId } = await taskContext.of(taskId);
|
||||
const manifest = await deps.registry.manifestOf(activityId);
|
||||
const catalog = new Map(
|
||||
(await deps.toolbox.list()).map((c) => [
|
||||
c.name,
|
||||
{
|
||||
name: c.name,
|
||||
description: c.description,
|
||||
input: c.input,
|
||||
...(c.output === undefined ? {} : { output: c.output }),
|
||||
effect: c.effect === 'read' ? ('read' as const) : ('write' as const),
|
||||
},
|
||||
]),
|
||||
);
|
||||
const report = compileToolBus({
|
||||
declaredTools: manifest.declarations.tools.map((t) => t.name),
|
||||
catalog,
|
||||
declaredModels: manifest.declarations.models,
|
||||
});
|
||||
// 声明了却查无此工具 / 已停用:如实记痕,不静默 —— Agent 手上少了件东西,得看得见
|
||||
if (report.missing.length > 0 || report.disabled.length > 0) {
|
||||
await memoryRecorder.append({
|
||||
taskId,
|
||||
kind: 'observation',
|
||||
title: `工具总线缺件:未装 ${report.missing.join('、') || '无'};已停用 ${report.disabled.join('、') || '无'}`,
|
||||
} as never);
|
||||
}
|
||||
return report.tools;
|
||||
};
|
||||
|
||||
/*
|
||||
* 开环 or 单拍:`JCAI_AGENT_OPEN_LOOP=1` 时用 Mastra Agent 放开拍内工具循环。
|
||||
* 缺省仍是单拍版 —— 开环把「一拍调几次模型」交给了模型自己,
|
||||
* 得有第一个真 loop 活动压着测过才好当缺省(见 debt M5-2)。
|
||||
*/
|
||||
const agentRunner =
|
||||
globalThis.process.env['JCAI_AGENT_OPEN_LOOP'] === '1'
|
||||
? createMastraAgentRunner({
|
||||
gateway: deps.modelGateway,
|
||||
modelOf,
|
||||
busOf,
|
||||
onUsage: onAgentUsage,
|
||||
contextOf: async (taskId) => {
|
||||
const ctx = await taskContext.of(taskId);
|
||||
return { taskId, ownerId: ctx.ownerId, humanCallCap: ctx.humanCallCap };
|
||||
},
|
||||
ports: {
|
||||
invoke: async (input) => {
|
||||
const { activityId } = await taskContext.of(input.taskId);
|
||||
return toolGateway.invoke({ ...input, activityId });
|
||||
},
|
||||
isWriteEffect: (tool) => toolGateway.isWriteEffect(tool),
|
||||
registerIntent: (input) => toolGateway.registerIntent(input),
|
||||
issueCard: (input) => cards.issue(input),
|
||||
listen: (input) => listeners.register(input),
|
||||
generate: async (input) => {
|
||||
const result = await deps.modelGateway.generate(input);
|
||||
await onAgentUsage(input.taskId, result.usage.inputTokens + result.usage.outputTokens);
|
||||
return { text: result.text, ...(result.object === undefined ? {} : { object: result.object }) };
|
||||
},
|
||||
},
|
||||
})
|
||||
: createAgentRunner({ gateway: deps.modelGateway, onUsage: onAgentUsage, modelOf });
|
||||
|
||||
/* ── loop-engine ──────────────────────────────────────────────── */
|
||||
const loop = createTurnRunner({
|
||||
state: createLoopStateStore(db),
|
||||
sfeel,
|
||||
data: taskData,
|
||||
agent: createAgentRunner({
|
||||
gateway: deps.modelGateway,
|
||||
// loop 的每一回合都要问模型 —— 不计 token,预算熔断就永远不会响
|
||||
onUsage: (taskId, tokens) => taskStore.addConsumption(taskId, { tokens }).then(() => undefined),
|
||||
modelOf: async (taskId) => {
|
||||
const { activityId } = await taskContext.of(taskId);
|
||||
const manifest = await deps.registry.manifestOf(activityId);
|
||||
return manifest.declarations.models[0] ?? 'default';
|
||||
},
|
||||
}),
|
||||
agent: agentRunner,
|
||||
tools: toolGateway,
|
||||
cards,
|
||||
listeners,
|
||||
|
||||
Reference in New Issue
Block a user