Files
JX-BPOS/apps/api/src/modules/loop-engine/contract.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

113 lines
5.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* module: loop-engine —— 对外类型(编码规范 §10.1;语义出处 AM 03 / 04)。
*
* loop 的运行 = **CMMN 语义引擎(下层,零 LLM)× Agent 回路(上层,临场裁量)**。
* 两层的耦合点是任务数据(驱动耦合)与任务记忆(记录同轴)。
*/
import type { LoopModel, TaskId } from '@contracts';
import type { CaseModel, PlanItem, PlanItemType } from './internal/domain/cmmn-parse.ts';
import type {
CaseRuntime,
CaseState,
EnabledEntry,
EnabledSet,
ItemRuntime,
ItemState,
Navigation,
TerminalAuthority,
} from './internal/domain/lifecycle.ts';
import type { DecisionResult, DecisionTrace, Gap } from './internal/domain/dmn.ts';
export type {
CaseModel,
CaseRuntime,
CaseState,
DecisionResult,
DecisionTrace,
EnabledEntry,
EnabledSet,
Gap,
ItemRuntime,
ItemState,
LoopModel,
Navigation,
PlanItem,
PlanItemType,
TerminalAuthority,
};
export { CmmnParseError } from './internal/domain/cmmn-parse.ts';
export { IllegalTransitionError } from './internal/domain/lifecycle.ts';
export class ForbiddenTerminalError extends Error {
constructor(readonly by: TerminalAuthority) {
super(`终局迁移不得由 ${by} 产生:Completed 必经放行,Terminated 只出自 Harness 与负责人(AM 4.1)`);
this.name = 'ForbiddenTerminalError';
}
}
export class AutomaticOnWorldTouchingError extends Error {
constructor(readonly itemId: string) {
super(`计划项 ${itemId} 触达世界却未声明 manualActivationRule —— Enabled ≠ Active(AM 12.2)`);
this.name = 'AutomaticOnWorldTouchingError';
}
}
/** Agent 的三型行动 —— 每一步行动属于且仅属于其一(AM 4.2) */
export type AgentAction =
/** 调工具:系统调用与 LLM 工具 */
| { readonly kind: 'tool'; readonly itemId: string; readonly tool: string; readonly input: Record<string, unknown> }
/** 调人:四卡之一 —— 调人 = 立监听听人 */
| { readonly kind: 'human'; readonly itemId: string; readonly card: Record<string, unknown>; readonly workOrder: 'execution' | 'acceptance' }
/** 立监听:AI 不能调用世界,对世界只有两种合法姿势 —— 触达它(动作)与等它(监听) */
| { readonly kind: 'listen'; readonly itemId: string; readonly eventType: 'data' | 'time'; readonly keyFilter: Record<string, string>; readonly deadline: string }
/** 启动一个 Enabled 项(manualStart,留痕) */
| { readonly kind: 'start'; readonly itemId: string }
/** 规划裁量项入场 */
| { readonly kind: 'plan'; readonly itemId: string }
/** 弃用某项(此单不走这条) */
| { readonly kind: 'disable'; readonly itemId: string; readonly reason: string }
/** 评估语义性前提(评估留痕,可回放可归因) */
| { readonly kind: 'judge'; readonly criterion: string; readonly satisfied: boolean; readonly rationale: string }
/** 判定成果已齐,提交放行 */
| { readonly kind: 'submit'; readonly rationale: string }
/**
* 本回合**已在工具循环里调过人**:卡已发、监听已立(工具总线执行的)。
* 引擎只收尾把这一拍置 awaiting —— 不能再发一张,否则负责人工位上堆两张同样的卡。
* 开环模式(Mastra Agent 自主连环调工具)专用;单拍模式给不出这一型。
*/
| { readonly kind: 'asked'; readonly cardId: string; readonly rationale: string };
export type TurnOutcome = {
readonly runtime: CaseRuntime;
readonly enabled: EnabledSet;
readonly navigation: Navigation;
readonly action?: AgentAction;
readonly status: 'running' | 'awaiting' | 'submitted' | 'finished' | 'blocked';
readonly blockedReason?: string;
/** 终局唯放行:finished 时带裁决 —— released=true 才算走完,选口随放行确认 */
readonly released?: boolean;
readonly selectedPorts?: Record<string, string>;
};
export type LoopEngineService = {
/** 装载:XML 进语义引擎,说明书进 Agent 上下文;版本全部入信封 */
load(taskId: TaskId, model: LoopModel): Promise<CaseRuntime>;
runtime(taskId: TaskId): Promise<CaseRuntime | undefined>;
/** 发布 Enabled 集与红绿灯 —— 引擎推进的终点 */
publish(taskId: TaskId): Promise<EnabledSet>;
/** 跑一个回合:定位 → 择向 → 决断 → 行动 → 写数据 */
runTurn(taskId: TaskId): Promise<TurnOutcome>;
/** 事件驱动续跑:数据 / 时间 / 人 / 控制 */
/**
* 事件到达:**只记不跑**(登记 occur / 收验收裁决,存快照即返)。
* 从前这里自跑一整拍、deliver 随后的 tick 又跑一拍 —— 每次人卡回传
* 多耗一次不计 steps/activeMs 预算的模型决策。推进统一归 tick。
*/
onEvent(taskId: TaskId, event: { kind: 'data' | 'time' | 'human' | 'control'; ref: string; payload: Record<string, unknown> }): Promise<void>;
/** DMN 确定性求值 —— 能算的不问 LLM */
decide(taskId: TaskId, decision: string): Promise<DecisionResult>;
/** 驳回:相关计划项以新实例重新入场(迁移不是状态) */
reenter(taskId: TaskId, itemId: string): Promise<CaseRuntime>;
};