@sema-agent/core
Version:
Stateless, task-oriented AI agent core
118 lines (117 loc) • 6.53 kB
TypeScript
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;