Merge origin/master into worktree/explicit-turn-signal

This commit is contained in:
Yichen Jiang
2026-07-20 21:38:49 +08:00
1322 changed files with 47895 additions and 20257 deletions
+10 -18
View File
@@ -1,15 +1,14 @@
/**
* 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.
* Advisory per-agent repeat-call detector. It enriches post-execute decisions
* with logged model context without vetoing or rewriting calls. Configuration
* and chain semantics live in the package README; rationale lives in the
* repeat-tool-guard Agent Note.
* @module @deepseek-ai/dsh-repeat-tool-guard
*/
import type { Context } from 'cordis'
import z from 'schemastery'
import type { AgentId, HookContext, PromptDecision } from '@deepseek-ai/dsh-agent'
import type { Agent, HookContext, PromptDecision } from '@deepseek-ai/dsh-agent'
import type { MessageSource } from '@deepseek-ai/dsh-llm'
import type { PostToolDecision, ToolExecution } from '@deepseek-ai/dsh-tools'
@@ -141,7 +140,7 @@ function validateThresholds(values: number[]): number[] {
/**
* Prepend the guard's reminder while preserving every downstream context's
* source, envelope, and metadata.
* source and metadata.
*/
function prependContext(ours: HookContext, theirs: HookContext[] | undefined): HookContext[] {
return [ours, ...theirs ?? []]
@@ -169,9 +168,7 @@ 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>()
const chains = new WeakMap<Agent, Chain>()
/** Whether a tool participates in the chain (untracked calls are transparent: they neither count nor reset). */
function tracked(toolName: string): boolean {
@@ -194,9 +191,9 @@ export function apply(ctx: Context, config: Config): void {
if (!tracked(exec.name)) return undefined
const canonical = canonicalize(exec.arguments)
const key = JSON.stringify([exec.name, canonical])
const chain = chains.get(exec.agent.id)
const chain = chains.get(exec.agent)
const count = chain !== undefined && chain.key === key ? chain.count + 1 : 1
chains.set(exec.agent.id, { key, count })
chains.set(exec.agent, { key, count })
if (!thresholdSet.has(count)) return undefined
const text = count === thresholds[0]
? GENTLE_REMINDER
@@ -226,12 +223,7 @@ export function apply(ctx: Context, config: Config): void {
// loop. Pure reset hook: always delegates (attaching nothing, vetoing
// nothing).
ctx.on('agent/prompt-submit', (agent, _content, _source, _signal, next): Promise<PromptDecision> => {
chains.delete(agent.id)
chains.delete(agent)
return next()
})
// Drop state when an agent goes away, bounding the map over harness lifetime.
ctx.on('agent/status', (agent, status) => {
if (status === 'disposed') chains.delete(agent.id)
})
}