Threads
For AI agents: see llms.txt for the complete documentation index. Markdown versions are available by adding .md to a page URL or requesting Accept: text/markdown.
Threads are a way to group messages together in a linear history. All messages saved in the Agent component are associated with a thread. When a message is generated based on a prompt, it saves the user message and generated agent message(s) automatically.
Threads can be associated with a user, and messages can each individually be associated with a user. By default, messages are associated with the thread's user.
Creating a thread
You can create a thread in a mutation or action. If you create it in an action,
it will also return a thread (see below) and you can start calling LLMs and
generating messages. If you specify a userId, the thread will be associated with
that user and messages will be saved to the user's history.
import { createThread } from "@convex-dev/agent";
const threadId = await createThread(ctx, components.agent);
You may also pass in metadata to set on the thread:
const userId = await getAuthUserId(ctx);
const threadId = await createThread(ctx, components.agent, {
userId,
title: "My thread",
summary: "This is a summary of the thread",
});
Metadata may be provided as context to the agent automatically in the future, but for now it's a convenience that helps organize threads in the Playground.
Generating a message in a thread
You can generate a message in a thread via the agent functions:
agent.generateText, agent.generateObject, agent.streamText, and
agent.streamObject. Any agent can generate a message in a thread created by
any other agent.
const agent = new Agent(components.agent, { languageModel, instructions });
export const generateReplyToPrompt = action({
args: { prompt: v.string(), threadId: v.string() },
handler: async (ctx, { prompt, threadId }) => {
// await authorizeThreadAccess(ctx, threadId);
const result = await agent.generateText(ctx, { threadId }, { prompt });
return result.text;
},
});
See Messages for more details on creating and saving messages.
Continuing a thread using the thread object from agent.continueThread
You can also continue a thread by creating an agent-specific thread object,
either when calling agent.createThread or agent.continueThread from within
an action. This allows calling methods without specifying those parameters each
time.
const { thread } = await agent.continueThread(ctx, { threadId });
const result = await thread.generateText({ prompt });
The thread from continueThread or createThread (available in actions only)
is a Thread object, which has convenience methods that are thread-specific:
thread.getMetadata()to get theuserId,title,summaryetc.thread.updateMetadata({ patch: { title, summary, userId} })to update the metadatathread.generateText({ prompt, ... })- equivalent toagent.generateText(ctx, { threadId }, { prompt, ... })thread.streamText({ prompt, ... })- equivalent toagent.streamText(ctx, { threadId }, { prompt, ... })thread.generateObject({ prompt, ... })- equivalent toagent.generateObject(ctx, { threadId }, { prompt, ... })thread.streamObject({ prompt, ... })- equivalent toagent.streamObject(ctx, { threadId }, { prompt, ... })
See Messages docs for more details on generating messages.
Deleting threads
You can delete threads by their threadId.
Asynchronously (from a mutation or action):
await agent.deleteThreadAsync(ctx, { threadId });
Synchronously in batches (from an action):
await agent.deleteThreadSync(ctx, { threadId });
You can also delete all threads by a user by their userId.
await agent.deleteThreadsByUserId(ctx, { userId });
Getting all threads owned by a user
const threads = await ctx.runQuery(
components.agent.threads.listThreadsByUserId,
{ userId, paginationOpts: args.paginationOpts },
);
Listing threads by recent activity
Threads carry no updatedAt. Keeping one on the thread would mean patching the
thread document on every message, which makes concurrent generations in one
thread contend on a single row. Keep the activity index in your app instead,
where the mutation that accepts a user message already runs:
// convex/schema.ts
threadActivity: defineTable({
userId: v.string(),
threadId: v.string(),
lastActivityAt: v.number(),
})
.index("by_threadId", ["threadId"])
.index("by_userId_lastActivityAt", ["userId", "lastActivityAt"]),
Upsert the row by by_threadId in the same mutation that saves the prompt,
delete it by the same index when the thread is deleted, and list with the
activity index:
const recent = await ctx.db
.query("threadActivity")
.withIndex("by_userId_lastActivityAt", (q) => q.eq("userId", userId))
.order("desc")
.paginate(paginationOpts);
One write per user turn, no component reads to list, and the ordering is whatever your app defines as activity.
Deleting all threads and messages associated with a user
Asynchronously (from a mutation or action):
await ctx.runMutation(components.agent.users.deleteAllForUserIdAsync, {
userId,
});
Synchronously (from an action):
await ctx.runMutation(components.agent.users.deleteAllForUserId, { userId });
Getting messages in a thread
See messages.mdx for more details.
import { listMessages } from "@convex-dev/agent";
const messages = await listMessages(ctx, components.agent, {
threadId,
excludeToolMessages: true,
paginationOpts: { cursor: null, numItems: 10 }, // null means start from the beginning
});
Or for the UIMessage type (easier for rendering UIs):
import { listUIMessages } from "@convex-dev/agent";
const messages = await listUIMessages(ctx, components.agent, {
threadId,
paginationOpts: { cursor: null, numItems: 10 },
});