DeepSeek Harness thinking-only output: empty stop succeeds
"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:
- A completion with only reasoning.
llm-pi-aiatstream.ts:96-99checks onlymessage.content.length === 0insidecase 'stop', andllm-deepseekattranslate.ts:149checks onlyblocks.size === 0. - Thinking blocks count toward content. pi-ai's
thinking_start,thinking_delta, andthinking_endshare thecontentIndexcounter with text, and DeepSeek'scontent_block_startputs every block, including reasoning, intoblocks. - 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 returnscompleted. - The other case is
stop_reason: 'length', where finish becomesmax-tokensand the retry plugin never sees it, because the plugin hooks onlyagent/request-error, which is open only toerrorandaborted.
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:
- Open the failing session log and find the
turn/endfor that turn. Expected: a{ turn, reason }object. - If
reason.kind === 'max-tokens', classify it as budget exhaustion. Expected: the lastassistant/messageis usually truncated. - If
reason.kind === 'completed', inspect the content blocks of the lastassistant/message. Expected: only reasoning blocks with no text and no tool call means the first case. - Search for
llm/retryandllm/retry-startedevents. 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:
- Understand what the plugin does: it wraps the chunk stream returned by
llm/streamand watches the terminal finish. Expected: the model request is unchanged; only the last frame of the response stream is rewritten. - When finish is
stopand the turn has neither a text block nor a tool-call block, replace it with an error finish carryingEMPTY_RESPONSE. Expected: the loop recordsassistant/attemptinstead of appending an empty assistant message. - 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.
- 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:
- 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.
- Apply the same relaxation to the DeepSeek Messages path rather than changing one side. Expected: both provider paths behave the same.
- Keep
max-tokensas its own classification and do not fold it intoEMPTY_RESPONSE. Expected: budget exhaustion can still be surfaced to the user separately. - 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. - 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:
- 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.
- Shorten individual sessions, splitting long tasks into new sessions or compacting first. Expected: less context pressure and a stronger tendency to emit visible text.
- 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:
| Path | Code change | Outcome of an empty turn | Main cost |
|---|---|---|---|
| silent-completion-guard | no | retried or explicitly failed | depends on a third-party plugin |
| adapter reclassification | yes | enters the default retry | must sync both provider paths |
| lower reasoning | no | less likely, still possible | thinking 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:
- While the step runs, there is a live status line reading "analyzing request" with a shimmer animation.
- When the step group closes and contains only reasoning, the title becomes "analysis complete".
- 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.
- 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:
retryPolicy: 'always'does not cover these two terminal states; it is not a universal switch.- The retry budget is counted per step, and the
llmRetryprojection is cleared atstep/startandturn/end, so five retries apply to one step only. max-tokensis sticky for the whole turn; once set, later steps do not recover automatically.- 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
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.
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.
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.
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
- deepseek-harness Discussion #9893: the agent repeatedly finishes thinking with no output in long sessions· deepseek-ai (GitHub Discussions)
- deepseek-ai/deepseek-harness official repository· GitHub