Chat Management
Create, update, list, and delete chats with metadata and usage tracking
A chat is the top-level container for a conversation. It holds messages, branches, and checkpoints in a graph-based store. Every ContextEngine instance is scoped to a single chat.
Creating a Chat
Chats are created automatically when ContextEngine first initializes (on save() or resolve()). You provide a chatId and userId:
import {
ContextEngine,
SqliteContextStore,
XmlRenderer,
role,
user,
assistant,
} from '@deepagents/context';
const store = new SqliteContextStore('./app.db');
const context = new ContextEngine({
store,
chatId: 'chat-001',
userId: 'user-001',
metadata: { source: 'web' },
});
context.set(role('You are a helpful assistant.'), user('Hello!'));
await context.save();The engine calls upsertChat() internally, so re-using the same chatId resumes the existing chat rather than creating a duplicate. The metadata option in the constructor is merged into the chat on first initialization.
Chat ID Strategies
const chatId = crypto.randomUUID();
const chatId = `user-${userId}-${Date.now()}`;
const chatId = `chat-${nextId++}`;Accessing Chat Metadata
The chat property returns a ChatMeta object after the engine has initialized (after any save() or resolve() call). Before initialization it returns null.
const context = new ContextEngine({ store, chatId: 'chat-001', userId: 'user-001' });
context.chat; // null (not yet initialized)
await context.save();
context.chat;
// {
// id: 'chat-001',
// userId: 'user-001',
// createdAt: 1703123456789,
// updatedAt: 1703123456789,
// title: undefined,
// metadata: undefined,
// }
context.chatId; // 'chat-001'ChatMeta
| Field | Type | Description |
|---|---|---|
id | string | Chat identifier |
userId | string | Owner of the chat |
createdAt | number | Unix timestamp (ms) |
updatedAt | number | Unix timestamp (ms), auto-updated |
title | string | undefined | User-provided title |
metadata | Record<string, unknown> | undefined | Custom key-value data |
Updating Chat Metadata
Use updateChat() to set a title or attach custom metadata:
await context.updateChat({
title: 'Help with TypeScript',
metadata: {
tags: ['coding', 'typescript'],
priority: 'high',
},
});
console.log(context.chat?.title); // 'Help with TypeScript'Metadata is merged with existing values, not replaced:
await context.updateChat({ metadata: { category: 'support' } });
await context.updateChat({ metadata: { resolved: true } });
console.log(context.chat?.metadata);
// { category: 'support', resolved: true }Automatic Title Generation
The chat() streaming function can automatically generate a title for new chats. When a chat has no title, the first user message is used to derive one through a two-phase approach.
Static Title
staticChatTitle() extracts the text content from a UIMessage and truncates it to 100 characters. This is applied immediately so the chat has a readable title without waiting for an LLM call.
import { staticChatTitle } from '@deepagents/context';
const title = staticChatTitle(userMessage);
// "help me write an essay about space exploration and..."
await context.updateChat({ title });AI-Generated Title
generateChatTitle() uses an LLM to produce a short 2-5 word title. It falls back to staticChatTitle() if the LLM call fails.
import { generateChatTitle } from '@deepagents/context';
const title = await generateChatTitle({
message: userMessage,
model: myModel,
abortSignal: controller.signal, // optional
});
// "Space Essay Help"GenerateChatTitleOptions
| Option | Type | Required | Description |
|---|---|---|---|
message | UIMessage | Yes | The user message to summarize |
model | AgentModel | Yes | The model used to generate the title |
abortSignal | AbortSignal | No | Signal to cancel the generation |
How chat() Uses Titles
When you call chat(), titles are handled automatically:
- If
generateTitle: trueand the agent has amodel, an LLM call generates a 2-5 word title. If the LLM call fails, it falls back tostaticChatTitle(). - If
generateTitleisfalse(the default) or the agent has no model,staticChatTitle()is applied immediately. - Once the title resolves, it is emitted as a
{ type: 'data-chat-title', data: title }event on the UI stream and persisted viacontext.updateChat().
import { chat } from '@deepagents/context';
const stream = await chat(agent, messages, {
generateTitle: true,
});The ChatAgentLike interface exposes an optional model property. If model is not present on the agent, only the static title is applied.
The generateTitle option is part of ChatOptions, which also accepts contextVariables, transform, abortSignal, onError, messageMetadata, and finalAssistantMetadata.
Listing Chats
Use the store's listChats() method directly. Results are sorted by updatedAt descending (most recent first).
const chats = await store.listChats();
for (const chat of chats) {
console.log(`${chat.id}: ${chat.title ?? 'Untitled'}`);
console.log(` Messages: ${chat.messageCount}, Branches: ${chat.branchCount}`);
console.log(` Updated: ${new Date(chat.updatedAt).toLocaleString()}`);
}Filtering and Pagination
const chats = await store.listChats({
userId: 'user-001',
limit: 20,
offset: 0,
});Filter by a top-level metadata field with exact match:
const archived = await store.listChats({
userId: 'user-001',
metadata: { key: 'archived', value: true },
});ListChatsOptions
| Option | Type | Description |
|---|---|---|
userId | string | Filter by user |
metadata | { key: string; value: string | number | boolean } | Exact match on a top-level metadata field |
limit | number | Max results |
offset | number | Skip N results (pagination) |
ChatInfo
Each item returned from listChats() is a ChatInfo:
| Field | Type | Description |
|---|---|---|
id | string | Chat identifier |
userId | string | Owner |
title | string | undefined | Title |
metadata | Record<string, unknown> | undefined | Custom data |
messageCount | number | Total messages in the chat |
branchCount | number | Total branches |
createdAt | number | Unix timestamp (ms) |
updatedAt | number | Unix timestamp (ms) |
Deleting a Chat
Delete a chat and all associated data (messages, branches, checkpoints):
const deleted = await store.deleteChat('chat-001');
// true if deleted, false if not foundScope deletion to a specific user to prevent unauthorized deletes:
const deleted = await store.deleteChat('chat-001', { userId: 'user-001' });
// false if chat belongs to a different userUsage Tracking
Track cumulative token usage across the lifetime of a chat with trackUsage(). It accumulates LanguageModelUsage from the AI SDK into chat.metadata.usage:
import { generateText } from 'ai';
import { groq } from '@ai-sdk/groq';
const context = new ContextEngine({ store, chatId: 'chat-001', userId: 'user-001' });
context.set(role('You are helpful.'), user('What is TypeScript?'));
const { systemPrompt, messages } = await context.resolve({ renderer: new XmlRenderer() });
const result = await generateText({
model: groq('gpt-oss-20b'),
system: systemPrompt,
messages,
});
context.set(assistant(result.text));
await context.save();
await context.trackUsage(await result.usage);
console.log(context.chat?.metadata?.usage);
// { inputTokens: 45, outputTokens: 120, totalTokens: 165 }Each call to trackUsage() adds to the running total. Concurrent calls are safe -- the method reads fresh data from the store before accumulating.
Multi-User Chat Patterns
Shared Store, User-Scoped Queries
const store = new SqliteContextStore('./app.db');
function getUserChats(userId: string) {
return store.listChats({ userId, limit: 50 });
}
function createChat(userId: string) {
return new ContextEngine({
store,
chatId: crypto.randomUUID(),
userId,
});
}Safe Deletion
async function deleteUserChat(chatId: string, userId: string) {
const deleted = await store.deleteChat(chatId, { userId });
if (!deleted) {
throw new Error('Chat not found or not owned by this user');
}
}Complete Example
import { generateText } from 'ai';
import { groq } from '@ai-sdk/groq';
import {
ContextEngine,
SqliteContextStore,
XmlRenderer,
role,
user,
assistant,
} from '@deepagents/context';
const store = new SqliteContextStore('./chats.db');
async function sendMessage(chatId: string, userId: string, text: string) {
const context = new ContextEngine({ store, chatId, userId })
.set(role('You are a helpful assistant.'));
context.set(user(text));
const { systemPrompt, messages } = await context.resolve({
renderer: new XmlRenderer(),
});
const result = await generateText({
model: groq('gpt-oss-20b'),
system: systemPrompt,
messages,
});
context.set(assistant(result.text));
await context.save();
if (!context.chat?.title) {
await context.updateChat({ title: text.slice(0, 100) });
}
await context.trackUsage(await result.usage);
return result.text;
}
async function listUserChats(userId: string) {
return store.listChats({ userId, limit: 50 });
}
async function deleteChat(chatId: string, userId: string) {
return store.deleteChat(chatId, { userId });
}When using the chat() streaming function instead of generateText(), title generation is handled automatically -- pass generateTitle: true to enable AI-powered titles.
Next Steps
- chat() - Streaming orchestration with automatic persistence, titles, and usage tracking
- Context Engine - Full API reference
- Checkpoints - Named restore points
- Branching - Explore multiple conversation paths
- Storage - Store implementations and schema