@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 +21 -0
- package/README.md +290 -0
- package/convex/conversations.ts +310 -0
- package/convex/convex.config.ts +22 -0
- package/convex/messages.ts +220 -0
- package/convex/schema.ts +126 -0
- package/convex/steps.ts +132 -0
- package/convex/validators.ts +161 -0
- package/convex/workflows.ts +248 -0
- package/convex/workingMemory.ts +159 -0
- package/dist/client/index.cjs +288 -0
- package/dist/client/index.cjs.map +1 -0
- package/dist/client/index.d.cts +566 -0
- package/dist/client/index.d.ts +566 -0
- package/dist/client/index.js +286 -0
- package/dist/client/index.js.map +1 -0
- package/dist/index.cjs +437 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +322 -0
- package/dist/index.d.ts +322 -0
- package/dist/index.js +435 -0
- package/dist/index.js.map +1 -0
- package/package.json +87 -0
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;
|