dsh-dsml-rescue 0.1.0 → 0.2.1
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 +44 -34
- package/README_zh.md +33 -27
- package/lib/index.d.ts +6 -2
- package/lib/index.js +64 -79
- package/locale/en.json +1 -1
- package/locale/zh.json +1 -1
- package/package.json +14 -14
package/README.md
CHANGED
|
@@ -5,15 +5,16 @@
|
|
|
5
5
|
<h1 align="center">dsh-dsml-rescue</h1>
|
|
6
6
|
|
|
7
7
|
<div align="center">
|
|
8
|
-
<p><strong>
|
|
9
|
-
<p><em
|
|
8
|
+
<p><strong>A stalled turn gets another one</strong></p>
|
|
9
|
+
<p><em>空转的轮次,替你续上</em></p>
|
|
10
10
|
|
|
11
11
|
<p>
|
|
12
|
-
<a href="https://github.com/deepseek-ai/deepseek-harness"><img src="https://img.shields.io/badge/powered_by-dsh-4D6BFE?style=flat&logo=deepseek&logoColor=white" alt="powered by dsh"></a>
|
|
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>
|
|
13
13
|
</p>
|
|
14
14
|
|
|
15
15
|
<p>
|
|
16
16
|
<a href="https://github.com/dsh-plugin-lab/dsh-dsml-rescue/actions/workflows/ci.yml"><img src="https://github.com/dsh-plugin-lab/dsh-dsml-rescue/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
|
|
17
|
+
<a href="https://www.npmjs.com/package/dsh-dsml-rescue"><img src="https://img.shields.io/npm/v/dsh-dsml-rescue.svg?style=flat" alt="npm"></a>
|
|
17
18
|
<a href="LICENSE"><img src="https://img.shields.io/badge/License-MIT-blue.svg?style=flat" alt="MIT License"></a>
|
|
18
19
|
</p>
|
|
19
20
|
|
|
@@ -28,51 +29,60 @@
|
|
|
28
29
|
|
|
29
30
|
---
|
|
30
31
|
|
|
31
|
-
|
|
32
|
-
instead of a structured call. The call never happens, the turn closes having produced nothing, and
|
|
33
|
-
the conversation waits for someone to type "continue". This plugin types it for you.
|
|
32
|
+
## Does this look familiar?
|
|
34
33
|
|
|
35
|
-
|
|
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:
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
</||DSML|| parameter>
|
|
39
|
+
</||DSML|| invoke>
|
|
40
|
+
</||DSML|| calls>
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
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".
|
|
36
46
|
|
|
37
|
-
|
|
38
|
-
- Sends one neutral sentence: take the next step, or close out with a summary.
|
|
39
|
-
- Waits for you — if you are already typing, it stays out of the way.
|
|
40
|
-
- Stops after a few attempts instead of spending requests forever.
|
|
41
|
-
- Covers child agents too, so a delegated run is not reported finished while it is stuck.
|
|
47
|
+
This plugin types it.
|
|
42
48
|
|
|
43
|
-
##
|
|
49
|
+
## What it does
|
|
44
50
|
|
|
45
|
-
|
|
46
|
-
|
|
51
|
+
- Continues a stalled turn: DSML markup at the end of the reasoning, and nothing else produced.
|
|
52
|
+
- Sends one short, neutral line — carry on, or wrap up if the work is done.
|
|
53
|
+
- Backs off while you are typing.
|
|
54
|
+
- Gives up after a few attempts instead of burning requests.
|
|
55
|
+
- Covers subagents too, so a delegated task is not falsely reported as completed while it is stuck.
|
|
56
|
+
|
|
57
|
+
## Install
|
|
47
58
|
|
|
48
59
|
```bash
|
|
49
60
|
dsh plugin --profile web add dsh-dsml-rescue
|
|
50
61
|
```
|
|
51
62
|
|
|
52
|
-
|
|
63
|
+
Then restart `dsh --profile web`. There is nothing to configure — the defaults are the intended
|
|
64
|
+
behaviour.
|
|
53
65
|
|
|
54
|
-
|
|
66
|
+
Working from source: clone, `pnpm install && pnpm run build`, then
|
|
55
67
|
`dsh plugin --profile web add "$PWD"`.
|
|
56
68
|
|
|
57
69
|
## What it will not do
|
|
58
70
|
|
|
59
|
-
- **It does not fix the cause.** The
|
|
60
|
-
|
|
61
|
-
- **It does not rescue
|
|
62
|
-
|
|
63
|
-
- **It does not touch your conversation.** Nothing is rewritten or
|
|
64
|
-
lets the host
|
|
65
|
-
|
|
66
|
-
##
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
[docs/ARCHITECTURE.md](docs/ARCHITECTURE.md).
|
|
75
|
-
What each check establishes, and a repeatable end-to-end run → [docs/VERIFICATION.md](docs/VERIFICATION.md).
|
|
71
|
+
- **It does not fix the cause.** The markup leaks below the model, out of reach of any prompt. What
|
|
72
|
+
it buys you is the wait, shortened.
|
|
73
|
+
- **It does not rescue other empty turns.** No DSML markup, no rescue: a cancellation, a failed
|
|
74
|
+
request, or a reply cut off at the output limit is left alone on purpose.
|
|
75
|
+
- **It does not touch your conversation.** Nothing is rewritten or replayed; it adds one message and
|
|
76
|
+
lets the host carry on.
|
|
77
|
+
|
|
78
|
+
## Going deeper
|
|
79
|
+
|
|
80
|
+
- Which three settings exist and what each one changes →
|
|
81
|
+
[docs/ARCHITECTURE.md](docs/ARCHITECTURE.md#configuration)
|
|
82
|
+
- How a stalled turn is recognised, and the rules that must not be broken →
|
|
83
|
+
[docs/ARCHITECTURE.md](docs/ARCHITECTURE.md)
|
|
84
|
+
- What the tests prove, and how to reproduce the whole thing locally →
|
|
85
|
+
[docs/VERIFICATION.md](docs/VERIFICATION.md)
|
|
76
86
|
|
|
77
87
|
## License
|
|
78
88
|
|
package/README_zh.md
CHANGED
|
@@ -5,15 +5,16 @@
|
|
|
5
5
|
<h1 align="center">dsh-dsml-rescue</h1>
|
|
6
6
|
|
|
7
7
|
<div align="center">
|
|
8
|
-
<p><strong
|
|
9
|
-
<p><em>
|
|
8
|
+
<p><strong>空转的轮次,替你续上</strong></p>
|
|
9
|
+
<p><em>A stalled turn gets another one</em></p>
|
|
10
10
|
|
|
11
11
|
<p>
|
|
12
|
-
<a href="https://github.com/deepseek-ai/deepseek-harness"><img src="https://img.shields.io/badge/powered_by-dsh-4D6BFE?style=flat&logo=deepseek&logoColor=white" alt="powered by dsh"></a>
|
|
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>
|
|
13
13
|
</p>
|
|
14
14
|
|
|
15
15
|
<p>
|
|
16
16
|
<a href="https://github.com/dsh-plugin-lab/dsh-dsml-rescue/actions/workflows/ci.yml"><img src="https://github.com/dsh-plugin-lab/dsh-dsml-rescue/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
|
|
17
|
+
<a href="https://www.npmjs.com/package/dsh-dsml-rescue"><img src="https://img.shields.io/npm/v/dsh-dsml-rescue.svg?style=flat" alt="npm"></a>
|
|
17
18
|
<a href="LICENSE"><img src="https://img.shields.io/badge/License-MIT-blue.svg?style=flat" alt="MIT License"></a>
|
|
18
19
|
</p>
|
|
19
20
|
|
|
@@ -28,44 +29,49 @@
|
|
|
28
29
|
|
|
29
30
|
---
|
|
30
31
|
|
|
31
|
-
|
|
32
|
+
## 这场景眼熟吗
|
|
32
33
|
|
|
33
|
-
|
|
34
|
+
你提了要求,模型推理了一会儿,然后这一轮就这么结束了——没有回复、没有工具调用,什么都没有。推理过程的最后几行是这样:
|
|
34
35
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
36
|
+
```
|
|
37
|
+
</||DSML|| parameter>
|
|
38
|
+
</||DSML|| invoke>
|
|
39
|
+
</||DSML|| calls>
|
|
40
|
+
```
|
|
40
41
|
|
|
41
|
-
|
|
42
|
+
DSML(DeepSeek Markup Language)是 DeepSeek 表示工具调用的协议标记。某些 provider 路由会把这段标记当成**普通推理文本**返回,而不是真的发出调用;于是调用没有发生,这一轮空着结束,所有人都在等一句「继续」。
|
|
42
43
|
|
|
43
|
-
|
|
44
|
+
这个插件替你把这句「继续」发出去。
|
|
45
|
+
|
|
46
|
+
## 它做什么
|
|
47
|
+
|
|
48
|
+
- 只救空转的轮次:推理过程末尾带着 DSML 标记,而且没有产出任何别的内容。
|
|
49
|
+
- 发一句很短、很中性的话:接着做,或者已经做完就收尾。
|
|
50
|
+
- 你正在打字时它让开。
|
|
51
|
+
- 连着几次没救回来就收手,不会一直烧请求。
|
|
52
|
+
- 子代理也管:被派出去的任务不会在卡住时被误报为已完成。
|
|
53
|
+
|
|
54
|
+
## 安装
|
|
44
55
|
|
|
45
56
|
```bash
|
|
46
57
|
dsh plugin --profile web add dsh-dsml-rescue
|
|
47
58
|
```
|
|
48
59
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
从源码装:`git clone` 后 `pnpm install && pnpm run build`,再 `dsh plugin --profile web add "$PWD"`。
|
|
52
|
-
|
|
53
|
-
## 它不做什么
|
|
60
|
+
然后重启 `dsh --profile web`。不需要配置,默认值就是预期行为。
|
|
54
61
|
|
|
55
|
-
|
|
56
|
-
- **不救其他空转。** 没有标记就不救——用户主动取消、请求失败、以及输出被长度截断后停在原地的回合,都不碰。
|
|
57
|
-
- **不动你的对话。** 不改写、不重发;只多加一条消息,然后交给宿主继续跑。
|
|
62
|
+
从源码装:先 clone,`pnpm install && pnpm run build`,再 `dsh plugin --profile web add "$PWD"`。
|
|
58
63
|
|
|
59
|
-
##
|
|
64
|
+
## 它不会做什么
|
|
60
65
|
|
|
61
|
-
|
|
62
|
-
|
|
66
|
+
- **不修根因。** 标记泄漏发生在模型之下,提示词够不着。它换来的只有那段等待被缩短。
|
|
67
|
+
- **不救别的空转轮次。** 没有 DSML 标记就不出手:你主动取消、请求失败、回复被输出上限截断,这些都刻意不碰。
|
|
68
|
+
- **不动你的对话。** 不改写、不重放历史,只多加一条消息,剩下的交给宿主。
|
|
63
69
|
|
|
64
|
-
##
|
|
70
|
+
## 想了解更多
|
|
65
71
|
|
|
66
|
-
|
|
67
|
-
[docs/ARCHITECTURE.md](docs/ARCHITECTURE.md)
|
|
68
|
-
|
|
72
|
+
- 有哪三个配置项、各自会改变什么 → [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md#configuration)
|
|
73
|
+
- 怎么认定一个空转轮次、哪些规则不能破 → [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md)
|
|
74
|
+
- 测试能证明什么、怎么在本机完整复现 → [docs/VERIFICATION.md](docs/VERIFICATION.md)
|
|
69
75
|
|
|
70
76
|
## 许可
|
|
71
77
|
|
package/lib/index.d.ts
CHANGED
|
@@ -4,6 +4,10 @@ import { Context } from "@deepseek-ai/cordis";
|
|
|
4
4
|
declare const name = "dsh-dsml-rescue";
|
|
5
5
|
/** The agent registry, needed to map a session back to its live agent. */
|
|
6
6
|
declare const inject: string[];
|
|
7
|
+
/** Rescues allowed in a row before an agent is left alone. */
|
|
8
|
+
declare const DEFAULT_MAX_CONSECUTIVE = 3;
|
|
9
|
+
/** Whether child agents are rescued by default. */
|
|
10
|
+
declare const DEFAULT_APPLY_TO_SUBAGENTS = true;
|
|
7
11
|
/** Validated configuration; the defaults are the documented behaviour. */
|
|
8
12
|
interface Config {
|
|
9
13
|
/** Whether the plugin acts at all. Turn it off without uninstalling. */
|
|
@@ -23,7 +27,7 @@ declare const Config: z<Config>;
|
|
|
23
27
|
/**
|
|
24
28
|
* Mount the plugin.
|
|
25
29
|
* @param ctx - host context.
|
|
26
|
-
* @param config - validated configuration.
|
|
30
|
+
* @param config - validated configuration; the schema fills in every default.
|
|
27
31
|
*/
|
|
28
32
|
declare function apply(ctx: Context, config: Config): void;
|
|
29
|
-
export { Config, apply, inject, name };
|
|
33
|
+
export { Config, DEFAULT_APPLY_TO_SUBAGENTS, DEFAULT_MAX_CONSECUTIVE, apply, inject, name };
|
package/lib/index.js
CHANGED
|
@@ -60,7 +60,7 @@ function hasLeakedProtocolTail(reasoning) {
|
|
|
60
60
|
/**
|
|
61
61
|
* The one judgment this plugin makes: does a finished turn deserve a rescue?
|
|
62
62
|
*
|
|
63
|
-
*
|
|
63
|
+
* It deserves one when the turn ended normally yet produced nothing the loop or
|
|
64
64
|
* the user can act on, and its reasoning ends in leaked protocol markup. Both
|
|
65
65
|
* halves are required — a turn that produced nothing for another reason stays
|
|
66
66
|
* untouched, because rescuing it would paper over an unrelated failure.
|
|
@@ -70,21 +70,20 @@ function hasLeakedProtocolTail(reasoning) {
|
|
|
70
70
|
*
|
|
71
71
|
* @module dsh-dsml-rescue/detect
|
|
72
72
|
*/
|
|
73
|
-
/** Whether a text block carries anything other than whitespace. */
|
|
74
|
-
function carriesText(block) {
|
|
75
|
-
return block.type === "text" && block.text !== void 0 && /\S/.test(block.text);
|
|
76
|
-
}
|
|
77
73
|
/**
|
|
78
|
-
* Whether
|
|
74
|
+
* Whether one block produced something a caller could act on.
|
|
79
75
|
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
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
|
|
82
79
|
* output than to have produced nothing, and a false rescue costs a request.
|
|
83
|
-
* @param
|
|
84
|
-
* @returns whether
|
|
80
|
+
* @param block - one content block of the closing message.
|
|
81
|
+
* @returns whether the block is visible text, a tool call, or an unknown kind.
|
|
85
82
|
*/
|
|
86
|
-
function
|
|
87
|
-
|
|
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);
|
|
88
87
|
}
|
|
89
88
|
/**
|
|
90
89
|
* Concatenate every reasoning block of a message.
|
|
@@ -92,7 +91,7 @@ function hasActionableContent(blocks) {
|
|
|
92
91
|
* @returns the joined reasoning text, or the empty string when there is none.
|
|
93
92
|
*/
|
|
94
93
|
function joinReasoning(blocks) {
|
|
95
|
-
return blocks.
|
|
94
|
+
return blocks.filter((block) => block.type === "reasoning" && block.text !== void 0).map((block) => block.text ?? "").join("\n");
|
|
96
95
|
}
|
|
97
96
|
/**
|
|
98
97
|
* Decide whether one finished turn is a leaking stall.
|
|
@@ -102,7 +101,7 @@ function joinReasoning(blocks) {
|
|
|
102
101
|
*/
|
|
103
102
|
function isRescueCandidate(observation) {
|
|
104
103
|
if (!observation.completed) return false;
|
|
105
|
-
if (
|
|
104
|
+
if (observation.blocks.some(isActionable)) return false;
|
|
106
105
|
return hasLeakedProtocolTail(joinReasoning(observation.blocks));
|
|
107
106
|
}
|
|
108
107
|
// src/policy.ts
|
|
@@ -147,8 +146,33 @@ function evaluate(input) {
|
|
|
147
146
|
const INITIAL_STATE = {
|
|
148
147
|
consecutive: 0,
|
|
149
148
|
turn: void 0,
|
|
150
|
-
blocks: []
|
|
149
|
+
blocks: [],
|
|
150
|
+
outputTokens: void 0
|
|
151
151
|
};
|
|
152
|
+
/** Nothing to do, with the state unchanged. */
|
|
153
|
+
function nothing(state) {
|
|
154
|
+
return {
|
|
155
|
+
state,
|
|
156
|
+
decision: "ignore",
|
|
157
|
+
round: 0
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Whether the step being closed spent its whole output budget.
|
|
162
|
+
*
|
|
163
|
+
* Reads as exhausted only when both numbers are known: an unknown ceiling or a
|
|
164
|
+
* missing count reports "not exhausted" on purpose, because a wrong `true`
|
|
165
|
+
* would silence the rescue this plugin exists for, while a wrong `false` only
|
|
166
|
+
* costs the streak one attempt.
|
|
167
|
+
* @param state - state carrying the closing message's output tokens.
|
|
168
|
+
* @param ceiling - the ceiling the request was sent under, when known.
|
|
169
|
+
* @returns whether the model was cut off rather than finished.
|
|
170
|
+
*/
|
|
171
|
+
function spentBudget(state, ceiling) {
|
|
172
|
+
if (state.outputTokens === void 0) return false;
|
|
173
|
+
if (ceiling === void 0 || ceiling <= 0) return false;
|
|
174
|
+
return state.outputTokens >= ceiling;
|
|
175
|
+
}
|
|
152
176
|
/**
|
|
153
177
|
* Advance one agent's machine by one event.
|
|
154
178
|
*
|
|
@@ -162,41 +186,25 @@ const INITIAL_STATE = {
|
|
|
162
186
|
*/
|
|
163
187
|
function reduce(state, event, limits) {
|
|
164
188
|
switch (event.kind) {
|
|
165
|
-
case "assistant-message": return {
|
|
166
|
-
state
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
decision: "ignore",
|
|
180
|
-
round: 0
|
|
181
|
-
};
|
|
182
|
-
case "turn-end": return event.completed ? {
|
|
183
|
-
state,
|
|
184
|
-
decision: "ignore",
|
|
185
|
-
round: 0
|
|
186
|
-
} : {
|
|
187
|
-
state: {
|
|
188
|
-
...state,
|
|
189
|
-
consecutive: 0
|
|
190
|
-
},
|
|
191
|
-
decision: "ignore",
|
|
192
|
-
round: 0
|
|
193
|
-
};
|
|
189
|
+
case "assistant-message": return nothing({
|
|
190
|
+
...state,
|
|
191
|
+
turn: event.turn,
|
|
192
|
+
blocks: event.blocks,
|
|
193
|
+
outputTokens: event.outputTokens
|
|
194
|
+
});
|
|
195
|
+
case "human-message": return nothing({
|
|
196
|
+
...state,
|
|
197
|
+
consecutive: 0
|
|
198
|
+
});
|
|
199
|
+
case "turn-end": return event.completed ? nothing(state) : nothing({
|
|
200
|
+
...state,
|
|
201
|
+
consecutive: 0
|
|
202
|
+
});
|
|
194
203
|
case "turn-stopping": {
|
|
195
|
-
const blocks = state.turn === event.turn ? state.blocks : [];
|
|
196
204
|
const outcome = evaluate({
|
|
197
|
-
candidate:
|
|
205
|
+
candidate: isRescueCandidate({
|
|
198
206
|
completed: true,
|
|
199
|
-
blocks
|
|
207
|
+
blocks: state.turn === event.turn && !spentBudget(state, event.outputCeiling) ? state.blocks : []
|
|
200
208
|
}),
|
|
201
209
|
consecutive: state.consecutive,
|
|
202
210
|
maxConsecutive: limits.maxConsecutive,
|
|
@@ -253,38 +261,12 @@ function humanPending(agent) {
|
|
|
253
261
|
return agent.inbox.nextTurn.some((message) => message.source.kind === "user") || agent.inbox.nextStep.some((message) => message.source.kind === "user");
|
|
254
262
|
}
|
|
255
263
|
/**
|
|
256
|
-
* Whether the step being closed spent its whole output budget.
|
|
257
|
-
*
|
|
258
|
-
* The seam carries no stop reason, so this compares the closing message's
|
|
259
|
-
* output-token count with the ceiling the request was sent under. Both numbers
|
|
260
|
-
* are the caller's own accounting — the count is what the provider reported and
|
|
261
|
-
* the ceiling is what this session last requested — so equality means the model
|
|
262
|
-
* was cut off, not that it chose to stop.
|
|
263
|
-
*
|
|
264
|
-
* The comparison is deliberately one-sided: an unknown ceiling or a missing
|
|
265
|
-
* count reports "not exhausted", because a wrong `true` would silence the
|
|
266
|
-
* rescue this plugin exists for, while a wrong `false` only costs the rescue
|
|
267
|
-
* one attempt of the streak.
|
|
268
|
-
* @param agent - the agent whose turn is closing.
|
|
269
|
-
* @param turn - the turn being closed.
|
|
270
|
-
* @param usage - the closing message's token accounting, when the host logged it.
|
|
271
|
-
* @returns whether the output budget is spent.
|
|
272
|
-
*/
|
|
273
|
-
function outputExhausted(agent, usage) {
|
|
274
|
-
if (usage === void 0) return false;
|
|
275
|
-
const ceiling = agent.session.requestHeader()?.config.maxTokens;
|
|
276
|
-
if (ceiling === void 0 || ceiling <= 0) return false;
|
|
277
|
-
return usage.outputTokens >= ceiling;
|
|
278
|
-
}
|
|
279
|
-
/**
|
|
280
264
|
* Install the turn watcher and its continuation side effect.
|
|
281
265
|
* @param ctx - plugin context; listeners are disposed with it.
|
|
282
266
|
* @param config - validated limits.
|
|
283
267
|
*/
|
|
284
268
|
function install(ctx, config) {
|
|
285
269
|
const states = /* @__PURE__ */ new Map();
|
|
286
|
-
/** Output accounting of the last assistant message, per agent. */
|
|
287
|
-
const usageByAgent = /* @__PURE__ */ new Map();
|
|
288
270
|
/** Apply one reducer event and store the result. */
|
|
289
271
|
function advance(agent, event) {
|
|
290
272
|
const step = reduce(states.get(agent) ?? INITIAL_STATE, event, { maxConsecutive: config.maxConsecutive });
|
|
@@ -303,9 +285,9 @@ function install(ctx, config) {
|
|
|
303
285
|
advance(agent, {
|
|
304
286
|
kind: "assistant-message",
|
|
305
287
|
turn: event.data.turn,
|
|
306
|
-
blocks: event.data.message.content
|
|
288
|
+
blocks: event.data.message.content,
|
|
289
|
+
outputTokens: event.data.usage?.outputTokens
|
|
307
290
|
});
|
|
308
|
-
usageByAgent.set(agent, event.data.usage);
|
|
309
291
|
return;
|
|
310
292
|
case "user/message":
|
|
311
293
|
if (event.data.source.kind === "user") advance(agent, { kind: "human-message" });
|
|
@@ -326,7 +308,7 @@ function install(ctx, config) {
|
|
|
326
308
|
kind: "turn-stopping",
|
|
327
309
|
turn,
|
|
328
310
|
humanPending: humanPending(agent),
|
|
329
|
-
|
|
311
|
+
outputCeiling: agent.session.requestHeader()?.config.maxTokens
|
|
330
312
|
});
|
|
331
313
|
if (step.decision === "give-up") {
|
|
332
314
|
ctx.logger.warn(`dsml-rescue: agent "${agent.id}" stalled ${config.maxConsecutive} times in a row; stopped continuing it`);
|
|
@@ -345,7 +327,6 @@ function install(ctx, config) {
|
|
|
345
327
|
});
|
|
346
328
|
ctx.on("agent/disposed", ({ agent }) => {
|
|
347
329
|
states.delete(agent);
|
|
348
|
-
usageByAgent.delete(agent);
|
|
349
330
|
});
|
|
350
331
|
}
|
|
351
332
|
// src/index.ts
|
|
@@ -353,6 +334,10 @@ function install(ctx, config) {
|
|
|
353
334
|
const name = "dsh-dsml-rescue";
|
|
354
335
|
/** The agent registry, needed to map a session back to its live agent. */
|
|
355
336
|
const inject = ["agents"];
|
|
337
|
+
/** Rescues allowed in a row before an agent is left alone. */
|
|
338
|
+
const DEFAULT_MAX_CONSECUTIVE = 3;
|
|
339
|
+
/** Whether child agents are rescued by default. */
|
|
340
|
+
const DEFAULT_APPLY_TO_SUBAGENTS = true;
|
|
356
341
|
const Config = z.object({
|
|
357
342
|
enabled: z.boolean().default(true),
|
|
358
343
|
maxConsecutive: z.natural().min(1).default(3),
|
|
@@ -361,7 +346,7 @@ const Config = z.object({
|
|
|
361
346
|
/**
|
|
362
347
|
* Mount the plugin.
|
|
363
348
|
* @param ctx - host context.
|
|
364
|
-
* @param config - validated configuration.
|
|
349
|
+
* @param config - validated configuration; the schema fills in every default.
|
|
365
350
|
*/
|
|
366
351
|
function apply(ctx, config) {
|
|
367
352
|
if (config.enabled === false) return;
|
|
@@ -370,4 +355,4 @@ function apply(ctx, config) {
|
|
|
370
355
|
applyToSubagents: config.applyToSubagents ?? true
|
|
371
356
|
});
|
|
372
357
|
}
|
|
373
|
-
export { Config, apply, inject, name };
|
|
358
|
+
export { Config, DEFAULT_APPLY_TO_SUBAGENTS, DEFAULT_MAX_CONSECUTIVE, apply, inject, name };
|
package/locale/en.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"meta": {
|
|
3
3
|
"title": "DSML Rescue",
|
|
4
|
-
"description": "
|
|
4
|
+
"description": "Auto-continues turns stalled by DSML tool-call markup leaking into reasoning."
|
|
5
5
|
}
|
|
6
6
|
}
|
package/locale/zh.json
CHANGED
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-dsml-rescue",
|
|
3
|
-
"version": "0.1
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.2.1",
|
|
4
|
+
"description": "Auto-continues turns stalled by DSML tool-call markup leaking into reasoning.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"dsh",
|
|
7
7
|
"deepseek-harness",
|
|
@@ -50,17 +50,6 @@
|
|
|
50
50
|
"patch": "./cordis.patch.yml"
|
|
51
51
|
}
|
|
52
52
|
},
|
|
53
|
-
"scripts": {
|
|
54
|
-
"build": "tsdown",
|
|
55
|
-
"typecheck": "tsc -p tsconfig.json",
|
|
56
|
-
"test": "vitest run",
|
|
57
|
-
"test:watch": "vitest",
|
|
58
|
-
"lint": "eslint .",
|
|
59
|
-
"format": "prettier --write .",
|
|
60
|
-
"format:check": "prettier --check .",
|
|
61
|
-
"check:release": "node scripts/check-release.mjs",
|
|
62
|
-
"check": "pnpm run typecheck && pnpm run build && pnpm run test && pnpm run check:release"
|
|
63
|
-
},
|
|
64
53
|
"peerDependencies": {
|
|
65
54
|
"@deepseek-ai/cordis": "^4.0.4",
|
|
66
55
|
"@deepseek-ai/dsh-llm": "^0.1.7-rc.1 || ^0.2.0-rc.1 || ^0.2.1-rc.1",
|
|
@@ -83,5 +72,16 @@
|
|
|
83
72
|
"typescript": "^5.9.3",
|
|
84
73
|
"typescript-eslint": "^8.70.1",
|
|
85
74
|
"vitest": "^4.1.11"
|
|
75
|
+
},
|
|
76
|
+
"scripts": {
|
|
77
|
+
"build": "tsdown",
|
|
78
|
+
"typecheck": "tsc -p tsconfig.json",
|
|
79
|
+
"test": "vitest run",
|
|
80
|
+
"test:watch": "vitest",
|
|
81
|
+
"lint": "eslint .",
|
|
82
|
+
"format": "prettier --write .",
|
|
83
|
+
"format:check": "prettier --check .",
|
|
84
|
+
"check:release": "node scripts/check-release.mjs",
|
|
85
|
+
"check": "pnpm run typecheck && pnpm run build && pnpm run test && pnpm run check:release"
|
|
86
86
|
}
|
|
87
|
-
}
|
|
87
|
+
}
|