DeepSeek Harness thinking-only output: empty stop succeeds

TroubleshootingPublished 2026-10-20Author: DeepSeek Plugin Market
DeepSeek HarnessEMPTY_RESPONSEreasoning modelsempty outputretry policy
DeepSeek Harness counts a reasoning-only stop as success and never retries it. Add a guard plugin, reclassify in the adapter, or lower reasoning.

"It finished thinking and said nothing" is not one failure but two different turn terminal states, and neither triggers a retry. One is a reasoning-only stop that the adapter treats as a normal completion; the other is max-tokens after the output budget runs out, which is treated as a turn outcome. The first comes from a test that is too narrow, the second from a classification outside the failure codes, and the fixes differ (Discussion #9893).

DeepSeek Harness terminal states and how to read turn/end

The two empty outputs follow two code paths, and troubleshooting them together leads to the wrong conclusion. Split them apart:

  1. A completion with only reasoning. llm-pi-ai at stream.ts:96-99 checks only message.content.length === 0 inside case 'stop', and llm-deepseek at translate.ts:149 checks only blocks.size === 0.
  2. Thinking blocks count toward content. pi-ai's thinking_start, thinking_delta, and thinking_end share the contentIndex counter with text, and DeepSeek's content_block_start puts every block, including reasoning, into blocks.
  3. So "a long stretch of thinking, then stop" leaves a non-empty block set, which maps to an ordinary { kind: 'stop' }; the loop sees no tool call and returns completed.
  4. The other case is stop_reason: 'length', where finish becomes max-tokens and the retry plugin never sees it, because the plugin hooks only agent/request-error, which is open only to error and aborted.

According to the thread, the repository contains a test that pins the first behavior, named "keeps a thinking-only stop successful (any block counts as content)", which shows this is the current design rather than an accident.

The distinction lives in turn/end.reason.kind in the persistence log; do not guess it. Steps:

  1. Open the failing session log and find the turn/end for that turn. Expected: a { turn, reason } object.
  2. If reason.kind === 'max-tokens', classify it as budget exhaustion. Expected: the last assistant/message is usually truncated.
  3. If reason.kind === 'completed', inspect the content blocks of the last assistant/message. Expected: only reasoning blocks with no text and no tool call means the first case.
  4. Search for llm/retry and llm/retry-started events. Expected: none in the whole log, which means the retry path was never entered.

Step 4 matters: it separates "retry did not work" from "retry was never reached". The latter is what both terminal states share.

DSH plugin path 1: install silent-completion-guard

This works today without touching core, at the cost of turning empty turns into visible errors; check the plugin version and compatibility on DSH Plugin Hub. The idea and the steps:

  1. Understand what the plugin does: it wraps the chunk stream returned by llm/stream and watches the terminal finish. Expected: the model request is unchanged; only the last frame of the response stream is rewritten.
  2. When finish is stop and the turn has neither a text block nor a tool-call block, replace it with an error finish carrying EMPTY_RESPONSE. Expected: the loop records assistant/attempt instead of appending an empty assistant message.
  3. Let the default retry policy take over: five attempts with backoff from 500 milliseconds to 10 seconds. Expected: a model that answers on the second try ends normally; five empty attempts end the turn with an explicit error.
  4. Reproduce once in a long session. Expected: either a body, or a real error in the transcript instead of "analysis complete" next to an empty bubble.

It does not change why the model only thinks; it only stops that state from being swallowed silently. According to the thread, the responder froze it as @argszero/dsh-silent-completion-guard. The same author also has @argszero/dsh-empty-response-retry, which covers the adjacent case where an unclassifiable provider error code swallows a transient failure.

DeepSeek Harness path 2: reclassify the empty stop in the adapter

This suits people who can change source and want the classification fixed at the root; both adapters need separate changes. Steps:

  1. Relax the pi-ai check from "no content blocks at all" to "no text block and no tool-call block". Expected: a reasoning-only stop no longer counts as success.
  2. Apply the same relaxation to the DeepSeek Messages path rather than changing one side. Expected: both provider paths behave the same.
  3. Keep max-tokens as its own classification and do not fold it into EMPTY_RESPONSE. Expected: budget exhaustion can still be surfaced to the user separately.
  4. Update the test that pins the old behavior to assert that an empty stop raises EMPTY_RESPONSE. Expected: test and implementation stay in sync and cannot regress silently.
  5. Run one long session and one short session. Expected: short sessions are unaffected and empty turns in long sessions enter retry.

Another upstream shape is a new outcome kind for "settled but no visible output" so the retry policy can see it. That is a design question, and the semantics are best aligned in the discussion before changing code.

DeepSeek Harness path 3: lower reasoning and context pressure

This reduces the trigger rate rather than fixing the classification, and suits deployments that cannot change code yet. Three things you can do:

  1. Lower the reasoning level or switch to a model that does not force thinking. Expected: fewer tokens spent thinking per turn and noticeably fewer empty bodies.
  2. Shorten individual sessions, splitting long tasks into new sessions or compacting first. Expected: less context pressure and a stronger tendency to emit visible text.
  3. State in the system prompt that the final answer must contain visible text. Expected: it reduces some "think then stop" cases but cannot guarantee it.

According to the thread, the reporter's session had reached 51 turns, 405 steps, and 48 percent context pressure, and empty turns recurred at that scale. Lowering reasoning makes them less frequent, but while the classification stays the same they remain possible.

The three paths compare as follows:

PathCode changeOutcome of an empty turnMain cost
silent-completion-guardnoretried or explicitly faileddepends on a third-party plugin
adapter reclassificationyesenters the default retrymust sync both provider paths
lower reasoningnoless likely, still possiblethinking quality may drop

If it happens occasionally, Path 3 is enough. If it reproduces reliably in long sessions, go to Path 1 and stop hoping a prompt will fix it.

DeepSeek Harness: why the UI says "analysis complete"

The frontend is not lying; it faithfully displays a step that ended with nothing to show. The mechanism:

  1. While the step runs, there is a live status line reading "analyzing request" with a shimmer animation.
  2. When the step group closes and contains only reasoning, the title becomes "analysis complete".
  3. The assistant node itself renders nothing, because the renderer requires streaming, interrupted, or a non-tool-call block; reasoning belongs to the process fold and does not count as body text.
  4. The empty bubble is therefore not a missing indicator but a missing terminal state that can express "settled without speaking".

To judge from the UI, do not look only at the bubble; expand the process area and confirm it contains only thinking blocks.

DeepSeek Harness: notes for handling empty turns

Read turn/end before deciding which layer to change; do not start by tuning model parameters. Four points:

  1. retryPolicy: 'always' does not cover these two terminal states; it is not a universal switch.
  2. The retry budget is counted per step, and the llmRetry projection is cleared at step/start and turn/end, so five retries apply to one step only.
  3. max-tokens is sticky for the whole turn; once set, later steps do not recover automatically.
  4. Keep this separate from the neighboring problem where a last step with only reasoning completes the turn. That belongs to turn-completion logic, not to the provider empty-response test.

Treating "thinking present, body absent" as an event that must fail explicitly is more reliable than adding another "please output the answer" line to the prompt.

Source: Discussion #9893, deepseek-ai/deepseek-harness. For the related context-pressure problem, see context window overflow.

FAQ

The DeepSeek Harness agent says it finished analyzing but the body is empty. Why is the turn successful?

DeepSeek Harness treats a stop with only reasoning as a normal completion. The thinking block itself occupies a content block, while the EMPTY_RESPONSE test requires zero blocks. A completion with thinking but no text is therefore mapped to an ordinary stop, and the agent loop sees no tool call and marks the turn completed.

Does retryPolicy always make DeepSeek Harness retry turns that only think?

DeepSeek Harness retryPolicy always does not cover these terminal states; the retry plugin hooks agent/request-error, which is only reached for error and aborted. A thinking-only stop is a normal completion and max-tokens is a turn outcome, so neither is a failure code. always only relaxes the attempt count for failed model requests; it does not reclassify these two terminal states.

How does silent-completion-guard turn a DeepSeek Harness empty stop into a retryable error?

DeepSeek Harness silent-completion-guard wraps the chunk stream returned by llm/stream and watches the terminal frame. If finish is stop and the turn produced neither a text block nor a tool-call block, it substitutes an error finish carrying EMPTY_RESPONSE. The downstream loop records the attempt as assistant/attempt, the default policy retries five times, and it reports a clear error when retries are exhausted.

Does lowering the model's reasoning level fix DeepSeek Harness thinking-only output for good?

DeepSeek Harness lowering the reasoning level only reduces the probability. It cuts the output budget and context pressure spent on thinking each turn, so empty bodies become less frequent, but the classification logic is unchanged and long sessions can still hit it. Stability requires classifying the empty stop as a failure in the adapter or a plugin.

Related Terms

EMPTY_RESPONSE
EMPTY_RESPONSE is one of the DSH retry error codes. It means the model ended normally without any content, and the default retry policy retries it five times with backoff from 500 milliseconds to 10 seconds.— https://github.com/deepseek-ai/deepseek-harness/discussions/9893
reasoning-only stop
A reasoning-only stop is a turn that ends after emitting thinking blocks with no text block and no tool-call block. The current adapter treats it as a normal completion rather than an empty response.— https://github.com/deepseek-ai/deepseek-harness/discussions/9893
turn/end
turn/end is the persistent event that marks the end of a turn, carrying turn and reason. reason.kind can be completed, aborted, blocked, error, max-tokens, or interrupted.— https://github.com/deepseek-ai/deepseek-harness/discussions/9893
max-tokens
max-tokens is a turn end reason meaning the output budget was exhausted. It is not a failure code, so the retry policy never sees it, and it is sticky for the whole turn.— https://github.com/deepseek-ai/deepseek-harness/discussions/9893

Sources