Open-source LangGraph thread history persistence for CopilotKit. Restore chat history on page refresh with your own LangGraph deployment.
Building a chat app with CopilotKit and LangGraph? You've probably noticed:
- Page refresh = empty chat - All messages disappear
- Thread switching loses context - No automatic history restoration
- No persistence of agent state - Users lose context
CopilotKit's default runtime doesn't automatically fetch historical messages from LangGraph's checkpoint system. This package adds that capability.
With this package:
- Chat history restored on page load
- Seamless thread switching
- Agent state preserved
- Works with any LangGraph deployment (LangGraph Cloud, self-hosted)
- MIT licensed, open-source
npm install copilotkit-langgraph-history
# or
pnpm add copilotkit-langgraph-history
# or
yarn add copilotkit-langgraph-historyThis package requires the following peer dependencies:
npm install @copilotkit/runtime @copilotkitnext/runtime @ag-ui/core @langchain/langgraph-sdk rxjs// app/api/copilotkit/route.ts
import { CopilotRuntime, createCopilotEndpointSingleRoute } from "@copilotkit/runtime/v2";
import {
HistoryHydratingAgentRunner,
createIsolatedAgent,
} from "copilotkit-langgraph-history";
const deploymentUrl = process.env.LANGGRAPH_DEPLOYMENT_URL!;
const langsmithApiKey = process.env.LANGSMITH_API_KEY;
const graphId = "my-agent";
function createRuntime() {
// Create isolated agent (prevents serverless state contamination)
const agent = createIsolatedAgent({
deploymentUrl,
graphId,
langsmithApiKey,
});
// Create history-hydrating runner
const runner = new HistoryHydratingAgentRunner({
agent,
deploymentUrl,
graphId,
langsmithApiKey,
historyLimit: 100, // Max messages to load
});
return new CopilotRuntime({
agents: { [graphId]: agent },
runner,
});
}
export const POST = async (req: Request) => {
const runtime = createRuntime();
const route = createCopilotEndpointSingleRoute({
runtime,
basePath: "/api/copilotkit",
});
return route.handleRequest(req);
};import { CopilotKit } from "@copilotkit/react-core";
import { CopilotChat } from "@copilotkit/react-ui";
function App() {
return (
<CopilotKit
runtimeUrl="/api/copilotkit"
agent="my-agent"
threadId={threadId} // Pass your thread ID here
>
<CopilotChat />
</CopilotKit>
);
}| Option | Type | Default | Description |
|---|---|---|---|
agent |
LangGraphAgent |
required | The LangGraphAgent instance |
deploymentUrl |
string |
required | LangGraph deployment URL |
graphId |
string |
required | Graph identifier |
langsmithApiKey |
string |
undefined |
LangSmith API key |
historyLimit |
number |
100 |
Max checkpoints to fetch (max 1000) |
clientTimeoutMs |
number |
1800000 |
HTTP timeout (default 30 min) |
debug |
boolean |
false |
Enable debug logging |
stateExtractor |
function |
undefined |
Custom state extraction |
| Option | Type | Default | Description |
|---|---|---|---|
deploymentUrl |
string |
required | LangGraph deployment URL |
graphId |
string |
required | Graph identifier |
langsmithApiKey |
string |
undefined |
LangSmith API key |
clientTimeoutMs |
number |
1800000 |
HTTP timeout |
debug |
boolean |
false |
Enable debug mode |
If you need to extract custom fields from the CopilotKit request:
const runner = new HistoryHydratingAgentRunner({
agent,
deploymentUrl,
graphId,
stateExtractor: (input, forwardedProps) => ({
// Extract from forwardedProps.configurable (useCoAgent config)
tenantId: forwardedProps?.configurable?.tenantId as string,
userId: forwardedProps?.configurable?.userId as string,
// Or from input.state (useCoAgent initialState)
...input.state,
}),
});In serverless environments (especially Vercel Fluid Compute), Node.js module-level state can be shared between bundled routes. This causes a critical bug where the LangGraph deployment URL gets contaminated between different agent configurations.
createIsolatedAgent fixes this by:
- Creating agents with frozen, immutable config
- Verifying the internal client URL matches expected
- Force-replacing the client if contamination is detected
Always use createIsolatedAgent instead of new LangGraphAgent() in serverless environments.
Enable debug logging to troubleshoot issues:
const runner = new HistoryHydratingAgentRunner({
// ...
debug: true,
});This logs:
- History fetching progress
- Message transformation details
- Stream processing events
- State extraction results
When a client connects to an existing thread:
- Fetch History: Retrieves all checkpoints from LangGraph via
client.threads.getHistory() - Extract Messages: Processes checkpoints chronologically, deduplicating messages by ID
- Transform Format: Converts LangGraph messages to CopilotKit format
- Emit Events: Sends
MESSAGES_SNAPSHOTandSTATE_SNAPSHOTevents to frontend - Join Stream: If thread is busy, joins the active execution stream
on_chat_model_stream→TEXT_MESSAGE_CONTENTon_chat_model_start→TEXT_MESSAGE_STARTon_chat_model_end→TEXT_MESSAGE_ENDon_tool_start→TOOL_CALL_STARTon_tool_end→TOOL_CALL_END- Custom CopilotKit events (manual message/tool/state emission)
- Interrupt events
// Core
export { HistoryHydratingAgentRunner } from "copilotkit-langgraph-history";
export { createIsolatedAgent } from "copilotkit-langgraph-history";
// Types
export type {
HistoryHydratingRunnerConfig,
StateExtractor,
CreateIsolatedAgentConfig,
LangGraphMessage,
ThreadState,
} from "copilotkit-langgraph-history";
// Constants
export {
DEFAULT_TIMEOUT,
DEFAULT_HISTORY_LIMIT,
MAX_HISTORY_LIMIT,
} from "copilotkit-langgraph-history";
// Event Enums
export {
CustomEventNames,
LangGraphEventTypes,
} from "copilotkit-langgraph-history";
// Utilities (advanced)
export {
transformMessages,
extractContent,
processStreamChunk,
} from "copilotkit-langgraph-history";# Required
LANGGRAPH_DEPLOYMENT_URL=https://your-deployment.langchain.com
# Optional (for authentication)
LANGSMITH_API_KEY=your-api-key- Ensure the thread exists in LangGraph
- Check that
deploymentUrlis correct - Verify
langsmithApiKeyhas access to the deployment
- Confirm
threadIdis being passed to<CopilotKit> - Check browser console for hydration errors
- Enable
debug: trueto see detailed logs
This is expected when the runner detects and fixes serverless state contamination. The client is automatically replaced with the correct URL.
Contributions are welcome! Please feel free to submit a Pull Request.
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
MIT License - see LICENSE for details.
Created by Daniel Frey.
Inspired by the need for thread history persistence in CopilotKit + LangGraph applications.