Files
deepseek-harness/packages/guard/repeat-tool-reminder

description, kind
description kind
Advisory loop-hygiene guard that nudges the model out of identical tool-call loops, for users and maintainers choosing, configuring, or debugging the plugin. package-reference

@deepseek-ai/dsh-repeat-tool-reminder

English | 中文

Summary

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.

Table of Contents


Use this package

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.

When to choose it

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.

Setting the thresholds and scope

When you want to change when reminders fire or which tools they cover, mount the plugin with configuration:

- name: '@deepseek-ai/dsh-repeat-tool-reminder'
  config:
    thresholds: [3, 5, 8]        # remind at 3, 5, and 8 consecutive repeats
    include: []                  # track every tool; list patterns to track only some
    exclude: [todo_write]        # never track these tools
    argumentsPreviewChars: 500   # cap on arguments shown in the detailed reminder
Field Default Meaning
thresholds [3, 5, 8] Repeat counts that trigger a reminder
include [] Only these tools are tracked; empty means every tool
exclude [] These tools are never tracked; calls to them neither count nor reset
argumentsPreviewChars 500 How many characters of the repeated arguments the detailed reminder shows

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 documents every accepted value.

What you get

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.


Understand the implementation

Implementation internals — click to expand

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.

Design philosophy

The guard is built on four commitments:

  • 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.
  • 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.
  • 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.
  • Fail loud at load. thresholds and argumentsPreviewChars validate in apply and throw, never falling back to defaults.

Detection: the repeat chain

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>.

  • 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.
  • 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.
  • 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.
  • 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.
  • 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.

Reminder delivery

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.

Source map

File Role
src/index.ts Plugin entry: Config schema, fail-loud validation, chain listeners
src/invariant.ts Invariant companion (no runtime invariant: the chain is private to one post-execute listener)

Further Exploration

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.


Model Experience

First-threshold context message

What the model sees

At the first configured consecutive-repeat threshold, that agent receives the reminder below. No tool schema or normal-call text is added.

First-threshold 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.

Token effect

Zero tokens before the threshold. The reminder is retained history for that agent.

KV Cache effect

Append-only; newly visible content follows the reusable request prefix and does not invalidate existing KV-cache entries.

Later-threshold context message

What the model sees

A later threshold receives the detailed reminder template below. A capped argument preview ends exactly … (+<omitted> more chars).

Later-threshold reminder
Repeated tool call detected:
- tool: <toolName>
- consecutive_calls: <count>
- arguments: <canonicalArguments>
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.

Token effect

Each reminder is retained history; argumentsPreviewChars bounds its data-dependent argument text, while agents keep independent counters.

KV Cache effect

Append-only; newly visible content follows the reusable request prefix and does not invalidate existing KV-cache entries.

Known Limitations and Deferred Work

These limits define when the guard is a poor fit. They are current package constraints, not a task backlog.

  • 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.
  • Compaction does not reset chains — a chain spanning a compaction checkpoint keeps counting.
  • Advisory only — escalating to a blocking form at a high threshold is not implemented, though PostToolDecision already supports blocking.
  • No subagent chain-sharing — chains stay isolated per agent; a parent and its subagent repeating the same call never combine.
  • Legitimate idempotent polling still draws nudges past the thresholds — the pressure valves are the thresholds/exclude config.
  • Past the highest threshold a chain goes silent — reminders fire only at exact configured counts, never beyond them.

Dev Note

Working context for maintainers — click to expand

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.

The repeat-tool-guard feature note records the original design and alternatives under the former package name; the naming ledger records the rename to repeat-tool-reminder and its reason.