@genkit-ai/ai
Version:
Genkit AI framework generative AI APIs.
212 lines (208 loc) • 10.8 kB
TypeScript
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 };