Repository navigation
Description
Summary
In Code Mode, the model intermittently writes its tool call as plain
assistant text instead of emitting a structured tool call. The text uses a
DSML-style dialect — inner parameters and closers are marked |DSML|
(DeepSeek Model Spec Language) while the opening tag often is not:
<parameter name="read">
<|DSML| parameter name="path" string="true">/root/zorq-sigma-bridge/app/api/catalog.py</|DSML| parameter>
<|DSML| parameter name="oldString" string="true">placeholder</|DSML| parameter>
</|DSML| invoke>
</|DSML| calls>
OpenCode does not parse or execute that text: no tool part is created, the
step finishes with reason stop and the session goes idle, and the intended
tool call is silently lost. The run then stalls and only proceeds when another
turn is issued.
Environment
- opencode version: 2.0.32 (latest release, Oct 16 2026)
- OS: Linux 6.18.54.1-microsoft-standard-WSL2 x86_64 GNU/Linux
- Agent:
build, Code Mode - Provider/model:
opencode/big-pickle, variantdefault - Plugins: none (no
opencode.json/.opencode/opencode.json)
Reproduction
- Run a long multi-step
buildtask in Code Mode. - Observe the last assistant message of a step. Instead of a structured tool
call it contains prose followed by raw call markup, and notoolpart.
Concrete transcript from session ses_ea9cd281cffe9pJbrBsyPQSpup (read-only
query against ~/.local/share/opencode/opencode.db):
10:50:33 assistant [text, TOOL:grep:completed, TOOL:shell:completed] <- real calls, fine
10:50:36 assistant [text*MK*] <- only text, NO tool part
10:50:39 idle <- step ends
10:52:03 user <- new turn needed to continue
The text*MK* message body looks like:
Let me look at `get_deployed_stems` and `_refresh_deployed_flag` — this is the core of the bug.
<parameter name="path">/root/zorq-sigma-bridge/app/…
The read it asks for never runs in that step; the same read is repeated and
executed only in the following turn.
finish reason confirms the root cause described in #43385
Every assistant message whose text part carries the markup finishes the step
with reason stop, never tool-calls:
10:40:14 MK finish=stop rawFinish=stop tokens_out=102
10:50:36 MK finish=stop rawFinish=stop tokens_out=117
10:52:10 MK finish=stop rawFinish=stop tokens_out=94
10:53:24 MK finish=stop rawFinish=stop tokens_out=225
while ordinary messages in the same session that make real calls report:
01:05:35 NORMAL finish=tool-calls rawFinish=tool_calls tokens_out=70
01:05:41 NORMAL finish=tool-calls rawFinish=tool_calls tokens_out=80
Aggregate over the three affected sessions (assistant messages whose text part
contains call markup):
{ "stop": 47, "tool-calls": 14 }
The 14 tool-calls cases are mixed messages where a structured call was made
and markup was written alongside it — those steps continue normally and the
markup is only noise. The 47 stop cases are the ones that halt the loop,
because no structured tool call was emitted.
This is exactly the failure mode analyzed in #43385: the call arrives as
text-delta, no tool-call event is produced, step-finish reports stop,
and the agent loop terminates mid-task.
The dialect is DSML, and often self-inconsistent
Counting only assistant text parts in the three affected sessions:
plain "<parameter name=" opens ......... 43
"|DSML|"-marked parts/closers ......... 30
"<invoke" .............................. 9
"antml:" ............................... 0
antml: never appears in model output for these sessions — the dialect is
DSML-shaped, not Anthropic's. And it is frequently malformed: 29 of 43
markup messages close with </|DSML| invoke> / </|DSML| calls> while the
opening <parameter name="…"> carries no |DSML| marker (see the sample in
the Summary), so even a naive text parser would not be able to read them
directly.
Normal structured calls in the same sessions are unaffected — e.g. shell
2046×, read 685×, edit 508× completed — which is why this reads as a
dialect/normalization problem rather than a fault in any individual tool.
Expected Behavior
The intended tool call should not be dropped. Either:
- parse the text-form call and execute it (a text fallback parser), or
- immediately re-prompt the model to emit a real tool call instead of ending
the step and leaving the session idle,
and in all cases surface why the step ended.
Actual Behavior
No tool part is created, the step ends with idle, the call is lost, and the
session needs an extra turn to move on — effectively stalling long agent runs.
Occurrences (text parts containing call markup, excluding transcripts where the
agent itself quotes the markup while discussing it):
| session | text-parts with markup | window |
|---|---|---|
ses_eaea51780ffex7kzsnKf1s1lak |
7 | 19 Oct 23:02 |
ses_eaa3947b1ffeexwKdX3g4RKT3t (fork of the above) |
36 | 19 Oct 23:02 → 20 Oct 01:04 |
ses_ea9cd281cffe9pJbrBsyPQSpup (new session) |
6 | 20 Oct 10:40 → 10:54 |
9 other sessions on the same provider/model/version never produced it, so it
is intermittent and session-dependent.
Additional Context
- Prompt-level workarounds do not help. A repo
AGENTS.mdrule was added
that forbids emitting this markup (initially listing the forbidden tokens as
examples, then rewritten to avoid mentioning them at all). The rules were
removed from the instructions before the last reproduction: session
ses_ea9cd281…was created at 01:05 on 20 Oct, after the instruction files
were already clean, and still reproduced at 10:40–10:54. Its recorded
instruction blob contains no markup tokens. - Not a skills/plugins issue. All installed skill files were scanned for
<invoke/<parameter name=/antml:/function_calls— no matches;
no plugins are configured. - The markup always appears in the final assistant message of a step,
immediately before theidlemessage, and is interleaved with normal
structuredtoolcalls earlier in the same step. - A separate issue in the same environment is a recurring
AI.Error: Rate limit exceeded(provider.quota,FreeUsageLimitError) on
the free tier; it is independent of the markup problem and is not
contributing to the reports above.
Prior art — #43385 already identified the cause and a fix
#43385 ("MiMo V2.5 outputs tml:function_calls as plain text instead of
structured tool calls") reports the same failure mode and contains a root-cause
analysis that matches this environment one-to-one. It was closed only because it
was filed against OpenCode 1.x, with an explicit invitation to open a new issue
for 2.0:
OpenCode 2.0 is now out and development is focused on V2, so we're closing
issues filed against OpenCode 1. […] if this is still relevant on 2.0,
reopen this issue or open a new one with your 2.0 version. — @thdxr
Root cause as described there (still true on 2.0.32):
- the runtime only consumes OpenAI-style structured
tool_calls
(delta.tool_calls[].function.{name,arguments}); - there is no text-based fallback parser, so the call arrives as
text-delta, notool-callevent is ever emitted,step-finishreports
stop, and the agent loop stops.
Code paths named there:
packages/llm/src/protocols/utils/tool-stream.ts— parses only structured SSEtool_callspackages/opencode/src/session/processor.ts— handles onlytool-call/tool-resulteventspackages/opencode/src/session/llm/ai-sdk.ts—toLLMEvents()forwards only structured tool calls
Suggested fix (reproposed for 2.0): when a step finishes with reason stop but
the accumulated text contains call-shaped markup (<invoke name=…>,
<parameter name=…>, |DSML|-marked parameters, <function_calls>, …), parse
that text and emit synthetic tool-call events so execution continues — or at
minimum re-prompt the model for a real tool call instead of ending the step.
Note the opener/closer markers can disagree (see the DSML section above), so the
parser needs to tolerate unbalanced markers rather than requiring an exact
document shape.
Related
- MiMo V2.5 outputs tml:function_calls as plain text instead of structured tool calls #43385 — same bug on 1.x; closed for version only, re-file invited for 2.0 (see above)
- Raw <tool_calls> markup in reasoning breaks tool calls on several models #25644 — raw
<tool_calls>markup in reasoning breaks tool calls (stale-closed) - Empty bash tool arguments {} can still be submitted and interrupt the session #27871 — a different stuck-session bug: a
pendingtool part withinput: {}left behind when the stream dies betweentool-input-startandtool-call(stale-closed; notoolpart exists in this report)
Activity
opencode-agent commentedon Oct 20, 2026
Thanks for the extra detail, @untitledds! I think this is a duplicate of #57344, which you opened earlier today. It describes the same problem: in Code Mode on 2.0.32,
opencode/big-picklewrites<invoke>/<parameter>markup as plain text, no tool part is created and the step ends idle. It cites the same sessions, and it is already open and triaged.The new evidence here (every markup-only message ending with
finish=stop/rawFinish=stop, the{ stop: 47, tool-calls: 14 }totals, and the pointers to #43385) is useful. It would be best added as a comment on #57344 so everything stays in one place.Possible duplicates and related issues, ordered by likelihood:
The bot will close this issue as a duplicate in 1 day unless you reply explaining how it differs from #57344.
opencode-agent commentedon Oct 21, 2026
Closing this as a duplicate of #57344, since nobody has replied to say how it differs. Both issues describe the same problem: in Code Mode on 2.0.32,
opencode/big-picklewrites tool-call markup as plain text, no tool part is created and the step ends idle.Please add the extra evidence from this issue to #57344 so it's all in one place. That means the
finish=stop/rawFinish=stopbreakdown, the{ stop: 47, tool-calls: 14 }totals and the #43385 pointers. If this turns out to be a different problem after all, reply here and we'll take another look.Thanks for the thorough investigation, @untitledds!