test-history-agui
v0.1.3
Published
Open-source CopilotKit + LangGraph chat history persistence. Restore thread history on page refresh.
Maintainers
Readme
copilotkit-langgraph-history
Open-source LangGraph thread history persistence for CopilotKit. Restore chat history on page refresh with your own LangGraph deployment.
The Problem
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.
The Solution
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
Installation
npm install copilotkit-langgraph-history
# or
pnpm add copilotkit-langgraph-history
# or
yarn add copilotkit-langgraph-historyPeer Dependencies
This package requires the following peer dependencies:
npm install @copilotkit/runtime @copilotkitnext/runtime @ag-ui/core @langchain/langgraph-sdk rxjsQuick Start
Next.js App Router
// 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);
};Frontend (React)
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>
);
}Configuration
HistoryHydratingAgentRunner Options
| 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.
createIsolatedAgent Options
| 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 |
Advanced Usage
Custom State Extraction
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,
}),
});Why createIsolatedAgent?
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.
Custom Client (Self-Hosted FastAPI)
For self-hosted LangGraph servers (e.g., FastAPI with LangGraphAGUIAgent), you can provide a custom client that implements the HistoryClientInterface:
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}`
);
return res.json();
},
getState: async (threadId) => {
const res = await fetch(`${FASTAPI_URL}/threads/${threadId}/state`);
return res.json();
},
},
runs: {
list: async (threadId) => {
const res = await fetch(`${FASTAPI_URL}/runs?thread_id=${threadId}`);
return res.json();
},
joinStream: async function* (threadId, runId, options) {
// Implement SSE streaming to match your FastAPI endpoint
const res = await fetch(
`${FASTAPI_URL}/runs/${runId}/join?thread_id=${threadId}`,
{ 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:
pip install copilotkit-langgraph-historyThen in your FastAPI server:
from copilotkit_history import add_history_endpoints
add_history_endpoints(app, graph) # One line!Debug Mode
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
How It Works
History Hydration Flow
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
Event Types Handled
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
API Reference
Exports
// 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";HistoryClientInterface
Interface for custom client implementations. Implement this to connect to self-hosted servers:
interface HistoryClientInterface {
threads: {
getHistory(threadId: string, options?: { limit?: number }): Promise<ThreadState[]>;
getState(threadId: string): Promise<ThreadState>;
};
runs: {
list(threadId: string): Promise<HistoryRun[]>;
joinStream(threadId: string, runId: string, options?: JoinStreamOptions): AsyncIterable<HistoryStreamChunk>;
};
}Environment Variables
# Required
LANGGRAPH_DEPLOYMENT_URL=https://your-deployment.langchain.com
# Optional (for authentication)
LANGSMITH_API_KEY=your-api-keyTroubleshooting
"No history found for thread"
- Ensure the thread exists in LangGraph
- Check that
deploymentUrlis correct - Verify
langsmithApiKeyhas access to the deployment
Messages not loading on refresh
- Confirm
threadIdis being passed to<CopilotKit> - Check browser console for hydration errors
- Enable
debug: trueto see detailed logs
"URL mismatch detected" warning
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.
- 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
License
MIT License - see LICENSE for details.
Credits
Created by Daniel Frey.
Inspired by the need for thread history persistence in CopilotKit + LangGraph applications.
