Skip to content

Studio Code: Continue a turn whose tool call leaked into the reply as DSML markup - #5346

Merged
youknowriad merged 3 commits into
trunkfrom
continue-turn-after-leaked-tool-call-markup
Oct 23, 2026
Merged

youknowriad merged 3 commits into
trunkfrom
continue-turn-after-leaked-tool-call-markup

Conversation

@youknowriad

@youknowriad youknowriad commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

Related issues

  • Reported in #studio: on 2.1.0-dev38, a fast turn showed </|DSML|parameter> </|DSML|invoke> </|DSML|tool_calls> as reply text and then the agent stopped. The same kind of stop showed up repeatedly during a support rotation.
  • Builds on Studio Code: Continue a turn that ends with an empty reply #5205, which continues turns that end with an empty reply.

How AI was used in this PR

Indent (Claude) traced the report to the runtime and wrote the first version; Claude Code then slimmed it down to reuse #5205's continue. Both ran the tests, typecheck and lint, and Claude Code checked it end to end against the real proxy with a fault-injecting proxy (below). No evals were run.

Proposed Changes

The fast tier runs on DeepSeek, which writes tool calls in its own DSML markup. The upstream that serves it usually turns that markup into structured tool calls, but sometimes part of it comes back as reply text instead. The tool never runs, the user sees raw <|DSML|…> tags, and since the reply has text and no tool call, the agent just stops.

With this change, leaked markup is removed from the final reply, so it never shows in the transcript, the saved session or the next request. When the reply lost its only tool call, the text around the leaked call goes too: the reply is then empty, and the agent picks the turn back up the same way it already does after an empty reply (#5205), from its own reasoning. A reply that still has a real tool call is only cleaned, since the turn keeps going anyway.

The call isn't rebuilt from the leaked text: in the reported case only the closing tags came through, so the tool name and arguments were already gone.

Trade-offs: the raw tags can still flash while the reply streams, since only the final message is cleaned, and the short lead-in a model writes before a tool call ("Let me check the file.") is dropped along with the leaked call. The real fix belongs in the upstream's tool parser; this keeps sessions going until then, including on providers we don't control.

Testing Instructions

  1. npx vitest run apps/cli/ai/runtimes/pi/tests/leaked-tool-call-markup.test.ts
  2. End to end, with a fault-injecting proxy: point WPCOM_AI_PROXY_BASE_URL at a local server that forwards to the wpcom AI proxy, but rewrites one streamed reply so its tool call comes back as DSML text (the call's deltas removed, the tags added as content, finish_reason set to stop or left at tool_calls). Then run a fast turn that needs two tool calls through apps/cli/dist/cli/eval-runner.mjs. Expected: the session JSONL has an empty assistant reply, then a studio.empty_stop_continue custom message, then the same tool call made for real, and no |DSML| anywhere. Six runs passed this way: the leak on the first or the second call, closing tags only or a full call, and both finish reasons.

Pre-merge Checklist

  • Have you checked for TypeScript, React or other console errors?

🤖 Generated with Claude Code

youknowriad and others added 2 commits October 23, 2026 19:33
…nue instead of a second path

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…as a tool call

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@youknowriad
youknowriad merged commit d42e339 into trunk Oct 23, 2026
13 of 14 checks passed
@youknowriad
youknowriad deleted the continue-turn-after-leaked-tool-call-markup branch October 23, 2026 20:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant