npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@oracle/langgraph-checkpoint-oracledb

v0.1.0

Published

Oracle AI Database integration for LangGraph

Readme

@oracle/langgraph-checkpoint-oracledb

Oracle AI Database persistence for LangGraph.js. This package provides:

  • OracleCheckpointSaver, a BaseCheckpointSaver implementation for graph checkpoints.
  • OracleStore, a BaseStore implementation for long-term memory and optional Oracle VECTOR search.

Requirements

  • Node.js 18 or newer for runtime use.
  • Oracle Database connectivity through oracledb.
  • Oracle VECTOR support only when using OracleStore with an index configuration.
  • langgraph-oracledb 1.0.2 or newer when Python reads checkpoints written by this saver.

The integration tests use:

export ORACLE_USER="your_user"
export ORACLE_PASSWORD="your_password"
export ORACLE_CONNECT_STRING="host:port/service_name"

A .env in the package root works too. pnpm test:int refuses to start when those variables are missing, because every test would skip and the run would still pass. To skip deliberately, set ALLOW_SKIPPED_ORACLE_INT_TESTS=1.

Installation

pnpm add @oracle/langgraph-checkpoint-oracledb \
  @langchain/core @langchain/langgraph-checkpoint

Usage

Assume workflow is an already configured LangGraph workflow.

import { OracleCheckpointSaver } from "@oracle/langgraph-checkpoint-oracledb";

const checkpointer = new OracleCheckpointSaver({
  connection: {
    user: process.env.ORACLE_USER,
    password: process.env.ORACLE_PASSWORD,
    connectString: process.env.ORACLE_CONNECT_STRING,
  },
});

// Call setup() before first use.
await checkpointer.setup();

const graph = workflow.compile({ checkpointer });
await graph.invoke(
  { input: "remember this" },
  { configurable: { thread_id: "user-123" } }
);

The saver supports checkpoint listing, pending writes, custom serializers, child checkpoint namespaces, and deleteThread(threadId).

Checkpoint metadata filters are evaluated in Oracle before ordering and limiting results. Objects and arrays use recursive containment semantics, so { cfg: { a: 1 } } matches a stored { cfg: { a: 1, b: 2 } }. This matches the metadata @> ... behaviour of the Postgres savers in both languages. A filter key is always a literal key, so { "a.b": 1 } looks for a key named a.b rather than a nested a -> b path. null matches only an explicitly present JSON null. Filter values must be plain JSON; unsupported JavaScript-specific values fail before the query runs.

The checkpoint schema follows the public Python langgraph-oracledb saver.

Language-neutral JSON values, byte arrays, and default pending writes can be read in both directions. A custom serializer must use compatible type tags and bytes in both languages; language-specific values such as Python pickle or Python-specific MessagePack extensions are not portable.

What a custom serializer does and does not see

Every changed channel value passes through the serializer and is stored in the versioned checkpoint_blobs table. This lets a checkpoint reconstruct its complete state from channel_versions without duplicating unchanged values. metadata is stored as JSON at any size.

| Value | Passes through the serializer | | --------------------- | ----------------------------- | | Changed channel value | yes | | metadata | no, at any size | | Pending writes | yes |

This matters if the serializer is doing more than serialising. An encrypting serializer applies to every changed channel value, but not to metadata, which remains readable in the database, for instance through JSON_SERIALIZE(checkpoint). jsonSizeThresholdMb is retained for constructor compatibility but no longer controls checkpoint storage.

The Python langgraph-oracledb saver reads this format from release 1.0.2 onward, where blob versions are compared as text. Earlier releases return empty channel_values for checkpoints written here. Python still writes legacy inline snapshots; when this saver appends to a thread that Python wrote, it materialises every carried channel value into checkpoint_blobs, so both readers keep the full state.

Reading a thread the other language wrote needs nothing further. Continuing one thread from both languages does: LangGraph.js assigns numeric channel versions and its BaseCheckpointSaver.getNextVersion throws on the string versions Python writes, while LangGraph in Python cannot order its string versions against the numbers a JavaScript run leaves behind. Emit Python's zero-padded string format from the first checkpoint of such a thread:

