UNPKG

@genkit-ai/ai

Version:

Genkit AI framework generative AI APIs.

1 lines 12.5 kB
{"version":3,"sources":["../src/session.ts"],"sourcesContent":["/**\n * Copyright 2026 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { getAsyncContext, type ActionContext } from '@genkit-ai/core';\n\nimport { EventEmitter } from '@genkit-ai/core/async';\nimport type { Registry } from '@genkit-ai/core/registry';\nimport {\n type Artifact,\n type SessionSnapshot,\n type SessionState,\n} from './agent-types.mjs';\nimport { MessageData } from './model-types.mjs';\n\n// Re-export the shared agent/session wire schemas + types from their canonical\n// home (./agent-types.ts) so existing imports from './session.mjs' keep working.\nexport {\n AgentFinishReasonSchema,\n ArtifactSchema,\n SessionSnapshotSchema,\n SessionStateSchema,\n type AgentFinishReason,\n type Artifact,\n type SessionSnapshot,\n type SessionState,\n} from './agent-types.mjs';\n\n/**\n * Input type for {@link SessionStore.saveSnapshot}.\n *\n * Identical to {@link SessionSnapshot} except that `snapshotId` is optional.\n * When omitted the store is responsible for assigning a new identifier\n * (enabling stores to encode grouping or routing information in the ID).\n * When provided the store performs an upsert - updating the existing snapshot.\n */\nexport type SessionSnapshotInput<S = unknown> = Omit<\n SessionSnapshot<S>,\n 'snapshotId'\n> & {\n snapshotId?: string;\n};\n\n/**\n * Options provided to the session store methods.\n */\nexport interface SessionStoreOptions {\n context?: ActionContext;\n}\n\n/**\n * Lookup options for {@link SessionStore.getSnapshot}.\n *\n * Exactly one of `snapshotId` or `sessionId` must be provided:\n *\n * - `snapshotId` loads that specific snapshot.\n * - `sessionId` loads the *latest* (leaf) snapshot of the session - the most\n * recent snapshot that no other snapshot points to as its parent. This is\n * the common case for simple session storage (e.g. `useChat`) where the\n * client only tracks a stable session id and lets the server remember the\n * conversation. A session with *branching* snapshots (more than one leaf,\n * e.g. after a regenerate) has no single \"latest\"; by default the store\n * returns the most-recent leaf, but it can be configured to reject the\n * lookup with `FAILED_PRECONDITION` - in which case, resume by `snapshotId`.\n */\nexport interface GetSnapshotOptions {\n snapshotId?: string;\n sessionId?: string;\n context?: ActionContext;\n}\n\n/**\n * A function that receives the current snapshot and returns the updated\n * snapshot to persist.\n *\n * - Return the mutated snapshot to save it.\n * - Return `null` to silently skip the update (no-op).\n * - Throw to abort with an error (e.g. precondition failure).\n */\nexport type SnapshotMutator<S = unknown> = (\n current: SessionSnapshot<S> | undefined\n) => SessionSnapshotInput<S> | null;\n\n/**\n * Interface for persistent session snapshot storage.\n */\nexport interface SessionStore<S = unknown> {\n /**\n * Loads a snapshot either by its `snapshotId` or by `sessionId`.\n *\n * See {@link GetSnapshotOptions} for the lookup semantics (exactly one of\n * `snapshotId` / `sessionId`; `sessionId` resolves to the session's latest\n * leaf snapshot, optionally rejecting branching histories).\n */\n getSnapshot(\n opts: GetSnapshotOptions\n ): Promise<SessionSnapshot<S> | undefined>;\n\n /**\n * Atomically reads the current snapshot (if `snapshotId` is provided),\n * passes it to `mutator`, and persists the result.\n *\n * - When `snapshotId` is provided the store reads the existing snapshot\n * and passes it to the mutator. The mutator can inspect the current\n * state (e.g. to check for concurrent status changes) and return the\n * updated snapshot to save, or `null` to skip the write.\n * - When `snapshotId` is `undefined` the store passes `undefined` to\n * the mutator (signaling a new snapshot). The store assigns a new\n * identifier.\n *\n * Implementations should ensure the read→mutate→write cycle is atomic\n * to prevent race conditions (e.g. a \"done\" write overwriting a\n * concurrent \"aborted\" status).\n *\n * The mutator can:\n *\n * - Return a snapshot to save it.\n * - Return `null` to silently skip the write.\n * - Throw to abort with an error.\n *\n * @returns The `snapshotId` that was used, or `null` when the mutator\n * returned `null`.\n */\n saveSnapshot(\n snapshotId: string | undefined,\n mutator: SnapshotMutator<S>,\n options?: SessionStoreOptions\n ): Promise<string | null>;\n\n onSnapshotStateChange?(\n snapshotId: string,\n callback: (snapshot: SessionSnapshot<S>) => void,\n options?: SessionStoreOptions\n ): void | (() => void);\n}\n\n/**\n * State manager for a session turn, tracking messages, custom state, and artifacts.\n */\nexport class Session<S = unknown> extends EventEmitter {\n private state: SessionState<S>;\n private version: number = 0;\n\n /** Stable identifier that correlates traces across agent turns. */\n readonly sessionId: string;\n\n constructor(initialState: SessionState<S>) {\n super();\n // Clone so we never alias (or mutate) the caller's object: the session\n // owns its state, and a handler mutating it must not reach back into the\n // caller's / chat's state.\n const state = structuredClone(initialState);\n this.sessionId = state.sessionId || globalThis.crypto.randomUUID();\n state.sessionId = this.sessionId;\n this.state = state;\n }\n\n /**\n * Returns a deep copy of the current session state.\n */\n getState(): SessionState<S> {\n return structuredClone(this.state);\n }\n\n /**\n * Retrieves all messages associated with the session.\n *\n * Returns a copy so callers cannot mutate the session's internal message\n * array in place.\n */\n getMessages(): MessageData[] {\n return structuredClone(this.state.messages || []);\n }\n\n /**\n * Appends a list of messages to the session.\n */\n addMessages(messages: MessageData[]) {\n this.state.messages = [...(this.state.messages || []), ...messages];\n this.version++;\n }\n\n /**\n * Overwrites the session messages.\n */\n setMessages(messages: MessageData[]) {\n this.state.messages = messages;\n this.version++;\n }\n\n /**\n * Retrieves the custom state of the session.\n */\n getCustom(): S | undefined {\n return this.state.custom;\n }\n\n /**\n * Updates the custom state of the session using a mutator function.\n */\n updateCustom(fn: (custom?: S) => S) {\n this.state.custom = fn(this.state.custom);\n this.version++;\n this.emit('customChanged');\n }\n\n /**\n * Retrieves the list of artifacts generated during the session.\n */\n getArtifacts(): Artifact[] {\n return this.state.artifacts || [];\n }\n\n /**\n * Adds artifacts to the session, deduplicating items by name.\n * Emits 'artifactAdded' for new artifacts and 'artifactUpdated' for replacements.\n */\n addArtifacts(artifacts: Artifact[]) {\n const existing = this.state.artifacts || [];\n const added: Artifact[] = [];\n const updated: Artifact[] = [];\n\n for (const a of artifacts) {\n if (a.name) {\n const idx = existing.findIndex((e) => e.name === a.name);\n if (idx >= 0) {\n existing[idx] = a;\n updated.push(a);\n continue;\n }\n }\n existing.push(a);\n added.push(a);\n }\n\n this.state.artifacts = existing;\n if (added.length + updated.length > 0) {\n this.version++;\n }\n for (const a of added) {\n this.emit('artifactAdded', a);\n }\n for (const a of updated) {\n this.emit('artifactUpdated', a);\n }\n }\n\n /**\n * Runs the provided function inside the session's context.\n */\n run<O>(fn: () => O) {\n return getAsyncContext().run('ai.session', this, fn);\n }\n\n /**\n * Gets the current mutation version of the session state.\n */\n getVersion(): number {\n return this.version;\n }\n}\n\n/**\n * Utility to execute a function bound to a Session instance context.\n */\n\nexport function runWithSession<S = any, O = any>(\n registry: Registry,\n session: Session<S>,\n fn: () => O\n): O {\n return getAsyncContext().run('ai.session', session, fn);\n}\n\n/**\n * Returns the Session instance active in the current context.\n */\nexport function getCurrentSession<S = any>(\n registry: Registry\n): Session<S> | undefined {\n return getAsyncContext().getStore('ai.session');\n}\n\n/**\n * Error thrown during session execution.\n */\nexport class SessionError extends Error {\n constructor(msg: string) {\n super(msg);\n }\n}\n\n/**\n * Validates that `sessionId` is a non-empty string, throwing a descriptive\n * error otherwise.\n *\n * Session ids can be minted by the client and can be any non-empty string\n * (e.g. a UUID, or an application-specific identifier). We only reject empty /\n * blank values so the id stays usable as a key (and as a directory name in\n * {@link FileSessionStore}).\n */\nexport function assertValidSessionId(sessionId: string): void {\n if (typeof sessionId !== 'string' || sessionId.trim() === '') {\n throw new Error(\n `Invalid sessionId: expected a non-empty string, got \"${sessionId}\".`\n );\n }\n}\n\n/**\n * Mints a new `snapshotId` (a plain random UUID).\n *\n * The runtime normally supplies the snapshotId to the store at save time, but\n * some flows need the id *ahead of time* - e.g. an agent turn that wants to\n * know the snapshotId at turn *start* (to name a git branch / worktree after\n * it) and have the snapshot persisted at turn end reuse that very id, or the\n * detach path which pre-reserves the in-flight snapshot's id.\n */\nexport function reserveSnapshotId(): string {\n return globalThis.crypto.randomUUID();\n}\n"],"mappings":"AAgBA,SAAS,uBAA2C;AAEpD,SAAS,oBAAoB;AAW7B;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAKK;AAiHA,MAAM,gBAA6B,aAAa;AAAA,EAC7C;AAAA,EACA,UAAkB;AAAA;AAAA,EAGjB;AAAA,EAET,YAAY,cAA+B;AACzC,UAAM;AAIN,UAAM,QAAQ,gBAAgB,YAAY;AAC1C,SAAK,YAAY,MAAM,aAAa,WAAW,OAAO,WAAW;AACjE,UAAM,YAAY,KAAK;AACvB,SAAK,QAAQ;AAAA,EACf;AAAA;AAAA;AAAA;AAAA,EAKA,WAA4B;AAC1B,WAAO,gBAAgB,KAAK,KAAK;AAAA,EACnC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,cAA6B;AAC3B,WAAO,gBAAgB,KAAK,MAAM,YAAY,CAAC,CAAC;AAAA,EAClD;AAAA;AAAA;AAAA;AAAA,EAKA,YAAY,UAAyB;AACnC,SAAK,MAAM,WAAW,CAAC,GAAI,KAAK,MAAM,YAAY,CAAC,GAAI,GAAG,QAAQ;AAClE,SAAK;AAAA,EACP;AAAA;AAAA;AAAA;AAAA,EAKA,YAAY,UAAyB;AACnC,SAAK,MAAM,WAAW;AACtB,SAAK;AAAA,EACP;AAAA;AAAA;AAAA;AAAA,EAKA,YAA2B;AACzB,WAAO,KAAK,MAAM;AAAA,EACpB;AAAA;AAAA;AAAA;AAAA,EAKA,aAAa,IAAuB;AAClC,SAAK,MAAM,SAAS,GAAG,KAAK,MAAM,MAAM;AACxC,SAAK;AACL,SAAK,KAAK,eAAe;AAAA,EAC3B;AAAA;AAAA;AAAA;AAAA,EAKA,eAA2B;AACzB,WAAO,KAAK,MAAM,aAAa,CAAC;AAAA,EAClC;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,aAAa,WAAuB;AAClC,UAAM,WAAW,KAAK,MAAM,aAAa,CAAC;AAC1C,UAAM,QAAoB,CAAC;AAC3B,UAAM,UAAsB,CAAC;AAE7B,eAAW,KAAK,WAAW;AACzB,UAAI,EAAE,MAAM;AACV,cAAM,MAAM,SAAS,UAAU,CAAC,MAAM,EAAE,SAAS,EAAE,IAAI;AACvD,YAAI,OAAO,GAAG;AACZ,mBAAS,GAAG,IAAI;AAChB,kBAAQ,KAAK,CAAC;AACd;AAAA,QACF;AAAA,MACF;AACA,eAAS,KAAK,CAAC;AACf,YAAM,KAAK,CAAC;AAAA,IACd;AAEA,SAAK,MAAM,YAAY;AACvB,QAAI,MAAM,SAAS,QAAQ,SAAS,GAAG;AACrC,WAAK;AAAA,IACP;AACA,eAAW,KAAK,OAAO;AACrB,WAAK,KAAK,iBAAiB,CAAC;AAAA,IAC9B;AACA,eAAW,KAAK,SAAS;AACvB,WAAK,KAAK,mBAAmB,CAAC;AAAA,IAChC;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,IAAO,IAAa;AAClB,WAAO,gBAAgB,EAAE,IAAI,cAAc,MAAM,EAAE;AAAA,EACrD;AAAA;AAAA;AAAA;AAAA,EAKA,aAAqB;AACnB,WAAO,KAAK;AAAA,EACd;AACF;AAMO,SAAS,eACd,UACA,SACA,IACG;AACH,SAAO,gBAAgB,EAAE,IAAI,cAAc,SAAS,EAAE;AACxD;AAKO,SAAS,kBACd,UACwB;AACxB,SAAO,gBAAgB,EAAE,SAAS,YAAY;AAChD;AAKO,MAAM,qBAAqB,MAAM;AAAA,EACtC,YAAY,KAAa;AACvB,UAAM,GAAG;AAAA,EACX;AACF;AAWO,SAAS,qBAAqB,WAAyB;AAC5D,MAAI,OAAO,cAAc,YAAY,UAAU,KAAK,MAAM,IAAI;AAC5D,UAAM,IAAI;AAAA,MACR,wDAAwD,SAAS;AAAA,IACnE;AAAA,EACF;AACF;AAWO,SAAS,oBAA4B;AAC1C,SAAO,WAAW,OAAO,WAAW;AACtC;","names":[]}