forked from ChaiHao/BPOS-MVP
装配(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
140 lines
5.6 KiB
TypeScript
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',
|
|
};
|
|
}
|
|
}
|