loop:模型问不出结果时转人裁断,而不是抛异常空转

A3 / A2 起来后每一拍都抛 `Authentication Fails` 并被推进器吞掉:
任务永远 running、预算照烧、负责人工作台上什么也看不到。

模型这一轮给不出行动(端点不可达 / 鉴权失败 / 超时),**不是异常路径,是断点的一种**:
机器当下决断不了就该调人 ——「人在环是一种被调用的工具」。世界一步都没碰,
交回给人是安全且诚实的落点。改在 app 的 agent-runner 收口(引擎保持纯粹),
与 flow 的步级 `failure.fallback` 同构。

缺口作为**依据**摆在卡上,不当表单项让人逐条填。

现状:A3 渠道寻源(林佩萱)装载 21 个计划项、A2 替代选型(王梓名)装载 19 个,
两个 loop 都在等人回卡,不再空转。

gates: tsc / eslint / depcruise / 393 测试 全绿

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KeDBx2PHLavbURXB8LJJ7m
This commit is contained in:
2026-07-27 04:57:16 +08:00
co-authored by Claude Opus 5
parent 9de33a5416
commit c68a562db1
+34 -9
View File
@@ -70,15 +70,40 @@ export function createAgentRunner(deps: AgentRunnerDeps): {
.join('\n'); .join('\n');
const model = await deps.modelOf(taskId); const model = await deps.modelOf(taskId);
const result = await deps.gateway.generate({ try {
model, const result = await deps.gateway.generate({
prompt, model,
outputSchema: ACTION_SCHEMA, prompt,
taskId, outputSchema: ACTION_SCHEMA,
label: 'loop-turn', taskId,
}); label: 'loop-turn',
});
return normalize(result.object, enabled); return normalize(result.object, enabled);
} catch (error) {
/*
* 模型这一轮问不出结果(端点不可达、鉴权失败、超时…)。
*
* 这**不是**异常路径,是断点的一种:机器当下决断不了,就该调人 ——
* 「人在环是一种被调用的工具」。抛出去只会让任务挂着空转:
* 推进器每一拍都重试、每一拍都记一行错,预算照烧,负责人却看不到任何东西。
* 世界一步都没碰,交回给人是安全且诚实的落点。
*/
const detail = error instanceof Error ? error.message : String(error);
return {
kind: 'human',
itemId: enabled.entries[0]?.id ?? 'breakpoint',
workOrder: 'execution',
card: {
title: '模型这一轮未能决断,转人裁断',
reason: detail,
// 缺口是**依据**不是表单项:摆给人看,别让人逐条填
gaps: enabled.gaps.join('、') || '(无)',
enabled: enabled.entries.map((e) => `${e.id} ${e.name}`).join('、') || '(空)',
ask: '请判断下一步:接着走哪一项,或补齐缺口后让环继续。',
fields: ['decision'],
},
};
}
}, },
}; };
} }