Open-source CopilotKit + LangGraph chat history persistence. Restore thread history on page refresh.
npm install test-history-agui


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
``bash`
npm install copilotkit-langgraph-historyor
pnpm add copilotkit-langgraph-historyor
yarn add copilotkit-langgraph-history
This package requires the following peer dependencies:
`bash`
npm install @copilotkit/runtime @copilotkitnext/runtime @ag-ui/core @langchain/langgraph-sdk rxjs
`typescript
// 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);
};
`
`tsx
import { CopilotKit } from "@copilotkit/react-core";
import { CopilotChat } from "@copilotkit/react-ui";
function App() {
return (
agent="my-agent"
threadId={threadId} // Pass your thread ID here
>
);
}
`
| 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 |client
| | HistoryClientInterface | undefined | Custom client for self-hosted servers |
\* deploymentUrl and graphId are required unless a custom client is provided.
| 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:
`typescript`
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:
1. Creating agents with frozen, immutable config
2. Verifying the internal client URL matches expected
3. Force-replacing the client if contamination is detected
Always use createIsolatedAgent instead of new LangGraphAgent() in serverless environments.
For self-hosted LangGraph servers (e.g., FastAPI with LangGraphAGUIAgent), you can provide a custom client that implements the HistoryClientInterface:
`typescript
import {
HistoryHydratingAgentRunner,
HistoryClientInterface,
} from "copilotkit-langgraph-history";
// Create a custom client for your FastAPI server
const customClient: HistoryClientInterface = {
threads: {
getHistory: async (threadId, options) => {
const res = await fetch(
${FASTAPI_URL}/threads/${threadId}/history?limit=${options?.limit ?? 100}${FASTAPI_URL}/threads/${threadId}/state
);
return res.json();
},
getState: async (threadId) => {
const res = await fetch();${FASTAPI_URL}/runs?thread_id=${threadId}
return res.json();
},
},
runs: {
list: async (threadId) => {
const res = await fetch();${FASTAPI_URL}/runs/${runId}/join?thread_id=${threadId}
return res.json();
},
joinStream: async function* (threadId, runId, options) {
// Implement SSE streaming to match your FastAPI endpoint
const res = await fetch(
,
{ headers: { Accept: "text/event-stream" } }
);
// Parse and yield stream chunks...
},
},
};
// Use the custom client
const runner = new HistoryHydratingAgentRunner({
agent,
client: customClient, // Custom client instead of deploymentUrl
historyLimit: 100,
});
`
Note: When using a custom client, the deploymentUrl, graphId, langsmithApiKey, and clientTimeoutMs options are ignored for history operations.
For a complete FastAPI implementation, see the companion Python package in the python/ directory, or install it:
`bash`
pip install copilotkit-langgraph-history
Then in your FastAPI server:
`python
from copilotkit_history import add_history_endpoints
add_history_endpoints(app, graph) # One line!
`
Enable debug logging to troubleshoot issues:
`typescript`
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:
1. Fetch History: Retrieves all checkpoints from LangGraph via client.threads.getHistory()MESSAGES_SNAPSHOT
2. Extract Messages: Processes checkpoints chronologically, deduplicating messages by ID
3. Transform Format: Converts LangGraph messages to CopilotKit format
4. Emit Events: Sends and STATE_SNAPSHOT events to frontend
5. 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
`typescript
// Core
export { HistoryHydratingAgentRunner } from "copilotkit-langgraph-history";
export { createIsolatedAgent } from "copilotkit-langgraph-history";
// Types
export type {
HistoryHydratingRunnerConfig,
StateExtractor,
CreateIsolatedAgentConfig,
LangGraphMessage,
ThreadState,
// Custom client interface (for self-hosted servers)
HistoryClientInterface,
HistoryRun,
HistoryStreamChunk,
JoinStreamOptions,
} 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";
`
Interface for custom client implementations. Implement this to connect to self-hosted servers:
`typescript`
interface HistoryClientInterface {
threads: {
getHistory(threadId: string, options?: { limit?: number }): Promise
getState(threadId: string): Promise
};
runs: {
list(threadId: string): Promise
joinStream(threadId: string, runId: string, options?: JoinStreamOptions): AsyncIterable
};
}
`envRequired
LANGGRAPH_DEPLOYMENT_URL=https://your-deployment.langchain.com
Troubleshooting
$3
- Ensure the thread exists in LangGraph
- Check that
deploymentUrl is correct
- Verify langsmithApiKey has access to the deployment$3
- Confirm
threadId is being passed to
- Check browser console for hydration errors
- Enable debug: true to see detailed logs$3
This is expected when the runner detects and fixes serverless state contamination. The client is automatically replaced with the correct URL.
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
1. Fork the repository
2. Create your feature branch (
git checkout -b feature/amazing-feature)
3. Commit your changes (git commit -m 'Add amazing feature')
4. Push to the branch (git push origin feature/amazing-feature`)MIT License - see LICENSE for details.
Created by Daniel Frey.
Inspired by the need for thread history persistence in CopilotKit + LangGraph applications.