dsh-dsml-rescue 0.3.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -5,8 +5,8 @@
5
5
  <h1 align="center">dsh-dsml-rescue</h1>
6
6
 
7
7
  <div align="center">
8
- <p><strong>A stalled turn gets another one</strong></p>
9
- <p><em>空转的轮次,替你续上</em></p>
8
+ <p><strong>A turn stalled by leaked DSML markup gets another one</strong></p>
9
+ <p><em>DSML 泄漏导致的空转,替你续上</em></p>
10
10
 
11
11
  <p>
12
12
  <a href="https://github.com/deepseek-ai/deepseek-harness"><img src="https://img.shields.io/badge/powered_by-dsh-4D6BFE?style=flat-square&logo=deepseek&logoColor=white" alt="powered by dsh"></a>
@@ -32,7 +32,7 @@
32
32
  ## Does this look familiar?
33
33
 
34
34
  You asked for something. The model reasoned for a while. Then the turn simply ended — no answer, no
35
- tool call, nothing. The reasoning output ended with lines like these:
35
+ tool call, nothing. What it produced ended with lines like these:
36
36
 
37
37
  ```
38
38
  </||DSML|| parameter>
@@ -40,16 +40,21 @@ tool call, nothing. The reasoning output ended with lines like these:
40
40
  </||DSML|| calls>
41
41
  ```
42
42
 
43
+ <p align="center">
44
+ <img src="assets/dsml-leak-rescue.png" width="900" alt="The web UI after a stalled turn: what the model produced ends with leaked DSML markup, then the turn stops with no answer">
45
+ </p>
46
+
43
47
  DSML (DeepSeek Markup Language) is how DeepSeek formats a tool call. Some provider routes hand that
44
- markup back as **ordinary reasoning text** instead of an actual call, so the call never happens, the
45
- turn closes with no output, and everything waits for someone to type "continue".
48
+ markup back as ordinary message content instead of an actual call — in the reasoning, or in the
49
+ visible reply. Either way the call never happens, the turn closes with no output, and everything
50
+ waits for someone to type "continue".
46
51
 
47
52
  This plugin types it.
48
53
 
49
54
  ## What it does
50
55
 
51
- - Continues a stalled turn: DSML markup at the end of the reasoning, and nothing else produced.
52
- Neither half alone is enough — an empty turn without the markup is a different failure.
56
+ - Continues a stalled turn: DSML markup where the message ends, and no tool call. Both halves are
57
+ needed — an empty turn without the markup is a different failure.
53
58
  - Sends one short, neutral line: take the next step — never an offer to declare the work finished.
54
59
  - Backs off while you are typing.
55
60
  - Gives up after a few attempts instead of burning requests.
package/README_zh.md CHANGED
@@ -5,8 +5,8 @@
5
5
  <h1 align="center">dsh-dsml-rescue</h1>
6
6
 
7
7
  <div align="center">
8
- <p><strong>空转的轮次,替你续上</strong></p>
9
- <p><em>A stalled turn gets another one</em></p>
8
+ <p><strong>DSML 泄漏导致的空转,替你续上</strong></p>
9
+ <p><em>A turn stalled by leaked DSML markup gets another one</em></p>
10
10
 
11
11
  <p>
12
12
  <a href="https://github.com/deepseek-ai/deepseek-harness"><img src="https://img.shields.io/badge/powered_by-dsh-4D6BFE?style=flat-square&logo=deepseek&logoColor=white" alt="powered by dsh"></a>
@@ -31,7 +31,7 @@
31
31
 
32
32
  ## 这场景眼熟吗
33
33
 
34
- 你提了要求,模型推理了一会儿,然后这一轮就这么结束了——没有回复、没有工具调用,什么都没有。推理过程的最后几行是这样:
34
+ 你提了要求,模型推理了一会儿,然后这一轮就这么结束了——没有回复、没有工具调用,什么都没有。它产出的内容,最后几行是这样:
35
35
 
