Files
JX-BPOS/apps/api/src/app/agent-runner.ts
T
JCAI Main AgentandClaude Opus 5 78dc03bb58 app 装配收尾:18 个模块接上,MVP 判据 1-2 端到端跑通
装配(app 是唯一组装层):
- wiring/engines.ts:skill/flow/loop/task/process/trigger 六个模块经端口接起来;
  两个范式引擎按统一 ParadigmEngine 契约暴露给 task —— 对上层不可区分
- agent-runner.ts:loop 的 Agent 回路经 ModelGateway(两缝之一,回放审看得见);
  出参过 schema 框住三型行动,不合规退化为「等人」而不是乱动世界
- container:interaction / human-tool / activity-registry / release 全部接上;
  event 的 TriggerMatcher 与 org 的 InFlightWorkProbe 以延迟绑定解环,端口不变
- workers/consumers.ts:派发器的两个消费者(任务监听推进旧单 / 触发器开新单,独立判定)
- tools/first-party.ts:查历史价(录制回放替身)+ 三分类打标(纯工具,DMN 确定性求值)
- seed-quote.ts:治理台四件事一次做完 —— 绑 LLM / 注册工具两审 / 经安装通道装活动包 /
  发布流程包(闭合校验通过)+ 配触发器与邮件通道

已验证(真机):
1. 治理台配置全过;活动包静态审 0 条提示、流程包闭合校验通过
2. 一封 RFQ 邮件入站 → 归属解析 → 触发器命中 1 → 起一条工作流 →
   A1 任务按岗位唯一人解析落到 BD 冯兵,runtime=running

修复:zod 判别式重复值(周期触发两型展开为 periodic-calendar / periodic-data,
triggerKindOf 对回三类分类学);活动构建脚本移出 activities(它要用 fs,
而活动包零环境能力 —— depcruise 拦下后按边界搬家)

358 个测试通过,门禁五道全绿

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KeDBx2PHLavbURXB8LJJ7m
2026-07-27 02:24:36 +08:00

140 lines
5.6 KiB
TypeScript

