agentfootprint
Version:
The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.
128 lines • 5.91 kB
TypeScript
/**
* CacheGate — runtime decider that gates cache-marker application.
*
* Runs every iteration AFTER the CacheDecision subflow produces
* `scope.cacheMarkers` and BEFORE the BuildLLMRequest stage applies
* them. Three rules can fall through to "no-markers" (skip caching);
* default branch is "apply-markers" (proceed with caching).
*
* Why a decider stage and not a function: footprintjs's `decide()`
* captures evidence on `FlowRecorder.onDecision` natively. The
* `cacheRecorder()` (Phase 9) reads
* `event.evidence.rules.find(r => r.matched).inputs[]` to surface
* WHY caching was applied or skipped each iter. Same channel
* footprintjs uses for every other decision; same renderer in Lens.
*
* Three rules (evaluated top-down; first match wins):
* 1. Kill switch — `Agent.create({ caching: 'off' })` was set
* 2. Hit-rate floor — recent hit rate < 30%; cache writes outpacing
* reads, auto-disable to avoid the cache-write penalty
* 3. Skill churn — active skills changing too rapidly for caching
* to amortize (Anthropic LLM expert's concern from Phase 4 review)
*
* Default branch (no rule matches): `'apply-markers'`.
*/
import { type DecisionResult, type TypedScope } from 'footprintjs';
/**
* Subset of agent scope state the CacheGate decider reads.
* Phase 6 wires these keys into the agent's main chart.
*/
export interface CacheGateState {
/**
* Global kill switch. When `true`, decider skips caching
* unconditionally. Set via `Agent.create({ caching: 'off' })`.
*/
readonly cachingDisabled: boolean;
/**
* Running cache hit rate from the last N iterations (0..1).
* `undefined` when no cache events have fired yet (e.g., iter 1 of
* the first turn — no history). The cacheRecorder (Phase 9)
* computes this from `CacheMetrics` events.
*/
readonly recentHitRate: number | undefined;
/**
* Rolling window of active-skill IDs across recent iterations,
* one per iteration (latest LAST). Maintained by the
* UpdateSkillHistory function stage (Phase 6 mount).
*
* `undefined` entry = no skill active that iteration.
*/
readonly skillHistory: readonly (string | undefined)[];
}
/**
* Hit-rate floor below which we auto-disable caching. The 30% number
* is calibrated for Anthropic's pricing: cache write costs +25%
* premium, cache read costs 90% off. Break-even at ~25% hit rate.
* 30% gives a buffer; below that we're losing money on writes that
* never recoup.
*
* Reasoning: if hit rate is X, cost-per-token vs no caching is
* (1 - X) * 1.0 + X * 0.1 // baseline
* minus
* write_iters * 1.25 + read_iters * 0.1 // with caching
* Solving for break-even gives X ≈ 0.25 for typical agent shapes.
*/
export declare const HIT_RATE_FLOOR = 0.3;
/**
* Window size for skill-churn detection. Last 5 iterations of
* active skill IDs are inspected.
*/
export declare const SKILL_CHURN_WINDOW = 5;
/**
* Threshold above which skill churn is considered detected: this many
* UNIQUE skills in the rolling window. With window=5 and threshold=3,
* the pattern A → B → A → C still triggers (3 unique skills in 4 iters).
*/
export declare const SKILL_CHURN_THRESHOLD = 3;
/**
* Pure helper: detect skill churn given a rolling history.
* Exported for direct testing without decider/scope ceremony.
*/
export declare function detectSkillChurn(history: readonly (string | undefined)[], windowSize?: number, threshold?: number): boolean;
/**
* Branch routing keys for the CacheGate decider. Two outcomes:
* apply markers (proceed with cache) or skip (no markers this iter).
*/
export type CacheGateBranch = 'apply-markers' | 'no-markers';
/**
* The decider function. Mounted via `addDeciderFunction` in the
* agent's main chart in Phase 6.
*
* Returns a `DecisionResult` (footprintjs's `decide()` helper output)
* which the engine unwraps via `.branch` for routing AND publishes
* `evidence.rules[matched].inputs[]` to FlowRecorder.onDecision.
* cacheRecorder (Phase 9) subscribes to that channel for the audit trail.
*
* For non-routing consumers (testing the decision in isolation), read
* the `.branch` field of the returned DecisionResult.
*/
export declare function cacheGateDecide(scope: TypedScope<CacheGateState>): DecisionResult;
/**
* Update the skill-history rolling window. Called as a function
* stage BEFORE the CacheGate decider. Reads the current iteration's
* active skill (the MOST-RECENTLY activated one — the TAIL of
* `activatedInjectionIds`) and appends it to the `skillHistory` array.
*
* WHY THE TAIL, NOT THE HEAD: `read_skill` APPENDS each newly-activated
* skill to the end of `activatedInjectionIds`, and the list is cumulative
* + deduped per turn (reset only at seed). So the HEAD is frozen at the
* FIRST skill activated and never changes mid-turn — sampling it made the
* window record one constant value, so `detectSkillChurn` could never fire
* (the skill-churn cache rule was effectively dead). The tail tracks what
* the agent just switched to, which is the churn signal we actually want.
*
* KNOWN LIMITATION: if several skills are activated in the SAME iteration,
* only the last (tail) is recorded for that iteration — churn can
* under-count a multi-skill burst. A fully order-independent signal would
* read `activatedInjectionIds.length` directly in the gate; that is a
* larger change deferred for now.
*
* Window length is bounded at `SKILL_CHURN_WINDOW * 2` so the array
* doesn't grow unboundedly across long agent runs. Old entries
* fall off the front naturally.
*/
export declare function updateSkillHistory(scope: TypedScope<{
activatedInjectionIds?: readonly string[];
skillHistory: readonly (string | undefined)[];
}>): void;
//# sourceMappingURL=CacheGateDecider.d.ts.map