Remember conversations in your app

Resume a conversation, render its history and extend the framework’s ActiveRecord models.

On this pageSource-backed Markdown

Continue a conversation

Pass the previous conversationId to the same agent with the same authenticated user and scope. The agent loads saved history before the next model turn and rejects a conversation belonging to a different user, scope or agent.

Treat conversation IDs as references, not permission. Apply the same ownership checks to your history and request-inspection routes. Direct text generation can append tracking rows to a conversation, but does not automatically replay its history; use an agent for that.

Resume from trusted route context
ts
const result = await new HelpAgent({
	user: userId, scope: teamId, conversationId, guidePath: 'help.md',
}).run('Which page should I open?');

Work with the saved models

AiConversation groups a conversation. AiMessage stores text, tool lifecycle events and provider reasoning summaries when enabled. AiRequest stores inputs, responses, status, timestamps, provider metadata, token counts and costs. Root and child requests preserve the relationship between an agent run and its provider work.

The generated app registers these models, plus AiRateLimitBucket and AiRateLimitReservation, in its migrations. Use ordinary ActiveRecord queries from your app. Raw prompts and tool results can contain private data: authorize access and choose a retention policy for your application.

Render saved history

After authorizing the conversation, load its messages in sequence order and its requests. agentConversationTimeline() returns the message, tool and reasoning-summary items used by the streaming UI contract. Pass your agent’s tool definitions to attach app-specific tool labels.

The formatter can include prompt-context items for debugging. Filter those out of ordinary user views. It formats records; it does not authorize access or provide a UI component.

After authorizing the conversation
ts
import { AiMessage, AiRequest, agentConversationTimeline } from '@db3.ai/app/ai';

const messages = await AiMessage.where({ conversation: conversationId }).orderBy('sequence').all();
const requests = await AiRequest.where({ conversation: conversationId }).all();
const timeline = agentConversationTimeline(messages, [], requests);

Add your own fields

Create a model in your app that extends the framework model. This example adds a support topic while retaining the existing table and fields.

Replace AiConversation with your subclass in the app’s model registry, configure ai.models.conversation, generate a migration, review it and apply it. The service uses your configured constructor for its reads and writes. Register one class per table. If you change table names, also override the related link fields.

server/models/Conversation.ts
ts
import { AiConversation } from '@db3.ai/app/ai';
import type { FieldBuilder } from '@db3.ai/app/db';

/** App-owned conversation fields appear in normal ActiveRecord queries and migrations. */
export class Conversation extends AiConversation {
	/** Keeps the framework fields and adds an application label. */
	static override fields(field: FieldBuilder) {
		return { ...super.fields(field), topic: field.string({ default: 'support', index: true }) };
	}

	declare topic: string | null;
}

Use your model throughout AI

The same models option accepts request and message subclasses. The default user and scope fields store app identifiers; subclasses may replace them with typed links to your app’s models.

In your existing App options
ts
ai: {
	...config.ai,
	models: { conversation: Conversation },
},