@lakitu/sdk 0.1.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.
Files changed (111) hide show
  1. package/README.md +166 -0
  2. package/convex/_generated/api.d.ts +45 -0
  3. package/convex/_generated/api.js +23 -0
  4. package/convex/_generated/dataModel.d.ts +58 -0
  5. package/convex/_generated/server.d.ts +143 -0
  6. package/convex/_generated/server.js +93 -0
  7. package/convex/cloud/CLAUDE.md +238 -0
  8. package/convex/cloud/_generated/api.ts +84 -0
  9. package/convex/cloud/_generated/component.ts +861 -0
  10. package/convex/cloud/_generated/dataModel.ts +60 -0
  11. package/convex/cloud/_generated/server.ts +156 -0
  12. package/convex/cloud/convex.config.ts +16 -0
  13. package/convex/cloud/index.ts +29 -0
  14. package/convex/cloud/intentSchema/generate.ts +447 -0
  15. package/convex/cloud/intentSchema/index.ts +16 -0
  16. package/convex/cloud/intentSchema/types.ts +418 -0
  17. package/convex/cloud/ksaPolicy.ts +554 -0
  18. package/convex/cloud/mail.ts +92 -0
  19. package/convex/cloud/schema.ts +322 -0
  20. package/convex/cloud/utils/kanbanContext.ts +229 -0
  21. package/convex/cloud/workflows/agentBoard.ts +451 -0
  22. package/convex/cloud/workflows/agentPrompt.ts +272 -0
  23. package/convex/cloud/workflows/agentThread.ts +374 -0
  24. package/convex/cloud/workflows/compileSandbox.ts +146 -0
  25. package/convex/cloud/workflows/crudBoard.ts +217 -0
  26. package/convex/cloud/workflows/crudKSAs.ts +262 -0
  27. package/convex/cloud/workflows/crudLorobeads.ts +371 -0
  28. package/convex/cloud/workflows/crudSkills.ts +205 -0
  29. package/convex/cloud/workflows/crudThreads.ts +708 -0
  30. package/convex/cloud/workflows/lifecycleSandbox.ts +1396 -0
  31. package/convex/cloud/workflows/sandboxConvex.ts +1046 -0
  32. package/convex/sandbox/README.md +90 -0
  33. package/convex/sandbox/_generated/api.d.ts +2934 -0
  34. package/convex/sandbox/_generated/api.js +23 -0
  35. package/convex/sandbox/_generated/dataModel.d.ts +60 -0
  36. package/convex/sandbox/_generated/server.d.ts +143 -0
  37. package/convex/sandbox/_generated/server.js +93 -0
  38. package/convex/sandbox/actions/bash.ts +130 -0
  39. package/convex/sandbox/actions/browser.ts +282 -0
  40. package/convex/sandbox/actions/file.ts +336 -0
  41. package/convex/sandbox/actions/lsp.ts +325 -0
  42. package/convex/sandbox/actions/pdf.ts +119 -0
  43. package/convex/sandbox/agent/codeExecLoop.ts +535 -0
  44. package/convex/sandbox/agent/decisions.ts +284 -0
  45. package/convex/sandbox/agent/index.ts +515 -0
  46. package/convex/sandbox/agent/subagents.ts +651 -0
  47. package/convex/sandbox/brandResearch/index.ts +417 -0
  48. package/convex/sandbox/context/index.ts +7 -0
  49. package/convex/sandbox/context/session.ts +402 -0
  50. package/convex/sandbox/convex.config.ts +17 -0
  51. package/convex/sandbox/index.ts +51 -0
  52. package/convex/sandbox/nodeActions/codeExec.ts +130 -0
  53. package/convex/sandbox/planning/beads.ts +187 -0
  54. package/convex/sandbox/planning/index.ts +8 -0
  55. package/convex/sandbox/planning/sync.ts +194 -0
  56. package/convex/sandbox/prompts/codeExec.ts +852 -0
  57. package/convex/sandbox/prompts/modes.ts +231 -0
  58. package/convex/sandbox/prompts/system.ts +142 -0
  59. package/convex/sandbox/schema.ts +510 -0
  60. package/convex/sandbox/state/artifacts.ts +99 -0
  61. package/convex/sandbox/state/checkpoints.ts +341 -0
  62. package/convex/sandbox/state/files.ts +383 -0
  63. package/convex/sandbox/state/index.ts +10 -0
  64. package/convex/sandbox/state/verification.actions.ts +268 -0
  65. package/convex/sandbox/state/verification.ts +101 -0
  66. package/convex/sandbox/tsconfig.json +25 -0
  67. package/convex/sandbox/utils/codeExecHelpers.ts +52 -0
  68. package/dist/cli/commands/build.d.ts +19 -0
  69. package/dist/cli/commands/build.d.ts.map +1 -0
  70. package/dist/cli/commands/build.js +223 -0
  71. package/dist/cli/commands/init.d.ts +16 -0
  72. package/dist/cli/commands/init.d.ts.map +1 -0
  73. package/dist/cli/commands/init.js +148 -0
  74. package/dist/cli/commands/publish.d.ts +12 -0
  75. package/dist/cli/commands/publish.d.ts.map +1 -0
  76. package/dist/cli/commands/publish.js +33 -0
  77. package/dist/cli/index.d.ts +14 -0
  78. package/dist/cli/index.d.ts.map +1 -0
  79. package/dist/cli/index.js +40 -0
  80. package/dist/sdk/builders.d.ts +104 -0
  81. package/dist/sdk/builders.d.ts.map +1 -0
  82. package/dist/sdk/builders.js +214 -0
  83. package/dist/sdk/index.d.ts +29 -0
  84. package/dist/sdk/index.d.ts.map +1 -0
  85. package/dist/sdk/index.js +38 -0
  86. package/dist/sdk/types.d.ts +107 -0
  87. package/dist/sdk/types.d.ts.map +1 -0
  88. package/dist/sdk/types.js +6 -0
  89. package/ksa/README.md +263 -0
  90. package/ksa/_generated/REFERENCE.md +2954 -0
  91. package/ksa/_generated/registry.ts +257 -0
  92. package/ksa/_shared/configReader.ts +302 -0
  93. package/ksa/_shared/configSchemas.ts +649 -0
  94. package/ksa/_shared/gateway.ts +175 -0
  95. package/ksa/_shared/ksaBehaviors.ts +411 -0
  96. package/ksa/_shared/ksaProxy.ts +248 -0
  97. package/ksa/_shared/localDb.ts +302 -0
  98. package/ksa/index.ts +134 -0
  99. package/package.json +93 -0
  100. package/runtime/browser/agent-browser.ts +330 -0
  101. package/runtime/entrypoint.ts +194 -0
  102. package/runtime/lsp/manager.ts +366 -0
  103. package/runtime/pdf/pdf-generator.ts +50 -0
  104. package/runtime/pdf/renderer.ts +357 -0
  105. package/runtime/pdf/schema.ts +97 -0
  106. package/runtime/services/file-watcher.ts +191 -0
  107. package/template/build.ts +307 -0
  108. package/template/e2b/Dockerfile +69 -0
  109. package/template/e2b/e2b.toml +13 -0
  110. package/template/e2b/prebuild.sh +68 -0
  111. package/template/e2b/start.sh +14 -0