class SharedVersionSaver extends OracleCheckpointSaver {
  // LangGraph.js types channel versions as numbers; emit Python's string format
  // so a thread can be continued from either language.
  getNextVersion(current: number | undefined): number {
    const previous = Number.parseInt(String(current ?? 0), 10);
    const random = Math.random().toString().slice(2, 18).padEnd(16, "0");
    const next = `${String(previous + 1).padStart(32, "0")}.${random}`;
    return next as unknown as number;
  }
}

Both components also accept Python's user/password@dsn connection string, so a from_conn_string snippet ports across unchanged:

import {
  OracleCheckpointSaver,
  OracleStore,
} from "@oracle/langgraph-checkpoint-oracledb";

const connString = "user/password@localhost:1521/FREEPDB1";

const checkpointer = OracleCheckpointSaver.fromConnString(connString);
const store = OracleStore.fromConnString(connString, {
  tableSuffix: "memory",
  poolConfig: { minSize: 1, maxSize: 10 },
});

Usage with an existing pool or connection

import oracledb from "oracledb";
import { OracleCheckpointSaver } from "@oracle/langgraph-checkpoint-oracledb";

const pool = await oracledb.createPool({
  user: process.env.ORACLE_USER,
  password: process.env.ORACLE_PASSWORD,
  connectString: process.env.ORACLE_CONNECT_STRING,
});

const checkpointer = new OracleCheckpointSaver({ pool });
await checkpointer.setup();

An existing pool or raw connection remains the caller's responsibility. Raw connection operations are serialized; use a pool for concurrent graph runs.

Store

Import OracleStore from the store subpath:

import { OracleStore } from "@oracle/langgraph-checkpoint-oracledb/store";

const store = new OracleStore({
  connection: {
    user: process.env.ORACLE_USER,
    password: process.env.ORACLE_PASSWORD,
    connectString: process.env.ORACLE_CONNECT_STRING,
  },
  // Uses STORE_MEMORY, shared with Python's table_suffix="memory".
  tableSuffix: "memory",
});

await store.put(["memories", "user-1"], "profile", {
  name: "Ada",
  score: 10,
});

const item = await store.get(["memories", "user-1"], "profile");
const results = await store.search(["memories"], {
  filter: { score: { $gte: 5 } },
  limit: 10,
});

Optional TTL values are expressed in minutes. A write may override the store default, and searches may request a refresh for the items they return:

const store = new OracleStore({
  connection: {
    user: process.env.ORACLE_USER,
    password: process.env.ORACLE_PASSWORD,
    connectString: process.env.ORACLE_CONNECT_STRING,
  },
  tableSuffix: "memory",
  ttl: {
    defaultTtl: 60,
    refreshOnRead: true,
    sweepIntervalMinutes: 15,
  },
});

await store.put(
  ["memories", "user-1"],
  "temporary",
  { text: "..." },
  undefined,
  { ttl: 5 }
);
await store.search(["memories"], { refreshTtl: true });
const deletedCount = await store.sweepExpiredItems();

Expired items are excluded from get, search, vector search, and namespace listing even when no sweeper is configured. sweepExpiredItems() deletes them; related vector rows are removed by the shared schema's foreign-key cascade.

OracleStore supports get, put, delete, search, batch, and listNamespaces. Tables are created automatically by default; call setup() once to create them up front, or let the first operation do it. Set ensureTable: false when the tables must already exist.

Non-vector search returns the most recently updated items first, and vector search returns only items that have an embedding, both as in Python. A query passed to a store with no index configuration is ignored rather than rejected, again matching Python, so the result is the plain filtered listing. Text embedded from a document matches Python's get_text_at_path exactly, so the same document produces the same vector in either language.

Supported scalar filter operators are $eq, $ne, $gt, $gte, $lt, $lte, $in, $nin, and $exists. Filters that cannot be translated to bound Oracle SQL are rejected instead of being evaluated by an unbounded client-side scan. Namespace listing supports prefix, suffix, wildcard labels, maxDepth, limit, and offset.

Store with Oracle VECTOR search

Pass an IndexConfig to enable vector indexing:

Assume myEmbeddings is a LangChain embeddings implementation compatible with IndexConfig.

const store = new OracleStore({
  connection: {
    user: process.env.ORACLE_USER,
    password: process.env.ORACLE_PASSWORD,
    connectString: process.env.ORACLE_CONNECT_STRING,
  },
  index: {
    dims: 1536,
    embeddings: myEmbeddings,
    fields: ["text", "metadata.summary"],
  },
});

