cascade-ai 0.5.1 → 0.9.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +147 -28
- package/bin/cascade.js +14 -1
- package/dist/cli.cjs +4157 -1364
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +4095 -1303
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +2214 -430
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +562 -70
- package/dist/index.d.ts +562 -70
- package/dist/index.js +2208 -425
- package/dist/index.js.map +1 -1
- package/dist/keytar-VMICNFEJ.node +0 -0
- package/package.json +11 -12
- package/web/dist/assets/index-DFRrUnoJ.js +246 -0
- package/web/dist/assets/react-BP1N17hq.js +1 -0
- package/web/dist/assets/reactflow-Clz8xC7C.js +33 -0
- package/web/dist/index.html +2 -2
- package/dist/keytar-F4YAPN53.node +0 -0
- package/web/dist/assets/index-BFrwdYDg.js +0 -225
- package/web/dist/assets/react-Cpp6qqoq.js +0 -1
- package/web/dist/assets/reactflow-B1e2RnXD.js +0 -48
package/dist/index.cjs
CHANGED
|
@@ -6,20 +6,21 @@ var glob = require('glob');
|
|
|
6
6
|
var Anthropic = require('@anthropic-ai/sdk');
|
|
7
7
|
var OpenAI = require('openai');
|
|
8
8
|
var genai = require('@google/genai');
|
|
9
|
-
var
|
|
10
|
-
var
|
|
11
|
-
var
|
|
9
|
+
var fs4 = require('fs/promises');
|
|
10
|
+
var path18 = require('path');
|
|
11
|
+
var os4 = require('os');
|
|
12
12
|
var ignoreFactory = require('ignore');
|
|
13
13
|
var child_process = require('child_process');
|
|
14
14
|
var util = require('util');
|
|
15
|
-
var
|
|
15
|
+
var fs17 = require('fs');
|
|
16
16
|
var simpleGit = require('simple-git');
|
|
17
17
|
var PDFDocument = require('pdfkit');
|
|
18
|
+
var dns = require('dns/promises');
|
|
19
|
+
var net = require('net');
|
|
18
20
|
var index_js = require('@modelcontextprotocol/sdk/client/index.js');
|
|
19
21
|
var stdio_js = require('@modelcontextprotocol/sdk/client/stdio.js');
|
|
20
22
|
var zod = require('zod');
|
|
21
|
-
var
|
|
22
|
-
var vm = require('vm');
|
|
23
|
+
var worker_threads = require('worker_threads');
|
|
23
24
|
var Database = require('better-sqlite3');
|
|
24
25
|
var http = require('http');
|
|
25
26
|
var url = require('url');
|
|
@@ -56,13 +57,14 @@ var EventEmitter__default = /*#__PURE__*/_interopDefault(EventEmitter);
|
|
|
56
57
|
var crypto__default = /*#__PURE__*/_interopDefault(crypto);
|
|
57
58
|
var Anthropic__default = /*#__PURE__*/_interopDefault(Anthropic);
|
|
58
59
|
var OpenAI__default = /*#__PURE__*/_interopDefault(OpenAI);
|
|
59
|
-
var
|
|
60
|
-
var
|
|
61
|
-
var
|
|
60
|
+
var fs4__default = /*#__PURE__*/_interopDefault(fs4);
|
|
61
|
+
var path18__default = /*#__PURE__*/_interopDefault(path18);
|
|
62
|
+
var os4__default = /*#__PURE__*/_interopDefault(os4);
|
|
62
63
|
var ignoreFactory__namespace = /*#__PURE__*/_interopNamespace(ignoreFactory);
|
|
63
|
-
var
|
|
64
|
+
var fs17__default = /*#__PURE__*/_interopDefault(fs17);
|
|
64
65
|
var PDFDocument__default = /*#__PURE__*/_interopDefault(PDFDocument);
|
|
65
|
-
var
|
|
66
|
+
var dns__default = /*#__PURE__*/_interopDefault(dns);
|
|
67
|
+
var net__default = /*#__PURE__*/_interopDefault(net);
|
|
66
68
|
var Database__default = /*#__PURE__*/_interopDefault(Database);
|
|
67
69
|
var express__default = /*#__PURE__*/_interopDefault(express);
|
|
68
70
|
var rateLimit__default = /*#__PURE__*/_interopDefault(rateLimit);
|
|
@@ -111,13 +113,13 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
111
113
|
var keytar_default;
|
|
112
114
|
var init_keytar = __esm({
|
|
113
115
|
"node_modules/keytar/build/Release/keytar.node"() {
|
|
114
|
-
keytar_default = "./keytar-
|
|
116
|
+
keytar_default = "./keytar-VMICNFEJ.node";
|
|
115
117
|
}
|
|
116
118
|
});
|
|
117
119
|
|
|
118
|
-
// node-file
|
|
120
|
+
// node-file:/home/runner/work/Cascade-AI/Cascade-AI/node_modules/keytar/build/Release/keytar.node
|
|
119
121
|
var require_keytar = __commonJS({
|
|
120
|
-
"node-file
|
|
122
|
+
"node-file:/home/runner/work/Cascade-AI/Cascade-AI/node_modules/keytar/build/Release/keytar.node"(exports, module) {
|
|
121
123
|
init_keytar();
|
|
122
124
|
try {
|
|
123
125
|
module.exports = __require(keytar_default);
|
|
@@ -128,7 +130,7 @@ var require_keytar = __commonJS({
|
|
|
128
130
|
|
|
129
131
|
// node_modules/keytar/lib/keytar.js
|
|
130
132
|
var require_keytar2 = __commonJS({
|
|
131
|
-
"node_modules/keytar/lib/keytar.js"(exports
|
|
133
|
+
"node_modules/keytar/lib/keytar.js"(exports, module) {
|
|
132
134
|
var keytar = require_keytar();
|
|
133
135
|
function checkRequired(val, name) {
|
|
134
136
|
if (!val || val.length <= 0) {
|
|
@@ -165,7 +167,7 @@ var require_keytar2 = __commonJS({
|
|
|
165
167
|
});
|
|
166
168
|
|
|
167
169
|
// src/constants.ts
|
|
168
|
-
var CASCADE_VERSION = "0.
|
|
170
|
+
var CASCADE_VERSION = "0.9.6";
|
|
169
171
|
var CASCADE_CONFIG_DIR = ".cascade";
|
|
170
172
|
var CASCADE_MD_FILE = "CASCADE.md";
|
|
171
173
|
var CASCADE_IGNORE_FILE = ".cascadeignore";
|
|
@@ -333,7 +335,7 @@ var MODELS = {
|
|
|
333
335
|
isLocal: false
|
|
334
336
|
},
|
|
335
337
|
"gemini-2.5-pro": {
|
|
336
|
-
id: "gemini-2.5-pro
|
|
338
|
+
id: "gemini-2.5-pro",
|
|
337
339
|
name: "Gemini 2.5 Pro",
|
|
338
340
|
provider: "gemini",
|
|
339
341
|
contextWindow: 1e6,
|
|
@@ -345,7 +347,7 @@ var MODELS = {
|
|
|
345
347
|
isLocal: false
|
|
346
348
|
},
|
|
347
349
|
"gemini-2.5-flash": {
|
|
348
|
-
id: "gemini-2.5-flash
|
|
350
|
+
id: "gemini-2.5-flash",
|
|
349
351
|
name: "Gemini 2.5 Flash",
|
|
350
352
|
provider: "gemini",
|
|
351
353
|
contextWindow: 1e6,
|
|
@@ -410,6 +412,9 @@ var MODELS = {
|
|
|
410
412
|
minSizeB: 7
|
|
411
413
|
}
|
|
412
414
|
};
|
|
415
|
+
for (const _m of Object.values(MODELS)) {
|
|
416
|
+
if (_m.supportsToolUse === void 0) _m.supportsToolUse = !_m.isLocal;
|
|
417
|
+
}
|
|
413
418
|
var T1_MODEL_PRIORITY = [
|
|
414
419
|
"claude-opus-4",
|
|
415
420
|
"claude-sonnet-4",
|
|
@@ -469,12 +474,15 @@ var TOOL_NAMES = {
|
|
|
469
474
|
PDF_CREATE: "pdf_create",
|
|
470
475
|
RUN_CODE: "run_code",
|
|
471
476
|
PEER_MESSAGE: "peer_message",
|
|
472
|
-
WEB_SEARCH: "web_search"
|
|
477
|
+
WEB_SEARCH: "web_search",
|
|
478
|
+
REQUEST_WORKERS: "request_workers"
|
|
473
479
|
};
|
|
474
480
|
var DEFAULT_APPROVAL_REQUIRED = [
|
|
475
481
|
TOOL_NAMES.SHELL,
|
|
476
482
|
TOOL_NAMES.FILE_DELETE,
|
|
477
483
|
TOOL_NAMES.FILE_WRITE,
|
|
484
|
+
TOOL_NAMES.FILE_EDIT,
|
|
485
|
+
TOOL_NAMES.GIT,
|
|
478
486
|
TOOL_NAMES.BROWSER,
|
|
479
487
|
TOOL_NAMES.GITHUB,
|
|
480
488
|
"pdf_create",
|
|
@@ -521,9 +529,16 @@ var AnthropicProvider = class extends BaseProvider {
|
|
|
521
529
|
client;
|
|
522
530
|
constructor(config, model) {
|
|
523
531
|
super(config, model);
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
532
|
+
if (config.authToken) {
|
|
533
|
+
this.client = new Anthropic__default.default({
|
|
534
|
+
authToken: config.authToken,
|
|
535
|
+
defaultHeaders: { "anthropic-beta": "oauth-2025-04-20" }
|
|
536
|
+
});
|
|
537
|
+
} else {
|
|
538
|
+
this.client = new Anthropic__default.default({
|
|
539
|
+
apiKey: config.apiKey
|
|
540
|
+
});
|
|
541
|
+
}
|
|
527
542
|
}
|
|
528
543
|
async generate(options) {
|
|
529
544
|
const chunks = [];
|
|
@@ -546,7 +561,7 @@ var AnthropicProvider = class extends BaseProvider {
|
|
|
546
561
|
system: options.systemPrompt,
|
|
547
562
|
messages,
|
|
548
563
|
tools: tools?.length ? tools : void 0
|
|
549
|
-
});
|
|
564
|
+
}, { signal: options.signal });
|
|
550
565
|
let isThinking = false;
|
|
551
566
|
for await (const event of stream) {
|
|
552
567
|
if (event.type === "content_block_delta") {
|
|
@@ -633,7 +648,7 @@ var AnthropicProvider = class extends BaseProvider {
|
|
|
633
648
|
}
|
|
634
649
|
async isAvailable() {
|
|
635
650
|
try {
|
|
636
|
-
return !!this.config.apiKey;
|
|
651
|
+
return !!(this.config.apiKey || this.config.authToken);
|
|
637
652
|
} catch {
|
|
638
653
|
return false;
|
|
639
654
|
}
|
|
@@ -734,7 +749,7 @@ var OpenAIProvider = class extends BaseProvider {
|
|
|
734
749
|
};
|
|
735
750
|
let stream;
|
|
736
751
|
try {
|
|
737
|
-
stream = await this.client.chat.completions.create(params);
|
|
752
|
+
stream = await this.client.chat.completions.create(params, { signal: options.signal });
|
|
738
753
|
} catch (err) {
|
|
739
754
|
if (err.message && err.message.includes("max_completion_tokens")) {
|
|
740
755
|
const fallbackParams = { ...params };
|
|
@@ -743,7 +758,7 @@ var OpenAIProvider = class extends BaseProvider {
|
|
|
743
758
|
if (this.model.id.includes("o1") || this.model.id.includes("o3")) {
|
|
744
759
|
fallbackParams.temperature = 1;
|
|
745
760
|
}
|
|
746
|
-
stream = await this.client.chat.completions.create(fallbackParams);
|
|
761
|
+
stream = await this.client.chat.completions.create(fallbackParams, { signal: options.signal });
|
|
747
762
|
} else {
|
|
748
763
|
throw err;
|
|
749
764
|
}
|
|
@@ -957,7 +972,8 @@ var GeminiProvider = class extends BaseProvider {
|
|
|
957
972
|
{ category: genai.HarmCategory.HARM_CATEGORY_HARASSMENT, threshold: genai.HarmBlockThreshold.BLOCK_NONE },
|
|
958
973
|
{ category: genai.HarmCategory.HARM_CATEGORY_HATE_SPEECH, threshold: genai.HarmBlockThreshold.BLOCK_NONE }
|
|
959
974
|
],
|
|
960
|
-
tools: options.tools?.length ? [{ functionDeclarations: options.tools.map(this.convertTool) }] : void 0
|
|
975
|
+
tools: options.tools?.length ? [{ functionDeclarations: options.tools.map(this.convertTool) }] : void 0,
|
|
976
|
+
abortSignal: options.signal
|
|
961
977
|
}
|
|
962
978
|
});
|
|
963
979
|
let fullContent = "";
|
|
@@ -1159,6 +1175,8 @@ var GeminiProvider = class extends BaseProvider {
|
|
|
1159
1175
|
};
|
|
1160
1176
|
}
|
|
1161
1177
|
};
|
|
1178
|
+
|
|
1179
|
+
// src/providers/ollama.ts
|
|
1162
1180
|
var TOOL_CAPABLE_FAMILIES = [
|
|
1163
1181
|
"llama3.1",
|
|
1164
1182
|
"llama3.2",
|
|
@@ -1195,9 +1213,10 @@ var OllamaProvider = class extends BaseProvider {
|
|
|
1195
1213
|
parameters: t.inputSchema
|
|
1196
1214
|
}
|
|
1197
1215
|
}));
|
|
1198
|
-
const response = await
|
|
1199
|
-
|
|
1200
|
-
{
|
|
1216
|
+
const response = await fetch(`${this.baseUrl}/api/chat`, {
|
|
1217
|
+
method: "POST",
|
|
1218
|
+
headers: { "Content-Type": "application/json" },
|
|
1219
|
+
body: JSON.stringify({
|
|
1201
1220
|
model: this.model.id,
|
|
1202
1221
|
messages,
|
|
1203
1222
|
stream: true,
|
|
@@ -1206,61 +1225,43 @@ var OllamaProvider = class extends BaseProvider {
|
|
|
1206
1225
|
num_predict: options.maxTokens ?? this.model.maxOutputTokens,
|
|
1207
1226
|
temperature: options.temperature ?? 0.7
|
|
1208
1227
|
}
|
|
1209
|
-
},
|
|
1210
|
-
|
|
1211
|
-
);
|
|
1228
|
+
}),
|
|
1229
|
+
signal: options.signal
|
|
1230
|
+
});
|
|
1231
|
+
if (!response.ok || !response.body) {
|
|
1232
|
+
throw new Error(`Ollama chat request failed: ${response.status} ${response.statusText}`);
|
|
1233
|
+
}
|
|
1212
1234
|
let fullContent = "";
|
|
1213
1235
|
let inputTokens = 0;
|
|
1214
1236
|
let outputTokens = 0;
|
|
1215
1237
|
const pendingToolCalls = [];
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
if (!line.trim()) continue;
|
|
1224
|
-
try {
|
|
1225
|
-
const parsed = JSON.parse(line);
|
|
1226
|
-
if (parsed.message?.content) {
|
|
1227
|
-
fullContent += parsed.message.content;
|
|
1228
|
-
onChunk({ text: parsed.message.content, finishReason: null });
|
|
1229
|
-
}
|
|
1230
|
-
if (parsed.message?.tool_calls?.length) {
|
|
1231
|
-
pendingToolCalls.push(...parsed.message.tool_calls);
|
|
1232
|
-
}
|
|
1233
|
-
if (parsed.done) {
|
|
1234
|
-
inputTokens = parsed.prompt_eval_count ?? 0;
|
|
1235
|
-
outputTokens = parsed.eval_count ?? 0;
|
|
1236
|
-
}
|
|
1237
|
-
} catch {
|
|
1238
|
-
}
|
|
1238
|
+
const handleLine = (line) => {
|
|
1239
|
+
if (!line.trim()) return;
|
|
1240
|
+
try {
|
|
1241
|
+
const parsed = JSON.parse(line);
|
|
1242
|
+
if (parsed.message?.content) {
|
|
1243
|
+
fullContent += parsed.message.content;
|
|
1244
|
+
onChunk({ text: parsed.message.content, finishReason: null });
|
|
1239
1245
|
}
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
const tail = buffer.trim();
|
|
1243
|
-
if (tail) {
|
|
1244
|
-
try {
|
|
1245
|
-
const parsed = JSON.parse(tail);
|
|
1246
|
-
if (parsed.message?.content) {
|
|
1247
|
-
fullContent += parsed.message.content;
|
|
1248
|
-
onChunk({ text: parsed.message.content, finishReason: null });
|
|
1249
|
-
}
|
|
1250
|
-
if (parsed.message?.tool_calls?.length) {
|
|
1251
|
-
pendingToolCalls.push(...parsed.message.tool_calls);
|
|
1252
|
-
}
|
|
1253
|
-
if (parsed.done) {
|
|
1254
|
-
inputTokens = parsed.prompt_eval_count ?? inputTokens;
|
|
1255
|
-
outputTokens = parsed.eval_count ?? outputTokens;
|
|
1256
|
-
}
|
|
1257
|
-
} catch {
|
|
1258
|
-
}
|
|
1246
|
+
if (parsed.message?.tool_calls?.length) {
|
|
1247
|
+
pendingToolCalls.push(...parsed.message.tool_calls);
|
|
1259
1248
|
}
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1249
|
+
if (parsed.done) {
|
|
1250
|
+
inputTokens = parsed.prompt_eval_count ?? inputTokens;
|
|
1251
|
+
outputTokens = parsed.eval_count ?? outputTokens;
|
|
1252
|
+
}
|
|
1253
|
+
} catch {
|
|
1254
|
+
}
|
|
1255
|
+
};
|
|
1256
|
+
let buffer = "";
|
|
1257
|
+
const decoder = new TextDecoder();
|
|
1258
|
+
for await (const chunk of response.body) {
|
|
1259
|
+
buffer += decoder.decode(chunk, { stream: true });
|
|
1260
|
+
const lines = buffer.split("\n");
|
|
1261
|
+
buffer = lines.pop() ?? "";
|
|
1262
|
+
for (const line of lines) handleLine(line);
|
|
1263
|
+
}
|
|
1264
|
+
handleLine(buffer);
|
|
1264
1265
|
const toolCalls = pendingToolCalls.map((tc, i) => {
|
|
1265
1266
|
let input;
|
|
1266
1267
|
if (typeof tc.function.arguments === "string") {
|
|
@@ -1292,9 +1293,11 @@ var OllamaProvider = class extends BaseProvider {
|
|
|
1292
1293
|
}
|
|
1293
1294
|
async listModels() {
|
|
1294
1295
|
try {
|
|
1295
|
-
const response = await
|
|
1296
|
+
const response = await fetch(`${this.baseUrl}/api/tags`);
|
|
1297
|
+
if (!response.ok) return [];
|
|
1298
|
+
const data = await response.json();
|
|
1296
1299
|
const supportedKeywords = ["llama3", "llama2", "gemma", "mistral", "mixtral", "qwen", "phi3", "codellama", "deepseek", "llava", "starcoder", "stable-code", "nomic-embed"];
|
|
1297
|
-
return
|
|
1300
|
+
return data.models.filter((m) => {
|
|
1298
1301
|
const name = m.name.toLowerCase();
|
|
1299
1302
|
return supportedKeywords.some((k) => name.includes(k));
|
|
1300
1303
|
}).map((m) => ({
|
|
@@ -1316,11 +1319,15 @@ var OllamaProvider = class extends BaseProvider {
|
|
|
1316
1319
|
}
|
|
1317
1320
|
}
|
|
1318
1321
|
async isAvailable() {
|
|
1322
|
+
const ac = new AbortController();
|
|
1323
|
+
const timer = setTimeout(() => ac.abort(), 2e3);
|
|
1319
1324
|
try {
|
|
1320
|
-
await
|
|
1321
|
-
return
|
|
1325
|
+
const response = await fetch(`${this.baseUrl}/api/tags`, { signal: ac.signal });
|
|
1326
|
+
return response.ok;
|
|
1322
1327
|
} catch {
|
|
1323
1328
|
return false;
|
|
1329
|
+
} finally {
|
|
1330
|
+
clearTimeout(timer);
|
|
1324
1331
|
}
|
|
1325
1332
|
}
|
|
1326
1333
|
convertMessages(messages, systemPrompt) {
|
|
@@ -1423,6 +1430,19 @@ var ModelSelector = class {
|
|
|
1423
1430
|
addDynamicModel(model) {
|
|
1424
1431
|
this.availableModels.set(model.id, model);
|
|
1425
1432
|
}
|
|
1433
|
+
/**
|
|
1434
|
+
* Permanently drop a model from the available set for this session. Used by
|
|
1435
|
+
* the router's 404 / "model not found" self-heal so a dead id is never
|
|
1436
|
+
* selected again after it fails once.
|
|
1437
|
+
*/
|
|
1438
|
+
removeModel(id) {
|
|
1439
|
+
this.availableModels.delete(id);
|
|
1440
|
+
}
|
|
1441
|
+
/** Look up an available model by exact id (post-discovery/pricing lookups). */
|
|
1442
|
+
getModelById(id) {
|
|
1443
|
+
const m = this.availableModels.get(id);
|
|
1444
|
+
return m && this.availableProviders.has(m.provider) ? m : void 0;
|
|
1445
|
+
}
|
|
1426
1446
|
getAvailableModelsForProvider(provider) {
|
|
1427
1447
|
const models = /* @__PURE__ */ new Map();
|
|
1428
1448
|
for (const model of this.availableModels.values()) {
|
|
@@ -1439,6 +1459,7 @@ var ModelSelector = class {
|
|
|
1439
1459
|
model = this.resolveDynamicModel(overrideModelId);
|
|
1440
1460
|
}
|
|
1441
1461
|
if (model && this.availableProviders.has(model.provider)) return model;
|
|
1462
|
+
return null;
|
|
1442
1463
|
}
|
|
1443
1464
|
if (requireVision) {
|
|
1444
1465
|
return this.selectVisionModel();
|
|
@@ -1501,6 +1522,14 @@ var ModelSelector = class {
|
|
|
1501
1522
|
candidates.push(model);
|
|
1502
1523
|
}
|
|
1503
1524
|
}
|
|
1525
|
+
const localOnly = this.availableProviders.size > 0 && Array.from(this.availableProviders).every((p) => p === "ollama");
|
|
1526
|
+
if (localOnly) {
|
|
1527
|
+
for (const model of this.availableModels.values()) {
|
|
1528
|
+
if (model.isLocal && this.availableProviders.has(model.provider) && !candidates.some((c) => c.id === model.id)) {
|
|
1529
|
+
candidates.push(model);
|
|
1530
|
+
}
|
|
1531
|
+
}
|
|
1532
|
+
}
|
|
1504
1533
|
return candidates;
|
|
1505
1534
|
}
|
|
1506
1535
|
isProviderAvailable(provider) {
|
|
@@ -1905,6 +1934,267 @@ var ModelProfiler = class {
|
|
|
1905
1934
|
}
|
|
1906
1935
|
};
|
|
1907
1936
|
|
|
1937
|
+
// src/core/router/savings.ts
|
|
1938
|
+
var NO_SAVINGS = { savedUsd: 0, savedPct: 0, counterfactualUsd: 0 };
|
|
1939
|
+
function computeDelegationSavings(stats, t1Model) {
|
|
1940
|
+
if (!t1Model) return NO_SAVINGS;
|
|
1941
|
+
let counterfactualUsd = 0;
|
|
1942
|
+
const tiers = /* @__PURE__ */ new Set([
|
|
1943
|
+
...Object.keys(stats.inputTokensByTier),
|
|
1944
|
+
...Object.keys(stats.outputTokensByTier)
|
|
1945
|
+
]);
|
|
1946
|
+
for (const tier of tiers) {
|
|
1947
|
+
counterfactualUsd += calculateCost(
|
|
1948
|
+
stats.inputTokensByTier[tier] ?? 0,
|
|
1949
|
+
stats.outputTokensByTier[tier] ?? 0,
|
|
1950
|
+
t1Model
|
|
1951
|
+
);
|
|
1952
|
+
}
|
|
1953
|
+
const savedUsd = counterfactualUsd - stats.totalCostUsd;
|
|
1954
|
+
if (!(savedUsd > 0) || counterfactualUsd <= 0) {
|
|
1955
|
+
return { ...NO_SAVINGS, counterfactualUsd: Math.max(0, counterfactualUsd) };
|
|
1956
|
+
}
|
|
1957
|
+
return {
|
|
1958
|
+
savedUsd,
|
|
1959
|
+
savedPct: Math.round(savedUsd / counterfactualUsd * 1e3) / 10,
|
|
1960
|
+
counterfactualUsd
|
|
1961
|
+
};
|
|
1962
|
+
}
|
|
1963
|
+
var DEFAULT_SNAPSHOT_URL = "https://raw.githubusercontent.com/Varun-SV/Cascade-AI/main/src/core/router/benchmark-data.json";
|
|
1964
|
+
var OPENROUTER_MODELS_URL = "https://openrouter.ai/api/v1/models";
|
|
1965
|
+
var FETCH_TIMEOUT_MS = 8e3;
|
|
1966
|
+
var DEFAULT_CACHE_FILE = path18__default.default.join(os4__default.default.homedir(), GLOBAL_CONFIG_DIR, "benchmarks-cache.json");
|
|
1967
|
+
function normalizeModelId(id) {
|
|
1968
|
+
let s = id.toLowerCase();
|
|
1969
|
+
const slash = s.lastIndexOf("/");
|
|
1970
|
+
if (slash !== -1) s = s.slice(slash + 1);
|
|
1971
|
+
s = s.replace(/-preview(?:-\d{2}-\d{2})?$/, "");
|
|
1972
|
+
s = s.replace(/-\d{8}$/, "");
|
|
1973
|
+
s = s.replace(/[:@].*$/, "");
|
|
1974
|
+
return s;
|
|
1975
|
+
}
|
|
1976
|
+
var LiveDataProvider = class {
|
|
1977
|
+
snapshot = null;
|
|
1978
|
+
prices = /* @__PURE__ */ new Map();
|
|
1979
|
+
source = "bundled";
|
|
1980
|
+
fetchedAt = 0;
|
|
1981
|
+
loaded = false;
|
|
1982
|
+
refreshing = null;
|
|
1983
|
+
opts;
|
|
1984
|
+
constructor(opts = {}) {
|
|
1985
|
+
this.opts = {
|
|
1986
|
+
live: opts.live ?? true,
|
|
1987
|
+
pricingLive: opts.pricingLive ?? true,
|
|
1988
|
+
refreshHours: opts.refreshHours ?? 24,
|
|
1989
|
+
cacheFile: opts.cacheFile ?? DEFAULT_CACHE_FILE,
|
|
1990
|
+
sourceUrl: opts.sourceUrl
|
|
1991
|
+
};
|
|
1992
|
+
}
|
|
1993
|
+
/** Load cached data from disk (cheap, no network). Safe to call repeatedly. */
|
|
1994
|
+
async load() {
|
|
1995
|
+
if (this.loaded) return;
|
|
1996
|
+
this.loaded = true;
|
|
1997
|
+
try {
|
|
1998
|
+
const raw = await fs4__default.default.readFile(this.opts.cacheFile, "utf-8");
|
|
1999
|
+
const cache = JSON.parse(raw);
|
|
2000
|
+
if (cache.snapshot?.families) {
|
|
2001
|
+
this.snapshot = cache.snapshot;
|
|
2002
|
+
this.source = "cache";
|
|
2003
|
+
}
|
|
2004
|
+
if (cache.prices) {
|
|
2005
|
+
for (const [id, p] of Object.entries(cache.prices)) this.prices.set(id, p);
|
|
2006
|
+
}
|
|
2007
|
+
this.fetchedAt = cache.fetchedAt ?? 0;
|
|
2008
|
+
} catch {
|
|
2009
|
+
}
|
|
2010
|
+
}
|
|
2011
|
+
/**
|
|
2012
|
+
* Refresh from the network if the cache is older than the TTL. Coalesces
|
|
2013
|
+
* concurrent callers and never throws — failures keep last-known-good data.
|
|
2014
|
+
*/
|
|
2015
|
+
async refresh(force = false) {
|
|
2016
|
+
if (this.refreshing) return this.refreshing;
|
|
2017
|
+
this.refreshing = this.doRefresh(force).finally(() => {
|
|
2018
|
+
this.refreshing = null;
|
|
2019
|
+
});
|
|
2020
|
+
return this.refreshing;
|
|
2021
|
+
}
|
|
2022
|
+
async doRefresh(force) {
|
|
2023
|
+
await this.load();
|
|
2024
|
+
const ttlMs = this.opts.refreshHours * 36e5;
|
|
2025
|
+
const fresh = ttlMs > 0 && Date.now() - this.fetchedAt < ttlMs;
|
|
2026
|
+
if (!force && fresh && this.source !== "bundled") return;
|
|
2027
|
+
const [snap, prices] = await Promise.all([
|
|
2028
|
+
this.opts.live ? this.fetchSnapshot() : Promise.resolve(null),
|
|
2029
|
+
this.opts.pricingLive ? this.fetchPrices() : Promise.resolve(null)
|
|
2030
|
+
]);
|
|
2031
|
+
let changed = false;
|
|
2032
|
+
if (snap) {
|
|
2033
|
+
this.snapshot = snap;
|
|
2034
|
+
this.source = "live";
|
|
2035
|
+
changed = true;
|
|
2036
|
+
}
|
|
2037
|
+
if (prices && prices.size > 0) {
|
|
2038
|
+
this.prices = prices;
|
|
2039
|
+
changed = true;
|
|
2040
|
+
}
|
|
2041
|
+
if (changed) {
|
|
2042
|
+
this.fetchedAt = Date.now();
|
|
2043
|
+
await this.saveCache();
|
|
2044
|
+
}
|
|
2045
|
+
}
|
|
2046
|
+
async fetchSnapshot() {
|
|
2047
|
+
const url = this.opts.sourceUrl ?? DEFAULT_SNAPSHOT_URL;
|
|
2048
|
+
try {
|
|
2049
|
+
const resp = await withTimeout(fetch(url), FETCH_TIMEOUT_MS, "benchmark fetch timed out");
|
|
2050
|
+
if (!resp.ok) return null;
|
|
2051
|
+
const data = await resp.json();
|
|
2052
|
+
if (!data || typeof data !== "object" || !data.families || typeof data.families !== "object") {
|
|
2053
|
+
return null;
|
|
2054
|
+
}
|
|
2055
|
+
return data;
|
|
2056
|
+
} catch {
|
|
2057
|
+
return null;
|
|
2058
|
+
}
|
|
2059
|
+
}
|
|
2060
|
+
async fetchPrices() {
|
|
2061
|
+
try {
|
|
2062
|
+
const resp = await withTimeout(fetch(OPENROUTER_MODELS_URL), FETCH_TIMEOUT_MS, "pricing fetch timed out");
|
|
2063
|
+
if (!resp.ok) return null;
|
|
2064
|
+
const data = await resp.json();
|
|
2065
|
+
if (!Array.isArray(data?.data)) return null;
|
|
2066
|
+
const out = /* @__PURE__ */ new Map();
|
|
2067
|
+
for (const m of data.data) {
|
|
2068
|
+
if (!m?.id || !m.pricing) continue;
|
|
2069
|
+
const input = Number(m.pricing.prompt) * 1e3;
|
|
2070
|
+
const output = Number(m.pricing.completion) * 1e3;
|
|
2071
|
+
if (!Number.isFinite(input) || !Number.isFinite(output)) continue;
|
|
2072
|
+
out.set(normalizeModelId(m.id), { input, output });
|
|
2073
|
+
}
|
|
2074
|
+
return out;
|
|
2075
|
+
} catch {
|
|
2076
|
+
return null;
|
|
2077
|
+
}
|
|
2078
|
+
}
|
|
2079
|
+
async saveCache() {
|
|
2080
|
+
try {
|
|
2081
|
+
await fs4__default.default.mkdir(path18__default.default.dirname(this.opts.cacheFile), { recursive: true });
|
|
2082
|
+
const cache = {
|
|
2083
|
+
fetchedAt: this.fetchedAt,
|
|
2084
|
+
snapshot: this.snapshot ?? void 0,
|
|
2085
|
+
prices: Object.fromEntries(this.prices)
|
|
2086
|
+
};
|
|
2087
|
+
await fs4__default.default.writeFile(this.opts.cacheFile, JSON.stringify(cache, null, 2), "utf-8");
|
|
2088
|
+
} catch {
|
|
2089
|
+
}
|
|
2090
|
+
}
|
|
2091
|
+
/** Quality profile for a model family, or null when we have no live/cached data. */
|
|
2092
|
+
getQualityProfile(family) {
|
|
2093
|
+
return this.snapshot?.families?.[family] ?? null;
|
|
2094
|
+
}
|
|
2095
|
+
/** Current per-1k price for a model id, or null when unknown. */
|
|
2096
|
+
getLivePrice(modelId) {
|
|
2097
|
+
return this.prices.get(normalizeModelId(modelId)) ?? null;
|
|
2098
|
+
}
|
|
2099
|
+
/**
|
|
2100
|
+
* Returns a price-corrected copy of each model when live pricing is known,
|
|
2101
|
+
* leaving the original untouched (so the shared catalog is never mutated).
|
|
2102
|
+
*/
|
|
2103
|
+
applyLivePricing(models) {
|
|
2104
|
+
return models.map((m) => {
|
|
2105
|
+
const p = this.getLivePrice(m.id);
|
|
2106
|
+
if (!p) return m;
|
|
2107
|
+
return { ...m, inputCostPer1kTokens: p.input, outputCostPer1kTokens: p.output };
|
|
2108
|
+
});
|
|
2109
|
+
}
|
|
2110
|
+
/** Where the active quality data came from — for /why and `cascade models`. */
|
|
2111
|
+
getDataSource() {
|
|
2112
|
+
return this.source;
|
|
2113
|
+
}
|
|
2114
|
+
getGeneratedAt() {
|
|
2115
|
+
return this.snapshot?.generatedAt ?? null;
|
|
2116
|
+
}
|
|
2117
|
+
hasLivePricing() {
|
|
2118
|
+
return this.prices.size > 0;
|
|
2119
|
+
}
|
|
2120
|
+
};
|
|
2121
|
+
|
|
2122
|
+
// src/core/router/benchmarks.ts
|
|
2123
|
+
var liveProvider = null;
|
|
2124
|
+
function setBenchmarkLiveProvider(provider) {
|
|
2125
|
+
liveProvider = provider;
|
|
2126
|
+
}
|
|
2127
|
+
var FAMILY_BENCHMARKS = {
|
|
2128
|
+
// Anthropic — strongest at coding and agentic tool-use.
|
|
2129
|
+
"claude-opus": { code: 95, analysis: 92, creative: 90, data: 88 },
|
|
2130
|
+
"claude-sonnet": { code: 93, analysis: 88, creative: 87, data: 85 },
|
|
2131
|
+
"claude-haiku": { code: 80, analysis: 75, creative: 76, data: 72 },
|
|
2132
|
+
// OpenAI — strong all-round, particularly creative/writing.
|
|
2133
|
+
"gpt-4.1": { code: 90, analysis: 89, creative: 91, data: 87 },
|
|
2134
|
+
"gpt-4.1-mini": { code: 82, analysis: 80, creative: 83, data: 79 },
|
|
2135
|
+
"gpt-4.1-nano": { code: 70, analysis: 68, creative: 72, data: 66 },
|
|
2136
|
+
"gpt-4o": { code: 86, analysis: 85, creative: 90, data: 84 },
|
|
2137
|
+
"gpt-4o-mini": { code: 76, analysis: 74, creative: 80, data: 72 },
|
|
2138
|
+
// Google — strongest at analysis/data and long-context.
|
|
2139
|
+
"gemini-2.5-pro": { code: 90, analysis: 93, creative: 86, data: 92 },
|
|
2140
|
+
"gemini-2.5-flash": { code: 82, analysis: 83, creative: 80, data: 82 },
|
|
2141
|
+
"gemini-1.5-pro": { code: 82, analysis: 84, creative: 82, data: 85 },
|
|
2142
|
+
"gemini-2.0-flash": { code: 79, analysis: 80, creative: 79, data: 80 },
|
|
2143
|
+
"gemini-flash-lite": { code: 68, analysis: 68, creative: 70, data: 68 },
|
|
2144
|
+
// Local (Ollama) — lower absolute scores; the ordering is what matters when a
|
|
2145
|
+
// tier is restricted to local-only models.
|
|
2146
|
+
"deepseek": { code: 80, analysis: 72, creative: 68, data: 74 },
|
|
2147
|
+
"qwen": { code: 78, analysis: 73, creative: 72, data: 74 },
|
|
2148
|
+
"codellama": { code: 76, analysis: 60, creative: 55, data: 60 },
|
|
2149
|
+
"llama-70b": { code: 74, analysis: 72, creative: 73, data: 70 },
|
|
2150
|
+
"mistral": { code: 62, analysis: 64, creative: 66, data: 60 },
|
|
2151
|
+
"gemma": { code: 58, analysis: 60, creative: 62, data: 57 },
|
|
2152
|
+
"llama-small": { code: 55, analysis: 56, creative: 60, data: 54 }
|
|
2153
|
+
};
|
|
2154
|
+
var FAMILY_MATCHERS = [
|
|
2155
|
+
[/opus/i, "claude-opus"],
|
|
2156
|
+
[/sonnet/i, "claude-sonnet"],
|
|
2157
|
+
[/haiku/i, "claude-haiku"],
|
|
2158
|
+
[/gpt-?4\.1-nano/i, "gpt-4.1-nano"],
|
|
2159
|
+
[/gpt-?4\.1-mini/i, "gpt-4.1-mini"],
|
|
2160
|
+
[/gpt-?4\.1/i, "gpt-4.1"],
|
|
2161
|
+
[/gpt-?4o-mini/i, "gpt-4o-mini"],
|
|
2162
|
+
[/gpt-?4o/i, "gpt-4o"],
|
|
2163
|
+
[/gemini-?2\.5-pro/i, "gemini-2.5-pro"],
|
|
2164
|
+
[/gemini-?2\.5-flash/i, "gemini-2.5-flash"],
|
|
2165
|
+
[/gemini-?1\.5-pro/i, "gemini-1.5-pro"],
|
|
2166
|
+
[/gemini-?2\.0-flash-lite/i, "gemini-flash-lite"],
|
|
2167
|
+
[/gemini-?2\.0-flash/i, "gemini-2.0-flash"],
|
|
2168
|
+
[/codellama|code-llama|starcoder|stable-code/i, "codellama"],
|
|
2169
|
+
[/deepseek/i, "deepseek"],
|
|
2170
|
+
[/qwen/i, "qwen"],
|
|
2171
|
+
[/llama.?3.*70b|llama3:70b|llama-3-70b/i, "llama-70b"],
|
|
2172
|
+
[/llama/i, "llama-small"],
|
|
2173
|
+
[/mistral|mixtral/i, "mistral"],
|
|
2174
|
+
[/gemma/i, "gemma"]
|
|
2175
|
+
];
|
|
2176
|
+
function resolveFamily(model) {
|
|
2177
|
+
const hay = `${model.id} ${model.name}`;
|
|
2178
|
+
for (const [re, fam] of FAMILY_MATCHERS) {
|
|
2179
|
+
if (re.test(hay)) return fam;
|
|
2180
|
+
}
|
|
2181
|
+
return null;
|
|
2182
|
+
}
|
|
2183
|
+
function benchmarkScore01(model, taskType) {
|
|
2184
|
+
const fam = resolveFamily(model);
|
|
2185
|
+
if (!fam) return 0.5;
|
|
2186
|
+
const profile = liveProvider?.getQualityProfile(fam) ?? FAMILY_BENCHMARKS[fam];
|
|
2187
|
+
if (!profile) return 0.5;
|
|
2188
|
+
let score;
|
|
2189
|
+
if (taskType === "mixed") {
|
|
2190
|
+
const vals = Object.values(profile).filter((v) => typeof v === "number");
|
|
2191
|
+
score = vals.length ? vals.reduce((a, b) => a + b, 0) / vals.length : 50;
|
|
2192
|
+
} else {
|
|
2193
|
+
score = profile[taskType] ?? 50;
|
|
2194
|
+
}
|
|
2195
|
+
return Math.max(0, Math.min(1, score / 100));
|
|
2196
|
+
}
|
|
2197
|
+
|
|
1908
2198
|
// src/core/router/index.ts
|
|
1909
2199
|
var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
|
|
1910
2200
|
selector;
|
|
@@ -1923,6 +2213,12 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
|
|
|
1923
2213
|
tierModels = /* @__PURE__ */ new Map();
|
|
1924
2214
|
config;
|
|
1925
2215
|
sessionCostUsd = 0;
|
|
2216
|
+
// Per-run accounting for the hard per-task cap. Reset by beginRun() at the
|
|
2217
|
+
// start of every `cascade run`, independent of the session-wide budget.
|
|
2218
|
+
runTokens = 0;
|
|
2219
|
+
runCostUsd = 0;
|
|
2220
|
+
runBudgetExceeded = false;
|
|
2221
|
+
runBudgetExceededReason;
|
|
1926
2222
|
/**
|
|
1927
2223
|
* Budget state machine — guards against two concurrent `generate()` calls
|
|
1928
2224
|
* each firing the warning or both slipping past the hard cap. All
|
|
@@ -1933,6 +2229,12 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
|
|
|
1933
2229
|
budgetExceededReason;
|
|
1934
2230
|
tpmLimiter;
|
|
1935
2231
|
localQueue;
|
|
2232
|
+
taskAnalyzer;
|
|
2233
|
+
liveData;
|
|
2234
|
+
/** Snapshot of configured/default tier models, taken before Cascade Auto overrides them. */
|
|
2235
|
+
originalTierModels;
|
|
2236
|
+
/** The current run's abort signal — injected into every provider call so a cancel aborts in-flight requests. */
|
|
2237
|
+
runSignal;
|
|
1936
2238
|
/** Thrown when the configured budget is exceeded. */
|
|
1937
2239
|
static BudgetExceededError = class extends Error {
|
|
1938
2240
|
constructor(msg) {
|
|
@@ -1959,10 +2261,17 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
|
|
|
1959
2261
|
if (!override) continue;
|
|
1960
2262
|
const model = this.selector.selectForTier(tier, override);
|
|
1961
2263
|
if (!model) {
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
2264
|
+
const knownProviders = ["anthropic", "openai", "gemini", "azure", "openai-compatible", "ollama"];
|
|
2265
|
+
const hasProviderPrefix = override.includes(":") && knownProviders.some((p) => override.startsWith(p + ":"));
|
|
2266
|
+
if (hasProviderPrefix) {
|
|
2267
|
+
const provider = override.split(":")[0];
|
|
2268
|
+
throw new Error(
|
|
2269
|
+
`Configured model "${override}" for ${tier} cannot be used: provider '${provider}' is not available or unreachable. Check that the provider is running and accessible.`
|
|
2270
|
+
);
|
|
2271
|
+
}
|
|
2272
|
+
throw new Error(
|
|
2273
|
+
`Configured model "${override}" for ${tier} could not be loaded. Check provider availability and exact model name.`
|
|
2274
|
+
);
|
|
1966
2275
|
}
|
|
1967
2276
|
this.tierModels.set(tier, model);
|
|
1968
2277
|
this.ensureProvider(model, config.providers);
|
|
@@ -1987,19 +2296,93 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
|
|
|
1987
2296
|
profiler.profileAll(allModels).catch(() => {
|
|
1988
2297
|
});
|
|
1989
2298
|
}
|
|
2299
|
+
/**
|
|
2300
|
+
* Cascade Auto live data: discover/validate real model ids from each cloud
|
|
2301
|
+
* provider, then fetch current public quality scores + per-token prices and
|
|
2302
|
+
* apply the prices to the available-model set. Best-effort and safe to run in
|
|
2303
|
+
* the background — any failure leaves the bundled catalog/benchmarks in effect.
|
|
2304
|
+
*/
|
|
2305
|
+
async refreshLiveData() {
|
|
2306
|
+
const benchCfg = this.config.benchmarks ?? {};
|
|
2307
|
+
if (!this.liveData) {
|
|
2308
|
+
this.liveData = new LiveDataProvider({
|
|
2309
|
+
live: benchCfg.live,
|
|
2310
|
+
pricingLive: benchCfg.pricingLive,
|
|
2311
|
+
refreshHours: benchCfg.refreshHours,
|
|
2312
|
+
sourceUrl: benchCfg.sourceUrl
|
|
2313
|
+
});
|
|
2314
|
+
setBenchmarkLiveProvider(this.liveData);
|
|
2315
|
+
}
|
|
2316
|
+
await this.discoverProviderModels();
|
|
2317
|
+
await this.liveData.refresh().catch(() => {
|
|
2318
|
+
});
|
|
2319
|
+
this.applyLivePricing();
|
|
2320
|
+
}
|
|
2321
|
+
/** Returns the live-data provider once refreshLiveData has run (UX/insight). */
|
|
2322
|
+
getLiveData() {
|
|
2323
|
+
return this.liveData;
|
|
2324
|
+
}
|
|
2325
|
+
/**
|
|
2326
|
+
* Query each available cloud provider's live model list and register the
|
|
2327
|
+
* results. Confirms catalog ids still exist and surfaces newly released
|
|
2328
|
+
* models without a package upgrade. Mirrors discoverOllamaModels.
|
|
2329
|
+
*/
|
|
2330
|
+
async discoverProviderModels() {
|
|
2331
|
+
const cloud = ["anthropic", "openai", "gemini", "azure", "openai-compatible"];
|
|
2332
|
+
const tasks = cloud.map(async (type) => {
|
|
2333
|
+
if (!this.selector.isProviderAvailable(type)) return;
|
|
2334
|
+
const seed = this.getAnyModelForProvider(type);
|
|
2335
|
+
if (!seed) return;
|
|
2336
|
+
const cfg = this.config.providers.find((p) => p.type === type) ?? { type };
|
|
2337
|
+
try {
|
|
2338
|
+
const provider = this.createProvider(cfg, seed);
|
|
2339
|
+
if (typeof provider.listModels !== "function") return;
|
|
2340
|
+
const models = await provider.listModels();
|
|
2341
|
+
for (const m of models) this.selector.addDynamicModel(m);
|
|
2342
|
+
} catch {
|
|
2343
|
+
}
|
|
2344
|
+
});
|
|
2345
|
+
await Promise.allSettled(tasks);
|
|
2346
|
+
}
|
|
2347
|
+
/**
|
|
2348
|
+
* Replace available models with live-priced copies and refresh the already
|
|
2349
|
+
* resolved tier models so shared-tier cost accounting uses current prices.
|
|
2350
|
+
*/
|
|
2351
|
+
applyLivePricing() {
|
|
2352
|
+
if (!this.liveData?.hasLivePricing()) return;
|
|
2353
|
+
const updated = this.liveData.applyLivePricing(this.selector.getAllAvailableModels());
|
|
2354
|
+
for (const m of updated) this.selector.addDynamicModel(m);
|
|
2355
|
+
for (const tier of ["T1", "T2", "T3"]) {
|
|
2356
|
+
const cur = this.tierModels.get(tier);
|
|
2357
|
+
if (!cur) continue;
|
|
2358
|
+
const fresh = this.selector.getModelById(cur.id);
|
|
2359
|
+
if (fresh) this.tierModels.set(tier, fresh);
|
|
2360
|
+
}
|
|
2361
|
+
}
|
|
1990
2362
|
async generate(tier, options, onChunk, requireVision = false) {
|
|
1991
2363
|
if (this.budgetState === "exceeded") {
|
|
1992
2364
|
throw new _CascadeRouter.BudgetExceededError(
|
|
1993
2365
|
this.budgetExceededReason ?? "Session budget exceeded."
|
|
1994
2366
|
);
|
|
1995
2367
|
}
|
|
2368
|
+
if (this.runBudgetExceeded) {
|
|
2369
|
+
throw new _CascadeRouter.BudgetExceededError(
|
|
2370
|
+
this.runBudgetExceededReason ?? "Per-task budget exceeded."
|
|
2371
|
+
);
|
|
2372
|
+
}
|
|
1996
2373
|
const limits = this.config?.tierLimits;
|
|
1997
2374
|
const tierKey = tier.toLowerCase();
|
|
1998
2375
|
const tierMaxTokens = limits?.[`${tierKey}MaxTokens`];
|
|
1999
2376
|
if (tierMaxTokens && (!options.maxTokens || options.maxTokens > tierMaxTokens)) {
|
|
2000
2377
|
options = { ...options, maxTokens: tierMaxTokens };
|
|
2001
2378
|
}
|
|
2002
|
-
|
|
2379
|
+
if (this.runSignal && !options.signal) {
|
|
2380
|
+
options = { ...options, signal: this.runSignal };
|
|
2381
|
+
}
|
|
2382
|
+
if (options.model && !requireVision) {
|
|
2383
|
+
this.ensureProvider(options.model, this.config.providers);
|
|
2384
|
+
}
|
|
2385
|
+
const model = requireVision ? this.selector.selectVisionModel() : options.model ?? this.tierModels.get(tier);
|
|
2003
2386
|
if (!model) throw new Error(`No model available for tier ${tier}`);
|
|
2004
2387
|
const provider = this.getProvider(model);
|
|
2005
2388
|
if (!provider) throw new Error(`No provider for model ${model.id}`);
|
|
@@ -2028,16 +2411,33 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
|
|
|
2028
2411
|
`Local model ${model.id} inference timed out after ${inferenceTimeoutMs}ms`
|
|
2029
2412
|
);
|
|
2030
2413
|
} else if (useStream && onChunk) {
|
|
2414
|
+
const cloudTimeoutMs = this.config.cloudInferenceTimeoutMs ?? 12e4;
|
|
2031
2415
|
try {
|
|
2032
|
-
result = await
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2416
|
+
result = await withTimeout(
|
|
2417
|
+
provider.generateStream(options, (chunk) => {
|
|
2418
|
+
const text = typeof chunk?.text === "string" ? chunk.text : "";
|
|
2419
|
+
if (text) onChunk({ ...chunk, text });
|
|
2420
|
+
}),
|
|
2421
|
+
cloudTimeoutMs,
|
|
2422
|
+
`Model ${model.id} stream timed out after ${cloudTimeoutMs}ms`
|
|
2423
|
+
);
|
|
2424
|
+
} catch (streamErr) {
|
|
2425
|
+
if (streamErr instanceof Error && streamErr.name === "AbortError" || this.runSignal?.aborted || options.signal?.aborted) {
|
|
2426
|
+
throw streamErr;
|
|
2427
|
+
}
|
|
2428
|
+
result = await withTimeout(
|
|
2429
|
+
provider.generate(options),
|
|
2430
|
+
cloudTimeoutMs,
|
|
2431
|
+
`Model ${model.id} inference timed out after ${cloudTimeoutMs}ms`
|
|
2432
|
+
);
|
|
2038
2433
|
}
|
|
2039
2434
|
} else {
|
|
2040
|
-
|
|
2435
|
+
const cloudTimeoutMs = this.config.cloudInferenceTimeoutMs ?? 12e4;
|
|
2436
|
+
result = await withTimeout(
|
|
2437
|
+
provider.generate(options),
|
|
2438
|
+
cloudTimeoutMs,
|
|
2439
|
+
`Model ${model.id} inference timed out after ${cloudTimeoutMs}ms`
|
|
2440
|
+
);
|
|
2041
2441
|
}
|
|
2042
2442
|
const correctedCost = calculateCost(
|
|
2043
2443
|
result.usage.inputTokens,
|
|
@@ -2058,6 +2458,9 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
|
|
|
2058
2458
|
this.failover.recordSuccess(model.provider);
|
|
2059
2459
|
return result;
|
|
2060
2460
|
} catch (err) {
|
|
2461
|
+
if (err instanceof Error && err.name === "AbortError" || this.runSignal?.aborted || options.signal?.aborted) {
|
|
2462
|
+
throw new CascadeCancelledError("Run cancelled");
|
|
2463
|
+
}
|
|
2061
2464
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
2062
2465
|
if (this.isRateLimitError(errMsg)) {
|
|
2063
2466
|
this.failover.recordFailure(model.provider, "rate_limit");
|
|
@@ -2065,11 +2468,35 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
|
|
|
2065
2468
|
if (fallback) {
|
|
2066
2469
|
this.tierModels.set(tier, fallback);
|
|
2067
2470
|
this.ensureProvider(fallback, this.config.providers);
|
|
2471
|
+
this.emit("failover", {
|
|
2472
|
+
tier,
|
|
2473
|
+
from: `${model.provider}:${model.id}`,
|
|
2474
|
+
to: `${fallback.provider}:${fallback.id}`,
|
|
2475
|
+
reason: "rate limit"
|
|
2476
|
+
});
|
|
2068
2477
|
releaseLocalSlot?.();
|
|
2069
2478
|
releaseLocalSlot = void 0;
|
|
2070
2479
|
return this.generate(tier, options, onChunk, requireVision);
|
|
2071
2480
|
}
|
|
2072
2481
|
}
|
|
2482
|
+
if (isModelNotFoundError(errMsg)) {
|
|
2483
|
+
this.selector.removeModel(model.id);
|
|
2484
|
+
const next = this.selector.selectForTier(tier);
|
|
2485
|
+
if (next && next.id !== model.id) {
|
|
2486
|
+
this.tierModels.set(tier, next);
|
|
2487
|
+
this.ensureProvider(next, this.config.providers);
|
|
2488
|
+
this.emit("failover", {
|
|
2489
|
+
tier,
|
|
2490
|
+
from: `${model.provider}:${model.id}`,
|
|
2491
|
+
to: `${next.provider}:${next.id}`,
|
|
2492
|
+
reason: "model not found"
|
|
2493
|
+
});
|
|
2494
|
+
releaseLocalSlot?.();
|
|
2495
|
+
releaseLocalSlot = void 0;
|
|
2496
|
+
const retryOpts = options.model && options.model.id === model.id ? { ...options, model: void 0 } : options;
|
|
2497
|
+
return this.generate(tier, retryOpts, onChunk, requireVision);
|
|
2498
|
+
}
|
|
2499
|
+
}
|
|
2073
2500
|
throw err;
|
|
2074
2501
|
} finally {
|
|
2075
2502
|
releaseLocalSlot?.();
|
|
@@ -2078,18 +2505,74 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
|
|
|
2078
2505
|
getModelForTier(tier) {
|
|
2079
2506
|
return this.tierModels.get(tier);
|
|
2080
2507
|
}
|
|
2508
|
+
/** Reflection settings for workers (config.reflection). Off unless enabled. */
|
|
2509
|
+
getReflectionConfig() {
|
|
2510
|
+
const r = this.config?.reflection;
|
|
2511
|
+
return { enabled: r?.enabled === true, maxRounds: r?.maxRounds ?? 1 };
|
|
2512
|
+
}
|
|
2513
|
+
/** T3→T2 reinforcement settings (config.reinforcements). Off unless enabled. */
|
|
2514
|
+
getReinforcementsConfig() {
|
|
2515
|
+
const r = this.config?.reinforcements;
|
|
2516
|
+
return { enabled: r?.enabled === true, maxPerSection: r?.maxPerSection ?? 4 };
|
|
2517
|
+
}
|
|
2518
|
+
/**
|
|
2519
|
+
* Resolved T3 wave execution mode. 'auto' becomes 'sequential' when the T3
|
|
2520
|
+
* tier resolves to a LOCAL model (the single-GPU queue serializes anyway, so
|
|
2521
|
+
* running them in parallel just thrashes it), and 'parallel' for cloud.
|
|
2522
|
+
*/
|
|
2523
|
+
getT3ExecutionMode() {
|
|
2524
|
+
const mode = this.config?.t3Execution ?? "auto";
|
|
2525
|
+
if (mode === "parallel" || mode === "sequential") return mode;
|
|
2526
|
+
return this.tierModels.get("T3")?.isLocal ? "sequential" : "parallel";
|
|
2527
|
+
}
|
|
2081
2528
|
/**
|
|
2082
2529
|
* Cascade Auto: temporarily override the model for a tier.
|
|
2083
2530
|
* Used by TaskAnalyzer to inject task-optimal models before execution.
|
|
2084
2531
|
* The override is valid for the current task only — restored by restoreTierModels().
|
|
2085
2532
|
*/
|
|
2086
2533
|
overrideTierModel(tier, model) {
|
|
2534
|
+
if (!this.originalTierModels) {
|
|
2535
|
+
this.originalTierModels = new Map(this.tierModels);
|
|
2536
|
+
}
|
|
2087
2537
|
this.tierModels.set(tier, model);
|
|
2088
2538
|
this.ensureProvider(model, this.config.providers);
|
|
2089
2539
|
}
|
|
2540
|
+
/**
|
|
2541
|
+
* Restore tier models to the configured/default baseline captured before the
|
|
2542
|
+
* first Cascade Auto override. Called at the end of each run so `/why`, the
|
|
2543
|
+
* status bar, and the next run reflect the configured models, not stale picks.
|
|
2544
|
+
*/
|
|
2545
|
+
restoreTierModels() {
|
|
2546
|
+
if (this.originalTierModels) {
|
|
2547
|
+
this.tierModels = new Map(this.originalTierModels);
|
|
2548
|
+
this.originalTierModels = void 0;
|
|
2549
|
+
}
|
|
2550
|
+
}
|
|
2551
|
+
/** Set (or clear) the current run's abort signal for instant cancellation. */
|
|
2552
|
+
setRunSignal(signal) {
|
|
2553
|
+
this.runSignal = signal;
|
|
2554
|
+
}
|
|
2090
2555
|
getSelector() {
|
|
2091
2556
|
return this.selector;
|
|
2092
2557
|
}
|
|
2558
|
+
/** Wire the Cascade Auto task analyzer used for per-subtask model routing. */
|
|
2559
|
+
setTaskAnalyzer(analyzer) {
|
|
2560
|
+
this.taskAnalyzer = analyzer;
|
|
2561
|
+
}
|
|
2562
|
+
/**
|
|
2563
|
+
* Cascade Auto per-subtask routing: pick the benchmark-best model for a
|
|
2564
|
+
* specific subtask's text, scoped to the tier's eligible candidates. Returns
|
|
2565
|
+
* null when Cascade Auto is off (callers then use the shared tier model).
|
|
2566
|
+
* Pure heuristic — no extra LLM call.
|
|
2567
|
+
*/
|
|
2568
|
+
async selectModelForSubtask(tier, text) {
|
|
2569
|
+
if (!this.config?.cascadeAuto || !this.taskAnalyzer || !text.trim()) return null;
|
|
2570
|
+
try {
|
|
2571
|
+
return await this.taskAnalyzer.selectModel(text, tier, this.selector);
|
|
2572
|
+
} catch {
|
|
2573
|
+
return null;
|
|
2574
|
+
}
|
|
2575
|
+
}
|
|
2093
2576
|
getStats() {
|
|
2094
2577
|
return {
|
|
2095
2578
|
totalTokens: this.stats.totalTokens,
|
|
@@ -2102,6 +2585,14 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
|
|
|
2102
2585
|
outputTokensByTier: { ...this.stats.outputTokensByTier }
|
|
2103
2586
|
};
|
|
2104
2587
|
}
|
|
2588
|
+
/**
|
|
2589
|
+
* What did delegation save? Compares actual spend against the
|
|
2590
|
+
* counterfactual of every call running on the T1 model. This is the
|
|
2591
|
+
* number only a tiered hierarchy can show.
|
|
2592
|
+
*/
|
|
2593
|
+
getDelegationSavings() {
|
|
2594
|
+
return computeDelegationSavings(this.stats, this.tierModels.get("T1"));
|
|
2595
|
+
}
|
|
2105
2596
|
/**
|
|
2106
2597
|
* Returns a human-readable cost summary broken down by tier.
|
|
2107
2598
|
* Example: { T1: "$0.0120 (2 calls, 1500 tokens)", T2: "$0.0043 (6 calls, 4200 tokens)", ... }
|
|
@@ -2160,6 +2651,11 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
|
|
|
2160
2651
|
* Sets (or clears) a runtime session budget cap (USD).
|
|
2161
2652
|
* Pass null to remove the cap.
|
|
2162
2653
|
*/
|
|
2654
|
+
/** Raise/set the per-task token cap at runtime (used by /continue resume). */
|
|
2655
|
+
setMaxTokensPerRun(maxTokens) {
|
|
2656
|
+
if (!this.config) return;
|
|
2657
|
+
this.config = { ...this.config, budget: { ...this.config.budget, maxTokensPerRun: maxTokens } };
|
|
2658
|
+
}
|
|
2163
2659
|
setSessionBudget(usd) {
|
|
2164
2660
|
if (!this.config) return;
|
|
2165
2661
|
if (!this.config.budget) {
|
|
@@ -2262,7 +2758,39 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
|
|
|
2262
2758
|
this.stats.tokensByTier[tier] = (this.stats.tokensByTier[tier] ?? 0) + usage.totalTokens;
|
|
2263
2759
|
this.stats.inputTokensByTier[tier] = (this.stats.inputTokensByTier[tier] ?? 0) + usage.inputTokens;
|
|
2264
2760
|
this.stats.outputTokensByTier[tier] = (this.stats.outputTokensByTier[tier] ?? 0) + usage.outputTokens;
|
|
2761
|
+
this.runTokens += usage.totalTokens;
|
|
2762
|
+
this.runCostUsd += usage.estimatedCostUsd;
|
|
2265
2763
|
this.updateBudgetState();
|
|
2764
|
+
this.enforceRunBudget();
|
|
2765
|
+
}
|
|
2766
|
+
/**
|
|
2767
|
+
* Resets per-run accounting at the start of each `cascade run`. Session
|
|
2768
|
+
* totals and a session-wide budget halt are deliberately preserved; only the
|
|
2769
|
+
* per-task ceiling is cleared so the next task starts with a fresh allowance.
|
|
2770
|
+
*/
|
|
2771
|
+
beginRun() {
|
|
2772
|
+
this.runTokens = 0;
|
|
2773
|
+
this.runCostUsd = 0;
|
|
2774
|
+
this.runBudgetExceeded = false;
|
|
2775
|
+
this.runBudgetExceededReason = void 0;
|
|
2776
|
+
}
|
|
2777
|
+
/**
|
|
2778
|
+
* Enforce the hard per-task ceiling. Once tripped, the flag makes every
|
|
2779
|
+
* subsequent (and concurrent) generate() call in this run fail fast.
|
|
2780
|
+
*/
|
|
2781
|
+
enforceRunBudget() {
|
|
2782
|
+
if (this.runBudgetExceeded) return;
|
|
2783
|
+
const budget = this.config?.budget;
|
|
2784
|
+
const maxTokens = budget?.maxTokensPerRun;
|
|
2785
|
+
const maxCost = budget?.maxCostPerRunUsd;
|
|
2786
|
+
const overTokens = maxTokens != null && this.runTokens >= maxTokens;
|
|
2787
|
+
const overCost = maxCost != null && this.runCostUsd >= maxCost;
|
|
2788
|
+
if (!overTokens && !overCost) return;
|
|
2789
|
+
const reason = overTokens ? `Per-task token cap of ${maxTokens.toLocaleString()} reached (used ${this.runTokens.toLocaleString()}). Stopping this run to avoid runaway cost \u2014 raise budget.maxTokensPerRun for larger jobs.` : `Per-task cost cap of $${maxCost.toFixed(4)} reached (spent $${this.runCostUsd.toFixed(4)}). Stopping this run to avoid runaway cost.`;
|
|
2790
|
+
this.runBudgetExceeded = true;
|
|
2791
|
+
this.runBudgetExceededReason = reason;
|
|
2792
|
+
this.emit("budget:exceeded", { reason, spentUsd: this.sessionCostUsd });
|
|
2793
|
+
throw new _CascadeRouter.BudgetExceededError(reason);
|
|
2266
2794
|
}
|
|
2267
2795
|
/**
|
|
2268
2796
|
* Single point of truth for budget state transitions. Called after each
|
|
@@ -2312,6 +2840,9 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
|
|
|
2312
2840
|
return /rate.?limit|429|too.?many.?requests|quota/i.test(msg);
|
|
2313
2841
|
}
|
|
2314
2842
|
};
|
|
2843
|
+
function isModelNotFoundError(msg) {
|
|
2844
|
+
return /not[_\s]?found|404|does not exist|no such model|unknown model|invalid model|model_not_found|not supported for generatecontent|is not supported for/i.test(msg);
|
|
2845
|
+
}
|
|
2315
2846
|
var BaseTier = class extends EventEmitter__default.default {
|
|
2316
2847
|
id;
|
|
2317
2848
|
role;
|
|
@@ -2594,60 +3125,95 @@ var AuditLogger = class {
|
|
|
2594
3125
|
|
|
2595
3126
|
// src/tools/text-tool-parser.ts
|
|
2596
3127
|
var TOOL_CALL_RE = /<tool_call>\s*([\s\S]*?)\s*<\/tool_call>/g;
|
|
2597
|
-
var JSON_BLOCK_RE = /```json
|
|
2598
|
-
var FUNCTION_OBJ_RE = /\{\s*"function"\s*:\s*\{[^}]*"name"\s*:[^}]*\}\s*\}/g;
|
|
3128
|
+
var JSON_BLOCK_RE = /```(?:json|tool_call|tool)?\s*([\s\S]*?)```/g;
|
|
2599
3129
|
function parseTextToolCalls(text) {
|
|
2600
|
-
const
|
|
2601
|
-
if (
|
|
2602
|
-
const
|
|
2603
|
-
if (
|
|
2604
|
-
return
|
|
3130
|
+
const xml = collect(text, TOOL_CALL_RE);
|
|
3131
|
+
if (xml.length > 0) return xml;
|
|
3132
|
+
const fenced = collect(text, JSON_BLOCK_RE);
|
|
3133
|
+
if (fenced.length > 0) return fenced;
|
|
3134
|
+
return tryBareObjects(text);
|
|
2605
3135
|
}
|
|
2606
|
-
function
|
|
3136
|
+
function collect(text, re) {
|
|
2607
3137
|
const results = [];
|
|
2608
3138
|
let match;
|
|
2609
|
-
|
|
2610
|
-
while ((match =
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
results.push({ name: raw.name, input });
|
|
2616
|
-
} catch {
|
|
2617
|
-
}
|
|
3139
|
+
re.lastIndex = 0;
|
|
3140
|
+
while ((match = re.exec(text)) !== null) {
|
|
3141
|
+
const body = (match[1] ?? "").trim();
|
|
3142
|
+
const parsed = parseJsonLoose(body);
|
|
3143
|
+
const call = coerceCall(parsed);
|
|
3144
|
+
if (call) results.push(call);
|
|
2618
3145
|
}
|
|
2619
3146
|
return results;
|
|
2620
3147
|
}
|
|
2621
|
-
function
|
|
3148
|
+
function tryBareObjects(text) {
|
|
2622
3149
|
const results = [];
|
|
2623
|
-
let
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
3150
|
+
for (let i = 0; i < text.length; i++) {
|
|
3151
|
+
if (text[i] !== "{") continue;
|
|
3152
|
+
let depth = 0;
|
|
3153
|
+
let inStr = false;
|
|
3154
|
+
let esc = false;
|
|
3155
|
+
let end = -1;
|
|
3156
|
+
for (let j = i; j < text.length; j++) {
|
|
3157
|
+
const c = text[j];
|
|
3158
|
+
if (esc) {
|
|
3159
|
+
esc = false;
|
|
3160
|
+
continue;
|
|
3161
|
+
}
|
|
3162
|
+
if (c === "\\") {
|
|
3163
|
+
esc = true;
|
|
3164
|
+
continue;
|
|
3165
|
+
}
|
|
3166
|
+
if (c === '"') {
|
|
3167
|
+
inStr = !inStr;
|
|
3168
|
+
continue;
|
|
3169
|
+
}
|
|
3170
|
+
if (inStr) continue;
|
|
3171
|
+
if (c === "{") depth++;
|
|
3172
|
+
else if (c === "}") {
|
|
3173
|
+
depth--;
|
|
3174
|
+
if (depth === 0) {
|
|
3175
|
+
end = j;
|
|
3176
|
+
break;
|
|
3177
|
+
}
|
|
3178
|
+
}
|
|
2632
3179
|
}
|
|
3180
|
+
if (end === -1) break;
|
|
3181
|
+
const candidate = text.slice(i, end + 1);
|
|
3182
|
+
if (/['"]name['"]\s*:/.test(candidate) && /['"](?:input|arguments)['"]\s*:/.test(candidate)) {
|
|
3183
|
+
const call = coerceCall(parseJsonLoose(candidate));
|
|
3184
|
+
if (call) results.push(call);
|
|
3185
|
+
}
|
|
3186
|
+
i = end;
|
|
2633
3187
|
}
|
|
2634
3188
|
return results;
|
|
2635
3189
|
}
|
|
2636
|
-
function
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
3190
|
+
function parseJsonLoose(raw) {
|
|
3191
|
+
if (!raw) return null;
|
|
3192
|
+
try {
|
|
3193
|
+
return JSON.parse(raw);
|
|
3194
|
+
} catch {
|
|
2641
3195
|
try {
|
|
2642
|
-
|
|
2643
|
-
const fn = raw.function;
|
|
2644
|
-
if (!fn || typeof fn.name !== "string") continue;
|
|
2645
|
-
const input = typeof fn.arguments === "object" && fn.arguments !== null ? fn.arguments : {};
|
|
2646
|
-
results.push({ name: fn.name, input });
|
|
3196
|
+
return JSON.parse(raw.replace(/'/g, '"'));
|
|
2647
3197
|
} catch {
|
|
3198
|
+
return null;
|
|
2648
3199
|
}
|
|
2649
3200
|
}
|
|
2650
|
-
|
|
3201
|
+
}
|
|
3202
|
+
function coerceCall(raw) {
|
|
3203
|
+
if (!raw || typeof raw !== "object") return null;
|
|
3204
|
+
const obj = raw;
|
|
3205
|
+
const fn = obj.function && typeof obj.function === "object" ? obj.function : null;
|
|
3206
|
+
const name = typeof obj.name === "string" ? obj.name : fn && typeof fn.name === "string" ? fn.name : null;
|
|
3207
|
+
if (!name) return null;
|
|
3208
|
+
const rawInput = obj.input ?? obj.arguments ?? (fn ? fn.input ?? fn.arguments : void 0);
|
|
3209
|
+
let input = {};
|
|
3210
|
+
if (rawInput && typeof rawInput === "object") {
|
|
3211
|
+
input = rawInput;
|
|
3212
|
+
} else if (typeof rawInput === "string") {
|
|
3213
|
+
const parsed = parseJsonLoose(rawInput);
|
|
3214
|
+
if (parsed && typeof parsed === "object") input = parsed;
|
|
3215
|
+
}
|
|
3216
|
+
return { name, input };
|
|
2651
3217
|
}
|
|
2652
3218
|
function toToolCall(parsed, index) {
|
|
2653
3219
|
return {
|
|
@@ -2658,32 +3224,59 @@ function toToolCall(parsed, index) {
|
|
|
2658
3224
|
}
|
|
2659
3225
|
function buildTextToolSystemPrompt(tools) {
|
|
2660
3226
|
const toolDefs = tools.map((t) => {
|
|
2661
|
-
const
|
|
2662
|
-
const
|
|
2663
|
-
|
|
2664
|
-
|
|
3227
|
+
const schema = t.inputSchema ?? {};
|
|
3228
|
+
const props = schema.properties && typeof schema.properties === "object" ? schema.properties : {};
|
|
3229
|
+
const required = Array.isArray(schema.required) ? schema.required : [];
|
|
3230
|
+
const paramLines = Object.entries(props).map(([k, v]) => {
|
|
3231
|
+
const type = typeof v.type === "string" ? v.type : "any";
|
|
3232
|
+
const desc = typeof v.description === "string" ? v.description : k;
|
|
3233
|
+
const req = required.includes(k) ? " [required]" : "";
|
|
3234
|
+
const enumVals = Array.isArray(v.enum) ? ` (one of: ${v.enum.map((e) => JSON.stringify(e)).join(", ")})` : "";
|
|
3235
|
+
return ` - ${k} (${type})${req}: ${desc}${enumVals}`;
|
|
3236
|
+
});
|
|
3237
|
+
return `\u2022 ${t.name} \u2014 ${t.description}${paramLines.length ? "\n" + paramLines.join("\n") : "\n (no parameters)"}`;
|
|
2665
3238
|
}).join("\n");
|
|
2666
3239
|
return `
|
|
2667
3240
|
TOOL USE INSTRUCTIONS:
|
|
2668
|
-
You do not have native tool-use capability. To call a tool,
|
|
3241
|
+
You do not have native tool-use capability. To call a tool, output a single <tool_call> block containing JSON with the tool name and its input arguments:
|
|
2669
3242
|
|
|
2670
3243
|
<tool_call>
|
|
2671
|
-
{"name": "<tool_name>", "input": {
|
|
3244
|
+
{"name": "<tool_name>", "input": { ...arguments... }}
|
|
2672
3245
|
</tool_call>
|
|
2673
3246
|
|
|
3247
|
+
Rules:
|
|
3248
|
+
- Use exactly the parameter names shown below and include every [required] parameter.
|
|
3249
|
+
- For parameters that list "one of", use one of those values verbatim.
|
|
3250
|
+
- Emit valid JSON with double quotes. Call only ONE tool at a time, then wait for the result.
|
|
3251
|
+
|
|
2674
3252
|
Available tools:
|
|
2675
3253
|
${toolDefs}
|
|
2676
3254
|
|
|
2677
3255
|
EXAMPLE \u2014 calling the "shell" tool to list files:
|
|
2678
3256
|
<tool_call>
|
|
2679
|
-
{"name": "shell", "input": {"command": "ls -la
|
|
3257
|
+
{"name": "shell", "input": {"command": "ls -la"}}
|
|
2680
3258
|
</tool_call>
|
|
2681
3259
|
|
|
2682
|
-
|
|
2683
|
-
Only call one tool at a time. When you have enough information, provide your final answer.`;
|
|
3260
|
+
When you have enough information, stop calling tools and write your final answer.`;
|
|
2684
3261
|
}
|
|
2685
3262
|
|
|
2686
3263
|
// src/core/tiers/t3-worker.ts
|
|
3264
|
+
var CriticalToolError = class extends Error {
|
|
3265
|
+
constructor(message, toolName) {
|
|
3266
|
+
super(message);
|
|
3267
|
+
this.toolName = toolName;
|
|
3268
|
+
this.name = "CriticalToolError";
|
|
3269
|
+
}
|
|
3270
|
+
toolName;
|
|
3271
|
+
};
|
|
3272
|
+
var WorkerStallError = class extends Error {
|
|
3273
|
+
constructor(message, partialOutput) {
|
|
3274
|
+
super(message);
|
|
3275
|
+
this.partialOutput = partialOutput;
|
|
3276
|
+
this.name = "WorkerStallError";
|
|
3277
|
+
}
|
|
3278
|
+
partialOutput;
|
|
3279
|
+
};
|
|
2687
3280
|
var T3_SYSTEM_PROMPT = `You are a T3 Worker agent in the Cascade AI system. Your job is to execute a specific subtask completely and accurately.
|
|
2688
3281
|
|
|
2689
3282
|
Rules:
|
|
@@ -2705,6 +3298,10 @@ var T3Worker = class extends BaseTier {
|
|
|
2705
3298
|
store;
|
|
2706
3299
|
audit;
|
|
2707
3300
|
tools = [];
|
|
3301
|
+
/** 0 = top-level worker (may request reinforcements); 1 = a spawned reinforcement (may not). */
|
|
3302
|
+
reinforcementDepth = 0;
|
|
3303
|
+
/** Sibling-worker requests this worker made via request_workers (T3→T2). */
|
|
3304
|
+
pendingReinforcements = [];
|
|
2708
3305
|
/** @deprecated — kept only as fallback when no escalator is attached */
|
|
2709
3306
|
sessionApprovals = /* @__PURE__ */ new Map();
|
|
2710
3307
|
peerBus;
|
|
@@ -2717,10 +3314,22 @@ var T3Worker = class extends BaseTier {
|
|
|
2717
3314
|
this.log(`Peer message from ${msg.fromId}: ${msg.type}`);
|
|
2718
3315
|
this.receivePeerSync(msg.fromId, msg.payload);
|
|
2719
3316
|
});
|
|
3317
|
+
this.peerBus.on("broadcast", (msg) => {
|
|
3318
|
+
const payload = msg?.payload;
|
|
3319
|
+
if (payload?.type === "TOOL_CREATED" && payload.spec && this.toolCreator) {
|
|
3320
|
+
this.toolCreator.registerSpec(payload.spec);
|
|
3321
|
+
this.tools = this.toolRegistry.getToolDefinitions();
|
|
3322
|
+
this.log(`Registered peer tool "${payload.spec.name}" from broadcast.`);
|
|
3323
|
+
}
|
|
3324
|
+
});
|
|
2720
3325
|
}
|
|
2721
3326
|
setPermissionEscalator(escalator) {
|
|
2722
3327
|
this.permissionEscalator = escalator;
|
|
2723
3328
|
}
|
|
3329
|
+
/** Marks this worker as a spawned reinforcement (depth 1 — cannot request more). */
|
|
3330
|
+
markAsReinforcement() {
|
|
3331
|
+
this.reinforcementDepth = 1;
|
|
3332
|
+
}
|
|
2724
3333
|
setToolCreator(creator) {
|
|
2725
3334
|
this.toolCreator = creator;
|
|
2726
3335
|
}
|
|
@@ -2741,6 +3350,31 @@ var T3Worker = class extends BaseTier {
|
|
|
2741
3350
|
this.setLabel(assignment.subtaskTitle);
|
|
2742
3351
|
this.setStatus("ACTIVE");
|
|
2743
3352
|
this.tools = this.toolRegistry.getToolDefinitions();
|
|
3353
|
+
if (this.reinforcementDepth === 0 && this.router.getReinforcementsConfig?.()?.enabled) {
|
|
3354
|
+
this.tools = [...this.tools, {
|
|
3355
|
+
name: "request_workers",
|
|
3356
|
+
description: "Ask your manager to spawn additional sibling workers for sub-problems you discover are too large or parallelizable to finish alone. Use sparingly \u2014 only when the work genuinely needs to fan out.",
|
|
3357
|
+
inputSchema: {
|
|
3358
|
+
type: "object",
|
|
3359
|
+
properties: {
|
|
3360
|
+
subtasks: {
|
|
3361
|
+
type: "array",
|
|
3362
|
+
description: "New sibling subtasks for your manager to spawn.",
|
|
3363
|
+
items: {
|
|
3364
|
+
type: "object",
|
|
3365
|
+
properties: {
|
|
3366
|
+
title: { type: "string" },
|
|
3367
|
+
description: { type: "string" },
|
|
3368
|
+
expectedOutput: { type: "string" }
|
|
3369
|
+
},
|
|
3370
|
+
required: ["title", "description"]
|
|
3371
|
+
}
|
|
3372
|
+
}
|
|
3373
|
+
},
|
|
3374
|
+
required: ["subtasks"]
|
|
3375
|
+
}
|
|
3376
|
+
}];
|
|
3377
|
+
}
|
|
2744
3378
|
if (assignment.dependsOn?.length && this.peerBus) {
|
|
2745
3379
|
this.sendStatusUpdate({
|
|
2746
3380
|
progressPct: 0,
|
|
@@ -2841,12 +3475,31 @@ Now execute your subtask using this context where relevant.`
|
|
|
2841
3475
|
return this.buildResult("ESCALATED", output, { checksRun, passed, failed }, issues, correctionAttempts);
|
|
2842
3476
|
}
|
|
2843
3477
|
}
|
|
3478
|
+
const reflectCfg = this.router.getReflectionConfig?.() ?? { enabled: false, maxRounds: 1 };
|
|
3479
|
+
if (reflectCfg.enabled) {
|
|
3480
|
+
this.sendStatusUpdate({ progressPct: 85, currentAction: "Reflecting on output", status: "IN_PROGRESS" });
|
|
3481
|
+
output = await this.reflectAndImprove(assignment, output, reflectCfg.maxRounds);
|
|
3482
|
+
}
|
|
2844
3483
|
this.setStatus("COMPLETED", output);
|
|
2845
3484
|
this.sendStatusUpdate({ progressPct: 100, currentAction: "Subtask complete", status: "IN_PROGRESS", output });
|
|
2846
3485
|
this.peerBus?.publish(this.id, assignment.subtaskId, output, "COMPLETED");
|
|
2847
3486
|
return this.buildResult("COMPLETED", output, { checksRun, passed, failed }, issues, correctionAttempts);
|
|
2848
3487
|
} catch (err) {
|
|
2849
3488
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
3489
|
+
if (err instanceof WorkerStallError) {
|
|
3490
|
+
issues.push(`Stalled: ${errMsg}`);
|
|
3491
|
+
const finalOutput2 = err.partialOutput || output || errMsg;
|
|
3492
|
+
this.setStatus("FAILED", finalOutput2);
|
|
3493
|
+
this.peerBus?.publish(this.id, assignment.subtaskId, finalOutput2, "FAILED");
|
|
3494
|
+
return this.buildResult("ESCALATED", finalOutput2, { checksRun, passed, failed }, issues, correctionAttempts);
|
|
3495
|
+
}
|
|
3496
|
+
if (err instanceof CriticalToolError) {
|
|
3497
|
+
issues.push(`[CRITICAL_TOOL_ERROR] ${err.toolName}: ${errMsg}`);
|
|
3498
|
+
const finalOutput2 = output || `Tool "${err.toolName}" failed unrecoverably: ${errMsg}`;
|
|
3499
|
+
this.setStatus("FAILED", finalOutput2);
|
|
3500
|
+
this.peerBus?.publish(this.id, assignment.subtaskId, finalOutput2, "FAILED");
|
|
3501
|
+
return this.buildResult("ESCALATED", finalOutput2, { checksRun, passed, failed }, issues, correctionAttempts);
|
|
3502
|
+
}
|
|
2850
3503
|
issues.push(`Execution error: ${errMsg}`);
|
|
2851
3504
|
const finalOutput = output || errMsg;
|
|
2852
3505
|
this.setStatus("FAILED", finalOutput);
|
|
@@ -2884,8 +3537,17 @@ Now execute your subtask using this context where relevant.`
|
|
|
2884
3537
|
const MAX_ITERATIONS = 15;
|
|
2885
3538
|
const requiresArtifact = this.requiresArtifact();
|
|
2886
3539
|
tools = [...tools];
|
|
2887
|
-
|
|
2888
|
-
|
|
3540
|
+
let subtaskModel;
|
|
3541
|
+
try {
|
|
3542
|
+
const subtaskText = `${this.assignment?.subtaskTitle ?? ""} ${this.assignment?.description ?? ""} ${this.assignment?.expectedOutput ?? ""}`;
|
|
3543
|
+
subtaskModel = await this.router.selectModelForSubtask("T3", subtaskText) ?? void 0;
|
|
3544
|
+
if (subtaskModel) {
|
|
3545
|
+
this.log(`Cascade Auto: routing this subtask to ${subtaskModel.provider}:${subtaskModel.id}`);
|
|
3546
|
+
}
|
|
3547
|
+
} catch {
|
|
3548
|
+
}
|
|
3549
|
+
const effectiveModel = subtaskModel ?? this.router.getModelForTier("T3");
|
|
3550
|
+
const useTextTools = effectiveModel?.supportsToolUse === false && tools.length > 0;
|
|
2889
3551
|
const textToolSuffix = useTextTools ? buildTextToolSystemPrompt(tools) : "";
|
|
2890
3552
|
while (iterations < MAX_ITERATIONS) {
|
|
2891
3553
|
iterations++;
|
|
@@ -2897,7 +3559,8 @@ Now execute your subtask using this context where relevant.`
|
|
|
2897
3559
|
HIERARCHY CONTEXT: ${this.hierarchyContext}` : "") + textToolSuffix,
|
|
2898
3560
|
// Don't pass tools array when model can't use them natively
|
|
2899
3561
|
tools: useTextTools ? void 0 : tools.length ? tools : void 0,
|
|
2900
|
-
maxTokens: 4096
|
|
3562
|
+
maxTokens: 4096,
|
|
3563
|
+
...subtaskModel ? { model: subtaskModel } : {}
|
|
2901
3564
|
};
|
|
2902
3565
|
const result = await this.router.generate(
|
|
2903
3566
|
"T3",
|
|
@@ -2921,10 +3584,17 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : "") + textToolSuffix,
|
|
|
2921
3584
|
}
|
|
2922
3585
|
stalledArtifactIterations += 1;
|
|
2923
3586
|
if (stalledArtifactIterations >= 2) {
|
|
3587
|
+
const partial = result.content || "";
|
|
2924
3588
|
if (stalledArtifactIterations === 2) {
|
|
2925
|
-
throw new
|
|
3589
|
+
throw new WorkerStallError(
|
|
3590
|
+
`Worker stalled waiting for artifact creation. Requesting dynamic tool generation from T2 Manager for: ${this.assignment?.subtaskTitle ?? "unknown task"}`,
|
|
3591
|
+
partial
|
|
3592
|
+
);
|
|
2926
3593
|
}
|
|
2927
|
-
throw new
|
|
3594
|
+
throw new WorkerStallError(
|
|
3595
|
+
"Artifact-producing task stalled without creating or verifying the required files",
|
|
3596
|
+
partial
|
|
3597
|
+
);
|
|
2928
3598
|
}
|
|
2929
3599
|
await this.context.addMessage({
|
|
2930
3600
|
role: "user",
|
|
@@ -2961,7 +3631,41 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : "") + textToolSuffix,
|
|
|
2961
3631
|
toolCalls: allToolCalls
|
|
2962
3632
|
};
|
|
2963
3633
|
}
|
|
3634
|
+
/**
|
|
3635
|
+
* Lightweight argument check against the tool's JSON Schema: required fields
|
|
3636
|
+
* present and enum values in range. Not a full validator — just the two
|
|
3637
|
+
* failure modes weak models hit most. Returns an error message, or null if OK.
|
|
3638
|
+
*/
|
|
3639
|
+
validateToolInput(tc) {
|
|
3640
|
+
const def = this.tools.find((t) => t.name === tc.name);
|
|
3641
|
+
const schema = def?.inputSchema;
|
|
3642
|
+
if (!schema) return null;
|
|
3643
|
+
const required = Array.isArray(schema.required) ? schema.required : [];
|
|
3644
|
+
const missing = required.filter((k) => tc.input[k] === void 0 || tc.input[k] === null || tc.input[k] === "");
|
|
3645
|
+
if (missing.length) {
|
|
3646
|
+
return `Tool error: missing required parameter(s) for "${tc.name}": ${missing.join(", ")}. Expected: ${JSON.stringify(schema)}. Supply them and call the tool again.`;
|
|
3647
|
+
}
|
|
3648
|
+
if (schema.properties) {
|
|
3649
|
+
for (const [k, prop] of Object.entries(schema.properties)) {
|
|
3650
|
+
const allowed = Array.isArray(prop?.enum) ? prop.enum : null;
|
|
3651
|
+
if (allowed && tc.input[k] !== void 0 && !allowed.includes(tc.input[k])) {
|
|
3652
|
+
return `Tool error: invalid value for "${k}" in "${tc.name}": ${JSON.stringify(tc.input[k])}. Must be one of ${JSON.stringify(allowed)}.`;
|
|
3653
|
+
}
|
|
3654
|
+
}
|
|
3655
|
+
}
|
|
3656
|
+
return null;
|
|
3657
|
+
}
|
|
2964
3658
|
async executeTool(tc) {
|
|
3659
|
+
if (tc.name === "request_workers") {
|
|
3660
|
+
const msg = this.recordReinforcements(tc.input);
|
|
3661
|
+
this.emit("tool:result", { id: tc.id, tierId: this.id, toolName: tc.name, output: msg, durationMs: 0 });
|
|
3662
|
+
return msg;
|
|
3663
|
+
}
|
|
3664
|
+
const validationError = this.validateToolInput(tc);
|
|
3665
|
+
if (validationError) {
|
|
3666
|
+
this.emit("tool:result", { id: tc.id, tierId: this.id, toolName: tc.name, error: validationError, durationMs: 0 });
|
|
3667
|
+
return validationError;
|
|
3668
|
+
}
|
|
2965
3669
|
const needsApproval = this.toolRegistry.requiresApproval(tc.name);
|
|
2966
3670
|
if (needsApproval) {
|
|
2967
3671
|
if (this.permissionEscalator) {
|
|
@@ -2982,7 +3686,14 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : "") + textToolSuffix,
|
|
|
2982
3686
|
const wasApproved = this.sessionApprovals.get(tc.name);
|
|
2983
3687
|
if (!wasApproved) return `Tool ${tc.name} was denied by user.`;
|
|
2984
3688
|
} else {
|
|
3689
|
+
const LEGACY_APPROVAL_TIMEOUT_MS = 6e5;
|
|
2985
3690
|
const legacyDecision = await new Promise((resolve) => {
|
|
3691
|
+
const eventName = `tool:approval-response:${this.id}-${tc.id}`;
|
|
3692
|
+
const timer = setTimeout(() => {
|
|
3693
|
+
this.removeAllListeners(eventName);
|
|
3694
|
+
resolve({ approved: false });
|
|
3695
|
+
}, LEGACY_APPROVAL_TIMEOUT_MS);
|
|
3696
|
+
timer.unref?.();
|
|
2986
3697
|
this.emit("tool:approval-request", {
|
|
2987
3698
|
id: `${this.id}-${tc.id}`,
|
|
2988
3699
|
tierId: this.id,
|
|
@@ -2991,7 +3702,10 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : "") + textToolSuffix,
|
|
|
2991
3702
|
description: `T3 (${this.assignment?.subtaskTitle}) wants to run "${tc.name}"`,
|
|
2992
3703
|
isDangerous: this.toolRegistry.isDangerous(tc.name)
|
|
2993
3704
|
});
|
|
2994
|
-
this.once(
|
|
3705
|
+
this.once(eventName, (d) => {
|
|
3706
|
+
clearTimeout(timer);
|
|
3707
|
+
resolve(d);
|
|
3708
|
+
});
|
|
2995
3709
|
});
|
|
2996
3710
|
if (legacyDecision.always) this.sessionApprovals.set(tc.name, legacyDecision.approved);
|
|
2997
3711
|
if (!legacyDecision.approved) return `Tool ${tc.name} was denied by user.`;
|
|
@@ -3010,8 +3724,8 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : "") + textToolSuffix,
|
|
|
3010
3724
|
tierId: this.id,
|
|
3011
3725
|
sessionId: this.taskId,
|
|
3012
3726
|
requireApproval: false,
|
|
3013
|
-
saveSnapshot: async (
|
|
3014
|
-
this.store?.addFileSnapshot(this.taskId,
|
|
3727
|
+
saveSnapshot: async (path19, content) => {
|
|
3728
|
+
this.store?.addFileSnapshot(this.taskId, path19, content);
|
|
3015
3729
|
},
|
|
3016
3730
|
sendPeerSync: (to, syncType, content) => {
|
|
3017
3731
|
this.peerBus?.send(this.id, to, syncType, this.assignment?.subtaskId ?? "", content);
|
|
@@ -3035,7 +3749,10 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : "") + textToolSuffix,
|
|
|
3035
3749
|
const durationMs = Date.now() - toolStartMs;
|
|
3036
3750
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
3037
3751
|
this.emit("tool:result", { id: tc.id, tierId: this.id, toolName: tc.name, error: errMsg, durationMs });
|
|
3038
|
-
|
|
3752
|
+
if (/\b(429|rate.?limit|authentication|api.?key|forbidden|401|403)\b/i.test(errMsg)) {
|
|
3753
|
+
throw new CriticalToolError(errMsg, tc.name);
|
|
3754
|
+
}
|
|
3755
|
+
return await this.adaptiveFallback(tc, `Tool error: ${errMsg}`);
|
|
3039
3756
|
}
|
|
3040
3757
|
}
|
|
3041
3758
|
/**
|
|
@@ -3113,6 +3830,11 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : "") + textToolSuffix,
|
|
|
3113
3830
|
*/
|
|
3114
3831
|
async coordinateFileIntents(assignment) {
|
|
3115
3832
|
if (!this.peerBus) return;
|
|
3833
|
+
const haystack = `${assignment.description}
|
|
3834
|
+
${assignment.expectedOutput}`;
|
|
3835
|
+
if (!/\b(create|write|save|generate|produce|output|edit|update|modify|append|overwrite|rewrite)\b/i.test(haystack)) {
|
|
3836
|
+
return;
|
|
3837
|
+
}
|
|
3116
3838
|
const plannedFiles = this.extractArtifactPaths(assignment);
|
|
3117
3839
|
if (!plannedFiles.length) return;
|
|
3118
3840
|
this.peerBus.broadcast(this.id, {
|
|
@@ -3123,16 +3845,22 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : "") + textToolSuffix,
|
|
|
3123
3845
|
await new Promise((r) => setTimeout(r, 500));
|
|
3124
3846
|
const sortedFiles = [...plannedFiles].sort();
|
|
3125
3847
|
for (const filePath of sortedFiles) {
|
|
3126
|
-
|
|
3127
|
-
this.
|
|
3128
|
-
|
|
3129
|
-
|
|
3130
|
-
|
|
3131
|
-
|
|
3848
|
+
try {
|
|
3849
|
+
if (this.peerBus.isFileLocked(filePath)) {
|
|
3850
|
+
this.log(`[T3] Waiting for file lock: ${filePath}`);
|
|
3851
|
+
this.sendStatusUpdate({
|
|
3852
|
+
progressPct: 5,
|
|
3853
|
+
currentAction: `Waiting for peer to finish editing: ${filePath}`,
|
|
3854
|
+
status: "IN_PROGRESS"
|
|
3855
|
+
});
|
|
3856
|
+
await this.peerBus.waitForFileRelease(filePath, 1e4).catch(() => {
|
|
3857
|
+
});
|
|
3858
|
+
}
|
|
3859
|
+
await this.peerBus.lockFile(this.id, filePath, 1e4).catch(() => {
|
|
3132
3860
|
});
|
|
3133
|
-
|
|
3861
|
+
} catch (err) {
|
|
3862
|
+
this.log(`[T3] Lock coordination skipped for ${filePath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
3134
3863
|
}
|
|
3135
|
-
await this.peerBus.lockFile(this.id, filePath);
|
|
3136
3864
|
}
|
|
3137
3865
|
const origPublish = this.peerBus.publish.bind(this.peerBus);
|
|
3138
3866
|
const bus = this.peerBus;
|
|
@@ -3163,13 +3891,13 @@ ${assignment.expectedOutput}`;
|
|
|
3163
3891
|
const artifactPaths = this.extractArtifactPaths(assignment);
|
|
3164
3892
|
if (!artifactPaths.length) return { ok: true, issues: [] };
|
|
3165
3893
|
const issues = [];
|
|
3166
|
-
const { exec:
|
|
3894
|
+
const { exec: exec2 } = await import('child_process');
|
|
3167
3895
|
const { promisify: promisify4 } = await import('util');
|
|
3168
|
-
const execAsync2 = promisify4(
|
|
3896
|
+
const execAsync2 = promisify4(exec2);
|
|
3169
3897
|
for (const artifactPath of artifactPaths) {
|
|
3170
|
-
const absolutePath =
|
|
3898
|
+
const absolutePath = path18__default.default.resolve(process.cwd(), artifactPath);
|
|
3171
3899
|
try {
|
|
3172
|
-
const stat = await
|
|
3900
|
+
const stat = await fs4__default.default.stat(absolutePath);
|
|
3173
3901
|
if (!stat.isFile()) {
|
|
3174
3902
|
issues.push(`Expected artifact is not a file: ${artifactPath}`);
|
|
3175
3903
|
continue;
|
|
@@ -3179,7 +3907,7 @@ ${assignment.expectedOutput}`;
|
|
|
3179
3907
|
continue;
|
|
3180
3908
|
}
|
|
3181
3909
|
if (!/\.pdf$/i.test(artifactPath)) {
|
|
3182
|
-
const content = await
|
|
3910
|
+
const content = await fs4__default.default.readFile(absolutePath, "utf-8");
|
|
3183
3911
|
if (!content.trim()) {
|
|
3184
3912
|
issues.push(`Artifact content is empty: ${artifactPath}`);
|
|
3185
3913
|
continue;
|
|
@@ -3188,7 +3916,7 @@ ${assignment.expectedOutput}`;
|
|
|
3188
3916
|
issues.push(`PDF artifact looks too small to be valid: ${artifactPath}`);
|
|
3189
3917
|
continue;
|
|
3190
3918
|
}
|
|
3191
|
-
const ext =
|
|
3919
|
+
const ext = path18__default.default.extname(absolutePath).toLowerCase();
|
|
3192
3920
|
try {
|
|
3193
3921
|
if (ext === ".ts" || ext === ".tsx") {
|
|
3194
3922
|
await execAsync2(`npx tsc --noEmit ${absolutePath}`, { timeout: 1e4 });
|
|
@@ -3210,6 +3938,61 @@ ${stdout}`);
|
|
|
3210
3938
|
}
|
|
3211
3939
|
return { ok: issues.length === 0, issues };
|
|
3212
3940
|
}
|
|
3941
|
+
/**
|
|
3942
|
+
* Reflection / self-critique: critique the output against the broader GOAL
|
|
3943
|
+
* (not just the subtask spec the self-test checks) and revise once if it falls
|
|
3944
|
+
* short. Two cheap calls per round — a JSON verdict, then a rewrite only if
|
|
3945
|
+
* needed. Best-effort: any parse/error just keeps the current output.
|
|
3946
|
+
*/
|
|
3947
|
+
async reflectAndImprove(assignment, output, maxRounds) {
|
|
3948
|
+
const sys = this.systemPromptOverride + (this.hierarchyContext ? `
|
|
3949
|
+
|
|
3950
|
+
HIERARCHY CONTEXT: ${this.hierarchyContext}` : "");
|
|
3951
|
+
let current = output;
|
|
3952
|
+
for (let round = 0; round < Math.max(1, maxRounds); round++) {
|
|
3953
|
+
try {
|
|
3954
|
+
const verdict = await this.router.generate("T3", {
|
|
3955
|
+
messages: [{
|
|
3956
|
+
role: "user",
|
|
3957
|
+
content: `Does this output FULLY achieve the goal \u2014 not just the literal task, but the intent behind it?
|
|
3958
|
+
|
|
3959
|
+
Goal / expected: ${assignment.expectedOutput}
|
|
3960
|
+
Subtask: ${assignment.description}
|
|
3961
|
+
|
|
3962
|
+
Output:
|
|
3963
|
+
${current}
|
|
3964
|
+
|
|
3965
|
+
Reply with ONLY JSON: {"sufficient": true|false, "notes": "what is weak or missing if not sufficient"}`
|
|
3966
|
+
}],
|
|
3967
|
+
systemPrompt: sys,
|
|
3968
|
+
maxTokens: 400
|
|
3969
|
+
});
|
|
3970
|
+
const parsed = JSON.parse(/\{[\s\S]*\}/.exec(verdict.content)?.[0] ?? "{}");
|
|
3971
|
+
if (parsed.sufficient !== false) break;
|
|
3972
|
+
const improved = await this.router.generate("T3", {
|
|
3973
|
+
messages: [{
|
|
3974
|
+
role: "user",
|
|
3975
|
+
content: `Improve the following so it fully achieves the goal. Address specifically: ${parsed.notes ?? "gaps vs the goal"}.
|
|
3976
|
+
Output ONLY the improved result \u2014 no preamble, no commentary.
|
|
3977
|
+
|
|
3978
|
+
Goal / expected: ${assignment.expectedOutput}
|
|
3979
|
+
|
|
3980
|
+
Current output:
|
|
3981
|
+
${current}`
|
|
3982
|
+
}],
|
|
3983
|
+
systemPrompt: sys,
|
|
3984
|
+
maxTokens: 4096
|
|
3985
|
+
});
|
|
3986
|
+
const next = (improved.content ?? "").trim();
|
|
3987
|
+
if (!next) break;
|
|
3988
|
+
current = next;
|
|
3989
|
+
this.log("Reflection: revised output for better goal alignment.");
|
|
3990
|
+
} catch {
|
|
3991
|
+
break;
|
|
3992
|
+
}
|
|
3993
|
+
}
|
|
3994
|
+
return current;
|
|
3995
|
+
}
|
|
3213
3996
|
async selfTest(assignment, output) {
|
|
3214
3997
|
const prompt = `Self-test this output against the assignment requirements.
|
|
3215
3998
|
|
|
@@ -3284,6 +4067,35 @@ ${assignment.constraints.map((c) => `- ${c}`).join("\n")}
|
|
|
3284
4067
|
|
|
3285
4068
|
Begin execution now.`;
|
|
3286
4069
|
}
|
|
4070
|
+
/**
|
|
4071
|
+
* Records a request_workers call (T3→T2 reinforcement). Capped at
|
|
4072
|
+
* maxPerSection; reinforcement workers (depth 1) cannot request more.
|
|
4073
|
+
*/
|
|
4074
|
+
recordReinforcements(input) {
|
|
4075
|
+
if (this.reinforcementDepth !== 0) {
|
|
4076
|
+
return "request_workers is unavailable to reinforcement workers \u2014 complete your assigned subtask.";
|
|
4077
|
+
}
|
|
4078
|
+
const max = this.router.getReinforcementsConfig?.()?.maxPerSection ?? 4;
|
|
4079
|
+
const raw = Array.isArray(input.subtasks) ? input.subtasks : [];
|
|
4080
|
+
let added = 0;
|
|
4081
|
+
for (const s of raw) {
|
|
4082
|
+
if (this.pendingReinforcements.length >= max) break;
|
|
4083
|
+
const o = s;
|
|
4084
|
+
if (typeof o?.title !== "string" || typeof o?.description !== "string") continue;
|
|
4085
|
+
this.pendingReinforcements.push({
|
|
4086
|
+
subtaskId: `reinf-${this.id}-${this.pendingReinforcements.length + 1}`,
|
|
4087
|
+
subtaskTitle: o.title,
|
|
4088
|
+
description: o.description,
|
|
4089
|
+
expectedOutput: typeof o.expectedOutput === "string" ? o.expectedOutput : o.title,
|
|
4090
|
+
constraints: [],
|
|
4091
|
+
peerT3Ids: [],
|
|
4092
|
+
parentT2: this.parentId ?? "root",
|
|
4093
|
+
dependsOn: []
|
|
4094
|
+
});
|
|
4095
|
+
added++;
|
|
4096
|
+
}
|
|
4097
|
+
return added > 0 ? `Requested ${added} reinforcement worker(s) from your manager; they will run in parallel. Focus on your own part \u2014 do not redo their work.` : "No valid reinforcement subtasks (each needs a title and description), or the per-section limit was reached.";
|
|
4098
|
+
}
|
|
3287
4099
|
buildResult(status, output, testResults, issues, correctionAttempts) {
|
|
3288
4100
|
return {
|
|
3289
4101
|
subtaskId: this.assignment?.subtaskId ?? "",
|
|
@@ -3292,7 +4104,8 @@ Begin execution now.`;
|
|
|
3292
4104
|
testResults,
|
|
3293
4105
|
issues,
|
|
3294
4106
|
peerSyncsUsed: this.peerSyncBuffer.map((m) => m.fromId),
|
|
3295
|
-
correctionAttempts
|
|
4107
|
+
correctionAttempts,
|
|
4108
|
+
reinforcements: this.pendingReinforcements.length ? this.pendingReinforcements : void 0
|
|
3296
4109
|
};
|
|
3297
4110
|
}
|
|
3298
4111
|
isFileOperation(toolName) {
|
|
@@ -3311,6 +4124,17 @@ var PeerBus = class extends EventEmitter__default.default {
|
|
|
3311
4124
|
/** Called when any peer message or broadcast is sent — used for dashboard visibility. */
|
|
3312
4125
|
onPeerMessage;
|
|
3313
4126
|
sessionId = "";
|
|
4127
|
+
/** Surface coordination traffic (locks, barriers) to the visibility hook. */
|
|
4128
|
+
emitCoordination(fromId, text) {
|
|
4129
|
+
this.onPeerMessage?.({
|
|
4130
|
+
fromId,
|
|
4131
|
+
toId: void 0,
|
|
4132
|
+
syncType: "COORDINATION",
|
|
4133
|
+
payload: text,
|
|
4134
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4135
|
+
sessionId: this.sessionId
|
|
4136
|
+
});
|
|
4137
|
+
}
|
|
3314
4138
|
register(peerId) {
|
|
3315
4139
|
this.members.add(peerId);
|
|
3316
4140
|
}
|
|
@@ -3447,8 +4271,10 @@ var PeerBus = class extends EventEmitter__default.default {
|
|
|
3447
4271
|
const existing = this.fileLocks.get(filePath);
|
|
3448
4272
|
if (!existing) {
|
|
3449
4273
|
this.fileLocks.set(filePath, { holderId: tierId, lockedAt: (/* @__PURE__ */ new Date()).toISOString(), waiters: [] });
|
|
4274
|
+
this.emitCoordination(tierId, `\u{1F512} locked ${filePath}`);
|
|
3450
4275
|
return;
|
|
3451
4276
|
}
|
|
4277
|
+
this.emitCoordination(tierId, `\u23F3 waiting for ${filePath} (held by ${existing.holderId})`);
|
|
3452
4278
|
return new Promise((resolve, reject) => {
|
|
3453
4279
|
const timer = setTimeout(() => {
|
|
3454
4280
|
reject(new Error(`File lock timeout for ${filePath} (held by ${existing.holderId})`));
|
|
@@ -3456,6 +4282,7 @@ var PeerBus = class extends EventEmitter__default.default {
|
|
|
3456
4282
|
existing.waiters.push(() => {
|
|
3457
4283
|
clearTimeout(timer);
|
|
3458
4284
|
this.fileLocks.set(filePath, { holderId: tierId, lockedAt: (/* @__PURE__ */ new Date()).toISOString(), waiters: [] });
|
|
4285
|
+
this.emitCoordination(tierId, `\u{1F512} locked ${filePath}`);
|
|
3459
4286
|
resolve();
|
|
3460
4287
|
});
|
|
3461
4288
|
});
|
|
@@ -3466,6 +4293,7 @@ var PeerBus = class extends EventEmitter__default.default {
|
|
|
3466
4293
|
releaseFile(tierId, filePath) {
|
|
3467
4294
|
const lock = this.fileLocks.get(filePath);
|
|
3468
4295
|
if (!lock || lock.holderId !== tierId) return;
|
|
4296
|
+
this.emitCoordination(tierId, `\u{1F513} released ${filePath}`);
|
|
3469
4297
|
const nextWaiter = lock.waiters.shift();
|
|
3470
4298
|
if (nextWaiter) {
|
|
3471
4299
|
nextWaiter();
|
|
@@ -3545,6 +4373,7 @@ var PeerBus = class extends EventEmitter__default.default {
|
|
|
3545
4373
|
}
|
|
3546
4374
|
const bar = this.barriers.get(barrierName);
|
|
3547
4375
|
bar.arrived.add(peerId);
|
|
4376
|
+
this.emitCoordination(peerId, `\u22A8 barrier "${barrierName}" (${bar.arrived.size}/${bar.total})`);
|
|
3548
4377
|
if (bar.arrived.size >= bar.total) {
|
|
3549
4378
|
this.emit(`barrier:${barrierName}`);
|
|
3550
4379
|
return;
|
|
@@ -3577,6 +4406,7 @@ var T2Manager = class extends BaseTier {
|
|
|
3577
4406
|
router;
|
|
3578
4407
|
toolRegistry;
|
|
3579
4408
|
assignment;
|
|
4409
|
+
sectionModel;
|
|
3580
4410
|
t3Workers = /* @__PURE__ */ new Map();
|
|
3581
4411
|
escalations = [];
|
|
3582
4412
|
peerSyncBuffer = [];
|
|
@@ -3586,6 +4416,8 @@ var T2Manager = class extends BaseTier {
|
|
|
3586
4416
|
t2PeerBus;
|
|
3587
4417
|
permissionEscalator;
|
|
3588
4418
|
toolCreator;
|
|
4419
|
+
/** Optional boardroom gate (Moderate / root-T2 runs) — pauses after decomposition. */
|
|
4420
|
+
planApprovalCallback;
|
|
3589
4421
|
/** AbortController for the current T3 wave — aborted on cancel-and-respawn */
|
|
3590
4422
|
waveAbortController = null;
|
|
3591
4423
|
setPeerBus(bus) {
|
|
@@ -3623,6 +4455,10 @@ var T2Manager = class extends BaseTier {
|
|
|
3623
4455
|
setToolCreator(creator) {
|
|
3624
4456
|
this.toolCreator = creator;
|
|
3625
4457
|
}
|
|
4458
|
+
/** Boardroom gate for Moderate (root-T2) runs: pause after decomposition. */
|
|
4459
|
+
setPlanApprovalCallback(cb) {
|
|
4460
|
+
this.planApprovalCallback = cb;
|
|
4461
|
+
}
|
|
3626
4462
|
/**
|
|
3627
4463
|
* Phase 1 of T2 peer discussion: broadcast this section's plan so sibling T2s
|
|
3628
4464
|
* and T1 can detect overlaps and coordinate execution order.
|
|
@@ -3676,9 +4512,39 @@ var T2Manager = class extends BaseTier {
|
|
|
3676
4512
|
status: "IN_PROGRESS"
|
|
3677
4513
|
});
|
|
3678
4514
|
this.log(`T2 managing section: ${assignment.sectionTitle}`);
|
|
4515
|
+
this.sectionModel = void 0;
|
|
4516
|
+
try {
|
|
4517
|
+
const sectionText = `${assignment.sectionTitle} ${assignment.description} ${assignment.expectedOutput}`;
|
|
4518
|
+
this.sectionModel = await this.router.selectModelForSubtask("T2", sectionText) ?? void 0;
|
|
4519
|
+
if (this.sectionModel) {
|
|
4520
|
+
this.log(`Cascade Auto: routing this section to ${this.sectionModel.provider}:${this.sectionModel.id}`);
|
|
4521
|
+
}
|
|
4522
|
+
} catch {
|
|
4523
|
+
}
|
|
3679
4524
|
try {
|
|
3680
4525
|
this.throwIfCancelled();
|
|
3681
|
-
|
|
4526
|
+
let subtasks = assignment.t3Subtasks.length > 0 ? assignment.t3Subtasks : await this.decomposeSection(assignment);
|
|
4527
|
+
if (this.planApprovalCallback) {
|
|
4528
|
+
const decision = await this.planApprovalCallback(subtasks, assignment.sectionTitle);
|
|
4529
|
+
if (!decision.approved) {
|
|
4530
|
+
const output = "Plan rejected \u2014 nothing was executed.";
|
|
4531
|
+
this.setStatus("COMPLETED", output);
|
|
4532
|
+
this.sendStatusUpdate({ progressPct: 100, currentAction: "Plan rejected by user", status: "IN_PROGRESS", output });
|
|
4533
|
+
return { sectionId: assignment.sectionId, sectionTitle: assignment.sectionTitle, status: "COMPLETED", t3Results: [], sectionSummary: output, issues: [] };
|
|
4534
|
+
}
|
|
4535
|
+
if (decision.keepSubtaskIds?.length) {
|
|
4536
|
+
const keep = new Set(decision.keepSubtaskIds);
|
|
4537
|
+
subtasks = subtasks.filter((s) => keep.has(s.subtaskId));
|
|
4538
|
+
}
|
|
4539
|
+
if (decision.note?.trim()) {
|
|
4540
|
+
subtasks = await this.decomposeSection({
|
|
4541
|
+
...assignment,
|
|
4542
|
+
description: `${assignment.description}
|
|
4543
|
+
|
|
4544
|
+
Guidance (must be followed): ${decision.note}`
|
|
4545
|
+
});
|
|
4546
|
+
}
|
|
4547
|
+
}
|
|
3682
4548
|
this.sendStatusUpdate({
|
|
3683
4549
|
progressPct: 20,
|
|
3684
4550
|
currentAction: `Dispatching ${subtasks.length} T3 workers`,
|
|
@@ -3752,7 +4618,8 @@ Return ONLY the JSON array.`;
|
|
|
3752
4618
|
systemPrompt: this.systemPromptOverride + T2_SYSTEM_PROMPT + (this.hierarchyContext ? `
|
|
3753
4619
|
|
|
3754
4620
|
HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
3755
|
-
maxTokens: 2e3
|
|
4621
|
+
maxTokens: 2e3,
|
|
4622
|
+
...this.sectionModel ? { model: this.sectionModel } : {}
|
|
3756
4623
|
});
|
|
3757
4624
|
try {
|
|
3758
4625
|
const jsonMatch = /\[[\s\S]*\]/.exec(result.content);
|
|
@@ -3856,6 +4723,8 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
|
3856
4723
|
let remaining = new Set(sanitizedAssignments.map((a) => a.subtaskId));
|
|
3857
4724
|
let wave = 0;
|
|
3858
4725
|
let respawnBudget = 1;
|
|
4726
|
+
const reinforceCfg = this.router.getReinforcementsConfig?.() ?? { enabled: false, maxPerSection: 4 };
|
|
4727
|
+
let reinforcementsAdded = 0;
|
|
3859
4728
|
while (remaining.size > 0) {
|
|
3860
4729
|
const runnableIds = [...remaining].filter((id) => (inDegree.get(id) ?? 0) === 0);
|
|
3861
4730
|
if (runnableIds.length === 0) {
|
|
@@ -3880,15 +4749,27 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
|
3880
4749
|
const waveSignal = AbortSignal.any(
|
|
3881
4750
|
[this.signal, this.waveAbortController.signal].filter(Boolean)
|
|
3882
4751
|
);
|
|
3883
|
-
const
|
|
3884
|
-
|
|
3885
|
-
|
|
3886
|
-
|
|
3887
|
-
|
|
3888
|
-
|
|
3889
|
-
|
|
3890
|
-
|
|
3891
|
-
)
|
|
4752
|
+
const runOne = async (id) => {
|
|
4753
|
+
const assignment = sanitizedAssignments.find((a) => a.subtaskId === id);
|
|
4754
|
+
const worker = workerMap.get(id);
|
|
4755
|
+
const result = await worker.execute(assignment, taskId, waveSignal);
|
|
4756
|
+
resultMap.set(id, result);
|
|
4757
|
+
return result;
|
|
4758
|
+
};
|
|
4759
|
+
let waveResults;
|
|
4760
|
+
if (this.router.getT3ExecutionMode?.() === "sequential") {
|
|
4761
|
+
this.log(`Wave ${wave}: running ${runnableIds.length} subtask(s) sequentially (local tier)`);
|
|
4762
|
+
waveResults = [];
|
|
4763
|
+
for (const id of runnableIds) {
|
|
4764
|
+
try {
|
|
4765
|
+
waveResults.push({ status: "fulfilled", value: await runOne(id) });
|
|
4766
|
+
} catch (reason) {
|
|
4767
|
+
waveResults.push({ status: "rejected", reason });
|
|
4768
|
+
}
|
|
4769
|
+
}
|
|
4770
|
+
} else {
|
|
4771
|
+
waveResults = await Promise.allSettled(runnableIds.map(runOne));
|
|
4772
|
+
}
|
|
3892
4773
|
const escalatedToolIdx = respawnBudget > 0 ? waveResults.findIndex(
|
|
3893
4774
|
(r) => r.status === "fulfilled" && r.value.status === "ESCALATED" && r.value.issues.some((iss) => iss.includes("dynamic tool generation"))
|
|
3894
4775
|
) : -1;
|
|
@@ -3916,6 +4797,8 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
|
3916
4797
|
[SYSTEM]: Dynamic tool "${toolName}" is now available \u2014 use it to complete your task.`;
|
|
3917
4798
|
}
|
|
3918
4799
|
}
|
|
4800
|
+
const spec = this.toolCreator.getSpec(toolName);
|
|
4801
|
+
if (spec) this.t3PeerBus.broadcast(this.id, { type: "TOOL_CREATED", spec });
|
|
3919
4802
|
}
|
|
3920
4803
|
for (const id of runnableIds) {
|
|
3921
4804
|
this.t3PeerBus.clearOutput(id);
|
|
@@ -3961,6 +4844,35 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
|
3961
4844
|
inDegree.set(dependent, Math.max(0, (inDegree.get(dependent) ?? 0) - 1));
|
|
3962
4845
|
}
|
|
3963
4846
|
}
|
|
4847
|
+
if (reinforceCfg.enabled && reinforcementsAdded < reinforceCfg.maxPerSection) {
|
|
4848
|
+
let addedThisWave = 0;
|
|
4849
|
+
for (const id of runnableIds) {
|
|
4850
|
+
for (const req of resultMap.get(id)?.reinforcements ?? []) {
|
|
4851
|
+
if (reinforcementsAdded >= reinforceCfg.maxPerSection) break;
|
|
4852
|
+
reinforcementsAdded++;
|
|
4853
|
+
addedThisWave++;
|
|
4854
|
+
const assignment = {
|
|
4855
|
+
...req,
|
|
4856
|
+
subtaskId: `reinf-${this.id}-${reinforcementsAdded}`,
|
|
4857
|
+
dependsOn: [],
|
|
4858
|
+
peerT3Ids: []
|
|
4859
|
+
};
|
|
4860
|
+
sanitizedAssignments.push(assignment);
|
|
4861
|
+
adj.set(assignment.subtaskId, /* @__PURE__ */ new Set());
|
|
4862
|
+
inDegree.set(assignment.subtaskId, 0);
|
|
4863
|
+
remaining.add(assignment.subtaskId);
|
|
4864
|
+
const fresh = this.buildWorkerMap([assignment], taskId);
|
|
4865
|
+
for (const [k, v] of fresh) {
|
|
4866
|
+
v.markAsReinforcement();
|
|
4867
|
+
workerMap.set(k, v);
|
|
4868
|
+
}
|
|
4869
|
+
this.log(`Reinforcement: spawned worker "${assignment.subtaskTitle}" (requested by ${id})`);
|
|
4870
|
+
}
|
|
4871
|
+
}
|
|
4872
|
+
if (addedThisWave > 0) {
|
|
4873
|
+
this.sendStatusUpdate({ progressPct: 55, currentAction: `Added ${addedThisWave} reinforcement worker(s)`, status: "IN_PROGRESS" });
|
|
4874
|
+
}
|
|
4875
|
+
}
|
|
3964
4876
|
}
|
|
3965
4877
|
return [...resultMap.values()];
|
|
3966
4878
|
}
|
|
@@ -4070,7 +4982,8 @@ NEW OUTPUTS TO INTEGRATE:
|
|
|
4070
4982
|
systemPrompt: this.systemPromptOverride + "You are a T2 Manager. Summarize the work of your T3 workers succinctly." + (this.hierarchyContext ? `
|
|
4071
4983
|
|
|
4072
4984
|
HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
4073
|
-
maxTokens: 500
|
|
4985
|
+
maxTokens: 500,
|
|
4986
|
+
...this.sectionModel ? { model: this.sectionModel } : {}
|
|
4074
4987
|
});
|
|
4075
4988
|
currentSummary = result.content;
|
|
4076
4989
|
} catch (err) {
|
|
@@ -4119,7 +5032,8 @@ Reply with exactly one word: YES, NO, or UNSURE.`;
|
|
|
4119
5032
|
|
|
4120
5033
|
HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
4121
5034
|
maxTokens: 10,
|
|
4122
|
-
temperature: 0
|
|
5035
|
+
temperature: 0,
|
|
5036
|
+
...this.sectionModel ? { model: this.sectionModel } : {}
|
|
4123
5037
|
});
|
|
4124
5038
|
const answer = result.content.trim().toUpperCase();
|
|
4125
5039
|
if (answer.includes("YES")) {
|
|
@@ -4227,6 +5141,7 @@ var T1Administrator = class extends BaseTier {
|
|
|
4227
5141
|
taskGoal = "";
|
|
4228
5142
|
peerMessageCallback;
|
|
4229
5143
|
peerMessageSessionId = "";
|
|
5144
|
+
planApprovalCallback;
|
|
4230
5145
|
constructor(router, toolRegistry, config) {
|
|
4231
5146
|
super("T1", "T1");
|
|
4232
5147
|
this.router = router;
|
|
@@ -4253,6 +5168,17 @@ var T1Administrator = class extends BaseTier {
|
|
|
4253
5168
|
this.t2PeerBus.onPeerMessage = cb;
|
|
4254
5169
|
this.t2PeerBus.sessionId = sessionId;
|
|
4255
5170
|
}
|
|
5171
|
+
/**
|
|
5172
|
+
* Install a "boardroom" gate: called with T1's plan BEFORE any T2 manager
|
|
5173
|
+
* spawns. When unset, plans proceed immediately (headless/SDK unchanged).
|
|
5174
|
+
*/
|
|
5175
|
+
setPlanApprovalCallback(cb) {
|
|
5176
|
+
this.planApprovalCallback = cb;
|
|
5177
|
+
}
|
|
5178
|
+
/** Decompose a prompt into a plan WITHOUT executing it (powers /plan preview). */
|
|
5179
|
+
async previewPlan(prompt) {
|
|
5180
|
+
return this.decomposeTask(prompt);
|
|
5181
|
+
}
|
|
4256
5182
|
async execute(userPrompt, images, systemContext, signal) {
|
|
4257
5183
|
this.signal = signal;
|
|
4258
5184
|
this.taskId = crypto.randomUUID();
|
|
@@ -4271,29 +5197,72 @@ var T1Administrator = class extends BaseTier {
|
|
|
4271
5197
|
enrichedPrompt = await this.analyzeImages(userPrompt, images);
|
|
4272
5198
|
}
|
|
4273
5199
|
this.throwIfCancelled();
|
|
4274
|
-
|
|
4275
|
-
this.sendStatusUpdate({
|
|
4276
|
-
progressPct: 10,
|
|
4277
|
-
currentAction: `Plan ready: ${plan.complexity} \u2192 ${plan.sections.length} sections`,
|
|
4278
|
-
status: "IN_PROGRESS"
|
|
4279
|
-
});
|
|
4280
|
-
this.emit("plan", { taskId: this.taskId, plan });
|
|
4281
|
-
this.
|
|
5200
|
+
let plan = await this.decomposeTask(enrichedPrompt, systemContext);
|
|
5201
|
+
this.sendStatusUpdate({
|
|
5202
|
+
progressPct: 10,
|
|
5203
|
+
currentAction: `Plan ready: ${plan.complexity} \u2192 ${plan.sections.length} sections`,
|
|
5204
|
+
status: "IN_PROGRESS"
|
|
5205
|
+
});
|
|
5206
|
+
this.emit("plan", { taskId: this.taskId, plan });
|
|
5207
|
+
if (this.planApprovalCallback) {
|
|
5208
|
+
const maxRounds = this.config.planReview?.maxRevisionRounds ?? 5;
|
|
5209
|
+
const reviewer = this.config.planReview?.autoReviewer === true;
|
|
5210
|
+
let round = 0;
|
|
5211
|
+
for (; ; ) {
|
|
5212
|
+
const critique = reviewer ? await this.reviewPlan(plan, enrichedPrompt) ?? void 0 : void 0;
|
|
5213
|
+
this.sendStatusUpdate({
|
|
5214
|
+
progressPct: 10,
|
|
5215
|
+
currentAction: "Boardroom: waiting for plan approval",
|
|
5216
|
+
status: "IN_PROGRESS"
|
|
5217
|
+
});
|
|
5218
|
+
const decision = await this.planApprovalCallback(plan, { critique });
|
|
5219
|
+
if (!decision.approved) {
|
|
5220
|
+
const output2 = "Plan rejected in the boardroom \u2014 nothing was executed. Rephrase the request or adjust the plan with a new prompt.";
|
|
5221
|
+
this.setStatus("COMPLETED", output2);
|
|
5222
|
+
this.sendStatusUpdate({ progressPct: 100, currentAction: "Plan rejected by user", status: "IN_PROGRESS", output: output2 });
|
|
5223
|
+
return { output: output2, t2Results: [], taskId: this.taskId, complexity: plan.complexity };
|
|
5224
|
+
}
|
|
5225
|
+
if (decision.editedPlan?.sections?.length) {
|
|
5226
|
+
plan = decision.editedPlan;
|
|
5227
|
+
try {
|
|
5228
|
+
this.validatePlan(plan);
|
|
5229
|
+
} catch {
|
|
5230
|
+
}
|
|
5231
|
+
this.emit("plan", { taskId: this.taskId, plan });
|
|
5232
|
+
}
|
|
5233
|
+
if (decision.note?.trim() && round < maxRounds) {
|
|
5234
|
+
round++;
|
|
5235
|
+
this.log(`Boardroom note \u2014 re-planning (round ${round}/${maxRounds}): ${decision.note}`);
|
|
5236
|
+
plan = await this.decomposeTask(
|
|
5237
|
+
`${enrichedPrompt}
|
|
5238
|
+
|
|
5239
|
+
Board guidance (must be followed in the plan): ${decision.note}`,
|
|
5240
|
+
systemContext
|
|
5241
|
+
);
|
|
5242
|
+
this.emit("plan", { taskId: this.taskId, plan });
|
|
5243
|
+
continue;
|
|
5244
|
+
}
|
|
5245
|
+
break;
|
|
5246
|
+
}
|
|
5247
|
+
}
|
|
5248
|
+
this.throwIfCancelled();
|
|
4282
5249
|
let allT2Results = await this.dispatchT2Managers(plan.sections);
|
|
4283
5250
|
let pass = 1;
|
|
4284
|
-
const
|
|
4285
|
-
|
|
5251
|
+
const maxReplanPasses = this.config.maxReplanPasses ?? 2;
|
|
5252
|
+
const okCount = (rs) => rs.filter((r) => r.status === "COMPLETED" || r.status === "PARTIAL").length;
|
|
5253
|
+
while (pass <= maxReplanPasses) {
|
|
4286
5254
|
const reviewResult = await this.reviewT2Outputs(enrichedPrompt, plan, allT2Results);
|
|
4287
5255
|
if (reviewResult.approved) {
|
|
4288
5256
|
this.log("T1 Review passed.");
|
|
4289
5257
|
break;
|
|
4290
5258
|
}
|
|
4291
|
-
this.log(`T1 Review rejected outputs. Replanning (Pass ${pass}). Reason: ${reviewResult.reason}`);
|
|
5259
|
+
this.log(`T1 Review rejected outputs. Replanning (Pass ${pass}/${maxReplanPasses}). Reason: ${reviewResult.reason}`);
|
|
4292
5260
|
this.sendStatusUpdate({
|
|
4293
5261
|
progressPct: 80 + pass * 5,
|
|
4294
5262
|
currentAction: `Review failed: ${reviewResult.reason}. Replanning...`,
|
|
4295
5263
|
status: "IN_PROGRESS"
|
|
4296
5264
|
});
|
|
5265
|
+
const okBefore = okCount(allT2Results);
|
|
4297
5266
|
const correctionPlan = await this.decomposeTask(`The previous execution plan failed to fully satisfy the original goal or encountered errors.
|
|
4298
5267
|
Review reason: ${reviewResult.reason}
|
|
4299
5268
|
|
|
@@ -4302,6 +5271,10 @@ Original goal: ${enrichedPrompt}
|
|
|
4302
5271
|
Create a CORRECTION PLAN that contains only the new sections needed to fix the issues. Do not repeat successful sections.`);
|
|
4303
5272
|
const correctionResults = await this.dispatchT2Managers(correctionPlan.sections);
|
|
4304
5273
|
allT2Results = [...allT2Results, ...correctionResults];
|
|
5274
|
+
if (okCount(allT2Results) <= okBefore) {
|
|
5275
|
+
this.log("T1 Review: corrective pass made no net progress \u2014 stopping early with the best partial result.");
|
|
5276
|
+
break;
|
|
5277
|
+
}
|
|
4305
5278
|
pass++;
|
|
4306
5279
|
}
|
|
4307
5280
|
this.sendStatusUpdate({
|
|
@@ -4370,6 +5343,34 @@ If no, reply with "REJECTED: [Detailed reason explaining exactly what is missing
|
|
|
4370
5343
|
|
|
4371
5344
|
[Image context: ${result.content}]`;
|
|
4372
5345
|
}
|
|
5346
|
+
/**
|
|
5347
|
+
* Automated reviewer pass: a single T1 critique of the plan before the user
|
|
5348
|
+
* sees it (planReview.autoReviewer). Best-effort — returns null on any error
|
|
5349
|
+
* so it never blocks the approval gate.
|
|
5350
|
+
*/
|
|
5351
|
+
async reviewPlan(plan, goal) {
|
|
5352
|
+
try {
|
|
5353
|
+
const sections = plan.sections.map((s, i) => `${i + 1}. ${s.sectionTitle} \u2014 ${s.description} (${s.t3Subtasks?.length ?? 0} subtasks${s.dependsOn?.length ? `, depends on: ${s.dependsOn.join(", ")}` : ""})`).join("\n");
|
|
5354
|
+
const prompt = `You are a senior engineer reviewing an execution plan BEFORE it runs.
|
|
5355
|
+
|
|
5356
|
+
GOAL:
|
|
5357
|
+
${goal}
|
|
5358
|
+
|
|
5359
|
+
PLAN (${plan.complexity}, ${plan.sections.length} sections):
|
|
5360
|
+
${sections}
|
|
5361
|
+
|
|
5362
|
+
In 3-5 terse bullets, flag the most important RISKS, GAPS, or over-/under-decomposition the operator should weigh before approving. If the plan is sound, say so in one line. Output plain-text bullets only - no preamble.`;
|
|
5363
|
+
const result = await this.router.generate("T1", {
|
|
5364
|
+
messages: [{ role: "user", content: prompt }],
|
|
5365
|
+
systemPrompt: "You are a concise, critical plan reviewer. Be specific and brief.",
|
|
5366
|
+
maxTokens: 400
|
|
5367
|
+
});
|
|
5368
|
+
const text = (result.content ?? "").trim();
|
|
5369
|
+
return text.length ? text : null;
|
|
5370
|
+
} catch {
|
|
5371
|
+
return null;
|
|
5372
|
+
}
|
|
5373
|
+
}
|
|
4373
5374
|
async decomposeTask(prompt, systemContext) {
|
|
4374
5375
|
const contextSection = systemContext ? `
|
|
4375
5376
|
Project context:
|
|
@@ -4669,7 +5670,14 @@ Leave dependsOn empty for sections that can run immediately in parallel.`;
|
|
|
4669
5670
|
async compileFinalOutput(originalPrompt, plan, t2Results) {
|
|
4670
5671
|
const completedSections = t2Results.filter((r) => r.status !== "FAILED");
|
|
4671
5672
|
if (!completedSections.length) {
|
|
4672
|
-
|
|
5673
|
+
const allIssues = t2Results.flatMap((r) => r.t3Results.flatMap((t) => t.issues));
|
|
5674
|
+
const critical = allIssues.find((i) => i.includes("[CRITICAL_TOOL_ERROR]"));
|
|
5675
|
+
const stalled = allIssues.find((i) => /^Stalled:/.test(i));
|
|
5676
|
+
const topReason = critical ?? stalled ?? allIssues[0] ?? "no specific reason recorded";
|
|
5677
|
+
const sectionWord = t2Results.length === 1 ? "section" : "sections";
|
|
5678
|
+
return `Task failed \u2014 ${topReason}
|
|
5679
|
+
|
|
5680
|
+
All ${t2Results.length} ${sectionWord} encountered errors. Run \`/logs\` for details.`;
|
|
4673
5681
|
}
|
|
4674
5682
|
const sectionsText = completedSections.map((r) => `**${r.sectionTitle}**
|
|
4675
5683
|
${r.sectionSummary}
|
|
@@ -4789,7 +5797,7 @@ var ShellTool = class extends BaseTool {
|
|
|
4789
5797
|
const timeout = input["timeout"] ?? 3e4;
|
|
4790
5798
|
this.validateCommand(command);
|
|
4791
5799
|
try {
|
|
4792
|
-
const { stdout, stderr } = await execAsync(command, { cwd, timeout });
|
|
5800
|
+
const { stdout, stderr } = await execAsync(command, { cwd, timeout, windowsHide: true });
|
|
4793
5801
|
const out = [stdout, stderr].filter(Boolean).join("\n").trim();
|
|
4794
5802
|
return out || "(no output)";
|
|
4795
5803
|
} catch (err) {
|
|
@@ -4803,11 +5811,14 @@ ${[e.stdout, e.stderr].filter(Boolean).join("\n").trim()}`;
|
|
|
4803
5811
|
}
|
|
4804
5812
|
validateCommand(command) {
|
|
4805
5813
|
const builtinDangerous = [
|
|
4806
|
-
|
|
4807
|
-
|
|
4808
|
-
|
|
4809
|
-
|
|
4810
|
-
|
|
5814
|
+
/\brm\s+(?:-\w+\s+)*-\w*[rf]\w*[rf]\w*\s+(?:\/|~|\$HOME)(?:\s|$)/,
|
|
5815
|
+
// rm -rf / , rm -fr ~
|
|
5816
|
+
/>\s*\/dev\/[sh]d[a-z]/,
|
|
5817
|
+
/\bmkfs[.\s]/,
|
|
5818
|
+
/\bdd\s+.*\bof=\/dev\/[sh]d[a-z]/,
|
|
5819
|
+
/\bchmod\s+(?:-\w+\s+)*-?R?\s*777\s+\//,
|
|
5820
|
+
/:\(\)\s*\{\s*:\s*\|\s*:?\s*&\s*\}\s*;/
|
|
5821
|
+
// fork bomb :(){ :|:& };:
|
|
4811
5822
|
];
|
|
4812
5823
|
for (const pattern of builtinDangerous) {
|
|
4813
5824
|
if (pattern.test(command)) {
|
|
@@ -4846,16 +5857,16 @@ function resolveInWorkspace(workspaceRoot, input) {
|
|
|
4846
5857
|
if (typeof input !== "string" || input.length === 0) {
|
|
4847
5858
|
throw new WorkspaceSandboxError(String(input), workspaceRoot);
|
|
4848
5859
|
}
|
|
4849
|
-
const root =
|
|
4850
|
-
const abs =
|
|
4851
|
-
const rel =
|
|
4852
|
-
if (rel === "" || rel === ".") ; else if (rel.startsWith("..") ||
|
|
5860
|
+
const root = path18__default.default.resolve(workspaceRoot);
|
|
5861
|
+
const abs = path18__default.default.isAbsolute(input) ? path18__default.default.resolve(input) : path18__default.default.resolve(root, input);
|
|
5862
|
+
const rel = path18__default.default.relative(root, abs);
|
|
5863
|
+
if (rel === "" || rel === ".") ; else if (rel.startsWith("..") || path18__default.default.isAbsolute(rel)) {
|
|
4853
5864
|
throw new WorkspaceSandboxError(input, root);
|
|
4854
5865
|
}
|
|
4855
5866
|
try {
|
|
4856
|
-
const real =
|
|
4857
|
-
const realRel =
|
|
4858
|
-
if (realRel !== "" && realRel !== "." && (realRel.startsWith("..") ||
|
|
5867
|
+
const real = fs17__default.default.realpathSync(abs);
|
|
5868
|
+
const realRel = path18__default.default.relative(root, real);
|
|
5869
|
+
if (realRel !== "" && realRel !== "." && (realRel.startsWith("..") || path18__default.default.isAbsolute(realRel))) {
|
|
4859
5870
|
throw new WorkspaceSandboxError(input, root);
|
|
4860
5871
|
}
|
|
4861
5872
|
} catch (e) {
|
|
@@ -4882,7 +5893,7 @@ var FileReadTool = class extends BaseTool {
|
|
|
4882
5893
|
const absPath = resolveInWorkspace(this.workspaceRoot, filePath);
|
|
4883
5894
|
const offset = input["offset"] ?? 1;
|
|
4884
5895
|
const limit = input["limit"];
|
|
4885
|
-
const content = await
|
|
5896
|
+
const content = await fs4__default.default.readFile(absPath, "utf-8");
|
|
4886
5897
|
const lines = content.split("\n");
|
|
4887
5898
|
const start = Math.max(0, offset - 1);
|
|
4888
5899
|
const end = limit ? start + limit : lines.length;
|
|
@@ -4911,13 +5922,13 @@ var FileWriteTool = class extends BaseTool {
|
|
|
4911
5922
|
const content = input["content"];
|
|
4912
5923
|
if (options.saveSnapshot) {
|
|
4913
5924
|
try {
|
|
4914
|
-
const oldContent = await
|
|
5925
|
+
const oldContent = await fs4__default.default.readFile(absPath, "utf-8");
|
|
4915
5926
|
await options.saveSnapshot(absPath, oldContent);
|
|
4916
5927
|
} catch {
|
|
4917
5928
|
}
|
|
4918
5929
|
}
|
|
4919
|
-
await
|
|
4920
|
-
await
|
|
5930
|
+
await fs4__default.default.mkdir(path18__default.default.dirname(absPath), { recursive: true });
|
|
5931
|
+
await fs4__default.default.writeFile(absPath, content, "utf-8");
|
|
4921
5932
|
return `Written ${content.length} characters to ${filePath}`;
|
|
4922
5933
|
}
|
|
4923
5934
|
};
|
|
@@ -4943,7 +5954,7 @@ var FileEditTool = class extends BaseTool {
|
|
|
4943
5954
|
const oldString = input["old_string"];
|
|
4944
5955
|
const newString = input["new_string"];
|
|
4945
5956
|
const replaceAll = input["replace_all"] ?? false;
|
|
4946
|
-
const rawContent = await
|
|
5957
|
+
const rawContent = await fs4__default.default.readFile(absPath, "utf-8");
|
|
4947
5958
|
if (options.saveSnapshot) {
|
|
4948
5959
|
await options.saveSnapshot(absPath, rawContent);
|
|
4949
5960
|
}
|
|
@@ -4955,7 +5966,7 @@ var FileEditTool = class extends BaseTool {
|
|
|
4955
5966
|
);
|
|
4956
5967
|
}
|
|
4957
5968
|
const updated = replaceAll ? content.split(normalizedOld).join(newString) : content.replace(normalizedOld, newString);
|
|
4958
|
-
await
|
|
5969
|
+
await fs4__default.default.writeFile(absPath, updated, "utf-8");
|
|
4959
5970
|
const count = replaceAll ? content.split(normalizedOld).length - 1 : 1;
|
|
4960
5971
|
return `Replaced ${count} occurrence(s) in ${filePath}`;
|
|
4961
5972
|
}
|
|
@@ -4978,12 +5989,12 @@ var FileDeleteTool = class extends BaseTool {
|
|
|
4978
5989
|
const absPath = resolveInWorkspace(this.workspaceRoot, filePath);
|
|
4979
5990
|
if (options.saveSnapshot) {
|
|
4980
5991
|
try {
|
|
4981
|
-
const oldContent = await
|
|
5992
|
+
const oldContent = await fs4__default.default.readFile(absPath, "utf-8");
|
|
4982
5993
|
await options.saveSnapshot(absPath, oldContent);
|
|
4983
5994
|
} catch {
|
|
4984
5995
|
}
|
|
4985
5996
|
}
|
|
4986
|
-
await
|
|
5997
|
+
await fs4__default.default.rm(absPath, { recursive: false });
|
|
4987
5998
|
return `Deleted ${filePath}`;
|
|
4988
5999
|
}
|
|
4989
6000
|
};
|
|
@@ -5000,7 +6011,7 @@ var FileListTool = class extends BaseTool {
|
|
|
5000
6011
|
async execute(input, _options) {
|
|
5001
6012
|
const inputPath = input["path"] || ".";
|
|
5002
6013
|
const absPath = resolveInWorkspace(this.workspaceRoot, inputPath);
|
|
5003
|
-
const entries = await
|
|
6014
|
+
const entries = await fs4__default.default.readdir(absPath, { withFileTypes: true });
|
|
5004
6015
|
return entries.map((e) => `${e.isDirectory() ? "[DIR] " : " "}${e.name}`).join("\n") || "(empty directory)";
|
|
5005
6016
|
}
|
|
5006
6017
|
};
|
|
@@ -5093,6 +6104,8 @@ var GitTool = class extends BaseTool {
|
|
|
5093
6104
|
return lines.join("\n") || "Working tree clean";
|
|
5094
6105
|
}
|
|
5095
6106
|
};
|
|
6107
|
+
|
|
6108
|
+
// src/tools/github.ts
|
|
5096
6109
|
var GitHubTool = class extends BaseTool {
|
|
5097
6110
|
name = "github";
|
|
5098
6111
|
description = "Interact with GitHub or GitLab: create PRs, list issues, comment on issues.";
|
|
@@ -5117,6 +6130,34 @@ var GitHubTool = class extends BaseTool {
|
|
|
5117
6130
|
isDangerous() {
|
|
5118
6131
|
return true;
|
|
5119
6132
|
}
|
|
6133
|
+
// ── fetch helpers (replace axios) ──────────────
|
|
6134
|
+
async request(url, init) {
|
|
6135
|
+
const res = await fetch(url, init);
|
|
6136
|
+
const text = await res.text();
|
|
6137
|
+
let data;
|
|
6138
|
+
try {
|
|
6139
|
+
data = text ? JSON.parse(text) : void 0;
|
|
6140
|
+
} catch {
|
|
6141
|
+
data = text;
|
|
6142
|
+
}
|
|
6143
|
+
if (!res.ok) {
|
|
6144
|
+
const err = new Error(`HTTP ${res.status}`);
|
|
6145
|
+
err.status = res.status;
|
|
6146
|
+
err.data = data;
|
|
6147
|
+
throw err;
|
|
6148
|
+
}
|
|
6149
|
+
return data;
|
|
6150
|
+
}
|
|
6151
|
+
apiGet(url, headers) {
|
|
6152
|
+
return this.request(url, { headers });
|
|
6153
|
+
}
|
|
6154
|
+
apiPost(url, body, headers) {
|
|
6155
|
+
return this.request(url, {
|
|
6156
|
+
method: "POST",
|
|
6157
|
+
headers: { ...headers, "Content-Type": "application/json" },
|
|
6158
|
+
body: JSON.stringify(body)
|
|
6159
|
+
});
|
|
6160
|
+
}
|
|
5120
6161
|
async execute(input, _options) {
|
|
5121
6162
|
const platform = input["platform"] ?? "github";
|
|
5122
6163
|
const operation = input["operation"];
|
|
@@ -5139,10 +6180,10 @@ var GitHubTool = class extends BaseTool {
|
|
|
5139
6180
|
}
|
|
5140
6181
|
return await this.executeGitLab(operation, repo, token, input);
|
|
5141
6182
|
} catch (err) {
|
|
5142
|
-
const
|
|
5143
|
-
if (
|
|
5144
|
-
const status =
|
|
5145
|
-
const msg =
|
|
6183
|
+
const httpErr = err;
|
|
6184
|
+
if (httpErr?.status) {
|
|
6185
|
+
const status = httpErr.status;
|
|
6186
|
+
const msg = httpErr.data?.message ?? "";
|
|
5146
6187
|
switch (status) {
|
|
5147
6188
|
case 401:
|
|
5148
6189
|
return `Authentication failed: Your ${platform} token is invalid or expired. Check your token and try again.`;
|
|
@@ -5155,10 +6196,10 @@ var GitHubTool = class extends BaseTool {
|
|
|
5155
6196
|
case 429:
|
|
5156
6197
|
return `Rate limited by ${platform}. Please wait a moment before trying again.`;
|
|
5157
6198
|
default:
|
|
5158
|
-
return `${platform} API error (${status}): ${msg || (
|
|
6199
|
+
return `${platform} API error (${status}): ${msg || (httpErr.message ?? "Unknown error")}`;
|
|
5159
6200
|
}
|
|
5160
6201
|
}
|
|
5161
|
-
return `${platform} request failed: ${
|
|
6202
|
+
return `${platform} request failed: ${httpErr.message ?? String(err)}`;
|
|
5162
6203
|
}
|
|
5163
6204
|
}
|
|
5164
6205
|
async executeGitHub(operation, repo, token, input) {
|
|
@@ -5169,35 +6210,35 @@ var GitHubTool = class extends BaseTool {
|
|
|
5169
6210
|
const base = `https://api.github.com/repos/${repo}`;
|
|
5170
6211
|
switch (operation) {
|
|
5171
6212
|
case "list_issues": {
|
|
5172
|
-
const
|
|
5173
|
-
return
|
|
6213
|
+
const data = await this.apiGet(`${base}/issues`, headers);
|
|
6214
|
+
return data.map((i) => `#${i.number} [${i.state}] ${i.title}`).join("\n");
|
|
5174
6215
|
}
|
|
5175
6216
|
case "list_prs": {
|
|
5176
|
-
const
|
|
5177
|
-
return
|
|
6217
|
+
const data = await this.apiGet(`${base}/pulls`, headers);
|
|
6218
|
+
return data.map((p) => `#${p.number} [${p.state}] ${p.title} (${p.head.ref} \u2192 ${p.base.ref})`).join("\n");
|
|
5178
6219
|
}
|
|
5179
6220
|
case "create_pr": {
|
|
5180
|
-
const
|
|
6221
|
+
const data = await this.apiPost(`${base}/pulls`, {
|
|
5181
6222
|
title: input["title"],
|
|
5182
6223
|
body: input["body"] ?? "",
|
|
5183
6224
|
head: input["head"],
|
|
5184
6225
|
base: input["base"] ?? "main"
|
|
5185
|
-
},
|
|
5186
|
-
return `Created PR #${
|
|
6226
|
+
}, headers);
|
|
6227
|
+
return `Created PR #${data.number}: ${data.html_url}`;
|
|
5187
6228
|
}
|
|
5188
6229
|
case "comment_issue": {
|
|
5189
6230
|
const num = input["issue_number"];
|
|
5190
|
-
await
|
|
6231
|
+
await this.apiPost(`${base}/issues/${num}/comments`, { body: input["body"] }, headers);
|
|
5191
6232
|
return `Comment added to #${num}`;
|
|
5192
6233
|
}
|
|
5193
6234
|
case "get_pr": {
|
|
5194
6235
|
const num = input["issue_number"];
|
|
5195
|
-
const
|
|
5196
|
-
return `PR #${num}: ${
|
|
5197
|
-
State: ${
|
|
5198
|
-
${
|
|
6236
|
+
const data = await this.apiGet(`${base}/pulls/${num}`, headers);
|
|
6237
|
+
return `PR #${num}: ${data.title}
|
|
6238
|
+
State: ${data.state}
|
|
6239
|
+
${data.html_url}
|
|
5199
6240
|
|
|
5200
|
-
${
|
|
6241
|
+
${data.body}`;
|
|
5201
6242
|
}
|
|
5202
6243
|
default:
|
|
5203
6244
|
throw new Error(`Unknown GitHub operation: ${operation}`);
|
|
@@ -5209,35 +6250,35 @@ ${response.data.body}`;
|
|
|
5209
6250
|
const base = `https://gitlab.com/api/v4/projects/${encodedRepo}`;
|
|
5210
6251
|
switch (operation) {
|
|
5211
6252
|
case "list_issues": {
|
|
5212
|
-
const
|
|
5213
|
-
return
|
|
6253
|
+
const data = await this.apiGet(`${base}/issues`, headers);
|
|
6254
|
+
return data.map((i) => `#${i.iid} [${i.state}] ${i.title}`).join("\n");
|
|
5214
6255
|
}
|
|
5215
6256
|
case "create_pr": {
|
|
5216
|
-
const
|
|
6257
|
+
const data = await this.apiPost(`${base}/merge_requests`, {
|
|
5217
6258
|
title: input["title"],
|
|
5218
6259
|
description: input["body"] ?? "",
|
|
5219
6260
|
source_branch: input["head"],
|
|
5220
6261
|
target_branch: input["base"] ?? "main"
|
|
5221
|
-
},
|
|
5222
|
-
return `Created MR !${
|
|
6262
|
+
}, headers);
|
|
6263
|
+
return `Created MR !${data.iid}: ${data.web_url}`;
|
|
5223
6264
|
}
|
|
5224
6265
|
case "list_prs": {
|
|
5225
|
-
const
|
|
5226
|
-
return
|
|
6266
|
+
const data = await this.apiGet(`${base}/merge_requests`, headers);
|
|
6267
|
+
return data.map((p) => `!${p.iid} [${p.state}] ${p.title} (${p.source_branch} \u2192 ${p.target_branch})`).join("\n");
|
|
5227
6268
|
}
|
|
5228
6269
|
case "comment_issue": {
|
|
5229
6270
|
const num = input["issue_number"];
|
|
5230
|
-
await
|
|
6271
|
+
await this.apiPost(`${base}/issues/${num}/notes`, { body: input["body"] }, headers);
|
|
5231
6272
|
return `Comment added to #${num}`;
|
|
5232
6273
|
}
|
|
5233
6274
|
case "get_pr": {
|
|
5234
6275
|
const num = input["issue_number"];
|
|
5235
|
-
const
|
|
5236
|
-
return `MR !${num}: ${
|
|
5237
|
-
State: ${
|
|
5238
|
-
${
|
|
6276
|
+
const data = await this.apiGet(`${base}/merge_requests/${num}`, headers);
|
|
6277
|
+
return `MR !${num}: ${data.title}
|
|
6278
|
+
State: ${data.state}
|
|
6279
|
+
${data.web_url}
|
|
5239
6280
|
|
|
5240
|
-
${
|
|
6281
|
+
${data.description}`;
|
|
5241
6282
|
}
|
|
5242
6283
|
default:
|
|
5243
6284
|
throw new Error(`GitLab operation not supported: ${operation}`);
|
|
@@ -5383,8 +6424,8 @@ var ImageAnalyzeTool = class extends BaseTool {
|
|
|
5383
6424
|
}
|
|
5384
6425
|
};
|
|
5385
6426
|
async function fileToImageAttachment(filePath) {
|
|
5386
|
-
const data = await
|
|
5387
|
-
const ext =
|
|
6427
|
+
const data = await fs4__default.default.readFile(filePath);
|
|
6428
|
+
const ext = path18__default.default.extname(filePath).toLowerCase();
|
|
5388
6429
|
const mimeMap = {
|
|
5389
6430
|
".jpg": "image/jpeg",
|
|
5390
6431
|
".jpeg": "image/jpeg",
|
|
@@ -5418,14 +6459,14 @@ var PDFCreateTool = class extends BaseTool {
|
|
|
5418
6459
|
const filePath = input["path"];
|
|
5419
6460
|
const content = input["content"];
|
|
5420
6461
|
const title = input["title"];
|
|
5421
|
-
const dir =
|
|
5422
|
-
if (!
|
|
5423
|
-
|
|
6462
|
+
const dir = path18__default.default.dirname(filePath);
|
|
6463
|
+
if (!fs17__default.default.existsSync(dir)) {
|
|
6464
|
+
fs17__default.default.mkdirSync(dir, { recursive: true });
|
|
5424
6465
|
}
|
|
5425
6466
|
return new Promise((resolve, reject) => {
|
|
5426
6467
|
try {
|
|
5427
6468
|
const doc = new PDFDocument__default.default({ margin: 50 });
|
|
5428
|
-
const stream =
|
|
6469
|
+
const stream = fs17__default.default.createWriteStream(filePath);
|
|
5429
6470
|
doc.pipe(stream);
|
|
5430
6471
|
if (title) {
|
|
5431
6472
|
doc.info["Title"] = title;
|
|
@@ -5503,24 +6544,22 @@ var CodeInterpreterTool = class extends BaseTool {
|
|
|
5503
6544
|
}
|
|
5504
6545
|
cmdPrefix = NODE_CMD;
|
|
5505
6546
|
}
|
|
5506
|
-
const tmpDir =
|
|
5507
|
-
if (!
|
|
5508
|
-
|
|
6547
|
+
const tmpDir = path18__default.default.join(this.workspaceRoot, ".cascade", "tmp");
|
|
6548
|
+
if (!fs17__default.default.existsSync(tmpDir)) {
|
|
6549
|
+
fs17__default.default.mkdirSync(tmpDir, { recursive: true });
|
|
5509
6550
|
}
|
|
5510
6551
|
const extension = language === "python" ? "py" : "js";
|
|
5511
6552
|
const fileName = `intp_${crypto.randomUUID().slice(0, 8)}.${extension}`;
|
|
5512
|
-
const filePath =
|
|
5513
|
-
|
|
5514
|
-
const
|
|
5515
|
-
const quotedArgs = args.map((a) => `"${a}"`).join(" ");
|
|
5516
|
-
const fullCmd = `${cmdPrefix} ${quotedPath}${quotedArgs ? " " + quotedArgs : ""}`;
|
|
6553
|
+
const filePath = path18__default.default.join(tmpDir, fileName);
|
|
6554
|
+
fs17__default.default.writeFileSync(filePath, code, "utf-8");
|
|
6555
|
+
const execArgs = [filePath, ...args];
|
|
5517
6556
|
return new Promise((resolve) => {
|
|
5518
6557
|
const startMs = Date.now();
|
|
5519
|
-
child_process.
|
|
6558
|
+
child_process.execFile(cmdPrefix, execArgs, { cwd: this.workspaceRoot, timeout: 3e4 }, (error, stdout, stderr) => {
|
|
5520
6559
|
const duration = Date.now() - startMs;
|
|
5521
6560
|
try {
|
|
5522
|
-
if (
|
|
5523
|
-
|
|
6561
|
+
if (fs17__default.default.existsSync(filePath)) {
|
|
6562
|
+
fs17__default.default.unlinkSync(filePath);
|
|
5524
6563
|
}
|
|
5525
6564
|
} catch (cleanupErr) {
|
|
5526
6565
|
console.error(`Failed to cleanup interpreter script ${filePath}:`, cleanupErr);
|
|
@@ -5799,7 +6838,7 @@ var GlobTool = class extends BaseTool {
|
|
|
5799
6838
|
};
|
|
5800
6839
|
async execute(input, _options) {
|
|
5801
6840
|
const pattern = input["pattern"];
|
|
5802
|
-
const searchPath = input["path"] ?
|
|
6841
|
+
const searchPath = input["path"] ? path18__default.default.resolve(this.workspaceRoot, input["path"]) : this.workspaceRoot;
|
|
5803
6842
|
const matches = await glob.glob(pattern, {
|
|
5804
6843
|
cwd: searchPath,
|
|
5805
6844
|
ignore: ["node_modules/**", ".git/**", "dist/**", "build/**"],
|
|
@@ -5812,7 +6851,7 @@ var GlobTool = class extends BaseTool {
|
|
|
5812
6851
|
const withMtime = await Promise.all(
|
|
5813
6852
|
matches.map(async (rel) => {
|
|
5814
6853
|
try {
|
|
5815
|
-
const stat = await
|
|
6854
|
+
const stat = await fs4__default.default.stat(path18__default.default.join(searchPath, rel));
|
|
5816
6855
|
return { rel, mtime: stat.mtimeMs };
|
|
5817
6856
|
} catch {
|
|
5818
6857
|
return { rel, mtime: 0 };
|
|
@@ -5861,7 +6900,7 @@ var GrepTool = class extends BaseTool {
|
|
|
5861
6900
|
};
|
|
5862
6901
|
async execute(input, _options) {
|
|
5863
6902
|
const pattern = input["pattern"];
|
|
5864
|
-
const searchPath = input["path"] ?
|
|
6903
|
+
const searchPath = input["path"] ? path18__default.default.resolve(this.workspaceRoot, input["path"]) : this.workspaceRoot;
|
|
5865
6904
|
const globPattern = input["glob"];
|
|
5866
6905
|
const outputMode = input["output_mode"] ?? "content";
|
|
5867
6906
|
const context = input["context"] ?? 0;
|
|
@@ -5915,15 +6954,15 @@ var GrepTool = class extends BaseTool {
|
|
|
5915
6954
|
nodir: true
|
|
5916
6955
|
});
|
|
5917
6956
|
} catch {
|
|
5918
|
-
files = [
|
|
6957
|
+
files = [path18__default.default.relative(searchPath, searchPath) || "."];
|
|
5919
6958
|
}
|
|
5920
6959
|
const results = [];
|
|
5921
6960
|
let totalCount = 0;
|
|
5922
6961
|
for (const rel of files) {
|
|
5923
|
-
const abs =
|
|
6962
|
+
const abs = path18__default.default.join(searchPath, rel);
|
|
5924
6963
|
let content;
|
|
5925
6964
|
try {
|
|
5926
|
-
content = await
|
|
6965
|
+
content = await fs4__default.default.readFile(abs, "utf-8");
|
|
5927
6966
|
} catch {
|
|
5928
6967
|
continue;
|
|
5929
6968
|
}
|
|
@@ -5961,6 +7000,92 @@ Total: ${totalCount} matches`);
|
|
|
5961
7000
|
return results.join("\n");
|
|
5962
7001
|
}
|
|
5963
7002
|
};
|
|
7003
|
+
var SsrfBlockedError = class extends Error {
|
|
7004
|
+
constructor(message) {
|
|
7005
|
+
super(message);
|
|
7006
|
+
this.name = "SsrfBlockedError";
|
|
7007
|
+
}
|
|
7008
|
+
};
|
|
7009
|
+
var ALLOWED_PROTOCOLS = /* @__PURE__ */ new Set(["http:", "https:"]);
|
|
7010
|
+
var MAX_REDIRECTS = 5;
|
|
7011
|
+
function allowLocal() {
|
|
7012
|
+
return process.env["CASCADE_ALLOW_LOCAL_FETCH"] === "1";
|
|
7013
|
+
}
|
|
7014
|
+
function isPrivateAddress(ip) {
|
|
7015
|
+
const type = net__default.default.isIP(ip);
|
|
7016
|
+
if (type === 4) return isPrivateIPv4(ip);
|
|
7017
|
+
if (type === 6) return isPrivateIPv6(ip);
|
|
7018
|
+
return true;
|
|
7019
|
+
}
|
|
7020
|
+
function isPrivateIPv4(ip) {
|
|
7021
|
+
const parts = ip.split(".").map((p) => Number(p));
|
|
7022
|
+
if (parts.length !== 4 || parts.some((p) => Number.isNaN(p) || p < 0 || p > 255)) return true;
|
|
7023
|
+
const [a, b] = parts;
|
|
7024
|
+
if (a === 0) return true;
|
|
7025
|
+
if (a === 10) return true;
|
|
7026
|
+
if (a === 127) return true;
|
|
7027
|
+
if (a === 169 && b === 254) return true;
|
|
7028
|
+
if (a === 172 && b >= 16 && b <= 31) return true;
|
|
7029
|
+
if (a === 192 && b === 168) return true;
|
|
7030
|
+
if (a === 100 && b >= 64 && b <= 127) return true;
|
|
7031
|
+
if (a >= 224) return true;
|
|
7032
|
+
return false;
|
|
7033
|
+
}
|
|
7034
|
+
function isPrivateIPv6(ip) {
|
|
7035
|
+
const lower = ip.toLowerCase().replace(/^\[|\]$/g, "");
|
|
7036
|
+
if (lower === "::1" || lower === "::") return true;
|
|
7037
|
+
if (lower.startsWith("fe80")) return true;
|
|
7038
|
+
if (lower.startsWith("fc") || lower.startsWith("fd")) return true;
|
|
7039
|
+
const mapped = /^::ffff:(\d+\.\d+\.\d+\.\d+)$/.exec(lower);
|
|
7040
|
+
if (mapped) return isPrivateIPv4(mapped[1]);
|
|
7041
|
+
return false;
|
|
7042
|
+
}
|
|
7043
|
+
async function assertPublicUrl(rawUrl) {
|
|
7044
|
+
let url;
|
|
7045
|
+
try {
|
|
7046
|
+
url = new URL(rawUrl);
|
|
7047
|
+
} catch {
|
|
7048
|
+
throw new SsrfBlockedError(`Invalid URL: ${rawUrl}`);
|
|
7049
|
+
}
|
|
7050
|
+
if (!ALLOWED_PROTOCOLS.has(url.protocol)) {
|
|
7051
|
+
throw new SsrfBlockedError(`Blocked URL scheme "${url.protocol}" \u2014 only http and https are allowed.`);
|
|
7052
|
+
}
|
|
7053
|
+
if (allowLocal()) return url;
|
|
7054
|
+
const host = url.hostname.replace(/^\[|\]$/g, "");
|
|
7055
|
+
if (net__default.default.isIP(host)) {
|
|
7056
|
+
if (isPrivateAddress(host)) {
|
|
7057
|
+
throw new SsrfBlockedError(`Blocked request to non-public address ${host}.`);
|
|
7058
|
+
}
|
|
7059
|
+
return url;
|
|
7060
|
+
}
|
|
7061
|
+
if (host === "localhost" || host.endsWith(".localhost") || host.endsWith(".local")) {
|
|
7062
|
+
throw new SsrfBlockedError(`Blocked request to local hostname "${host}".`);
|
|
7063
|
+
}
|
|
7064
|
+
let addresses;
|
|
7065
|
+
try {
|
|
7066
|
+
const records = await dns__default.default.lookup(host, { all: true });
|
|
7067
|
+
addresses = records.map((r) => r.address);
|
|
7068
|
+
} catch {
|
|
7069
|
+
throw new SsrfBlockedError(`Could not resolve host "${host}".`);
|
|
7070
|
+
}
|
|
7071
|
+
if (addresses.length === 0 || addresses.some((addr) => isPrivateAddress(addr))) {
|
|
7072
|
+
throw new SsrfBlockedError(`Blocked request to "${host}" \u2014 resolves to a non-public address.`);
|
|
7073
|
+
}
|
|
7074
|
+
return url;
|
|
7075
|
+
}
|
|
7076
|
+
async function safeFetch(rawUrl, init = {}) {
|
|
7077
|
+
let currentUrl = (await assertPublicUrl(rawUrl)).toString();
|
|
7078
|
+
for (let i = 0; i <= MAX_REDIRECTS; i++) {
|
|
7079
|
+
const resp = await fetch(currentUrl, { ...init, redirect: "manual" });
|
|
7080
|
+
if (resp.status < 300 || resp.status >= 400) return resp;
|
|
7081
|
+
const location = resp.headers.get("location");
|
|
7082
|
+
if (!location) return resp;
|
|
7083
|
+
const next = new URL(location, currentUrl);
|
|
7084
|
+
await assertPublicUrl(next.toString());
|
|
7085
|
+
currentUrl = next.toString();
|
|
7086
|
+
}
|
|
7087
|
+
throw new SsrfBlockedError(`Too many redirects (>${MAX_REDIRECTS}).`);
|
|
7088
|
+
}
|
|
5964
7089
|
|
|
5965
7090
|
// src/tools/web-fetch.ts
|
|
5966
7091
|
var MAX_CHARS = 5e4;
|
|
@@ -5994,15 +7119,17 @@ var WebFetchTool = class extends BaseTool {
|
|
|
5994
7119
|
const url = input["url"];
|
|
5995
7120
|
let resp;
|
|
5996
7121
|
try {
|
|
5997
|
-
resp = await
|
|
7122
|
+
resp = await safeFetch(url, {
|
|
5998
7123
|
headers: {
|
|
5999
7124
|
"User-Agent": "Cascade-AI/1.0 WebFetchTool",
|
|
6000
7125
|
Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,text/plain;q=0.8,*/*;q=0.5"
|
|
6001
7126
|
},
|
|
6002
|
-
signal: AbortSignal.timeout(TIMEOUT_MS)
|
|
6003
|
-
redirect: "follow"
|
|
7127
|
+
signal: AbortSignal.timeout(TIMEOUT_MS)
|
|
6004
7128
|
});
|
|
6005
7129
|
} catch (err) {
|
|
7130
|
+
if (err instanceof SsrfBlockedError) {
|
|
7131
|
+
return `Refused to fetch ${url}: ${err.message}`;
|
|
7132
|
+
}
|
|
6006
7133
|
return `Failed to fetch ${url}: ${err instanceof Error ? err.message : String(err)}`;
|
|
6007
7134
|
}
|
|
6008
7135
|
if (!resp.ok) {
|
|
@@ -6194,10 +7321,10 @@ var ToolRegistry = class extends EventEmitter__default.default {
|
|
|
6194
7321
|
}
|
|
6195
7322
|
isIgnored(filePath) {
|
|
6196
7323
|
if (!filePath) return false;
|
|
6197
|
-
const abs =
|
|
6198
|
-
const rel =
|
|
6199
|
-
if (!rel || rel.startsWith("..") ||
|
|
6200
|
-
const posixRel = rel.split(
|
|
7324
|
+
const abs = path18__default.default.resolve(this.workspaceRoot, filePath);
|
|
7325
|
+
const rel = path18__default.default.relative(this.workspaceRoot, abs);
|
|
7326
|
+
if (!rel || rel.startsWith("..") || path18__default.default.isAbsolute(rel)) return true;
|
|
7327
|
+
const posixRel = rel.split(path18__default.default.sep).join("/");
|
|
6201
7328
|
return this.ignoreMatcher.ignores(posixRel);
|
|
6202
7329
|
}
|
|
6203
7330
|
};
|
|
@@ -6221,9 +7348,11 @@ var McpClient = class _McpClient {
|
|
|
6221
7348
|
tools = /* @__PURE__ */ new Map();
|
|
6222
7349
|
trustedServers;
|
|
6223
7350
|
approvalCallback;
|
|
7351
|
+
onWarn;
|
|
6224
7352
|
constructor(options = {}) {
|
|
6225
7353
|
this.trustedServers = new Set(options.trustedServers ?? []);
|
|
6226
7354
|
this.approvalCallback = options.approvalCallback;
|
|
7355
|
+
this.onWarn = options.onWarn ?? ((message) => console.warn(message));
|
|
6227
7356
|
}
|
|
6228
7357
|
async connect(server) {
|
|
6229
7358
|
if (!this.trustedServers.has(server.name)) {
|
|
@@ -6252,7 +7381,7 @@ var McpClient = class _McpClient {
|
|
|
6252
7381
|
for (const tool of toolsResult.tools) {
|
|
6253
7382
|
for (const existing of this.tools.values()) {
|
|
6254
7383
|
if (existing.name === tool.name && existing.serverName !== server.name) {
|
|
6255
|
-
|
|
7384
|
+
this.onWarn(
|
|
6256
7385
|
`[mcp] Tool "${tool.name}" is exposed by both "${existing.serverName}" and "${server.name}". Cascade disambiguates internally via mcp::<server>::<tool>.`
|
|
6257
7386
|
);
|
|
6258
7387
|
break;
|
|
@@ -6332,6 +7461,19 @@ var PermissionEscalator = class extends EventEmitter__default.default {
|
|
|
6332
7461
|
t1Evaluator;
|
|
6333
7462
|
/** Pending user-decision resolvers keyed by request ID */
|
|
6334
7463
|
pendingUserDecisions = /* @__PURE__ */ new Map();
|
|
7464
|
+
/** ms to wait for a user approval decision before denying for safety. */
|
|
7465
|
+
approvalTimeoutMs;
|
|
7466
|
+
/** Autonomous mode (autonomy: 'auto'): non-dangerous tools auto-approve. */
|
|
7467
|
+
autonomous;
|
|
7468
|
+
constructor(approvalTimeoutMs = 6e5, autonomous = false) {
|
|
7469
|
+
super();
|
|
7470
|
+
this.approvalTimeoutMs = approvalTimeoutMs;
|
|
7471
|
+
this.autonomous = autonomous;
|
|
7472
|
+
}
|
|
7473
|
+
/** Toggle autonomous auto-approval at runtime (e.g. from /auto). */
|
|
7474
|
+
setAutonomous(on) {
|
|
7475
|
+
this.autonomous = on;
|
|
7476
|
+
}
|
|
6335
7477
|
setT2Evaluator(evaluator) {
|
|
6336
7478
|
this.t2Evaluator = evaluator;
|
|
6337
7479
|
}
|
|
@@ -6344,7 +7486,7 @@ var PermissionEscalator = class extends EventEmitter__default.default {
|
|
|
6344
7486
|
*/
|
|
6345
7487
|
async requestPermission(req) {
|
|
6346
7488
|
const cacheKey = `${req.parentT2Id}:${req.toolName}`;
|
|
6347
|
-
if (this.sessionCache.has(cacheKey)) {
|
|
7489
|
+
if (!req.forceReprompt && this.sessionCache.has(cacheKey)) {
|
|
6348
7490
|
return {
|
|
6349
7491
|
requestId: req.id,
|
|
6350
7492
|
approved: this.sessionCache.get(cacheKey),
|
|
@@ -6364,6 +7506,15 @@ var PermissionEscalator = class extends EventEmitter__default.default {
|
|
|
6364
7506
|
this.sessionCache.set(cacheKey, true);
|
|
6365
7507
|
return decision;
|
|
6366
7508
|
}
|
|
7509
|
+
if (this.autonomous && !req.isDangerous) {
|
|
7510
|
+
return {
|
|
7511
|
+
requestId: req.id,
|
|
7512
|
+
approved: true,
|
|
7513
|
+
always: false,
|
|
7514
|
+
decidedBy: "T1",
|
|
7515
|
+
reasoning: "Autonomous mode \u2014 non-dangerous tool auto-approved"
|
|
7516
|
+
};
|
|
7517
|
+
}
|
|
6367
7518
|
if (this.t2Evaluator) {
|
|
6368
7519
|
try {
|
|
6369
7520
|
const t2Decision = await this.t2Evaluator(req);
|
|
@@ -6404,13 +7555,28 @@ var PermissionEscalator = class extends EventEmitter__default.default {
|
|
|
6404
7555
|
}
|
|
6405
7556
|
waitForUserDecision(req) {
|
|
6406
7557
|
return new Promise((resolve) => {
|
|
7558
|
+
let timer;
|
|
6407
7559
|
const wrappedResolver = (decision) => {
|
|
7560
|
+
if (timer) clearTimeout(timer);
|
|
6408
7561
|
if (decision.always) {
|
|
6409
7562
|
this.sessionCache.set(`${req.parentT2Id}:${req.toolName}`, decision.approved);
|
|
6410
7563
|
}
|
|
6411
7564
|
resolve(decision);
|
|
6412
7565
|
};
|
|
6413
7566
|
this.pendingUserDecisions.set(req.id, wrappedResolver);
|
|
7567
|
+
if (this.approvalTimeoutMs > 0 && Number.isFinite(this.approvalTimeoutMs)) {
|
|
7568
|
+
timer = setTimeout(() => {
|
|
7569
|
+
if (this.pendingUserDecisions.delete(req.id)) {
|
|
7570
|
+
resolve({
|
|
7571
|
+
requestId: req.id,
|
|
7572
|
+
approved: false,
|
|
7573
|
+
decidedBy: "USER",
|
|
7574
|
+
reasoning: `Approval timed out after ${this.approvalTimeoutMs}ms \u2014 denied for safety`
|
|
7575
|
+
});
|
|
7576
|
+
}
|
|
7577
|
+
}, this.approvalTimeoutMs);
|
|
7578
|
+
timer.unref?.();
|
|
7579
|
+
}
|
|
6414
7580
|
this.emit("permission:user-required", req);
|
|
6415
7581
|
});
|
|
6416
7582
|
}
|
|
@@ -6428,11 +7594,14 @@ var PermissionEscalator = class extends EventEmitter__default.default {
|
|
|
6428
7594
|
};
|
|
6429
7595
|
var ProviderConfigSchema = zod.z.object({
|
|
6430
7596
|
type: zod.z.enum(["anthropic", "openai", "gemini", "azure", "openai-compatible", "ollama"]),
|
|
7597
|
+
label: zod.z.string().optional(),
|
|
6431
7598
|
apiKey: zod.z.string().optional(),
|
|
6432
7599
|
baseUrl: zod.z.string().url().optional(),
|
|
6433
7600
|
deploymentName: zod.z.string().optional(),
|
|
6434
7601
|
apiVersion: zod.z.string().optional(),
|
|
6435
|
-
model: zod.z.string().optional()
|
|
7602
|
+
model: zod.z.string().optional(),
|
|
7603
|
+
authToken: zod.z.string().optional(),
|
|
7604
|
+
credentialSource: zod.z.string().optional()
|
|
6436
7605
|
});
|
|
6437
7606
|
var ModelOverridesSchema = zod.z.object({
|
|
6438
7607
|
t1: zod.z.string().optional(),
|
|
@@ -6462,10 +7631,12 @@ var ToolsConfigSchema = zod.z.object({
|
|
|
6462
7631
|
requireApprovalFor: zod.z.array(zod.z.string()).default([]),
|
|
6463
7632
|
browserEnabled: zod.z.boolean().default(false),
|
|
6464
7633
|
mcpServers: zod.z.array(McpServerConfigSchema).optional(),
|
|
7634
|
+
mcpTrusted: zod.z.array(zod.z.string()).optional(),
|
|
6465
7635
|
/** Web search backends — at least one should be configured for best results */
|
|
6466
7636
|
webSearch: WebSearchConfigSchema.optional()
|
|
6467
7637
|
});
|
|
6468
7638
|
var HookDefinitionSchema = zod.z.object({
|
|
7639
|
+
name: zod.z.string().optional(),
|
|
6469
7640
|
command: zod.z.string(),
|
|
6470
7641
|
tools: zod.z.array(zod.z.string()).optional(),
|
|
6471
7642
|
timeout: zod.z.number().optional()
|
|
@@ -6478,6 +7649,13 @@ var HooksConfigSchema = zod.z.object({
|
|
|
6478
7649
|
});
|
|
6479
7650
|
var DashboardConfigSchema = zod.z.object({
|
|
6480
7651
|
port: zod.z.number().default(4891),
|
|
7652
|
+
/**
|
|
7653
|
+
* Interface to bind the dashboard HTTP/WebSocket server to. Defaults to
|
|
7654
|
+
* loopback so the dashboard — which exposes /api/run (arbitrary task
|
|
7655
|
+
* execution) and config mutation — is never reachable from the network
|
|
7656
|
+
* unless the operator explicitly opts in (e.g. "0.0.0.0" for team mode).
|
|
7657
|
+
*/
|
|
7658
|
+
host: zod.z.string().default("127.0.0.1"),
|
|
6481
7659
|
auth: zod.z.boolean().default(true),
|
|
6482
7660
|
teamMode: zod.z.enum(["single", "multi"]).default("single"),
|
|
6483
7661
|
secret: zod.z.string().optional()
|
|
@@ -6500,6 +7678,15 @@ var TierLimitsSchema = zod.z.object({
|
|
|
6500
7678
|
var BudgetConfigSchema = zod.z.object({
|
|
6501
7679
|
dailyBudgetUsd: zod.z.number().optional(),
|
|
6502
7680
|
sessionBudgetUsd: zod.z.number().optional(),
|
|
7681
|
+
/**
|
|
7682
|
+
* Hard per-task token ceiling. A single `cascade run` is stopped once its
|
|
7683
|
+
* combined token usage crosses this, so a mis-routed trivial task can never
|
|
7684
|
+
* fan out into a runaway multi-agent burn. Resets every run. Raise it for
|
|
7685
|
+
* genuinely large jobs. Defaults to 200k.
|
|
7686
|
+
*/
|
|
7687
|
+
maxTokensPerRun: zod.z.number().int().positive().default(2e5),
|
|
7688
|
+
/** Optional hard per-task cost ceiling (USD). Unset = only the token cap applies. */
|
|
7689
|
+
maxCostPerRunUsd: zod.z.number().positive().optional(),
|
|
6503
7690
|
warnAtPct: zod.z.number().default(80)
|
|
6504
7691
|
});
|
|
6505
7692
|
var WorkspaceConfigSchema = zod.z.object({
|
|
@@ -6528,6 +7715,32 @@ var CascadeConfigSchema = zod.z.object({
|
|
|
6528
7715
|
* Heuristic-first with AI inference fallback (adds ~0–500ms per task).
|
|
6529
7716
|
*/
|
|
6530
7717
|
cascadeAuto: zod.z.boolean().default(false),
|
|
7718
|
+
/**
|
|
7719
|
+
* Cascade Auto trade-off bias when picking a model for a task:
|
|
7720
|
+
* - 'balanced' (default): quality × cost-efficiency — cheap models win
|
|
7721
|
+
* trivial tasks, strong models win hard ones.
|
|
7722
|
+
* - 'quality': pick the highest-benchmark model; cost only breaks ties.
|
|
7723
|
+
* - 'cost': pick the cheapest model that clears a per-task quality floor.
|
|
7724
|
+
*/
|
|
7725
|
+
autoBias: zod.z.enum(["balanced", "quality", "cost"]).default("balanced"),
|
|
7726
|
+
/**
|
|
7727
|
+
* Public-benchmark data source for Cascade Auto. All fields have safe
|
|
7728
|
+
* defaults so zero config "just works" — live data is fetched in the
|
|
7729
|
+
* background and the bundled snapshot is used until it arrives (or offline).
|
|
7730
|
+
*/
|
|
7731
|
+
benchmarks: zod.z.object({
|
|
7732
|
+
/** Fetch current quality scores from a public source. Default: true. */
|
|
7733
|
+
live: zod.z.boolean().default(true),
|
|
7734
|
+
/** How long a fetched snapshot stays fresh before re-fetching (hours). */
|
|
7735
|
+
refreshHours: zod.z.number().min(0).default(24),
|
|
7736
|
+
/**
|
|
7737
|
+
* Override the quality-benchmark source URL (must return the snapshot
|
|
7738
|
+
* JSON shape). When unset, the maintained GitHub-raw snapshot is used.
|
|
7739
|
+
*/
|
|
7740
|
+
sourceUrl: zod.z.string().url().optional(),
|
|
7741
|
+
/** Fetch current per-token prices from OpenRouter (free, no key). */
|
|
7742
|
+
pricingLive: zod.z.boolean().default(true)
|
|
7743
|
+
}).default({}),
|
|
6531
7744
|
/**
|
|
6532
7745
|
* Runtime Tool Creation: when true, T3 workers can generate and register new tools
|
|
6533
7746
|
* at runtime via the ToolCreator when no existing tool can handle a required operation.
|
|
@@ -6535,6 +7748,13 @@ var CascadeConfigSchema = zod.z.object({
|
|
|
6535
7748
|
* HTTP calls from generated tools require approval.
|
|
6536
7749
|
*/
|
|
6537
7750
|
enableToolCreation: zod.z.boolean().default(true),
|
|
7751
|
+
/**
|
|
7752
|
+
* Persist runtime-generated tools to .cascade/dynamic-tools.json and reload them
|
|
7753
|
+
* on startup for cross-run dedup. Reloaded (and peer-received) tools are always
|
|
7754
|
+
* treated as UNTRUSTED — their dangerous actions re-escalate. Set false to disable
|
|
7755
|
+
* persistence entirely.
|
|
7756
|
+
*/
|
|
7757
|
+
persistDynamicTools: zod.z.boolean().default(true),
|
|
6538
7758
|
/**
|
|
6539
7759
|
* External plugin paths or npm package names to load at startup.
|
|
6540
7760
|
* Each entry must export a default ToolPlugin object.
|
|
@@ -6551,7 +7771,89 @@ var CascadeConfigSchema = zod.z.object({
|
|
|
6551
7771
|
* Timeout in milliseconds for a single local model inference call.
|
|
6552
7772
|
* Local models can take minutes for large parameter counts. Default: 5 minutes.
|
|
6553
7773
|
*/
|
|
6554
|
-
localInferenceTimeoutMs: zod.z.number().int().min(1e3).default(3e5)
|
|
7774
|
+
localInferenceTimeoutMs: zod.z.number().int().min(1e3).default(3e5),
|
|
7775
|
+
/**
|
|
7776
|
+
* Timeout (ms) for a single cloud LLM call (streaming or not). Guards against
|
|
7777
|
+
* a stalled provider stream hanging the whole run with no output. On timeout
|
|
7778
|
+
* the call errors and the worker escalates. Default: 2 minutes.
|
|
7779
|
+
*/
|
|
7780
|
+
cloudInferenceTimeoutMs: zod.z.number().int().min(1e3).default(12e4),
|
|
7781
|
+
/**
|
|
7782
|
+
* Timeout (ms) for a tool-approval decision. If no decision arrives in time the
|
|
7783
|
+
* request is DENIED (never auto-approved) so the run continues rather than
|
|
7784
|
+
* hanging on an unanswered prompt. Default: 10 minutes.
|
|
7785
|
+
*/
|
|
7786
|
+
approvalTimeoutMs: zod.z.number().int().min(1e3).default(6e5),
|
|
7787
|
+
/**
|
|
7788
|
+
* Boardroom plan approval: pause after the plan is produced so the user can
|
|
7789
|
+
* review the org chart (sections, workers, estimated cost) before any worker
|
|
7790
|
+
* spawns. Scope:
|
|
7791
|
+
* 'never' — never pause (default; no behavior change).
|
|
7792
|
+
* 'complex' — pause Complex runs only ('always' is kept as an alias).
|
|
7793
|
+
* 'all' — pause Moderate and Complex runs.
|
|
7794
|
+
* Headless/SDK consumers without a listener auto-approve, so pausing is safe
|
|
7795
|
+
* outside the TUI.
|
|
7796
|
+
*/
|
|
7797
|
+
planApproval: zod.z.enum(["never", "complex", "all", "always"]).default("never"),
|
|
7798
|
+
/**
|
|
7799
|
+
* Plan-review behaviour for the boardroom gate:
|
|
7800
|
+
* autoReviewer — a reviewer model critiques the plan (gaps/risks/cost)
|
|
7801
|
+
* before you see it, and the critique is shown in the dialog.
|
|
7802
|
+
* editable — allow editing the plan (drop sections) in the dialog.
|
|
7803
|
+
* maxRevisionRounds — how many steering-note → re-plan → re-ask rounds the
|
|
7804
|
+
* boardroom allows before proceeding with the last plan.
|
|
7805
|
+
*/
|
|
7806
|
+
planReview: zod.z.object({
|
|
7807
|
+
autoReviewer: zod.z.boolean().default(false),
|
|
7808
|
+
editable: zod.z.boolean().default(true),
|
|
7809
|
+
maxRevisionRounds: zod.z.number().int().min(1).max(20).default(5)
|
|
7810
|
+
}).default({}),
|
|
7811
|
+
/**
|
|
7812
|
+
* Autonomy level. 'manual' (default): plan + tool approvals prompt as usual.
|
|
7813
|
+
* 'auto': hands-off — the plan gate auto-approves and the escalator
|
|
7814
|
+
* auto-approves NON-dangerous tools, while dangerous tools still escalate and
|
|
7815
|
+
* budget caps remain the hard stop. Toggle at runtime with /auto.
|
|
7816
|
+
*/
|
|
7817
|
+
autonomy: zod.z.enum(["manual", "auto"]).default("manual"),
|
|
7818
|
+
/**
|
|
7819
|
+
* Max corrective re-plan passes T1's reviewer runs before returning the best
|
|
7820
|
+
* partial result. The run also stops early when a pass makes no net progress.
|
|
7821
|
+
*/
|
|
7822
|
+
maxReplanPasses: zod.z.number().int().min(0).max(10).default(2),
|
|
7823
|
+
/**
|
|
7824
|
+
* Reflection / self-critique. When enabled, after a worker's pass/fail self-test
|
|
7825
|
+
* succeeds it runs a goal-alignment critique and revises once if the output is
|
|
7826
|
+
* weak against the broader goal (not just the subtask spec). Off by default — it
|
|
7827
|
+
* adds an LLM call per worker.
|
|
7828
|
+
*/
|
|
7829
|
+
reflection: zod.z.object({
|
|
7830
|
+
enabled: zod.z.boolean().default(false),
|
|
7831
|
+
maxRounds: zod.z.number().int().min(1).max(3).default(1)
|
|
7832
|
+
}).default({}),
|
|
7833
|
+
/**
|
|
7834
|
+
* T3 worker execution within a dependency wave:
|
|
7835
|
+
* 'auto' (default) — sequential when the T3 tier is a LOCAL model (a single
|
|
7836
|
+
* GPU serializes anyway, so parallel just thrashes the queue), parallel for
|
|
7837
|
+
* cloud models.
|
|
7838
|
+
* 'parallel' / 'sequential' — force it.
|
|
7839
|
+
*/
|
|
7840
|
+
t3Execution: zod.z.enum(["auto", "parallel", "sequential"]).default("auto"),
|
|
7841
|
+
/**
|
|
7842
|
+
* T3→T2 reinforcement: when enabled, a worker that discovers its subtask should
|
|
7843
|
+
* fan out can call the `request_workers` tool to have its T2 manager spawn
|
|
7844
|
+
* sibling workers for the new pieces (no 4th tier; bounded). Off by default.
|
|
7845
|
+
*/
|
|
7846
|
+
reinforcements: zod.z.object({
|
|
7847
|
+
enabled: zod.z.boolean().default(false),
|
|
7848
|
+
maxPerSection: zod.z.number().int().min(1).max(20).default(4)
|
|
7849
|
+
}).default({}),
|
|
7850
|
+
/**
|
|
7851
|
+
* Render the TUI in the terminal's alternate screen buffer (like vim).
|
|
7852
|
+
* Flicker-proof and restores the shell on exit, but native scrollback is
|
|
7853
|
+
* unavailable — history scrolls in-app with PgUp/PgDn. Also enabled per
|
|
7854
|
+
* session with the --alt-screen flag. Default: off.
|
|
7855
|
+
*/
|
|
7856
|
+
altScreen: zod.z.boolean().default(false)
|
|
6555
7857
|
});
|
|
6556
7858
|
|
|
6557
7859
|
// src/config/validate.ts
|
|
@@ -6689,14 +7991,20 @@ var TASK_TYPE_TAGS = {
|
|
|
6689
7991
|
};
|
|
6690
7992
|
var TaskAnalyzer = class {
|
|
6691
7993
|
tracker;
|
|
7994
|
+
bias;
|
|
6692
7995
|
lastProfile = null;
|
|
6693
7996
|
lastSelectedModels = /* @__PURE__ */ new Map();
|
|
6694
|
-
constructor(tracker) {
|
|
7997
|
+
constructor(tracker, bias = "balanced") {
|
|
6695
7998
|
this.tracker = tracker;
|
|
7999
|
+
this.bias = bias;
|
|
6696
8000
|
}
|
|
6697
8001
|
setTracker(tracker) {
|
|
6698
8002
|
this.tracker = tracker;
|
|
6699
8003
|
}
|
|
8004
|
+
/** Change the cost/quality bias at runtime (e.g. when config reloads). */
|
|
8005
|
+
setBias(bias) {
|
|
8006
|
+
this.bias = bias;
|
|
8007
|
+
}
|
|
6700
8008
|
/** Returns the TaskProfile from the most recent analyze() call — used for outcome recording. */
|
|
6701
8009
|
getLastProfile() {
|
|
6702
8010
|
return this.lastProfile;
|
|
@@ -6756,7 +8064,16 @@ var TaskAnalyzer = class {
|
|
|
6756
8064
|
const perf = this.tracker?.performanceScore(model.id, profile.type) ?? 0.5;
|
|
6757
8065
|
const costEff = this.costEfficiency(model, profile.complexity);
|
|
6758
8066
|
const match = this.taskMatchScore(model, profile);
|
|
6759
|
-
|
|
8067
|
+
const benchmark = 0.3 + 0.7 * benchmarkScore01(model, profile.type);
|
|
8068
|
+
switch (this.bias) {
|
|
8069
|
+
case "quality":
|
|
8070
|
+
return perf * match * benchmark ** 2 * (0.85 + 0.15 * costEff);
|
|
8071
|
+
case "cost":
|
|
8072
|
+
return perf * match * costEff ** 1.5 * Math.sqrt(benchmark);
|
|
8073
|
+
case "balanced":
|
|
8074
|
+
default:
|
|
8075
|
+
return perf * costEff * match * benchmark;
|
|
8076
|
+
}
|
|
6760
8077
|
}
|
|
6761
8078
|
costEfficiency(model, complexity) {
|
|
6762
8079
|
if (this.tracker) return this.tracker.costEfficiencyScore(model, complexity);
|
|
@@ -6776,7 +8093,7 @@ var TaskAnalyzer = class {
|
|
|
6776
8093
|
analysisCache.clear();
|
|
6777
8094
|
}
|
|
6778
8095
|
};
|
|
6779
|
-
var DEFAULT_STATS_FILE =
|
|
8096
|
+
var DEFAULT_STATS_FILE = path18__default.default.join(os4__default.default.homedir(), ".cascade", "model-perf.json");
|
|
6780
8097
|
var ModelPerformanceTracker = class {
|
|
6781
8098
|
stats = /* @__PURE__ */ new Map();
|
|
6782
8099
|
statsFile;
|
|
@@ -6788,7 +8105,7 @@ var ModelPerformanceTracker = class {
|
|
|
6788
8105
|
if (this.loaded) return;
|
|
6789
8106
|
this.loaded = true;
|
|
6790
8107
|
try {
|
|
6791
|
-
const raw = await
|
|
8108
|
+
const raw = await fs4__default.default.readFile(this.statsFile, "utf-8");
|
|
6792
8109
|
const parsed = JSON.parse(raw);
|
|
6793
8110
|
for (const [key, stat] of Object.entries(parsed)) {
|
|
6794
8111
|
this.stats.set(key, stat);
|
|
@@ -6798,10 +8115,10 @@ var ModelPerformanceTracker = class {
|
|
|
6798
8115
|
}
|
|
6799
8116
|
async save() {
|
|
6800
8117
|
try {
|
|
6801
|
-
await
|
|
8118
|
+
await fs4__default.default.mkdir(path18__default.default.dirname(this.statsFile), { recursive: true });
|
|
6802
8119
|
const obj = {};
|
|
6803
8120
|
for (const [key, stat] of this.stats) obj[key] = stat;
|
|
6804
|
-
await
|
|
8121
|
+
await fs4__default.default.writeFile(this.statsFile, JSON.stringify(obj, null, 2), "utf-8");
|
|
6805
8122
|
} catch {
|
|
6806
8123
|
}
|
|
6807
8124
|
}
|
|
@@ -6849,6 +8166,96 @@ var ModelPerformanceTracker = class {
|
|
|
6849
8166
|
return Math.max(0.1, 1 - normalised * complexityWeight);
|
|
6850
8167
|
}
|
|
6851
8168
|
};
|
|
8169
|
+
var DYNAMIC_TOOLS_FILE = "dynamic-tools.json";
|
|
8170
|
+
function normalizeToolSchema(schema) {
|
|
8171
|
+
if (schema && schema["type"] === "object" && typeof schema["properties"] === "object") {
|
|
8172
|
+
return schema;
|
|
8173
|
+
}
|
|
8174
|
+
const properties = schema && typeof schema === "object" ? schema : {};
|
|
8175
|
+
return {
|
|
8176
|
+
type: "object",
|
|
8177
|
+
properties,
|
|
8178
|
+
required: Object.keys(properties)
|
|
8179
|
+
};
|
|
8180
|
+
}
|
|
8181
|
+
function capabilityKey(text) {
|
|
8182
|
+
return Array.from(
|
|
8183
|
+
new Set((text.toLowerCase().match(/[a-z0-9]+/g) ?? []).filter((w) => w.length > 2))
|
|
8184
|
+
).sort().join(" ");
|
|
8185
|
+
}
|
|
8186
|
+
var DYNAMIC_TOOL_TIMEOUT_MS = 15e3;
|
|
8187
|
+
var DYNAMIC_FETCH_MAX = 1e6;
|
|
8188
|
+
var HARNESS_SRC = `
|
|
8189
|
+
const { parentPort, workerData } = require('node:worker_threads');
|
|
8190
|
+
const { executeCode, input } = workerData;
|
|
8191
|
+
let nextId = 0;
|
|
8192
|
+
const pending = new Map();
|
|
8193
|
+
function bridge(kind, payload) {
|
|
8194
|
+
return new Promise((resolve, reject) => {
|
|
8195
|
+
const id = nextId++;
|
|
8196
|
+
pending.set(id, { resolve, reject });
|
|
8197
|
+
parentPort.postMessage(Object.assign({ kind, id }, payload));
|
|
8198
|
+
});
|
|
8199
|
+
}
|
|
8200
|
+
parentPort.on('message', (msg) => {
|
|
8201
|
+
const p = pending.get(msg.id);
|
|
8202
|
+
if (!p) return;
|
|
8203
|
+
pending.delete(msg.id);
|
|
8204
|
+
if (msg.error !== undefined) p.reject(new Error(msg.error));
|
|
8205
|
+
else p.resolve(msg.value);
|
|
8206
|
+
});
|
|
8207
|
+
const callTool = (name, toolInput) => bridge('callTool', { name: name, input: toolInput });
|
|
8208
|
+
const fetch = async (url, init) => {
|
|
8209
|
+
const safeInit = init && typeof init === 'object'
|
|
8210
|
+
? { method: init.method, headers: init.headers, body: typeof init.body === 'string' ? init.body : undefined }
|
|
8211
|
+
: undefined;
|
|
8212
|
+
const r = await bridge('fetch', { url: url, init: safeInit });
|
|
8213
|
+
return {
|
|
8214
|
+
ok: r.ok, status: r.status, statusText: r.statusText,
|
|
8215
|
+
headers: { get: (k) => (String(k).toLowerCase() === 'content-type' ? r.contentType : null) },
|
|
8216
|
+
text: async () => r.body,
|
|
8217
|
+
json: async () => JSON.parse(r.body),
|
|
8218
|
+
};
|
|
8219
|
+
};
|
|
8220
|
+
(async () => {
|
|
8221
|
+
const AsyncFunction = Object.getPrototypeOf(async function () {}).constructor;
|
|
8222
|
+
const fn = new AsyncFunction('input', 'callTool', 'fetch', 'console', executeCode);
|
|
8223
|
+
return await fn(input, callTool, fetch, { log() {}, error() {} });
|
|
8224
|
+
})()
|
|
8225
|
+
.then((r) => parentPort.postMessage({ kind: 'result', value: String(r == null ? '' : r) }))
|
|
8226
|
+
.catch((e) => parentPort.postMessage({ kind: 'result', value: 'Tool error: ' + (e && e.message ? e.message : String(e)) }));
|
|
8227
|
+
`;
|
|
8228
|
+
function isExecutableToolCode(code) {
|
|
8229
|
+
try {
|
|
8230
|
+
const AsyncFunction = Object.getPrototypeOf(async function() {
|
|
8231
|
+
}).constructor;
|
|
8232
|
+
new AsyncFunction("input", "callTool", "fetch", "console", code);
|
|
8233
|
+
return true;
|
|
8234
|
+
} catch {
|
|
8235
|
+
return false;
|
|
8236
|
+
}
|
|
8237
|
+
}
|
|
8238
|
+
async function bridgeFetch(url, init) {
|
|
8239
|
+
try {
|
|
8240
|
+
const i = init && typeof init === "object" ? init : {};
|
|
8241
|
+
const resp = await safeFetch(url, {
|
|
8242
|
+
method: typeof i["method"] === "string" ? i["method"] : void 0,
|
|
8243
|
+
headers: i["headers"],
|
|
8244
|
+
body: typeof i["body"] === "string" ? i["body"] : void 0
|
|
8245
|
+
});
|
|
8246
|
+
const contentType = resp.headers.get("content-type") ?? "";
|
|
8247
|
+
let body = "";
|
|
8248
|
+
try {
|
|
8249
|
+
body = await resp.text();
|
|
8250
|
+
} catch {
|
|
8251
|
+
body = "";
|
|
8252
|
+
}
|
|
8253
|
+
if (body.length > DYNAMIC_FETCH_MAX) body = body.slice(0, DYNAMIC_FETCH_MAX);
|
|
8254
|
+
return { ok: resp.ok, status: resp.status, statusText: resp.statusText, contentType, body };
|
|
8255
|
+
} catch (err) {
|
|
8256
|
+
return { __error: err instanceof Error ? err.message : String(err) };
|
|
8257
|
+
}
|
|
8258
|
+
}
|
|
6852
8259
|
var DynamicTool = class extends BaseTool {
|
|
6853
8260
|
name;
|
|
6854
8261
|
description;
|
|
@@ -6856,8 +8263,12 @@ var DynamicTool = class extends BaseTool {
|
|
|
6856
8263
|
executeCode;
|
|
6857
8264
|
_isDangerous;
|
|
6858
8265
|
registry;
|
|
6859
|
-
escalator
|
|
6860
|
-
|
|
8266
|
+
/** Resolve the CURRENT escalator at call time — covers tools registered before
|
|
8267
|
+
* the per-run escalator was wired (persisted at init, received from a peer). */
|
|
8268
|
+
getEscalator;
|
|
8269
|
+
/** Untrusted = loaded from disk / a peer; its dangerous calls always re-prompt. */
|
|
8270
|
+
trusted;
|
|
8271
|
+
constructor(spec, registry, getEscalator, trusted) {
|
|
6861
8272
|
super();
|
|
6862
8273
|
this.name = spec.name;
|
|
6863
8274
|
this.description = spec.description;
|
|
@@ -6865,32 +8276,35 @@ var DynamicTool = class extends BaseTool {
|
|
|
6865
8276
|
this.executeCode = spec.executeCode;
|
|
6866
8277
|
this._isDangerous = spec.isDangerous;
|
|
6867
8278
|
this.registry = registry;
|
|
6868
|
-
this.
|
|
8279
|
+
this.getEscalator = getEscalator;
|
|
8280
|
+
this.trusted = trusted;
|
|
6869
8281
|
}
|
|
6870
8282
|
isDangerous() {
|
|
6871
8283
|
return this._isDangerous;
|
|
6872
8284
|
}
|
|
6873
8285
|
async execute(input, options) {
|
|
6874
8286
|
const registry = this.registry;
|
|
6875
|
-
const escalator = this.escalator;
|
|
6876
8287
|
const callTool = async (toolName, toolInput) => {
|
|
6877
8288
|
if (!registry.hasTool(toolName)) return `Tool not found: ${toolName}`;
|
|
6878
8289
|
if (registry.isDangerous(toolName)) {
|
|
6879
|
-
|
|
6880
|
-
|
|
6881
|
-
|
|
6882
|
-
|
|
6883
|
-
|
|
6884
|
-
|
|
6885
|
-
|
|
6886
|
-
|
|
6887
|
-
|
|
6888
|
-
|
|
6889
|
-
|
|
6890
|
-
|
|
6891
|
-
|
|
6892
|
-
|
|
6893
|
-
|
|
8290
|
+
const escalator = this.getEscalator();
|
|
8291
|
+
if (!escalator) {
|
|
8292
|
+
return `Permission denied for "${toolName}": dynamic tool "${this.name}" has no approver available (default-deny).`;
|
|
8293
|
+
}
|
|
8294
|
+
const req = {
|
|
8295
|
+
id: `dynamic-${this.name}-${toolName}-${Date.now()}`,
|
|
8296
|
+
requestedBy: `dynamic_tool:${this.name}`,
|
|
8297
|
+
parentT2Id: options.tierId,
|
|
8298
|
+
toolName,
|
|
8299
|
+
input: toolInput,
|
|
8300
|
+
isDangerous: true,
|
|
8301
|
+
subtaskContext: `Dynamic tool "${this.name}" (${this.trusted ? "trusted" : "UNTRUSTED"}) requesting access to "${toolName}"`,
|
|
8302
|
+
sectionContext: `Dynamic tool "${this.name}"`,
|
|
8303
|
+
forceReprompt: !this.trusted
|
|
8304
|
+
};
|
|
8305
|
+
const decision = await escalator.requestPermission(req);
|
|
8306
|
+
if (!decision.approved) {
|
|
8307
|
+
return `Permission denied for ${toolName} (decided by ${decision.decidedBy}).`;
|
|
6894
8308
|
}
|
|
6895
8309
|
}
|
|
6896
8310
|
try {
|
|
@@ -6900,41 +8314,52 @@ var DynamicTool = class extends BaseTool {
|
|
|
6900
8314
|
return `Error calling ${toolName}: ${err instanceof Error ? err.message : String(err)}`;
|
|
6901
8315
|
}
|
|
6902
8316
|
};
|
|
6903
|
-
|
|
6904
|
-
|
|
6905
|
-
|
|
6906
|
-
|
|
6907
|
-
|
|
6908
|
-
|
|
6909
|
-
|
|
6910
|
-
|
|
6911
|
-
|
|
6912
|
-
|
|
6913
|
-
|
|
6914
|
-
clearTimeout,
|
|
6915
|
-
Promise,
|
|
6916
|
-
Error,
|
|
6917
|
-
String,
|
|
6918
|
-
Number,
|
|
6919
|
-
Boolean,
|
|
6920
|
-
Array,
|
|
6921
|
-
Object,
|
|
6922
|
-
result: void 0
|
|
6923
|
-
};
|
|
6924
|
-
const context = vm.createContext(sandbox);
|
|
6925
|
-
const wrapped = `(async () => { ${this.executeCode} })().then(r => { result = String(r ?? ''); }).catch(e => { result = 'Tool error: ' + e.message; });`;
|
|
6926
|
-
try {
|
|
6927
|
-
const promise = vm.runInContext(wrapped, context, {
|
|
6928
|
-
timeout: 15e3,
|
|
6929
|
-
breakOnSigint: true,
|
|
6930
|
-
filename: `dynamic_tool_${this.name}.js`,
|
|
6931
|
-
displayErrors: true
|
|
8317
|
+
return this.runInWorker(input, callTool);
|
|
8318
|
+
}
|
|
8319
|
+
/** Spawn the worker, service its callTool/fetch bridge, enforce the kill timeout. */
|
|
8320
|
+
runInWorker(input, callTool) {
|
|
8321
|
+
const timeoutMs = Math.max(200, Number(process.env["CASCADE_DYNAMIC_TOOL_TIMEOUT_MS"]) || DYNAMIC_TOOL_TIMEOUT_MS);
|
|
8322
|
+
return new Promise((resolve) => {
|
|
8323
|
+
let settled = false;
|
|
8324
|
+
const worker = new worker_threads.Worker(HARNESS_SRC, {
|
|
8325
|
+
eval: true,
|
|
8326
|
+
workerData: { executeCode: this.executeCode, input },
|
|
8327
|
+
resourceLimits: { maxOldGenerationSizeMb: 128 }
|
|
6932
8328
|
});
|
|
6933
|
-
|
|
6934
|
-
|
|
6935
|
-
|
|
6936
|
-
|
|
6937
|
-
|
|
8329
|
+
const finish = (value) => {
|
|
8330
|
+
if (settled) return;
|
|
8331
|
+
settled = true;
|
|
8332
|
+
clearTimeout(timer);
|
|
8333
|
+
void worker.terminate();
|
|
8334
|
+
resolve(value);
|
|
8335
|
+
};
|
|
8336
|
+
const timer = setTimeout(
|
|
8337
|
+
() => finish(`Dynamic tool "${this.name}" timed out after ${timeoutMs}ms and was terminated.`),
|
|
8338
|
+
timeoutMs
|
|
8339
|
+
);
|
|
8340
|
+
timer.unref?.();
|
|
8341
|
+
worker.on("message", (msg) => {
|
|
8342
|
+
if (msg?.kind === "result") {
|
|
8343
|
+
finish(typeof msg.value === "string" ? msg.value : String(msg.value ?? ""));
|
|
8344
|
+
} else if (msg?.kind === "callTool") {
|
|
8345
|
+
void (async () => {
|
|
8346
|
+
const value = await callTool(String(msg.name), msg.input ?? {});
|
|
8347
|
+
if (!settled) worker.postMessage({ id: msg.id, value });
|
|
8348
|
+
})();
|
|
8349
|
+
} else if (msg?.kind === "fetch") {
|
|
8350
|
+
void (async () => {
|
|
8351
|
+
const r = await bridgeFetch(String(msg.url), msg.init);
|
|
8352
|
+
if (settled) return;
|
|
8353
|
+
if ("__error" in r) worker.postMessage({ id: msg.id, error: r.__error });
|
|
8354
|
+
else worker.postMessage({ id: msg.id, value: r });
|
|
8355
|
+
})();
|
|
8356
|
+
}
|
|
8357
|
+
});
|
|
8358
|
+
worker.on("error", (err) => finish(`Dynamic tool error: ${err instanceof Error ? err.message : String(err)}`));
|
|
8359
|
+
worker.on("exit", (code) => {
|
|
8360
|
+
if (code !== 0) finish(`Dynamic tool "${this.name}" exited unexpectedly (code ${code}).`);
|
|
8361
|
+
});
|
|
8362
|
+
});
|
|
6938
8363
|
}
|
|
6939
8364
|
};
|
|
6940
8365
|
var TOOL_CREATOR_PROMPT = `You are a tool-generation assistant for the Cascade AI system.
|
|
@@ -6967,52 +8392,153 @@ var ToolCreator = class {
|
|
|
6967
8392
|
router;
|
|
6968
8393
|
registry;
|
|
6969
8394
|
escalator;
|
|
6970
|
-
|
|
6971
|
-
|
|
8395
|
+
workspacePath;
|
|
8396
|
+
/** When false, persisted tools are neither loaded nor written. */
|
|
8397
|
+
persistEnabled;
|
|
8398
|
+
logger;
|
|
8399
|
+
/** name → spec, for persistence, broadcast, and re-registration. */
|
|
8400
|
+
specs = /* @__PURE__ */ new Map();
|
|
8401
|
+
/** capability fingerprint → tool name, so the same need isn't re-generated. */
|
|
8402
|
+
capabilityIndex = /* @__PURE__ */ new Map();
|
|
8403
|
+
constructor(router, registry, workspacePath, persistEnabled = true) {
|
|
6972
8404
|
this.router = router;
|
|
6973
8405
|
this.registry = registry;
|
|
8406
|
+
this.workspacePath = workspacePath;
|
|
8407
|
+
this.persistEnabled = persistEnabled;
|
|
6974
8408
|
}
|
|
6975
8409
|
setPermissionEscalator(escalator) {
|
|
6976
8410
|
this.escalator = escalator;
|
|
6977
8411
|
}
|
|
8412
|
+
/** Route diagnostics through the host (Cascade) so they survive the Ink TUI. */
|
|
8413
|
+
setLogger(fn) {
|
|
8414
|
+
this.logger = fn;
|
|
8415
|
+
}
|
|
8416
|
+
/** Returns the stored spec for a created tool (for peer broadcast). */
|
|
8417
|
+
getSpec(name) {
|
|
8418
|
+
return this.specs.get(name);
|
|
8419
|
+
}
|
|
8420
|
+
log(msg) {
|
|
8421
|
+
if (this.logger) this.logger(msg);
|
|
8422
|
+
}
|
|
6978
8423
|
/**
|
|
6979
8424
|
* Generate a new tool from a description and register it with the ToolRegistry.
|
|
6980
|
-
*
|
|
6981
|
-
*
|
|
8425
|
+
* Returns the tool name on success, or null on failure (with a logged reason —
|
|
8426
|
+
* failures are no longer swallowed silently). Reuses an existing tool when the
|
|
8427
|
+
* same capability has already been created (dedup) so peers/runs don't
|
|
8428
|
+
* regenerate identical tools.
|
|
6982
8429
|
*/
|
|
6983
8430
|
async createTool(description, context) {
|
|
8431
|
+
const key = capabilityKey(`${description} ${context}`);
|
|
8432
|
+
const existing = this.capabilityIndex.get(key);
|
|
8433
|
+
if (existing && this.registry.hasTool(existing)) {
|
|
8434
|
+
this.log(`[tool-creator] Reusing existing tool "${existing}" for: ${description.slice(0, 80)}`);
|
|
8435
|
+
return existing;
|
|
8436
|
+
}
|
|
6984
8437
|
const prompt = `${TOOL_CREATOR_PROMPT}
|
|
6985
8438
|
|
|
6986
8439
|
Task context: ${context.slice(0, 200)}
|
|
6987
8440
|
Required capability: ${description.slice(0, 300)}`;
|
|
8441
|
+
let spec = null;
|
|
8442
|
+
for (let attempt = 1; attempt <= 2 && !spec; attempt++) {
|
|
8443
|
+
try {
|
|
8444
|
+
const result = await this.router.generate("T3", {
|
|
8445
|
+
messages: [{ role: "user", content: prompt }],
|
|
8446
|
+
maxTokens: 800
|
|
8447
|
+
});
|
|
8448
|
+
const jsonMatch = /\{[\s\S]*\}/.exec(result.content);
|
|
8449
|
+
if (!jsonMatch) {
|
|
8450
|
+
this.log(`[tool-creator] Attempt ${attempt}: model returned no JSON object.`);
|
|
8451
|
+
continue;
|
|
8452
|
+
}
|
|
8453
|
+
const parsed = JSON.parse(jsonMatch[0]);
|
|
8454
|
+
if (!parsed.name || !parsed.description || !parsed.executeCode || !parsed.inputSchema) {
|
|
8455
|
+
this.log(`[tool-creator] Attempt ${attempt}: spec missing required fields (name/description/executeCode/inputSchema).`);
|
|
8456
|
+
continue;
|
|
8457
|
+
}
|
|
8458
|
+
spec = parsed;
|
|
8459
|
+
} catch (err) {
|
|
8460
|
+
this.log(`[tool-creator] Attempt ${attempt} failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
8461
|
+
}
|
|
8462
|
+
}
|
|
8463
|
+
if (!spec) {
|
|
8464
|
+
this.log(`[tool-creator] Could not generate a tool for: ${description.slice(0, 80)}`);
|
|
8465
|
+
return null;
|
|
8466
|
+
}
|
|
8467
|
+
spec.inputSchema = normalizeToolSchema(spec.inputSchema);
|
|
8468
|
+
if (this.specs.has(spec.name) || this.registry.hasTool(spec.name)) {
|
|
8469
|
+
spec.name = `${spec.name}_${Date.now() % 1e4}`;
|
|
8470
|
+
}
|
|
8471
|
+
if (!isExecutableToolCode(spec.executeCode)) {
|
|
8472
|
+
this.log(`[tool-creator] Generated code for "${spec.name}" has a syntax error \u2014 discarded.`);
|
|
8473
|
+
return null;
|
|
8474
|
+
}
|
|
8475
|
+
this.registerSpec(spec, true);
|
|
8476
|
+
this.capabilityIndex.set(key, spec.name);
|
|
8477
|
+
this.log(`[tool-creator] Created tool "${spec.name}".`);
|
|
8478
|
+
void this.persist();
|
|
8479
|
+
return spec.name;
|
|
8480
|
+
}
|
|
8481
|
+
/**
|
|
8482
|
+
* Register a spec (from createTool, disk, or a peer) into the registry.
|
|
8483
|
+
* Idempotent — a name already present is skipped. `trusted` is set by the
|
|
8484
|
+
* caller and never inherited from disk: createTool passes true; persisted and
|
|
8485
|
+
* peer-broadcast specs pass false, so their dangerous actions always re-escalate.
|
|
8486
|
+
* The DynamicTool resolves the escalator lazily (`() => this.escalator`) so a
|
|
8487
|
+
* later setPermissionEscalator covers tools registered before the run wired it.
|
|
8488
|
+
*/
|
|
8489
|
+
registerSpec(spec, trusted = false) {
|
|
8490
|
+
spec.trusted = trusted;
|
|
8491
|
+
if (this.registry.hasTool(spec.name)) {
|
|
8492
|
+
this.specs.set(spec.name, spec);
|
|
8493
|
+
return;
|
|
8494
|
+
}
|
|
8495
|
+
const tool = new DynamicTool(spec, this.registry, () => this.escalator, trusted);
|
|
8496
|
+
this.registry.register(tool);
|
|
8497
|
+
this.specs.set(spec.name, spec);
|
|
8498
|
+
this.capabilityIndex.set(capabilityKey(`${spec.description}`), spec.name);
|
|
8499
|
+
}
|
|
8500
|
+
/** Load tools persisted by previous runs and register them — as UNTRUSTED, and
|
|
8501
|
+
* only after re-validating each spec (its source could have been tampered with
|
|
8502
|
+
* or authored during a prior prompt-injected run). Untrusted tools re-escalate
|
|
8503
|
+
* any dangerous action, so a silently-reloaded tool can't act without approval. */
|
|
8504
|
+
async loadPersistedTools() {
|
|
8505
|
+
if (!this.workspacePath || !this.persistEnabled) return;
|
|
8506
|
+
const file = path18__default.default.join(this.workspacePath, ".cascade", DYNAMIC_TOOLS_FILE);
|
|
6988
8507
|
try {
|
|
6989
|
-
const
|
|
6990
|
-
|
|
6991
|
-
|
|
6992
|
-
|
|
6993
|
-
|
|
6994
|
-
|
|
6995
|
-
|
|
6996
|
-
|
|
6997
|
-
|
|
6998
|
-
|
|
8508
|
+
const raw = await fs4__default.default.readFile(file, "utf-8");
|
|
8509
|
+
const specs = JSON.parse(raw);
|
|
8510
|
+
if (!Array.isArray(specs)) return;
|
|
8511
|
+
let loaded = 0;
|
|
8512
|
+
let skipped = 0;
|
|
8513
|
+
for (const spec of specs) {
|
|
8514
|
+
if (!(spec?.name && spec.description && spec.executeCode && spec.inputSchema) || !isExecutableToolCode(spec.executeCode)) {
|
|
8515
|
+
skipped++;
|
|
8516
|
+
continue;
|
|
8517
|
+
}
|
|
8518
|
+
spec.inputSchema = normalizeToolSchema(spec.inputSchema);
|
|
8519
|
+
this.registerSpec(spec, false);
|
|
8520
|
+
loaded++;
|
|
6999
8521
|
}
|
|
7000
|
-
|
|
7001
|
-
|
|
7002
|
-
} catch {
|
|
7003
|
-
return null;
|
|
8522
|
+
if (loaded || skipped) {
|
|
8523
|
+
this.log(`[tool-creator] Loaded ${loaded} persisted tool(s) as untrusted${skipped ? `, skipped ${skipped} invalid` : ""}.`);
|
|
7004
8524
|
}
|
|
7005
|
-
const tool = new DynamicTool(spec, this.registry, this.escalator);
|
|
7006
|
-
this.registry.register(tool);
|
|
7007
|
-
this.createdTools.add(spec.name);
|
|
7008
|
-
return spec.name;
|
|
7009
8525
|
} catch {
|
|
7010
|
-
|
|
8526
|
+
}
|
|
8527
|
+
}
|
|
8528
|
+
async persist() {
|
|
8529
|
+
if (!this.workspacePath || !this.persistEnabled) return;
|
|
8530
|
+
const dir = path18__default.default.join(this.workspacePath, ".cascade");
|
|
8531
|
+
const file = path18__default.default.join(dir, DYNAMIC_TOOLS_FILE);
|
|
8532
|
+
try {
|
|
8533
|
+
await fs4__default.default.mkdir(dir, { recursive: true });
|
|
8534
|
+
await fs4__default.default.writeFile(file, JSON.stringify(Array.from(this.specs.values()), null, 2), "utf-8");
|
|
8535
|
+
} catch (err) {
|
|
8536
|
+
this.log(`[tool-creator] Failed to persist tools: ${err instanceof Error ? err.message : String(err)}`);
|
|
7011
8537
|
}
|
|
7012
8538
|
}
|
|
7013
8539
|
/** Returns the names of all tools created in this session. */
|
|
7014
8540
|
getCreatedTools() {
|
|
7015
|
-
return Array.from(this.
|
|
8541
|
+
return Array.from(this.specs.keys());
|
|
7016
8542
|
}
|
|
7017
8543
|
};
|
|
7018
8544
|
|
|
@@ -7022,7 +8548,11 @@ var Cascade = class _Cascade extends EventEmitter__default.default {
|
|
|
7022
8548
|
toolRegistry;
|
|
7023
8549
|
mcpClient;
|
|
7024
8550
|
config;
|
|
8551
|
+
/** Orchestration decisions for the CURRENT run — cleared on each run(). */
|
|
8552
|
+
decisionLog = [];
|
|
7025
8553
|
initialized = false;
|
|
8554
|
+
/** Last task that stopped at the budget cap — powers /continue (resumeRun). */
|
|
8555
|
+
lastInterruptedRun;
|
|
7026
8556
|
initPromise;
|
|
7027
8557
|
store;
|
|
7028
8558
|
audit;
|
|
@@ -7030,15 +8560,23 @@ var Cascade = class _Cascade extends EventEmitter__default.default {
|
|
|
7030
8560
|
taskAnalyzer;
|
|
7031
8561
|
perfTracker;
|
|
7032
8562
|
toolCreator;
|
|
8563
|
+
workspacePath;
|
|
7033
8564
|
constructor(config, workspacePath, store) {
|
|
7034
8565
|
super();
|
|
7035
8566
|
this.config = validateConfig(config);
|
|
8567
|
+
this.workspacePath = workspacePath;
|
|
7036
8568
|
this.store = store;
|
|
7037
8569
|
this.router = new CascadeRouter();
|
|
7038
8570
|
this.mcpClient = new McpClient({
|
|
7039
8571
|
trustedServers: this.config.tools.mcpTrusted,
|
|
7040
8572
|
approvalCallback: async (server) => {
|
|
7041
8573
|
return await this.requestMcpApproval(server);
|
|
8574
|
+
},
|
|
8575
|
+
// Route warnings through the event stream when anyone is listening —
|
|
8576
|
+
// a raw console write while the TUI is live corrupts Ink's frame.
|
|
8577
|
+
onWarn: (message) => {
|
|
8578
|
+
if (this.listenerCount("log") > 0) this.emit("log", { level: "warn", message });
|
|
8579
|
+
else console.warn(message);
|
|
7042
8580
|
}
|
|
7043
8581
|
});
|
|
7044
8582
|
this.toolRegistry = new ToolRegistry(this.config.tools, workspacePath);
|
|
@@ -7048,11 +8586,15 @@ var Cascade = class _Cascade extends EventEmitter__default.default {
|
|
|
7048
8586
|
if (this.config.cascadeAuto === true) {
|
|
7049
8587
|
this.perfTracker = new ModelPerformanceTracker();
|
|
7050
8588
|
void this.perfTracker.load();
|
|
7051
|
-
this.taskAnalyzer = new TaskAnalyzer(this.perfTracker);
|
|
8589
|
+
this.taskAnalyzer = new TaskAnalyzer(this.perfTracker, this.config.autoBias ?? "balanced");
|
|
8590
|
+
this.router.setTaskAnalyzer(this.taskAnalyzer);
|
|
7052
8591
|
}
|
|
7053
8592
|
const cfg = this.config;
|
|
7054
8593
|
if (cfg["enableToolCreation"] === true) {
|
|
7055
|
-
this.toolCreator = new ToolCreator(this.router, this.toolRegistry);
|
|
8594
|
+
this.toolCreator = new ToolCreator(this.router, this.toolRegistry, this.workspacePath, cfg["persistDynamicTools"] !== false);
|
|
8595
|
+
this.toolCreator.setLogger((m) => {
|
|
8596
|
+
if (this.listenerCount("log") > 0) this.emit("log", { level: "info", message: m });
|
|
8597
|
+
});
|
|
7056
8598
|
}
|
|
7057
8599
|
}
|
|
7058
8600
|
setStore(store) {
|
|
@@ -7083,6 +8625,17 @@ var Cascade = class _Cascade extends EventEmitter__default.default {
|
|
|
7083
8625
|
this.emit("mcp:approval-required", { server });
|
|
7084
8626
|
});
|
|
7085
8627
|
}
|
|
8628
|
+
recordDecision(kind, detail) {
|
|
8629
|
+
this.decisionLog.push({ at: (/* @__PURE__ */ new Date()).toISOString(), kind, detail });
|
|
8630
|
+
}
|
|
8631
|
+
/**
|
|
8632
|
+
* The orchestration decision trail for the most recent run: complexity
|
|
8633
|
+
* verdict (and why), which model served each tier, failovers, and
|
|
8634
|
+
* escalations. Powers the /why command.
|
|
8635
|
+
*/
|
|
8636
|
+
getDecisionLog() {
|
|
8637
|
+
return [...this.decisionLog];
|
|
8638
|
+
}
|
|
7086
8639
|
/** Resolve a pending MCP server approval from a REPL / dashboard listener. */
|
|
7087
8640
|
resolveMcpApproval(serverName, approved) {
|
|
7088
8641
|
const resolver = this.pendingMcpApprovals.get(serverName);
|
|
@@ -7091,6 +8644,125 @@ var Cascade = class _Cascade extends EventEmitter__default.default {
|
|
|
7091
8644
|
resolver(approved);
|
|
7092
8645
|
}
|
|
7093
8646
|
}
|
|
8647
|
+
// ── Boardroom plan approval ─────────────────────────────────────────
|
|
8648
|
+
// Same gate pattern as MCP approvals, with the opposite default: plans
|
|
8649
|
+
// are work the user asked for, so no listener (SDK/headless) or a
|
|
8650
|
+
// timeout means PROCEED, not reject.
|
|
8651
|
+
pendingPlanApproval;
|
|
8652
|
+
async requestPlanApproval(plan, taskId, critique, summary) {
|
|
8653
|
+
if (this.config.autonomy === "auto") {
|
|
8654
|
+
return { approved: true };
|
|
8655
|
+
}
|
|
8656
|
+
if (this.listenerCount("plan:approval-required") === 0) {
|
|
8657
|
+
return { approved: true };
|
|
8658
|
+
}
|
|
8659
|
+
const t2Count = plan.sections.length;
|
|
8660
|
+
const t3Count = plan.sections.reduce((sum, s) => sum + (s.t3Subtasks?.length ?? 0), 0);
|
|
8661
|
+
return await new Promise((resolve) => {
|
|
8662
|
+
const timeout = setTimeout(() => {
|
|
8663
|
+
if (this.pendingPlanApproval) {
|
|
8664
|
+
this.pendingPlanApproval = void 0;
|
|
8665
|
+
resolve({ approved: true });
|
|
8666
|
+
}
|
|
8667
|
+
}, 12e4);
|
|
8668
|
+
this.pendingPlanApproval = (decision) => {
|
|
8669
|
+
clearTimeout(timeout);
|
|
8670
|
+
this.pendingPlanApproval = void 0;
|
|
8671
|
+
resolve(decision);
|
|
8672
|
+
};
|
|
8673
|
+
this.emit("plan:approval-required", {
|
|
8674
|
+
taskId,
|
|
8675
|
+
plan,
|
|
8676
|
+
t2Count,
|
|
8677
|
+
t3Count,
|
|
8678
|
+
estCostUsd: this.estimatePlanCost(plan),
|
|
8679
|
+
critique,
|
|
8680
|
+
summary
|
|
8681
|
+
});
|
|
8682
|
+
});
|
|
8683
|
+
}
|
|
8684
|
+
/**
|
|
8685
|
+
* Resolve a pending boardroom plan approval from a REPL / dashboard listener.
|
|
8686
|
+
* An optional `note` re-plans and re-asks; an optional `editedPlan` is applied
|
|
8687
|
+
* directly (no re-decompose).
|
|
8688
|
+
*/
|
|
8689
|
+
resolvePlanApproval(approved, note, editedPlan) {
|
|
8690
|
+
this.pendingPlanApproval?.({ approved, note, editedPlan });
|
|
8691
|
+
}
|
|
8692
|
+
/**
|
|
8693
|
+
* Autonomy control (used by the /auto command). 'auto' makes the next run
|
|
8694
|
+
* hands-off: the plan gate auto-approves and non-dangerous tools auto-approve,
|
|
8695
|
+
* while dangerous tools still escalate and budget caps remain the hard stop.
|
|
8696
|
+
*/
|
|
8697
|
+
setAutonomy(mode) {
|
|
8698
|
+
this.config = { ...this.config, autonomy: mode };
|
|
8699
|
+
}
|
|
8700
|
+
getAutonomy() {
|
|
8701
|
+
return this.config.autonomy === "auto" ? "auto" : "manual";
|
|
8702
|
+
}
|
|
8703
|
+
/**
|
|
8704
|
+
* Preview T1's decomposition for a prompt WITHOUT executing it (powers /plan).
|
|
8705
|
+
* Idempotent init guard, so it works before the first run.
|
|
8706
|
+
*/
|
|
8707
|
+
async previewPlan(prompt) {
|
|
8708
|
+
await this.init();
|
|
8709
|
+
const t1 = new T1Administrator(this.router, this.toolRegistry, this.config);
|
|
8710
|
+
if (this.store) t1.setStore(this.store);
|
|
8711
|
+
return t1.previewPlan(prompt);
|
|
8712
|
+
}
|
|
8713
|
+
/** True when a task stopped at the budget cap and can be resumed via /continue. */
|
|
8714
|
+
hasResumableRun() {
|
|
8715
|
+
return this.lastInterruptedRun != null;
|
|
8716
|
+
}
|
|
8717
|
+
/**
|
|
8718
|
+
* Raise the per-run token budget for a resume and return the continuation
|
|
8719
|
+
* prompt (or null when nothing is resumable). Consumes the interrupted-run
|
|
8720
|
+
* state. The REPL submits the returned prompt through its normal flow so the
|
|
8721
|
+
* resumed run renders like any other; `resumeRun` wraps this for SDK callers.
|
|
8722
|
+
*/
|
|
8723
|
+
prepareResume(opts = {}) {
|
|
8724
|
+
const last = this.lastInterruptedRun;
|
|
8725
|
+
if (!last) return null;
|
|
8726
|
+
this.lastInterruptedRun = void 0;
|
|
8727
|
+
const raised = opts.maxTokens ?? Math.round((this.config.budget?.maxTokensPerRun ?? 2e5) * 2);
|
|
8728
|
+
this.config = { ...this.config, budget: { ...this.config.budget, maxTokensPerRun: raised } };
|
|
8729
|
+
this.router.setMaxTokensPerRun(raised);
|
|
8730
|
+
return `Continue and FINISH this task. A previous attempt was interrupted before completion; any files already created are on disk \u2014 build on them, do NOT recreate them. Complete only the remaining work.
|
|
8731
|
+
|
|
8732
|
+
Original task: ${last.prompt}` + (last.partialOutput ? `
|
|
8733
|
+
|
|
8734
|
+
Partial result so far:
|
|
8735
|
+
${last.partialOutput}` : "");
|
|
8736
|
+
}
|
|
8737
|
+
/**
|
|
8738
|
+
* Resume the last budget-capped task with a raised budget (SDK/headless).
|
|
8739
|
+
* Returns null when there is nothing to resume.
|
|
8740
|
+
*/
|
|
8741
|
+
async resumeRun(opts = {}) {
|
|
8742
|
+
const prompt = this.prepareResume(opts);
|
|
8743
|
+
if (!prompt) return null;
|
|
8744
|
+
return this.run({ prompt });
|
|
8745
|
+
}
|
|
8746
|
+
/**
|
|
8747
|
+
* Rough pre-execution cost estimate for a plan: ~3 T2 calls per section
|
|
8748
|
+
* plus ~4 T3 calls per subtask at typical token volumes. A ballpark for
|
|
8749
|
+
* the approval dialog, not an invoice — always label it "est."
|
|
8750
|
+
*/
|
|
8751
|
+
estimatePlanCost(plan) {
|
|
8752
|
+
const T2_CALLS_PER_SECTION = 3;
|
|
8753
|
+
const T3_CALLS_PER_SUBTASK = 4;
|
|
8754
|
+
const IN_TOKENS = 1500;
|
|
8755
|
+
const OUT_TOKENS = 700;
|
|
8756
|
+
const t2Model = this.router.getTierModel("T2");
|
|
8757
|
+
const t3Model = this.router.getTierModel("T3");
|
|
8758
|
+
let est = 0;
|
|
8759
|
+
for (const section of plan.sections) {
|
|
8760
|
+
if (t2Model) est += T2_CALLS_PER_SECTION * calculateCost(IN_TOKENS, OUT_TOKENS, t2Model);
|
|
8761
|
+
const subtasks = section.t3Subtasks?.length ?? 1;
|
|
8762
|
+
if (t3Model) est += subtasks * T3_CALLS_PER_SUBTASK * calculateCost(IN_TOKENS, OUT_TOKENS, t3Model);
|
|
8763
|
+
}
|
|
8764
|
+
return est;
|
|
8765
|
+
}
|
|
7094
8766
|
async init() {
|
|
7095
8767
|
if (this.initialized) return;
|
|
7096
8768
|
if (this.initPromise) return this.initPromise;
|
|
@@ -7099,6 +8771,9 @@ var Cascade = class _Cascade extends EventEmitter__default.default {
|
|
|
7099
8771
|
this.router.on("budget:warning", (payload) => {
|
|
7100
8772
|
this.emit("budget:warning", payload);
|
|
7101
8773
|
});
|
|
8774
|
+
this.router.on("failover", (e) => {
|
|
8775
|
+
this.recordDecision("failover", `${e.tier} ${e.from} \u2192 ${e.to} (${e.reason})`);
|
|
8776
|
+
});
|
|
7102
8777
|
this.router.on("budget:exceeded", (payload) => {
|
|
7103
8778
|
this.emit("budget:exceeded", payload);
|
|
7104
8779
|
for (const [name, resolver] of this.pendingMcpApprovals) {
|
|
@@ -7136,7 +8811,12 @@ var Cascade = class _Cascade extends EventEmitter__default.default {
|
|
|
7136
8811
|
this.router.profileModels(this.store).catch(() => {
|
|
7137
8812
|
});
|
|
7138
8813
|
}
|
|
8814
|
+
if (this.config.cascadeAuto) {
|
|
8815
|
+
this.router.refreshLiveData().catch(() => {
|
|
8816
|
+
});
|
|
8817
|
+
}
|
|
7139
8818
|
this.initOptionalFeatures();
|
|
8819
|
+
if (this.toolCreator) await this.toolCreator.loadPersistedTools();
|
|
7140
8820
|
this.initialized = true;
|
|
7141
8821
|
})();
|
|
7142
8822
|
try {
|
|
@@ -7162,6 +8842,20 @@ var Cascade = class _Cascade extends EventEmitter__default.default {
|
|
|
7162
8842
|
const wordCount = prompt.trim().split(/\s+/).length;
|
|
7163
8843
|
return wordCount <= 12 && LOW_COMPLEXITY.some((re) => re.test(prompt.trim()));
|
|
7164
8844
|
}
|
|
8845
|
+
/**
|
|
8846
|
+
* Read-only inquiries about existing content ("read / review / explain /
|
|
8847
|
+
* summarize / analyze this file or codebase and tell me …") are single-agent
|
|
8848
|
+
* work — one worker with file/grep tools answers directly, no T1→T2→T3 fan-out.
|
|
8849
|
+
* They must NOT ask to create, build, implement, refactor, or save an artifact;
|
|
8850
|
+
* those stay on the heavier classifier path. This keeps trivial "what does this
|
|
8851
|
+
* do?" requests from being mis-routed into a multi-agent, multi-thousand-token run.
|
|
8852
|
+
*/
|
|
8853
|
+
looksLikeReadOnlyInquiry(prompt) {
|
|
8854
|
+
const p = prompt.trim();
|
|
8855
|
+
const inquiry = /\b(?:read|review|explain|describe|summari[sz]e|analy[sz]e|assess|evaluate|inspect|examine|explore|go through|look at|tell me about|what (?:is|are|does|do)|is it|understand|novelty|novel idea)\b/i.test(p);
|
|
8856
|
+
const producesArtifact = /\b(?:create|build|implement|generate|write|refactor|rewrite|add|fix|deploy|install|migrate|scaffold|set up|save (?:a|the)|report|\.(?:pdf|md|txt|json|csv|py|js|ts|tsx|jsx|html|docx?))\b/i.test(p);
|
|
8857
|
+
return inquiry && !producesArtifact;
|
|
8858
|
+
}
|
|
7165
8859
|
// Cache glob scan results per workspace path to avoid repeated I/O.
|
|
7166
8860
|
static globCache = /* @__PURE__ */ new Map();
|
|
7167
8861
|
async countWorkspaceFiles(workspacePath) {
|
|
@@ -7181,9 +8875,22 @@ var Cascade = class _Cascade extends EventEmitter__default.default {
|
|
|
7181
8875
|
}
|
|
7182
8876
|
}
|
|
7183
8877
|
async determineComplexity(prompt, workspacePath, conversationHistory = []) {
|
|
7184
|
-
if (this.isCasualGreeting(prompt))
|
|
7185
|
-
|
|
7186
|
-
|
|
8878
|
+
if (this.isCasualGreeting(prompt)) {
|
|
8879
|
+
this.recordDecision("complexity", "Simple \u2014 heuristic: casual greeting (no classifier call)");
|
|
8880
|
+
return "Simple";
|
|
8881
|
+
}
|
|
8882
|
+
if (this.looksLikeSimpleArtifactTask(prompt)) {
|
|
8883
|
+
this.recordDecision("complexity", "Simple \u2014 heuristic: single-file artifact task (no classifier call)");
|
|
8884
|
+
return "Simple";
|
|
8885
|
+
}
|
|
8886
|
+
if (this.looksLikeConversational(prompt)) {
|
|
8887
|
+
this.recordDecision("complexity", "Simple \u2014 heuristic: short conversational message (no classifier call)");
|
|
8888
|
+
return "Simple";
|
|
8889
|
+
}
|
|
8890
|
+
if (this.looksLikeReadOnlyInquiry(prompt)) {
|
|
8891
|
+
this.recordDecision("complexity", "Simple \u2014 heuristic: read-only inquiry over existing content (single agent, no classifier call)");
|
|
8892
|
+
return "Simple";
|
|
8893
|
+
}
|
|
7187
8894
|
let workspaceContext = "";
|
|
7188
8895
|
try {
|
|
7189
8896
|
const count = await this.countWorkspaceFiles(workspacePath);
|
|
@@ -7203,10 +8910,12 @@ Classification:
|
|
|
7203
8910
|
Important rules:
|
|
7204
8911
|
- Treat short follow-ups like "proceed", "continue", "do it", "yes" as referring to the recent context.
|
|
7205
8912
|
- If the earlier context is complex, keep the inherited complexity unless the user clearly narrows scope.
|
|
8913
|
+
- Reading, explaining, summarizing, or analyzing existing files/code and answering a question \u2014 WITHOUT creating files or implementing changes \u2014 is "Simple" (single agent), never "Complex".
|
|
7206
8914
|
- If the task asks for a simple single-file artifact like hello.txt, it is usually Moderate.
|
|
7207
8915
|
- If the task asks for a saved report, PDF, implementation, or deeper verification workflow, it is at least Moderate and often Complex.
|
|
7208
8916
|
|
|
7209
|
-
Respond with
|
|
8917
|
+
Respond with the verdict word first, then a dash and a short reason (under 12 words).
|
|
8918
|
+
Format: <Simple|Moderate|Complex> \u2014 <reason>`;
|
|
7210
8919
|
const recentHistory = conversationHistory.slice(-6);
|
|
7211
8920
|
const contextBlock = recentHistory.map((message, index) => {
|
|
7212
8921
|
const content = typeof message.content === "string" ? message.content : message.content.map((block) => block.type === "text" ? block.text : "[non-text]").join(" ");
|
|
@@ -7221,26 +8930,36 @@ ${prompt}` : prompt;
|
|
|
7221
8930
|
const result = await this.router.generate("T1", {
|
|
7222
8931
|
messages: [{ role: "user", content: routedPrompt }],
|
|
7223
8932
|
systemPrompt: sysPrompt,
|
|
7224
|
-
maxTokens:
|
|
8933
|
+
maxTokens: 40,
|
|
7225
8934
|
temperature: 0
|
|
7226
8935
|
});
|
|
7227
|
-
const content = result.content.trim()
|
|
7228
|
-
|
|
7229
|
-
|
|
7230
|
-
|
|
8936
|
+
const content = result.content.trim();
|
|
8937
|
+
const firstWord = (content.split(/[\s—–-]+/)[0] ?? "").toLowerCase();
|
|
8938
|
+
const reason = content.replace(/^\S+\s*[—–-]*\s*/, "").trim();
|
|
8939
|
+
const verdict = firstWord.includes("simple") ? "Simple" : firstWord.includes("moderate") ? "Moderate" : "Complex";
|
|
8940
|
+
this.recordDecision("complexity", `${verdict} \u2014 classifier: ${reason || "no reason given"}`);
|
|
8941
|
+
return verdict;
|
|
7231
8942
|
} catch {
|
|
7232
8943
|
const followUpPrompt = /^(proceed|continue|go ahead|do it|yes|yep|ok|okay|carry on)$/i.test(prompt.trim());
|
|
7233
|
-
if (followUpPrompt && recentHistory.length > 0)
|
|
7234
|
-
|
|
8944
|
+
if (followUpPrompt && recentHistory.length > 0) {
|
|
8945
|
+
this.recordDecision("complexity", "Complex \u2014 classifier unavailable; short follow-up inherits prior context");
|
|
8946
|
+
return "Complex";
|
|
8947
|
+
}
|
|
8948
|
+
this.recordDecision("complexity", "Moderate \u2014 classifier unavailable; defaulting to the mid-cost route");
|
|
8949
|
+
return "Moderate";
|
|
7235
8950
|
}
|
|
7236
8951
|
}
|
|
7237
8952
|
async run(options) {
|
|
7238
8953
|
await this.init();
|
|
8954
|
+
this.router.beginRun();
|
|
8955
|
+
this.router.setRunSignal(options.signal);
|
|
7239
8956
|
const startMs = Date.now();
|
|
7240
8957
|
const taskId = crypto.randomUUID();
|
|
7241
|
-
|
|
8958
|
+
this.decisionLog = [];
|
|
8959
|
+
const escalator = new PermissionEscalator(this.config.approvalTimeoutMs ?? 6e5, this.config.autonomy === "auto");
|
|
7242
8960
|
escalator.on("permission:user-required", async (req) => {
|
|
7243
8961
|
this.emit("permission:user-required", req);
|
|
8962
|
+
this.recordDecision("escalation", `"${req.toolName}" by ${req.requestedBy} \u2014 T2 and T1 both unsure, escalated to user`);
|
|
7244
8963
|
const enrichedRequest = {
|
|
7245
8964
|
id: req.id,
|
|
7246
8965
|
tierId: req.requestedBy,
|
|
@@ -7277,16 +8996,32 @@ ${prompt}` : prompt;
|
|
|
7277
8996
|
toolCreationEnabled: this.config["enableToolCreation"] === true
|
|
7278
8997
|
});
|
|
7279
8998
|
this.emit("tier:root", { role: complexity === "Simple" ? "T3" : complexity === "Moderate" ? "T2" : "T1" });
|
|
8999
|
+
const tiersInPlay = complexity === "Simple" ? ["T3"] : complexity === "Moderate" ? ["T2", "T3"] : ["T1", "T2", "T3"];
|
|
7280
9000
|
if (this.taskAnalyzer) {
|
|
7281
|
-
|
|
7282
|
-
|
|
9001
|
+
await Promise.all(tiersInPlay.map(async (tier) => {
|
|
9002
|
+
const tierKey = tier.toLowerCase();
|
|
9003
|
+
if (this.config.models?.[tierKey]) return;
|
|
7283
9004
|
try {
|
|
7284
9005
|
const model = await this.taskAnalyzer.selectModel(options.prompt, tier, this.router.getSelector());
|
|
7285
|
-
if (model)
|
|
9006
|
+
if (model) {
|
|
9007
|
+
this.router.overrideTierModel(tier, model);
|
|
9008
|
+
const taskType = this.taskAnalyzer.getLastProfile()?.type ?? "mixed";
|
|
9009
|
+
const bench = Math.round(benchmarkScore01(model, taskType) * 100);
|
|
9010
|
+
const price = model.inputCostPer1kTokens === 0 && model.outputCostPer1kTokens === 0 ? "free" : `$${model.outputCostPer1kTokens.toFixed(4)}/1K out`;
|
|
9011
|
+
const dataSrc = this.router.getLiveData()?.getDataSource() ?? "bundled";
|
|
9012
|
+
this.recordDecision(
|
|
9013
|
+
"model",
|
|
9014
|
+
`${tier} \u2192 ${model.provider}:${model.id} \u2014 Cascade Auto: best value for ${taskType} (bench ${bench}/100, ${price}, data: ${dataSrc})`
|
|
9015
|
+
);
|
|
9016
|
+
}
|
|
7286
9017
|
} catch {
|
|
7287
9018
|
}
|
|
7288
9019
|
}));
|
|
7289
9020
|
}
|
|
9021
|
+
this.recordDecision("model", tiersInPlay.map((tier) => {
|
|
9022
|
+
const m = this.router.getTierModel(tier);
|
|
9023
|
+
return m ? `${tier} ${m.provider}:${m.id}${m.isLocal ? " \u2302local" : ""}` : `${tier} (none)`;
|
|
9024
|
+
}).join(" \xB7 "));
|
|
7290
9025
|
const toolCreator = this.toolCreator;
|
|
7291
9026
|
if (toolCreator) toolCreator.setPermissionEscalator(escalator);
|
|
7292
9027
|
let finalOutput = "";
|
|
@@ -7368,6 +9103,25 @@ ${prompt}` : prompt;
|
|
|
7368
9103
|
if (toolCreator) t2.setToolCreator(toolCreator);
|
|
7369
9104
|
t2.setPeerMessageCallback((e) => this.emit("peer:message", e), options.sessionId ?? "");
|
|
7370
9105
|
bindTierEvents(t2);
|
|
9106
|
+
if (this.config.planApproval === "all") {
|
|
9107
|
+
t2.setPlanApprovalCallback(async (subtasks) => {
|
|
9108
|
+
const pseudoPlan = {
|
|
9109
|
+
complexity: "Moderate",
|
|
9110
|
+
reasoning: "",
|
|
9111
|
+
sections: subtasks.map((st) => ({
|
|
9112
|
+
sectionId: st.subtaskId,
|
|
9113
|
+
sectionTitle: st.subtaskTitle,
|
|
9114
|
+
description: st.description,
|
|
9115
|
+
t3Subtasks: []
|
|
9116
|
+
}))
|
|
9117
|
+
};
|
|
9118
|
+
const n = subtasks.length;
|
|
9119
|
+
const summary = `${n} worker${n !== 1 ? "s" : ""} \xB7 1 root manager \xB7 est. $${this.estimatePlanCost(pseudoPlan).toFixed(4)}`;
|
|
9120
|
+
const decision = await this.requestPlanApproval(pseudoPlan, taskId, void 0, summary);
|
|
9121
|
+
const keepSubtaskIds = decision.editedPlan?.sections?.map((s) => s.sectionId).filter((id) => Boolean(id));
|
|
9122
|
+
return { approved: decision.approved, note: decision.note, keepSubtaskIds };
|
|
9123
|
+
});
|
|
9124
|
+
}
|
|
7371
9125
|
const assignment = {
|
|
7372
9126
|
sectionId: taskId,
|
|
7373
9127
|
sectionTitle: "Direct Task",
|
|
@@ -7399,17 +9153,33 @@ ${prompt}` : prompt;
|
|
|
7399
9153
|
t1.setPeerMessageCallback((e) => this.emit("peer:message", e), options.sessionId ?? "");
|
|
7400
9154
|
bindTierEvents(t1);
|
|
7401
9155
|
t1.on("plan", (e) => this.emit("plan", e));
|
|
9156
|
+
if (this.config.planApproval != null && this.config.planApproval !== "never") {
|
|
9157
|
+
t1.setPlanApprovalCallback(async (plan, meta) => {
|
|
9158
|
+
const decision = await this.requestPlanApproval(plan, taskId, meta?.critique);
|
|
9159
|
+
this.recordDecision("escalation", decision.approved ? `Boardroom: plan approved (${plan.sections.length} sections)${decision.note ? " with a steering note" : ""}${decision.editedPlan ? " (edited)" : ""}` : "Boardroom: plan rejected \u2014 run stopped before any T2 spawned");
|
|
9160
|
+
return decision;
|
|
9161
|
+
});
|
|
9162
|
+
}
|
|
7402
9163
|
const result = await t1.execute(options.prompt, options.images, void 0, options.signal);
|
|
7403
9164
|
finalOutput = result.output;
|
|
7404
9165
|
t2Results = result.t2Results;
|
|
7405
9166
|
}
|
|
7406
9167
|
} catch (err) {
|
|
7407
|
-
if (err instanceof CascadeCancelledError) {
|
|
9168
|
+
if (err instanceof CascadeCancelledError || err instanceof Error && err.name === "AbortError" || options.signal?.aborted) {
|
|
7408
9169
|
this.emit("run:cancelled", {
|
|
9170
|
+
taskId,
|
|
9171
|
+
reason: err instanceof Error ? err.message : "Task cancelled",
|
|
9172
|
+
partialOutput: finalOutput || ""
|
|
9173
|
+
});
|
|
9174
|
+
runError = null;
|
|
9175
|
+
} else if (err instanceof Error && err.name === "BudgetExceededError") {
|
|
9176
|
+
this.emit("run:budget-exceeded", {
|
|
7409
9177
|
taskId,
|
|
7410
9178
|
reason: err.message,
|
|
7411
9179
|
partialOutput: finalOutput || ""
|
|
7412
9180
|
});
|
|
9181
|
+
this.lastInterruptedRun = { prompt: options.prompt, partialOutput: finalOutput || "", taskId };
|
|
9182
|
+
if (!finalOutput) finalOutput = `\u26A0 Stopped to avoid runaway cost: ${err.message}`;
|
|
7413
9183
|
runError = null;
|
|
7414
9184
|
} else {
|
|
7415
9185
|
runError = err;
|
|
@@ -7420,6 +9190,8 @@ ${prompt}` : prompt;
|
|
|
7420
9190
|
escalator.cancelAllPending();
|
|
7421
9191
|
} catch {
|
|
7422
9192
|
}
|
|
9193
|
+
this.router.restoreTierModels();
|
|
9194
|
+
this.router.setRunSignal(void 0);
|
|
7423
9195
|
if (this.taskAnalyzer) {
|
|
7424
9196
|
try {
|
|
7425
9197
|
const stats2 = this.router.getStats();
|
|
@@ -7527,7 +9299,7 @@ var Keystore = class {
|
|
|
7527
9299
|
const creds = await this.keytar.findCredentials(KEYTAR_SERVICE);
|
|
7528
9300
|
this.cache = Object.fromEntries(creds.map((c) => [c.account, c.password]));
|
|
7529
9301
|
this.backend = "keytar";
|
|
7530
|
-
if (password &&
|
|
9302
|
+
if (password && fs17__default.default.existsSync(this.storePath)) {
|
|
7531
9303
|
try {
|
|
7532
9304
|
const fileEntries = this.decryptFile(password);
|
|
7533
9305
|
for (const [k, v] of Object.entries(fileEntries)) {
|
|
@@ -7546,7 +9318,7 @@ var Keystore = class {
|
|
|
7546
9318
|
"Keystore unlock requires a password because the OS keychain (keytar) is not available on this system."
|
|
7547
9319
|
);
|
|
7548
9320
|
}
|
|
7549
|
-
if (!
|
|
9321
|
+
if (!fs17__default.default.existsSync(this.storePath)) {
|
|
7550
9322
|
const salt = crypto__default.default.randomBytes(SALT_LEN);
|
|
7551
9323
|
this.masterKey = this.deriveKey(password, salt);
|
|
7552
9324
|
this.writeWithSalt({}, salt);
|
|
@@ -7560,7 +9332,7 @@ var Keystore = class {
|
|
|
7560
9332
|
}
|
|
7561
9333
|
/** Synchronous legacy unlock kept for AES-only environments. */
|
|
7562
9334
|
unlockSync(password) {
|
|
7563
|
-
if (!
|
|
9335
|
+
if (!fs17__default.default.existsSync(this.storePath)) {
|
|
7564
9336
|
const salt = crypto__default.default.randomBytes(SALT_LEN);
|
|
7565
9337
|
this.masterKey = this.deriveKey(password, salt);
|
|
7566
9338
|
this.writeWithSalt({}, salt);
|
|
@@ -7618,7 +9390,7 @@ var Keystore = class {
|
|
|
7618
9390
|
}
|
|
7619
9391
|
}
|
|
7620
9392
|
decryptFile(password, knownSalt) {
|
|
7621
|
-
if (!
|
|
9393
|
+
if (!fs17__default.default.existsSync(this.storePath)) return {};
|
|
7622
9394
|
try {
|
|
7623
9395
|
const { salt, ciphertext, iv, tag } = this.readRaw();
|
|
7624
9396
|
const useSalt = knownSalt ?? salt;
|
|
@@ -7640,8 +9412,8 @@ var Keystore = class {
|
|
|
7640
9412
|
const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
|
|
7641
9413
|
const tag = cipher.getAuthTag();
|
|
7642
9414
|
const out = Buffer.concat([raw.salt, iv, tag, ciphertext]);
|
|
7643
|
-
|
|
7644
|
-
|
|
9415
|
+
fs17__default.default.mkdirSync(path18__default.default.dirname(this.storePath), { recursive: true });
|
|
9416
|
+
fs17__default.default.writeFileSync(this.storePath, out, { mode: 384 });
|
|
7645
9417
|
}
|
|
7646
9418
|
writeWithSalt(data, salt) {
|
|
7647
9419
|
if (!this.masterKey) throw new Error("writeWithSalt called before masterKey was set");
|
|
@@ -7651,11 +9423,11 @@ var Keystore = class {
|
|
|
7651
9423
|
const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
|
|
7652
9424
|
const tag = cipher.getAuthTag();
|
|
7653
9425
|
const out = Buffer.concat([salt, iv, tag, ciphertext]);
|
|
7654
|
-
|
|
7655
|
-
|
|
9426
|
+
fs17__default.default.mkdirSync(path18__default.default.dirname(this.storePath), { recursive: true });
|
|
9427
|
+
fs17__default.default.writeFileSync(this.storePath, out, { mode: 384 });
|
|
7656
9428
|
}
|
|
7657
9429
|
readRaw() {
|
|
7658
|
-
const buf =
|
|
9430
|
+
const buf = fs17__default.default.readFileSync(this.storePath);
|
|
7659
9431
|
let offset = 0;
|
|
7660
9432
|
const salt = buf.subarray(offset, offset + SALT_LEN);
|
|
7661
9433
|
offset += SALT_LEN;
|
|
@@ -7688,9 +9460,9 @@ var CascadeIgnore = class {
|
|
|
7688
9460
|
]);
|
|
7689
9461
|
}
|
|
7690
9462
|
async load(workspacePath) {
|
|
7691
|
-
const filePath =
|
|
9463
|
+
const filePath = path18__default.default.join(workspacePath, ".cascadeignore");
|
|
7692
9464
|
try {
|
|
7693
|
-
const content = await
|
|
9465
|
+
const content = await fs4__default.default.readFile(filePath, "utf-8");
|
|
7694
9466
|
const lines = content.split("\n").filter((l) => l.trim() && !l.startsWith("#"));
|
|
7695
9467
|
this.ig.add(lines);
|
|
7696
9468
|
this.loaded = true;
|
|
@@ -7699,7 +9471,7 @@ var CascadeIgnore = class {
|
|
|
7699
9471
|
}
|
|
7700
9472
|
isIgnored(filePath, workspacePath) {
|
|
7701
9473
|
try {
|
|
7702
|
-
const relative = workspacePath ?
|
|
9474
|
+
const relative = workspacePath ? path18__default.default.relative(workspacePath, filePath) : filePath;
|
|
7703
9475
|
return this.ig.ignores(relative);
|
|
7704
9476
|
} catch {
|
|
7705
9477
|
return false;
|
|
@@ -7710,9 +9482,9 @@ var CascadeIgnore = class {
|
|
|
7710
9482
|
}
|
|
7711
9483
|
};
|
|
7712
9484
|
async function loadCascadeMd(workspacePath) {
|
|
7713
|
-
const filePath =
|
|
9485
|
+
const filePath = path18__default.default.join(workspacePath, "CASCADE.md");
|
|
7714
9486
|
try {
|
|
7715
|
-
const raw = await
|
|
9487
|
+
const raw = await fs4__default.default.readFile(filePath, "utf-8");
|
|
7716
9488
|
return parseCascadeMd(raw);
|
|
7717
9489
|
} catch {
|
|
7718
9490
|
return null;
|
|
@@ -7741,7 +9513,7 @@ ${raw.trim()}`;
|
|
|
7741
9513
|
var MemoryStore = class _MemoryStore {
|
|
7742
9514
|
db;
|
|
7743
9515
|
constructor(dbPath) {
|
|
7744
|
-
|
|
9516
|
+
fs17__default.default.mkdirSync(path18__default.default.dirname(dbPath), { recursive: true });
|
|
7745
9517
|
try {
|
|
7746
9518
|
this.db = new Database__default.default(dbPath, { timeout: 5e3 });
|
|
7747
9519
|
this.db.pragma("journal_mode = WAL");
|
|
@@ -8499,15 +10271,15 @@ var ConfigManager = class {
|
|
|
8499
10271
|
globalDir;
|
|
8500
10272
|
constructor(workspacePath = process.cwd()) {
|
|
8501
10273
|
this.workspacePath = workspacePath;
|
|
8502
|
-
this.globalDir =
|
|
10274
|
+
this.globalDir = path18__default.default.join(os4__default.default.homedir(), GLOBAL_CONFIG_DIR);
|
|
8503
10275
|
}
|
|
8504
10276
|
async load() {
|
|
8505
10277
|
this.config = await this.loadConfig();
|
|
8506
10278
|
this.ignore = new CascadeIgnore();
|
|
8507
10279
|
await this.ignore.load(this.workspacePath);
|
|
8508
10280
|
this.cascadeMd = await loadCascadeMd(this.workspacePath);
|
|
8509
|
-
this.keystore = new Keystore(
|
|
8510
|
-
this.store = new MemoryStore(
|
|
10281
|
+
this.keystore = new Keystore(path18__default.default.join(this.globalDir, GLOBAL_KEYSTORE_FILE));
|
|
10282
|
+
this.store = new MemoryStore(path18__default.default.join(this.workspacePath, CASCADE_DB_FILE));
|
|
8511
10283
|
await this.injectEnvKeys();
|
|
8512
10284
|
await this.ensureDefaultIdentity();
|
|
8513
10285
|
}
|
|
@@ -8530,9 +10302,9 @@ var ConfigManager = class {
|
|
|
8530
10302
|
return this.workspacePath;
|
|
8531
10303
|
}
|
|
8532
10304
|
async save() {
|
|
8533
|
-
const configPath =
|
|
8534
|
-
await
|
|
8535
|
-
await
|
|
10305
|
+
const configPath = path18__default.default.join(this.workspacePath, CASCADE_CONFIG_FILE);
|
|
10306
|
+
await fs4__default.default.mkdir(path18__default.default.dirname(configPath), { recursive: true });
|
|
10307
|
+
await fs4__default.default.writeFile(configPath, JSON.stringify(this.config, null, 2), "utf-8");
|
|
8536
10308
|
}
|
|
8537
10309
|
async updateConfig(updates) {
|
|
8538
10310
|
this.config = validateConfig({ ...this.config, ...updates });
|
|
@@ -8555,9 +10327,9 @@ var ConfigManager = class {
|
|
|
8555
10327
|
return configProvider?.apiKey;
|
|
8556
10328
|
}
|
|
8557
10329
|
async loadConfig() {
|
|
8558
|
-
const configPath =
|
|
10330
|
+
const configPath = path18__default.default.join(this.workspacePath, CASCADE_CONFIG_FILE);
|
|
8559
10331
|
try {
|
|
8560
|
-
const raw = await
|
|
10332
|
+
const raw = await fs4__default.default.readFile(configPath, "utf-8");
|
|
8561
10333
|
return validateConfig(JSON.parse(raw));
|
|
8562
10334
|
} catch (err) {
|
|
8563
10335
|
if (err.code === "ENOENT") {
|
|
@@ -8636,12 +10408,13 @@ async function streamCascade(prompt, onToken, options = {}) {
|
|
|
8636
10408
|
}
|
|
8637
10409
|
});
|
|
8638
10410
|
}
|
|
10411
|
+
var JWT_ALGORITHM = "HS256";
|
|
8639
10412
|
function createToken(user, secret) {
|
|
8640
|
-
return jwt__default.default.sign(user, secret, { expiresIn: "24h" });
|
|
10413
|
+
return jwt__default.default.sign(user, secret, { expiresIn: "24h", algorithm: JWT_ALGORITHM });
|
|
8641
10414
|
}
|
|
8642
10415
|
function verifyToken(token, secret) {
|
|
8643
10416
|
try {
|
|
8644
|
-
return jwt__default.default.verify(token, secret);
|
|
10417
|
+
return jwt__default.default.verify(token, secret, { algorithms: [JWT_ALGORITHM] });
|
|
8645
10418
|
} catch {
|
|
8646
10419
|
return null;
|
|
8647
10420
|
}
|
|
@@ -8772,7 +10545,7 @@ var DashboardSocket = class {
|
|
|
8772
10545
|
this.io.close();
|
|
8773
10546
|
}
|
|
8774
10547
|
};
|
|
8775
|
-
var __dirname$1 =
|
|
10548
|
+
var __dirname$1 = path18__default.default.dirname(url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href))));
|
|
8776
10549
|
var DashboardServer = class {
|
|
8777
10550
|
app;
|
|
8778
10551
|
httpServer;
|
|
@@ -8783,12 +10556,14 @@ var DashboardServer = class {
|
|
|
8783
10556
|
globalStore = null;
|
|
8784
10557
|
broadcastTimer = null;
|
|
8785
10558
|
port;
|
|
10559
|
+
host;
|
|
8786
10560
|
workspacePath;
|
|
8787
10561
|
constructor(config, store, workspacePath = process.cwd()) {
|
|
8788
10562
|
this.config = config;
|
|
8789
10563
|
this.store = store;
|
|
8790
10564
|
this.workspacePath = workspacePath;
|
|
8791
10565
|
this.port = config.dashboard.port ?? DEFAULT_DASHBOARD_PORT;
|
|
10566
|
+
this.host = config.dashboard.host ?? "127.0.0.1";
|
|
8792
10567
|
this.dashboardSecret = this.resolveDashboardSecret();
|
|
8793
10568
|
this.app = express__default.default();
|
|
8794
10569
|
this.httpServer = http.createServer(this.app);
|
|
@@ -8801,10 +10576,19 @@ var DashboardServer = class {
|
|
|
8801
10576
|
this.setupRoutes();
|
|
8802
10577
|
}
|
|
8803
10578
|
async start() {
|
|
10579
|
+
const isLoopback = this.host === "127.0.0.1" || this.host === "::1" || this.host === "localhost";
|
|
10580
|
+
if (!isLoopback) {
|
|
10581
|
+
console.warn(
|
|
10582
|
+
`\u26A0 Dashboard is binding to ${this.host}:${this.port} \u2014 reachable from the network. It exposes task execution (/api/run) and config endpoints. Ensure dashboard.auth is enabled and CASCADE_DASHBOARD_PASSWORD is set.`
|
|
10583
|
+
);
|
|
10584
|
+
if (!this.config.dashboard.auth) {
|
|
10585
|
+
console.warn("\u26A0 Dashboard auth is DISABLED while bound to a non-loopback interface \u2014 this allows unauthenticated remote task execution.");
|
|
10586
|
+
}
|
|
10587
|
+
}
|
|
8804
10588
|
await new Promise((resolve, reject) => {
|
|
8805
10589
|
const onError = (err) => reject(err);
|
|
8806
10590
|
this.httpServer.once("error", onError);
|
|
8807
|
-
this.httpServer.listen(this.port, () => {
|
|
10591
|
+
this.httpServer.listen(this.port, this.host, () => {
|
|
8808
10592
|
this.httpServer.off("error", onError);
|
|
8809
10593
|
resolve();
|
|
8810
10594
|
});
|
|
@@ -8838,15 +10622,15 @@ var DashboardServer = class {
|
|
|
8838
10622
|
resolveDashboardSecret() {
|
|
8839
10623
|
const fromConfig = this.config.dashboard.secret ?? process.env["CASCADE_DASHBOARD_SECRET"];
|
|
8840
10624
|
if (fromConfig) return fromConfig;
|
|
8841
|
-
const secretPath =
|
|
10625
|
+
const secretPath = path18__default.default.join(this.workspacePath, CASCADE_DASHBOARD_SECRET_FILE);
|
|
8842
10626
|
try {
|
|
8843
|
-
if (
|
|
8844
|
-
const existing =
|
|
10627
|
+
if (fs17__default.default.existsSync(secretPath)) {
|
|
10628
|
+
const existing = fs17__default.default.readFileSync(secretPath, "utf-8").trim();
|
|
8845
10629
|
if (existing.length >= 16) return existing;
|
|
8846
10630
|
}
|
|
8847
10631
|
const generated = crypto.randomUUID();
|
|
8848
|
-
|
|
8849
|
-
|
|
10632
|
+
fs17__default.default.mkdirSync(path18__default.default.dirname(secretPath), { recursive: true });
|
|
10633
|
+
fs17__default.default.writeFileSync(secretPath, generated, { encoding: "utf-8", mode: 384 });
|
|
8850
10634
|
if (this.config.dashboard.auth) {
|
|
8851
10635
|
console.warn(
|
|
8852
10636
|
`Dashboard auth enabled with no secret configured; persisted a generated secret to ${secretPath}. Set CASCADE_DASHBOARD_SECRET or config.dashboard.secret to override.`
|
|
@@ -8873,7 +10657,7 @@ var DashboardServer = class {
|
|
|
8873
10657
|
// ── Setup ─────────────────────────────────────
|
|
8874
10658
|
getGlobalStore() {
|
|
8875
10659
|
if (!this.globalStore) {
|
|
8876
|
-
const globalDbPath =
|
|
10660
|
+
const globalDbPath = path18__default.default.join(os4__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
|
|
8877
10661
|
this.globalStore = new MemoryStore(globalDbPath);
|
|
8878
10662
|
}
|
|
8879
10663
|
return this.globalStore;
|
|
@@ -8934,12 +10718,12 @@ var DashboardServer = class {
|
|
|
8934
10718
|
}
|
|
8935
10719
|
}
|
|
8936
10720
|
watchRuntimeChanges() {
|
|
8937
|
-
const workspaceDbPath =
|
|
8938
|
-
const globalDbPath =
|
|
10721
|
+
const workspaceDbPath = path18__default.default.join(this.workspacePath, CASCADE_DB_FILE);
|
|
10722
|
+
const globalDbPath = path18__default.default.join(os4__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
|
|
8939
10723
|
const watchPaths = [workspaceDbPath, globalDbPath].filter((p, index, arr) => arr.indexOf(p) === index);
|
|
8940
10724
|
for (const watchPath of watchPaths) {
|
|
8941
|
-
if (!
|
|
8942
|
-
|
|
10725
|
+
if (!fs17__default.default.existsSync(watchPath)) continue;
|
|
10726
|
+
fs17__default.default.watchFile(watchPath, { interval: 3e3 }, () => {
|
|
8943
10727
|
this.throttledBroadcast(watchPath === globalDbPath ? "global" : "workspace");
|
|
8944
10728
|
});
|
|
8945
10729
|
}
|
|
@@ -9069,7 +10853,7 @@ var DashboardServer = class {
|
|
|
9069
10853
|
const sessionId = req.params.id;
|
|
9070
10854
|
this.store.deleteSession(sessionId);
|
|
9071
10855
|
this.store.deleteRuntimeSession(sessionId);
|
|
9072
|
-
const globalDbPath =
|
|
10856
|
+
const globalDbPath = path18__default.default.join(os4__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
|
|
9073
10857
|
const globalStore = new MemoryStore(globalDbPath);
|
|
9074
10858
|
try {
|
|
9075
10859
|
globalStore.deleteRuntimeSession(sessionId);
|
|
@@ -9083,7 +10867,7 @@ var DashboardServer = class {
|
|
|
9083
10867
|
});
|
|
9084
10868
|
this.app.delete("/api/sessions", auth, (req, res) => {
|
|
9085
10869
|
const body = req.body;
|
|
9086
|
-
const globalDbPath =
|
|
10870
|
+
const globalDbPath = path18__default.default.join(os4__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
|
|
9087
10871
|
if (body?.ids && Array.isArray(body.ids) && body.ids.length > 0) {
|
|
9088
10872
|
const globalStore = new MemoryStore(globalDbPath);
|
|
9089
10873
|
try {
|
|
@@ -9106,7 +10890,7 @@ var DashboardServer = class {
|
|
|
9106
10890
|
});
|
|
9107
10891
|
this.app.delete("/api/runtime", auth, (_req, res) => {
|
|
9108
10892
|
this.store.deleteAllRuntimeNodes();
|
|
9109
|
-
const globalDbPath =
|
|
10893
|
+
const globalDbPath = path18__default.default.join(os4__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
|
|
9110
10894
|
const globalStore = new MemoryStore(globalDbPath);
|
|
9111
10895
|
try {
|
|
9112
10896
|
globalStore.deleteAllRuntimeNodes();
|
|
@@ -9179,12 +10963,12 @@ var DashboardServer = class {
|
|
|
9179
10963
|
if (body["tierLimits"]) this.config.tierLimits = { ...this.config.tierLimits, ...body["tierLimits"] };
|
|
9180
10964
|
if (body["budget"]) this.config.budget = { ...this.config.budget, ...body["budget"] };
|
|
9181
10965
|
try {
|
|
9182
|
-
const configPath =
|
|
9183
|
-
const existing =
|
|
10966
|
+
const configPath = path18__default.default.join(this.workspacePath, CASCADE_CONFIG_FILE);
|
|
10967
|
+
const existing = fs17__default.default.existsSync(configPath) ? JSON.parse(fs17__default.default.readFileSync(configPath, "utf-8")) : {};
|
|
9184
10968
|
const updated = { ...existing, tierLimits: this.config.tierLimits, budget: this.config.budget };
|
|
9185
10969
|
const tmp = configPath + ".tmp";
|
|
9186
|
-
|
|
9187
|
-
|
|
10970
|
+
fs17__default.default.writeFileSync(tmp, JSON.stringify(updated, null, 2), "utf-8");
|
|
10971
|
+
fs17__default.default.renameSync(tmp, configPath);
|
|
9188
10972
|
res.json({ ok: true });
|
|
9189
10973
|
} catch (err) {
|
|
9190
10974
|
res.status(500).json({ error: `Failed to save config: ${err instanceof Error ? err.message : String(err)}` });
|
|
@@ -9212,7 +10996,7 @@ var DashboardServer = class {
|
|
|
9212
10996
|
this.app.get("/api/runtime", auth, (req, res) => {
|
|
9213
10997
|
const scope = req.query["scope"] ?? "workspace";
|
|
9214
10998
|
if (scope === "global") {
|
|
9215
|
-
const globalDbPath =
|
|
10999
|
+
const globalDbPath = path18__default.default.join(os4__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
|
|
9216
11000
|
const globalStore = new MemoryStore(globalDbPath);
|
|
9217
11001
|
try {
|
|
9218
11002
|
res.json({
|
|
@@ -9285,13 +11069,13 @@ var DashboardServer = class {
|
|
|
9285
11069
|
}))
|
|
9286
11070
|
});
|
|
9287
11071
|
});
|
|
9288
|
-
const prodPath =
|
|
9289
|
-
const devPath =
|
|
9290
|
-
const webDistPath =
|
|
9291
|
-
if (
|
|
11072
|
+
const prodPath = path18__default.default.resolve(__dirname$1, "../web/dist");
|
|
11073
|
+
const devPath = path18__default.default.resolve(__dirname$1, "../../web/dist");
|
|
11074
|
+
const webDistPath = fs17__default.default.existsSync(prodPath) ? prodPath : devPath;
|
|
11075
|
+
if (fs17__default.default.existsSync(webDistPath)) {
|
|
9292
11076
|
this.app.use(express__default.default.static(webDistPath));
|
|
9293
11077
|
this.app.get("*", (_req, res) => {
|
|
9294
|
-
res.sendFile(
|
|
11078
|
+
res.sendFile(path18__default.default.join(webDistPath, "index.html"));
|
|
9295
11079
|
});
|
|
9296
11080
|
} else {
|
|
9297
11081
|
this.app.get("/", (_req, res) => {
|