{"version":3,"file":"breaker.d.ts","sourceRoot":"","sources":["../../../src/core/storm/breaker.ts"],"names":[],"mappings":"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","sourcesContent":["import type { AgentToolResult } from \"@apholdings/jensen-agent-core\";\nimport { stableHash } from \"../tool-call/canonicalize.js\";\nimport { fingerprintCall } from \"./fingerprint.js\";\n\n/**\n * Provider-independent Tool Storm Breaker.\n *\n * Detects repeated / alternating / no-progress tool calls and escalates through\n * bounded, user-visible stages. Progress is determined ONLY from structured\n * events (authoritative state change), never from model prose.\n */\n\nexport const TOOL_CALL_DUPLICATE_NO_PROGRESS = \"TOOL_CALL_DUPLICATE_NO_PROGRESS\";\nexport const TOOL_CALL_STORM_BLOCKED = \"TOOL_CALL_STORM_BLOCKED\";\nexport const TOOL_STRATEGY_PIVOT_REQUIRED = \"TOOL_STRATEGY_PIVOT_REQUIRED\";\n\nexport type StormStage =\n\t| \"fresh\"\n\t| \"duplicate_annotate\"\n\t| \"no_progress_reflect\"\n\t| \"storm_blocked\"\n\t| \"strategy_pivot_required\";\n\nexport interface StormThresholds {\n\t/** Duplicate count at which we start annotating. */\n\tannotate: number;\n\t/** Duplicate count at which we require structured reflection. */\n\treflect: number;\n\t/** Duplicate count at which we block the repeated call. */\n\tblock: number;\n\t/** Duplicate count at which we fail the current strategy. */\n\tterminal: number;\n\t/** Max storm history entries retained. */\n\tmaxHistory: number;\n}\n\nexport const DEFAULT_STORM_THRESHOLDS: StormThresholds = {\n\tannotate: 2,\n\treflect: 3,\n\tblock: 4,\n\tterminal: 6,\n\tmaxHistory: 256,\n};\n\n/** Structured event describing authoritative progress/state change. */\nexport type StormProgressSignal =\n\t| { kind: \"new_evidence\"; evidenceHash: string }\n\t| { kind: \"file_content_hash\"; path: string; hash: string }\n\t| { kind: \"diagnostics_hash\"; hash: string }\n\t| { kind: \"process_state\"; identity: string; state: string }\n\t| { kind: \"provider_result\"; resultHash: string }\n\t| { kind: \"transaction_state\"; transactionId: string; state: string }\n\t| { kind: \"user_input\"; inputHash: string }\n\t| { kind: \"retry_window_elapsed\"; elapsedMs: number };\n\nexport interface StormContext {\n\tworkspaceScope?: string;\n\trunScope?: string;\n\t/** Authoritative current state against which to detect progress. */\n\tprogressSignals: StormProgressSignal[];\n\t/** Whether the tool is read-only (required for authoritative cache reuse). */\n\treadOnly: boolean;\n\t/** Whether the policy permits reusing a prior result (default true for read-only). */\n\tpolicyAllowsReuse?: boolean;\n}\n\nexport interface StormClassifyInput extends StormContext {\n\ttoolName: string;\n\tcanonicalArgsHash: string;\n\t/** Optional cached prior result for reuse. */\n\tpriorResult?: { result: AgentToolResult<any>; validityFingerprint: string; at: number };\n\t/** Current prior result validity fingerprint (authoritative state identity). */\n\tcurrentValidityFingerprint?: string;\n}\n\nexport type StormDecision =\n\t| { stage: \"fresh\"; execute: true }\n\t| {\n\t\t\tstage: \"duplicate_annotate\";\n\t\t\texecute: true;\n\t\t\treason: string;\n\t\t\tcachedResult?: AgentToolResult<any>;\n\t\t\tduplicateCount: number;\n\t  }\n\t| { stage: \"no_progress_reflect\"; execute: true; reason: string; duplicateCount: number; requireReflection: true }\n\t| {\n\t\t\tstage: \"storm_blocked\";\n\t\t\texecute: false;\n\t\t\terrorCode: \"TOOL_CALL_STORM_BLOCKED\";\n\t\t\treason: string;\n\t\t\tduplicateCount: number;\n\t  }\n\t| {\n\t\t\tstage: \"strategy_pivot_required\";\n\t\t\texecute: false;\n\t\t\terrorCode: \"TOOL_STRATEGY_PIVOT_REQUIRED\";\n\t\t\treason: string;\n\t\t\tduplicateCount: number;\n\t  };\n\ninterface HistoryEntry {\n\tfingerprint: string;\n\tfirstAt: number;\n\tlastAt: number;\n\tcount: number;\n\tlastStateHash: string | null;\n\tcachedResult?: { result: AgentToolResult<any>; validityFingerprint: string; at: number };\n}\n\n/**\n * Durable storm-breaker state for one run/workspace. One instance per active\n * agent strategy; reset at user turn boundaries. Reads only structured signals\n * for progress.\n */\nexport class StormBreaker {\n\tprivate history = new Map<string, HistoryEntry>();\n\tprivate thresholds: StormThresholds;\n\n\tconstructor(\n\t\tthresholds: Partial<StormThresholds> = {},\n\t\tprivate now: () => number = () => Date.now(),\n\t) {\n\t\tthis.thresholds = { ...DEFAULT_STORM_THRESHOLDS, ...thresholds };\n\t}\n\n\t/** Combine structured progress signals into an authoritative state hash. */\n\tprivate stateHash(signals: StormProgressSignal[]): string | null {\n\t\tif (signals.length === 0) return null;\n\t\treturn stableHash(signals.map((s) => ({ k: s.kind, v: \"value\" in s ? s.value : null })));\n\t}\n\n\t/**\n\t * Classify a call. Returns whether to execute and with what annotation.\n\t * Progress detection: if the authoritative state hash differs from the last\n\t * recorded state for this fingerprint, the call is treated as FRESH (context\n\t * changed → not a duplicate).\n\t */\n\tclassify(input: StormClassifyInput): StormDecision {\n\t\tconst fp = fingerprintCall({\n\t\t\ttoolName: input.toolName,\n\t\t\tcanonicalArgsHash: input.canonicalArgsHash,\n\t\t\tworkspaceScope: input.workspaceScope,\n\t\t\trunScope: input.runScope,\n\t\t});\n\t\tconst stateHash = this.stateHash(input.progressSignals);\n\t\tconst now = this.now();\n\n\t\tlet entry = this.history.get(fp);\n\t\tif (!entry) {\n\t\t\tentry = { fingerprint: fp, firstAt: now, lastAt: now, count: 1, lastStateHash: stateHash };\n\t\t\tthis.history.set(fp, entry);\n\t\t\tif (this.history.size > this.thresholds.maxHistory) {\n\t\t\t\tthis.evictOldest();\n\t\t\t}\n\t\t\treturn { stage: \"fresh\", execute: true };\n\t\t}\n\n\t\t// Progress check: state changed since last recorded → not a duplicate.\n\t\t// A transition from null (no signal) to a concrete hash also counts as\n\t\t// authoritative progress.\n\t\tif (stateHash !== null && stateHash !== entry.lastStateHash) {\n\t\t\tentry.count = 1;\n\t\t\tentry.lastStateHash = stateHash;\n\t\t\tentry.firstAt = now;\n\t\t\treturn { stage: \"fresh\", execute: true };\n\t\t}\n\t\tentry.lastStateHash = stateHash;\n\t\tentry.count += 1;\n\t\tentry.lastAt = now;\n\n\t\tconst reuseAllowed =\n\t\t\tinput.readOnly &&\n\t\t\tinput.policyAllowsReuse !== false &&\n\t\t\tentry.cachedResult !== undefined &&\n\t\t\tentry.cachedResult.validityFingerprint === input.currentValidityFingerprint;\n\n\t\tconst cached = reuseAllowed ? entry.cachedResult!.result : undefined;\n\t\tconst count = entry.count;\n\n\t\tif (count >= this.thresholds.terminal) {\n\t\t\treturn {\n\t\t\t\tstage: \"strategy_pivot_required\",\n\t\t\t\texecute: false,\n\t\t\t\terrorCode: TOOL_STRATEGY_PIVOT_REQUIRED,\n\t\t\t\treason: `call ${input.toolName} repeated ${count}x with zero authoritative progress`,\n\t\t\t\tduplicateCount: count,\n\t\t\t};\n\t\t}\n\t\tif (count >= this.thresholds.block) {\n\t\t\treturn {\n\t\t\t\tstage: \"storm_blocked\",\n\t\t\t\texecute: false,\n\t\t\t\terrorCode: TOOL_CALL_STORM_BLOCKED,\n\t\t\t\treason: `repeated identical ${input.toolName} call (${count}x) with no new evidence`,\n\t\t\t\tduplicateCount: count,\n\t\t\t};\n\t\t}\n\t\tif (count >= this.thresholds.reflect) {\n\t\t\treturn {\n\t\t\t\tstage: \"no_progress_reflect\",\n\t\t\t\texecute: true,\n\t\t\t\treason: `no progress detected across ${count} identical ${input.toolName} calls`,\n\t\t\t\tduplicateCount: count,\n\t\t\t\trequireReflection: true,\n\t\t\t};\n\t\t}\n\t\t// annotate (>= annotate threshold); reuse cache when allowed.\n\t\tif (count >= this.thresholds.annotate) {\n\t\t\treturn {\n\t\t\t\tstage: \"duplicate_annotate\",\n\t\t\t\texecute: true,\n\t\t\t\treason: `duplicate ${input.toolName} call (${count}x)`,\n\t\t\t\tcachedResult: cached,\n\t\t\t\tduplicateCount: count,\n\t\t\t};\n\t\t}\n\t\treturn { stage: \"fresh\", execute: true };\n\t}\n\n\t/** Record a cached authoritative result for later read-only reuse. */\n\trecordResult(input: {\n\t\ttoolName: string;\n\t\tcanonicalArgsHash: string;\n\t\tworkspaceScope?: string;\n\t\trunScope?: string;\n\t\tresult: AgentToolResult<any>;\n\t\tvalidityFingerprint: string;\n\t}): void {\n\t\tconst fp = fingerprintCall({\n\t\t\ttoolName: input.toolName,\n\t\t\tcanonicalArgsHash: input.canonicalArgsHash,\n\t\t\tworkspaceScope: input.workspaceScope,\n\t\t\trunScope: input.runScope,\n\t\t});\n\t\tconst entry = this.history.get(fp) ?? {\n\t\t\tfingerprint: fp,\n\t\t\tfirstAt: this.now(),\n\t\t\tlastAt: this.now(),\n\t\t\tcount: 1,\n\t\t\tlastStateHash: null,\n\t\t};\n\t\tentry.cachedResult = { result: input.result, validityFingerprint: input.validityFingerprint, at: this.now() };\n\t\tthis.history.set(fp, entry);\n\t}\n\n\t/** Register an authoritative progress signal so subsequent calls are fresh. */\n\tnoteProgress(signals: StormProgressSignal[]): void {\n\t\tif (signals.length === 0) return;\n\t\tconst hash = this.stateHash(signals);\n\t\tif (hash === null) return;\n\t\tfor (const entry of this.history.values()) {\n\t\t\tif (entry.lastStateHash !== hash) {\n\t\t\t\tentry.lastStateHash = hash;\n\t\t\t\tentry.count = 1; // state changed → the next identical call is fresh\n\t\t\t}\n\t\t}\n\t}\n\n\t/** Reset per-turn state (fresh user turn). */\n\treset(): void {\n\t\tthis.history.clear();\n\t}\n\n\tsnapshot(): { historySize: number; counts: Record<string, number> } {\n\t\tconst counts: Record<string, number> = {};\n\t\tfor (const [fp, e] of this.history) counts[fp] = e.count;\n\t\treturn { historySize: this.history.size, counts };\n\t}\n\n\tprivate evictOldest(): void {\n\t\tlet oldestKey: string | null = null;\n\t\tlet oldestAt = Infinity;\n\t\tfor (const [k, e] of this.history) {\n\t\t\tif (e.lastAt < oldestAt) {\n\t\t\t\toldestAt = e.lastAt;\n\t\t\t\toldestKey = k;\n\t\t\t}\n\t\t}\n\t\tif (oldestKey) this.history.delete(oldestKey);\n\t}\n}\n"]}