Merge remote-tracking branch 'origin/master' into codex/project-instruction-files
# Conflicts: # AGENTS.md # docs/config-catalog.md # docs/cordis-catalog/events.md # docs/cordis-catalog/services.md # docs/core-data-structures/core.md # docs/event-producer-consumer.md # docs/persistence-catalog.md # docs/rfc/implemented/architecture/2026-07-05-reconstructable-requests.md # docs/rfc/implemented/feature/2026-06-15-code-mode.md # docs/rfc/implemented/feature/2026-06-30-hook-bridges.md # docs/rfc/implemented/feature/2026-06-30-interception-seams.md # docs/rfc/implemented/feature/2026-07-08-repeat-tool-guard.md # docs/rfc/proposed/simplification/2026-07-04-prune-dead-core-spine-surface.md # examples/AGENTS.md # examples/acp-agent/cordis.yml # examples/acp-agent/tests/acp.snapshot.ts # examples/echo-agent/cordis.yml # examples/sandbox-acp-agent/cordis.yml # packages/cordis/tool-cordis/src/api-catalog.ts # packages/core/agent-core/README.md # packages/core/agent-core/src/index.ts # packages/core/agent-loop/README.md # packages/core/agent-loop/src/loop.ts # packages/core/agent-loop/tests/interception.spec.ts # packages/core/agent/src/types.ts # packages/core/tools/README.md # packages/core/tools/src/code-mode.ts # packages/core/tools/src/index.ts # packages/fs/fs-local/src/index.ts # packages/fs/fs/README.md # packages/fs/fs/src/index.ts # packages/guard/repeat-tool-guard/README.md # packages/guard/repeat-tool-guard/src/index.ts # packages/hooks/hooks-claude/src/index.ts # packages/hooks/hooks-codex/src/index.ts # packages/ui/acp-agent/src/index.ts
This commit is contained in:
@@ -35,3 +35,42 @@ Reminders ride the post-execute decision's `additionalContexts` (source `{kind:
|
||||
## Testing
|
||||
|
||||
Unit suites drive a real agent loop against a mock adapter (no network) and cover the chain semantics above to per-file 100%. The snapshot tier owns the transcript surface: a scripted-replay scenario repeats a call five times and pins both reminder tiers (gentle at 3, detailed at 5) as `context/message`s in the ACP transcript.
|
||||
|
||||
## 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.
|
||||
|
||||
**Token effect**: Zero tokens before the threshold. The reminder is retained history for that agent.
|
||||
|
||||
#### First-threshold reminder
|
||||
|
||||
```markdown
|
||||
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.
|
||||
```
|
||||
|
||||
### 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)`.
|
||||
|
||||
**Token effect**: Each reminder is retained history; `argumentsPreviewChars` bounds its data-dependent argument text, while agents keep independent counters.
|
||||
|
||||
#### Later-threshold reminder
|
||||
|
||||
```markdown
|
||||
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.
|
||||
```
|
||||
|
||||
## Known Limitations and Deferred Work
|
||||
|
||||
- **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 `block` 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 `thresholds`/`exclude` config.
|
||||
- **Past the highest threshold a chain goes silent** — reminders fire only at exact configured counts, never beyond them.
|
||||
|
||||
@@ -1,37 +1,9 @@
|
||||
/**
|
||||
* Repeat-tool-call guard: advisory loop-breaker for agents stuck re-issuing
|
||||
* the same tool call with identical arguments.
|
||||
*
|
||||
* Not a model-facing tool — it registers no tool, never vetoes or rewrites a
|
||||
* call, and adds exactly one behavior: watch each agent's stream of tool calls
|
||||
* through the `tools/post-execute` waterfall, count runs of consecutive calls
|
||||
* to the same tool with identical canonicalized arguments, and at configured
|
||||
* run lengths fold an escalating advisory reminder onto the decision's
|
||||
* `additionalContexts`. The loop appends that context as a logged
|
||||
* `context/message` after the step's tool results, so the reminder is
|
||||
* model-visible, source-attributed, and reconstructable from the session log
|
||||
* with no new session event. Decision record:
|
||||
* docs/rfc/implemented/feature/2026-07-08-repeat-tool-guard.md.
|
||||
*
|
||||
* ```yaml
|
||||
* - id: repeat-tool-guard
|
||||
* name: '@deepseek-ai/dsh-repeat-tool-guard'
|
||||
* config:
|
||||
* thresholds: [3, 5, 8] # consecutive counts that trigger a reminder
|
||||
* include: [] # tool-name patterns to track; empty = all tools
|
||||
* exclude: [todo_write] # tool-name patterns transparent to the chain
|
||||
* ```
|
||||
*
|
||||
* Chain state is keyed per {@link AgentId} — the tool registry is a
|
||||
* context-level singleton whose waterfalls interleave every agent's calls, so
|
||||
* a shared counter would let one agent's repetition trip another's reminder.
|
||||
* State is in-memory only: a session resumed from persistence starts with a
|
||||
* fresh chain (the guard is a heuristic nudge, not a logged invariant).
|
||||
*
|
||||
* Plugin export shape: named exports, NO default. The cordis Loader's
|
||||
* `unwrapExports` does `exports.default ?? exports`, so a stray default would
|
||||
* collapse the module to the bare `apply` (see docs/postmortem/0001).
|
||||
*
|
||||
* Advisory repeat-call loop breaker. It never registers, blocks, or rewrites a tool; configured
|
||||
* consecutive canonical calls add source-attributed context after downstream post-policy. The
|
||||
* loop logs that model-visible reminder as reconstructable context. Counters are per agent and
|
||||
* in-memory, so one agent cannot trip another and resumed sessions start fresh. Named exports
|
||||
* preserve loader metadata. See the package README for chain semantics and thresholds.
|
||||
* @module @deepseek-ai/dsh-repeat-tool-guard
|
||||
*/
|
||||
|
||||
@@ -197,6 +169,8 @@ export function apply(ctx: Context, config: Config): void {
|
||||
throw new Error(`repeat-tool-guard: invalid argumentsPreviewChars ${argumentsPreviewChars} — must be an integer >= 1`)
|
||||
}
|
||||
|
||||
// TODO(agent-keyed-repeat-chain): key a WeakMap by the Agent itself; that
|
||||
// removes the disposal-only status listener and cannot collide on id reuse.
|
||||
const chains = new Map<AgentId, Chain>()
|
||||
|
||||
/** Whether a tool participates in the chain (untracked calls are transparent: they neither count nor reset). */
|
||||
|
||||
Reference in New Issue
Block a user