@deepseek-ai/dsh-repeat-tool-reminder 0.1.1-rc.2 → 0.1.2-alpha.3

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.i18n.yaml CHANGED
@@ -2,5 +2,5 @@
2
2
  # side as of the last confirmed-consistent state. Both languages carry equal authority;
3
3
  # after editing either side, bring the other along and re-record with:
4
4
  # pnpm run verify-translation-pairing --write packages/guard/repeat-tool-reminder/README.md
5
- README.md: cfece8150dce8e2fe45b4e942bf7188f46836551
6
- README.zh.md: 439480d876ad07d8bc095b2534a5a2ce732bfc9b
5
+ README.md: 94d6ac6d3805c499a53473dc5c1aa1a290026fa2
6
+ README.zh.md: d4f7253cd720da9e91b403392ccc9a5b44eb6aa3
package/README.md CHANGED
@@ -1,39 +1,118 @@
1
+ ---
2
+ description: "Advisory loop-hygiene guard that nudges the model out of identical tool-call loops, for users and maintainers choosing, configuring, or debugging the plugin."
3
+ kind: "package-reference"
4
+ ---
5
+
1
6
  # @deepseek-ai/dsh-repeat-tool-reminder
2
7
 
3
8
  English | [中文](README.zh.md)
4
9
 