/**
* loop 的 Agent 回路 —— `AgentPort` 的实现(app 层装配,编码规范 §5.2)。
*
* 「回合由 usecase 驱动,**不放开自由 maxSteps**,预算在环外由 Harness 强制」——
* 所以这里只做**一次**决断:把 Enabled 集与红绿灯喂给模型,要它在三型行动里选一个。
*
* 一切模型调用经 `ModelGateway`(两缝之一),replay 模式下自动走录制 —— 回放审看得见。
* 见 docs/debt.md · M5-2:§6.1 规定这里用 Mastra `Agent`;当前直接经 ModelGateway,
* 端口不变,换实现时 usecase 不动。
*/
import type { AgentAction, EnabledSet } from '@modules/loop-engine';
import type { ModelGateway } from '@modules/llm';
import type { TaskId } from '@contracts';
/** Agent 只能选三型行动之一(外加规划 / 弃用 / 评估 / 提交)—— schema 把它框住 */
const ACTION_SCHEMA = {
type: 'object',
required: ['kind'],
properties: {
kind: { enum: ['start', 'plan', 'disable', 'judge', 'tool', 'human', 'listen', 'submit'] },
itemId: { type: 'string' },
tool: { type: 'string' },
input: { type: 'object' },
workOrder: { enum: ['execution', 'acceptance'] },
card: { type: 'object' },
eventType: { enum: ['data', 'time'] },
keyFilter: { type: 'object' },
deadline: { type: 'string' },
criterion: { type: 'string' },
satisfied: { type: 'boolean' },
rationale: { type: 'string' },
reason: { type: 'string' },
},
} as const;
export type AgentRunnerDeps = {
readonly gateway: ModelGateway;
/** 活动声明的自定义模型名 */
readonly modelOf: (taskId: TaskId) => Promise<string>;
};
export function createAgentRunner(deps: AgentRunnerDeps): {
decide(input: { taskId: TaskId; narrative: string; enabled: EnabledSet; scope: Record<string, unknown> }): Promise<AgentAction>;
} {
return {
async decide({ taskId, narrative, enabled, scope }) {
// 交集为空即断点 —— 引擎已经算好了,这里只把它讲给模型听
const breakpoint = enabled.entries.length === 0;
const prompt = [
'你是这一单任务的案例工作者。下面是语义引擎发布的**当下可为**与**目标缺口**。',
'你只能选一个行动,且必须落在三型行动之内:调工具 / 调人 / 立监听(外加 start、plan、disable、judge、submit)。',
'规矩:①触达世界的动作只能 tool(写类会被登记为动作意向,放行时才落地)',
'②缺口只有人知道 → human;缺口在世界侧 → listen(必须给 deadline,ISO-8601)',
'③成果已齐才 submit。只输出 JSON。',
'',
`【案例说明书与活动记忆】\n${narrative}`,
'',
`【当下可为(Enabled 集)】\n${
enabled.entries.map((e) => `- ${e.id} ${e.name}(${e.type}${e.requiresManualStart ? ' · 须你启动' : ''}${e.discretionary ? ' · 裁量项' : ''})`).join('\n') || '(空)'
}`,
`【目标缺口】\n${enabled.gaps.join('、') || '(无)'}`,
enabled.judgements.length === 0
? ''
: `【待你评估的语义性前提】\n${enabled.judgements.map((j) => `- ${j.criterion}:${j.statement}`).join('\n')}`,
breakpoint ? '【注意】当下可为 ∩ 目标所需 = 空 —— 这是依赖断点,请调人或立监听。' : '',
`【任务数据】\n${JSON.stringify(scope).slice(0, 4000)}`,
]
.filter((line) => line !== '')
.join('\n');
const model = await deps.modelOf(taskId);
const result = await deps.gateway.generate({
model,
prompt,
outputSchema: ACTION_SCHEMA,
taskId,
label: 'loop-turn',
});
return normalize(result.object, enabled);
},
};
}
/** 模型出参归一为 AgentAction;不合规一律退化为「立监听等人」而不是乱动世界 */
function normalize(object: unknown, enabled: EnabledSet): AgentAction {
const raw = (object ?? {}) as Record<string, unknown>;
const kind = String(raw['kind'] ?? '');
const itemId = String(raw['itemId'] ?? enabled.entries[0]?.id ?? '');
switch (kind) {
case 'start':
case 'plan':
return { kind: kind as 'start' | 'plan', itemId };
case 'disable':
return { kind: 'disable', itemId, reason: String(raw['reason'] ?? '此单不走这条') };
case 'judge':
return {
kind: 'judge',
criterion: String(raw['criterion'] ?? ''),
satisfied: raw['satisfied'] === true,
rationale: String(raw['rationale'] ?? ''),
};
case 'tool':
return {
kind: 'tool',
itemId,
tool: String(raw['tool'] ?? ''),
input: (raw['input'] ?? {}) as Record<string, unknown>,
};
case 'human':
return {
kind: 'human',
itemId,
card: (raw['card'] ?? {}) as Record<string, unknown>,
workOrder: raw['workOrder'] === 'acceptance' ? 'acceptance' : 'execution',
};
case 'listen':
return {
kind: 'listen',
itemId,
eventType: raw['eventType'] === 'time' ? 'time' : 'data',
keyFilter: (raw['keyFilter'] ?? {}) as Record<string, string>,
// 时限必配 —— 模型没给就按三天兜底,绝不留幽灵等待
deadline: typeof raw['deadline'] === 'string' && /^P/.test(raw['deadline']) ? raw['deadline'] : 'P3D',
};
case 'submit':
return { kind: 'submit', rationale: String(raw['rationale'] ?? '判据已收敛') };
default:
// 看不懂就等人,不擅自触达世界
return {
kind: 'human',
itemId,
card: { title: '需要你判断下一步', reason: '模型未给出合规的行动' },
workOrder: 'execution',
};
}
}