dsh-personal-system-rules 0.5.1 → 0.6.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/CHANGELOG.md +7 -0
- package/README.md +11 -7
- package/client.js +69 -1
- package/index.js +52 -4
- package/locale/en.json +1 -1
- package/locale/zh.json +1 -1
- package/loop-guard.js +36 -0
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# 更新日志
|
|
2
2
|
|
|
3
|
+
## 0.6.0 — 2026-11-04
|
|
4
|
+
|
|
5
|
+
- 新增任务状态守卫:本轮的 todo 列表里还有 `in_progress` 项、且连续 `maxStaleTodoSteps` 步没有重写列表时,经 `agent/pre-step` 注入一条提醒;之后每再累积同样步数提醒一次。默认 6 步,`0` 关闭。
|
|
6
|
+
- 本轮没有 todo 列表(短任务可以主动跳过)或列表已全部 `completed` 时都不触发,避免把有意不写列表的任务也当成「没更新」。
|
|
7
|
+
- 设置页「守卫」卡片新增「任务状态」分组,阈值与文案都可在弹窗编辑,文案支持 `{count}`(未更新步数)与 `{inProgress}`(进行中项数)。
|
|
8
|
+
- 检测逻辑新增纯函数 `staleTodoSteps()`,`test/loop-guard.test.js` 与 `test/guards.test.js` 各有覆盖。
|
|
9
|
+
|
|
3
10
|
## 0.5.1 — 2026-11-04
|
|
4
11
|
|
|
5
12
|
- 声明并验证对 DeepSeek Harness 0.2.2 预发布版的支持:`settings.section` 与 `plugins.bundle.config` 的注册契约、`describe/mutate` 设置读写、locale 的 `register(ns, {zh, en})` 形式、`loader/volatile-update` 热更新路径在 0.2.2 均未变。`engines.dsh` 已覆盖该版本。
|
package/README.md
CHANGED
|
@@ -2,18 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://github.com/NingYuanLee/dsh-personal-system-rules/actions/workflows/ci.yml) [](https://www.npmjs.com/package/dsh-personal-system-rules) [](LICENSE)
|
|
4
4
|
|
|
5
|
-
把个性化规则文件注入 DeepSeek Harness(dsh
|
|
5
|
+
把个性化规则文件注入 DeepSeek Harness(dsh)的**系统提示**,并提供**静默提醒**、**重复操作循环检测**与**任务状态守卫**;规则正文与守卫参数都能在设置弹窗「个性化规则」里编辑。
|
|
6
6
|
|
|
7
|
-
A dsh plugin that injects a personal rules file as a system-prompt section and adds silent-step / repeated-action loop guards. The rules body and guard options are editable from the dsh settings dialog.
|
|
7
|
+
A dsh plugin that injects a personal rules file as a system-prompt section and adds silent-step / repeated-action loop guards and a stale-todo guard. The rules body and guard options are editable from the dsh settings dialog.
|
|
8
8
|
|
|
9
9
|
## 功能
|
|
10
10
|
|
|
11
11
|
1. **系统提示段**:把 `~/.dsh/PERSONAL-SYSTEM-RULES.md`(或 `config.file` 指定文件)注册成段 `user:agents-md`,order 默认 10150,原样注入;`watchFile` 1 秒轮询,改完下一次模型请求生效,不用重启。
|
|
12
12
|
2. **静默提醒**:连续 `maxSilentSteps`(默认 2)步没有可见文本时,注入一条提醒要求模型说明进展;同一条链只提醒一次,`0` 关闭。
|
|
13
13
|
3. **循环检测**:连续 `repeatLimit` 步执行**完全相同**的工具调用(工具名 + 归一化参数)时触发,`repeatAction` 可选 `remind`(提醒)或 `stop`(像用户点停止一样中止本轮并保留排队输入);默认 0 关闭。
|
|
14
|
-
4.
|
|
14
|
+
4. **任务状态守卫**:本轮的 todo 列表里还有 `in_progress` 项、且连续 `maxStaleTodoSteps`(默认 6)步没有重写列表时提醒一次,之后再每隔同样步数提醒;本轮没建列表(短任务)时不打扰,全部完成时也不触发。`0` 关闭。
|
|
15
|
+
5. **设置弹窗页面**:`设置 → 个性化规则` 与插件详情页是同一个页面,含「规则正文」和「守卫」两块;保存经官方 `settings` 机制写回 profile patch 并热更新。
|
|
15
16
|
|
|
16
|
-
|
|
17
|
+
三个守卫共用同一个 `agent/pre-step` 钩子;检测逻辑是纯函数,独立测试。
|
|
17
18
|
|
|
18
19
|
## 安装
|
|
19
20
|
|
|
@@ -27,13 +28,13 @@ dsh plugin --profile my-profile add dsh-personal-system-rules
|
|
|
27
28
|
**从 git(固定 tag):**
|
|
28
29
|
|
|
29
30
|
```powershell
|
|
30
|
-
dsh plugin --profile my-profile add "git+https://github.com/NingYuanLee/dsh-personal-system-rules.git#v0.
|
|
31
|
+
dsh plugin --profile my-profile add "git+https://github.com/NingYuanLee/dsh-personal-system-rules.git#v0.6.0"
|
|
31
32
|
```
|
|
32
33
|
|
|
33
34
|
**从 GitHub Release tarball:**
|
|
34
35
|
|
|
35
36
|
```powershell
|
|
36
|
-
dsh plugin --profile my-profile add "https://github.com/NingYuanLee/dsh-personal-system-rules/releases/download/v0.
|
|
37
|
+
dsh plugin --profile my-profile add "https://github.com/NingYuanLee/dsh-personal-system-rules/releases/download/v0.6.0/dsh-personal-system-rules-0.6.0.tgz"
|
|
37
38
|
```
|
|
38
39
|
|
|
39
40
|
包没有 `prepare` / `postinstall` 脚本,安装不执行构建代码。
|
|
@@ -53,11 +54,14 @@ dsh plugin --profile my-profile add "https://github.com/NingYuanLee/dsh-personal
|
|
|
53
54
|
repeatLimit: 0
|
|
54
55
|
repeatAction: remind
|
|
55
56
|
repeatReminder: ''
|
|
57
|
+
maxStaleTodoSteps: 6
|
|
58
|
+
staleTodoReminder: ''
|
|
56
59
|
```
|
|
57
60
|
|
|
58
|
-
- `maxSilentSteps`、`silentReminder`、`repeatLimit`、`repeatAction`、`repeatReminder` 是 `volatile()` 字段,可在设置弹窗编辑并热更新。
|
|
61
|
+
- `maxSilentSteps`、`silentReminder`、`repeatLimit`、`repeatAction`、`repeatReminder`、`maxStaleTodoSteps`、`staleTodoReminder` 是 `volatile()` 字段,可在设置弹窗编辑并热更新。
|
|
59
62
|
- `file`、`header`、`order` 是普通配置,只在 `cordis.patch.yml` 里改;改 `file` 后需要重载插件。
|
|
60
63
|
- `repeatLimit: 0` 关闭循环检测;文本回复或新用户轮次会重置重复计数。
|
|
64
|
+
- `maxStaleTodoSteps: 0` 关闭任务状态守卫;提醒文案可含 `{count}`(未更新的步数)与 `{inProgress}`(进行中的项数)。
|
|
61
65
|
- 设置弹窗保存会写回 profile 的 `cordis.patch.yml`。
|
|
62
66
|
|
|
63
67
|
## 兼容性
|
package/client.js
CHANGED
|
@@ -10,6 +10,7 @@ const READ = "systemAgentsMd/read";
|
|
|
10
10
|
const WRITE = "systemAgentsMd/write";
|
|
11
11
|
const DEFAULT_REMINDER = "\u4F60\u5DF2\u7ECF\u8FDE\u7EED {count} \u6B65\u6CA1\u6709\u5411\u7528\u6237\u8BF4\u660E\u8FDB\u5C55\u3002\u8BF7\u5728\u672C\u6B65\u5F00\u5934\u7528\u4E00\u53E5\u8BDD\u8BF4\u660E\u6B63\u5728\u505A\u4EC0\u4E48\u3001\u4E3A\u4EC0\u4E48\uFF0C\u518D\u7EE7\u7EED\u8C03\u7528\u5DE5\u5177\u3002";
|
|
12
12
|
const DEFAULT_REPEAT_REMINDER = "\u4F60\u5DF2\u7ECF\u8FDE\u7EED {count} \u6B65\u6267\u884C\u5B8C\u5168\u76F8\u540C\u7684\u64CD\u4F5C\u3002\u8BF7\u505C\u6B62\u91CD\u590D\uFF1A\u5148\u8BF4\u660E\u5F53\u524D\u963B\u585E\u548C\u5DF2\u5B8C\u6210\u7684\u90E8\u5206\uFF0C\u518D\u6362\u4E00\u79CD\u505A\u6CD5\uFF1B\u5982\u679C\u786E\u5B9E\u9700\u8981\u7EE7\u7EED\u540C\u4E00\u64CD\u4F5C\uFF0C\u8BF7\u5148\u5411\u7528\u6237\u8BF4\u660E\u539F\u56E0\u3002";
|
|
13
|
+
const DEFAULT_STALE_TODO_REMINDER = "\u4EFB\u52A1\u5217\u8868\u91CC\u6709 {inProgress} \u9879\u4ECD\u6807\u7740\u8FDB\u884C\u4E2D\uFF0C\u4F46\u5DF2\u8FDE\u7EED {count} \u6B65\u6CA1\u6709\u66F4\u65B0\u3002\u8BF7\u91CD\u5199 todo \u5217\u8868\uFF1A\u628A\u5DF2\u5B8C\u6210\u7684\u9879\u6807\u6210 completed\uFF0C\u53EA\u7559\u4E0B\u771F\u6B63\u5728\u505A\u7684\u4E00\u9879\uFF0C\u518D\u7EE7\u7EED\u3002";
|
|
13
14
|
const zh = {
|
|
14
15
|
nav: "\u4E2A\u6027\u5316\u89C4\u5219",
|
|
15
16
|
title: "\u4E2A\u6027\u5316\u89C4\u5219",
|
|
@@ -31,6 +32,11 @@ const zh = {
|
|
|
31
32
|
loopActionStop: "\u76F4\u63A5\u53EB\u505C\u672C\u8F6E",
|
|
32
33
|
loopReminderLabel: "\u63D0\u9192\u6587\u6848",
|
|
33
34
|
loopReminderHint: "\u53EF\u7528 {count} \u8868\u793A\u5DF2\u91CD\u590D\u7684\u6B65\u6570\uFF1B\u7559\u7A7A\u4F7F\u7528\u9ED8\u8BA4\u6587\u6848\u3002",
|
|
35
|
+
staleTitle: "\u4EFB\u52A1\u72B6\u6001",
|
|
36
|
+
staleStepsLabel: "\u4EFB\u52A1\u5217\u8868\u672A\u66F4\u65B0\u7684\u6B65\u6570\u9608\u503C",
|
|
37
|
+
staleStepsHint: "\u5217\u8868\u91CC\u8FD8\u6709\u300C\u8FDB\u884C\u4E2D\u300D\u7684\u9879\u3001\u4E14\u8FDE\u7EED\u8FD9\u4E48\u591A\u6B65\u6CA1\u6709\u91CD\u5199 todo \u65F6\u63D0\u9192\uFF1B0 \u8868\u793A\u5173\u95ED\u3002\u9ED8\u8BA4 6\u3002",
|
|
38
|
+
staleReminderLabel: "\u63D0\u9192\u6587\u6848",
|
|
39
|
+
staleReminderHint: "\u53EF\u7528 {count} \u8868\u793A\u672A\u66F4\u65B0\u7684\u6B65\u6570\u3001{inProgress} \u8868\u793A\u8FDB\u884C\u4E2D\u7684\u9879\u6570\uFF1B\u7559\u7A7A\u4F7F\u7528\u9ED8\u8BA4\u6587\u6848\u3002",
|
|
34
40
|
invalidLimit: "\u91CD\u590D\u6B21\u6570\u5FC5\u987B\u662F\u4E0D\u5C0F\u4E8E 0 \u7684\u6574\u6570\u3002",
|
|
35
41
|
saveGuard: "\u4FDD\u5B58\u5B88\u536B",
|
|
36
42
|
savingGuard: "\u4FDD\u5B58\u4E2D",
|
|
@@ -78,6 +84,11 @@ const en = {
|
|
|
78
84
|
loopActionStop: "Stop this turn",
|
|
79
85
|
loopReminderLabel: "Reminder text",
|
|
80
86
|
loopReminderHint: "Use {count} for the repeated step count; leave blank for the default.",
|
|
87
|
+
staleTitle: "Task status",
|
|
88
|
+
staleStepsLabel: "Steps without a todo update",
|
|
89
|
+
staleStepsHint: "Triggers while the list still has items in progress and it has not been rewritten for this many steps; 0 disables it. Default 6.",
|
|
90
|
+
staleReminderLabel: "Reminder text",
|
|
91
|
+
staleReminderHint: "Use {count} for the stale step count and {inProgress} for the in-progress items; leave blank for the default.",
|
|
81
92
|
invalidLimit: "The repeat limit must be an integer of 0 or more.",
|
|
82
93
|
saveGuard: "Save guards",
|
|
83
94
|
savingGuard: "Saving",
|
|
@@ -191,6 +202,7 @@ function RulesSection(props) {
|
|
|
191
202
|
const area = useRef(null);
|
|
192
203
|
const reminderArea = useRef(null);
|
|
193
204
|
const loopArea = useRef(null);
|
|
205
|
+
const staleArea = useRef(null);
|
|
194
206
|
const request = useRef(0);
|
|
195
207
|
const [configPhase, setConfigPhase] = useState("loading");
|
|
196
208
|
const [configNs, setConfigNs] = useState(SETTINGS_NS);
|
|
@@ -202,6 +214,8 @@ function RulesSection(props) {
|
|
|
202
214
|
const [repeatLimitDraft, setRepeatLimitDraft] = useState("");
|
|
203
215
|
const [repeatActionDraft, setRepeatActionDraft] = useState("remind");
|
|
204
216
|
const [loopReminderDraft, setLoopReminderDraft] = useState("");
|
|
217
|
+
const [staleStepsDraft, setStaleStepsDraft] = useState("");
|
|
218
|
+
const [staleReminderDraft, setStaleReminderDraft] = useState("");
|
|
205
219
|
function applyLoaded(value) {
|
|
206
220
|
setPath(value.path || "");
|
|
207
221
|
setDraft(value.text || "");
|
|
@@ -273,6 +287,8 @@ function RulesSection(props) {
|
|
|
273
287
|
setRepeatLimitDraft(Number.isFinite(value.repeatLimit) && value.repeatLimit > 0 ? String(value.repeatLimit) : "");
|
|
274
288
|
setRepeatActionDraft(value.repeatAction === "stop" ? "stop" : "remind");
|
|
275
289
|
setLoopReminderDraft(typeof value.repeatReminder === "string" ? value.repeatReminder : "");
|
|
290
|
+
setStaleStepsDraft(Number.isFinite(value.maxStaleTodoSteps) && value.maxStaleTodoSteps > 0 ? String(value.maxStaleTodoSteps) : "");
|
|
291
|
+
setStaleReminderDraft(typeof value.staleTodoReminder === "string" ? value.staleTodoReminder : "");
|
|
276
292
|
setRevision(Number.isFinite(row.revision) ? row.revision : 0);
|
|
277
293
|
setConfigPhase("ready");
|
|
278
294
|
} catch (error) {
|
|
@@ -374,13 +390,25 @@ function RulesSection(props) {
|
|
|
374
390
|
}
|
|
375
391
|
const reminder = reminderDraft.trim() === "" ? "" : reminderDraft;
|
|
376
392
|
const loopReminder = loopReminderDraft.trim() === "" ? "" : loopReminderDraft;
|
|
393
|
+
const staleText = staleStepsDraft.trim();
|
|
394
|
+
let staleSteps = 0;
|
|
395
|
+
if (staleText !== "") {
|
|
396
|
+
staleSteps = Number(staleText);
|
|
397
|
+
if (!Number.isInteger(staleSteps) || staleSteps < 0) {
|
|
398
|
+
setConfigNote({ tone: "error", text: tr("invalidSteps") });
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
const staleReminder = staleReminderDraft.trim() === "" ? "" : staleReminderDraft;
|
|
377
403
|
const action = repeatActionDraft === "stop" ? "stop" : "remind";
|
|
378
404
|
const ops = [
|
|
379
405
|
stepsText === "" ? { op: "unset", path: ["maxSilentSteps"] } : { op: "set", path: ["maxSilentSteps"], value: steps },
|
|
380
406
|
reminder === "" ? { op: "unset", path: ["silentReminder"] } : { op: "set", path: ["silentReminder"], value: reminder },
|
|
381
407
|
limitText === "" ? { op: "unset", path: ["repeatLimit"] } : { op: "set", path: ["repeatLimit"], value: limit },
|
|
382
408
|
action === "remind" ? { op: "unset", path: ["repeatAction"] } : { op: "set", path: ["repeatAction"], value: action },
|
|
383
|
-
loopReminder === "" ? { op: "unset", path: ["repeatReminder"] } : { op: "set", path: ["repeatReminder"], value: loopReminder }
|
|
409
|
+
loopReminder === "" ? { op: "unset", path: ["repeatReminder"] } : { op: "set", path: ["repeatReminder"], value: loopReminder },
|
|
410
|
+
staleText === "" ? { op: "unset", path: ["maxStaleTodoSteps"] } : { op: "set", path: ["maxStaleTodoSteps"], value: staleSteps },
|
|
411
|
+
staleReminder === "" ? { op: "unset", path: ["staleTodoReminder"] } : { op: "set", path: ["staleTodoReminder"], value: staleReminder }
|
|
384
412
|
];
|
|
385
413
|
setOptionsBusy(true);
|
|
386
414
|
setConfigNote(null);
|
|
@@ -398,6 +426,8 @@ function RulesSection(props) {
|
|
|
398
426
|
setRepeatLimitDraft(Number.isFinite(view.value.repeatLimit) && view.value.repeatLimit > 0 ? String(view.value.repeatLimit) : "");
|
|
399
427
|
setRepeatActionDraft(view.value.repeatAction === "stop" ? "stop" : "remind");
|
|
400
428
|
setLoopReminderDraft(typeof view.value.repeatReminder === "string" ? view.value.repeatReminder : "");
|
|
429
|
+
setStaleStepsDraft(Number.isFinite(view.value.maxStaleTodoSteps) && view.value.maxStaleTodoSteps > 0 ? String(view.value.maxStaleTodoSteps) : "");
|
|
430
|
+
setStaleReminderDraft(typeof view.value.staleTodoReminder === "string" ? view.value.staleTodoReminder : "");
|
|
401
431
|
}
|
|
402
432
|
setConfigNote({ tone: "ok", text: tr("guardSaved") });
|
|
403
433
|
} catch (error) {
|
|
@@ -586,6 +616,44 @@ function RulesSection(props) {
|
|
|
586
616
|
})
|
|
587
617
|
),
|
|
588
618
|
h("p", { className: "dsh-agents-md-hint" }, tr("loopReminderHint")),
|
|
619
|
+
h("p", { className: "dsh-agents-md-group" }, tr("staleTitle")),
|
|
620
|
+
h(
|
|
621
|
+
"label",
|
|
622
|
+
{ className: "dsh-agents-md-label" },
|
|
623
|
+
tr("staleStepsLabel"),
|
|
624
|
+
h("input", {
|
|
625
|
+
className: "dsh-agents-md-field",
|
|
626
|
+
type: "number",
|
|
627
|
+
min: "0",
|
|
628
|
+
step: "1",
|
|
629
|
+
value: staleStepsDraft,
|
|
630
|
+
disabled: optionsBusy || configPhase !== "ready",
|
|
631
|
+
onChange: (event) => {
|
|
632
|
+
setStaleStepsDraft(event.target.value);
|
|
633
|
+
setConfigNote(null);
|
|
634
|
+
}
|
|
635
|
+
})
|
|
636
|
+
),
|
|
637
|
+
h("p", { className: "dsh-agents-md-hint" }, tr("staleStepsHint")),
|
|
638
|
+
h(
|
|
639
|
+
"label",
|
|
640
|
+
{ className: "dsh-agents-md-label" },
|
|
641
|
+
tr("staleReminderLabel"),
|
|
642
|
+
h("textarea", {
|
|
643
|
+
ref: staleArea,
|
|
644
|
+
className: "dsh-agents-md-field dsh-agents-md-field-area",
|
|
645
|
+
rows: 3,
|
|
646
|
+
value: staleReminderDraft,
|
|
647
|
+
spellCheck: false,
|
|
648
|
+
disabled: optionsBusy || configPhase !== "ready",
|
|
649
|
+
placeholder: DEFAULT_STALE_TODO_REMINDER,
|
|
650
|
+
onChange: (event) => {
|
|
651
|
+
setStaleReminderDraft(event.target.value);
|
|
652
|
+
setConfigNote(null);
|
|
653
|
+
}
|
|
654
|
+
})
|
|
655
|
+
),
|
|
656
|
+
h("p", { className: "dsh-agents-md-hint" }, tr("staleReminderHint")),
|
|
589
657
|
configNote ? h("p", {
|
|
590
658
|
className: configNote.tone === "ok" ? "dsh-agents-md-ok" : "dsh-agents-md-error",
|
|
591
659
|
role: configNote.tone === "ok" ? "status" : "alert"
|
package/index.js
CHANGED
|
@@ -4,7 +4,7 @@ import { homedir } from 'node:os'
|
|
|
4
4
|
import { join } from 'node:path'
|
|
5
5
|
import { pathToFileURL } from 'node:url'
|
|
6
6
|
import { MAX_BYTES, readAgentsFile, writeAgentsFile } from './file.js'
|
|
7
|
-
import { countRepeatSteps } from './loop-guard.js'
|
|
7
|
+
import { countRepeatSteps, staleTodoSteps } from './loop-guard.js'
|
|
8
8
|
|
|
9
9
|
/** Cordis plugin name. */
|
|
10
10
|
export const name = 'system-agents-md'
|
|
@@ -15,6 +15,7 @@ export const inject = ['systemPrompt']
|
|
|
15
15
|
const SECTION_NAME = 'user:agents-md'
|
|
16
16
|
const DEFAULT_ORDER = 10150
|
|
17
17
|
const DEFAULT_MAX_SILENT_STEPS = 2
|
|
18
|
+
const DEFAULT_MAX_STALE_TODO_STEPS = 6
|
|
18
19
|
const DEFAULT_RULES_FILE = 'PERSONAL-SYSTEM-RULES.md'
|
|
19
20
|
const DEFAULT_HEADER = [
|
|
20
21
|
'# 用户全局系统层规则',
|
|
@@ -28,6 +29,9 @@ const DEFAULT_SILENT_REMINDER = '你已经连续 {count} 步没有向用户说
|
|
|
28
29
|
/** Default reminder used when the same tool call repeats beyond the limit. */
|
|
29
30
|
const DEFAULT_REPEAT_REMINDER = '你已经连续 {count} 步执行完全相同的操作。请停止重复:先说明当前阻塞和已完成的部分,再换一种做法;如果确实需要继续同一操作,请先向用户说明原因。'
|
|
30
31
|
|
|
32
|
+
/** Default reminder used when the task list stops moving while items stay in progress. */
|
|
33
|
+
const DEFAULT_STALE_TODO_REMINDER = '任务列表里有 {inProgress} 项仍标着进行中,但已连续 {count} 步没有更新。请重写 todo 列表:把已完成的项标成 completed,只留下真正在做的一项,再继续。'
|
|
34
|
+
|
|
31
35
|
/** Wire namespace for the detail-page editor and the settings dialog. */
|
|
32
36
|
export const FILE_SERVICE = 'systemAgentsMd'
|
|
33
37
|
|
|
@@ -71,6 +75,8 @@ async function loadConfigSchema() {
|
|
|
71
75
|
repeatLimit: z.number().min(0).default(0).volatile(),
|
|
72
76
|
repeatAction: z.union([z.const('remind'), z.const('stop')]).default('remind').volatile(),
|
|
73
77
|
repeatReminder: z.string().default('').volatile(),
|
|
78
|
+
maxStaleTodoSteps: z.number().min(0).default(DEFAULT_MAX_STALE_TODO_STEPS).volatile(),
|
|
79
|
+
staleTodoReminder: z.string().default('').volatile(),
|
|
74
80
|
})
|
|
75
81
|
}
|
|
76
82
|
|
|
@@ -139,7 +145,22 @@ function resolveSettings(config = {}) {
|
|
|
139
145
|
const repeatAction = unwrapVolatile(config.repeatAction) === 'stop' ? 'stop' : 'remind'
|
|
140
146
|
const repeatReminderValue = unwrapVolatile(config.repeatReminder)
|
|
141
147
|
const repeatTemplate = typeof repeatReminderValue === 'string' && repeatReminderValue.trim() ? repeatReminderValue : DEFAULT_REPEAT_REMINDER
|
|
142
|
-
|
|
148
|
+
const staleStepsValue = Number(unwrapVolatile(config.maxStaleTodoSteps))
|
|
149
|
+
const maxStaleTodoSteps = Number.isFinite(staleStepsValue) ? Math.max(0, Math.floor(staleStepsValue)) : DEFAULT_MAX_STALE_TODO_STEPS
|
|
150
|
+
const staleReminderValue = unwrapVolatile(config.staleTodoReminder)
|
|
151
|
+
const staleTemplate = typeof staleReminderValue === 'string' && staleReminderValue.trim() ? staleReminderValue : DEFAULT_STALE_TODO_REMINDER
|
|
152
|
+
return {
|
|
153
|
+
file,
|
|
154
|
+
order,
|
|
155
|
+
header,
|
|
156
|
+
maxSilentSteps,
|
|
157
|
+
silentTemplate,
|
|
158
|
+
repeatLimit,
|
|
159
|
+
repeatAction,
|
|
160
|
+
repeatTemplate,
|
|
161
|
+
maxStaleTodoSteps,
|
|
162
|
+
staleTemplate,
|
|
163
|
+
}
|
|
143
164
|
}
|
|
144
165
|
|
|
145
166
|
/**
|
|
@@ -345,14 +366,17 @@ export function apply(ctx, config = {}) {
|
|
|
345
366
|
}
|
|
346
367
|
}, 'system-agents-md.section')
|
|
347
368
|
|
|
348
|
-
// One pre-step hook drives
|
|
369
|
+
// One pre-step hook drives three guards:
|
|
349
370
|
// - silent guard: consecutive steps without visible text;
|
|
350
371
|
// - loop guard: consecutive identical tool-call steps (remind, or stop the
|
|
351
|
-
// turn the way the user's own stop does, keeping queued input)
|
|
372
|
+
// turn the way the user's own stop does, keeping queued input);
|
|
373
|
+
// - stale-todo guard: a task list left with `in_progress` items that the
|
|
374
|
+
// model has not rewritten for several steps.
|
|
352
375
|
// Settings edits are volatile, so `current` is read on every step. A guard
|
|
353
376
|
// disabled with 0 resets its own per-agent state.
|
|
354
377
|
const silentRemindedAt = new WeakMap()
|
|
355
378
|
const repeatState = new WeakMap()
|
|
379
|
+
const staleRemindedAt = new WeakMap()
|
|
356
380
|
ctx.effect(() => ctx.on('agent/pre-step', async (payload, next) => {
|
|
357
381
|
const decision = await next()
|
|
358
382
|
if (decision.kind === 'reject') return decision
|
|
@@ -402,6 +426,30 @@ export function apply(ctx, config = {}) {
|
|
|
402
426
|
}
|
|
403
427
|
}
|
|
404
428
|
|
|
429
|
+
const staleLimit = current.maxStaleTodoSteps
|
|
430
|
+
if (staleLimit <= 0) {
|
|
431
|
+
staleRemindedAt.delete(agent)
|
|
432
|
+
} else {
|
|
433
|
+
const stale = staleTodoSteps(agent)
|
|
434
|
+
if (!stale.hasList || stale.inProgress === 0 || stale.steps < staleLimit) {
|
|
435
|
+
staleRemindedAt.delete(agent)
|
|
436
|
+
} else if (stale.steps >= (staleRemindedAt.get(agent) ?? 0) + staleLimit) {
|
|
437
|
+
// One reminder per staleLimit further steps: the counter cannot drop
|
|
438
|
+
// until the model rewrites the list, so a plain `> last` would nag on
|
|
439
|
+
// every following step.
|
|
440
|
+
staleRemindedAt.set(agent, stale.steps)
|
|
441
|
+
additions.push(createUserMessage({
|
|
442
|
+
content: [{
|
|
443
|
+
type: 'text',
|
|
444
|
+
text: current.staleTemplate
|
|
445
|
+
.replaceAll('{count}', String(stale.steps))
|
|
446
|
+
.replaceAll('{inProgress}', String(stale.inProgress)),
|
|
447
|
+
}],
|
|
448
|
+
source: { kind: 'system-agents-md', form: 'notice', summary: stale.steps + ' steps since the last todo update' },
|
|
449
|
+
}))
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
405
453
|
return additions.length === 0 ? decision : { ...decision, messages: additions }
|
|
406
454
|
}), 'system-agents-md.guards')
|
|
407
455
|
|
package/locale/en.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"meta": {
|
|
3
3
|
"title": "Personalized rules",
|
|
4
|
-
"description": "Inject a user-chosen rules file as a system-prompt section, and configure its body plus silent-step
|
|
4
|
+
"description": "Inject a user-chosen rules file as a system-prompt section, and configure its body plus silent-step, repeated-action and stale-todo guards from the DSH settings dialog or the plugin detail page. Inactive in the minimal preset; nothing is injected when the plugin is disabled or fails to load."
|
|
5
5
|
}
|
|
6
6
|
}
|
package/locale/zh.json
CHANGED
package/loop-guard.js
CHANGED
|
@@ -88,3 +88,39 @@ export function repeatStreak(steps) {
|
|
|
88
88
|
export function countRepeatSteps(agent) {
|
|
89
89
|
return repeatStreak(recentToolSteps(agent))
|
|
90
90
|
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Steps taken since the newest `todo/write` in the current turn, plus how many
|
|
94
|
+
* items that list left `in_progress`.
|
|
95
|
+
*
|
|
96
|
+
* The todo projection is reset by `turn/start`, so a list that is still
|
|
97
|
+
* non-empty was necessarily written in this turn. `hasList` is false when the
|
|
98
|
+
* turn wrote no list yet: the model may be skipping todos on purpose (short
|
|
99
|
+
* tasks), so callers must not nag in that case.
|
|
100
|
+
* @param {object} agent live agent (session surface + eventAt)
|
|
101
|
+
* @returns {{ steps: number, inProgress: number, hasList: boolean }}
|
|
102
|
+
*/
|
|
103
|
+
export function staleTodoSteps(agent) {
|
|
104
|
+
const nodes = agent?.session?.surface?.nodes
|
|
105
|
+
if (!Array.isArray(nodes)) return { steps: 0, inProgress: 0, hasList: false }
|
|
106
|
+
let steps = 0
|
|
107
|
+
for (let index = nodes.length - 1; index >= 0; index -= 1) {
|
|
108
|
+
const event = agent.session.eventAt(nodes[index])
|
|
109
|
+
if (event === undefined) continue
|
|
110
|
+
if (event.type === 'todo/write') {
|
|
111
|
+
const todos = Array.isArray(event.data?.todos) ? event.data.todos : []
|
|
112
|
+
return {
|
|
113
|
+
steps,
|
|
114
|
+
inProgress: todos.filter((todo) => todo?.status === 'in_progress').length,
|
|
115
|
+
hasList: true,
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
if (event.type === 'assistant/message') {
|
|
119
|
+
steps += 1
|
|
120
|
+
continue
|
|
121
|
+
}
|
|
122
|
+
if (event.type === 'turn/start') break
|
|
123
|
+
if (event.type === 'user/message' && event.data?.source?.kind === 'user') break
|
|
124
|
+
}
|
|
125
|
+
return { steps, inProgress: 0, hasList: false }
|
|
126
|
+
}
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-personal-system-rules",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"displayName": "个性化规则",
|
|
5
|
-
"description": "Inject a user-chosen rules file as a system-prompt section, and configure its body plus silent-step
|
|
5
|
+
"description": "Inject a user-chosen rules file as a system-prompt section, and configure its body plus silent-step, repeated-action and stale-todo guards from the DSH settings dialog or the plugin detail page. Inactive in the minimal preset; nothing is injected when the plugin is disabled or fails to load.",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"author": "NingYuanLee",
|
|
8
8
|
"type": "module",
|