{"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":"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","names":[]}