> ## Documentation Index
> Fetch the complete documentation index at: https://engramviz.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# LangGraph Store

> Capture LangGraph memory evidence and replay incidents from isolated graph checkpoints.

`@engramviz/adapter-langgraph` instruments LangGraph's cross-thread `Store`
interface. It captures durable memory writes, searches, reads, and deletes while
preserving the distinction between retrieval and model context.

## Install

```bash theme={"dark"}
npm install @engramviz/sdk @engramviz/adapter-langgraph
```

The adapter supports `@langchain/langgraph` 1.x and wraps any compatible Store
implementation, including `InMemoryStore` and persistent Store backends.

## Wrap the Store

Wrap the Store before compiling the graph, then run the graph inside an Engram
turn:

```ts theme={"dark"}
import { InMemoryStore } from "@langchain/langgraph";
import {
  instrumentLangGraphStore,
  langGraphMemoryIds
} from "@engramviz/adapter-langgraph";
import { EngramClient, getActiveEngramTurn } from "@engramviz/sdk";

const engram = new EngramClient({ adapter: "langgraph" });
const store = instrumentLangGraphStore(new InMemoryStore(), engram);

const graph = workflow.compile({ store });

const answer = await engram.withTurn(
  {
    input: "Where do I live?",
    provider: { id: "langgraph", model: "my-agent" }
  },
  async () => {
    return graph.invoke({ input: "Where do I live?" });
  }
);
```

Inside a graph node, LangGraph exposes the Store as `runtime.store`. Store
searches are captured automatically. Report context loading only after the
application actually copies results into the model input:

```ts theme={"dark"}
const memories = await runtime.store.search(
  ["users", userId, "memories"],
  { query: state.input, limit: 5 }
);

const loaded = selectForPrompt(memories);
await getActiveEngramTurn()?.load(langGraphMemoryIds(loaded));
```

<Warning>
  `search` proves that LangGraph returned candidates. It does not prove that the
  application placed them in a prompt. Engram never converts a search result into
  active context automatically.
</Warning>

## Captured operations

| LangGraph Store call | Engram operation                     | Evidence                                           |
| -------------------- | ------------------------------------ | -------------------------------------------------- |
| `put`                | `store` by default                   | Namespace, key, value, and upsert semantics        |
| `search`             | `retrieve`                           | Ranked candidates, scores, selected IDs, and limit |
| `get`                | `retrieve`                           | Direct keyed lookup                                |
| `delete`             | `delete`                             | Namespace-qualified memory ID                      |
| `batch`              | Operation for each direct batch item | Mapped from the batch request and result           |

LangGraph `put` is an upsert, so the adapter cannot know whether a key existed
without adding another Store read. Use `classifyPut` when the application knows
that a write is an update:

```ts theme={"dark"}
const store = instrumentLangGraphStore(rawStore, engram, {
  classifyPut: ({ key }) => knownKeys.has(key) ? "update" : "store"
});
```

Memory IDs include the full namespace and key. For example,
`["users", "user-1", "memories"]` plus `"city"` becomes
`langgraph:users/user-1/memories/city`. This prevents identical keys in
different namespaces from collapsing into one Engram memory.

## Checkpoints are different

LangGraph checkpointers persist graph state within a thread and enable resume,
history, and replay. LangGraph Store persists arbitrary information across
threads. Engram's adapter observes the latter as durable memory.

It intentionally does not turn every checkpoint value into a memory event.
Doing so would make transient execution state look like a durable user fact.
Instrument an explicit application boundary separately if checkpoint state is
part of the memory behavior being investigated.

## Capture a replay boundary

For an incident to rerun the actual graph, capture state at an explicit node
boundary while an Engram turn is active:

```ts theme={"dark"}
import {
  captureLangGraphReplayCheckpoint
} from "@engramviz/adapter-langgraph";

const config = { configurable: { thread_id: turnId } };
await engram.withTurn({ input: question }, async () => {
  const seeded = await graph.updateState(config, initialState, "entry");
  await captureLangGraphReplayCheckpoint(graph, seeded, { asNode: "entry" });
  return graph.invoke(null, seeded);
});
```

The helper automatically attaches the checkpoint to the active Engram turn.
Pass `{ attachToActiveTurn: false }` only when storing the returned checkpoint
yourself, or pass an explicit `turn` for applications that cannot use async
context. `asNode` is required because replay must resume from a known graph
boundary. Engram stores JSON-compatible state values, not the checkpointer
implementation or arbitrary closures.

## Define the real replay executor

Export a provider-neutral executor from a local module:

```ts theme={"dark"}
import { defineLangGraphExecutor } from "@engramviz/adapter-langgraph";

export default defineLangGraphExecutor({
  id: "support-agent",
  version: "1.0.0",
  supportedSideEffectModes: ["blocked"],

  async createRuntime({ variant, sideEffectMode }) {
    const store = await cloneReplayStore();
    return {
      graph: buildGraph({ store, tools: replaySafeTools }),
      config: { configurable: { thread_id: `replay-${variant}` } },
      isolation: {
        checkpoint: "isolated",
        memoryStore: "isolated",
        sideEffects: sideEffectMode
      }
    };
  },

  applyIntervention({ checkpoint, intervention }) {
    return applyMemoryPolicy(checkpoint.values, intervention);
  },

  observe({ finalState, source, variant }) {
    return mapGraphStateToDecisionRun(finalState, source, variant);
  }
});
```

Scaffold the module and shared project configuration once:

```bash theme={"dark"}
npx --yes @engramviz/cli init --project support-agent --framework langgraph
npx --yes @engramviz/cli dev
```

Studio and `engram test` both discover `engram.executor.mjs` through
`engram.config.json`. This prevents a passing CI test from silently using a
different replay implementation than the engineer used during diagnosis.

Engram runs an untreated baseline first and rejects a causal comparison when it
cannot reproduce the captured answer. It then applies the intervention only to
the treatment fork, reruns the graph, and compares memory state, retrieval,
selection, active context, and answer.

<Warning>
  The executor checks the isolation declarations, but your application must make
  them true. Clone or reconstruct checkpoint and Store state for each variant.
  Block, record, or safely sandbox tool and network effects. Never replay against
  a mutable production Store.
</Warning>

<CardGroup cols={2}>
  <Card title="Run the LangGraph example" icon="play" href="/examples/langgraph-memory">
    Exercise a real StateGraph and InMemoryStore with deterministic capture.
  </Card>

  <Card title="Run the support-agent quickstart" icon="headset" href="/examples/langgraph-support-agent">
    Exercise the model-backed production-shaped workflow and shared executor.
  </Card>

  <Card title="Understand the evidence model" icon="badge-check" href="/concepts/evidence-model">
    See what observed, mapped, derived, and unavailable evidence mean.
  </Card>
</CardGroup>

## LangGraph references

* [Memory overview](https://docs.langchain.com/oss/javascript/langgraph/add-memory)
* [Persistence and Store](https://docs.langchain.com/oss/javascript/langgraph/persistence)
* [InMemoryStore API](https://langchain-ai.github.io/langgraphjs/reference/classes/langgraph.InMemoryStore.html)
