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:
Yichen Jiang
2026-07-14 19:50:25 +08:00
720 changed files with 21199 additions and 14129 deletions

View File

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

View File

@@ -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). */