UNPKG

agentfootprint

Version:

The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.

133 lines • 6.67 kB
/** * CacheDecision subflow — provider-agnostic translation from * `activeInjections + DSL directives` → `CacheMarker[]`. * * This is the core "policy → markers" Lego layer. It runs every * iteration (after slot subflows produce their output, before the * CacheGate decider). Pure transform: no IO, no LLM calls, no * provider knowledge. * * Algorithm: * 1. Build a `CachePolicyContext` from agent state * 2. For each injection in `activeInjections`, evaluate its * `metadata.cache` directive against the context → cacheable boolean * 3. For each slot (system / tools / messages): * a. Walk the slot's contributions in order * b. Find the LAST index that's contiguous-from-start cacheable * c. Emit one CacheMarker at that boundary if any cacheable * 4. The moving breakpoint: when EVERYTHING the policies govern before * the conversation is cacheable — every tools entry, every system * entry, every delivered message — mark the conversation's LAST * message, so the next call (which repeats the conversation and * appends to it) reads the whole prefix back. See * `conversationTailMarker`. * * Nothing is marked when the tool schemas change on every call * (`toolsVaryPerCall`, the findings offer): tools render first on a * breakpoint wire, so every breakpoint would pay the write premium on bytes * the next call does not send. * * Each marker is provider-agnostic. Provider strategy translates * to wire format in Phase 6+. * * Special case — base system prompt: the agent's * `agent.getSystemPromptCachePolicy()` value is folded in at index 0 * of the system slot. Always-on injections (Steering / Fact / * always-active rules) follow. */ import type { TypedScope } from 'footprintjs'; import type { CacheMarker, CachePolicy, CachePolicyContext } from './types.js'; import type { LLMMessage } from '../provider-contract/types.js'; import type { Injection } from '../injection-engine/types.js'; /** * Subflow scope state. Set via inputMapper from the agent's parent * scope; produces `cacheMarkers` consumed by the BuildLLMRequest stage. */ export interface CacheDecisionState { readonly activeInjections: readonly Injection[]; readonly iteration: number; readonly maxIterations: number; readonly userMessage: string; readonly lastToolName?: string; /** * Cumulative input tokens spent across all LLM calls in THIS * `agent.run()` invocation only. Resets at the start of each turn * (each `agent.run()` call). Predicates can use this for budget- * aware cache invalidation (e.g., "flush cache after 50K tokens"). */ readonly cumulativeInputTokens: number; /** * Base system prompt's cache policy (from * `agent.getSystemPromptCachePolicy()`). Folded in at index 0 of * the system slot's cache evaluation, ahead of any always-on * injections. */ readonly systemPromptCachePolicy: CachePolicy; /** Global kill switch. When `true`, subflow emits zero markers. */ readonly cachingDisabled: boolean; /** * The window as the request will carry it (7.21) — post-window-strategy, * post-delivery. Only the `messages` marker needs it, and it needs it for * one reason: that marker's `boundaryIndex` is read by providers as an * index INTO THIS ARRAY, so it has to be computed here from the array * itself. Optional so the standalone/unit uses of `computeCacheMarkers` * that predate delivery keep working; absent means no messages marker — * neither a delivered-message one nor the conversation tail. */ readonly history?: readonly LLMMessage[]; /** * `true` when the tool schemas are re-decorated on every call — the * findings offer (`.findings()`) binds the served result ids into every * tool's `_findings` schema, newest first, so the list changes whenever a * result is served. Tools render FIRST on a breakpoint wire (Anthropic: * tools → system → messages), so no prefix through them repeats from one * tool-calling iteration to the next, and every breakpoint would pay the * cache-write premium on bytes the next call does not send — measured on * the inputs bench (`bench/cache`), 97% of request bytes marked and 5% of * the repeat calls' bytes read back. The * decision places none. Absent ≡ `false`. The agent charts pass it at BUILD * time (`cacheDecisionStage`), never through scope. */ readonly toolsVaryPerCall?: boolean; cacheMarkers: readonly CacheMarker[]; } /** * Evaluate a `CachePolicy` against the current context. * Returns `true` if the policy says THIS iteration's content is cacheable. */ export declare function evaluateCachePolicy(policy: CachePolicy, ctx: CachePolicyContext): boolean; /** * Identify which slots an injection contributes to. An injection can * target multiple slots simultaneously (Skills target both system + * tools); we visit each contributing slot independently. */ export declare function injectionTargetSlots(injection: Injection): ReadonlyArray<'system' | 'tools' | 'messages'>; /** * Pure transform: state → markers. Exported so tests can exercise * the algorithm directly without the FlowChartExecutor ceremony of * mounting the subflow as a child of a parent chart. * * The subflow body (`decide` below) is a thin wrapper that pulls * state from scope and delegates here. */ export declare function computeCacheMarkers(state: Omit<CacheDecisionState, 'cacheMarkers'>): readonly CacheMarker[]; /** What a chart knows about its own wire when it builds the decision stage. */ export interface CacheDecisionStageOptions { /** * The chart re-decorates every tool schema per call (the findings door) — * see {@link CacheDecisionState.toolsVaryPerCall}. A BUILD-time fact, so it * is closed over here rather than read from scope: an unarmed agent's * decision stage reads exactly the keys it always did. */ readonly toolsVaryPerCall?: boolean; } /** * The decision stage, built for one chart. Thin scope-binding wrapper around * `computeCacheMarkers`, so it can serve as the ROOT stage of the `sf-cache` * subflow (a chart cannot start with a nested subflow) — no logic * duplication. */ export declare function cacheDecisionStage(options?: CacheDecisionStageOptions): (scope: TypedScope<CacheDecisionState>) => void; /** The decision stage for a chart whose tool schemas are stable per call. */ export declare const decideCacheMarkers: (scope: TypedScope<CacheDecisionState>) => void; //# sourceMappingURL=CacheDecisionSubflow.d.ts.map