diff --git a/src/agent/loop.ts b/src/agent/loop.ts index e99da715..997200da 100644 --- a/src/agent/loop.ts +++ b/src/agent/loop.ts @@ -17,6 +17,7 @@ import { resetToolSessionState } from '../tools/index.js'; import { CORE_TOOL_NAMES, dynamicToolsEnabled } from '../tools/tool-categories.js'; import { createActivateToolCapability } from '../tools/activate.js'; import { recordUsage } from '../stats/tracker.js'; +import { loadConfig } from '../commands/config.js'; import { recordSessionUsage } from '../stats/session-tracker.js'; import { appendAudit, extractLastUserPrompt } from '../stats/audit.js'; import { estimateCost, OPUS_PRICING } from '../pricing.js'; @@ -546,7 +547,21 @@ export async function interactiveSession( let consecutiveTinyResponses = 0; // Count of consecutive calls with <10 output tokens const MAX_TINY_RESPONSES = 2; // Break after N tiny responses — if 2 calls return near-empty, something is wrong let turnSpend = 0; // Cost spent this user turn (USD) - const MAX_TURN_SPEND_USD = 0.25; // Hard circuit breaker per user message (lowered — user wallets are real money) + // Hard circuit breaker per user message — defends user wallets against + // a runaway model+tool combo on a single prompt. User-overridable via + // `franklin config set max-turn-spend-usd `. Explicit "0" or a + // negative number disables the cap; a non-numeric / unparseable value + // is treated as a typo and falls back to the safe default rather than + // silently removing the wallet guard. + const turnSpendCap = (() => { + const raw = loadConfig()['max-turn-spend-usd']; + if (raw == null) return 0.25; + const parsed = Number(raw); + if (!Number.isFinite(parsed)) return 0.25; // typo → keep default + if (parsed <= 0) return Infinity; // explicit opt-out + return parsed; + })(); + const MAX_TURN_SPEND_USD = turnSpendCap; // ── Turn analysis (one classifier call, drives routing + prefetch) ── // Single LLM pass that answers every routing-adjacent question the diff --git a/src/commands/config.ts b/src/commands/config.ts index 3c3c7702..2411e4c0 100644 --- a/src/commands/config.ts +++ b/src/commands/config.ts @@ -14,6 +14,7 @@ const VALID_KEYS = [ 'smart-routing', 'permission-mode', 'max-turns', + 'max-turn-spend-usd', 'auto-compact', 'session-save', 'debug', @@ -29,6 +30,13 @@ export interface AppConfig { 'smart-routing'?: string; 'permission-mode'?: string; 'max-turns'?: string; + /** + * Hard per-turn spend ceiling in USD (default $0.25). Numeric string, + * e.g. "0.5" or "2". Set to "0" to disable the cap. The agent loop + * stops a turn the moment cumulative cost crosses this threshold, + * preventing a runaway model + tool combo from draining the wallet. + */ + 'max-turn-spend-usd'?: string; 'auto-compact'?: string; 'session-save'?: string; 'debug'?: string;