@sema-agent/core
Version:
Stateless, task-oriented AI agent core
63 lines (62 loc) • 3.27 kB
TypeScript
/**
* Two-phase prefix-cache-break detector (design/31). Each turn it records a structural fingerprint of
* the prefix (system prompt / per-tool schema / tool set / model namespace) and compares the turn's
* `cacheRead` against the prior warm turn; on a confirmed drop it attributes the break to a root cause
* (model switch / a specific tool's drift / tool-set change / system-prefix change / server-or-TTL).
*
* The strict superset of the task-end "low hit-rate" heuristic: it pinpoints the BREAK TURN and names
* the cause, instead of only reporting an aggregate at the end. Provider-agnostic — the only provider
* input is `cacheRead` (OpenAI `cached_tokens` / Anthropic `cache_read_input_tokens`, already normalized).
*
* v1 is task-scoped (compares turns within one run; system/tools are stable so the high-value per-tool
* attribution mostly fires on a model switch or across runs). A per-session v2 would feed a persisted
* prior snapshot — the `observe` contract is already shaped for that.
*/
export interface CacheBreakFinding {
turn: number;
cacheReadBefore: number;
cacheReadAfter: number;
cause: "model-switch" | "tool-schema" | "tool-set" | "system-prefix" | "server-or-ttl";
detail: string;
}
export interface ToolFingerprintInput {
name: string;
description: string;
parameters: unknown;
}
/**
* Project a tool list into the structural inputs the detector fingerprints. Shared by the initial
* snapshot in `prepareTask` AND the per-materialization refresh in tool-disclosure (design/36): both
* MUST derive the fingerprint the same way, or a deferred tool going placeholder→full schema would
* be hashed inconsistently. Keep this the single source of "what counts as the cacheable tool prefix".
*/
export declare function toolsToFingerprintInputs(tools: ReadonlyArray<{
name: string;
description: string;
parameters: unknown;
}>): ToolFingerprintInput[];
export declare class CacheBreakDetector {
private prev;
/**
* Reset the baseline. **Call ONLY after a SUCCESSFUL compaction** (design/31 red line #11): a legit
* prefix shrink isn't a break, but resetting when compaction did NOT happen opens a false-negative
* window (a real break right after would be missed).
*/
notifyCompaction(): void;
/**
* Record this turn's fingerprint + compare cacheRead to the prior warm turn. Returns a finding on a
* confirmed break. Hashes are computed here per call **by design**, taking the raw prefix rather than
* pre-computed hashes. NOTE (design/36): the inputs are NOT necessarily stable within a run — deferred
* tools materialize placeholder→full schema mid-task, so the caller refreshes the fingerprint at
* materialization (via `toolsToFingerprintInputs`). A resulting `tool-schema`/`tool-set` finding on
* that turn is EXPECTED and truthful (the cacheable prefix genuinely changed); the bug this guards
* against is the opposite — a frozen fingerprint hiding that change (a false negative).
*/
observe(input: {
turn: number;
systemPrompt: string;
tools: ToolFingerprintInput[];
modelKey: string;
cacheRead: number;
}): CacheBreakFinding | undefined;
}