UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

118 lines (117 loc) • 6.53 kB
import type { ToolFingerprintInput } from "../core/cache-break-detector.js"; /** Every prefix-shaping input a request carries — the CLASSIFICATION DOMAIN. Adding a field here * without classifying it in {@link PREFIX_INPUT_CLASSIFICATION} is a compile error. */ export interface RawPrefixInputs { /** Provider namespace (cache namespaces never span providers). */ provider: string; /** Model id (a switch is a new cache namespace). */ modelId: string; /** The run's prompt identity (`promptIdentityOf`: catalog, renderers, overlay layers, binding) — the manifest's * `identity.artifact`. */ artifactDigest: string; /** Presentation/policy axis: promptProfile + fable mitigations (prose forks on these). */ promptProfile: string; fableMitigations: boolean; /** Physical block layout as SERVED (derived from the composed system blocks; single-block default). */ layoutGroups: ReadonlyArray<{ groupId: string; cacheControlBoundary: boolean; }>; /** The composed stable system text (digested — text never enters the snapshot object). * UNSALTED by design, same rule as the tool wireSchemaDigest ("schemas are code"): a full-text * sha of a LONG composed prompt is an equality oracle only for someone who already possesses the * exact bytes — unlike the SHORT guessable manifest blocks (env facts, one-line memory) whose * dictionary-preimage risk is why block hashes are salted. Cross-run comparability is the whole * point of this identity; salting would destroy it. */ stableSystemText: string; /** Wire-face tool projections (name/description/schema — the mounted set, post-defer). */ toolWire: ReadonlyArray<ToolFingerprintInput>; /** Request-shaping controls that alter the wire body. `thinkingLevel: null` = no level declared (the * request carries no thinking key — provider default), which is NOT the same request as an explicit * `"off"` (the disable key) and so must not share its identity. */ thinkingLevel: string | null; maxTokens: number | undefined; hasOutputSchema: boolean; /** Routing/audit fields — provably NOT part of the cacheable prefix. */ sessionId: string; taskId: string | undefined; /** Per-call variables — never reusable, never identity. */ turn: number; cacheRead: number; } export type PrefixInputClass = "cache-identity" | "volatile-no-reuse" | "proven-irrelevant"; /** The EXHAUSTIVE classification (§10.2). The `satisfies` bound is the CI gate: a new * {@link RawPrefixInputs} field fails typecheck until classified here; the runtime pin * (`test/turn-snapshot.test.ts`) asserts totality survives refactors. Reasons ride inline. */ export declare const PREFIX_INPUT_CLASSIFICATION: { readonly provider: "cache-identity"; readonly modelId: "cache-identity"; readonly artifactDigest: "cache-identity"; readonly promptProfile: "cache-identity"; readonly fableMitigations: "cache-identity"; readonly layoutGroups: "cache-identity"; readonly stableSystemText: "cache-identity"; readonly toolWire: "cache-identity"; readonly thinkingLevel: "cache-identity"; readonly maxTokens: "cache-identity"; readonly hasOutputSchema: "cache-identity"; readonly sessionId: "proven-irrelevant"; readonly taskId: "proven-irrelevant"; readonly turn: "volatile-no-reuse"; readonly cacheRead: "volatile-no-reuse"; }; /** The lowering face (§10.1): version + wire form + that form's KNOWN intentional divergences. */ export interface LoweringRecord { version: string; form: "openai-merged" | "anthropic-blocks" | "other"; intentionalDivergences: readonly string[]; } export declare const LOWERING_VERSION = "sema-lowering@1"; export declare function loweringRecordFor(api: string): LoweringRecord; /** The nine identity elements, individually digested (observable without shipping prompt text). */ export interface CacheIdentityElements { provider: string; modelId: string; artifactDigest: string; policyDigest: string; layoutDigest: string; stableSystemDigest: string; toolWireDigest: string; requestPolicyDigest: string; loweringVersion: string; } export interface TurnPromptSnapshot { elements: CacheIdentityElements; /** sha256(canonical(elements)) — the ONE nine-element identity value. */ cacheIdentity: string; lowering: LoweringRecord; /** invoked after EVERY refresh so the run loop can emit the changed identity * (`prompt.snapshot_changed` trace); traces must never keep claiming a superseded prefix. */ onChange?: (snapshot: TurnPromptSnapshot) => void; /** Refresh the tool half (design/36 rematerialize follows the declared mid-run change). */ refreshTools(toolWire: RawPrefixInputs["toolWire"]): void; /** Refresh the stable-system half (a compaction-boundary center change — pass the COMMITTED identity so the * identity never mixes old-artifact/new-prompt). */ refreshStableSystem(stableSystemText: string, artifactDigest?: string): void; /** request-policy element follows mid-run wire changes (finalize lanes flip the * thinking level; a served-model swap re-namespaces the cache). */ refreshRequestPolicy(p: { thinkingLevel?: string; maxTokens?: number | null; }): void; refreshModel(m: { provider: string; modelId: string; }): void; } /** Build the run's snapshot from classified raw inputs. Volatile/irrelevant fields are accepted so * the CLASSIFICATION stays total over everything callers hold, but only identity-class fields can * reach the digest — enforced structurally (this function reads exactly the identity fields). * * `promptHasher`: the digest applied to COMPOSED PROMPT TEXT — callers inject a * KEYED/salted hasher (the run loop uses the per-process manifest salt): a fixed public prefix + * short unknown suffix (an env fact, a memory note, a tenant string) makes an unsalted full-text * sha ENUMERABLE, recreating exactly the disclosure the salted block hashes prevent. Consequence, * documented: `stableSystemDigest` (and thus `cacheIdentity`) compares within one process only; * cross-process reconciliation rides the CODE-class digests (artifact/toolWire — unsalted). */ export declare function buildTurnPromptSnapshot(raw: Omit<RawPrefixInputs, "turn" | "cacheRead">, api: string, promptHasher?: (text: string) => string): TurnPromptSnapshot;