@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.
- package/README.md +166 -0
- package/convex/_generated/api.d.ts +45 -0
- package/convex/_generated/api.js +23 -0
- package/convex/_generated/dataModel.d.ts +58 -0
- package/convex/_generated/server.d.ts +143 -0
- package/convex/_generated/server.js +93 -0
- package/convex/cloud/CLAUDE.md +238 -0
- package/convex/cloud/_generated/api.ts +84 -0
- package/convex/cloud/_generated/component.ts +861 -0
- package/convex/cloud/_generated/dataModel.ts +60 -0
- package/convex/cloud/_generated/server.ts +156 -0
- package/convex/cloud/convex.config.ts +16 -0
- package/convex/cloud/index.ts +29 -0
- package/convex/cloud/intentSchema/generate.ts +447 -0
- package/convex/cloud/intentSchema/index.ts +16 -0
- package/convex/cloud/intentSchema/types.ts +418 -0
- package/convex/cloud/ksaPolicy.ts +554 -0
- package/convex/cloud/mail.ts +92 -0
- package/convex/cloud/schema.ts +322 -0
- package/convex/cloud/utils/kanbanContext.ts +229 -0
- package/convex/cloud/workflows/agentBoard.ts +451 -0
- package/convex/cloud/workflows/agentPrompt.ts +272 -0
- package/convex/cloud/workflows/agentThread.ts +374 -0
- package/convex/cloud/workflows/compileSandbox.ts +146 -0
- package/convex/cloud/workflows/crudBoard.ts +217 -0
- package/convex/cloud/workflows/crudKSAs.ts +262 -0
- package/convex/cloud/workflows/crudLorobeads.ts +371 -0
- package/convex/cloud/workflows/crudSkills.ts +205 -0
- package/convex/cloud/workflows/crudThreads.ts +708 -0
- package/convex/cloud/workflows/lifecycleSandbox.ts +1396 -0
- package/convex/cloud/workflows/sandboxConvex.ts +1046 -0
- package/convex/sandbox/README.md +90 -0
- package/convex/sandbox/_generated/api.d.ts +2934 -0
- package/convex/sandbox/_generated/api.js +23 -0
- package/convex/sandbox/_generated/dataModel.d.ts +60 -0
- package/convex/sandbox/_generated/server.d.ts +143 -0
- package/convex/sandbox/_generated/server.js +93 -0
- package/convex/sandbox/actions/bash.ts +130 -0
- package/convex/sandbox/actions/browser.ts +282 -0
- package/convex/sandbox/actions/file.ts +336 -0
- package/convex/sandbox/actions/lsp.ts +325 -0
- package/convex/sandbox/actions/pdf.ts +119 -0
- package/convex/sandbox/agent/codeExecLoop.ts +535 -0
- package/convex/sandbox/agent/decisions.ts +284 -0
- package/convex/sandbox/agent/index.ts +515 -0
- package/convex/sandbox/agent/subagents.ts +651 -0
- package/convex/sandbox/brandResearch/index.ts +417 -0
- package/convex/sandbox/context/index.ts +7 -0
- package/convex/sandbox/context/session.ts +402 -0
- package/convex/sandbox/convex.config.ts +17 -0
- package/convex/sandbox/index.ts +51 -0
- package/convex/sandbox/nodeActions/codeExec.ts +130 -0
- package/convex/sandbox/planning/beads.ts +187 -0
- package/convex/sandbox/planning/index.ts +8 -0
- package/convex/sandbox/planning/sync.ts +194 -0
- package/convex/sandbox/prompts/codeExec.ts +852 -0
- package/convex/sandbox/prompts/modes.ts +231 -0
- package/convex/sandbox/prompts/system.ts +142 -0
- package/convex/sandbox/schema.ts +510 -0
- package/convex/sandbox/state/artifacts.ts +99 -0
- package/convex/sandbox/state/checkpoints.ts +341 -0
- package/convex/sandbox/state/files.ts +383 -0
- package/convex/sandbox/state/index.ts +10 -0
- package/convex/sandbox/state/verification.actions.ts +268 -0
- package/convex/sandbox/state/verification.ts +101 -0
- package/convex/sandbox/tsconfig.json +25 -0
- package/convex/sandbox/utils/codeExecHelpers.ts +52 -0
- package/dist/cli/commands/build.d.ts +19 -0
- package/dist/cli/commands/build.d.ts.map +1 -0
- package/dist/cli/commands/build.js +223 -0
- package/dist/cli/commands/init.d.ts +16 -0
- package/dist/cli/commands/init.d.ts.map +1 -0
- package/dist/cli/commands/init.js +148 -0
- package/dist/cli/commands/publish.d.ts +12 -0
- package/dist/cli/commands/publish.d.ts.map +1 -0
- package/dist/cli/commands/publish.js +33 -0
- package/dist/cli/index.d.ts +14 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +40 -0
- package/dist/sdk/builders.d.ts +104 -0
- package/dist/sdk/builders.d.ts.map +1 -0
- package/dist/sdk/builders.js +214 -0
- package/dist/sdk/index.d.ts +29 -0
- package/dist/sdk/index.d.ts.map +1 -0
- package/dist/sdk/index.js +38 -0
- package/dist/sdk/types.d.ts +107 -0
- package/dist/sdk/types.d.ts.map +1 -0
- package/dist/sdk/types.js +6 -0
- package/ksa/README.md +263 -0
- package/ksa/_generated/REFERENCE.md +2954 -0
- package/ksa/_generated/registry.ts +257 -0
- package/ksa/_shared/configReader.ts +302 -0
- package/ksa/_shared/configSchemas.ts +649 -0
- package/ksa/_shared/gateway.ts +175 -0
- package/ksa/_shared/ksaBehaviors.ts +411 -0
- package/ksa/_shared/ksaProxy.ts +248 -0
- package/ksa/_shared/localDb.ts +302 -0
- package/ksa/index.ts +134 -0
- package/package.json +93 -0
- package/runtime/browser/agent-browser.ts +330 -0
- package/runtime/entrypoint.ts +194 -0
- package/runtime/lsp/manager.ts +366 -0
- package/runtime/pdf/pdf-generator.ts +50 -0
- package/runtime/pdf/renderer.ts +357 -0
- package/runtime/pdf/schema.ts +97 -0
- package/runtime/services/file-watcher.ts +191 -0
- package/template/build.ts +307 -0
- package/template/e2b/Dockerfile +69 -0
- package/template/e2b/e2b.toml +13 -0
- package/template/e2b/prebuild.sh +68 -0
- package/template/e2b/start.sh +14 -0
|
@@ -0,0 +1,1046 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sandbox Convex Lifecycle
|
|
3
|
+
*
|
|
4
|
+
* Manages E2B sandboxes running self-hosted Convex with the Agent SDK.
|
|
5
|
+
*
|
|
6
|
+
* Key differences from lifecycleSandbox.ts:
|
|
7
|
+
* - Uses self-hosted Convex backend instead of OpenCode
|
|
8
|
+
* - Native Convex streaming (no SSE/event forwarder)
|
|
9
|
+
* - Direct Convex client communication
|
|
10
|
+
* - Checkpoint-based chaining for long tasks
|
|
11
|
+
*
|
|
12
|
+
* Flow:
|
|
13
|
+
* 1. Create session in cloud Convex
|
|
14
|
+
* 2. Spawn E2B sandbox with self-hosted Convex
|
|
15
|
+
* 3. Wait for Convex backend to be ready
|
|
16
|
+
* 4. Deploy sandbox-agent functions
|
|
17
|
+
* 5. Start agent thread with prompt
|
|
18
|
+
* 6. Poll stream deltas for real-time UI
|
|
19
|
+
* 7. Collect results on completion
|
|
20
|
+
* 8. Checkpoint if timeout (for chaining)
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { v } from "convex/values";
|
|
24
|
+
import {
|
|
25
|
+
action,
|
|
26
|
+
mutation,
|
|
27
|
+
query,
|
|
28
|
+
internalMutation,
|
|
29
|
+
internalQuery,
|
|
30
|
+
internalAction,
|
|
31
|
+
} from "../_generated/server";
|
|
32
|
+
import { api, internal } from "../_generated/api";
|
|
33
|
+
import * as jose from "jose";
|
|
34
|
+
import { getServicePathsForKSAs, getDefaultKSAs, validateKSAs } from "../ksaPolicy";
|
|
35
|
+
|
|
36
|
+
// =============================================================================
|
|
37
|
+
// LOCAL E2B HELPERS (avoid calling parent app's internal functions)
|
|
38
|
+
// =============================================================================
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Call a Lakitu sandbox action using Convex client
|
|
42
|
+
* Local implementation to avoid parent app dependency
|
|
43
|
+
*/
|
|
44
|
+
async function callLakituAction(
|
|
45
|
+
sandboxUrl: string,
|
|
46
|
+
functionPath: string,
|
|
47
|
+
functionArgs: any,
|
|
48
|
+
timeoutMs?: number
|
|
49
|
+
): Promise<{ success: boolean; data?: any; error?: string; durationMs: number }> {
|
|
50
|
+
const start = Date.now();
|
|
51
|
+
const { ConvexHttpClient } = await import("convex/browser");
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
const client = new ConvexHttpClient(sandboxUrl);
|
|
55
|
+
|
|
56
|
+
const [modulePath, funcName] = functionPath.split(":");
|
|
57
|
+
if (!modulePath || !funcName) {
|
|
58
|
+
throw new Error(`Invalid function path: ${functionPath}`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
console.log(`[callLakituAction] Calling ${functionPath}`);
|
|
62
|
+
|
|
63
|
+
const { anyApi } = await import("convex/server");
|
|
64
|
+
|
|
65
|
+
const pathParts = modulePath.split("/");
|
|
66
|
+
let funcRef: any = anyApi;
|
|
67
|
+
for (const part of pathParts) {
|
|
68
|
+
funcRef = funcRef[part];
|
|
69
|
+
}
|
|
70
|
+
funcRef = funcRef[funcName];
|
|
71
|
+
|
|
72
|
+
const result = await client.action(funcRef, functionArgs);
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
success: true,
|
|
76
|
+
data: result,
|
|
77
|
+
durationMs: Date.now() - start,
|
|
78
|
+
};
|
|
79
|
+
} catch (e: any) {
|
|
80
|
+
console.error(`[callLakituAction] Error:`, e);
|
|
81
|
+
return {
|
|
82
|
+
success: false,
|
|
83
|
+
error: e.message,
|
|
84
|
+
durationMs: Date.now() - start,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Sandbox cleanup - handled by parent app or natural timeout
|
|
91
|
+
* Components can't use E2B SDK (requires Node.js), so we just log.
|
|
92
|
+
* Sandboxes have automatic 10-minute timeouts in E2B.
|
|
93
|
+
*/
|
|
94
|
+
function logSandboxCleanup(sandboxId: string): void {
|
|
95
|
+
console.log(`[sandboxConvex] Sandbox ${sandboxId} will be cleaned up by timeout or parent app`);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Cloud Convex URL for gateway calls
|
|
99
|
+
const CLOUD_CONVEX_URL =
|
|
100
|
+
process.env.CONVEX_URL || "https://earnest-shrimp-308.convex.cloud";
|
|
101
|
+
|
|
102
|
+
// Gateway URL (HTTP actions endpoint) - .convex.site for HTTP routes
|
|
103
|
+
const GATEWAY_URL = (process.env.CONVEX_URL || "https://earnest-shrimp-308.convex.cloud")
|
|
104
|
+
.replace(".convex.cloud", ".convex.site");
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Generate a JWT for sandbox -> cloud gateway auth
|
|
108
|
+
* @param sessionId - Session ID to encode in JWT
|
|
109
|
+
* @param providedJwt - Optional pre-generated JWT (for when env var isn't accessible)
|
|
110
|
+
*/
|
|
111
|
+
async function generateSandboxJwt(sessionId: string, providedJwt?: string): Promise<string> {
|
|
112
|
+
// If a JWT was provided (e.g., by the parent app wrapper), use it
|
|
113
|
+
if (providedJwt) {
|
|
114
|
+
return providedJwt;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const secret = process.env.SANDBOX_JWT_SECRET;
|
|
118
|
+
if (!secret) {
|
|
119
|
+
throw new Error("SANDBOX_JWT_SECRET not configured");
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const jwt = await new jose.SignJWT({ sessionId })
|
|
123
|
+
.setProtectedHeader({ alg: "HS256" })
|
|
124
|
+
.setIssuedAt()
|
|
125
|
+
.setExpirationTime("24h")
|
|
126
|
+
.sign(new TextEncoder().encode(secret));
|
|
127
|
+
|
|
128
|
+
return jwt;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// ============================================
|
|
132
|
+
// Types
|
|
133
|
+
// ============================================
|
|
134
|
+
|
|
135
|
+
const sessionStatusType = v.union(
|
|
136
|
+
v.literal("pending"),
|
|
137
|
+
v.literal("starting"),
|
|
138
|
+
v.literal("running"),
|
|
139
|
+
v.literal("completed"),
|
|
140
|
+
v.literal("failed"),
|
|
141
|
+
v.literal("cancelled"),
|
|
142
|
+
v.literal("checkpointed"),
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
// ============================================
|
|
146
|
+
// Session CRUD
|
|
147
|
+
// ============================================
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Create a new Convex sandbox session
|
|
151
|
+
*/
|
|
152
|
+
export const createSession = mutation({
|
|
153
|
+
args: {
|
|
154
|
+
projectId: v.string(),
|
|
155
|
+
prompt: v.string(),
|
|
156
|
+
config: v.optional(v.any()),
|
|
157
|
+
},
|
|
158
|
+
handler: async (ctx, args) => {
|
|
159
|
+
return await ctx.db.insert("convexSandboxSessions", {
|
|
160
|
+
projectId: args.projectId,
|
|
161
|
+
prompt: args.prompt,
|
|
162
|
+
status: "pending",
|
|
163
|
+
config: args.config,
|
|
164
|
+
createdAt: Date.now(),
|
|
165
|
+
updatedAt: Date.now(),
|
|
166
|
+
iteration: 0,
|
|
167
|
+
});
|
|
168
|
+
},
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Get session by ID
|
|
173
|
+
*/
|
|
174
|
+
export const getSession = query({
|
|
175
|
+
args: { sessionId: v.id("convexSandboxSessions") },
|
|
176
|
+
handler: async (ctx, args) => {
|
|
177
|
+
return await ctx.db.get(args.sessionId);
|
|
178
|
+
},
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Get session with logs
|
|
183
|
+
*/
|
|
184
|
+
export const getSessionWithLogs = query({
|
|
185
|
+
args: { sessionId: v.id("convexSandboxSessions") },
|
|
186
|
+
handler: async (ctx, args) => {
|
|
187
|
+
const session = await ctx.db.get(args.sessionId);
|
|
188
|
+
if (!session) return null;
|
|
189
|
+
|
|
190
|
+
const logs = await ctx.db
|
|
191
|
+
.query("convexSandboxLogs")
|
|
192
|
+
.withIndex("by_session", (q) => q.eq("sessionId", args.sessionId))
|
|
193
|
+
.order("asc")
|
|
194
|
+
.take(500);
|
|
195
|
+
|
|
196
|
+
return {
|
|
197
|
+
...session,
|
|
198
|
+
logs: logs.map((l) => ({
|
|
199
|
+
message: l.message,
|
|
200
|
+
level: l.level,
|
|
201
|
+
timestamp: l.timestamp,
|
|
202
|
+
})),
|
|
203
|
+
};
|
|
204
|
+
},
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Get active session for a card (projectId = cardId)
|
|
209
|
+
* Used by frontend to subscribe to real-time logs
|
|
210
|
+
*/
|
|
211
|
+
export const getActiveSessionForCard = query({
|
|
212
|
+
args: { cardId: v.string() },
|
|
213
|
+
handler: async (ctx, args) => {
|
|
214
|
+
// Find the most recent running/pending session for this card
|
|
215
|
+
const sessions = await ctx.db
|
|
216
|
+
.query("convexSandboxSessions")
|
|
217
|
+
.withIndex("by_project", (q) => q.eq("projectId", args.cardId))
|
|
218
|
+
.order("desc")
|
|
219
|
+
.take(5);
|
|
220
|
+
|
|
221
|
+
// Find active session (running, starting, or pending)
|
|
222
|
+
const activeSession = sessions.find(
|
|
223
|
+
(s) => s.status === "running" || s.status === "starting" || s.status === "pending"
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
const sessionToUse = activeSession || sessions[0];
|
|
227
|
+
if (!sessionToUse) return null;
|
|
228
|
+
|
|
229
|
+
// Get logs for session
|
|
230
|
+
const rawLogs = await ctx.db
|
|
231
|
+
.query("convexSandboxLogs")
|
|
232
|
+
.withIndex("by_session", (q) => q.eq("sessionId", sessionToUse._id))
|
|
233
|
+
.order("asc")
|
|
234
|
+
.take(500);
|
|
235
|
+
|
|
236
|
+
// Parse logs - return structured format
|
|
237
|
+
const logs = rawLogs.map((l) => {
|
|
238
|
+
// Try to parse as JSON (structured log)
|
|
239
|
+
if (l.stepType) {
|
|
240
|
+
try {
|
|
241
|
+
return JSON.parse(l.message);
|
|
242
|
+
} catch {
|
|
243
|
+
return { type: "text", label: l.message };
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
// Plain string log - wrap in basic structure
|
|
247
|
+
return { type: "text", label: l.message };
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
return {
|
|
251
|
+
...sessionToUse,
|
|
252
|
+
logs, // Now returns array of structured objects
|
|
253
|
+
};
|
|
254
|
+
},
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Get active session for a thread (projectId = "thread-{threadId}")
|
|
259
|
+
* Used by frontend to subscribe to real-time chain of thought
|
|
260
|
+
*/
|
|
261
|
+
export const getActiveSessionForThread = query({
|
|
262
|
+
args: { threadId: v.string() },
|
|
263
|
+
handler: async (ctx, args) => {
|
|
264
|
+
const projectId = `thread-${args.threadId}`;
|
|
265
|
+
|
|
266
|
+
// Find the most recent running/pending session for this thread
|
|
267
|
+
const sessions = await ctx.db
|
|
268
|
+
.query("convexSandboxSessions")
|
|
269
|
+
.withIndex("by_project", (q) => q.eq("projectId", projectId))
|
|
270
|
+
.order("desc")
|
|
271
|
+
.take(5);
|
|
272
|
+
|
|
273
|
+
// Find active session (running, starting, or pending)
|
|
274
|
+
const activeSession = sessions.find(
|
|
275
|
+
(s) => s.status === "running" || s.status === "starting" || s.status === "pending"
|
|
276
|
+
);
|
|
277
|
+
|
|
278
|
+
const sessionToUse = activeSession || sessions[0];
|
|
279
|
+
if (!sessionToUse) return null;
|
|
280
|
+
|
|
281
|
+
// Get logs for session
|
|
282
|
+
const rawLogs = await ctx.db
|
|
283
|
+
.query("convexSandboxLogs")
|
|
284
|
+
.withIndex("by_session", (q) => q.eq("sessionId", sessionToUse._id))
|
|
285
|
+
.order("asc")
|
|
286
|
+
.take(500);
|
|
287
|
+
|
|
288
|
+
// Parse logs - return structured format
|
|
289
|
+
const logs = rawLogs.map((l) => {
|
|
290
|
+
// Try to parse as JSON (structured log)
|
|
291
|
+
if (l.stepType) {
|
|
292
|
+
try {
|
|
293
|
+
return JSON.parse(l.message);
|
|
294
|
+
} catch {
|
|
295
|
+
return { type: "text", label: l.message };
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
// Plain string log - wrap in basic structure
|
|
299
|
+
return { type: "text", label: l.message };
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
return {
|
|
303
|
+
...sessionToUse,
|
|
304
|
+
logs, // Now returns array of structured objects
|
|
305
|
+
};
|
|
306
|
+
},
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* List sessions for a project
|
|
311
|
+
*/
|
|
312
|
+
export const listSessions = query({
|
|
313
|
+
args: {
|
|
314
|
+
projectId: v.string(),
|
|
315
|
+
limit: v.optional(v.number()),
|
|
316
|
+
},
|
|
317
|
+
handler: async (ctx, args) => {
|
|
318
|
+
return await ctx.db
|
|
319
|
+
.query("convexSandboxSessions")
|
|
320
|
+
.withIndex("by_project", (q) => q.eq("projectId", args.projectId))
|
|
321
|
+
.order("desc")
|
|
322
|
+
.take(args.limit ?? 50);
|
|
323
|
+
},
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Update session status
|
|
328
|
+
*/
|
|
329
|
+
export const updateSession = internalMutation({
|
|
330
|
+
args: {
|
|
331
|
+
sessionId: v.id("convexSandboxSessions"),
|
|
332
|
+
status: v.optional(sessionStatusType),
|
|
333
|
+
sandboxId: v.optional(v.string()),
|
|
334
|
+
sandboxUrl: v.optional(v.string()),
|
|
335
|
+
threadId: v.optional(v.string()),
|
|
336
|
+
output: v.optional(v.any()),
|
|
337
|
+
error: v.optional(v.string()),
|
|
338
|
+
checkpointId: v.optional(v.string()),
|
|
339
|
+
metrics: v.optional(v.any()),
|
|
340
|
+
config: v.optional(v.any()),
|
|
341
|
+
},
|
|
342
|
+
handler: async (ctx, args) => {
|
|
343
|
+
const { sessionId, ...updates } = args;
|
|
344
|
+
const filtered = Object.fromEntries(
|
|
345
|
+
Object.entries(updates).filter(([_, v]) => v !== undefined),
|
|
346
|
+
);
|
|
347
|
+
|
|
348
|
+
await ctx.db.patch(sessionId, {
|
|
349
|
+
...filtered,
|
|
350
|
+
updatedAt: Date.now(),
|
|
351
|
+
...(args.status === "completed" || args.status === "failed"
|
|
352
|
+
? { completedAt: Date.now() }
|
|
353
|
+
: {}),
|
|
354
|
+
});
|
|
355
|
+
},
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Append log to session
|
|
360
|
+
*/
|
|
361
|
+
export const appendLog = internalMutation({
|
|
362
|
+
args: {
|
|
363
|
+
sessionId: v.id("convexSandboxSessions"),
|
|
364
|
+
message: v.string(),
|
|
365
|
+
level: v.optional(
|
|
366
|
+
v.union(v.literal("info"), v.literal("warn"), v.literal("error")),
|
|
367
|
+
),
|
|
368
|
+
},
|
|
369
|
+
handler: async (ctx, args) => {
|
|
370
|
+
await ctx.db.insert("convexSandboxLogs", {
|
|
371
|
+
sessionId: args.sessionId,
|
|
372
|
+
message: args.message,
|
|
373
|
+
level: args.level ?? "info",
|
|
374
|
+
timestamp: Date.now(),
|
|
375
|
+
});
|
|
376
|
+
},
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Append multiple logs - supports both string and structured formats
|
|
381
|
+
*/
|
|
382
|
+
export const appendLogs = mutation({
|
|
383
|
+
args: {
|
|
384
|
+
sessionId: v.id("convexSandboxSessions"),
|
|
385
|
+
logs: v.array(v.union(
|
|
386
|
+
v.string(),
|
|
387
|
+
v.object({
|
|
388
|
+
type: v.string(), // thinking, tool, search, file, text
|
|
389
|
+
label: v.string(),
|
|
390
|
+
status: v.optional(v.string()), // active, complete, error
|
|
391
|
+
icon: v.optional(v.string()),
|
|
392
|
+
details: v.optional(v.string()),
|
|
393
|
+
})
|
|
394
|
+
)),
|
|
395
|
+
},
|
|
396
|
+
handler: async (ctx, args) => {
|
|
397
|
+
const now = Date.now();
|
|
398
|
+
for (const log of args.logs) {
|
|
399
|
+
if (typeof log === "string") {
|
|
400
|
+
// Legacy string format
|
|
401
|
+
await ctx.db.insert("convexSandboxLogs", {
|
|
402
|
+
sessionId: args.sessionId,
|
|
403
|
+
message: log,
|
|
404
|
+
level: "info",
|
|
405
|
+
timestamp: now,
|
|
406
|
+
});
|
|
407
|
+
} else {
|
|
408
|
+
// Structured format - store as JSON in message field
|
|
409
|
+
await ctx.db.insert("convexSandboxLogs", {
|
|
410
|
+
sessionId: args.sessionId,
|
|
411
|
+
message: JSON.stringify(log),
|
|
412
|
+
level: "info",
|
|
413
|
+
timestamp: now,
|
|
414
|
+
// Store type separately for filtering
|
|
415
|
+
stepType: log.type,
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
},
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
// ============================================
|
|
423
|
+
// Sandbox Actions
|
|
424
|
+
// ============================================
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* Start a Convex sandbox session
|
|
428
|
+
*/
|
|
429
|
+
export const startSession = action({
|
|
430
|
+
args: {
|
|
431
|
+
projectId: v.string(),
|
|
432
|
+
prompt: v.string(),
|
|
433
|
+
config: v.optional(v.any()),
|
|
434
|
+
},
|
|
435
|
+
handler: async (ctx, args) => {
|
|
436
|
+
// Create session
|
|
437
|
+
const sessionId = await ctx.runMutation(
|
|
438
|
+
api.workflows.sandboxConvex.createSession,
|
|
439
|
+
{
|
|
440
|
+
projectId: args.projectId,
|
|
441
|
+
prompt: args.prompt,
|
|
442
|
+
config: args.config,
|
|
443
|
+
},
|
|
444
|
+
);
|
|
445
|
+
|
|
446
|
+
// Run sandbox
|
|
447
|
+
const result = await ctx.runAction(
|
|
448
|
+
internal.workflows.sandboxConvex.runConvexSandbox,
|
|
449
|
+
{
|
|
450
|
+
sessionId,
|
|
451
|
+
prompt: args.prompt,
|
|
452
|
+
config: args.config,
|
|
453
|
+
},
|
|
454
|
+
);
|
|
455
|
+
|
|
456
|
+
return { sessionId, ...result };
|
|
457
|
+
},
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* Run the Convex sandbox - OPTIMIZED
|
|
462
|
+
*
|
|
463
|
+
* Key optimizations:
|
|
464
|
+
* 1. Single E2B action for create+start+waitReady (was 3 actions + 4 log mutations)
|
|
465
|
+
* 2. Fire-and-forget logs via scheduler (non-blocking)
|
|
466
|
+
* 3. Minimal awaits in critical path
|
|
467
|
+
*/
|
|
468
|
+
export const runConvexSandbox = internalAction({
|
|
469
|
+
args: {
|
|
470
|
+
sessionId: v.id("convexSandboxSessions"),
|
|
471
|
+
prompt: v.string(),
|
|
472
|
+
config: v.optional(v.any()),
|
|
473
|
+
checkpointId: v.optional(v.string()),
|
|
474
|
+
},
|
|
475
|
+
handler: async (ctx, args) => {
|
|
476
|
+
const startTime = Date.now();
|
|
477
|
+
let sandboxId: string | null = null;
|
|
478
|
+
|
|
479
|
+
// Helper: fire-and-forget log (non-blocking)
|
|
480
|
+
const logAsync = (message: string) => {
|
|
481
|
+
ctx.scheduler.runAfter(0, internal.workflows.sandboxConvex.appendLog, {
|
|
482
|
+
sessionId: args.sessionId,
|
|
483
|
+
message,
|
|
484
|
+
});
|
|
485
|
+
};
|
|
486
|
+
|
|
487
|
+
// Single mutation: update status + log in one call
|
|
488
|
+
await ctx.runMutation(
|
|
489
|
+
internal.workflows.sandboxConvex.updateSession,
|
|
490
|
+
{
|
|
491
|
+
sessionId: args.sessionId,
|
|
492
|
+
status: "starting",
|
|
493
|
+
},
|
|
494
|
+
);
|
|
495
|
+
logAsync("🚀 Starting Convex sandbox...");
|
|
496
|
+
|
|
497
|
+
// Extract and validate allowedKSAs early for file-system policy enforcement
|
|
498
|
+
const configObj = (args.config || {}) as {
|
|
499
|
+
allowedKSAs?: string[];
|
|
500
|
+
skillConfigs?: Record<string, Record<string, unknown>>;
|
|
501
|
+
[key: string]: any;
|
|
502
|
+
};
|
|
503
|
+
let allowedKSAs = configObj.allowedKSAs;
|
|
504
|
+
const skillConfigs = configObj.skillConfigs || {};
|
|
505
|
+
|
|
506
|
+
// Default to all KSAs if not specified
|
|
507
|
+
if (!allowedKSAs || allowedKSAs.length === 0) {
|
|
508
|
+
allowedKSAs = getDefaultKSAs("all");
|
|
509
|
+
console.log(`[sandboxConvex] No allowedKSAs specified, using defaults: ${allowedKSAs.join(", ")}`);
|
|
510
|
+
} else {
|
|
511
|
+
// Validate the KSA names
|
|
512
|
+
const { valid, invalid } = validateKSAs(allowedKSAs);
|
|
513
|
+
if (invalid.length > 0) {
|
|
514
|
+
console.warn(`[sandboxConvex] Unknown KSAs ignored: ${invalid.join(", ")}`);
|
|
515
|
+
}
|
|
516
|
+
allowedKSAs = valid;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
try {
|
|
520
|
+
// Generate JWT FIRST - needed as sandbox env var for KSAs
|
|
521
|
+
// Accept pre-generated JWT from config (for when component can't access env var)
|
|
522
|
+
const sandboxJwt = await generateSandboxJwt(args.sessionId, configObj.sandboxJwt);
|
|
523
|
+
|
|
524
|
+
// Get session to check if it's a thread-based session
|
|
525
|
+
const session = await ctx.runQuery(api.workflows.sandboxConvex.getSession, {
|
|
526
|
+
sessionId: args.sessionId,
|
|
527
|
+
});
|
|
528
|
+
const projectId = session?.projectId || "";
|
|
529
|
+
const isThreadSession = projectId.startsWith("thread-");
|
|
530
|
+
const envThreadId = isThreadSession ? projectId.replace("thread-", "") : undefined;
|
|
531
|
+
|
|
532
|
+
// Build env vars - include THREAD_ID for thread-based sessions
|
|
533
|
+
const envs: Record<string, string> = {
|
|
534
|
+
GATEWAY_URL: GATEWAY_URL,
|
|
535
|
+
SANDBOX_JWT: sandboxJwt,
|
|
536
|
+
};
|
|
537
|
+
if (envThreadId) {
|
|
538
|
+
envs.THREAD_ID = envThreadId;
|
|
539
|
+
}
|
|
540
|
+
// Also pass CARD_ID if provided in config (for kanban workflows)
|
|
541
|
+
if (configObj.cardId) {
|
|
542
|
+
envs.CARD_ID = configObj.cardId;
|
|
543
|
+
}
|
|
544
|
+
// Pass WORKSPACE_ID if provided (for workspace-scoped threads)
|
|
545
|
+
if (configObj.workspaceId) {
|
|
546
|
+
envs.WORKSPACE_ID = configObj.workspaceId;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// Check if sandbox was pre-created by parent app (preferred for component isolation)
|
|
550
|
+
let sandboxUrl: string;
|
|
551
|
+
let timings: Record<string, number>;
|
|
552
|
+
let fromPool: boolean;
|
|
553
|
+
let deletedKSAs: string[] = [];
|
|
554
|
+
|
|
555
|
+
if (configObj.preCreatedSandbox) {
|
|
556
|
+
// Use pre-created sandbox from parent app
|
|
557
|
+
const preSandbox = configObj.preCreatedSandbox;
|
|
558
|
+
sandboxId = preSandbox.sandboxId;
|
|
559
|
+
sandboxUrl = preSandbox.sandboxUrl;
|
|
560
|
+
timings = preSandbox.timings || { totalMs: 0 };
|
|
561
|
+
fromPool = preSandbox.fromPool || false;
|
|
562
|
+
deletedKSAs = preSandbox.deletedKSAs || [];
|
|
563
|
+
console.log(`[sandboxConvex] Using pre-created sandbox: ${sandboxId}`);
|
|
564
|
+
} else {
|
|
565
|
+
// No pre-created sandbox - this is an error in component context
|
|
566
|
+
// The parent app wrapper (api.lakitu.startSession) should always provide preCreatedSandbox
|
|
567
|
+
throw new Error(
|
|
568
|
+
"preCreatedSandbox not provided. Use api.lakitu.startSession wrapper instead of calling component directly."
|
|
569
|
+
);
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// Fire-and-forget: log timing details
|
|
573
|
+
if (fromPool) {
|
|
574
|
+
logAsync(`📦 Sandbox ready in ${timings.totalMs}ms (from pool: claim=${timings.claimMs}ms, connect=${timings.connectMs}ms, policy=${timings.policyMs || 0}ms)`);
|
|
575
|
+
} else {
|
|
576
|
+
logAsync(`📦 Sandbox ready in ${timings.totalMs}ms (new: create=${timings.createMs}ms, policy=${timings.policyMs || 0}ms)`);
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// Update session with sandbox info (single mutation)
|
|
580
|
+
await ctx.runMutation(
|
|
581
|
+
internal.workflows.sandboxConvex.updateSession,
|
|
582
|
+
{
|
|
583
|
+
sessionId: args.sessionId,
|
|
584
|
+
status: "running",
|
|
585
|
+
sandboxId,
|
|
586
|
+
sandboxUrl,
|
|
587
|
+
},
|
|
588
|
+
);
|
|
589
|
+
|
|
590
|
+
logAsync("🤖 Starting agent thread...");
|
|
591
|
+
|
|
592
|
+
// Convert KSA names to service paths for gateway enforcement
|
|
593
|
+
// (allowedKSAs already validated above, before sandbox creation)
|
|
594
|
+
const allowedServices = getServicePathsForKSAs(allowedKSAs);
|
|
595
|
+
console.log(`[sandboxConvex] KSAs: [${allowedKSAs.join(", ")}] => Services: [${allowedServices.join(", ")}]`);
|
|
596
|
+
if (deletedKSAs && deletedKSAs.length > 0) {
|
|
597
|
+
logAsync(`🔒 KSA policy: removed ${deletedKSAs.join(", ")} from sandbox`);
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
// Update session with allowed services for gateway policy
|
|
601
|
+
await ctx.runMutation(
|
|
602
|
+
internal.workflows.sandboxConvex.updateSession,
|
|
603
|
+
{
|
|
604
|
+
sessionId: args.sessionId,
|
|
605
|
+
// Store allowedServices in session config for gateway to check
|
|
606
|
+
config: {
|
|
607
|
+
...configObj,
|
|
608
|
+
allowedKSAs,
|
|
609
|
+
allowedServices,
|
|
610
|
+
} as any,
|
|
611
|
+
},
|
|
612
|
+
);
|
|
613
|
+
|
|
614
|
+
// Build context with gateway config and sessionId for real-time logs
|
|
615
|
+
// Model config is passed from parent app via configObj.model (from unified settings)
|
|
616
|
+
const agentContext = {
|
|
617
|
+
...configObj,
|
|
618
|
+
allowedKSAs, // Pass to agent so it knows what's available
|
|
619
|
+
skillConfigs, // KSA-specific configurations from stage
|
|
620
|
+
sessionId: args.sessionId, // For real-time chain of thought forwarding
|
|
621
|
+
// Pass cloud thread ID for artifact uploads (different from sandbox-local threadId)
|
|
622
|
+
cloudThreadId: envThreadId,
|
|
623
|
+
// Model config from unified settings (passed by parent app)
|
|
624
|
+
model: configObj.model,
|
|
625
|
+
fallbackModels: configObj.fallbackModels,
|
|
626
|
+
maxTokens: configObj.maxTokens,
|
|
627
|
+
temperature: configObj.temperature,
|
|
628
|
+
gatewayConfig: {
|
|
629
|
+
convexUrl: CLOUD_CONVEX_URL,
|
|
630
|
+
jwt: sandboxJwt,
|
|
631
|
+
},
|
|
632
|
+
};
|
|
633
|
+
|
|
634
|
+
// Log skill configs if any have custom instructions
|
|
635
|
+
const skillsWithInstructions = Object.entries(skillConfigs)
|
|
636
|
+
.filter(([_, cfg]) => cfg.instructions)
|
|
637
|
+
.map(([name]) => name);
|
|
638
|
+
if (skillsWithInstructions.length > 0) {
|
|
639
|
+
logAsync(`📋 Custom instructions for: ${skillsWithInstructions.join(", ")}`);
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
// Log intent schema if present (generated by agentThread for guidance)
|
|
643
|
+
const intentSchema = configObj.intentSchema as {
|
|
644
|
+
intent?: { summary?: string };
|
|
645
|
+
ksas?: { priority?: string[] };
|
|
646
|
+
meta?: { confidence?: string; latencyMs?: number };
|
|
647
|
+
} | undefined;
|
|
648
|
+
if (intentSchema?.intent?.summary) {
|
|
649
|
+
const priorityKSAs = intentSchema.ksas?.priority?.slice(0, 3).join(", ") || "none";
|
|
650
|
+
const confidence = intentSchema.meta?.confidence || "unknown";
|
|
651
|
+
logAsync(`🎯 Intent: "${intentSchema.intent.summary}" (${confidence} confidence, priority KSAs: ${priorityKSAs})`);
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
console.log(
|
|
655
|
+
`[sandboxConvex] Calling agent with gatewayConfig.convexUrl=${CLOUD_CONVEX_URL}, jwt length=${sandboxJwt.length}`,
|
|
656
|
+
);
|
|
657
|
+
|
|
658
|
+
// Call the sandbox Convex to start the agent using Convex client
|
|
659
|
+
// Uses code execution mode with single execute_code tool
|
|
660
|
+
const agentResult = await callLakituAction(
|
|
661
|
+
sandboxUrl,
|
|
662
|
+
"agent/index:startCodeExecThread",
|
|
663
|
+
{
|
|
664
|
+
prompt: args.prompt,
|
|
665
|
+
context: agentContext,
|
|
666
|
+
},
|
|
667
|
+
180000, // 3 minutes for agent to complete
|
|
668
|
+
);
|
|
669
|
+
|
|
670
|
+
if (!agentResult.success) {
|
|
671
|
+
throw new Error(`Agent start failed: ${agentResult.error}`);
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
const threadId = agentResult.data?.threadId;
|
|
675
|
+
const agentText = agentResult.data?.text;
|
|
676
|
+
const agentToolCalls = agentResult.data?.toolCalls;
|
|
677
|
+
|
|
678
|
+
// Validate threadId was returned
|
|
679
|
+
if (!threadId) {
|
|
680
|
+
throw new Error(
|
|
681
|
+
`Agent did not return threadId. Response: ${JSON.stringify(agentResult.data)}`,
|
|
682
|
+
);
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
await ctx.runMutation(
|
|
686
|
+
internal.workflows.sandboxConvex.updateSession,
|
|
687
|
+
{
|
|
688
|
+
sessionId: args.sessionId,
|
|
689
|
+
threadId,
|
|
690
|
+
},
|
|
691
|
+
);
|
|
692
|
+
|
|
693
|
+
await ctx.runMutation(
|
|
694
|
+
internal.workflows.sandboxConvex.appendLog,
|
|
695
|
+
{
|
|
696
|
+
sessionId: args.sessionId,
|
|
697
|
+
message: `✅ Agent thread started: ${threadId}`,
|
|
698
|
+
},
|
|
699
|
+
);
|
|
700
|
+
|
|
701
|
+
// Check if agent completed synchronously (Lakitu returns full result)
|
|
702
|
+
if (agentText !== undefined) {
|
|
703
|
+
// Agent completed - extract output and finalize
|
|
704
|
+
const elapsed = Date.now() - startTime;
|
|
705
|
+
const output = {
|
|
706
|
+
response: agentText,
|
|
707
|
+
toolCalls: agentToolCalls || [],
|
|
708
|
+
messageCount: 1,
|
|
709
|
+
};
|
|
710
|
+
|
|
711
|
+
// OPTIMIZED: Single mutation for completion, fire-and-forget log + sandbox kill
|
|
712
|
+
await ctx.runMutation(
|
|
713
|
+
internal.workflows.sandboxConvex.updateSession,
|
|
714
|
+
{
|
|
715
|
+
sessionId: args.sessionId,
|
|
716
|
+
status: "completed",
|
|
717
|
+
output,
|
|
718
|
+
metrics: {
|
|
719
|
+
totalMs: elapsed,
|
|
720
|
+
pollCount: 0,
|
|
721
|
+
synchronous: true,
|
|
722
|
+
},
|
|
723
|
+
},
|
|
724
|
+
);
|
|
725
|
+
|
|
726
|
+
// Fire-and-forget: log + sandbox cleanup (don't await)
|
|
727
|
+
logAsync(`✅ Completed synchronously in ${(elapsed / 1000).toFixed(1)}s`);
|
|
728
|
+
logSandboxCleanup(sandboxId);
|
|
729
|
+
|
|
730
|
+
return {
|
|
731
|
+
success: true,
|
|
732
|
+
status: "completed",
|
|
733
|
+
sandboxId,
|
|
734
|
+
threadId,
|
|
735
|
+
output,
|
|
736
|
+
};
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
// OPTIMIZED: Schedule both in parallel (fire-and-forget)
|
|
740
|
+
ctx.scheduler.runAfter(5000, internal.workflows.sandboxConvex.pollCompletion, {
|
|
741
|
+
sessionId: args.sessionId,
|
|
742
|
+
sandboxId,
|
|
743
|
+
sandboxUrl,
|
|
744
|
+
threadId,
|
|
745
|
+
pollCount: 0,
|
|
746
|
+
startTime,
|
|
747
|
+
});
|
|
748
|
+
ctx.scheduler.runAfter(540000, internal.workflows.sandboxConvex.timeoutWatchdog, {
|
|
749
|
+
sessionId: args.sessionId,
|
|
750
|
+
sandboxId,
|
|
751
|
+
startTime,
|
|
752
|
+
});
|
|
753
|
+
|
|
754
|
+
return {
|
|
755
|
+
success: true,
|
|
756
|
+
status: "running",
|
|
757
|
+
sandboxId,
|
|
758
|
+
threadId,
|
|
759
|
+
};
|
|
760
|
+
} catch (error) {
|
|
761
|
+
const message =
|
|
762
|
+
error instanceof Error ? error.message : String(error);
|
|
763
|
+
|
|
764
|
+
// OPTIMIZED: Single await for critical update, fire-and-forget the rest
|
|
765
|
+
await ctx.runMutation(
|
|
766
|
+
internal.workflows.sandboxConvex.updateSession,
|
|
767
|
+
{
|
|
768
|
+
sessionId: args.sessionId,
|
|
769
|
+
status: "failed",
|
|
770
|
+
error: message,
|
|
771
|
+
},
|
|
772
|
+
);
|
|
773
|
+
|
|
774
|
+
// Fire-and-forget: log + sandbox cleanup
|
|
775
|
+
logAsync(`❌ Error: ${message}`);
|
|
776
|
+
if (sandboxId) {
|
|
777
|
+
logSandboxCleanup(sandboxId);
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
return { success: false, error: message };
|
|
781
|
+
}
|
|
782
|
+
},
|
|
783
|
+
});
|
|
784
|
+
|
|
785
|
+
/**
|
|
786
|
+
* Poll for agent completion - OPTIMIZED
|
|
787
|
+
*
|
|
788
|
+
* Key optimizations:
|
|
789
|
+
* 1. Parallel HTTP fetches for deltas + status
|
|
790
|
+
* 2. Fire-and-forget logs and sandbox cleanup
|
|
791
|
+
* 3. Reduced polling interval (2s instead of 5s)
|
|
792
|
+
*/
|
|
793
|
+
export const pollCompletion = internalAction({
|
|
794
|
+
args: {
|
|
795
|
+
sessionId: v.id("convexSandboxSessions"),
|
|
796
|
+
sandboxId: v.string(),
|
|
797
|
+
sandboxUrl: v.string(),
|
|
798
|
+
threadId: v.string(),
|
|
799
|
+
pollCount: v.number(),
|
|
800
|
+
startTime: v.number(),
|
|
801
|
+
},
|
|
802
|
+
handler: async (ctx, args) => {
|
|
803
|
+
const maxPolls = 150; // ~5 minutes at 2s intervals
|
|
804
|
+
const pollInterval = 2000; // Reduced from 5s to 2s
|
|
805
|
+
|
|
806
|
+
// Helper for fire-and-forget log
|
|
807
|
+
const logAsync = (message: string) => {
|
|
808
|
+
ctx.scheduler.runAfter(0, internal.workflows.sandboxConvex.appendLog, {
|
|
809
|
+
sessionId: args.sessionId,
|
|
810
|
+
message,
|
|
811
|
+
});
|
|
812
|
+
};
|
|
813
|
+
|
|
814
|
+
try {
|
|
815
|
+
// Check if session is already completed
|
|
816
|
+
const session = await ctx.runQuery(
|
|
817
|
+
api.workflows.sandboxConvex.getSession,
|
|
818
|
+
{ sessionId: args.sessionId },
|
|
819
|
+
);
|
|
820
|
+
|
|
821
|
+
if (
|
|
822
|
+
session?.status === "completed" ||
|
|
823
|
+
session?.status === "failed" ||
|
|
824
|
+
session?.status === "cancelled"
|
|
825
|
+
) {
|
|
826
|
+
// Already done, fire-and-forget cleanup
|
|
827
|
+
logSandboxCleanup(args.sandboxId);
|
|
828
|
+
return;
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
// OPTIMIZED: Parallel fetches for deltas + status
|
|
832
|
+
const [deltasRes, statusRes] = await Promise.all([
|
|
833
|
+
fetch(`${args.sandboxUrl}/api/run/agent/index/getStreamDeltas`, {
|
|
834
|
+
method: "POST",
|
|
835
|
+
headers: { "Content-Type": "application/json" },
|
|
836
|
+
body: JSON.stringify({ args: { threadId: args.threadId } }),
|
|
837
|
+
signal: AbortSignal.timeout(5000),
|
|
838
|
+
}).catch(() => null),
|
|
839
|
+
fetch(`${args.sandboxUrl}/api/run/agent/index/getThreadMessages`, {
|
|
840
|
+
method: "POST",
|
|
841
|
+
headers: { "Content-Type": "application/json" },
|
|
842
|
+
body: JSON.stringify({ args: { threadId: args.threadId } }),
|
|
843
|
+
signal: AbortSignal.timeout(5000),
|
|
844
|
+
}),
|
|
845
|
+
]);
|
|
846
|
+
|
|
847
|
+
// Process deltas (fire-and-forget log append)
|
|
848
|
+
if (deltasRes?.ok) {
|
|
849
|
+
const deltas = await deltasRes.json();
|
|
850
|
+
if (deltas?.length > 0) {
|
|
851
|
+
const logs = deltas.map((d: any) => formatDelta(d)).filter(Boolean);
|
|
852
|
+
if (logs.length > 0) {
|
|
853
|
+
// Fire-and-forget
|
|
854
|
+
ctx.scheduler.runAfter(0, api.workflows.sandboxConvex.appendLogs, {
|
|
855
|
+
sessionId: args.sessionId,
|
|
856
|
+
logs,
|
|
857
|
+
});
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
if (!statusRes.ok) {
|
|
863
|
+
throw new Error(`Status fetch failed: ${statusRes.status}`);
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
const messages = await statusRes.json();
|
|
867
|
+
const lastMessage = messages?.[messages.length - 1];
|
|
868
|
+
|
|
869
|
+
// Check if the last message indicates completion
|
|
870
|
+
const isComplete =
|
|
871
|
+
lastMessage?.role === "assistant" &&
|
|
872
|
+
!lastMessage?.inProgress &&
|
|
873
|
+
messages.length > 1;
|
|
874
|
+
|
|
875
|
+
if (isComplete) {
|
|
876
|
+
const elapsed = Date.now() - args.startTime;
|
|
877
|
+
const output = extractOutput(messages);
|
|
878
|
+
|
|
879
|
+
// Critical: update session status (must await)
|
|
880
|
+
await ctx.runMutation(
|
|
881
|
+
internal.workflows.sandboxConvex.updateSession,
|
|
882
|
+
{
|
|
883
|
+
sessionId: args.sessionId,
|
|
884
|
+
status: "completed",
|
|
885
|
+
output,
|
|
886
|
+
metrics: {
|
|
887
|
+
totalMs: elapsed,
|
|
888
|
+
pollCount: args.pollCount,
|
|
889
|
+
messageCount: messages.length,
|
|
890
|
+
},
|
|
891
|
+
},
|
|
892
|
+
);
|
|
893
|
+
|
|
894
|
+
// Fire-and-forget: log + sandbox cleanup
|
|
895
|
+
logAsync(`✅ Completed in ${(elapsed / 1000).toFixed(1)}s`);
|
|
896
|
+
logSandboxCleanup(args.sandboxId);
|
|
897
|
+
return;
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
// Not done, schedule next poll (fire-and-forget)
|
|
901
|
+
if (args.pollCount < maxPolls) {
|
|
902
|
+
ctx.scheduler.runAfter(pollInterval, internal.workflows.sandboxConvex.pollCompletion, {
|
|
903
|
+
...args,
|
|
904
|
+
pollCount: args.pollCount + 1,
|
|
905
|
+
});
|
|
906
|
+
} else {
|
|
907
|
+
throw new Error(`Timeout: Agent still running after ${maxPolls} polls`);
|
|
908
|
+
}
|
|
909
|
+
} catch (error) {
|
|
910
|
+
const message =
|
|
911
|
+
error instanceof Error ? error.message : String(error);
|
|
912
|
+
|
|
913
|
+
// Critical: update session status (must await)
|
|
914
|
+
await ctx.runMutation(
|
|
915
|
+
internal.workflows.sandboxConvex.updateSession,
|
|
916
|
+
{
|
|
917
|
+
sessionId: args.sessionId,
|
|
918
|
+
status: "failed",
|
|
919
|
+
error: message,
|
|
920
|
+
},
|
|
921
|
+
);
|
|
922
|
+
|
|
923
|
+
// Fire-and-forget: log + cleanup
|
|
924
|
+
logAsync(`❌ Poll error: ${message}`);
|
|
925
|
+
logSandboxCleanup(args.sandboxId);
|
|
926
|
+
}
|
|
927
|
+
},
|
|
928
|
+
});
|
|
929
|
+
|
|
930
|
+
/**
|
|
931
|
+
* Timeout watchdog - OPTIMIZED
|
|
932
|
+
*/
|
|
933
|
+
export const timeoutWatchdog = internalAction({
|
|
934
|
+
args: {
|
|
935
|
+
sessionId: v.id("convexSandboxSessions"),
|
|
936
|
+
sandboxId: v.string(),
|
|
937
|
+
startTime: v.number(),
|
|
938
|
+
},
|
|
939
|
+
handler: async (ctx, args) => {
|
|
940
|
+
const session = await ctx.runQuery(
|
|
941
|
+
api.workflows.sandboxConvex.getSession,
|
|
942
|
+
{ sessionId: args.sessionId },
|
|
943
|
+
);
|
|
944
|
+
|
|
945
|
+
if (
|
|
946
|
+
session?.status === "completed" ||
|
|
947
|
+
session?.status === "failed" ||
|
|
948
|
+
session?.status === "cancelled"
|
|
949
|
+
) {
|
|
950
|
+
return;
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
const elapsed = Date.now() - args.startTime;
|
|
954
|
+
|
|
955
|
+
// Critical: update session status (must await)
|
|
956
|
+
await ctx.runMutation(
|
|
957
|
+
internal.workflows.sandboxConvex.updateSession,
|
|
958
|
+
{
|
|
959
|
+
sessionId: args.sessionId,
|
|
960
|
+
status: "failed",
|
|
961
|
+
error: `Timeout after ${(elapsed / 1000 / 60).toFixed(1)} minutes`,
|
|
962
|
+
},
|
|
963
|
+
);
|
|
964
|
+
|
|
965
|
+
// Fire-and-forget: log + cleanup
|
|
966
|
+
ctx.scheduler.runAfter(0, internal.workflows.sandboxConvex.appendLog, {
|
|
967
|
+
sessionId: args.sessionId,
|
|
968
|
+
message: `⏱️ Session timed out after ${(elapsed / 1000 / 60).toFixed(1)} minutes`,
|
|
969
|
+
});
|
|
970
|
+
logSandboxCleanup(args.sandboxId);
|
|
971
|
+
},
|
|
972
|
+
});
|
|
973
|
+
|
|
974
|
+
/**
|
|
975
|
+
* Cancel a session
|
|
976
|
+
*/
|
|
977
|
+
export const cancelSession = action({
|
|
978
|
+
args: { sessionId: v.id("convexSandboxSessions") },
|
|
979
|
+
handler: async (ctx, args) => {
|
|
980
|
+
const session = await ctx.runQuery(
|
|
981
|
+
api.workflows.sandboxConvex.getSession,
|
|
982
|
+
{ sessionId: args.sessionId },
|
|
983
|
+
);
|
|
984
|
+
|
|
985
|
+
if (!session) {
|
|
986
|
+
throw new Error("Session not found");
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
await ctx.runMutation(
|
|
990
|
+
internal.workflows.sandboxConvex.updateSession,
|
|
991
|
+
{
|
|
992
|
+
sessionId: args.sessionId,
|
|
993
|
+
status: "cancelled",
|
|
994
|
+
},
|
|
995
|
+
);
|
|
996
|
+
|
|
997
|
+
if (session.sandboxId) {
|
|
998
|
+
logSandboxCleanup(session.sandboxId);
|
|
999
|
+
}
|
|
1000
|
+
},
|
|
1001
|
+
});
|
|
1002
|
+
|
|
1003
|
+
// ============================================
|
|
1004
|
+
// Helpers
|
|
1005
|
+
// ============================================
|
|
1006
|
+
|
|
1007
|
+
function formatDelta(delta: any): string | null {
|
|
1008
|
+
if (!delta) return null;
|
|
1009
|
+
|
|
1010
|
+
if (delta.type === "text" && delta.text) {
|
|
1011
|
+
return delta.text.slice(0, 200);
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
if (delta.type === "tool-call") {
|
|
1015
|
+
return `🔧 ${delta.toolName}...`;
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
if (delta.type === "tool-result") {
|
|
1019
|
+
const success = delta.result?.success !== false;
|
|
1020
|
+
return `${success ? "✅" : "❌"} ${delta.toolName}`;
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
return null;
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
function extractOutput(messages: any[]): any {
|
|
1027
|
+
const toolCalls: any[] = [];
|
|
1028
|
+
let response = "";
|
|
1029
|
+
|
|
1030
|
+
for (const msg of messages) {
|
|
1031
|
+
if (msg.role === "assistant") {
|
|
1032
|
+
if (msg.content) {
|
|
1033
|
+
response = msg.content;
|
|
1034
|
+
}
|
|
1035
|
+
if (msg.toolCalls) {
|
|
1036
|
+
toolCalls.push(...msg.toolCalls);
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
return {
|
|
1042
|
+
response,
|
|
1043
|
+
toolCalls,
|
|
1044
|
+
messageCount: messages.length,
|
|
1045
|
+
};
|
|
1046
|
+
}
|