Skip to content

TaskTool returns no diagnostic context when subagent result text is empty or missing #24447

Closed as not planned

Description

@gmnstr

Summary

TaskTool appears to have no diagnostic fallback when a subagent result contains no usable text output. In packages/opencode/src/tool/task.ts, it serializes the last text part and falls back to "" if none is found.

In one local case on 1.14.22, this surfaced in the parent session as an empty <task_result>, which made it unclear whether the subagent completed without text, errored, or produced only non-text parts.

Relevant code

In packages/opencode/src/tool/task.ts:

result.parts.findLast((item) => item.type === "text")?.text ?? ""

This appears to collapse "no text part exists" into an empty result with no diagnostic context.

Observed behavior

I hit this while invoking a code-reviewer subagent through the task tool. The parent session received:

<task_result>

</task_result>

Smaller subagent calls worked, so the subagent type was available and functional.

I do not yet have a minimal standalone reproduction. This issue combines a local observed symptom with the source behavior above.

Why this is hard to debug

From the parent session, there is no indication whether the subagent:

  • completed without text
  • errored before producing text
  • produced only tool/reasoning parts
  • was interrupted
  • hit an adapter/provider edge case

The parent receives an empty <task_result> body, with no diagnostic context explaining why.

Expected behavior

If the subagent final message has no text part, the task result should include an explicit diagnostic instead of silently returning an empty result.

For example, if available and safe to expose:

Subagent completed but produced no text part.
finish: <finish reason>
error: <error if present>
parts: <part type summary>
session: <child session id>

Possible fix direction

Distinguish between:

  1. A text part that exists but is intentionally empty
  2. No text part existing at all

Illustrative only; a real patch may need to guard on the result role/type:

const textPart = result.parts.findLast((item) => item.type === "text")
const info = result.info.role === "assistant" ? result.info : undefined

const body =
  textPart?.text ??
  [
    "Subagent completed without a text response.",
    `finish: ${info?.finish ?? "unknown"}`,
    `error: ${info?.error ? JSON.stringify(info.error) : "none"}`,
    `parts: ${result.parts.map((part) => part.type).join(", ") || "none"}`,
  ].join("\n")

Related

Possibly related to #18423, where an Ollama subagent executes tool calls but returns empty text to the orchestrator.

Activity

  1. added
    coreAnything pertaining to core functionality of the application (opencode server stuff)
    on Apr 26, 2026
  2. github-actions commented on Apr 26, 2026

    @github-actions
    Contributor

    This issue might be a duplicate of or closely related to existing issues. Please check:

    If this is distinct from those issues, it would help to clarify what differentiates the root cause or the fix scope.

  3. gmnstr commented on Apr 26, 2026

    @gmnstr
    Author

    I agree these are related.

    After checking the linked issues, I think the closest overlaps are:

    The distinction I’m trying to preserve here is the TaskTool serialization boundary rather than the upstream cause.

    This issue is not claiming to explain all empty subagent results, task hangs, or provider-specific text loss. The narrower problem is that this expression:

    result.parts.findLast((item) => item.type === "text")?.text ?? ""

    makes several states indistinguishable to the parent:

    • a text part exists but is empty
    • no text part exists
    • only non-text parts exist
    • the child result has diagnostic metadata but no text
    • an upstream runtime/provider/adapter issue produced no usable text

    So even if #21250 fixes trailing empty assistant runs, and even if #5204 fixes provider-error propagation, TaskTool can still return a blank successful <task_result> when no usable text part is available.

    I think this issue is best treated as diagnostic/serialization hardening: avoid returning a blank <task_result> when the task result has no usable text to serialize.

  4. removed
    coreAnything pertaining to core functionality of the application (opencode server stuff)
    on May 3, 2026
  5. niStee commented on May 27, 2026

    @niStee

    After looking at the different PRs trying to solve this (#28021, #29176, and #29048), I realized that none of the approaches are perfect in isolation because they don't differentiate between legitimate empty outputs and illegitimate empty outputs.

    1. The 'Fail Fast' approach (fix(tool): trigger fallback on empty task output #29048): Unconditionally throwing an error when there is no text successfully triggers the FallbackProvider (which is necessary for silent failures and rate limits), but it accidentally breaks legitimate use cases (e.g., when a subagent successfully uses a tool but doesn't emit final text).
    2. The 'Diagnostic String' approach (feat(task-tool): emit diagnostic when subagent returns no text part (#24447) #28021, fix(task): surface non-text subagent results #29176): Returning a diagnostic string fixes the legitimate use cases, but because it returns a string instead of throwing an error, the FallbackProvider never intercepts the failure when it actually is a rate limit or silent crash.

    We need a hybrid approach! The TaskTool should look at the finishReason or the parts array:

    • If finishReason === 'tool-calls' or it successfully emitted tool parts -> Return the diagnostic string (legitimate zero answer).
    • If finishReason === 'unknown' or 'error' and the parts array is empty -> Throw an explicit error so the fallback chain can catch it and switch models (illegitimate zero answer).

    I will work on updating my PR to implement this combined logic!

  6. github-actions commented on Jul 26, 2026

    @github-actions
    Contributor

    To stay organized issues are automatically closed after 60 days of no activity. If the issue is still relevant please open a new one.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions