/** * 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; }; export function createAgentRunner(deps: AgentRunnerDeps): { decide(input: { taskId: TaskId; narrative: string; enabled: EnabledSet; scope: Record }): Promise; } { 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; 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, }; case 'human': return { kind: 'human', itemId, card: (raw['card'] ?? {}) as Record, workOrder: raw['workOrder'] === 'acceptance' ? 'acceptance' : 'execution', }; case 'listen': return { kind: 'listen', itemId, eventType: raw['eventType'] === 'time' ? 'time' : 'data', keyFilter: (raw['keyFilter'] ?? {}) as Record, // 时限必配 —— 模型没给就按三天兜底,绝不留幽灵等待 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', }; } }