await store.put(["memories", "user-1"], "note", {
  text: "Ada likes database systems and agent memory.",
});

const results = await store.search(["memories", "user-1"], {
  query: "database memory",
  limit: 5,
});

Vector index type, distance metric, and accuracy

index.index_type and index.accuracy mirror the Python langgraph-oracledb options, down to the property names, because the same values are hashed into the table suffix and stored in the shared STORE_CONFIGS table:

const store = new OracleStore({
  connection: oracleConnection,
  index: {
    dims: 1536,
    embeddings: myEmbeddings,
    fields: ["text"],
    accuracy: 95,
    index_type: {
      type: "hnsw", // or "ivf"
      neighbors: 16,
      efconstruction: 200,
      distance_metric: "COSINE", // or "EUCLIDEAN" / "DOT"
    },
  },
});

| Option | Applies to | Range | | --------------------------- | ---------- | ---------------------------- | | accuracy | both | 1-100 | | distance_metric | both | COSINE, EUCLIDEAN, DOT | | neighbors | hnsw | 2-2048 | | efconstruction | hnsw | 1-65535 | | neighbor_partitions | ivf | 1-10000000 | | samples_per_partition | ivf | 1 or more | | min_vectors_per_partition | ivf | 0 or more |

Every value is validated when the store is constructed, before any statement is built. Unknown index_type keys are rejected rather than ignored.

setup() creates the matching VECTOR index and records it as vector migration 1, the same as Python does, so a store is queryable through its index straight after the first use. An HNSW index requires a database with a vector memory area (VECTOR_MEMORY_SIZE); IVF does not.

Because the index configuration is part of the derived table suffix, two stores with different index settings get their own isolated tables. When a tableSuffix is given explicitly, setup() compares the configuration with the one registered in STORE_CONFIGS and fails on a dimension, distance, accuracy, or index parameter mismatch instead of returning wrongly ranked rows.

The index is described entirely by the configuration; there is no separate index-creation call, matching Python. Two build hints have no Python equivalent and are therefore kept out of the table suffix and STORE_CONFIGS, so they never change which tables a store resolves to:

const store = new OracleStore({
  connection: oracleConnection,
  index: {
    dims: 1536,
    embeddings: myEmbeddings,
    index_name: "LG_MEMORY_IVF_IDX", // default: derived from the configuration
    parallel: 4, // degree of parallelism for the index build
    index_type: { type: "ivf", neighbor_partitions: 1 },
  },
});

Existing indexes are reported by getDiagnostics() under vector.observedIndexes.

Tables and cleanup

Both components accept a tableSuffix. Without one the checkpoint saver uses the bare names Python creates, so the two languages share tables by default:

CHECKPOINTS             CHECKPOINTS_<SUFFIX>
CHECKPOINT_BLOBS        CHECKPOINT_BLOBS_<SUFFIX>
CHECKPOINT_WRITES       CHECKPOINT_WRITES_<SUFFIX>
CHECKPOINT_MIGRATIONS   CHECKPOINT_MIGRATIONS_<SUFFIX>

A suffix isolates independent checkpoint sets within one Oracle schema, which is what PostgresSaver uses its schema option for. Python's checkpointer has no equivalent yet, so a suffixed set is reachable only from JavaScript for now.

Suffixes follow Python's table_suffix rule: they must start with a letter and contain only letters, digits, or underscores, up to 64 characters. Names are emitted as unquoted Oracle identifiers, so they are matched case-insensitively (memory and MEMORY address ..._MEMORY), and anything that would require double quoting is rejected rather than quoted.

The Store accepts tableSuffix and uses the same schema, names, namespace/key encoding, and migration history as langgraph-oracledb for Python:

STORE_<SUFFIX>
STORE_VECTORS_<SUFFIX>
STORE_MIGRATIONS_<SUFFIX>
VECTOR_MIGRATIONS_<SUFFIX>

Without an index configuration the default suffix is novec. With an index configuration it is derived deterministically from dimensions, fields, and index parameters, as in Python. Specify the same suffix explicitly when applications in both languages must share one Store. Empty keys are rejected because Oracle treats an empty string as NULL and the shared key column is NOT NULL.

Remove checkpoint data for one thread with:

await checkpointer.deleteThread("thread-id");

Delete store items individually:

await store.delete(["memories", "user-1"], "profile");

Close resources created by the components when finished:

await store.stop();
await checkpointer.end();