/** * Cache-safe prompt injection helpers. * * Provider prompt caches are exact byte-prefix matches over the rendered * request (tools → system → messages). Any transform that rewrites or * reorders earlier conversation content invalidates the cache for everything * after the first changed byte, so every later request in the session re-pays * full input cost and latency. * * These helpers are the single supported way for hooks to add content to the * outgoing payload: * * - `appendTaggedSyntheticPart` appends deterministic content at the tail of * an existing message. Safe because re-running the transform on the next * turn reproduces the same bytes at the same position. * - `stripTaggedContent` + `appendTrailingVolatileMessage` own content that * changes between turns (job boards, status blocks): strip every previously * injected occurrence, then re-append one synthetic message at the very end * of the payload, so churn only ever costs the tail of the prompt. * * Rules the helpers encode (and the cache-safety property tests enforce): * never mutate or reorder earlier messages, never inject unmarked parts, and * never put timestamps or randomness into content injected before the tail. * See docs/cache-verification.md. */ import { type MessageInfo, type MessagePart, type MessageWithParts } from './types'; /** * Cache hint mirrored from the v2 LLM `ContentPart.cache` (`LLM.CacheHint`). * Honored by anthropic-messages / google-vertex / bedrock-converse / * openrouter as a manual cache-breakpoint placement; a no-op elsewhere. */ export interface SyntheticPartCacheHint { type: 'ephemeral' | 'persistent'; ttlSeconds?: number; } interface TaggedSyntheticPartSpec { /** Text content of the injected part. */ text: string; /** * Metadata key marking the part as plugin-injected. Used for dedupe and * strip-before-reappend; must be stable for the lifetime of the feature. */ metadataKey: string; /** Additional metadata merged into the part (the tag key always wins). */ extraMetadata?: Record; /** * Optional cache hint copied onto the created part. v1 callers never * pass it, so the v1 payload stays byte-identical; the v2 context * bridge scopes a per-request default via * `runWithSyntheticPartCacheHintScope` + * `setDefaultSyntheticPartCacheHint` so every part injected on v2 * carries it, with concurrent transforms isolated. */ cache?: SyntheticPartCacheHint; } /** * Run `fn` with an isolated cache-hint scope: `setDefaultSyntheticPartCacheHint` * calls inside `fn` (and its async descendants) mutate only this scope, so * concurrent callers cannot interleave their set/restore operations. */ export declare function runWithSyntheticPartCacheHintScope(fn: () => T): T; /** * Set the scoped default cache hint for parts created while the returned * restore function has not been called. Returns a restore closure that * reinstates the previous default (call it in a `finally`). */ export declare function setDefaultSyntheticPartCacheHint(hint: SyntheticPartCacheHint | undefined): () => void; /** Build a synthetic text part tagged with the given metadata key. */ export declare function createTaggedSyntheticPart(spec: TaggedSyntheticPartSpec): MessagePart; /** True when the part is a synthetic part tagged with the metadata key. */ export declare function isTaggedPart(part: unknown, metadataKey: string): boolean; /** True when any part of the message carries the tag. */ export declare function hasTaggedPart(message: MessageWithParts, metadataKey: string): boolean; /** * Append deterministic content as a tagged synthetic part at the message * tail. The content must be a pure function of session-stable inputs so the * next turn's transform reproduces identical bytes at the same position. */ export declare function appendTaggedSyntheticPart(message: MessageWithParts, spec: TaggedSyntheticPartSpec): void; /** * Remove every part tagged with the metadata key across all messages and * drop messages this empties (covers both legacy in-message placement and * whole synthetic trailing messages). */ export declare function stripTaggedContent(messages: unknown[], metadataKey: string): void; /** * Append volatile content as its own synthetic message at the very end of * the payload. Call `stripTaggedContent` first so at most one instance * exists; the volatile zone must stay strictly behind all stable content. */ export declare function appendTrailingVolatileMessage(messages: unknown[], info: MessageInfo, spec: TaggedSyntheticPartSpec): void; /** * True when the message consists solely of parts tagged with the metadata * key — i.e. it is a plugin-owned volatile trailing message. Used by the * cache-safety tests to separate the stable prefix from the volatile tail. */ export declare function isVolatileTaggedMessage(message: unknown, metadataKey: string): boolean; export {};