Initial commit: invest-copilot app
This commit is contained in:
@@ -0,0 +1,93 @@
|
||||
/**
|
||||
* pi-loop-guard — Infinite loop prevention extension
|
||||
*
|
||||
* Counts tool calls across turns and aborts the agent when a configurable
|
||||
* threshold is exceeded. This prevents the agent from looping indefinitely
|
||||
* when errors repeat (e.g., database connection failures, missing API keys).
|
||||
*
|
||||
* Usage:
|
||||
* pi install git:unraid.local:3003/pi-dev/pi-loop-guard
|
||||
*
|
||||
* Configuration (in .pi/settings.json or ~/.pi/agent/settings.json):
|
||||
* {
|
||||
* "loopGuard": {
|
||||
* "maxToolCalls": 50,
|
||||
* "maxTurns": 20
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
|
||||
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
||||
|
||||
interface LoopGuardSettings {
|
||||
maxToolCalls: number;
|
||||
maxTurns: number;
|
||||
}
|
||||
|
||||
const DEFAULT_SETTINGS: LoopGuardSettings = {
|
||||
maxToolCalls: 50,
|
||||
maxTurns: 20,
|
||||
};
|
||||
|
||||
function readSettings(ctx: ExtensionContext): LoopGuardSettings {
|
||||
try {
|
||||
const raw = ctx.sessionManager.getEntries();
|
||||
for (const entry of raw) {
|
||||
if (entry.type === "custom" && entry.customType === "loopguard-settings") {
|
||||
const s = entry.data as Partial<LoopGuardSettings>;
|
||||
return {
|
||||
maxToolCalls: s.maxToolCalls ?? DEFAULT_SETTINGS.maxToolCalls,
|
||||
maxTurns: s.maxTurns ?? DEFAULT_SETTINGS.maxTurns,
|
||||
};
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Ignore errors reading settings
|
||||
}
|
||||
return { ...DEFAULT_SETTINGS };
|
||||
}
|
||||
|
||||
export default function (pi: ExtensionAPI) {
|
||||
let toolCallCount = 0;
|
||||
let turnCount = 0;
|
||||
let settings: LoopGuardSettings = { ...DEFAULT_SETTINGS };
|
||||
|
||||
// Load settings on session start
|
||||
pi.on("session_start", async (_event, ctx) => {
|
||||
settings = readSettings(ctx);
|
||||
});
|
||||
|
||||
// Count tool calls and turns
|
||||
pi.on("turn_end", async (_event, ctx) => {
|
||||
const entries = ctx.sessionManager.getEntries();
|
||||
const lastEntry = entries[entries.length - 1];
|
||||
|
||||
if (lastEntry?.type === "message" && lastEntry.message?.role === "assistant") {
|
||||
const content = lastEntry.message.content as Array<{ type: string }>;
|
||||
const toolCallsInTurn = content.filter((c) => c.type === "toolCall").length;
|
||||
toolCallCount += toolCallsInTurn;
|
||||
}
|
||||
|
||||
turnCount++;
|
||||
|
||||
if (toolCallCount >= settings.maxToolCalls) {
|
||||
ctx.abort();
|
||||
ctx.ui.notify(
|
||||
`Loop guard: stopped after ${toolCallCount} tool calls (threshold: ${settings.maxToolCalls}). Possible infinite loop detected.`,
|
||||
"error",
|
||||
);
|
||||
} else if (turnCount >= settings.maxTurns) {
|
||||
ctx.abort();
|
||||
ctx.ui.notify(
|
||||
`Loop guard: stopped after ${turnCount} turns (threshold: ${settings.maxTurns}). Possible infinite loop detected.`,
|
||||
"error",
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// Reset counters on session shutdown
|
||||
pi.on("session_shutdown", async () => {
|
||||
toolCallCount = 0;
|
||||
turnCount = 0;
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user