import type {
ExtensionAPI,
ToolCallEvent,
ToolResultEvent,
} from "@earendil-works/pi-coding-agent";
export const DEFAULT_THRESHOLDS = [3, 5, 8] as const;
export interface RepeatToolGuardOptions {
/** Consecutive repeat counts that should receive a reminder. Defaults to 3, 5, and 8. */
thresholds?: readonly number[];
/** Tool name patterns to track. If omitted, all tools are tracked. Supports `*` wildcards. */
include?: readonly string[];
/** Tool name patterns to ignore. Supports `*` wildcards. */
exclude?: readonly string[];
/** Environment source for PI_REPEAT_TOOL_GUARD_* variables. Defaults to process.env. */
env?: NodeJS.ProcessEnv;
}
type ToolResultContent = ToolResultEvent["content"];
export interface ToolResultPatch {
content?: ToolResultContent;
details?: ToolResultEvent["details"];
isError?: boolean;
}
const REMINDER_TEXT_1 =
"\n\n\n" +
"You are repeating the exact same tool call with identical parameters. " +
"Please carefully analyze the previous result. If the task is not yet complete, " +
"try a different method or parameters instead of repeating the same call.\n" +
"";
export function makeReminderText(toolName: string, repeatCount: number, canonicalInput: string): string {
return (
"\n\n\n" +
"You have repeatedly called the same tool with identical parameters many times.\n" +
"Repeated tool call detected:\n" +
`- tool: ${toolName}\n` +
`- repeated_times: ${repeatCount}\n` +
`- arguments: ${canonicalInput}\n` +
"The previous repeated calls did not make progress. Do not call this exact same tool " +
"with the exact same arguments again.\n" +
"Carefully inspect the latest tool result and choose a different next action, " +
"different parameters, or finish the task if enough evidence has been gathered.\n" +
""
);
}
export function parseListEnv(name: string, env: NodeJS.ProcessEnv = process.env): string[] {
return (env[name] ?? "")
.split(",")
.map((item) => item.trim())
.filter(Boolean);
}
export function normalizeThresholds(values: readonly number[] | undefined): number[] {
if (!values || values.length === 0) return [...DEFAULT_THRESHOLDS];
const parsed = values.filter((item) => Number.isInteger(item) && item > 1);
return parsed.length === 0 ? [...DEFAULT_THRESHOLDS] : [...new Set(parsed)].sort((a, b) => a - b);
}
export function parseThresholds(env: NodeJS.ProcessEnv = process.env): number[] {
const raw = parseListEnv("PI_REPEAT_TOOL_GUARD_THRESHOLDS", env);
if (raw.length === 0) return [...DEFAULT_THRESHOLDS];
const parsed = raw.map((item) => Number.parseInt(item, 10));
return normalizeThresholds(parsed);
}
export function wildcardPatternToRegExp(pattern: string): RegExp {
const escaped = pattern.replace(/[|\\{}()[\]^$+?.]/g, "\\$&");
return new RegExp(`^${escaped.replaceAll("*", ".*")}$`);
}
export function compilePatterns(patterns: readonly string[]): RegExp[] {
return patterns.map(wildcardPatternToRegExp);
}
export function matchesAny(value: string, patterns: readonly RegExp[]): boolean {
return patterns.some((pattern) => pattern.test(value));
}
export function sortJsonValue(value: unknown, seen: WeakSet