research-copilot 0.2.0 → 0.2.2
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 +3222 -412
- package/app/out/preload/index.js +26 -0
- package/app/out/renderer/assets/{MilkdownMarkdownEditor-Czh2N6UQ.js → MilkdownMarkdownEditor-jaF-aGPn.js} +50 -50
- package/app/out/renderer/assets/{arc-BWoErJNa.js → arc-C1kBmvvR.js} +1 -1
- package/app/out/renderer/assets/{blockDiagram-c4efeb88-Bod-vAlS.js → blockDiagram-c4efeb88-Do93X2rs.js} +8 -8
- package/app/out/renderer/assets/{c4Diagram-c83219d4-CTVUA_li.js → c4Diagram-c83219d4-DgxxcZWC.js} +3 -3
- package/app/out/renderer/assets/{channel-CxGr5Q5E.js → channel-Co_M0Svj.js} +1 -1
- package/app/out/renderer/assets/{classDiagram-beda092f-DABwUrsU.js → classDiagram-beda092f-CQlHgE6H.js} +6 -6
- package/app/out/renderer/assets/{classDiagram-v2-2358418a-CFt8hqf5.js → classDiagram-v2-2358418a-CkGG3aI2.js} +10 -10
- package/app/out/renderer/assets/{clone-BL91dKYn.js → clone-C18Y6dgC.js} +1 -1
- package/app/out/renderer/assets/{createText-1719965b-DGkv4rEO.js → createText-1719965b-DGRc6nys.js} +2 -2
- package/app/out/renderer/assets/{edges-96097737-Gf41lQOd.js → edges-96097737-BXvJ4fAK.js} +3 -3
- package/app/out/renderer/assets/{erDiagram-0228fc6a-Dj75BiRy.js → erDiagram-0228fc6a-CXjPp0pt.js} +5 -5
- package/app/out/renderer/assets/{flowDb-c6c81e3f-C_xVBMxS.js → flowDb-c6c81e3f-CNhpbtw_.js} +1 -1
- package/app/out/renderer/assets/{flowDiagram-50d868cf-B-lLn2XC.js → flowDiagram-50d868cf-KZ_BUCPA.js} +12 -12
- package/app/out/renderer/assets/{flowDiagram-v2-4f6560a1-BFnLU3PE.js → flowDiagram-v2-4f6560a1-IMv50KZP.js} +12 -12
- package/app/out/renderer/assets/{flowchart-elk-definition-6af322e1-DmjfyXbt.js → flowchart-elk-definition-6af322e1-BFwFiPvq.js} +6 -6
- package/app/out/renderer/assets/{ganttDiagram-a2739b55-BTPRekAy.js → ganttDiagram-a2739b55-D0-ehN-T.js} +3 -3
- package/app/out/renderer/assets/{gitGraphDiagram-82fe8481-1riYxgGS.js → gitGraphDiagram-82fe8481-DUyIR0Dv.js} +2 -2
- package/app/out/renderer/assets/{graph-CvDtMlX-.js → graph-DnTq2_3F.js} +1 -1
- package/app/out/renderer/assets/{index-5325376f-BGaoNMNN.js → index-5325376f-CBwuFbRF.js} +6 -6
- package/app/out/renderer/assets/{index-8tvmsRje.js → index-7hDGClrI.js} +3 -3
- package/app/out/renderer/assets/{index-CUPy7R5v.js → index-BB-a1ajC.js} +2122 -471
- package/app/out/renderer/assets/{index-Bii7x9Rr.js → index-BHcU72Rm.js} +3 -3
- package/app/out/renderer/assets/{index-qS7qbXvX.js → index-BQ7qz1CD.js} +3 -3
- package/app/out/renderer/assets/{index-DrvR7Peq.js → index-BVYoMX5H.js} +3 -3
- package/app/out/renderer/assets/{index-CXN1f9OT.js → index-BpKrXGYD.js} +3 -3
- package/app/out/renderer/assets/{index-0kPJXDfu.js → index-C1oXjI4L.js} +3 -3
- package/app/out/renderer/assets/{index-BK5rYWMs.js → index-CKXwBmK7.js} +5 -5
- package/app/out/renderer/assets/{index-B9lieynj.js → index-COZSDrEw.js} +6 -6
- package/app/out/renderer/assets/{index-Ctwkk-AW.css → index-CT1HtzVp.css} +165 -14
- package/app/out/renderer/assets/{index-zr8uxb8p.js → index-CjffvluT.js} +6 -6
- package/app/out/renderer/assets/{index-CnL9yPzK.js → index-D6jljsup.js} +3 -3
- package/app/out/renderer/assets/{index-BxOmAXUZ.js → index-D6r8msaQ.js} +3 -3
- package/app/out/renderer/assets/{index-D2fFfHUR.js → index-DWU4ia28.js} +6 -6
- package/app/out/renderer/assets/{index-BVNrdWzl.js → index-DZbrRR7w.js} +6 -6
- package/app/out/renderer/assets/{index-BCOrnr8q.js → index-Diy30-34.js} +4 -4
- package/app/out/renderer/assets/{index-NHbUPOmb.js → index-DuhageEr.js} +3 -3
- package/app/out/renderer/assets/{index-BnRwUKpv.js → index-ESFHcvWy.js} +3 -3
- package/app/out/renderer/assets/{index-B4djqBxS.js → index-JT8OCsRP.js} +1 -1
- package/app/out/renderer/assets/{index-cAZJ88Np.js → index-bMe3RSkw.js} +6 -6
- package/app/out/renderer/assets/{index-3LdRym1K.js → index-gH-w4EHk.js} +3 -3
- package/app/out/renderer/assets/{index-O3gvL3-Z.js → index-h_fNksib.js} +3 -3
- package/app/out/renderer/assets/{index-y5XZ-0EB.js → index-u0FZRZON.js} +4 -4
- package/app/out/renderer/assets/{index-BgSz3yUy.js → index-yanwpi6t.js} +6 -6
- package/app/out/renderer/assets/{infoDiagram-8eee0895-Cq8aXV8u.js → infoDiagram-8eee0895-Qra4japr.js} +2 -2
- package/app/out/renderer/assets/{journeyDiagram-c64418c1-D4ewDrYD.js → journeyDiagram-c64418c1-BTN9SgOL.js} +4 -4
- package/app/out/renderer/assets/{layout-CZmLZO9t.js → layout-DGrHHJdN.js} +2 -2
- package/app/out/renderer/assets/{line-D7kWOiRx.js → line-DXtxdS2B.js} +1 -1
- package/app/out/renderer/assets/{linear-B055Dz0c.js → linear-CexrSQK6.js} +1 -1
- package/app/out/renderer/assets/{mindmap-definition-8da855dc-D6EW4QCj.js → mindmap-definition-8da855dc-pvG2hzEB.js} +3 -3
- package/app/out/renderer/assets/{pieDiagram-a8764435-BX_Dz4T9.js → pieDiagram-a8764435-D_neFVMq.js} +3 -3
- package/app/out/renderer/assets/{quadrantDiagram-1e28029f-BsI6xGsm.js → quadrantDiagram-1e28029f-C47W3UMp.js} +3 -3
- package/app/out/renderer/assets/{requirementDiagram-08caed73-c2d8T0BS.js → requirementDiagram-08caed73-DW4Bo_fu.js} +5 -5
- package/app/out/renderer/assets/{sankeyDiagram-a04cb91d-CkDhRKRC.js → sankeyDiagram-a04cb91d-D_3PD7JI.js} +2 -2
- package/app/out/renderer/assets/{sequenceDiagram-c5b8d532-DS0RKYnD.js → sequenceDiagram-c5b8d532-BW6nGtuQ.js} +3 -3
- package/app/out/renderer/assets/{stateDiagram-1ecb1508-BjTK27QX.js → stateDiagram-1ecb1508-CDgBJ3-T.js} +6 -6
- package/app/out/renderer/assets/{stateDiagram-v2-c2b004d7-D1wWbeR3.js → stateDiagram-v2-c2b004d7-CBw5TtXo.js} +10 -10
- package/app/out/renderer/assets/{styles-b4e223ce-DXUfbXTM.js → styles-b4e223ce-DeeiEsuW.js} +1 -1
- package/app/out/renderer/assets/{styles-ca3715f6-CE_JRTmB.js → styles-ca3715f6-CMpiebrG.js} +1 -1
- package/app/out/renderer/assets/{styles-d45a18b0-CdtAXXSE.js → styles-d45a18b0-CZe9hU7H.js} +4 -4
- package/app/out/renderer/assets/{svgDrawCommon-b86b1483-dCxPWgBl.js → svgDrawCommon-b86b1483-CmJZfZzJ.js} +1 -1
- package/app/out/renderer/assets/{timeline-definition-faaaa080-B7ZP3Dqw.js → timeline-definition-faaaa080-Beo2kiiz.js} +3 -3
- package/app/out/renderer/assets/{xychartDiagram-f5964ef8-CXagmo1Q.js → xychartDiagram-f5964ef8-DYmo7moz.js} +5 -5
- package/app/out/renderer/index.html +2 -2
- package/app/package.json +1 -1
- package/package.json +1 -1
package/app/out/main/index.mjs
CHANGED
|
@@ -1,20 +1,22 @@
|
|
|
1
1
|
import { app, shell, ipcMain, BrowserWindow, dialog, Menu } from "electron";
|
|
2
|
+
import { setMaxListeners } from "node:events";
|
|
2
3
|
import fs, { existsSync as existsSync$1 } from "node:fs";
|
|
3
|
-
import { execFile, execSync } from "node:child_process";
|
|
4
|
-
import { resolve, join, sep, isAbsolute, extname, basename, dirname, relative } from "path";
|
|
4
|
+
import { execFile, spawn, execSync } from "node:child_process";
|
|
5
|
+
import path, { resolve, join, sep, isAbsolute, extname, basename, dirname, relative } from "path";
|
|
5
6
|
import { existsSync, statSync, readdirSync, readFileSync, writeFileSync, mkdirSync, appendFileSync, rmSync, unlinkSync, renameSync } from "fs";
|
|
6
7
|
import os$1, { homedir } from "os";
|
|
8
|
+
import { createHash, randomUUID } from "crypto";
|
|
7
9
|
import { Agent } from "@mariozechner/pi-agent-core";
|
|
8
10
|
import { completeSimple, getModel } from "@mariozechner/pi-ai";
|
|
9
11
|
import { createCodingTools, createGrepTool, createFindTool, createLsTool, DEFAULT_COMPACTION_SETTINGS, estimateTokens, shouldCompact, generateSummary } from "@mariozechner/pi-coding-agent";
|
|
10
12
|
import { Type } from "@sinclair/typebox";
|
|
11
|
-
import
|
|
13
|
+
import { mkdir, writeFile } from "fs/promises";
|
|
14
|
+
import path$1 from "node:path";
|
|
12
15
|
import fsp from "node:fs/promises";
|
|
13
|
-
import { createHash } from "node:crypto";
|
|
16
|
+
import crypto$1, { createHash as createHash$1 } from "node:crypto";
|
|
14
17
|
import { promisify } from "node:util";
|
|
15
18
|
import os from "node:os";
|
|
16
19
|
import { fileURLToPath } from "node:url";
|
|
17
|
-
import { createHash as createHash$1 } from "crypto";
|
|
18
20
|
import { execFile as execFile$1 } from "child_process";
|
|
19
21
|
import __cjs_mod__ from "node:module";
|
|
20
22
|
const __filename = import.meta.filename;
|
|
@@ -233,10 +235,6 @@ function registerConfigHandlers(handleRaw) {
|
|
|
233
235
|
return { success: true };
|
|
234
236
|
});
|
|
235
237
|
}
|
|
236
|
-
function getFileName(path2) {
|
|
237
|
-
if (!path2) return "";
|
|
238
|
-
return path2.split("/").pop() || path2;
|
|
239
|
-
}
|
|
240
238
|
function inferMimeType(path2) {
|
|
241
239
|
const ext = extname(path2).toLowerCase();
|
|
242
240
|
if (ext === ".md" || ext === ".txt") return "text/plain";
|
|
@@ -264,30 +262,83 @@ function loadOrCreateSessionId(rootPathKey, path2) {
|
|
|
264
262
|
writeFileSync(sessionFile, JSON.stringify({ sessionId: newId }));
|
|
265
263
|
return newId;
|
|
266
264
|
}
|
|
267
|
-
|
|
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) {
|
|
268
306
|
const openaiApiKey = (process.env.OPENAI_API_KEY || "").trim();
|
|
269
307
|
const anthropicApiKey = (process.env.ANTHROPIC_API_KEY || "").trim();
|
|
270
|
-
const
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
apiKey: openaiApiKey,
|
|
277
|
-
authMode: "api-key",
|
|
278
|
-
isAnthropicModel: false,
|
|
279
|
-
billingSource: "api-key"
|
|
280
|
-
};
|
|
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";
|
|
281
314
|
}
|
|
282
|
-
|
|
283
|
-
|
|
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
|
+
}
|
|
284
341
|
}
|
|
285
|
-
return {
|
|
286
|
-
apiKey: anthropicApiKey,
|
|
287
|
-
authMode: "api-key",
|
|
288
|
-
isAnthropicModel: true,
|
|
289
|
-
billingSource: "api-key"
|
|
290
|
-
};
|
|
291
342
|
}
|
|
292
343
|
function registerFileHandlers(handle, getCtx) {
|
|
293
344
|
handle("file:list-root", () => {
|
|
@@ -522,6 +573,51 @@ function registerAuthHandlers(handleRaw) {
|
|
|
522
573
|
hasApiKey: !!(process.env.OPENAI_API_KEY || "").trim()
|
|
523
574
|
};
|
|
524
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
|
+
});
|
|
525
621
|
}
|
|
526
622
|
function registerFolderOpenHandler(handle, getCtx) {
|
|
527
623
|
handle("folder:open-with", async (appName) => {
|
|
@@ -576,13 +672,39 @@ function truncateHeadTail(text, maxChars, headRatio = 0.7) {
|
|
|
576
672
|
...[truncated ${truncatedChars} chars]
|
|
577
673
|
${text.slice(-tailChars)}`;
|
|
578
674
|
}
|
|
675
|
+
function truncateStructuredData(data, maxChars) {
|
|
676
|
+
const json = JSON.stringify(data, null, 2);
|
|
677
|
+
if (json.length <= maxChars) return data;
|
|
678
|
+
const obj = { ...data };
|
|
679
|
+
let largestKey = "";
|
|
680
|
+
let largestSize = 0;
|
|
681
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
682
|
+
if (typeof v === "string" && v.length > largestSize) {
|
|
683
|
+
largestKey = k;
|
|
684
|
+
largestSize = v.length;
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
if (largestKey) {
|
|
688
|
+
const overhead = json.length - largestSize;
|
|
689
|
+
const fieldBudget = Math.max(1e3, maxChars - overhead);
|
|
690
|
+
obj[largestKey] = truncateHeadTail(obj[largestKey], fieldBudget);
|
|
691
|
+
}
|
|
692
|
+
return obj;
|
|
693
|
+
}
|
|
579
694
|
function toAgentResult(toolName, result) {
|
|
580
695
|
let text;
|
|
696
|
+
const MAX_RESULT_CHARS = 1e5;
|
|
581
697
|
if (result.success) {
|
|
582
698
|
if (result.data === void 0 || result.data === null) {
|
|
583
699
|
text = `[${toolName}] OK`;
|
|
584
700
|
} else if (typeof result.data === "string") {
|
|
585
|
-
text = result.data;
|
|
701
|
+
text = truncateHeadTail(result.data, MAX_RESULT_CHARS);
|
|
702
|
+
} else if (typeof result.data === "object" && result.data !== null && !Array.isArray(result.data)) {
|
|
703
|
+
const bounded = truncateStructuredData(
|
|
704
|
+
result.data,
|
|
705
|
+
MAX_RESULT_CHARS
|
|
706
|
+
);
|
|
707
|
+
text = JSON.stringify(bounded, null, 2);
|
|
586
708
|
} else {
|
|
587
709
|
text = JSON.stringify(result.data, null, 2);
|
|
588
710
|
}
|
|
@@ -607,10 +729,11 @@ ${result.suggestions.map((s) => `- ${s}`).join("\n")}`);
|
|
|
607
729
|
}
|
|
608
730
|
text = parts.join("\n");
|
|
609
731
|
}
|
|
610
|
-
|
|
611
|
-
|
|
732
|
+
if (text.length > MAX_RESULT_CHARS) {
|
|
733
|
+
text = truncateHeadTail(text, MAX_RESULT_CHARS);
|
|
734
|
+
}
|
|
612
735
|
return {
|
|
613
|
-
content: [{ type: "text", text
|
|
736
|
+
content: [{ type: "text", text }],
|
|
614
737
|
details: { success: result.success, tool_name: toolName }
|
|
615
738
|
};
|
|
616
739
|
}
|
|
@@ -639,7 +762,9 @@ const PATHS = {
|
|
|
639
762
|
memory: ".research-pilot/memory",
|
|
640
763
|
// Skills
|
|
641
764
|
skills: ".research-pilot/skills",
|
|
642
|
-
skillsConfig: ".research-pilot/skills-config.json"
|
|
765
|
+
skillsConfig: ".research-pilot/skills-config.json",
|
|
766
|
+
// Local compute runs
|
|
767
|
+
computeRuns: ".research-pilot/compute-runs"
|
|
643
768
|
};
|
|
644
769
|
function nowIso() {
|
|
645
770
|
return (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -1255,7 +1380,12 @@ function createResearchMemoryTools(params) {
|
|
|
1255
1380
|
];
|
|
1256
1381
|
}
|
|
1257
1382
|
function slugify(text) {
|
|
1258
|
-
|
|
1383
|
+
const slug = text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 60);
|
|
1384
|
+
if (slug.length < 3) {
|
|
1385
|
+
const hash = createHash("sha256").update(text.toLowerCase().trim()).digest("hex").slice(0, 12);
|
|
1386
|
+
return slug ? `${slug}-${hash}` : hash;
|
|
1387
|
+
}
|
|
1388
|
+
return slug;
|
|
1259
1389
|
}
|
|
1260
1390
|
function memoryFilename(type, name) {
|
|
1261
1391
|
return `${type}_${slugify(name)}.md`;
|
|
@@ -1267,22 +1397,40 @@ function ensureMemoryDir(projectPath) {
|
|
|
1267
1397
|
const dir = memoryDir(projectPath);
|
|
1268
1398
|
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
1269
1399
|
}
|
|
1400
|
+
function yamlSafe(value) {
|
|
1401
|
+
if (!value) return '""';
|
|
1402
|
+
if (/[:\#{}\[\]"'`\n\r|>]/.test(value) || value !== value.trim()) {
|
|
1403
|
+
const escaped = value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\r/g, "\\r");
|
|
1404
|
+
return `"${escaped}"`;
|
|
1405
|
+
}
|
|
1406
|
+
return value;
|
|
1407
|
+
}
|
|
1270
1408
|
function formatFrontmatter(fm) {
|
|
1271
1409
|
return [
|
|
1272
1410
|
"---",
|
|
1273
|
-
`name: ${fm.name}`,
|
|
1274
|
-
`description: ${fm.description}`,
|
|
1411
|
+
`name: ${yamlSafe(fm.name)}`,
|
|
1412
|
+
`description: ${yamlSafe(fm.description)}`,
|
|
1275
1413
|
`type: ${fm.type}`,
|
|
1276
1414
|
"---"
|
|
1277
1415
|
].join("\n");
|
|
1278
1416
|
}
|
|
1417
|
+
function yamlUnescape(raw) {
|
|
1418
|
+
const trimmed = raw.trim();
|
|
1419
|
+
if (trimmed.startsWith('"') && trimmed.endsWith('"')) {
|
|
1420
|
+
return trimmed.slice(1, -1).replace(/\\n/g, "\n").replace(/\\r/g, "\r").replace(/\\"/g, '"').replace(/\\\\/g, "\\");
|
|
1421
|
+
}
|
|
1422
|
+
if (trimmed.startsWith("'") && trimmed.endsWith("'")) {
|
|
1423
|
+
return trimmed.slice(1, -1).replace(/''/g, "'");
|
|
1424
|
+
}
|
|
1425
|
+
return trimmed;
|
|
1426
|
+
}
|
|
1279
1427
|
function parseFrontmatter(text) {
|
|
1280
1428
|
const match = text.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
|
|
1281
1429
|
if (!match) return null;
|
|
1282
1430
|
const fm = {};
|
|
1283
1431
|
for (const line of match[1].split("\n")) {
|
|
1284
1432
|
const kv = line.match(/^(\w+):\s*(.+)$/);
|
|
1285
|
-
if (kv) fm[kv[1]] = kv[2]
|
|
1433
|
+
if (kv) fm[kv[1]] = yamlUnescape(kv[2]);
|
|
1286
1434
|
}
|
|
1287
1435
|
if (!fm.name || !fm.type) return null;
|
|
1288
1436
|
const validTypes = ["user", "feedback", "project", "reference"];
|
|
@@ -1339,9 +1487,24 @@ function listMemoryFiles(projectPath) {
|
|
|
1339
1487
|
return [];
|
|
1340
1488
|
}
|
|
1341
1489
|
}
|
|
1342
|
-
function findMemoryByName(projectPath, name) {
|
|
1490
|
+
function findMemoryByName(projectPath, name, type) {
|
|
1491
|
+
const lower = name.toLowerCase();
|
|
1492
|
+
const entries = listMemoryFiles(projectPath);
|
|
1493
|
+
return entries.find(
|
|
1494
|
+
(e) => e.frontmatter.name.toLowerCase() === lower && (!type || e.frontmatter.type === type)
|
|
1495
|
+
) ?? null;
|
|
1496
|
+
}
|
|
1497
|
+
function findAllMemoriesByName(projectPath, name) {
|
|
1343
1498
|
const lower = name.toLowerCase();
|
|
1344
|
-
return listMemoryFiles(projectPath).
|
|
1499
|
+
return listMemoryFiles(projectPath).filter((e) => e.frontmatter.name.toLowerCase() === lower);
|
|
1500
|
+
}
|
|
1501
|
+
let _indexWriteLock = Promise.resolve();
|
|
1502
|
+
function withIndexLock(fn) {
|
|
1503
|
+
const next = _indexWriteLock.then(fn, fn);
|
|
1504
|
+
_indexWriteLock = next.then(() => {
|
|
1505
|
+
}, () => {
|
|
1506
|
+
});
|
|
1507
|
+
return next;
|
|
1345
1508
|
}
|
|
1346
1509
|
function buildMemoryIndex(entries) {
|
|
1347
1510
|
if (entries.length === 0) return "";
|
|
@@ -1381,6 +1544,9 @@ function migrateAgentMemoryToFile(projectPath) {
|
|
|
1381
1544
|
if (markerIdx < 0) return false;
|
|
1382
1545
|
const agentMemory = content.slice(markerIdx + marker.length).trim();
|
|
1383
1546
|
if (!agentMemory || /\[.*\]\(memory\/.*\)/.test(agentMemory)) return false;
|
|
1547
|
+
const legacyFilename = memoryFilename("project", "legacy-notes");
|
|
1548
|
+
const legacyPath = join(memoryDir(projectPath), legacyFilename);
|
|
1549
|
+
if (existsSync(legacyPath)) return false;
|
|
1384
1550
|
ensureMemoryDir(projectPath);
|
|
1385
1551
|
const entry = {
|
|
1386
1552
|
frontmatter: {
|
|
@@ -1435,73 +1601,98 @@ function createSaveMemoryTool(projectPath) {
|
|
|
1435
1601
|
if (!content) return toolError("MISSING_PARAMETER", "content is required.", {
|
|
1436
1602
|
suggestions: ["Provide the memory content to save."]
|
|
1437
1603
|
});
|
|
1438
|
-
|
|
1439
|
-
const
|
|
1440
|
-
const description =
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
message: `Memory saved: ${name} (${type})`,
|
|
1462
|
-
filename,
|
|
1463
|
-
totalMemories: allEntries.length,
|
|
1464
|
-
agentMdChars: indexResult.charCount
|
|
1604
|
+
const lines = content.split("\n");
|
|
1605
|
+
const firstNonEmpty = lines.find((l) => l.trim().length > 0) || "";
|
|
1606
|
+
const description = firstNonEmpty.replace(/^#+\s*/, "").trim().slice(0, 120) || name;
|
|
1607
|
+
return withIndexLock(() => {
|
|
1608
|
+
ensureMemoryDir(projectPath);
|
|
1609
|
+
const filename = memoryFilename(type, name);
|
|
1610
|
+
const entry = {
|
|
1611
|
+
frontmatter: { name, description, type },
|
|
1612
|
+
content,
|
|
1613
|
+
filename
|
|
1614
|
+
};
|
|
1615
|
+
writeMemoryFile(projectPath, entry);
|
|
1616
|
+
const allEntries = listMemoryFiles(projectPath);
|
|
1617
|
+
const indexResult = updateAgentMdIndex(projectPath, allEntries);
|
|
1618
|
+
if (!indexResult.success) {
|
|
1619
|
+
deleteMemoryFile(projectPath, filename);
|
|
1620
|
+
return toolError(
|
|
1621
|
+
"OUTPUT_TOO_LARGE",
|
|
1622
|
+
"agent.md index exceeded size limit. Remove some memories first.",
|
|
1623
|
+
{
|
|
1624
|
+
suggestions: ["Use delete-memory to remove outdated entries before saving new ones."]
|
|
1625
|
+
}
|
|
1626
|
+
);
|
|
1465
1627
|
}
|
|
1466
|
-
|
|
1628
|
+
return {
|
|
1629
|
+
success: true,
|
|
1630
|
+
data: {
|
|
1631
|
+
message: `Memory saved: ${name} (${type})`,
|
|
1632
|
+
filename,
|
|
1633
|
+
totalMemories: allEntries.length,
|
|
1634
|
+
agentMdChars: indexResult.charCount
|
|
1635
|
+
}
|
|
1636
|
+
};
|
|
1637
|
+
});
|
|
1467
1638
|
}
|
|
1468
1639
|
};
|
|
1469
1640
|
}
|
|
1470
1641
|
function createDeleteMemoryTool(projectPath) {
|
|
1471
1642
|
return {
|
|
1472
1643
|
name: "delete-memory",
|
|
1473
|
-
description: "Delete a memory by name. Removes the file and its index entry in agent.md.",
|
|
1644
|
+
description: "Delete a memory by name. Removes the file and its index entry in agent.md. If multiple memories share the same name (different types), specify type to disambiguate.",
|
|
1474
1645
|
parameters: {
|
|
1475
1646
|
type: "object",
|
|
1476
1647
|
properties: {
|
|
1477
1648
|
name: {
|
|
1478
1649
|
type: "string",
|
|
1479
1650
|
description: "Name of the memory to delete (case-insensitive match)"
|
|
1651
|
+
},
|
|
1652
|
+
type: {
|
|
1653
|
+
type: "string",
|
|
1654
|
+
enum: VALID_TYPES$1,
|
|
1655
|
+
description: "Optional: memory type to disambiguate when multiple memories share the same name"
|
|
1480
1656
|
}
|
|
1481
1657
|
},
|
|
1482
1658
|
required: ["name"]
|
|
1483
1659
|
},
|
|
1484
1660
|
execute: async (input) => {
|
|
1485
1661
|
const name = String(input.name || "").trim();
|
|
1662
|
+
const type = input.type ? String(input.type) : void 0;
|
|
1486
1663
|
if (!name) return toolError("MISSING_PARAMETER", "name is required.", {
|
|
1487
1664
|
suggestions: ["Provide the name of the memory to delete."]
|
|
1488
1665
|
});
|
|
1489
|
-
const
|
|
1490
|
-
if (
|
|
1666
|
+
const allMatches = findAllMemoriesByName(projectPath, name);
|
|
1667
|
+
if (allMatches.length === 0) {
|
|
1491
1668
|
return toolError("NOT_FOUND", `Memory not found: "${name}"`, {
|
|
1492
1669
|
suggestions: ["Check the memory name — it is case-insensitive. Current memories are listed in agent.md."]
|
|
1493
1670
|
});
|
|
1494
1671
|
}
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
}
|
|
1504
|
-
|
|
1672
|
+
if (allMatches.length > 1 && !type) {
|
|
1673
|
+
const types = allMatches.map((m) => m.frontmatter.type).join(", ");
|
|
1674
|
+
return toolError("AMBIGUOUS", `Multiple memories named "${name}" (types: ${types}). Specify type to disambiguate.`, {
|
|
1675
|
+
suggestions: [`Add type parameter: one of ${types}`]
|
|
1676
|
+
});
|
|
1677
|
+
}
|
|
1678
|
+
const existing = type ? findMemoryByName(projectPath, name, type) : allMatches[0];
|
|
1679
|
+
if (!existing) {
|
|
1680
|
+
return toolError("NOT_FOUND", `Memory not found: "${name}" with type "${type}"`, {
|
|
1681
|
+
suggestions: ["Check the memory name and type."]
|
|
1682
|
+
});
|
|
1683
|
+
}
|
|
1684
|
+
return withIndexLock(() => {
|
|
1685
|
+
deleteMemoryFile(projectPath, existing.filename);
|
|
1686
|
+
const allEntries = listMemoryFiles(projectPath);
|
|
1687
|
+
updateAgentMdIndex(projectPath, allEntries);
|
|
1688
|
+
return {
|
|
1689
|
+
success: true,
|
|
1690
|
+
data: {
|
|
1691
|
+
message: `Memory deleted: ${name}`,
|
|
1692
|
+
totalMemories: allEntries.length
|
|
1693
|
+
}
|
|
1694
|
+
};
|
|
1695
|
+
});
|
|
1505
1696
|
}
|
|
1506
1697
|
};
|
|
1507
1698
|
}
|
|
@@ -1526,8 +1717,11 @@ const WEB_DEFAULTS = {
|
|
|
1526
1717
|
maxFetchMaxChars: 2e5,
|
|
1527
1718
|
defaultFetchTimeoutMs: 3e4,
|
|
1528
1719
|
maxArxivCacheEntries: 100,
|
|
1529
|
-
arxivSearchCacheTtlMs: 10 * 60 * 1e3
|
|
1720
|
+
arxivSearchCacheTtlMs: 10 * 60 * 1e3,
|
|
1530
1721
|
// 10 min
|
|
1722
|
+
/** Content above this size is saved to disk; agent gets preview + file path */
|
|
1723
|
+
fetchPersistThresholdChars: 3e4,
|
|
1724
|
+
fetchPreviewChars: 2e3
|
|
1531
1725
|
};
|
|
1532
1726
|
class ProviderRateGate {
|
|
1533
1727
|
constructor(minIntervalMs) {
|
|
@@ -1774,7 +1968,6 @@ function createWebSearchTool(ctx) {
|
|
|
1774
1968
|
const providerRequested = normalizeSearchProvider(params.provider);
|
|
1775
1969
|
const braveApiKey = process.env.BRAVE_API_KEY?.trim();
|
|
1776
1970
|
let effectiveProvider = providerRequested === "auto" ? braveApiKey ? "brave" : "arxiv" : providerRequested;
|
|
1777
|
-
ctx.onToolCall?.("web_search", { query, count, provider: effectiveProvider });
|
|
1778
1971
|
let results = [];
|
|
1779
1972
|
try {
|
|
1780
1973
|
if (effectiveProvider === "brave") {
|
|
@@ -1818,7 +2011,6 @@ function createWebSearchTool(ctx) {
|
|
|
1818
2011
|
count: results.length,
|
|
1819
2012
|
results
|
|
1820
2013
|
};
|
|
1821
|
-
ctx.onToolResult?.("web_search", payload);
|
|
1822
2014
|
return toAgentResult("web_search", {
|
|
1823
2015
|
success: true,
|
|
1824
2016
|
data: payload
|
|
@@ -1830,7 +2022,7 @@ function createWebFetchTool(ctx) {
|
|
|
1830
2022
|
return {
|
|
1831
2023
|
name: "web_fetch",
|
|
1832
2024
|
label: "Web Fetch",
|
|
1833
|
-
description: "Fetch a URL and extract readable text or markdown
|
|
2025
|
+
description: "Fetch a URL and extract readable text or markdown. Content over 30K chars is saved to disk — use the read tool on the returned content_path to access full content.",
|
|
1834
2026
|
parameters: WebFetchSchema,
|
|
1835
2027
|
execute: async (_toolCallId, rawParams) => {
|
|
1836
2028
|
const params = rawParams;
|
|
@@ -1859,7 +2051,6 @@ function createWebFetchTool(ctx) {
|
|
|
1859
2051
|
const maxChars = typeof maxCharsRaw === "number" ? Math.max(100, Math.min(WEB_DEFAULTS.maxFetchMaxChars, Math.floor(maxCharsRaw))) : WEB_DEFAULTS.defaultFetchMaxChars;
|
|
1860
2052
|
const timeoutSecRaw = (typeof params.timeout_sec === "number" && Number.isFinite(params.timeout_sec) ? params.timeout_sec : void 0) ?? (typeof params.timeoutSec === "number" && Number.isFinite(params.timeoutSec) ? params.timeoutSec : void 0);
|
|
1861
2053
|
const timeoutMs = typeof timeoutSecRaw === "number" ? Math.max(1e3, Math.floor(timeoutSecRaw * 1e3)) : WEB_DEFAULTS.defaultFetchTimeoutMs;
|
|
1862
|
-
ctx.onToolCall?.("web_fetch", { url: url.toString(), extractMode, maxChars });
|
|
1863
2054
|
let response;
|
|
1864
2055
|
const controller = new AbortController();
|
|
1865
2056
|
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
@@ -1897,16 +2088,37 @@ Source: ${url.toString()}
|
|
|
1897
2088
|
---
|
|
1898
2089
|
|
|
1899
2090
|
${sliced}` : sliced;
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
2091
|
+
let payload;
|
|
2092
|
+
if (output.length > WEB_DEFAULTS.fetchPersistThresholdChars) {
|
|
2093
|
+
const hash = createHash("md5").update(url.toString() + Date.now()).digest("hex").slice(0, 12);
|
|
2094
|
+
const ext = extractMode === "markdown" ? "md" : "txt";
|
|
2095
|
+
const contentDir = path.join(ctx.projectPath, "web-content");
|
|
2096
|
+
await mkdir(contentDir, { recursive: true });
|
|
2097
|
+
const filePath = path.join(contentDir, `${hash}.${ext}`);
|
|
2098
|
+
await writeFile(filePath, output, "utf-8");
|
|
2099
|
+
const previewRaw = output.slice(0, WEB_DEFAULTS.fetchPreviewChars);
|
|
2100
|
+
const lastNl = previewRaw.lastIndexOf("\n");
|
|
2101
|
+
const preview = (lastNl > WEB_DEFAULTS.fetchPreviewChars * 0.5 ? previewRaw.slice(0, lastNl) : previewRaw) + "\n...";
|
|
2102
|
+
payload = {
|
|
2103
|
+
url: url.toString(),
|
|
2104
|
+
status_code: response.status,
|
|
2105
|
+
content_type: contentType,
|
|
2106
|
+
extract_mode: extractMode,
|
|
2107
|
+
chars: normalized.length,
|
|
2108
|
+
content_path: path.relative(ctx.workspacePath, filePath),
|
|
2109
|
+
preview
|
|
2110
|
+
};
|
|
2111
|
+
} else {
|
|
2112
|
+
payload = {
|
|
2113
|
+
url: url.toString(),
|
|
2114
|
+
status_code: response.status,
|
|
2115
|
+
content_type: contentType,
|
|
2116
|
+
extract_mode: extractMode,
|
|
2117
|
+
chars: normalized.length,
|
|
2118
|
+
truncated,
|
|
2119
|
+
content: output || "(empty response)"
|
|
2120
|
+
};
|
|
2121
|
+
}
|
|
1910
2122
|
return toAgentResult("web_fetch", {
|
|
1911
2123
|
success: response.ok,
|
|
1912
2124
|
data: payload,
|
|
@@ -2774,7 +2986,6 @@ function createLiteratureSearchTool(ctx) {
|
|
|
2774
2986
|
suggestions: ["Ensure the agent runtime has an LLM provider configured (callLlm in ResearchToolContext)."]
|
|
2775
2987
|
}));
|
|
2776
2988
|
}
|
|
2777
|
-
ctx.onToolCall?.("literature-search", { query, context: extraContext });
|
|
2778
2989
|
const planUserPrompt = extraContext ? `Research request: ${query}
|
|
2779
2990
|
|
|
2780
2991
|
Additional context: ${extraContext}` : `Research request: ${query}`;
|
|
@@ -2951,9 +3162,9 @@ Additional context: ${extraContext}` : `Research request: ${query}`;
|
|
|
2951
3162
|
}
|
|
2952
3163
|
}
|
|
2953
3164
|
}
|
|
2954
|
-
const reviewDir = path.join(ctx.projectPath, ".research-pilot", "literature-runs", runId);
|
|
3165
|
+
const reviewDir = path$1.join(ctx.projectPath, ".research-pilot", "literature-runs", runId);
|
|
2955
3166
|
fs.mkdirSync(reviewDir, { recursive: true });
|
|
2956
|
-
const fullReviewPath = path.join(reviewDir, "review.json");
|
|
3167
|
+
const fullReviewPath = path$1.join(reviewDir, "review.json");
|
|
2957
3168
|
fs.writeFileSync(fullReviewPath, JSON.stringify({
|
|
2958
3169
|
plan,
|
|
2959
3170
|
allPapersCount: deduplicated.length,
|
|
@@ -2962,7 +3173,7 @@ Additional context: ${extraContext}` : `Research request: ${query}`;
|
|
|
2962
3173
|
queriesUsed,
|
|
2963
3174
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2964
3175
|
}, null, 2), "utf-8");
|
|
2965
|
-
const relReviewPath = path.relative(ctx.projectPath, fullReviewPath);
|
|
3176
|
+
const relReviewPath = path$1.relative(ctx.projectPath, fullReviewPath);
|
|
2966
3177
|
const payload = {
|
|
2967
3178
|
totalFound: deduplicated.length,
|
|
2968
3179
|
reviewedCount: review.relevantPapers.length,
|
|
@@ -2985,12 +3196,11 @@ Additional context: ${extraContext}` : `Research request: ${query}`;
|
|
|
2985
3196
|
runId,
|
|
2986
3197
|
queriesUsed: queriesUsed.slice(0, 10)
|
|
2987
3198
|
};
|
|
2988
|
-
ctx.onToolResult?.("literature-search", payload);
|
|
2989
3199
|
return toAgentResult("literature-search", toolSuccess(payload, pipelineWarnings.length > 0 ? pipelineWarnings : void 0));
|
|
2990
3200
|
}
|
|
2991
3201
|
};
|
|
2992
3202
|
}
|
|
2993
|
-
const execFileAsync$
|
|
3203
|
+
const execFileAsync$3 = promisify(execFile);
|
|
2994
3204
|
const DEFAULT_MAX_OUTPUT_CHARS = 5e5;
|
|
2995
3205
|
const DEFAULT_PREVIEW_CHARS = 4e3;
|
|
2996
3206
|
const DEFAULT_COMMAND_TIMEOUT_MS = 12e4;
|
|
@@ -3028,16 +3238,16 @@ const FORMAT_EXTENSIONS = {
|
|
|
3028
3238
|
zip: "zip"
|
|
3029
3239
|
};
|
|
3030
3240
|
function resolveWithinProject(projectPath, targetPath) {
|
|
3031
|
-
const root = path.resolve(projectPath);
|
|
3032
|
-
const resolved = targetPath.startsWith("/") ? path.resolve(targetPath) : path.resolve(root, targetPath);
|
|
3033
|
-
const rel = path.relative(root, resolved);
|
|
3034
|
-
if (rel.startsWith("..") || path.isAbsolute(rel)) {
|
|
3241
|
+
const root = path$1.resolve(projectPath);
|
|
3242
|
+
const resolved = targetPath.startsWith("/") ? path$1.resolve(targetPath) : path$1.resolve(root, targetPath);
|
|
3243
|
+
const rel = path$1.relative(root, resolved);
|
|
3244
|
+
if (rel.startsWith("..") || path$1.isAbsolute(rel)) {
|
|
3035
3245
|
throw new Error(`Path escapes project directory: ${targetPath}`);
|
|
3036
3246
|
}
|
|
3037
3247
|
return resolved;
|
|
3038
3248
|
}
|
|
3039
3249
|
function toProjectRelative(projectPath, absolutePath) {
|
|
3040
|
-
return path.relative(path.resolve(projectPath), absolutePath);
|
|
3250
|
+
return path$1.relative(path$1.resolve(projectPath), absolutePath);
|
|
3041
3251
|
}
|
|
3042
3252
|
function isHttpUrl(value) {
|
|
3043
3253
|
try {
|
|
@@ -3057,7 +3267,7 @@ function sanitizeBaseName(value) {
|
|
|
3057
3267
|
function inferUrlBaseName(sourceUrl) {
|
|
3058
3268
|
try {
|
|
3059
3269
|
const parsed = new URL(sourceUrl);
|
|
3060
|
-
const candidate = path.parse(parsed.pathname).name || parsed.hostname;
|
|
3270
|
+
const candidate = path$1.parse(parsed.pathname).name || parsed.hostname;
|
|
3061
3271
|
return sanitizeBaseName(candidate);
|
|
3062
3272
|
} catch {
|
|
3063
3273
|
return sanitizeBaseName(sourceUrl);
|
|
@@ -3067,7 +3277,7 @@ function outputExtensionForMode(mode) {
|
|
|
3067
3277
|
return mode === "text" ? ".txt" : ".md";
|
|
3068
3278
|
}
|
|
3069
3279
|
function extensionFromPath(filePath) {
|
|
3070
|
-
const ext = path.extname(filePath).replace(".", "").toLowerCase();
|
|
3280
|
+
const ext = path$1.extname(filePath).replace(".", "").toLowerCase();
|
|
3071
3281
|
return ext || void 0;
|
|
3072
3282
|
}
|
|
3073
3283
|
function normalizeMode(value) {
|
|
@@ -3121,7 +3331,7 @@ async function detectTextLikeFile(filePath) {
|
|
|
3121
3331
|
}
|
|
3122
3332
|
async function runCommand(command, args, timeoutMs) {
|
|
3123
3333
|
try {
|
|
3124
|
-
await execFileAsync$
|
|
3334
|
+
await execFileAsync$3(command, args, {
|
|
3125
3335
|
timeout: timeoutMs,
|
|
3126
3336
|
maxBuffer: 10 * 1024 * 1024
|
|
3127
3337
|
});
|
|
@@ -3212,7 +3422,7 @@ print(json.dumps(result, ensure_ascii=False))
|
|
|
3212
3422
|
let foundAnyRuntime = false;
|
|
3213
3423
|
for (const attempt of attempts) {
|
|
3214
3424
|
try {
|
|
3215
|
-
const { stdout } = await execFileAsync$
|
|
3425
|
+
const { stdout } = await execFileAsync$3(
|
|
3216
3426
|
attempt.command,
|
|
3217
3427
|
["-c", script, inputPath, JSON.stringify(ranges)],
|
|
3218
3428
|
{ timeout: timeoutMs, maxBuffer: 50 * 1024 * 1024, encoding: "utf8" }
|
|
@@ -3301,15 +3511,15 @@ async function downloadToProject(projectPath, sourceUrl) {
|
|
|
3301
3511
|
if (bytes.length > DEFAULT_MAX_DOWNLOAD_BYTES) {
|
|
3302
3512
|
return { ok: false, error: `Downloaded file too large (${bytes.length} bytes > ${DEFAULT_MAX_DOWNLOAD_BYTES} bytes)` };
|
|
3303
3513
|
}
|
|
3304
|
-
const downloadDir = path.join(path.resolve(projectPath), ".research-pilot", "cache", "downloads");
|
|
3514
|
+
const downloadDir = path$1.join(path$1.resolve(projectPath), ".research-pilot", "cache", "downloads");
|
|
3305
3515
|
await fsp.mkdir(downloadDir, { recursive: true });
|
|
3306
3516
|
const urlObj = new URL(sourceUrl);
|
|
3307
3517
|
const fromExt = extensionFromPath(urlObj.pathname);
|
|
3308
3518
|
const fromContentType = detectFormatFromContentType(response.headers.get("content-type"));
|
|
3309
3519
|
const chosenExt = FORMAT_EXTENSIONS[fromContentType || ""] || FORMAT_EXTENSIONS[fromExt || ""] || "bin";
|
|
3310
|
-
const hash = createHash("sha256").update(sourceUrl).digest("hex").slice(0, 12);
|
|
3520
|
+
const hash = createHash$1("sha256").update(sourceUrl).digest("hex").slice(0, 12);
|
|
3311
3521
|
const fileName = `${isoStamp()}-${hash}.${chosenExt}`;
|
|
3312
|
-
const inputPath = path.join(downloadDir, fileName);
|
|
3522
|
+
const inputPath = path$1.join(downloadDir, fileName);
|
|
3313
3523
|
await fsp.writeFile(inputPath, bytes);
|
|
3314
3524
|
return {
|
|
3315
3525
|
ok: true,
|
|
@@ -3389,9 +3599,9 @@ ${sections}
|
|
|
3389
3599
|
`;
|
|
3390
3600
|
}
|
|
3391
3601
|
function buildPerRangeOutputPath(baseAbsolutePath, range, mode) {
|
|
3392
|
-
const parsed = path.parse(baseAbsolutePath);
|
|
3602
|
+
const parsed = path$1.parse(baseAbsolutePath);
|
|
3393
3603
|
const ext = parsed.ext || outputExtensionForMode(mode);
|
|
3394
|
-
return path.join(parsed.dir, `${parsed.name}-${rangeFileToken(range)}${ext}`);
|
|
3604
|
+
return path$1.join(parsed.dir, `${parsed.name}-${rangeFileToken(range)}${ext}`);
|
|
3395
3605
|
}
|
|
3396
3606
|
function buildPreview(text, maxPreviewChars = DEFAULT_PREVIEW_CHARS) {
|
|
3397
3607
|
const content = text.slice(0, maxPreviewChars);
|
|
@@ -3403,14 +3613,14 @@ function buildPreview(text, maxPreviewChars = DEFAULT_PREVIEW_CHARS) {
|
|
|
3403
3613
|
};
|
|
3404
3614
|
}
|
|
3405
3615
|
function defaultOutputPath(args) {
|
|
3406
|
-
const outputDir = path.join(
|
|
3407
|
-
path.resolve(args.projectPath),
|
|
3616
|
+
const outputDir = path$1.join(
|
|
3617
|
+
path$1.resolve(args.projectPath),
|
|
3408
3618
|
".research-pilot",
|
|
3409
3619
|
"cache",
|
|
3410
3620
|
"converted"
|
|
3411
3621
|
);
|
|
3412
|
-
const baseName = args.isUrl ? inferUrlBaseName(args.sourceRaw) : sanitizeBaseName(path.parse(args.inputPath || "document").name || "document");
|
|
3413
|
-
return path.join(outputDir, `${baseName}-${isoStamp()}${outputExtensionForMode(args.mode)}`);
|
|
3622
|
+
const baseName = args.isUrl ? inferUrlBaseName(args.sourceRaw) : sanitizeBaseName(path$1.parse(args.inputPath || "document").name || "document");
|
|
3623
|
+
return path$1.join(outputDir, `${baseName}-${isoStamp()}${outputExtensionForMode(args.mode)}`);
|
|
3414
3624
|
}
|
|
3415
3625
|
function failure(payload) {
|
|
3416
3626
|
return toAgentResult("convert_document", { success: false, error: payload.error, data: payload });
|
|
@@ -3593,7 +3803,7 @@ function createConvertDocumentTool(ctx) {
|
|
|
3593
3803
|
mode
|
|
3594
3804
|
});
|
|
3595
3805
|
}
|
|
3596
|
-
await fsp.mkdir(path.dirname(outputAbsolutePath), { recursive: true });
|
|
3806
|
+
await fsp.mkdir(path$1.dirname(outputAbsolutePath), { recursive: true });
|
|
3597
3807
|
let converter;
|
|
3598
3808
|
let producedText = "";
|
|
3599
3809
|
let pageCount;
|
|
@@ -3626,7 +3836,7 @@ function createConvertDocumentTool(ctx) {
|
|
|
3626
3836
|
anyTruncated = true;
|
|
3627
3837
|
}
|
|
3628
3838
|
const segmentAbsPath = buildPerRangeOutputPath(outputAbsolutePath, range, mode);
|
|
3629
|
-
await fsp.mkdir(path.dirname(segmentAbsPath), { recursive: true });
|
|
3839
|
+
await fsp.mkdir(path$1.dirname(segmentAbsPath), { recursive: true });
|
|
3630
3840
|
await fsp.writeFile(segmentAbsPath, segmentText, "utf8");
|
|
3631
3841
|
const segmentRelPath = toProjectRelative(projectPath, segmentAbsPath);
|
|
3632
3842
|
const segmentPreview = buildPreview(segmentText);
|
|
@@ -3722,7 +3932,7 @@ function createConvertDocumentTool(ctx) {
|
|
|
3722
3932
|
}
|
|
3723
3933
|
};
|
|
3724
3934
|
}
|
|
3725
|
-
const execFileAsync = promisify(execFile);
|
|
3935
|
+
const execFileAsync$2 = promisify(execFile);
|
|
3726
3936
|
const DATA_ANALYSIS_SYSTEM = loadPrompt("data-analysis-system");
|
|
3727
3937
|
const DATA_ANALYSIS_TASKS = loadPrompt("data-analysis-tasks");
|
|
3728
3938
|
const DATA_CODE_TEMPLATE = loadPrompt("data-code-template");
|
|
@@ -3765,7 +3975,7 @@ function createDataAnalyzeTool(ctx) {
|
|
|
3765
3975
|
suggestions: ["Valid task types: analyze, visualize, transform, model."]
|
|
3766
3976
|
}));
|
|
3767
3977
|
}
|
|
3768
|
-
const absDataFile = path.resolve(ctx.workspacePath, filePath);
|
|
3978
|
+
const absDataFile = path$1.resolve(ctx.workspacePath, filePath);
|
|
3769
3979
|
if (!fs.existsSync(absDataFile)) {
|
|
3770
3980
|
return toAgentResult("data_analyze", toolError("FILE_NOT_FOUND", `File not found: ${filePath}`, {
|
|
3771
3981
|
suggestions: [
|
|
@@ -3775,18 +3985,17 @@ function createDataAnalyzeTool(ctx) {
|
|
|
3775
3985
|
context: { workspacePath: ctx.workspacePath, resolvedPath: absDataFile }
|
|
3776
3986
|
}));
|
|
3777
3987
|
}
|
|
3778
|
-
ctx.onToolCall?.("data_analyze", { file_path: filePath, instructions, task_type: taskType });
|
|
3779
3988
|
const runId = Date.now().toString(36);
|
|
3780
|
-
const outputBase = path.join(ctx.workspacePath, ".research-pilot", "data-runs", runId);
|
|
3781
|
-
const figuresDir = path.join(outputBase, "figures");
|
|
3782
|
-
const tablesDir = path.join(outputBase, "tables");
|
|
3783
|
-
const dataDir = path.join(outputBase, "data");
|
|
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");
|
|
3784
3993
|
fs.mkdirSync(figuresDir, { recursive: true });
|
|
3785
3994
|
fs.mkdirSync(tablesDir, { recursive: true });
|
|
3786
3995
|
fs.mkdirSync(dataDir, { recursive: true });
|
|
3787
|
-
const resultsFile = path.join(outputBase, "results.json");
|
|
3996
|
+
const resultsFile = path$1.join(outputBase, "results.json");
|
|
3788
3997
|
const rawPreview = fs.readFileSync(absDataFile, "utf-8").slice(0, 2e3);
|
|
3789
|
-
const ext = path.extname(absDataFile).toLowerCase();
|
|
3998
|
+
const ext = path$1.extname(absDataFile).toLowerCase();
|
|
3790
3999
|
const formatHint = ext === ".csv" ? "CSV" : ext === ".tsv" ? "TSV" : ext === ".json" ? "JSON" : ext === ".xlsx" ? "XLSX" : "unknown";
|
|
3791
4000
|
if (!ctx.callLlm) {
|
|
3792
4001
|
return toAgentResult("data_analyze", toolError("LLM_UNAVAILABLE", "LLM not available for code generation.", {
|
|
@@ -3834,10 +4043,10 @@ function createDataAnalyzeTool(ctx) {
|
|
|
3834
4043
|
""
|
|
3835
4044
|
].join("\n");
|
|
3836
4045
|
const fullScript = DATA_CODE_TEMPLATE + pathDefinitions + "\n" + generatedCode;
|
|
3837
|
-
const scriptPath = path.join(outputBase, "script.py");
|
|
4046
|
+
const scriptPath = path$1.join(outputBase, "script.py");
|
|
3838
4047
|
fs.writeFileSync(scriptPath, fullScript, "utf-8");
|
|
3839
4048
|
try {
|
|
3840
|
-
const { stdout, stderr } = await execFileAsync("python3", [scriptPath], {
|
|
4049
|
+
const { stdout, stderr } = await execFileAsync$2("python3", [scriptPath], {
|
|
3841
4050
|
cwd: ctx.workspacePath,
|
|
3842
4051
|
timeout: 12e4,
|
|
3843
4052
|
// 2 minutes
|
|
@@ -3861,7 +4070,7 @@ function createDataAnalyzeTool(ctx) {
|
|
|
3861
4070
|
outputs.push({
|
|
3862
4071
|
name: f,
|
|
3863
4072
|
type,
|
|
3864
|
-
path: path.relative(ctx.workspacePath, path.join(dir, f))
|
|
4073
|
+
path: path$1.relative(ctx.workspacePath, path$1.join(dir, f))
|
|
3865
4074
|
});
|
|
3866
4075
|
}
|
|
3867
4076
|
}
|
|
@@ -3874,26 +4083,25 @@ function createDataAnalyzeTool(ctx) {
|
|
|
3874
4083
|
summary: manifest.summary,
|
|
3875
4084
|
warnings: manifest.warnings
|
|
3876
4085
|
} : void 0,
|
|
3877
|
-
scriptPath: path.relative(ctx.workspacePath, scriptPath),
|
|
4086
|
+
scriptPath: path$1.relative(ctx.workspacePath, scriptPath),
|
|
3878
4087
|
runId
|
|
3879
4088
|
};
|
|
3880
|
-
ctx.onToolResult?.("data_analyze", payload);
|
|
3881
4089
|
return toAgentResult("data_analyze", { success: true, data: payload });
|
|
3882
4090
|
} catch (err) {
|
|
3883
4091
|
const errorDetail = err.stderr?.slice(0, 2e3) || err.message;
|
|
3884
4092
|
return toAgentResult("data_analyze", toolError("EXECUTION_FAILED", `Python execution failed: ${errorDetail}`, {
|
|
3885
4093
|
retryable: true,
|
|
3886
4094
|
suggestions: [
|
|
3887
|
-
`Review the generated script at ${path.relative(ctx.workspacePath, scriptPath)} for errors.`,
|
|
4095
|
+
`Review the generated script at ${path$1.relative(ctx.workspacePath, scriptPath)} for errors.`,
|
|
3888
4096
|
"Check that required Python packages (pandas, matplotlib, seaborn, etc.) are installed.",
|
|
3889
4097
|
"Try simplifying the analysis instructions."
|
|
3890
4098
|
],
|
|
3891
4099
|
context: {
|
|
3892
|
-
scriptPath: path.relative(ctx.workspacePath, scriptPath),
|
|
4100
|
+
scriptPath: path$1.relative(ctx.workspacePath, scriptPath),
|
|
3893
4101
|
runId
|
|
3894
4102
|
},
|
|
3895
4103
|
data: {
|
|
3896
|
-
scriptPath: path.relative(ctx.workspacePath, scriptPath),
|
|
4104
|
+
scriptPath: path$1.relative(ctx.workspacePath, scriptPath),
|
|
3897
4105
|
runId
|
|
3898
4106
|
}
|
|
3899
4107
|
}));
|
|
@@ -3901,133 +4109,2205 @@ function createDataAnalyzeTool(ctx) {
|
|
|
3901
4109
|
}
|
|
3902
4110
|
};
|
|
3903
4111
|
}
|
|
3904
|
-
function
|
|
3905
|
-
|
|
3906
|
-
|
|
3907
|
-
|
|
3908
|
-
|
|
3909
|
-
|
|
3910
|
-
|
|
3911
|
-
|
|
3912
|
-
|
|
3913
|
-
|
|
3914
|
-
|
|
3915
|
-
|
|
3916
|
-
|
|
3917
|
-
|
|
3918
|
-
|
|
3919
|
-
|
|
3920
|
-
|
|
3921
|
-
|
|
3922
|
-
|
|
3923
|
-
|
|
3924
|
-
|
|
3925
|
-
|
|
3926
|
-
|
|
3927
|
-
|
|
3928
|
-
|
|
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;
|
|
3929
4140
|
try {
|
|
3930
|
-
const
|
|
3931
|
-
|
|
3932
|
-
} catch
|
|
3933
|
-
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
3934
|
-
return toAgentResult(tool.name, toolError("EXECUTION_FAILED", errorMsg, {
|
|
3935
|
-
retryable: false,
|
|
3936
|
-
suggestions: ["Check tool parameters and try again."]
|
|
3937
|
-
}));
|
|
4141
|
+
const record = JSON.parse(trimmed);
|
|
4142
|
+
this.records.set(record.runId, record);
|
|
4143
|
+
} catch {
|
|
3938
4144
|
}
|
|
3939
4145
|
}
|
|
3940
|
-
};
|
|
3941
|
-
}
|
|
3942
|
-
function createResearchTools(ctx) {
|
|
3943
|
-
const tools = [];
|
|
3944
|
-
tools.push(createWebSearchTool(ctx));
|
|
3945
|
-
tools.push(createWebFetchTool(ctx));
|
|
3946
|
-
tools.push(createLiteratureSearchTool(ctx));
|
|
3947
|
-
tools.push(createConvertDocumentTool(ctx));
|
|
3948
|
-
tools.push(createDataAnalyzeTool(ctx));
|
|
3949
|
-
const artifactTools = createResearchMemoryTools({
|
|
3950
|
-
sessionId: ctx.sessionId,
|
|
3951
|
-
projectPath: ctx.projectPath
|
|
3952
|
-
});
|
|
3953
|
-
for (const tool of artifactTools) {
|
|
3954
|
-
tools.push(wrapResearchTool(tool));
|
|
3955
4146
|
}
|
|
3956
|
-
|
|
3957
|
-
|
|
3958
|
-
|
|
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;
|
|
3959
4154
|
}
|
|
3960
|
-
|
|
3961
|
-
|
|
3962
|
-
|
|
3963
|
-
|
|
3964
|
-
|
|
3965
|
-
|
|
3966
|
-
|
|
3967
|
-
|
|
3968
|
-
|
|
3969
|
-
|
|
3970
|
-
- Each memory should be atomic — one concept per entry.
|
|
3971
|
-
- If nothing is worth saving, return an empty array.
|
|
3972
|
-
|
|
3973
|
-
Return ONLY a JSON array (no markdown fences, no explanation):
|
|
3974
|
-
[{"type":"user|feedback|project|reference","name":"short-name","description":"one line","content":"full text"}]
|
|
3975
|
-
Or: []`;
|
|
3976
|
-
function simplifyMessages(messages, maxMessages) {
|
|
3977
|
-
const recent = messages.slice(-20);
|
|
3978
|
-
const result = [];
|
|
3979
|
-
for (const msg of recent) {
|
|
3980
|
-
if (msg.role !== "user" && msg.role !== "assistant") continue;
|
|
3981
|
-
let content = "";
|
|
3982
|
-
if (typeof msg.content === "string") {
|
|
3983
|
-
content = msg.content;
|
|
3984
|
-
} else if (Array.isArray(msg.content)) {
|
|
3985
|
-
for (const block of msg.content) {
|
|
3986
|
-
if (block && typeof block === "object" && "type" in block) {
|
|
3987
|
-
if (block.type === "text" && "text" in block) {
|
|
3988
|
-
const text = block.text;
|
|
3989
|
-
content += text.length > 500 ? text.slice(0, 500) + "...[truncated]" : text;
|
|
3990
|
-
content += "\n";
|
|
3991
|
-
} else if (block.type === "tool_use" && "name" in block) {
|
|
3992
|
-
content += `[Called ${block.name}]
|
|
3993
|
-
`;
|
|
3994
|
-
}
|
|
3995
|
-
}
|
|
3996
|
-
}
|
|
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();
|
|
3997
4165
|
}
|
|
3998
|
-
|
|
3999
|
-
|
|
4000
|
-
|
|
4001
|
-
|
|
4002
|
-
|
|
4003
|
-
|
|
4004
|
-
|
|
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();
|
|
4005
4212
|
}
|
|
4213
|
+
return updated;
|
|
4006
4214
|
}
|
|
4007
|
-
|
|
4008
|
-
|
|
4009
|
-
|
|
4010
|
-
|
|
4011
|
-
|
|
4012
|
-
|
|
4013
|
-
|
|
4014
|
-
|
|
4015
|
-
|
|
4016
|
-
|
|
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++;
|
|
4017
4233
|
}
|
|
4018
4234
|
}
|
|
4019
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");
|
|
4020
4247
|
}
|
|
4021
|
-
return false;
|
|
4022
4248
|
}
|
|
4023
|
-
|
|
4024
|
-
|
|
4025
|
-
|
|
4026
|
-
if (agentCalledSaveMemoryThisTurn(messages)) {
|
|
4027
|
-
if (config.debug) console.log("[Extractor] Skipped — agent called save-memory this turn");
|
|
4028
|
-
return;
|
|
4249
|
+
class ProcessSandbox {
|
|
4250
|
+
constructor() {
|
|
4251
|
+
this.name = "process";
|
|
4029
4252
|
}
|
|
4030
|
-
|
|
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
|
+
});
|
|
6005
|
+
}
|
|
6006
|
+
};
|
|
6007
|
+
}
|
|
6008
|
+
function createExecuteTool(runner, ctx) {
|
|
6009
|
+
return {
|
|
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
|
+
}),
|
|
6023
|
+
execute: async (_toolCallId, rawParams) => {
|
|
6024
|
+
const params = rawParams;
|
|
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").']
|
|
6029
|
+
}));
|
|
6030
|
+
}
|
|
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."]
|
|
6066
|
+
}));
|
|
6067
|
+
}
|
|
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."]
|
|
6091
|
+
}));
|
|
6092
|
+
}
|
|
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."));
|
|
6113
|
+
}
|
|
6114
|
+
const result = runner.getStatus(runId);
|
|
6115
|
+
if (!result) {
|
|
6116
|
+
return toAgentResult("local_compute_status", toolError("NOT_FOUND", `Run not found: ${runId}`));
|
|
6117
|
+
}
|
|
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."));
|
|
6138
|
+
}
|
|
6139
|
+
try {
|
|
6140
|
+
await runner.stop(runId);
|
|
6141
|
+
return toAgentResult("local_compute_stop", {
|
|
6142
|
+
success: true,
|
|
6143
|
+
data: { run_id: runId, status: "cancelled" }
|
|
6144
|
+
});
|
|
6145
|
+
} catch (err) {
|
|
6146
|
+
return toAgentResult("local_compute_stop", toolError(
|
|
6147
|
+
"EXECUTION_FAILED",
|
|
6148
|
+
err instanceof Error ? err.message : String(err)
|
|
6149
|
+
));
|
|
6150
|
+
}
|
|
6151
|
+
}
|
|
6152
|
+
};
|
|
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
|
+
}
|
|
6170
|
+
function wrapResearchTool(tool) {
|
|
6171
|
+
const properties = {};
|
|
6172
|
+
const jsonProps = tool.parameters.properties ?? {};
|
|
6173
|
+
const requiredFields = tool.parameters.required ?? [];
|
|
6174
|
+
for (const [key, prop] of Object.entries(jsonProps)) {
|
|
6175
|
+
const isRequired = requiredFields.includes(key);
|
|
6176
|
+
let schema;
|
|
6177
|
+
if (prop.enum) {
|
|
6178
|
+
schema = Type.Union(prop.enum.map((v) => Type.Literal(v)));
|
|
6179
|
+
} else if (prop.type === "number") {
|
|
6180
|
+
schema = Type.Number({ description: prop.description });
|
|
6181
|
+
} else if (prop.type === "array") {
|
|
6182
|
+
schema = Type.Array(Type.String(), { description: prop.description });
|
|
6183
|
+
} else {
|
|
6184
|
+
schema = Type.String({ description: prop.description });
|
|
6185
|
+
}
|
|
6186
|
+
properties[key] = isRequired ? schema : Type.Optional(schema);
|
|
6187
|
+
}
|
|
6188
|
+
const parametersSchema = Type.Object(properties);
|
|
6189
|
+
return {
|
|
6190
|
+
name: tool.name,
|
|
6191
|
+
description: tool.description,
|
|
6192
|
+
label: tool.name,
|
|
6193
|
+
parameters: parametersSchema,
|
|
6194
|
+
execute: async (_toolCallId, params, _signal) => {
|
|
6195
|
+
try {
|
|
6196
|
+
const result = await tool.execute(params);
|
|
6197
|
+
return toAgentResult(tool.name, result);
|
|
6198
|
+
} catch (err) {
|
|
6199
|
+
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
6200
|
+
return toAgentResult(tool.name, toolError("EXECUTION_FAILED", errorMsg, {
|
|
6201
|
+
retryable: false,
|
|
6202
|
+
suggestions: ["Check tool parameters and try again."]
|
|
6203
|
+
}));
|
|
6204
|
+
}
|
|
6205
|
+
}
|
|
6206
|
+
};
|
|
6207
|
+
}
|
|
6208
|
+
function createResearchTools(ctx) {
|
|
6209
|
+
const tools = [];
|
|
6210
|
+
const destroyers = [];
|
|
6211
|
+
tools.push(createWebSearchTool());
|
|
6212
|
+
tools.push(createWebFetchTool(ctx));
|
|
6213
|
+
tools.push(createLiteratureSearchTool(ctx));
|
|
6214
|
+
tools.push(createConvertDocumentTool(ctx));
|
|
6215
|
+
tools.push(createDataAnalyzeTool(ctx));
|
|
6216
|
+
const artifactTools = createResearchMemoryTools({
|
|
6217
|
+
sessionId: ctx.sessionId,
|
|
6218
|
+
projectPath: ctx.projectPath
|
|
6219
|
+
});
|
|
6220
|
+
for (const tool of artifactTools) {
|
|
6221
|
+
tools.push(wrapResearchTool(tool));
|
|
6222
|
+
}
|
|
6223
|
+
const structuredMemoryTools = createMemoryTools(ctx.projectPath);
|
|
6224
|
+
for (const tool of structuredMemoryTools) {
|
|
6225
|
+
tools.push(wrapResearchTool(tool));
|
|
6226
|
+
}
|
|
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
|
+
};
|
|
6241
|
+
}
|
|
6242
|
+
const VALID_TYPES = ["user", "feedback", "project", "reference"];
|
|
6243
|
+
const EXTRACTION_PROMPT = `Analyze the recent conversation above and extract information worth remembering across sessions.
|
|
6244
|
+
|
|
6245
|
+
Rules:
|
|
6246
|
+
- Only extract DURABLE, IMPORTANT information — things a future session would need.
|
|
6247
|
+
- Types: "user" (preferences/background), "feedback" (behavior corrections), "project" (decisions/deadlines), "reference" (external pointers).
|
|
6248
|
+
- Ignore text inside "[Previous conversation summary]" or "[Session context]" markers — that is old context, not new information.
|
|
6249
|
+
- Do NOT extract: routine task results, ephemeral details, things already in workspace files.
|
|
6250
|
+
- Each memory should be atomic — one concept per entry.
|
|
6251
|
+
- If nothing is worth saving, return an empty array.
|
|
6252
|
+
|
|
6253
|
+
Return ONLY a JSON array (no markdown fences, no explanation):
|
|
6254
|
+
[{"type":"user|feedback|project|reference","name":"short-name","description":"one line","content":"full text"}]
|
|
6255
|
+
Or: []`;
|
|
6256
|
+
function simplifyMessages(messages, maxMessages) {
|
|
6257
|
+
const recent = messages.slice(-20);
|
|
6258
|
+
const result = [];
|
|
6259
|
+
for (const msg of recent) {
|
|
6260
|
+
if (msg.role !== "user" && msg.role !== "assistant") continue;
|
|
6261
|
+
let content = "";
|
|
6262
|
+
if (typeof msg.content === "string") {
|
|
6263
|
+
content = msg.content;
|
|
6264
|
+
} else if (Array.isArray(msg.content)) {
|
|
6265
|
+
for (const block of msg.content) {
|
|
6266
|
+
if (block && typeof block === "object" && "type" in block) {
|
|
6267
|
+
if (block.type === "text" && "text" in block) {
|
|
6268
|
+
const text = block.text;
|
|
6269
|
+
content += text.length > 500 ? text.slice(0, 500) + "...[truncated]" : text;
|
|
6270
|
+
content += "\n";
|
|
6271
|
+
} else if (block.type === "tool_use" && "name" in block) {
|
|
6272
|
+
content += `[Called ${block.name}]
|
|
6273
|
+
`;
|
|
6274
|
+
}
|
|
6275
|
+
}
|
|
6276
|
+
}
|
|
6277
|
+
}
|
|
6278
|
+
content = content.trim();
|
|
6279
|
+
if (content) {
|
|
6280
|
+
result.push({
|
|
6281
|
+
role: msg.role,
|
|
6282
|
+
content: content.slice(0, 2e3),
|
|
6283
|
+
timestamp: Date.now()
|
|
6284
|
+
});
|
|
6285
|
+
}
|
|
6286
|
+
}
|
|
6287
|
+
return result;
|
|
6288
|
+
}
|
|
6289
|
+
function agentCalledSaveMemoryThisTurn(messages) {
|
|
6290
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
6291
|
+
const msg = messages[i];
|
|
6292
|
+
if (msg.role === "user") break;
|
|
6293
|
+
if (msg.role === "assistant" && Array.isArray(msg.content)) {
|
|
6294
|
+
for (const block of msg.content) {
|
|
6295
|
+
if (block && typeof block === "object" && "type" in block && block.type === "tool_use") {
|
|
6296
|
+
if (block.name === "save-memory") return true;
|
|
6297
|
+
}
|
|
6298
|
+
}
|
|
6299
|
+
}
|
|
6300
|
+
}
|
|
6301
|
+
return false;
|
|
6302
|
+
}
|
|
6303
|
+
async function maybeExtractMemories(config, messages, turnCount, extractEveryN = 3) {
|
|
6304
|
+
if (process.env.RESEARCH_COPILOT_AUTO_EXTRACT !== "1") return;
|
|
6305
|
+
if (turnCount % extractEveryN !== 0) return;
|
|
6306
|
+
if (agentCalledSaveMemoryThisTurn(messages)) {
|
|
6307
|
+
if (config.debug) console.log("[Extractor] Skipped — agent called save-memory this turn");
|
|
6308
|
+
return;
|
|
6309
|
+
}
|
|
6310
|
+
try {
|
|
4031
6311
|
const simplified = simplifyMessages(messages, 20);
|
|
4032
6312
|
if (simplified.length < 2) return;
|
|
4033
6313
|
simplified.push({
|
|
@@ -4045,33 +6325,57 @@ async function maybeExtractMemories(config, messages, turnCount, extractEveryN =
|
|
|
4045
6325
|
const textContent = result.content.find((c) => c.type === "text");
|
|
4046
6326
|
const text = textContent?.text?.trim() ?? "";
|
|
4047
6327
|
if (!text || text === "[]") return;
|
|
4048
|
-
|
|
4049
|
-
const
|
|
4050
|
-
|
|
6328
|
+
let jsonStr;
|
|
6329
|
+
const fenceMatch = text.match(/```(?:json)?\s*([\s\S]*?)```/);
|
|
6330
|
+
if (fenceMatch) {
|
|
6331
|
+
jsonStr = fenceMatch[1].trim();
|
|
6332
|
+
} else {
|
|
6333
|
+
const firstBracket = text.indexOf("[");
|
|
6334
|
+
const lastBracket = text.lastIndexOf("]");
|
|
6335
|
+
if (firstBracket !== -1 && lastBracket > firstBracket) {
|
|
6336
|
+
jsonStr = text.slice(firstBracket, lastBracket + 1);
|
|
6337
|
+
} else {
|
|
6338
|
+
jsonStr = text;
|
|
6339
|
+
}
|
|
6340
|
+
}
|
|
6341
|
+
let extracted;
|
|
6342
|
+
try {
|
|
6343
|
+
extracted = JSON.parse(jsonStr);
|
|
6344
|
+
} catch (parseErr) {
|
|
6345
|
+
if (config.debug) {
|
|
6346
|
+
console.warn("[Extractor] JSON parse failed:", parseErr, "Raw text:", text.slice(0, 200));
|
|
6347
|
+
}
|
|
6348
|
+
return;
|
|
6349
|
+
}
|
|
4051
6350
|
if (!Array.isArray(extracted) || extracted.length === 0) return;
|
|
4052
6351
|
ensureMemoryDir(config.projectPath);
|
|
4053
|
-
|
|
6352
|
+
const validEntries = [];
|
|
4054
6353
|
for (const mem of extracted) {
|
|
4055
6354
|
if (!mem.type || !mem.name || !mem.content) continue;
|
|
4056
6355
|
if (!VALID_TYPES.includes(mem.type)) continue;
|
|
4057
|
-
const
|
|
6356
|
+
const desc = (mem.description || "").trim();
|
|
6357
|
+
const contentFirstLine = mem.content.split("\n").find((l) => l.trim().length > 0) || "";
|
|
6358
|
+
const description = (desc || contentFirstLine.replace(/^#+\s*/, "").trim().slice(0, 120) || mem.name).replace(/\n/g, " ");
|
|
6359
|
+
validEntries.push({
|
|
4058
6360
|
frontmatter: {
|
|
4059
6361
|
name: mem.name,
|
|
4060
|
-
description
|
|
6362
|
+
description,
|
|
4061
6363
|
type: mem.type
|
|
4062
6364
|
},
|
|
4063
6365
|
content: mem.content,
|
|
4064
6366
|
filename: memoryFilename(mem.type, mem.name)
|
|
4065
|
-
};
|
|
4066
|
-
writeMemoryFile(config.projectPath, entry);
|
|
4067
|
-
written++;
|
|
6367
|
+
});
|
|
4068
6368
|
}
|
|
4069
|
-
if (
|
|
6369
|
+
if (validEntries.length === 0) return;
|
|
6370
|
+
await withIndexLock(() => {
|
|
6371
|
+
for (const entry of validEntries) {
|
|
6372
|
+
writeMemoryFile(config.projectPath, entry);
|
|
6373
|
+
}
|
|
4070
6374
|
const allEntries = listMemoryFiles(config.projectPath);
|
|
4071
6375
|
updateAgentMdIndex(config.projectPath, allEntries);
|
|
4072
|
-
|
|
4073
|
-
|
|
4074
|
-
}
|
|
6376
|
+
});
|
|
6377
|
+
if (config.debug) {
|
|
6378
|
+
console.log(`[Extractor] Saved ${validEntries.length} memories from conversation`);
|
|
4075
6379
|
}
|
|
4076
6380
|
} catch (err) {
|
|
4077
6381
|
if (config.debug) {
|
|
@@ -4123,7 +6427,7 @@ function discoverSkillFiles(rootDir) {
|
|
|
4123
6427
|
continue;
|
|
4124
6428
|
}
|
|
4125
6429
|
for (const entry of entries) {
|
|
4126
|
-
const abs = path.join(current.dir, entry.name);
|
|
6430
|
+
const abs = path$1.join(current.dir, entry.name);
|
|
4127
6431
|
if (entry.isFile() && entry.name === SKILL_FILE_NAME) {
|
|
4128
6432
|
files.push(abs);
|
|
4129
6433
|
continue;
|
|
@@ -4154,7 +6458,7 @@ function parseSkillFile(skillFile, displayPath, source) {
|
|
|
4154
6458
|
tags,
|
|
4155
6459
|
triggers,
|
|
4156
6460
|
path: displayPath,
|
|
4157
|
-
dir: path.dirname(skillFile),
|
|
6461
|
+
dir: path$1.dirname(skillFile),
|
|
4158
6462
|
source,
|
|
4159
6463
|
content
|
|
4160
6464
|
};
|
|
@@ -4170,34 +6474,34 @@ function inferCategory(name, description) {
|
|
|
4170
6474
|
return "General";
|
|
4171
6475
|
}
|
|
4172
6476
|
function loadBuiltinSkills() {
|
|
4173
|
-
const skillsRoot = _builtinSkillsRoot ?? path.dirname(fileURLToPath(import.meta.url));
|
|
6477
|
+
const skillsRoot = _builtinSkillsRoot ?? path$1.dirname(fileURLToPath(import.meta.url));
|
|
4174
6478
|
if (!fs.existsSync(skillsRoot) || !fs.statSync(skillsRoot).isDirectory()) {
|
|
4175
6479
|
return [];
|
|
4176
6480
|
}
|
|
4177
6481
|
const files = discoverSkillFiles(skillsRoot);
|
|
4178
6482
|
const byName = /* @__PURE__ */ new Map();
|
|
4179
6483
|
for (const file of files) {
|
|
4180
|
-
const entry = parseSkillFile(file, `[builtin] ${path.relative(skillsRoot, file)}`, "builtin");
|
|
6484
|
+
const entry = parseSkillFile(file, `[builtin] ${path$1.relative(skillsRoot, file)}`, "builtin");
|
|
4181
6485
|
if (entry) byName.set(entry.name, entry);
|
|
4182
6486
|
}
|
|
4183
6487
|
return Array.from(byName.values()).sort((a, b) => a.name.localeCompare(b.name));
|
|
4184
6488
|
}
|
|
4185
6489
|
function loadWorkspaceSkills(workspacePath) {
|
|
4186
|
-
const skillRoot = path.resolve(workspacePath, ".research-pilot", "skills");
|
|
6490
|
+
const skillRoot = path$1.resolve(workspacePath, ".research-pilot", "skills");
|
|
4187
6491
|
if (!fs.existsSync(skillRoot) || !fs.statSync(skillRoot).isDirectory()) {
|
|
4188
6492
|
return [];
|
|
4189
6493
|
}
|
|
4190
6494
|
const files = discoverSkillFiles(skillRoot);
|
|
4191
|
-
const entries = files.map((file) => parseSkillFile(file, path.relative(workspacePath, file), "workspace")).filter((entry) => entry !== null);
|
|
6495
|
+
const entries = files.map((file) => parseSkillFile(file, path$1.relative(workspacePath, file), "workspace")).filter((entry) => entry !== null);
|
|
4192
6496
|
return entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
4193
6497
|
}
|
|
4194
6498
|
function loadUserSkills() {
|
|
4195
|
-
const userRoot = path.resolve(os.homedir(), ".research-pilot", "skills");
|
|
6499
|
+
const userRoot = path$1.resolve(os.homedir(), ".research-pilot", "skills");
|
|
4196
6500
|
if (!fs.existsSync(userRoot) || !fs.statSync(userRoot).isDirectory()) {
|
|
4197
6501
|
return [];
|
|
4198
6502
|
}
|
|
4199
6503
|
const files = discoverSkillFiles(userRoot);
|
|
4200
|
-
const entries = files.map((file) => parseSkillFile(file, `[user] ~/.research-pilot/skills/${path.relative(userRoot, file)}`, "user")).filter((entry) => entry !== null);
|
|
6504
|
+
const entries = files.map((file) => parseSkillFile(file, `[user] ~/.research-pilot/skills/${path$1.relative(userRoot, file)}`, "user")).filter((entry) => entry !== null);
|
|
4201
6505
|
return entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
4202
6506
|
}
|
|
4203
6507
|
function loadAllSkills(workspacePath) {
|
|
@@ -4242,7 +6546,7 @@ function resolveSkillDependencies(allSkills, directSelection) {
|
|
|
4242
6546
|
return result;
|
|
4243
6547
|
}
|
|
4244
6548
|
function readEnabledSkills(workspacePath) {
|
|
4245
|
-
const configPath = path.resolve(workspacePath, ".research-pilot", "skills-config.json");
|
|
6549
|
+
const configPath = path$1.resolve(workspacePath, ".research-pilot", "skills-config.json");
|
|
4246
6550
|
try {
|
|
4247
6551
|
const raw = fs.readFileSync(configPath, "utf8");
|
|
4248
6552
|
const config = JSON.parse(raw);
|
|
@@ -4252,9 +6556,9 @@ function readEnabledSkills(workspacePath) {
|
|
|
4252
6556
|
return null;
|
|
4253
6557
|
}
|
|
4254
6558
|
function writeEnabledSkills(workspacePath, enabledSkills) {
|
|
4255
|
-
const configDir = path.resolve(workspacePath, ".research-pilot");
|
|
6559
|
+
const configDir = path$1.resolve(workspacePath, ".research-pilot");
|
|
4256
6560
|
if (!fs.existsSync(configDir)) fs.mkdirSync(configDir, { recursive: true });
|
|
4257
|
-
const configPath = path.join(configDir, "skills-config.json");
|
|
6561
|
+
const configPath = path$1.join(configDir, "skills-config.json");
|
|
4258
6562
|
fs.writeFileSync(configPath, JSON.stringify({ enabledSkills }, null, 2), "utf8");
|
|
4259
6563
|
}
|
|
4260
6564
|
function buildSkillManifests(workspacePath) {
|
|
@@ -4278,15 +6582,15 @@ function buildSkillManifests(workspacePath) {
|
|
|
4278
6582
|
});
|
|
4279
6583
|
}
|
|
4280
6584
|
function installSkillToWorkspace(workspacePath, skillName, skillDir) {
|
|
4281
|
-
const destDir = path.resolve(workspacePath, ".research-pilot", "skills", skillName);
|
|
6585
|
+
const destDir = path$1.resolve(workspacePath, ".research-pilot", "skills", skillName);
|
|
4282
6586
|
if (!fs.existsSync(destDir)) fs.mkdirSync(destDir, { recursive: true });
|
|
4283
6587
|
copyDirSync(skillDir, destDir);
|
|
4284
6588
|
}
|
|
4285
6589
|
function copyDirSync(src, dest) {
|
|
4286
6590
|
const entries = fs.readdirSync(src, { withFileTypes: true });
|
|
4287
6591
|
for (const entry of entries) {
|
|
4288
|
-
const srcPath = path.join(src, entry.name);
|
|
4289
|
-
const destPath = path.join(dest, entry.name);
|
|
6592
|
+
const srcPath = path$1.join(src, entry.name);
|
|
6593
|
+
const destPath = path$1.join(dest, entry.name);
|
|
4290
6594
|
if (entry.isDirectory()) {
|
|
4291
6595
|
if (!fs.existsSync(destPath)) fs.mkdirSync(destPath, { recursive: true });
|
|
4292
6596
|
copyDirSync(srcPath, destPath);
|
|
@@ -4479,6 +6783,7 @@ function writeExplainSnapshot(projectPath, snapshot) {
|
|
|
4479
6783
|
async function createCoordinator(config) {
|
|
4480
6784
|
const {
|
|
4481
6785
|
apiKey,
|
|
6786
|
+
getApiKeyOverride,
|
|
4482
6787
|
model: modelId,
|
|
4483
6788
|
projectPath = process.cwd(),
|
|
4484
6789
|
debug = false,
|
|
@@ -4487,9 +6792,11 @@ async function createCoordinator(config) {
|
|
|
4487
6792
|
onStream,
|
|
4488
6793
|
onToolCall,
|
|
4489
6794
|
onToolResult,
|
|
6795
|
+
onToolProgress,
|
|
4490
6796
|
onUsage,
|
|
4491
6797
|
onSkillLoaded
|
|
4492
6798
|
} = config;
|
|
6799
|
+
const resolveApiKey = getApiKeyOverride ?? (async () => apiKey);
|
|
4493
6800
|
let turnCount = 0;
|
|
4494
6801
|
let activeTurnToolCallCount = null;
|
|
4495
6802
|
const turnHistory = [];
|
|
@@ -4534,13 +6841,14 @@ async function createCoordinator(config) {
|
|
|
4534
6841
|
const routerByProvider = {
|
|
4535
6842
|
anthropic: "claude-haiku-4-5-20251001",
|
|
4536
6843
|
openai: "gpt-5.4-nano",
|
|
6844
|
+
"openai-codex": "gpt-5.4-nano",
|
|
4537
6845
|
google: "gemini-2.0-flash-lite"
|
|
4538
6846
|
};
|
|
4539
6847
|
let mainProvider = null;
|
|
4540
6848
|
if (modelId) {
|
|
4541
6849
|
const parts = modelId.split(":");
|
|
4542
6850
|
if (parts.length === 2) {
|
|
4543
|
-
mainProvider = parts[0];
|
|
6851
|
+
mainProvider = parts[0] === "openai-codex" ? "openai-codex" : parts[0];
|
|
4544
6852
|
} else {
|
|
4545
6853
|
mainProvider = modelId.startsWith("claude-") ? "anthropic" : modelId.startsWith("gpt-") || modelId.startsWith("o3") || modelId.startsWith("o4") ? "openai" : modelId.startsWith("gemini-") ? "google" : null;
|
|
4546
6854
|
}
|
|
@@ -4558,11 +6866,11 @@ async function createCoordinator(config) {
|
|
|
4558
6866
|
}
|
|
4559
6867
|
}
|
|
4560
6868
|
}
|
|
4561
|
-
const wrappedOnToolResult = (tool, result, args) => {
|
|
6869
|
+
const wrappedOnToolResult = (tool, result, args, toolCallId) => {
|
|
4562
6870
|
if (activeTurnToolCallCount !== null) {
|
|
4563
6871
|
activeTurnToolCallCount++;
|
|
4564
6872
|
}
|
|
4565
|
-
onToolResult?.(tool, result, args);
|
|
6873
|
+
onToolResult?.(tool, result, args, toolCallId);
|
|
4566
6874
|
};
|
|
4567
6875
|
const toolCtx = {
|
|
4568
6876
|
workspacePath: projectPath,
|
|
@@ -4570,17 +6878,18 @@ async function createCoordinator(config) {
|
|
|
4570
6878
|
projectPath,
|
|
4571
6879
|
callLlm: async (systemPrompt, userContent) => {
|
|
4572
6880
|
if (!piModel) throw new Error("No model available for sub-call");
|
|
6881
|
+
const currentKey = await resolveApiKey();
|
|
4573
6882
|
const result = await completeSimple(piModel, {
|
|
4574
6883
|
systemPrompt,
|
|
4575
6884
|
messages: [{ role: "user", content: userContent, timestamp: Date.now() }]
|
|
4576
|
-
}, { maxTokens: 4096, apiKey });
|
|
6885
|
+
}, { maxTokens: 4096, apiKey: currentKey });
|
|
4577
6886
|
const textContent = result.content.find((c) => c.type === "text");
|
|
4578
6887
|
return textContent?.text ?? "";
|
|
4579
6888
|
},
|
|
4580
6889
|
onToolCall,
|
|
4581
6890
|
onToolResult: wrappedOnToolResult
|
|
4582
6891
|
};
|
|
4583
|
-
const researchAgentTools = createResearchTools(toolCtx);
|
|
6892
|
+
const { tools: researchAgentTools, destroy: destroyResearchTools } = createResearchTools(toolCtx);
|
|
4584
6893
|
const codingTools = createCodingTools(projectPath);
|
|
4585
6894
|
const grepTool = createGrepTool(projectPath);
|
|
4586
6895
|
const findTool = createFindTool(projectPath);
|
|
@@ -4603,17 +6912,17 @@ async function createCoordinator(config) {
|
|
|
4603
6912
|
loadSkillTool
|
|
4604
6913
|
];
|
|
4605
6914
|
const skillsCatalog = buildSkillsCatalogPrompt(skills);
|
|
4606
|
-
const
|
|
6915
|
+
const baseSystemPrompt = SYSTEM_PROMPT + (skillsCatalog ? "\n\n" + skillsCatalog : "");
|
|
4607
6916
|
let compactionSummary;
|
|
4608
6917
|
const agent = new Agent({
|
|
4609
6918
|
initialState: {
|
|
4610
|
-
systemPrompt:
|
|
6919
|
+
systemPrompt: baseSystemPrompt,
|
|
4611
6920
|
model: piModel ?? void 0,
|
|
4612
6921
|
tools: allTools,
|
|
4613
6922
|
thinkingLevel: reasoningEffort === "high" ? "high" : reasoningEffort === "medium" ? "medium" : "low"
|
|
4614
6923
|
},
|
|
4615
6924
|
sessionId,
|
|
4616
|
-
getApiKey:
|
|
6925
|
+
getApiKey: resolveApiKey,
|
|
4617
6926
|
// ── Context compaction via transformContext ──
|
|
4618
6927
|
// Before each LLM call, check if accumulated messages exceed the model's
|
|
4619
6928
|
// context window. If so, summarize old messages and keep only recent ones.
|
|
@@ -4652,11 +6961,12 @@ async function createCoordinator(config) {
|
|
|
4652
6961
|
const messagesToSummarize = messages.slice(0, cutIndex);
|
|
4653
6962
|
const messagesToKeep = messages.slice(cutIndex);
|
|
4654
6963
|
try {
|
|
6964
|
+
const currentKey = await resolveApiKey();
|
|
4655
6965
|
const summary = await generateSummary(
|
|
4656
6966
|
messagesToSummarize,
|
|
4657
6967
|
piModel,
|
|
4658
6968
|
settings.reserveTokens,
|
|
4659
|
-
|
|
6969
|
+
currentKey,
|
|
4660
6970
|
signal,
|
|
4661
6971
|
void 0,
|
|
4662
6972
|
compactionSummary
|
|
@@ -4685,14 +6995,14 @@ The conversation continues below.`,
|
|
|
4685
6995
|
}
|
|
4686
6996
|
},
|
|
4687
6997
|
beforeToolCall: async (ctx) => {
|
|
4688
|
-
onToolCall?.(ctx.toolCall.name, ctx.args);
|
|
6998
|
+
onToolCall?.(ctx.toolCall.name, ctx.args, ctx.toolCall.id);
|
|
4689
6999
|
if (debug) {
|
|
4690
7000
|
console.log(` [Tool] ${ctx.toolCall.name}(${JSON.stringify(ctx.args).slice(0, 120)}...)`);
|
|
4691
7001
|
}
|
|
4692
7002
|
return void 0;
|
|
4693
7003
|
},
|
|
4694
7004
|
afterToolCall: async (ctx) => {
|
|
4695
|
-
wrappedOnToolResult(ctx.toolCall.name, ctx.result, ctx.args);
|
|
7005
|
+
wrappedOnToolResult(ctx.toolCall.name, ctx.result, ctx.args, ctx.toolCall.id);
|
|
4696
7006
|
if (ctx.toolCall.name === "load_skill" && onSkillLoaded) {
|
|
4697
7007
|
const args = ctx.args;
|
|
4698
7008
|
const result = ctx.result;
|
|
@@ -4703,7 +7013,7 @@ The conversation continues below.`,
|
|
|
4703
7013
|
return void 0;
|
|
4704
7014
|
}
|
|
4705
7015
|
});
|
|
4706
|
-
if (onStream || onUsage) {
|
|
7016
|
+
if (onStream || onUsage || onToolProgress) {
|
|
4707
7017
|
agent.subscribe((event) => {
|
|
4708
7018
|
if (event.type === "message_update" && onStream) {
|
|
4709
7019
|
if (event.assistantMessageEvent.type === "text_delta") {
|
|
@@ -4717,6 +7027,15 @@ The conversation continues below.`,
|
|
|
4717
7027
|
onUsage(usage, usage.cost);
|
|
4718
7028
|
}
|
|
4719
7029
|
}
|
|
7030
|
+
if (onToolProgress) {
|
|
7031
|
+
if (event.type === "tool_execution_start") {
|
|
7032
|
+
onToolProgress(event.toolName, event.toolCallId, "start", { args: event.args });
|
|
7033
|
+
} else if (event.type === "tool_execution_update") {
|
|
7034
|
+
onToolProgress(event.toolName, event.toolCallId, "update", { partialResult: event.partialResult });
|
|
7035
|
+
} else if (event.type === "tool_execution_end") {
|
|
7036
|
+
onToolProgress(event.toolName, event.toolCallId, "end", { result: event.result, isError: event.isError });
|
|
7037
|
+
}
|
|
7038
|
+
}
|
|
4720
7039
|
});
|
|
4721
7040
|
}
|
|
4722
7041
|
async function clearSessionMemory() {
|
|
@@ -4736,6 +7055,7 @@ The conversation continues below.`,
|
|
|
4736
7055
|
const historyText = turnHistory.map((t, i) => `Turn ${turnCount - turnHistory.length + i + 1}: User: ${t.userMessage}
|
|
4737
7056
|
Assistant: ${t.response}`).join("\n\n");
|
|
4738
7057
|
try {
|
|
7058
|
+
const currentKey = await resolveApiKey();
|
|
4739
7059
|
const result = await completeSimple(intentRouterModel, {
|
|
4740
7060
|
systemPrompt: "You summarize research conversations concisely. Output JSON with keys: summary (string), topicsDiscussed (string[]), openQuestions (string[]). Output ONLY valid JSON.",
|
|
4741
7061
|
messages: [{
|
|
@@ -4747,7 +7067,7 @@ ${historyText}`,
|
|
|
4747
7067
|
}]
|
|
4748
7068
|
}, {
|
|
4749
7069
|
maxTokens: 512,
|
|
4750
|
-
apiKey
|
|
7070
|
+
apiKey: currentKey
|
|
4751
7071
|
});
|
|
4752
7072
|
const textContent = result.content.find((c) => c.type === "text");
|
|
4753
7073
|
const text = textContent?.text?.trim() ?? "";
|
|
@@ -4773,12 +7093,20 @@ ${historyText}`,
|
|
|
4773
7093
|
}
|
|
4774
7094
|
}
|
|
4775
7095
|
}
|
|
7096
|
+
if (process.env.ENABLE_LOCAL_COMPUTE === "1") {
|
|
7097
|
+
probeStaticProfile().then((profile) => {
|
|
7098
|
+
const envGuidance = generateAgentGuidance(profile);
|
|
7099
|
+
agent.setSystemPrompt(baseSystemPrompt + "\n\n" + envGuidance);
|
|
7100
|
+
}).catch(() => {
|
|
7101
|
+
});
|
|
7102
|
+
}
|
|
4776
7103
|
return {
|
|
4777
7104
|
agent,
|
|
4778
7105
|
async chat(message, mentions, images) {
|
|
4779
7106
|
try {
|
|
4780
7107
|
const intents = detectIntentsByRules(message);
|
|
4781
|
-
const
|
|
7108
|
+
const currentKey = await resolveApiKey();
|
|
7109
|
+
const matchedSkillNames = await matchSkillsWithLLM(intentRouterModel, currentKey, message, skills);
|
|
4782
7110
|
const matchedSkills = matchedSkillNames.map((name) => skills.find((s) => s.name === name)).filter((s) => s !== void 0);
|
|
4783
7111
|
for (const s of matchedSkills) {
|
|
4784
7112
|
onSkillLoaded?.(s.name);
|
|
@@ -4819,31 +7147,24 @@ ${historyText}`,
|
|
|
4819
7147
|
console.log(`[Chat] Matched skills: ${skillList}`);
|
|
4820
7148
|
console.log(`[Chat] Sending message to agent (${mentions?.filter((m) => !m.error).length ?? 0} mentions, summary=${!!latestSummary})...`);
|
|
4821
7149
|
}
|
|
4822
|
-
let enrichedSystem =
|
|
7150
|
+
let enrichedSystem = baseSystemPrompt;
|
|
4823
7151
|
if (agentMdContent) {
|
|
4824
7152
|
enrichedSystem = `${enrichedSystem}
|
|
4825
7153
|
|
|
4826
7154
|
## User Instructions (agent.md)
|
|
4827
7155
|
|
|
4828
7156
|
${agentMdContent}`;
|
|
4829
|
-
}
|
|
4830
|
-
if (skillSummariesPrompt) {
|
|
4831
|
-
enrichedSystem = `${enrichedSystem}
|
|
4832
|
-
|
|
4833
|
-
${skillSummariesPrompt}`;
|
|
4834
7157
|
}
|
|
4835
7158
|
agent.setSystemPrompt(enrichedSystem);
|
|
4836
|
-
|
|
4837
|
-
if (
|
|
4838
|
-
|
|
4839
|
-
|
|
4840
|
-
|
|
4841
|
-
userMessage = `${contextParts.join("\n\n")}
|
|
7159
|
+
const contextParts = [];
|
|
7160
|
+
if (summaryContext) contextParts.push(summaryContext);
|
|
7161
|
+
if (skillSummariesPrompt) contextParts.push(skillSummariesPrompt);
|
|
7162
|
+
if (mentionContext) contextParts.push(mentionContext);
|
|
7163
|
+
let userMessage = contextParts.length > 0 ? `${contextParts.join("\n\n")}
|
|
4842
7164
|
|
|
4843
7165
|
---
|
|
4844
7166
|
|
|
4845
|
-
${message}
|
|
4846
|
-
}
|
|
7167
|
+
${message}` : message;
|
|
4847
7168
|
let perTurnToolCallCount = 0;
|
|
4848
7169
|
activeTurnToolCallCount = 0;
|
|
4849
7170
|
try {
|
|
@@ -4889,8 +7210,9 @@ ${message}`;
|
|
|
4889
7210
|
});
|
|
4890
7211
|
if (turnHistory.length > 8) turnHistory.shift();
|
|
4891
7212
|
void maybeGenerateSummary();
|
|
7213
|
+
const memoryKey = await resolveApiKey();
|
|
4892
7214
|
void maybeExtractMemories(
|
|
4893
|
-
{ projectPath, model: piModel, apiKey, systemPrompt: enrichedSystem, debug },
|
|
7215
|
+
{ projectPath, model: piModel, apiKey: memoryKey, systemPrompt: enrichedSystem, debug },
|
|
4894
7216
|
agent.state.messages,
|
|
4895
7217
|
turnCount
|
|
4896
7218
|
);
|
|
@@ -4913,6 +7235,7 @@ ${message}`;
|
|
|
4913
7235
|
clearSessionMemory,
|
|
4914
7236
|
async destroy() {
|
|
4915
7237
|
agent.abort();
|
|
7238
|
+
await destroyResearchTools();
|
|
4916
7239
|
}
|
|
4917
7240
|
};
|
|
4918
7241
|
}
|
|
@@ -4949,10 +7272,10 @@ function sessionSummaryGet(projectPath, sessionId) {
|
|
|
4949
7272
|
}
|
|
4950
7273
|
}
|
|
4951
7274
|
class RateLimiter {
|
|
4952
|
-
constructor(
|
|
7275
|
+
constructor(configs2) {
|
|
4953
7276
|
this.timestamps = /* @__PURE__ */ new Map();
|
|
4954
7277
|
this.activeCounts = /* @__PURE__ */ new Map();
|
|
4955
|
-
this.configs =
|
|
7278
|
+
this.configs = configs2;
|
|
4956
7279
|
}
|
|
4957
7280
|
/**
|
|
4958
7281
|
* Wait until a request slot is available for the given source.
|
|
@@ -5565,7 +7888,7 @@ function parseMentions(message) {
|
|
|
5565
7888
|
return { cleanMessage, mentions };
|
|
5566
7889
|
}
|
|
5567
7890
|
function getCacheKey(filePath, mtime) {
|
|
5568
|
-
const hash = createHash
|
|
7891
|
+
const hash = createHash("sha256").update(`${filePath}:${mtime}`).digest("hex").slice(0, 16);
|
|
5569
7892
|
const name = basename(filePath).replace(/[^a-zA-Z0-9.-]/g, "_");
|
|
5570
7893
|
return `${name}-${hash}.json`;
|
|
5571
7894
|
}
|
|
@@ -6037,6 +8360,10 @@ class RealtimeBuffer {
|
|
|
6037
8360
|
isStreaming = false;
|
|
6038
8361
|
progressItems = [];
|
|
6039
8362
|
activityEvents = [];
|
|
8363
|
+
/** Tool events for chat-inline rendering (mirrors tool-events-store) */
|
|
8364
|
+
toolEvents = [];
|
|
8365
|
+
/** Track tool-call start times keyed by toolCallId for duration computation */
|
|
8366
|
+
toolCallStartTimes = /* @__PURE__ */ new Map();
|
|
6040
8367
|
/** Append a streaming text chunk (called from onStream callback) */
|
|
6041
8368
|
appendChunk(chunk) {
|
|
6042
8369
|
this.streamingText += chunk;
|
|
@@ -6051,18 +8378,45 @@ class RealtimeBuffer {
|
|
|
6051
8378
|
this.progressItems.push(item);
|
|
6052
8379
|
}
|
|
6053
8380
|
}
|
|
6054
|
-
/** Record an activity event */
|
|
8381
|
+
/** Record an activity event and track tool-call start times */
|
|
6055
8382
|
pushActivity(event) {
|
|
8383
|
+
if (event.type === "tool-call" && event.toolCallId) {
|
|
8384
|
+
this.toolCallStartTimes.set(event.toolCallId, Date.now());
|
|
8385
|
+
}
|
|
6056
8386
|
this.activityEvents.push(event);
|
|
6057
8387
|
}
|
|
8388
|
+
/** Record a tool event for chat-inline rendering */
|
|
8389
|
+
pushToolEvent(event) {
|
|
8390
|
+
this.toolEvents.push(event);
|
|
8391
|
+
}
|
|
8392
|
+
/** Update a tool event by toolCallId (for tool-result merge) */
|
|
8393
|
+
updateToolEvent(toolCallId, patch) {
|
|
8394
|
+
const idx = this.toolEvents.findLastIndex((e) => e.toolCallId === toolCallId);
|
|
8395
|
+
if (idx !== -1) {
|
|
8396
|
+
this.toolEvents[idx] = { ...this.toolEvents[idx], ...patch };
|
|
8397
|
+
}
|
|
8398
|
+
}
|
|
8399
|
+
/** Clear tool events (on new run or finalize) */
|
|
8400
|
+
clearToolEvents() {
|
|
8401
|
+
this.toolEvents = [];
|
|
8402
|
+
}
|
|
8403
|
+
/** Pop and return the start time for a tool-call, or undefined if not found */
|
|
8404
|
+
popToolCallStartTime(toolCallId) {
|
|
8405
|
+
const t = this.toolCallStartTimes.get(toolCallId);
|
|
8406
|
+
if (t !== void 0) this.toolCallStartTimes.delete(toolCallId);
|
|
8407
|
+
return t;
|
|
8408
|
+
}
|
|
6058
8409
|
/** Clear progress and activity (called on project close or explicit reset) */
|
|
6059
8410
|
clearRun() {
|
|
6060
8411
|
this.progressItems = [];
|
|
6061
8412
|
this.activityEvents = [];
|
|
8413
|
+
this.toolEvents = [];
|
|
6062
8414
|
}
|
|
6063
8415
|
/** Clear only activity events (called on new agent run) */
|
|
6064
8416
|
clearActivity() {
|
|
6065
8417
|
this.activityEvents = [];
|
|
8418
|
+
this.toolEvents = [];
|
|
8419
|
+
this.toolCallStartTimes.clear();
|
|
6066
8420
|
}
|
|
6067
8421
|
/** Mark streaming finished (called on agent:done) */
|
|
6068
8422
|
finishStreaming() {
|
|
@@ -6075,6 +8429,8 @@ class RealtimeBuffer {
|
|
|
6075
8429
|
this.isStreaming = false;
|
|
6076
8430
|
this.progressItems = [];
|
|
6077
8431
|
this.activityEvents = [];
|
|
8432
|
+
this.toolEvents = [];
|
|
8433
|
+
this.toolCallStartTimes.clear();
|
|
6078
8434
|
}
|
|
6079
8435
|
/** Return a snapshot the renderer can use to hydrate stores */
|
|
6080
8436
|
getSnapshot() {
|
|
@@ -6082,17 +8438,366 @@ class RealtimeBuffer {
|
|
|
6082
8438
|
streamingText: this.streamingText,
|
|
6083
8439
|
isStreaming: this.isStreaming,
|
|
6084
8440
|
progressItems: [...this.progressItems],
|
|
6085
|
-
activityEvents: [...this.activityEvents]
|
|
8441
|
+
activityEvents: [...this.activityEvents],
|
|
8442
|
+
toolEvents: [...this.toolEvents]
|
|
6086
8443
|
};
|
|
6087
8444
|
}
|
|
6088
8445
|
}
|
|
6089
8446
|
function createRealtimeBuffer() {
|
|
6090
8447
|
return new RealtimeBuffer();
|
|
6091
8448
|
}
|
|
8449
|
+
function getFileName(path2) {
|
|
8450
|
+
if (!path2) return "";
|
|
8451
|
+
const parts = path2.replace(/\\/g, "/").split("/");
|
|
8452
|
+
return parts[parts.length - 1] || path2;
|
|
8453
|
+
}
|
|
8454
|
+
function truncStr(s, max) {
|
|
8455
|
+
if (!s) return "";
|
|
8456
|
+
return s.length > max ? s.slice(0, max - 3) + "..." : s;
|
|
8457
|
+
}
|
|
8458
|
+
function safeRecord(obj) {
|
|
8459
|
+
return obj && typeof obj === "object" ? obj : {};
|
|
8460
|
+
}
|
|
8461
|
+
function extractResultText(result) {
|
|
8462
|
+
const r = safeRecord(result);
|
|
8463
|
+
const content = r.content;
|
|
8464
|
+
return content?.[0]?.text || "";
|
|
8465
|
+
}
|
|
8466
|
+
function lastNLines(text, n) {
|
|
8467
|
+
const lines = text.split("\n").filter(Boolean);
|
|
8468
|
+
return lines.slice(-n).join("\n");
|
|
8469
|
+
}
|
|
8470
|
+
const configs = [
|
|
8471
|
+
// ── File tools ────────────────────────
|
|
8472
|
+
{
|
|
8473
|
+
name: "read",
|
|
8474
|
+
displayName: "Read",
|
|
8475
|
+
icon: "FileText",
|
|
8476
|
+
category: "file",
|
|
8477
|
+
formatCallSummary: (a) => {
|
|
8478
|
+
const path2 = a.path || "";
|
|
8479
|
+
const offset = a.offset;
|
|
8480
|
+
const limit = a.limit;
|
|
8481
|
+
const suffix = offset ? ` · lines ${offset}-${offset + (limit || 2e3)}` : "";
|
|
8482
|
+
return `${getFileName(path2)}${suffix}`;
|
|
8483
|
+
},
|
|
8484
|
+
formatCallDetail: (a) => ({ path: a.path, offset: a.offset, limit: a.limit }),
|
|
8485
|
+
formatResultSummary: (result) => {
|
|
8486
|
+
const text = extractResultText(result);
|
|
8487
|
+
const lineCount = text ? text.split("\n").length : 0;
|
|
8488
|
+
return lineCount ? `${lineCount} lines` : "Read completed";
|
|
8489
|
+
},
|
|
8490
|
+
formatResultDetail: (result) => {
|
|
8491
|
+
const text = extractResultText(result);
|
|
8492
|
+
return { lineCount: text ? text.split("\n").length : 0 };
|
|
8493
|
+
}
|
|
8494
|
+
},
|
|
8495
|
+
{
|
|
8496
|
+
name: "write",
|
|
8497
|
+
displayName: "Write",
|
|
8498
|
+
icon: "FileText",
|
|
8499
|
+
category: "file",
|
|
8500
|
+
formatCallSummary: (a) => getFileName(a.path || ""),
|
|
8501
|
+
formatCallDetail: (a) => ({ path: a.path }),
|
|
8502
|
+
formatResultSummary: (_, a) => `Written: ${getFileName(a?.path || "")}`,
|
|
8503
|
+
formatResultDetail: (_, a) => ({ path: a?.path })
|
|
8504
|
+
},
|
|
8505
|
+
{
|
|
8506
|
+
name: "edit",
|
|
8507
|
+
displayName: "Edit",
|
|
8508
|
+
icon: "FileText",
|
|
8509
|
+
category: "file",
|
|
8510
|
+
formatCallSummary: (a) => getFileName(a.path || ""),
|
|
8511
|
+
formatCallDetail: (a) => ({ path: a.path }),
|
|
8512
|
+
formatResultSummary: (_, a) => `Edited: ${getFileName(a?.path || "")}`,
|
|
8513
|
+
formatResultDetail: (_, a) => ({ path: a?.path })
|
|
8514
|
+
},
|
|
8515
|
+
// ── Code tools ────────────────────────
|
|
8516
|
+
{
|
|
8517
|
+
name: "bash",
|
|
8518
|
+
displayName: "Bash",
|
|
8519
|
+
icon: "Terminal",
|
|
8520
|
+
category: "code",
|
|
8521
|
+
formatCallSummary: (a) => {
|
|
8522
|
+
const cmd = a.command || "";
|
|
8523
|
+
return cmd.length > 60 ? `$ ${cmd.slice(0, 57)}...` : `$ ${cmd}`;
|
|
8524
|
+
},
|
|
8525
|
+
formatCallDetail: (a) => ({ command: truncStr(a.command, 200) }),
|
|
8526
|
+
formatResultSummary: () => "Command completed",
|
|
8527
|
+
formatResultDetail: (result) => {
|
|
8528
|
+
const text = extractResultText(result);
|
|
8529
|
+
const lines = text.split("\n").filter(Boolean);
|
|
8530
|
+
return { outputLines: lines.length, outputPreview: truncStr(lastNLines(text, 3), 200) };
|
|
8531
|
+
},
|
|
8532
|
+
formatProgress: (partial) => {
|
|
8533
|
+
const text = partial?.content?.[0]?.text;
|
|
8534
|
+
if (typeof text === "string" && text.length > 0) {
|
|
8535
|
+
return lastNLines(text, 5);
|
|
8536
|
+
}
|
|
8537
|
+
return void 0;
|
|
8538
|
+
}
|
|
8539
|
+
},
|
|
8540
|
+
// ── Search tools ────────────────────────
|
|
8541
|
+
{
|
|
8542
|
+
name: "grep",
|
|
8543
|
+
displayName: "Search",
|
|
8544
|
+
icon: "Search",
|
|
8545
|
+
category: "search",
|
|
8546
|
+
formatCallSummary: (a) => `"${truncStr(a.pattern, 30)}"${a.path ? ` in ${a.path}` : ""}`,
|
|
8547
|
+
formatCallDetail: (a) => ({ pattern: a.pattern, path: a.path, glob: a.glob }),
|
|
8548
|
+
formatResultSummary: (result) => {
|
|
8549
|
+
const text = extractResultText(result);
|
|
8550
|
+
const count = text.split("\n").filter(Boolean).length;
|
|
8551
|
+
return `${count} results`;
|
|
8552
|
+
},
|
|
8553
|
+
formatResultDetail: (result) => {
|
|
8554
|
+
const text = extractResultText(result);
|
|
8555
|
+
return { matchCount: text.split("\n").filter(Boolean).length };
|
|
8556
|
+
}
|
|
8557
|
+
},
|
|
8558
|
+
{
|
|
8559
|
+
name: "glob",
|
|
8560
|
+
displayName: "Find Files",
|
|
8561
|
+
icon: "Search",
|
|
8562
|
+
category: "search",
|
|
8563
|
+
formatCallSummary: (a) => a.pattern || "",
|
|
8564
|
+
formatCallDetail: (a) => ({ pattern: a.pattern, path: a.path }),
|
|
8565
|
+
formatResultSummary: (result) => {
|
|
8566
|
+
const text = extractResultText(result);
|
|
8567
|
+
const count = text.split("\n").filter(Boolean).length;
|
|
8568
|
+
return `${count} files`;
|
|
8569
|
+
},
|
|
8570
|
+
formatResultDetail: (result) => {
|
|
8571
|
+
const text = extractResultText(result);
|
|
8572
|
+
return { fileCount: text.split("\n").filter(Boolean).length };
|
|
8573
|
+
}
|
|
8574
|
+
},
|
|
8575
|
+
{
|
|
8576
|
+
name: "find",
|
|
8577
|
+
displayName: "Find",
|
|
8578
|
+
icon: "Search",
|
|
8579
|
+
category: "search",
|
|
8580
|
+
formatCallSummary: (a) => truncStr(a.pattern || a.path, 40),
|
|
8581
|
+
formatCallDetail: (a) => ({ pattern: a.pattern, path: a.path }),
|
|
8582
|
+
formatResultSummary: () => "Find completed",
|
|
8583
|
+
formatResultDetail: () => ({})
|
|
8584
|
+
},
|
|
8585
|
+
{
|
|
8586
|
+
name: "ls",
|
|
8587
|
+
displayName: "List",
|
|
8588
|
+
icon: "FileText",
|
|
8589
|
+
category: "file",
|
|
8590
|
+
formatCallSummary: (a) => a.path || ".",
|
|
8591
|
+
formatCallDetail: (a) => ({ path: a.path }),
|
|
8592
|
+
formatResultSummary: () => "Listed",
|
|
8593
|
+
formatResultDetail: () => ({})
|
|
8594
|
+
},
|
|
8595
|
+
// ── Web tools ────────────────────────
|
|
8596
|
+
{
|
|
8597
|
+
name: "fetch",
|
|
8598
|
+
displayName: "Fetch",
|
|
8599
|
+
icon: "Globe",
|
|
8600
|
+
category: "web",
|
|
8601
|
+
formatCallSummary: (a) => truncStr(a.url, 50),
|
|
8602
|
+
formatCallDetail: (a) => ({ url: a.url }),
|
|
8603
|
+
formatResultSummary: (result) => {
|
|
8604
|
+
const text = extractResultText(result);
|
|
8605
|
+
const kb = (text.length / 1024).toFixed(1);
|
|
8606
|
+
return `${kb}KB received`;
|
|
8607
|
+
},
|
|
8608
|
+
formatResultDetail: (result) => {
|
|
8609
|
+
const text = extractResultText(result);
|
|
8610
|
+
return { sizeKB: parseFloat((text.length / 1024).toFixed(1)) };
|
|
8611
|
+
}
|
|
8612
|
+
},
|
|
8613
|
+
{
|
|
8614
|
+
name: "web_fetch",
|
|
8615
|
+
displayName: "Web Fetch",
|
|
8616
|
+
icon: "Globe",
|
|
8617
|
+
category: "web",
|
|
8618
|
+
formatCallSummary: (a) => truncStr(a.url, 50),
|
|
8619
|
+
formatCallDetail: (a) => ({ url: a.url }),
|
|
8620
|
+
formatResultSummary: (result) => {
|
|
8621
|
+
const r = safeRecord(result);
|
|
8622
|
+
const data = safeRecord(r.data);
|
|
8623
|
+
const charCount = data.charCount;
|
|
8624
|
+
if (charCount) return `${(charCount / 1024).toFixed(1)}KB received`;
|
|
8625
|
+
return "Fetch completed";
|
|
8626
|
+
},
|
|
8627
|
+
formatResultDetail: (result) => {
|
|
8628
|
+
const r = safeRecord(result);
|
|
8629
|
+
const data = safeRecord(r.data);
|
|
8630
|
+
return { charCount: data.charCount, url: data.url };
|
|
8631
|
+
}
|
|
8632
|
+
},
|
|
8633
|
+
{
|
|
8634
|
+
name: "web_search",
|
|
8635
|
+
displayName: "Web Search",
|
|
8636
|
+
icon: "Globe",
|
|
8637
|
+
category: "web",
|
|
8638
|
+
formatCallSummary: (a) => truncStr(a.query, 50),
|
|
8639
|
+
formatCallDetail: (a) => ({ query: a.query }),
|
|
8640
|
+
formatResultSummary: () => "Search completed",
|
|
8641
|
+
formatResultDetail: () => ({})
|
|
8642
|
+
},
|
|
8643
|
+
// ── Research tools ────────────────────────
|
|
8644
|
+
{
|
|
8645
|
+
name: "literature-search",
|
|
8646
|
+
displayName: "Literature Search",
|
|
8647
|
+
icon: "BookOpen",
|
|
8648
|
+
category: "research",
|
|
8649
|
+
formatCallSummary: (a) => truncStr(a.query, 40),
|
|
8650
|
+
formatCallDetail: (a) => ({ query: a.query, maxResults: a.max_results }),
|
|
8651
|
+
formatResultSummary: (result) => {
|
|
8652
|
+
const r = safeRecord(result);
|
|
8653
|
+
const data = safeRecord(r.data);
|
|
8654
|
+
const totalFound = data.totalPapersFound ?? 0;
|
|
8655
|
+
const saved = data.papersAutoSaved ?? 0;
|
|
8656
|
+
const coverage = data.coverage;
|
|
8657
|
+
if (totalFound > 0) {
|
|
8658
|
+
let s = `Found ${totalFound} papers`;
|
|
8659
|
+
if (coverage?.score != null) s += ` (${Math.round(coverage.score * 100)}%)`;
|
|
8660
|
+
if (saved > 0) s += `, saved ${saved}`;
|
|
8661
|
+
return s;
|
|
8662
|
+
}
|
|
8663
|
+
const local = data.localPapersUsed ?? 0;
|
|
8664
|
+
const external = data.externalPapersUsed ?? 0;
|
|
8665
|
+
return `Found ${local + external} papers`;
|
|
8666
|
+
},
|
|
8667
|
+
formatResultDetail: (result) => {
|
|
8668
|
+
const r = safeRecord(result);
|
|
8669
|
+
const data = safeRecord(r.data);
|
|
8670
|
+
return {
|
|
8671
|
+
papersFound: data.totalPapersFound ?? 0,
|
|
8672
|
+
papersSaved: data.papersAutoSaved ?? 0,
|
|
8673
|
+
coverage: data.coverage?.score
|
|
8674
|
+
};
|
|
8675
|
+
}
|
|
8676
|
+
},
|
|
8677
|
+
{
|
|
8678
|
+
name: "lit-subtopic",
|
|
8679
|
+
displayName: "Sub-topic Search",
|
|
8680
|
+
icon: "BookOpen",
|
|
8681
|
+
category: "research",
|
|
8682
|
+
formatCallSummary: (a) => a._summary || "Searching sub-topic",
|
|
8683
|
+
formatCallDetail: (a) => ({ summary: a._summary }),
|
|
8684
|
+
formatResultSummary: (result) => safeRecord(result).data || "Search completed",
|
|
8685
|
+
formatResultDetail: () => ({})
|
|
8686
|
+
},
|
|
8687
|
+
{
|
|
8688
|
+
name: "lit-enrich",
|
|
8689
|
+
displayName: "Enrich Papers",
|
|
8690
|
+
icon: "BookOpen",
|
|
8691
|
+
category: "research",
|
|
8692
|
+
formatCallSummary: (a) => a._summary || "Enriching paper metadata",
|
|
8693
|
+
formatCallDetail: (a) => ({ summary: a._summary }),
|
|
8694
|
+
formatResultSummary: (result) => safeRecord(result).data || "Enriched metadata",
|
|
8695
|
+
formatResultDetail: () => ({})
|
|
8696
|
+
},
|
|
8697
|
+
{
|
|
8698
|
+
name: "lit-autosave",
|
|
8699
|
+
displayName: "Save Papers",
|
|
8700
|
+
icon: "BookOpen",
|
|
8701
|
+
category: "research",
|
|
8702
|
+
formatCallSummary: (a) => a._summary || "Saving papers",
|
|
8703
|
+
formatCallDetail: (a) => ({ summary: a._summary }),
|
|
8704
|
+
formatResultSummary: (result) => safeRecord(result).data || "Saved papers",
|
|
8705
|
+
formatResultDetail: () => ({})
|
|
8706
|
+
},
|
|
8707
|
+
{
|
|
8708
|
+
name: "data_analyze",
|
|
8709
|
+
displayName: "Data Analysis",
|
|
8710
|
+
icon: "Database",
|
|
8711
|
+
category: "research",
|
|
8712
|
+
formatCallSummary: (a) => getFileName(a.file_path || "") || "data",
|
|
8713
|
+
formatCallDetail: (a) => ({ file_path: a.file_path }),
|
|
8714
|
+
formatResultSummary: () => "Analysis completed",
|
|
8715
|
+
formatResultDetail: () => ({})
|
|
8716
|
+
},
|
|
8717
|
+
// ── Artifact tools ────────────────────────
|
|
8718
|
+
{
|
|
8719
|
+
name: "artifact-create",
|
|
8720
|
+
displayName: "Create Artifact",
|
|
8721
|
+
icon: "Sparkles",
|
|
8722
|
+
category: "memory",
|
|
8723
|
+
formatCallSummary: (a) => {
|
|
8724
|
+
const type = (a.type || "artifact").toLowerCase();
|
|
8725
|
+
const title = truncStr(a.title, 35);
|
|
8726
|
+
return `${type}: ${title}`;
|
|
8727
|
+
},
|
|
8728
|
+
formatCallDetail: (a) => ({ type: a.type, title: a.title }),
|
|
8729
|
+
formatResultSummary: (result) => {
|
|
8730
|
+
const data = safeRecord(safeRecord(result).data);
|
|
8731
|
+
const type = data.type || "artifact";
|
|
8732
|
+
const title = truncStr(data.title, 30);
|
|
8733
|
+
return title ? `Created ${type}: ${title}` : `Created ${type}`;
|
|
8734
|
+
},
|
|
8735
|
+
formatResultDetail: (result) => {
|
|
8736
|
+
const data = safeRecord(safeRecord(result).data);
|
|
8737
|
+
return { type: data.type, title: data.title };
|
|
8738
|
+
}
|
|
8739
|
+
},
|
|
8740
|
+
{
|
|
8741
|
+
name: "artifact-update",
|
|
8742
|
+
displayName: "Update Artifact",
|
|
8743
|
+
icon: "Sparkles",
|
|
8744
|
+
category: "memory",
|
|
8745
|
+
formatCallSummary: (a) => truncStr(a.id, 30),
|
|
8746
|
+
formatCallDetail: (a) => ({ id: a.id }),
|
|
8747
|
+
formatResultSummary: () => "Updated",
|
|
8748
|
+
formatResultDetail: () => ({})
|
|
8749
|
+
},
|
|
8750
|
+
{
|
|
8751
|
+
name: "artifact-search",
|
|
8752
|
+
displayName: "Search Artifacts",
|
|
8753
|
+
icon: "Search",
|
|
8754
|
+
category: "memory",
|
|
8755
|
+
formatCallSummary: (a) => truncStr(a.query, 40),
|
|
8756
|
+
formatCallDetail: (a) => ({ query: a.query, types: a.types }),
|
|
8757
|
+
formatResultSummary: () => "Search completed",
|
|
8758
|
+
formatResultDetail: () => ({})
|
|
8759
|
+
},
|
|
8760
|
+
// ── System tools ────────────────────────
|
|
8761
|
+
{
|
|
8762
|
+
name: "convert_document",
|
|
8763
|
+
displayName: "Convert Document",
|
|
8764
|
+
icon: "FileText",
|
|
8765
|
+
category: "system",
|
|
8766
|
+
formatCallSummary: (a) => getFileName(a.source || ""),
|
|
8767
|
+
formatCallDetail: (a) => ({ source: a.source }),
|
|
8768
|
+
formatResultSummary: (result, a) => {
|
|
8769
|
+
const data = safeRecord(safeRecord(result).data);
|
|
8770
|
+
const skill = data.converterSkill;
|
|
8771
|
+
const sourceName = getFileName(a?.source || "");
|
|
8772
|
+
return skill ? `Converted ${sourceName} via ${skill}` : `Converted ${sourceName}`;
|
|
8773
|
+
},
|
|
8774
|
+
formatResultDetail: (result) => {
|
|
8775
|
+
const data = safeRecord(safeRecord(result).data);
|
|
8776
|
+
return { converterSkill: data.converterSkill, outputFile: data.outputFile };
|
|
8777
|
+
}
|
|
8778
|
+
},
|
|
8779
|
+
{
|
|
8780
|
+
name: "load_skill",
|
|
8781
|
+
displayName: "Load Skill",
|
|
8782
|
+
icon: "Sparkles",
|
|
8783
|
+
category: "system",
|
|
8784
|
+
formatCallSummary: (a) => a.name || "skill",
|
|
8785
|
+
formatCallDetail: (a) => ({ name: a.name }),
|
|
8786
|
+
formatResultSummary: (_, a) => `Loaded: ${a?.name || "skill"}`,
|
|
8787
|
+
formatResultDetail: () => ({})
|
|
8788
|
+
}
|
|
8789
|
+
];
|
|
8790
|
+
const registry = /* @__PURE__ */ new Map();
|
|
8791
|
+
for (const config of configs) {
|
|
8792
|
+
registry.set(config.name, config);
|
|
8793
|
+
}
|
|
8794
|
+
function getToolRenderConfig(toolName) {
|
|
8795
|
+
return registry.get(toolName);
|
|
8796
|
+
}
|
|
6092
8797
|
const EMPTY = {
|
|
6093
8798
|
version: 1,
|
|
6094
8799
|
updatedAt: (/* @__PURE__ */ new Date(0)).toISOString(),
|
|
6095
|
-
totals: { tokens: 0, promptTokens: 0, cachedTokens: 0, cost: 0, calls: 0 }
|
|
8800
|
+
totals: { tokens: 0, promptTokens: 0, completionTokens: 0, cachedTokens: 0, cacheWriteTokens: 0, cost: 0, calls: 0 }
|
|
6096
8801
|
};
|
|
6097
8802
|
function usagePath(baseDir) {
|
|
6098
8803
|
return join(baseDir, "usage.json");
|
|
@@ -6102,6 +8807,9 @@ function loadUsageTotals(baseDir) {
|
|
|
6102
8807
|
const raw = readFileSync(usagePath(baseDir), "utf-8");
|
|
6103
8808
|
const parsed = JSON.parse(raw);
|
|
6104
8809
|
if (!parsed?.totals) return { ...EMPTY };
|
|
8810
|
+
const t = parsed.totals;
|
|
8811
|
+
t.completionTokens ??= 0;
|
|
8812
|
+
t.cacheWriteTokens ??= 0;
|
|
6105
8813
|
return parsed;
|
|
6106
8814
|
} catch {
|
|
6107
8815
|
return { ...EMPTY };
|
|
@@ -6130,15 +8838,17 @@ function writeAtomically(filePath, data) {
|
|
|
6130
8838
|
}
|
|
6131
8839
|
}
|
|
6132
8840
|
}
|
|
6133
|
-
function accumulateUsage(baseDir, promptTokens, completionTokens, cachedTokens, cost) {
|
|
8841
|
+
function accumulateUsage(baseDir, promptTokens, completionTokens, cachedTokens, cacheWriteTokens, cost) {
|
|
6134
8842
|
const existing = loadUsageTotals(baseDir);
|
|
6135
8843
|
const next = {
|
|
6136
8844
|
version: 1,
|
|
6137
8845
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
6138
8846
|
totals: {
|
|
6139
|
-
tokens: existing.totals.tokens + promptTokens + completionTokens,
|
|
8847
|
+
tokens: existing.totals.tokens + promptTokens + completionTokens + cachedTokens,
|
|
6140
8848
|
promptTokens: existing.totals.promptTokens + promptTokens,
|
|
8849
|
+
completionTokens: existing.totals.completionTokens + completionTokens,
|
|
6141
8850
|
cachedTokens: existing.totals.cachedTokens + cachedTokens,
|
|
8851
|
+
cacheWriteTokens: existing.totals.cacheWriteTokens + cacheWriteTokens,
|
|
6142
8852
|
cost: existing.totals.cost + cost,
|
|
6143
8853
|
calls: existing.totals.calls + 1
|
|
6144
8854
|
}
|
|
@@ -6150,105 +8860,52 @@ function resetUsageTotals(baseDir) {
|
|
|
6150
8860
|
const cleared = {
|
|
6151
8861
|
version: 1,
|
|
6152
8862
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
6153
|
-
totals: { tokens: 0, promptTokens: 0, cachedTokens: 0, cost: 0, calls: 0 }
|
|
8863
|
+
totals: { tokens: 0, promptTokens: 0, completionTokens: 0, cachedTokens: 0, cacheWriteTokens: 0, cost: 0, calls: 0 }
|
|
6154
8864
|
};
|
|
6155
8865
|
writeAtomically(usagePath(baseDir), JSON.stringify(cleared, null, 2));
|
|
6156
8866
|
return cleared;
|
|
6157
8867
|
}
|
|
6158
8868
|
function formatToolCall(tool, args) {
|
|
6159
8869
|
const a = args && typeof args === "object" ? args : {};
|
|
6160
|
-
|
|
6161
|
-
|
|
6162
|
-
|
|
6163
|
-
|
|
6164
|
-
|
|
6165
|
-
|
|
6166
|
-
|
|
6167
|
-
case "lit-autosave":
|
|
6168
|
-
return { label: a._summary || "Saving papers", icon: "file" };
|
|
6169
|
-
case "data-analyze":
|
|
6170
|
-
return { label: `Analyze: ${getFileName(a.filePath || "") || "data"}`, icon: "file" };
|
|
6171
|
-
case "convert_to_markdown": {
|
|
6172
|
-
const sourcePath = a.path || a.uri || "";
|
|
6173
|
-
return { label: `Convert: ${getFileName(sourcePath)}`, icon: "file" };
|
|
6174
|
-
}
|
|
6175
|
-
case "artifact-create": {
|
|
6176
|
-
const type = (a.type || "artifact").toLowerCase();
|
|
6177
|
-
const title = (a.title || type).slice(0, 35);
|
|
6178
|
-
return { label: `Create ${type}: ${title}`, icon: "file" };
|
|
6179
|
-
}
|
|
6180
|
-
case "read":
|
|
6181
|
-
return { label: `Read: ${getFileName(a.path || "")}`, icon: "file" };
|
|
6182
|
-
case "write":
|
|
6183
|
-
return { label: `Write: ${getFileName(a.path || "")}`, icon: "file" };
|
|
6184
|
-
case "edit":
|
|
6185
|
-
return { label: `Edit: ${getFileName(a.path || "")}`, icon: "file" };
|
|
6186
|
-
case "bash":
|
|
6187
|
-
return { label: `Run command`, icon: "terminal" };
|
|
6188
|
-
case "glob":
|
|
6189
|
-
return { label: `Search files: ${a.pattern || ""}`, icon: "search" };
|
|
6190
|
-
case "grep":
|
|
6191
|
-
return { label: `Search content: ${(a.pattern || "").slice(0, 30)}`, icon: "search" };
|
|
6192
|
-
case "fetch":
|
|
6193
|
-
return { label: `Fetch: ${(a.url || "").slice(0, 40)}`, icon: "network" };
|
|
6194
|
-
default:
|
|
6195
|
-
return { label: `${tool}`, icon: "tool" };
|
|
8870
|
+
const config = getToolRenderConfig(tool);
|
|
8871
|
+
if (config) {
|
|
8872
|
+
return {
|
|
8873
|
+
label: `${config.displayName}: ${config.formatCallSummary(a)}`,
|
|
8874
|
+
icon: config.icon,
|
|
8875
|
+
detail: config.formatCallDetail(a)
|
|
8876
|
+
};
|
|
6196
8877
|
}
|
|
8878
|
+
return { label: `${tool}`, icon: "tool" };
|
|
6197
8879
|
}
|
|
6198
8880
|
function formatToolResult(tool, result, args) {
|
|
6199
|
-
const r = result && typeof result === "object" ? result : {};
|
|
6200
8881
|
const a = args && typeof args === "object" ? args : {};
|
|
6201
|
-
const
|
|
6202
|
-
|
|
6203
|
-
|
|
6204
|
-
|
|
6205
|
-
|
|
6206
|
-
|
|
6207
|
-
|
|
6208
|
-
|
|
6209
|
-
|
|
6210
|
-
if (saved > 0) summary2 += `, saved ${saved}`;
|
|
6211
|
-
return { label: summary2, icon: "search" };
|
|
6212
|
-
}
|
|
6213
|
-
const local = data.localPapersUsed ?? 0;
|
|
6214
|
-
const external = data.externalPapersUsed ?? 0;
|
|
6215
|
-
const savedV1 = data.savedPapers ?? 0;
|
|
6216
|
-
let summary = `Found ${local + external} papers`;
|
|
6217
|
-
if (local > 0) summary += ` (${local} local)`;
|
|
6218
|
-
if (savedV1 > 0) summary += `, saved ${savedV1}`;
|
|
6219
|
-
return { label: summary, icon: "search" };
|
|
6220
|
-
}
|
|
6221
|
-
case "lit-subtopic":
|
|
6222
|
-
return { label: r.data || "Search completed", icon: "search" };
|
|
6223
|
-
case "lit-enrich":
|
|
6224
|
-
return { label: r.data || "Enriched metadata", icon: "search" };
|
|
6225
|
-
case "lit-autosave":
|
|
6226
|
-
return { label: r.data || "Saved papers", icon: "file" };
|
|
6227
|
-
case "convert_to_markdown": {
|
|
6228
|
-
const sourcePath = a.path || a.uri || "";
|
|
6229
|
-
const skill = typeof data.converterSkill === "string" ? data.converterSkill : "";
|
|
6230
|
-
const script = typeof data.converterScript === "string" ? data.converterScript : "";
|
|
6231
|
-
if (skill && script) return { label: `Converted ${getFileName(sourcePath)} via ${skill}/${script}`, icon: "file" };
|
|
6232
|
-
if (skill) return { label: `Converted ${getFileName(sourcePath)} via ${skill}`, icon: "file" };
|
|
6233
|
-
return { label: `Converted ${getFileName(sourcePath)}`, icon: "file" };
|
|
6234
|
-
}
|
|
6235
|
-
case "artifact-create": {
|
|
6236
|
-
const type = data.type || "artifact";
|
|
6237
|
-
const title = data.title || "";
|
|
6238
|
-
return { label: title ? `Created ${type}: ${title.slice(0, 30)}` : `Created ${type}`, icon: "file" };
|
|
6239
|
-
}
|
|
6240
|
-
default: {
|
|
6241
|
-
const success2 = r.success !== false;
|
|
6242
|
-
return { label: success2 ? `${tool} completed` : `${tool} failed`, icon: "tool" };
|
|
6243
|
-
}
|
|
8882
|
+
const r = result && typeof result === "object" ? result : {};
|
|
8883
|
+
const success2 = r.success !== false;
|
|
8884
|
+
const config = getToolRenderConfig(tool);
|
|
8885
|
+
if (config && success2) {
|
|
8886
|
+
return {
|
|
8887
|
+
label: config.formatResultSummary(result, a),
|
|
8888
|
+
icon: config.icon,
|
|
8889
|
+
detail: config.formatResultDetail(result, a)
|
|
8890
|
+
};
|
|
6244
8891
|
}
|
|
8892
|
+
if (config && !success2) {
|
|
8893
|
+
const errorMsg = r.error || "";
|
|
8894
|
+
const brief = errorMsg.length > 60 ? errorMsg.slice(0, 57) + "..." : errorMsg;
|
|
8895
|
+
return {
|
|
8896
|
+
label: brief ? `${config.displayName} failed: ${brief}` : `${config.displayName} failed`,
|
|
8897
|
+
icon: config.icon,
|
|
8898
|
+
detail: config.formatResultDetail(result, a)
|
|
8899
|
+
};
|
|
8900
|
+
}
|
|
8901
|
+
return { label: success2 ? `${tool} completed` : `${tool} failed`, icon: "tool", detail: { success: success2 } };
|
|
6245
8902
|
}
|
|
6246
8903
|
const windowStates = /* @__PURE__ */ new Map();
|
|
6247
8904
|
let ipcHandlersRegistered = false;
|
|
6248
8905
|
function createWindowRuntimeState() {
|
|
6249
8906
|
return {
|
|
6250
8907
|
coordinator: null,
|
|
6251
|
-
currentModel: "gpt-5.4",
|
|
8908
|
+
currentModel: "openai:gpt-5.4",
|
|
6252
8909
|
currentReasoningEffort: "medium",
|
|
6253
8910
|
currentAuthMode: "none",
|
|
6254
8911
|
projectPath: "",
|
|
@@ -6379,11 +9036,27 @@ async function ensureCoordinator(state, win, model, options) {
|
|
|
6379
9036
|
if (!state.coordinator) {
|
|
6380
9037
|
const apiKey = resolvedAuth.apiKey;
|
|
6381
9038
|
const runProjectPath = state.projectPath;
|
|
9039
|
+
const getApiKeyOverride = resolvedAuth.authMode === "subscription" ? async () => {
|
|
9040
|
+
const creds = loadCodexCredentials();
|
|
9041
|
+
if (!creds) throw new Error("ChatGPT subscription credentials not found. Please sign in again.");
|
|
9042
|
+
if (creds.expires < Date.now() + 6e4) {
|
|
9043
|
+
try {
|
|
9044
|
+
const { refreshOpenAICodexToken } = await import("@mariozechner/pi-ai/oauth");
|
|
9045
|
+
const newCreds = await refreshOpenAICodexToken(creds);
|
|
9046
|
+
saveCodexCredentials(newCreds);
|
|
9047
|
+
return newCreds.access;
|
|
9048
|
+
} catch {
|
|
9049
|
+
return creds.access;
|
|
9050
|
+
}
|
|
9051
|
+
}
|
|
9052
|
+
return creds.access;
|
|
9053
|
+
} : void 0;
|
|
6382
9054
|
const initEvent = { type: "system", summary: "Initializing agent (first run may take 1-2 minutes for document processing setup)..." };
|
|
6383
9055
|
state.realtimeBuffer.pushActivity(initEvent);
|
|
6384
9056
|
safeSend(win, "agent:activity", initEvent);
|
|
6385
9057
|
state.coordinator = await createCoordinator({
|
|
6386
9058
|
apiKey,
|
|
9059
|
+
getApiKeyOverride,
|
|
6387
9060
|
model: state.currentModel,
|
|
6388
9061
|
reasoningEffort: state.currentReasoningEffort,
|
|
6389
9062
|
projectPath: state.projectPath,
|
|
@@ -6393,13 +9066,15 @@ async function ensureCoordinator(state, win, model, options) {
|
|
|
6393
9066
|
state.realtimeBuffer.appendChunk(chunk);
|
|
6394
9067
|
safeSend(win, "agent:stream-chunk", chunk);
|
|
6395
9068
|
},
|
|
6396
|
-
onToolCall: (tool, args) => {
|
|
6397
|
-
const
|
|
6398
|
-
const
|
|
9069
|
+
onToolCall: (tool, args, toolCallId) => {
|
|
9070
|
+
const id = toolCallId || randomUUID();
|
|
9071
|
+
const { label, detail } = formatToolCall(tool, args);
|
|
9072
|
+
const event = { type: "tool-call", tool, toolCallId: id, summary: label, detail };
|
|
6399
9073
|
state.realtimeBuffer.pushActivity(event);
|
|
9074
|
+
state.realtimeBuffer.pushToolEvent({ type: "tool-call", tool, toolCallId: id, summary: label, detail });
|
|
6400
9075
|
safeSend(win, "agent:activity", event);
|
|
6401
9076
|
},
|
|
6402
|
-
onToolResult: (tool, result, args) => {
|
|
9077
|
+
onToolResult: (tool, result, args, toolCallId) => {
|
|
6403
9078
|
if (tool.startsWith("todo-") && result && typeof result === "object" && "success" in result) {
|
|
6404
9079
|
const r2 = result;
|
|
6405
9080
|
if (r2.success && r2.item) {
|
|
@@ -6414,16 +9089,16 @@ async function ensureCoordinator(state, win, model, options) {
|
|
|
6414
9089
|
safeSend(win, "agent:file-created", absPath);
|
|
6415
9090
|
}
|
|
6416
9091
|
}
|
|
6417
|
-
if (tool === "
|
|
9092
|
+
if (tool === "convert_document" && result && typeof result === "object" && "success" in result) {
|
|
6418
9093
|
const r2 = result;
|
|
6419
9094
|
if (r2.success && r2.data?.outputFile) {
|
|
6420
9095
|
safeSend(win, "agent:file-created", r2.data.outputFile);
|
|
6421
9096
|
}
|
|
6422
9097
|
}
|
|
6423
|
-
if (tool === "
|
|
9098
|
+
if (tool === "convert_document" && result && typeof result === "object" && "success" in result) {
|
|
6424
9099
|
const r2 = result;
|
|
6425
|
-
if (r2.success && r2.data?.outputFile && args && typeof args === "object" && "
|
|
6426
|
-
const sourcePath = args.
|
|
9100
|
+
if (r2.success && r2.data?.outputFile && args && typeof args === "object" && "source" in args) {
|
|
9101
|
+
const sourcePath = args.source;
|
|
6427
9102
|
const absSourcePath = isAbsolute(sourcePath) ? sourcePath : resolve(runProjectPath, sourcePath);
|
|
6428
9103
|
const absOutputPath = resolve(runProjectPath, r2.data.outputFile);
|
|
6429
9104
|
if (existsSync(absOutputPath)) {
|
|
@@ -6454,14 +9129,72 @@ async function ensureCoordinator(state, win, model, options) {
|
|
|
6454
9129
|
}
|
|
6455
9130
|
}
|
|
6456
9131
|
}
|
|
9132
|
+
if (tool === "local_compute_execute" && result && typeof result === "object" && "success" in result) {
|
|
9133
|
+
const cr = result;
|
|
9134
|
+
if (cr.success && cr.data) {
|
|
9135
|
+
safeSend(win, "compute:run-update", {
|
|
9136
|
+
runId: cr.data.run_id,
|
|
9137
|
+
status: cr.data.status,
|
|
9138
|
+
currentPhase: cr.data.current_phase,
|
|
9139
|
+
command: args?.command ?? "",
|
|
9140
|
+
sandbox: cr.data.sandbox,
|
|
9141
|
+
weight: cr.data.weight,
|
|
9142
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
9143
|
+
});
|
|
9144
|
+
}
|
|
9145
|
+
}
|
|
9146
|
+
if ((tool === "local_compute_status" || tool === "local_compute_wait") && result && typeof result === "object" && "success" in result) {
|
|
9147
|
+
const cr = result;
|
|
9148
|
+
if (cr.success && cr.data?.run_id) {
|
|
9149
|
+
const isComplete = ["completed", "failed", "timed_out", "cancelled"].includes(cr.data.status);
|
|
9150
|
+
const channel = isComplete ? "compute:run-complete" : "compute:run-update";
|
|
9151
|
+
safeSend(win, channel, {
|
|
9152
|
+
runId: cr.data.run_id,
|
|
9153
|
+
status: cr.data.status,
|
|
9154
|
+
currentPhase: cr.data.current_phase,
|
|
9155
|
+
exitCode: cr.data.exit_code,
|
|
9156
|
+
elapsedSeconds: cr.data.elapsed_seconds,
|
|
9157
|
+
outputBytes: cr.data.output_bytes,
|
|
9158
|
+
outputLines: cr.data.output_lines,
|
|
9159
|
+
stalled: cr.data.stalled,
|
|
9160
|
+
progress: cr.data.progress,
|
|
9161
|
+
outputTail: cr.data.output_tail?.slice(-2048),
|
|
9162
|
+
failure: cr.data.failure
|
|
9163
|
+
});
|
|
9164
|
+
}
|
|
9165
|
+
}
|
|
9166
|
+
if (tool === "local_compute_stop" && result && typeof result === "object" && "success" in result) {
|
|
9167
|
+
const cr = result;
|
|
9168
|
+
if (cr.success && cr.data?.run_id) {
|
|
9169
|
+
safeSend(win, "compute:run-complete", {
|
|
9170
|
+
runId: cr.data.run_id,
|
|
9171
|
+
status: "cancelled"
|
|
9172
|
+
});
|
|
9173
|
+
}
|
|
9174
|
+
}
|
|
6457
9175
|
const r = result;
|
|
6458
9176
|
const success2 = r?.success !== false;
|
|
6459
9177
|
const error = !success2 ? r?.error || "Unknown error" : void 0;
|
|
6460
|
-
const
|
|
6461
|
-
const
|
|
9178
|
+
const { label: resultLabel, detail: resultDetail } = formatToolResult(tool, result, args);
|
|
9179
|
+
const startTime = toolCallId ? state.realtimeBuffer.popToolCallStartTime(toolCallId) : void 0;
|
|
9180
|
+
const durationMs = startTime ? Date.now() - startTime : void 0;
|
|
9181
|
+
const actEvent = { type: "tool-result", tool, toolCallId, summary: resultLabel, success: success2, error, resultDetail, durationMs };
|
|
6462
9182
|
state.realtimeBuffer.pushActivity(actEvent);
|
|
9183
|
+
if (toolCallId) {
|
|
9184
|
+
state.realtimeBuffer.updateToolEvent(toolCallId, {
|
|
9185
|
+
type: "tool-result",
|
|
9186
|
+
summary: resultLabel,
|
|
9187
|
+
success: success2,
|
|
9188
|
+
resultDetail,
|
|
9189
|
+
durationMs
|
|
9190
|
+
});
|
|
9191
|
+
}
|
|
6463
9192
|
safeSend(win, "agent:activity", actEvent);
|
|
6464
9193
|
},
|
|
9194
|
+
// Tool execution progress (real-time updates during tool execution)
|
|
9195
|
+
onToolProgress: (tool, toolCallId, phase, data) => {
|
|
9196
|
+
safeSend(win, "agent:tool-progress", { tool, toolCallId, phase, data, timestamp: Date.now() });
|
|
9197
|
+
},
|
|
6465
9198
|
// Skill activation tracking
|
|
6466
9199
|
onSkillLoaded: (skillName) => {
|
|
6467
9200
|
safeSend(win, "agent:skill-loaded", skillName);
|
|
@@ -6473,12 +9206,14 @@ async function ensureCoordinator(state, win, model, options) {
|
|
|
6473
9206
|
const promptTokens = usage.input ?? 0;
|
|
6474
9207
|
const completionTokens = usage.output ?? 0;
|
|
6475
9208
|
const cachedTokens = usage.cacheRead ?? 0;
|
|
9209
|
+
const cacheWriteTokens = usage.cacheWrite ?? 0;
|
|
6476
9210
|
const baseDir = join(runProjectPath, PATHS.root);
|
|
6477
|
-
accumulateUsage(baseDir, promptTokens, completionTokens, cachedTokens, rawCost);
|
|
9211
|
+
accumulateUsage(baseDir, promptTokens, completionTokens, cachedTokens, cacheWriteTokens, rawCost);
|
|
6478
9212
|
const usageEvent = {
|
|
6479
9213
|
promptTokens,
|
|
6480
9214
|
completionTokens,
|
|
6481
9215
|
cachedTokens,
|
|
9216
|
+
cacheWriteTokens,
|
|
6482
9217
|
cost: rawCost,
|
|
6483
9218
|
rawCost,
|
|
6484
9219
|
billableCost: rawCost,
|
|
@@ -6492,9 +9227,36 @@ async function ensureCoordinator(state, win, model, options) {
|
|
|
6492
9227
|
const readyEvent = { type: "system", summary: "Agent ready" };
|
|
6493
9228
|
state.realtimeBuffer.pushActivity(readyEvent);
|
|
6494
9229
|
safeSend(win, "agent:activity", readyEvent);
|
|
9230
|
+
probeStaticProfile().then((profile) => {
|
|
9231
|
+
safeSend(win, "compute:environment", {
|
|
9232
|
+
os: profile.os,
|
|
9233
|
+
arch: profile.arch,
|
|
9234
|
+
cpuCores: profile.cpuCores,
|
|
9235
|
+
totalMemoryMb: profile.totalMemoryMb,
|
|
9236
|
+
gpu: profile.gpu.model,
|
|
9237
|
+
mlxAvailable: profile.gpu.mlxAvailable,
|
|
9238
|
+
sandbox: profile.dockerAvailable ? "docker" : "process"
|
|
9239
|
+
});
|
|
9240
|
+
}).catch(() => {
|
|
9241
|
+
});
|
|
6495
9242
|
}
|
|
6496
9243
|
return state.coordinator;
|
|
6497
9244
|
}
|
|
9245
|
+
async function destroyAllCoordinators() {
|
|
9246
|
+
const promises = [];
|
|
9247
|
+
for (const [, state] of windowStates) {
|
|
9248
|
+
if (state.coordinator) {
|
|
9249
|
+
promises.push(
|
|
9250
|
+
state.coordinator.destroy().catch(() => {
|
|
9251
|
+
})
|
|
9252
|
+
);
|
|
9253
|
+
}
|
|
9254
|
+
}
|
|
9255
|
+
await Promise.race([
|
|
9256
|
+
Promise.all(promises),
|
|
9257
|
+
new Promise((resolve2) => setTimeout(resolve2, 8e3))
|
|
9258
|
+
]);
|
|
9259
|
+
}
|
|
6498
9260
|
function registerIpcHandlers() {
|
|
6499
9261
|
if (ipcHandlersRegistered) return;
|
|
6500
9262
|
ipcHandlersRegistered = true;
|
|
@@ -6961,6 +9723,24 @@ function registerIpcHandlers() {
|
|
|
6961
9723
|
}
|
|
6962
9724
|
});
|
|
6963
9725
|
handleWindow("session:current", ({ state }) => ({ sessionId: state.sessionId, projectPath: state.projectPath }));
|
|
9726
|
+
handleWindow("compute:probe-environment", async ({ win }) => {
|
|
9727
|
+
try {
|
|
9728
|
+
const profile = await probeStaticProfile();
|
|
9729
|
+
const env = {
|
|
9730
|
+
os: profile.os,
|
|
9731
|
+
arch: profile.arch,
|
|
9732
|
+
cpuCores: profile.cpuCores,
|
|
9733
|
+
totalMemoryMb: profile.totalMemoryMb,
|
|
9734
|
+
gpu: profile.gpu.model,
|
|
9735
|
+
mlxAvailable: profile.gpu.mlxAvailable,
|
|
9736
|
+
sandbox: profile.dockerAvailable ? "docker" : "process"
|
|
9737
|
+
};
|
|
9738
|
+
safeSend(win, "compute:environment", env);
|
|
9739
|
+
return env;
|
|
9740
|
+
} catch {
|
|
9741
|
+
return null;
|
|
9742
|
+
}
|
|
9743
|
+
});
|
|
6964
9744
|
handleWindow("project:pick-folder", async ({ win, state }) => {
|
|
6965
9745
|
const result = await dialog.showOpenDialog(win, {
|
|
6966
9746
|
properties: ["openDirectory", "createDirectory"]
|
|
@@ -6985,11 +9765,33 @@ function registerIpcHandlers() {
|
|
|
6985
9765
|
if (existsSync(prefsFile)) {
|
|
6986
9766
|
try {
|
|
6987
9767
|
const prefs = JSON.parse(readFileSync(prefsFile, "utf-8"));
|
|
6988
|
-
if (prefs.selectedModel)
|
|
9768
|
+
if (prefs.selectedModel) {
|
|
9769
|
+
const m = prefs.selectedModel;
|
|
9770
|
+
if (!m.includes(":")) {
|
|
9771
|
+
const provider = m.startsWith("claude-") ? "anthropic" : m.startsWith("gemini-") ? "google" : "openai";
|
|
9772
|
+
state.currentModel = `${provider}:${m}`;
|
|
9773
|
+
} else {
|
|
9774
|
+
state.currentModel = m;
|
|
9775
|
+
}
|
|
9776
|
+
}
|
|
6989
9777
|
if (prefs.reasoningEffort) state.currentReasoningEffort = prefs.reasoningEffort;
|
|
6990
9778
|
} catch {
|
|
6991
9779
|
}
|
|
6992
9780
|
}
|
|
9781
|
+
if (process.env.ENABLE_LOCAL_COMPUTE === "1") {
|
|
9782
|
+
probeStaticProfile().then((profile) => {
|
|
9783
|
+
safeSend(win, "compute:environment", {
|
|
9784
|
+
os: profile.os,
|
|
9785
|
+
arch: profile.arch,
|
|
9786
|
+
cpuCores: profile.cpuCores,
|
|
9787
|
+
totalMemoryMb: profile.totalMemoryMb,
|
|
9788
|
+
gpu: profile.gpu.model,
|
|
9789
|
+
mlxAvailable: profile.gpu.mlxAvailable,
|
|
9790
|
+
sandbox: profile.dockerAvailable ? "docker" : "process"
|
|
9791
|
+
});
|
|
9792
|
+
}).catch(() => {
|
|
9793
|
+
});
|
|
9794
|
+
}
|
|
6993
9795
|
return { projectPath: state.projectPath, sessionId: state.sessionId };
|
|
6994
9796
|
}
|
|
6995
9797
|
return null;
|
|
@@ -7015,7 +9817,7 @@ function registerIpcHandlers() {
|
|
|
7015
9817
|
state.realtimeBuffer.reset();
|
|
7016
9818
|
state.projectPath = "";
|
|
7017
9819
|
state.sessionId = crypto.randomUUID();
|
|
7018
|
-
state.currentModel = "gpt-5.4";
|
|
9820
|
+
state.currentModel = "openai:gpt-5.4";
|
|
7019
9821
|
state.currentReasoningEffort = "medium";
|
|
7020
9822
|
state.currentAuthMode = "none";
|
|
7021
9823
|
} finally {
|
|
@@ -7104,7 +9906,11 @@ function destroyAllTerminals() {
|
|
|
7104
9906
|
terminals.delete(id);
|
|
7105
9907
|
}
|
|
7106
9908
|
}
|
|
9909
|
+
setMaxListeners(20);
|
|
7107
9910
|
loadApiKeysFromConfig();
|
|
9911
|
+
if (!process.env.PI_CACHE_RETENTION) {
|
|
9912
|
+
process.env.PI_CACHE_RETENTION = "long";
|
|
9913
|
+
}
|
|
7108
9914
|
if (process.platform === "darwin" && !is.dev) {
|
|
7109
9915
|
try {
|
|
7110
9916
|
const shellPath = process.env.SHELL || "/bin/zsh";
|
|
@@ -7259,6 +10065,10 @@ app.on("window-all-closed", () => {
|
|
|
7259
10065
|
destroyAllTerminals();
|
|
7260
10066
|
if (process.platform !== "darwin") app.quit();
|
|
7261
10067
|
});
|
|
7262
|
-
app.on("before-quit", () => {
|
|
10068
|
+
app.on("before-quit", (event) => {
|
|
7263
10069
|
destroyAllTerminals();
|
|
10070
|
+
event.preventDefault();
|
|
10071
|
+
destroyAllCoordinators().finally(() => {
|
|
10072
|
+
app.exit(0);
|
|
10073
|
+
});
|
|
7264
10074
|
});
|