@agentforge-ai/cli 0.4.3 → 0.5.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/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 +185 -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/schema.ts +150 -83
- 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 +397 -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 +256 -49
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- 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 +185 -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/schema.ts +150 -83
- 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 +397 -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,110 @@
|
|
|
1
|
+
import { v } from "convex/values";
|
|
2
|
+
import { mutation, query } from "./_generated/server";
|
|
3
|
+
|
|
4
|
+
// Query: List folders
|
|
5
|
+
export const list = query({
|
|
6
|
+
args: {
|
|
7
|
+
parentId: v.optional(v.id("folders")),
|
|
8
|
+
projectId: v.optional(v.id("projects")),
|
|
9
|
+
userId: v.optional(v.string()),
|
|
10
|
+
},
|
|
11
|
+
handler: async (ctx, args) => {
|
|
12
|
+
if (args.projectId) {
|
|
13
|
+
return await ctx.db
|
|
14
|
+
.query("folders")
|
|
15
|
+
.withIndex("byProjectId", (q) => q.eq("projectId", args.projectId!))
|
|
16
|
+
.collect();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (args.parentId) {
|
|
20
|
+
return await ctx.db
|
|
21
|
+
.query("folders")
|
|
22
|
+
.withIndex("byParentId", (q) => q.eq("parentId", args.parentId!))
|
|
23
|
+
.collect();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (args.userId) {
|
|
27
|
+
return await ctx.db
|
|
28
|
+
.query("folders")
|
|
29
|
+
.withIndex("byUserId", (q) => q.eq("userId", args.userId!))
|
|
30
|
+
.collect();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return await ctx.db.query("folders").collect();
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// Query: Get folder by ID
|
|
38
|
+
export const get = query({
|
|
39
|
+
args: { id: v.id("folders") },
|
|
40
|
+
handler: async (ctx, args) => {
|
|
41
|
+
return await ctx.db.get(args.id);
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// Mutation: Create folder
|
|
46
|
+
export const create = mutation({
|
|
47
|
+
args: {
|
|
48
|
+
name: v.string(),
|
|
49
|
+
parentId: v.optional(v.id("folders")),
|
|
50
|
+
projectId: v.optional(v.id("projects")),
|
|
51
|
+
userId: v.optional(v.string()),
|
|
52
|
+
},
|
|
53
|
+
handler: async (ctx, args) => {
|
|
54
|
+
const now = Date.now();
|
|
55
|
+
const folderId = await ctx.db.insert("folders", {
|
|
56
|
+
...args,
|
|
57
|
+
createdAt: now,
|
|
58
|
+
updatedAt: now,
|
|
59
|
+
});
|
|
60
|
+
return folderId;
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// Mutation: Update folder
|
|
65
|
+
export const update = mutation({
|
|
66
|
+
args: {
|
|
67
|
+
id: v.id("folders"),
|
|
68
|
+
name: v.optional(v.string()),
|
|
69
|
+
parentId: v.optional(v.id("folders")),
|
|
70
|
+
},
|
|
71
|
+
handler: async (ctx, args) => {
|
|
72
|
+
const { id, ...updates } = args;
|
|
73
|
+
await ctx.db.patch(id, {
|
|
74
|
+
...updates,
|
|
75
|
+
updatedAt: Date.now(),
|
|
76
|
+
});
|
|
77
|
+
return id;
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// Mutation: Delete folder
|
|
82
|
+
export const remove = mutation({
|
|
83
|
+
args: { id: v.id("folders") },
|
|
84
|
+
handler: async (ctx, args) => {
|
|
85
|
+
// Delete all files in the folder
|
|
86
|
+
const files = await ctx.db
|
|
87
|
+
.query("files")
|
|
88
|
+
.withIndex("byFolderId", (q) => q.eq("folderId", args.id!))
|
|
89
|
+
.collect();
|
|
90
|
+
|
|
91
|
+
for (const file of files) {
|
|
92
|
+
await ctx.db.delete(file._id);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Delete all subfolders recursively
|
|
96
|
+
const subfolders = await ctx.db
|
|
97
|
+
.query("folders")
|
|
98
|
+
.withIndex("byParentId", (q) => q.eq("parentId", args.id!))
|
|
99
|
+
.collect();
|
|
100
|
+
|
|
101
|
+
for (const subfolder of subfolders) {
|
|
102
|
+
// Recursive delete (will be called via mutation)
|
|
103
|
+
await ctx.db.delete(subfolder._id);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Delete the folder itself
|
|
107
|
+
await ctx.db.delete(args.id);
|
|
108
|
+
return { success: true };
|
|
109
|
+
},
|
|
110
|
+
});
|
|
@@ -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
|
+
.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
|
+
.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
|
+
.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
|
+
.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
|
+
});
|