Thread persistence keeps conversations alive across page refreshes, browser restarts, and server deployments. This guide covers configuring checkpointers on the Python side and wiring up thread management in your Angular components with agent().
How it works
LangGraph checkpoints agent state at every super-step. Each checkpoint is keyed by a thread ID. agent() connects to these checkpoints automatically, so your users resume exactly where they left off — even if your server restarted between sessions.
Python: Checkpointer Setup
Every LangGraph agent needs a checkpointer to persist state between invocations. The checkpointer you choose depends on your environment.
from langgraph.checkpoint.memory import MemorySaverfrom langgraph.graph import START, END, MessagesState, StateGraphfrom langchain_openai import ChatOpenAIllm = ChatOpenAI(model="gpt-5-mini")def call_model(state: MessagesState) -> dict: return {"messages": [llm.invoke(state["messages"])]}builder = StateGraph(MessagesState)builder.add_node("model", call_model)builder.add_edge(START, "model")builder.add_edge("model", END)# MemorySaver stores checkpoints in-process memory# Fast for development — lost when the process restartsgraph = builder.compile(checkpointer=MemorySaver())
Production checkpointers
MemorySaver is for development only — all state vanishes when the process exits. For anything users depend on, use PostgresSaver. SqliteSaver is a middle ground for prototypes and single-server deployments where you need persistence without a database.
Python: Thread IDs in Graph Invocation
The thread ID is how LangGraph associates a conversation with its checkpoint history. Pass it in the configurable dict every time you invoke the graph:
# First message creates the threadresult = graph.invoke( {"messages": [{"role": "user", "content": "What is LangGraph?"}]}, config={"configurable": {"thread_id": "user_123"}})# Second message continues the same conversationresult = graph.invoke( {"messages": [{"role": "user", "content": "How does it handle state?"}]}, config={"configurable": {"thread_id": "user_123"}})# The agent sees both messages — the full history is restored from the checkpoint
Thread ID strategy
Use stable, user-scoped identifiers for thread IDs. A common pattern is f"{user_id}_{session_id}" — this prevents cross-user data leaks and lets one user have multiple conversations.
Angular: Basic Thread Persistence
Save the thread ID to localStorage so conversations survive page refreshes. agent() handles thread creation and restoration automatically.
import { ChangeDetectionStrategy, Component } from '@angular/core';import { signal } from '@angular/core';import { agent } from '@ngaf/langgraph';@Component({ selector: 'app-chat', templateUrl: './chat.component.html', changeDetection: ChangeDetectionStrategy.OnPush,})export class ChatComponent { chat = agent({ assistantId: 'chat_agent', // Restore thread from localStorage on mount threadId: signal(localStorage.getItem('threadId')), // Persist thread ID whenever a new thread is created onThreadId: (id) => localStorage.setItem('threadId', id), }); send(text: string) { this.chat.submit({ message: text }); }}
Angular: Thread-List Component
A real chat application needs a sidebar showing all conversations. Here is a full thread-list component that manages multiple threads alongside your chat resource.
When you pass a Signal as threadId, agent() reacts to every change. Set the signal and the conversation switches automatically — no imperative calls needed.
activeThreadId = signal<string | null>(null);chat = agent({ assistantId: 'chat_agent', threadId: this.activeThreadId, // Signal — switches reactively onThreadId: (id) => this.activeThreadId.set(id),});// Clicking a thread in the sidebar triggers a reactive switchselectThread(id: string) { this.activeThreadId.set(id); // agent detects the signal change, fetches the thread's // checkpoint from the server, and updates all derived signals}
Thread loading state
Use the isThreadLoading() signal to show a skeleton UI while agent() fetches checkpoint state from the server. This avoids a flash of empty content when switching threads.
Manual Thread Switching
Use switchThread() for imperative thread changes. This is useful when you want to explicitly control when the switch happens — for example, after an animation completes or a modal closes.
// Start a fresh conversation (null = new thread on next submit)newConversation() { this.chat.switchThread(null);}// Jump to a specific threadloadConversation(threadId: string) { this.chat.switchThread(threadId);}// Fork a conversation — create a new thread from current stateforkConversation() { this.chat.switchThread(null); this.chat.submit({ messages: this.chat.messages(), });}
Checkpoint Recovery
When a connection drops mid-stream, joinStream() reconnects to an in-progress run without restarting the agent. This prevents duplicate work and lost tokens.
// Rejoin a running stream after a network interruptionawait chat.joinStream(runId, lastEventId);// Picks up from the last event — no duplicate agent execution
Automatic recovery
In most cases agent() handles reconnection internally. Use joinStream() directly only when you need explicit control — for example, when restoring a run ID from a URL parameter after a full page reload.
Thread Lifecycle
1
Component mounts
agent() reads the threadId signal. If it contains a value, the existing thread's checkpoint is fetched from the server.
2
User sends first message
If threadId is null, agent() creates a new thread via the LangGraph API and fires onThreadId with the new ID.
3
Agent streams response
Each super-step is checkpointed server-side. The messages() signal updates in real time as events arrive.
4
User switches threads
Setting the threadId signal (or calling switchThread()) loads the target thread's latest checkpoint. All signals update to reflect the restored state.
5
Connection drops
joinStream() reconnects to the in-progress run. The agent does not restart — streaming resumes from the last received event.