Repository navigation
subagent: finish:"error" / MALFORMED_FUNCTION_CALL with empty content is reported to the parent as a successful completion #52378
Description
Description
Summary
OpenCode reports a failed subagent run to the parent as a successful one.
A Task subagent's last assistant message ended with finish:"error", rawFinish:"MALFORMED_FUNCTION_CALL", content:[], and 0 output tokens. OpenCode then set the child session to idle with outcome:"succeeded", and the parent got state="completed" with "Subagent completed without a text response." The parent cannot tell a provider failure from a real empty answer. In a review panel, an orchestrator can read that as a clean review. This happened 3 times in one session.
Expected Behavior
- The child ends in an error state (for example
idleoutcome:"failed", or the existing error outcome), with the provider error attached. - The parent receives
state="error", and the tool result carries the finish reason (MALFORMED_FUNCTION_CALL) and the provider error text. - Optional: retry the failed step once automatically. This failure may be transient (see Frequency below).
Actual Behavior
Evidence from ~/.local/share/opencode/opencode.db (sqlite3 -readonly). I removed prompts and file contents. All other fields are as stored, except providerState, which is trimmed.
Child ses_f0ca68c11ffescNxFG4bNp3AjF, last assistant message (seq 33, 2026-09-30 19:25:35):
{
"agent": "prosecutor-tests",
"model": { "id": "vmc/adlc-prosecutor-tests", "providerID": "vercel", "variant": "default" },
"content": [],
"finish": "error",
"rawFinish": "MALFORMED_FUNCTION_CALL",
"providerState": {
"routing": {
"originalModelId": "google/gemini-3.8-flash",
"finalProvider": "google",
"modelAttempts": [
{
"canonicalSlug": "google/gemini-3.8-flash",
"success": false,
"providerAttempts": [
{
"provider": "google",
"success": false,
"error": "stream finished with error: MALFORMED_FUNCTION_CALL"
}
]
}
]
}
},
"tokens": { "input": 3771, "output": 0, "reasoning": 37, "cache": { "read": 20366, "write": 0 } }
}Next event (seq 36, 19:25:37):
{ "time": { "created": 1790789137877 }, "outcome": "succeeded" }Parent (root session), synthetic message seq 2311:
<subagent sessionID="ses_f0ca68c11ffescNxFG4bNp3AjF" state="completed" description="P5 panel 2 tests">
Subagent completed without a text response.
</subagent>
The same pattern appears in two other child sessions:
| Child session | Last assistant seq | finish / rawFinish |
Output tokens | Idle outcome | Parent message |
|---|---|---|---|---|---|
ses_f0ca68c11ffescNxFG4bNp3AjF |
33 (19:25:35) | error / MALFORMED_FUNCTION_CALL |
0 | succeeded (seq 36) |
seq 2311, state="completed" |
ses_f0ca60ab7ffe7Tp09wDsPvc6Yq |
40 (19:26:14) | error / MALFORMED_FUNCTION_CALL |
0 | succeeded (seq 43) |
seq 2355, state="completed" |
ses_f0c89759cffe0N4N0oDTX5gp4M |
33 (19:57:20) | error / MALFORMED_FUNCTION_CALL |
0 | succeeded (seq 36) |
seq 2686, state="completed" |
session_v2.idle_outcome is succeeded for all three. Each child made 4–5 valid tool calls before the failing step.
Query used:
sqlite3 -readonly ~/.local/share/opencode/opencode.db "select seq, data from session_message where session_id='<child>' and type in ('assistant','idle') order by seq desc limit 2"Additional Context
- Frequency: 3 out of 5 invocations of this subagent in one session. The same model worked earlier that day.
- Impact: the parent retried by hand, then switched to a different model. The cost was small (≈ $0.12), but an empty "completed" result looks like "no findings" to a reviewing orchestrator.
- Workaround: tell the orchestrator to treat an empty subagent reply as a failed run, retry once, then switch to another model.
rawFinishis mapped inpackages/opencode/src/session/llm/ai-sdk.ts. The error seems to get lost wherefinish:"error"becomes the child's idle outcome or the parent's subagentstate.- Provider:
vercel(AI Gateway). Virtual modelvmc/adlc-prosecutor-tests→google/gemini-3.8-flash, routed to Google. - Install/channel: latest (Homebrew
anomalyco/tap/opencode-v2, plus OpenChamber's bundledopencode-cli, driven viaopencode serve). Shell: /bin/zsh.
Possibly related. These issues also report a failed stream that reaches the parent as an empty success, but none of them covers v2 with finish:"error" / MALFORMED_FUNCTION_CALL:
- Subagent stream error can surface as empty task result #38866: Subagent stream error can surface as empty task result (v1.18.3).
- Sub-agent Task silently returns empty result when the final response stream fails (e.g. 503) — no error, no retry, completed work lost #40527: Sub-agent Task silently returns empty result when the final response stream fails (v1.18.13, 503).
- Aborted provider stream recorded as clean stop (finish=unknown, zero usage, no text) — subagent returns empty with no error #37852: Aborted provider stream recorded as clean stop (
finish=unknown), subagent returns empty with no error. - Session idles silently when a turn ends with
stopand no model output #50250: Session idles silently when a turn ends withstopand no model output.
Plugins
@adlc/opencode, opencode.browser, openchamber-agent-tool
OpenCode version
Sessions recorded by v2.0.18 (session_v2.version, OpenChamber's bundled opencode-cli). The same machine also has Homebrew opencode-v2 v2.0.20. I have not checked whether 2.0.19 or 2.0.20 changed this code path.
Steps to reproduce
Intermittent. It depends on the model sending a malformed tool call.
- Configure a subagent pinned to a Gemini Flash model (here, through the Vercel AI Gateway).
- From a parent session, invoke it with the Task tool, with a tool-heavy brief (read or grep over a large diff).
- After several valid tool steps, the provider stream sometimes ends with
stream finished with error: MALFORMED_FUNCTION_CALL. - Look at the parent: the task result is
state="completed"with "Subagent completed without a text response."
Screenshot and/or share link
No response
Operating System
macOS (Darwin 25.6.0 arm64)
Terminal
xterm-256color (sessions driven through OpenChamber via opencode serve)
Activity
github-actions commentedon Sep 30, 2026
This issue might be a duplicate of existing issues. Please check:
r266-tech commentedon Sep 30, 2026
Confirmed the downstream failure on
v2at f46fa72 (package version 2.0.20), using the real session runner, job lifecycle and temporary database with the existingtool-subagent.test.tsharness. This is a synthetic provider-event reproduction, not a live Vercel/Gemini reproduction.Replace the
completionItfixture'sTestLLM.text(...)fallback with:For both foreground and background subagent calls, the assistant persists
finish: "error",rawFinish: "MALFORMED_FUNCTION_CALL", empty content and noerrorfield. The child outcome issucceeded, the job iscompleted, and the parent gets the empty completed result. Changing only the normalized reason to"stop"gives the expected successful empty-response control (2 controls pass; both error-outcome assertions fail).The relevant seam is
packages/core/src/session/runner/step.ts: a successfully ended stream withfinish === "unknown"synthesizes a failure, but explicit"error"does not. It reachesOutcome.Completedwithoutrecord.failure;subagent-job.tsthen accepts the assistant becausemessage.erroris absent. Empty text alone should remain a valid control.AI-assisted investigation and test execution. Sharing the reproduction here for the existing assignee; no parallel PR opened.
tpischke commentedon Oct 1, 2026
Same class of failure with
finish: "length", on a local OpenAI-compatible provider (OpenCode 2.0.21).A subagent's last assistant message was cut by the request's output limit while still in its reasoning block: the message holds one
reasoningpart and notextpart,finishislength. OpenCode set the child session toidlewithoutcome: "succeeded"and the parent received "Subagent completed without a text response."Seen three times in one session on 2026-10-01: two review subagents (32,768 output tokens each, all reasoning) and one implementer subagent that had left uncommitted edits. In all three the parent could only infer the failure from the empty result; it could not see the finish reason, so it could not tell a truncated turn from a subagent that had nothing to say. For the implementer it guessed "tooling failure" and re-dispatched with the same brief.
So the expected behaviour described above would cover this case too: a last message that ends with
finish: "length"and no text should end the child with a failed outcome, and the tool result should carry the finish reason.r266-tech commentedon Oct 1, 2026
Confirmed the
lengthcase on currentv2at 0f9f0a9 (2.0.21), using the real runner/job/database and syntheticTestLLMevents. No live provider calls.The existing
tool-subagent.test.tscompletion fixture can use this fallback:In both foreground and background cases, the assistant retains reasoning and
finish: "length", but noerror; the child issucceeded, the job iscompleted, and the parent receives “Subagent completed without a text response.” The completion notification also omits the finish reason.Changing only the normalized finish to
stoppasses both controls. Emptystopcontrols also pass; the earliererrorreproduction still fails. Overall: 4 controls pass, 4 expected-failure-outcome assertions fail. The runner'sstep.tsstill special-cases onlyunknownafter a successfully ended stream. A fix limited to explicit provider errors would therefore leave this truncation case uncovered.AI-assisted investigation and test execution; sharing with the existing assignee.
rebelliard commentedon Oct 25, 2026
Still happens on current
dev(32b5701), and #54713 fixes it. I ran its tests ondevwith and without thestep.tschange: 2 fail without, 13/13 pass with. It keepserrorterminal with no retry, which works for me.Root cause is
step.ts#L171-L172. After a cleanly ended stream, onlyfinish === "unknown"becomes a failure. An expliciterrorfinish completes normally, andsubagent-job.ts#L58then accepts the message becausemessage.erroris undefined.The query above now finds 9 of these in my database: 6 on 2.0.18, 1 on 2.0.20, 2 on 2.0.22, last on 2026-10-08. None on 2.0.25 or 2.0.32, but routing metadata isn't stored on those versions, so I can't tell whether that alias was still hitting Gemini Flash.
@tpischke's
finish: "length"case is a different path.devalready handles it inllm.ts#L299-L328: two continuations, then anoutput-limitstep failure. I have nolengthcases locally, so I didn't test it.