Skip to main content

Framework adapters

mindD is framework-agnostic on purpose: the server knows nothing about LangGraph, CrewAI or the Vercel AI SDK. The adapters below live in the SDKs and translate each framework's persistence interface onto blocks that already exist, so you get durable, multi-tenant, policy-governed memory without bespoke glue.

Each adapter is optional and pulls in its framework only when you install it.

AdapterFrameworkSDKBlocks used
MindDSaverLangGraph checkpointerPythonkv
MindDStorageCrewAI memory storagePythonsemantic + kv
createChatStoreVercel AI SDK chat persistenceTypeScriptkv

LangGraph checkpointer

mindd.ext.langgraph.MindDSaver implements LangGraph's BaseCheckpointSaver on top of the kv block, so a graph gets durable thread state by pointing at a running sidecar.

pip install 'mindd[langgraph]'
from mindd import MindD
from mindd.ext.langgraph import MindDSaver

client = MindD("127.0.0.1:7777", token=TOKEN)
saver = MindDSaver(client, namespace="checkpoints")

graph = builder.compile(checkpointer=saver)
graph.invoke({"messages": [...]}, {"configurable": {"thread_id": "t1"}})
ArgumentDefaultMeaning
clientrequireda MindD instance
namespace"checkpoints"the kv namespace holding all state
serdeLangGraph's defaultserializer protocol

The storage model mirrors LangGraph's reference InMemorySaver, so runtime behaviour is identical. Everything lives under one kv namespace, keyed by role:

  • cp/<thread>/<ns>/<checkpoint_id>: the checkpoint (channel values popped out) plus its metadata and parent id, as one JSON envelope.
  • bl/<thread>/<ns>/<channel>/<version>: one channel value per version. Checkpoints reference these by channel_versions, so unchanged channels are shared across checkpoints.
  • wr/<thread>/<ns>/<checkpoint_id>/<task_id>/<idx>: a pending write.

Per-thread queries (get-latest, list) are served by a kv prefix scan. The episodic block is deliberately not used: its cursor stream is per namespace with no server-side per-thread filter, which is the wrong access pattern for a checkpointer.

Config. Declare the namespace and mint a token that covers it:

namespaces:
- { block: kv, name: checkpoints, backend: pg-main }
mindctl token issue --tenant acme --agent graph-1 \
--ns 'kv/checkpoints' --ops 'kv.get,kv.put,kv.delete,kv.scan' --ttl 1h

Turn on tenant isolation if different tenants share the deployment, and consider encrypt: true on the namespace since checkpoints carry conversation state. See Encryption at rest.

CrewAI memory storage

mindd.ext.crewai.MindDStorage implements CrewAI 1.x's StorageBackend protocol, so a crew's memories live on a running mindD instead of a local vector file.

pip install 'mindd[crewai]'
from crewai import Crew
from crewai.memory.unified_memory import Memory
from mindd import MindD
from mindd.ext.crewai import MindDStorage

client = MindD("127.0.0.1:7777", token=TOKEN)
memory = Memory(storage=MindDStorage(client, namespace="memories"))
crew = Crew(agents=[...], tasks=[...], memory=memory)

Or register it process-wide:

from crewai.memory.storage.factory import set_memory_storage_factory
set_memory_storage_factory(lambda spec: MindDStorage(client))
ArgumentDefaultMeaning
clientrequireda MindD instance
namespace"memories"name used for both the semantic and the kv namespace
semantic_namespacefalls back to namespaceoverride the vector namespace
kv_namespacefalls back to namespaceoverride the record namespace
over_fetch3candidate multiplier before scope filtering

Two blocks are used under one shared namespace name:

  • semantic, the vector index. save upserts (id, content, vector); search runs an ids-only ANN query, then records are rehydrated from kv.
  • kv, the record store and scope index. Each record is written under id/<id> (direct lookup) and rec<scope>/<id> (prefix scan), which backs get_record, list/count, and the scope and category introspection the memory runtime calls during encode and recall.
CrewAI owns the embedding

CrewAI populates MemoryRecord.embedding before save and hands search a query_embedding. The adapter stores those vectors as-is and searches by vector, so the mindD semantic namespace's dimensions must equal the CrewAI embedder's output dimension. The server-side embedder is never invoked on the write path, but it still has to be declared, because every semantic namespace requires an embedder block.

Config.

namespaces:
- { block: kv, name: memories, backend: pg-main }
- block: semantic
name: memories
backend: pg-main
embedder:
provider: fake # never called; dimensions must match CrewAI's
model: unused
dimensions: 1536
mindctl token issue --tenant acme --agent crew-1 \
--ns 'kv/memories,semantic/memories' \
--ops 'kv.get,kv.put,kv.delete,kv.scan,semantic.upsert,semantic.search,semantic.delete' \
--ttl 1h

kv/memories and semantic/memories are distinct namespaces: the capability match runs against the qualified <block>/<name> form.

Vercel AI SDK chat persistence

@mindd/client/ai implements the AI SDK's persistence convention (a chat is its full UIMessage[], loaded by id and re-saved after each turn) on the kv block, one key per chat.

npm install @mindd/client ai
import { MindD } from "@mindd/client";
import { createChatStore } from "@mindd/client/ai";

const m = new MindD("127.0.0.1:7777", { token: process.env.MINDD_TOKEN! });
const store = createChatStore(m, { namespace: "chats" });

const chatId = await store.createChat();
const messages = await store.loadChat(chatId);
import { streamText, convertToModelMessages } from "ai";

const result = streamText({ model, messages: convertToModelMessages(messages) });
return result.toUIMessageStreamResponse({
originalMessages: messages,
onFinish: ({ messages }) => store.saveChat({ chatId, messages }),
});
OptionDefaultMeaning
namespace"chats"the kv namespace holding the chats
generateIdcrypto.randomUUID()new-chat id generator

The store also exposes deleteChat(id) and listChats(). ai is an optional peer dependency, so importing the base client never pulls it in.

Config.

namespaces:
- { block: kv, name: chats, backend: pg-main }
mindctl token issue --tenant acme --agent web \
--ns 'kv/chats' --ops 'kv.get,kv.put,kv.delete,kv.scan' --ttl 1h

Why the adapters are thin

Everything an adapter needs already exists at the block level: TTLs, keyset pagination, optimistic concurrency, bitemporal semantic records. What the adapters add is a mapping from one framework's interface to those primitives. That means the policy engine, capability scoping, tenant isolation, encryption at rest and observability all apply unchanged, which is the point of putting memory behind a sidecar in the first place.

Adding an adapter for another framework is a single file against a public SDK.