@deepseek-ai/dsh-repeat-tool-reminder 0.0.1-rc.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/LICENSE +28 -0
- package/README.i18n.yaml +6 -0
- package/README.md +90 -0
- package/README.zh.md +90 -0
- package/lib/index.js +323 -0
- package/lib/invariant.js +23 -0
- package/lib/types/index.d.ts +43 -0
- package/lib/types/invariant.d.ts +16 -0
- package/package.json +53 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
BSD 3-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026, DeepSeek
|
|
4
|
+
|
|
5
|
+
Redistribution and use in source and binary forms, with or without
|
|
6
|
+
modification, are permitted provided that the following conditions are met:
|
|
7
|
+
|
|
8
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
9
|
+
list of conditions and the following disclaimer.
|
|
10
|
+
|
|
11
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
12
|
+
this list of conditions and the following disclaimer in the documentation
|
|
13
|
+
and/or other materials provided with the distribution.
|
|
14
|
+
|
|
15
|
+
3. Neither the name of the copyright holder nor the names of its
|
|
16
|
+
contributors may be used to endorse or promote products derived from
|
|
17
|
+
this software without specific prior written permission.
|
|
18
|
+
|
|
19
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
20
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
21
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
22
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
23
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
24
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
25
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
26
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
27
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
28
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
package/README.i18n.yaml
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
|
2
|
+
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
|
3
|
+
# after editing either side, bring the other along and re-record with:
|
|
4
|
+
# pnpm run verify-translation-pairing --write packages/guard/repeat-tool-reminder/README.md
|
|
5
|
+
README.md: cfece8150dce8e2fe45b4e942bf7188f46836551
|
|
6
|
+
README.zh.md: 439480d876ad07d8bc095b2534a5a2ce732bfc9b
|
package/README.md
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# @deepseek-ai/dsh-repeat-tool-reminder
|
|
2
|
+
|
|
3
|
+
English | [中文](README.zh.md)
|
|
4
|
+
|
|
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).
|
|
6
|
+
|
|
7
|
+
## Config
|
|
8
|
+
|
|
9
|
+
```yaml
|
|
10
|
+
- id: repeat-tool-reminder
|
|
11
|
+
name: '@deepseek-ai/dsh-repeat-tool-reminder'
|
|
12
|
+
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
|
|
17
|
+
```
|
|
18
|
+
|
|
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).
|
|
20
|
+
|
|
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.
|
|
22
|
+
|
|
23
|
+
## Chain semantics
|
|
24
|
+
|
|
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.
|
|
26
|
+
|
|
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.
|
|
29
|
+
- **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.
|
|
32
|
+
|
|
33
|
+
## Reminder delivery
|
|
34
|
+
|
|
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.
|
|
36
|
+
|
|
37
|
+
## Model Experience
|
|
38
|
+
|
|
39
|
+
### First-threshold context message
|
|
40
|
+
|
|
41
|
+
#### What the model sees
|
|
42
|
+
|
|
43
|
+
At the first configured consecutive-repeat threshold, that agent receives the reminder below. No tool schema or normal-call text is added.
|
|
44
|
+
|
|
45
|
+
##### First-threshold reminder
|
|
46
|
+
|
|
47
|
+
```markdown
|
|
48
|
+
You are repeating the exact same tool call with identical arguments. Carefully analyze the previous result before calling again: if the task is not complete, try a different approach or different arguments instead of repeating the call.
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
#### Token effect
|
|
52
|
+
|
|
53
|
+
Zero tokens before the threshold. The reminder is retained history for that agent.
|
|
54
|
+
|
|
55
|
+
#### KV Cache effect
|
|
56
|
+
|
|
57
|
+
Append-only; newly visible content follows the reusable request prefix and does not invalidate existing KV-cache entries.
|
|
58
|
+
|
|
59
|
+
### Later-threshold context message
|
|
60
|
+
|
|
61
|
+
#### What the model sees
|
|
62
|
+
|
|
63
|
+
A later threshold receives the detailed reminder template below. A capped argument preview ends exactly `… (+<omitted> more chars)`.
|
|
64
|
+
|
|
65
|
+
##### Later-threshold reminder
|
|
66
|
+
|
|
67
|
+
```markdown
|
|
68
|
+
Repeated tool call detected:
|
|
69
|
+
- tool: <toolName>
|
|
70
|
+
- consecutive_calls: <count>
|
|
71
|
+
- arguments: <canonicalArguments>
|
|
72
|
+
The repeated calls are not making progress. Do not call this tool with these exact arguments again. Inspect the latest result and choose a different action, different arguments, or finish the task if enough evidence has been gathered.
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
#### Token effect
|
|
76
|
+
|
|
77
|
+
Each reminder is retained history; `argumentsPreviewChars` bounds its data-dependent argument text, while agents keep independent counters.
|
|
78
|
+
|
|
79
|
+
#### KV Cache effect
|
|
80
|
+
|
|
81
|
+
Append-only; newly visible content follows the reusable request prefix and does not invalidate existing KV-cache entries.
|
|
82
|
+
|
|
83
|
+
## Known Limitations and Deferred Work
|
|
84
|
+
|
|
85
|
+
- **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
|
+
- **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.
|
|
88
|
+
- **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.
|
|
90
|
+
- **Past the highest threshold a chain goes silent** — reminders fire only at exact configured counts, never beyond them.
|
package/README.zh.md
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# @deepseek-ai/dsh-repeat-tool-reminder
|
|
2
|
+
|
|
3
|
+
[English](README.md) | 中文
|
|
4
|
+
|
|
5
|
+
这是一个仅提供建议的循环中断器,而非面向模型的工具:它不会出现在工具列表中,不会否决或改写调用,只增加一种行为。它监视每个 agent(智能体)的工具调用流,统计以完全相同的规范化参数连续调用同一工具的次数;达到所配置的连续次数时,它会注入逐级增强的提示,要求模型停止重复、重新阅读上一次结果,并改用其他方案或结束任务。究竟是换一种方式重试、收集更多证据还是完成任务,仍完全由模型决定:合理的重复调用既不会延迟,也不会受阻。决策记录见 [repeat-tool-reminder Agent Note](../../../.agents/notes/archived/feature/2026-07-08-repeat-tool-guard.md)。
|
|
6
|
+
|
|
7
|
+
## 配置
|
|
8
|
+
|
|
9
|
+
```yaml
|
|
10
|
+
- id: repeat-tool-reminder
|
|
11
|
+
name: '@deepseek-ai/dsh-repeat-tool-reminder'
|
|
12
|
+
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
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
插件加载时,`thresholds` 会对错误配置快速失败:空列表、非整数、小于 2 的值或重复值都会抛出错误,绝不静默回退到默认值;`argumentsPreviewChars` 同样只接受大于等于 1 的整数。系统会将列表按升序规范化;第一个阈值只发送简短的通用提醒,后续每个阈值都会发送详细版本,列出工具、连续次数和规范参数。参数内容截取前 `argumentsPreviewChars` 个字符,并附带省略字符数标记,避免循环中的 `write`/`edit` 载荷无限制进入下一次请求(链键始终比较完整的规范字符串;此上限只约束提醒,不影响检测)。
|
|
20
|
+
|
|
21
|
+
`include`/`exclude` 条目支持 `*` 通配符,并针对调用时实际存在的工具执行谓词判断,而不是引用注册表条目。因此,与当前任何已注册工具都不匹配的模式并非错误(未加载 MCP 工具的部署中,`exclude: [mcp_*]` 仍然有效);这与 `toolOrder` 的引用目标检查不同。
|
|
22
|
+
|
|
23
|
+
## 链语义
|
|
24
|
+
|
|
25
|
+
链键为「`(tool name, canonical arguments)`」:规范化过程会对键进行深度排序,然后执行 `JSON.stringify`,因此仅属性顺序不同的参数对象会视为相同。若某次调用与上一条受跟踪调用相同,该 agent 的连续计数器递增;换成另一条受跟踪调用则重置为 1。
|
|
26
|
+
|
|
27
|
+
- **不受跟踪的调用对链透明。** 被 `include`/`exclude` 排除的调用既不递增计数器,也不重置计数器;因此,`grep X → todo_write → grep X` 仍算作连续两次 `grep X`,即使 `todo_write` 已被排除。这正是排除机制的价值:循环中穿插的记录类工具不能掩盖循环。
|
|
28
|
+
- **被拒绝的调用也计数。** 检测位于 `tools/post-execute`;即便调用被 `tools/pre-execute` 监听器拒绝,该事件也会运行。模型反复尝试被拒绝的调用,恰恰是需要打断的循环。
|
|
29
|
+
- **忽略没有 agent 的调用。** 直接调用 `ctx.tools.execute()` 的调用方没有需要提醒的模型,也没有可作为键的活跃 agent 对象。
|
|
30
|
+
- **按 agent 分键。** 工具注册表位于上下文层级,subagent 会交错通过同一个 waterfall(瀑布式事件),因此每条链使用 `WeakMap<Agent, Chain>`,以活跃 agent 对象为键。一个 agent 的重复调用绝不会触发另一个 agent 的提醒。用户提示词(`agent/pre-step`)会重置提交该提示词的 agent 链;对象生命周期会自然限制弱引用条目的寿命,无需 dispose(资源释放)监听器。
|
|
31
|
+
- **仅驻留内存。** 从持久化恢复的会话会从一条全新的链开始:guard 是启发式提醒,并非有日志记录的不变量;提醒会延后,这是可接受的代价。
|
|
32
|
+
|
|
33
|
+
## 提醒传递
|
|
34
|
+
|
|
35
|
+
提醒通过 post-execute 决策中的 `additionalContexts`(来源为 `{kind: 'plugin', plugin: 'repeat-tool-reminder'}`)传递,绝不替换 `content`;用于审计的 `tool/result` 事件仍保留工具自己的输出。循环会缓冲这段上下文,并在该步骤的工具结果之后将其作为注入的 `user/message` 追加;会话会将它渲染为普通的合成用户消息。因此,提醒对模型可见、带有来源归属,并且无需增加会话事件即可从会话日志重建。guard 始终通过 `next()` 委派,并将自己的提醒放在下游决策的上下文数组之前(两种结果都适用:被阻止的调用也会收到提醒);每个条目保留自己的来源和元数据。
|
|
36
|
+
|
|
37
|
+
## 模型体验
|
|
38
|
+
|
|
39
|
+
### 首个阈值的上下文消息
|
|
40
|
+
|
|
41
|
+
#### 模型看到的内容
|
|
42
|
+
|
|
43
|
+
达到第一个配置的连续重复阈值时,对应 agent 会收到以下提醒。系统不会添加工具 schema 或正常调用文本。
|
|
44
|
+
|
|
45
|
+
##### 首个阈值提醒
|
|
46
|
+
|
|
47
|
+
```markdown
|
|
48
|
+
You are repeating the exact same tool call with identical arguments. Carefully analyze the previous result before calling again: if the task is not complete, try a different approach or different arguments instead of repeating the call.
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
#### Token 影响
|
|
52
|
+
|
|
53
|
+
达到阈值前为零 token。提醒会作为该 agent 的历史记录保留。
|
|
54
|
+
|
|
55
|
+
#### KV Cache 影响
|
|
56
|
+
|
|
57
|
+
仅追加;新出现的内容位于可复用请求前缀之后,不会使现有 KV Cache 条目失效。
|
|
58
|
+
|
|
59
|
+
### 后续阈值的上下文消息
|
|
60
|
+
|
|
61
|
+
#### 模型看到的内容
|
|
62
|
+
|
|
63
|
+
达到后续阈值时,agent 会收到以下详细提醒模板。受上限约束的参数预览严格以 `… (+<omitted> more chars)` 结尾。
|
|
64
|
+
|
|
65
|
+
##### 后续阈值提醒
|
|
66
|
+
|
|
67
|
+
```markdown
|
|
68
|
+
Repeated tool call detected:
|
|
69
|
+
- tool: <toolName>
|
|
70
|
+
- consecutive_calls: <count>
|
|
71
|
+
- arguments: <canonicalArguments>
|
|
72
|
+
The repeated calls are not making progress. Do not call this tool with these exact arguments again. Inspect the latest result and choose a different action, different arguments, or finish the task if enough evidence has been gathered.
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
#### Token 影响
|
|
76
|
+
|
|
77
|
+
每条提醒都会作为历史记录保留;`argumentsPreviewChars` 会限制随数据变化的参数文本长度,而各 agent 仍使用独立计数器。
|
|
78
|
+
|
|
79
|
+
#### KV Cache 影响
|
|
80
|
+
|
|
81
|
+
仅追加;新出现的内容位于可复用请求前缀之后,不会使现有 KV Cache 条目失效。
|
|
82
|
+
|
|
83
|
+
## 已知限制与暂缓事项
|
|
84
|
+
|
|
85
|
+
- **仅检测精确匹配**:规范化过程会对键进行深度排序,因此近似变体(稍作修改的路径、值内增加的空白)可以绕过链;在没有需求证据前,不采用模糊匹配。
|
|
86
|
+
- **压缩(compaction)不会重置链**:跨越压缩检查点的链会继续计数。
|
|
87
|
+
- **仅提供建议**:尚未实现达到较高阈值后升级为 `block`,但 `PostToolDecision` 已支持阻止调用。
|
|
88
|
+
- **subagent 之间不共享链**:链始终按 agent 隔离;即使父 agent 与其 subagent 重复相同调用,也不会合并计数。
|
|
89
|
+
- **合理的幂等轮询超过阈值后仍会收到提醒**:可通过 `thresholds`/`exclude` 配置释放压力。
|
|
90
|
+
- **超过最高阈值后链不再提醒**:提醒只在精确达到所配置的次数时触发,超过后不会继续发送。
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import z from "@deepseek-ai/schemastery";
|
|
3
|
+
import "@deepseek-ai/cordis";
|
|
4
|
+
//#region ../../llm/llm/src/brand.ts
|
|
5
|
+
/**
|
|
6
|
+
* Brand a message identifier.
|
|
7
|
+
* @param id - the opaque message identifier.
|
|
8
|
+
* @returns the same string, branded; no validation is performed.
|
|
9
|
+
*/
|
|
10
|
+
function MessageId(id) {
|
|
11
|
+
return id;
|
|
12
|
+
}
|
|
13
|
+
//#endregion
|
|
14
|
+
//#region ../../llm/llm/src/call-config.ts
|
|
15
|
+
/**
|
|
16
|
+
* Deep-freeze a value in place with an iterative traversal, guarding cycles,
|
|
17
|
+
* so later mutation throws without imposing a JavaScript call-stack depth cap.
|
|
18
|
+
* {@link AbortSignal} objects are deliberately skipped because they are the
|
|
19
|
+
* request's live cancellation channel and freezing them breaks abort.
|
|
20
|
+
* @param value - the value to freeze in place.
|
|
21
|
+
* @returns the same value, frozen.
|
|
22
|
+
*/
|
|
23
|
+
function deepFreeze(value) {
|
|
24
|
+
const seen = /* @__PURE__ */ new WeakSet();
|
|
25
|
+
const pending = [{
|
|
26
|
+
kind: "visit",
|
|
27
|
+
node: value
|
|
28
|
+
}];
|
|
29
|
+
while (pending.length > 0) {
|
|
30
|
+
const task = pending.pop();
|
|
31
|
+
/* v8 ignore next -- the loop condition guarantees one pending task. */
|
|
32
|
+
if (task === void 0) continue;
|
|
33
|
+
if (task.kind === "property") {
|
|
34
|
+
pending.push({
|
|
35
|
+
kind: "visit",
|
|
36
|
+
node: task.source[task.key]
|
|
37
|
+
});
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
const node = task.node;
|
|
41
|
+
if (node === null || typeof node !== "object") continue;
|
|
42
|
+
if (node instanceof AbortSignal) continue;
|
|
43
|
+
if (seen.has(node)) continue;
|
|
44
|
+
seen.add(node);
|
|
45
|
+
Object.freeze(node);
|
|
46
|
+
const keys = Object.keys(node);
|
|
47
|
+
for (let index = keys.length - 1; index >= 0; index--) {
|
|
48
|
+
const key = keys[index];
|
|
49
|
+
/* v8 ignore next -- the loop is bounded by the captured key count. */
|
|
50
|
+
if (key === void 0) continue;
|
|
51
|
+
pending.push({
|
|
52
|
+
kind: "property",
|
|
53
|
+
source: node,
|
|
54
|
+
key
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return value;
|
|
59
|
+
}
|
|
60
|
+
//#endregion
|
|
61
|
+
//#region ../../llm/llm/src/message.ts
|
|
62
|
+
/** Message value types, identity, and immutable construction helpers. */
|
|
63
|
+
/**
|
|
64
|
+
* Detach and deep-freeze a message whose identity already exists.
|
|
65
|
+
* @param message - complete message, including its stable identity.
|
|
66
|
+
* @returns an immutable snapshot that preserves the identity.
|
|
67
|
+
*/
|
|
68
|
+
function freezeMessage(message) {
|
|
69
|
+
return deepFreeze(structuredClone(message));
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Create one identified message and freeze it before publication.
|
|
73
|
+
* @param input - complete role, content, and source for a new message.
|
|
74
|
+
* @returns an immutable message with a fresh stable identity.
|
|
75
|
+
*/
|
|
76
|
+
function createMessage(input) {
|
|
77
|
+
return freezeMessage({
|
|
78
|
+
...input,
|
|
79
|
+
id: MessageId(crypto.randomUUID())
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Create one identified user-role message and freeze it before publication.
|
|
84
|
+
* @param input - complete content and source for a new user message.
|
|
85
|
+
* @returns an immutable user message with a fresh stable identity.
|
|
86
|
+
*/
|
|
87
|
+
function createUserMessage(input) {
|
|
88
|
+
return createMessage({
|
|
89
|
+
...input,
|
|
90
|
+
role: "user"
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
//#endregion
|
|
94
|
+
//#region ../../util/timeout/src/index.ts
|
|
95
|
+
/** Largest delay Node schedules without clamping it to one millisecond. */
|
|
96
|
+
const MAX_TIMER_DELAY_MS = 2147483647;
|
|
97
|
+
//#endregion
|
|
98
|
+
//#region ../../llm/llm/src/error.ts
|
|
99
|
+
/**
|
|
100
|
+
* Canonical provider-neutral code for a response that completed normally but
|
|
101
|
+
* carried no content blocks at all. Providers occasionally emit a degenerate
|
|
102
|
+
* completion (a terminal stop with zero output); adapters classify it as this
|
|
103
|
+
* failure instead of yielding an empty assistant message, because an empty
|
|
104
|
+
* message silently ends the turn with nothing for the user or the loop to act
|
|
105
|
+
* on. The attempt produced nothing durable, so retry policy treats it as safe
|
|
106
|
+
* to repeat.
|
|
107
|
+
*/
|
|
108
|
+
const EMPTY_RESPONSE_CODE = "EMPTY_RESPONSE";
|
|
109
|
+
new RegExp(String.raw`(?:^|[^a-z0-9])context[\s_-](?:length|window)[\s_-]` + String.raw`(?:exceed(?:ed|s)?|overflow(?:ed)?|limit[\s_-]exceeded)(?:$|[^a-z0-9])`, "i");
|
|
110
|
+
new RegExp(String.raw`\b(?:request|prompt|input|messages?)\s+(?:is\s+|are\s+)?` + String.raw`too\s+(?:large|long)\s+for\s+(?:(?:this|the)\s+)?` + String.raw`(?:model(?:'s)?\s+)?context(?:\s+window)?\b`, "i");
|
|
111
|
+
new RegExp(String.raw`\b(?:input|prompt|request|messages?)\b.{0,40}` + String.raw`\b(?:exceed(?:s|ed)?|overflows?|is\s+larger\s+than)\b.{0,40}` + String.raw`\b(?:the\s+)?(?:model(?:'s)?\s+)?context(?:\s+(?:length|window))?\b`, "i");
|
|
112
|
+
//#endregion
|
|
113
|
+
//#region ../../llm/llm/src/retry-policy.ts
|
|
114
|
+
/**
|
|
115
|
+
* Provider-owned request-retry policy configuration and resolution.
|
|
116
|
+
*
|
|
117
|
+
* Adapters expose one resolved policy per registered provider route; the
|
|
118
|
+
* optional dsh-llm-retry plugin executes it on the agent's failed-step extension point.
|
|
119
|
+
*
|
|
120
|
+
* @module @deepseek-ai/dsh-llm/retry-policy
|
|
121
|
+
*/
|
|
122
|
+
const DEFAULT_MAX_RETRIES = 2;
|
|
123
|
+
const DEFAULT_INITIAL_DELAY_MS = 500;
|
|
124
|
+
const DEFAULT_MAX_DELAY_MS = 1e4;
|
|
125
|
+
const DEFAULT_JITTER_RATIO = .1;
|
|
126
|
+
const DEFAULT_RETRYABLE_CODES = Object.freeze([
|
|
127
|
+
EMPTY_RESPONSE_CODE,
|
|
128
|
+
"RATE_LIMIT",
|
|
129
|
+
"SERVER",
|
|
130
|
+
"TIMEOUT",
|
|
131
|
+
"TRANSPORT"
|
|
132
|
+
]);
|
|
133
|
+
const backoffSchema = z.object({
|
|
134
|
+
initialDelayMs: z.number().max(MAX_TIMER_DELAY_MS).default(DEFAULT_INITIAL_DELAY_MS),
|
|
135
|
+
maxDelayMs: z.number().max(MAX_TIMER_DELAY_MS).default(DEFAULT_MAX_DELAY_MS),
|
|
136
|
+
jitterRatio: z.number().min(0).max(1).default(DEFAULT_JITTER_RATIO)
|
|
137
|
+
});
|
|
138
|
+
const normalPolicySchema = z.object({
|
|
139
|
+
mode: z.const("normal").required(),
|
|
140
|
+
maxRetries: z.number().step(1).min(0).max(Number.MAX_SAFE_INTEGER).default(DEFAULT_MAX_RETRIES),
|
|
141
|
+
retryableCodes: z.array(z.string()).default([...DEFAULT_RETRYABLE_CODES]),
|
|
142
|
+
backoff: backoffSchema
|
|
143
|
+
});
|
|
144
|
+
const alwaysPolicySchema = z.object({
|
|
145
|
+
mode: z.const("always").required(),
|
|
146
|
+
backoff: backoffSchema
|
|
147
|
+
});
|
|
148
|
+
z.union([normalPolicySchema, alwaysPolicySchema]);
|
|
149
|
+
//#endregion
|
|
150
|
+
//#region ../../llm/llm/src/attribution.ts
|
|
151
|
+
/**
|
|
152
|
+
* Centralize the non-secret product identity every provider request sends as `User-Agent`, keeping
|
|
153
|
+
* adapters from drifting. See
|
|
154
|
+
* `.agents/notes/implemented/architecture/2026-06-21-mandatory-app-attribution-headers.md`.
|
|
155
|
+
*
|
|
156
|
+
* App-attribution vocabulary for provider requests.
|
|
157
|
+
* @module @deepseek-ai/dsh-llm/attribution
|
|
158
|
+
*/
|
|
159
|
+
const { version } = createRequire(import.meta.url)("../package.json");
|
|
160
|
+
//#endregion
|
|
161
|
+
//#region lib/types/index.js
|
|
162
|
+
/**
|
|
163
|
+
* Advisory per-agent repeat-call detector. It enriches post-execute decisions
|
|
164
|
+
* with logged model context without vetoing or rewriting calls. Configuration
|
|
165
|
+
* and chain semantics live in the package README; rationale lives in the
|
|
166
|
+
* repeat-tool-reminder Agent Note.
|
|
167
|
+
* @module @deepseek-ai/dsh-repeat-tool-reminder
|
|
168
|
+
*/
|
|
169
|
+
const name = "repeat-tool-reminder";
|
|
170
|
+
const Config = z.object({
|
|
171
|
+
thresholds: z.array(z.number()).default([
|
|
172
|
+
3,
|
|
173
|
+
5,
|
|
174
|
+
8
|
|
175
|
+
]),
|
|
176
|
+
include: z.array(z.string()).default([]),
|
|
177
|
+
exclude: z.array(z.string()).default([]),
|
|
178
|
+
argumentsPreviewChars: z.number().default(500)
|
|
179
|
+
});
|
|
180
|
+
/**
|
|
181
|
+
* The `{kind:'plugin'}` source stamped on every reminder this guard injects —
|
|
182
|
+
* the label is load-bearing (an unlabeled context would render as a user
|
|
183
|
+
* prompt in derived history).
|
|
184
|
+
*/
|
|
185
|
+
const PLUGIN_SOURCE = {
|
|
186
|
+
kind: "plugin",
|
|
187
|
+
plugin: "repeat-tool-reminder"
|
|
188
|
+
};
|
|
189
|
+
/**
|
|
190
|
+
* The gentle first-threshold reminder. Keyed to `thresholds[0]`, not a literal
|
|
191
|
+
* count, so a custom first threshold keeps the gentle-then-detailed escalation.
|
|
192
|
+
*/
|
|
193
|
+
const GENTLE_REMINDER = "You are repeating the exact same tool call with identical arguments. Carefully analyze the previous result before calling again: if the task is not complete, try a different approach or different arguments instead of repeating the call.";
|
|
194
|
+
/** The detailed later-threshold reminder naming the tool, the run length, and the canonical arguments. */
|
|
195
|
+
function detailedReminder(toolName, count, canonicalArguments) {
|
|
196
|
+
return `Repeated tool call detected:
|
|
197
|
+
- tool: ${toolName}\n- consecutive_calls: ${count}\n- arguments: ${canonicalArguments}\nThe repeated calls are not making progress. Do not call this tool with these exact arguments again. Inspect the latest result and choose a different action, different arguments, or finish the task if enough evidence has been gathered.`;
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Deep key-sort of a parsed-JSON value so two argument objects that differ
|
|
201
|
+
* only in property order canonicalize identically. Arguments reach the guard
|
|
202
|
+
* as the loop's `JSON.parse` output (or its raw-string fallback for malformed
|
|
203
|
+
* argument JSON), so JSON's value domain is the whole input domain — no
|
|
204
|
+
* bigint, cycle, or `undefined` handling exists because no input path can
|
|
205
|
+
* produce them.
|
|
206
|
+
*/
|
|
207
|
+
function sortJsonValue(value) {
|
|
208
|
+
if (Array.isArray(value)) return value.map(sortJsonValue);
|
|
209
|
+
if (value !== null && typeof value === "object") {
|
|
210
|
+
const record = value;
|
|
211
|
+
const sorted = {};
|
|
212
|
+
for (const key of Object.keys(record).sort()) sorted[key] = sortJsonValue(record[key]);
|
|
213
|
+
return sorted;
|
|
214
|
+
}
|
|
215
|
+
return value;
|
|
216
|
+
}
|
|
217
|
+
/** Canonical string form of a call's arguments: deep key-sort, then stringify. */
|
|
218
|
+
function canonicalize(argumentsValue) {
|
|
219
|
+
return JSON.stringify(sortJsonValue(argumentsValue));
|
|
220
|
+
}
|
|
221
|
+
/** Compile one `*`-wildcard pattern to an anchored RegExp (every other regex metacharacter is matched literally). */
|
|
222
|
+
function wildcardToRegExp(pattern) {
|
|
223
|
+
const escaped = pattern.replace(/[|\\{}()[\]^$+?.]/g, String.raw`\$&`);
|
|
224
|
+
return new RegExp(`^${escaped.replaceAll("*", ".*")}$`);
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Head-truncate the canonical arguments for quoting in the detailed reminder,
|
|
228
|
+
* marking how much was omitted. Bounds only the model-visible text — the
|
|
229
|
+
* chain key always uses the full canonical string.
|
|
230
|
+
*/
|
|
231
|
+
function previewArguments(canonical, cap) {
|
|
232
|
+
if (canonical.length <= cap) return canonical;
|
|
233
|
+
return `${canonical.slice(0, cap)}… (+${canonical.length - cap} more chars)`;
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Validate `thresholds` per the fail-loud contract and return them sorted
|
|
237
|
+
* ascending (the escalation rule reads `thresholds[0]` as the gentle tier, so
|
|
238
|
+
* order is normalized here, once).
|
|
239
|
+
*/
|
|
240
|
+
function validateThresholds(values) {
|
|
241
|
+
if (values.length === 0) throw new Error("repeat-tool-reminder: `thresholds` must not be empty");
|
|
242
|
+
for (const value of values) if (!Number.isInteger(value) || value < 2) throw new Error(`repeat-tool-reminder: invalid threshold ${value} — every threshold must be an integer >= 2`);
|
|
243
|
+
if (new Set(values).size !== values.length) throw new Error("repeat-tool-reminder: `thresholds` must not contain duplicates");
|
|
244
|
+
return [...values].sort((a, b) => a - b);
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Prepend the guard's reminder while preserving every downstream context's
|
|
248
|
+
* source and metadata.
|
|
249
|
+
*/
|
|
250
|
+
function prependContext(ours, theirs) {
|
|
251
|
+
return [ours, ...theirs ?? []];
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* Install the guard's listeners.
|
|
255
|
+
* @param ctx - plugin context; listeners are scoped to it and disposed with it.
|
|
256
|
+
* @param config - validated {@link Config}; `thresholds` is re-checked fail-loud here.
|
|
257
|
+
*/
|
|
258
|
+
function apply(ctx, config) {
|
|
259
|
+
const thresholds = validateThresholds(config.thresholds);
|
|
260
|
+
const thresholdSet = new Set(thresholds);
|
|
261
|
+
const includePatterns = config.include.map(wildcardToRegExp);
|
|
262
|
+
const excludePatterns = config.exclude.map(wildcardToRegExp);
|
|
263
|
+
const argumentsPreviewChars = config.argumentsPreviewChars;
|
|
264
|
+
if (!Number.isInteger(argumentsPreviewChars) || argumentsPreviewChars < 1) throw new Error(`repeat-tool-reminder: invalid argumentsPreviewChars ${argumentsPreviewChars} — must be an integer >= 1`);
|
|
265
|
+
const chains = /* @__PURE__ */ new WeakMap();
|
|
266
|
+
/** Whether a tool participates in the chain (untracked calls are transparent: they neither count nor reset). */
|
|
267
|
+
function tracked(toolName) {
|
|
268
|
+
if (includePatterns.length > 0 && !includePatterns.some((pattern) => pattern.test(toolName))) return false;
|
|
269
|
+
return !excludePatterns.some((pattern) => pattern.test(toolName));
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Advance the calling agent's chain for one attempt and return the reminder
|
|
273
|
+
* to deliver, if this attempt's run length hits a configured threshold.
|
|
274
|
+
* Counting happens here — in post-execute — because denied calls also flow
|
|
275
|
+
* through this waterfall (`ToolRuntime.execute` routes a deny through the
|
|
276
|
+
* same pipeline), and a model hammering a denied call is exactly the loop
|
|
277
|
+
* worth breaking.
|
|
278
|
+
*/
|
|
279
|
+
function observe(exec) {
|
|
280
|
+
if (!exec.agent) return void 0;
|
|
281
|
+
if (!tracked(exec.name)) return void 0;
|
|
282
|
+
const canonical = canonicalize(exec.arguments);
|
|
283
|
+
const key = JSON.stringify([exec.name, canonical]);
|
|
284
|
+
const chain = chains.get(exec.agent);
|
|
285
|
+
const count = chain !== void 0 && chain.key === key ? chain.count + 1 : 1;
|
|
286
|
+
chains.set(exec.agent, {
|
|
287
|
+
key,
|
|
288
|
+
count
|
|
289
|
+
});
|
|
290
|
+
if (!thresholdSet.has(count)) return void 0;
|
|
291
|
+
return createUserMessage({
|
|
292
|
+
content: [{
|
|
293
|
+
type: "text",
|
|
294
|
+
text: count === thresholds[0] ? GENTLE_REMINDER : detailedReminder(exec.name, count, previewArguments(canonical, argumentsPreviewChars))
|
|
295
|
+
}],
|
|
296
|
+
source: {
|
|
297
|
+
...PLUGIN_SOURCE,
|
|
298
|
+
form: "notice",
|
|
299
|
+
summary: `${exec.name} × ${count}`
|
|
300
|
+
}
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
ctx.on("tools/post-execute", async (exec, _result, next) => {
|
|
304
|
+
const reminder = observe(exec);
|
|
305
|
+
const downstream = await next();
|
|
306
|
+
if (!reminder) return downstream;
|
|
307
|
+
if (downstream.kind === "block") return {
|
|
308
|
+
kind: "block",
|
|
309
|
+
feedback: downstream.feedback,
|
|
310
|
+
additionalContexts: prependContext(reminder, downstream.additionalContexts)
|
|
311
|
+
};
|
|
312
|
+
return {
|
|
313
|
+
...downstream,
|
|
314
|
+
additionalContexts: prependContext(reminder, downstream.additionalContexts)
|
|
315
|
+
};
|
|
316
|
+
});
|
|
317
|
+
ctx.on("agent/pre-step", ({ agent, messages }, next) => {
|
|
318
|
+
if (messages.some((message) => message.source.kind === "user")) chains.delete(agent);
|
|
319
|
+
return next();
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
//#endregion
|
|
323
|
+
export { Config, apply, name };
|
package/lib/invariant.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
//#region lib/types/invariant.js
|
|
2
|
+
/**
|
|
3
|
+
* Package-owned invariant companion for `@deepseek-ai/dsh-repeat-tool-reminder`.
|
|
4
|
+
* @module @deepseek-ai/dsh-repeat-tool-reminder/invariant
|
|
5
|
+
*/
|
|
6
|
+
const PACKAGE_NAME = "@deepseek-ai/dsh-repeat-tool-reminder";
|
|
7
|
+
/** Cordis companion plugin name. */
|
|
8
|
+
const name = "repeat-tool-reminder-invariant";
|
|
9
|
+
/** Service required before the companion can reserve package ownership. */
|
|
10
|
+
const inject = ["invariants"];
|
|
11
|
+
/**
|
|
12
|
+
* No runtime invariant: the repeat chain is private to one post-execute listener and exposes no
|
|
13
|
+
* package-owned event or snapshot that an independent companion can observe.
|
|
14
|
+
*/
|
|
15
|
+
const install = () => {};
|
|
16
|
+
/**
|
|
17
|
+
* Register this package's invariant companion.
|
|
18
|
+
* @param ctx - Cordis context carrying the invariant service.
|
|
19
|
+
* @returns the installed registration's disposer after setup succeeds.
|
|
20
|
+
*/
|
|
21
|
+
const apply = (ctx) => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install));
|
|
22
|
+
//#endregion
|
|
23
|
+
export { apply, inject, name };
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Advisory per-agent repeat-call detector. It enriches post-execute decisions
|
|
3
|
+
* with logged model context without vetoing or rewriting calls. Configuration
|
|
4
|
+
* and chain semantics live in the package README; rationale lives in the
|
|
5
|
+
* repeat-tool-reminder Agent Note.
|
|
6
|
+
* @module @deepseek-ai/dsh-repeat-tool-reminder
|
|
7
|
+
*/
|
|
8
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
9
|
+
import z from '@deepseek-ai/schemastery';
|
|
10
|
+
export declare const name = "repeat-tool-reminder";
|
|
11
|
+
/**
|
|
12
|
+
* Plugin config, validated by the same-named schemastery schema plus the
|
|
13
|
+
* load-time checks in `apply` (misconfiguration fails loud: an empty
|
|
14
|
+
* `thresholds` list, a non-integer, a value below 2, or a duplicate throws at
|
|
15
|
+
* plugin load, never a silent fall-back). `include`/`exclude` entries are
|
|
16
|
+
* `*`-wildcard predicates over tool names at call time, not references to
|
|
17
|
+
* registry entries — a pattern matching no currently registered tool is valid
|
|
18
|
+
* (`exclude: [mcp_*]` must stay legal in a deployment that loads no MCP tools).
|
|
19
|
+
*/
|
|
20
|
+
export interface Config {
|
|
21
|
+
/** Consecutive-repeat counts that trigger a reminder (default `[3, 5, 8]`). */
|
|
22
|
+
thresholds?: number[];
|
|
23
|
+
/** Tool-name patterns to track; empty means every tool is tracked. */
|
|
24
|
+
include?: string[];
|
|
25
|
+
/** Tool-name patterns transparent to the chain (neither count nor reset). */
|
|
26
|
+
exclude?: string[];
|
|
27
|
+
/**
|
|
28
|
+
* Maximum characters of canonical arguments quoted in the DETAILED reminder
|
|
29
|
+
* (default 500). Large payloads (a `write` body, a long command) would
|
|
30
|
+
* otherwise ride into the next request unbounded — precisely in a loop
|
|
31
|
+
* scenario; the cap bounds the reminder, never the detection (the chain key
|
|
32
|
+
* always compares the FULL canonical string).
|
|
33
|
+
*/
|
|
34
|
+
argumentsPreviewChars?: number;
|
|
35
|
+
}
|
|
36
|
+
export declare const Config: z<Config>;
|
|
37
|
+
/**
|
|
38
|
+
* Install the guard's listeners.
|
|
39
|
+
* @param ctx - plugin context; listeners are scoped to it and disposed with it.
|
|
40
|
+
* @param config - validated {@link Config}; `thresholds` is re-checked fail-loud here.
|
|
41
|
+
*/
|
|
42
|
+
export declare function apply(ctx: Context, config: Config): void;
|
|
43
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Package-owned invariant companion for `@deepseek-ai/dsh-repeat-tool-reminder`.
|
|
3
|
+
* @module @deepseek-ai/dsh-repeat-tool-reminder/invariant
|
|
4
|
+
*/
|
|
5
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
6
|
+
/** Cordis companion plugin name. */
|
|
7
|
+
export declare const name = "repeat-tool-reminder-invariant";
|
|
8
|
+
/** Service required before the companion can reserve package ownership. */
|
|
9
|
+
export declare const inject: string[];
|
|
10
|
+
/**
|
|
11
|
+
* Register this package's invariant companion.
|
|
12
|
+
* @param ctx - Cordis context carrying the invariant service.
|
|
13
|
+
* @returns the installed registration's disposer after setup succeeds.
|
|
14
|
+
*/
|
|
15
|
+
export declare const apply: (ctx: Context) => Promise<() => void>;
|
|
16
|
+
//# sourceMappingURL=invariant.d.ts.map
|
package/package.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@deepseek-ai/dsh-repeat-tool-reminder",
|
|
3
|
+
"description": "Repeat-tool-call guard plugin: advisory reminders when an agent loops on identical tool calls",
|
|
4
|
+
"version": "0.0.1-rc.3",
|
|
5
|
+
"publishConfig": {
|
|
6
|
+
"access": "restricted"
|
|
7
|
+
},
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
|
|
11
|
+
"directory": "packages/guard/repeat-tool-reminder"
|
|
12
|
+
},
|
|
13
|
+
"type": "module",
|
|
14
|
+
"main": "lib/index.js",
|
|
15
|
+
"types": "lib/types/index.d.ts",
|
|
16
|
+
"exports": {
|
|
17
|
+
".": {
|
|
18
|
+
"types": "./lib/types/index.d.ts",
|
|
19
|
+
"default": "./lib/index.js"
|
|
20
|
+
},
|
|
21
|
+
"./invariant": {
|
|
22
|
+
"types": "./lib/types/invariant.d.ts",
|
|
23
|
+
"default": "./lib/invariant.js"
|
|
24
|
+
},
|
|
25
|
+
"./src/*": "./src/*",
|
|
26
|
+
"./package.json": "./package.json"
|
|
27
|
+
},
|
|
28
|
+
"files": [
|
|
29
|
+
"lib/index.js",
|
|
30
|
+
"lib/invariant.js",
|
|
31
|
+
"lib/types/**/*.d.ts"
|
|
32
|
+
],
|
|
33
|
+
"license": "BSD-3-Clause",
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"@deepseek-ai/schemastery": "^3.18.1-rc.1"
|
|
36
|
+
},
|
|
37
|
+
"peerDependencies": {
|
|
38
|
+
"@deepseek-ai/dsh-invariants": "^0.0.1-rc.3",
|
|
39
|
+
"@deepseek-ai/dsh-agent": "^0.0.1-rc.3",
|
|
40
|
+
"@deepseek-ai/dsh-tools": "^0.0.1-rc.3",
|
|
41
|
+
"@deepseek-ai/cordis": "^4.0.1-rc.1"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"@deepseek-ai/dsh-agent": "^0.0.1-rc.3",
|
|
45
|
+
"@deepseek-ai/dsh-agent-loop-testkit": "^0.0.1-rc.3",
|
|
46
|
+
"@deepseek-ai/dsh-invariants": "^0.0.1-rc.3",
|
|
47
|
+
"@deepseek-ai/dsh-llm": "^0.0.1-rc.3",
|
|
48
|
+
"@deepseek-ai/dsh-session": "^0.0.1-rc.3",
|
|
49
|
+
"@deepseek-ai/dsh-agent-loop": "^0.0.1-rc.3",
|
|
50
|
+
"@deepseek-ai/dsh-tools": "^0.0.1-rc.3",
|
|
51
|
+
"@deepseek-ai/cordis": "^4.0.1-rc.1"
|
|
52
|
+
}
|
|
53
|
+
}
|