@vergilye/dsh-auto-continue 0.0.0-stage → 2.0.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/LICENSE +21 -0
- package/README.md +210 -2
- package/cordis.patch.yml +32 -0
- package/icon.svg +9 -0
- package/index.js +1503 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +54 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 VergilYe
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,211 @@
|
|
|
1
|
-
#
|
|
1
|
+
# dsh-auto-continue v2
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
重写版。目标只有一个:**轮次不能在「话没说完」或「只写了下一步计划」的时候关闭。**
|
|
4
|
+
|
|
5
|
+
## 为什么重写
|
|
6
|
+
|
|
7
|
+
v1 能用,但踩了六个坑,v2 把它们写成设计约束:
|
|
8
|
+
|
|
9
|
+
1. **括号收尾标记会诱发退化**。v1 用 `[DONE: …]`,结果模型(就是我们自己)在长回合末尾反复输出 `]]]`。v2 用 fenced block,块内一行清单,没有需要配对的括号。
|
|
10
|
+
2. **`systemPrompt.section()` 返回清理函数,必须注册**。v1 忽略了它,每次插件重载都往在线会话里追加一条系统提示词节点(实测累积到 8 条)。v2 用 `ctx.effect` 注册,重载后始终只有一条。
|
|
11
|
+
3. **服务键是 `systemPrompt`,不是 `system-prompt`**。后者是事件名。v1 写错,导致协议段从未注入,日志里只有一条 `no systemPrompt service is mounted`,而且**不报错**。
|
|
12
|
+
4. **不能把开局 prompt 当成人类接管**。它会出现在每个 turn 的窗口里,若用它清零计数,`maxNudges` 与指纹去重会双双失效。
|
|
13
|
+
5. **`maxNudges` 必须从持久日志推导**。取本 turn 里已有几条 `source.kind = auto-continue` 的 user/message,这样多实例、重载、重启都封得住。
|
|
14
|
+
6. **损坏流的清理不能 await**。上游卡死时,`await iterator.return()` 会把重试一起卡住。v2 改成事后触发、不等待。
|
|
15
|
+
|
|
16
|
+
## 三层防护
|
|
17
|
+
|
|
18
|
+
### 1. 请求层:`llm/stream` guard
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
adapter 流 → guardStream → 逐 chunk 透传(UI 照常实时输出)
|
|
22
|
+
├─ streamIdleMs 内无 chunk → error finish (STREAM_STALL)
|
|
23
|
+
├─ 上游没 finish 就断 → error finish (STREAM_TRUNCATED)
|
|
24
|
+
└─ finish=stop 但正文未收尾 → error finish (STREAM_TRUNCATED)
|
|
25
|
+
↓ loop 判为本次尝试失败 → agent/request-error
|
|
26
|
+
↓ 本插件返回 {kind:'retry'} → 同一步内重发
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
- 默认对所有路由生效(`allowModels` 为空数组);配了白名单则只对名单内路由生效。跳过 `purpose` 为标题/压缩的辅助请求。
|
|
30
|
+
- 截断改判每会话最多 `maxGuardRetries` 次;一条干净收尾的流会把额度还回来。
|
|
31
|
+
- 每个 `(agent,turn,step)` 的 guard 重试也封顶,用尽后交给正常重试策略。
|
|
32
|
+
- 判定前先剥离协议块,否则每一条健康的 ```` ```done ```` 收尾都会被当成「没说完」而无限重试。
|
|
33
|
+
|
|
34
|
+
### 2. 协议层:收尾声明(默认开)
|
|
35
|
+
|
|
36
|
+
插件用 `SystemPrompt.section()` 注入一段要求(order 9500,**不改全局提示词**,卸载即撤):
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
```done
|
|
40
|
+
✅ 已完成:<一句话结果摘要>
|
|
41
|
+
```
|
|
42
|
+
或
|
|
43
|
+
```ask
|
|
44
|
+
❓ 待确认:<需要用户拍板的具体问题>
|
|
45
|
+
```
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
判定是确定性的:
|
|
49
|
+
|
|
50
|
+
| 收尾 | 动作 |
|
|
51
|
+
|---|---|
|
|
52
|
+
| `done` 且摘要非空,**且块是消息的最后一样东西** | 放行(`protocol-done`) |
|
|
53
|
+
| `ask` 且问题非空,**且块是消息的最后一样东西** | 放行(`protocol-ask`) |
|
|
54
|
+
| 无块 / 摘要为空 | 推一次(`protocol-missing` / `protocol-empty`) |
|
|
55
|
+
| **正文完全为空**(一个字都没有) | 推一次(`protocol-empty-text`) |
|
|
56
|
+
| **块后面还在说话**(块不在末尾) | 推一次(`protocol-missing`) |
|
|
57
|
+
|
|
58
|
+
**没有例外**:只要缺声明就推,不看本回合有没有工具调用、是不是纯聊天、句子有没有收住。协议要求每条回复都声明收尾,所以「看起来像聊天」不构成放行理由。
|
|
59
|
+
|
|
60
|
+
**「块必须最后」是结构性判据**:消息最后一个非空行必须是闭合围栏,且该块就是收尾块。模型出现过这样的形态——先写 ```` ```done ````,紧接着又补「等等,我还没做完,不能给 done」继续干活;只认块、不看位置就会把未完成的回合关掉。判据因此收紧为「块不在末尾 = 没声明」。已知取舍:块后若残留纯语法垃圾(多余围栏、泄漏的工具调用标签),也会被判未声明而多推一次。
|
|
61
|
+
|
|
62
|
+
标记样式可切:`fence-check`(默认,块内清单)/ `fence`(块内纯文本)/ `line`(`DONE:` 行首)/ `plain`(`[DONE: …]`,仅兼容用)。
|
|
63
|
+
|
|
64
|
+
### 3. 兜底层:三个启发式域(协议关闭时才用)
|
|
65
|
+
|
|
66
|
+
| 域 | 判据 |
|
|
67
|
+
|---|---|
|
|
68
|
+
| `retryScope` | 本 turn 有 `llm/retry`,且 `failure.code ∈ retryCodes`(默认 TRANSPORT/TIMEOUT/SERVER) |
|
|
69
|
+
| `truncationScope` | 末句未收住(`:,、(` 或无标点结尾) |
|
|
70
|
+
| `extendedScope` | 收尾区(最后两个非空行)命中计划短语 |
|
|
71
|
+
|
|
72
|
+
共同闸门:最后一条是**纯文本 `stop`**、且**不是在问用户**。
|
|
73
|
+
|
|
74
|
+
## 配置
|
|
75
|
+
|
|
76
|
+
| 字段 | 默认 | 含义 |
|
|
77
|
+
|---|---|---|
|
|
78
|
+
| `enabled` | `true` | 关掉后不注册任何监听器 |
|
|
79
|
+
| `maxNudges` | `2` | **连续无效**推送上限(推完有推进的不计、并清零);`0` 等同未安装 |
|
|
80
|
+
| `nudgeMinIntervalMs` | `5000` | 两次推送的最小间隔,防止一次短暂断连就把次数烧光 |
|
|
81
|
+
| `delayMs` | `0` | 额外的推送前等待,期间 abort 就放弃 |
|
|
82
|
+
| `dryRun` | `false` | 只记日志不推 |
|
|
83
|
+
| `completionProtocol` | `true` | 收尾协议 |
|
|
84
|
+
| `markerStyle` | `fence-check` | 标记样式 |
|
|
85
|
+
| `streamGuard` | `true` | 请求层防护 |
|
|
86
|
+
| `streamIdleMs` | `90000` | 流内静默多久判卡死(本机适配器自身超时 300s) |
|
|
87
|
+
| `maxGuardRetries` | `1` | 截断改判上限 / 每 step guard 重试上限 |
|
|
88
|
+
| `retryScope` / `truncationScope` / `extendedScope` | `true` | 兜底三域 |
|
|
89
|
+
| `retryCodes` | TRANSPORT/TIMEOUT/SERVER | 算「供应商侧故障」的码 |
|
|
90
|
+
| `allowModels` | `[]` | 生效路由;默认空数组 = 全部路由,收窄时填 `provider/model` 列表 |
|
|
91
|
+
| `planPatterns` / `isAskingPatterns` | 见代码 | 正则源串,非法项被忽略 |
|
|
92
|
+
| `nudgeText` / `protocolNudgeText` / `giveUpNoticeText` | 见代码 | 推送正文 / 放弃提示正文 |
|
|
93
|
+
| `logDecisions` / `logFile` | `false` / `$DSH_HOME/dsh-auto-continue/events.jsonl` | 诊断日志 |
|
|
94
|
+
|
|
95
|
+
## 安全阀
|
|
96
|
+
|
|
97
|
+
1. 额度只记「连续无效推送」:推完模型确实干了活(有工具调用)就不算消耗,也把之前的欠账清零;连续 `maxNudges` 次推了没反应才停。计数从会话日志的持久记录推导。
|
|
98
|
+
2. **没有总次数上限**:一个持续在推进的 turn 可以被无限制地重新打开(日志里的 `attempts` 只用于观测,不参与任何门限)。只有「连续无效」会累积,所以病态循环(模型反复停但什么也不干)仍会在 `maxNudges` 次后停下。
|
|
99
|
+
3. 两次推送至少间隔 `nudgeMinIntervalMs`(默认 5 秒),一次瞬时断连不会连着烧掉额度。
|
|
100
|
+
4. **放弃时会在对话里留一条可见提示**(`giveUpNoticeText`),说明自动续接已停止、可能是上游断连或配额问题,每个 turn 只留一次;同时写一条 `event: giveup` 日志。
|
|
101
|
+
5. 同 turn 同末条消息指纹只推一次。
|
|
102
|
+
6. `goal.phase === 'active'` 时整个跳过(不与 goal round driver 抢)。
|
|
103
|
+
7. 中途人类接管(首条 assistant 之后的人类消息)清零计数、指纹与放弃标记;**开局 prompt 不算**。
|
|
104
|
+
8. `signal.aborted` / `agent.status !== 'running'` / 非白名单路由不推。
|
|
105
|
+
9. 监听器内所有异常吞掉并记日志,绝不冒泡到主循环。
|
|
106
|
+
10. 截断改判与 guard 重试都有上限,`maxGuardRetries` 用尽后放行。
|
|
107
|
+
|
|
108
|
+
## 验证
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
cd plugins/dsh-auto-continue
|
|
112
|
+
npm test # = node --test "test/*.test.mjs"
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
当前:**63 tests / 63 pass**。覆盖配置解析、标记解析(四种样式,含「块不在末尾不算声明」与其语法残留取舍)、剥离、`isSettled`、决策正反例(缺声明必推 / 正文为空必推 / 协议关闭时维持原判 / 默认全路由 vs 显式白名单)、`foldTurn` 记账(含“有推进的推送不算欠账”)、`stateFor` 清零、`apply` 装配(含 section 清理函数注册)、监听器安全(注入抛错、放弃提示只发一次且留 `giveup` 事件、有推进的推送不吃额度、长回合无总次数上限)、以及 stream guard 的透传/截断/卡死/无 finish/上游抛错/预算耗尽/辅助请求放行与白名单收窄。
|
|
116
|
+
|
|
117
|
+
## 已知限制
|
|
118
|
+
|
|
119
|
+
- **只治「说得出来」的问题**:网关静默切断、模型自述计划、重连后停半截都能覆盖;真的网络断连仍由 DSH 自己的退避重试处理,SSE 保活做不到(在适配器内部)。
|
|
120
|
+
- **协议依赖模型合规**:漏写标记会被推一次(有上限)。合规率无法事前保证,只能靠日志观测。
|
|
121
|
+
- **不记录 `stopReason` 的路由不可判**(如 `deepseek-account/deepseek-flash`):判据要求 `stop`,取不到就静默跳过。
|
|
122
|
+
- **协议层依赖宿主提供 `systemPrompt` 服务**:某些上下文里该服务不存在,协议段会静默缺失(日志留一条 `no system-prompt service is mounted`),此时只剩请求层 guard 与兜底三域。
|
|
123
|
+
- **依赖 `Session.eventAt` / `snapshotEvents`**:两者已被标 `@deprecated`,未来移除会让窗口读取返回 `undefined`(只记 skip,不推,不会崩)。
|
|
124
|
+
|
|
125
|
+
## 安装 / 卸载
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
git clone https://github.com/VergilYe/dsh-auto-continue.git
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
然后在 DSH 里:
|
|
132
|
+
|
|
133
|
+
```
|
|
134
|
+
plugin_manager action: install_bundle target: <clone 出来的目录>
|
|
135
|
+
plugin_manager action: remove_bundle target: @vergilye/dsh-auto-continue
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
也可以直接从 npm 安装(推荐,市场里的一键安装走的也是这条):
|
|
139
|
+
|
|
140
|
+
```
|
|
141
|
+
dsh plugin --profile web add @vergilye/dsh-auto-continue
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
或者从 GitHub 安装(profile bundle 走 bundle patch,不需要构建):
|
|
145
|
+
|
|
146
|
+
```
|
|
147
|
+
dsh plugin add git+https://github.com/VergilYe/dsh-auto-continue.git
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
零依赖、无安装脚本、无构建步骤。不读凭据、不联网、无遥测;只写
|
|
151
|
+
`$DSH_HOME/dsh-auto-continue/events.jsonl`(诊断日志,含末段文本片段与 sessionId,
|
|
152
|
+
只留在本机;不想留可把 `logDecisions` 设为 `false`)。
|
|
153
|
+
|
|
154
|
+
### 下载即用
|
|
155
|
+
|
|
156
|
+
`allowModels` 默认为**空数组 = 所有路由**,所以装完不需要任何配置,你自己的模型直接就是
|
|
157
|
+
生效范围。三层防护(stream guard / 收尾协议 / 兜底三域)会一起对这个 profile 的所有请求
|
|
158
|
+
生效,与具体的 provider/model 无关。
|
|
159
|
+
|
|
160
|
+
### 只想对某些模型生效(可选)
|
|
161
|
+
|
|
162
|
+
如果不想让别的模型也被这套行为管,在 **profile 自己的补丁层**里按行 id 覆盖即可:
|
|
163
|
+
|
|
164
|
+
`~/.dsh/profiles/<profile>/cordis.patch.yml`
|
|
165
|
+
|
|
166
|
+
```yaml
|
|
167
|
+
- id: auto-continue
|
|
168
|
+
name: "@vergilye/dsh-auto-continue"
|
|
169
|
+
config:
|
|
170
|
+
allowModels:
|
|
171
|
+
- open-design/deepseek-v4.1-flash
|
|
172
|
+
- zai-coding-cn/glm-5.3
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
- 行 id 是 bundle 补丁里的 `auto-continue`(`plugin_manager` 报出的 `include:auto-continue`
|
|
176
|
+
是宿主内层 id,写补丁时用前者)。
|
|
177
|
+
- 这个补丁层「在 bundle 层之后应用」,所以能覆盖插件自带配置,插件升级也不会被冲掉。
|
|
178
|
+
- 想查实际路由字符串:看 `$DSH_HOME/dsh-auto-continue/events.jsonl` 里的 `route` 字段。
|
|
179
|
+
- 空数组或省略该键 = 全部路由(默认)。
|
|
180
|
+
|
|
181
|
+
同一段 YAML 也可以直接交给你的 AI agent 去写——它有文件读写权限即可完成,不需要额外工具。
|
|
182
|
+
|
|
183
|
+
### 在插件市场里找到它
|
|
184
|
+
|
|
185
|
+
DSH 的「市场」读取社区索引
|
|
186
|
+
[`dsh-plugin-catalog`](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin)。
|
|
187
|
+
本插件的收录条目已准备好([`market/VergilYe__dsh-auto-continue.yml`](market/VergilYe__dsh-auto-continue.yml)),
|
|
188
|
+
投稿步骤与 CI 要求见 [PUBLISHING.md](PUBLISHING.md)。
|
|
189
|
+
收录后可在 **设置 → 插件 → 市场** 直接搜索 `auto continue` 一键安装。
|
|
190
|
+
|
|
191
|
+
## 它解决什么问题(背景)
|
|
192
|
+
|
|
193
|
+
模型请求被网关静默切断时,客户端拿到的仍是**合法**的 `stopReason: "stop"`——没有错误、
|
|
194
|
+
没有重试、没有异常。DSH 于是认为这一轮正常结束,用户看到的却是一句「接下来我要……」就
|
|
195
|
+
没了下文,只能手动输入「继续」。
|
|
196
|
+
|
|
197
|
+
这类问题靠「更好的错误处理」解决不了,因为**没有错误**。三种形态要各自对付:
|
|
198
|
+
|
|
199
|
+
| 形态 | 现象 | 手段 |
|
|
200
|
+
|---|---|---|
|
|
201
|
+
| A 重连变体 | 本 turn 有 `llm/retry`,之后只叙述计划 | `retryScope` |
|
|
202
|
+
| B 静默截断 | 无错误、`stop=stop`,但正文停在冒号/半句 | `llm/stream` guard + `truncationScope` |
|
|
203
|
+
| C 自述计划 | 语法完整,把下一步计划当结论 | 收尾协议 + `extendedScope` |
|
|
204
|
+
|
|
205
|
+
用「用户手动输入『继续』」当标签在本机语料上标定:**只认重连的口径召回约 1.4%**,
|
|
206
|
+
三形态 + 协议才能覆盖大部分真实的「停半截」。
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
## License
|
|
210
|
+
|
|
211
|
+
MIT
|
package/cordis.patch.yml
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# dsh-auto-continue v2 — keep a turn from closing on an unfinished reply.
|
|
2
|
+
#
|
|
3
|
+
# Three behaviours, all optional:
|
|
4
|
+
# 1. streamGuard — on `llm/stream`, a silent stall or a mid-thought stop becomes a
|
|
5
|
+
# retryable error, so DSH's own retry re-sends inside the same step.
|
|
6
|
+
# 2. completionProtocol — injects one system-prompt section asking for an explicit
|
|
7
|
+
# end-of-reply fenced block (```done / ```ask). Declared ⇒ close; missing after
|
|
8
|
+
# real work ⇒ one short push.
|
|
9
|
+
# 3. heuristic scopes (retryScope / truncationScope / extendedScope) — used when the
|
|
10
|
+
# protocol is off.
|
|
11
|
+
#
|
|
12
|
+
# No provider/model route and no credential is preset here. `allowModels` is left unset,
|
|
13
|
+
# which means every route: a fresh install must work on whichever model the user runs.
|
|
14
|
+
# To narrow it, override this row's config in the profile patch layer (see "只想对某些模型
|
|
15
|
+
# 生效" in README.md) rather than here, so an upgrade cannot wipe that choice.
|
|
16
|
+
- insert:
|
|
17
|
+
- id: auto-continue
|
|
18
|
+
name: "@vergilye/dsh-auto-continue"
|
|
19
|
+
config:
|
|
20
|
+
enabled: true
|
|
21
|
+
maxNudges: 2
|
|
22
|
+
delayMs: 0
|
|
23
|
+
dryRun: false
|
|
24
|
+
completionProtocol: true
|
|
25
|
+
markerStyle: fence-check
|
|
26
|
+
streamGuard: true
|
|
27
|
+
streamIdleMs: 90000
|
|
28
|
+
maxGuardRetries: 1
|
|
29
|
+
retryScope: true
|
|
30
|
+
truncationScope: true
|
|
31
|
+
extendedScope: true
|
|
32
|
+
logDecisions: true
|
package/icon.svg
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" role="img" aria-label="Auto continue">
|
|
2
|
+
<!-- Original artwork: a turn that resumes after being cut, drawn as a broken ring
|
|
3
|
+
with an arrow re-entering it and a check mark for the declared ending. -->
|
|
4
|
+
<g fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round">
|
|
5
|
+
<path d="M20.2 12a8.2 8.2 0 1 1-2.4-5.8" />
|
|
6
|
+
<path d="M20.6 4.2v4.4h-4.4" />
|
|
7
|
+
</g>
|
|
8
|
+
<path d="M8.4 12.2l2.4 2.6 4.6-5" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
|
9
|
+
</svg>
|