36
36
  ```
37
37
  </||DSML|| parameter>
@@ -39,13 +39,17 @@
39
39
  </||DSML|| calls>
40
40
  ```
41
41
 
42
- DSML(DeepSeek Markup Language)是 DeepSeek 表示工具调用的协议标记。某些 provider 路由会把这段标记当成**普通推理文本**返回,而不是真的发出调用;于是调用没有发生,这一轮空着结束,所有人都在等一句「继续」。
42
+ <p align="center">
43
+ <img src="assets/dsml-leak-rescue.png" width="900" alt="网页界面里的一次空转:模型产出的内容末尾是泄漏的 DSML 标记,这一轮就此停下,没有回答">
44
+ </p>
45
+
46
+ DSML(DeepSeek Markup Language)是 DeepSeek 表示工具调用的协议标记。某些 provider 路由会把这段标记当成普通消息内容返回——可能落在推理里,也可能落在正文里——而不是真的发出调用;于是调用没有发生,这一轮空着结束,所有人都在等一句「继续」。
43
47
 
44
48
  这个插件替你把这句「继续」发出去。
45
49
 
46
50
  ## 它做什么
47
51
 
48
- - 只救空转的轮次:推理过程末尾带着 DSML 标记,而且没有产出任何别的内容。两个条件缺一不可——空转但末尾没有标记,是另一种失败。
52
+ - 救空转的轮次:标记落在消息末尾,而且没有工具调用。两个条件缺一不可——空转但没有标记,是另一种失败。
49
53
  - 发一句很短、很中性的话:接着做下一步,不给「已经做完了」这种台阶。
50
54
  - 你正在打字时它让开。
51
55
  - 连着几次没救回来就收手,不会一直烧请求。
package/lib/index.js CHANGED
@@ -2,13 +2,14 @@ import z from "@deepseek-ai/schemastery";
2
2
  import { boundContextSummary, createUserMessage } from "@deepseek-ai/dsh-llm";
3
3
  // src/protocol.ts
4
4
  /**
5
- * Protocol-mark detection over one reasoning stream.
5
+ * Protocol-mark detection over one message's content.
6
6
  *
7
7
  * Some provider routes deliver the model's tool-call dialect as ordinary
8
- * reasoning text instead of a structured tool call. The dialect's closing
9
- * tokens then sit at the very end of the thinking stream, and the turn ends
10
- * having done nothing. This module answers one question about that text: does
11
- * it end with such a token? It knows nothing about turns, agents, or sessions.
8
+ * content instead of a structured tool call. The dialect's closing tokens then
9
+ * sit at the very end of the message, and the turn ends having done nothing.
10
+ * This module answers one question about that text: does it end with such a
11
+ * token? It knows nothing about turns, agents, or sessions, and nothing about
12
+ * which content kind the route chose to put it in.
12
13
  *
13
14
  * @module dsh-dsml-rescue/protocol
14
15
  */
@@ -19,11 +20,32 @@ import { boundContextSummary, createUserMessage } from "@deepseek-ai/dsh-llm";
19
20
  */
20
21
  const MARKER = /\|\|\s*dsml\s*\|\|/i;
21
22
  /**
22
- * How many trailing non-blank lines are considered. A leak puts its closing
23
- * tokens at the end; scanning further back starts matching prose *about* the
24
- * dialect, including examples a model quotes while explaining it.
23
+ * Remove fenced code blocks, keeping a fence opener's line prefix.
24
+ *
25
+ * A fence is quotation: a model that transcribes the call it meant to make puts
26
+ * the transcription inside one, and that transcription is not what the message
27
+ * says. Fences can open mid-line, so this cuts at the delimiter rather than at
28
+ * the start of the line.
29
+ * @param text - one message's content.
30
+ * @returns the same text with fenced spans blanked out.
25
31
  */
