@deepseek-ai/dsh-repeat-tool-reminder 0.1.2-rc.1 → 0.1.5-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.i18n.yaml +2 -2
- package/README.md +2 -2
- package/README.zh.md +2 -2
- package/lib/index.js +108 -5
- package/package.json +10 -10
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:
|
|
6
|
-
README.zh.md:
|
|
5
|
+
README.md: 13035b68e80c9eea3e87bb7c8315fbaf9b84a3c3
|
|
6
|
+
README.zh.md: 330dd22be31cbb128fe7807dd7e370c4ea2e1fe7
|
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@ English | [中文](README.zh.md)
|
|
|
9
9
|
|
|
10
10
|
## Summary
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
This package helps a model escape loops in which it calls the same tool with identical arguments without making progress. At configured repeat counts, it asks the model to inspect the previous result and change approach or finish. The reminder is advisory: it never blocks or delays a legitimate repeated call. Repeats are tracked separately for each agent and cleared by a new user message. The `dsh` base bundle enables the package with reminders at 3, 5, and 8 repeats.
|
|
13
13
|
|
|
14
14
|
## Table of Contents
|
|
15
15
|
|
|
@@ -181,6 +181,6 @@ These limits define when the guard is a poor fit. They are current package const
|
|
|
181
181
|
|
|
182
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
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/
|
|
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/archived/architecture/2026-08-11-repository-naming-contract-and-rename-ledger.md) records the rename to `repeat-tool-reminder` and its reason.
|
|
185
185
|
|
|
186
186
|
</details>
|
package/README.zh.md
CHANGED
|
@@ -9,7 +9,7 @@ kind: "package-reference"
|
|
|
9
9
|
|
|
10
10
|
## 概述
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
本包帮助模型跳出以相同参数反复调用同一工具却没有进展的循环。达到配置的重复次数时,它会要求模型检查上一次结果并改变方法或结束任务。提醒只是建议,绝不会阻止或延迟合理的重复调用。每个 agent 的重复分别跟踪,新的用户消息会清除计数。`dsh` base 组合默认启用本包,并在重复 3、5、8 次时提醒。
|
|
13
13
|
|
|
14
14
|
## 目录
|
|
15
15
|
|
|
@@ -181,6 +181,6 @@ The repeated calls are not making progress. Do not call this tool with these exa
|
|
|
181
181
|
|
|
182
182
|
本开发备注是维护者的工作上下文:开放问题与尚未决定的探索方向。它明确不具权威性——已交付的行为、限制与既定理由以上文、包代码和相关 Agent Note 为准。
|
|
183
183
|
|
|
184
|
-
[repeat-tool-guard Agent Note](../../../.agents/notes/archived/feature/2026-07-08-repeat-tool-guard.md) 以旧包名记录了原始设计与备选方案;[改名台账](../../../.agents/notes/
|
|
184
|
+
[repeat-tool-guard Agent Note](../../../.agents/notes/archived/feature/2026-07-08-repeat-tool-guard.md) 以旧包名记录了原始设计与备选方案;[改名台账](../../../.agents/notes/archived/architecture/2026-08-11-repository-naming-contract-and-rename-ledger.md) 记录了改名为 `repeat-tool-reminder` 及其原因。
|
|
185
185
|
|
|
186
186
|
</details>
|
package/lib/index.js
CHANGED
|
@@ -495,6 +495,9 @@ function harnessErrorCode(error) {
|
|
|
495
495
|
}
|
|
496
496
|
//#endregion
|
|
497
497
|
//#region ../../llm/llm/src/content.ts
|
|
498
|
+
function quoted(value) {
|
|
499
|
+
return JSON.stringify(value);
|
|
500
|
+
}
|
|
498
501
|
/**
|
|
499
502
|
* Stable text shown to a model that cannot accept one durable image reference.
|
|
500
503
|
* @param ref - durable normalized attachment omitted from the request.
|
|
@@ -514,6 +517,76 @@ function textOnlyImageText(ref) {
|
|
|
514
517
|
function contentHasImage(content) {
|
|
515
518
|
return content.some((block) => block.type === "image" || block.type === "tool-result" && contentHasImage(block.content));
|
|
516
519
|
}
|
|
520
|
+
/**
|
|
521
|
+
* True when typed model content contains a file block, walking nested
|
|
522
|
+
* tool-result content on the same recursion every file policy shares.
|
|
523
|
+
* Reads current content on every call without retaining scan results.
|
|
524
|
+
* @param content - typed model content blocks.
|
|
525
|
+
* @returns whether any nested block is a file.
|
|
526
|
+
*/
|
|
527
|
+
function contentHasFile(content) {
|
|
528
|
+
for (const block of content) if (block.type === "file" || block.type === "tool-result" && contentHasFile(block.content)) return true;
|
|
529
|
+
return false;
|
|
530
|
+
}
|
|
531
|
+
/**
|
|
532
|
+
* Stable model-facing handle for one durable file reference: the address of
|
|
533
|
+
* the verbatim stored copy and the instruction to read it on demand. This is
|
|
534
|
+
* the only representation a provider ever receives for a file.
|
|
535
|
+
* @param ref - durable verbatim file reference.
|
|
536
|
+
* @param readonlyPath - execution-world path of the stored copy, when resolvable.
|
|
537
|
+
* @returns deterministic handle text naming the file, its size, and its address.
|
|
538
|
+
*/
|
|
539
|
+
function fileHandleText(ref, readonlyPath) {
|
|
540
|
+
const digest = String(ref.attachmentId).slice(7, 15);
|
|
541
|
+
const identity = `File ${quoted(ref.name)} (${ref.bytes} bytes, sha256:${digest})`;
|
|
542
|
+
if (readonlyPath === void 0) return `[${identity} was uploaded, but the current execution environment cannot access a readable path. Report that limitation if its contents are needed; do not claim to have read it.]`;
|
|
543
|
+
return `[${identity}: verbatim read-only copy saved at ${quoted(readonlyPath)}. Read that path with your file tools when its contents are needed; copy it to a writable location before modifying it. When delegating file work, include this saved path in the delegation prompt; only subagents sharing this execution environment can read it.]`;
|
|
544
|
+
}
|
|
545
|
+
/** Replace every file occurrence, including nested tool results, with handle text. */
|
|
546
|
+
function replaceFilesWithHandles(blocks, resolvePath) {
|
|
547
|
+
let next;
|
|
548
|
+
for (const [index, block] of blocks.entries()) {
|
|
549
|
+
if (block.type === "file") {
|
|
550
|
+
next ??= blocks.slice(0, index);
|
|
551
|
+
next.push({
|
|
552
|
+
type: "text",
|
|
553
|
+
text: fileHandleText(block.attachment, resolvePath(block.attachment))
|
|
554
|
+
});
|
|
555
|
+
continue;
|
|
556
|
+
}
|
|
557
|
+
if (block.type === "tool-result") {
|
|
558
|
+
const content = replaceFilesWithHandles(block.content, resolvePath);
|
|
559
|
+
if (content !== block.content) {
|
|
560
|
+
next ??= blocks.slice(0, index);
|
|
561
|
+
next.push({
|
|
562
|
+
...block,
|
|
563
|
+
content
|
|
564
|
+
});
|
|
565
|
+
continue;
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
next?.push(block);
|
|
569
|
+
}
|
|
570
|
+
return next ?? blocks;
|
|
571
|
+
}
|
|
572
|
+
/**
|
|
573
|
+
* Project durable file history into deterministic handle text for every model
|
|
574
|
+
* route. Unlike images, no provider receives file blocks natively, so this
|
|
575
|
+
* projection is unconditional in request assembly.
|
|
576
|
+
* @param messages - complete request history.
|
|
577
|
+
* @param resolvePath - resolve one reference's current execution-world read path.
|
|
578
|
+
* @returns the original list without files, otherwise shallow message copies with handle text.
|
|
579
|
+
*/
|
|
580
|
+
function projectFilesToText(messages, resolvePath) {
|
|
581
|
+
if (!messages.some((message) => contentHasFile(message.content))) return messages;
|
|
582
|
+
return messages.map((message) => {
|
|
583
|
+
const content = replaceFilesWithHandles(message.content, resolvePath);
|
|
584
|
+
return content === message.content ? message : {
|
|
585
|
+
...message,
|
|
586
|
+
content
|
|
587
|
+
};
|
|
588
|
+
});
|
|
589
|
+
}
|
|
517
590
|
/** Replace every image occurrence, including nested tool results, for a text-only model. */
|
|
518
591
|
function replaceImagesForTextModel(blocks) {
|
|
519
592
|
let next;
|
|
@@ -950,6 +1023,15 @@ var LlmError = class extends HarnessError {
|
|
|
950
1023
|
imageRequestPricing(provider, model) {
|
|
951
1024
|
return this.adapters.get(provider)?.adapter.imageRequestPricing(provider, model);
|
|
952
1025
|
}
|
|
1026
|
+
/**
|
|
1027
|
+
* Resolve the exact text one durable file occurrence contributes to every
|
|
1028
|
+
* provider request in the current execution environment.
|
|
1029
|
+
* @param ref - durable verbatim file reference from model history.
|
|
1030
|
+
* @returns the same deterministic handle text used at adapter dispatch.
|
|
1031
|
+
*/
|
|
1032
|
+
fileRequestText(ref) {
|
|
1033
|
+
return fileHandleText(ref, this.fileReadPath(ref));
|
|
1034
|
+
}
|
|
953
1035
|
/** Detach typed adapter-owned modality metadata. */
|
|
954
1036
|
detachedModalities(modalities) {
|
|
955
1037
|
return modalities === void 0 ? void 0 : [...modalities];
|
|
@@ -999,6 +1081,8 @@ var LlmError = class extends HarnessError {
|
|
|
999
1081
|
const context = resolved.context;
|
|
1000
1082
|
if (context !== void 0 && (!Number.isInteger(context.contextWindow) || context.contextWindow <= 0)) throw new LlmError(`adapter returned invalid context metadata for provider "${provider}" model "${model}"`, "INVALID_MODEL_CONTEXT");
|
|
1001
1083
|
const inputModalities = this.detachedModalities(resolved.inputModalities);
|
|
1084
|
+
const systemPromptUpdate = resolved.systemPromptUpdate;
|
|
1085
|
+
if (systemPromptUpdate !== void 0 && systemPromptUpdate !== "in-history") throw new LlmError(`adapter returned invalid system prompt update mode for provider "${provider}" model "${model}"`, "INVALID_MODEL_INFO");
|
|
1002
1086
|
const defaultMaxTokens = resolved.defaultMaxTokens;
|
|
1003
1087
|
if (defaultMaxTokens !== void 0 && (!Number.isSafeInteger(defaultMaxTokens) || defaultMaxTokens <= 0)) throw new LlmError(`adapter returned invalid default maxTokens for provider "${provider}" model "${model}"`, "INVALID_MODEL_MAX_TOKENS");
|
|
1004
1088
|
const info = {
|
|
@@ -1008,7 +1092,8 @@ var LlmError = class extends HarnessError {
|
|
|
1008
1092
|
...resolved.description === void 0 ? {} : { description: resolved.description },
|
|
1009
1093
|
...inputModalities === void 0 ? {} : { inputModalities },
|
|
1010
1094
|
...context === void 0 ? {} : { context: { contextWindow: context.contextWindow } },
|
|
1011
|
-
...defaultMaxTokens === void 0 ? {} : { defaultMaxTokens }
|
|
1095
|
+
...defaultMaxTokens === void 0 ? {} : { defaultMaxTokens },
|
|
1096
|
+
...resolved.systemPromptUpdate === void 0 ? {} : { systemPromptUpdate: resolved.systemPromptUpdate }
|
|
1012
1097
|
};
|
|
1013
1098
|
const reasoning = resolved.reasoning;
|
|
1014
1099
|
if (reasoning === void 0) return info;
|
|
@@ -1102,6 +1187,7 @@ var LlmError = class extends HarnessError {
|
|
|
1102
1187
|
adapterDefaults,
|
|
1103
1188
|
...context === void 0 ? {} : { context },
|
|
1104
1189
|
...modelInfo.inputModalities === void 0 ? {} : { inputModalities: Object.freeze([...modelInfo.inputModalities]) },
|
|
1190
|
+
...modelInfo.systemPromptUpdate === void 0 ? {} : { systemPromptUpdate: modelInfo.systemPromptUpdate },
|
|
1105
1191
|
stream: (options) => {
|
|
1106
1192
|
if (dispatched) throw new LlmError("a prepared LLM call can only be dispatched once", "INVALID_PREPARED_CALL");
|
|
1107
1193
|
if (!callConfigEquals(options, resolvedConfig)) throw new LlmError("prepared LLM call config changed before adapter dispatch", "INVALID_PREPARED_CALL");
|
|
@@ -1143,6 +1229,20 @@ var LlmError = class extends HarnessError {
|
|
|
1143
1229
|
return Object.isFrozen(options) ? deepFreeze(filtered) : filtered;
|
|
1144
1230
|
}
|
|
1145
1231
|
/**
|
|
1232
|
+
* Resolve the current execution-world read path of one durable file
|
|
1233
|
+
* reference through the mounted attachment and filesystem providers.
|
|
1234
|
+
*/
|
|
1235
|
+
fileReadPath(ref) {
|
|
1236
|
+
let hostPath;
|
|
1237
|
+
try {
|
|
1238
|
+
hostPath = this.ctx.get("attachments")?.fileHostPath(ref);
|
|
1239
|
+
} catch {
|
|
1240
|
+
return;
|
|
1241
|
+
}
|
|
1242
|
+
if (hostPath === void 0) return void 0;
|
|
1243
|
+
return this.ctx.get("fs")?.processPathFromHostPath(hostPath);
|
|
1244
|
+
}
|
|
1245
|
+
/**
|
|
1146
1246
|
* Final adapter boundary. Adapter selection, dispatch, iterator construction,
|
|
1147
1247
|
* and iteration failures become one terminal failure chunk. Middleware and
|
|
1148
1248
|
* downstream consumer failures remain thrown plugin or consumer errors.
|
|
@@ -1173,13 +1273,16 @@ var LlmError = class extends HarnessError {
|
|
|
1173
1273
|
...options,
|
|
1174
1274
|
...resolvedConfig
|
|
1175
1275
|
};
|
|
1176
|
-
|
|
1276
|
+
let projectedMessages = resolvedOptions.messages;
|
|
1277
|
+
if (projectedMessages.some((message) => contentHasFile(message.content))) projectedMessages = projectFilesToText(projectedMessages, (ref) => this.fileReadPath(ref));
|
|
1278
|
+
if (modelInfo.inputModalities !== void 0 && !modelInfo.inputModalities.includes("image") && projectedMessages.some((message) => contentHasImage(message.content))) projectedMessages = projectImagesForTextModel(projectedMessages);
|
|
1279
|
+
const projectedOptions = projectedMessages === resolvedOptions.messages ? resolvedOptions : Object.isFrozen(resolvedOptions) ? deepFreeze({
|
|
1177
1280
|
...resolvedOptions,
|
|
1178
|
-
messages:
|
|
1281
|
+
messages: projectedMessages
|
|
1179
1282
|
}) : {
|
|
1180
1283
|
...resolvedOptions,
|
|
1181
|
-
messages:
|
|
1182
|
-
}
|
|
1284
|
+
messages: projectedMessages
|
|
1285
|
+
};
|
|
1183
1286
|
iterator = dispatch(this.forAdapter(projectedOptions, adapter))[Symbol.asyncIterator]();
|
|
1184
1287
|
} catch (error) {
|
|
1185
1288
|
yield adapterFailureChunk(error, options.signal);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@deepseek-ai/dsh-repeat-tool-reminder",
|
|
3
3
|
"description": "Repeat-tool-call guard plugin: advisory reminders when an agent loops on identical tool calls",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.5-alpha.1",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|
|
@@ -30,18 +30,18 @@
|
|
|
30
30
|
"@deepseek-ai/schemastery": "^3.18.2"
|
|
31
31
|
},
|
|
32
32
|
"peerDependencies": {
|
|
33
|
-
"@deepseek-ai/dsh-
|
|
33
|
+
"@deepseek-ai/dsh-agent": "^0.1.5-alpha.1",
|
|
34
34
|
"@deepseek-ai/cordis": "^4.0.2",
|
|
35
|
-
"@deepseek-ai/dsh-
|
|
35
|
+
"@deepseek-ai/dsh-tools": "^0.1.5-alpha.1"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
|
-
"@deepseek-ai/dsh-agent": "^0.1.
|
|
39
|
-
"@deepseek-ai/dsh-agent-loop": "^0.1.
|
|
40
|
-
"@deepseek-ai/dsh-agent-loop-testkit": "^0.1.
|
|
41
|
-
"@deepseek-ai/dsh-llm": "^0.1.
|
|
42
|
-
"@deepseek-ai/dsh-session": "^0.1.
|
|
43
|
-
"@deepseek-ai/dsh-tools": "^0.1.
|
|
38
|
+
"@deepseek-ai/dsh-agent": "^0.1.5-alpha.1",
|
|
39
|
+
"@deepseek-ai/dsh-agent-loop": "^0.1.5-alpha.1",
|
|
40
|
+
"@deepseek-ai/dsh-agent-loop-testkit": "^0.1.5-alpha.1",
|
|
41
|
+
"@deepseek-ai/dsh-llm": "^0.1.5-alpha.1",
|
|
42
|
+
"@deepseek-ai/dsh-session": "^0.1.5-alpha.1",
|
|
43
|
+
"@deepseek-ai/dsh-tools": "^0.1.5-alpha.1",
|
|
44
44
|
"@deepseek-ai/cordis": "^4.0.2",
|
|
45
|
-
"@deepseek-ai/dsh-session-projection": "^0.1.
|
|
45
|
+
"@deepseek-ai/dsh-session-projection": "^0.1.5-alpha.1"
|
|
46
46
|
}
|
|
47
47
|
}
|