Whetstone.
MemoryAccessing storage
Module 4, Lesson 216 min

Accessing storage

The store is where long-term memory lives, and the way you reach it explains something you will meet again in the auth module, so this lesson is doing double duty.

Namespaced key-value, and why the structure is the point

The store is namespaced key-value memory. A namespace locates a region; a key locates an item inside that region.

That sounds like a filing convention and it is actually the security boundary. Because the store’s own organising structure is the namespace, that is what scoping acts on. When you get to authorization you will find that the store is the one resource scoped by rewriting the namespace rather than by returning a metadata filter, and the reason is right here: there is no metadata to filter on, because a key-value item is not a row with columns. The namespace is the only handle, so the namespace is what gets used.

What you do to it

Three operations carry the concept: write an item, read an item back by key, and search within a namespace.

The first two are ordinary key-value work. Search is the one that matters, because it is what turns the store from a configuration table into agent memory: it lets the agent retrieve something relevant without already knowing the key it was filed under. That is the difference between memory and a lookup table, and it is why the store supports search at all.

The actual methods

BaseStore is the interface, and the full method set is five, not three. The three above are the concept; these are the names:

BaseStore, the shape you will actually see
get(namespace: string[], key: string): Promise<Item | null>
put(namespace: string[], key: string, value: Record<string, any>, index?: false | string[]): Promise<void>
search(namespacePrefix: string[], options?: { filter?, limit?, offset?, query? }): Promise<SearchItem[]>
delete(namespace: string[], key: string): Promise<void>
listNamespaces(options?: { prefix?, suffix?, maxDepth?, limit?, offset? }): Promise<string[][]>

Four details in there are worth more than the signatures themselves:

  • The write method is put, not set or write. Naming questions are cheap marks and this is the one most likely to be offered against a plausible synonym.
  • search takes a namespace prefix, while get, put and delete take an exact namespace. That asymmetry is the whole reason a hierarchical namespace scheme is worth designing: prefixes are what make a level of the hierarchy searchable as a unit.
  • search accepts both filter and query. filter is metadata matching, query is semantic similarity. Having both in one method is why the store can serve retrieval and lookup from the same call.
  • The namespace is a sequence, not a string. ["org-123", "user-456", "preferences"], not "org-123/user-456/preferences". In Python it is a tuple, in TypeScript an array. A dotted or slashed string is a wrong answer that reads as right.

Python uses the same names in snake_case (list_namespaces) and prefixes every async variant with a: aget, aput, asearch, adelete, alist_namespaces. The store is also one of the nine API groups, so a client can read and write memory directly over HTTP without going through a run.

Configuring it

The store key in langgraph.json is where the store is declared, in the where-state-goes family alongside checkpointer. TTLs are configured separately for the two, which is the practical consequence of them having different lifespans.

The wider point, one more time, is that both memory components are declared at build time even though everything in them is runtime data. That is why a memory configuration change is a revision, not a live edit.

The sorting question worth practising

Before writing anything to memory, ask one question: does this need to outlive this conversation?

If yes, it is store data, and it needs a namespace that reflects who it belongs to. If no, it is thread state, and the checkpointer is already handling it without you doing anything. The most common mistake is not choosing wrongly between the two, it is writing working state into the store because the store feels more permanent and permanent feels safer. It is not safer. It is a growing pile of stale context that the agent will faithfully retrieve six weeks later.

Practice

Try it yourself

Recall

The namespace is the organising idea

Everything the store does, and everything authorization does to it later, comes back to this one structural choice.

How is data organised in the store, and why does that structure matter beyond mere tidiness?

Reveal answer

The store is namespaced key-value memory: a namespace locates a region of memory, and a key locates an item within it. That structure matters because the namespace is the store's own scoping mechanism, which is why authorization for the store works by rewriting the namespace rather than by returning a metadata filter the way every other resource does. A namespacing choice that looks like a tidiness decision is in fact the multi-tenancy boundary.

Quiz

Which API group for long-term memory

You need to read a user preference that was written during a completely different conversation last month. Which group of the Agent Server API do you reach for?

  1. AStore, because cross-thread long-term memory is exactly what it is for
  2. BThreads, because all persisted state is reached through the thread that produced it
  3. CSystem, because durable configuration is an operational concern
  4. DStateless Runs, because reading memory without a conversation is a stateless operation
Show answer

Correct answer: A — Store, because cross-thread long-term memory is exactly what it is for

Store is one of the nine API groups and it is the one that owns cross-thread long-term memory. The tempting wrong answer is Threads, because the checkpointer is thread-scoped and it is easy to over-generalise that into all persistence being reachable through a thread: the whole reason the store exists as a separate component is that some memory must not be trapped inside one conversation. The Stateless Runs option is a nice distractor because the word stateless is doing exactly the wrong kind of work, describing whether a run persists rather than what you are reading.

Quiz

Which component holds it

Four pieces of data. One of them belongs in the checkpointer rather than the store.

  1. AA user's preferred tone of voice, reused across every future conversation
  2. BThe list of tools this user's organization has enabled for everyone in it
  3. CThe partial results accumulated so far by the run currently executing
  4. DFacts the agent has learned about a user over several months
Show answer

Correct answer: C — The partial results accumulated so far by the run currently executing

Partial results of the currently executing run are the working state of one thread as it progresses, which is precisely the checkpointer's job: short-term and thread-scoped. The tempting wrong answer is the tool allowlist, because it feels like configuration and configuration is a word this domain overloads badly: an organization's enabled tools outlive any single conversation, so if it is being stored at all it is cross-thread and belongs to the store. The tone preference and the long-run learned facts are both unambiguously long-term cross-thread memory.

Recall

What you actually do to a store

Get the operation names right, and notice that one of them is not like the others.

What are the core operations against a store, what are the methods actually called, and which operation is qualitatively different from the rest?

Reveal answer

Writing an item, reading an item back by its key, and searching within a namespace. On BaseStore the full method set is get, put, search, delete and listNamespaces (list_namespaces in Python, where every async variant is prefixed with a). Two naming traps: the write method is put, not set or write; and search takes a namespace prefix while get, put and delete take an exact namespace. Search is the qualitatively different one, because it retrieves by relevance rather than by an identifier you already knew, which is what makes the store agent memory rather than a configuration table. It accepts filter for metadata matching and query for semantic similarity in the same call.

Do

Design a namespace scheme on paper

Ten minutes, no server. This is the exercise that makes the auth lesson two lessons from now feel obvious rather than arbitrary.

  • Take a multi-tenant product where each organization has many users, and write down a namespace scheme covering per-user memory, per-organization memory, and shared memory that is not tenant-specific.
  • For each level, write what a namespace rewrite would have to do in order to confine a caller to their own slice.
  • Now write down what goes wrong if the tenant identifier appears in the key rather than in the namespace.
  • Finally, mark which parts of your scheme belong in the store at all and which are really thread state pretending to be memory.
Done whenYou have a written three-level namespace scheme, a stated rewrite rule per level, an explanation of why putting the tenant in the key breaks scoping, and a marked separation between genuine store data and misfiled thread state.
Sign in to track your progress →