26
- const TAIL_LINES = 3;
32
+ function withoutFences(text) {
33
+ const kept = [];
34
+ let fenced = false;
35
+ for (const line of text.split(/\r?\n/)) {
36
+ let rest = line;
37
+ let visible = "";
38
+ for (;;) {
39
+ const at = rest.search(/```|~~~/);
40
+ if (at < 0) break;
41
+ if (!fenced) visible += rest.slice(0, at);
42
+ rest = rest.slice(at + 3);
43
+ fenced = !fenced;
44
+ }
45
+ kept.push(fenced ? visible : visible + rest);
46
+ }
47
+ return kept.join("\n");
48
+ }
27
49
  /**
28
50
  * A line that is protocol markup rather than a sentence.
29
51
  *
@@ -37,22 +59,23 @@ function isMarkupLine(line) {
37
59
  return line.startsWith("<") && MARKER.test(line);
38
60
  }
39
61
  /**
40
- * Whether a reasoning stream ends with leaked protocol markup.
62
+ * Whether a message's content ends with leaked protocol markup.
41
63
  *
42
- * Empty or absent reasoning is not a leak: a turn that produced nothing at all
43
- * is a different failure and stays outside this plugin's scope.
44
- * @param reasoning - the complete reasoning text of one assistant message.
45
- * @returns whether any of its last few non-blank lines is protocol markup.
64
+ * Only the last non-blank line is read. A leak is what the message stops on, so
65
+ * anything said after the markup is output, and a turn that produced output is
66
+ * not a stall. Fences are read per stream: a fence cannot span two blocks, and
67
+ * carrying one block's fence state into the next would flip the reading of a
68
+ * block that never opened one.
69
+ * @param streams - one text per text-bearing block, in stream order.
70
+ * @returns whether the last non-blank line of the content is protocol markup.
46
71
  */
47
- function hasLeakedProtocolTail(reasoning) {
48
- if (reasoning === "") return false;
49
- const lines = reasoning.normalize("NFKC").split(/\r?\n/);
50
- let scanned = 0;
51
- for (let index = lines.length - 1; index >= 0 && scanned < TAIL_LINES; index -= 1) {
72
+ function hasLeakedProtocolTail(streams) {
73
+ const lines = [];
74
+ for (const stream of streams) for (const line of withoutFences(stream).normalize("NFKC").split(/\r?\n/)) lines.push(line);
75
+ for (let index = lines.length - 1; index >= 0; index -= 1) {
52
76
  const line = (lines[index] ?? "").trim();
53
77
  if (line === "") continue;
54
- scanned += 1;
55
- if (isMarkupLine(line)) return true;
78
+ return isMarkupLine(line);
56
79
  }
57
80
  return false;
58
81
  }
@@ -60,10 +83,10 @@ function hasLeakedProtocolTail(reasoning) {
60
83
  /**
61
84
  * The one judgment this plugin makes: does a finished turn deserve a rescue?
62
85
  *
63
- * It deserves one when the turn ended normally yet produced nothing the loop or
64
- * the user can act on, and its reasoning ends in leaked protocol markup. Both
65
- * halves are required — a turn that produced nothing for another reason stays
66
- * untouched, because rescuing it would paper over an unrelated failure.
86
+ * It deserves one when the turn ended normally and its closing message ends in
87
+ * leaked protocol markup. Reasoning and visible text are both content: which of
88
+ * them the route put the markup in says nothing about whether the work happened,
89
+ * and the message's last line says everything.
67
90
  *
68
91
  * Pure: no host types, no events, no clock. The caller adapts real session
69
92
  * events into {@link TurnObservation}.
@@ -71,38 +94,34 @@ function hasLeakedProtocolTail(reasoning) {
71
94
  * @module dsh-dsml-rescue/detect
72
95
  */
73
96
  /**
74
- * Whether one block produced something a caller could act on.
97
+ * Whether a block carries message content rather than an effect.
75
98
  *
76
- * Reasoning never counts — that is where the leak lives, not where the work
77
- * lands. An unknown block type does count, on purpose: a provider that adds a
78
- * content kind this plugin has never seen is far likelier to have produced
79
- * output than to have produced nothing, and a false rescue costs a request.
99
+ * Everything else — a tool call, or a content kind this plugin has never seen —
100
+ * is output the turn produced, so it ends the question.
80
101
  * @param block - one content block of the closing message.
81
- * @returns whether the block is visible text, a tool call, or an unknown kind.
102
+ * @returns whether the block is reasoning or visible text.
82
103
  */
83
- function isActionable(block) {
84
- if (block.type === "reasoning") return false;
85
- if (block.type !== "text") return true;
86
- return block.text !== void 0 && /\S/.test(block.text);
104
+ function isTextBearing(block) {
105
+ return block.type === "reasoning" || block.type === "text";
87
106
  }
88
107
  /**
89
- * Concatenate every reasoning block of a message.
108
+ * The text of every text-bearing block, in stream order.
90
109
  * @param blocks - the turn's closing content blocks.
91
- * @returns the joined reasoning text, or the empty string when there is none.
110
+ * @returns one entry per block that carries text, empty entries included.
92
111
  */
93
- function joinReasoning(blocks) {
94
- return blocks.filter((block) => block.type === "reasoning" && block.text !== void 0).map((block) => block.text ?? "").join("\n");
112
+ function streams(blocks) {
113
+ return blocks.filter((block) => isTextBearing(block) && block.text !== void 0).map((block) => block.text ?? "");
95
114
  }
96
115
  /**
97
116
  * Decide whether one finished turn is a leaking stall.
98
117
  * @param observation - the turn's end reason and its closing message blocks.
99
- * @returns true only when the turn completed, produced nothing actionable, and
100
- * its reasoning ends in protocol markup.
118
+ * @returns true only when the turn completed, produced no non-text block, and
119
+ * its content ends in protocol markup.
101
120
  */
102
121
  function isRescueCandidate(observation) {
103
122
  if (!observation.completed) return false;
104
- if (observation.blocks.some(isActionable)) return false;
105
- return hasLeakedProtocolTail(joinReasoning(observation.blocks));
123
+ if (observation.blocks.some((block) => !isTextBearing(block))) return false;
124
+ return hasLeakedProtocolTail(streams(observation.blocks));
106
125
  }
107
126
  // src/policy.ts
108
127
  /**
@@ -241,9 +260,9 @@ function reduce(state, event, limits) {
241
260
  *
242
261
  * The rescued turn ended with a tool call that never landed, so the next step
243
262
  * has not happened yet; a "say you are done" clause has nothing true to be right
244
- * about. Taking one is not free either: a prose answer is actionable output, so
245
- * detection stops calling the turn a stall and the run ends with the work
246
- * undone.
263
+ * about. Taking one is not free either: a prose answer is what detection reads as
264
+ * the message's end, so the turn stops counting as a stall and the run ends with
265
+ * the work undone.
247
266
  */
248
267
  const CONTINUE_PROMPT = "Continue with the task. Take the next concrete step now, using a tool if needed.";
249
268
  /**
package/locale/en.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "meta": {
3
3
  "title": "DSML Rescue",
4
- "description": "Auto-continues turns stalled by DSML tool-call markup leaking into reasoning."
4
+ "description": "Auto-continues turns stalled by DSML tool-call markup, wherever the route leaks it."
5
5
  }
6
6
  }
package/locale/zh.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "meta": {
3
3
  "title": "DSML 续跑",
4
- "description": "当 DSML 工具调用标记泄漏进推理过程导致回合停滞时,自动续跑。"
4
+ "description": "当 DSML 工具调用标记泄漏在消息末尾(推理或正文皆可)导致回合停滞时,自动续跑。"
5
5
  }
6
6
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dsh-dsml-rescue",
3
- "version": "0.3.1",
4
- "description": "Auto-continues turns stalled by DSML tool-call markup leaking into reasoning.",
3
+ "version": "0.4.0",
4
+ "description": "Auto-continues turns stalled by DSML tool-call markup, wherever the route leaks it.",
5
5
  "keywords": [
6
6
  "dsh",
7
7
  "deepseek-harness",