UNPKG

@genkit-ai/ai

Version:

Genkit AI framework generative AI APIs.

212 lines (208 loc) 10.8 kB
import { SessionStore, SessionStoreOptions, GetSnapshotOptions, SnapshotMutator } from './session.js'; import { SessionSnapshot } from './agent-types.js'; import '@genkit-ai/core'; import '@genkit-ai/core/async'; import '@genkit-ai/core/registry'; import './model-types.js'; import './parts.js'; /** * Copyright 2026 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ /** * In-memory implementation of persistent Session Store. */ declare class InMemorySessionStore<S = unknown> implements SessionStore<S> { private snapshots; private listeners; private rejectBranchingSessions; /** * @param options.rejectBranchingSessions When `true`, a `sessionId` lookup * that resolves to a branched history (more than one leaf) throws * `FAILED_PRECONDITION` instead of returning the latest leaf. Defaults to * `false`; opt in (e.g. in dev) to surface accidental branching early. */ constructor(options?: { rejectBranchingSessions?: boolean; }); getSnapshot(opts: GetSnapshotOptions): Promise<SessionSnapshot<S> | undefined>; saveSnapshot(snapshotId: string | undefined, mutator: SnapshotMutator<S>, options?: SessionStoreOptions): Promise<string | null>; onSnapshotStateChange(snapshotId: string, callback: (snapshot: SessionSnapshot<S>) => void, options?: SessionStoreOptions): void | (() => void); } /** * A Node.js file-system backed session snapshot store. * * Snapshots are stored as flat JSON files keyed by their `snapshotId`, under an * optional per-tenant sub-directory `prefix`: * * File layout: `dirPath/<prefix>/<snapshotId>.json` * * `getSnapshot({ sessionId })` resolves the session's current leaf via a tiny * per-session pointer file (`<prefix>/.pointers/<sessionId>.json`, see * {@link PointerDoc}) - one pointer read plus one snapshot read. When the * pointer is missing (e.g. a legacy store) or stale it transparently falls back * to scanning the prefix directory and selecting the single leaf whose * `sessionId` matches, then rewrites the pointer so subsequent lookups are fast * again. */ declare class FileSessionStore<S = unknown> implements SessionStore<S> { private dirPath; private maxPersistedChainLength?; private snapshotPathPrefix?; private rejectBranchingSessions; private snapshotWatchPollIntervalMs; /** * Per-file write locks. The {@link SessionStore} contract (and the * abort-aware mutator that branches on `current.status`) assumes * read-modify-write is atomic, but on the file system a read and the * `writeFile` below it are not. Without a lock two concurrent saves can * read the same `current` and the later write clobbers the earlier one * (e.g. a `completed` write overwriting a concurrent `aborted`). We * serialize saves per resolved file path with a simple promise chain. */ private writeLocks; /** * @param dirPath Directory where snapshot JSON files are stored. * @param options.maxPersistedChainLength When set, snapshots older than this * many entries in a chain are automatically deleted on each save. * @param options.snapshotPathPrefix Returns a sub-directory prefix derived * from the call's {@link SessionStoreOptions} (e.g. the authenticated user * id from `options.context`), useful for multi-tenant isolation: all reads * and writes are scoped to that prefix, so one tenant can never see * another's snapshots. Defaults to `"global"`. * @param options.rejectBranchingSessions When `true`, a `sessionId` lookup * that resolves to a branched history (more than one leaf) throws * `FAILED_PRECONDITION` instead of returning the latest leaf. Defaults to * `false`; opt in (e.g. in dev) to surface accidental branching early. * @param options.snapshotWatchPollIntervalMs Polling interval (ms) for the * {@link FileSessionStore.onSnapshotStateChange} fallback that backstops * `fs.watch` (which can miss events on some filesystems, e.g. network * mounts). Defaults to {@link DEFAULT_SNAPSHOT_WATCH_POLL_INTERVAL_MS}. */ constructor(dirPath: string, options?: { maxPersistedChainLength?: number; snapshotPathPrefix?: (options?: SessionStoreOptions) => string; rejectBranchingSessions?: boolean; snapshotWatchPollIntervalMs?: number; }); private ensureDir; /** Resolves the (per-tenant) directory snapshots are stored under. */ private prefixDir; /** * Resolves the file path for a given snapshotId: `<prefix>/<snapshotId>.json`. */ private getFilePath; /** Resolves the (per-tenant) directory holding per-session pointer files. */ private pointersDir; /** * Resolves the pointer file path for a session, validating `sessionId` is a * plain basename so it can never escape the pointers directory. Pure: it does * not create the directory, so the read path stays side-effect free. The * write path calls {@link ensureDir} before writing. */ private getPointerPath; /** * Reads the per-session {@link PointerDoc}, or `undefined` when it is missing * (legacy store / not yet written) or unreadable / corrupt - callers fall * back to a full directory scan in that case. Best-effort: any IO/parse error * resolves to `undefined` so the optimization can never make a lookup (or * save) fail where the scan-only baseline would have succeeded. An invalid * `sessionId` still throws (path validation is resolved outside the try) so it * fails fast rather than silently being ignored. */ private readPointer; /** * Atomically writes the per-session {@link PointerDoc}. Best-effort: a * pointer write failure is swallowed since the pointer is only an * optimization - `sessionId` lookups still self-heal via the full scan. An * invalid `sessionId` still throws (path validation is resolved outside the * try) so it fails fast rather than silently being ignored. */ private writePointer; /** * Serializes async work per resolved file path so a read-modify-write in * {@link saveSnapshot} is not interleaved with a concurrent one for the same * snapshot (see {@link writeLocks}). */ private withFileLock; getSnapshot(opts: GetSnapshotOptions): Promise<SessionSnapshot<S> | undefined>; /** * Loads a single snapshot file by its id (no sessionId branch). Used by * internal traversal (parent chains) where we always have a concrete id. */ private getSnapshotById; /** * Resolves the latest (leaf) snapshot for a session. * * Fast path: read the per-session pointer file and load the leaf it names - * one pointer read plus one snapshot read, independent of session count / * length. The pointer is skipped (and the scan used) when * `rejectBranchingSessions` is set, since detecting branches requires seeing * every leaf. * * Fallback (no/stale/corrupt pointer, or branch detection): scan every * snapshot file in the prefix directory, keep those whose `sessionId` * matches, select the single leaf, and refresh the pointer so later lookups * take the fast path. * * Known limitation: the fast path trusts the pointer when the snapshot it * names still exists and belongs to the session - it does not re-verify that * it is the actual leaf. So if a save succeeds but the subsequent (best-effort) * `writePointer` does not (crash/disk error), or two new saves for the same * session race and the older one writes the pointer last, the pointer can * linger on a valid-but-older same-session snapshot and lookups return it * until the next save advances the pointer. This is the accepted trade-off for * a best-effort cache: verifying leaf-ness on every read would reintroduce the * full scan the pointer exists to avoid. Callers needing strict guarantees can * resume by `snapshotId`, or set `rejectBranchingSessions` (which always * scans). */ private getLatestSnapshotForSession; saveSnapshot(snapshotId: string | undefined, mutator: SnapshotMutator<S>, options?: SessionStoreOptions): Promise<string | null>; private saveSnapshotUnlocked; /** * Writes `contents` to `filePath` atomically: write to a temp file in the * same directory, then rename over the target. `rename` is atomic on POSIX * and Windows, so a concurrent reader in {@link getSnapshot} never observes a * half-written (torn) file. */ private atomicWrite; /** * Watches a single snapshot file for changes and invokes `callback` with the * parsed snapshot whenever it changes. * * Unlike {@link InMemorySessionStore}, file-backed snapshots are frequently * mutated by a *different* process (e.g. the request handler that received an * abort writes `status: 'aborted'`, while a detached background worker is the * one watching). Detecting that requires observing the filesystem rather than * in-process `saveSnapshot` calls. * * Reliability comes from two layers: * - `fs.watch` on the (per-tenant) prefix directory, filtered to the target * `<snapshotId>.json`. This is low latency but can miss events on some * filesystems (network mounts, certain container volumes). * - A polling fallback (`snapshotWatchPollIntervalMs`) that re-reads the file * on an interval, backstopping any events `fs.watch` drops. Its timer is * `unref`'d so it never keeps the process alive on its own. * * Callbacks are de-duplicated by serialized content, so the noisy/duplicate * events `fs.watch` emits collapse into one callback per real change. * Transient read errors (e.g. a partially written file mid-rewrite, or a * not-yet-created file) are swallowed; the next event/poll re-reads. * * @returns An unsubscribe function that stops watching and polling. */ onSnapshotStateChange(snapshotId: string, callback: (snapshot: SessionSnapshot<S>) => void, options?: SessionStoreOptions): void | (() => void); } export { FileSessionStore, InMemorySessionStore };