@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,451 @@
1
+ import { v } from "convex/values";
2
+ import { WorkflowManager } from "@convex-dev/workflow";
3
+ import { components, api, internal } from "../_generated/api";
4
+ import { action, internalAction } from "../_generated/server";
5
+ import { buildSystemPrompt } from "../utils/kanbanContext";
6
+
7
+ const workflow = new WorkflowManager(components.workflow);
8
+
9
+ /**
10
+ * Card Execution Workflow - Durable multi-stage pipeline
11
+ *
12
+ * Each step waits for completion before proceeding:
13
+ * 1. Setup - Load card, board, task
14
+ * 2. Run Agent - Execute in E2B sandbox (waits for completion)
15
+ * 3. Collect Artifacts - Gather files from sandbox workspace
16
+ * 4. QA Check - Verify deliverables are met
17
+ * 5. Advance/Block - Move to next stage or block workflow
18
+ */
19
+ export const cardExecutionWorkflow = workflow.define({
20
+ args: {
21
+ cardId: v.string(),
22
+ boardId: v.string(),
23
+ taskId: v.string(),
24
+ runId: v.string(),
25
+ },
26
+ handler: async (step, args) => {
27
+ const { cardId, boardId, taskId, runId } = args;
28
+
29
+ // ═══════════════════════════════════════════════════════════════════════
30
+ // STEP 1: SETUP - Load entities and validate
31
+ // ═══════════════════════════════════════════════════════════════════════
32
+ const card = await step.runQuery(internal.features.kanban.boards.getCardInternal, { id: cardId });
33
+ if (!card) throw new Error("Card not found");
34
+
35
+ const board = await step.runQuery(internal.features.kanban.boards.getInternal, { id: boardId });
36
+ if (!board) throw new Error("Board not found");
37
+
38
+ const task = board.tasks.find((t: any) => t._id === taskId);
39
+ if (!task) throw new Error("Task not found");
40
+
41
+ // Skip if not an agent stage
42
+ const isAgentStage = task.stageType === "agent" || !task.stageType;
43
+ const hasAutomation = task.automation?.enabled;
44
+ if (!isAgentStage && !hasAutomation) {
45
+ console.log(`⏭️ Stage ${task.name} is not agent type, skipping`);
46
+ return { skipped: true, cardId };
47
+ }
48
+
49
+ // Mark as running
50
+ await step.runMutation(internal.features.kanban.executor.updateCardRunStatus, { runId, status: "running" });
51
+ await step.runMutation(internal.features.kanban.boards.updateCardStatusInternal, { id: cardId, status: "running" });
52
+ console.log(`📍 Stage "${task.name}" starting for card ${cardId}`);
53
+
54
+ // ═══════════════════════════════════════════════════════════════════════
55
+ // STEP 2: RUN AGENT - Execute in E2B sandbox (waits for completion)
56
+ // ═══════════════════════════════════════════════════════════════════════
57
+
58
+ // Query artifacts from table (single source of truth) for context
59
+ const artifacts = await step.runQuery(api.features.kanban.artifacts.listCardArtifacts, { cardId });
60
+ const cardWithArtifacts = {
61
+ ...card,
62
+ context: {
63
+ ...card.context,
64
+ artifacts: artifacts.map((a: any) => ({ id: a._id, type: a.type, name: a.name, createdAt: a.createdAt })),
65
+ },
66
+ };
67
+
68
+ const systemPrompt = buildSystemPrompt(board, task, cardWithArtifacts as any, board.tasks);
69
+ let userPrompt = task.automation?.prompt ||
70
+ (card.context?.variables?.message as string) ||
71
+ `Complete the "${task.name}" stage for this project.`;
72
+
73
+ // Check for retry context and add problem details to prompt
74
+ const retryContext = (card.context?.variables as any)?._retryContext;
75
+ if (retryContext?.isRetry) {
76
+ console.log(`🔄 This is retry #${retryContext.retryCount} - adding problem context to prompt`);
77
+ const problem = retryContext.previousProblem;
78
+ const retryGuidance = `
79
+
80
+ ## ⚠️ RETRY CONTEXT - IMPORTANT
81
+
82
+ This is retry attempt #${retryContext.retryCount}. The previous attempt had issues:
83
+
84
+ **Problem:** ${problem.type}
85
+ **Details:** ${problem.message}
86
+ **Missing deliverables:** ${problem.missing?.join(', ') || 'none'}
87
+ **Already produced:** ${problem.produced?.map((p: any) => `${p.name} (${p.type})`).join(', ') || 'nothing'}
88
+
89
+ **DO NOT** recreate artifacts that were already produced successfully.
90
+ **DO** create only the missing deliverables listed above.
91
+ **VERIFY** each deliverable is saved before completing.
92
+
93
+ Use beads tracking to ensure all required deliverables are created.
94
+ `;
95
+ userPrompt = retryGuidance + "\n\n---\n\n" + userPrompt;
96
+ }
97
+
98
+ // Base tools always available
99
+ // Note: OpenCode has built-in tools (read, write, edit, glob, grep) for file ops
100
+ // We only pass "automation" for artifact management (save to Convex)
101
+ const baseTools = ["automation"];
102
+
103
+ // Derive tools from deliverables - each deliverable type brings its tool
104
+ // Tool names must match the actual Lakitu tool names (underscore format)
105
+ const deliverableToolMap: Record<string, string[]> = {
106
+ pdf: ["pdf_create"], // The actual tool name in Lakitu
107
+ // For markdown/doc/etc, the agent uses automation_saveArtifact directly
108
+ };
109
+
110
+ const deliverables = (task.deliverables || []).map((d: any) => ({ name: d.name, type: d.type }));
111
+ const deliverableTools = deliverables.flatMap((d: any) => deliverableToolMap[d.type] || []);
112
+
113
+ // Derive tools and prompts from skills
114
+ const taskSkillIds = (task.skills || []).map((s: any) => s.id);
115
+ let skillTools: string[] = [];
116
+ let skillPrompts: string[] = [];
117
+
118
+ if (taskSkillIds.length > 0) {
119
+ const skills = await step.runQuery(api.workflows.crudSkills.getByIds, { skillIds: taskSkillIds });
120
+ skillTools = skills.flatMap((skill: any) => skill.toolIds || []);
121
+ skillPrompts = skills.filter((s: any) => s.prompt).map((s: any) => s.prompt);
122
+ }
123
+
124
+ // Explicit tools from automation config
125
+ const explicitTools = task.automation?.tools || [];
126
+
127
+ // Combine all tools (deduplicated)
128
+ const rawTools = [...new Set([...baseTools, ...deliverableTools, ...skillTools, ...explicitTools])];
129
+
130
+ // Map skill tool IDs to OpenCode's built-in tool names
131
+ // This allows skills to use semantic tool names that get translated to OpenCode's tools
132
+ const toolNameMap: Record<string, string> = {
133
+ web_search: "websearch", // Skill's web_search -> OpenCode's websearch
134
+ web_scrape: "webfetch", // Skill's web_scrape -> OpenCode's webfetch
135
+ web_fetch: "webfetch", // Alternative name
136
+ search: "websearch", // Simple alias
137
+ scrape: "webfetch", // Simple alias
138
+ };
139
+
140
+ const tools = rawTools.map((t) => toolNameMap[t] || t);
141
+
142
+ // Build enhanced system prompt with skill guidance
143
+ let enhancedSystemPrompt = systemPrompt;
144
+ if (skillPrompts.length > 0) {
145
+ enhancedSystemPrompt += `\n\n## SKILL GUIDANCE\n${skillPrompts.join('\n\n')}`;
146
+ }
147
+
148
+ // Log task configuration
149
+ const goals = (task.goals || []).filter((g: any) => g.text).map((g: any) => g.text);
150
+ console.log(`🚀 Running agent for stage "${task.name}"`);
151
+ console.log(`🎯 Goals: ${goals.join("; ") || "none"}`);
152
+ console.log(`📚 Skills: ${taskSkillIds.join(", ") || "none"}`);
153
+ console.log(`🔧 Tools: ${tools.join(", ")}`);
154
+ console.log(`📋 Deliverables: ${deliverables.map((d: any) => `${d.name}(${d.type})`).join(", ") || "none"}`);
155
+
156
+ // Initialize Beads issues for this stage (goals become tasks, deliverables block completion)
157
+ const beadsConfig = {
158
+ stage: {
159
+ id: taskId.toString(),
160
+ name: task.name,
161
+ },
162
+ goals: goals.map((text: string, i: number) => ({
163
+ id: `goal-${i}`,
164
+ title: text,
165
+ type: "goal" as const,
166
+ })),
167
+ deliverables: deliverables.map((d: any, i: number) => ({
168
+ id: `deliv-${i}`,
169
+ title: `Produce: ${d.name}`,
170
+ type: "deliverable" as const,
171
+ fileType: d.type,
172
+ })),
173
+ };
174
+
175
+ let agentResult: { output?: string; artifacts?: any[]; sandboxId?: string; error?: string };
176
+
177
+ try {
178
+ // This action WAITS for the agent to complete
179
+ agentResult = await step.runAction(internal.workflows.agentBoard.runAgentStep, {
180
+ cardId, runId, boardId,
181
+ projectId: cardId, // Use cardId so frontend can subscribe to logs by card
182
+ systemPrompt: enhancedSystemPrompt,
183
+ userPrompt,
184
+ model: task.automation?.model || "anthropic/claude-haiku-4.5",
185
+ tools,
186
+ stageName: task.name,
187
+ deliverables,
188
+ beadsConfig,
189
+ });
190
+
191
+ if (agentResult.error) {
192
+ throw new Error(agentResult.error);
193
+ }
194
+
195
+ console.log(`✅ Agent completed for stage "${task.name}"`);
196
+
197
+ // Save agent response to card messages (for thread persistence)
198
+ if (agentResult.output) {
199
+ await step.runMutation(internal.features.kanban.boards.addCardMessageInternal, {
200
+ cardId,
201
+ message: {
202
+ id: `agent-${taskId}-${Date.now()}`,
203
+ role: "assistant" as const,
204
+ content: agentResult.output,
205
+ type: "response",
206
+ },
207
+ });
208
+ }
209
+ } catch (error: any) {
210
+ if (error.message === "Session cancelled by user") {
211
+ console.log(`🛑 Workflow cancelled for card ${cardId}`);
212
+ return { success: false, cancelled: true };
213
+ }
214
+ console.error(`❌ Agent failed:`, error.message);
215
+ await step.runMutation(internal.features.kanban.executor.failCardRun, { runId, error: error.message });
216
+ await step.runMutation(internal.features.kanban.boards.updateCardStatusInternal, { id: cardId, status: "error" });
217
+ throw error;
218
+ }
219
+
220
+ // ═══════════════════════════════════════════════════════════════════════
221
+ // STEP 3: SAVE ARTIFACTS - Persist collected files to database
222
+ // ═══════════════════════════════════════════════════════════════════════
223
+ const collectedArtifacts = agentResult.artifacts || [];
224
+ console.log(`💾 Saving ${collectedArtifacts.length} collected artifacts to DB...`);
225
+
226
+ for (const artifact of collectedArtifacts) {
227
+ try {
228
+ await step.runAction(api.features.kanban.artifacts.saveArtifactWithBackup, {
229
+ cardId,
230
+ runId,
231
+ artifact: {
232
+ type: artifact.type || "markdown",
233
+ name: `${task.name}: ${artifact.name}`,
234
+ content: artifact.content,
235
+ metadata: {
236
+ path: artifact.path,
237
+ collectedAt: Date.now(),
238
+ },
239
+ },
240
+ });
241
+ console.log(` ✅ Saved: ${artifact.name}`);
242
+ } catch (e: any) {
243
+ console.warn(` ⚠️ Failed to save ${artifact.name}: ${e.message}`);
244
+ }
245
+ }
246
+
247
+ // ═══════════════════════════════════════════════════════════════════════
248
+ // STEP 4: QA CHECK - Verify deliverables and advance/block
249
+ // ═══════════════════════════════════════════════════════════════════════
250
+ console.log(`🔍 Running QA check...`);
251
+
252
+ await step.runMutation(internal.features.kanban.executor.completeCardRun, {
253
+ runId,
254
+ output: {
255
+ summary: agentResult.output || "Stage completed",
256
+ artifacts: collectedArtifacts,
257
+ },
258
+ });
259
+
260
+ console.log(`✅ Stage "${task.name}" workflow complete`);
261
+
262
+ return {
263
+ success: true,
264
+ cardId,
265
+ runId,
266
+ stageName: task.name,
267
+ artifactCount: agentResult.artifacts?.length || 0,
268
+ };
269
+ },
270
+ });
271
+
272
+ /**
273
+ * Run agent step - Executes in Lakitu E2B sandbox and WAITS for completion
274
+ * Uses self-hosted Convex with Agent SDK (Lakitu template)
275
+ */
276
+ export const runAgentStep = internalAction({
277
+ args: {
278
+ cardId: v.string(),
279
+ runId: v.string(),
280
+ boardId: v.string(),
281
+ projectId: v.string(),
282
+ systemPrompt: v.string(),
283
+ userPrompt: v.string(),
284
+ model: v.string(),
285
+ tools: v.array(v.string()),
286
+ stageName: v.string(),
287
+ deliverables: v.optional(v.array(v.object({ name: v.string(), type: v.string() }))),
288
+ beadsConfig: v.optional(v.object({
289
+ stage: v.object({ id: v.string(), name: v.string() }),
290
+ goals: v.array(v.object({ id: v.string(), title: v.string(), type: v.literal("goal") })),
291
+ deliverables: v.array(v.object({ id: v.string(), title: v.string(), type: v.literal("deliverable"), fileType: v.string() })),
292
+ })),
293
+ },
294
+ handler: async (ctx, args) => {
295
+ try {
296
+ // Build prompt with deliverable instructions
297
+ let agentPrompt = args.userPrompt;
298
+
299
+ // Note: Detailed deliverable instructions are in Lakitu system prompt.
300
+ // The systemPrompt from buildSystemPrompt() already includes deliverables list.
301
+ // We just pass the prompt through without adding duplicate instructions.
302
+
303
+ // Combine system prompt and user prompt for Lakitu agent
304
+ const fullPrompt = args.systemPrompt
305
+ ? `${args.systemPrompt}\n\n---\n\n${agentPrompt}`
306
+ : agentPrompt;
307
+
308
+ console.log(`📍 Starting Lakitu session for card ${args.cardId}`);
309
+
310
+ // Get board to extract userId/orgId for session config
311
+ const board = await ctx.runQuery(internal.features.kanban.boards.getInternal, { id: args.boardId });
312
+ if (!board) {
313
+ return { error: "Board not found" };
314
+ }
315
+
316
+ // Start Lakitu sandbox session (creates session AND starts sandbox)
317
+ const result = await ctx.runAction(api.workflows.sandboxConvex.startSession, {
318
+ projectId: args.projectId,
319
+ prompt: fullPrompt,
320
+ config: {
321
+ model: args.model,
322
+ tools: args.tools,
323
+ cardId: args.cardId,
324
+ runId: args.runId,
325
+ stageName: args.stageName,
326
+ deliverables: args.deliverables,
327
+ // Include userId/orgId so gateway can inject them into internal calls
328
+ userId: board.userId,
329
+ orgId: board.orgId,
330
+ },
331
+ });
332
+
333
+ if (!result.success) {
334
+ return { error: result.error || "Failed to start session" };
335
+ }
336
+
337
+ const sessionId = result.sessionId;
338
+ console.log(`⏳ Waiting for Lakitu completion (session ${sessionId})...`);
339
+
340
+ // Poll for completion (session is running asynchronously)
341
+ for (let i = 0; i < 600; i++) { // 10 min max
342
+ await new Promise(r => setTimeout(r, 1000));
343
+
344
+ const session = await ctx.runQuery(api.workflows.sandboxConvex.getSession, {
345
+ sessionId,
346
+ });
347
+
348
+ if (!session) {
349
+ return { error: "Session not found" };
350
+ }
351
+
352
+ if (session.status === "completed") {
353
+ console.log(`✅ Lakitu completed for session ${sessionId}`);
354
+ const output = session.output as any;
355
+ return {
356
+ output: output?.response || "",
357
+ artifacts: output?.artifacts || [],
358
+ sandboxId: session.sandboxId,
359
+ };
360
+ }
361
+
362
+ if (session.status === "failed") {
363
+ return { error: session.error || "Sandbox failed" };
364
+ }
365
+
366
+ if (session.status === "cancelled") {
367
+ console.log(`🛑 Session was cancelled`);
368
+ return { error: "Session cancelled by user" };
369
+ }
370
+
371
+ // Every 30s: log status
372
+ if (i % 30 === 0 && i > 0) {
373
+ const sessionWithLogs = await ctx.runQuery(api.workflows.sandboxConvex.getSessionWithLogs, {
374
+ sessionId,
375
+ });
376
+ const logCount = sessionWithLogs?.logs?.length || 0;
377
+ console.log(`⏳ [${i}s] status=${session.status}, logs=${logCount}`);
378
+ }
379
+ }
380
+
381
+ console.error(`⏰ Timeout waiting for Lakitu completion`);
382
+ return { error: "Timeout waiting for sandbox completion (10 min)" };
383
+ } catch (error: any) {
384
+ console.error(`❌ runAgentStep failed:`, error.message);
385
+ return { error: error.message };
386
+ }
387
+ },
388
+ });
389
+
390
+ /**
391
+ * Start the card execution workflow (entry point)
392
+ * Public action - called from parent app via scheduler
393
+ */
394
+ export const startCardExecution = action({
395
+ args: {
396
+ cardId: v.string(),
397
+ boardId: v.string(),
398
+ taskId: v.string(),
399
+ runId: v.string(),
400
+ },
401
+ handler: async (ctx, args) => {
402
+ console.log(`🚀 Starting workflow for card ${args.cardId}...`);
403
+ try {
404
+ const workflowId = await workflow.start(
405
+ ctx,
406
+ internal.workflows.agentBoard.cardExecutionWorkflow,
407
+ args
408
+ );
409
+ console.log(`📍 Workflow started: ${workflowId}`);
410
+ return workflowId;
411
+ } catch (error) {
412
+ console.error(`❌ Failed to start workflow:`, error);
413
+ // Note: Parent app should handle card status updates on failure
414
+ throw error;
415
+ }
416
+ },
417
+ });
418
+
419
+ /**
420
+ * Stop a running card execution - kills sandbox, cancels session
421
+ * Public action - called from parent app via scheduler
422
+ */
423
+ export const stopCardExecution = action({
424
+ args: { cardId: v.string() },
425
+ handler: async (ctx, args) => {
426
+ console.log(`🛑 Stopping card ${args.cardId}...`);
427
+
428
+ // Find active Lakitu sessions for this card
429
+ const sessions = await ctx.runQuery(api.workflows.sandboxConvex.listSessions, {
430
+ projectId: args.cardId, // Sessions are tracked by cardId
431
+ limit: 10,
432
+ });
433
+
434
+ // Cancel any running sessions
435
+ for (const session of sessions) {
436
+ if (session.status === "running" || session.status === "starting") {
437
+ try {
438
+ await ctx.runAction(api.workflows.sandboxConvex.cancelSession, {
439
+ sessionId: session._id,
440
+ });
441
+ console.log(`✅ Cancelled session ${session._id}`);
442
+ } catch (e) {
443
+ console.log(`⚠️ Could not cancel session ${session._id}: ${e}`);
444
+ }
445
+ }
446
+ }
447
+
448
+ console.log(`✅ Card ${args.cardId} stopped`);
449
+ // Note: Parent app should handle card run cancellation
450
+ },
451
+ });