research-copilot 0.2.1 → 0.2.3
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/app/out/main/index.mjs +2520 -201
- package/app/out/preload/index.js +21 -0
- package/app/out/renderer/assets/{MilkdownMarkdownEditor-D7GYpVZn.js → MilkdownMarkdownEditor-DjFLwyh4.js} +50 -50
- package/app/out/renderer/assets/{arc-Kp4J_Jd7.js → arc-7NllWjXF.js} +1 -1
- package/app/out/renderer/assets/{blockDiagram-c4efeb88-DkMSdn8j.js → blockDiagram-c4efeb88-DsOU-_zA.js} +8 -8
- package/app/out/renderer/assets/{c4Diagram-c83219d4-DqAGxrYw.js → c4Diagram-c83219d4-U2jdevgc.js} +3 -3
- package/app/out/renderer/assets/{channel-S4GQrISQ.js → channel-Cjd8jvi8.js} +1 -1
- package/app/out/renderer/assets/{classDiagram-beda092f-B7AsTCEg.js → classDiagram-beda092f-Yj0hyRiU.js} +6 -6
- package/app/out/renderer/assets/{classDiagram-v2-2358418a-B4oFy-In.js → classDiagram-v2-2358418a-CK0FL8Pk.js} +10 -10
- package/app/out/renderer/assets/{clone-Dv1e6zYr.js → clone-I16owcK3.js} +1 -1
- package/app/out/renderer/assets/{createText-1719965b-HBXHvWlI.js → createText-1719965b-D_GY3BGW.js} +2 -2
- package/app/out/renderer/assets/{edges-96097737-B6X5lcC0.js → edges-96097737-DKD2znJk.js} +3 -3
- package/app/out/renderer/assets/{erDiagram-0228fc6a-BmBmTBlH.js → erDiagram-0228fc6a-CxrCv4uP.js} +5 -5
- package/app/out/renderer/assets/{flowDb-c6c81e3f-CObz36ob.js → flowDb-c6c81e3f-8o4wFApi.js} +1 -1
- package/app/out/renderer/assets/{flowDiagram-50d868cf-C2hFHxwF.js → flowDiagram-50d868cf-B1eYKlZC.js} +12 -12
- package/app/out/renderer/assets/{flowDiagram-v2-4f6560a1-DEe8EygW.js → flowDiagram-v2-4f6560a1-Y4_T8Xi1.js} +12 -12
- package/app/out/renderer/assets/{flowchart-elk-definition-6af322e1-CgTtfYKk.js → flowchart-elk-definition-6af322e1-BjDPU2Ol.js} +6 -6
- package/app/out/renderer/assets/{ganttDiagram-a2739b55-C5Pq4zEy.js → ganttDiagram-a2739b55-CdQAT7up.js} +3 -3
- package/app/out/renderer/assets/{gitGraphDiagram-82fe8481-oLp0f8Ll.js → gitGraphDiagram-82fe8481-Bl7KP_G5.js} +2 -2
- package/app/out/renderer/assets/{graph-51iZ6wgR.js → graph-Bje_pjwC.js} +1 -1
- package/app/out/renderer/assets/{index-5325376f-yLvOW-Os.js → index-5325376f-DB5UfMSQ.js} +6 -6
- package/app/out/renderer/assets/{index-CmpSV9Ld.js → index-BHl_tELv.js} +5 -5
- package/app/out/renderer/assets/{index-DppxBL77.js → index-BWkpc_Fb.js} +3 -3
- package/app/out/renderer/assets/{index-BMsuFGn6.js → index-BX5-hWqk.js} +3 -3
- package/app/out/renderer/assets/{index-BSd80-j9.js → index-BbbK94Tl.js} +4 -4
- package/app/out/renderer/assets/{index-CAOQIqEc.js → index-BlSmCt8A.js} +6 -6
- package/app/out/renderer/assets/{index-ohN9yRWw.js → index-BnGN3XMZ.js} +6 -6
- package/app/out/renderer/assets/{index-_Z53hJps.js → index-CDWVrn8L.js} +3 -3
- package/app/out/renderer/assets/{index-Du-Z3sl4.js → index-CISiP4Vc.js} +1295 -487
- package/app/out/renderer/assets/{index-L4DJn7cw.css → index-CT1HtzVp.css} +157 -10
- package/app/out/renderer/assets/{index-shoMWskw.js → index-CTrj2aB2.js} +3 -3
- package/app/out/renderer/assets/{index-Cn2e13ja.js → index-CU1ei-Q-.js} +6 -6
- package/app/out/renderer/assets/{index-Bscx_5dF.js → index-CoIRGlb6.js} +3 -3
- package/app/out/renderer/assets/{index-32eUzqVW.js → index-D9YGyBA9.js} +3 -3
- package/app/out/renderer/assets/{index-B9a4DKM-.js → index-DCr-i28b.js} +3 -3
- package/app/out/renderer/assets/{index-BQA_Kvr6.js → index-DFmh36hv.js} +3 -3
- package/app/out/renderer/assets/{index-FGsCVYSr.js → index-DIB3jH2J.js} +1 -1
- package/app/out/renderer/assets/{index-CTmGCKqa.js → index-DRgsVAez.js} +4 -4
- package/app/out/renderer/assets/{index-BfWWn8B_.js → index-DihmTRDX.js} +6 -6
- package/app/out/renderer/assets/{index-UajPJYNV.js → index-DkTtiwdL.js} +3 -3
- package/app/out/renderer/assets/{index-D_Y7v6pE.js → index-K7KmUSPg.js} +3 -3
- package/app/out/renderer/assets/{index-y1Od1ed6.js → index-TtJcntKb.js} +3 -3
- package/app/out/renderer/assets/{index-_iFRQTkA.js → index-UYsogkLv.js} +6 -6
- package/app/out/renderer/assets/{index-AuZa-hTj.js → index-bPpSppnq.js} +3 -3
- package/app/out/renderer/assets/{index-DjqJjt6u.js → index-hI44wokU.js} +6 -6
- package/app/out/renderer/assets/{infoDiagram-8eee0895-Cm0Hm5ZX.js → infoDiagram-8eee0895-Dfty_4jM.js} +2 -2
- package/app/out/renderer/assets/{journeyDiagram-c64418c1-A2Gw9bVu.js → journeyDiagram-c64418c1-BzDYpsjx.js} +4 -4
- package/app/out/renderer/assets/{layout-C5N2nTfF.js → layout-DbHSNFr9.js} +2 -2
- package/app/out/renderer/assets/{line-Dn6BEQAK.js → line-jV7QLjZ_.js} +1 -1
- package/app/out/renderer/assets/{linear-8wk0rPUX.js → linear-CIeuNnVa.js} +1 -1
- package/app/out/renderer/assets/{mindmap-definition-8da855dc-BVy6ISnb.js → mindmap-definition-8da855dc-CeDEmTB-.js} +3 -3
- package/app/out/renderer/assets/{pieDiagram-a8764435-B9_axIHE.js → pieDiagram-a8764435-Bpm1FcX3.js} +3 -3
- package/app/out/renderer/assets/{quadrantDiagram-1e28029f-B1kmkDFg.js → quadrantDiagram-1e28029f-CEB7PxJa.js} +3 -3
- package/app/out/renderer/assets/{requirementDiagram-08caed73-C_bNWUtT.js → requirementDiagram-08caed73-CZJU8B_1.js} +5 -5
- package/app/out/renderer/assets/{sankeyDiagram-a04cb91d-CD2h1LiI.js → sankeyDiagram-a04cb91d-JXpwgeE0.js} +2 -2
- package/app/out/renderer/assets/{sequenceDiagram-c5b8d532-B6d6cuqi.js → sequenceDiagram-c5b8d532-CtvDQG5X.js} +3 -3
- package/app/out/renderer/assets/{stateDiagram-1ecb1508-CkuNj_3H.js → stateDiagram-1ecb1508-DQRUQm_b.js} +6 -6
- package/app/out/renderer/assets/{stateDiagram-v2-c2b004d7-CevZ3tno.js → stateDiagram-v2-c2b004d7-qWcRpqpV.js} +10 -10
- package/app/out/renderer/assets/{styles-b4e223ce-DAe5WQrg.js → styles-b4e223ce-6DWryYQB.js} +1 -1
- package/app/out/renderer/assets/{styles-ca3715f6-BDSX88bY.js → styles-ca3715f6-CZ9SeO17.js} +1 -1
- package/app/out/renderer/assets/{styles-d45a18b0-SE9h7les.js → styles-d45a18b0-Bkmie_Qv.js} +4 -4
- package/app/out/renderer/assets/{svgDrawCommon-b86b1483-D1mpNbDQ.js → svgDrawCommon-b86b1483-BWRX1ZPq.js} +1 -1
- package/app/out/renderer/assets/{timeline-definition-faaaa080-7Ha-nm4M.js → timeline-definition-faaaa080-XcgfjspK.js} +3 -3
- package/app/out/renderer/assets/{xychartDiagram-f5964ef8-DLy7iyZW.js → xychartDiagram-f5964ef8-C0CQzgr8.js} +5 -5
- package/app/out/renderer/index.html +2 -2
- package/package.json +1 -1
package/app/out/main/index.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { app, shell, ipcMain, BrowserWindow, dialog, Menu } from "electron";
|
|
2
2
|
import { setMaxListeners } from "node:events";
|
|
3
3
|
import fs, { existsSync as existsSync$1 } from "node:fs";
|
|
4
|
-
import { execFile, execSync } from "node:child_process";
|
|
4
|
+
import { execFile, spawn, execSync } from "node:child_process";
|
|
5
5
|
import path, { resolve, join, sep, isAbsolute, extname, basename, dirname, relative } from "path";
|
|
6
6
|
import { existsSync, statSync, readdirSync, readFileSync, writeFileSync, mkdirSync, appendFileSync, rmSync, unlinkSync, renameSync } from "fs";
|
|
7
7
|
import os$1, { homedir } from "os";
|
|
@@ -13,7 +13,7 @@ import { Type } from "@sinclair/typebox";
|
|
|
13
13
|
import { mkdir, writeFile } from "fs/promises";
|
|
14
14
|
import path$1 from "node:path";
|
|
15
15
|
import fsp from "node:fs/promises";
|
|
16
|
-
import { createHash as createHash$1 } from "node:crypto";
|
|
16
|
+
import crypto$1, { createHash as createHash$1 } from "node:crypto";
|
|
17
17
|
import { promisify } from "node:util";
|
|
18
18
|
import os from "node:os";
|
|
19
19
|
import { fileURLToPath } from "node:url";
|
|
@@ -262,30 +262,83 @@ function loadOrCreateSessionId(rootPathKey, path2) {
|
|
|
262
262
|
writeFileSync(sessionFile, JSON.stringify({ sessionId: newId }));
|
|
263
263
|
return newId;
|
|
264
264
|
}
|
|
265
|
-
|
|
265
|
+
const CODEX_CRED_FILE = join(CONFIG_DIR, "openai-codex-credentials.json");
|
|
266
|
+
function loadCodexCredentials() {
|
|
267
|
+
try {
|
|
268
|
+
if (existsSync(CODEX_CRED_FILE)) {
|
|
269
|
+
const data = JSON.parse(readFileSync(CODEX_CRED_FILE, "utf-8"));
|
|
270
|
+
if (data.access && data.refresh) return data;
|
|
271
|
+
}
|
|
272
|
+
} catch {
|
|
273
|
+
}
|
|
274
|
+
try {
|
|
275
|
+
const codexHome = join(homedir(), ".codex");
|
|
276
|
+
const codexAuth = join(codexHome, "auth.json");
|
|
277
|
+
if (existsSync(codexAuth)) {
|
|
278
|
+
const data = JSON.parse(readFileSync(codexAuth, "utf-8"));
|
|
279
|
+
if (data.tokens?.access_token && data.tokens?.refresh_token) {
|
|
280
|
+
return {
|
|
281
|
+
access: data.tokens.access_token,
|
|
282
|
+
refresh: data.tokens.refresh_token,
|
|
283
|
+
expires: data.tokens.expires_at ? data.tokens.expires_at * 1e3 : Date.now() + 36e5,
|
|
284
|
+
accountId: data.tokens.account_id
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
} catch {
|
|
289
|
+
}
|
|
290
|
+
return null;
|
|
291
|
+
}
|
|
292
|
+
function saveCodexCredentials(creds) {
|
|
293
|
+
if (!existsSync(CONFIG_DIR)) mkdirSync(CONFIG_DIR, { recursive: true });
|
|
294
|
+
writeFileSync(CODEX_CRED_FILE, JSON.stringify(creds, null, 2), { mode: 384 });
|
|
295
|
+
}
|
|
296
|
+
function clearCodexCredentials() {
|
|
297
|
+
try {
|
|
298
|
+
if (existsSync(CODEX_CRED_FILE)) {
|
|
299
|
+
const { unlinkSync: unlinkSync2 } = require2("fs");
|
|
300
|
+
unlinkSync2(CODEX_CRED_FILE);
|
|
301
|
+
}
|
|
302
|
+
} catch {
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
function resolveCoordinatorAuth(compositeKey) {
|
|
266
306
|
const openaiApiKey = (process.env.OPENAI_API_KEY || "").trim();
|
|
267
307
|
const anthropicApiKey = (process.env.ANTHROPIC_API_KEY || "").trim();
|
|
268
|
-
const
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
apiKey: openaiApiKey,
|
|
275
|
-
authMode: "api-key",
|
|
276
|
-
isAnthropicModel: false,
|
|
277
|
-
billingSource: "api-key"
|
|
278
|
-
};
|
|
308
|
+
const i = compositeKey.indexOf(":");
|
|
309
|
+
let provider;
|
|
310
|
+
if (i > 0) {
|
|
311
|
+
provider = compositeKey.slice(0, i);
|
|
312
|
+
} else {
|
|
313
|
+
provider = compositeKey.startsWith("claude-") ? "anthropic" : "openai";
|
|
279
314
|
}
|
|
280
|
-
|
|
281
|
-
|
|
315
|
+
switch (provider) {
|
|
316
|
+
case "openai-codex": {
|
|
317
|
+
const creds = loadCodexCredentials();
|
|
318
|
+
if (!creds) {
|
|
319
|
+
throw new Error("ChatGPT subscription login required. Please sign in via the model selector.");
|
|
320
|
+
}
|
|
321
|
+
return {
|
|
322
|
+
apiKey: creds.access,
|
|
323
|
+
authMode: "subscription",
|
|
324
|
+
isAnthropicModel: false,
|
|
325
|
+
billingSource: "subscription",
|
|
326
|
+
piProvider: "openai-codex"
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
case "anthropic": {
|
|
330
|
+
if (!anthropicApiKey) {
|
|
331
|
+
throw new Error("ANTHROPIC_API_KEY is required for the selected Anthropic model.");
|
|
332
|
+
}
|
|
333
|
+
return { apiKey: anthropicApiKey, authMode: "api-key", isAnthropicModel: true, billingSource: "api-key" };
|
|
334
|
+
}
|
|
335
|
+
default: {
|
|
336
|
+
if (!openaiApiKey) {
|
|
337
|
+
throw new Error("OPENAI_API_KEY is required for the selected OpenAI model.");
|
|
338
|
+
}
|
|
339
|
+
return { apiKey: openaiApiKey, authMode: "api-key", isAnthropicModel: false, billingSource: "api-key" };
|
|
340
|
+
}
|
|
282
341
|
}
|
|
283
|
-
return {
|
|
284
|
-
apiKey: anthropicApiKey,
|
|
285
|
-
authMode: "api-key",
|
|
286
|
-
isAnthropicModel: true,
|
|
287
|
-
billingSource: "api-key"
|
|
288
|
-
};
|
|
289
342
|
}
|
|
290
343
|
function registerFileHandlers(handle, getCtx) {
|
|
291
344
|
handle("file:list-root", () => {
|
|
@@ -520,6 +573,51 @@ function registerAuthHandlers(handleRaw) {
|
|
|
520
573
|
hasApiKey: !!(process.env.OPENAI_API_KEY || "").trim()
|
|
521
574
|
};
|
|
522
575
|
});
|
|
576
|
+
handleRaw("auth:get-openai-codex-status", () => {
|
|
577
|
+
const creds = loadCodexCredentials();
|
|
578
|
+
return {
|
|
579
|
+
isLoggedIn: !!creds,
|
|
580
|
+
isExpired: creds ? creds.expires < Date.now() : false
|
|
581
|
+
};
|
|
582
|
+
});
|
|
583
|
+
handleRaw("auth:openai-codex-login", async () => {
|
|
584
|
+
const { loginOpenAICodex } = await import("@mariozechner/pi-ai/oauth");
|
|
585
|
+
const { shell: shell2 } = await import("electron");
|
|
586
|
+
try {
|
|
587
|
+
const creds = await loginOpenAICodex({
|
|
588
|
+
onAuth: (info) => {
|
|
589
|
+
shell2.openExternal(info.url);
|
|
590
|
+
},
|
|
591
|
+
onPrompt: async (prompt) => {
|
|
592
|
+
console.warn("[OAuth] Unexpected prompt:", prompt.message);
|
|
593
|
+
return "";
|
|
594
|
+
},
|
|
595
|
+
onProgress: (msg) => {
|
|
596
|
+
console.log("[OAuth]", msg);
|
|
597
|
+
}
|
|
598
|
+
});
|
|
599
|
+
saveCodexCredentials(creds);
|
|
600
|
+
return { success: true };
|
|
601
|
+
} catch (err) {
|
|
602
|
+
return { success: false, error: err.message || "OAuth login failed" };
|
|
603
|
+
}
|
|
604
|
+
});
|
|
605
|
+
handleRaw("auth:openai-codex-logout", () => {
|
|
606
|
+
clearCodexCredentials();
|
|
607
|
+
return { success: true };
|
|
608
|
+
});
|
|
609
|
+
handleRaw("auth:openai-codex-refresh", async () => {
|
|
610
|
+
const creds = loadCodexCredentials();
|
|
611
|
+
if (!creds) return { success: false, error: "Not logged in" };
|
|
612
|
+
try {
|
|
613
|
+
const { refreshOpenAICodexToken } = await import("@mariozechner/pi-ai/oauth");
|
|
614
|
+
const newCreds = await refreshOpenAICodexToken(creds);
|
|
615
|
+
saveCodexCredentials(newCreds);
|
|
616
|
+
return { success: true };
|
|
617
|
+
} catch (err) {
|
|
618
|
+
return { success: false, error: err.message || "Token refresh failed" };
|
|
619
|
+
}
|
|
620
|
+
});
|
|
523
621
|
}
|
|
524
622
|
function registerFolderOpenHandler(handle, getCtx) {
|
|
525
623
|
handle("folder:open-with", async (appName) => {
|
|
@@ -664,7 +762,9 @@ const PATHS = {
|
|
|
664
762
|
memory: ".research-pilot/memory",
|
|
665
763
|
// Skills
|
|
666
764
|
skills: ".research-pilot/skills",
|
|
667
|
-
skillsConfig: ".research-pilot/skills-config.json"
|
|
765
|
+
skillsConfig: ".research-pilot/skills-config.json",
|
|
766
|
+
// Local compute runs
|
|
767
|
+
computeRuns: ".research-pilot/compute-runs"
|
|
668
768
|
};
|
|
669
769
|
function nowIso() {
|
|
670
770
|
return (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -3100,7 +3200,7 @@ Additional context: ${extraContext}` : `Research request: ${query}`;
|
|
|
3100
3200
|
}
|
|
3101
3201
|
};
|
|
3102
3202
|
}
|
|
3103
|
-
const execFileAsync$
|
|
3203
|
+
const execFileAsync$3 = promisify(execFile);
|
|
3104
3204
|
const DEFAULT_MAX_OUTPUT_CHARS = 5e5;
|
|
3105
3205
|
const DEFAULT_PREVIEW_CHARS = 4e3;
|
|
3106
3206
|
const DEFAULT_COMMAND_TIMEOUT_MS = 12e4;
|
|
@@ -3231,7 +3331,7 @@ async function detectTextLikeFile(filePath) {
|
|
|
3231
3331
|
}
|
|
3232
3332
|
async function runCommand(command, args, timeoutMs) {
|
|
3233
3333
|
try {
|
|
3234
|
-
await execFileAsync$
|
|
3334
|
+
await execFileAsync$3(command, args, {
|
|
3235
3335
|
timeout: timeoutMs,
|
|
3236
3336
|
maxBuffer: 10 * 1024 * 1024
|
|
3237
3337
|
});
|
|
@@ -3322,7 +3422,7 @@ print(json.dumps(result, ensure_ascii=False))
|
|
|
3322
3422
|
let foundAnyRuntime = false;
|
|
3323
3423
|
for (const attempt of attempts) {
|
|
3324
3424
|
try {
|
|
3325
|
-
const { stdout } = await execFileAsync$
|
|
3425
|
+
const { stdout } = await execFileAsync$3(
|
|
3326
3426
|
attempt.command,
|
|
3327
3427
|
["-c", script, inputPath, JSON.stringify(ranges)],
|
|
3328
3428
|
{ timeout: timeoutMs, maxBuffer: 50 * 1024 * 1024, encoding: "utf8" }
|
|
@@ -3828,187 +3928,2245 @@ function createConvertDocumentTool(ctx) {
|
|
|
3828
3928
|
page_count: pageCount,
|
|
3829
3929
|
page_ranges: pageRanges.length > 0 ? pageRanges.map((r) => r.raw) : void 0
|
|
3830
3930
|
};
|
|
3831
|
-
return success(payload);
|
|
3931
|
+
return success(payload);
|
|
3932
|
+
}
|
|
3933
|
+
};
|
|
3934
|
+
}
|
|
3935
|
+
const execFileAsync$2 = promisify(execFile);
|
|
3936
|
+
const DATA_ANALYSIS_SYSTEM = loadPrompt("data-analysis-system");
|
|
3937
|
+
const DATA_ANALYSIS_TASKS = loadPrompt("data-analysis-tasks");
|
|
3938
|
+
const DATA_CODE_TEMPLATE = loadPrompt("data-code-template");
|
|
3939
|
+
const TASK_DESCRIPTIONS = {
|
|
3940
|
+
analyze: "Statistical Analysis",
|
|
3941
|
+
visualize: "Data Visualization",
|
|
3942
|
+
transform: "Data Transformation",
|
|
3943
|
+
model: "Statistical Modeling"
|
|
3944
|
+
};
|
|
3945
|
+
const DataAnalyzeSchema = Type.Object({
|
|
3946
|
+
file_path: Type.String({ description: "Relative path to data file (CSV, JSON, TSV, XLSX)" }),
|
|
3947
|
+
instructions: Type.String({ description: "What analysis to perform" }),
|
|
3948
|
+
task_type: Type.Optional(
|
|
3949
|
+
Type.String({ description: "analyze | visualize | transform | model (default: analyze)" })
|
|
3950
|
+
)
|
|
3951
|
+
});
|
|
3952
|
+
function createDataAnalyzeTool(ctx) {
|
|
3953
|
+
return {
|
|
3954
|
+
name: "data_analyze",
|
|
3955
|
+
label: "Data Analysis",
|
|
3956
|
+
description: "Analyze a dataset using Python. Supports statistics, visualization (matplotlib/seaborn), data transformation, and modeling. Generated outputs (figures, tables) are saved to disk.\nUsage guidelines: (1) Use this tool for ANY analysis, visualization, statistics, or modeling — do not compute from raw data with read/grep. (2) Generate only the outputs the user requested; no extras.",
|
|
3957
|
+
parameters: DataAnalyzeSchema,
|
|
3958
|
+
execute: async (_toolCallId, rawParams) => {
|
|
3959
|
+
const params = rawParams;
|
|
3960
|
+
const filePath = typeof params.file_path === "string" ? params.file_path.trim() : "";
|
|
3961
|
+
const instructions = typeof params.instructions === "string" ? params.instructions.trim() : "";
|
|
3962
|
+
const taskType = typeof params.task_type === "string" ? params.task_type.trim().toLowerCase() : "analyze";
|
|
3963
|
+
if (!filePath) {
|
|
3964
|
+
return toAgentResult("data_analyze", toolError("MISSING_PARAMETER", "Missing file_path.", {
|
|
3965
|
+
suggestions: ["Provide a relative path to the data file (CSV, JSON, TSV, or XLSX)."]
|
|
3966
|
+
}));
|
|
3967
|
+
}
|
|
3968
|
+
if (!instructions) {
|
|
3969
|
+
return toAgentResult("data_analyze", toolError("MISSING_PARAMETER", "Missing instructions.", {
|
|
3970
|
+
suggestions: ["Describe what analysis to perform on the data."]
|
|
3971
|
+
}));
|
|
3972
|
+
}
|
|
3973
|
+
if (!["analyze", "visualize", "transform", "model"].includes(taskType)) {
|
|
3974
|
+
return toAgentResult("data_analyze", toolError("INVALID_PARAMETER", `Invalid task_type: ${taskType}. Use: analyze | visualize | transform | model.`, {
|
|
3975
|
+
suggestions: ["Valid task types: analyze, visualize, transform, model."]
|
|
3976
|
+
}));
|
|
3977
|
+
}
|
|
3978
|
+
const absDataFile = path$1.resolve(ctx.workspacePath, filePath);
|
|
3979
|
+
if (!fs.existsSync(absDataFile)) {
|
|
3980
|
+
return toAgentResult("data_analyze", toolError("FILE_NOT_FOUND", `File not found: ${filePath}`, {
|
|
3981
|
+
suggestions: [
|
|
3982
|
+
"Verify the file path is relative to the workspace root.",
|
|
3983
|
+
"Use the find or glob tool to locate the data file."
|
|
3984
|
+
],
|
|
3985
|
+
context: { workspacePath: ctx.workspacePath, resolvedPath: absDataFile }
|
|
3986
|
+
}));
|
|
3987
|
+
}
|
|
3988
|
+
const runId = Date.now().toString(36);
|
|
3989
|
+
const outputBase = path$1.join(ctx.workspacePath, ".research-pilot", "data-runs", runId);
|
|
3990
|
+
const figuresDir = path$1.join(outputBase, "figures");
|
|
3991
|
+
const tablesDir = path$1.join(outputBase, "tables");
|
|
3992
|
+
const dataDir = path$1.join(outputBase, "data");
|
|
3993
|
+
fs.mkdirSync(figuresDir, { recursive: true });
|
|
3994
|
+
fs.mkdirSync(tablesDir, { recursive: true });
|
|
3995
|
+
fs.mkdirSync(dataDir, { recursive: true });
|
|
3996
|
+
const resultsFile = path$1.join(outputBase, "results.json");
|
|
3997
|
+
const rawPreview = fs.readFileSync(absDataFile, "utf-8").slice(0, 2e3);
|
|
3998
|
+
const ext = path$1.extname(absDataFile).toLowerCase();
|
|
3999
|
+
const formatHint = ext === ".csv" ? "CSV" : ext === ".tsv" ? "TSV" : ext === ".json" ? "JSON" : ext === ".xlsx" ? "XLSX" : "unknown";
|
|
4000
|
+
if (!ctx.callLlm) {
|
|
4001
|
+
return toAgentResult("data_analyze", toolError("LLM_UNAVAILABLE", "LLM not available for code generation.", {
|
|
4002
|
+
suggestions: ["Ensure the agent runtime has an LLM provider configured (callLlm in ResearchToolContext)."]
|
|
4003
|
+
}));
|
|
4004
|
+
}
|
|
4005
|
+
const taskSection = DATA_ANALYSIS_TASKS.split(/^## /m).find((s) => s.startsWith(taskType));
|
|
4006
|
+
const taskDesc = taskSection ? `## ${taskSection}` : "";
|
|
4007
|
+
const userPrompt = [
|
|
4008
|
+
`Task type: ${taskType} (${TASK_DESCRIPTIONS[taskType] ?? "Analysis"})`,
|
|
4009
|
+
"",
|
|
4010
|
+
taskDesc,
|
|
4011
|
+
"",
|
|
4012
|
+
`Data file format: ${formatHint}`,
|
|
4013
|
+
`Data file preview (first 2000 chars):`,
|
|
4014
|
+
"```",
|
|
4015
|
+
rawPreview,
|
|
4016
|
+
"```",
|
|
4017
|
+
"",
|
|
4018
|
+
`Instructions: ${instructions}`,
|
|
4019
|
+
"",
|
|
4020
|
+
"IMPORTANT: Use the pre-defined path variables (DATA_FILE, FIGURES_DIR, TABLES_DIR, DATA_DIR, RESULTS_FILE).",
|
|
4021
|
+
"Call write_results() at the end with your outputs list and summary dict.",
|
|
4022
|
+
"Output ONLY the Python code in a ```python code block."
|
|
4023
|
+
].join("\n");
|
|
4024
|
+
let generatedCode;
|
|
4025
|
+
try {
|
|
4026
|
+
const llmResponse = await ctx.callLlm(DATA_ANALYSIS_SYSTEM, userPrompt);
|
|
4027
|
+
const codeMatch = llmResponse.match(/```python\n([\s\S]*?)```/) || llmResponse.match(/```\n([\s\S]*?)```/);
|
|
4028
|
+
generatedCode = codeMatch ? codeMatch[1] : llmResponse;
|
|
4029
|
+
} catch (err) {
|
|
4030
|
+
return toAgentResult("data_analyze", toolError("EXECUTION_FAILED", `Code generation failed: ${err.message}`, {
|
|
4031
|
+
retryable: true,
|
|
4032
|
+
suggestions: ["Retry — the LLM may produce valid code on a subsequent attempt.", "Try simplifying the instructions."]
|
|
4033
|
+
}));
|
|
4034
|
+
}
|
|
4035
|
+
const pathDefinitions = [
|
|
4036
|
+
"",
|
|
4037
|
+
"# Pre-defined paths (set by the tool runtime)",
|
|
4038
|
+
`DATA_FILE = ${JSON.stringify(absDataFile)}`,
|
|
4039
|
+
`FIGURES_DIR = ${JSON.stringify(figuresDir)}`,
|
|
4040
|
+
`TABLES_DIR = ${JSON.stringify(tablesDir)}`,
|
|
4041
|
+
`DATA_DIR = ${JSON.stringify(dataDir)}`,
|
|
4042
|
+
`RESULTS_FILE = ${JSON.stringify(resultsFile)}`,
|
|
4043
|
+
""
|
|
4044
|
+
].join("\n");
|
|
4045
|
+
const fullScript = DATA_CODE_TEMPLATE + pathDefinitions + "\n" + generatedCode;
|
|
4046
|
+
const scriptPath = path$1.join(outputBase, "script.py");
|
|
4047
|
+
fs.writeFileSync(scriptPath, fullScript, "utf-8");
|
|
4048
|
+
try {
|
|
4049
|
+
const { stdout, stderr } = await execFileAsync$2("python3", [scriptPath], {
|
|
4050
|
+
cwd: ctx.workspacePath,
|
|
4051
|
+
timeout: 12e4,
|
|
4052
|
+
// 2 minutes
|
|
4053
|
+
maxBuffer: 10 * 1024 * 1024
|
|
4054
|
+
});
|
|
4055
|
+
let manifest = null;
|
|
4056
|
+
if (fs.existsSync(resultsFile)) {
|
|
4057
|
+
try {
|
|
4058
|
+
manifest = JSON.parse(fs.readFileSync(resultsFile, "utf-8"));
|
|
4059
|
+
} catch {
|
|
4060
|
+
}
|
|
4061
|
+
}
|
|
4062
|
+
const outputs = [];
|
|
4063
|
+
for (const [dir, type] of [
|
|
4064
|
+
[figuresDir, "figure"],
|
|
4065
|
+
[tablesDir, "table"],
|
|
4066
|
+
[dataDir, "data"]
|
|
4067
|
+
]) {
|
|
4068
|
+
if (fs.existsSync(dir)) {
|
|
4069
|
+
for (const f of fs.readdirSync(dir)) {
|
|
4070
|
+
outputs.push({
|
|
4071
|
+
name: f,
|
|
4072
|
+
type,
|
|
4073
|
+
path: path$1.relative(ctx.workspacePath, path$1.join(dir, f))
|
|
4074
|
+
});
|
|
4075
|
+
}
|
|
4076
|
+
}
|
|
4077
|
+
}
|
|
4078
|
+
const payload = {
|
|
4079
|
+
stdout: stdout.slice(0, 4e3),
|
|
4080
|
+
stderr: stderr ? stderr.slice(0, 1e3) : void 0,
|
|
4081
|
+
outputs,
|
|
4082
|
+
manifest: manifest ? {
|
|
4083
|
+
summary: manifest.summary,
|
|
4084
|
+
warnings: manifest.warnings
|
|
4085
|
+
} : void 0,
|
|
4086
|
+
scriptPath: path$1.relative(ctx.workspacePath, scriptPath),
|
|
4087
|
+
runId
|
|
4088
|
+
};
|
|
4089
|
+
return toAgentResult("data_analyze", { success: true, data: payload });
|
|
4090
|
+
} catch (err) {
|
|
4091
|
+
const errorDetail = err.stderr?.slice(0, 2e3) || err.message;
|
|
4092
|
+
return toAgentResult("data_analyze", toolError("EXECUTION_FAILED", `Python execution failed: ${errorDetail}`, {
|
|
4093
|
+
retryable: true,
|
|
4094
|
+
suggestions: [
|
|
4095
|
+
`Review the generated script at ${path$1.relative(ctx.workspacePath, scriptPath)} for errors.`,
|
|
4096
|
+
"Check that required Python packages (pandas, matplotlib, seaborn, etc.) are installed.",
|
|
4097
|
+
"Try simplifying the analysis instructions."
|
|
4098
|
+
],
|
|
4099
|
+
context: {
|
|
4100
|
+
scriptPath: path$1.relative(ctx.workspacePath, scriptPath),
|
|
4101
|
+
runId
|
|
4102
|
+
},
|
|
4103
|
+
data: {
|
|
4104
|
+
scriptPath: path$1.relative(ctx.workspacePath, scriptPath),
|
|
4105
|
+
runId
|
|
4106
|
+
}
|
|
4107
|
+
}));
|
|
4108
|
+
}
|
|
4109
|
+
}
|
|
4110
|
+
};
|
|
4111
|
+
}
|
|
4112
|
+
function isTerminal(state) {
|
|
4113
|
+
return state === "completed" || state === "failed" || state === "timed_out" || state === "cancelled";
|
|
4114
|
+
}
|
|
4115
|
+
const RUNS_FILE = "runs.jsonl";
|
|
4116
|
+
const EVICT_AGE_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
4117
|
+
const FLUSH_INTERVAL_MS = 3e4;
|
|
4118
|
+
class RunStore {
|
|
4119
|
+
constructor(projectPath) {
|
|
4120
|
+
this.records = /* @__PURE__ */ new Map();
|
|
4121
|
+
this.loaded = false;
|
|
4122
|
+
this.dirty = false;
|
|
4123
|
+
this.flushTimer = null;
|
|
4124
|
+
this.dir = path$1.join(projectPath, ".research-pilot", "compute-runs");
|
|
4125
|
+
this.filePath = path$1.join(this.dir, RUNS_FILE);
|
|
4126
|
+
}
|
|
4127
|
+
ensureDir() {
|
|
4128
|
+
if (!fs.existsSync(this.dir)) {
|
|
4129
|
+
fs.mkdirSync(this.dir, { recursive: true });
|
|
4130
|
+
}
|
|
4131
|
+
}
|
|
4132
|
+
load() {
|
|
4133
|
+
if (this.loaded) return;
|
|
4134
|
+
this.loaded = true;
|
|
4135
|
+
if (!fs.existsSync(this.filePath)) return;
|
|
4136
|
+
const content = fs.readFileSync(this.filePath, "utf-8");
|
|
4137
|
+
for (const line of content.split("\n")) {
|
|
4138
|
+
const trimmed = line.trim();
|
|
4139
|
+
if (!trimmed) continue;
|
|
4140
|
+
try {
|
|
4141
|
+
const record = JSON.parse(trimmed);
|
|
4142
|
+
this.records.set(record.runId, record);
|
|
4143
|
+
} catch {
|
|
4144
|
+
}
|
|
4145
|
+
}
|
|
4146
|
+
}
|
|
4147
|
+
writeToDisk() {
|
|
4148
|
+
this.ensureDir();
|
|
4149
|
+
const lines = Array.from(this.records.values()).map((r) => JSON.stringify(r)).join("\n");
|
|
4150
|
+
const tmpPath = this.filePath + ".tmp." + process.pid + "." + Date.now();
|
|
4151
|
+
fs.writeFileSync(tmpPath, lines + "\n", "utf-8");
|
|
4152
|
+
fs.renameSync(tmpPath, this.filePath);
|
|
4153
|
+
this.dirty = false;
|
|
4154
|
+
}
|
|
4155
|
+
/**
|
|
4156
|
+
* Mark store as dirty. Starts the periodic flush timer if not running.
|
|
4157
|
+
*/
|
|
4158
|
+
markDirty() {
|
|
4159
|
+
this.dirty = true;
|
|
4160
|
+
if (!this.flushTimer) {
|
|
4161
|
+
this.flushTimer = setInterval(() => {
|
|
4162
|
+
if (this.dirty) this.writeToDisk();
|
|
4163
|
+
}, FLUSH_INTERVAL_MS);
|
|
4164
|
+
if (this.flushTimer.unref) this.flushTimer.unref();
|
|
4165
|
+
}
|
|
4166
|
+
}
|
|
4167
|
+
/**
|
|
4168
|
+
* Flush immediately (synchronous). Called on critical events:
|
|
4169
|
+
* - createRun (new record must survive crash)
|
|
4170
|
+
* - Terminal state transition (completed/failed/cancelled/timed_out)
|
|
4171
|
+
* - Shutdown (destroy)
|
|
4172
|
+
*/
|
|
4173
|
+
flushNow() {
|
|
4174
|
+
if (this.dirty) this.writeToDisk();
|
|
4175
|
+
}
|
|
4176
|
+
/**
|
|
4177
|
+
* Stop the periodic flush timer. Called on destroy.
|
|
4178
|
+
*/
|
|
4179
|
+
stopFlushTimer() {
|
|
4180
|
+
if (this.flushTimer) {
|
|
4181
|
+
clearInterval(this.flushTimer);
|
|
4182
|
+
this.flushTimer = null;
|
|
4183
|
+
}
|
|
4184
|
+
}
|
|
4185
|
+
getRun(runId) {
|
|
4186
|
+
this.load();
|
|
4187
|
+
return this.records.get(runId);
|
|
4188
|
+
}
|
|
4189
|
+
getAllRuns() {
|
|
4190
|
+
this.load();
|
|
4191
|
+
return Array.from(this.records.values());
|
|
4192
|
+
}
|
|
4193
|
+
getActiveRuns() {
|
|
4194
|
+
this.load();
|
|
4195
|
+
return Array.from(this.records.values()).filter((r) => !isTerminal(r.status));
|
|
4196
|
+
}
|
|
4197
|
+
createRun(record) {
|
|
4198
|
+
this.load();
|
|
4199
|
+
this.records.set(record.runId, record);
|
|
4200
|
+
this.writeToDisk();
|
|
4201
|
+
}
|
|
4202
|
+
updateRun(runId, patch) {
|
|
4203
|
+
this.load();
|
|
4204
|
+
const existing = this.records.get(runId);
|
|
4205
|
+
if (!existing) return void 0;
|
|
4206
|
+
const updated = { ...existing, ...patch };
|
|
4207
|
+
this.records.set(runId, updated);
|
|
4208
|
+
if (patch.status && isTerminal(patch.status)) {
|
|
4209
|
+
this.writeToDisk();
|
|
4210
|
+
} else {
|
|
4211
|
+
this.markDirty();
|
|
4212
|
+
}
|
|
4213
|
+
return updated;
|
|
4214
|
+
}
|
|
4215
|
+
/**
|
|
4216
|
+
* Remove completed runs older than maxAgeMs.
|
|
4217
|
+
*/
|
|
4218
|
+
evictOld(maxAgeMs = EVICT_AGE_MS) {
|
|
4219
|
+
this.load();
|
|
4220
|
+
const cutoff = Date.now() - maxAgeMs;
|
|
4221
|
+
let evicted = 0;
|
|
4222
|
+
for (const [id, record] of this.records) {
|
|
4223
|
+
if (isTerminal(record.status) && record.completedAt) {
|
|
4224
|
+
const completedTime = new Date(record.completedAt).getTime();
|
|
4225
|
+
if (completedTime < cutoff) {
|
|
4226
|
+
this.records.delete(id);
|
|
4227
|
+
const runDir = this.getRunDir(id);
|
|
4228
|
+
try {
|
|
4229
|
+
fs.rmSync(runDir, { recursive: true, force: true });
|
|
4230
|
+
} catch {
|
|
4231
|
+
}
|
|
4232
|
+
evicted++;
|
|
4233
|
+
}
|
|
4234
|
+
}
|
|
4235
|
+
}
|
|
4236
|
+
if (evicted > 0) this.writeToDisk();
|
|
4237
|
+
return evicted;
|
|
4238
|
+
}
|
|
4239
|
+
getRunDir(runId) {
|
|
4240
|
+
return path$1.join(this.dir, runId);
|
|
4241
|
+
}
|
|
4242
|
+
getOutputPath(runId) {
|
|
4243
|
+
return path$1.join(this.dir, runId, "output.log");
|
|
4244
|
+
}
|
|
4245
|
+
getStderrPath(runId) {
|
|
4246
|
+
return path$1.join(this.dir, runId, "output.log.stderr");
|
|
4247
|
+
}
|
|
4248
|
+
}
|
|
4249
|
+
class ProcessSandbox {
|
|
4250
|
+
constructor() {
|
|
4251
|
+
this.name = "process";
|
|
4252
|
+
}
|
|
4253
|
+
async available() {
|
|
4254
|
+
return true;
|
|
4255
|
+
}
|
|
4256
|
+
async spawn(config) {
|
|
4257
|
+
const outputDir = path$1.dirname(config.outputPath);
|
|
4258
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
4259
|
+
const stderrPath = config.outputPath + ".stderr";
|
|
4260
|
+
const wrappedCommand = `( ${config.command} ) 2> >(tee -a ${JSON.stringify(stderrPath)} >&2) >> ${JSON.stringify(config.outputPath)} 2>&1`;
|
|
4261
|
+
fs.writeFileSync(config.outputPath, "");
|
|
4262
|
+
fs.writeFileSync(stderrPath, "");
|
|
4263
|
+
const child = spawn("/bin/bash", ["-c", wrappedCommand], {
|
|
4264
|
+
cwd: config.workDir,
|
|
4265
|
+
env: { ...process.env, ...config.env },
|
|
4266
|
+
stdio: ["ignore", "ignore", "ignore"],
|
|
4267
|
+
detached: true
|
|
4268
|
+
});
|
|
4269
|
+
const pid = child.pid;
|
|
4270
|
+
if (pid === void 0) {
|
|
4271
|
+
throw new Error("Failed to spawn process: no PID returned");
|
|
4272
|
+
}
|
|
4273
|
+
if (config.signal) {
|
|
4274
|
+
const onAbort = () => {
|
|
4275
|
+
try {
|
|
4276
|
+
process.kill(-pid, "SIGTERM");
|
|
4277
|
+
} catch {
|
|
4278
|
+
}
|
|
4279
|
+
};
|
|
4280
|
+
config.signal.addEventListener("abort", onAbort, { once: true });
|
|
4281
|
+
child.on("exit", () => config.signal.removeEventListener("abort", onAbort));
|
|
4282
|
+
}
|
|
4283
|
+
let settled = false;
|
|
4284
|
+
const waitPromise = new Promise((resolve2) => {
|
|
4285
|
+
child.on("exit", (code, signal) => {
|
|
4286
|
+
if (settled) return;
|
|
4287
|
+
settled = true;
|
|
4288
|
+
resolve2({
|
|
4289
|
+
exitCode: code ?? 1,
|
|
4290
|
+
exitSignal: signal ?? void 0
|
|
4291
|
+
});
|
|
4292
|
+
});
|
|
4293
|
+
child.on("error", () => {
|
|
4294
|
+
if (settled) return;
|
|
4295
|
+
settled = true;
|
|
4296
|
+
resolve2({ exitCode: 1, exitSignal: void 0 });
|
|
4297
|
+
});
|
|
4298
|
+
});
|
|
4299
|
+
return {
|
|
4300
|
+
pid,
|
|
4301
|
+
async kill(sig = "SIGTERM") {
|
|
4302
|
+
try {
|
|
4303
|
+
process.kill(-pid, sig);
|
|
4304
|
+
} catch {
|
|
4305
|
+
}
|
|
4306
|
+
},
|
|
4307
|
+
wait() {
|
|
4308
|
+
return waitPromise;
|
|
4309
|
+
},
|
|
4310
|
+
async cleanup() {
|
|
4311
|
+
try {
|
|
4312
|
+
child.unref();
|
|
4313
|
+
} catch {
|
|
4314
|
+
}
|
|
4315
|
+
}
|
|
4316
|
+
};
|
|
4317
|
+
}
|
|
4318
|
+
}
|
|
4319
|
+
promisify(execFile);
|
|
4320
|
+
let cachedProviders = null;
|
|
4321
|
+
async function detectProviders() {
|
|
4322
|
+
if (cachedProviders) return cachedProviders;
|
|
4323
|
+
const providers = [];
|
|
4324
|
+
providers.push(new ProcessSandbox());
|
|
4325
|
+
cachedProviders = providers;
|
|
4326
|
+
return providers;
|
|
4327
|
+
}
|
|
4328
|
+
async function getProvider(preference) {
|
|
4329
|
+
const providers = await detectProviders();
|
|
4330
|
+
if (preference && preference !== "auto") {
|
|
4331
|
+
const match = providers.find((p) => p.name === preference);
|
|
4332
|
+
if (match) return match;
|
|
4333
|
+
}
|
|
4334
|
+
return providers[0];
|
|
4335
|
+
}
|
|
4336
|
+
function deriveFailure(run) {
|
|
4337
|
+
if (run.status === "completed") return void 0;
|
|
4338
|
+
if (run.status === "cancelled") return void 0;
|
|
4339
|
+
const stderr = run.stderrTail ?? "";
|
|
4340
|
+
const exit = run.exitCode;
|
|
4341
|
+
if (exit === 137 || /\bMemoryError\b|OOM|Cannot allocate memory/i.test(stderr) || exit === 137 && /\bKilled\b$/m.test(stderr)) {
|
|
4342
|
+
return {
|
|
4343
|
+
code: "OOM_KILLED",
|
|
4344
|
+
retryable: true,
|
|
4345
|
+
message: "Process was killed due to insufficient memory.",
|
|
4346
|
+
suggestions: [
|
|
4347
|
+
"Reduce the dataset size or batch size.",
|
|
4348
|
+
"Close other memory-intensive applications.",
|
|
4349
|
+
"Process large data in chunks if possible."
|
|
4350
|
+
]
|
|
4351
|
+
};
|
|
4352
|
+
}
|
|
4353
|
+
if (run.status === "timed_out") {
|
|
4354
|
+
const timeoutMin = Math.round(run.timeoutMs / 6e4);
|
|
4355
|
+
return {
|
|
4356
|
+
code: "TIMEOUT",
|
|
4357
|
+
retryable: true,
|
|
4358
|
+
message: `Process exceeded the ${timeoutMin}-minute timeout.`,
|
|
4359
|
+
suggestions: [
|
|
4360
|
+
"Increase the timeout_minutes parameter.",
|
|
4361
|
+
"Optimize the code for faster execution.",
|
|
4362
|
+
"Process a smaller subset of the data."
|
|
4363
|
+
]
|
|
4364
|
+
};
|
|
4365
|
+
}
|
|
4366
|
+
if (run.status === "stalled") {
|
|
4367
|
+
return {
|
|
4368
|
+
code: "STALL",
|
|
4369
|
+
retryable: true,
|
|
4370
|
+
message: "Process stopped producing output and appears stuck.",
|
|
4371
|
+
suggestions: [
|
|
4372
|
+
"Check for deadlocks or blocking I/O in the code.",
|
|
4373
|
+
"Add progress logging to detect where execution hangs.",
|
|
4374
|
+
"Check if the process is waiting for interactive input."
|
|
4375
|
+
]
|
|
4376
|
+
};
|
|
4377
|
+
}
|
|
4378
|
+
if (/ModuleNotFoundError|No module named/i.test(stderr)) {
|
|
4379
|
+
const moduleMatch = stderr.match(/No module named '([^']+)'/i) ?? stderr.match(/ModuleNotFoundError:\s*No module named\s+'?([^\s']+)/i);
|
|
4380
|
+
const moduleName = moduleMatch?.[1] ?? "unknown";
|
|
4381
|
+
return {
|
|
4382
|
+
code: "MODULE_NOT_FOUND",
|
|
4383
|
+
retryable: true,
|
|
4384
|
+
message: `Python module not found: ${moduleName}`,
|
|
4385
|
+
suggestions: [
|
|
4386
|
+
`Add "${moduleName}" to requirements.txt and retry.`,
|
|
4387
|
+
`Or install directly: pip install ${moduleName}`
|
|
4388
|
+
]
|
|
4389
|
+
};
|
|
4390
|
+
}
|
|
4391
|
+
if (/PermissionError|EACCES|Permission denied/i.test(stderr)) {
|
|
4392
|
+
return {
|
|
4393
|
+
code: "PERMISSION_DENIED",
|
|
4394
|
+
retryable: false,
|
|
4395
|
+
message: "Permission denied during execution.",
|
|
4396
|
+
suggestions: [
|
|
4397
|
+
"Check file permissions on input/output paths.",
|
|
4398
|
+
"Ensure the output directory is writable."
|
|
4399
|
+
]
|
|
4400
|
+
};
|
|
4401
|
+
}
|
|
4402
|
+
if (/Traceback \(most recent call last\)/i.test(stderr) || /^\w+Error:/m.test(stderr) || /^\w+Exception:/m.test(stderr)) {
|
|
4403
|
+
const lines = stderr.split("\n").filter((l) => l.trim());
|
|
4404
|
+
const lastErrorLine = lines[lines.length - 1] ?? "";
|
|
4405
|
+
return {
|
|
4406
|
+
code: "PYTHON_ERROR",
|
|
4407
|
+
retryable: true,
|
|
4408
|
+
message: `Python error: ${lastErrorLine.slice(0, 200)}`,
|
|
4409
|
+
suggestions: [
|
|
4410
|
+
"Read the stderr output to understand the error.",
|
|
4411
|
+
"Fix the code and retry."
|
|
4412
|
+
]
|
|
4413
|
+
};
|
|
4414
|
+
}
|
|
4415
|
+
if (run.exitSignal) {
|
|
4416
|
+
return {
|
|
4417
|
+
code: "SIGNAL_KILLED",
|
|
4418
|
+
retryable: run.exitSignal === "SIGTERM",
|
|
4419
|
+
// SIGTERM is often retryable
|
|
4420
|
+
message: `Process was killed by signal: ${run.exitSignal}`,
|
|
4421
|
+
suggestions: [
|
|
4422
|
+
"Check if another process or the system killed this task.",
|
|
4423
|
+
"If the signal was SIGTERM, it may be safe to retry."
|
|
4424
|
+
]
|
|
4425
|
+
};
|
|
4426
|
+
}
|
|
4427
|
+
return {
|
|
4428
|
+
code: "COMMAND_FAILED",
|
|
4429
|
+
retryable: false,
|
|
4430
|
+
message: `Command exited with code ${exit ?? "unknown"}.`,
|
|
4431
|
+
suggestions: [
|
|
4432
|
+
"Review stderr output for details on the failure.",
|
|
4433
|
+
"Fix the script error and re-submit."
|
|
4434
|
+
]
|
|
4435
|
+
};
|
|
4436
|
+
}
|
|
4437
|
+
function extractProgress(tail) {
|
|
4438
|
+
const cooperative = extractCooperativeProgress(tail);
|
|
4439
|
+
if (cooperative) return cooperative;
|
|
4440
|
+
return extractRegexProgress(tail);
|
|
4441
|
+
}
|
|
4442
|
+
function extractCooperativeProgress(tail) {
|
|
4443
|
+
const lines = tail.split("\n");
|
|
4444
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
4445
|
+
const line = lines[i].trim();
|
|
4446
|
+
if (!line.startsWith("##PROGRESS##")) continue;
|
|
4447
|
+
const jsonStr = line.slice("##PROGRESS##".length).trim();
|
|
4448
|
+
try {
|
|
4449
|
+
const data = JSON.parse(jsonStr);
|
|
4450
|
+
const progress = {};
|
|
4451
|
+
if (typeof data.step === "number") progress.currentStep = data.step;
|
|
4452
|
+
if (typeof data.total === "number") progress.totalSteps = data.total;
|
|
4453
|
+
if (typeof data.percentage === "number") progress.percentage = data.percentage;
|
|
4454
|
+
if (typeof data.phase === "string") progress.phase = data.phase;
|
|
4455
|
+
if (typeof data.eta === "number") progress.etaSeconds = data.eta;
|
|
4456
|
+
if (typeof data.eta_seconds === "number") progress.etaSeconds = data.eta_seconds;
|
|
4457
|
+
const knownKeys = /* @__PURE__ */ new Set(["step", "total", "percentage", "phase", "eta", "eta_seconds"]);
|
|
4458
|
+
const metrics = {};
|
|
4459
|
+
for (const [k, v] of Object.entries(data)) {
|
|
4460
|
+
if (!knownKeys.has(k) && typeof v === "number") metrics[k] = v;
|
|
4461
|
+
}
|
|
4462
|
+
if (Object.keys(metrics).length > 0) progress.metrics = metrics;
|
|
4463
|
+
if (progress.percentage === void 0 && progress.currentStep !== void 0 && progress.totalSteps) {
|
|
4464
|
+
progress.percentage = Math.round(progress.currentStep / progress.totalSteps * 100);
|
|
4465
|
+
}
|
|
4466
|
+
return progress;
|
|
4467
|
+
} catch {
|
|
4468
|
+
}
|
|
4469
|
+
}
|
|
4470
|
+
return void 0;
|
|
4471
|
+
}
|
|
4472
|
+
function extractRegexProgress(tail) {
|
|
4473
|
+
const progress = {};
|
|
4474
|
+
let found = false;
|
|
4475
|
+
const normalizedTail = tail.replace(/\r/g, "\n");
|
|
4476
|
+
const tqdmMatches = [...normalizedTail.matchAll(/(\d+)%\|[^|]*\|\s*(\d+)\/(\d+)\s*\[[\d:]+<([\d:?]+)/g)];
|
|
4477
|
+
const tqdm = tqdmMatches.length > 0 ? tqdmMatches[tqdmMatches.length - 1] : null;
|
|
4478
|
+
if (tqdm) {
|
|
4479
|
+
progress.percentage = parseInt(tqdm[1], 10);
|
|
4480
|
+
progress.currentStep = parseInt(tqdm[2], 10);
|
|
4481
|
+
progress.totalSteps = parseInt(tqdm[3], 10);
|
|
4482
|
+
progress.etaSeconds = parseTimeStr(tqdm[4]);
|
|
4483
|
+
found = true;
|
|
4484
|
+
}
|
|
4485
|
+
if (!found) {
|
|
4486
|
+
const epoch = normalizedTail.match(/[Ee]pochs?\s+(\d+)\s*[/of]+\s*(\d+)/);
|
|
4487
|
+
if (epoch) {
|
|
4488
|
+
progress.currentStep = parseInt(epoch[1], 10);
|
|
4489
|
+
progress.totalSteps = parseInt(epoch[2], 10);
|
|
4490
|
+
progress.percentage = Math.round(progress.currentStep / progress.totalSteps * 100);
|
|
4491
|
+
progress.phase = "training";
|
|
4492
|
+
found = true;
|
|
4493
|
+
}
|
|
4494
|
+
}
|
|
4495
|
+
if (!found) {
|
|
4496
|
+
const step = normalizedTail.match(/[Ss]teps?\s+(\d+)\s*[/of]+\s*(\d+)/);
|
|
4497
|
+
if (step) {
|
|
4498
|
+
progress.currentStep = parseInt(step[1], 10);
|
|
4499
|
+
progress.totalSteps = parseInt(step[2], 10);
|
|
4500
|
+
progress.percentage = Math.round(progress.currentStep / progress.totalSteps * 100);
|
|
4501
|
+
found = true;
|
|
4502
|
+
}
|
|
4503
|
+
}
|
|
4504
|
+
if (!found) {
|
|
4505
|
+
const pctMatches = [...normalizedTail.matchAll(/(\d{1,3})%/g)];
|
|
4506
|
+
const pct = pctMatches.length > 0 ? pctMatches[pctMatches.length - 1] : null;
|
|
4507
|
+
if (pct) {
|
|
4508
|
+
const val = parseInt(pct[1], 10);
|
|
4509
|
+
if (val >= 0 && val <= 100) {
|
|
4510
|
+
progress.percentage = val;
|
|
4511
|
+
found = true;
|
|
4512
|
+
}
|
|
4513
|
+
}
|
|
4514
|
+
}
|
|
4515
|
+
const metrics = {};
|
|
4516
|
+
const metricPattern = /\b(loss|acc(?:uracy)?|f1|auc|mse|mae|rmse|r2|lr|learning_rate|val_loss|val_acc(?:uracy)?)\s*[=:]\s*([\d.]+(?:e[+-]?\d+)?)/gi;
|
|
4517
|
+
for (const m of normalizedTail.matchAll(metricPattern)) {
|
|
4518
|
+
const key = m[1].toLowerCase();
|
|
4519
|
+
const val = parseFloat(m[2]);
|
|
4520
|
+
if (!isNaN(val)) metrics[key] = val;
|
|
4521
|
+
}
|
|
4522
|
+
if (Object.keys(metrics).length > 0) {
|
|
4523
|
+
progress.metrics = metrics;
|
|
4524
|
+
found = true;
|
|
4525
|
+
}
|
|
4526
|
+
const lastLines = normalizedTail.slice(-2048).toLowerCase();
|
|
4527
|
+
if (/download|fetching|pulling/i.test(lastLines)) progress.phase = "downloading";
|
|
4528
|
+
else if (/train|fitting|epoch/i.test(lastLines)) progress.phase = "training";
|
|
4529
|
+
else if (/evaluat|validat|testing/i.test(lastLines)) progress.phase = "evaluating";
|
|
4530
|
+
else if (/preprocess|clean|transform/i.test(lastLines)) progress.phase = "preprocessing";
|
|
4531
|
+
else if (/saving|export|writing.*output/i.test(lastLines)) progress.phase = "saving";
|
|
4532
|
+
return found ? progress : void 0;
|
|
4533
|
+
}
|
|
4534
|
+
function parseTimeStr(s) {
|
|
4535
|
+
if (!s || s.includes("?")) return void 0;
|
|
4536
|
+
const parts = s.split(":").map((p) => parseInt(p, 10));
|
|
4537
|
+
if (parts.some(isNaN)) return void 0;
|
|
4538
|
+
if (parts.length === 2) return parts[0] * 60 + parts[1];
|
|
4539
|
+
if (parts.length === 3) return parts[0] * 3600 + parts[1] * 60 + parts[2];
|
|
4540
|
+
return void 0;
|
|
4541
|
+
}
|
|
4542
|
+
const MAX_HEAVY_CONCURRENT = 1;
|
|
4543
|
+
const MAX_TOTAL_CONCURRENT = 3;
|
|
4544
|
+
const MIN_FREE_MEMORY_MB = 500;
|
|
4545
|
+
const MIN_FREE_DISK_MB = 500;
|
|
4546
|
+
function classifyWeight(timeoutMinutes, command) {
|
|
4547
|
+
if (timeoutMinutes <= 2) return "light";
|
|
4548
|
+
if (timeoutMinutes <= 10 && /\b(plot|viz|chart|figure|draw|render)\b/i.test(command)) return "light";
|
|
4549
|
+
return "heavy";
|
|
4550
|
+
}
|
|
4551
|
+
function canAdmit(snapshot, weight) {
|
|
4552
|
+
const activeHeavy = snapshot.activeRuns.filter((r) => r.weight === "heavy").length;
|
|
4553
|
+
const totalActive = snapshot.activeRuns.length;
|
|
4554
|
+
if (weight === "heavy" && activeHeavy >= MAX_HEAVY_CONCURRENT) {
|
|
4555
|
+
const heavyRun = snapshot.activeRuns.find((r) => r.weight === "heavy");
|
|
4556
|
+
return {
|
|
4557
|
+
allowed: false,
|
|
4558
|
+
reason: `A heavy compute run is already active${heavyRun ? ` (${heavyRun.runId})` : ""}. Wait for it to finish, or stop it first.`
|
|
4559
|
+
};
|
|
4560
|
+
}
|
|
4561
|
+
if (totalActive >= MAX_TOTAL_CONCURRENT) {
|
|
4562
|
+
return {
|
|
4563
|
+
allowed: false,
|
|
4564
|
+
reason: `Too many concurrent runs (${totalActive}/${MAX_TOTAL_CONCURRENT}). Wait for one to finish, or stop one.`
|
|
4565
|
+
};
|
|
4566
|
+
}
|
|
4567
|
+
if (snapshot.freeMemoryMb < MIN_FREE_MEMORY_MB) {
|
|
4568
|
+
return {
|
|
4569
|
+
allowed: false,
|
|
4570
|
+
reason: `Low memory (${Math.round(snapshot.freeMemoryMb)}MB free, need ${MIN_FREE_MEMORY_MB}MB). Close applications to free resources.`
|
|
4571
|
+
};
|
|
4572
|
+
}
|
|
4573
|
+
if (snapshot.freeDiskMb < MIN_FREE_DISK_MB) {
|
|
4574
|
+
return {
|
|
4575
|
+
allowed: false,
|
|
4576
|
+
reason: `Low disk space (${Math.round(snapshot.freeDiskMb)}MB free, need ${MIN_FREE_DISK_MB}MB).`
|
|
4577
|
+
};
|
|
4578
|
+
}
|
|
4579
|
+
return { allowed: true, reason: "Resources available" };
|
|
4580
|
+
}
|
|
4581
|
+
const execFileAsync$1 = promisify(execFile);
|
|
4582
|
+
async function checkSyntax(scriptPath, pythonPath) {
|
|
4583
|
+
const start = Date.now();
|
|
4584
|
+
if (!scriptPath.endsWith(".py")) {
|
|
4585
|
+
return { name: "syntax", status: "passed", message: "Not a Python script — skipped.", durationMs: Date.now() - start };
|
|
4586
|
+
}
|
|
4587
|
+
if (!fs.existsSync(scriptPath)) {
|
|
4588
|
+
return { name: "syntax", status: "failed", message: `Script not found: ${scriptPath}`, durationMs: Date.now() - start };
|
|
4589
|
+
}
|
|
4590
|
+
try {
|
|
4591
|
+
await execFileAsync$1(pythonPath, ["-m", "py_compile", scriptPath], { timeout: 1e4 });
|
|
4592
|
+
return { name: "syntax", status: "passed", message: "Python syntax OK.", durationMs: Date.now() - start };
|
|
4593
|
+
} catch (err) {
|
|
4594
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
4595
|
+
const syntaxLine = msg.split("\n").find((l) => /SyntaxError|IndentationError|TabError/i.test(l)) ?? msg.slice(0, 200);
|
|
4596
|
+
return { name: "syntax", status: "failed", message: `Syntax error: ${syntaxLine}`, durationMs: Date.now() - start };
|
|
4597
|
+
}
|
|
4598
|
+
}
|
|
4599
|
+
async function checkImports(scriptPath, pythonPath) {
|
|
4600
|
+
const start = Date.now();
|
|
4601
|
+
if (!scriptPath.endsWith(".py") || !fs.existsSync(scriptPath)) {
|
|
4602
|
+
return { name: "imports", status: "passed", message: "Skipped.", durationMs: Date.now() - start };
|
|
4603
|
+
}
|
|
4604
|
+
const content = fs.readFileSync(scriptPath, "utf-8");
|
|
4605
|
+
const imports = /* @__PURE__ */ new Set();
|
|
4606
|
+
for (const match of content.matchAll(/^\s*(?:import|from)\s+([a-zA-Z_][a-zA-Z0-9_]*)/gm)) {
|
|
4607
|
+
const mod = match[1];
|
|
4608
|
+
if (STDLIB_MODULES.has(mod)) continue;
|
|
4609
|
+
imports.add(mod);
|
|
4610
|
+
}
|
|
4611
|
+
if (imports.size === 0) {
|
|
4612
|
+
return { name: "imports", status: "passed", message: "No third-party imports detected.", durationMs: Date.now() - start };
|
|
4613
|
+
}
|
|
4614
|
+
const importList = Array.from(imports);
|
|
4615
|
+
const testScript = importList.map((mod) => `try:
|
|
4616
|
+
import ${mod}
|
|
4617
|
+
except ImportError:
|
|
4618
|
+
print("MISSING:" + "${mod}")`).join("\n");
|
|
4619
|
+
const missing = [];
|
|
4620
|
+
try {
|
|
4621
|
+
const { stdout } = await execFileAsync$1(pythonPath, ["-c", testScript], { timeout: 3e4 });
|
|
4622
|
+
for (const line of stdout.split("\n")) {
|
|
4623
|
+
if (line.startsWith("MISSING:")) missing.push(line.slice(8));
|
|
4624
|
+
}
|
|
4625
|
+
} catch {
|
|
4626
|
+
return { name: "imports", status: "warning", message: `Could not verify imports: ${importList.join(", ")}`, durationMs: Date.now() - start };
|
|
4627
|
+
}
|
|
4628
|
+
if (missing.length === 0) {
|
|
4629
|
+
return { name: "imports", status: "passed", message: `All ${imports.size} imports available.`, durationMs: Date.now() - start };
|
|
4630
|
+
}
|
|
4631
|
+
return {
|
|
4632
|
+
name: "imports",
|
|
4633
|
+
status: "failed",
|
|
4634
|
+
message: `Missing modules: ${missing.join(", ")}. Add to requirements.txt or pip install.`,
|
|
4635
|
+
durationMs: Date.now() - start
|
|
4636
|
+
};
|
|
4637
|
+
}
|
|
4638
|
+
function checkDataPaths(command, workDir) {
|
|
4639
|
+
const start = Date.now();
|
|
4640
|
+
const pathPattern = /(?:["']([^"']+\.(?:csv|tsv|json|jsonl|xlsx|xls|parquet|feather|h5|hdf5|npy|npz|pkl|pickle|txt|dat))["']|(\S+\.(?:csv|tsv|json|jsonl|xlsx|xls|parquet|feather|h5|hdf5|npy|npz|pkl|pickle)))/gi;
|
|
4641
|
+
const refs = [];
|
|
4642
|
+
for (const m of command.matchAll(pathPattern)) {
|
|
4643
|
+
refs.push(m[1] ?? m[2]);
|
|
4644
|
+
}
|
|
4645
|
+
if (refs.length === 0) {
|
|
4646
|
+
return { name: "data_paths", status: "passed", message: "No data file references detected in command.", durationMs: Date.now() - start };
|
|
4647
|
+
}
|
|
4648
|
+
const missing = [];
|
|
4649
|
+
for (const ref of refs) {
|
|
4650
|
+
const abs = path$1.isAbsolute(ref) ? ref : path$1.resolve(workDir, ref);
|
|
4651
|
+
if (!fs.existsSync(abs)) missing.push(ref);
|
|
4652
|
+
}
|
|
4653
|
+
if (missing.length === 0) {
|
|
4654
|
+
return { name: "data_paths", status: "passed", message: `All ${refs.length} data files found.`, durationMs: Date.now() - start };
|
|
4655
|
+
}
|
|
4656
|
+
return {
|
|
4657
|
+
name: "data_paths",
|
|
4658
|
+
status: "failed",
|
|
4659
|
+
message: `Data files not found: ${missing.join(", ")}`,
|
|
4660
|
+
durationMs: Date.now() - start
|
|
4661
|
+
};
|
|
4662
|
+
}
|
|
4663
|
+
function checkDiskSpace(workDir) {
|
|
4664
|
+
const start = Date.now();
|
|
4665
|
+
try {
|
|
4666
|
+
const output = execSync("df -m .", { cwd: workDir, encoding: "utf-8", timeout: 3e3 });
|
|
4667
|
+
const lines = output.trim().split("\n");
|
|
4668
|
+
if (lines.length >= 2) {
|
|
4669
|
+
const parts = lines[1].split(/\s+/);
|
|
4670
|
+
const availMb = parseInt(parts[3], 10);
|
|
4671
|
+
if (!isNaN(availMb)) {
|
|
4672
|
+
if (availMb < 500) {
|
|
4673
|
+
return { name: "disk_space", status: "failed", message: `Only ${availMb}MB free disk space. Need at least 500MB.`, durationMs: Date.now() - start };
|
|
4674
|
+
}
|
|
4675
|
+
if (availMb < 2e3) {
|
|
4676
|
+
return { name: "disk_space", status: "warning", message: `${availMb}MB free disk space. May be tight for large outputs.`, durationMs: Date.now() - start };
|
|
4677
|
+
}
|
|
4678
|
+
return { name: "disk_space", status: "passed", message: `${availMb}MB free disk space.`, durationMs: Date.now() - start };
|
|
4679
|
+
}
|
|
4680
|
+
}
|
|
4681
|
+
} catch {
|
|
4682
|
+
}
|
|
4683
|
+
return { name: "disk_space", status: "passed", message: "Could not detect disk space — proceeding.", durationMs: Date.now() - start };
|
|
4684
|
+
}
|
|
4685
|
+
function checkOutputDir(workDir) {
|
|
4686
|
+
const start = Date.now();
|
|
4687
|
+
try {
|
|
4688
|
+
const testFile = path$1.join(workDir, ".compute-preflight-test");
|
|
4689
|
+
fs.writeFileSync(testFile, "test");
|
|
4690
|
+
fs.unlinkSync(testFile);
|
|
4691
|
+
return { name: "output_dir", status: "passed", message: "Working directory is writable.", durationMs: Date.now() - start };
|
|
4692
|
+
} catch {
|
|
4693
|
+
return { name: "output_dir", status: "failed", message: `Working directory is not writable: ${workDir}`, durationMs: Date.now() - start };
|
|
4694
|
+
}
|
|
4695
|
+
}
|
|
4696
|
+
async function runPreflight(opts) {
|
|
4697
|
+
const pythonPath = opts.pythonPath ?? "python3";
|
|
4698
|
+
const scriptPath = opts.scriptPath ?? extractScriptPath(opts.command, opts.workDir);
|
|
4699
|
+
const [syntax, imports, dataPaths, diskSpace, outputDir] = await Promise.all([
|
|
4700
|
+
scriptPath ? checkSyntax(scriptPath, pythonPath) : Promise.resolve(null),
|
|
4701
|
+
scriptPath ? checkImports(scriptPath, pythonPath) : Promise.resolve(null),
|
|
4702
|
+
Promise.resolve(checkDataPaths(opts.command, opts.workDir)),
|
|
4703
|
+
Promise.resolve(checkDiskSpace(opts.workDir)),
|
|
4704
|
+
Promise.resolve(checkOutputDir(opts.workDir))
|
|
4705
|
+
]);
|
|
4706
|
+
const checks = [syntax, imports, dataPaths, diskSpace, outputDir].filter(Boolean);
|
|
4707
|
+
const blockingIssues = checks.filter((c) => c.status === "failed").map((c) => c.message);
|
|
4708
|
+
const warnings = checks.filter((c) => c.status === "warning").map((c) => c.message);
|
|
4709
|
+
return {
|
|
4710
|
+
passed: blockingIssues.length === 0,
|
|
4711
|
+
checks,
|
|
4712
|
+
blockingIssues,
|
|
4713
|
+
warnings
|
|
4714
|
+
};
|
|
4715
|
+
}
|
|
4716
|
+
function extractScriptPath(command, workDir) {
|
|
4717
|
+
const match = command.match(/python3?\s+([^\s]+\.py)/);
|
|
4718
|
+
if (!match) return void 0;
|
|
4719
|
+
const scriptRef = match[1];
|
|
4720
|
+
return path$1.isAbsolute(scriptRef) ? scriptRef : path$1.resolve(workDir, scriptRef);
|
|
4721
|
+
}
|
|
4722
|
+
const STDLIB_MODULES = /* @__PURE__ */ new Set([
|
|
4723
|
+
"abc",
|
|
4724
|
+
"aifc",
|
|
4725
|
+
"argparse",
|
|
4726
|
+
"array",
|
|
4727
|
+
"ast",
|
|
4728
|
+
"asynchat",
|
|
4729
|
+
"asyncio",
|
|
4730
|
+
"asyncore",
|
|
4731
|
+
"atexit",
|
|
4732
|
+
"base64",
|
|
4733
|
+
"bdb",
|
|
4734
|
+
"binascii",
|
|
4735
|
+
"binhex",
|
|
4736
|
+
"bisect",
|
|
4737
|
+
"builtins",
|
|
4738
|
+
"bz2",
|
|
4739
|
+
"calendar",
|
|
4740
|
+
"cgi",
|
|
4741
|
+
"cgitb",
|
|
4742
|
+
"chunk",
|
|
4743
|
+
"cmath",
|
|
4744
|
+
"cmd",
|
|
4745
|
+
"code",
|
|
4746
|
+
"codecs",
|
|
4747
|
+
"codeop",
|
|
4748
|
+
"collections",
|
|
4749
|
+
"colorsys",
|
|
4750
|
+
"compileall",
|
|
4751
|
+
"concurrent",
|
|
4752
|
+
"configparser",
|
|
4753
|
+
"contextlib",
|
|
4754
|
+
"contextvars",
|
|
4755
|
+
"copy",
|
|
4756
|
+
"copyreg",
|
|
4757
|
+
"cProfile",
|
|
4758
|
+
"crypt",
|
|
4759
|
+
"csv",
|
|
4760
|
+
"ctypes",
|
|
4761
|
+
"curses",
|
|
4762
|
+
"dataclasses",
|
|
4763
|
+
"datetime",
|
|
4764
|
+
"dbm",
|
|
4765
|
+
"decimal",
|
|
4766
|
+
"difflib",
|
|
4767
|
+
"dis",
|
|
4768
|
+
"distutils",
|
|
4769
|
+
"doctest",
|
|
4770
|
+
"email",
|
|
4771
|
+
"encodings",
|
|
4772
|
+
"enum",
|
|
4773
|
+
"errno",
|
|
4774
|
+
"faulthandler",
|
|
4775
|
+
"fcntl",
|
|
4776
|
+
"filecmp",
|
|
4777
|
+
"fileinput",
|
|
4778
|
+
"fnmatch",
|
|
4779
|
+
"fractions",
|
|
4780
|
+
"ftplib",
|
|
4781
|
+
"functools",
|
|
4782
|
+
"gc",
|
|
4783
|
+
"getopt",
|
|
4784
|
+
"getpass",
|
|
4785
|
+
"gettext",
|
|
4786
|
+
"glob",
|
|
4787
|
+
"grp",
|
|
4788
|
+
"gzip",
|
|
4789
|
+
"hashlib",
|
|
4790
|
+
"heapq",
|
|
4791
|
+
"hmac",
|
|
4792
|
+
"html",
|
|
4793
|
+
"http",
|
|
4794
|
+
"idlelib",
|
|
4795
|
+
"imaplib",
|
|
4796
|
+
"imghdr",
|
|
4797
|
+
"imp",
|
|
4798
|
+
"importlib",
|
|
4799
|
+
"inspect",
|
|
4800
|
+
"io",
|
|
4801
|
+
"ipaddress",
|
|
4802
|
+
"itertools",
|
|
4803
|
+
"json",
|
|
4804
|
+
"keyword",
|
|
4805
|
+
"lib2to3",
|
|
4806
|
+
"linecache",
|
|
4807
|
+
"locale",
|
|
4808
|
+
"logging",
|
|
4809
|
+
"lzma",
|
|
4810
|
+
"mailbox",
|
|
4811
|
+
"mailcap",
|
|
4812
|
+
"marshal",
|
|
4813
|
+
"math",
|
|
4814
|
+
"mimetypes",
|
|
4815
|
+
"mmap",
|
|
4816
|
+
"modulefinder",
|
|
4817
|
+
"multiprocessing",
|
|
4818
|
+
"netrc",
|
|
4819
|
+
"nis",
|
|
4820
|
+
"nntplib",
|
|
4821
|
+
"numbers",
|
|
4822
|
+
"operator",
|
|
4823
|
+
"optparse",
|
|
4824
|
+
"os",
|
|
4825
|
+
"ossaudiodev",
|
|
4826
|
+
"pathlib",
|
|
4827
|
+
"pdb",
|
|
4828
|
+
"pickle",
|
|
4829
|
+
"pickletools",
|
|
4830
|
+
"pipes",
|
|
4831
|
+
"pkgutil",
|
|
4832
|
+
"platform",
|
|
4833
|
+
"plistlib",
|
|
4834
|
+
"poplib",
|
|
4835
|
+
"posix",
|
|
4836
|
+
"posixpath",
|
|
4837
|
+
"pprint",
|
|
4838
|
+
"profile",
|
|
4839
|
+
"pstats",
|
|
4840
|
+
"pty",
|
|
4841
|
+
"pwd",
|
|
4842
|
+
"py_compile",
|
|
4843
|
+
"pyclbr",
|
|
4844
|
+
"pydoc",
|
|
4845
|
+
"queue",
|
|
4846
|
+
"quopri",
|
|
4847
|
+
"random",
|
|
4848
|
+
"re",
|
|
4849
|
+
"readline",
|
|
4850
|
+
"reprlib",
|
|
4851
|
+
"resource",
|
|
4852
|
+
"rlcompleter",
|
|
4853
|
+
"runpy",
|
|
4854
|
+
"sched",
|
|
4855
|
+
"secrets",
|
|
4856
|
+
"select",
|
|
4857
|
+
"selectors",
|
|
4858
|
+
"shelve",
|
|
4859
|
+
"shlex",
|
|
4860
|
+
"shutil",
|
|
4861
|
+
"signal",
|
|
4862
|
+
"site",
|
|
4863
|
+
"smtpd",
|
|
4864
|
+
"smtplib",
|
|
4865
|
+
"sndhdr",
|
|
4866
|
+
"socket",
|
|
4867
|
+
"socketserver",
|
|
4868
|
+
"sqlite3",
|
|
4869
|
+
"sre_compile",
|
|
4870
|
+
"sre_constants",
|
|
4871
|
+
"sre_parse",
|
|
4872
|
+
"ssl",
|
|
4873
|
+
"stat",
|
|
4874
|
+
"statistics",
|
|
4875
|
+
"string",
|
|
4876
|
+
"stringprep",
|
|
4877
|
+
"struct",
|
|
4878
|
+
"subprocess",
|
|
4879
|
+
"sunau",
|
|
4880
|
+
"symtable",
|
|
4881
|
+
"sys",
|
|
4882
|
+
"sysconfig",
|
|
4883
|
+
"syslog",
|
|
4884
|
+
"tabnanny",
|
|
4885
|
+
"tarfile",
|
|
4886
|
+
"telnetlib",
|
|
4887
|
+
"tempfile",
|
|
4888
|
+
"termios",
|
|
4889
|
+
"test",
|
|
4890
|
+
"textwrap",
|
|
4891
|
+
"threading",
|
|
4892
|
+
"time",
|
|
4893
|
+
"timeit",
|
|
4894
|
+
"tkinter",
|
|
4895
|
+
"token",
|
|
4896
|
+
"tokenize",
|
|
4897
|
+
"tomllib",
|
|
4898
|
+
"trace",
|
|
4899
|
+
"traceback",
|
|
4900
|
+
"tracemalloc",
|
|
4901
|
+
"tty",
|
|
4902
|
+
"turtle",
|
|
4903
|
+
"turtledemo",
|
|
4904
|
+
"types",
|
|
4905
|
+
"typing",
|
|
4906
|
+
"unicodedata",
|
|
4907
|
+
"unittest",
|
|
4908
|
+
"urllib",
|
|
4909
|
+
"uu",
|
|
4910
|
+
"uuid",
|
|
4911
|
+
"venv",
|
|
4912
|
+
"warnings",
|
|
4913
|
+
"wave",
|
|
4914
|
+
"weakref",
|
|
4915
|
+
"webbrowser",
|
|
4916
|
+
"winreg",
|
|
4917
|
+
"winsound",
|
|
4918
|
+
"wsgiref",
|
|
4919
|
+
"xdrlib",
|
|
4920
|
+
"xml",
|
|
4921
|
+
"xmlrpc",
|
|
4922
|
+
"zipapp",
|
|
4923
|
+
"zipfile",
|
|
4924
|
+
"zipimport",
|
|
4925
|
+
"zlib",
|
|
4926
|
+
// Common aliases / submodules
|
|
4927
|
+
"os",
|
|
4928
|
+
"sys",
|
|
4929
|
+
"typing",
|
|
4930
|
+
"collections",
|
|
4931
|
+
"__future__"
|
|
4932
|
+
]);
|
|
4933
|
+
const EXPERIENCE_FILE = "experience.jsonl";
|
|
4934
|
+
class ExperienceStore {
|
|
4935
|
+
constructor(projectPath) {
|
|
4936
|
+
this.records = null;
|
|
4937
|
+
const dir = path$1.join(projectPath, ".research-pilot", "compute-runs");
|
|
4938
|
+
this.filePath = path$1.join(dir, EXPERIENCE_FILE);
|
|
4939
|
+
}
|
|
4940
|
+
load() {
|
|
4941
|
+
if (this.records !== null) return this.records;
|
|
4942
|
+
this.records = [];
|
|
4943
|
+
if (!fs.existsSync(this.filePath)) return this.records;
|
|
4944
|
+
const content = fs.readFileSync(this.filePath, "utf-8");
|
|
4945
|
+
for (const line of content.split("\n")) {
|
|
4946
|
+
const trimmed = line.trim();
|
|
4947
|
+
if (!trimmed) continue;
|
|
4948
|
+
try {
|
|
4949
|
+
this.records.push(JSON.parse(trimmed));
|
|
4950
|
+
} catch {
|
|
4951
|
+
}
|
|
4952
|
+
}
|
|
4953
|
+
return this.records;
|
|
4954
|
+
}
|
|
4955
|
+
flush() {
|
|
4956
|
+
const dir = path$1.dirname(this.filePath);
|
|
4957
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
4958
|
+
const records = this.load();
|
|
4959
|
+
const trimmed = records.slice(-200);
|
|
4960
|
+
const content = trimmed.map((r) => JSON.stringify(r)).join("\n") + "\n";
|
|
4961
|
+
const tmpPath = this.filePath + ".tmp." + process.pid;
|
|
4962
|
+
fs.writeFileSync(tmpPath, content, "utf-8");
|
|
4963
|
+
fs.renameSync(tmpPath, this.filePath);
|
|
4964
|
+
this.records = trimmed;
|
|
4965
|
+
}
|
|
4966
|
+
/**
|
|
4967
|
+
* Record a completed run's experience.
|
|
4968
|
+
*/
|
|
4969
|
+
record(entry) {
|
|
4970
|
+
this.load();
|
|
4971
|
+
this.records.push(entry);
|
|
4972
|
+
this.flush();
|
|
4973
|
+
}
|
|
4974
|
+
/**
|
|
4975
|
+
* Find relevant past experience by taskKind.
|
|
4976
|
+
* Primary: exact match. Fallback: most recent regardless of kind.
|
|
4977
|
+
*/
|
|
4978
|
+
findRelevant(taskKind, limit = 10) {
|
|
4979
|
+
const all = this.load();
|
|
4980
|
+
const exact = all.filter((r) => r.taskKind === taskKind);
|
|
4981
|
+
if (exact.length > 0) return exact.slice(-limit);
|
|
4982
|
+
return all.slice(-limit);
|
|
4983
|
+
}
|
|
4984
|
+
/**
|
|
4985
|
+
* Get all records (for UI display or export).
|
|
4986
|
+
*/
|
|
4987
|
+
getAll() {
|
|
4988
|
+
return [...this.load()];
|
|
4989
|
+
}
|
|
4990
|
+
/**
|
|
4991
|
+
* Compute summary statistics for a taskKind.
|
|
4992
|
+
*/
|
|
4993
|
+
summarize(taskKind) {
|
|
4994
|
+
const records = this.findRelevant(taskKind);
|
|
4995
|
+
if (records.length === 0) return void 0;
|
|
4996
|
+
const successes = records.filter((r) => r.outcome === "success");
|
|
4997
|
+
const failures = records.filter((r) => r.outcome === "failed");
|
|
4998
|
+
const durations = successes.map((r) => r.durationSeconds);
|
|
4999
|
+
const avgDuration = durations.length > 0 ? Math.round(durations.reduce((a, b) => a + b, 0) / durations.length) : void 0;
|
|
5000
|
+
const failureCodes = {};
|
|
5001
|
+
for (const f of failures) {
|
|
5002
|
+
if (f.failureCode) {
|
|
5003
|
+
failureCodes[f.failureCode] = (failureCodes[f.failureCode] ?? 0) + 1;
|
|
5004
|
+
}
|
|
5005
|
+
}
|
|
5006
|
+
return {
|
|
5007
|
+
taskKind,
|
|
5008
|
+
totalRuns: records.length,
|
|
5009
|
+
successes: successes.length,
|
|
5010
|
+
failures: failures.length,
|
|
5011
|
+
avgDurationSeconds: avgDuration,
|
|
5012
|
+
commonFailures: failureCodes,
|
|
5013
|
+
lastRun: records[records.length - 1]
|
|
5014
|
+
};
|
|
5015
|
+
}
|
|
5016
|
+
}
|
|
5017
|
+
function inferTaskKind(command, scriptContent) {
|
|
5018
|
+
const combined = (command + " " + (scriptContent ?? "")).toLowerCase();
|
|
5019
|
+
let framework = "python";
|
|
5020
|
+
if (/\bimport\s+(?:torch|pytorch)\b|from\s+torch\b/.test(combined)) framework = "pytorch";
|
|
5021
|
+
else if (/\bimport\s+tensorflow\b|from\s+tensorflow\b|import\s+keras\b/.test(combined)) framework = "tensorflow";
|
|
5022
|
+
else if (/\bimport\s+mlx\b|from\s+mlx\b/.test(combined)) framework = "mlx";
|
|
5023
|
+
else if (/\bimport\s+sklearn\b|from\s+sklearn\b/.test(combined)) framework = "sklearn";
|
|
5024
|
+
else if (/\bimport\s+pandas\b|from\s+pandas\b/.test(combined)) framework = "pandas";
|
|
5025
|
+
else if (/\bimport\s+(?:matplotlib|seaborn|plotly)\b/.test(combined)) framework = "matplotlib";
|
|
5026
|
+
else if (/\bimport\s+(?:numpy|scipy)\b/.test(combined)) framework = "numpy";
|
|
5027
|
+
let action = "script";
|
|
5028
|
+
if (/\.fit\(|\.train\(|train|epoch/i.test(combined)) action = "training";
|
|
5029
|
+
else if (/\.predict\(|\.score\(|evaluat|test.*accuracy/i.test(combined)) action = "evaluation";
|
|
5030
|
+
else if (/preprocess|clean|transform|etl|\.to_csv\(|\.to_parquet\(/i.test(combined)) action = "etl";
|
|
5031
|
+
else if (/\.savefig\(|\.show\(|plot|chart|graph|viz/i.test(combined)) action = "viz";
|
|
5032
|
+
else if (/download|fetch|scrape|request/i.test(combined)) action = "download";
|
|
5033
|
+
else if (/statist|correlat|describe|summary/i.test(combined)) action = "analysis";
|
|
5034
|
+
return `${framework}-${action}`;
|
|
5035
|
+
}
|
|
5036
|
+
const POLL_INTERVAL_MS = 5e3;
|
|
5037
|
+
const OUTPUT_TAIL_BYTES = 8192;
|
|
5038
|
+
const STDERR_TAIL_BYTES = 4096;
|
|
5039
|
+
const MAX_TIMEOUT_MS = 24 * 60 * 6e4;
|
|
5040
|
+
const MAX_OUTPUT_BYTES = 1024 * 1024 * 1024;
|
|
5041
|
+
const DESTROY_KILL_TIMEOUT_MS = 5e3;
|
|
5042
|
+
function nextRunId() {
|
|
5043
|
+
return "cr-" + crypto$1.randomBytes(4).toString("hex");
|
|
5044
|
+
}
|
|
5045
|
+
function getPidStartTime(pid) {
|
|
5046
|
+
try {
|
|
5047
|
+
if (process.platform === "darwin") {
|
|
5048
|
+
const raw = execSync(`ps -p ${pid} -o lstart=`, { encoding: "utf-8", timeout: 3e3 }).trim();
|
|
5049
|
+
if (!raw) return void 0;
|
|
5050
|
+
return new Date(raw).getTime();
|
|
5051
|
+
} else {
|
|
5052
|
+
const stat = fs.readFileSync(`/proc/${pid}/stat`, "utf-8");
|
|
5053
|
+
const fields = stat.split(" ");
|
|
5054
|
+
const startTicks = parseInt(fields[21], 10);
|
|
5055
|
+
if (isNaN(startTicks)) return void 0;
|
|
5056
|
+
const uptime = parseFloat(fs.readFileSync("/proc/uptime", "utf-8").split(" ")[0]);
|
|
5057
|
+
const hz = 100;
|
|
5058
|
+
const bootTime = Date.now() - uptime * 1e3;
|
|
5059
|
+
return Math.round(bootTime + startTicks / hz * 1e3);
|
|
5060
|
+
}
|
|
5061
|
+
} catch {
|
|
5062
|
+
return void 0;
|
|
5063
|
+
}
|
|
5064
|
+
}
|
|
5065
|
+
function isPidAlive(pid) {
|
|
5066
|
+
try {
|
|
5067
|
+
process.kill(pid, 0);
|
|
5068
|
+
return true;
|
|
5069
|
+
} catch {
|
|
5070
|
+
return false;
|
|
5071
|
+
}
|
|
5072
|
+
}
|
|
5073
|
+
function isStaleRun(record) {
|
|
5074
|
+
if (!record.pid) return true;
|
|
5075
|
+
if (!isPidAlive(record.pid)) return true;
|
|
5076
|
+
if (record.pidStartTime) {
|
|
5077
|
+
const currentStartTime = getPidStartTime(record.pid);
|
|
5078
|
+
if (currentStartTime === void 0) return true;
|
|
5079
|
+
if (Math.abs(currentStartTime - record.pidStartTime) > 2e3) return true;
|
|
5080
|
+
}
|
|
5081
|
+
return false;
|
|
5082
|
+
}
|
|
5083
|
+
function takeSnapshot(store) {
|
|
5084
|
+
const activeRuns = store.getActiveRuns().map((r) => ({
|
|
5085
|
+
runId: r.runId,
|
|
5086
|
+
weight: r.weight
|
|
5087
|
+
}));
|
|
5088
|
+
let freeDiskMb = 1e4;
|
|
5089
|
+
try {
|
|
5090
|
+
const dfOutput = execSync("df -m .", { encoding: "utf-8", timeout: 3e3 });
|
|
5091
|
+
const lines = dfOutput.trim().split("\n");
|
|
5092
|
+
if (lines.length >= 2) {
|
|
5093
|
+
const parts = lines[1].split(/\s+/);
|
|
5094
|
+
const available = parseInt(parts[3], 10);
|
|
5095
|
+
if (!isNaN(available)) freeDiskMb = available;
|
|
5096
|
+
}
|
|
5097
|
+
} catch {
|
|
5098
|
+
}
|
|
5099
|
+
return {
|
|
5100
|
+
freeMemoryMb: Math.round(os.freemem() / (1024 * 1024)),
|
|
5101
|
+
cpuLoadPercent: Math.round(os.loadavg()[0] / os.cpus().length * 100),
|
|
5102
|
+
freeDiskMb,
|
|
5103
|
+
activeRuns
|
|
5104
|
+
};
|
|
5105
|
+
}
|
|
5106
|
+
function readFileTail(filePath, maxBytes) {
|
|
5107
|
+
try {
|
|
5108
|
+
const stat = fs.statSync(filePath);
|
|
5109
|
+
if (stat.size === 0) return "";
|
|
5110
|
+
const start = Math.max(0, stat.size - maxBytes);
|
|
5111
|
+
const fd = fs.openSync(filePath, "r");
|
|
5112
|
+
const buf = Buffer.alloc(Math.min(stat.size, maxBytes));
|
|
5113
|
+
fs.readSync(fd, buf, 0, buf.length, start);
|
|
5114
|
+
fs.closeSync(fd);
|
|
5115
|
+
return buf.toString("utf-8");
|
|
5116
|
+
} catch {
|
|
5117
|
+
return "";
|
|
5118
|
+
}
|
|
5119
|
+
}
|
|
5120
|
+
function getFileSize(filePath) {
|
|
5121
|
+
try {
|
|
5122
|
+
return fs.statSync(filePath).size;
|
|
5123
|
+
} catch {
|
|
5124
|
+
return 0;
|
|
5125
|
+
}
|
|
5126
|
+
}
|
|
5127
|
+
function estimateLines(bytes, tail) {
|
|
5128
|
+
if (bytes === 0 || tail.length === 0) return 0;
|
|
5129
|
+
const tailLines = tail.split("\n").length;
|
|
5130
|
+
if (tail.length >= bytes) return tailLines;
|
|
5131
|
+
return Math.max(tailLines, Math.round(bytes / tail.length * tailLines));
|
|
5132
|
+
}
|
|
5133
|
+
class ComputeRunner {
|
|
5134
|
+
constructor(opts) {
|
|
5135
|
+
this.handles = /* @__PURE__ */ new Map();
|
|
5136
|
+
this.pollTimer = null;
|
|
5137
|
+
this.pollInFlight = /* @__PURE__ */ new Set();
|
|
5138
|
+
this.store = new RunStore(opts.projectPath);
|
|
5139
|
+
this.experience = new ExperienceStore(opts.projectPath);
|
|
5140
|
+
this.workspacePath = opts.workspacePath;
|
|
5141
|
+
this.projectPath = opts.projectPath;
|
|
5142
|
+
this.reconcileStaleRuns();
|
|
5143
|
+
const active = this.store.getActiveRuns();
|
|
5144
|
+
if (active.length > 0) this.ensurePolling();
|
|
5145
|
+
this.store.evictOld();
|
|
5146
|
+
}
|
|
5147
|
+
/**
|
|
5148
|
+
* On startup, detect and transition stale 'running'/'stalled' records whose
|
|
5149
|
+
* processes are gone (crashed app, killed process, PID reused).
|
|
5150
|
+
*/
|
|
5151
|
+
reconcileStaleRuns() {
|
|
5152
|
+
const active = this.store.getActiveRuns();
|
|
5153
|
+
for (const run of active) {
|
|
5154
|
+
if (isStaleRun(run)) {
|
|
5155
|
+
this.store.updateRun(run.runId, {
|
|
5156
|
+
status: "failed",
|
|
5157
|
+
completedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5158
|
+
error: "Process no longer running (app crashed or process was killed).",
|
|
5159
|
+
stalled: false
|
|
5160
|
+
});
|
|
5161
|
+
}
|
|
5162
|
+
}
|
|
5163
|
+
}
|
|
5164
|
+
// -------------------------------------------------------------------------
|
|
5165
|
+
// Submit
|
|
5166
|
+
// -------------------------------------------------------------------------
|
|
5167
|
+
async submit(config) {
|
|
5168
|
+
const timeoutMs = Math.min(
|
|
5169
|
+
(config.timeoutMinutes ?? 60) * 6e4,
|
|
5170
|
+
MAX_TIMEOUT_MS
|
|
5171
|
+
);
|
|
5172
|
+
const stallThresholdMs = (config.stallThresholdMinutes ?? 5) * 6e4;
|
|
5173
|
+
const weight = classifyWeight(config.timeoutMinutes ?? 60, config.command);
|
|
5174
|
+
const snapshot = takeSnapshot(this.store);
|
|
5175
|
+
const decision = canAdmit(snapshot, weight);
|
|
5176
|
+
if (!decision.allowed) {
|
|
5177
|
+
throw new Error(`Scheduler: ${decision.reason}`);
|
|
5178
|
+
}
|
|
5179
|
+
const workDir = config.workDir ? path$1.resolve(this.workspacePath, config.workDir) : this.workspacePath;
|
|
5180
|
+
const preflight = await runPreflight({
|
|
5181
|
+
command: config.command,
|
|
5182
|
+
workDir
|
|
5183
|
+
});
|
|
5184
|
+
if (!preflight.passed) {
|
|
5185
|
+
throw new Error(`Preflight failed: ${preflight.blockingIssues.join("; ")}`);
|
|
5186
|
+
}
|
|
5187
|
+
const provider = await getProvider(config.sandbox);
|
|
5188
|
+
const runId = nextRunId();
|
|
5189
|
+
this.store.getRunDir(runId);
|
|
5190
|
+
const outputPath = this.store.getOutputPath(runId);
|
|
5191
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
5192
|
+
const record = {
|
|
5193
|
+
runId,
|
|
5194
|
+
status: "running",
|
|
5195
|
+
weight,
|
|
5196
|
+
currentPhase: config.smokeCommand ? "smoke" : "full",
|
|
5197
|
+
command: config.command,
|
|
5198
|
+
smokeCommand: config.smokeCommand,
|
|
5199
|
+
workDir: config.workDir ?? ".",
|
|
5200
|
+
sandboxWorkDir: workDir,
|
|
5201
|
+
sandbox: provider.name,
|
|
5202
|
+
env: config.env,
|
|
5203
|
+
createdAt: now,
|
|
5204
|
+
startedAt: now,
|
|
5205
|
+
outputPath,
|
|
5206
|
+
outputBytes: 0,
|
|
5207
|
+
outputLines: 0,
|
|
5208
|
+
timeoutMs,
|
|
5209
|
+
stallThresholdMs,
|
|
5210
|
+
stalled: false,
|
|
5211
|
+
retryCount: config.parentRunId ? (this.store.getRun(config.parentRunId)?.retryCount ?? 0) + 1 : 0,
|
|
5212
|
+
parentRunId: config.parentRunId
|
|
5213
|
+
};
|
|
5214
|
+
this.store.createRun(record);
|
|
5215
|
+
const commandToRun = config.smokeCommand ?? config.command;
|
|
5216
|
+
try {
|
|
5217
|
+
const handle = await provider.spawn({
|
|
5218
|
+
command: commandToRun,
|
|
5219
|
+
workDir,
|
|
5220
|
+
outputPath,
|
|
5221
|
+
env: config.env
|
|
5222
|
+
});
|
|
5223
|
+
this.handles.set(runId, handle);
|
|
5224
|
+
this.ensurePolling();
|
|
5225
|
+
const pid = typeof handle.pid === "number" ? handle.pid : void 0;
|
|
5226
|
+
const pidStartTime = pid ? getPidStartTime(pid) : void 0;
|
|
5227
|
+
if (pid) {
|
|
5228
|
+
this.store.updateRun(runId, { pid, pidStartTime });
|
|
5229
|
+
}
|
|
5230
|
+
handle.wait().then(async (result) => {
|
|
5231
|
+
await this.handleExit(runId, result, config);
|
|
5232
|
+
}).catch(() => {
|
|
5233
|
+
});
|
|
5234
|
+
} catch (err) {
|
|
5235
|
+
this.store.updateRun(runId, {
|
|
5236
|
+
status: "failed",
|
|
5237
|
+
completedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5238
|
+
error: err instanceof Error ? err.message : String(err)
|
|
5239
|
+
});
|
|
5240
|
+
throw err;
|
|
5241
|
+
}
|
|
5242
|
+
return Object.assign(record, { preflight });
|
|
5243
|
+
}
|
|
5244
|
+
// -------------------------------------------------------------------------
|
|
5245
|
+
// Exit handler
|
|
5246
|
+
// -------------------------------------------------------------------------
|
|
5247
|
+
async handleExit(runId, result, config) {
|
|
5248
|
+
const run = this.store.getRun(runId);
|
|
5249
|
+
if (!run || isTerminal(run.status)) return;
|
|
5250
|
+
const stderrPath = this.store.getStderrPath(runId);
|
|
5251
|
+
const stderrTail = readFileTail(stderrPath, STDERR_TAIL_BYTES);
|
|
5252
|
+
if (run.currentPhase === "smoke" && result.exitCode === 0 && config.smokeCommand && config.command !== config.smokeCommand) {
|
|
5253
|
+
this.store.updateRun(runId, {
|
|
5254
|
+
currentPhase: "full",
|
|
5255
|
+
stderrTail
|
|
5256
|
+
});
|
|
5257
|
+
const provider = await getProvider(run.sandbox);
|
|
5258
|
+
try {
|
|
5259
|
+
const handle2 = await provider.spawn({
|
|
5260
|
+
command: config.command,
|
|
5261
|
+
workDir: run.sandboxWorkDir,
|
|
5262
|
+
outputPath: run.outputPath,
|
|
5263
|
+
env: config.env
|
|
5264
|
+
});
|
|
5265
|
+
this.handles.set(runId, handle2);
|
|
5266
|
+
handle2.wait().then(async (fullResult) => {
|
|
5267
|
+
await this.handleExit(runId, fullResult, { ...config, smokeCommand: void 0 });
|
|
5268
|
+
}).catch(() => {
|
|
5269
|
+
});
|
|
5270
|
+
return;
|
|
5271
|
+
} catch (err) {
|
|
5272
|
+
this.store.updateRun(runId, {
|
|
5273
|
+
status: "failed",
|
|
5274
|
+
completedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5275
|
+
error: err instanceof Error ? err.message : String(err)
|
|
5276
|
+
});
|
|
5277
|
+
return;
|
|
5278
|
+
}
|
|
5279
|
+
}
|
|
5280
|
+
const status = result.exitCode === 0 ? "completed" : "failed";
|
|
5281
|
+
const completedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
5282
|
+
const updated = this.store.updateRun(runId, {
|
|
5283
|
+
status,
|
|
5284
|
+
exitCode: result.exitCode,
|
|
5285
|
+
exitSignal: result.exitSignal,
|
|
5286
|
+
completedAt,
|
|
5287
|
+
stderrTail,
|
|
5288
|
+
stalled: false
|
|
5289
|
+
// Clear stall flag on terminal transition
|
|
5290
|
+
});
|
|
5291
|
+
if (updated) {
|
|
5292
|
+
try {
|
|
5293
|
+
const durationSeconds = updated.startedAt ? Math.round((Date.now() - new Date(updated.startedAt).getTime()) / 1e3) : 0;
|
|
5294
|
+
const taskKind = inferTaskKind(updated.command);
|
|
5295
|
+
const failure2 = status !== "completed" ? deriveFailure(updated) : void 0;
|
|
5296
|
+
this.experience.record({
|
|
5297
|
+
runId,
|
|
5298
|
+
taskKind,
|
|
5299
|
+
sandbox: updated.sandbox,
|
|
5300
|
+
outcome: status === "completed" ? "success" : "failed",
|
|
5301
|
+
failureCode: failure2?.code,
|
|
5302
|
+
durationSeconds,
|
|
5303
|
+
retryCount: updated.retryCount,
|
|
5304
|
+
timestamp: completedAt
|
|
5305
|
+
});
|
|
5306
|
+
} catch {
|
|
5307
|
+
}
|
|
5308
|
+
}
|
|
5309
|
+
const handle = this.handles.get(runId);
|
|
5310
|
+
if (handle) {
|
|
5311
|
+
await handle.cleanup().catch(() => {
|
|
5312
|
+
});
|
|
5313
|
+
this.handles.delete(runId);
|
|
5314
|
+
}
|
|
5315
|
+
this.stopPollingIfIdle();
|
|
5316
|
+
}
|
|
5317
|
+
// -------------------------------------------------------------------------
|
|
5318
|
+
// Stop (cancel)
|
|
5319
|
+
// -------------------------------------------------------------------------
|
|
5320
|
+
async stop(runId) {
|
|
5321
|
+
const run = this.store.getRun(runId);
|
|
5322
|
+
if (!run || isTerminal(run.status)) return;
|
|
5323
|
+
const handle = this.handles.get(runId);
|
|
5324
|
+
if (handle) {
|
|
5325
|
+
await handle.kill("SIGTERM");
|
|
5326
|
+
setTimeout(async () => {
|
|
5327
|
+
await handle.kill("SIGKILL").catch(() => {
|
|
5328
|
+
});
|
|
5329
|
+
}, 3e3);
|
|
5330
|
+
}
|
|
5331
|
+
const stderrPath = this.store.getStderrPath(runId);
|
|
5332
|
+
const stderrTail = readFileTail(stderrPath, STDERR_TAIL_BYTES);
|
|
5333
|
+
this.store.updateRun(runId, {
|
|
5334
|
+
status: "cancelled",
|
|
5335
|
+
completedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5336
|
+
stderrTail
|
|
5337
|
+
});
|
|
5338
|
+
this.handles.delete(runId);
|
|
5339
|
+
this.stopPollingIfIdle();
|
|
5340
|
+
}
|
|
5341
|
+
// -------------------------------------------------------------------------
|
|
5342
|
+
// Status queries
|
|
5343
|
+
// -------------------------------------------------------------------------
|
|
5344
|
+
getStatus(runId) {
|
|
5345
|
+
const run = this.store.getRun(runId);
|
|
5346
|
+
if (!run) return void 0;
|
|
5347
|
+
const outputTail = readFileTail(run.outputPath, OUTPUT_TAIL_BYTES);
|
|
5348
|
+
const structured = extractProgress(outputTail);
|
|
5349
|
+
const elapsed = run.startedAt ? (Date.now() - new Date(run.startedAt).getTime()) / 1e3 : 0;
|
|
5350
|
+
const failure2 = isTerminal(run.status) ? deriveFailure(run) : void 0;
|
|
5351
|
+
return {
|
|
5352
|
+
status: run.status,
|
|
5353
|
+
currentPhase: run.currentPhase,
|
|
5354
|
+
exitCode: run.exitCode,
|
|
5355
|
+
outputTail,
|
|
5356
|
+
outputBytes: run.outputBytes,
|
|
5357
|
+
outputLines: run.outputLines,
|
|
5358
|
+
elapsedSeconds: Math.round(elapsed),
|
|
5359
|
+
stalled: run.stalled,
|
|
5360
|
+
progress: structured,
|
|
5361
|
+
failure: failure2
|
|
5362
|
+
};
|
|
5363
|
+
}
|
|
5364
|
+
/**
|
|
5365
|
+
* Block until the run reaches a terminal state, stalls, or timeout.
|
|
5366
|
+
*/
|
|
5367
|
+
async waitForCompletion(runId, timeoutMs) {
|
|
5368
|
+
const deadline = Date.now() + timeoutMs;
|
|
5369
|
+
while (Date.now() < deadline) {
|
|
5370
|
+
const status = this.getStatus(runId);
|
|
5371
|
+
if (!status) return void 0;
|
|
5372
|
+
if (isTerminal(status.status)) return status;
|
|
5373
|
+
if (status.stalled) return status;
|
|
5374
|
+
await new Promise((resolve2) => setTimeout(resolve2, Math.min(5e3, deadline - Date.now())));
|
|
5375
|
+
}
|
|
5376
|
+
return this.getStatus(runId);
|
|
5377
|
+
}
|
|
5378
|
+
// -------------------------------------------------------------------------
|
|
5379
|
+
// Polling — shared interval for all active runs
|
|
5380
|
+
// -------------------------------------------------------------------------
|
|
5381
|
+
ensurePolling() {
|
|
5382
|
+
if (this.pollTimer) return;
|
|
5383
|
+
this.pollTimer = setInterval(() => {
|
|
5384
|
+
this.pollAll();
|
|
5385
|
+
}, POLL_INTERVAL_MS);
|
|
5386
|
+
if (this.pollTimer.unref) this.pollTimer.unref();
|
|
5387
|
+
}
|
|
5388
|
+
stopPollingIfIdle() {
|
|
5389
|
+
const active = this.store.getActiveRuns();
|
|
5390
|
+
if (active.length === 0 && this.pollTimer) {
|
|
5391
|
+
clearInterval(this.pollTimer);
|
|
5392
|
+
this.pollTimer = null;
|
|
5393
|
+
}
|
|
5394
|
+
}
|
|
5395
|
+
pollAll() {
|
|
5396
|
+
const active = this.store.getActiveRuns();
|
|
5397
|
+
for (const record of active) {
|
|
5398
|
+
if (this.pollInFlight.has(record.runId)) continue;
|
|
5399
|
+
this.pollInFlight.add(record.runId);
|
|
5400
|
+
try {
|
|
5401
|
+
this.pollOnce(record.runId);
|
|
5402
|
+
} finally {
|
|
5403
|
+
this.pollInFlight.delete(record.runId);
|
|
5404
|
+
}
|
|
5405
|
+
}
|
|
5406
|
+
this.stopPollingIfIdle();
|
|
5407
|
+
}
|
|
5408
|
+
pollOnce(runId) {
|
|
5409
|
+
const run = this.store.getRun(runId);
|
|
5410
|
+
if (!run || isTerminal(run.status)) return;
|
|
5411
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
5412
|
+
const currentBytes = getFileSize(run.outputPath);
|
|
5413
|
+
const outputGrew = currentBytes > run.outputBytes;
|
|
5414
|
+
const tail = readFileTail(run.outputPath, OUTPUT_TAIL_BYTES);
|
|
5415
|
+
const lines = estimateLines(currentBytes, tail);
|
|
5416
|
+
const patch = {
|
|
5417
|
+
outputBytes: currentBytes,
|
|
5418
|
+
outputLines: lines
|
|
5419
|
+
};
|
|
5420
|
+
if (outputGrew) {
|
|
5421
|
+
patch.lastOutputAt = now;
|
|
5422
|
+
if (run.stalled) {
|
|
5423
|
+
patch.stalled = false;
|
|
5424
|
+
patch.status = "running";
|
|
5425
|
+
}
|
|
5426
|
+
}
|
|
5427
|
+
if (!outputGrew && run.lastOutputAt) {
|
|
5428
|
+
const silentMs = Date.now() - new Date(run.lastOutputAt).getTime();
|
|
5429
|
+
if (silentMs > run.stallThresholdMs && !run.stalled) {
|
|
5430
|
+
patch.stalled = true;
|
|
5431
|
+
patch.status = "stalled";
|
|
5432
|
+
}
|
|
5433
|
+
}
|
|
5434
|
+
if (currentBytes > MAX_OUTPUT_BYTES) {
|
|
5435
|
+
const handle = this.handles.get(run.runId);
|
|
5436
|
+
if (handle) {
|
|
5437
|
+
handle.kill("SIGTERM").catch(() => {
|
|
5438
|
+
});
|
|
5439
|
+
setTimeout(() => {
|
|
5440
|
+
handle.kill("SIGKILL").catch(() => {
|
|
5441
|
+
});
|
|
5442
|
+
}, 3e3);
|
|
5443
|
+
this.handles.delete(run.runId);
|
|
5444
|
+
}
|
|
5445
|
+
const stderrTail = readFileTail(this.store.getStderrPath(run.runId), STDERR_TAIL_BYTES);
|
|
5446
|
+
this.store.updateRun(run.runId, {
|
|
5447
|
+
...patch,
|
|
5448
|
+
status: "failed",
|
|
5449
|
+
completedAt: now,
|
|
5450
|
+
stalled: false,
|
|
5451
|
+
stderrTail,
|
|
5452
|
+
error: `Output exceeded ${Math.round(MAX_OUTPUT_BYTES / (1024 * 1024))}MB limit. Process killed.`
|
|
5453
|
+
});
|
|
5454
|
+
return;
|
|
5455
|
+
}
|
|
5456
|
+
if (run.startedAt) {
|
|
5457
|
+
const elapsedMs = Date.now() - new Date(run.startedAt).getTime();
|
|
5458
|
+
if (elapsedMs > run.timeoutMs) {
|
|
5459
|
+
const handle = this.handles.get(run.runId);
|
|
5460
|
+
if (handle) {
|
|
5461
|
+
handle.kill("SIGTERM").catch(() => {
|
|
5462
|
+
});
|
|
5463
|
+
setTimeout(() => {
|
|
5464
|
+
handle.kill("SIGKILL").catch(() => {
|
|
5465
|
+
});
|
|
5466
|
+
}, 3e3);
|
|
5467
|
+
this.handles.delete(run.runId);
|
|
5468
|
+
}
|
|
5469
|
+
const stderrTail = readFileTail(this.store.getStderrPath(run.runId), STDERR_TAIL_BYTES);
|
|
5470
|
+
this.store.updateRun(run.runId, {
|
|
5471
|
+
...patch,
|
|
5472
|
+
status: "timed_out",
|
|
5473
|
+
completedAt: now,
|
|
5474
|
+
stalled: false,
|
|
5475
|
+
// Clear stall flag on terminal transition
|
|
5476
|
+
stderrTail
|
|
5477
|
+
});
|
|
5478
|
+
return;
|
|
5479
|
+
}
|
|
5480
|
+
}
|
|
5481
|
+
this.store.updateRun(run.runId, patch);
|
|
5482
|
+
}
|
|
5483
|
+
// -------------------------------------------------------------------------
|
|
5484
|
+
// Lifecycle
|
|
5485
|
+
// -------------------------------------------------------------------------
|
|
5486
|
+
/**
|
|
5487
|
+
* Stop all active runs and clean up. Called on coordinator destroy / app quit.
|
|
5488
|
+
* Sends SIGTERM, waits up to 5s, then SIGKILL any survivors.
|
|
5489
|
+
*/
|
|
5490
|
+
async destroy() {
|
|
5491
|
+
if (this.pollTimer) {
|
|
5492
|
+
clearInterval(this.pollTimer);
|
|
5493
|
+
this.pollTimer = null;
|
|
5494
|
+
}
|
|
5495
|
+
this.store.stopFlushTimer();
|
|
5496
|
+
this.store.flushNow();
|
|
5497
|
+
if (this.handles.size === 0) return;
|
|
5498
|
+
const entries = [...this.handles.entries()];
|
|
5499
|
+
for (const [, handle] of entries) {
|
|
5500
|
+
await handle.kill("SIGTERM").catch(() => {
|
|
5501
|
+
});
|
|
5502
|
+
}
|
|
5503
|
+
const deadline = Date.now() + DESTROY_KILL_TIMEOUT_MS;
|
|
5504
|
+
const pending = new Map(entries);
|
|
5505
|
+
while (pending.size > 0 && Date.now() < deadline) {
|
|
5506
|
+
for (const [runId, handle] of pending) {
|
|
5507
|
+
const record = this.store.getRun(runId);
|
|
5508
|
+
if (!record?.pid || !isPidAlive(record.pid)) {
|
|
5509
|
+
pending.delete(runId);
|
|
5510
|
+
}
|
|
5511
|
+
}
|
|
5512
|
+
if (pending.size > 0) {
|
|
5513
|
+
await new Promise((resolve2) => setTimeout(resolve2, 500));
|
|
5514
|
+
}
|
|
5515
|
+
}
|
|
5516
|
+
for (const [, handle] of pending) {
|
|
5517
|
+
await handle.kill("SIGKILL").catch(() => {
|
|
5518
|
+
});
|
|
5519
|
+
}
|
|
5520
|
+
for (const [runId, handle] of entries) {
|
|
5521
|
+
await handle.cleanup().catch(() => {
|
|
5522
|
+
});
|
|
5523
|
+
this.store.updateRun(runId, {
|
|
5524
|
+
status: "cancelled",
|
|
5525
|
+
completedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5526
|
+
stalled: false
|
|
5527
|
+
});
|
|
5528
|
+
}
|
|
5529
|
+
this.handles.clear();
|
|
5530
|
+
this.store.flushNow();
|
|
5531
|
+
}
|
|
5532
|
+
/**
|
|
5533
|
+
* Get the RunStore for direct queries (used by tools).
|
|
5534
|
+
*/
|
|
5535
|
+
getStore() {
|
|
5536
|
+
return this.store;
|
|
5537
|
+
}
|
|
5538
|
+
/**
|
|
5539
|
+
* Get the ExperienceStore for queries.
|
|
5540
|
+
*/
|
|
5541
|
+
getExperience() {
|
|
5542
|
+
return this.experience;
|
|
5543
|
+
}
|
|
5544
|
+
}
|
|
5545
|
+
const PROFILER_SYSTEM_PROMPT = `You are a compute task profiler. Analyze the given script and command, then output a JSON object with these fields:
|
|
5546
|
+
|
|
5547
|
+
{
|
|
5548
|
+
"cpuDensity": "low" | "medium" | "high",
|
|
5549
|
+
"gpuDensity": "none" | "light" | "heavy",
|
|
5550
|
+
"memoryPattern": "constant" | "growing" | "spike",
|
|
5551
|
+
"ioPattern": "read_heavy" | "write_heavy" | "balanced" | "minimal",
|
|
5552
|
+
"chunkable": boolean, // Can be split into smaller data pieces
|
|
5553
|
+
"resumable": boolean, // Produces checkpoints, can restart
|
|
5554
|
+
"idempotent": boolean, // Safe to re-run without side effects
|
|
5555
|
+
"hasExternalSideEffects": boolean, // API calls, DB writes, emails
|
|
5556
|
+
"networkRequired": boolean,
|
|
5557
|
+
"smokeSupported": boolean, // Has --smoke or similar quick-validation flag
|
|
5558
|
+
"expectedDurationClass": "seconds" | "minutes" | "hours",
|
|
5559
|
+
"confidence": "high" | "medium" | "low",
|
|
5560
|
+
"reasoning": "Brief explanation of your analysis"
|
|
5561
|
+
}
|
|
5562
|
+
|
|
5563
|
+
Guidelines:
|
|
5564
|
+
- cpuDensity: "high" for training loops, simulations, heavy computation
|
|
5565
|
+
- gpuDensity: "heavy" if torch/tensorflow/mlx with .fit() or training loops; "light" for inference; "none" for CPU-only
|
|
5566
|
+
- memoryPattern: "spike" if large dataset loaded at once; "growing" if accumulating; "constant" if streaming/fixed
|
|
5567
|
+
- ioPattern: "read_heavy" if large data loads; "write_heavy" if many output files; "balanced" if both
|
|
5568
|
+
- chunkable: true if data can be split (pandas, numpy batch processing)
|
|
5569
|
+
- resumable: true if checkpointing detected (torch.save, callbacks)
|
|
5570
|
+
- smokeSupported: true ONLY if --smoke, --dry-run, or --validate flag is in argparse/click
|
|
5571
|
+
- Be conservative with confidence: "high" only if the pattern is unmistakable
|
|
5572
|
+
|
|
5573
|
+
Output ONLY the JSON object, no explanation outside it.`;
|
|
5574
|
+
async function profileTask(command, scriptContent, callLlm) {
|
|
5575
|
+
if (!callLlm || !scriptContent) return defaultProfile(command);
|
|
5576
|
+
try {
|
|
5577
|
+
const response = await callLlm(
|
|
5578
|
+
PROFILER_SYSTEM_PROMPT,
|
|
5579
|
+
`Command: ${command}
|
|
5580
|
+
|
|
5581
|
+
Script content:
|
|
5582
|
+
${scriptContent.slice(0, 8e3)}`
|
|
5583
|
+
);
|
|
5584
|
+
const parsed = parseJsonFromLlm(response);
|
|
5585
|
+
if (!parsed) return defaultProfile(command);
|
|
5586
|
+
return mergeWithDefaults$1(parsed, command);
|
|
5587
|
+
} catch {
|
|
5588
|
+
return defaultProfile(command);
|
|
5589
|
+
}
|
|
5590
|
+
}
|
|
5591
|
+
function defaultProfile(command) {
|
|
5592
|
+
const cmd = command.toLowerCase();
|
|
5593
|
+
const isTrain = /train|fit|epoch/i.test(cmd);
|
|
5594
|
+
const isViz = /plot|chart|viz|figure|graph/i.test(cmd);
|
|
5595
|
+
return {
|
|
5596
|
+
cpuDensity: isTrain ? "high" : isViz ? "low" : "medium",
|
|
5597
|
+
gpuDensity: "none",
|
|
5598
|
+
memoryPattern: "constant",
|
|
5599
|
+
ioPattern: "balanced",
|
|
5600
|
+
chunkable: false,
|
|
5601
|
+
resumable: false,
|
|
5602
|
+
idempotent: true,
|
|
5603
|
+
hasExternalSideEffects: false,
|
|
5604
|
+
networkRequired: false,
|
|
5605
|
+
smokeSupported: false,
|
|
5606
|
+
expectedDurationClass: isViz ? "seconds" : isTrain ? "hours" : "minutes",
|
|
5607
|
+
confidence: "low",
|
|
5608
|
+
reasoning: "Default profile (LLM unavailable or no script content)."
|
|
5609
|
+
};
|
|
5610
|
+
}
|
|
5611
|
+
function mergeWithDefaults$1(partial, command) {
|
|
5612
|
+
const defaults = defaultProfile(command);
|
|
5613
|
+
return {
|
|
5614
|
+
cpuDensity: partial.cpuDensity ?? defaults.cpuDensity,
|
|
5615
|
+
gpuDensity: partial.gpuDensity ?? defaults.gpuDensity,
|
|
5616
|
+
memoryPattern: partial.memoryPattern ?? defaults.memoryPattern,
|
|
5617
|
+
ioPattern: partial.ioPattern ?? defaults.ioPattern,
|
|
5618
|
+
chunkable: partial.chunkable ?? defaults.chunkable,
|
|
5619
|
+
resumable: partial.resumable ?? defaults.resumable,
|
|
5620
|
+
idempotent: partial.idempotent ?? defaults.idempotent,
|
|
5621
|
+
hasExternalSideEffects: partial.hasExternalSideEffects ?? defaults.hasExternalSideEffects,
|
|
5622
|
+
networkRequired: partial.networkRequired ?? defaults.networkRequired,
|
|
5623
|
+
smokeSupported: partial.smokeSupported ?? defaults.smokeSupported,
|
|
5624
|
+
expectedDurationClass: partial.expectedDurationClass ?? defaults.expectedDurationClass,
|
|
5625
|
+
confidence: partial.confidence ?? defaults.confidence,
|
|
5626
|
+
reasoning: partial.reasoning ?? defaults.reasoning
|
|
5627
|
+
};
|
|
5628
|
+
}
|
|
5629
|
+
function parseJsonFromLlm(response) {
|
|
5630
|
+
let text = response.replace(/```json\s*/g, "").replace(/```\s*/g, "").trim();
|
|
5631
|
+
const start = text.indexOf("{");
|
|
5632
|
+
const end = text.lastIndexOf("}");
|
|
5633
|
+
if (start === -1 || end === -1 || end <= start) return null;
|
|
5634
|
+
try {
|
|
5635
|
+
return JSON.parse(text.slice(start, end + 1));
|
|
5636
|
+
} catch {
|
|
5637
|
+
return null;
|
|
5638
|
+
}
|
|
5639
|
+
}
|
|
5640
|
+
const execFileAsync = promisify(execFile);
|
|
5641
|
+
async function probePython() {
|
|
5642
|
+
let version = "unknown";
|
|
5643
|
+
try {
|
|
5644
|
+
const { stdout } = await execFileAsync("python3", ["--version"], { timeout: 5e3 });
|
|
5645
|
+
version = stdout.trim().replace("Python ", "");
|
|
5646
|
+
} catch {
|
|
5647
|
+
}
|
|
5648
|
+
let packages = [];
|
|
5649
|
+
try {
|
|
5650
|
+
const { stdout } = await execFileAsync("python3", ["-m", "pip", "list", "--format=json"], { timeout: 15e3 });
|
|
5651
|
+
const parsed = JSON.parse(stdout);
|
|
5652
|
+
packages = parsed.map((p) => p.name.toLowerCase());
|
|
5653
|
+
} catch {
|
|
5654
|
+
try {
|
|
5655
|
+
const { stdout } = await execFileAsync("pip3", ["list", "--format=json"], { timeout: 15e3 });
|
|
5656
|
+
const parsed = JSON.parse(stdout);
|
|
5657
|
+
packages = parsed.map((p) => p.name.toLowerCase());
|
|
5658
|
+
} catch {
|
|
5659
|
+
}
|
|
5660
|
+
}
|
|
5661
|
+
return { version, packages };
|
|
5662
|
+
}
|
|
5663
|
+
async function probeGpu(pipPackages) {
|
|
5664
|
+
const platform = os.platform();
|
|
5665
|
+
const arch = os.arch();
|
|
5666
|
+
if (platform === "darwin" && arch === "arm64") {
|
|
5667
|
+
let mlxAvailable = false;
|
|
5668
|
+
const mlxPackages = [];
|
|
5669
|
+
const mlxRelated = ["mlx", "mlx-nn", "mlx-data", "mlx-lm", "mlx-optimizers", "mlx-audio", "mlx-vlm"];
|
|
5670
|
+
for (const pkg of mlxRelated) {
|
|
5671
|
+
if (pipPackages.includes(pkg)) {
|
|
5672
|
+
mlxPackages.push(pkg);
|
|
5673
|
+
}
|
|
5674
|
+
}
|
|
5675
|
+
if (pipPackages.includes("mlx")) {
|
|
5676
|
+
try {
|
|
5677
|
+
await execFileAsync("python3", ["-c", "import mlx; print(mlx.__version__)"], { timeout: 1e4 });
|
|
5678
|
+
mlxAvailable = true;
|
|
5679
|
+
} catch {
|
|
5680
|
+
}
|
|
5681
|
+
}
|
|
5682
|
+
let model = "Apple Silicon";
|
|
5683
|
+
try {
|
|
5684
|
+
const output = execSync("sysctl -n machdep.cpu.brand_string", { encoding: "utf-8", timeout: 3e3 }).trim();
|
|
5685
|
+
if (output) model = output;
|
|
5686
|
+
} catch {
|
|
5687
|
+
}
|
|
5688
|
+
return {
|
|
5689
|
+
type: "apple_silicon",
|
|
5690
|
+
model,
|
|
5691
|
+
memoryMb: Math.round(os.totalmem() / (1024 * 1024)),
|
|
5692
|
+
// Unified memory
|
|
5693
|
+
mlxAvailable,
|
|
5694
|
+
mlxPackages,
|
|
5695
|
+
cudaAvailable: false,
|
|
5696
|
+
metalAvailable: true
|
|
5697
|
+
};
|
|
5698
|
+
}
|
|
5699
|
+
try {
|
|
5700
|
+
const { stdout } = await execFileAsync("nvidia-smi", [
|
|
5701
|
+
"--query-gpu=name,memory.total",
|
|
5702
|
+
"--format=csv,noheader,nounits"
|
|
5703
|
+
], { timeout: 5e3 });
|
|
5704
|
+
const parts = stdout.trim().split(",").map((s) => s.trim());
|
|
5705
|
+
return {
|
|
5706
|
+
type: "nvidia",
|
|
5707
|
+
model: parts[0] ?? "NVIDIA GPU",
|
|
5708
|
+
memoryMb: parseInt(parts[1] ?? "0", 10) || void 0,
|
|
5709
|
+
mlxAvailable: false,
|
|
5710
|
+
mlxPackages: [],
|
|
5711
|
+
cudaAvailable: true,
|
|
5712
|
+
metalAvailable: false
|
|
5713
|
+
};
|
|
5714
|
+
} catch {
|
|
5715
|
+
}
|
|
5716
|
+
return {
|
|
5717
|
+
type: "none",
|
|
5718
|
+
model: "No GPU detected",
|
|
5719
|
+
mlxAvailable: false,
|
|
5720
|
+
mlxPackages: [],
|
|
5721
|
+
cudaAvailable: false,
|
|
5722
|
+
metalAvailable: false
|
|
5723
|
+
};
|
|
5724
|
+
}
|
|
5725
|
+
async function probeDocker() {
|
|
5726
|
+
try {
|
|
5727
|
+
await execFileAsync("docker", ["info"], { timeout: 5e3 });
|
|
5728
|
+
return true;
|
|
5729
|
+
} catch {
|
|
5730
|
+
return false;
|
|
5731
|
+
}
|
|
5732
|
+
}
|
|
5733
|
+
let cachedProfile = null;
|
|
5734
|
+
async function probeStaticProfile() {
|
|
5735
|
+
if (cachedProfile) return cachedProfile;
|
|
5736
|
+
const [python, docker] = await Promise.all([
|
|
5737
|
+
probePython(),
|
|
5738
|
+
probeDocker()
|
|
5739
|
+
]);
|
|
5740
|
+
const gpu = await probeGpu(python.packages);
|
|
5741
|
+
const platform = os.platform();
|
|
5742
|
+
cachedProfile = {
|
|
5743
|
+
os: platform === "darwin" ? "darwin" : platform === "linux" ? "linux" : "other",
|
|
5744
|
+
arch: os.arch(),
|
|
5745
|
+
cpuCores: os.cpus().length,
|
|
5746
|
+
cpuModel: os.cpus()[0]?.model ?? "Unknown CPU",
|
|
5747
|
+
totalMemoryMb: Math.round(os.totalmem() / (1024 * 1024)),
|
|
5748
|
+
gpu,
|
|
5749
|
+
pythonVersion: python.version,
|
|
5750
|
+
pipPackages: python.packages,
|
|
5751
|
+
dockerAvailable: docker
|
|
5752
|
+
};
|
|
5753
|
+
return cachedProfile;
|
|
5754
|
+
}
|
|
5755
|
+
function generateAgentGuidance(profile) {
|
|
5756
|
+
const lines = [];
|
|
5757
|
+
lines.push("## Local Compute Environment");
|
|
5758
|
+
lines.push("");
|
|
5759
|
+
lines.push(`Machine: ${profile.os} ${profile.arch}, ${profile.cpuCores} CPU cores, ${profile.totalMemoryMb}MB RAM`);
|
|
5760
|
+
lines.push(`Python: ${profile.pythonVersion}`);
|
|
5761
|
+
if (profile.gpu.type === "apple_silicon") {
|
|
5762
|
+
lines.push(`GPU: ${profile.gpu.model} (${profile.gpu.memoryMb}MB unified memory, Metal available)`);
|
|
5763
|
+
if (profile.gpu.mlxAvailable) {
|
|
5764
|
+
lines.push("");
|
|
5765
|
+
lines.push("### MLX Acceleration Available");
|
|
5766
|
+
lines.push(`Installed MLX packages: ${profile.gpu.mlxPackages.join(", ")}`);
|
|
5767
|
+
lines.push("When writing ML training code for local_compute_execute:");
|
|
5768
|
+
lines.push("- Prefer mlx over PyTorch/TensorFlow for training — native Metal acceleration, no CUDA needed");
|
|
5769
|
+
lines.push("- Use mlx.core.array for GPU-accelerated array operations");
|
|
5770
|
+
lines.push("- Check mlx-community on HuggingFace for MLX-converted model weights");
|
|
5771
|
+
lines.push("- mlx uses lazy evaluation — call mx.eval() to force computation");
|
|
5772
|
+
lines.push("- mlx supports 4-bit quantization via mlx-lm for large models");
|
|
5773
|
+
} else {
|
|
5774
|
+
lines.push("");
|
|
5775
|
+
lines.push("MLX is NOT installed. For Apple Silicon ML acceleration, install: pip install mlx mlx-nn");
|
|
5776
|
+
lines.push("Without MLX, PyTorch MPS backend is available for GPU-accelerated training on Apple Silicon.");
|
|
5777
|
+
}
|
|
5778
|
+
} else if (profile.gpu.type === "nvidia") {
|
|
5779
|
+
lines.push(`GPU: ${profile.gpu.model}${profile.gpu.memoryMb ? ` (${profile.gpu.memoryMb}MB VRAM)` : ""}, CUDA available`);
|
|
5780
|
+
lines.push("PyTorch/TensorFlow can use CUDA acceleration.");
|
|
5781
|
+
} else {
|
|
5782
|
+
lines.push("GPU: None detected. ML training will use CPU only.");
|
|
5783
|
+
lines.push("Consider using smaller models/datasets, or running on a machine with a GPU.");
|
|
5784
|
+
}
|
|
5785
|
+
if (profile.dockerAvailable) {
|
|
5786
|
+
lines.push("");
|
|
5787
|
+
lines.push("Docker: Available. Can use docker sandbox for stronger isolation.");
|
|
5788
|
+
}
|
|
5789
|
+
lines.push("");
|
|
5790
|
+
lines.push("### Sandbox Guidelines");
|
|
5791
|
+
lines.push("When writing scripts for local_compute_execute:");
|
|
5792
|
+
lines.push("- All required packages must be importable (preflight checks imports before running)");
|
|
5793
|
+
lines.push("- For long-running tasks, consider adding a --smoke flag for quick validation");
|
|
5794
|
+
lines.push('- Print progress lines: ##PROGRESS## {"step": N, "total": M, "loss": 0.85, "phase": "training"}');
|
|
5795
|
+
lines.push("- Write output files to the working directory (results, figures, checkpoints)");
|
|
5796
|
+
return lines.join("\n");
|
|
5797
|
+
}
|
|
5798
|
+
const RISK_ASSESSMENT_PROMPT = `You are a compute execution risk assessor. Given the task profile, system environment, and past experience, assess risks and recommend execution parameters.
|
|
5799
|
+
|
|
5800
|
+
Output a JSON object:
|
|
5801
|
+
{
|
|
5802
|
+
"feasible": boolean,
|
|
5803
|
+
"risks": [
|
|
5804
|
+
{ "severity": "low"|"medium"|"high"|"blocking", "category": "memory"|"disk"|"gpu"|"dependency"|"network"|"timeout", "message": "...", "mitigation": "..." }
|
|
5805
|
+
],
|
|
5806
|
+
"recommendedSandbox": "docker" | "process",
|
|
5807
|
+
"recommendedTimeoutMinutes": number,
|
|
5808
|
+
"recommendedStallThresholdMinutes": number,
|
|
5809
|
+
"warnings": ["..."],
|
|
5810
|
+
"agentGuidance": ["tips for the coding agent..."]
|
|
5811
|
+
}
|
|
5812
|
+
|
|
5813
|
+
Guidelines:
|
|
5814
|
+
- Mark "blocking" severity only for showstopper issues (no GPU when GPU required, <100MB disk)
|
|
5815
|
+
- recommendedTimeoutMinutes: use experience average × 2 if available, else estimate from task type
|
|
5816
|
+
- recommendedSandbox: prefer "process" on macOS (Docker lacks GPU passthrough), "docker" on Linux with NVIDIA
|
|
5817
|
+
- For Apple Silicon with MLX: recommend process sandbox for Metal GPU access
|
|
5818
|
+
- Be actionable in mitigations and guidance
|
|
5819
|
+
|
|
5820
|
+
Output ONLY the JSON object.`;
|
|
5821
|
+
async function assessRisk(opts) {
|
|
5822
|
+
const { taskProfile, env, snapshot, experience, command, callLlm } = opts;
|
|
5823
|
+
if (!callLlm) return defaultAdvice(env, snapshot);
|
|
5824
|
+
try {
|
|
5825
|
+
const facts = formatFacts(taskProfile, env, snapshot, experience, command);
|
|
5826
|
+
const response = await callLlm(RISK_ASSESSMENT_PROMPT, facts);
|
|
5827
|
+
const parsed = parseJsonFromLlm(response);
|
|
5828
|
+
if (!parsed) return defaultAdvice(env, snapshot);
|
|
5829
|
+
return mergeWithDefaults(parsed, env, snapshot);
|
|
5830
|
+
} catch {
|
|
5831
|
+
return defaultAdvice(env, snapshot);
|
|
5832
|
+
}
|
|
5833
|
+
}
|
|
5834
|
+
function formatFacts(taskProfile, env, snapshot, experience, command) {
|
|
5835
|
+
const sections = [];
|
|
5836
|
+
if (taskProfile) {
|
|
5837
|
+
sections.push(`## Task Profile
|
|
5838
|
+
${JSON.stringify(taskProfile, null, 2)}`);
|
|
5839
|
+
}
|
|
5840
|
+
sections.push(`## Command
|
|
5841
|
+
${command}`);
|
|
5842
|
+
sections.push(`## Environment
|
|
5843
|
+
OS: ${env.os} ${env.arch}
|
|
5844
|
+
CPU: ${env.cpuCores} cores (${env.cpuModel})
|
|
5845
|
+
RAM: ${env.totalMemoryMb}MB total, ~${snapshot.freeMemoryMb}MB free
|
|
5846
|
+
GPU: ${env.gpu.type === "none" ? "None" : `${env.gpu.model} (${env.gpu.type})`}
|
|
5847
|
+
MLX: ${env.gpu.mlxAvailable ? `Yes (${env.gpu.mlxPackages.join(", ")})` : "No"}
|
|
5848
|
+
CUDA: ${env.gpu.cudaAvailable ? "Yes" : "No"}
|
|
5849
|
+
Python: ${env.pythonVersion}
|
|
5850
|
+
Docker: ${env.dockerAvailable ? "Yes" : "No"}
|
|
5851
|
+
Disk free: ~${snapshot.freeDiskMb}MB
|
|
5852
|
+
CPU load: ${snapshot.cpuLoadPercent}%
|
|
5853
|
+
Active runs: ${snapshot.activeRuns.length}`);
|
|
5854
|
+
if (experience) {
|
|
5855
|
+
sections.push(`## Past Experience (${experience.taskKind})
|
|
5856
|
+
Total runs: ${experience.totalRuns}
|
|
5857
|
+
Successes: ${experience.successes}, Failures: ${experience.failures}
|
|
5858
|
+
${experience.avgDurationSeconds ? `Avg duration: ${Math.round(experience.avgDurationSeconds / 60)} min` : ""}
|
|
5859
|
+
${Object.keys(experience.commonFailures).length > 0 ? `Common failures: ${JSON.stringify(experience.commonFailures)}` : ""}`);
|
|
5860
|
+
}
|
|
5861
|
+
return sections.join("\n\n");
|
|
5862
|
+
}
|
|
5863
|
+
function defaultAdvice(env, snapshot) {
|
|
5864
|
+
const risks = [];
|
|
5865
|
+
const warnings = [];
|
|
5866
|
+
if (snapshot.freeMemoryMb < 1e3) {
|
|
5867
|
+
risks.push({
|
|
5868
|
+
severity: snapshot.freeMemoryMb < 500 ? "high" : "medium",
|
|
5869
|
+
category: "memory",
|
|
5870
|
+
message: `Only ${snapshot.freeMemoryMb}MB free memory.`,
|
|
5871
|
+
mitigation: "Close memory-intensive applications."
|
|
5872
|
+
});
|
|
5873
|
+
}
|
|
5874
|
+
if (snapshot.freeDiskMb < 2e3) {
|
|
5875
|
+
risks.push({
|
|
5876
|
+
severity: snapshot.freeDiskMb < 500 ? "blocking" : "medium",
|
|
5877
|
+
category: "disk",
|
|
5878
|
+
message: `Only ${snapshot.freeDiskMb}MB free disk space.`
|
|
5879
|
+
});
|
|
5880
|
+
}
|
|
5881
|
+
if (snapshot.cpuLoadPercent > 80) {
|
|
5882
|
+
warnings.push(`High CPU load (${snapshot.cpuLoadPercent}%). Run may be slower than expected.`);
|
|
5883
|
+
}
|
|
5884
|
+
return {
|
|
5885
|
+
feasible: !risks.some((r) => r.severity === "blocking"),
|
|
5886
|
+
risks,
|
|
5887
|
+
recommendedSandbox: "process",
|
|
5888
|
+
recommendedTimeoutMinutes: 60,
|
|
5889
|
+
recommendedStallThresholdMinutes: 5,
|
|
5890
|
+
warnings,
|
|
5891
|
+
agentGuidance: []
|
|
5892
|
+
};
|
|
5893
|
+
}
|
|
5894
|
+
function mergeWithDefaults(partial, env, snapshot) {
|
|
5895
|
+
const defaults = defaultAdvice(env, snapshot);
|
|
5896
|
+
return {
|
|
5897
|
+
feasible: partial.feasible ?? defaults.feasible,
|
|
5898
|
+
risks: partial.risks ?? defaults.risks,
|
|
5899
|
+
recommendedSandbox: partial.recommendedSandbox ?? defaults.recommendedSandbox,
|
|
5900
|
+
recommendedTimeoutMinutes: partial.recommendedTimeoutMinutes ?? defaults.recommendedTimeoutMinutes,
|
|
5901
|
+
recommendedStallThresholdMinutes: partial.recommendedStallThresholdMinutes ?? defaults.recommendedStallThresholdMinutes,
|
|
5902
|
+
warnings: partial.warnings ?? defaults.warnings,
|
|
5903
|
+
agentGuidance: partial.agentGuidance ?? defaults.agentGuidance
|
|
5904
|
+
};
|
|
5905
|
+
}
|
|
5906
|
+
function createLocalComputeTools(ctx) {
|
|
5907
|
+
const runner = new ComputeRunner({
|
|
5908
|
+
projectPath: ctx.projectPath,
|
|
5909
|
+
workspacePath: ctx.workspacePath
|
|
5910
|
+
});
|
|
5911
|
+
const tools = [
|
|
5912
|
+
createPlanTool(runner, ctx),
|
|
5913
|
+
createExecuteTool(runner, ctx),
|
|
5914
|
+
createWaitTool(runner),
|
|
5915
|
+
createStatusTool(runner),
|
|
5916
|
+
createStopTool(runner)
|
|
5917
|
+
];
|
|
5918
|
+
return {
|
|
5919
|
+
tools,
|
|
5920
|
+
destroy: () => runner.destroy()
|
|
5921
|
+
};
|
|
5922
|
+
}
|
|
5923
|
+
function createPlanTool(runner, ctx) {
|
|
5924
|
+
return {
|
|
5925
|
+
name: "local_compute_plan",
|
|
5926
|
+
label: "Local Compute: Plan",
|
|
5927
|
+
description: "Analyze a script before execution: profile the task, assess risks, and get recommendations.\nOptional — you can skip this and call local_compute_execute directly for simple tasks.\nUse this for complex or risky tasks (large datasets, GPU training, unfamiliar code).",
|
|
5928
|
+
parameters: Type.Object({
|
|
5929
|
+
command: Type.String({ description: "Shell command to execute" }),
|
|
5930
|
+
script_path: Type.Optional(Type.String({ description: "Relative path to the main script (for deeper analysis)" })),
|
|
5931
|
+
sandbox: Type.Optional(Type.String({ description: '"docker" | "process" | "auto"' })),
|
|
5932
|
+
timeout_minutes: Type.Optional(Type.Number({ description: "Suggested timeout" })),
|
|
5933
|
+
smoke_command: Type.Optional(Type.String({ description: 'Quick validation command (e.g., "python3 script.py --smoke")' }))
|
|
5934
|
+
}),
|
|
5935
|
+
execute: async (_toolCallId, rawParams) => {
|
|
5936
|
+
const params = rawParams;
|
|
5937
|
+
const command = typeof params.command === "string" ? params.command.trim() : "";
|
|
5938
|
+
if (!command) {
|
|
5939
|
+
return toAgentResult("local_compute_plan", toolError("MISSING_PARAMETER", "command is required."));
|
|
5940
|
+
}
|
|
5941
|
+
let scriptContent;
|
|
5942
|
+
if (typeof params.script_path === "string") {
|
|
5943
|
+
const scriptPath = path$1.isAbsolute(params.script_path) ? params.script_path : path$1.resolve(ctx.workspacePath, params.script_path);
|
|
5944
|
+
try {
|
|
5945
|
+
scriptContent = fs.readFileSync(scriptPath, "utf-8");
|
|
5946
|
+
} catch {
|
|
5947
|
+
}
|
|
5948
|
+
}
|
|
5949
|
+
const taskProfile = await profileTask(command, scriptContent, ctx.callLlm);
|
|
5950
|
+
let env;
|
|
5951
|
+
try {
|
|
5952
|
+
env = await probeStaticProfile();
|
|
5953
|
+
} catch {
|
|
5954
|
+
return toAgentResult("local_compute_plan", toolError("EXECUTION_FAILED", "Failed to probe system environment."));
|
|
5955
|
+
}
|
|
5956
|
+
let freeDiskMb = 1e4;
|
|
5957
|
+
try {
|
|
5958
|
+
const dfOut = execSync("df -m .", { cwd: ctx.workspacePath, encoding: "utf-8", timeout: 3e3 });
|
|
5959
|
+
const parts = dfOut.trim().split("\n")[1]?.split(/\s+/);
|
|
5960
|
+
const avail = parseInt(parts?.[3] ?? "", 10);
|
|
5961
|
+
if (!isNaN(avail)) freeDiskMb = avail;
|
|
5962
|
+
} catch {
|
|
5963
|
+
}
|
|
5964
|
+
const snapshot = {
|
|
5965
|
+
freeMemoryMb: Math.round(os.freemem() / (1024 * 1024)),
|
|
5966
|
+
cpuLoadPercent: Math.round(os.loadavg()[0] / os.cpus().length * 100),
|
|
5967
|
+
freeDiskMb,
|
|
5968
|
+
activeRuns: runner.getStore().getActiveRuns().map((r) => ({ runId: r.runId, weight: r.weight }))
|
|
5969
|
+
};
|
|
5970
|
+
const taskKind = inferTaskKind(command, scriptContent);
|
|
5971
|
+
const experience = runner.getExperience().summarize(taskKind);
|
|
5972
|
+
const riskAdvice = await assessRisk({
|
|
5973
|
+
taskProfile,
|
|
5974
|
+
env,
|
|
5975
|
+
snapshot,
|
|
5976
|
+
experience,
|
|
5977
|
+
command,
|
|
5978
|
+
callLlm: ctx.callLlm
|
|
5979
|
+
});
|
|
5980
|
+
return toAgentResult("local_compute_plan", {
|
|
5981
|
+
success: true,
|
|
5982
|
+
data: {
|
|
5983
|
+
task_profile: taskProfile,
|
|
5984
|
+
risk_assessment: {
|
|
5985
|
+
feasible: riskAdvice.feasible,
|
|
5986
|
+
risks: riskAdvice.risks,
|
|
5987
|
+
warnings: riskAdvice.warnings
|
|
5988
|
+
},
|
|
5989
|
+
recommendations: {
|
|
5990
|
+
sandbox: riskAdvice.recommendedSandbox,
|
|
5991
|
+
timeout_minutes: riskAdvice.recommendedTimeoutMinutes,
|
|
5992
|
+
stall_threshold_minutes: riskAdvice.recommendedStallThresholdMinutes,
|
|
5993
|
+
agent_guidance: riskAdvice.agentGuidance
|
|
5994
|
+
},
|
|
5995
|
+
experience_summary: experience ? {
|
|
5996
|
+
task_kind: experience.taskKind,
|
|
5997
|
+
total_runs: experience.totalRuns,
|
|
5998
|
+
successes: experience.successes,
|
|
5999
|
+
failures: experience.failures,
|
|
6000
|
+
avg_duration_seconds: experience.avgDurationSeconds,
|
|
6001
|
+
common_failures: experience.commonFailures
|
|
6002
|
+
} : null
|
|
6003
|
+
}
|
|
6004
|
+
});
|
|
3832
6005
|
}
|
|
3833
6006
|
};
|
|
3834
6007
|
}
|
|
3835
|
-
|
|
3836
|
-
const DATA_ANALYSIS_SYSTEM = loadPrompt("data-analysis-system");
|
|
3837
|
-
const DATA_ANALYSIS_TASKS = loadPrompt("data-analysis-tasks");
|
|
3838
|
-
const DATA_CODE_TEMPLATE = loadPrompt("data-code-template");
|
|
3839
|
-
const TASK_DESCRIPTIONS = {
|
|
3840
|
-
analyze: "Statistical Analysis",
|
|
3841
|
-
visualize: "Data Visualization",
|
|
3842
|
-
transform: "Data Transformation",
|
|
3843
|
-
model: "Statistical Modeling"
|
|
3844
|
-
};
|
|
3845
|
-
const DataAnalyzeSchema = Type.Object({
|
|
3846
|
-
file_path: Type.String({ description: "Relative path to data file (CSV, JSON, TSV, XLSX)" }),
|
|
3847
|
-
instructions: Type.String({ description: "What analysis to perform" }),
|
|
3848
|
-
task_type: Type.Optional(
|
|
3849
|
-
Type.String({ description: "analyze | visualize | transform | model (default: analyze)" })
|
|
3850
|
-
)
|
|
3851
|
-
});
|
|
3852
|
-
function createDataAnalyzeTool(ctx) {
|
|
6008
|
+
function createExecuteTool(runner, ctx) {
|
|
3853
6009
|
return {
|
|
3854
|
-
name: "
|
|
3855
|
-
label: "
|
|
3856
|
-
description:
|
|
3857
|
-
parameters:
|
|
6010
|
+
name: "local_compute_execute",
|
|
6011
|
+
label: "Local Compute: Execute",
|
|
6012
|
+
description: 'Execute a command in a sandboxed local environment. Use this for long-running tasks like ML training, data preprocessing, or heavy analysis that may take minutes to hours.\nThe command runs asynchronously. Use local_compute_wait or local_compute_status to monitor.\nFor long-running scripts, consider adding a --smoke flag for quick validation.\n\nProgress monitoring: print lines starting with ##PROGRESS## followed by JSON for structured progress:\n ##PROGRESS## {"step": 3, "total": 10, "loss": 0.85, "phase": "training"}',
|
|
6013
|
+
parameters: Type.Object({
|
|
6014
|
+
command: Type.String({ description: 'Shell command to execute (e.g., "python3 train.py")' }),
|
|
6015
|
+
work_dir: Type.Optional(Type.String({ description: "Relative path within workspace (default: workspace root)" })),
|
|
6016
|
+
sandbox: Type.Optional(Type.String({ description: '"docker" | "process" | "auto" (default: auto)' })),
|
|
6017
|
+
timeout_minutes: Type.Optional(Type.Number({ description: "Max runtime in minutes (default: 60, max: 1440)" })),
|
|
6018
|
+
stall_threshold_minutes: Type.Optional(Type.Number({ description: "Minutes without output before flagging stall (default: 5)" })),
|
|
6019
|
+
env: Type.Optional(Type.Record(Type.String(), Type.String(), { description: "Extra environment variables" })),
|
|
6020
|
+
smoke_command: Type.Optional(Type.String({ description: 'Quick validation command (e.g., "python3 train.py --smoke"). Runs before full command.' })),
|
|
6021
|
+
parent_run_id: Type.Optional(Type.String({ description: "Previous failed run ID (for retry lineage tracking)" }))
|
|
6022
|
+
}),
|
|
3858
6023
|
execute: async (_toolCallId, rawParams) => {
|
|
3859
6024
|
const params = rawParams;
|
|
3860
|
-
const
|
|
3861
|
-
|
|
3862
|
-
|
|
3863
|
-
|
|
3864
|
-
return toAgentResult("data_analyze", toolError("MISSING_PARAMETER", "Missing file_path.", {
|
|
3865
|
-
suggestions: ["Provide a relative path to the data file (CSV, JSON, TSV, or XLSX)."]
|
|
6025
|
+
const command = typeof params.command === "string" ? params.command.trim() : "";
|
|
6026
|
+
if (!command) {
|
|
6027
|
+
return toAgentResult("local_compute_execute", toolError("MISSING_PARAMETER", "command is required.", {
|
|
6028
|
+
suggestions: ['Provide a shell command to execute (e.g., "python3 train.py").']
|
|
3866
6029
|
}));
|
|
3867
6030
|
}
|
|
3868
|
-
|
|
3869
|
-
|
|
3870
|
-
|
|
6031
|
+
const config = {
|
|
6032
|
+
command,
|
|
6033
|
+
workDir: typeof params.work_dir === "string" ? params.work_dir : void 0,
|
|
6034
|
+
sandbox: typeof params.sandbox === "string" ? params.sandbox : "auto",
|
|
6035
|
+
timeoutMinutes: typeof params.timeout_minutes === "number" ? params.timeout_minutes : 60,
|
|
6036
|
+
stallThresholdMinutes: typeof params.stall_threshold_minutes === "number" ? params.stall_threshold_minutes : 5,
|
|
6037
|
+
env: typeof params.env === "object" && params.env !== null ? params.env : void 0,
|
|
6038
|
+
smokeCommand: typeof params.smoke_command === "string" ? params.smoke_command : void 0,
|
|
6039
|
+
parentRunId: typeof params.parent_run_id === "string" ? params.parent_run_id : void 0
|
|
6040
|
+
};
|
|
6041
|
+
try {
|
|
6042
|
+
const run = await runner.submit(config);
|
|
6043
|
+
ctx.onToolCall?.("local_compute_execute", { command, runId: run.runId });
|
|
6044
|
+
return toAgentResult("local_compute_execute", {
|
|
6045
|
+
success: true,
|
|
6046
|
+
data: {
|
|
6047
|
+
run_id: run.runId,
|
|
6048
|
+
sandbox: run.sandbox,
|
|
6049
|
+
status: run.status,
|
|
6050
|
+
current_phase: run.currentPhase,
|
|
6051
|
+
output_path: run.outputPath,
|
|
6052
|
+
weight: run.weight
|
|
6053
|
+
}
|
|
6054
|
+
});
|
|
6055
|
+
} catch (err) {
|
|
6056
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
6057
|
+
if (msg.startsWith("Scheduler:")) {
|
|
6058
|
+
return toAgentResult("local_compute_execute", toolError("EXECUTION_FAILED", msg, {
|
|
6059
|
+
retryable: true,
|
|
6060
|
+
suggestions: ["Wait for the active run to finish, or stop it with local_compute_stop."]
|
|
6061
|
+
}));
|
|
6062
|
+
}
|
|
6063
|
+
return toAgentResult("local_compute_execute", toolError("EXECUTION_FAILED", msg, {
|
|
6064
|
+
retryable: false,
|
|
6065
|
+
suggestions: ["Check the command and try again."]
|
|
3871
6066
|
}));
|
|
3872
6067
|
}
|
|
3873
|
-
|
|
3874
|
-
|
|
3875
|
-
|
|
6068
|
+
}
|
|
6069
|
+
};
|
|
6070
|
+
}
|
|
6071
|
+
function createWaitTool(runner) {
|
|
6072
|
+
return {
|
|
6073
|
+
name: "local_compute_wait",
|
|
6074
|
+
label: "Local Compute: Wait",
|
|
6075
|
+
description: "Wait for a compute run to complete. Blocks until the run finishes, stalls, or the wait timeout elapses.\nReturns immediately if the run has already completed or stalled.",
|
|
6076
|
+
parameters: Type.Object({
|
|
6077
|
+
run_id: Type.String({ description: "The run ID returned by local_compute_execute" }),
|
|
6078
|
+
timeout_seconds: Type.Optional(Type.Number({ description: "Max seconds to wait (default: 120, max: 600)" }))
|
|
6079
|
+
}),
|
|
6080
|
+
execute: async (_toolCallId, rawParams) => {
|
|
6081
|
+
const params = rawParams;
|
|
6082
|
+
const runId = typeof params.run_id === "string" ? params.run_id.trim() : "";
|
|
6083
|
+
if (!runId) {
|
|
6084
|
+
return toAgentResult("local_compute_wait", toolError("MISSING_PARAMETER", "run_id is required."));
|
|
6085
|
+
}
|
|
6086
|
+
const timeoutSec = typeof params.timeout_seconds === "number" ? Math.min(params.timeout_seconds, 600) : 120;
|
|
6087
|
+
const result = await runner.waitForCompletion(runId, timeoutSec * 1e3);
|
|
6088
|
+
if (!result) {
|
|
6089
|
+
return toAgentResult("local_compute_wait", toolError("NOT_FOUND", `Run not found: ${runId}`, {
|
|
6090
|
+
suggestions: ["Check the run_id. Use local_compute_execute to start a new run."]
|
|
3876
6091
|
}));
|
|
3877
6092
|
}
|
|
3878
|
-
|
|
3879
|
-
|
|
3880
|
-
|
|
3881
|
-
|
|
3882
|
-
|
|
3883
|
-
|
|
3884
|
-
|
|
3885
|
-
|
|
3886
|
-
|
|
6093
|
+
return toAgentResult("local_compute_wait", {
|
|
6094
|
+
success: true,
|
|
6095
|
+
data: formatStatusResult(runId, result)
|
|
6096
|
+
});
|
|
6097
|
+
}
|
|
6098
|
+
};
|
|
6099
|
+
}
|
|
6100
|
+
function createStatusTool(runner) {
|
|
6101
|
+
return {
|
|
6102
|
+
name: "local_compute_status",
|
|
6103
|
+
label: "Local Compute: Status",
|
|
6104
|
+
description: "Check the current status of a compute run. Non-blocking — returns immediately.\nIncludes output tail, progress, stall detection, and failure analysis.",
|
|
6105
|
+
parameters: Type.Object({
|
|
6106
|
+
run_id: Type.String({ description: "The run ID to check" })
|
|
6107
|
+
}),
|
|
6108
|
+
execute: async (_toolCallId, rawParams) => {
|
|
6109
|
+
const params = rawParams;
|
|
6110
|
+
const runId = typeof params.run_id === "string" ? params.run_id.trim() : "";
|
|
6111
|
+
if (!runId) {
|
|
6112
|
+
return toAgentResult("local_compute_status", toolError("MISSING_PARAMETER", "run_id is required."));
|
|
3887
6113
|
}
|
|
3888
|
-
const
|
|
3889
|
-
|
|
3890
|
-
|
|
3891
|
-
const tablesDir = path$1.join(outputBase, "tables");
|
|
3892
|
-
const dataDir = path$1.join(outputBase, "data");
|
|
3893
|
-
fs.mkdirSync(figuresDir, { recursive: true });
|
|
3894
|
-
fs.mkdirSync(tablesDir, { recursive: true });
|
|
3895
|
-
fs.mkdirSync(dataDir, { recursive: true });
|
|
3896
|
-
const resultsFile = path$1.join(outputBase, "results.json");
|
|
3897
|
-
const rawPreview = fs.readFileSync(absDataFile, "utf-8").slice(0, 2e3);
|
|
3898
|
-
const ext = path$1.extname(absDataFile).toLowerCase();
|
|
3899
|
-
const formatHint = ext === ".csv" ? "CSV" : ext === ".tsv" ? "TSV" : ext === ".json" ? "JSON" : ext === ".xlsx" ? "XLSX" : "unknown";
|
|
3900
|
-
if (!ctx.callLlm) {
|
|
3901
|
-
return toAgentResult("data_analyze", toolError("LLM_UNAVAILABLE", "LLM not available for code generation.", {
|
|
3902
|
-
suggestions: ["Ensure the agent runtime has an LLM provider configured (callLlm in ResearchToolContext)."]
|
|
3903
|
-
}));
|
|
6114
|
+
const result = runner.getStatus(runId);
|
|
6115
|
+
if (!result) {
|
|
6116
|
+
return toAgentResult("local_compute_status", toolError("NOT_FOUND", `Run not found: ${runId}`));
|
|
3904
6117
|
}
|
|
3905
|
-
|
|
3906
|
-
|
|
3907
|
-
|
|
3908
|
-
|
|
3909
|
-
|
|
3910
|
-
|
|
3911
|
-
|
|
3912
|
-
|
|
3913
|
-
|
|
3914
|
-
|
|
3915
|
-
|
|
3916
|
-
|
|
3917
|
-
|
|
3918
|
-
|
|
3919
|
-
|
|
3920
|
-
|
|
3921
|
-
|
|
3922
|
-
|
|
3923
|
-
|
|
3924
|
-
|
|
3925
|
-
try {
|
|
3926
|
-
const llmResponse = await ctx.callLlm(DATA_ANALYSIS_SYSTEM, userPrompt);
|
|
3927
|
-
const codeMatch = llmResponse.match(/```python\n([\s\S]*?)```/) || llmResponse.match(/```\n([\s\S]*?)```/);
|
|
3928
|
-
generatedCode = codeMatch ? codeMatch[1] : llmResponse;
|
|
3929
|
-
} catch (err) {
|
|
3930
|
-
return toAgentResult("data_analyze", toolError("EXECUTION_FAILED", `Code generation failed: ${err.message}`, {
|
|
3931
|
-
retryable: true,
|
|
3932
|
-
suggestions: ["Retry — the LLM may produce valid code on a subsequent attempt.", "Try simplifying the instructions."]
|
|
3933
|
-
}));
|
|
6118
|
+
return toAgentResult("local_compute_status", {
|
|
6119
|
+
success: true,
|
|
6120
|
+
data: formatStatusResult(runId, result)
|
|
6121
|
+
});
|
|
6122
|
+
}
|
|
6123
|
+
};
|
|
6124
|
+
}
|
|
6125
|
+
function createStopTool(runner) {
|
|
6126
|
+
return {
|
|
6127
|
+
name: "local_compute_stop",
|
|
6128
|
+
label: "Local Compute: Stop",
|
|
6129
|
+
description: "Stop (cancel) a running compute task. The process is killed.",
|
|
6130
|
+
parameters: Type.Object({
|
|
6131
|
+
run_id: Type.String({ description: "The run ID to stop" })
|
|
6132
|
+
}),
|
|
6133
|
+
execute: async (_toolCallId, rawParams) => {
|
|
6134
|
+
const params = rawParams;
|
|
6135
|
+
const runId = typeof params.run_id === "string" ? params.run_id.trim() : "";
|
|
6136
|
+
if (!runId) {
|
|
6137
|
+
return toAgentResult("local_compute_stop", toolError("MISSING_PARAMETER", "run_id is required."));
|
|
3934
6138
|
}
|
|
3935
|
-
const pathDefinitions = [
|
|
3936
|
-
"",
|
|
3937
|
-
"# Pre-defined paths (set by the tool runtime)",
|
|
3938
|
-
`DATA_FILE = ${JSON.stringify(absDataFile)}`,
|
|
3939
|
-
`FIGURES_DIR = ${JSON.stringify(figuresDir)}`,
|
|
3940
|
-
`TABLES_DIR = ${JSON.stringify(tablesDir)}`,
|
|
3941
|
-
`DATA_DIR = ${JSON.stringify(dataDir)}`,
|
|
3942
|
-
`RESULTS_FILE = ${JSON.stringify(resultsFile)}`,
|
|
3943
|
-
""
|
|
3944
|
-
].join("\n");
|
|
3945
|
-
const fullScript = DATA_CODE_TEMPLATE + pathDefinitions + "\n" + generatedCode;
|
|
3946
|
-
const scriptPath = path$1.join(outputBase, "script.py");
|
|
3947
|
-
fs.writeFileSync(scriptPath, fullScript, "utf-8");
|
|
3948
6139
|
try {
|
|
3949
|
-
|
|
3950
|
-
|
|
3951
|
-
|
|
3952
|
-
|
|
3953
|
-
maxBuffer: 10 * 1024 * 1024
|
|
6140
|
+
await runner.stop(runId);
|
|
6141
|
+
return toAgentResult("local_compute_stop", {
|
|
6142
|
+
success: true,
|
|
6143
|
+
data: { run_id: runId, status: "cancelled" }
|
|
3954
6144
|
});
|
|
3955
|
-
let manifest = null;
|
|
3956
|
-
if (fs.existsSync(resultsFile)) {
|
|
3957
|
-
try {
|
|
3958
|
-
manifest = JSON.parse(fs.readFileSync(resultsFile, "utf-8"));
|
|
3959
|
-
} catch {
|
|
3960
|
-
}
|
|
3961
|
-
}
|
|
3962
|
-
const outputs = [];
|
|
3963
|
-
for (const [dir, type] of [
|
|
3964
|
-
[figuresDir, "figure"],
|
|
3965
|
-
[tablesDir, "table"],
|
|
3966
|
-
[dataDir, "data"]
|
|
3967
|
-
]) {
|
|
3968
|
-
if (fs.existsSync(dir)) {
|
|
3969
|
-
for (const f of fs.readdirSync(dir)) {
|
|
3970
|
-
outputs.push({
|
|
3971
|
-
name: f,
|
|
3972
|
-
type,
|
|
3973
|
-
path: path$1.relative(ctx.workspacePath, path$1.join(dir, f))
|
|
3974
|
-
});
|
|
3975
|
-
}
|
|
3976
|
-
}
|
|
3977
|
-
}
|
|
3978
|
-
const payload = {
|
|
3979
|
-
stdout: stdout.slice(0, 4e3),
|
|
3980
|
-
stderr: stderr ? stderr.slice(0, 1e3) : void 0,
|
|
3981
|
-
outputs,
|
|
3982
|
-
manifest: manifest ? {
|
|
3983
|
-
summary: manifest.summary,
|
|
3984
|
-
warnings: manifest.warnings
|
|
3985
|
-
} : void 0,
|
|
3986
|
-
scriptPath: path$1.relative(ctx.workspacePath, scriptPath),
|
|
3987
|
-
runId
|
|
3988
|
-
};
|
|
3989
|
-
return toAgentResult("data_analyze", { success: true, data: payload });
|
|
3990
6145
|
} catch (err) {
|
|
3991
|
-
|
|
3992
|
-
|
|
3993
|
-
|
|
3994
|
-
|
|
3995
|
-
`Review the generated script at ${path$1.relative(ctx.workspacePath, scriptPath)} for errors.`,
|
|
3996
|
-
"Check that required Python packages (pandas, matplotlib, seaborn, etc.) are installed.",
|
|
3997
|
-
"Try simplifying the analysis instructions."
|
|
3998
|
-
],
|
|
3999
|
-
context: {
|
|
4000
|
-
scriptPath: path$1.relative(ctx.workspacePath, scriptPath),
|
|
4001
|
-
runId
|
|
4002
|
-
},
|
|
4003
|
-
data: {
|
|
4004
|
-
scriptPath: path$1.relative(ctx.workspacePath, scriptPath),
|
|
4005
|
-
runId
|
|
4006
|
-
}
|
|
4007
|
-
}));
|
|
6146
|
+
return toAgentResult("local_compute_stop", toolError(
|
|
6147
|
+
"EXECUTION_FAILED",
|
|
6148
|
+
err instanceof Error ? err.message : String(err)
|
|
6149
|
+
));
|
|
4008
6150
|
}
|
|
4009
6151
|
}
|
|
4010
6152
|
};
|
|
4011
6153
|
}
|
|
6154
|
+
function formatStatusResult(runId, result) {
|
|
6155
|
+
const data = {
|
|
6156
|
+
run_id: runId,
|
|
6157
|
+
status: result.status,
|
|
6158
|
+
current_phase: result.currentPhase,
|
|
6159
|
+
elapsed_seconds: result.elapsedSeconds,
|
|
6160
|
+
output_bytes: result.outputBytes,
|
|
6161
|
+
output_lines: result.outputLines,
|
|
6162
|
+
stalled: result.stalled
|
|
6163
|
+
};
|
|
6164
|
+
if (result.exitCode !== void 0) data.exit_code = result.exitCode;
|
|
6165
|
+
if (result.outputTail) data.output_tail = result.outputTail;
|
|
6166
|
+
if (result.progress) data.progress = result.progress;
|
|
6167
|
+
if (result.failure) data.failure = result.failure;
|
|
6168
|
+
return data;
|
|
6169
|
+
}
|
|
4012
6170
|
function wrapResearchTool(tool) {
|
|
4013
6171
|
const properties = {};
|
|
4014
6172
|
const jsonProps = tool.parameters.properties ?? {};
|
|
@@ -4049,6 +6207,7 @@ function wrapResearchTool(tool) {
|
|
|
4049
6207
|
}
|
|
4050
6208
|
function createResearchTools(ctx) {
|
|
4051
6209
|
const tools = [];
|
|
6210
|
+
const destroyers = [];
|
|
4052
6211
|
tools.push(createWebSearchTool());
|
|
4053
6212
|
tools.push(createWebFetchTool(ctx));
|
|
4054
6213
|
tools.push(createLiteratureSearchTool(ctx));
|
|
@@ -4065,7 +6224,20 @@ function createResearchTools(ctx) {
|
|
|
4065
6224
|
for (const tool of structuredMemoryTools) {
|
|
4066
6225
|
tools.push(wrapResearchTool(tool));
|
|
4067
6226
|
}
|
|
4068
|
-
|
|
6227
|
+
if (process.env.ENABLE_LOCAL_COMPUTE === "1") {
|
|
6228
|
+
const compute = createLocalComputeTools(ctx);
|
|
6229
|
+
tools.push(...compute.tools);
|
|
6230
|
+
destroyers.push(compute.destroy);
|
|
6231
|
+
}
|
|
6232
|
+
return {
|
|
6233
|
+
tools,
|
|
6234
|
+
destroy: async () => {
|
|
6235
|
+
for (const d of destroyers) {
|
|
6236
|
+
await d().catch(() => {
|
|
6237
|
+
});
|
|
6238
|
+
}
|
|
6239
|
+
}
|
|
6240
|
+
};
|
|
4069
6241
|
}
|
|
4070
6242
|
const VALID_TYPES = ["user", "feedback", "project", "reference"];
|
|
4071
6243
|
const EXTRACTION_PROMPT = `Analyze the recent conversation above and extract information worth remembering across sessions.
|
|
@@ -4611,6 +6783,7 @@ function writeExplainSnapshot(projectPath, snapshot) {
|
|
|
4611
6783
|
async function createCoordinator(config) {
|
|
4612
6784
|
const {
|
|
4613
6785
|
apiKey,
|
|
6786
|
+
getApiKeyOverride,
|
|
4614
6787
|
model: modelId,
|
|
4615
6788
|
projectPath = process.cwd(),
|
|
4616
6789
|
debug = false,
|
|
@@ -4623,6 +6796,7 @@ async function createCoordinator(config) {
|
|
|
4623
6796
|
onUsage,
|
|
4624
6797
|
onSkillLoaded
|
|
4625
6798
|
} = config;
|
|
6799
|
+
const resolveApiKey = getApiKeyOverride ?? (async () => apiKey);
|
|
4626
6800
|
let turnCount = 0;
|
|
4627
6801
|
let activeTurnToolCallCount = null;
|
|
4628
6802
|
const turnHistory = [];
|
|
@@ -4667,13 +6841,15 @@ async function createCoordinator(config) {
|
|
|
4667
6841
|
const routerByProvider = {
|
|
4668
6842
|
anthropic: "claude-haiku-4-5-20251001",
|
|
4669
6843
|
openai: "gpt-5.4-nano",
|
|
6844
|
+
"openai-codex": "gpt-5.4-mini",
|
|
6845
|
+
// nano not available in openai-codex provider
|
|
4670
6846
|
google: "gemini-2.0-flash-lite"
|
|
4671
6847
|
};
|
|
4672
6848
|
let mainProvider = null;
|
|
4673
6849
|
if (modelId) {
|
|
4674
6850
|
const parts = modelId.split(":");
|
|
4675
6851
|
if (parts.length === 2) {
|
|
4676
|
-
mainProvider = parts[0];
|
|
6852
|
+
mainProvider = parts[0] === "openai-codex" ? "openai-codex" : parts[0];
|
|
4677
6853
|
} else {
|
|
4678
6854
|
mainProvider = modelId.startsWith("claude-") ? "anthropic" : modelId.startsWith("gpt-") || modelId.startsWith("o3") || modelId.startsWith("o4") ? "openai" : modelId.startsWith("gemini-") ? "google" : null;
|
|
4679
6855
|
}
|
|
@@ -4703,17 +6879,18 @@ async function createCoordinator(config) {
|
|
|
4703
6879
|
projectPath,
|
|
4704
6880
|
callLlm: async (systemPrompt, userContent) => {
|
|
4705
6881
|
if (!piModel) throw new Error("No model available for sub-call");
|
|
6882
|
+
const currentKey = await resolveApiKey();
|
|
4706
6883
|
const result = await completeSimple(piModel, {
|
|
4707
6884
|
systemPrompt,
|
|
4708
6885
|
messages: [{ role: "user", content: userContent, timestamp: Date.now() }]
|
|
4709
|
-
}, { maxTokens: 4096, apiKey });
|
|
6886
|
+
}, { maxTokens: 4096, apiKey: currentKey });
|
|
4710
6887
|
const textContent = result.content.find((c) => c.type === "text");
|
|
4711
6888
|
return textContent?.text ?? "";
|
|
4712
6889
|
},
|
|
4713
6890
|
onToolCall,
|
|
4714
6891
|
onToolResult: wrappedOnToolResult
|
|
4715
6892
|
};
|
|
4716
|
-
const researchAgentTools = createResearchTools(toolCtx);
|
|
6893
|
+
const { tools: researchAgentTools, destroy: destroyResearchTools } = createResearchTools(toolCtx);
|
|
4717
6894
|
const codingTools = createCodingTools(projectPath);
|
|
4718
6895
|
const grepTool = createGrepTool(projectPath);
|
|
4719
6896
|
const findTool = createFindTool(projectPath);
|
|
@@ -4736,17 +6913,17 @@ async function createCoordinator(config) {
|
|
|
4736
6913
|
loadSkillTool
|
|
4737
6914
|
];
|
|
4738
6915
|
const skillsCatalog = buildSkillsCatalogPrompt(skills);
|
|
4739
|
-
const
|
|
6916
|
+
const baseSystemPrompt = SYSTEM_PROMPT + (skillsCatalog ? "\n\n" + skillsCatalog : "");
|
|
4740
6917
|
let compactionSummary;
|
|
4741
6918
|
const agent = new Agent({
|
|
4742
6919
|
initialState: {
|
|
4743
|
-
systemPrompt:
|
|
6920
|
+
systemPrompt: baseSystemPrompt,
|
|
4744
6921
|
model: piModel ?? void 0,
|
|
4745
6922
|
tools: allTools,
|
|
4746
6923
|
thinkingLevel: reasoningEffort === "high" ? "high" : reasoningEffort === "medium" ? "medium" : "low"
|
|
4747
6924
|
},
|
|
4748
6925
|
sessionId,
|
|
4749
|
-
getApiKey:
|
|
6926
|
+
getApiKey: resolveApiKey,
|
|
4750
6927
|
// ── Context compaction via transformContext ──
|
|
4751
6928
|
// Before each LLM call, check if accumulated messages exceed the model's
|
|
4752
6929
|
// context window. If so, summarize old messages and keep only recent ones.
|
|
@@ -4785,11 +6962,12 @@ async function createCoordinator(config) {
|
|
|
4785
6962
|
const messagesToSummarize = messages.slice(0, cutIndex);
|
|
4786
6963
|
const messagesToKeep = messages.slice(cutIndex);
|
|
4787
6964
|
try {
|
|
6965
|
+
const currentKey = await resolveApiKey();
|
|
4788
6966
|
const summary = await generateSummary(
|
|
4789
6967
|
messagesToSummarize,
|
|
4790
6968
|
piModel,
|
|
4791
6969
|
settings.reserveTokens,
|
|
4792
|
-
|
|
6970
|
+
currentKey,
|
|
4793
6971
|
signal,
|
|
4794
6972
|
void 0,
|
|
4795
6973
|
compactionSummary
|
|
@@ -4878,6 +7056,7 @@ The conversation continues below.`,
|
|
|
4878
7056
|
const historyText = turnHistory.map((t, i) => `Turn ${turnCount - turnHistory.length + i + 1}: User: ${t.userMessage}
|
|
4879
7057
|
Assistant: ${t.response}`).join("\n\n");
|
|
4880
7058
|
try {
|
|
7059
|
+
const currentKey = await resolveApiKey();
|
|
4881
7060
|
const result = await completeSimple(intentRouterModel, {
|
|
4882
7061
|
systemPrompt: "You summarize research conversations concisely. Output JSON with keys: summary (string), topicsDiscussed (string[]), openQuestions (string[]). Output ONLY valid JSON.",
|
|
4883
7062
|
messages: [{
|
|
@@ -4889,7 +7068,7 @@ ${historyText}`,
|
|
|
4889
7068
|
}]
|
|
4890
7069
|
}, {
|
|
4891
7070
|
maxTokens: 512,
|
|
4892
|
-
apiKey
|
|
7071
|
+
apiKey: currentKey
|
|
4893
7072
|
});
|
|
4894
7073
|
const textContent = result.content.find((c) => c.type === "text");
|
|
4895
7074
|
const text = textContent?.text?.trim() ?? "";
|
|
@@ -4915,12 +7094,20 @@ ${historyText}`,
|
|
|
4915
7094
|
}
|
|
4916
7095
|
}
|
|
4917
7096
|
}
|
|
7097
|
+
if (process.env.ENABLE_LOCAL_COMPUTE === "1") {
|
|
7098
|
+
probeStaticProfile().then((profile) => {
|
|
7099
|
+
const envGuidance = generateAgentGuidance(profile);
|
|
7100
|
+
agent.setSystemPrompt(baseSystemPrompt + "\n\n" + envGuidance);
|
|
7101
|
+
}).catch(() => {
|
|
7102
|
+
});
|
|
7103
|
+
}
|
|
4918
7104
|
return {
|
|
4919
7105
|
agent,
|
|
4920
7106
|
async chat(message, mentions, images) {
|
|
4921
7107
|
try {
|
|
4922
7108
|
const intents = detectIntentsByRules(message);
|
|
4923
|
-
const
|
|
7109
|
+
const currentKey = await resolveApiKey();
|
|
7110
|
+
const matchedSkillNames = await matchSkillsWithLLM(intentRouterModel, currentKey, message, skills);
|
|
4924
7111
|
const matchedSkills = matchedSkillNames.map((name) => skills.find((s) => s.name === name)).filter((s) => s !== void 0);
|
|
4925
7112
|
for (const s of matchedSkills) {
|
|
4926
7113
|
onSkillLoaded?.(s.name);
|
|
@@ -4961,7 +7148,7 @@ ${historyText}`,
|
|
|
4961
7148
|
console.log(`[Chat] Matched skills: ${skillList}`);
|
|
4962
7149
|
console.log(`[Chat] Sending message to agent (${mentions?.filter((m) => !m.error).length ?? 0} mentions, summary=${!!latestSummary})...`);
|
|
4963
7150
|
}
|
|
4964
|
-
let enrichedSystem =
|
|
7151
|
+
let enrichedSystem = baseSystemPrompt;
|
|
4965
7152
|
if (agentMdContent) {
|
|
4966
7153
|
enrichedSystem = `${enrichedSystem}
|
|
4967
7154
|
|
|
@@ -5024,8 +7211,9 @@ ${message}` : message;
|
|
|
5024
7211
|
});
|
|
5025
7212
|
if (turnHistory.length > 8) turnHistory.shift();
|
|
5026
7213
|
void maybeGenerateSummary();
|
|
7214
|
+
const memoryKey = await resolveApiKey();
|
|
5027
7215
|
void maybeExtractMemories(
|
|
5028
|
-
{ projectPath, model: piModel, apiKey, systemPrompt: enrichedSystem, debug },
|
|
7216
|
+
{ projectPath, model: piModel, apiKey: memoryKey, systemPrompt: enrichedSystem, debug },
|
|
5029
7217
|
agent.state.messages,
|
|
5030
7218
|
turnCount
|
|
5031
7219
|
);
|
|
@@ -5048,6 +7236,7 @@ ${message}` : message;
|
|
|
5048
7236
|
clearSessionMemory,
|
|
5049
7237
|
async destroy() {
|
|
5050
7238
|
agent.abort();
|
|
7239
|
+
await destroyResearchTools();
|
|
5051
7240
|
}
|
|
5052
7241
|
};
|
|
5053
7242
|
}
|
|
@@ -6717,7 +8906,7 @@ let ipcHandlersRegistered = false;
|
|
|
6717
8906
|
function createWindowRuntimeState() {
|
|
6718
8907
|
return {
|
|
6719
8908
|
coordinator: null,
|
|
6720
|
-
currentModel: "gpt-5.4",
|
|
8909
|
+
currentModel: "openai:gpt-5.4",
|
|
6721
8910
|
currentReasoningEffort: "medium",
|
|
6722
8911
|
currentAuthMode: "none",
|
|
6723
8912
|
projectPath: "",
|
|
@@ -6848,11 +9037,27 @@ async function ensureCoordinator(state, win, model, options) {
|
|
|
6848
9037
|
if (!state.coordinator) {
|
|
6849
9038
|
const apiKey = resolvedAuth.apiKey;
|
|
6850
9039
|
const runProjectPath = state.projectPath;
|
|
9040
|
+
const getApiKeyOverride = resolvedAuth.authMode === "subscription" ? async () => {
|
|
9041
|
+
const creds = loadCodexCredentials();
|
|
9042
|
+
if (!creds) throw new Error("ChatGPT subscription credentials not found. Please sign in again.");
|
|
9043
|
+
if (creds.expires < Date.now() + 6e4) {
|
|
9044
|
+
try {
|
|
9045
|
+
const { refreshOpenAICodexToken } = await import("@mariozechner/pi-ai/oauth");
|
|
9046
|
+
const newCreds = await refreshOpenAICodexToken(creds);
|
|
9047
|
+
saveCodexCredentials(newCreds);
|
|
9048
|
+
return newCreds.access;
|
|
9049
|
+
} catch {
|
|
9050
|
+
return creds.access;
|
|
9051
|
+
}
|
|
9052
|
+
}
|
|
9053
|
+
return creds.access;
|
|
9054
|
+
} : void 0;
|
|
6851
9055
|
const initEvent = { type: "system", summary: "Initializing agent (first run may take 1-2 minutes for document processing setup)..." };
|
|
6852
9056
|
state.realtimeBuffer.pushActivity(initEvent);
|
|
6853
9057
|
safeSend(win, "agent:activity", initEvent);
|
|
6854
9058
|
state.coordinator = await createCoordinator({
|
|
6855
9059
|
apiKey,
|
|
9060
|
+
getApiKeyOverride,
|
|
6856
9061
|
model: state.currentModel,
|
|
6857
9062
|
reasoningEffort: state.currentReasoningEffort,
|
|
6858
9063
|
projectPath: state.projectPath,
|
|
@@ -6925,6 +9130,49 @@ async function ensureCoordinator(state, win, model, options) {
|
|
|
6925
9130
|
}
|
|
6926
9131
|
}
|
|
6927
9132
|
}
|
|
9133
|
+
if (tool === "local_compute_execute" && result && typeof result === "object" && "success" in result) {
|
|
9134
|
+
const cr = result;
|
|
9135
|
+
if (cr.success && cr.data) {
|
|
9136
|
+
safeSend(win, "compute:run-update", {
|
|
9137
|
+
runId: cr.data.run_id,
|
|
9138
|
+
status: cr.data.status,
|
|
9139
|
+
currentPhase: cr.data.current_phase,
|
|
9140
|
+
command: args?.command ?? "",
|
|
9141
|
+
sandbox: cr.data.sandbox,
|
|
9142
|
+
weight: cr.data.weight,
|
|
9143
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
9144
|
+
});
|
|
9145
|
+
}
|
|
9146
|
+
}
|
|
9147
|
+
if ((tool === "local_compute_status" || tool === "local_compute_wait") && result && typeof result === "object" && "success" in result) {
|
|
9148
|
+
const cr = result;
|
|
9149
|
+
if (cr.success && cr.data?.run_id) {
|
|
9150
|
+
const isComplete = ["completed", "failed", "timed_out", "cancelled"].includes(cr.data.status);
|
|
9151
|
+
const channel = isComplete ? "compute:run-complete" : "compute:run-update";
|
|
9152
|
+
safeSend(win, channel, {
|
|
9153
|
+
runId: cr.data.run_id,
|
|
9154
|
+
status: cr.data.status,
|
|
9155
|
+
currentPhase: cr.data.current_phase,
|
|
9156
|
+
exitCode: cr.data.exit_code,
|
|
9157
|
+
elapsedSeconds: cr.data.elapsed_seconds,
|
|
9158
|
+
outputBytes: cr.data.output_bytes,
|
|
9159
|
+
outputLines: cr.data.output_lines,
|
|
9160
|
+
stalled: cr.data.stalled,
|
|
9161
|
+
progress: cr.data.progress,
|
|
9162
|
+
outputTail: cr.data.output_tail?.slice(-2048),
|
|
9163
|
+
failure: cr.data.failure
|
|
9164
|
+
});
|
|
9165
|
+
}
|
|
9166
|
+
}
|
|
9167
|
+
if (tool === "local_compute_stop" && result && typeof result === "object" && "success" in result) {
|
|
9168
|
+
const cr = result;
|
|
9169
|
+
if (cr.success && cr.data?.run_id) {
|
|
9170
|
+
safeSend(win, "compute:run-complete", {
|
|
9171
|
+
runId: cr.data.run_id,
|
|
9172
|
+
status: "cancelled"
|
|
9173
|
+
});
|
|
9174
|
+
}
|
|
9175
|
+
}
|
|
6928
9176
|
const r = result;
|
|
6929
9177
|
const success2 = r?.success !== false;
|
|
6930
9178
|
const error = !success2 ? r?.error || "Unknown error" : void 0;
|
|
@@ -6980,9 +9228,36 @@ async function ensureCoordinator(state, win, model, options) {
|
|
|
6980
9228
|
const readyEvent = { type: "system", summary: "Agent ready" };
|
|
6981
9229
|
state.realtimeBuffer.pushActivity(readyEvent);
|
|
6982
9230
|
safeSend(win, "agent:activity", readyEvent);
|
|
9231
|
+
probeStaticProfile().then((profile) => {
|
|
9232
|
+
safeSend(win, "compute:environment", {
|
|
9233
|
+
os: profile.os,
|
|
9234
|
+
arch: profile.arch,
|
|
9235
|
+
cpuCores: profile.cpuCores,
|
|
9236
|
+
totalMemoryMb: profile.totalMemoryMb,
|
|
9237
|
+
gpu: profile.gpu.model,
|
|
9238
|
+
mlxAvailable: profile.gpu.mlxAvailable,
|
|
9239
|
+
sandbox: profile.dockerAvailable ? "docker" : "process"
|
|
9240
|
+
});
|
|
9241
|
+
}).catch(() => {
|
|
9242
|
+
});
|
|
6983
9243
|
}
|
|
6984
9244
|
return state.coordinator;
|
|
6985
9245
|
}
|
|
9246
|
+
async function destroyAllCoordinators() {
|
|
9247
|
+
const promises = [];
|
|
9248
|
+
for (const [, state] of windowStates) {
|
|
9249
|
+
if (state.coordinator) {
|
|
9250
|
+
promises.push(
|
|
9251
|
+
state.coordinator.destroy().catch(() => {
|
|
9252
|
+
})
|
|
9253
|
+
);
|
|
9254
|
+
}
|
|
9255
|
+
}
|
|
9256
|
+
await Promise.race([
|
|
9257
|
+
Promise.all(promises),
|
|
9258
|
+
new Promise((resolve2) => setTimeout(resolve2, 8e3))
|
|
9259
|
+
]);
|
|
9260
|
+
}
|
|
6986
9261
|
function registerIpcHandlers() {
|
|
6987
9262
|
if (ipcHandlersRegistered) return;
|
|
6988
9263
|
ipcHandlersRegistered = true;
|
|
@@ -7449,6 +9724,24 @@ function registerIpcHandlers() {
|
|
|
7449
9724
|
}
|
|
7450
9725
|
});
|
|
7451
9726
|
handleWindow("session:current", ({ state }) => ({ sessionId: state.sessionId, projectPath: state.projectPath }));
|
|
9727
|
+
handleWindow("compute:probe-environment", async ({ win }) => {
|
|
9728
|
+
try {
|
|
9729
|
+
const profile = await probeStaticProfile();
|
|
9730
|
+
const env = {
|
|
9731
|
+
os: profile.os,
|
|
9732
|
+
arch: profile.arch,
|
|
9733
|
+
cpuCores: profile.cpuCores,
|
|
9734
|
+
totalMemoryMb: profile.totalMemoryMb,
|
|
9735
|
+
gpu: profile.gpu.model,
|
|
9736
|
+
mlxAvailable: profile.gpu.mlxAvailable,
|
|
9737
|
+
sandbox: profile.dockerAvailable ? "docker" : "process"
|
|
9738
|
+
};
|
|
9739
|
+
safeSend(win, "compute:environment", env);
|
|
9740
|
+
return env;
|
|
9741
|
+
} catch {
|
|
9742
|
+
return null;
|
|
9743
|
+
}
|
|
9744
|
+
});
|
|
7452
9745
|
handleWindow("project:pick-folder", async ({ win, state }) => {
|
|
7453
9746
|
const result = await dialog.showOpenDialog(win, {
|
|
7454
9747
|
properties: ["openDirectory", "createDirectory"]
|
|
@@ -7473,11 +9766,33 @@ function registerIpcHandlers() {
|
|
|
7473
9766
|
if (existsSync(prefsFile)) {
|
|
7474
9767
|
try {
|
|
7475
9768
|
const prefs = JSON.parse(readFileSync(prefsFile, "utf-8"));
|
|
7476
|
-
if (prefs.selectedModel)
|
|
9769
|
+
if (prefs.selectedModel) {
|
|
9770
|
+
const m = prefs.selectedModel;
|
|
9771
|
+
if (!m.includes(":")) {
|
|
9772
|
+
const provider = m.startsWith("claude-") ? "anthropic" : m.startsWith("gemini-") ? "google" : "openai";
|
|
9773
|
+
state.currentModel = `${provider}:${m}`;
|
|
9774
|
+
} else {
|
|
9775
|
+
state.currentModel = m;
|
|
9776
|
+
}
|
|
9777
|
+
}
|
|
7477
9778
|
if (prefs.reasoningEffort) state.currentReasoningEffort = prefs.reasoningEffort;
|
|
7478
9779
|
} catch {
|
|
7479
9780
|
}
|
|
7480
9781
|
}
|
|
9782
|
+
if (process.env.ENABLE_LOCAL_COMPUTE === "1") {
|
|
9783
|
+
probeStaticProfile().then((profile) => {
|
|
9784
|
+
safeSend(win, "compute:environment", {
|
|
9785
|
+
os: profile.os,
|
|
9786
|
+
arch: profile.arch,
|
|
9787
|
+
cpuCores: profile.cpuCores,
|
|
9788
|
+
totalMemoryMb: profile.totalMemoryMb,
|
|
9789
|
+
gpu: profile.gpu.model,
|
|
9790
|
+
mlxAvailable: profile.gpu.mlxAvailable,
|
|
9791
|
+
sandbox: profile.dockerAvailable ? "docker" : "process"
|
|
9792
|
+
});
|
|
9793
|
+
}).catch(() => {
|
|
9794
|
+
});
|
|
9795
|
+
}
|
|
7481
9796
|
return { projectPath: state.projectPath, sessionId: state.sessionId };
|
|
7482
9797
|
}
|
|
7483
9798
|
return null;
|
|
@@ -7503,7 +9818,7 @@ function registerIpcHandlers() {
|
|
|
7503
9818
|
state.realtimeBuffer.reset();
|
|
7504
9819
|
state.projectPath = "";
|
|
7505
9820
|
state.sessionId = crypto.randomUUID();
|
|
7506
|
-
state.currentModel = "gpt-5.4";
|
|
9821
|
+
state.currentModel = "openai:gpt-5.4";
|
|
7507
9822
|
state.currentReasoningEffort = "medium";
|
|
7508
9823
|
state.currentAuthMode = "none";
|
|
7509
9824
|
} finally {
|
|
@@ -7751,6 +10066,10 @@ app.on("window-all-closed", () => {
|
|
|
7751
10066
|
destroyAllTerminals();
|
|
7752
10067
|
if (process.platform !== "darwin") app.quit();
|
|
7753
10068
|
});
|
|
7754
|
-
app.on("before-quit", () => {
|
|
10069
|
+
app.on("before-quit", (event) => {
|
|
7755
10070
|
destroyAllTerminals();
|
|
10071
|
+
event.preventDefault();
|
|
10072
|
+
destroyAllCoordinators().finally(() => {
|
|
10073
|
+
app.exit(0);
|
|
10074
|
+
});
|
|
7756
10075
|
});
|