Repository navigation
Replies: 1 comment
|
Both cited lines are real on
:99 if (message.content.length === 0) {
:104 code: EMPTY_RESPONSE_CODE,
:550 if (toolCalls.length === 0) return { kind: 'completed' }The architectural point. That matters for where to patch. Fixing One more thing to name. The Your workaround also confirms the seam, which is worth a sentence in the report: |
Uh oh!
There was an error while loading. Please reload this page.
Turn silently ends as "completed" when the final step contains only reasoning blocks
Repo: deepseek-ai/deepseek-harness (reported via GitHub Discussions, as CONTRIBUTING.md asks)
Packages:
@deepseek-ai/dsh-agent-loop0.2.1-rc.2 (packages/core/agent-loop),@deepseek-ai/dsh-llm-pi-ai0.2.1-rc.2 (packages/llm/llm-pi-ai)App: DeepSeek Harness Desktop (
@deepseek-ai/dsh-desktop0.2.1-rc.2), macOS arm64Summary
When a model step finishes with
stopand its content contains onlyreasoningblocks (no text, no tool call), the agent loop records the turn ascompletedand goes idle. The user sees the agent "stop by itself" mid-task, with an empty final answer, and has to send another message to make it continue.Typical trigger: a reasoning model that "announces" its next action inside its reasoning ("Let me apply the edit.") and ends the response without emitting the tool call. Observed in real sessions with
opencode-go/deepseek-v4.1-flash(reasoningEffortmax) via an OpenAI-Responses-compatible provider; in two of those cases the reasoning text even ends with DeepSeek's raw DSML tool-call markup (</||DSML|| parameter>), i.e. the tool call leaked into reasoning instead of being emitted as a function call.Root cause
dsh-llm-pi-aimapStopReasononly treats astopwith zero content blocks asEMPTY_RESPONSE. A message with reasoning blocks only passes as a normal{ kind: "stop" }:dsh-agent-loopstep handling then ends the turn because there are no tool calls:So "no user-visible output and no action" is indistinguishable from "model finished the task".
Evidence from local sessions
Scanned all local sessions (
~/.dsh/sessions/**/session.v4.jsonl.zstd):turn/endwithreason.kind = completed; 3 of them end on an assistant message whose content is[reasoning]only, withfinish.reason = stop/stopReason: "stop".Reproduction (deterministic)
fault-proxy.mjs(attached) is a small proxy in front of any OpenAI-Responses endpoint. For the first/v1/responsesrequest it strips everymessage/function_callitem and theoutput_text/content_partevents from the SSE stream, keeping only the reasoning item. All later requests are forwarded unchanged.node fault-proxy.mjs(listens on 127.0.0.1:4011, forwards to 127.0.0.1:4000).dsh-llm-pi-aiprovider (api: openai-responses) athttp://127.0.0.1:4011/v1, using a reasoning model.dsh --profile <headless-profile> --json "Create file a.txt containing 1, then report done."Actual: 1 step,
turn_endreasoncompleted,{"type":"final","text":""}, anda.txtis not created.Expected: the loop treats an action-less, text-less
stopas an incomplete step and continues, or surfaces it as a retryable error.Workaround that works (local plugin)
A small plugin listening on
agent/turn-stopping: if the last assistant message of the turn has notool-calland no non-emptytext, it callsagent.steer(...)with a continue reminder (source.kind: "stall-guard",form: "notice"), at most 2 times per turn. With the same fault proxy, the run continues to step 5, createsa.txt, and returns a proper final answer.Suggested fixes (any one of these)
mapStopReason, treatstopwith notextand notool-callblocks (reasoning only) likeEMPTY_RESPONSE, so the existing retry policy applies.completedonly if the last message has visible text. A reasoning-only stop should trigger one bounded "continue" step, or a distinct turn-end reason so the UI can show "model stopped without answering" with a Continue button.fault-proxy.txt
All reactions