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 injectAgent().

How it works

LangGraph checkpoints agent state at every super-step. Each checkpoint is keyed by a thread ID. injectAgent() connects to these checkpoints automatically, so your users resume exactly where they left off — even if your server restarted between sessions.

Adapter-defined behavior

"Restore a prior thread's messages when the user switches to it" is a behavior the @threadplane/langgraph adapter implements because the LangGraph protocol exposes per-thread checkpoint history. The runtime-neutral Agent contract in @threadplane/chat doesn't require this — adapters built on event-stream protocols (like @threadplane/ag-ui) typically can't offer it. If you're writing your own adapter, the Writing an Adapter guide covers the design choice.

#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())
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 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 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. injectAgent() handles thread creation and restoration automatically; the configuration lives in your root provideAgent({...}) call.

import { signal } from '@angular/core';
import { provideAgent } from '@threadplane/langgraph';
 
export const appConfig: ApplicationConfig = {
  providers: [
    provideAgent({
      apiUrl: 'http://localhost:2024',
      assistantId: 'chat_agent',
      // Restore thread from localStorage on app start
      threadId: signal(localStorage.getItem('threadId')),
      // Persist thread ID whenever a new thread is created
      onThreadId: (id) => localStorage.setItem('threadId', id),
    }),
  ],
};

#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 singleton. The active-thread signal is held in shared state and wired into provideAgent({...}) at bootstrap; the component reads back through injectAgent().

// thread-state.ts — shared signals injected by app.config.ts
import { signal } from '@angular/core';
 
export const activeThreadId = signal<string | null>(null);

#Reactive Thread Switching

When you pass a Signal as threadId to provideAgent({...}), injectAgent() reacts to every change. Set the signal and the conversation switches automatically — no imperative calls needed.

// In app.config.ts:
provideAgent({
  apiUrl: '...',
  assistantId: 'chat_agent',
  threadId: activeThreadId,           // Signal — switches reactively
  onThreadId: (id) => activeThreadId.set(id),
});
 
// Anywhere in the app — clicking a thread in the sidebar triggers a reactive switch
selectThread(id: string) {
  activeThreadId.set(id);
  // injectAgent() 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 injectAgent() 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 execution
Automatic recovery

In most cases injectAgent() 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

injectAgent() 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, injectAgent() 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.

#What's Next