@agentforge-ai/cli 0.4.2 → 0.5.0
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/dist/default/README.md +81 -81
- package/dist/default/convex/agents.ts +204 -0
- package/dist/default/convex/apiKeys.ts +133 -0
- package/dist/default/convex/cronJobs.ts +224 -0
- package/dist/default/convex/files.ts +103 -0
- package/dist/default/convex/folders.ts +110 -0
- package/dist/default/convex/heartbeat.ts +371 -0
- package/dist/default/convex/logs.ts +66 -0
- package/dist/default/convex/mastraIntegration.ts +184 -0
- package/dist/default/convex/mcpConnections.ts +127 -0
- package/dist/default/convex/messages.ts +90 -0
- package/dist/default/convex/projects.ts +114 -0
- package/dist/default/convex/sessions.ts +174 -0
- package/dist/default/convex/settings.ts +79 -0
- package/dist/default/convex/skills.ts +178 -0
- package/dist/default/convex/threads.ts +100 -0
- package/dist/default/convex/usage.ts +195 -0
- package/dist/default/convex/vault.ts +383 -0
- package/dist/default/dashboard/app/main.tsx +7 -3
- package/dist/default/dashboard/app/routes/agents.tsx +103 -161
- package/dist/default/dashboard/app/routes/chat.tsx +163 -317
- package/dist/default/dashboard/app/routes/connections.tsx +247 -386
- package/dist/default/dashboard/app/routes/cron.tsx +127 -286
- package/dist/default/dashboard/app/routes/files.tsx +184 -167
- package/dist/default/dashboard/app/routes/index.tsx +63 -96
- package/dist/default/dashboard/app/routes/projects.tsx +106 -225
- package/dist/default/dashboard/app/routes/sessions.tsx +87 -253
- package/dist/default/dashboard/app/routes/settings.tsx +316 -532
- package/dist/default/dashboard/app/routes/skills.tsx +329 -216
- package/dist/default/dashboard/app/routes/usage.tsx +107 -150
- package/dist/default/dashboard/tsconfig.json +3 -2
- package/dist/default/dashboard/vite.config.ts +6 -0
- package/dist/index.js +279 -50
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/templates/default/README.md +81 -81
- package/templates/default/convex/agents.ts +204 -0
- package/templates/default/convex/apiKeys.ts +133 -0
- package/templates/default/convex/cronJobs.ts +224 -0
- package/templates/default/convex/files.ts +103 -0
- package/templates/default/convex/folders.ts +110 -0
- package/templates/default/convex/heartbeat.ts +371 -0
- package/templates/default/convex/logs.ts +66 -0
- package/templates/default/convex/mastraIntegration.ts +184 -0
- package/templates/default/convex/mcpConnections.ts +127 -0
- package/templates/default/convex/messages.ts +90 -0
- package/templates/default/convex/projects.ts +114 -0
- package/templates/default/convex/sessions.ts +174 -0
- package/templates/default/convex/settings.ts +79 -0
- package/templates/default/convex/skills.ts +178 -0
- package/templates/default/convex/threads.ts +100 -0
- package/templates/default/convex/usage.ts +195 -0
- package/templates/default/convex/vault.ts +383 -0
- package/templates/default/dashboard/app/main.tsx +7 -3
- package/templates/default/dashboard/app/routes/agents.tsx +103 -161
- package/templates/default/dashboard/app/routes/chat.tsx +163 -317
- package/templates/default/dashboard/app/routes/connections.tsx +247 -386
- package/templates/default/dashboard/app/routes/cron.tsx +127 -286
- package/templates/default/dashboard/app/routes/files.tsx +184 -167
- package/templates/default/dashboard/app/routes/index.tsx +63 -96
- package/templates/default/dashboard/app/routes/projects.tsx +106 -225
- package/templates/default/dashboard/app/routes/sessions.tsx +87 -253
- package/templates/default/dashboard/app/routes/settings.tsx +316 -532
- package/templates/default/dashboard/app/routes/skills.tsx +329 -216
- package/templates/default/dashboard/app/routes/usage.tsx +107 -150
- package/templates/default/dashboard/tsconfig.json +3 -2
- package/templates/default/dashboard/vite.config.ts +6 -0
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
import { v } from "convex/values";
|
|
2
|
+
import { mutation, query, action } from "./_generated/server";
|
|
3
|
+
import { api } from "./_generated/api";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* HEARTBEAT System
|
|
7
|
+
*
|
|
8
|
+
* Similar to OpenClaw's HEARTBEAT.md, this system allows agents to:
|
|
9
|
+
* 1. Check on ongoing conversations
|
|
10
|
+
* 2. Continue unfinished tasks
|
|
11
|
+
* 3. Perform scheduled tasks
|
|
12
|
+
* 4. Maintain context across sessions
|
|
13
|
+
*
|
|
14
|
+
* The heartbeat table stores the current state of each agent's awareness
|
|
15
|
+
* of ongoing tasks, pending actions, and conversation context.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
// Add heartbeat table to schema (this will need to be added to schema.ts)
|
|
19
|
+
// heartbeats: defineTable({
|
|
20
|
+
// agentId: v.string(),
|
|
21
|
+
// threadId: v.optional(v.id("threads")),
|
|
22
|
+
// status: v.string(), // "active", "waiting", "completed", "error"
|
|
23
|
+
// currentTask: v.optional(v.string()),
|
|
24
|
+
// pendingTasks: v.array(v.string()),
|
|
25
|
+
// context: v.string(), // Markdown-formatted context
|
|
26
|
+
// lastCheck: v.number(),
|
|
27
|
+
// nextCheck: v.number(),
|
|
28
|
+
// metadata: v.optional(v.any()),
|
|
29
|
+
// })
|
|
30
|
+
|
|
31
|
+
// Query: Get heartbeat for an agent
|
|
32
|
+
export const get = query({
|
|
33
|
+
args: {
|
|
34
|
+
agentId: v.string(),
|
|
35
|
+
threadId: v.optional(v.id("threads")),
|
|
36
|
+
},
|
|
37
|
+
handler: async (ctx, args) => {
|
|
38
|
+
const heartbeats = await ctx.db
|
|
39
|
+
.query("heartbeats")
|
|
40
|
+
.withIndex("byAgentId", (q) => q.eq("agentId", args.agentId))
|
|
41
|
+
.take(100).collect();
|
|
42
|
+
|
|
43
|
+
if (args.threadId) {
|
|
44
|
+
return heartbeats.find((h) => h.threadId === args.threadId);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return heartbeats[0]; // Return the most recent one
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// Query: Get all active heartbeats
|
|
52
|
+
export const listActive = query({
|
|
53
|
+
args: {
|
|
54
|
+
userId: v.optional(v.string()),
|
|
55
|
+
},
|
|
56
|
+
handler: async (ctx, args) => {
|
|
57
|
+
const heartbeats = await ctx.db
|
|
58
|
+
.query("heartbeats")
|
|
59
|
+
.withIndex("byStatus", (q) => q.eq("status", "active"))
|
|
60
|
+
.take(100).collect();
|
|
61
|
+
|
|
62
|
+
if (args.userId) {
|
|
63
|
+
// Filter by userId if provided
|
|
64
|
+
const userAgents = await ctx.db
|
|
65
|
+
.query("agents")
|
|
66
|
+
.withIndex("byUserId", (q) => q.eq("userId", args.userId))
|
|
67
|
+
.take(100).collect();
|
|
68
|
+
|
|
69
|
+
const userAgentIds = new Set(userAgents.map((a) => a.id));
|
|
70
|
+
return heartbeats.filter((h) => userAgentIds.has(h.agentId));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return heartbeats;
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// Query: Get heartbeats due for check
|
|
78
|
+
export const getDueForCheck = query({
|
|
79
|
+
args: {},
|
|
80
|
+
handler: async (ctx) => {
|
|
81
|
+
const now = Date.now();
|
|
82
|
+
const heartbeats = await ctx.db
|
|
83
|
+
.query("heartbeats")
|
|
84
|
+
.withIndex("byNextCheck")
|
|
85
|
+
.take(100).collect();
|
|
86
|
+
|
|
87
|
+
return heartbeats.filter((h) => h.nextCheck <= now && h.status === "active");
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// Mutation: Create or update heartbeat
|
|
92
|
+
export const upsert = mutation({
|
|
93
|
+
args: {
|
|
94
|
+
agentId: v.string(),
|
|
95
|
+
threadId: v.optional(v.id("threads")),
|
|
96
|
+
status: v.string(),
|
|
97
|
+
currentTask: v.optional(v.string()),
|
|
98
|
+
pendingTasks: v.array(v.string()),
|
|
99
|
+
context: v.string(),
|
|
100
|
+
checkIntervalMs: v.optional(v.number()), // How often to check (default: 5 minutes)
|
|
101
|
+
metadata: v.optional(v.any()),
|
|
102
|
+
},
|
|
103
|
+
handler: async (ctx, args) => {
|
|
104
|
+
const { checkIntervalMs = 5 * 60 * 1000, ...data } = args;
|
|
105
|
+
const now = Date.now();
|
|
106
|
+
|
|
107
|
+
// Check if heartbeat exists
|
|
108
|
+
const existing = await ctx.db
|
|
109
|
+
.query("heartbeats")
|
|
110
|
+
.withIndex("byAgentId", (q) => q.eq("agentId", args.agentId))
|
|
111
|
+
.filter((q) =>
|
|
112
|
+
args.threadId
|
|
113
|
+
? q.eq(q.field("threadId"), args.threadId)
|
|
114
|
+
: q.eq(q.field("threadId"), undefined)
|
|
115
|
+
)
|
|
116
|
+
.first();
|
|
117
|
+
|
|
118
|
+
if (existing) {
|
|
119
|
+
// Update existing heartbeat
|
|
120
|
+
await ctx.db.patch(existing._id, {
|
|
121
|
+
...data,
|
|
122
|
+
lastCheck: now,
|
|
123
|
+
nextCheck: now + checkIntervalMs,
|
|
124
|
+
});
|
|
125
|
+
return existing._id;
|
|
126
|
+
} else {
|
|
127
|
+
// Create new heartbeat
|
|
128
|
+
const heartbeatId = await ctx.db.insert("heartbeats", {
|
|
129
|
+
...data,
|
|
130
|
+
lastCheck: now,
|
|
131
|
+
nextCheck: now + checkIntervalMs,
|
|
132
|
+
});
|
|
133
|
+
return heartbeatId;
|
|
134
|
+
}
|
|
135
|
+
},
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
// Mutation: Update heartbeat status
|
|
139
|
+
export const updateStatus = mutation({
|
|
140
|
+
args: {
|
|
141
|
+
agentId: v.string(),
|
|
142
|
+
threadId: v.optional(v.id("threads")),
|
|
143
|
+
status: v.string(),
|
|
144
|
+
currentTask: v.optional(v.string()),
|
|
145
|
+
},
|
|
146
|
+
handler: async (ctx, args) => {
|
|
147
|
+
const heartbeat = await ctx.db
|
|
148
|
+
.query("heartbeats")
|
|
149
|
+
.withIndex("byAgentId", (q) => q.eq("agentId", args.agentId))
|
|
150
|
+
.filter((q) =>
|
|
151
|
+
args.threadId
|
|
152
|
+
? q.eq(q.field("threadId"), args.threadId)
|
|
153
|
+
: q.eq(q.field("threadId"), undefined)
|
|
154
|
+
)
|
|
155
|
+
.first();
|
|
156
|
+
|
|
157
|
+
if (!heartbeat) {
|
|
158
|
+
throw new Error(`Heartbeat not found for agent ${args.agentId}`);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
await ctx.db.patch(heartbeat._id, {
|
|
162
|
+
status: args.status,
|
|
163
|
+
currentTask: args.currentTask,
|
|
164
|
+
lastCheck: Date.now(),
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
return heartbeat._id;
|
|
168
|
+
},
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
// Mutation: Add pending task
|
|
172
|
+
export const addPendingTask = mutation({
|
|
173
|
+
args: {
|
|
174
|
+
agentId: v.string(),
|
|
175
|
+
threadId: v.optional(v.id("threads")),
|
|
176
|
+
task: v.string(),
|
|
177
|
+
},
|
|
178
|
+
handler: async (ctx, args) => {
|
|
179
|
+
const heartbeat = await ctx.db
|
|
180
|
+
.query("heartbeats")
|
|
181
|
+
.withIndex("byAgentId", (q) => q.eq("agentId", args.agentId))
|
|
182
|
+
.filter((q) =>
|
|
183
|
+
args.threadId
|
|
184
|
+
? q.eq(q.field("threadId"), args.threadId)
|
|
185
|
+
: q.eq(q.field("threadId"), undefined)
|
|
186
|
+
)
|
|
187
|
+
.first();
|
|
188
|
+
|
|
189
|
+
if (!heartbeat) {
|
|
190
|
+
throw new Error(`Heartbeat not found for agent ${args.agentId}`);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const updatedTasks = [...heartbeat.pendingTasks, args.task];
|
|
194
|
+
|
|
195
|
+
await ctx.db.patch(heartbeat._id, {
|
|
196
|
+
pendingTasks: updatedTasks,
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
return heartbeat._id;
|
|
200
|
+
},
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
// Mutation: Remove pending task
|
|
204
|
+
export const removePendingTask = mutation({
|
|
205
|
+
args: {
|
|
206
|
+
agentId: v.string(),
|
|
207
|
+
threadId: v.optional(v.id("threads")),
|
|
208
|
+
task: v.string(),
|
|
209
|
+
},
|
|
210
|
+
handler: async (ctx, args) => {
|
|
211
|
+
const heartbeat = await ctx.db
|
|
212
|
+
.query("heartbeats")
|
|
213
|
+
.withIndex("byAgentId", (q) => q.eq("agentId", args.agentId))
|
|
214
|
+
.filter((q) =>
|
|
215
|
+
args.threadId
|
|
216
|
+
? q.eq(q.field("threadId"), args.threadId)
|
|
217
|
+
: q.eq(q.field("threadId"), undefined)
|
|
218
|
+
)
|
|
219
|
+
.first();
|
|
220
|
+
|
|
221
|
+
if (!heartbeat) {
|
|
222
|
+
throw new Error(`Heartbeat not found for agent ${args.agentId}`);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const updatedTasks = heartbeat.pendingTasks.filter((t) => t !== args.task);
|
|
226
|
+
|
|
227
|
+
await ctx.db.patch(heartbeat._id, {
|
|
228
|
+
pendingTasks: updatedTasks,
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
return heartbeat._id;
|
|
232
|
+
},
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
// Mutation: Update context
|
|
236
|
+
export const updateContext = mutation({
|
|
237
|
+
args: {
|
|
238
|
+
agentId: v.string(),
|
|
239
|
+
threadId: v.optional(v.id("threads")),
|
|
240
|
+
context: v.string(),
|
|
241
|
+
},
|
|
242
|
+
handler: async (ctx, args) => {
|
|
243
|
+
const heartbeat = await ctx.db
|
|
244
|
+
.query("heartbeats")
|
|
245
|
+
.withIndex("byAgentId", (q) => q.eq("agentId", args.agentId))
|
|
246
|
+
.filter((q) =>
|
|
247
|
+
args.threadId
|
|
248
|
+
? q.eq(q.field("threadId"), args.threadId)
|
|
249
|
+
: q.eq(q.field("threadId"), undefined)
|
|
250
|
+
)
|
|
251
|
+
.first();
|
|
252
|
+
|
|
253
|
+
if (!heartbeat) {
|
|
254
|
+
throw new Error(`Heartbeat not found for agent ${args.agentId}`);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
await ctx.db.patch(heartbeat._id, {
|
|
258
|
+
context: args.context,
|
|
259
|
+
lastCheck: Date.now(),
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
return heartbeat._id;
|
|
263
|
+
},
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
// Action: Generate heartbeat context from thread
|
|
267
|
+
export const generateContext = action({
|
|
268
|
+
args: {
|
|
269
|
+
agentId: v.string(),
|
|
270
|
+
threadId: v.id("threads"),
|
|
271
|
+
},
|
|
272
|
+
handler: async (ctx, args) => {
|
|
273
|
+
// Get thread and messages
|
|
274
|
+
const thread = await ctx.runQuery(api.threads.get, { id: args.threadId });
|
|
275
|
+
const messages = await ctx.runQuery(api.messages.list, {
|
|
276
|
+
threadId: args.threadId,
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
if (!thread) {
|
|
280
|
+
throw new Error(`Thread ${args.threadId} not found`);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Generate markdown context
|
|
284
|
+
let context = `# Heartbeat Context\n\n`;
|
|
285
|
+
context += `**Agent ID:** ${args.agentId}\n`;
|
|
286
|
+
context += `**Thread:** ${thread.name || "Unnamed"}\n`;
|
|
287
|
+
context += `**Last Updated:** ${new Date().toISOString()}\n\n`;
|
|
288
|
+
|
|
289
|
+
context += `## Recent Conversation\n\n`;
|
|
290
|
+
|
|
291
|
+
// Include last 10 messages
|
|
292
|
+
const recentMessages = messages.slice(-10);
|
|
293
|
+
for (const msg of recentMessages) {
|
|
294
|
+
const timestamp = new Date(msg.createdAt).toLocaleString();
|
|
295
|
+
context += `### ${msg.role.toUpperCase()} (${timestamp})\n`;
|
|
296
|
+
context += `${msg.content}\n\n`;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
context += `## Status\n\n`;
|
|
300
|
+
context += `- Total messages: ${messages.length}\n`;
|
|
301
|
+
context += `- Last activity: ${new Date(thread.updatedAt).toLocaleString()}\n`;
|
|
302
|
+
|
|
303
|
+
return context;
|
|
304
|
+
},
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
// Action: Process heartbeat check
|
|
308
|
+
export const processCheck = action({
|
|
309
|
+
args: {
|
|
310
|
+
agentId: v.string(),
|
|
311
|
+
threadId: v.optional(v.id("threads")),
|
|
312
|
+
},
|
|
313
|
+
handler: async (ctx, args) => {
|
|
314
|
+
// Get heartbeat
|
|
315
|
+
const heartbeat = await ctx.runQuery(api.heartbeat.get, {
|
|
316
|
+
agentId: args.agentId,
|
|
317
|
+
threadId: args.threadId,
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
if (!heartbeat) {
|
|
321
|
+
return { success: false, message: "Heartbeat not found" };
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Check if there are pending tasks
|
|
325
|
+
if (heartbeat.pendingTasks.length > 0) {
|
|
326
|
+
// TODO: Integrate with Mastra to execute pending tasks
|
|
327
|
+
// For now, just log
|
|
328
|
+
console.log(`Agent ${args.agentId} has ${heartbeat.pendingTasks.length} pending tasks`);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Update last check time
|
|
332
|
+
await ctx.runMutation(api.heartbeat.updateStatus, {
|
|
333
|
+
agentId: args.agentId,
|
|
334
|
+
threadId: args.threadId,
|
|
335
|
+
status: heartbeat.status,
|
|
336
|
+
currentTask: heartbeat.currentTask,
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
return {
|
|
340
|
+
success: true,
|
|
341
|
+
pendingTasks: heartbeat.pendingTasks.length,
|
|
342
|
+
status: heartbeat.status,
|
|
343
|
+
};
|
|
344
|
+
},
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
// Mutation: Delete heartbeat
|
|
348
|
+
export const remove = mutation({
|
|
349
|
+
args: {
|
|
350
|
+
agentId: v.string(),
|
|
351
|
+
threadId: v.optional(v.id("threads")),
|
|
352
|
+
},
|
|
353
|
+
handler: async (ctx, args) => {
|
|
354
|
+
const heartbeat = await ctx.db
|
|
355
|
+
.query("heartbeats")
|
|
356
|
+
.withIndex("byAgentId", (q) => q.eq("agentId", args.agentId))
|
|
357
|
+
.filter((q) =>
|
|
358
|
+
args.threadId
|
|
359
|
+
? q.eq(q.field("threadId"), args.threadId)
|
|
360
|
+
: q.eq(q.field("threadId"), undefined)
|
|
361
|
+
)
|
|
362
|
+
.first();
|
|
363
|
+
|
|
364
|
+
if (!heartbeat) {
|
|
365
|
+
throw new Error(`Heartbeat not found for agent ${args.agentId}`);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
await ctx.db.delete(heartbeat._id);
|
|
369
|
+
return { success: true };
|
|
370
|
+
},
|
|
371
|
+
});
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { v } from "convex/values";
|
|
2
|
+
import { mutation, query } from "./_generated/server";
|
|
3
|
+
|
|
4
|
+
// Query: List recent logs
|
|
5
|
+
export const list = query({
|
|
6
|
+
args: {
|
|
7
|
+
level: v.optional(v.string()),
|
|
8
|
+
source: v.optional(v.string()),
|
|
9
|
+
limit: v.optional(v.number()),
|
|
10
|
+
},
|
|
11
|
+
handler: async (ctx, args) => {
|
|
12
|
+
let q = ctx.db.query("logs").withIndex("byTimestamp").order("desc");
|
|
13
|
+
|
|
14
|
+
const results = await q.collect();
|
|
15
|
+
|
|
16
|
+
let filtered = results;
|
|
17
|
+
if (args.level) {
|
|
18
|
+
filtered = filtered.filter((l) => l.level === args.level);
|
|
19
|
+
}
|
|
20
|
+
if (args.source) {
|
|
21
|
+
filtered = filtered.filter((l) => l.source === args.source);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return filtered.slice(0, args.limit || 100);
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
// Mutation: Add a log entry
|
|
29
|
+
export const add = mutation({
|
|
30
|
+
args: {
|
|
31
|
+
level: v.union(
|
|
32
|
+
v.literal("debug"),
|
|
33
|
+
v.literal("info"),
|
|
34
|
+
v.literal("warn"),
|
|
35
|
+
v.literal("error")
|
|
36
|
+
),
|
|
37
|
+
source: v.string(),
|
|
38
|
+
message: v.string(),
|
|
39
|
+
metadata: v.optional(v.any()),
|
|
40
|
+
userId: v.optional(v.string()),
|
|
41
|
+
},
|
|
42
|
+
handler: async (ctx, args) => {
|
|
43
|
+
return await ctx.db.insert("logs", {
|
|
44
|
+
...args,
|
|
45
|
+
timestamp: Date.now(),
|
|
46
|
+
});
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// Mutation: Clear logs older than a given timestamp
|
|
51
|
+
export const clearOld = mutation({
|
|
52
|
+
args: { olderThan: v.number() },
|
|
53
|
+
handler: async (ctx, args) => {
|
|
54
|
+
const old = await ctx.db
|
|
55
|
+
.query("logs")
|
|
56
|
+
.withIndex("byTimestamp")
|
|
57
|
+
.filter((q) => q.lt(q.field("timestamp"), args.olderThan))
|
|
58
|
+
.collect();
|
|
59
|
+
|
|
60
|
+
for (const log of old) {
|
|
61
|
+
await ctx.db.delete(log._id);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return old.length;
|
|
65
|
+
},
|
|
66
|
+
});
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { action } from "./_generated/server";
|
|
2
|
+
import { v } from "convex/values";
|
|
3
|
+
import { api } from "./_generated/api";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Mastra Integration Actions for Convex
|
|
7
|
+
*
|
|
8
|
+
* These actions run in the Node.js runtime and can use Mastra
|
|
9
|
+
* to execute agents and manage workflows.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
// Action: Execute agent with Mastra
|
|
13
|
+
export const executeAgent = action({
|
|
14
|
+
args: {
|
|
15
|
+
agentId: v.string(),
|
|
16
|
+
prompt: v.string(),
|
|
17
|
+
threadId: v.optional(v.id("threads")),
|
|
18
|
+
userId: v.optional(v.string()),
|
|
19
|
+
stream: v.optional(v.boolean()),
|
|
20
|
+
},
|
|
21
|
+
handler: async (ctx, args) => {
|
|
22
|
+
// Get agent configuration from database
|
|
23
|
+
const agent = await ctx.runQuery(api.agents.get, { id: args.agentId });
|
|
24
|
+
|
|
25
|
+
if (!agent) {
|
|
26
|
+
throw new Error(`Agent ${args.agentId} not found`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Create or get thread
|
|
30
|
+
let threadId = args.threadId;
|
|
31
|
+
if (!threadId) {
|
|
32
|
+
threadId = await ctx.runMutation(api.threads.create, {
|
|
33
|
+
agentId: args.agentId,
|
|
34
|
+
userId: args.userId,
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Add user message to thread
|
|
39
|
+
await ctx.runMutation(api.messages.add, {
|
|
40
|
+
threadId,
|
|
41
|
+
role: "user",
|
|
42
|
+
content: args.prompt,
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// Create session
|
|
46
|
+
const sessionId = `session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
47
|
+
await ctx.runMutation(api.sessions.create, {
|
|
48
|
+
sessionId,
|
|
49
|
+
threadId,
|
|
50
|
+
agentId: args.agentId,
|
|
51
|
+
userId: args.userId,
|
|
52
|
+
channel: "dashboard",
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
// Import Mastra dynamically (Node.js runtime)
|
|
57
|
+
const { Agent } = await import("@mastra/core/agent");
|
|
58
|
+
|
|
59
|
+
// Format model string for Mastra
|
|
60
|
+
const modelString = agent.model.includes("/")
|
|
61
|
+
? agent.model
|
|
62
|
+
: `${agent.provider}/${agent.model}`;
|
|
63
|
+
|
|
64
|
+
// Create Mastra agent
|
|
65
|
+
const mastraAgent = new Agent({
|
|
66
|
+
id: agent.id,
|
|
67
|
+
name: agent.name,
|
|
68
|
+
instructions: agent.instructions,
|
|
69
|
+
model: modelString,
|
|
70
|
+
tools: agent.tools || {},
|
|
71
|
+
...(agent.temperature && { temperature: agent.temperature }),
|
|
72
|
+
...(agent.maxTokens && { maxTokens: agent.maxTokens }),
|
|
73
|
+
...(agent.topP && { topP: agent.topP }),
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// Get conversation history for context
|
|
77
|
+
const messages = await ctx.runQuery(api.messages.list, { threadId });
|
|
78
|
+
|
|
79
|
+
// Build context from message history
|
|
80
|
+
const context = messages
|
|
81
|
+
.slice(-10) // Last 10 messages for context
|
|
82
|
+
.map((m) => `${m.role}: ${m.content}`)
|
|
83
|
+
.join("\n");
|
|
84
|
+
|
|
85
|
+
// Execute agent
|
|
86
|
+
const result = await mastraAgent.generate(args.prompt, {
|
|
87
|
+
...(args.stream && { stream: args.stream }),
|
|
88
|
+
context: context || undefined,
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// Extract response content
|
|
92
|
+
const responseContent = typeof result === "string"
|
|
93
|
+
? result
|
|
94
|
+
: result.text || result.content || JSON.stringify(result);
|
|
95
|
+
|
|
96
|
+
// Add assistant message to thread
|
|
97
|
+
await ctx.runMutation(api.messages.add, {
|
|
98
|
+
threadId,
|
|
99
|
+
role: "assistant",
|
|
100
|
+
content: responseContent,
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
// Update session status
|
|
104
|
+
await ctx.runMutation(api.sessions.updateStatus, {
|
|
105
|
+
sessionId,
|
|
106
|
+
status: "completed",
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// Record usage (if available in result)
|
|
110
|
+
if (result.usage) {
|
|
111
|
+
await ctx.runMutation(api.usage.record, {
|
|
112
|
+
agentId: args.agentId,
|
|
113
|
+
sessionId,
|
|
114
|
+
provider: agent.provider,
|
|
115
|
+
model: agent.model,
|
|
116
|
+
promptTokens: result.usage.promptTokens || 0,
|
|
117
|
+
completionTokens: result.usage.completionTokens || 0,
|
|
118
|
+
totalTokens: result.usage.totalTokens || 0,
|
|
119
|
+
cost: result.usage.cost,
|
|
120
|
+
userId: args.userId,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
success: true,
|
|
126
|
+
threadId,
|
|
127
|
+
sessionId,
|
|
128
|
+
response: responseContent,
|
|
129
|
+
usage: result.usage,
|
|
130
|
+
};
|
|
131
|
+
} catch (error: any) {
|
|
132
|
+
// Update session status to error
|
|
133
|
+
await ctx.runMutation(api.sessions.updateStatus, {
|
|
134
|
+
sessionId,
|
|
135
|
+
status: "error",
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
// Add error message
|
|
139
|
+
await ctx.runMutation(api.messages.add, {
|
|
140
|
+
threadId,
|
|
141
|
+
role: "assistant",
|
|
142
|
+
content: `Error: ${error.message}`,
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
throw error;
|
|
146
|
+
}
|
|
147
|
+
},
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
// Action: Stream agent response
|
|
151
|
+
export const streamAgent = action({
|
|
152
|
+
args: {
|
|
153
|
+
agentId: v.string(),
|
|
154
|
+
prompt: v.string(),
|
|
155
|
+
threadId: v.id("threads"),
|
|
156
|
+
userId: v.optional(v.string()),
|
|
157
|
+
},
|
|
158
|
+
handler: async (ctx, args) => {
|
|
159
|
+
// Similar to executeAgent but with streaming support
|
|
160
|
+
// This would require WebSocket or SSE implementation
|
|
161
|
+
// For now, return a placeholder
|
|
162
|
+
return {
|
|
163
|
+
success: true,
|
|
164
|
+
message: "Streaming support coming soon",
|
|
165
|
+
};
|
|
166
|
+
},
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
// Action: Execute workflow with multiple agents
|
|
170
|
+
export const executeWorkflow = action({
|
|
171
|
+
args: {
|
|
172
|
+
workflowId: v.string(),
|
|
173
|
+
input: v.any(),
|
|
174
|
+
userId: v.optional(v.string()),
|
|
175
|
+
},
|
|
176
|
+
handler: async (ctx, args) => {
|
|
177
|
+
// Placeholder for workflow execution
|
|
178
|
+
// This would orchestrate multiple agents in sequence or parallel
|
|
179
|
+
return {
|
|
180
|
+
success: true,
|
|
181
|
+
message: "Workflow execution coming soon",
|
|
182
|
+
};
|
|
183
|
+
},
|
|
184
|
+
});
|