mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-17 13:55:50 +00:00
fix(workflow): schema 模式弃用 StructuredOutput 工具契约,改鲁棒 JSON 文本解析
上一轮 70a2f76 把"agent 长 tool chain 后忘调 StructuredOutput"当作死因,
加 prompt 头尾双强制。但实测跑 5 个 review agent 4 个 dead,detail 全是
"StructuredOutput tool is not available as a deferred tool"——根因是
该工具从未注入 workflow sub-agent 的工具集(assembleToolPool 默认池不含,
只有 stop_hook 路径 execAgentHook.ts 显式 createStructuredOutputTool())。
prompt 反复要求调一个不可达的工具,agent 困扰、长篇辩解、最终没产 JSON。
- claudeCodeBackend.ts:
- extractStructuredOutput 重写:括号栈扫描替代 indexOf/lastIndexOf,
处理嵌套对象、字符串内的括号、转义符;新增 fenced code block
优先路径(```json / ```),多 JSON 块取第一个 parse 成功的;
只返回 plain object(拒 array/number/string/null)。不做语法修复
(尾逗号/单引号/注释)——避免在字符串内误改(如 "http://" 被 // 注释正则吃)。
- schema 模式 prompt 简化:删首尾双 STRUCTURED OUTPUT 强制(600+ token),
改成指示 agent 在最后文本块 emit raw JSON;明确告知"StructuredOutput
is not available in this environment",消除调用幻觉。
- hooks.ts: detail.slice 用 typeof === 'string' 守卫;catch 块用
e instanceof Error ? e.message : String(e)(旧 journal / 第三方 adapter
可能写非 string detail,直接 .slice 会抛 TypeError 击穿日志)。
- claudeCodeBackend.test.ts: +9 测试覆盖 fenced / 嵌套 / 字符串内括号 /
转义引号 / 多块取首 / 类型守卫 / 损坏 JSON。
precheck: 5663 pass / 0 fail。
Co-Authored-By: glm-5.2 <zai-org@claude-code-best.win>
This commit is contained in:
@@ -166,23 +166,29 @@ export function makeHooks(
|
||||
// 都给一次重试机会;WorkflowAbortedError(kill)不重试——是用户意图。
|
||||
// 重试仍失败:dead 保持 dead;throw 降级为 dead(不让一个 agent 击穿 workflow)。
|
||||
// budget 不重复扣:dead 不 addOutputTokens;重试 ok 才扣一次(最终 ok 时)。
|
||||
// dead.reason 透传到日志(审计 8/12 dead 都是 no-structured-output 时直接可见)。
|
||||
// dead.reason 透传到日志:no-structured-output(agent 最终文本块没产 plain-object JSON)
|
||||
// 是高频死因;detail 进日志能立刻看到 agent 最后说了什么。
|
||||
// detail 用 String() 包裹防御:旧 journal 或第三方 adapter 可能写入非 string(损坏数据),
|
||||
// 直接 .slice 会抛 TypeError 击穿日志路径。
|
||||
let result: AgentRunResult
|
||||
try {
|
||||
result = await invokeBackend()
|
||||
if (result.kind === 'dead') {
|
||||
const detailStr =
|
||||
typeof result.detail === 'string' ? result.detail : ''
|
||||
ctx.ports.logger.warn?.(
|
||||
`agent "${label ?? `#${agentId}`}" returned dead` +
|
||||
(result.reason ? ` (${result.reason})` : '') +
|
||||
(result.detail ? `: ${result.detail.slice(0, 150)}` : '') +
|
||||
(detailStr ? `: ${detailStr.slice(0, 150)}` : '') +
|
||||
'; retrying once',
|
||||
)
|
||||
result = await invokeBackend()
|
||||
}
|
||||
} catch (e) {
|
||||
if (e instanceof WorkflowAbortedError) throw e
|
||||
const eMsg = e instanceof Error ? e.message : String(e)
|
||||
ctx.ports.logger.warn?.(
|
||||
`agent "${label ?? `#${agentId}`}" threw (${(e as Error).message}); retrying once`,
|
||||
`agent "${label ?? `#${agentId}`}" threw (${eMsg}); retrying once`,
|
||||
)
|
||||
try {
|
||||
result = await invokeBackend()
|
||||
@@ -192,7 +198,7 @@ export function makeHooks(
|
||||
result = {
|
||||
kind: 'dead',
|
||||
reason: 'runagent-threw',
|
||||
detail: (e2 as Error).message,
|
||||
detail: e2 instanceof Error ? e2.message : String(e2),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user