A reviewer tells the code-review agent “the maximum line length is 88 characters” in one conversation, and expects next week’s review to apply it. A checkpointer cannot do that: it saves the messages of one thread, the run identified by a thread_id, and a new review starts a new thread with no messages. A LangGraph store keeps data outside any thread, as JSON documents under a namespace (a tuple such as ("conventions", "acme/api")) and a key. In this lesson the agent saves each repository’s review rules to a store and reads them at the start of every thread. Recorded on Gemini (gemini-3.8-flash), thread t2 changed the rule, and thread t3, which shares no messages with it, reviewed a pull request against the new rule:
When you need this. Reviewers teach the assistant a repository convention in one conversation and want it used in another thread. Store their stated rules under that repository’s namespace, separately from the messages saved for each thread. The example compares overwriting a topic key with accumulating conflicting rules.
Pass the repository as context
Download the complete lab, extract it and open a terminal in add-a-long-term-memory-store/. The README lists its runtime and recorded examples.
python3 -m venv .venv
source .venv/bin/activate
python -m pip install -r requirements-lock.txtThe namespace decides which memories a run can read and overwrite, so the model must not choose it. The caller sets the repository for each run in a context object: StateGraph(MessagesState, context_schema=ReviewContext) declares its type, and invoke(..., context=ReviewContext(repo="acme/api")) supplies it. Nodes read it as runtime.context, and tools as runtime.context through ToolRuntime. The save tool has no repository argument, and a test checks that its schema offers the model only topic and quote. This routes memory to the caller-selected repository; it does not authenticate that caller or check their repository permissions. A service must authorize the repository before supplying this context. This lab does not implement that service boundary.
"""A code-review agent that keeps each repository's review conventions in a LangGraph store."""
import os
import uuid
from dataclasses import dataclass
from typing import Literal
from langchain_core.messages import HumanMessage, SystemMessage
from langchain_core.tools import tool
from langchain_google_genai import ChatGoogleGenerativeAI
from langchain_ollama import ChatOllama
from langgraph.graph import START, MessagesState, StateGraph
from langgraph.prebuilt import ToolNode, ToolRuntime, tools_condition
from langgraph.runtime import Runtime
from review_queue import get_diff
Topic = Literal["line_length", "tests", "logging", "dependencies"]
SYSTEM = """You review pull requests in the repository {repo}. Use get_diff to read a pull request.
Call remember_convention only when the developer's latest message states a rule for this repository,
then confirm in one sentence. Judge pull requests only against the saved conventions below,
and answer in at most two sentences.
Saved conventions for {repo}:
{conventions}"""
# Gemini by default; REVIEW_MODEL=gemma4:e2b runs the same graph on a local Ollama model.
MODEL = os.environ.get("REVIEW_MODEL", "gemini-3.8-flash")
def chat_model():
if MODEL.startswith("gemini"):
return ChatGoogleGenerativeAI(model=MODEL) # reads GOOGLE_API_KEY from the environment
return ChatOllama(model=MODEL, temperature=0, reasoning=False)
@dataclass
class ReviewContext:
repo: str # set by the caller for each run, never by the model
def namespace(repo: str) -> tuple[str, ...]:
return ("conventions", repo)
def make_remember(key_by_topic: bool):
@tool
def remember_convention(topic: Topic, quote: str, runtime: ToolRuntime[ReviewContext]) -> str:
"""Save a review rule the developer just stated for this repository.
quote: the developer's words that state the rule, copied exactly. These words are saved as the rule."""
said = next(m.content for m in reversed(runtime.state["messages"]) if isinstance(m, HumanMessage))
start = said.lower().find(quote.strip().lower()) if quote.strip() else -1
if start < 0:
return "Not saved: the quote is not in the developer's latest message."
# The rule is the developer's own words, cut from their message: the model cannot add its own wording.
rule = said[start:start + len(quote.strip())]
# Keyed by topic, a new rule replaces the old one. A fresh ID per write keeps both.
key = topic if key_by_topic else str(uuid.uuid4())
value = {"topic": topic, "rule": rule, "thread": runtime.config["configurable"]["thread_id"]}
runtime.store.put(namespace(runtime.context.repo), key, value)
return f"Saved the {topic} rule for {runtime.context.repo}."
return remember_convention
def recall(store, repo: str) -> str:
items = sorted(store.search(namespace(repo), limit=50), key=lambda item: item.updated_at)
return "\n".join(f"- {item.value['topic']}: {item.value['rule']}" for item in items) or "- none saved"
def build_graph(store, model=None, *, checkpointer=None, key_by_topic=True):
tools = [get_diff, make_remember(key_by_topic)]
llm = (model or chat_model()).bind_tools(tools)
def agent(state: MessagesState, runtime: Runtime[ReviewContext]) -> dict:
repo = runtime.context.repo
system = SYSTEM.format(repo=repo, conventions=recall(runtime.store, repo))
return {"messages": [llm.invoke([SystemMessage(system), *state["messages"]])]}
graph = StateGraph(MessagesState, context_schema=ReviewContext)
graph.add_node("agent", agent)
graph.add_node("tools", ToolNode(tools))
graph.add_edge(START, "agent")
graph.add_conditional_edges("agent", tools_condition)
graph.add_edge("tools", "agent")
# The checkpointer keeps each thread's messages; the store keeps conventions across threads.
return graph.compile(checkpointer=checkpointer, store=store)Inspect review_queue.py
"""Pull requests in two repositories. The diff summaries name the longest changed line."""
from langchain_core.tools import tool
PULL_REQUESTS = {
501: {"repo": "acme/api", "title": "Add pagination to GET /projects",
"diff": "api/projects.py: add limit and cursor query parameters; longest changed line is 96 characters\n"
"tests/test_projects.py: add test_second_page"},
502: {"repo": "acme/web", "title": "Show the project count in the sidebar",
"diff": "web/sidebar.tsx: render the project count badge; longest changed line is 96 characters"},
}
@tool
def get_diff(number: int) -> str:
"""Get the changed files and a one-line summary of each change for a pull request."""
return PULL_REQUESTS[number]["diff"]Save a rule from a tool
remember_convention writes with runtime.store.put(namespace, key, value). Three choices in it decide what the store holds:
- The key is the topic.
putwith an existing key replaces the item, so a new line-length rule overwrites the old one.Topicis a fixedLiteralof four topics; a topic outside it fails schema validation and nothing is saved. - The value carries its source. Each item stores the topic, the rule and the thread that saved it, so a wrong memory can be traced and corrected. After the recorded run, the
acme/apiitem was{"topic":"line_length","rule":"The maximum line length is now 100 characters","thread":"t2"}. - The rule is the developer’s words. The tool has no argument for the model’s own wording of the rule. It looks for
quotein the developer’s latest message, read fromruntime.state, and saves that part of the message, with the developer’s capitalization, as the rule. A quote that is not in the message saves nothing. An earlier version took a separateruleargument and checked only the quote, so a model could quoteHelloand saveThe maximum line length is 9000 characters.; a test now sends that call and checks that no9000reaches the store. The check proves that the words were said, not that they state a rule: a quote ofHellois still saved as a line-length rule. If that matters, ask the developer to confirm the rule before theput.
A stale rule beside the new one. With a new uuid4 key per write, thread t2 added the 100-character rule next to the 88-character rule instead of replacing it. The store held 2 line-length rules, both in the prompt, newest last. The t3 review returned Yes, pull request 501 follows the line-length rule because the longest changed line is 96 characters, which is within the 100-character maximum.. This run selected the newer limit, but both conflicting rules still reached the model. With one key per topic, the same threads left one rule, and the review returned Yes, pull request 501 follows the line-length rule, as the longest changed line is 96 characters, which is within the 100-character limit.. Memory conflicts are resolved when you write, by choosing keys so a correction replaces what it corrects; left for the model at read time, a model is free to pick either rule or report the conflict instead of deciding. The cost of topic keys is that two rules on one topic cannot both be kept, and a rule outside the four topics cannot be saved at all.
Concurrent writes. This example stores one value per namespace/topic key, with no version history. Its tests exercise sequential runs, not concurrent writers. To correct a wrong memory, store.delete(namespace, key) removes it; the stored thread shows where it came from.
Read the rules in a node
The agent node takes a second parameter, runtime: Runtime[ReviewContext], and LangGraph passes the store and the context in it. recall lists the repository’s namespace with store.search(namespace, limit=50), sorts the items by updated_at, and the node puts them in the system prompt before every model call.
search with no query lists items under the namespace prefix; with a query, it runs semantic search, which needs an embedding index configured on the store and an embedding model. A repository has a handful of rules, so listing them is exact and needs no second model. Semantic search becomes worth its cost when the memories no longer fit in the prompt and the agent must choose among them.
A namespace prefix that is too short. search matches namespaces by prefix. In both runs, search(("conventions",)) returned the acme/api rules (1 item with topic keys, 2 with fresh IDs), so a review in acme/web that searched the shorter prefix would apply another repository’s rules. namespace() builds the full tuple from the context, and thread t4 in acme/web answered There are no saved line-length conventions for acme/web to evaluate pull request 502 against. For reference, the longest changed line in the pull request is 96 characters.
Run it on Gemini
Export your Gemini API key as GOOGLE_API_KEY and run the recorder. With REVIEW_MODEL=gemma4:e2b, the same graph runs on a local Ollama model.
python3 -m pip install -r requirements-lock.txt
python3 record.pyrecord.py runs four threads against one store, twice: once with the tool as written, and once with a fresh uuid4 key for every write. The basic example on the stores page also creates a new ID per memory, with uuid7().
Inspect record.py
"""Run the review agent on Gemini across separate threads that share one store, and save what it stored."""
import json
from langchain_core.messages import HumanMessage
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.store.memory import InMemoryStore
from graph import MODEL, ReviewContext, build_graph, namespace
STEPS = [ # thread, repository, message
("t1", "acme/api", "Remember for this repository: the maximum line length is 88 characters."),
("t2", "acme/api", "We changed the rule. The maximum line length is now 100 characters; remember that."),
("t3", "acme/api", "Review pull request 501. Does it follow our line-length rule?"),
("t4", "acme/web", "Review pull request 502. Does it follow our line-length rule?"),
]
def snapshot(store, prefix):
return [{"namespace": list(i.namespace), "key": i.key if len(i.key) < 20 else "<uuid4>", "value": i.value} for i in store.search(prefix, limit=50)]
def run(key_by_topic: bool) -> dict:
store = InMemoryStore()
graph = build_graph(store, checkpointer=InMemorySaver(), key_by_topic=key_by_topic)
threads = []
for thread, repo, text in STEPS:
result = graph.invoke({"messages": [HumanMessage(text)]}, {"configurable": {"thread_id": thread}},
context=ReviewContext(repo=repo))
calls = [{"name": c["name"], "args": c["args"]} for m in result["messages"] for c in getattr(m, "tool_calls", [])]
results = [m.content for m in result["messages"] if m.type == "tool"]
threads.append({"thread": thread, "repo": repo, "message": text, "tool_calls": calls, "tool_results": results,
"answer": result["messages"][-1].text, "store_after": snapshot(store, namespace(repo))})
# Thread state is per thread: t3 starts with none of t1's or t2's messages, yet it read their rule.
per_thread = {t: [m.type for m in graph.get_state({"configurable": {"thread_id": t}}).values["messages"]]
for t, _, _ in STEPS}
return {"key_by_topic": key_by_topic, "threads": threads, "thread_messages": per_thread,
"prefix_search": snapshot(store, ("conventions",))}
def print_store(r) -> None:
print(" search(('conventions',)):", json.dumps(r["prefix_search"]))
def main() -> None:
runs = [run(True), run(False)]
with open("runs.json", "w") as f:
json.dump({"model": MODEL, "runs": runs}, f, indent=1)
for r in runs:
print(f"#### keys: {'topic' if r['key_by_topic'] else 'uuid4 per memory'}")
for t in r["threads"]:
print(f"== {t['thread']} [{t['repo']}] {t['message']}")
for c, out in zip(t["tool_calls"], t["tool_results"]):
print(f" agent -> {c['name']}({json.dumps(c['args'])})")
print(f" tools -> {out}")
print(f" answer: {t['answer']}")
print(f" thread {t['thread']} messages: {' | '.join(r['thread_messages'][t['thread']])}")
print(f" store {json.dumps(namespace(t['repo']))}: {json.dumps([(i['key'], i['value']['rule']) for i in t['store_after']])}")
print_store(r)
if __name__ == "__main__":
main()Inspect the complete recorded runs
{
"model": "gemini-3.8-flash",
"runs": [
{
"key_by_topic": true,
"threads": [
{
"thread": "t1",
"repo": "acme/api",
"message": "Remember for this repository: the maximum line length is 88 characters.",
"tool_calls": [
{
"name": "remember_convention",
"args": {
"quote": "the maximum line length is 88 characters.",
"topic": "line_length"
}
}
],
"tool_results": [
"Saved the line_length rule for acme/api."
],
"answer": "I have saved the convention that the maximum line length is 88 characters for this repository.",
"store_after": [
{
"namespace": [
"conventions",
"acme/api"
],
"key": "line_length",
"value": {
"topic": "line_length",
"rule": "the maximum line length is 88 characters.",
"thread": "t1"
}
}
]
},
{
"thread": "t2",
"repo": "acme/api",
"message": "We changed the rule. The maximum line length is now 100 characters; remember that.",
"tool_calls": [
{
"name": "remember_convention",
"args": {
"quote": "The maximum line length is now 100 characters",
"topic": "line_length"
}
}
],
"tool_results": [
"Saved the line_length rule for acme/api."
],
"answer": "I have updated the convention to set the maximum line length to 100 characters.",
"store_after": [
{
"namespace": [
"conventions",
"acme/api"
],
"key": "line_length",
"value": {
"topic": "line_length",
"rule": "The maximum line length is now 100 characters",
"thread": "t2"
}
}
]
},
{
"thread": "t3",
"repo": "acme/api",
"message": "Review pull request 501. Does it follow our line-length rule?",
"tool_calls": [
{
"name": "get_diff",
"args": {
"number": 501
}
}
],
"tool_results": [
"api/projects.py: add limit and cursor query parameters; longest changed line is 96 characters\ntests/test_projects.py: add test_second_page"
],
"answer": "Yes, pull request 501 follows the line-length rule, as the longest changed line is 96 characters, which is within the 100-character limit.",
"store_after": [
{
"namespace": [
"conventions",
"acme/api"
],
"key": "line_length",
"value": {
"topic": "line_length",
"rule": "The maximum line length is now 100 characters",
"thread": "t2"
}
}
]
},
{
"thread": "t4",
"repo": "acme/web",
"message": "Review pull request 502. Does it follow our line-length rule?",
"tool_calls": [
{
"name": "get_diff",
"args": {
"number": 502
}
}
],
"tool_results": [
"web/sidebar.tsx: render the project count badge; longest changed line is 96 characters"
],
"answer": "There are no saved line-length conventions for acme/web to evaluate pull request 502 against. For reference, the longest changed line in the pull request is 96 characters.",
"store_after": []
}
],
"thread_messages": {
"t1": [
"human",
"ai",
"tool",
"ai"
],
"t2": [
"human",
"ai",
"tool",
"ai"
],
"t3": [
"human",
"ai",
"tool",
"ai"
],
"t4": [
"human",
"ai",
"tool",
"ai"
]
},
"prefix_search": [
{
"namespace": [
"conventions",
"acme/api"
],
"key": "line_length",
"value": {
"topic": "line_length",
"rule": "The maximum line length is now 100 characters",
"thread": "t2"
}
}
]
},
{
"key_by_topic": false,
"threads": [
{
"thread": "t1",
"repo": "acme/api",
"message": "Remember for this repository: the maximum line length is 88 characters.",
"tool_calls": [
{
"name": "remember_convention",
"args": {
"quote": "the maximum line length is 88 characters.",
"topic": "line_length"
}
}
],
"tool_results": [
"Saved the line_length rule for acme/api."
],
"answer": "I have saved the convention that the maximum line length is 88 characters for this repository.",
"store_after": [
{
"namespace": [
"conventions",
"acme/api"
],
"key": "<uuid4>",
"value": {
"topic": "line_length",
"rule": "the maximum line length is 88 characters.",
"thread": "t1"
}
}
]
},
{
"thread": "t2",
"repo": "acme/api",
"message": "We changed the rule. The maximum line length is now 100 characters; remember that.",
"tool_calls": [
{
"name": "remember_convention",
"args": {
"topic": "line_length",
"quote": "The maximum line length is now 100 characters"
}
}
],
"tool_results": [
"Saved the line_length rule for acme/api."
],
"answer": "I have saved the new line length rule of 100 characters for this repository.",
"store_after": [
{
"namespace": [
"conventions",
"acme/api"
],
"key": "<uuid4>",
"value": {
"topic": "line_length",
"rule": "the maximum line length is 88 characters.",
"thread": "t1"
}
},
{
"namespace": [
"conventions",
"acme/api"
],
"key": "<uuid4>",
"value": {
"topic": "line_length",
"rule": "The maximum line length is now 100 characters",
"thread": "t2"
}
}
]
},
{
"thread": "t3",
"repo": "acme/api",
"message": "Review pull request 501. Does it follow our line-length rule?",
"tool_calls": [
{
"name": "get_diff",
"args": {
"number": 501
}
}
],
"tool_results": [
"api/projects.py: add limit and cursor query parameters; longest changed line is 96 characters\ntests/test_projects.py: add test_second_page"
],
"answer": "Yes, pull request 501 follows the line-length rule because the longest changed line is 96 characters, which is within the 100-character maximum.",
"store_after": [
{
"namespace": [
"conventions",
"acme/api"
],
"key": "<uuid4>",
"value": {
"topic": "line_length",
"rule": "the maximum line length is 88 characters.",
"thread": "t1"
}
},
{
"namespace": [
"conventions",
"acme/api"
],
"key": "<uuid4>",
"value": {
"topic": "line_length",
"rule": "The maximum line length is now 100 characters",
"thread": "t2"
}
}
]
},
{
"thread": "t4",
"repo": "acme/web",
"message": "Review pull request 502. Does it follow our line-length rule?",
"tool_calls": [
{
"name": "get_diff",
"args": {
"number": 502
}
}
],
"tool_results": [
"web/sidebar.tsx: render the project count badge; longest changed line is 96 characters"
],
"answer": "There are no saved conventions regarding line length for acme/web to evaluate pull request 502 against. The longest changed line in the PR is 96 characters.",
"store_after": []
}
],
"thread_messages": {
"t1": [
"human",
"ai",
"tool",
"ai"
],
"t2": [
"human",
"ai",
"tool",
"ai"
],
"t3": [
"human",
"ai",
"tool",
"ai"
],
"t4": [
"human",
"ai",
"tool",
"ai"
]
},
"prefix_search": [
{
"namespace": [
"conventions",
"acme/api"
],
"key": "<uuid4>",
"value": {
"topic": "line_length",
"rule": "the maximum line length is 88 characters.",
"thread": "t1"
}
},
{
"namespace": [
"conventions",
"acme/api"
],
"key": "<uuid4>",
"value": {
"topic": "line_length",
"rule": "The maximum line length is now 100 characters",
"thread": "t2"
}
}
]
}
]
}The tool result, not the model’s reply, records whether a rule was saved: Saved the line_length rule comes from the put call, while the reply is the model’s summary of it.
Store or checkpointer
The graph is compiled with both. The checkpointer saved each thread’s own messages: thread t3 held human | ai | tool | ai, none of them from t1 or t2. The store held one item that every acme/api thread read. Keeping rules as messages in a thread would replay the whole conversation into every model call of that thread and reach no other thread. Keeping a conversation in the store would turn every exchange into a memory the agent must later filter. The two answer different questions: the checkpointer resumes a run, and the store answers “what does this agent know about this repository”.
InMemoryStore lives in the Python process and is empty after a restart. The documentation lists database-backed stores, such as PostgresStore, for production; they implement the same put, get, search and delete calls.
Test the memory store without a model
The offline tests replace Gemini with a scripted chat model and inspect the system prompt it receives:
python3 -m pytest -v test_graph.pyThe suite has 10 tests:
| Case | Expected behavior |
|---|---|
| A rule saved in one thread | Appears in the system prompt of a new thread. |
| A new rule on the same topic | Replaces the old one; the prompt no longer holds the old value. |
| A fresh ID per write | Keeps both rules, and both reach the prompt. |
| Another repository | Sees none of the first repository’s rules. |
| A shorter namespace prefix | Returns every repository’s items. |
| A rule the developer did not state | Is not saved. |
| A real quote with a different rule | This helper saves the matching developer quote rather than a separate rule written by the model. |
| An unknown topic | Becomes an error tool message; nothing is saved. |
| The repository argument | Is not in the tool schema the model sees. |
Keying by a new ID, shortening the namespace, removing the quote check, saving the model’s wording instead of the quote, or widening Topic to any string fails at least one test.
Inspect the tests and their output
"""Offline tests for the convention memory. A scripted chat model replaces Gemma, so no Ollama is needed."""
from itertools import count
from langchain_core.language_models.fake_chat_models import GenericFakeChatModel
from langchain_core.messages import AIMessage, HumanMessage
from langgraph.store.memory import InMemoryStore
from graph import ReviewContext, build_graph, make_remember, namespace
ids = count()
RULE_88 = "Remember: the maximum line length is 88 characters."
RULE_100 = "We changed it: the maximum line length is now 100 characters."
class ScriptedChat(GenericFakeChatModel):
"""Returns the scripted messages in order and keeps the system prompt of every call."""
prompts: list = []
def bind_tools(self, tools, **kwargs):
return self
def _generate(self, messages, *args, **kwargs):
self.prompts.append(messages[0].content)
return super()._generate(messages, *args, **kwargs)
def save(topic, quote, **extra):
return AIMessage("", tool_calls=[{"name": "remember_convention", "id": f"c{next(ids)}",
"args": {"topic": topic, "quote": quote, **extra}}])
def turn(store, repo, text, script, **options):
model = ScriptedChat(messages=iter([*script, AIMessage("Done.")]), prompts=[])
graph = build_graph(store, model, **options)
result = graph.invoke({"messages": [HumanMessage(text)]}, {"configurable": {"thread_id": f"t{next(ids)}"}},
context=ReviewContext(repo=repo))
return model.prompts, result["messages"]
def two_rules(**options):
store = InMemoryStore()
turn(store, "acme/api", RULE_88, [save("line_length", "maximum line length is 88")], **options)
turn(store, "acme/api", RULE_100, [save("line_length", "now 100 characters")], **options)
prompts, _ = turn(store, "acme/api", "Review 501.", [], **options)
return store, prompts[0]
def test_a_saved_rule_is_read_in_a_new_thread():
store = InMemoryStore()
turn(store, "acme/api", RULE_88, [save("line_length", "maximum line length is 88")])
prompts, _ = turn(store, "acme/api", "Review 501.", [])
assert "- line_length: maximum line length is 88" in prompts[0]
def test_a_new_rule_on_the_same_topic_replaces_the_old_one():
store, prompt = two_rules()
items = store.search(namespace("acme/api"))
assert [i.value["rule"] for i in items] == ["now 100 characters"]
assert "88" not in prompt
def test_a_fresh_id_per_write_keeps_the_stale_rule():
store, prompt = two_rules(key_by_topic=False)
assert len(store.search(namespace("acme/api"))) == 2
assert "maximum line length is 88" in prompt and "now 100 characters" in prompt
def test_repositories_do_not_see_each_others_conventions():
store = InMemoryStore()
turn(store, "acme/api", RULE_88, [save("line_length", "maximum line length is 88")])
prompts, _ = turn(store, "acme/web", "Review 502.", [])
assert "- none saved" in prompts[0] and "88" not in prompts[0]
def test_a_shorter_namespace_prefix_returns_every_repository():
store = InMemoryStore()
turn(store, "acme/api", RULE_88, [save("line_length", "maximum line length is 88")])
turn(store, "acme/web", "Remember: tests are required.", [save("tests", "tests are required")])
assert {i.namespace for i in store.search(("conventions",))} == {namespace("acme/api"), namespace("acme/web")}
assert {i.namespace for i in store.search(namespace("acme/web"))} == {namespace("acme/web")}
def test_a_rule_the_developer_did_not_state_is_not_saved():
store = InMemoryStore()
_, messages = turn(store, "acme/web", "Review 502.", [save("line_length", "lines may be 100 characters")])
assert messages[2].content.startswith("Not saved") and store.search(namespace("acme/web")) == []
def test_an_unknown_topic_is_refused_before_anything_is_saved():
store = InMemoryStore()
_, messages = turn(store, "acme/api", "Remember: use black.", [save("formatting", "use black")])
assert messages[2].status == "error" and store.search(namespace("acme/api")) == []
def test_the_model_cannot_choose_the_repository():
schema = make_remember(True).tool_call_schema.model_json_schema()
assert set(schema["properties"]) == {"topic", "quote"}
def test_the_saved_rule_is_the_developers_words_not_the_models():
store = InMemoryStore()
fabricated = save("line_length", "Hello", rule="The maximum line length is 9000 characters.")
_, messages = turn(store, "acme/api", "Hello, can you review 415?", [fabricated])
saved = [i.value for i in store.search(namespace("acme/api"))]
assert "9000" not in repr(saved) # the quote was real, but the model's own rule text is not stored
assert all(v["rule"] in "Hello, can you review 415?" for v in saved)
def test_the_rule_keeps_the_developers_capitalization():
store = InMemoryStore()
turn(store, "acme/api", "Remember: every PR needs a Unit Test.", [save("tests", "every pr needs a unit test")])
assert [i.value["rule"] for i in store.search(namespace("acme/api"))] == ["every PR needs a Unit Test"]test_graph.py::test_a_saved_rule_is_read_in_a_new_thread PASSED [ 10%]
test_graph.py::test_a_new_rule_on_the_same_topic_replaces_the_old_one PASSED [ 20%]
test_graph.py::test_a_fresh_id_per_write_keeps_the_stale_rule PASSED [ 30%]
test_graph.py::test_repositories_do_not_see_each_others_conventions PASSED [ 40%]
test_graph.py::test_a_shorter_namespace_prefix_returns_every_repository PASSED [ 50%]
test_graph.py::test_a_rule_the_developer_did_not_state_is_not_saved PASSED [ 60%]
test_graph.py::test_an_unknown_topic_is_refused_before_anything_is_saved PASSED [ 70%]
test_graph.py::test_the_model_cannot_choose_the_repository PASSED [ 80%]
test_graph.py::test_the_saved_rule_is_the_developers_words_not_the_models PASSED [ 90%]
test_graph.py::test_the_rule_keeps_the_developers_capitalization PASSED [100%]Running teardown with pytest sessionfinish...
============================== 10 passed in 0.39s ==============================Check your understanding
You change the tool to save every rule under str(uuid.uuid4()). A reviewer states a rule on Monday and corrects it on Tuesday. Decide what Wednesday’s review thread reads from the store before you answer.
Read the explanation
One key per topic lets a correction replace its earlier value. Fresh IDs retain both values; the saved model’s choice does not remove the conflict from the store.
What’s next
Return to the langgraph course to revisit its lessons.
References
- LangGraph: Stores: namespaces,
putandsearch, listing a namespace prefix, thelimitcut-off, semantic search, theRuntimestore and context in nodes, and database-backed stores. Checked October 21, 2026. - LangGraph: Persistence: checkpointers compared with stores, and compiling a graph with both. Checked October 21, 2026.
- LangChain: ChatGoogleGenerativeAI: the Gemini chat model integration. Checked October 21, 2026.
- LangGraph: Workflows and agents:
ToolNode. Checked October 21, 2026. ToolRuntimewithstore,contextandstate,InMemoryStore.deleteand theItemfields are taken from the installed langgraph 1.2.16 and langgraph-prebuilt 1.1.1 source.