Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion src/agent/loop.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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 <number>`. 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
Expand Down
8 changes: 8 additions & 0 deletions src/commands/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const VALID_KEYS = [
'smart-routing',
'permission-mode',
'max-turns',
'max-turn-spend-usd',
'auto-compact',
'session-save',
'debug',
Expand All @@ -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;
Expand Down
Loading