5
- An advisory loop-breaker, not a model-facing tool: it never appears in the tool list, never vetoes or rewrites a call, and adds exactly one behavior — it watches each agent's stream of tool calls, counts runs of consecutive calls to the same tool with identical canonicalized arguments, and at configured run lengths injects an escalating advisory reminder telling the model to stop repeating itself, re-read the last result, and either change approach or conclude. The decision (retry differently, gather more evidence, or finish) stays entirely with the model: a legitimately repeated call is delayed by nothing and blocked by nothing. Decision record: [the repeat-tool-reminder Agent Note](../../../.agents/notes/archived/feature/2026-07-08-repeat-tool-guard.md).
10
+ ## Summary
11
+
12
+ A model can get stuck calling the same tool with the same arguments — re-running a failing command, re-reading an unchanged file — burning time and tokens without making progress. `dsh-repeat-tool-reminder` notices the pattern and tells the model to stop: at chosen repeat counts it delivers a reminder to analyze the last result and either try a different approach or finish. The reminder is advice, never a block: a legitimate repeated call is delayed by nothing, and the decision to continue, change approach, or stop stays with the model. It tracks each agent separately, so one agent's loop never disturbs another's work, and a new user message clears the count. It ships enabled in the `dsh` base bundle with reminders at 3, 5, and 8 repeats.
13
+
14
+ ## Table of Contents
15
+
16
+ - [Use this package](#use-this-package)
17
+ - [Understand the implementation](#understand-the-implementation)
18
+ - [Further Exploration](#further-exploration)
19
+ - [Model Experience](#model-experience)
20
+ - [Known Limitations and Deferred Work](#known-limitations-and-deferred-work)
21
+ - [Dev Note](#dev-note)
22
+
23
+ -----
24
+
25
+ <a id="use-this-package"></a>
26
+ ## Use this package
27
+
28
+ Mount this plugin when the model should catch itself looping on identical tool calls. There is nothing to learn or wire: the `dsh` base bundle already runs it, and the defaults work for most sessions — tune the thresholds and tool scope below when you want the nudge sooner, later, or on fewer tools.
29
+
30
+ ### When to choose it
31
+
32
+ Choose it when the model works autonomously for long stretches and a stuck loop is the failure you want to break with advice rather than force. Avoid it when identical repeats are legitimate and must run undisturbed — the guard only reminds, and a reminder is a small extra message after the repeated call — and when near-identical variants must be caught, because only exact repeats (same tool, same arguments regardless of property order) are detected.
6
33
 
7
- ## Config
34
+ ### Setting the thresholds and scope
35
+
36
+ When you want to change when reminders fire or which tools they cover, mount the plugin with configuration:
8
37
 
9
38
  ```yaml
10
- - id: repeat-tool-reminder
11
- name: '@deepseek-ai/dsh-repeat-tool-reminder'
39
+ - name: '@deepseek-ai/dsh-repeat-tool-reminder'
12
40
  config:
13
- thresholds: [3, 5, 8] # default; consecutive counts that trigger a reminder
14
- include: [] # tool-name patterns to track; empty ⇒ all tools
15
- exclude: [todo_write] # tool-name patterns transparent to the chain
16
- argumentsPreviewChars: 500 # default; cap on arguments quoted in the detailed reminder
41
+ thresholds: [3, 5, 8] # remind at 3, 5, and 8 consecutive repeats
42
+ include: [] # track every tool; list patterns to track only some
43
+ exclude: [todo_write] # never track these tools
44
+ argumentsPreviewChars: 500 # cap on arguments shown in the detailed reminder
17
45
  ```
18
46
 
19
- `thresholds` fails loud at plugin load: an empty list, a non-integer, a value below 2, or a duplicate throws, never a silent fall-back to defaults; `argumentsPreviewChars` equally rejects anything but an integer >= 1. The list is normalized to ascending order; the FIRST threshold delivers a short generic nudge, every later threshold delivers the detailed form naming the tool, the run length, and the canonical arguments — head-truncated at `argumentsPreviewChars` with an omitted-count marker, so a looping `write`/`edit` payload cannot ride into the next request unbounded (the chain key always compares the FULL canonical string; the cap bounds the reminder, never the detection).
47
+ | Field | Default | Meaning |
48
+ |---|---|---|
49
+ | `thresholds` | `[3, 5, 8]` | Repeat counts that trigger a reminder |
50
+ | `include` | `[]` | Only these tools are tracked; empty means every tool |
51
+ | `exclude` | `[]` | These tools are never tracked; calls to them neither count nor reset |
52
+ | `argumentsPreviewChars` | `500` | How many characters of the repeated arguments the detailed reminder shows |
53
+
54
+ Invalid configuration fails at startup with a clear error — an empty `thresholds` list, a repeat count below 2, or a duplicate — never a silent change of behavior. The generated [configuration catalog](../../../docs/config-catalog.md#deepseek-aidsh-repeat-tool-reminder) documents every accepted value.
55
+
56
+ ### What you get
57
+
58
+ With the defaults, a model that repeats the same call with identical arguments receives a short reminder on the third repeat — to analyze the previous result before calling again — and detailed reminders on the fifth and eighth, naming the tool and the repeated arguments so it can decide whether to change approach, gather more evidence, or finish. A new user message clears the count, so a fresh instruction is never treated as a loop. Reminders appear in the conversation after the repeated call's result, attributed to the plugin, so the model reads them like any other message.
59
+
60
+ -----
61
+
62
+ <a id="understand-the-implementation"></a>
63
+ ## Understand the implementation
64
+
65
+ <details>
66
+ <summary>Implementation internals — click to expand</summary>
67
+
68
+ This section explains how the guard detects repeats and delivers reminders, and points at the code that realizes it; the observable behavior is fully covered in [Use this package](#use-this-package).
69
+
70
+ ### Design philosophy
20
71
 
21
- `include`/`exclude` entries support `*` wildcards and are predicates over whatever tools exist at call time, not references to registry entries — a pattern matching no currently registered tool is NOT an error (`exclude: [mcp_*]` stays valid in a deployment that loads no MCP tools), unlike `toolOrder`'s referent check.
72
+ The guard is built on four commitments:
22
73
 
23
- ## Chain semantics
74
+ - **Advisory, not veto.** The guard enriches post-execute decisions with model context; it never blocks or rewrites a call, so `PostToolDecision` blocking stays a later listener's job.
75
+ - **Count in post-execute.** Detection runs on `tools/post-execute`, which also fires for denied calls; counting there lets one listener cover every attempt with no cross-event state.
76
+ - **Exact-match canonicalization.** Arguments reach the guard as the loop's `JSON.parse` output (or its raw-string fallback), so JSON's value domain is the whole input domain and a deep key-sort plus `JSON.stringify` is a complete, deterministic identity — no bigint, cycle, or `undefined` handling exists because no input path can produce them.
77
+ - **Fail loud at load.** `thresholds` and `argumentsPreviewChars` validate in `apply` and throw, never falling back to defaults.
24
78
 
25
- The chain key is `(tool name, canonical arguments)` — canonicalization is a deep key-sort plus `JSON.stringify`, so argument objects differing only in property order count as identical. A call identical to the previous tracked call increments the agent's consecutive counter; a different tracked call resets it to 1.
79
+ ### Detection: the repeat chain
26
80
 
27
- - **Untracked calls are transparent to the chain.** A call excluded by `include`/`exclude` neither increments nor resets the counter, so `grep X → todo_write → grep X` still counts as two consecutive `grep X` when `todo_write` is excluded. This is what makes exclusion useful: bookkeeping tools interleaved into a loop must not launder it.
28
- - **Denied calls count.** Detection sits on `tools/post-execute`, which also runs for calls a `tools/pre-execute` listener denied — a model hammering a denied call is exactly the loop worth breaking.
81
+ Each agent's chain is keyed by `(tool name, canonical arguments)` — two calls with the same tool and canonically identical arguments (property order ignored) count as consecutive, and a different tracked call resets the count to 1. The chain lives in a `WeakMap<Agent, Chain>`.
82
+
83
+ - **Untracked calls are transparent to the chain.** A call excluded by `include`/`exclude` neither increments nor resets the counter, so `grep X → todo_write → grep X` still counts as two consecutive `grep X` when `todo_write` is excluded — bookkeeping tools interleaved into a loop do not launder it.
84
+ - **Denied calls count.** Detection sits on `tools/post-execute`, which also runs for calls a `tools/pre-execute` listener denied; a model hammering a denied call is exactly the loop worth breaking.
29
85
  - **Calls without an agent are ignored.** A direct `ctx.tools.execute()` caller has no model to remind and no live agent object to key on.
30
- - **Per-agent keying.** The tool registry is context-level and subagents interleave through the same waterfall, so a `WeakMap<Agent, Chain>` keys each chain by the live agent object; one agent's repetition never trips another's reminder. A user prompt (`agent/pre-step`) resets the submitting agent's chain, and object lifetime bounds the weak entry without a disposal listener.
31
- - **In-memory only.** A session resumed from persistence starts with a fresh chain — the guard is a heuristic nudge, not a logged invariant, later reminders are the accepted cost.
86
+ - **Per-agent keying, reset on user prompts.** One agent's repetition never trips another's reminder; a user prompt (`agent/pre-step`) deletes the submitting agent's chain, and object lifetime bounds the weak entry without a disposal listener.
87
+ - **In-memory only.** A session resumed from persistence starts with a fresh chain — the guard is a heuristic nudge, not a logged invariant, so reminders after a resume are the accepted cost.
88
+
89
+ ### Reminder delivery
90
+
91
+ Reminders ride the post-execute decision's `additionalContexts` (source `{kind: 'plugin', plugin: 'repeat-tool-reminder', form: 'notice', summary: '<tool> × <count>'}`), never a `content` replacement: the `tool/result` event stays the tool's own output for audit. The loop buffers the context and appends it as an injected `user/message` after the step's tool results, which the session renders as a plain synthetic user message — model-visible, source-attributed, and reconstructable from the session log with no new session event. The guard always delegates via `next()` and prepends its reminder to the downstream decision's context array, so both decision variants (a blocked call included) still get the nudge while every entry retains its own source and metadata.
92
+
93
+ ### Source map
94
+
95
+ | File | Role |
96
+ |---|---|
97
+ | [`src/index.ts`](src/index.ts) | Plugin entry: `Config` schema, fail-loud validation, chain listeners |
98
+ | [`src/invariant.ts`](src/invariant.ts) | Invariant companion (no runtime invariant: the chain is private to one post-execute listener) |
99
+
100
+ </details>
101
+
102
+ -----
103
+
104
+ <a id="further-exploration"></a>
105
+ ## Further Exploration
106
+
107
+ Read these pages when the package-level contract is not enough. They move from the tools waterfall to exhaustive configuration and the guard group map.
32
108
 
33
- ## Reminder delivery
109
+ - [Tools subsystem reference](../../../docs/subsystems/tools.md) — the `tools/execute` waterfall, `additionalContexts`, and decision shapes this guard consumes.
110
+ - [Generated configuration catalog](../../../docs/config-catalog.md#deepseek-aidsh-repeat-tool-reminder) — every accepted config field and its source declaration.
111
+ - [guard group map](../README.md) — the sibling guard packages and the loop-hygiene family.
34
112
 
35
- Reminders ride the post-execute decision's `additionalContexts` (source `{kind: 'plugin', plugin: 'repeat-tool-reminder'}`), never a `content` replacement: the `tool/result` event stays the tool's own output for audit. The loop buffers the context and appends it as an injected `user/message` after the step's tool results, which the session renders as a plain synthetic user message — so the reminder is model-visible, source-attributed, and reconstructable from the session log with no new session event. The guard always delegates via `next()` and prepends its reminder to the downstream decision's context array (both variants — a blocked call still gets the nudge); every entry retains its own source and metadata.
113
+ -----
36
114
 
115
+ <a id="model-experience"></a>
37
116
  ## Model Experience
38
117
 
39
118
  ### First-threshold context message
@@ -82,9 +161,26 @@ Append-only; newly visible content follows the reusable request prefix and does
82
161
 
83
162
  ## Known Limitations and Deferred Work
84
163
 
164
+ <a id="known-limitations-and-deferred-work"></a>
165
+
166
+
167
+ These limits define when the guard is a poor fit. They are current package constraints, not a task backlog.
168
+
85
169
  - **Exact-match detection only** — canonicalization is a deep key-sort, so near-identical variants (a tweaked path, extra whitespace inside a value) evade the chain; fuzzy matching is rejected pending evidence of need.
86
170
  - **Compaction does not reset chains** — a chain spanning a compaction checkpoint keeps counting.
87
- - **Advisory only** — escalating to `block` at a high threshold is not implemented, though `PostToolDecision` already supports blocking.
171
+ - **Advisory only** — escalating to a blocking form at a high threshold is not implemented, though `PostToolDecision` already supports blocking.
88
172
  - **No subagent chain-sharing** — chains stay isolated per agent; a parent and its subagent repeating the same call never combine.
89
- - **Legitimate idempotent polling still draws nudges** past the thresholds — the pressure valves are `thresholds`/`exclude` config.
173
+ - **Legitimate idempotent polling still draws nudges** past the thresholds — the pressure valves are the `thresholds`/`exclude` config.
90
174
  - **Past the highest threshold a chain goes silent** — reminders fire only at exact configured counts, never beyond them.
175
+
176
+ <a id="dev-note"></a>
177
+ ### Dev Note
178
+
179
+ <details>
180
+ <summary>Working context for maintainers — click to expand</summary>
181
+
182
+ This Dev Note is working context for maintainers: open questions and directions that are not decided. It is explicitly non-authoritative — shipped behavior, limits, and accepted rationale live in the sections above, the package code, and the linked Agent Notes.
183
+
184
+ The [repeat-tool-guard feature note](../../../.agents/notes/archived/feature/2026-07-08-repeat-tool-guard.md) records the original design and alternatives under the former package name; the [naming ledger](../../../.agents/notes/implemented/architecture/2026-08-11-repository-naming-contract-and-rename-ledger.md) records the rename to `repeat-tool-reminder` and its reason.
185
+
186
+ </details>
package/README.zh.md CHANGED
@@ -1,46 +1,125 @@
1
+ ---
2
+ description: "建议性循环卫生 guard:当 agent 重复完全相同的工具调用时提醒模型,供选择、配置或排查此插件的用户与维护者阅读。"
3
+ kind: "package-reference"
4
+ ---
5
+
1
6
  # @deepseek-ai/dsh-repeat-tool-reminder
2
7
 
3
8
  [English](README.md) | 中文
4
9
 
5
- 这是一个仅提供建议的循环中断器,而非面向模型的工具:它不会出现在工具列表中,不会否决或改写调用,只增加一种行为。它监视每个 agent(智能体)的工具调用流,统计以完全相同的规范化参数连续调用同一工具的次数;达到所配置的连续次数时,它会注入逐级增强的提示,要求模型停止重复、重新阅读上一次结果,并改用其他方案或结束任务。究竟是换一种方式重试、收集更多证据还是完成任务,仍完全由模型决定:合理的重复调用既不会延迟,也不会受阻。决策记录见 [repeat-tool-reminder Agent Note](../../../.agents/notes/archived/feature/2026-07-08-repeat-tool-guard.md)。
10
+ ## 概述
11
+
12
+ 模型可能会卡在以相同参数调用同一工具上——反复运行失败的命令、反复读取未变化的文件——白白消耗时间和 token 却没有进展。`dsh-repeat-tool-reminder` 会发现这种模式并让模型停下来:在选定的重复次数上,它送出一条提醒,要求模型分析上一次结果并改用其他方法或结束任务。提醒只是建议,绝非阻止:合理的重复调用不会被延迟分毫,是否继续、改变方法或停止仍由模型决定。它分别跟踪每个 agent(智能体),一个 agent 的循环绝不会干扰另一个 agent 的工作,新的用户消息会清零计数。它随 `dsh` base 组合默认启用,在 3、5、8 次重复时提醒。
13
+
14
+ ## 目录
15
+
16
+ - [使用本包](#use-this-package)
17
+ - [理解实现](#understand-the-implementation)
18
+ - [进一步探索](#further-exploration)
19
+ - [模型体验](#model-experience)
20
+ - [已知限制与延期工作](#known-limitations-and-deferred-work)
21
+ - [开发备注](#dev-note)
22
+
23
+ -----
24
+
25
+ <a id="use-this-package"></a>
26
+ ## 使用本包
27
+
28
+ 当模型应当自行发现自己在相同工具调用上循环时,挂载此插件。无需学习或接线:`dsh` base 组合已经运行它,默认值适用于大多数会话——想更早、更晚或在更少的工具上收到提醒时,调优下面的阈值与工具范围即可。
29
+
30
+ ### 何时选择
31
+
32
+ 当模型长时间自主工作、且卡住的循环是你想用建议而非强制来打破的失败模式时,选择它。当相同的重复是合理且必须不受打扰地运行时——guard 只会提醒,提醒只是重复调用之后的一条小消息——以及必须捕获近似变体时(因为只有精确重复——同一工具、同一参数且与属性顺序无关——才会被检测到),避免使用它。
6
33
 
7
- ## 配置
34
+ ### 设置阈值与范围
35
+
36
+ 想改变提醒何时触发或覆盖哪些工具时,用配置挂载插件:
8
37
 
9
38
  ```yaml
10
- - id: repeat-tool-reminder
11
- name: '@deepseek-ai/dsh-repeat-tool-reminder'
39
+ - name: '@deepseek-ai/dsh-repeat-tool-reminder'
12
40
  config:
13
- thresholds: [3, 5, 8] # default; consecutive counts that trigger a reminder
14
- include: [] # tool-name patterns to track; empty ⇒ all tools
15
- exclude: [todo_write] # tool-name patterns transparent to the chain
16
- argumentsPreviewChars: 500 # default; cap on arguments quoted in the detailed reminder
41
+ thresholds: [3, 5, 8] # remind at 3, 5, and 8 consecutive repeats
42
+ include: [] # track every tool; list patterns to track only some
43
+ exclude: [todo_write] # never track these tools
44
+ argumentsPreviewChars: 500 # cap on arguments shown in the detailed reminder
17
45
  ```
18
46
 
19
- 插件加载时,`thresholds` 会对错误配置快速失败:空列表、非整数、小于 2 的值或重复值都会抛出错误,绝不静默回退到默认值;`argumentsPreviewChars` 同样只接受大于等于 1 的整数。系统会将列表按升序规范化;第一个阈值只发送简短的通用提醒,后续每个阈值都会发送详细版本,列出工具、连续次数和规范参数。参数内容截取前 `argumentsPreviewChars` 个字符,并附带省略字符数标记,避免循环中的 `write`/`edit` 载荷无限制进入下一次请求(链键始终比较完整的规范字符串;此上限只约束提醒,不影响检测)。
47
+ | 字段 | 默认值 | 含义 |
48
+ |---|---|---|
49
+ | `thresholds` | `[3, 5, 8]` | 触发提醒的重复次数 |
50
+ | `include` | `[]` | 只跟踪这些工具;空表示所有工具 |
51
+ | `exclude` | `[]` | 绝不跟踪这些工具;对它们的调用既不计数也不重置 |
52
+ | `argumentsPreviewChars` | `500` | 详细提醒中显示多少字符的重复参数 |
53
+
54
+ 无效配置会在启动时以清晰错误失败——空的 `thresholds` 列表、小于 2 的重复次数或重复值——绝不会静默改变行为。生成的[配置目录](../../../docs/config-catalog.zh.md#deepseek-aidsh-repeat-tool-reminder)记录每个受支持的值。
55
+
56
+ ### 你会得到什么
57
+
58
+ 按默认值,以相同参数重复同一调用的模型会在第三次重复时收到简短提醒——先分析上一次结果再调用——并在第五次和第八次收到详细提醒,列出工具与重复参数,使其决定改变方法、收集更多证据还是结束任务。新的用户消息会清零计数,因此全新指令绝不会被当作循环。提醒出现在重复调用的结果之后、归属于插件,模型像阅读任何其他消息一样阅读它。
59
+
60
+ -----
61
+
62
+ <a id="understand-the-implementation"></a>
63
+ ## 理解实现
64
+
65
+ <details>
66
+ <summary>实现细节——点击展开</summary>
67
+
68
+ 本节解释 guard 如何检测重复并投递提醒,并指出实现它的代码位置;可观察行为已在[使用本包](#use-this-package)中完整说明。
69
+
70
+ ### 设计理念
20
71
 
21
- `include`/`exclude` 条目支持 `*` 通配符,并针对调用时实际存在的工具执行谓词判断,而不是引用注册表条目。因此,与当前任何已注册工具都不匹配的模式并非错误(未加载 MCP 工具的部署中,`exclude: [mcp_*]` 仍然有效);这与 `toolOrder` 的引用目标检查不同。
72
+ guard 建立在四项承诺之上:
22
73
 
23
- ## 链语义
74
+ - **仅建议,不否决。** guard 用模型上下文丰富 post-execute 决策;它从不阻止或改写调用,因此 `PostToolDecision` 阻止仍是后续监听器的事。
75
+ - **在 post-execute 中计数。** 检测运行在 `tools/post-execute` 上,被拒绝的调用同样会经过它;在那里计数让一个监听器即可覆盖所有尝试,无需跨事件状态。
76
+ - **精确匹配规范化。** 参数以循环的 `JSON.parse` 输出(或畸形参数 JSON 的原始字符串回退)到达 guard,因此 JSON 的值域就是全部输入域,深度键排序加 `JSON.stringify` 是完整、确定性的同一性判定——不存在 bigint、循环引用或 `undefined` 处理,因为没有输入路径能产生它们。
77
+ - **加载时快速失败。** `thresholds` 与 `argumentsPreviewChars` 在 `apply` 中校验并抛出错误,绝不回退到默认值。
24
78
 
25
- 链键为「`(tool name, canonical arguments)`」:规范化过程会对键进行深度排序,然后执行 `JSON.stringify`,因此仅属性顺序不同的参数对象会视为相同。若某次调用与上一条受跟踪调用相同,该 agent 的连续计数器递增;换成另一条受跟踪调用则重置为 1。
79
+ ### 检测:重复链
26
80
 
27
- - **不受跟踪的调用对链透明。** 被 `include`/`exclude` 排除的调用既不递增计数器,也不重置计数器;因此,`grep X → todo_write → grep X` 仍算作连续两次 `grep X`,即使 `todo_write` 已被排除。这正是排除机制的价值:循环中穿插的记录类工具不能掩盖循环。
28
- - **被拒绝的调用也计数。** 检测位于 `tools/post-execute`;即便调用被 `tools/pre-execute` 监听器拒绝,该事件也会运行。模型反复尝试被拒绝的调用,恰恰是需要打断的循环。
81
+ 每个 agent 的链以「`(tool name, canonical arguments)`」为键——同一工具且规范化后参数相同(忽略属性顺序)的两次调用计为连续,换成另一条受跟踪调用则把计数重置为 1。链保存在 `WeakMap<Agent, Chain>` 中。
82
+
83
+ - **不受跟踪的调用对链透明。** 被 `include`/`exclude` 排除的调用既不递增也不重置计数器,因此 `grep X → todo_write → grep X` 在 `todo_write` 被排除时仍算作连续两次 `grep X`——穿插进循环的记录类工具不能掩盖循环。
84
+ - **被拒绝的调用也计数。** 检测位于 `tools/post-execute`,被 `tools/pre-execute` 监听器拒绝的调用同样会经过它;模型反复尝试被拒绝的调用,恰恰是需要打破的循环。
29
85
  - **忽略没有 agent 的调用。** 直接调用 `ctx.tools.execute()` 的调用方没有需要提醒的模型,也没有可作为键的活跃 agent 对象。
30
- - **按 agent 分键。** 工具注册表位于上下文层级,subagent 会交错通过同一个 waterfall(瀑布式事件),因此每条链使用 `WeakMap<Agent, Chain>`,以活跃 agent 对象为键。一个 agent 的重复调用绝不会触发另一个 agent 的提醒。用户提示词(`agent/pre-step`)会重置提交该提示词的 agent 链;对象生命周期会自然限制弱引用条目的寿命,无需 dispose(资源释放)监听器。
31
- - **仅驻留内存。** 从持久化恢复的会话会从一条全新的链开始:guard 是启发式提醒,并非有日志记录的不变量;提醒会延后,这是可接受的代价。
86
+ - **按 agent 分键,用户提示词时重置。** 一个 agent 的重复绝不会触发另一个 agent 的提醒;用户提示词(`agent/pre-step`)会删除提交该提示词的 agent 链,对象生命周期限制弱引用条目的寿命,无需 dispose(资源释放)监听器。
87
+ - **仅驻留内存。** 从持久化恢复的会话以全新链开始——guard 是启发式提醒,而非记录在案的不变量,因此恢复后的提醒延后是可接受的代价。
88
+
89
+ ### 提醒传递
90
+
91
+ 提醒随 post-execute 决策的 `additionalContexts`(来源为 `{kind: 'plugin', plugin: 'repeat-tool-reminder', form: 'notice', summary: '<tool> × <count>'}`)传递,绝不替换 `content`:用于审计的 `tool/result` 事件仍保留工具自己的输出。循环会缓冲这段上下文,并在该步骤的工具结果之后作为注入的 `user/message` 追加,会话将其渲染为普通的合成用户消息——模型可见、带有来源归属,且无需新会话事件即可从会话日志重建。guard 始终通过 `next()` 委派,并把提醒放在下游决策的上下文数组之前,因此两种决策变体(包括被阻止的调用)都会收到提醒,同时每个条目保留自己的来源与元数据。
92
+
93
+ ### 源码地图
94
+
95
+ | 文件 | 职责 |
96
+ |---|---|
97
+ | [`src/index.ts`](src/index.ts) | 插件入口:`Config` schema、快速失败校验、链监听器 |
98
+ | [`src/invariant.ts`](src/invariant.ts) | 不变式伴生插件(无运行时不变式:链私有于一个 post-execute 监听器) |
99
+
100
+ </details>
101
+
102
+ -----
103
+
104
+ <a id="further-exploration"></a>
105
+ ## 进一步探索
106
+
107
+ 当包级约定不够用时阅读以下页面。它们从工具 waterfall 逐步进入穷尽式配置与 guard 组映射。
32
108
 
33
- ## 提醒传递
109
+ - [工具子系统参考](../../../docs/subsystems/tools.zh.md)——本 guard 消费的 `tools/execute` waterfall、`additionalContexts` 与决策形态。
110
+ - [生成配置目录](../../../docs/config-catalog.zh.md#deepseek-aidsh-repeat-tool-reminder)——每个受支持配置字段及其源声明。
111
+ - [guard 组映射](../README.zh.md)——同组的 guard 包与循环卫生家族。
34
112
 
35
- 提醒通过 post-execute 决策中的 `additionalContexts`(来源为 `{kind: 'plugin', plugin: 'repeat-tool-reminder'}`)传递,绝不替换 `content`;用于审计的 `tool/result` 事件仍保留工具自己的输出。循环会缓冲这段上下文,并在该步骤的工具结果之后将其作为注入的 `user/message` 追加;会话会将它渲染为普通的合成用户消息。因此,提醒对模型可见、带有来源归属,并且无需增加会话事件即可从会话日志重建。guard 始终通过 `next()` 委派,并将自己的提醒放在下游决策的上下文数组之前(两种结果都适用:被阻止的调用也会收到提醒);每个条目保留自己的来源和元数据。
113
+ -----
36
114
 
115
+ <a id="model-experience"></a>
37
116
  ## 模型体验
38
117
 
39
118
  ### 首个阈值的上下文消息
40
119
 
41
- #### 模型看到的内容
120
+ #### 模型看到什么
42
121
 
43
- 达到第一个配置的连续重复阈值时,对应 agent 会收到以下提醒。系统不会添加工具 schema 或正常调用文本。
122
+ 达到第一个配置的连续重复阈值时,对应 agent 会收到下面的提醒。不会添加工具 schema 或正常调用文本。
44
123
 
45
124
  ##### 首个阈值提醒
46
125
 
@@ -58,9 +137,9 @@ You are repeating the exact same tool call with identical arguments. Carefully a
58
137
 
59
138
  ### 后续阈值的上下文消息
60
139
 
61
- #### 模型看到的内容
140
+ #### 模型看到什么
62
141
 
63
- 达到后续阈值时,agent 会收到以下详细提醒模板。受上限约束的参数预览严格以 `… (+<omitted> more chars)` 结尾。
142
+ 达到后续阈值时,agent 会收到下面的详细提醒模板。受上限约束的参数预览严格以 `… (+<omitted> more chars)` 结尾。
64
143
 
65
144
  ##### 后续阈值提醒
66
145
 
@@ -74,17 +153,34 @@ The repeated calls are not making progress. Do not call this tool with these exa
74
153
 
75
154
  #### Token 影响
76
155
 
77
- 每条提醒都会作为历史记录保留;`argumentsPreviewChars` 会限制随数据变化的参数文本长度,而各 agent 仍使用独立计数器。
156
+ 每条提醒都会作为历史记录保留;`argumentsPreviewChars` 限制随数据变化的参数文本长度,而各 agent 仍使用独立计数器。
78
157
 
79
158
  #### KV Cache 影响
80
159
 
81
160
  仅追加;新出现的内容位于可复用请求前缀之后,不会使现有 KV Cache 条目失效。
82
161
 
83
- ## 已知限制与暂缓事项
162
+ ## 已知限制与延期工作
163
+
164
+ <a id="known-limitations-and-deferred-work"></a>
165
+
166
+
167
+ 这些限制说明 guard 何时不合适。它们是当前包约束,不是任务积压。
168
+
169
+ - **仅精确匹配检测**——规范化是深度键排序,因此近似变体(稍作修改的路径、值内多余的空白)会绕过链;在没有需求证据前,不采用模糊匹配。
170
+ - **压缩(compaction)不会重置链**——跨越压缩检查点的链会继续计数。
171
+ - **仅提供建议**——尚未实现高阈值时升级为阻止形式,但 `PostToolDecision` 已支持阻止。
172
+ - **subagent 之间不共享链**——链始终按 agent 隔离;父 agent 与其 subagent 重复相同调用也绝不合并。
173
+ - **合理的幂等轮询超过阈值后仍会收到提醒**——可通过 `thresholds`/`exclude` 配置释放压力。
174
+ - **超过最高阈值后链不再提醒**——提醒只在精确达到所配置的次数时触发,超过后不会继续发送。
175
+
176
+ <a id="dev-note"></a>
177
+ ### 开发备注
178
+
179
+ <details>
180
+ <summary>维护者的工作上下文——点击展开</summary>
181
+
182
+ 本开发备注是维护者的工作上下文:开放问题与尚未决定的探索方向。它明确不具权威性——已交付的行为、限制与既定理由以上文、包代码和相关 Agent Note 为准。
183
+
184
+ [repeat-tool-guard Agent Note](../../../.agents/notes/archived/feature/2026-07-08-repeat-tool-guard.md) 以旧包名记录了原始设计与备选方案;[改名台账](../../../.agents/notes/implemented/architecture/2026-08-11-repository-naming-contract-and-rename-ledger.zh.md) 记录了改名为 `repeat-tool-reminder` 及其原因。
84
185
 
85
- - **仅检测精确匹配**:规范化过程会对键进行深度排序,因此近似变体(稍作修改的路径、值内增加的空白)可以绕过链;在没有需求证据前,不采用模糊匹配。
86
- - **压缩(compaction)不会重置链**:跨越压缩检查点的链会继续计数。
87
- - **仅提供建议**:尚未实现达到较高阈值后升级为 `block`,但 `PostToolDecision` 已支持阻止调用。
88
- - **subagent 之间不共享链**:链始终按 agent 隔离;即使父 agent 与其 subagent 重复相同调用,也不会合并计数。
89
- - **合理的幂等轮询超过阈值后仍会收到提醒**:可通过 `thresholds`/`exclude` 配置释放压力。
90
- - **超过最高阈值后链不再提醒**:提醒只在精确达到所配置的次数时触发,超过后不会继续发送。
186
+ </details>