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 (132 loc) • 6.62 kB
TypeScript
/**
* 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;