fix review finding: cap the detailed reminder's argument payload
This commit is contained in:
@@ -59,12 +59,21 @@ export interface Config {
|
||||
include?: string[]
|
||||
/** Tool-name patterns transparent to the chain (neither count nor reset). */
|
||||
exclude?: string[]
|
||||
/**
|
||||
* Maximum characters of canonical arguments quoted in the DETAILED reminder
|
||||
* (default 500). Large payloads (a `write` body, a long command) would
|
||||
* otherwise ride into the next request unbounded — precisely in a loop
|
||||
* scenario; the cap bounds the reminder, never the detection (the chain key
|
||||
* always compares the FULL canonical string).
|
||||
*/
|
||||
argumentsPreviewChars?: number
|
||||
}
|
||||
|
||||
export const Config: z<Config> = z.object({
|
||||
thresholds: z.array(z.number()).default([3, 5, 8]),
|
||||
include: z.array(z.string()).default([]),
|
||||
exclude: z.array(z.string()).default([]),
|
||||
argumentsPreviewChars: z.number().default(500),
|
||||
})
|
||||
|
||||
/**
|
||||
@@ -128,6 +137,16 @@ function wildcardToRegExp(pattern: string): RegExp {
|
||||
return new RegExp(`^${escaped.replaceAll('*', '.*')}$`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Head-truncate the canonical arguments for quoting in the detailed reminder,
|
||||
* marking how much was omitted. Bounds only the model-visible text — the
|
||||
* chain key always uses the full canonical string.
|
||||
*/
|
||||
function previewArguments(canonical: string, cap: number): string {
|
||||
if (canonical.length <= cap) return canonical
|
||||
return `${canonical.slice(0, cap)}… (+${canonical.length - cap} more chars)`
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate `thresholds` per the fail-loud contract and return them sorted
|
||||
* ascending (the escalation rule reads `thresholds[0]` as the gentle tier, so
|
||||
@@ -173,11 +192,15 @@ interface Chain {
|
||||
* @param config - validated {@link Config}; `thresholds` is re-checked fail-loud here.
|
||||
*/
|
||||
export function apply(ctx: Context, config: Config): void {
|
||||
// schemastery's .default() guarantees the arrays are set after validation.
|
||||
// schemastery's .default() guarantees the fields are set after validation.
|
||||
const thresholds = validateThresholds(config.thresholds as number[])
|
||||
const thresholdSet = new Set(thresholds)
|
||||
const includePatterns = (config.include as string[]).map(wildcardToRegExp)
|
||||
const excludePatterns = (config.exclude as string[]).map(wildcardToRegExp)
|
||||
const argumentsPreviewChars = config.argumentsPreviewChars as number
|
||||
if (!Number.isInteger(argumentsPreviewChars) || argumentsPreviewChars < 1) {
|
||||
throw new Error(`repeat-tool-guard: invalid argumentsPreviewChars ${argumentsPreviewChars} — must be an integer >= 1`)
|
||||
}
|
||||
|
||||
const chains = new Map<AgentId, Chain>()
|
||||
|
||||
@@ -206,7 +229,9 @@ export function apply(ctx: Context, config: Config): void {
|
||||
const count = chain !== undefined && chain.key === key ? chain.count + 1 : 1
|
||||
chains.set(exec.agent.id, { key, count })
|
||||
if (!thresholdSet.has(count)) return undefined
|
||||
const text = count === thresholds[0] ? GENTLE_REMINDER : detailedReminder(exec.name, count, canonical)
|
||||
const text = count === thresholds[0]
|
||||
? GENTLE_REMINDER
|
||||
: detailedReminder(exec.name, count, previewArguments(canonical, argumentsPreviewChars))
|
||||
return { content: [{ type: 'text', text }], source: PLUGIN_SOURCE }
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user