@@ -0,0 +1,708 @@
1
+ /**
2
+ * Threads & Conversations CRUD
3
+ *
4
+ * Threads: Chat-based interaction with agent (per user)
5
+ * Conversations: Project-level message history (for board workflows)
6
+ */
7
+
8
+ import { v } from "convex/values";
9
+ import { mutation, query, internalMutation, internalQuery, MutationCtx, QueryCtx } from "../_generated/server";
10
+ import { Id } from "../_generated/dataModel";
11
+
12
+ // ============================================
13
+ // Auth Helper
14
+ // ============================================
15
+
16
+ async function checkThreadAccess(ctx: QueryCtx | MutationCtx, threadId: Id<"threads">, userId: string) {
17
+ const thread = await ctx.db.get(threadId);
18
+ if (!thread) throw new Error("Thread not found");
19
+ if (thread.userId !== userId) throw new Error("Unauthorized access to thread");
20
+ return thread;
21
+ }
22
+
23
+ // ============================================
24
+ // Threads (Chat Interface)
25
+ // ============================================
26
+
27
+ /** Create a new chat thread */
28
+ export const createThread = mutation({
29
+ args: {
30
+ userId: v.string(),
31
+ boardId: v.optional(v.string()),
32
+ workspaceId: v.optional(v.string()),
33
+ title: v.optional(v.string()),
34
+ orgId: v.optional(v.string()),
35
+ },
36
+ handler: async (ctx, args) => {
37
+ return await ctx.db.insert("threads", {
38
+ userId: args.userId,
39
+ orgId: args.orgId,
40
+ boardId: args.boardId,
41
+ workspaceId: args.workspaceId,
42
+ title: args.title || "New Chat",
43
+ createdAt: Date.now(),
44
+ updatedAt: Date.now(),
45
+ });
46
+ },
47
+ });
48
+
49
+ /** List threads for a user */
50
+ export const listThreads = query({
51
+ args: {
52
+ userId: v.string(),
53
+ orgId: v.optional(v.string()),
54
+ boardId: v.optional(v.string()),
55
+ workspaceId: v.optional(v.string()),
56
+ },
57
+ handler: async (ctx, args) => {
58
+ // If workspaceId is provided, use the workspace index for efficiency
59
+ if (args.workspaceId) {
60
+ const threads = await ctx.db
61
+ .query("threads")
62
+ .withIndex("by_workspace", (q) => q.eq("workspaceId", args.workspaceId))
63
+ .collect();
64
+ // Filter by userId for authorization
65
+ return threads
66
+ .filter(t => t.userId === args.userId)
67
+ .sort((a, b) => b.updatedAt - a.updatedAt);
68
+ }
69
+
70
+ const threads = args.orgId
71
+ ? await ctx.db
72
+ .query("threads")
73
+ .withIndex("by_org", (q) => q.eq("orgId", args.orgId))
74
+ .collect()
75
+ : await ctx.db
76
+ .query("threads")
77
+ .withIndex("by_user", (q) => q.eq("userId", args.userId))
78
+ .collect();
79
+
80
+ if (args.boardId) {
81
+ return threads.filter(t => t.boardId === args.boardId).sort((a, b) => b.updatedAt - a.updatedAt);
82
+ }
83
+
84
+ return threads.sort((a, b) => b.updatedAt - a.updatedAt);
85
+ },
86
+ });
87
+
88
+ /** List threads for a workspace (efficient indexed query) */
89
+ export const listWorkspaceThreads = query({
90
+ args: {
91
+ workspaceId: v.string(),
92
+ userId: v.string(),
93
+ },
94
+ handler: async (ctx, args) => {
95
+ const threads = await ctx.db
96
+ .query("threads")
97
+ .withIndex("by_workspace", (q) => q.eq("workspaceId", args.workspaceId))
98
+ .collect();
99
+
100
+ // Filter by userId for authorization
101
+ return threads
102
+ .filter(t => t.userId === args.userId)
103
+ .sort((a, b) => b.updatedAt - a.updatedAt);
104
+ },
105
+ });
106
+
107
+ /** Get messages in a thread */
108
+ export const getThreadMessages = query({
109
+ args: { threadId: v.id("threads"), userId: v.string() },
110
+ handler: async (ctx, args) => {
111
+ await checkThreadAccess(ctx, args.threadId, args.userId);
112
+ return await ctx.db
113
+ .query("threadMessages")
114
+ .withIndex("by_thread", (q) => q.eq("threadId", args.threadId))
115
+ .collect();
116
+ },
117
+ });
118
+
119
+ /** Send a message to a thread */
120
+ export const sendThreadMessage = mutation({
121
+ args: {
122
+ threadId: v.id("threads"),
123
+ userId: v.string(),
124
+ content: v.string(),
125
+ role: v.union(v.literal("user"), v.literal("assistant")),
126
+ metadata: v.optional(v.object({
127
+ type: v.optional(v.union(
128
+ v.literal("text"),
129
+ v.literal("subagent"),
130
+ v.literal("board_execution"),
131
+ v.literal("frame_preview"),
132
+ v.literal("artifact"),
133
+ v.literal("session_logs")
134
+ )),
135
+ data: v.optional(v.any()),
136
+ sessionLogs: v.optional(v.any()), // Embedded session logs for the message
137
+ generationTime: v.optional(v.number()),
138
+ thinking: v.optional(v.any()),
139
+ })),
140
+ },
141
+ handler: async (ctx, args) => {
142
+ await checkThreadAccess(ctx, args.threadId, args.userId);
143
+ const messageId = await ctx.db.insert("threadMessages", {
144
+ threadId: args.threadId,
145
+ role: args.role,
146
+ content: args.content,
147
+ createdAt: Date.now(),
148
+ metadata: args.metadata,
149
+ });
150
+
151
+ await ctx.db.patch(args.threadId, { updatedAt: Date.now() });
152
+ return messageId;
153
+ },
154
+ });
155
+
156
+ /** Delete a thread and its messages */
157
+ export const deleteThread = mutation({
158
+ args: { threadId: v.id("threads"), userId: v.string() },
159
+ handler: async (ctx, args) => {
160
+ await checkThreadAccess(ctx, args.threadId, args.userId);
161
+
162
+ const messages = await ctx.db
163
+ .query("threadMessages")
164
+ .withIndex("by_thread", (q) => q.eq("threadId", args.threadId))
165
+ .collect();
166
+
167
+ for (const msg of messages) {
168
+ await ctx.db.delete(msg._id);
169
+ }
170
+
171
+ await ctx.db.delete(args.threadId);
172
+ },
173
+ });
174
+
175
+ // ============================================
176
+ // Conversations (Project-Level for Workflows)
177
+ // ============================================
178
+
179
+ /** Save a message to project conversation */
180
+ export const saveConversationMessage = mutation({
181
+ args: {
182
+ projectId: v.string(),
183
+ role: v.union(v.literal("user"), v.literal("assistant")),
184
+ content: v.string(),
185
+ metadata: v.optional(v.any()),
186
+ },
187
+ handler: async (ctx, args) => {
188
+ let conversation = await ctx.db
189
+ .query("agentConversations")
190
+ .withIndex("by_project", (q) => q.eq("projectId", args.projectId))
191
+ .first();
192
+
193
+ if (!conversation) {
194
+ const id = await ctx.db.insert("agentConversations", {
195
+ projectId: args.projectId,
196
+ messages: [],
197
+ createdAt: Date.now(),
198
+ updatedAt: Date.now(),
199
+ });
200
+ conversation = await ctx.db.get(id);
201
+ }
202
+
203
+ if (!conversation) throw new Error("Failed to create conversation");
204
+
205
+ await ctx.db.patch(conversation._id, {
206
+ messages: [...conversation.messages, {
207
+ role: args.role,
208
+ content: args.content,
209
+ timestamp: Date.now(),
210
+ metadata: args.metadata,
211
+ }],
212
+ updatedAt: Date.now(),
213
+ });
214
+ },
215
+ });
216
+
217
+ /** Get project conversation */
218
+ export const getConversation = query({
219
+ args: { projectId: v.string() },
220
+ handler: async (ctx, args) => {
221
+ return await ctx.db
222
+ .query("agentConversations")
223
+ .withIndex("by_project", (q) => q.eq("projectId", args.projectId))
224
+ .first();
225
+ },
226
+ });
227
+
228
+ /** Clear project conversation */
229
+ export const clearConversation = mutation({
230
+ args: { projectId: v.string() },
231
+ handler: async (ctx, args) => {
232
+ const conversation = await ctx.db
233
+ .query("agentConversations")
234
+ .withIndex("by_project", (q) => q.eq("projectId", args.projectId))
235
+ .first();
236
+
237
+ if (conversation) {
238
+ await ctx.db.patch(conversation._id, {
239
+ messages: [],
240
+ updatedAt: Date.now(),
241
+ });
242
+ }
243
+ },
244
+ });
245
+
246
+ // ============================================
247
+ // Internal Mutations for Subagent Progress
248
+ // (Called from sandbox via gateway)
249
+ // ============================================
250
+
251
+ /** Emit subagent progress to a thread (internal - called via gateway) */
252
+ export const emitSubagentProgress = internalMutation({
253
+ args: {
254
+ threadId: v.string(),
255
+ subagentId: v.string(),
256
+ name: v.string(),
257
+ task: v.string(),
258
+ status: v.union(
259
+ v.literal("spawning"),
260
+ v.literal("running"),
261
+ v.literal("completed"),
262
+ v.literal("failed")
263
+ ),
264
+ progress: v.optional(v.number()),
265
+ result: v.optional(v.string()),
266
+ error: v.optional(v.string()),
267
+ children: v.optional(v.array(v.object({
268
+ id: v.string(),
269
+ type: v.union(
270
+ v.literal("thinking"),
271
+ v.literal("tool"),
272
+ v.literal("search"),
273
+ v.literal("file"),
274
+ v.literal("complete"),
275
+ v.literal("error")
276
+ ),
277
+ label: v.string(),
278
+ status: v.union(v.literal("active"), v.literal("complete"), v.literal("error")),
279
+ details: v.optional(v.string()),
280
+ }))),
281
+ },
282
+ handler: async (ctx, args) => {
283
+ // Find or create the subagent progress message
284
+ const existingMessage = await ctx.db
285
+ .query("threadMessages")
286
+ .withIndex("by_thread", (q) => q.eq("threadId", args.threadId as Id<"threads">))
287
+ .filter((q) =>
288
+ q.and(
289
+ q.eq(q.field("metadata.type"), "subagent"),
290
+ q.eq(q.field("metadata.data.subagentId"), args.subagentId)
291
+ )
292
+ )
293
+ .first();
294
+
295
+ const progressData = {
296
+ subagentId: args.subagentId,
297
+ name: args.name,
298
+ task: args.task,
299
+ status: args.status,
300
+ progress: args.progress,
301
+ result: args.result,
302
+ error: args.error,
303
+ children: args.children || [],
304
+ };
305
+
306
+ if (existingMessage) {
307
+ // Update existing message
308
+ await ctx.db.patch(existingMessage._id, {
309
+ metadata: {
310
+ type: "subagent" as const,
311
+ data: progressData,
312
+ },
313
+ });
314
+ } else {
315
+ // Create new message
316
+ await ctx.db.insert("threadMessages", {
317
+ threadId: args.threadId as Id<"threads">,
318
+ role: "assistant",
319
+ content: `Subagent "${args.name}": ${args.task}`,
320
+ createdAt: Date.now(),
321
+ metadata: {
322
+ type: "subagent" as const,
323
+ data: progressData,
324
+ },
325
+ });
326
+ }
327
+
328
+ // Update thread timestamp
329
+ await ctx.db.patch(args.threadId as Id<"threads">, { updatedAt: Date.now() });
330
+ },
331
+ });
332
+
333
+ /** Emit board execution result to a thread (internal - called via gateway) */
334
+ export const emitBoardExecution = internalMutation({
335
+ args: {
336
+ threadId: v.string(),
337
+ boardId: v.string(),
338
+ cardId: v.string(),
339
+ boardName: v.string(),
340
+ stageName: v.string(),
341
+ status: v.union(
342
+ v.literal("queued"),
343
+ v.literal("running"),
344
+ v.literal("completed"),
345
+ v.literal("failed")
346
+ ),
347
+ artifacts: v.optional(v.array(v.object({
348
+ id: v.string(),
349
+ name: v.string(),
350
+ type: v.string(),
351
+ }))),
352
+ summary: v.optional(v.string()),
353
+ error: v.optional(v.string()),
354
+ },
355
+ handler: async (ctx, args) => {
356
+ // Find or create the board execution message
357
+ const existingMessage = await ctx.db
358
+ .query("threadMessages")
359
+ .withIndex("by_thread", (q) => q.eq("threadId", args.threadId as Id<"threads">))
360
+ .filter((q) =>
361
+ q.and(
362
+ q.eq(q.field("metadata.type"), "board_execution"),
363
+ q.eq(q.field("metadata.data.cardId"), args.cardId)
364
+ )
365
+ )
366
+ .first();
367
+
368
+ const executionData = {
369
+ boardId: args.boardId,
370
+ cardId: args.cardId,
371
+ boardName: args.boardName,
372
+ stageName: args.stageName,
373
+ status: args.status,
374
+ artifacts: args.artifacts,
375
+ summary: args.summary,
376
+ error: args.error,
377
+ };
378
+
379
+ if (existingMessage) {
380
+ await ctx.db.patch(existingMessage._id, {
381
+ metadata: {
382
+ type: "board_execution" as const,
383
+ data: executionData,
384
+ },
385
+ });
386
+ } else {
387
+ await ctx.db.insert("threadMessages", {
388
+ threadId: args.threadId as Id<"threads">,
389
+ role: "assistant",
390
+ content: `Running board "${args.boardName}" - ${args.stageName}`,
391
+ createdAt: Date.now(),
392
+ metadata: {
393
+ type: "board_execution" as const,
394
+ data: executionData,
395
+ },
396
+ });
397
+ }
398
+
399
+ await ctx.db.patch(args.threadId as Id<"threads">, { updatedAt: Date.now() });
400
+ },
401
+ });
402
+
403
+ /** Emit frame preview to a thread (internal - called via gateway) */
404
+ export const emitFramePreview = internalMutation({
405
+ args: {
406
+ threadId: v.string(),
407
+ frameId: v.string(),
408
+ workspaceId: v.optional(v.string()),
409
+ name: v.string(),
410
+ code: v.string(),
411
+ codeType: v.union(
412
+ v.literal("html"),
413
+ v.literal("svelte"),
414
+ v.literal("htmx"),
415
+ v.literal("tailwind")
416
+ ),
417
+ dimensions: v.object({
418
+ width: v.number(),
419
+ height: v.number(),
420
+ }),
421
+ },
422
+ handler: async (ctx, args) => {
423
+ await ctx.db.insert("threadMessages", {
424
+ threadId: args.threadId as Id<"threads">,
425
+ role: "assistant",
426
+ content: `Created frame: ${args.name}`,
427
+ createdAt: Date.now(),
428
+ metadata: {
429
+ type: "frame_preview" as const,
430
+ data: {
431
+ frameId: args.frameId,
432
+ workspaceId: args.workspaceId,
433
+ name: args.name,
434
+ code: args.code,
435
+ codeType: args.codeType,
436
+ dimensions: args.dimensions,
437
+ },
438
+ },
439
+ });
440
+
441
+ await ctx.db.patch(args.threadId as Id<"threads">, { updatedAt: Date.now() });
442
+ },
443
+ });
444
+
445
+ /** Emit session logs (CoT) to a thread (internal - called via gateway when agent completes a response) */
446
+ export const emitSessionLogs = internalMutation({
447
+ args: {
448
+ threadId: v.string(),
449
+ messageId: v.optional(v.string()), // Associate with specific assistant message
450
+ logs: v.array(v.object({
451
+ type: v.union(
452
+ v.literal("plan"),
453
+ v.literal("thinking"),
454
+ v.literal("task"),
455
+ v.literal("search"),
456
+ v.literal("source"),
457
+ v.literal("file"),
458
+ v.literal("tool"),
459
+ v.literal("text"),
460
+ v.literal("error")
461
+ ),
462
+ label: v.string(),
463
+ status: v.optional(v.union(
464
+ v.literal("pending"),
465
+ v.literal("active"),
466
+ v.literal("complete"),
467
+ v.literal("error")
468
+ )),
469
+ details: v.optional(v.string()),
470
+ data: v.optional(v.any()),
471
+ })),
472
+ status: v.union(
473
+ v.literal("pending"),
474
+ v.literal("starting"),
475
+ v.literal("running"),
476
+ v.literal("completed"),
477
+ v.literal("failed"),
478
+ v.literal("cancelled")
479
+ ),
480
+ },
481
+ handler: async (ctx, args) => {
482
+ // If messageId provided, update that message's metadata
483
+ if (args.messageId) {
484
+ const message = await ctx.db.get(args.messageId as Id<"threadMessages">);
485
+ if (message) {
486
+ const existingMetadata = message.metadata || {};
487
+ await ctx.db.patch(message._id, {
488
+ metadata: {
489
+ ...existingMetadata,
490
+ sessionLogs: {
491
+ logs: args.logs,
492
+ status: args.status,
493
+ },
494
+ },
495
+ });
496
+ return;
497
+ }
498
+ }
499
+
500
+ // Otherwise create a new session_logs message
501
+ await ctx.db.insert("threadMessages", {
502
+ threadId: args.threadId as Id<"threads">,
503
+ role: "assistant",
504
+ content: "", // Empty content - the logs are in metadata
505
+ createdAt: Date.now(),
506
+ metadata: {
507
+ type: "session_logs" as const,
508
+ data: {
509
+ logs: args.logs,
510
+ status: args.status,
511
+ },
512
+ },
513
+ });
514
+
515
+ await ctx.db.patch(args.threadId as Id<"threads">, { updatedAt: Date.now() });
516
+ },
517
+ });
518
+
519
+ /** Emit artifact to a thread (internal - called via gateway when agent saves artifact) */
520
+ export const emitArtifact = internalMutation({
521
+ args: {
522
+ threadId: v.string(),
523
+ artifactId: v.string(),
524
+ name: v.string(),
525
+ type: v.union(
526
+ v.literal("markdown"),
527
+ v.literal("pdf"),
528
+ v.literal("code"),
529
+ v.literal("image"),
530
+ v.literal("json"),
531
+ v.literal("csv"),
532
+ v.literal("text")
533
+ ),
534
+ size: v.optional(v.number()),
535
+ url: v.optional(v.string()),
536
+ preview: v.optional(v.string()),
537
+ },
538
+ handler: async (ctx, args) => {
539
+ await ctx.db.insert("threadMessages", {
540
+ threadId: args.threadId as Id<"threads">,
541
+ role: "assistant",
542
+ content: `Saved artifact: ${args.name}`,
543
+ createdAt: Date.now(),
544
+ metadata: {
545
+ type: "artifact" as const,
546
+ data: {
547
+ artifactId: args.artifactId,
548
+ name: args.name,
549
+ type: args.type,
550
+ size: args.size,
551
+ url: args.url,
552
+ preview: args.preview,
553
+ },
554
+ },
555
+ });
556
+
557
+ await ctx.db.patch(args.threadId as Id<"threads">, { updatedAt: Date.now() });
558
+ },
559
+ });
560
+
561
+ // ============================================
562
+ // Thread Artifacts (for sandbox artifact storage)
563
+ // ============================================
564
+
565
+ /** Save an artifact to a thread (internal - called via gateway from sandbox) */
566
+ export const saveThreadArtifact = internalMutation({
567
+ args: {
568
+ threadId: v.string(),
569
+ sessionId: v.optional(v.string()),
570
+ artifact: v.object({
571
+ type: v.string(),
572
+ name: v.string(),
573
+ content: v.string(),
574
+ metadata: v.optional(v.any()),
575
+ }),
576
+ },
577
+ handler: async (ctx, args) => {
578
+ const artifactId = await ctx.db.insert("threadArtifacts", {
579
+ threadId: args.threadId as Id<"threads">,
580
+ sessionId: args.sessionId as Id<"convexSandboxSessions"> | undefined,
581
+ type: args.artifact.type,
582
+ name: args.artifact.name,
583
+ content: args.artifact.content,
584
+ metadata: args.artifact.metadata,
585
+ createdAt: Date.now(),
586
+ });
587
+
588
+ // Also emit as a thread message for visibility
589
+ await ctx.db.insert("threadMessages", {
590
+ threadId: args.threadId as Id<"threads">,
591
+ role: "assistant",
592
+ content: `Saved artifact: ${args.artifact.name}`,
593
+ createdAt: Date.now(),
594
+ metadata: {
595
+ type: "artifact" as const,
596
+ data: {
597
+ artifactId,
598
+ name: args.artifact.name,
599
+ type: args.artifact.type,
600
+ },
601
+ },
602
+ });
603
+
604
+ await ctx.db.patch(args.threadId as Id<"threads">, { updatedAt: Date.now() });
605
+
606
+ return artifactId;
607
+ },
608
+ });
609
+
610
+ /** List artifacts for a thread */
611
+ export const listThreadArtifacts = query({
612
+ args: {
613
+ threadId: v.id("threads"),
614
+ userId: v.string(),
615
+ },
616
+ handler: async (ctx, args) => {
617
+ await checkThreadAccess(ctx, args.threadId, args.userId);
618
+
619
+ const artifacts = await ctx.db
620
+ .query("threadArtifacts")
621
+ .withIndex("by_thread", q => q.eq("threadId", args.threadId))
622
+ .collect();
623
+
624
+ return artifacts.map(a => ({
625
+ id: a._id,
626
+ name: a.name,
627
+ type: a.type,
628
+ createdAt: a.createdAt,
629
+ }));
630
+ },
631
+ });
632
+
633
+ /** Get a thread artifact by ID */
634
+ export const getThreadArtifact = query({
635
+ args: {
636
+ artifactId: v.id("threadArtifacts"),
637
+ },
638
+ handler: async (ctx, args) => {
639
+ return await ctx.db.get(args.artifactId);
640
+ },
641
+ });
642
+
643
+ /** List thread artifacts (internal - called via gateway from sandbox) */
644
+ export const listThreadArtifactsInternal = internalQuery({
645
+ args: {
646
+ threadId: v.string(),
647
+ },
648
+ handler: async (ctx, args) => {
649
+ const artifacts = await ctx.db
650
+ .query("threadArtifacts")
651
+ .withIndex("by_thread", q => q.eq("threadId", args.threadId as Id<"threads">))
652
+ .collect();
653
+
654
+ return artifacts.map(a => ({
655
+ id: a._id,
656
+ name: a.name,
657
+ type: a.type,
658
+ createdAt: a.createdAt,
659
+ }));
660
+ },
661
+ });
662
+
663
+ // ============================================
664
+ // Debug (Internal Only)
665
+ // ============================================
666
+
667
+ /** Debug query to inspect thread without auth - for CLI debugging only */
668
+ export const debugThread = internalMutation({
669
+ args: { threadId: v.id("threads") },
670
+ handler: async (ctx, args) => {
671
+ const thread = await ctx.db.get(args.threadId);
672
+ if (!thread) return { error: "Thread not found" };
673
+
674
+ const messages = await ctx.db
675
+ .query("threadMessages")
676
+ .withIndex("by_thread", q => q.eq("threadId", args.threadId))
677
+ .collect();
678
+
679
+ // Check for related sandbox sessions by projectId pattern "thread-{threadId}"
680
+ const projectId = `thread-${args.threadId}`;
681
+ const sessions = await ctx.db
682
+ .query("convexSandboxSessions")
683
+ .withIndex("by_project", q => q.eq("projectId", projectId))
684
+ .order("desc")
685
+ .collect();
686
+
687
+ return {
688
+ thread,
689
+ messageCount: messages.length,
690
+ messages: messages.map(m => ({
691
+ id: m._id,
692
+ role: m.role,
693
+ content: m.content?.slice(0, 200) + (m.content?.length > 200 ? '...' : ''),
694
+ type: m.metadata?.type,
695
+ createdAt: new Date(m.createdAt).toISOString(),
696
+ })),
697
+ sessions: sessions.map(s => ({
698
+ id: s._id,
699
+ status: s.status,
700
+ sandboxId: s.sandboxId,
701
+ error: s.error,
702
+ output: s.output,
703
+ allowedKSAs: (s.config as any)?.allowedKSAs,
704
+ createdAt: new Date(s.createdAt).toISOString(),
705
+ })),
706
+ };
707
+ },
708
+ });