repeatguard

package
v0.1.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 9, 2026 License: MIT Imports: 6 Imported by: 0

README

repeatguard

Extension. Ejectable — the loop never names it. Without it, a model that re-issues the same call with identical arguments twenty turns running trips nothing and burns the entire turn budget.

This fills a real hole. The other loop breakers all fire at the wrong altitude: the goal gate catches a verbatim-repeated answer, compaction summarizes away a stale duplicate result after the fact, and the circuit breaker only counts tool failures. A tool that succeeds every time, called with identical arguments forever, trips none of them.

Ported from deepseek-harness's dsh-repeat-tool-reminder (MIT).

Model Experience

The chain of identical calls reaches the first threshold (default: 3)
What the model sees

A synthetic user message, appended after every tool result in the batch — so the model reads its own repeated output and the reminder together.

Verbatim text for this field
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

Fixed, ~50 tokens, at most once per threshold per chain.

KV cache effect

Append-only.

The chain reaches a later threshold (default: 5, then 8)
What the model sees

The detailed form, naming what is actually happening:

Repeated tool call detected:
- tool: run_sql
- consecutive_calls: 5
- arguments: {"limit":100,"q":"select count(*) from events"}
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

Capped. The quoted arguments are clamped to ArgumentsPreviewChars (default 500 runes) with an explicit … (+N more chars) marker, so a looping write-shaped payload cannot ride unbounded into every subsequent request. The chain key always compares the full canonical string; only the reminder text is bounded.

KV cache effect

Append-only.

New material enters the conversation

A steer, a follow-up, or another extension's injection resets the chain. The model has been given information it did not have, so its earlier repetition is no longer the same chain of reasoning — otherwise the steer that fixes the loop still eats the next reminder.

Token effect

Zero-direct.

Impact on the agent

  • Never blocks a call. A legitimately repeated poll is delayed by nothing. At the call site a real loop is indistinguishable from a legitimate retry; only the model knows which it is, so the plugin supplies the observation and leaves the judgment alone.
  • Denied and disabled calls are counted too — a model hammering a blocked call is precisely the loop worth breaking.
  • Arguments are compared canonically (nested keys sorted), because models re-emit the same object with keys shuffled constantly and treating those as different calls would miss most real loops.
  • update_plan is excluded by default, and exclusion is transparent: run_sql X → update_plan → run_sql X still counts as two consecutive run_sql X, so bookkeeping cannot launder a loop.
  • Thresholds are validated at composition. A value below 2 or a duplicate fails the build rather than producing a guard that silently never fires.

Known limitations and deferred work

  • Consecutive only. Alternating A B A B A B is a loop the plugin cannot see; it tracks one chain, not a cycle detector.
  • No cross-run memory. Counters are per-run by design, so an agent that loops, is resumed, and loops again identically starts from zero.
  • Advisory only. A model that ignores the reminder is unaffected; the run still relies on MaxTurns / MaxToolCalls as the real backstop.
  • The reminder is a user-role message. On providers that weight the last user turn heavily it can pull focus away from the actual task.

Documentation

Overview

Package repeatguard breaks a run out of a tool-call loop by TELLING the model it is in one.

It never blocks a call — a legitimately repeated poll must still work, and at the call site a real loop is indistinguishable from a legitimate retry. Only the model knows which it is, so the plugin supplies the observation and leaves the judgment alone.

Index

Constants

This section is empty.

Variables

View Source
var DefaultExclude []string

DefaultExclude is empty, and deliberately so: this plugin names no other capability's tools.

Bookkeeping tools are the case that used to live here as a hardcoded update_plan exclusion. They are now handled generically — the run tells the guard which tools are administrative (agentcore.BookkeepingTool), so a planner from any package is transparent to the chain and this plugin depends on none of them. Exclude remains for domain-specific patterns a composition knows about.

View Source
var DefaultThresholds = []int{3, 5, 8}

DefaultThresholds is the escalation ladder: a gentle nudge at 3, then progressively louder ones. Below 3 would fire on an ordinary retry-after-error.

Functions

This section is empty.

Types

type Plugin

type Plugin struct {
	// Thresholds are the consecutive-repeat counts that trigger a reminder. The
	// FIRST delivers a short generic nudge; every later one delivers the detailed
	// form naming the tool, the run length, and the arguments. nil uses
	// DefaultThresholds. Validated at composition: empty is fine (nil ⇒
	// defaults), but a value below 2 or a duplicate is a configuration error, not
	// a silent fallback.
	Thresholds []int
	// Include limits tracking to matching tool names ('*' wildcards allowed).
	// Empty tracks every tool.
	Include []string
	// Exclude makes matching tools transparent to the chain ('*' wildcards
	// allowed) — they neither increment nor reset the counter. This is what makes
	// exclusion useful: a bookkeeping call interleaved into a loop must not
	// launder it, so `run_sql X → update_plan → run_sql X` still counts as two
	// consecutive run_sql X. nil uses DefaultExclude.
	Exclude []string
	// ArgumentsPreviewChars caps how much of the canonical argument string the
	// detailed reminder quotes, so a looping write-shaped payload cannot ride
	// unbounded into every subsequent request. The chain key always compares the
	// FULL canonical string; this bounds only the reminder text. 0 uses
	// defaultArgumentsPreviewChars.
	ArgumentsPreviewChars int
}

Plugin tunes the repeated-tool-call reminder: an advisory loop-breaker that watches a run's stream of tool calls, counts runs of consecutive calls to the same tool with identical arguments, and injects an escalating reminder when the count reaches a configured threshold.

It is NOT a model-facing tool and never blocks: a legitimately repeated call is delayed by nothing. The decision — retry differently, gather more evidence, or finish — stays entirely with the model. That matters because a real loop is indistinguishable from a legitimate retry at the call site; only the model knows which it is, so the guard supplies the observation and leaves the judgment alone.

This fills a genuine hole. agentcore's existing loop breakers all fire at the wrong altitude for this failure: the goal gate catches a verbatim-repeated ANSWER, compaction summarizes stale duplicate results away AFTER the fact, and the circuit breaker only counts tool FAILURES. A tool that succeeds every time and is called with identical arguments twenty turns running — a query the model keeps re-issuing because it will not accept the answer — trips none of them, and burns the whole turn budget.

Ported from deepseek-harness's dsh-repeat-tool-reminder (MIT).

func At

func At(thresholds ...int) Plugin

At installs the guard with explicit thresholds (each at least 2).

func Default

func Default() Plugin

Default installs the guard with the default thresholds.

func (Plugin) BeginRun

BeginRun starts a fresh chain for this run, carrying the run's bookkeeping predicate so administrative tools stay transparent to the chain.

func (Plugin) Name

func (Plugin) Name() string

Name identifies the plugin and the extension it installs.

func (Plugin) Register

func (p Plugin) Register(r *agentcore.Registry) error

Register adds the plugin as a run extension, validating the thresholds eagerly so a typo'd ladder fails the composition instead of installing a guard that silently never fires.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL