Bug: in-process subagent failures drop the child's error diagnostic — delegating tool shows only "subagent run failed" #5754
Replies: 3 comments
|
@LiveNathan 你的诊断我在 源级确认
但两条路径把它丢掉:
Tool 层已就绪(关键——修复只要一份数据)
const diagnostic = result.diagnostic === undefined ? '' : `\nDiagnostic: ${result.diagnostic}`
参考 diff(仅 in-process 驱动, 一个函数 + 断言)改动 @@ packages/subagent/subagent-in-process-driver/src/index.ts @@
const lastEnd = foldConsumedWork(own).end
const output: ContentBlock[] = finalAssistantOutput(own) ?? []
const recorded = toStopReason(lastEnd?.data.reason)
+ // A failed turn carries the structured LlmFailure on the end reason.
+ // toStopReason() flattens it to the string "error"; surface the payload
+ // (e.g. a 429/400/401) as the seam's diagnostic so the delegating tool
+ // renders "Diagnostic: …" instead of a bare "subagent run failed".
+ const diagnostic = capturedDiagnostic(lastEnd)
const stopReason: SubagentStopReason = cancelled && recorded !== 'completed' ? 'aborted' : recorded
if (structured !== undefined) {
if (structured.captured !== undefined) {
- return { output, structured: structured.captured.value, stopReason }
+ return { output, structured: structured.captured.value, stopReason, ...(diagnostic === undefined ? {} : { diagnostic }) }
}
if (stopReason === 'completed') return { output, stopReason: cancelled ? 'aborted' : 'error' }
}
- return { output, stopReason }
+ return { output, stopReason, ...(diagnostic === undefined ? {} : { diagnostic }) }
}
+function capturedDiagnostic(reason: TurnEndReason | undefined): string | undefined {
+ if (reason?.kind !== 'error') return undefined
+ const message = reason.error?.message
+ return message === undefined || message.length === 0 ? undefined : limitSubagentDiagnostic(message)
+}
注入式断言(可加在 subagent-in-process-driver 既有 spec)
与 out-of-process 的一致性out-of-process 驱动(acp/claude-code/codex)已通过 一个点提醒 maintainer: 插件可观测性关联(顺带)这个「child 失败的 error 只留在子会话 log, 父只见 subagent run failed」的可观测性缺口, 与我的 |
|
Thanks for the line-by-line verification against Confirming both key points:
Your reference diff is essentially the patch. The test suggestion is spot on too — asserting Happy to put up a PR (driver change + test) unless a maintainer would rather take it from here. |
|
Independently reproduced on a newer tree, so the gap is still open past your I built the fix before finding this thread (I should have searched first — that If it helps, three things in my version go slightly beyond the reference diff,
Tests in the patch: driver — populated detail ( Patch: Since issues and PRs are disabled upstream, I am happy for this to be carried Cross-links so the threads converge instead of diverging: #4666 (where the |
Uh oh!
There was an error while loading. Please reload this page.
Summary
When an in-process (
spawn/fork) subagent child fails at the model-call level, the delegating tool result is only:No diagnostic, no child session pointer. The actual failure (HTTP 429 billing cap, 400 invalid request, auth error, …) exists only in the child's own session log (
~/.dsh/sessions/<workdir>/<childSessionId>/session.jsonl.zstd, theturn/endevent'sreason.error.message). This turns a 30-second diagnosis into a multi-day mystery: in our case a reviewer-seat subagent pinned togoogle/gemini-3.8-flashfailed for days, looking like a harness spawn bug, when the real errors were400: Thinking level MINIMAL is not supported for this modeland later429: project exceeded its monthly spending cap(see https://github.com/LiveNathan/showbook/issues/582).Root cause (v0.1.2-rc.1)
@deepseek-ai/dsh-subagent-in-process-driver—readResult():toStopReason()flattens theturn/endreason{ kind: "error", error: { message: … } }to the string"error"and discards the payload. The seam's result object already supports an optionaldiagnosticfield (settleRunResult()in@deepseek-ai/dsh-subagenteven has acollectDiagnosticsink — but only the out-of-process path feeds it), so the in-process driver is the gap:onError/collectDiagnostic→result.diagnostic→ surfaced by the tool'swithDiagnosticAndPartialText.reason.kind === "error"→readResultreturns{output, stopReason}only →stopReasonError()renders"subagent run failed"withdiagnostic === undefined.Suggested fix
In
readResult(), when the last consumed-work end carriesreason.kind === "error", copy the error message (already available on the event) into the returned result asdiagnostic. The tool layer already renders it (Diagnostic: …). One small change; out-of-process behavior is untouched.Environment
llm-pi-airoutegoogle/gemini-3.8-flash, in-process spawn subagentsAll reactions