LangGraph integration — CLS++ as a long-term memory BaseStore
clsplusplus.integrations.langgraph.CLSMemoryStore is a native subclass of
langgraph.store.base.BaseStore
— the long-term-memory store you pass as store= when you compile a LangGraph
graph. It routes every store operation through the CLS++ brain, so your agent's
long-term memory is persistent, shared across processes/machines, and reusable
across AI vendors that point at the same CLS++ namespace.
Implemented against LangGraph 1.x (BaseStore, Op, Item, SearchItem
re-exported from langgraph-checkpoint). CLSMemoryStore implements the two
abstract methods BaseStore requires — batch(ops) and abatch(ops) — so the
inherited get / put / search / delete / list_namespaces (and their
async variants) all work and funnel through CLS++.
Install
pip install clsplusplus[langgraph]
langgraph is an optional extra. The base package imports fine without it;
only this integration requires langgraph installed.
Environment
Get an API key from https://www.clsplusplus.com/profile#api-keys, then:
export CLS_API_KEY="cls_live_..."
# optional — defaults to https://www.clsplusplus.com
export CLS_BASE_URL="https://www.clsplusplus.com"
CLSMemoryStore(user=..., api_key=..., url=...) also accepts these directly;
unset values fall back to the env vars above.
Usage
Pass the store to compile(...); LangGraph injects it into any node that
declares a store parameter.
from langgraph.graph import StateGraph, START, MessagesState
from langgraph.store.base import BaseStore
from clsplusplus.integrations.langgraph import CLSMemoryStore
# One CLS++ "brain" namespace for this agent/tenant.
store = CLSMemoryStore(user="my-agent")
def remember(state: MessagesState, *, store: BaseStore):
user_id = state.get("user_id", "anon")
last = state["messages"][-1].content
# Write a long-term memory. The value is a dict; it is JSON-serialized
# into CLS++ and the (namespace, key) is preserved in metadata.
store.put(("memories", user_id), key="latest", value={"text": last})
return state
def recall(state: MessagesState, *, store: BaseStore):
user_id = state.get("user_id", "anon")
# Semantic recall — this is CLS++'s primary, best-supported operation.
hits = store.search(("memories", user_id), query=state["messages"][-1].content, limit=5)
context = "\n".join(item.value.get("text", "") for item in hits)
# ... inject `context` into your prompt ...
return state
builder = StateGraph(MessagesState)
builder.add_node("recall", recall)
builder.add_node("remember", remember)
builder.add_edge(START, "recall")
builder.add_edge("recall", "remember")
# IMPORTANT: pass the store at compile time.
graph = builder.compile(store=store)
Async graphs work too — CLSMemoryStore implements abatch, so astore.aput
/ asearch / aget are available (they run the sync CLS++ client in a worker
thread).
Operation mapping & limitations
| LangGraph op | CLS++ behavior |
|---|---|
put(ns, key, value) |
Brain.learn(json(value), source="langgraph", langgraph_namespace=ns, langgraph_key=key) |
put(ns, key, None) (delete) |
Brain.forget(key) — best-effort semantic delete |
search(prefix, query=..., limit=...) |
Brain.ask(query, limit) → list[SearchItem] with rank-derived descending score |
get(ns, key) |
best-effort: semantic recall seeded by key, returns the top hit |
delete(ns, key) |
Brain.forget(key) — best-effort semantic delete |
list_namespaces(...) |
returns [] |
CLS++ is a semantic memory (vector recall), not an exact key-value table.
That makes search the natural, fully-supported access path. The following are
intentional, documented limitations of mapping a KV interface onto semantic
recall:
getis best-effort. It returns the memory most semantically similar to the key, which is usually — but not guaranteed to be — the exact item written under that key. For exact KV semantics use a KV-backed store (e.g.PostgresStore); useCLSMemoryStorewheresearchis the main path.deleteis best-effort.Brain.forget(key)removes the closest semantic match to the key, not a guaranteed exact(namespace, key)row.list_namespacesreturns[]. The CLS++ SDK does not expose an enumerable index of written(namespace, key)tuples. Namespace identity is still preserved per item in metadata for relate-back; it is just not enumerable.- TTL is server-managed.
supports_ttlisFalse; per-item TTL passed toputis ignored — manage retention via CLS++.
The same BaseStore adapter pattern extends to other frameworks (LlamaIndex,
AutoGen) that accept a pluggable memory backend.