UNPKG

@genkit-ai/ai

Version:

Genkit AI framework generative AI APIs.

334 lines (302 loc) 10 kB
/** * 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. */ import { getAsyncContext, type ActionContext } from '@genkit-ai/core'; import { EventEmitter } from '@genkit-ai/core/async'; import type { Registry } from '@genkit-ai/core/registry'; import { type Artifact, type SessionSnapshot, type SessionState, } from './agent-types.js'; import { MessageData } from './model-types.js'; // Re-export the shared agent/session wire schemas + types from their canonical // home (./agent-types.ts) so existing imports from './session.js' keep working. export { AgentFinishReasonSchema, ArtifactSchema, SessionSnapshotSchema, SessionStateSchema, type AgentFinishReason, type Artifact, type SessionSnapshot, type SessionState, } from './agent-types.js'; /** * Input type for {@link SessionStore.saveSnapshot}. * * Identical to {@link SessionSnapshot} except that `snapshotId` is optional. * When omitted the store is responsible for assigning a new identifier * (enabling stores to encode grouping or routing information in the ID). * When provided the store performs an upsert - updating the existing snapshot. */ export type SessionSnapshotInput<S = unknown> = Omit< SessionSnapshot<S>, 'snapshotId' > & { snapshotId?: string; }; /** * Options provided to the session store methods. */ export interface SessionStoreOptions { context?: ActionContext; } /** * Lookup options for {@link SessionStore.getSnapshot}. * * Exactly one of `snapshotId` or `sessionId` must be provided: * * - `snapshotId` loads that specific snapshot. * - `sessionId` loads the *latest* (leaf) snapshot of the session - the most * recent snapshot that no other snapshot points to as its parent. This is * the common case for simple session storage (e.g. `useChat`) where the * client only tracks a stable session id and lets the server remember the * conversation. A session with *branching* snapshots (more than one leaf, * e.g. after a regenerate) has no single "latest"; by default the store * returns the most-recent leaf, but it can be configured to reject the * lookup with `FAILED_PRECONDITION` - in which case, resume by `snapshotId`. */ export interface GetSnapshotOptions { snapshotId?: string; sessionId?: string; context?: ActionContext; } /** * A function that receives the current snapshot and returns the updated * snapshot to persist. * * - Return the mutated snapshot to save it. * - Return `null` to silently skip the update (no-op). * - Throw to abort with an error (e.g. precondition failure). */ export type SnapshotMutator<S = unknown> = ( current: SessionSnapshot<S> | undefined ) => SessionSnapshotInput<S> | null; /** * Interface for persistent session snapshot storage. */ export interface SessionStore<S = unknown> { /** * Loads a snapshot either by its `snapshotId` or by `sessionId`. * * See {@link GetSnapshotOptions} for the lookup semantics (exactly one of * `snapshotId` / `sessionId`; `sessionId` resolves to the session's latest * leaf snapshot, optionally rejecting branching histories). */ getSnapshot( opts: GetSnapshotOptions ): Promise<SessionSnapshot<S> | undefined>; /** * Atomically reads the current snapshot (if `snapshotId` is provided), * passes it to `mutator`, and persists the result. * * - When `snapshotId` is provided the store reads the existing snapshot * and passes it to the mutator. The mutator can inspect the current * state (e.g. to check for concurrent status changes) and return the * updated snapshot to save, or `null` to skip the write. * - When `snapshotId` is `undefined` the store passes `undefined` to * the mutator (signaling a new snapshot). The store assigns a new * identifier. * * Implementations should ensure the read→mutate→write cycle is atomic * to prevent race conditions (e.g. a "done" write overwriting a * concurrent "aborted" status). * * The mutator can: * * - Return a snapshot to save it. * - Return `null` to silently skip the write. * - Throw to abort with an error. * * @returns The `snapshotId` that was used, or `null` when the mutator * returned `null`. */ saveSnapshot( snapshotId: string | undefined, mutator: SnapshotMutator<S>, options?: SessionStoreOptions ): Promise<string | null>; onSnapshotStateChange?( snapshotId: string, callback: (snapshot: SessionSnapshot<S>) => void, options?: SessionStoreOptions ): void | (() => void); } /** * State manager for a session turn, tracking messages, custom state, and artifacts. */ export class Session<S = unknown> extends EventEmitter { private state: SessionState<S>; private version: number = 0; /** Stable identifier that correlates traces across agent turns. */ readonly sessionId: string; constructor(initialState: SessionState<S>) { super(); // Clone so we never alias (or mutate) the caller's object: the session // owns its state, and a handler mutating it must not reach back into the // caller's / chat's state. const state = structuredClone(initialState); this.sessionId = state.sessionId || globalThis.crypto.randomUUID(); state.sessionId = this.sessionId; this.state = state; } /** * Returns a deep copy of the current session state. */ getState(): SessionState<S> { return structuredClone(this.state); } /** * Retrieves all messages associated with the session. * * Returns a copy so callers cannot mutate the session's internal message * array in place. */ getMessages(): MessageData[] { return structuredClone(this.state.messages || []); } /** * Appends a list of messages to the session. */ addMessages(messages: MessageData[]) { this.state.messages = [...(this.state.messages || []), ...messages]; this.version++; } /** * Overwrites the session messages. */ setMessages(messages: MessageData[]) { this.state.messages = messages; this.version++; } /** * Retrieves the custom state of the session. */ getCustom(): S | undefined { return this.state.custom; } /** * Updates the custom state of the session using a mutator function. */ updateCustom(fn: (custom?: S) => S) { this.state.custom = fn(this.state.custom); this.version++; this.emit('customChanged'); } /** * Retrieves the list of artifacts generated during the session. */ getArtifacts(): Artifact[] { return this.state.artifacts || []; } /** * Adds artifacts to the session, deduplicating items by name. * Emits 'artifactAdded' for new artifacts and 'artifactUpdated' for replacements. */ addArtifacts(artifacts: Artifact[]) { const existing = this.state.artifacts || []; const added: Artifact[] = []; const updated: Artifact[] = []; for (const a of artifacts) { if (a.name) { const idx = existing.findIndex((e) => e.name === a.name); if (idx >= 0) { existing[idx] = a; updated.push(a); continue; } } existing.push(a); added.push(a); } this.state.artifacts = existing; if (added.length + updated.length > 0) { this.version++; } for (const a of added) { this.emit('artifactAdded', a); } for (const a of updated) { this.emit('artifactUpdated', a); } } /** * Runs the provided function inside the session's context. */ run<O>(fn: () => O) { return getAsyncContext().run('ai.session', this, fn); } /** * Gets the current mutation version of the session state. */ getVersion(): number { return this.version; } } /** * Utility to execute a function bound to a Session instance context. */ export function runWithSession<S = any, O = any>( registry: Registry, session: Session<S>, fn: () => O ): O { return getAsyncContext().run('ai.session', session, fn); } /** * Returns the Session instance active in the current context. */ export function getCurrentSession<S = any>( registry: Registry ): Session<S> | undefined { return getAsyncContext().getStore('ai.session'); } /** * Error thrown during session execution. */ export class SessionError extends Error { constructor(msg: string) { super(msg); } } /** * Validates that `sessionId` is a non-empty string, throwing a descriptive * error otherwise. * * Session ids can be minted by the client and can be any non-empty string * (e.g. a UUID, or an application-specific identifier). We only reject empty / * blank values so the id stays usable as a key (and as a directory name in * {@link FileSessionStore}). */ export function assertValidSessionId(sessionId: string): void { if (typeof sessionId !== 'string' || sessionId.trim() === '') { throw new Error( `Invalid sessionId: expected a non-empty string, got "${sessionId}".` ); } } /** * Mints a new `snapshotId` (a plain random UUID). * * The runtime normally supplies the snapshotId to the store at save time, but * some flows need the id *ahead of time* - e.g. an agent turn that wants to * know the snapshotId at turn *start* (to name a git branch / worktree after * it) and have the snapshot persisted at turn end reuse that very id, or the * detach path which pre-reserves the in-flight snapshot's id. */ export function reserveSnapshotId(): string { return globalThis.crypto.randomUUID(); }