@o-zakstam/voltagent-convex 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 VoltAgent
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,290 @@
1
+ # @o-zakstam/voltagent-convex
2
+
3
+ Convex Storage Adapter for [VoltAgent](https://voltagent.dev) - A StorageAdapter implementation that persists conversation history, working memory, and workflow state to a [Convex](https://convex.dev) database using Convex Components.
4
+
5
+ ## Features
6
+
7
+ - Full `StorageAdapter` interface implementation
8
+ - Convex Component architecture for clean integration
9
+ - Conversation and message persistence
10
+ - Working memory support (conversation and user scopes)
11
+ - Workflow state management for suspendable workflows
12
+ - Conversation steps for observability
13
+ - Real-time updates with Convex subscriptions
14
+ - TypeScript support with full type definitions
15
+
16
+ ## Installation
17
+
18
+ ```bash
19
+ npm install @o-zakstam/voltagent-convex convex
20
+ # or
21
+ pnpm add @o-zakstam/voltagent-convex convex
22
+ # or
23
+ yarn add @o-zakstam/voltagent-convex convex
24
+ ```
25
+
26
+ ## Quick Start
27
+
28
+ ### 1. Install the VoltAgent Component
29
+
30
+ In your `convex/convex.config.ts`, import and install the VoltAgent component:
31
+
32
+ ```typescript
33
+ import { defineApp } from "convex/server";
34
+ import voltagent from "@o-zakstam/voltagent-convex/convex.config";
35
+
36
+ const app = defineApp();
37
+ app.use(voltagent);
38
+
39
+ export default app;
40
+ ```
41
+
42
+ ### 2. Create the API Wrapper Functions
43
+
44
+ Create a `convex/voltagent.ts` file that generates the public API functions:
45
+
46
+ ```typescript
47
+ import { components } from "./_generated/api";
48
+ import { defineVoltAgentAPI } from "@o-zakstam/voltagent-convex/api";
49
+
50
+ export const {
51
+ createConversation,
52
+ getConversation,
53
+ getConversations,
54
+ getConversationsByUserId,
55
+ queryConversations,
56
+ updateConversation,
57
+ deleteConversation,
58
+ addMessage,
59
+ addMessages,
60
+ getMessages,
61
+ clearMessages,
62
+ saveConversationSteps,
63
+ getConversationSteps,
64
+ getWorkingMemory,
65
+ setWorkingMemory,
66
+ deleteWorkingMemory,
67
+ getWorkflowState,
68
+ queryWorkflowRuns,
69
+ setWorkflowState,
70
+ updateWorkflowState,
71
+ getSuspendedWorkflowStates,
72
+ } = defineVoltAgentAPI(components.voltagent);
73
+ ```
74
+
75
+ ### 3. Run Convex code generation
76
+
77
+ After updating your config and creating the wrapper file, run:
78
+
79
+ ```bash
80
+ npx convex dev
81
+ ```
82
+
83
+ This will generate the component types in your `convex/_generated/` directory.
84
+
85
+ ### 4. Use the adapter in your VoltAgent
86
+
87
+ ```typescript
88
+ import { Agent, Memory, VoltAgent } from "@voltagent/core";
89
+ import { ConvexHttpClient } from "convex/browser";
90
+ import { ConvexMemoryAdapter } from "@o-zakstam/voltagent-convex";
91
+ import { api } from "./convex/_generated/api";
92
+ import { openai } from "@ai-sdk/openai";
93
+
94
+ // Create a Convex client
95
+ const convexClient = new ConvexHttpClient(process.env.CONVEX_URL!);
96
+
97
+ // Create the memory adapter
98
+ const memory = new Memory({
99
+ storage: new ConvexMemoryAdapter({
100
+ client: convexClient,
101
+ api: api.voltagent,
102
+ debug: process.env.NODE_ENV === "development",
103
+ }),
104
+ });
105
+
106
+ // Create your agent with Convex-backed memory
107
+ const agent = new Agent({
108
+ name: "Assistant",
109
+ instructions: "A helpful assistant that remembers conversations.",
110
+ model: openai("gpt-4o-mini"),
111
+ memory,
112
+ });
113
+
114
+ // Run VoltAgent
115
+ new VoltAgent({
116
+ agents: { agent },
117
+ });
118
+ ```
119
+
120
+ ## Configuration Options
121
+
122
+ ```typescript
123
+ interface ConvexMemoryAdapterOptions {
124
+ /**
125
+ * The Convex client instance.
126
+ * Can be ConvexHttpClient or ConvexReactClient.
127
+ */
128
+ client: ConvexClient;
129
+
130
+ /**
131
+ * The VoltAgent API from your Convex generated code.
132
+ * Import from "./convex/_generated/api" and use api.voltagent
133
+ */
134
+ api: VoltAgentApi;
135
+
136
+ /**
137
+ * Enable debug logging (default: false)
138
+ */
139
+ debug?: boolean;
140
+
141
+ /**
142
+ * Custom logger instance
143
+ */
144
+ logger?: Logger;
145
+ }
146
+ ```
147
+
148
+ ## Component Architecture
149
+
150
+ This package uses [Convex Components](https://docs.convex.dev/components) to provide a clean, isolated integration. The component:
151
+
152
+ - Creates its own isolated set of tables
153
+ - Manages all schema and functions internally
154
+ - Doesn't conflict with your existing Convex tables
155
+ - Updates automatically when you update the package
156
+
157
+ ## Table Structure
158
+
159
+ The component creates the following tables (isolated in the component namespace):
160
+
161
+ | Table | Description |
162
+ |-------|-------------|
163
+ | `conversations` | Stores conversation metadata |
164
+ | `messages` | Stores individual messages |
165
+ | `users` | Stores user-level working memory |
166
+ | `workflowStates` | Stores workflow execution state |
167
+ | `conversationSteps` | Stores detailed steps for observability |
168
+
169
+ ## Working Memory
170
+
171
+ The adapter supports both conversation-scoped and user-scoped working memory:
172
+
173
+ ```typescript
174
+ const memory = new Memory({
175
+ storage: new ConvexMemoryAdapter({ client, api: api.voltagent }),
176
+ workingMemory: {
177
+ enabled: true,
178
+ scope: "conversation", // or "user"
179
+ },
180
+ });
181
+ ```
182
+
183
+ ## Workflow State
184
+
185
+ For suspendable workflows, the adapter persists workflow state including:
186
+
187
+ - Workflow execution status
188
+ - Suspension checkpoints
189
+ - Workflow events for visualization
190
+ - Output and cancellation data
191
+
192
+ ## Using with React
193
+
194
+ With Convex React, you can use the `ConvexReactClient`:
195
+
196
+ ```typescript
197
+ import { useConvex } from "convex/react";
198
+ import { ConvexMemoryAdapter } from "@o-zakstam/voltagent-convex";
199
+ import { api } from "./convex/_generated/api";
200
+
201
+ function useMemoryAdapter() {
202
+ const convex = useConvex();
203
+
204
+ return new ConvexMemoryAdapter({
205
+ client: convex,
206
+ api: api.voltagent,
207
+ });
208
+ }
209
+ ```
210
+
211
+ ## Known Limitations
212
+
213
+ ### OperationContext Not Supported
214
+
215
+ The `OperationContext` parameter is accepted by all methods for interface compatibility but is currently not used. This means the following features from the VoltAgent interface are not implemented:
216
+
217
+ - **Multi-tenancy isolation**: Context-based tenant separation is not applied
218
+ - **Audit logging**: Operation context is not logged or stored
219
+ - **Access control**: Context-based permissions are not enforced
220
+
221
+ If you need these features, you can extend `ConvexMemoryAdapter` and override the relevant methods to implement custom context handling:
222
+
223
+ ```typescript
224
+ import { ConvexMemoryAdapter } from "@o-zakstam/voltagent-convex";
225
+ import type { OperationContext } from "@voltagent/core";
226
+
227
+ class CustomConvexMemoryAdapter extends ConvexMemoryAdapter {
228
+ async addMessage(
229
+ message: UIMessage,
230
+ userId: string,
231
+ conversationId: string,
232
+ context?: OperationContext,
233
+ ): Promise<void> {
234
+ // Custom context handling
235
+ if (context) {
236
+ const tenantId = context.context.get("tenantId");
237
+ // Apply tenant isolation logic
238
+ }
239
+ return super.addMessage(message, userId, conversationId, context);
240
+ }
241
+ }
242
+ ```
243
+
244
+ ## API Reference
245
+
246
+ ### ConvexMemoryAdapter
247
+
248
+ Implements the `StorageAdapter` interface from `@voltagent/core`:
249
+
250
+ #### Message Operations
251
+ - `addMessage(message, userId, conversationId)` - Add a single message
252
+ - `addMessages(messages, userId, conversationId)` - Add multiple messages
253
+ - `getMessages(userId, conversationId, options?)` - Get messages with filtering
254
+ - `clearMessages(userId, conversationId?)` - Clear messages
255
+
256
+ #### Conversation Operations
257
+ - `createConversation(input)` - Create a new conversation
258
+ - `getConversation(id)` - Get a conversation by ID
259
+ - `getConversations(resourceId)` - Get conversations by resource
260
+ - `getConversationsByUserId(userId, options?)` - Get user's conversations
261
+ - `queryConversations(options)` - Query with filters
262
+ - `updateConversation(id, updates)` - Update a conversation
263
+ - `deleteConversation(id)` - Delete a conversation
264
+
265
+ #### Working Memory Operations
266
+ - `getWorkingMemory(params)` - Get working memory content
267
+ - `setWorkingMemory(params)` - Set working memory content
268
+ - `deleteWorkingMemory(params)` - Delete working memory
269
+
270
+ #### Workflow State Operations
271
+ - `getWorkflowState(executionId)` - Get workflow state
272
+ - `queryWorkflowRuns(query)` - Query workflow runs
273
+ - `setWorkflowState(executionId, state)` - Set workflow state
274
+ - `updateWorkflowState(executionId, updates)` - Update workflow state
275
+ - `getSuspendedWorkflowStates(workflowId)` - Get suspended workflows
276
+
277
+ #### Conversation Steps Operations
278
+ - `saveConversationSteps(steps)` - Save conversation steps
279
+ - `getConversationSteps(userId, conversationId, options?)` - Get steps
280
+
281
+ ## License
282
+
283
+ MIT
284
+
285
+ ## Links
286
+
287
+ - [VoltAgent Documentation](https://voltagent.dev/docs/)
288
+ - [Convex Documentation](https://docs.convex.dev/)
289
+ - [Convex Components Documentation](https://docs.convex.dev/components)
290
+ - [GitHub Repository](https://github.com/zakstam/voltagent-convex)
@@ -0,0 +1,310 @@
1
+ /**
2
+ * Conversation-related Convex functions
3
+ */
4
+ import { mutation, query } from "./_generated/server";
5
+ import { v } from "convex/values";
6
+ import { vMetadata } from "./validators";
7
+
8
+ /**
9
+ * Create a new conversation
10
+ */
11
+ export const create = mutation({
12
+ args: {
13
+ id: v.string(),
14
+ resourceId: v.string(),
15
+ userId: v.string(),
16
+ title: v.string(),
17
+ metadata: vMetadata,
18
+ },
19
+ handler: async (ctx, args) => {
20
+ const existing = await ctx.db
21
+ .query("conversations")
22
+ .withIndex("by_visible_id", (q) => q.eq("visibleId", args.id))
23
+ .first();
24
+
25
+ if (existing) {
26
+ throw new Error("Conversation already exists");
27
+ }
28
+
29
+ const now = new Date().toISOString();
30
+ const id = await ctx.db.insert("conversations", {
31
+ visibleId: args.id,
32
+ resourceId: args.resourceId,
33
+ userId: args.userId,
34
+ title: args.title,
35
+ metadata: args.metadata || {},
36
+ createdAt: now,
37
+ updatedAt: now,
38
+ });
39
+
40
+ return {
41
+ _id: id,
42
+ id: args.id,
43
+ resourceId: args.resourceId,
44
+ userId: args.userId,
45
+ title: args.title,
46
+ metadata: args.metadata || {},
47
+ createdAt: now,
48
+ updatedAt: now,
49
+ };
50
+ },
51
+ });
52
+
53
+ /**
54
+ * Get a conversation by visible ID
55
+ */
56
+ export const get = query({
57
+ args: { id: v.string() },
58
+ handler: async (ctx, args) => {
59
+ const conversation = await ctx.db
60
+ .query("conversations")
61
+ .withIndex("by_visible_id", (q) => q.eq("visibleId", args.id))
62
+ .first();
63
+
64
+ if (!conversation) {
65
+ return null;
66
+ }
67
+
68
+ return {
69
+ id: conversation.visibleId,
70
+ resourceId: conversation.resourceId,
71
+ userId: conversation.userId,
72
+ title: conversation.title,
73
+ metadata: conversation.metadata,
74
+ createdAt: conversation.createdAt,
75
+ updatedAt: conversation.updatedAt,
76
+ };
77
+ },
78
+ });
79
+
80
+ /**
81
+ * Get conversations by resource ID
82
+ */
83
+ export const getByResourceId = query({
84
+ args: { resourceId: v.string() },
85
+ handler: async (ctx, args) => {
86
+ const conversations = await ctx.db
87
+ .query("conversations")
88
+ .withIndex("by_resource_id", (q) => q.eq("resourceId", args.resourceId))
89
+ .collect();
90
+
91
+ return conversations.map((c) => ({
92
+ id: c.visibleId,
93
+ resourceId: c.resourceId,
94
+ userId: c.userId,
95
+ title: c.title,
96
+ metadata: c.metadata,
97
+ createdAt: c.createdAt,
98
+ updatedAt: c.updatedAt,
99
+ }));
100
+ },
101
+ });
102
+
103
+ /**
104
+ * Get conversations by user ID with pagination and ordering
105
+ */
106
+ // Map API field names to Convex schema field names
107
+ const orderByFieldMap: Record<string, string> = {
108
+ created_at: "createdAt",
109
+ updated_at: "updatedAt",
110
+ title: "title",
111
+ // Also support camelCase for flexibility
112
+ createdAt: "createdAt",
113
+ updatedAt: "updatedAt",
114
+ };
115
+
116
+ export const getByUserId = query({
117
+ args: {
118
+ userId: v.string(),
119
+ limit: v.optional(v.number()),
120
+ offset: v.optional(v.number()),
121
+ orderBy: v.optional(v.string()),
122
+ orderDirection: v.optional(v.string()),
123
+ },
124
+ handler: async (ctx, args) => {
125
+ let conversations = await ctx.db
126
+ .query("conversations")
127
+ .withIndex("by_user_id", (q) => q.eq("userId", args.userId))
128
+ .collect();
129
+
130
+ // Map the orderBy field and default to createdAt DESC per VoltAgent spec
131
+ const sortField = orderByFieldMap[args.orderBy || ""] || "createdAt";
132
+ const orderDir = args.orderDirection === "ASC" ? 1 : -1;
133
+
134
+ conversations.sort((a, b) => {
135
+ const aVal = a[sortField as keyof typeof a] as string;
136
+ const bVal = b[sortField as keyof typeof b] as string;
137
+ return orderDir * aVal.localeCompare(bVal);
138
+ });
139
+
140
+ const offset = args.offset || 0;
141
+ const limit = args.limit || 50;
142
+ conversations = conversations.slice(offset, offset + limit);
143
+
144
+ return conversations.map((c) => ({
145
+ id: c.visibleId,
146
+ resourceId: c.resourceId,
147
+ userId: c.userId,
148
+ title: c.title,
149
+ metadata: c.metadata,
150
+ createdAt: c.createdAt,
151
+ updatedAt: c.updatedAt,
152
+ }));
153
+ },
154
+ });
155
+
156
+ /**
157
+ * Query conversations with filters
158
+ * Uses indexed queries to avoid loading all data into memory
159
+ */
160
+ export const queryConversations = query({
161
+ args: {
162
+ userId: v.optional(v.string()),
163
+ resourceId: v.optional(v.string()),
164
+ limit: v.optional(v.number()),
165
+ offset: v.optional(v.number()),
166
+ orderBy: v.optional(v.string()),
167
+ orderDirection: v.optional(v.string()),
168
+ },
169
+ handler: async (ctx, args) => {
170
+ // Use indexed queries based on available filters to avoid full table scans
171
+ let conversations;
172
+
173
+ if (args.userId) {
174
+ // Use user_id index - most common query pattern
175
+ conversations = await ctx.db
176
+ .query("conversations")
177
+ .withIndex("by_user_id", (q) => q.eq("userId", args.userId!))
178
+ .collect();
179
+
180
+ // Apply secondary filter if needed
181
+ if (args.resourceId) {
182
+ conversations = conversations.filter(
183
+ (c) => c.resourceId === args.resourceId
184
+ );
185
+ }
186
+ } else if (args.resourceId) {
187
+ // Use resource_id index
188
+ conversations = await ctx.db
189
+ .query("conversations")
190
+ .withIndex("by_resource_id", (q) => q.eq("resourceId", args.resourceId!))
191
+ .collect();
192
+ } else {
193
+ // No filters - apply a reasonable limit to prevent memory issues
194
+ // Return most recent conversations by default
195
+ const maxUnfilteredResults = 1000;
196
+ conversations = await ctx.db
197
+ .query("conversations")
198
+ .order("desc")
199
+ .take(maxUnfilteredResults);
200
+ }
201
+
202
+ // Map the orderBy field and default to createdAt DESC per VoltAgent spec
203
+ const sortField = orderByFieldMap[args.orderBy || ""] || "createdAt";
204
+ const orderDir = args.orderDirection === "ASC" ? 1 : -1;
205
+
206
+ conversations.sort((a, b) => {
207
+ const aVal = a[sortField as keyof typeof a] as string;
208
+ const bVal = b[sortField as keyof typeof b] as string;
209
+ return orderDir * aVal.localeCompare(bVal);
210
+ });
211
+
212
+ const offset = args.offset || 0;
213
+ const limit = args.limit || 50;
214
+ conversations = conversations.slice(offset, offset + limit);
215
+
216
+ return conversations.map((c) => ({
217
+ id: c.visibleId,
218
+ resourceId: c.resourceId,
219
+ userId: c.userId,
220
+ title: c.title,
221
+ metadata: c.metadata,
222
+ createdAt: c.createdAt,
223
+ updatedAt: c.updatedAt,
224
+ }));
225
+ },
226
+ });
227
+
228
+ /**
229
+ * Update a conversation
230
+ */
231
+ export const update = mutation({
232
+ args: {
233
+ id: v.string(),
234
+ title: v.optional(v.string()),
235
+ resourceId: v.optional(v.string()),
236
+ metadata: v.optional(vMetadata),
237
+ },
238
+ handler: async (ctx, args) => {
239
+ const conversation = await ctx.db
240
+ .query("conversations")
241
+ .withIndex("by_visible_id", (q) => q.eq("visibleId", args.id))
242
+ .first();
243
+
244
+ if (!conversation) {
245
+ throw new Error("Conversation not found");
246
+ }
247
+
248
+ const updates: Record<string, unknown> = {
249
+ updatedAt: new Date().toISOString(),
250
+ };
251
+
252
+ if (args.title !== undefined) updates.title = args.title;
253
+ if (args.resourceId !== undefined) updates.resourceId = args.resourceId;
254
+ if (args.metadata !== undefined) updates.metadata = args.metadata;
255
+
256
+ await ctx.db.patch(conversation._id, updates);
257
+
258
+ const updated = await ctx.db.get(conversation._id);
259
+ return {
260
+ id: updated!.visibleId,
261
+ resourceId: updated!.resourceId,
262
+ userId: updated!.userId,
263
+ title: updated!.title,
264
+ metadata: updated!.metadata,
265
+ createdAt: updated!.createdAt,
266
+ updatedAt: updated!.updatedAt,
267
+ };
268
+ },
269
+ });
270
+
271
+ /**
272
+ * Delete a conversation and all associated data
273
+ */
274
+ export const remove = mutation({
275
+ args: { id: v.string() },
276
+ handler: async (ctx, args) => {
277
+ const conversation = await ctx.db
278
+ .query("conversations")
279
+ .withIndex("by_visible_id", (q) => q.eq("visibleId", args.id))
280
+ .first();
281
+
282
+ if (!conversation) {
283
+ throw new Error("Conversation not found");
284
+ }
285
+
286
+ // Delete associated messages
287
+ const messages = await ctx.db
288
+ .query("messages")
289
+ .withIndex("by_conversation_id", (q) => q.eq("conversationId", args.id))
290
+ .collect();
291
+
292
+ for (const message of messages) {
293
+ await ctx.db.delete(message._id);
294
+ }
295
+
296
+ // Delete associated steps
297
+ const steps = await ctx.db
298
+ .query("conversationSteps")
299
+ .withIndex("by_conversation_id", (q) => q.eq("conversationId", args.id))
300
+ .collect();
301
+
302
+ for (const step of steps) {
303
+ await ctx.db.delete(step._id);
304
+ }
305
+
306
+ await ctx.db.delete(conversation._id);
307
+
308
+ return { success: true };
309
+ },
310
+ });
@@ -0,0 +1,22 @@
1
+ /**
2
+ * VoltAgent Convex Component Configuration
3
+ *
4
+ * This file defines the VoltAgent component for Convex.
5
+ * When users install this component in their Convex project,
6
+ * they get isolated tables and functions for VoltAgent memory storage.
7
+ */
8
+ import { defineComponent } from "convex/server";
9
+
10
+ /**
11
+ * VoltAgent component definition.
12
+ *
13
+ * This component provides:
14
+ * - Conversation storage with metadata
15
+ * - Message persistence with UIMessage format
16
+ * - Working memory (conversation and user scoped)
17
+ * - Workflow state management for suspendable workflows
18
+ * - Conversation steps for observability
19
+ */
20
+ const component = defineComponent("voltagent");
21
+
22
+ export default component;