@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,272 @@
1
+ /**
2
+ * Prompt-Based Agent Execution
3
+ *
4
+ * Simple prompt execution for tasks that don't need board workflow:
5
+ * - Board planning / generation
6
+ * - Direct research queries
7
+ * - One-off content generation
8
+ */
9
+
10
+ import { v } from "convex/values";
11
+ import { action, internalAction } from "../_generated/server";
12
+ import { api, internal } from "../_generated/api";
13
+ import { getToolDocs, PURPOSE_TOOLS } from "@agent/metadata";
14
+
15
+ // ============================================
16
+ // Simple Prompt Execution
17
+ // ============================================
18
+
19
+ /** Run a simple prompt and get results */
20
+ export const runPrompt = action({
21
+ args: {
22
+ projectId: v.string(),
23
+ prompt: v.string(),
24
+ systemPrompt: v.optional(v.string()),
25
+ tools: v.optional(v.array(v.string())),
26
+ model: v.optional(v.string()),
27
+ },
28
+ handler: async (ctx, args) => {
29
+ try {
30
+ // Combine prompts for Lakitu
31
+ const fullPrompt = args.systemPrompt
32
+ ? `${args.systemPrompt}\n\n---\n\n${args.prompt}`
33
+ : args.prompt;
34
+
35
+ // Start Lakitu sandbox session
36
+ const result = await ctx.runAction(api.workflows.sandboxConvex.startSession, {
37
+ projectId: args.projectId,
38
+ prompt: fullPrompt,
39
+ config: {
40
+ model: args.model || "anthropic/claude-3.5-haiku",
41
+ tools: args.tools || [],
42
+ },
43
+ });
44
+
45
+ if (!result.success) {
46
+ return { success: false, error: result.error || "Failed to start session" };
47
+ }
48
+
49
+ const sessionId = result.sessionId;
50
+
51
+ // Wait for completion
52
+ let session: any = null;
53
+ for (let i = 0; i < 300; i++) { // 5 min max
54
+ await new Promise(r => setTimeout(r, 1000));
55
+ session = await ctx.runQuery(api.workflows.sandboxConvex.getSession, { sessionId });
56
+
57
+ if (session?.status === "completed" || session?.status === "failed") {
58
+ break;
59
+ }
60
+ }
61
+
62
+ if (!session || session.status !== "completed") {
63
+ return { success: false, sessionId, error: "Timeout or session failed" };
64
+ }
65
+
66
+ const output = session.output as any;
67
+ return {
68
+ success: true,
69
+ sessionId,
70
+ output: output?.response || "",
71
+ artifacts: output?.artifacts || [],
72
+ };
73
+ } catch (error: any) {
74
+ return { success: false, error: error.message };
75
+ }
76
+ },
77
+ });
78
+
79
+ // ============================================
80
+ // Board Planning (Generate + Execute)
81
+ // ============================================
82
+
83
+ const boardPlanSchema = v.object({
84
+ title: v.string(),
85
+ description: v.string(),
86
+ stages: v.array(v.object({
87
+ name: v.string(),
88
+ type: v.union(v.literal("agent"), v.literal("human")),
89
+ description: v.string(),
90
+ skillId: v.optional(v.string()),
91
+ order: v.number(),
92
+ })),
93
+ });
94
+
95
+ /** Generate a board plan from user prompt */
96
+ export const generateBoardPlan = action({
97
+ args: {
98
+ workspaceId: v.string(),
99
+ userPrompt: v.string(),
100
+ },
101
+ handler: async (ctx, args) => {
102
+ const prompt = `Create a workflow board for: ${args.userPrompt}
103
+
104
+ Write a JSON file to /home/user/workspace/board_plan.json with this structure:
105
+ {
106
+ "title": "Board name",
107
+ "description": "What this workflow accomplishes",
108
+ "stages": [
109
+ {"name": "Stage Name", "type": "agent" or "human", "description": "What happens", "order": 0}
110
+ ]
111
+ }
112
+
113
+ Rules:
114
+ - Create 3-6 stages
115
+ - First stage: usually "human" (user provides input)
116
+ - Middle stages: "agent" (AI automated tasks)
117
+ - Last stage: "human" (review) or "agent" (deliver result)`;
118
+
119
+ try {
120
+ // Start Lakitu sandbox session
121
+ const result = await ctx.runAction(api.workflows.sandboxConvex.startSession, {
122
+ projectId: args.workspaceId,
123
+ prompt,
124
+ config: {
125
+ model: "anthropic/claude-3.5-haiku",
126
+ purpose: "board-planning",
127
+ },
128
+ });
129
+
130
+ if (!result.success) {
131
+ return { success: false, error: result.error || "Failed to start session" };
132
+ }
133
+
134
+ const sessionId = result.sessionId;
135
+
136
+ // Wait for completion
137
+ let session: any = null;
138
+ for (let i = 0; i < 120; i++) { // 2 min max for planning
139
+ await new Promise(r => setTimeout(r, 1000));
140
+ session = await ctx.runQuery(api.workflows.sandboxConvex.getSession, { sessionId });
141
+
142
+ if (session?.status === "completed" || session?.status === "failed") {
143
+ break;
144
+ }
145
+ }
146
+
147
+ if (!session || session.status !== "completed") {
148
+ return { success: false, sessionId, error: "Timeout or session failed" };
149
+ }
150
+
151
+ const output = session.output as any;
152
+ const response = output?.response || "";
153
+
154
+ // Try to parse plan from response
155
+ if (response) {
156
+ const jsonMatch = response.match(/\{[\s\S]*"title"[\s\S]*"stages"[\s\S]*\}/);
157
+ if (jsonMatch) {
158
+ try {
159
+ const plan = JSON.parse(jsonMatch[0]);
160
+ if (plan.title && plan.stages) {
161
+ return { success: true, sessionId, plan };
162
+ }
163
+ } catch {}
164
+ }
165
+ }
166
+
167
+ return {
168
+ success: false,
169
+ sessionId,
170
+ error: "Agent did not generate a structured plan",
171
+ debug: { response: response?.substring(0, 500) },
172
+ };
173
+ } catch (error: any) {
174
+ return { success: false, error: error.message };
175
+ }
176
+ },
177
+ });
178
+
179
+ /** Execute an approved board plan */
180
+ export const executeBoardPlan = action({
181
+ args: {
182
+ workspaceId: v.string(),
183
+ plan: boardPlanSchema,
184
+ },
185
+ handler: async (ctx, args) => {
186
+ const { workspaceId, plan } = args;
187
+
188
+ try {
189
+ const boardId = await ctx.runMutation(api.features.kanban.boards.create, {
190
+ workspaceId,
191
+ name: plan.title,
192
+ description: plan.description,
193
+ });
194
+
195
+ const taskIds: string[] = [];
196
+ for (const stage of plan.stages) {
197
+ const taskId = await ctx.runMutation(api.features.kanban.boards.addTask, {
198
+ boardId,
199
+ name: stage.name,
200
+ description: stage.description,
201
+ stageType: stage.type,
202
+ skillId: stage.skillId,
203
+ order: stage.order,
204
+ });
205
+ taskIds.push(taskId);
206
+ }
207
+
208
+ return { success: true, boardId, taskIds };
209
+ } catch (error: any) {
210
+ return { success: false, error: error.message };
211
+ }
212
+ },
213
+ });
214
+
215
+ // ============================================
216
+ // Research Queries
217
+ // ============================================
218
+
219
+ /** Run a research query */
220
+ export const runResearch = action({
221
+ args: {
222
+ projectId: v.string(),
223
+ query: v.string(),
224
+ depth: v.optional(v.union(v.literal("quick"), v.literal("thorough"))),
225
+ },
226
+ handler: async (ctx, args) => {
227
+ const tools = ["web", "vfs"];
228
+ const prompt = args.depth === "thorough"
229
+ ? `Research thoroughly: ${args.query}\n\nSave findings to /home/user/workspace/research.md`
230
+ : `Quick research: ${args.query}\n\nProvide a concise summary.`;
231
+
232
+ try {
233
+ // Start Lakitu sandbox session
234
+ const result = await ctx.runAction(api.workflows.sandboxConvex.startSession, {
235
+ projectId: args.projectId,
236
+ prompt,
237
+ config: { tools, purpose: "research" },
238
+ });
239
+
240
+ if (!result.success) {
241
+ return { success: false, error: result.error || "Failed to start session" };
242
+ }
243
+
244
+ const sessionId = result.sessionId;
245
+
246
+ // Wait for completion
247
+ let session: any = null;
248
+ for (let i = 0; i < 300; i++) { // 5 min max for research
249
+ await new Promise(r => setTimeout(r, 1000));
250
+ session = await ctx.runQuery(api.workflows.sandboxConvex.getSession, { sessionId });
251
+
252
+ if (session?.status === "completed" || session?.status === "failed") {
253
+ break;
254
+ }
255
+ }
256
+
257
+ if (!session || session.status !== "completed") {
258
+ return { success: false, sessionId, error: "Timeout or session failed" };
259
+ }
260
+
261
+ const output = session.output as any;
262
+ return {
263
+ success: true,
264
+ sessionId,
265
+ output: output?.response || "",
266
+ artifacts: output?.artifacts || [],
267
+ };
268
+ } catch (error: any) {
269
+ return { success: false, error: error.message };
270
+ }
271
+ },
272
+ });
@@ -0,0 +1,374 @@
1
+ /**
2
+ * Thread-Based Agent Chat
3
+ *
4
+ * Interactive chat with agents outside of board workflows:
5
+ * - Standalone chat threads
6
+ * - Multi-turn conversations
7
+ * - Skill-based responses
8
+ * - Intent Schema pre-analysis (runs parallel to sandbox warm-up)
9
+ */
10
+
11
+ import { v } from "convex/values";
12
+ import { action, query } from "../_generated/server";
13
+ import { api, internal } from "../_generated/api";
14
+ import { Doc, Id } from "../_generated/dataModel";
15
+ import { type IntentSchema, createDefaultIntentSchema } from "../intentSchema/types";
16
+ import { CORE_KSAS, getKSANames } from "../ksaPolicy";
17
+
18
+ // ============================================
19
+ // Chat Operations
20
+ // ============================================
21
+
22
+ /** Send a message to a thread and get agent response */
23
+ export const sendMessage = action({
24
+ args: {
25
+ threadId: v.id("threads"),
26
+ userId: v.string(),
27
+ content: v.string(),
28
+ skillIds: v.optional(v.array(v.string())),
29
+ // Sandbox config from main app (has env var access)
30
+ sandboxConfig: v.optional(v.object({
31
+ sandboxJwt: v.string(),
32
+ gatewayUrl: v.string(),
33
+ preCreatedSandbox: v.object({
34
+ sandboxId: v.string(),
35
+ sandboxUrl: v.string(),
36
+ timings: v.any(),
37
+ fromPool: v.boolean(),
38
+ deletedKSAs: v.array(v.string()),
39
+ }),
40
+ })),
41
+ },
42
+ handler: async (ctx, args) => {
43
+ // Get thread to verify access and get context
44
+ const threads = await ctx.runQuery(api.workflows.crudThreads.listThreads, {
45
+ userId: args.userId,
46
+ });
47
+ const thisThread = threads.find((t: Doc<"threads">) => t._id === args.threadId);
48
+ if (!thisThread) throw new Error("Thread not found or unauthorized");
49
+
50
+ // Save user message
51
+ await ctx.runMutation(api.workflows.crudThreads.sendThreadMessage, {
52
+ threadId: args.threadId,
53
+ userId: args.userId,
54
+ content: args.content,
55
+ role: "user",
56
+ });
57
+
58
+ // Get message history for context
59
+ const messages = await ctx.runQuery(api.workflows.crudThreads.getThreadMessages, {
60
+ threadId: args.threadId,
61
+ userId: args.userId,
62
+ });
63
+
64
+ // Build context from history
65
+ const historyContext = messages
66
+ .slice(-10) // Last 10 messages for context
67
+ .map((m: Doc<"threadMessages">) => `${m.role}: ${m.content}`)
68
+ .join('\n\n');
69
+
70
+ // Build system prompt based on skills
71
+ let systemPrompt = "You are a helpful AI assistant.";
72
+ let tools: string[] = ["vfs"];
73
+
74
+ if (args.skillIds?.length) {
75
+ const skills = await ctx.runQuery(api.workflows.crudSkills.getByIds, {
76
+ skillIds: args.skillIds,
77
+ });
78
+
79
+ interface SkillDoc { prompt?: string; toolIds?: string[] }
80
+ const skillPrompts = skills.filter((s: SkillDoc) => s.prompt).map((s: SkillDoc) => s.prompt);
81
+ if (skillPrompts.length) {
82
+ systemPrompt += `\n\n## Skills\n${skillPrompts.join('\n\n')}`;
83
+ }
84
+
85
+ tools = [...tools, ...skills.flatMap((s: SkillDoc) => s.toolIds || [])];
86
+ }
87
+
88
+ // Build full prompt with history
89
+ const prompt = historyContext
90
+ ? `Previous conversation:\n${historyContext}\n\nUser: ${args.content}`
91
+ : args.content;
92
+
93
+ try {
94
+ // Combine system prompt and user prompt for Lakitu agent
95
+ const fullPrompt = `${systemPrompt}\n\n---\n\n${prompt}`;
96
+
97
+ // ============================================
98
+ // INTENT SCHEMA: Generate in parallel with sandbox startup
99
+ // This provides structured guidance to the agent about:
100
+ // - Which KSAs to prioritize
101
+ // - Goals and deliverables
102
+ // - User policy constraints
103
+ // ============================================
104
+ let intentSchema: IntentSchema | null = null;
105
+
106
+ try {
107
+ // Generate intent schema with timeout (don't block if slow)
108
+ intentSchema = await ctx.runAction(
109
+ internal.intentSchema.generateIntentSchemaWithTimeout,
110
+ {
111
+ prompt: args.content, // Original user content (not full prompt)
112
+ threadContext: historyContext || undefined,
113
+ skillIds: args.skillIds,
114
+ timeoutMs: 3000, // 3 second timeout
115
+ }
116
+ );
117
+
118
+ if (intentSchema) {
119
+ console.log(
120
+ `[agentThread] Intent schema generated: ${intentSchema.intent.summary} (${intentSchema.meta.confidence} confidence, ${intentSchema.meta.latencyMs}ms)`
121
+ );
122
+ }
123
+ } catch (intentError) {
124
+ console.warn(`[agentThread] Intent schema generation failed: ${intentError}`);
125
+ // Continue without intent schema - agent will work fine without it
126
+ }
127
+
128
+ // Fallback to default schema if generation failed
129
+ if (!intentSchema) {
130
+ const enabledKSAs = args.skillIds?.length
131
+ ? [...CORE_KSAS, ...args.skillIds]
132
+ : getKSANames();
133
+ intentSchema = createDefaultIntentSchema(args.content, enabledKSAs);
134
+ }
135
+
136
+ // Determine allowed KSAs from intent schema or UI selection
137
+ // Priority: intent schema policy > UI skillIds > all KSAs
138
+ const allowedKSAs = intentSchema?.policy?.enabledKSAs?.length
139
+ ? intentSchema.policy.enabledKSAs
140
+ : args.skillIds?.length
141
+ ? [...CORE_KSAS, ...args.skillIds]
142
+ : undefined; // undefined = all KSAs allowed
143
+
144
+ // Start Lakitu sandbox session with user context and intent schema
145
+ const result = await ctx.runAction(api.workflows.sandboxConvex.startSession, {
146
+ projectId: `thread-${args.threadId}`,
147
+ prompt: fullPrompt,
148
+ config: {
149
+ tools,
150
+ userId: args.userId,
151
+ orgId: thisThread.orgId,
152
+ // Pass workspace context if thread is in a workspace
153
+ workspaceId: thisThread.workspaceId,
154
+ threadId: args.threadId,
155
+ // Include intent schema for agent guidance
156
+ intentSchema,
157
+ // Pass allowed KSAs for policy enforcement
158
+ allowedKSAs,
159
+ // Pass pre-created sandbox config from main app (if provided)
160
+ ...(args.sandboxConfig && {
161
+ sandboxJwt: args.sandboxConfig.sandboxJwt,
162
+ preCreatedSandbox: args.sandboxConfig.preCreatedSandbox,
163
+ }),
164
+ },
165
+ });
166
+
167
+ if (!result.success) {
168
+ return { success: false, error: result.error || "Failed to start session" };
169
+ }
170
+
171
+ const sessionId = result.sessionId;
172
+
173
+ // Wait for completion
174
+ let session: any = null;
175
+ for (let i = 0; i < 180; i++) { // 3 min max for chat
176
+ await new Promise(r => setTimeout(r, 1000));
177
+ session = await ctx.runQuery(api.workflows.sandboxConvex.getSession, { sessionId });
178
+
179
+ if (session?.status === "completed" || session?.status === "failed") {
180
+ break;
181
+ }
182
+ }
183
+
184
+ if (!session || session.status !== "completed") {
185
+ return { success: false, sessionId, error: "Timeout or session failed" };
186
+ }
187
+
188
+ const output = (session.output as any)?.response || "";
189
+
190
+ // Get session logs for persistence
191
+ const sessionWithLogs = await ctx.runQuery(api.workflows.sandboxConvex.getActiveSessionForThread, {
192
+ threadId: args.threadId,
193
+ });
194
+
195
+ // Filter and format logs for persistence (remove raw/technical output)
196
+ const rawPatterns = [
197
+ /^\d+\s*\|/, // Line numbers like "42 |"
198
+ /JSON\.stringify|JSON\.parse/, // Code snippets
199
+ /\.(ts|js|json|svelte):\d+/, // File:line references
200
+ /at\s+\w+\s+\(/, // Stack traces
201
+ /Error:|Exception:|throw\s+new/, // Error traces
202
+ /Task IDs:|task-\d+/, // Internal task IDs
203
+ /Bun v\d+|Node v\d+/, // Runtime info
204
+ /Local Convex (error|exception)/i, // Internal Convex errors
205
+ /body:\s*JSON|response\.(status|text)/, // HTTP code
206
+ /await\s+\w+|async\s+function/, // Async code
207
+ ];
208
+
209
+ const validTypes = ['plan', 'thinking', 'task', 'search', 'source', 'file', 'tool', 'text', 'error'];
210
+
211
+ const persistLogs = (sessionWithLogs?.logs || [])
212
+ .filter((log: any) => {
213
+ if (!log.type || !log.label) return false;
214
+ if (!validTypes.includes(log.type)) return false;
215
+ const label = log.label;
216
+ for (const pattern of rawPatterns) {
217
+ if (pattern.test(label)) return false;
218
+ }
219
+ return true;
220
+ })
221
+ .map((log: any) => ({
222
+ type: log.type,
223
+ label: log.label,
224
+ status: log.status || 'complete',
225
+ details: log.details,
226
+ data: log.data,
227
+ }));
228
+
229
+ // Calculate generation time
230
+ const generationTime = session.completedAt && session.createdAt
231
+ ? session.completedAt - session.createdAt
232
+ : undefined;
233
+
234
+ // Save assistant response with session logs metadata
235
+ if (output) {
236
+ await ctx.runMutation(api.workflows.crudThreads.sendThreadMessage, {
237
+ threadId: args.threadId,
238
+ userId: args.userId,
239
+ content: output,
240
+ role: "assistant",
241
+ metadata: persistLogs.length > 0 || generationTime ? {
242
+ sessionLogs: persistLogs.length > 0 ? {
243
+ logs: persistLogs,
244
+ status: session.status,
245
+ } : undefined,
246
+ generationTime,
247
+ } : undefined,
248
+ });
249
+ }
250
+
251
+ return {
252
+ success: true,
253
+ sessionId,
254
+ output,
255
+ };
256
+ } catch (error: unknown) {
257
+ const errorMessage = error instanceof Error ? error.message : String(error);
258
+ return { success: false, error: errorMessage };
259
+ }
260
+ },
261
+ });
262
+
263
+ /** Start a new thread with initial message */
264
+ export const startThread = action({
265
+ args: {
266
+ userId: v.string(),
267
+ content: v.string(),
268
+ title: v.optional(v.string()),
269
+ skillIds: v.optional(v.array(v.string())),
270
+ orgId: v.optional(v.string()),
271
+ boardId: v.optional(v.string()),
272
+ // Sandbox config from main app (has env var access)
273
+ sandboxConfig: v.optional(v.object({
274
+ sandboxJwt: v.string(),
275
+ gatewayUrl: v.string(),
276
+ preCreatedSandbox: v.object({
277
+ sandboxId: v.string(),
278
+ sandboxUrl: v.string(),
279
+ timings: v.any(),
280
+ fromPool: v.boolean(),
281
+ deletedKSAs: v.array(v.string()),
282
+ }),
283
+ })),
284
+ },
285
+ handler: async (ctx, args) => {
286
+ // Create thread
287
+ const threadId = await ctx.runMutation(api.workflows.crudThreads.createThread, {
288
+ userId: args.userId,
289
+ title: args.title || args.content.slice(0, 50),
290
+ orgId: args.orgId,
291
+ boardId: args.boardId,
292
+ });
293
+
294
+ // Send initial message (pass sandboxConfig if provided)
295
+ const result = await ctx.runAction(api.workflows.agentThread.sendMessage, {
296
+ threadId,
297
+ userId: args.userId,
298
+ content: args.content,
299
+ skillIds: args.skillIds,
300
+ sandboxConfig: args.sandboxConfig,
301
+ });
302
+
303
+ return {
304
+ threadId,
305
+ ...result,
306
+ };
307
+ },
308
+ });
309
+
310
+ /** Continue a conversation with context */
311
+ export const continueThread = action({
312
+ args: {
313
+ threadId: v.id("threads"),
314
+ userId: v.string(),
315
+ content: v.string(),
316
+ },
317
+ handler: async (ctx, args) => {
318
+ // Get thread to find associated skills
319
+ const threads = await ctx.runQuery(api.workflows.crudThreads.listThreads, {
320
+ userId: args.userId,
321
+ });
322
+ const thread = threads.find((t: Doc<"threads">) => t._id === args.threadId);
323
+
324
+ // Get skills from board if associated
325
+ const skillIds: string[] = [];
326
+ if (thread?.boardId) {
327
+ // Could fetch board skills here if needed
328
+ }
329
+
330
+ return await ctx.runAction(api.workflows.agentThread.sendMessage, {
331
+ threadId: args.threadId,
332
+ userId: args.userId,
333
+ content: args.content,
334
+ skillIds: skillIds.length ? skillIds : undefined,
335
+ });
336
+ },
337
+ });
338
+
339
+ // ============================================
340
+ // Thread Queries
341
+ // ============================================
342
+
343
+ /** Get thread with messages */
344
+ export const getThreadWithMessages = query({
345
+ args: { threadId: v.id("threads"), userId: v.string() },
346
+ handler: async (ctx, args) => {
347
+ const threads = await ctx.db
348
+ .query("threads")
349
+ .withIndex("by_user", q => q.eq("userId", args.userId))
350
+ .collect();
351
+
352
+ const thread = threads.find(t => t._id === args.threadId);
353
+ if (!thread) return null;
354
+
355
+ const messages = await ctx.db
356
+ .query("threadMessages")
357
+ .withIndex("by_thread", q => q.eq("threadId", args.threadId))
358
+ .collect();
359
+
360
+ return { ...thread, messages };
361
+ },
362
+ });
363
+
364
+ /** List user's recent threads */
365
+ export const listRecentThreads = query({
366
+ args: { userId: v.string(), limit: v.optional(v.number()) },
367
+ handler: async (ctx, args) => {
368
+ return await ctx.db
369
+ .query("threads")
370
+ .withIndex("by_user", q => q.eq("userId", args.userId))
371
+ .order("desc")
372
+ .take(args.limit || 20);
373
+ },
374
+ });