Persistence
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 streamResource().
LangGraph checkpoints agent state at every super-step. Each checkpoint is keyed by a thread ID. streamResource() 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 MemorySaver
from langgraph.graph import START, END, MessagesState, StateGraph
from langchain_openai import ChatOpenAI
llm = 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 restarts
graph = builder.compile(checkpointer=MemorySaver())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 thread
result = graph.invoke(
{"messages": [{"role": "user", "content": "What is LangGraph?"}]},
config={"configurable": {"thread_id": "user_123"}}
)
# Second message continues the same conversation
result = 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 checkpointUse 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. streamResource() handles thread creation and restoration automatically.
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { signal } from '@angular/core';
import { streamResource } from '@cacheplane/stream-resource';
@Component({
selector: 'app-chat',
templateUrl: './chat.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ChatComponent {
chat = streamResource<{ messages: BaseMessage[] }>({
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({ messages: [{ role: 'user', content: 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.
import { ChangeDetectionStrategy, Component, signal, computed } from '@angular/core';
import { streamResource } from '@cacheplane/stream-resource';
interface Thread {
id: string;
title: string;
updatedAt: Date;
}
@Component({
selector: 'app-thread-list',
templateUrl: './thread-list.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ThreadListComponent {
threads = signal<Thread[]>(this.loadThreads());
activeThreadId = signal<string | null>(null);
chat = streamResource<{ messages: BaseMessage[] }>({
assistantId: 'chat_agent',
threadId: this.activeThreadId,
onThreadId: (id) => {
this.activeThreadId.set(id);
this.addThread(id, 'New conversation');
},
});
activeThread = computed(() =>
this.threads().find((t) => t.id === this.activeThreadId())
);
selectThread(id: string) {
this.activeThreadId.set(id);
}
newConversation() {
this.chat.switchThread(null);
// A new thread ID is assigned on the next submit
}
private addThread(id: string, title: string) {
this.threads.update((list) => [
{ id, title, updatedAt: new Date() },
...list.filter((t) => t.id !== id),
]);
this.saveThreads();
}
private loadThreads(): Thread[] {
return JSON.parse(localStorage.getItem('threads') ?? '[]');
}
private saveThreads() {
localStorage.setItem('threads', JSON.stringify(this.threads()));
}
}Reactive Thread Switching
When you pass a Signal as threadId, streamResource() reacts to every change. Set the signal and the conversation switches automatically — no imperative calls needed.
activeThreadId = signal<string | null>(null);
chat = streamResource<{ messages: BaseMessage[] }>({
assistantId: 'chat_agent',
threadId: this.activeThreadId, // Signal — switches reactively
onThreadId: (id) => this.activeThreadId.set(id),
});
// Clicking a thread in the sidebar triggers a reactive switch
selectThread(id: string) {
this.activeThreadId.set(id);
// streamResource detects the signal change, fetches the thread's
// checkpoint from the server, and updates all derived signals
}Use the isThreadLoading() signal to show a skeleton UI while streamResource() 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 thread
loadConversation(threadId: string) {
this.chat.switchThread(threadId);
}
// Fork a conversation — create a new thread from current state
forkConversation() {
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 interruption
await chat.joinStream(runId, lastEventId);
// Picks up from the last event — no duplicate agent executionIn most cases streamResource() 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
streamResource() reads the threadId signal. If it contains a value, the existing thread's checkpoint is fetched from the server.
If threadId is null, streamResource() creates a new thread via the LangGraph API and fires onThreadId with the new ID.
Each super-step is checkpointed server-side. The messages() signal updates in real time as events arrive.
Setting the threadId signal (or calling switchThread()) loads the target thread's latest checkpoint. All signals update to reflect the restored state.
joinStream() reconnects to the in-progress run. The agent does not restart — streaming resumes from the last received event.
What's Next
Pause agent execution and wait for human approval before continuing.
Preserve long-term context across sessions with LangGraph's memory store.
Stream token-by-token responses and tool progress in real time.
Test thread persistence and switching deterministically with MockStreamTransport.