Skip to content

Assistant turn that ends with reasoning only (no text, no tool call, finish "stop") is settled silently and the agent stops #55378

Open

Description

@juancarlosm

Description

This is #49673 on OpenCode 2. That issue (DeepSeek V4.1 Flash: "output=0, finish=stop … the chat stops after tool calls with no answer at all") was closed only because it was filed against OpenCode 1; the maintainer asked to reopen it if it was still relevant on 2.0, and that never happened. We reproduced the same symptom on 2.0.29 with Gemini.

When the model ends a step with reasoning tokens only (no text, no tool call, finish_reason: "stop"), OpenCode settles the turn as a normal completion and shows nothing. The agent just stops right after a tool result, with no error.

The streamed response for that step:

{
  "choices": [{"index": 0, "finish_reason": "stop",
               "message": {"role": "assistant", "content": null, "tool_calls": []}}],
  "usage": {"completion_tokens": 24,
            "completion_tokens_details": {"text_tokens": 0, "reasoning_tokens": 24}}
}

All the output tokens are reasoning, nothing else comes back, and the turn ends.

Expected: a step with no text and no tool call is not settled as a successful turn: retry it, or surface it as an error so the user knows to continue.

Plugins

N/A

OpenCode version

2.0.29

Steps to reproduce

Not deterministic:

  1. Use a Gemini model (seen with gemini-3.5-flash-lite) in an agentic session with tool calls.
  2. Occasionally the step right after a tool result streams back only reasoning, then finish_reason: "stop".
  3. OpenCode ends the turn with no visible output and waits for the user as if it had answered.

Screenshot and/or share link

The tool output is shown, and then nothing: no message, no error.

Operating System

Linux

Terminal

N/A

Activity

  1. opencode-agent commented on Oct 14, 2026

    @opencode-agent
    Contributor

    Thanks for the clear report. I was able to reproduce this on 2.0.29 with a simulated model, and it still happens on the latest dev (34d8ded0c).

    What I did:

    1. The model makes a shell tool call and gets its output back.
    2. The next step returns a single chunk with content: null, no tool calls, finish_reason: "stop" and usage of 24 tokens, all reasoning.

    The TUI shows the tool output and then nothing: no answer, no error, and the session goes idle as if the turn had ended normally. The token counter still includes that step's usage. The recording and screenshot below show it, and the script below reproduces it.

    It looks like the session runner only treats an empty step as a failure when the finish reason is unknown. An empty stop is recorded as a normal completion.

    Related issues:

    recording.mp4

    Screenshot

    Screenshot

    Reproduction script (drive.ts)
    // Repro for #55378: after a tool result, the model ends a step with no text and no
    // tool call (finish_reason "stop", only reasoning usage). OpenCode settles it silently.
    import { Effect } from "effect"
    import { Llm, OpenCodeDriver } from "opencode-drive"
    
    export default OpenCodeDriver.use(
      {
        ...(process.env.OC_DEV ? { opencode: { dev: process.env.OC_DEV } } : {}),
        project: { git: true, files: { "README.md": "hello\n" } },
        tools: ["shell"],
        tui: { recording: true },
      },
      ({ ui, llm, tools }) =>
        Effect.gen(function* () {
          const shells = yield* tools.control("shell")
          // Step 1: a normal tool call.
          yield* llm.queue(
            Llm.toolCall({ index: 0, id: "call_ls", name: "shell", input: { command: "ls" } }),
            Llm.finish("tool-calls"),
          )
          // Step 2: the provider returns no content and no tool call, only reasoning usage, then "stop".
          yield* llm.queue(
            Llm.raw({
              id: "chatcmpl-empty",
              object: "chat.completion.chunk",
              created: 0,
              model: "gemini-3.5-flash-lite",
              choices: [{ index: 0, delta: { role: "assistant", content: null }, finish_reason: "stop" }],
              usage: {
                prompt_tokens: 100,
                completion_tokens: 24,
                total_tokens: 124,
                completion_tokens_details: { reasoning_tokens: 24 },
              },
            }),
          )
          yield* ui.submit("List the files, then tell me what you found")
          const shell = yield* shells.take("call_ls")
          yield* shell.succeed({ output: "README.md\n", exit: 0 })
          yield* Effect.sleep(4000)
          yield* ui.screenshot("after-empty-stop")
        }),
    )

    Run it with opencode-drive: opencode-drive run ./drive.ts. It drives opencode from your PATH; set OC_DEV to the path of an OpenCode checkout to run that from source instead.

  2. zonemeen commented on Oct 14, 2026

    @zonemeen

    I’d like to take this on. My plan is to detect stop responses with no text or tool calls, retry with a limit, and surface a clear error if retries fail. I’ll also add a regression test for reasoning-only responses after a tool result.

  3. yxlimo commented on Oct 17, 2026

    @yxlimo

    Reproducing the same symptom on OpenCode 2.0.32 through the free-tier channel (opencode.ai/inference/...), across three models — so this is not Gemini-specific: mimo-v2.6-flash-free (×18), deepseek-v4.1-flash-free (×1), glm-5.3-flash-free (×1) within ~48h of multi-session agentic usage.

    What we can add beyond the hypotheses discussed so far is a raw stream capture taken at a passive MITM tap for one occurrence (glm-5.3-flash-free): the upstream sent a complete ~800 B stream —

    data: {"choices":[{"index":0,"delta":{"content":""}}]}
    data: {"choices":[{"index":0,"delta":{},"finish_reason":"stop"}]}
    data: usage chunk (completion_tokens=7)
    data: [DONE]

    No tool-call deltas at any point. The session DB recorded exactly what the stream contained (finish:"stop", no tool parts). 13 consecutive control streams on the same capture were healthy with normal finish_reason:"tool_calls" endings — so (a) the client settles the turn precisely as received and nothing is dropped client-side, and (b) the tap itself does not perturb the stream.

    This shape matches the captured stream in this issue exactly (empty/reasoning-only content, finish_reason:"stop", no text, no tool calls). Note it differs from the two shapes in #44148 (finish_reason:"other" and clean-FIN cutoff, Go plan): so at least three distinct upstream failure shapes currently end in the same silent stop.

    Related context, also from below: #49673 was never reopened despite still reproducing (the free-tier hits above were on 2.0.x), and PR #40531 (retry empty responses) remains unmerged/conflicted — so the client-side fix path is still open while occurrence continues.

    One possibly-relevant lead, with the caveat that causality is unproven: across 17 occurrences we recorded, 7 had a Magic-Context historian sub-session request rejected with HTTP 403 ("free tier can only be used from within OpenCode") 1.0–1.7 s around the hit (enrichment ~150× vs baseline assistant messages). If the upstream throttles bursts of parallel free-tier-authenticated requests, that could correlate with the empty-stop shape. Happy to share sanitized capture scripts and evidence on request.

  4. thymikee commented on Oct 17, 2026

    @thymikee

    Also seen on 2.0.32 with callstack/Apex (OpenAI-compatible, served by vLLM through LiteLLM). On the gateway the stopping requests are logged as success with a normal stop. Two examples, from about 13.5k agentic steps:

    • Reasoning only: 13,020 reasoning tokens, 3 output tokens, no text, no tool call. The session went idle and the task was reported complete.
    • 706 reasoning tokens plus 53 non-reasoning output tokens, yet no text part and no tool part were stored. Whatever those 53 tokens were never reached the session. That may point to a dropped tool call rather than a model that simply stopped.

    We see a close variant more often: the step ends with text that announces an action ("…Rewriting:", "Let me run the test…"), then stop with no tool call. It's harder to detect, but the same "retry or surface it" handling would help there too.

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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions