cascade-ai 0.4.0 → 0.5.1
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 +35 -2
- package/dist/cli.cjs +2318 -1019
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +2316 -1016
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +1426 -329
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +177 -74
- package/dist/index.d.ts +177 -74
- package/dist/index.js +1422 -325
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/web/dist/assets/index-BFrwdYDg.js +225 -0
- package/web/dist/assets/index-C6Nd1mOj.css +1 -0
- package/web/dist/index.html +2 -2
- package/web/dist/assets/index-BvxaBI9b.js +0 -216
- package/web/dist/assets/index-DO_ICahS.css +0 -1
package/dist/index.cjs
CHANGED
|
@@ -7,19 +7,19 @@ var Anthropic = require('@anthropic-ai/sdk');
|
|
|
7
7
|
var OpenAI = require('openai');
|
|
8
8
|
var genai = require('@google/genai');
|
|
9
9
|
var axios2 = require('axios');
|
|
10
|
-
var
|
|
11
|
-
var
|
|
10
|
+
var fs3 = require('fs/promises');
|
|
11
|
+
var path16 = require('path');
|
|
12
12
|
var ignoreFactory = require('ignore');
|
|
13
13
|
var child_process = require('child_process');
|
|
14
14
|
var util = require('util');
|
|
15
|
+
var fs15 = require('fs');
|
|
15
16
|
var simpleGit = require('simple-git');
|
|
16
|
-
var fs11 = require('fs');
|
|
17
17
|
var PDFDocument = require('pdfkit');
|
|
18
18
|
var index_js = require('@modelcontextprotocol/sdk/client/index.js');
|
|
19
19
|
var stdio_js = require('@modelcontextprotocol/sdk/client/stdio.js');
|
|
20
20
|
var zod = require('zod');
|
|
21
|
+
var os3 = require('os');
|
|
21
22
|
var vm = require('vm');
|
|
22
|
-
var os2 = require('os');
|
|
23
23
|
var Database = require('better-sqlite3');
|
|
24
24
|
var http = require('http');
|
|
25
25
|
var url = require('url');
|
|
@@ -57,12 +57,12 @@ var crypto__default = /*#__PURE__*/_interopDefault(crypto);
|
|
|
57
57
|
var Anthropic__default = /*#__PURE__*/_interopDefault(Anthropic);
|
|
58
58
|
var OpenAI__default = /*#__PURE__*/_interopDefault(OpenAI);
|
|
59
59
|
var axios2__default = /*#__PURE__*/_interopDefault(axios2);
|
|
60
|
-
var
|
|
61
|
-
var
|
|
60
|
+
var fs3__default = /*#__PURE__*/_interopDefault(fs3);
|
|
61
|
+
var path16__default = /*#__PURE__*/_interopDefault(path16);
|
|
62
62
|
var ignoreFactory__namespace = /*#__PURE__*/_interopNamespace(ignoreFactory);
|
|
63
|
-
var
|
|
63
|
+
var fs15__default = /*#__PURE__*/_interopDefault(fs15);
|
|
64
64
|
var PDFDocument__default = /*#__PURE__*/_interopDefault(PDFDocument);
|
|
65
|
-
var
|
|
65
|
+
var os3__default = /*#__PURE__*/_interopDefault(os3);
|
|
66
66
|
var Database__default = /*#__PURE__*/_interopDefault(Database);
|
|
67
67
|
var express__default = /*#__PURE__*/_interopDefault(express);
|
|
68
68
|
var rateLimit__default = /*#__PURE__*/_interopDefault(rateLimit);
|
|
@@ -165,7 +165,7 @@ var require_keytar2 = __commonJS({
|
|
|
165
165
|
});
|
|
166
166
|
|
|
167
167
|
// src/constants.ts
|
|
168
|
-
var CASCADE_VERSION = "0.
|
|
168
|
+
var CASCADE_VERSION = "0.5.1";
|
|
169
169
|
var CASCADE_CONFIG_DIR = ".cascade";
|
|
170
170
|
var CASCADE_MD_FILE = "CASCADE.md";
|
|
171
171
|
var CASCADE_IGNORE_FILE = ".cascadeignore";
|
|
@@ -1159,6 +1159,22 @@ var GeminiProvider = class extends BaseProvider {
|
|
|
1159
1159
|
};
|
|
1160
1160
|
}
|
|
1161
1161
|
};
|
|
1162
|
+
var TOOL_CAPABLE_FAMILIES = [
|
|
1163
|
+
"llama3.1",
|
|
1164
|
+
"llama3.2",
|
|
1165
|
+
"llama3.3",
|
|
1166
|
+
"qwen2",
|
|
1167
|
+
"qwen2.5",
|
|
1168
|
+
"qwen3",
|
|
1169
|
+
"mistral-nemo",
|
|
1170
|
+
"mistral-small",
|
|
1171
|
+
"command-r",
|
|
1172
|
+
"firefunction"
|
|
1173
|
+
];
|
|
1174
|
+
function isToolCapable(modelName) {
|
|
1175
|
+
const name = modelName.toLowerCase();
|
|
1176
|
+
return TOOL_CAPABLE_FAMILIES.some((family) => name.includes(family));
|
|
1177
|
+
}
|
|
1162
1178
|
var OllamaProvider = class extends BaseProvider {
|
|
1163
1179
|
baseUrl;
|
|
1164
1180
|
constructor(config, model) {
|
|
@@ -1171,12 +1187,21 @@ var OllamaProvider = class extends BaseProvider {
|
|
|
1171
1187
|
}
|
|
1172
1188
|
async generateStream(options, onChunk) {
|
|
1173
1189
|
const messages = this.convertMessages(options.messages, options.systemPrompt);
|
|
1190
|
+
const ollamaTools = options.tools?.map((t) => ({
|
|
1191
|
+
type: "function",
|
|
1192
|
+
function: {
|
|
1193
|
+
name: t.name,
|
|
1194
|
+
description: t.description,
|
|
1195
|
+
parameters: t.inputSchema
|
|
1196
|
+
}
|
|
1197
|
+
}));
|
|
1174
1198
|
const response = await axios2__default.default.post(
|
|
1175
1199
|
`${this.baseUrl}/api/chat`,
|
|
1176
1200
|
{
|
|
1177
1201
|
model: this.model.id,
|
|
1178
1202
|
messages,
|
|
1179
1203
|
stream: true,
|
|
1204
|
+
tools: ollamaTools?.length ? ollamaTools : void 0,
|
|
1180
1205
|
options: {
|
|
1181
1206
|
num_predict: options.maxTokens ?? this.model.maxOutputTokens,
|
|
1182
1207
|
temperature: options.temperature ?? 0.7
|
|
@@ -1187,6 +1212,7 @@ var OllamaProvider = class extends BaseProvider {
|
|
|
1187
1212
|
let fullContent = "";
|
|
1188
1213
|
let inputTokens = 0;
|
|
1189
1214
|
let outputTokens = 0;
|
|
1215
|
+
const pendingToolCalls = [];
|
|
1190
1216
|
await new Promise((resolve, reject) => {
|
|
1191
1217
|
let buffer = "";
|
|
1192
1218
|
response.data.on("data", (chunk) => {
|
|
@@ -1201,6 +1227,9 @@ var OllamaProvider = class extends BaseProvider {
|
|
|
1201
1227
|
fullContent += parsed.message.content;
|
|
1202
1228
|
onChunk({ text: parsed.message.content, finishReason: null });
|
|
1203
1229
|
}
|
|
1230
|
+
if (parsed.message?.tool_calls?.length) {
|
|
1231
|
+
pendingToolCalls.push(...parsed.message.tool_calls);
|
|
1232
|
+
}
|
|
1204
1233
|
if (parsed.done) {
|
|
1205
1234
|
inputTokens = parsed.prompt_eval_count ?? 0;
|
|
1206
1235
|
outputTokens = parsed.eval_count ?? 0;
|
|
@@ -1218,6 +1247,9 @@ var OllamaProvider = class extends BaseProvider {
|
|
|
1218
1247
|
fullContent += parsed.message.content;
|
|
1219
1248
|
onChunk({ text: parsed.message.content, finishReason: null });
|
|
1220
1249
|
}
|
|
1250
|
+
if (parsed.message?.tool_calls?.length) {
|
|
1251
|
+
pendingToolCalls.push(...parsed.message.tool_calls);
|
|
1252
|
+
}
|
|
1221
1253
|
if (parsed.done) {
|
|
1222
1254
|
inputTokens = parsed.prompt_eval_count ?? inputTokens;
|
|
1223
1255
|
outputTokens = parsed.eval_count ?? outputTokens;
|
|
@@ -1229,11 +1261,30 @@ var OllamaProvider = class extends BaseProvider {
|
|
|
1229
1261
|
});
|
|
1230
1262
|
response.data.on("error", reject);
|
|
1231
1263
|
});
|
|
1232
|
-
|
|
1264
|
+
const toolCalls = pendingToolCalls.map((tc, i) => {
|
|
1265
|
+
let input;
|
|
1266
|
+
if (typeof tc.function.arguments === "string") {
|
|
1267
|
+
try {
|
|
1268
|
+
input = JSON.parse(tc.function.arguments);
|
|
1269
|
+
} catch {
|
|
1270
|
+
input = { __rawArguments: tc.function.arguments };
|
|
1271
|
+
}
|
|
1272
|
+
} else {
|
|
1273
|
+
input = tc.function.arguments;
|
|
1274
|
+
}
|
|
1275
|
+
return {
|
|
1276
|
+
id: `ollama-tool-${Date.now()}-${i}`,
|
|
1277
|
+
name: tc.function.name,
|
|
1278
|
+
input
|
|
1279
|
+
};
|
|
1280
|
+
});
|
|
1281
|
+
const finishReason = toolCalls.length ? "tool_use" : "stop";
|
|
1282
|
+
onChunk({ text: "", finishReason });
|
|
1233
1283
|
return {
|
|
1234
1284
|
content: fullContent,
|
|
1235
1285
|
usage: this.makeUsage(inputTokens, outputTokens),
|
|
1236
|
-
|
|
1286
|
+
toolCalls: toolCalls.length ? toolCalls : void 0,
|
|
1287
|
+
finishReason
|
|
1237
1288
|
};
|
|
1238
1289
|
}
|
|
1239
1290
|
async countTokens(text) {
|
|
@@ -1257,6 +1308,7 @@ var OllamaProvider = class extends BaseProvider {
|
|
|
1257
1308
|
maxOutputTokens: 4e3,
|
|
1258
1309
|
supportsStreaming: true,
|
|
1259
1310
|
isLocal: true,
|
|
1311
|
+
supportsToolUse: isToolCapable(m.name),
|
|
1260
1312
|
minSizeB: this.parseSizeB(m.details?.parameter_size)
|
|
1261
1313
|
}));
|
|
1262
1314
|
} catch {
|
|
@@ -1279,6 +1331,26 @@ var OllamaProvider = class extends BaseProvider {
|
|
|
1279
1331
|
result.push({ role: "system", content: typeof m.content === "string" ? m.content : "" });
|
|
1280
1332
|
continue;
|
|
1281
1333
|
}
|
|
1334
|
+
if (m.role === "tool") {
|
|
1335
|
+
result.push({
|
|
1336
|
+
role: "tool",
|
|
1337
|
+
content: typeof m.content === "string" ? m.content : JSON.stringify(m.content)
|
|
1338
|
+
});
|
|
1339
|
+
continue;
|
|
1340
|
+
}
|
|
1341
|
+
if (m.role === "assistant" && m.toolCalls?.length) {
|
|
1342
|
+
result.push({
|
|
1343
|
+
role: "assistant",
|
|
1344
|
+
content: typeof m.content === "string" ? m.content : "",
|
|
1345
|
+
tool_calls: m.toolCalls.map((tc) => ({
|
|
1346
|
+
function: {
|
|
1347
|
+
name: tc.name,
|
|
1348
|
+
arguments: tc.input
|
|
1349
|
+
}
|
|
1350
|
+
}))
|
|
1351
|
+
});
|
|
1352
|
+
continue;
|
|
1353
|
+
}
|
|
1282
1354
|
if (typeof m.content === "string") {
|
|
1283
1355
|
result.push({ role: m.role, content: m.content });
|
|
1284
1356
|
continue;
|
|
@@ -1411,6 +1483,26 @@ var ModelSelector = class {
|
|
|
1411
1483
|
return T3_MODEL_PRIORITY;
|
|
1412
1484
|
}
|
|
1413
1485
|
}
|
|
1486
|
+
getAllAvailableModels() {
|
|
1487
|
+
return Array.from(this.availableModels.values()).filter(
|
|
1488
|
+
(m) => this.availableProviders.has(m.provider)
|
|
1489
|
+
);
|
|
1490
|
+
}
|
|
1491
|
+
/**
|
|
1492
|
+
* Returns all available models eligible for the given tier, ordered by the
|
|
1493
|
+
* tier's priority chain. Use this as the candidate set for scored selection.
|
|
1494
|
+
*/
|
|
1495
|
+
getCandidatesForTier(tier) {
|
|
1496
|
+
const priority = this.getPriorityList(tier);
|
|
1497
|
+
const candidates = [];
|
|
1498
|
+
for (const key of priority) {
|
|
1499
|
+
const model = this.availableModels.get(key);
|
|
1500
|
+
if (model && this.availableProviders.has(model.provider)) {
|
|
1501
|
+
candidates.push(model);
|
|
1502
|
+
}
|
|
1503
|
+
}
|
|
1504
|
+
return candidates;
|
|
1505
|
+
}
|
|
1414
1506
|
isProviderAvailable(provider) {
|
|
1415
1507
|
return this.availableProviders.has(provider);
|
|
1416
1508
|
}
|
|
@@ -1616,11 +1708,203 @@ var TpmLimiter = class {
|
|
|
1616
1708
|
}
|
|
1617
1709
|
};
|
|
1618
1710
|
|
|
1711
|
+
// src/core/router/local-queue.ts
|
|
1712
|
+
var LocalRequestQueue = class {
|
|
1713
|
+
maxConcurrent;
|
|
1714
|
+
active = 0;
|
|
1715
|
+
queue = [];
|
|
1716
|
+
constructor(maxConcurrent = 1) {
|
|
1717
|
+
this.maxConcurrent = Math.max(1, maxConcurrent);
|
|
1718
|
+
}
|
|
1719
|
+
/**
|
|
1720
|
+
* Acquire a queue slot. Returns a `release` function that MUST be called
|
|
1721
|
+
* when the inference call is done (even on error). Rejects if the slot
|
|
1722
|
+
* cannot be acquired within `timeoutMs`.
|
|
1723
|
+
*/
|
|
1724
|
+
async acquire(timeoutMs) {
|
|
1725
|
+
if (this.active < this.maxConcurrent) {
|
|
1726
|
+
this.active++;
|
|
1727
|
+
return this.makeRelease();
|
|
1728
|
+
}
|
|
1729
|
+
return new Promise((resolve, reject) => {
|
|
1730
|
+
let settled = false;
|
|
1731
|
+
let timer;
|
|
1732
|
+
const resolver = (release) => {
|
|
1733
|
+
if (settled) return;
|
|
1734
|
+
settled = true;
|
|
1735
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
1736
|
+
resolve(release);
|
|
1737
|
+
};
|
|
1738
|
+
if (timeoutMs !== void 0 && timeoutMs > 0) {
|
|
1739
|
+
timer = setTimeout(() => {
|
|
1740
|
+
if (settled) return;
|
|
1741
|
+
settled = true;
|
|
1742
|
+
const idx = this.queue.indexOf(resolver);
|
|
1743
|
+
if (idx !== -1) this.queue.splice(idx, 1);
|
|
1744
|
+
reject(new Error(
|
|
1745
|
+
`Local model queue: timed out waiting for a free slot after ${timeoutMs}ms. Active: ${this.active}, Queued: ${this.queue.length}. Consider increasing localConcurrency or localInferenceTimeoutMs in your config.`
|
|
1746
|
+
));
|
|
1747
|
+
}, timeoutMs);
|
|
1748
|
+
}
|
|
1749
|
+
this.queue.push(resolver);
|
|
1750
|
+
});
|
|
1751
|
+
}
|
|
1752
|
+
/** Number of in-flight requests. */
|
|
1753
|
+
get activeCount() {
|
|
1754
|
+
return this.active;
|
|
1755
|
+
}
|
|
1756
|
+
/** Number of requests waiting for a slot. */
|
|
1757
|
+
get queueDepth() {
|
|
1758
|
+
return this.queue.length;
|
|
1759
|
+
}
|
|
1760
|
+
makeRelease() {
|
|
1761
|
+
let called = false;
|
|
1762
|
+
return () => {
|
|
1763
|
+
if (called) return;
|
|
1764
|
+
called = true;
|
|
1765
|
+
this.active--;
|
|
1766
|
+
const next = this.queue.shift();
|
|
1767
|
+
if (next) {
|
|
1768
|
+
this.active++;
|
|
1769
|
+
next(this.makeRelease());
|
|
1770
|
+
}
|
|
1771
|
+
};
|
|
1772
|
+
}
|
|
1773
|
+
};
|
|
1774
|
+
|
|
1619
1775
|
// src/utils/cost.ts
|
|
1620
1776
|
function calculateCost(inputTokens, outputTokens, model) {
|
|
1621
1777
|
return inputTokens / 1e3 * model.inputCostPer1kTokens + outputTokens / 1e3 * model.outputCostPer1kTokens;
|
|
1622
1778
|
}
|
|
1623
1779
|
|
|
1780
|
+
// src/utils/retry.ts
|
|
1781
|
+
var CascadeCancelledError = class extends Error {
|
|
1782
|
+
constructor(reason) {
|
|
1783
|
+
super(reason ?? "Run was cancelled via AbortSignal");
|
|
1784
|
+
this.name = "CascadeCancelledError";
|
|
1785
|
+
}
|
|
1786
|
+
};
|
|
1787
|
+
var CascadeToolError = class extends Error {
|
|
1788
|
+
/** A friendly message to show the user / T3 */
|
|
1789
|
+
userMessage;
|
|
1790
|
+
/** Whether this error class is retryable by default */
|
|
1791
|
+
retryable;
|
|
1792
|
+
constructor(userMessage, cause, retryable = false) {
|
|
1793
|
+
const causeMsg = cause instanceof Error ? cause.message : String(cause);
|
|
1794
|
+
super(`${userMessage}: ${causeMsg}`);
|
|
1795
|
+
this.name = "CascadeToolError";
|
|
1796
|
+
this.userMessage = userMessage;
|
|
1797
|
+
this.retryable = retryable;
|
|
1798
|
+
}
|
|
1799
|
+
};
|
|
1800
|
+
async function withTimeout(promise, timeoutMs, errorMessage = "Operation timed out") {
|
|
1801
|
+
let timer;
|
|
1802
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
1803
|
+
timer = setTimeout(
|
|
1804
|
+
() => reject(new Error(errorMessage)),
|
|
1805
|
+
timeoutMs
|
|
1806
|
+
);
|
|
1807
|
+
});
|
|
1808
|
+
try {
|
|
1809
|
+
return await Promise.race([promise, timeoutPromise]);
|
|
1810
|
+
} finally {
|
|
1811
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
1812
|
+
}
|
|
1813
|
+
}
|
|
1814
|
+
|
|
1815
|
+
// src/core/router/model-profiler.ts
|
|
1816
|
+
var SKIP_PATTERN = /embed|dall-e|whisper|tts|vision|instruct-vision|rerank/i;
|
|
1817
|
+
var SPECIALIZATION_KEYWORDS = {
|
|
1818
|
+
code: ["code", "coding", "programming", "developer", "software", "function", "debug", "typescript", "python", "javascript"],
|
|
1819
|
+
analysis: ["analysis", "analytical", "reasoning", "logic", "research", "evaluate", "assess", "explain"],
|
|
1820
|
+
creative: ["creative", "writing", "story", "poetry", "content", "blog", "essay", "narrative"],
|
|
1821
|
+
data: ["data", "sql", "statistics", "chart", "csv", "json", "excel", "spreadsheet", "math", "mathematical"],
|
|
1822
|
+
instruction: ["instruction", "instruction-following", "accurate", "precise", "factual"],
|
|
1823
|
+
multilingual: ["multilingual", "language", "translation", "linguistic"],
|
|
1824
|
+
long_context: ["long", "context", "document", "book", "summarize", "large"]
|
|
1825
|
+
};
|
|
1826
|
+
function extractSpecializations(description) {
|
|
1827
|
+
const lower = description.toLowerCase();
|
|
1828
|
+
const found = [];
|
|
1829
|
+
for (const [key, terms] of Object.entries(SPECIALIZATION_KEYWORDS)) {
|
|
1830
|
+
if (terms.some((t) => lower.includes(t))) {
|
|
1831
|
+
found.push(key);
|
|
1832
|
+
}
|
|
1833
|
+
}
|
|
1834
|
+
return found;
|
|
1835
|
+
}
|
|
1836
|
+
async function fetchOpenRouterModels() {
|
|
1837
|
+
try {
|
|
1838
|
+
const resp = await fetch("https://openrouter.ai/api/v1/models", {
|
|
1839
|
+
headers: { "User-Agent": "Cascade-AI/0.4.0" },
|
|
1840
|
+
signal: AbortSignal.timeout(8e3)
|
|
1841
|
+
});
|
|
1842
|
+
if (!resp.ok) return [];
|
|
1843
|
+
const data = await resp.json();
|
|
1844
|
+
return data.data ?? [];
|
|
1845
|
+
} catch {
|
|
1846
|
+
return [];
|
|
1847
|
+
}
|
|
1848
|
+
}
|
|
1849
|
+
async function queryModelDirectly(router, model) {
|
|
1850
|
+
try {
|
|
1851
|
+
const result = await router.generate("T3", {
|
|
1852
|
+
messages: [{
|
|
1853
|
+
role: "user",
|
|
1854
|
+
content: 'What are your top 3 task specializations? Reply with valid JSON only: {"specializations": ["<area1>", "<area2>", "<area3>"]}'
|
|
1855
|
+
}],
|
|
1856
|
+
maxTokens: 60
|
|
1857
|
+
});
|
|
1858
|
+
const match = /\{[\s\S]*?\}/.exec(result.content);
|
|
1859
|
+
if (!match) return [];
|
|
1860
|
+
const parsed = JSON.parse(match[0]);
|
|
1861
|
+
const specs = parsed.specializations;
|
|
1862
|
+
if (!Array.isArray(specs)) return [];
|
|
1863
|
+
return specs.filter((s) => typeof s === "string").slice(0, 5);
|
|
1864
|
+
} catch {
|
|
1865
|
+
return [];
|
|
1866
|
+
}
|
|
1867
|
+
}
|
|
1868
|
+
var ModelProfiler = class {
|
|
1869
|
+
store;
|
|
1870
|
+
router;
|
|
1871
|
+
constructor(store, router) {
|
|
1872
|
+
this.store = store;
|
|
1873
|
+
this.router = router;
|
|
1874
|
+
}
|
|
1875
|
+
/**
|
|
1876
|
+
* Profile all models that haven't been profiled yet.
|
|
1877
|
+
* Safe to call concurrently — SQLite upsert handles races.
|
|
1878
|
+
*/
|
|
1879
|
+
async profileAll(models) {
|
|
1880
|
+
const alreadyProfiled = new Set(this.store.getProfiledModelIds());
|
|
1881
|
+
const toProfile = models.filter(
|
|
1882
|
+
(m) => !alreadyProfiled.has(m.id) && !SKIP_PATTERN.test(m.id) && !SKIP_PATTERN.test(m.name)
|
|
1883
|
+
);
|
|
1884
|
+
if (toProfile.length === 0) return;
|
|
1885
|
+
const openRouterModels = await fetchOpenRouterModels();
|
|
1886
|
+
const orByNormalizedId = /* @__PURE__ */ new Map();
|
|
1887
|
+
for (const m of openRouterModels) {
|
|
1888
|
+
orByNormalizedId.set(m.id.toLowerCase(), m);
|
|
1889
|
+
const short = m.id.split("/").pop();
|
|
1890
|
+
if (short) orByNormalizedId.set(short.toLowerCase(), m);
|
|
1891
|
+
}
|
|
1892
|
+
await Promise.allSettled(
|
|
1893
|
+
toProfile.map(async (model) => {
|
|
1894
|
+
let specializations = [];
|
|
1895
|
+
const orMatch = orByNormalizedId.get(model.id.toLowerCase()) ?? orByNormalizedId.get(model.id.split("/").pop()?.toLowerCase() ?? "");
|
|
1896
|
+
if (orMatch?.description) {
|
|
1897
|
+
specializations = extractSpecializations(orMatch.description);
|
|
1898
|
+
}
|
|
1899
|
+
if (specializations.length === 0 && this.router) {
|
|
1900
|
+
specializations = await queryModelDirectly(this.router);
|
|
1901
|
+
}
|
|
1902
|
+
this.store.saveModelProfile(model.id, model.provider, specializations);
|
|
1903
|
+
})
|
|
1904
|
+
);
|
|
1905
|
+
}
|
|
1906
|
+
};
|
|
1907
|
+
|
|
1624
1908
|
// src/core/router/index.ts
|
|
1625
1909
|
var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
|
|
1626
1910
|
selector;
|
|
@@ -1648,6 +1932,7 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
|
|
|
1648
1932
|
budgetState = "ok";
|
|
1649
1933
|
budgetExceededReason;
|
|
1650
1934
|
tpmLimiter;
|
|
1935
|
+
localQueue;
|
|
1651
1936
|
/** Thrown when the configured budget is exceeded. */
|
|
1652
1937
|
static BudgetExceededError = class extends Error {
|
|
1653
1938
|
constructor(msg) {
|
|
@@ -1664,6 +1949,7 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
|
|
|
1664
1949
|
this.selector = new ModelSelector(availableProviders);
|
|
1665
1950
|
this.failover = new FailoverManager(this.selector);
|
|
1666
1951
|
this.tpmLimiter = new TpmLimiter(config.rateLimits?.providerTpm ?? {});
|
|
1952
|
+
this.localQueue = new LocalRequestQueue(config.localConcurrency ?? 1);
|
|
1667
1953
|
const ollamaCfg = config.providers.find((p) => p.type === "ollama");
|
|
1668
1954
|
if (availableProviders.has("ollama")) {
|
|
1669
1955
|
await this.discoverOllamaModels(ollamaCfg);
|
|
@@ -1690,6 +1976,17 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
|
|
|
1690
1976
|
}
|
|
1691
1977
|
}
|
|
1692
1978
|
}
|
|
1979
|
+
/**
|
|
1980
|
+
* Run model specialization profiling in the background.
|
|
1981
|
+
* Only profiles models that haven't been profiled yet (cache-first).
|
|
1982
|
+
* No-op if store is not provided.
|
|
1983
|
+
*/
|
|
1984
|
+
async profileModels(store) {
|
|
1985
|
+
const allModels = this.selector.getAllAvailableModels();
|
|
1986
|
+
const profiler = new ModelProfiler(store, this);
|
|
1987
|
+
profiler.profileAll(allModels).catch(() => {
|
|
1988
|
+
});
|
|
1989
|
+
}
|
|
1693
1990
|
async generate(tier, options, onChunk, requireVision = false) {
|
|
1694
1991
|
if (this.budgetState === "exceeded") {
|
|
1695
1992
|
throw new _CascadeRouter.BudgetExceededError(
|
|
@@ -1711,9 +2008,26 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
|
|
|
1711
2008
|
await this.tpmLimiter.acquire(model.provider, estimatedTokens);
|
|
1712
2009
|
}
|
|
1713
2010
|
const useStream = Boolean(onChunk) && model.supportsStreaming && typeof provider.generateStream === "function";
|
|
2011
|
+
let releaseLocalSlot;
|
|
2012
|
+
if (model.isLocal) {
|
|
2013
|
+
const inferenceTimeoutMs = this.config.localInferenceTimeoutMs ?? 3e5;
|
|
2014
|
+
const queueWaitMs = Math.round(inferenceTimeoutMs / 2);
|
|
2015
|
+
releaseLocalSlot = await this.localQueue.acquire(queueWaitMs);
|
|
2016
|
+
}
|
|
1714
2017
|
try {
|
|
1715
2018
|
let result;
|
|
1716
|
-
if (
|
|
2019
|
+
if (model.isLocal) {
|
|
2020
|
+
const inferenceTimeoutMs = this.config.localInferenceTimeoutMs ?? 3e5;
|
|
2021
|
+
const inferencePromise = useStream && onChunk ? provider.generateStream(options, (chunk) => {
|
|
2022
|
+
const text = typeof chunk?.text === "string" ? chunk.text : "";
|
|
2023
|
+
if (text) onChunk({ ...chunk, text });
|
|
2024
|
+
}) : provider.generate(options);
|
|
2025
|
+
result = await withTimeout(
|
|
2026
|
+
inferencePromise,
|
|
2027
|
+
inferenceTimeoutMs,
|
|
2028
|
+
`Local model ${model.id} inference timed out after ${inferenceTimeoutMs}ms`
|
|
2029
|
+
);
|
|
2030
|
+
} else if (useStream && onChunk) {
|
|
1717
2031
|
try {
|
|
1718
2032
|
result = await provider.generateStream(options, (chunk) => {
|
|
1719
2033
|
const text = typeof chunk?.text === "string" ? chunk.text : "";
|
|
@@ -1751,10 +2065,14 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
|
|
|
1751
2065
|
if (fallback) {
|
|
1752
2066
|
this.tierModels.set(tier, fallback);
|
|
1753
2067
|
this.ensureProvider(fallback, this.config.providers);
|
|
2068
|
+
releaseLocalSlot?.();
|
|
2069
|
+
releaseLocalSlot = void 0;
|
|
1754
2070
|
return this.generate(tier, options, onChunk, requireVision);
|
|
1755
2071
|
}
|
|
1756
2072
|
}
|
|
1757
2073
|
throw err;
|
|
2074
|
+
} finally {
|
|
2075
|
+
releaseLocalSlot?.();
|
|
1758
2076
|
}
|
|
1759
2077
|
}
|
|
1760
2078
|
getModelForTier(tier) {
|
|
@@ -1994,29 +2312,6 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
|
|
|
1994
2312
|
return /rate.?limit|429|too.?many.?requests|quota/i.test(msg);
|
|
1995
2313
|
}
|
|
1996
2314
|
};
|
|
1997
|
-
|
|
1998
|
-
// src/utils/retry.ts
|
|
1999
|
-
var CascadeCancelledError = class extends Error {
|
|
2000
|
-
constructor(reason) {
|
|
2001
|
-
super(reason ?? "Run was cancelled via AbortSignal");
|
|
2002
|
-
this.name = "CascadeCancelledError";
|
|
2003
|
-
}
|
|
2004
|
-
};
|
|
2005
|
-
var CascadeToolError = class extends Error {
|
|
2006
|
-
/** A friendly message to show the user / T3 */
|
|
2007
|
-
userMessage;
|
|
2008
|
-
/** Whether this error class is retryable by default */
|
|
2009
|
-
retryable;
|
|
2010
|
-
constructor(userMessage, cause, retryable = false) {
|
|
2011
|
-
const causeMsg = cause instanceof Error ? cause.message : String(cause);
|
|
2012
|
-
super(`${userMessage}: ${causeMsg}`);
|
|
2013
|
-
this.name = "CascadeToolError";
|
|
2014
|
-
this.userMessage = userMessage;
|
|
2015
|
-
this.retryable = retryable;
|
|
2016
|
-
}
|
|
2017
|
-
};
|
|
2018
|
-
|
|
2019
|
-
// src/core/tiers/base.ts
|
|
2020
2315
|
var BaseTier = class extends EventEmitter__default.default {
|
|
2021
2316
|
id;
|
|
2022
2317
|
role;
|
|
@@ -2297,6 +2592,97 @@ var AuditLogger = class {
|
|
|
2297
2592
|
}
|
|
2298
2593
|
};
|
|
2299
2594
|
|
|
2595
|
+
// src/tools/text-tool-parser.ts
|
|
2596
|
+
var TOOL_CALL_RE = /<tool_call>\s*([\s\S]*?)\s*<\/tool_call>/g;
|
|
2597
|
+
var JSON_BLOCK_RE = /```json\s*([\s\S]*?)\s*```/g;
|
|
2598
|
+
var FUNCTION_OBJ_RE = /\{\s*"function"\s*:\s*\{[^}]*"name"\s*:[^}]*\}\s*\}/g;
|
|
2599
|
+
function parseTextToolCalls(text) {
|
|
2600
|
+
const results = tryXmlBlocks(text);
|
|
2601
|
+
if (results.length > 0) return results;
|
|
2602
|
+
const jsonBlockResults = tryJsonCodeBlocks(text);
|
|
2603
|
+
if (jsonBlockResults.length > 0) return jsonBlockResults;
|
|
2604
|
+
return tryFunctionCallObjects(text);
|
|
2605
|
+
}
|
|
2606
|
+
function tryXmlBlocks(text) {
|
|
2607
|
+
const results = [];
|
|
2608
|
+
let match;
|
|
2609
|
+
TOOL_CALL_RE.lastIndex = 0;
|
|
2610
|
+
while ((match = TOOL_CALL_RE.exec(text)) !== null) {
|
|
2611
|
+
try {
|
|
2612
|
+
const raw = JSON.parse(match[1]);
|
|
2613
|
+
if (typeof raw.name !== "string") continue;
|
|
2614
|
+
const input = typeof raw.input === "object" && raw.input !== null ? raw.input : {};
|
|
2615
|
+
results.push({ name: raw.name, input });
|
|
2616
|
+
} catch {
|
|
2617
|
+
}
|
|
2618
|
+
}
|
|
2619
|
+
return results;
|
|
2620
|
+
}
|
|
2621
|
+
function tryJsonCodeBlocks(text) {
|
|
2622
|
+
const results = [];
|
|
2623
|
+
let match;
|
|
2624
|
+
JSON_BLOCK_RE.lastIndex = 0;
|
|
2625
|
+
while ((match = JSON_BLOCK_RE.exec(text)) !== null) {
|
|
2626
|
+
try {
|
|
2627
|
+
const raw = JSON.parse(match[1]);
|
|
2628
|
+
if (typeof raw.name !== "string") continue;
|
|
2629
|
+
const input = typeof raw.input === "object" && raw.input !== null ? raw.input : {};
|
|
2630
|
+
results.push({ name: raw.name, input });
|
|
2631
|
+
} catch {
|
|
2632
|
+
}
|
|
2633
|
+
}
|
|
2634
|
+
return results;
|
|
2635
|
+
}
|
|
2636
|
+
function tryFunctionCallObjects(text) {
|
|
2637
|
+
const results = [];
|
|
2638
|
+
let match;
|
|
2639
|
+
FUNCTION_OBJ_RE.lastIndex = 0;
|
|
2640
|
+
while ((match = FUNCTION_OBJ_RE.exec(text)) !== null) {
|
|
2641
|
+
try {
|
|
2642
|
+
const raw = JSON.parse(match[0]);
|
|
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 });
|
|
2647
|
+
} catch {
|
|
2648
|
+
}
|
|
2649
|
+
}
|
|
2650
|
+
return results;
|
|
2651
|
+
}
|
|
2652
|
+
function toToolCall(parsed, index) {
|
|
2653
|
+
return {
|
|
2654
|
+
id: `text-tool-${Date.now()}-${index}`,
|
|
2655
|
+
name: parsed.name,
|
|
2656
|
+
input: parsed.input
|
|
2657
|
+
};
|
|
2658
|
+
}
|
|
2659
|
+
function buildTextToolSystemPrompt(tools) {
|
|
2660
|
+
const toolDefs = tools.map((t) => {
|
|
2661
|
+
const props = t.inputSchema?.properties ?? {};
|
|
2662
|
+
const paramLines = Object.entries(props).map(([k, v]) => ` "${k}": "<${v.description ?? k}>"`);
|
|
2663
|
+
return `\u2022 ${t.name}: ${t.description}
|
|
2664
|
+
Input: {${paramLines.length ? "\n" + paramLines.join(",\n") + "\n " : ""}}`;
|
|
2665
|
+
}).join("\n");
|
|
2666
|
+
return `
|
|
2667
|
+
TOOL USE INSTRUCTIONS:
|
|
2668
|
+
You do not have native tool-use capability. To call a tool, write a <tool_call> block:
|
|
2669
|
+
|
|
2670
|
+
<tool_call>
|
|
2671
|
+
{"name": "<tool_name>", "input": {<parameters>}}
|
|
2672
|
+
</tool_call>
|
|
2673
|
+
|
|
2674
|
+
Available tools:
|
|
2675
|
+
${toolDefs}
|
|
2676
|
+
|
|
2677
|
+
EXAMPLE \u2014 calling the "shell" tool to list files:
|
|
2678
|
+
<tool_call>
|
|
2679
|
+
{"name": "shell", "input": {"command": "ls -la /workspace"}}
|
|
2680
|
+
</tool_call>
|
|
2681
|
+
|
|
2682
|
+
You will then receive a user message with the result, then continue your work.
|
|
2683
|
+
Only call one tool at a time. When you have enough information, provide your final answer.`;
|
|
2684
|
+
}
|
|
2685
|
+
|
|
2300
2686
|
// src/core/tiers/t3-worker.ts
|
|
2301
2687
|
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.
|
|
2302
2688
|
|
|
@@ -2498,6 +2884,9 @@ Now execute your subtask using this context where relevant.`
|
|
|
2498
2884
|
const MAX_ITERATIONS = 15;
|
|
2499
2885
|
const requiresArtifact = this.requiresArtifact();
|
|
2500
2886
|
tools = [...tools];
|
|
2887
|
+
const t3Model = this.router.getModelForTier("T3");
|
|
2888
|
+
const useTextTools = t3Model?.supportsToolUse === false && tools.length > 0;
|
|
2889
|
+
const textToolSuffix = useTextTools ? buildTextToolSystemPrompt(tools) : "";
|
|
2501
2890
|
while (iterations < MAX_ITERATIONS) {
|
|
2502
2891
|
iterations++;
|
|
2503
2892
|
this.throwIfCancelled();
|
|
@@ -2505,8 +2894,9 @@ Now execute your subtask using this context where relevant.`
|
|
|
2505
2894
|
messages: this.context.getMessages(),
|
|
2506
2895
|
systemPrompt: this.systemPromptOverride + systemPrompt + (this.hierarchyContext ? `
|
|
2507
2896
|
|
|
2508
|
-
HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
2509
|
-
|
|
2897
|
+
HIERARCHY CONTEXT: ${this.hierarchyContext}` : "") + textToolSuffix,
|
|
2898
|
+
// Don't pass tools array when model can't use them natively
|
|
2899
|
+
tools: useTextTools ? void 0 : tools.length ? tools : void 0,
|
|
2510
2900
|
maxTokens: 4096
|
|
2511
2901
|
};
|
|
2512
2902
|
const result = await this.router.generate(
|
|
@@ -2516,8 +2906,14 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
|
2516
2906
|
this.emit("stream:token", { tierId: this.id, text: chunk.text });
|
|
2517
2907
|
}
|
|
2518
2908
|
);
|
|
2519
|
-
|
|
2520
|
-
if (
|
|
2909
|
+
let effectiveToolCalls = result.toolCalls ?? [];
|
|
2910
|
+
if (useTextTools && effectiveToolCalls.length === 0) {
|
|
2911
|
+
const textCalls = parseTextToolCalls(result.content);
|
|
2912
|
+
effectiveToolCalls = textCalls.map((tc, i) => toToolCall(tc, i));
|
|
2913
|
+
}
|
|
2914
|
+
const effectiveResult = { ...result, toolCalls: effectiveToolCalls };
|
|
2915
|
+
await this.context.addMessage({ role: "assistant", content: result.content, toolCalls: effectiveToolCalls });
|
|
2916
|
+
if (!effectiveResult.toolCalls?.length) {
|
|
2521
2917
|
if (requiresArtifact) {
|
|
2522
2918
|
const artifactCheck = await this.verifyArtifacts(this.assignment);
|
|
2523
2919
|
if (artifactCheck.ok) {
|
|
@@ -2539,7 +2935,7 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
|
2539
2935
|
return { output: result.content, toolCalls: allToolCalls };
|
|
2540
2936
|
}
|
|
2541
2937
|
stalledArtifactIterations = 0;
|
|
2542
|
-
if (
|
|
2938
|
+
if (effectiveResult.finishReason === "stop" && effectiveResult.toolCalls.length === 0) {
|
|
2543
2939
|
if (requiresArtifact) {
|
|
2544
2940
|
const artifactCheck = await this.verifyArtifacts(this.assignment);
|
|
2545
2941
|
if (artifactCheck.ok) {
|
|
@@ -2549,7 +2945,7 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
|
2549
2945
|
return { output: result.content, toolCalls: allToolCalls };
|
|
2550
2946
|
}
|
|
2551
2947
|
}
|
|
2552
|
-
for (const tc of
|
|
2948
|
+
for (const tc of effectiveResult.toolCalls) {
|
|
2553
2949
|
allToolCalls.push(tc);
|
|
2554
2950
|
const toolResult = await this.executeTool(tc);
|
|
2555
2951
|
await this.context.addMessage({
|
|
@@ -2607,13 +3003,15 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
|
2607
3003
|
currentAction: `Using tool: ${tc.name}`,
|
|
2608
3004
|
status: "IN_PROGRESS"
|
|
2609
3005
|
});
|
|
3006
|
+
this.emit("tool:call", { id: tc.id, tierId: this.id, toolName: tc.name, input: tc.input });
|
|
3007
|
+
const toolStartMs = Date.now();
|
|
2610
3008
|
try {
|
|
2611
3009
|
const result = await this.toolRegistry.execute(tc.name, tc.input, {
|
|
2612
3010
|
tierId: this.id,
|
|
2613
3011
|
sessionId: this.taskId,
|
|
2614
3012
|
requireApproval: false,
|
|
2615
|
-
saveSnapshot: async (
|
|
2616
|
-
this.store?.addFileSnapshot(this.taskId,
|
|
3013
|
+
saveSnapshot: async (path17, content) => {
|
|
3014
|
+
this.store?.addFileSnapshot(this.taskId, path17, content);
|
|
2617
3015
|
},
|
|
2618
3016
|
sendPeerSync: (to, syncType, content) => {
|
|
2619
3017
|
this.peerBus?.send(this.id, to, syncType, this.assignment?.subtaskId ?? "", content);
|
|
@@ -2630,12 +3028,84 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
|
2630
3028
|
this.audit.fileChange(this.id, tc.input["path"] ?? "unknown", tc.name);
|
|
2631
3029
|
}
|
|
2632
3030
|
}
|
|
2633
|
-
|
|
3031
|
+
const durationMs = Date.now() - toolStartMs;
|
|
3032
|
+
this.emit("tool:result", { id: tc.id, tierId: this.id, toolName: tc.name, output: typeof result === "string" ? result : JSON.stringify(result), durationMs });
|
|
2634
3033
|
return typeof result === "string" ? result : JSON.stringify(result);
|
|
2635
3034
|
} catch (err) {
|
|
2636
|
-
|
|
3035
|
+
const durationMs = Date.now() - toolStartMs;
|
|
3036
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
3037
|
+
this.emit("tool:result", { id: tc.id, tierId: this.id, toolName: tc.name, error: errMsg, durationMs });
|
|
3038
|
+
return `Tool error: ${errMsg}`;
|
|
2637
3039
|
}
|
|
2638
3040
|
}
|
|
3041
|
+
/**
|
|
3042
|
+
* Adaptive fallback cascade — invoked when executeTool() fails.
|
|
3043
|
+
* Strategy order:
|
|
3044
|
+
* 1. Find a semantically similar registered tool and retry with same input
|
|
3045
|
+
* 2. Synthesize a new tool via ToolCreator (if available) and run it
|
|
3046
|
+
* 3. Return the original error so the agent loop can decide what to do next
|
|
3047
|
+
*/
|
|
3048
|
+
async adaptiveFallback(tc, originalError) {
|
|
3049
|
+
const altTool = this.findAlternativeTool(tc.name);
|
|
3050
|
+
if (altTool) {
|
|
3051
|
+
this.log(`Adaptive fallback: trying alternative tool "${altTool}" for failed "${tc.name}"`);
|
|
3052
|
+
this.sendStatusUpdate({ progressPct: 50, currentAction: `Fallback: trying ${altTool}`, status: "IN_PROGRESS" });
|
|
3053
|
+
try {
|
|
3054
|
+
const result = await this.toolRegistry.execute(altTool, tc.input, {
|
|
3055
|
+
tierId: this.id,
|
|
3056
|
+
sessionId: this.taskId,
|
|
3057
|
+
requireApproval: false
|
|
3058
|
+
});
|
|
3059
|
+
const str = typeof result === "string" ? result : JSON.stringify(result);
|
|
3060
|
+
if (!str.startsWith("Tool error:") && !str.startsWith("Error:")) {
|
|
3061
|
+
return `[Fallback via ${altTool}]: ${str}`;
|
|
3062
|
+
}
|
|
3063
|
+
} catch {
|
|
3064
|
+
}
|
|
3065
|
+
}
|
|
3066
|
+
if (this.toolCreator) {
|
|
3067
|
+
this.log(`Adaptive fallback: requesting dynamic tool synthesis for "${tc.name}"`);
|
|
3068
|
+
this.sendStatusUpdate({ progressPct: 55, currentAction: `Synthesizing fallback tool for: ${tc.name}`, status: "IN_PROGRESS" });
|
|
3069
|
+
try {
|
|
3070
|
+
const newToolName = await this.toolCreator.createTool(
|
|
3071
|
+
`Replacement for "${tc.name}" \u2014 original failed with: ${originalError.slice(0, 150)}`,
|
|
3072
|
+
this.assignment?.subtaskTitle ?? tc.name
|
|
3073
|
+
);
|
|
3074
|
+
if (newToolName) {
|
|
3075
|
+
this.log(`Adaptive fallback: synthesized "${newToolName}", retrying`);
|
|
3076
|
+
const result = await this.toolRegistry.execute(newToolName, tc.input, {
|
|
3077
|
+
tierId: this.id,
|
|
3078
|
+
sessionId: this.taskId,
|
|
3079
|
+
requireApproval: false
|
|
3080
|
+
});
|
|
3081
|
+
const str = typeof result === "string" ? result : JSON.stringify(result);
|
|
3082
|
+
if (!str.startsWith("Tool error:")) return `[Synthesized ${newToolName}]: ${str}`;
|
|
3083
|
+
}
|
|
3084
|
+
} catch {
|
|
3085
|
+
}
|
|
3086
|
+
}
|
|
3087
|
+
return originalError;
|
|
3088
|
+
}
|
|
3089
|
+
/**
|
|
3090
|
+
* Find a registered tool whose name/description semantically overlaps with
|
|
3091
|
+
* the failing tool. Returns the best candidate name, or null if none found.
|
|
3092
|
+
*/
|
|
3093
|
+
findAlternativeTool(failedToolName) {
|
|
3094
|
+
const failedKeywords = failedToolName.toLowerCase().split(/[_\-\s]+/);
|
|
3095
|
+
const allTools = this.toolRegistry.getToolDefinitions();
|
|
3096
|
+
let bestScore = 0;
|
|
3097
|
+
let bestName = null;
|
|
3098
|
+
for (const tool of allTools) {
|
|
3099
|
+
if (tool.name === failedToolName) continue;
|
|
3100
|
+
const toolWords = tool.name.toLowerCase().split(/[_\-\s]+/);
|
|
3101
|
+
const score = failedKeywords.filter((k) => toolWords.includes(k)).length;
|
|
3102
|
+
if (score > bestScore && score >= 1) {
|
|
3103
|
+
bestScore = score;
|
|
3104
|
+
bestName = tool.name;
|
|
3105
|
+
}
|
|
3106
|
+
}
|
|
3107
|
+
return bestName;
|
|
3108
|
+
}
|
|
2639
3109
|
/**
|
|
2640
3110
|
* Announce which files this T3 plans to edit, then acquire locks on them
|
|
2641
3111
|
* before competing siblings can claim them. T3s working on different files
|
|
@@ -2694,12 +3164,12 @@ ${assignment.expectedOutput}`;
|
|
|
2694
3164
|
if (!artifactPaths.length) return { ok: true, issues: [] };
|
|
2695
3165
|
const issues = [];
|
|
2696
3166
|
const { exec: exec3 } = await import('child_process');
|
|
2697
|
-
const { promisify:
|
|
2698
|
-
const execAsync2 =
|
|
3167
|
+
const { promisify: promisify4 } = await import('util');
|
|
3168
|
+
const execAsync2 = promisify4(exec3);
|
|
2699
3169
|
for (const artifactPath of artifactPaths) {
|
|
2700
|
-
const absolutePath =
|
|
3170
|
+
const absolutePath = path16__default.default.resolve(process.cwd(), artifactPath);
|
|
2701
3171
|
try {
|
|
2702
|
-
const stat = await
|
|
3172
|
+
const stat = await fs3__default.default.stat(absolutePath);
|
|
2703
3173
|
if (!stat.isFile()) {
|
|
2704
3174
|
issues.push(`Expected artifact is not a file: ${artifactPath}`);
|
|
2705
3175
|
continue;
|
|
@@ -2709,7 +3179,7 @@ ${assignment.expectedOutput}`;
|
|
|
2709
3179
|
continue;
|
|
2710
3180
|
}
|
|
2711
3181
|
if (!/\.pdf$/i.test(artifactPath)) {
|
|
2712
|
-
const content = await
|
|
3182
|
+
const content = await fs3__default.default.readFile(absolutePath, "utf-8");
|
|
2713
3183
|
if (!content.trim()) {
|
|
2714
3184
|
issues.push(`Artifact content is empty: ${artifactPath}`);
|
|
2715
3185
|
continue;
|
|
@@ -2718,7 +3188,7 @@ ${assignment.expectedOutput}`;
|
|
|
2718
3188
|
issues.push(`PDF artifact looks too small to be valid: ${artifactPath}`);
|
|
2719
3189
|
continue;
|
|
2720
3190
|
}
|
|
2721
|
-
const ext =
|
|
3191
|
+
const ext = path16__default.default.extname(absolutePath).toLowerCase();
|
|
2722
3192
|
try {
|
|
2723
3193
|
if (ext === ".ts" || ext === ".tsx") {
|
|
2724
3194
|
await execAsync2(`npx tsc --noEmit ${absolutePath}`, { timeout: 1e4 });
|
|
@@ -2836,6 +3306,11 @@ var PeerBus = class extends EventEmitter__default.default {
|
|
|
2836
3306
|
barriers = /* @__PURE__ */ new Map();
|
|
2837
3307
|
broadcastLog = [];
|
|
2838
3308
|
fileLocks = /* @__PURE__ */ new Map();
|
|
3309
|
+
/** subtaskIds whose T3 is being retried by T2 — dependents should re-wait rather than fail fast */
|
|
3310
|
+
retryPending = /* @__PURE__ */ new Set();
|
|
3311
|
+
/** Called when any peer message or broadcast is sent — used for dashboard visibility. */
|
|
3312
|
+
onPeerMessage;
|
|
3313
|
+
sessionId = "";
|
|
2839
3314
|
register(peerId) {
|
|
2840
3315
|
this.members.add(peerId);
|
|
2841
3316
|
}
|
|
@@ -2857,11 +3332,33 @@ var PeerBus = class extends EventEmitter__default.default {
|
|
|
2857
3332
|
this.waiters.delete(subtaskId);
|
|
2858
3333
|
}
|
|
2859
3334
|
/**
|
|
2860
|
-
*
|
|
3335
|
+
* Mark a subtask as retry-pending so dependents re-wait instead of failing fast
|
|
3336
|
+
* when they see an ESCALATED status.
|
|
3337
|
+
*/
|
|
3338
|
+
markRetryPending(subtaskId) {
|
|
3339
|
+
this.retryPending.add(subtaskId);
|
|
3340
|
+
this.outputs.delete(subtaskId);
|
|
3341
|
+
}
|
|
3342
|
+
/** Called by T2 after retry resolves (success or final failure). */
|
|
3343
|
+
clearRetryPending(subtaskId) {
|
|
3344
|
+
this.retryPending.delete(subtaskId);
|
|
3345
|
+
}
|
|
3346
|
+
/** Remove a single output entry so a respawned worker can republish without clearing prior-wave outputs. */
|
|
3347
|
+
clearOutput(subtaskId) {
|
|
3348
|
+
this.outputs.delete(subtaskId);
|
|
3349
|
+
this.waiters.delete(subtaskId);
|
|
3350
|
+
this.retryPending.delete(subtaskId);
|
|
3351
|
+
}
|
|
3352
|
+
isRetryPending(subtaskId) {
|
|
3353
|
+
return this.retryPending.has(subtaskId);
|
|
3354
|
+
}
|
|
3355
|
+
/**
|
|
3356
|
+
* Wait for a specific subtask's output — resolves immediately if already available.
|
|
3357
|
+
* If the output is ESCALATED but a retry is pending, waits for the retry result.
|
|
2861
3358
|
*/
|
|
2862
3359
|
waitFor(subtaskId, timeoutMs = 12e4) {
|
|
2863
3360
|
const existing = this.outputs.get(subtaskId);
|
|
2864
|
-
if (existing) return Promise.resolve(existing);
|
|
3361
|
+
if (existing && !this.retryPending.has(subtaskId)) return Promise.resolve(existing);
|
|
2865
3362
|
return new Promise((resolve, reject) => {
|
|
2866
3363
|
const resolver = (output) => {
|
|
2867
3364
|
clearTimeout(timer);
|
|
@@ -2892,6 +3389,7 @@ var PeerBus = class extends EventEmitter__default.default {
|
|
|
2892
3389
|
* Also logs to broadcastLog so collect() can retrieve recent broadcasts.
|
|
2893
3390
|
*/
|
|
2894
3391
|
broadcast(fromId, payload) {
|
|
3392
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
2895
3393
|
const msg = {
|
|
2896
3394
|
fromId,
|
|
2897
3395
|
toId: "*",
|
|
@@ -2899,10 +3397,18 @@ var PeerBus = class extends EventEmitter__default.default {
|
|
|
2899
3397
|
subtaskId: "",
|
|
2900
3398
|
syncType: "SHARE_OUTPUT",
|
|
2901
3399
|
payload,
|
|
2902
|
-
timestamp
|
|
3400
|
+
timestamp
|
|
2903
3401
|
};
|
|
2904
|
-
this.broadcastLog.push({ fromId, payload, timestamp
|
|
3402
|
+
this.broadcastLog.push({ fromId, payload, timestamp });
|
|
2905
3403
|
this.emit("broadcast", msg);
|
|
3404
|
+
this.onPeerMessage?.({
|
|
3405
|
+
fromId,
|
|
3406
|
+
toId: void 0,
|
|
3407
|
+
syncType: "SHARE_OUTPUT",
|
|
3408
|
+
payload: typeof payload === "string" ? payload : JSON.stringify(payload),
|
|
3409
|
+
timestamp,
|
|
3410
|
+
sessionId: this.sessionId
|
|
3411
|
+
});
|
|
2906
3412
|
}
|
|
2907
3413
|
/**
|
|
2908
3414
|
* Collect all broadcast messages received within a time window.
|
|
@@ -2988,6 +3494,16 @@ var PeerBus = class extends EventEmitter__default.default {
|
|
|
2988
3494
|
isFileLocked(filePath) {
|
|
2989
3495
|
return this.fileLocks.has(filePath);
|
|
2990
3496
|
}
|
|
3497
|
+
/**
|
|
3498
|
+
* Reset all runtime output/waiter state for a fresh T3 respawn wave.
|
|
3499
|
+
* Preserves member registrations and barrier definitions.
|
|
3500
|
+
*/
|
|
3501
|
+
reset() {
|
|
3502
|
+
this.outputs.clear();
|
|
3503
|
+
this.waiters.clear();
|
|
3504
|
+
this.retryPending.clear();
|
|
3505
|
+
this.broadcastLog = [];
|
|
3506
|
+
}
|
|
2991
3507
|
/**
|
|
2992
3508
|
* Clear broadcast log — call between phases to avoid stale announcements.
|
|
2993
3509
|
*/
|
|
@@ -2998,6 +3514,7 @@ var PeerBus = class extends EventEmitter__default.default {
|
|
|
2998
3514
|
* Send a targeted message to a specific peer
|
|
2999
3515
|
*/
|
|
3000
3516
|
send(fromId, toId, syncType, subtaskId, payload) {
|
|
3517
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
3001
3518
|
const msg = {
|
|
3002
3519
|
fromId,
|
|
3003
3520
|
toId,
|
|
@@ -3005,10 +3522,18 @@ var PeerBus = class extends EventEmitter__default.default {
|
|
|
3005
3522
|
subtaskId,
|
|
3006
3523
|
syncType,
|
|
3007
3524
|
payload,
|
|
3008
|
-
timestamp
|
|
3525
|
+
timestamp
|
|
3009
3526
|
};
|
|
3010
3527
|
this.emit(`message:${toId}`, msg);
|
|
3011
3528
|
this.emit("message", msg);
|
|
3529
|
+
this.onPeerMessage?.({
|
|
3530
|
+
fromId,
|
|
3531
|
+
toId,
|
|
3532
|
+
syncType: syncType ?? "SHARE_OUTPUT",
|
|
3533
|
+
payload: typeof payload === "string" ? payload : JSON.stringify(payload),
|
|
3534
|
+
timestamp,
|
|
3535
|
+
sessionId: this.sessionId
|
|
3536
|
+
});
|
|
3012
3537
|
}
|
|
3013
3538
|
/**
|
|
3014
3539
|
* Barrier — wait until N peers have all reached this point
|
|
@@ -3061,6 +3586,8 @@ var T2Manager = class extends BaseTier {
|
|
|
3061
3586
|
t2PeerBus;
|
|
3062
3587
|
permissionEscalator;
|
|
3063
3588
|
toolCreator;
|
|
3589
|
+
/** AbortController for the current T3 wave — aborted on cancel-and-respawn */
|
|
3590
|
+
waveAbortController = null;
|
|
3064
3591
|
setPeerBus(bus) {
|
|
3065
3592
|
this.t2PeerBus = bus;
|
|
3066
3593
|
this.t2PeerBus.register(this.id);
|
|
@@ -3069,6 +3596,14 @@ var T2Manager = class extends BaseTier {
|
|
|
3069
3596
|
this.receivePeerSync(msg.fromId, msg.payload);
|
|
3070
3597
|
});
|
|
3071
3598
|
}
|
|
3599
|
+
setPeerMessageCallback(cb, sessionId) {
|
|
3600
|
+
this.t3PeerBus.onPeerMessage = cb;
|
|
3601
|
+
this.t3PeerBus.sessionId = sessionId;
|
|
3602
|
+
if (this.t2PeerBus) {
|
|
3603
|
+
this.t2PeerBus.onPeerMessage = cb;
|
|
3604
|
+
this.t2PeerBus.sessionId = sessionId;
|
|
3605
|
+
}
|
|
3606
|
+
}
|
|
3072
3607
|
constructor(router, toolRegistry, parentId) {
|
|
3073
3608
|
super("T2", void 0, parentId);
|
|
3074
3609
|
this.router = router;
|
|
@@ -3236,6 +3771,26 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
|
3236
3771
|
}];
|
|
3237
3772
|
}
|
|
3238
3773
|
}
|
|
3774
|
+
buildWorkerMap(assignments, taskId) {
|
|
3775
|
+
const workerMap = /* @__PURE__ */ new Map();
|
|
3776
|
+
for (const a of assignments) {
|
|
3777
|
+
const worker = new T3Worker(this.router, this.toolRegistry, this.id);
|
|
3778
|
+
if (this.store) worker.setStore(this.store, taskId);
|
|
3779
|
+
worker.setPeerBus(this.t3PeerBus);
|
|
3780
|
+
if (this.permissionEscalator) worker.setPermissionEscalator(this.permissionEscalator);
|
|
3781
|
+
if (this.toolCreator) worker.setToolCreator(this.toolCreator);
|
|
3782
|
+
workerMap.set(a.subtaskId, worker);
|
|
3783
|
+
this.t3Workers.set(a.subtaskId, worker);
|
|
3784
|
+
worker.on("stream:token", (e) => this.emit("stream:token", e));
|
|
3785
|
+
worker.on("log", (e) => this.emit("log", e));
|
|
3786
|
+
worker.on("tier:status", (e) => this.emit("tier:status", e));
|
|
3787
|
+
worker.on("tool:approval-request", (e) => this.emit("tool:approval-request", {
|
|
3788
|
+
...e,
|
|
3789
|
+
__cascadeResponder: (decision) => worker.emit(`tool:approval-response:${e.id}`, decision)
|
|
3790
|
+
}));
|
|
3791
|
+
}
|
|
3792
|
+
return workerMap;
|
|
3793
|
+
}
|
|
3239
3794
|
async executeSubtasks(subtasks, taskId) {
|
|
3240
3795
|
const assignments = subtasks.map((s) => ({
|
|
3241
3796
|
...s,
|
|
@@ -3262,6 +3817,8 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
|
3262
3817
|
worker.on("stream:token", (e) => this.emit("stream:token", e));
|
|
3263
3818
|
worker.on("log", (e) => this.emit("log", e));
|
|
3264
3819
|
worker.on("tier:status", (e) => this.emit("tier:status", e));
|
|
3820
|
+
worker.on("tool:call", (e) => this.emit("tool:call", e));
|
|
3821
|
+
worker.on("tool:result", (e) => this.emit("tool:result", e));
|
|
3265
3822
|
worker.on("tool:approval-request", (e) => this.emit("tool:approval-request", {
|
|
3266
3823
|
...e,
|
|
3267
3824
|
__cascadeResponder: (decision) => worker.emit(`tool:approval-response:${e.id}`, decision)
|
|
@@ -3298,6 +3855,7 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
|
3298
3855
|
const sanitizedAssignments = this.breakCycles(assignments, adj, inDegree);
|
|
3299
3856
|
let remaining = new Set(sanitizedAssignments.map((a) => a.subtaskId));
|
|
3300
3857
|
let wave = 0;
|
|
3858
|
+
let respawnBudget = 1;
|
|
3301
3859
|
while (remaining.size > 0) {
|
|
3302
3860
|
const runnableIds = [...remaining].filter((id) => (inDegree.get(id) ?? 0) === 0);
|
|
3303
3861
|
if (runnableIds.length === 0) {
|
|
@@ -3318,15 +3876,62 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
|
3318
3876
|
status: "IN_PROGRESS"
|
|
3319
3877
|
});
|
|
3320
3878
|
this.throwIfCancelled();
|
|
3879
|
+
this.waveAbortController = new AbortController();
|
|
3880
|
+
const waveSignal = AbortSignal.any(
|
|
3881
|
+
[this.signal, this.waveAbortController.signal].filter(Boolean)
|
|
3882
|
+
);
|
|
3321
3883
|
const waveResults = await Promise.allSettled(
|
|
3322
3884
|
runnableIds.map(async (id) => {
|
|
3323
3885
|
const assignment = sanitizedAssignments.find((a) => a.subtaskId === id);
|
|
3324
3886
|
const worker = workerMap.get(id);
|
|
3325
|
-
const result = await worker.execute(assignment, taskId,
|
|
3887
|
+
const result = await worker.execute(assignment, taskId, waveSignal);
|
|
3326
3888
|
resultMap.set(id, result);
|
|
3327
3889
|
return result;
|
|
3328
3890
|
})
|
|
3329
3891
|
);
|
|
3892
|
+
const escalatedToolIdx = respawnBudget > 0 ? waveResults.findIndex(
|
|
3893
|
+
(r) => r.status === "fulfilled" && r.value.status === "ESCALATED" && r.value.issues.some((iss) => iss.includes("dynamic tool generation"))
|
|
3894
|
+
) : -1;
|
|
3895
|
+
if (escalatedToolIdx !== -1 && this.toolCreator) {
|
|
3896
|
+
respawnBudget--;
|
|
3897
|
+
this.waveAbortController.abort();
|
|
3898
|
+
const escalatedId = runnableIds[escalatedToolIdx];
|
|
3899
|
+
const escalatedAssignment = sanitizedAssignments.find((a) => a.subtaskId === escalatedId);
|
|
3900
|
+
this.log(`Wave ${wave}: tool escalation detected \u2014 synthesizing tool then respawning all ${runnableIds.length} worker(s)`);
|
|
3901
|
+
this.sendStatusUpdate({
|
|
3902
|
+
progressPct: 50,
|
|
3903
|
+
currentAction: `Synthesizing dynamic tool for: ${escalatedAssignment.subtaskTitle}`,
|
|
3904
|
+
status: "IN_PROGRESS"
|
|
3905
|
+
});
|
|
3906
|
+
const toolName = await this.toolCreator.createTool(
|
|
3907
|
+
`Help complete: ${escalatedAssignment.subtaskTitle}`,
|
|
3908
|
+
escalatedAssignment.description
|
|
3909
|
+
);
|
|
3910
|
+
if (toolName) {
|
|
3911
|
+
this.log(`Tool "${toolName}" created \u2014 respawning wave ${wave} workers`);
|
|
3912
|
+
for (const a of sanitizedAssignments) {
|
|
3913
|
+
if (runnableIds.includes(a.subtaskId)) {
|
|
3914
|
+
a.description += `
|
|
3915
|
+
|
|
3916
|
+
[SYSTEM]: Dynamic tool "${toolName}" is now available \u2014 use it to complete your task.`;
|
|
3917
|
+
}
|
|
3918
|
+
}
|
|
3919
|
+
}
|
|
3920
|
+
for (const id of runnableIds) {
|
|
3921
|
+
this.t3PeerBus.clearOutput(id);
|
|
3922
|
+
}
|
|
3923
|
+
const freshMap = this.buildWorkerMap(
|
|
3924
|
+
sanitizedAssignments.filter((a) => runnableIds.includes(a.subtaskId)),
|
|
3925
|
+
taskId
|
|
3926
|
+
);
|
|
3927
|
+
for (const [k, v] of freshMap) workerMap.set(k, v);
|
|
3928
|
+
for (const id of runnableIds) {
|
|
3929
|
+
remaining.add(id);
|
|
3930
|
+
inDegree.set(id, 0);
|
|
3931
|
+
}
|
|
3932
|
+
wave--;
|
|
3933
|
+
continue;
|
|
3934
|
+
}
|
|
3330
3935
|
for (let i = 0; i < runnableIds.length; i++) {
|
|
3331
3936
|
const id = runnableIds[i];
|
|
3332
3937
|
remaining.delete(id);
|
|
@@ -3334,61 +3939,22 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
|
3334
3939
|
if (r.status === "rejected") {
|
|
3335
3940
|
this.log(`T3 worker ${id} failed: ${r.reason instanceof Error ? r.reason.message : String(r.reason)} \u2014 retrying once`);
|
|
3336
3941
|
const assignment = sanitizedAssignments.find((a) => a.subtaskId === id);
|
|
3337
|
-
|
|
3338
|
-
|
|
3339
|
-
|
|
3340
|
-
|
|
3341
|
-
|
|
3342
|
-
this.log(`T3
|
|
3343
|
-
this.
|
|
3344
|
-
|
|
3345
|
-
|
|
3346
|
-
status: "
|
|
3942
|
+
try {
|
|
3943
|
+
const retried = await this.retryT3(assignment, taskId);
|
|
3944
|
+
resultMap.set(id, retried);
|
|
3945
|
+
} catch (retryErr) {
|
|
3946
|
+
const msg = retryErr instanceof Error ? retryErr.message : String(retryErr);
|
|
3947
|
+
this.log(`T3 retry for ${id} threw before publishing \u2014 unblocking dependents with FAILED`);
|
|
3948
|
+
this.t3PeerBus.publish(this.id, id, `Retry failed: ${msg}`, "FAILED");
|
|
3949
|
+
resultMap.set(id, {
|
|
3950
|
+
subtaskId: id,
|
|
3951
|
+
status: "FAILED",
|
|
3952
|
+
output: `Retry threw: ${msg}`,
|
|
3953
|
+
testResults: { checksRun: [], passed: [], failed: [] },
|
|
3954
|
+
issues: [msg],
|
|
3955
|
+
peerSyncsUsed: [],
|
|
3956
|
+
correctionAttempts: 1
|
|
3347
3957
|
});
|
|
3348
|
-
const toolName = await this.toolCreator.createTool(
|
|
3349
|
-
`Help complete: ${assignment.subtaskTitle}`,
|
|
3350
|
-
assignment.description
|
|
3351
|
-
);
|
|
3352
|
-
if (toolName) {
|
|
3353
|
-
this.log(`T2 verifying new tool: ${toolName}`);
|
|
3354
|
-
this.sendStatusUpdate({
|
|
3355
|
-
progressPct: 60,
|
|
3356
|
-
currentAction: `T2 Verifying new tool: ${toolName}`,
|
|
3357
|
-
status: "IN_PROGRESS"
|
|
3358
|
-
});
|
|
3359
|
-
try {
|
|
3360
|
-
const verifyResult = await this.router.generate("T2", {
|
|
3361
|
-
messages: [{ role: "user", content: `A new tool named "${toolName}" was just created dynamically to help with: ${assignment.description}. Based on its name and purpose, does this seem like a valid addition? Reply "VERIFIED" or "REJECTED".` }],
|
|
3362
|
-
systemPrompt: this.systemPromptOverride + "You are T2 Manager verifying a dynamic tool.",
|
|
3363
|
-
maxTokens: 50
|
|
3364
|
-
});
|
|
3365
|
-
if (!verifyResult.content.toUpperCase().includes("REJECTED")) {
|
|
3366
|
-
this.log(`T2 verification passed for ${toolName}. Restarting original T3.`);
|
|
3367
|
-
const retried = await this.retryT3({
|
|
3368
|
-
...assignment,
|
|
3369
|
-
description: `${assignment.description}
|
|
3370
|
-
|
|
3371
|
-
[SYSTEM NOTIFICATION]: A new dynamic tool "${toolName}" has been built and verified for you. Use it to complete your task.`
|
|
3372
|
-
}, taskId);
|
|
3373
|
-
resultMap.set(id, retried);
|
|
3374
|
-
} else {
|
|
3375
|
-
this.log(`T2 rejected the dynamic tool: ${toolName}`);
|
|
3376
|
-
resultMap.set(id, r.value);
|
|
3377
|
-
}
|
|
3378
|
-
} catch {
|
|
3379
|
-
const retried = await this.retryT3({
|
|
3380
|
-
...assignment,
|
|
3381
|
-
description: `${assignment.description}
|
|
3382
|
-
|
|
3383
|
-
[SYSTEM NOTIFICATION]: A new dynamic tool "${toolName}" has been built for you. Use it to complete your task.`
|
|
3384
|
-
}, taskId);
|
|
3385
|
-
resultMap.set(id, retried);
|
|
3386
|
-
}
|
|
3387
|
-
} else {
|
|
3388
|
-
resultMap.set(id, r.value);
|
|
3389
|
-
}
|
|
3390
|
-
} else {
|
|
3391
|
-
resultMap.set(id, r.value);
|
|
3392
3958
|
}
|
|
3393
3959
|
}
|
|
3394
3960
|
for (const dependent of adj.get(id) ?? []) {
|
|
@@ -3659,6 +4225,8 @@ var T1Administrator = class extends BaseTier {
|
|
|
3659
4225
|
toolCreator;
|
|
3660
4226
|
/** Stored overall task goal — used when evaluating escalated permissions */
|
|
3661
4227
|
taskGoal = "";
|
|
4228
|
+
peerMessageCallback;
|
|
4229
|
+
peerMessageSessionId = "";
|
|
3662
4230
|
constructor(router, toolRegistry, config) {
|
|
3663
4231
|
super("T1", "T1");
|
|
3664
4232
|
this.router = router;
|
|
@@ -3679,6 +4247,12 @@ var T1Administrator = class extends BaseTier {
|
|
|
3679
4247
|
setToolCreator(creator) {
|
|
3680
4248
|
this.toolCreator = creator;
|
|
3681
4249
|
}
|
|
4250
|
+
setPeerMessageCallback(cb, sessionId) {
|
|
4251
|
+
this.peerMessageCallback = cb;
|
|
4252
|
+
this.peerMessageSessionId = sessionId;
|
|
4253
|
+
this.t2PeerBus.onPeerMessage = cb;
|
|
4254
|
+
this.t2PeerBus.sessionId = sessionId;
|
|
4255
|
+
}
|
|
3682
4256
|
async execute(userPrompt, images, systemContext, signal) {
|
|
3683
4257
|
this.signal = signal;
|
|
3684
4258
|
this.taskId = crypto.randomUUID();
|
|
@@ -3898,6 +4472,9 @@ Leave dependsOn empty for sections that can run immediately in parallel.`;
|
|
|
3898
4472
|
manager.setStore(this.store);
|
|
3899
4473
|
}
|
|
3900
4474
|
manager.setPeerBus(this.t2PeerBus);
|
|
4475
|
+
if (this.peerMessageCallback) {
|
|
4476
|
+
manager.setPeerMessageCallback(this.peerMessageCallback, this.peerMessageSessionId);
|
|
4477
|
+
}
|
|
3901
4478
|
if (this.permissionEscalator) {
|
|
3902
4479
|
manager.setPermissionEscalator(this.permissionEscalator);
|
|
3903
4480
|
}
|
|
@@ -3908,6 +4485,8 @@ Leave dependsOn empty for sections that can run immediately in parallel.`;
|
|
|
3908
4485
|
bind(manager, "stream:token", (e) => this.emit("stream:token", e));
|
|
3909
4486
|
bind(manager, "log", (e) => this.emit("log", e));
|
|
3910
4487
|
bind(manager, "tier:status", (e) => this.emit("tier:status", e));
|
|
4488
|
+
bind(manager, "tool:call", (e) => this.emit("tool:call", e));
|
|
4489
|
+
bind(manager, "tool:result", (e) => this.emit("tool:result", e));
|
|
3911
4490
|
bind(manager, "tool:approval-request", (e) => this.emit("tool:approval-request", e));
|
|
3912
4491
|
bind(manager, "message", (msg) => {
|
|
3913
4492
|
if (msg.type === "PEER_SYNC") {
|
|
@@ -4267,13 +4846,21 @@ function resolveInWorkspace(workspaceRoot, input) {
|
|
|
4267
4846
|
if (typeof input !== "string" || input.length === 0) {
|
|
4268
4847
|
throw new WorkspaceSandboxError(String(input), workspaceRoot);
|
|
4269
4848
|
}
|
|
4270
|
-
const root =
|
|
4271
|
-
const abs =
|
|
4272
|
-
const rel =
|
|
4273
|
-
if (rel === "" || rel === ".")
|
|
4274
|
-
if (rel.startsWith("..") || path13__default.default.isAbsolute(rel)) {
|
|
4849
|
+
const root = path16__default.default.resolve(workspaceRoot);
|
|
4850
|
+
const abs = path16__default.default.isAbsolute(input) ? path16__default.default.resolve(input) : path16__default.default.resolve(root, input);
|
|
4851
|
+
const rel = path16__default.default.relative(root, abs);
|
|
4852
|
+
if (rel === "" || rel === ".") ; else if (rel.startsWith("..") || path16__default.default.isAbsolute(rel)) {
|
|
4275
4853
|
throw new WorkspaceSandboxError(input, root);
|
|
4276
4854
|
}
|
|
4855
|
+
try {
|
|
4856
|
+
const real = fs15__default.default.realpathSync(abs);
|
|
4857
|
+
const realRel = path16__default.default.relative(root, real);
|
|
4858
|
+
if (realRel !== "" && realRel !== "." && (realRel.startsWith("..") || path16__default.default.isAbsolute(realRel))) {
|
|
4859
|
+
throw new WorkspaceSandboxError(input, root);
|
|
4860
|
+
}
|
|
4861
|
+
} catch (e) {
|
|
4862
|
+
if (e instanceof WorkspaceSandboxError) throw e;
|
|
4863
|
+
}
|
|
4277
4864
|
return abs;
|
|
4278
4865
|
}
|
|
4279
4866
|
|
|
@@ -4295,7 +4882,7 @@ var FileReadTool = class extends BaseTool {
|
|
|
4295
4882
|
const absPath = resolveInWorkspace(this.workspaceRoot, filePath);
|
|
4296
4883
|
const offset = input["offset"] ?? 1;
|
|
4297
4884
|
const limit = input["limit"];
|
|
4298
|
-
const content = await
|
|
4885
|
+
const content = await fs3__default.default.readFile(absPath, "utf-8");
|
|
4299
4886
|
const lines = content.split("\n");
|
|
4300
4887
|
const start = Math.max(0, offset - 1);
|
|
4301
4888
|
const end = limit ? start + limit : lines.length;
|
|
@@ -4324,13 +4911,13 @@ var FileWriteTool = class extends BaseTool {
|
|
|
4324
4911
|
const content = input["content"];
|
|
4325
4912
|
if (options.saveSnapshot) {
|
|
4326
4913
|
try {
|
|
4327
|
-
const oldContent = await
|
|
4914
|
+
const oldContent = await fs3__default.default.readFile(absPath, "utf-8");
|
|
4328
4915
|
await options.saveSnapshot(absPath, oldContent);
|
|
4329
4916
|
} catch {
|
|
4330
4917
|
}
|
|
4331
4918
|
}
|
|
4332
|
-
await
|
|
4333
|
-
await
|
|
4919
|
+
await fs3__default.default.mkdir(path16__default.default.dirname(absPath), { recursive: true });
|
|
4920
|
+
await fs3__default.default.writeFile(absPath, content, "utf-8");
|
|
4334
4921
|
return `Written ${content.length} characters to ${filePath}`;
|
|
4335
4922
|
}
|
|
4336
4923
|
};
|
|
@@ -4356,7 +4943,7 @@ var FileEditTool = class extends BaseTool {
|
|
|
4356
4943
|
const oldString = input["old_string"];
|
|
4357
4944
|
const newString = input["new_string"];
|
|
4358
4945
|
const replaceAll = input["replace_all"] ?? false;
|
|
4359
|
-
const rawContent = await
|
|
4946
|
+
const rawContent = await fs3__default.default.readFile(absPath, "utf-8");
|
|
4360
4947
|
if (options.saveSnapshot) {
|
|
4361
4948
|
await options.saveSnapshot(absPath, rawContent);
|
|
4362
4949
|
}
|
|
@@ -4368,7 +4955,7 @@ var FileEditTool = class extends BaseTool {
|
|
|
4368
4955
|
);
|
|
4369
4956
|
}
|
|
4370
4957
|
const updated = replaceAll ? content.split(normalizedOld).join(newString) : content.replace(normalizedOld, newString);
|
|
4371
|
-
await
|
|
4958
|
+
await fs3__default.default.writeFile(absPath, updated, "utf-8");
|
|
4372
4959
|
const count = replaceAll ? content.split(normalizedOld).length - 1 : 1;
|
|
4373
4960
|
return `Replaced ${count} occurrence(s) in ${filePath}`;
|
|
4374
4961
|
}
|
|
@@ -4391,12 +4978,12 @@ var FileDeleteTool = class extends BaseTool {
|
|
|
4391
4978
|
const absPath = resolveInWorkspace(this.workspaceRoot, filePath);
|
|
4392
4979
|
if (options.saveSnapshot) {
|
|
4393
4980
|
try {
|
|
4394
|
-
const oldContent = await
|
|
4981
|
+
const oldContent = await fs3__default.default.readFile(absPath, "utf-8");
|
|
4395
4982
|
await options.saveSnapshot(absPath, oldContent);
|
|
4396
4983
|
} catch {
|
|
4397
4984
|
}
|
|
4398
4985
|
}
|
|
4399
|
-
await
|
|
4986
|
+
await fs3__default.default.rm(absPath, { recursive: false });
|
|
4400
4987
|
return `Deleted ${filePath}`;
|
|
4401
4988
|
}
|
|
4402
4989
|
};
|
|
@@ -4413,7 +5000,7 @@ var FileListTool = class extends BaseTool {
|
|
|
4413
5000
|
async execute(input, _options) {
|
|
4414
5001
|
const inputPath = input["path"] || ".";
|
|
4415
5002
|
const absPath = resolveInWorkspace(this.workspaceRoot, inputPath);
|
|
4416
|
-
const entries = await
|
|
5003
|
+
const entries = await fs3__default.default.readdir(absPath, { withFileTypes: true });
|
|
4417
5004
|
return entries.map((e) => `${e.isDirectory() ? "[DIR] " : " "}${e.name}`).join("\n") || "(empty directory)";
|
|
4418
5005
|
}
|
|
4419
5006
|
};
|
|
@@ -4796,8 +5383,8 @@ var ImageAnalyzeTool = class extends BaseTool {
|
|
|
4796
5383
|
}
|
|
4797
5384
|
};
|
|
4798
5385
|
async function fileToImageAttachment(filePath) {
|
|
4799
|
-
const data = await
|
|
4800
|
-
const ext =
|
|
5386
|
+
const data = await fs3__default.default.readFile(filePath);
|
|
5387
|
+
const ext = path16__default.default.extname(filePath).toLowerCase();
|
|
4801
5388
|
const mimeMap = {
|
|
4802
5389
|
".jpg": "image/jpeg",
|
|
4803
5390
|
".jpeg": "image/jpeg",
|
|
@@ -4831,14 +5418,14 @@ var PDFCreateTool = class extends BaseTool {
|
|
|
4831
5418
|
const filePath = input["path"];
|
|
4832
5419
|
const content = input["content"];
|
|
4833
5420
|
const title = input["title"];
|
|
4834
|
-
const dir =
|
|
4835
|
-
if (!
|
|
4836
|
-
|
|
5421
|
+
const dir = path16__default.default.dirname(filePath);
|
|
5422
|
+
if (!fs15__default.default.existsSync(dir)) {
|
|
5423
|
+
fs15__default.default.mkdirSync(dir, { recursive: true });
|
|
4837
5424
|
}
|
|
4838
5425
|
return new Promise((resolve, reject) => {
|
|
4839
5426
|
try {
|
|
4840
5427
|
const doc = new PDFDocument__default.default({ margin: 50 });
|
|
4841
|
-
const stream =
|
|
5428
|
+
const stream = fs15__default.default.createWriteStream(filePath);
|
|
4842
5429
|
doc.pipe(stream);
|
|
4843
5430
|
if (title) {
|
|
4844
5431
|
doc.info["Title"] = title;
|
|
@@ -4916,14 +5503,14 @@ var CodeInterpreterTool = class extends BaseTool {
|
|
|
4916
5503
|
}
|
|
4917
5504
|
cmdPrefix = NODE_CMD;
|
|
4918
5505
|
}
|
|
4919
|
-
const tmpDir =
|
|
4920
|
-
if (!
|
|
4921
|
-
|
|
5506
|
+
const tmpDir = path16__default.default.join(process.cwd(), ".cascade", "tmp");
|
|
5507
|
+
if (!fs15__default.default.existsSync(tmpDir)) {
|
|
5508
|
+
fs15__default.default.mkdirSync(tmpDir, { recursive: true });
|
|
4922
5509
|
}
|
|
4923
5510
|
const extension = language === "python" ? "py" : "js";
|
|
4924
5511
|
const fileName = `intp_${crypto.randomUUID().slice(0, 8)}.${extension}`;
|
|
4925
|
-
const filePath =
|
|
4926
|
-
|
|
5512
|
+
const filePath = path16__default.default.join(tmpDir, fileName);
|
|
5513
|
+
fs15__default.default.writeFileSync(filePath, code, "utf-8");
|
|
4927
5514
|
const quotedPath = `"${filePath}"`;
|
|
4928
5515
|
const quotedArgs = args.map((a) => `"${a}"`).join(" ");
|
|
4929
5516
|
const fullCmd = `${cmdPrefix} ${quotedPath}${quotedArgs ? " " + quotedArgs : ""}`;
|
|
@@ -4932,8 +5519,8 @@ var CodeInterpreterTool = class extends BaseTool {
|
|
|
4932
5519
|
child_process.exec(fullCmd, { cwd: process.cwd(), timeout: 3e4 }, (error, stdout, stderr) => {
|
|
4933
5520
|
const duration = Date.now() - startMs;
|
|
4934
5521
|
try {
|
|
4935
|
-
if (
|
|
4936
|
-
|
|
5522
|
+
if (fs15__default.default.existsSync(filePath)) {
|
|
5523
|
+
fs15__default.default.unlinkSync(filePath);
|
|
4937
5524
|
}
|
|
4938
5525
|
} catch (cleanupErr) {
|
|
4939
5526
|
console.error(`Failed to cleanup interpreter script ${filePath}:`, cleanupErr);
|
|
@@ -5193,6 +5780,253 @@ var WebSearchTool = class extends BaseTool {
|
|
|
5193
5780
|
return lines.join("\n");
|
|
5194
5781
|
}
|
|
5195
5782
|
};
|
|
5783
|
+
var GlobTool = class extends BaseTool {
|
|
5784
|
+
name = "glob";
|
|
5785
|
+
description = "Fast file pattern matching. Returns file paths matching a glob pattern, sorted by modification time. Use this to find files by name patterns.";
|
|
5786
|
+
inputSchema = {
|
|
5787
|
+
type: "object",
|
|
5788
|
+
properties: {
|
|
5789
|
+
pattern: {
|
|
5790
|
+
type: "string",
|
|
5791
|
+
description: 'Glob pattern to match files against, e.g. "**/*.ts", "src/**/*.tsx"'
|
|
5792
|
+
},
|
|
5793
|
+
path: {
|
|
5794
|
+
type: "string",
|
|
5795
|
+
description: "Directory to search in. Defaults to the workspace root."
|
|
5796
|
+
}
|
|
5797
|
+
},
|
|
5798
|
+
required: ["pattern"]
|
|
5799
|
+
};
|
|
5800
|
+
async execute(input, _options) {
|
|
5801
|
+
const pattern = input["pattern"];
|
|
5802
|
+
const searchPath = input["path"] ? path16__default.default.resolve(this.workspaceRoot, input["path"]) : this.workspaceRoot;
|
|
5803
|
+
const matches = await glob.glob(pattern, {
|
|
5804
|
+
cwd: searchPath,
|
|
5805
|
+
ignore: ["node_modules/**", ".git/**", "dist/**", "build/**"],
|
|
5806
|
+
nodir: true,
|
|
5807
|
+
dot: false
|
|
5808
|
+
});
|
|
5809
|
+
if (matches.length === 0) {
|
|
5810
|
+
return `No files matched pattern: ${pattern}`;
|
|
5811
|
+
}
|
|
5812
|
+
const withMtime = await Promise.all(
|
|
5813
|
+
matches.map(async (rel) => {
|
|
5814
|
+
try {
|
|
5815
|
+
const stat = await fs3__default.default.stat(path16__default.default.join(searchPath, rel));
|
|
5816
|
+
return { rel, mtime: stat.mtimeMs };
|
|
5817
|
+
} catch {
|
|
5818
|
+
return { rel, mtime: 0 };
|
|
5819
|
+
}
|
|
5820
|
+
})
|
|
5821
|
+
);
|
|
5822
|
+
withMtime.sort((a, b) => b.mtime - a.mtime);
|
|
5823
|
+
const lines = withMtime.map((f) => f.rel);
|
|
5824
|
+
return lines.join("\n");
|
|
5825
|
+
}
|
|
5826
|
+
};
|
|
5827
|
+
var execFileAsync = util.promisify(child_process.execFile);
|
|
5828
|
+
var GrepTool = class extends BaseTool {
|
|
5829
|
+
name = "grep";
|
|
5830
|
+
description = "Search file contents using a regex pattern. Returns matching lines with file paths and line numbers. Tries ripgrep (rg) first, falls back to Node.js regex scan.";
|
|
5831
|
+
inputSchema = {
|
|
5832
|
+
type: "object",
|
|
5833
|
+
properties: {
|
|
5834
|
+
pattern: {
|
|
5835
|
+
type: "string",
|
|
5836
|
+
description: "Regular expression pattern to search for in file contents"
|
|
5837
|
+
},
|
|
5838
|
+
path: {
|
|
5839
|
+
type: "string",
|
|
5840
|
+
description: "File or directory to search in. Defaults to workspace root."
|
|
5841
|
+
},
|
|
5842
|
+
glob: {
|
|
5843
|
+
type: "string",
|
|
5844
|
+
description: 'Glob pattern to filter files, e.g. "*.ts", "**/*.tsx"'
|
|
5845
|
+
},
|
|
5846
|
+
output_mode: {
|
|
5847
|
+
type: "string",
|
|
5848
|
+
enum: ["content", "files_with_matches", "count"],
|
|
5849
|
+
description: '"content" shows matching lines (default), "files_with_matches" shows file paths only, "count" shows match counts'
|
|
5850
|
+
},
|
|
5851
|
+
context: {
|
|
5852
|
+
type: "number",
|
|
5853
|
+
description: "Lines of context around each match (content mode only). Default: 0."
|
|
5854
|
+
},
|
|
5855
|
+
case_insensitive: {
|
|
5856
|
+
type: "boolean",
|
|
5857
|
+
description: "Case-insensitive search. Default: false."
|
|
5858
|
+
}
|
|
5859
|
+
},
|
|
5860
|
+
required: ["pattern"]
|
|
5861
|
+
};
|
|
5862
|
+
async execute(input, _options) {
|
|
5863
|
+
const pattern = input["pattern"];
|
|
5864
|
+
const searchPath = input["path"] ? path16__default.default.resolve(this.workspaceRoot, input["path"]) : this.workspaceRoot;
|
|
5865
|
+
const globPattern = input["glob"];
|
|
5866
|
+
const outputMode = input["output_mode"] ?? "content";
|
|
5867
|
+
const context = input["context"] ?? 0;
|
|
5868
|
+
const caseInsensitive = input["case_insensitive"] ?? false;
|
|
5869
|
+
try {
|
|
5870
|
+
const result = await this.runRipgrep(
|
|
5871
|
+
pattern,
|
|
5872
|
+
searchPath,
|
|
5873
|
+
globPattern,
|
|
5874
|
+
outputMode,
|
|
5875
|
+
context,
|
|
5876
|
+
caseInsensitive
|
|
5877
|
+
);
|
|
5878
|
+
return result;
|
|
5879
|
+
} catch {
|
|
5880
|
+
}
|
|
5881
|
+
return this.nodeScan(pattern, searchPath, globPattern, outputMode, context, caseInsensitive);
|
|
5882
|
+
}
|
|
5883
|
+
async runRipgrep(pattern, searchPath, globPattern, outputMode, context, caseInsensitive) {
|
|
5884
|
+
const args = ["--no-heading"];
|
|
5885
|
+
if (caseInsensitive) args.push("-i");
|
|
5886
|
+
if (outputMode === "files_with_matches") args.push("-l");
|
|
5887
|
+
else if (outputMode === "count") args.push("-c");
|
|
5888
|
+
else {
|
|
5889
|
+
args.push("-n");
|
|
5890
|
+
if (context > 0) args.push(`-C${context}`);
|
|
5891
|
+
}
|
|
5892
|
+
if (globPattern) args.push("--glob", globPattern);
|
|
5893
|
+
args.push("--", pattern, searchPath);
|
|
5894
|
+
const { stdout } = await execFileAsync("rg", args, {
|
|
5895
|
+
timeout: 15e3,
|
|
5896
|
+
maxBuffer: 2 * 1024 * 1024
|
|
5897
|
+
});
|
|
5898
|
+
const trimmed = stdout.trim();
|
|
5899
|
+
return trimmed || `No matches found for: ${pattern}`;
|
|
5900
|
+
}
|
|
5901
|
+
async nodeScan(pattern, searchPath, globPattern, outputMode, context, caseInsensitive) {
|
|
5902
|
+
const flags = caseInsensitive ? "gi" : "g";
|
|
5903
|
+
let regex;
|
|
5904
|
+
try {
|
|
5905
|
+
regex = new RegExp(pattern, flags);
|
|
5906
|
+
} catch {
|
|
5907
|
+
return `Invalid regex pattern: ${pattern}`;
|
|
5908
|
+
}
|
|
5909
|
+
const fileGlob = globPattern ?? "**/*";
|
|
5910
|
+
let files;
|
|
5911
|
+
try {
|
|
5912
|
+
files = await glob.glob(fileGlob, {
|
|
5913
|
+
cwd: searchPath,
|
|
5914
|
+
ignore: ["node_modules/**", ".git/**", "dist/**", "build/**"],
|
|
5915
|
+
nodir: true
|
|
5916
|
+
});
|
|
5917
|
+
} catch {
|
|
5918
|
+
files = [path16__default.default.relative(searchPath, searchPath) || "."];
|
|
5919
|
+
}
|
|
5920
|
+
const results = [];
|
|
5921
|
+
let totalCount = 0;
|
|
5922
|
+
for (const rel of files) {
|
|
5923
|
+
const abs = path16__default.default.join(searchPath, rel);
|
|
5924
|
+
let content;
|
|
5925
|
+
try {
|
|
5926
|
+
content = await fs3__default.default.readFile(abs, "utf-8");
|
|
5927
|
+
} catch {
|
|
5928
|
+
continue;
|
|
5929
|
+
}
|
|
5930
|
+
const lines = content.split("\n");
|
|
5931
|
+
const matchingLines = [];
|
|
5932
|
+
for (let i = 0; i < lines.length; i++) {
|
|
5933
|
+
if (regex.test(lines[i])) matchingLines.push(i);
|
|
5934
|
+
regex.lastIndex = 0;
|
|
5935
|
+
}
|
|
5936
|
+
if (matchingLines.length === 0) continue;
|
|
5937
|
+
totalCount += matchingLines.length;
|
|
5938
|
+
if (outputMode === "files_with_matches") {
|
|
5939
|
+
results.push(rel);
|
|
5940
|
+
} else if (outputMode === "count") {
|
|
5941
|
+
results.push(`${rel}: ${matchingLines.length}`);
|
|
5942
|
+
} else {
|
|
5943
|
+
const shown = /* @__PURE__ */ new Set();
|
|
5944
|
+
for (const lineIdx of matchingLines) {
|
|
5945
|
+
const start = Math.max(0, lineIdx - context);
|
|
5946
|
+
const end = Math.min(lines.length - 1, lineIdx + context);
|
|
5947
|
+
for (let i = start; i <= end; i++) shown.add(i);
|
|
5948
|
+
}
|
|
5949
|
+
const sortedIdxs = [...shown].sort((a, b) => a - b);
|
|
5950
|
+
for (const i of sortedIdxs) {
|
|
5951
|
+
const marker = matchingLines.includes(i) ? ":" : "-";
|
|
5952
|
+
results.push(`${rel}:${i + 1}${marker}${lines[i]}`);
|
|
5953
|
+
}
|
|
5954
|
+
}
|
|
5955
|
+
}
|
|
5956
|
+
if (results.length === 0) return `No matches found for: ${pattern}`;
|
|
5957
|
+
if (outputMode === "count") {
|
|
5958
|
+
results.push(`
|
|
5959
|
+
Total: ${totalCount} matches`);
|
|
5960
|
+
}
|
|
5961
|
+
return results.join("\n");
|
|
5962
|
+
}
|
|
5963
|
+
};
|
|
5964
|
+
|
|
5965
|
+
// src/tools/web-fetch.ts
|
|
5966
|
+
var MAX_CHARS = 5e4;
|
|
5967
|
+
var TIMEOUT_MS = 15e3;
|
|
5968
|
+
function stripHtml(html) {
|
|
5969
|
+
let text = html.replace(/<script[\s\S]*?<\/script>/gi, "").replace(/<style[\s\S]*?<\/style>/gi, "").replace(/<noscript[\s\S]*?<\/noscript>/gi, "");
|
|
5970
|
+
text = text.replace(/<br\s*\/?>/gi, "\n").replace(/<\/p>/gi, "\n").replace(/<\/div>/gi, "\n").replace(/<\/h[1-6]>/gi, "\n").replace(/<\/li>/gi, "\n").replace(/<\/tr>/gi, "\n").replace(/<\/td>/gi, " ");
|
|
5971
|
+
text = text.replace(/<[^>]+>/g, "");
|
|
5972
|
+
text = text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'").replace(/ /g, " ").replace(/&#(\d+);/g, (_, n) => String.fromCharCode(Number(n)));
|
|
5973
|
+
text = text.split("\n").map((l) => l.trim()).filter((l) => l.length > 0).join("\n");
|
|
5974
|
+
return text;
|
|
5975
|
+
}
|
|
5976
|
+
var WebFetchTool = class extends BaseTool {
|
|
5977
|
+
name = "web_fetch";
|
|
5978
|
+
description = "Fetch a URL and return its content as plain text (HTML stripped). Use for reading documentation, web pages, or any URL. Limit: 50,000 characters.";
|
|
5979
|
+
inputSchema = {
|
|
5980
|
+
type: "object",
|
|
5981
|
+
properties: {
|
|
5982
|
+
url: {
|
|
5983
|
+
type: "string",
|
|
5984
|
+
description: "The URL to fetch"
|
|
5985
|
+
},
|
|
5986
|
+
prompt: {
|
|
5987
|
+
type: "string",
|
|
5988
|
+
description: "Optional hint for what information to extract from the page (not used for filtering, just context)"
|
|
5989
|
+
}
|
|
5990
|
+
},
|
|
5991
|
+
required: ["url"]
|
|
5992
|
+
};
|
|
5993
|
+
async execute(input, _options) {
|
|
5994
|
+
const url = input["url"];
|
|
5995
|
+
let resp;
|
|
5996
|
+
try {
|
|
5997
|
+
resp = await fetch(url, {
|
|
5998
|
+
headers: {
|
|
5999
|
+
"User-Agent": "Cascade-AI/1.0 WebFetchTool",
|
|
6000
|
+
Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,text/plain;q=0.8,*/*;q=0.5"
|
|
6001
|
+
},
|
|
6002
|
+
signal: AbortSignal.timeout(TIMEOUT_MS),
|
|
6003
|
+
redirect: "follow"
|
|
6004
|
+
});
|
|
6005
|
+
} catch (err) {
|
|
6006
|
+
return `Failed to fetch ${url}: ${err instanceof Error ? err.message : String(err)}`;
|
|
6007
|
+
}
|
|
6008
|
+
if (!resp.ok) {
|
|
6009
|
+
return `HTTP ${resp.status} ${resp.statusText} from ${url}`;
|
|
6010
|
+
}
|
|
6011
|
+
const contentType = resp.headers.get("content-type") ?? "";
|
|
6012
|
+
let text;
|
|
6013
|
+
try {
|
|
6014
|
+
const raw = await resp.text();
|
|
6015
|
+
text = contentType.includes("html") ? stripHtml(raw) : raw;
|
|
6016
|
+
} catch (err) {
|
|
6017
|
+
return `Failed to read response body: ${err instanceof Error ? err.message : String(err)}`;
|
|
6018
|
+
}
|
|
6019
|
+
if (text.length > MAX_CHARS) {
|
|
6020
|
+
text = text.slice(0, MAX_CHARS) + `
|
|
6021
|
+
|
|
6022
|
+
[Content truncated at ${MAX_CHARS} characters]`;
|
|
6023
|
+
}
|
|
6024
|
+
return `URL: ${url}
|
|
6025
|
+
Content-Type: ${contentType}
|
|
6026
|
+
|
|
6027
|
+
${text}`;
|
|
6028
|
+
}
|
|
6029
|
+
};
|
|
5196
6030
|
|
|
5197
6031
|
// src/tools/mcp.ts
|
|
5198
6032
|
var McpToolWrapper = class extends BaseTool {
|
|
@@ -5218,7 +6052,7 @@ var McpToolWrapper = class extends BaseTool {
|
|
|
5218
6052
|
|
|
5219
6053
|
// src/tools/registry.ts
|
|
5220
6054
|
var ignore = ignoreFactory__namespace.default.default ?? ignoreFactory__namespace.default;
|
|
5221
|
-
var ToolRegistry = class {
|
|
6055
|
+
var ToolRegistry = class extends EventEmitter__default.default {
|
|
5222
6056
|
tools = /* @__PURE__ */ new Map();
|
|
5223
6057
|
config;
|
|
5224
6058
|
ignoreMatcher = ignore();
|
|
@@ -5226,12 +6060,36 @@ var ToolRegistry = class {
|
|
|
5226
6060
|
/** Loaded plugins, keyed by plugin name */
|
|
5227
6061
|
plugins = /* @__PURE__ */ new Map();
|
|
5228
6062
|
constructor(config, workspaceRoot = process.cwd()) {
|
|
6063
|
+
super();
|
|
5229
6064
|
this.config = config;
|
|
5230
6065
|
this.workspaceRoot = workspaceRoot;
|
|
5231
6066
|
this.registerDefaults();
|
|
5232
6067
|
}
|
|
5233
6068
|
register(tool) {
|
|
5234
6069
|
this.tools.set(tool.name, tool);
|
|
6070
|
+
this.emit("tool:added", tool.name);
|
|
6071
|
+
}
|
|
6072
|
+
/**
|
|
6073
|
+
* Wait until a named tool is registered, resolving immediately if it already exists.
|
|
6074
|
+
* T3 workers can call this after encountering a missing-tool error to resume
|
|
6075
|
+
* automatically once T2 synthesizes the tool.
|
|
6076
|
+
*/
|
|
6077
|
+
waitForTool(toolName, timeoutMs = 6e4) {
|
|
6078
|
+
if (this.tools.has(toolName)) return Promise.resolve();
|
|
6079
|
+
return new Promise((resolve, reject) => {
|
|
6080
|
+
const timer = setTimeout(() => {
|
|
6081
|
+
this.off("tool:added", handler);
|
|
6082
|
+
reject(new Error(`Timeout waiting for tool: ${toolName}`));
|
|
6083
|
+
}, timeoutMs);
|
|
6084
|
+
const handler = (name) => {
|
|
6085
|
+
if (name === toolName) {
|
|
6086
|
+
clearTimeout(timer);
|
|
6087
|
+
this.off("tool:added", handler);
|
|
6088
|
+
resolve();
|
|
6089
|
+
}
|
|
6090
|
+
};
|
|
6091
|
+
this.on("tool:added", handler);
|
|
6092
|
+
});
|
|
5235
6093
|
}
|
|
5236
6094
|
/**
|
|
5237
6095
|
* Register a ToolPlugin, loading all its tools into the registry.
|
|
@@ -5316,7 +6174,10 @@ var ToolRegistry = class {
|
|
|
5316
6174
|
new PDFCreateTool(),
|
|
5317
6175
|
new CodeInterpreterTool(),
|
|
5318
6176
|
new PeerCommunicationTool(),
|
|
5319
|
-
new WebSearchTool(this.config.webSearch)
|
|
6177
|
+
new WebSearchTool(this.config.webSearch),
|
|
6178
|
+
new GlobTool(),
|
|
6179
|
+
new GrepTool(),
|
|
6180
|
+
new WebFetchTool()
|
|
5320
6181
|
];
|
|
5321
6182
|
for (const tool of tools) {
|
|
5322
6183
|
tool.setWorkspaceRoot(this.workspaceRoot);
|
|
@@ -5333,10 +6194,10 @@ var ToolRegistry = class {
|
|
|
5333
6194
|
}
|
|
5334
6195
|
isIgnored(filePath) {
|
|
5335
6196
|
if (!filePath) return false;
|
|
5336
|
-
const abs =
|
|
5337
|
-
const rel =
|
|
5338
|
-
if (!rel || rel.startsWith("..") ||
|
|
5339
|
-
const posixRel = rel.split(
|
|
6197
|
+
const abs = path16__default.default.resolve(this.workspaceRoot, filePath);
|
|
6198
|
+
const rel = path16__default.default.relative(this.workspaceRoot, abs);
|
|
6199
|
+
if (!rel || rel.startsWith("..") || path16__default.default.isAbsolute(rel)) return true;
|
|
6200
|
+
const posixRel = rel.split(path16__default.default.sep).join("/");
|
|
5340
6201
|
return this.ignoreMatcher.ignores(posixRel);
|
|
5341
6202
|
}
|
|
5342
6203
|
};
|
|
@@ -5673,7 +6534,24 @@ var CascadeConfigSchema = zod.z.object({
|
|
|
5673
6534
|
* Generated tools are session-scoped and sandboxed in node:vm.
|
|
5674
6535
|
* HTTP calls from generated tools require approval.
|
|
5675
6536
|
*/
|
|
5676
|
-
enableToolCreation: zod.z.boolean().default(
|
|
6537
|
+
enableToolCreation: zod.z.boolean().default(true),
|
|
6538
|
+
/**
|
|
6539
|
+
* External plugin paths or npm package names to load at startup.
|
|
6540
|
+
* Each entry must export a default ToolPlugin object.
|
|
6541
|
+
* Example: ["./plugins/my-tool.js", "cascade-plugin-slack"]
|
|
6542
|
+
*/
|
|
6543
|
+
plugins: zod.z.array(zod.z.string()).default([]),
|
|
6544
|
+
/**
|
|
6545
|
+
* Maximum number of concurrent inference requests to any local model provider
|
|
6546
|
+
* (e.g. Ollama). Defaults to 1 to prevent GPU memory pressure when multiple
|
|
6547
|
+
* T3 workers run in parallel on a single-GPU machine.
|
|
6548
|
+
*/
|
|
6549
|
+
localConcurrency: zod.z.number().int().min(1).default(1),
|
|
6550
|
+
/**
|
|
6551
|
+
* Timeout in milliseconds for a single local model inference call.
|
|
6552
|
+
* Local models can take minutes for large parameter counts. Default: 5 minutes.
|
|
6553
|
+
*/
|
|
6554
|
+
localInferenceTimeoutMs: zod.z.number().int().min(1e3).default(3e5)
|
|
5677
6555
|
});
|
|
5678
6556
|
|
|
5679
6557
|
// src/config/validate.ts
|
|
@@ -5801,139 +6679,237 @@ function heuristicAnalyze(prompt) {
|
|
|
5801
6679
|
const estimatedTokens = wordCount * 5;
|
|
5802
6680
|
return { type, complexity, requiresReasoning, requiresVision, estimatedTokens, confidence };
|
|
5803
6681
|
}
|
|
5804
|
-
function selectModelFromProfile(profile, tier, selector) {
|
|
5805
|
-
if (profile.requiresVision) {
|
|
5806
|
-
return selector.selectVisionModel();
|
|
5807
|
-
}
|
|
5808
|
-
if (tier === "T1") {
|
|
5809
|
-
if (profile.complexity >= 4) {
|
|
5810
|
-
return selector.selectForTier("T1");
|
|
5811
|
-
} else {
|
|
5812
|
-
return selector.selectForTier("T2");
|
|
5813
|
-
}
|
|
5814
|
-
}
|
|
5815
|
-
if (tier === "T2") {
|
|
5816
|
-
if (profile.type === "code" || profile.type === "data") {
|
|
5817
|
-
return selector.selectForTier("T2");
|
|
5818
|
-
} else if (profile.complexity <= 2) {
|
|
5819
|
-
return selector.selectForTier("T3");
|
|
5820
|
-
}
|
|
5821
|
-
return selector.selectForTier("T2");
|
|
5822
|
-
}
|
|
5823
|
-
if (tier === "T3") {
|
|
5824
|
-
if (profile.complexity >= 4 || profile.requiresReasoning) {
|
|
5825
|
-
return selector.selectForTier("T2");
|
|
5826
|
-
} else if (profile.type === "creative") {
|
|
5827
|
-
return selector.selectForTier("T2");
|
|
5828
|
-
} else {
|
|
5829
|
-
return selector.selectForTier("T3");
|
|
5830
|
-
}
|
|
5831
|
-
}
|
|
5832
|
-
return selector.selectForTier(tier);
|
|
5833
|
-
}
|
|
5834
6682
|
var analysisCache = /* @__PURE__ */ new Map();
|
|
6683
|
+
var TASK_TYPE_TAGS = {
|
|
6684
|
+
code: ["code", "instruction"],
|
|
6685
|
+
analysis: ["analysis", "instruction"],
|
|
6686
|
+
creative: ["creative", "multilingual"],
|
|
6687
|
+
data: ["data", "code"],
|
|
6688
|
+
mixed: []
|
|
6689
|
+
};
|
|
5835
6690
|
var TaskAnalyzer = class {
|
|
5836
|
-
|
|
5837
|
-
|
|
5838
|
-
|
|
6691
|
+
tracker;
|
|
6692
|
+
lastProfile = null;
|
|
6693
|
+
lastSelectedModels = /* @__PURE__ */ new Map();
|
|
6694
|
+
constructor(tracker) {
|
|
6695
|
+
this.tracker = tracker;
|
|
6696
|
+
}
|
|
6697
|
+
setTracker(tracker) {
|
|
6698
|
+
this.tracker = tracker;
|
|
6699
|
+
}
|
|
6700
|
+
/** Returns the TaskProfile from the most recent analyze() call — used for outcome recording. */
|
|
6701
|
+
getLastProfile() {
|
|
6702
|
+
return this.lastProfile;
|
|
5839
6703
|
}
|
|
5840
6704
|
/**
|
|
5841
|
-
* Analyze a prompt and return a TaskProfile.
|
|
5842
|
-
*
|
|
6705
|
+
* Analyze a prompt and return a TaskProfile using pure heuristics.
|
|
6706
|
+
* Low confidence prompts fall back to a conservative mixed/moderate profile.
|
|
5843
6707
|
*/
|
|
5844
6708
|
async analyze(prompt) {
|
|
5845
6709
|
const cacheKey = prompt.slice(0, 200);
|
|
5846
6710
|
const cached = analysisCache.get(cacheKey);
|
|
5847
|
-
if (cached)
|
|
5848
|
-
|
|
5849
|
-
|
|
5850
|
-
try {
|
|
5851
|
-
const aiProfile = await this.aiInference(prompt);
|
|
5852
|
-
const merged = {
|
|
5853
|
-
type: aiProfile.type,
|
|
5854
|
-
complexity: aiProfile.complexity,
|
|
5855
|
-
requiresReasoning: aiProfile.requiresReasoning,
|
|
5856
|
-
requiresVision: heuristic.requiresVision || aiProfile.requiresVision,
|
|
5857
|
-
estimatedTokens: heuristic.estimatedTokens,
|
|
5858
|
-
confidence: 0.9
|
|
5859
|
-
// AI-backed
|
|
5860
|
-
};
|
|
5861
|
-
analysisCache.set(cacheKey, merged);
|
|
5862
|
-
return merged;
|
|
5863
|
-
} catch {
|
|
5864
|
-
}
|
|
6711
|
+
if (cached) {
|
|
6712
|
+
this.lastProfile = cached;
|
|
6713
|
+
return cached;
|
|
5865
6714
|
}
|
|
5866
|
-
|
|
5867
|
-
|
|
6715
|
+
const profile = heuristicAnalyze(prompt);
|
|
6716
|
+
analysisCache.set(cacheKey, profile);
|
|
6717
|
+
this.lastProfile = profile;
|
|
6718
|
+
return profile;
|
|
5868
6719
|
}
|
|
5869
6720
|
/**
|
|
5870
|
-
* Select the optimal model for a given tier
|
|
6721
|
+
* Select the optimal model for a given tier.
|
|
6722
|
+
* Scores tier-eligible models using cost efficiency + historical performance.
|
|
6723
|
+
* Falls back to the priority-list default when no candidates have history.
|
|
5871
6724
|
*/
|
|
5872
6725
|
async selectModel(prompt, tier, selector) {
|
|
5873
6726
|
const profile = await this.analyze(prompt);
|
|
5874
|
-
|
|
6727
|
+
if (profile.requiresVision) {
|
|
6728
|
+
return selector.selectVisionModel();
|
|
6729
|
+
}
|
|
6730
|
+
const candidates = selector.getCandidatesForTier(tier);
|
|
6731
|
+
if (candidates.length === 0) return selector.selectForTier(tier);
|
|
6732
|
+
const scored = candidates.map((m) => ({
|
|
6733
|
+
model: m,
|
|
6734
|
+
score: this.scoreModel(m, profile)
|
|
6735
|
+
}));
|
|
6736
|
+
scored.sort((a, b) => b.score - a.score);
|
|
6737
|
+
const best = scored[0]?.model ?? selector.selectForTier(tier);
|
|
6738
|
+
if (best) this.lastSelectedModels.set(tier, best);
|
|
6739
|
+
return best;
|
|
5875
6740
|
}
|
|
5876
|
-
|
|
5877
|
-
|
|
5878
|
-
|
|
5879
|
-
|
|
5880
|
-
|
|
5881
|
-
|
|
5882
|
-
|
|
5883
|
-
|
|
5884
|
-
|
|
5885
|
-
|
|
5886
|
-
|
|
5887
|
-
|
|
5888
|
-
|
|
5889
|
-
|
|
5890
|
-
|
|
5891
|
-
const
|
|
5892
|
-
const
|
|
5893
|
-
const
|
|
5894
|
-
|
|
5895
|
-
|
|
5896
|
-
|
|
5897
|
-
|
|
5898
|
-
|
|
5899
|
-
|
|
5900
|
-
|
|
5901
|
-
|
|
5902
|
-
|
|
6741
|
+
/**
|
|
6742
|
+
* Record the outcome of a completed run across all tiers that were selected
|
|
6743
|
+
* during this session and persist stats to disk.
|
|
6744
|
+
*/
|
|
6745
|
+
recordRunOutcome(outcome, costByTier) {
|
|
6746
|
+
if (!this.tracker || !this.lastProfile) return;
|
|
6747
|
+
const taskType = this.lastProfile.type;
|
|
6748
|
+
for (const [tier, model] of this.lastSelectedModels) {
|
|
6749
|
+
const cost = costByTier[tier] ?? 0;
|
|
6750
|
+
this.tracker.record(model.id, taskType, outcome, 0, cost);
|
|
6751
|
+
}
|
|
6752
|
+
this.lastSelectedModels.clear();
|
|
6753
|
+
void this.tracker.save();
|
|
6754
|
+
}
|
|
6755
|
+
scoreModel(model, profile) {
|
|
6756
|
+
const perf = this.tracker?.performanceScore(model.id, profile.type) ?? 0.5;
|
|
6757
|
+
const costEff = this.costEfficiency(model, profile.complexity);
|
|
6758
|
+
const match = this.taskMatchScore(model, profile);
|
|
6759
|
+
return perf * costEff * match;
|
|
6760
|
+
}
|
|
6761
|
+
costEfficiency(model, complexity) {
|
|
6762
|
+
if (this.tracker) return this.tracker.costEfficiencyScore(model, complexity);
|
|
6763
|
+
const blended = model.inputCostPer1kTokens + model.outputCostPer1kTokens * 2;
|
|
6764
|
+
const normalised = Math.min(1, blended / 0.05);
|
|
6765
|
+
const complexityWeight = (6 - complexity) / 5;
|
|
6766
|
+
return Math.max(0.1, 1 - normalised * complexityWeight);
|
|
6767
|
+
}
|
|
6768
|
+
taskMatchScore(model, profile) {
|
|
6769
|
+
const expected = TASK_TYPE_TAGS[profile.type];
|
|
6770
|
+
if (!model.specializations?.length || expected.length === 0) return 1;
|
|
6771
|
+
const matches = expected.filter((tag) => model.specializations.includes(tag)).length;
|
|
6772
|
+
return matches > 0 ? 1 + matches / expected.length * 0.3 : 0.8;
|
|
5903
6773
|
}
|
|
5904
6774
|
/** Clear the analysis cache (call between sessions). */
|
|
5905
6775
|
static clearCache() {
|
|
5906
6776
|
analysisCache.clear();
|
|
5907
6777
|
}
|
|
5908
6778
|
};
|
|
6779
|
+
var DEFAULT_STATS_FILE = path16__default.default.join(os3__default.default.homedir(), ".cascade", "model-perf.json");
|
|
6780
|
+
var ModelPerformanceTracker = class {
|
|
6781
|
+
stats = /* @__PURE__ */ new Map();
|
|
6782
|
+
statsFile;
|
|
6783
|
+
loaded = false;
|
|
6784
|
+
constructor(statsFile = DEFAULT_STATS_FILE) {
|
|
6785
|
+
this.statsFile = statsFile;
|
|
6786
|
+
}
|
|
6787
|
+
async load() {
|
|
6788
|
+
if (this.loaded) return;
|
|
6789
|
+
this.loaded = true;
|
|
6790
|
+
try {
|
|
6791
|
+
const raw = await fs3__default.default.readFile(this.statsFile, "utf-8");
|
|
6792
|
+
const parsed = JSON.parse(raw);
|
|
6793
|
+
for (const [key, stat] of Object.entries(parsed)) {
|
|
6794
|
+
this.stats.set(key, stat);
|
|
6795
|
+
}
|
|
6796
|
+
} catch {
|
|
6797
|
+
}
|
|
6798
|
+
}
|
|
6799
|
+
async save() {
|
|
6800
|
+
try {
|
|
6801
|
+
await fs3__default.default.mkdir(path16__default.default.dirname(this.statsFile), { recursive: true });
|
|
6802
|
+
const obj = {};
|
|
6803
|
+
for (const [key, stat] of this.stats) obj[key] = stat;
|
|
6804
|
+
await fs3__default.default.writeFile(this.statsFile, JSON.stringify(obj, null, 2), "utf-8");
|
|
6805
|
+
} catch {
|
|
6806
|
+
}
|
|
6807
|
+
}
|
|
6808
|
+
record(modelId, taskType, outcome, retries = 0, costUsd = 0) {
|
|
6809
|
+
const key = `${modelId}:${taskType}`;
|
|
6810
|
+
const s = this.stats.get(key) ?? {
|
|
6811
|
+
successCount: 0,
|
|
6812
|
+
failureCount: 0,
|
|
6813
|
+
totalRetries: 0,
|
|
6814
|
+
totalCostUsd: 0,
|
|
6815
|
+
sampleCount: 0
|
|
6816
|
+
};
|
|
6817
|
+
this.stats.set(key, {
|
|
6818
|
+
successCount: s.successCount + (outcome === "success" ? 1 : 0),
|
|
6819
|
+
failureCount: s.failureCount + (outcome === "failure" ? 1 : 0),
|
|
6820
|
+
totalRetries: s.totalRetries + retries,
|
|
6821
|
+
totalCostUsd: s.totalCostUsd + costUsd,
|
|
6822
|
+
sampleCount: s.sampleCount + 1
|
|
6823
|
+
});
|
|
6824
|
+
}
|
|
6825
|
+
/**
|
|
6826
|
+
* Returns 0.05–1.0; defaults to 0.5 (neutral prior) when no history exists.
|
|
6827
|
+
* High retry counts penalise the score.
|
|
6828
|
+
*/
|
|
6829
|
+
performanceScore(modelId, taskType) {
|
|
6830
|
+
const key = `${modelId}:${taskType}`;
|
|
6831
|
+
const s = this.stats.get(key);
|
|
6832
|
+
if (!s || s.sampleCount === 0) return 0.5;
|
|
6833
|
+
const successRate = s.successCount / s.sampleCount;
|
|
6834
|
+
const avgRetries = s.totalRetries / s.sampleCount;
|
|
6835
|
+
const retryPenalty = Math.min(0.4, avgRetries / 3);
|
|
6836
|
+
return Math.max(0.05, successRate * (1 - retryPenalty));
|
|
6837
|
+
}
|
|
6838
|
+
/**
|
|
6839
|
+
* Returns 0.1–1.0. Cheaper models score higher, with the penalty scaled
|
|
6840
|
+
* down for complex tasks (where capability matters more than cost).
|
|
6841
|
+
*
|
|
6842
|
+
* blended cost = input + 2 × output (output tokens are typically pricier).
|
|
6843
|
+
* normalised over $0.05 blended as the "expensive" ceiling.
|
|
6844
|
+
*/
|
|
6845
|
+
costEfficiencyScore(model, complexity) {
|
|
6846
|
+
const blended = model.inputCostPer1kTokens + model.outputCostPer1kTokens * 2;
|
|
6847
|
+
const normalised = Math.min(1, blended / 0.05);
|
|
6848
|
+
const complexityWeight = (6 - complexity) / 5;
|
|
6849
|
+
return Math.max(0.1, 1 - normalised * complexityWeight);
|
|
6850
|
+
}
|
|
6851
|
+
};
|
|
5909
6852
|
var DynamicTool = class extends BaseTool {
|
|
5910
6853
|
name;
|
|
5911
6854
|
description;
|
|
5912
6855
|
inputSchema;
|
|
5913
6856
|
executeCode;
|
|
5914
6857
|
_isDangerous;
|
|
5915
|
-
|
|
6858
|
+
registry;
|
|
6859
|
+
escalator;
|
|
6860
|
+
constructor(spec, registry, escalator) {
|
|
5916
6861
|
super();
|
|
5917
6862
|
this.name = spec.name;
|
|
5918
6863
|
this.description = spec.description;
|
|
5919
6864
|
this.inputSchema = spec.inputSchema;
|
|
5920
6865
|
this.executeCode = spec.executeCode;
|
|
5921
6866
|
this._isDangerous = spec.isDangerous;
|
|
6867
|
+
this.registry = registry;
|
|
6868
|
+
this.escalator = escalator;
|
|
5922
6869
|
}
|
|
5923
6870
|
isDangerous() {
|
|
5924
6871
|
return this._isDangerous;
|
|
5925
6872
|
}
|
|
5926
|
-
async execute(input,
|
|
6873
|
+
async execute(input, options) {
|
|
6874
|
+
const registry = this.registry;
|
|
6875
|
+
const escalator = this.escalator;
|
|
6876
|
+
const callTool = async (toolName, toolInput) => {
|
|
6877
|
+
if (!registry.hasTool(toolName)) return `Tool not found: ${toolName}`;
|
|
6878
|
+
if (registry.isDangerous(toolName)) {
|
|
6879
|
+
if (escalator) {
|
|
6880
|
+
const req = {
|
|
6881
|
+
id: `dynamic-${this.name}-${toolName}-${Date.now()}`,
|
|
6882
|
+
requestedBy: `dynamic_tool:${this.name}`,
|
|
6883
|
+
parentT2Id: options.tierId,
|
|
6884
|
+
toolName,
|
|
6885
|
+
input: toolInput,
|
|
6886
|
+
isDangerous: true,
|
|
6887
|
+
subtaskContext: `Dynamic tool "${this.name}" requesting access to "${toolName}"`,
|
|
6888
|
+
sectionContext: `Dynamic tool "${this.name}"`
|
|
6889
|
+
};
|
|
6890
|
+
const decision = await escalator.requestPermission(req);
|
|
6891
|
+
if (!decision.approved) {
|
|
6892
|
+
return `Permission denied for ${toolName} (decided by ${decision.decidedBy}).`;
|
|
6893
|
+
}
|
|
6894
|
+
}
|
|
6895
|
+
}
|
|
6896
|
+
try {
|
|
6897
|
+
const result = await registry.execute(toolName, toolInput, options);
|
|
6898
|
+
return typeof result === "string" ? result : JSON.stringify(result);
|
|
6899
|
+
} catch (err) {
|
|
6900
|
+
return `Error calling ${toolName}: ${err instanceof Error ? err.message : String(err)}`;
|
|
6901
|
+
}
|
|
6902
|
+
};
|
|
5927
6903
|
const sandbox = {
|
|
5928
6904
|
input,
|
|
5929
6905
|
fetch: globalThis.fetch,
|
|
6906
|
+
callTool,
|
|
5930
6907
|
JSON,
|
|
5931
6908
|
Math,
|
|
5932
6909
|
Date,
|
|
5933
6910
|
console: { log: () => {
|
|
5934
6911
|
}, error: () => {
|
|
5935
6912
|
} },
|
|
5936
|
-
// Silenced
|
|
5937
6913
|
setTimeout,
|
|
5938
6914
|
clearTimeout,
|
|
5939
6915
|
Promise,
|
|
@@ -5966,29 +6942,42 @@ Generate a minimal, safe JavaScript tool function for the described operation.
|
|
|
5966
6942
|
|
|
5967
6943
|
Rules:
|
|
5968
6944
|
- Return ONLY a JSON object with these fields: name, description, inputSchema, executeCode, isDangerous
|
|
5969
|
-
- executeCode is a self-contained JavaScript function body that:
|
|
5970
|
-
- Receives: input (object), fetch (
|
|
6945
|
+
- executeCode is a self-contained JavaScript async function body that:
|
|
6946
|
+
- Receives: input (object), fetch (for HTTP), callTool(toolName, input) (to call any registered cascade tool)
|
|
5971
6947
|
- Returns: a string result
|
|
5972
|
-
-
|
|
6948
|
+
- For file operations, prefer: await callTool('file_read', { path: input.path })
|
|
6949
|
+
- For shell commands, prefer: await callTool('shell', { command: 'ls -la' })
|
|
6950
|
+
- For pure computation / HTTP: use fetch or built-ins (JSON, Math, Date, String, Number, Array, Object)
|
|
5973
6951
|
- Must complete in under 15 seconds
|
|
5974
|
-
- isDangerous
|
|
6952
|
+
- isDangerous: true if the tool calls dangerous cascade tools (shell, file_write, file_delete, git) or makes HTTP calls that write data
|
|
5975
6953
|
- name must be snake_case, start with "dynamic_", max 40 chars
|
|
5976
6954
|
- description must be \u2264 120 chars
|
|
5977
6955
|
|
|
5978
|
-
Example
|
|
5979
|
-
|
|
6956
|
+
Example for a file-summary tool:
|
|
6957
|
+
{
|
|
6958
|
+
"name": "dynamic_summarize_file",
|
|
6959
|
+
"description": "Read a file and return a one-paragraph summary",
|
|
6960
|
+
"inputSchema": { "path": { "type": "string", "description": "File path to summarize" } },
|
|
6961
|
+
"executeCode": "const content = await callTool('file_read', { path: input.path }); return content.slice(0, 500);",
|
|
6962
|
+
"isDangerous": false
|
|
6963
|
+
}
|
|
5980
6964
|
|
|
5981
6965
|
Return ONLY valid JSON \u2014 no other text.`;
|
|
5982
6966
|
var ToolCreator = class {
|
|
5983
6967
|
router;
|
|
5984
6968
|
registry;
|
|
6969
|
+
escalator;
|
|
5985
6970
|
createdTools = /* @__PURE__ */ new Set();
|
|
5986
6971
|
constructor(router, registry) {
|
|
5987
6972
|
this.router = router;
|
|
5988
6973
|
this.registry = registry;
|
|
5989
6974
|
}
|
|
6975
|
+
setPermissionEscalator(escalator) {
|
|
6976
|
+
this.escalator = escalator;
|
|
6977
|
+
}
|
|
5990
6978
|
/**
|
|
5991
6979
|
* Generate a new tool from a description and register it with the ToolRegistry.
|
|
6980
|
+
* The generated tool has access to all registered cascade tools via callTool().
|
|
5992
6981
|
* Returns the tool name if successful, null if generation failed.
|
|
5993
6982
|
*/
|
|
5994
6983
|
async createTool(description, context) {
|
|
@@ -5999,26 +6988,21 @@ Required capability: ${description.slice(0, 300)}`;
|
|
|
5999
6988
|
try {
|
|
6000
6989
|
const result = await this.router.generate("T3", {
|
|
6001
6990
|
messages: [{ role: "user", content: prompt }],
|
|
6002
|
-
maxTokens:
|
|
6991
|
+
maxTokens: 800
|
|
6003
6992
|
});
|
|
6004
6993
|
const jsonMatch = /\{[\s\S]*\}/.exec(result.content);
|
|
6005
|
-
if (!jsonMatch)
|
|
6006
|
-
return null;
|
|
6007
|
-
}
|
|
6994
|
+
if (!jsonMatch) return null;
|
|
6008
6995
|
const spec = JSON.parse(jsonMatch[0]);
|
|
6009
|
-
if (!spec.name || !spec.description || !spec.executeCode || !spec.inputSchema)
|
|
6010
|
-
return null;
|
|
6011
|
-
}
|
|
6996
|
+
if (!spec.name || !spec.description || !spec.executeCode || !spec.inputSchema) return null;
|
|
6012
6997
|
if (this.createdTools.has(spec.name) || this.registry.hasTool(spec.name)) {
|
|
6013
6998
|
spec.name = `${spec.name}_${Date.now() % 1e4}`;
|
|
6014
6999
|
}
|
|
6015
7000
|
try {
|
|
6016
|
-
|
|
6017
|
-
|
|
6018
|
-
} catch (err) {
|
|
7001
|
+
new Function("input", "fetch", "callTool", spec.executeCode);
|
|
7002
|
+
} catch {
|
|
6019
7003
|
return null;
|
|
6020
7004
|
}
|
|
6021
|
-
const tool = new DynamicTool(spec);
|
|
7005
|
+
const tool = new DynamicTool(spec, this.registry, this.escalator);
|
|
6022
7006
|
this.registry.register(tool);
|
|
6023
7007
|
this.createdTools.add(spec.name);
|
|
6024
7008
|
return spec.name;
|
|
@@ -6026,16 +7010,14 @@ Required capability: ${description.slice(0, 300)}`;
|
|
|
6026
7010
|
return null;
|
|
6027
7011
|
}
|
|
6028
7012
|
}
|
|
6029
|
-
/**
|
|
6030
|
-
* Returns the names of all tools created in this session.
|
|
6031
|
-
*/
|
|
7013
|
+
/** Returns the names of all tools created in this session. */
|
|
6032
7014
|
getCreatedTools() {
|
|
6033
7015
|
return Array.from(this.createdTools);
|
|
6034
7016
|
}
|
|
6035
7017
|
};
|
|
6036
7018
|
|
|
6037
7019
|
// src/core/cascade.ts
|
|
6038
|
-
var Cascade = class extends EventEmitter__default.default {
|
|
7020
|
+
var Cascade = class _Cascade extends EventEmitter__default.default {
|
|
6039
7021
|
router;
|
|
6040
7022
|
toolRegistry;
|
|
6041
7023
|
mcpClient;
|
|
@@ -6046,6 +7028,7 @@ var Cascade = class extends EventEmitter__default.default {
|
|
|
6046
7028
|
audit;
|
|
6047
7029
|
telemetry;
|
|
6048
7030
|
taskAnalyzer;
|
|
7031
|
+
perfTracker;
|
|
6049
7032
|
toolCreator;
|
|
6050
7033
|
constructor(config, workspacePath, store) {
|
|
6051
7034
|
super();
|
|
@@ -6062,10 +7045,12 @@ var Cascade = class extends EventEmitter__default.default {
|
|
|
6062
7045
|
this.telemetry = config.telemetry?.enabled ? new Telemetry(config.telemetry, config.telemetry.distinctId ?? "anonymous") : noopTelemetry;
|
|
6063
7046
|
}
|
|
6064
7047
|
initOptionalFeatures() {
|
|
6065
|
-
|
|
6066
|
-
|
|
6067
|
-
this.
|
|
7048
|
+
if (this.config.cascadeAuto === true) {
|
|
7049
|
+
this.perfTracker = new ModelPerformanceTracker();
|
|
7050
|
+
void this.perfTracker.load();
|
|
7051
|
+
this.taskAnalyzer = new TaskAnalyzer(this.perfTracker);
|
|
6068
7052
|
}
|
|
7053
|
+
const cfg = this.config;
|
|
6069
7054
|
if (cfg["enableToolCreation"] === true) {
|
|
6070
7055
|
this.toolCreator = new ToolCreator(this.router, this.toolRegistry);
|
|
6071
7056
|
}
|
|
@@ -6131,6 +7116,26 @@ var Cascade = class extends EventEmitter__default.default {
|
|
|
6131
7116
|
}
|
|
6132
7117
|
}
|
|
6133
7118
|
}
|
|
7119
|
+
const pluginPaths = this.config["plugins"];
|
|
7120
|
+
if (pluginPaths?.length) {
|
|
7121
|
+
for (const pluginPath of pluginPaths) {
|
|
7122
|
+
try {
|
|
7123
|
+
const mod = await import(pluginPath);
|
|
7124
|
+
const plugin = mod.default ?? mod;
|
|
7125
|
+
if (plugin && Array.isArray(plugin.tools)) {
|
|
7126
|
+
this.toolRegistry.registerPlugin(plugin);
|
|
7127
|
+
} else {
|
|
7128
|
+
console.warn(`[cascade] Plugin "${pluginPath}" does not export a valid ToolPlugin.`);
|
|
7129
|
+
}
|
|
7130
|
+
} catch (err) {
|
|
7131
|
+
console.warn(`[cascade] Failed to load plugin "${pluginPath}":`, err);
|
|
7132
|
+
}
|
|
7133
|
+
}
|
|
7134
|
+
}
|
|
7135
|
+
if (this.config.cascadeAuto && this.store) {
|
|
7136
|
+
this.router.profileModels(this.store).catch(() => {
|
|
7137
|
+
});
|
|
7138
|
+
}
|
|
6134
7139
|
this.initOptionalFeatures();
|
|
6135
7140
|
this.initialized = true;
|
|
6136
7141
|
})();
|
|
@@ -6148,21 +7153,41 @@ var Cascade = class extends EventEmitter__default.default {
|
|
|
6148
7153
|
looksLikeSimpleArtifactTask(prompt) {
|
|
6149
7154
|
return /create .*\.(txt|md|json|csv)\b/i.test(prompt) && !/(research|compare|thorough|pdf|report|analy[sz]e|architecture|multi-agent)/i.test(prompt);
|
|
6150
7155
|
}
|
|
6151
|
-
|
|
6152
|
-
|
|
6153
|
-
|
|
6154
|
-
|
|
6155
|
-
|
|
6156
|
-
|
|
6157
|
-
|
|
6158
|
-
|
|
7156
|
+
looksLikeConversational(prompt) {
|
|
7157
|
+
const LOW_COMPLEXITY = [
|
|
7158
|
+
/^(?:hi|hello|hey|thanks|thank you|ok|okay|yes|no|sure|got it|sounds good)\b/i,
|
|
7159
|
+
/^(?:what is|what are|list|show me|tell me|who is|where is|when is|how do i)\b/i,
|
|
7160
|
+
/\b(?:simple|quick|brief|small|single|one-line|typo|rename)\b/i
|
|
7161
|
+
];
|
|
7162
|
+
const wordCount = prompt.trim().split(/\s+/).length;
|
|
7163
|
+
return wordCount <= 12 && LOW_COMPLEXITY.some((re) => re.test(prompt.trim()));
|
|
7164
|
+
}
|
|
7165
|
+
// Cache glob scan results per workspace path to avoid repeated I/O.
|
|
7166
|
+
static globCache = /* @__PURE__ */ new Map();
|
|
7167
|
+
async countWorkspaceFiles(workspacePath) {
|
|
7168
|
+
const now = Date.now();
|
|
7169
|
+
const cached = _Cascade.globCache.get(workspacePath);
|
|
7170
|
+
if (cached && cached.expiresAt > now) return cached.count;
|
|
6159
7171
|
try {
|
|
6160
7172
|
const files = await glob.glob("**/*.*", {
|
|
6161
7173
|
cwd: workspacePath,
|
|
6162
7174
|
ignore: ["node_modules/**", ".git/**", "dist/**", "build/**"],
|
|
6163
7175
|
nodir: true
|
|
6164
7176
|
});
|
|
6165
|
-
|
|
7177
|
+
_Cascade.globCache.set(workspacePath, { count: files.length, expiresAt: now + 3e4 });
|
|
7178
|
+
return files.length;
|
|
7179
|
+
} catch {
|
|
7180
|
+
return 0;
|
|
7181
|
+
}
|
|
7182
|
+
}
|
|
7183
|
+
async determineComplexity(prompt, workspacePath, conversationHistory = []) {
|
|
7184
|
+
if (this.isCasualGreeting(prompt)) return "Simple";
|
|
7185
|
+
if (this.looksLikeSimpleArtifactTask(prompt)) return "Simple";
|
|
7186
|
+
if (this.looksLikeConversational(prompt)) return "Simple";
|
|
7187
|
+
let workspaceContext = "";
|
|
7188
|
+
try {
|
|
7189
|
+
const count = await this.countWorkspaceFiles(workspacePath);
|
|
7190
|
+
workspaceContext = `Workspace Scout: Found ~${count} source files in the project.`;
|
|
6166
7191
|
} catch {
|
|
6167
7192
|
workspaceContext = "Workspace Scout: Could not scan workspace.";
|
|
6168
7193
|
}
|
|
@@ -6248,7 +7273,7 @@ ${prompt}` : prompt;
|
|
|
6248
7273
|
this.telemetry.capture("cascade:session_start", {
|
|
6249
7274
|
complexity,
|
|
6250
7275
|
providerCount: this.config.providers.length,
|
|
6251
|
-
cascadeAutoEnabled: this.config
|
|
7276
|
+
cascadeAutoEnabled: this.config.cascadeAuto === true,
|
|
6252
7277
|
toolCreationEnabled: this.config["enableToolCreation"] === true
|
|
6253
7278
|
});
|
|
6254
7279
|
this.emit("tier:root", { role: complexity === "Simple" ? "T3" : complexity === "Moderate" ? "T2" : "T1" });
|
|
@@ -6263,6 +7288,7 @@ ${prompt}` : prompt;
|
|
|
6263
7288
|
}));
|
|
6264
7289
|
}
|
|
6265
7290
|
const toolCreator = this.toolCreator;
|
|
7291
|
+
if (toolCreator) toolCreator.setPermissionEscalator(escalator);
|
|
6266
7292
|
let finalOutput = "";
|
|
6267
7293
|
let t2Results = [];
|
|
6268
7294
|
let runError = null;
|
|
@@ -6284,6 +7310,8 @@ ${prompt}` : prompt;
|
|
|
6284
7310
|
});
|
|
6285
7311
|
tier.on("log", (e) => this.emit("log", e));
|
|
6286
7312
|
tier.on("tier:status", (e) => this.emit("tier:status", e));
|
|
7313
|
+
tier.on("tool:call", (e) => this.emit("tool:call", e));
|
|
7314
|
+
tier.on("tool:result", (e) => this.emit("tool:result", e));
|
|
6287
7315
|
tier.on("tool:approval-request", async (request) => {
|
|
6288
7316
|
this.emit("tool:approval-request", request);
|
|
6289
7317
|
let decision = { approved: false };
|
|
@@ -6338,6 +7366,7 @@ ${prompt}` : prompt;
|
|
|
6338
7366
|
}
|
|
6339
7367
|
t2.setPermissionEscalator(escalator);
|
|
6340
7368
|
if (toolCreator) t2.setToolCreator(toolCreator);
|
|
7369
|
+
t2.setPeerMessageCallback((e) => this.emit("peer:message", e), options.sessionId ?? "");
|
|
6341
7370
|
bindTierEvents(t2);
|
|
6342
7371
|
const assignment = {
|
|
6343
7372
|
sectionId: taskId,
|
|
@@ -6367,6 +7396,7 @@ ${prompt}` : prompt;
|
|
|
6367
7396
|
}
|
|
6368
7397
|
t1.setPermissionEscalator(escalator);
|
|
6369
7398
|
if (toolCreator) t1.setToolCreator(toolCreator);
|
|
7399
|
+
t1.setPeerMessageCallback((e) => this.emit("peer:message", e), options.sessionId ?? "");
|
|
6370
7400
|
bindTierEvents(t1);
|
|
6371
7401
|
t1.on("plan", (e) => this.emit("plan", e));
|
|
6372
7402
|
const result = await t1.execute(options.prompt, options.images, void 0, options.signal);
|
|
@@ -6390,6 +7420,13 @@ ${prompt}` : prompt;
|
|
|
6390
7420
|
escalator.cancelAllPending();
|
|
6391
7421
|
} catch {
|
|
6392
7422
|
}
|
|
7423
|
+
if (this.taskAnalyzer) {
|
|
7424
|
+
try {
|
|
7425
|
+
const stats2 = this.router.getStats();
|
|
7426
|
+
this.taskAnalyzer.recordRunOutcome(runError ? "failure" : "success", stats2.costByTier);
|
|
7427
|
+
} catch {
|
|
7428
|
+
}
|
|
7429
|
+
}
|
|
6393
7430
|
try {
|
|
6394
7431
|
const stats2 = this.router.getStats();
|
|
6395
7432
|
const durationMs2 = Date.now() - startMs;
|
|
@@ -6490,7 +7527,7 @@ var Keystore = class {
|
|
|
6490
7527
|
const creds = await this.keytar.findCredentials(KEYTAR_SERVICE);
|
|
6491
7528
|
this.cache = Object.fromEntries(creds.map((c) => [c.account, c.password]));
|
|
6492
7529
|
this.backend = "keytar";
|
|
6493
|
-
if (password &&
|
|
7530
|
+
if (password && fs15__default.default.existsSync(this.storePath)) {
|
|
6494
7531
|
try {
|
|
6495
7532
|
const fileEntries = this.decryptFile(password);
|
|
6496
7533
|
for (const [k, v] of Object.entries(fileEntries)) {
|
|
@@ -6509,7 +7546,7 @@ var Keystore = class {
|
|
|
6509
7546
|
"Keystore unlock requires a password because the OS keychain (keytar) is not available on this system."
|
|
6510
7547
|
);
|
|
6511
7548
|
}
|
|
6512
|
-
if (!
|
|
7549
|
+
if (!fs15__default.default.existsSync(this.storePath)) {
|
|
6513
7550
|
const salt = crypto__default.default.randomBytes(SALT_LEN);
|
|
6514
7551
|
this.masterKey = this.deriveKey(password, salt);
|
|
6515
7552
|
this.writeWithSalt({}, salt);
|
|
@@ -6523,7 +7560,7 @@ var Keystore = class {
|
|
|
6523
7560
|
}
|
|
6524
7561
|
/** Synchronous legacy unlock kept for AES-only environments. */
|
|
6525
7562
|
unlockSync(password) {
|
|
6526
|
-
if (!
|
|
7563
|
+
if (!fs15__default.default.existsSync(this.storePath)) {
|
|
6527
7564
|
const salt = crypto__default.default.randomBytes(SALT_LEN);
|
|
6528
7565
|
this.masterKey = this.deriveKey(password, salt);
|
|
6529
7566
|
this.writeWithSalt({}, salt);
|
|
@@ -6581,7 +7618,7 @@ var Keystore = class {
|
|
|
6581
7618
|
}
|
|
6582
7619
|
}
|
|
6583
7620
|
decryptFile(password, knownSalt) {
|
|
6584
|
-
if (!
|
|
7621
|
+
if (!fs15__default.default.existsSync(this.storePath)) return {};
|
|
6585
7622
|
try {
|
|
6586
7623
|
const { salt, ciphertext, iv, tag } = this.readRaw();
|
|
6587
7624
|
const useSalt = knownSalt ?? salt;
|
|
@@ -6603,8 +7640,8 @@ var Keystore = class {
|
|
|
6603
7640
|
const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
|
|
6604
7641
|
const tag = cipher.getAuthTag();
|
|
6605
7642
|
const out = Buffer.concat([raw.salt, iv, tag, ciphertext]);
|
|
6606
|
-
|
|
6607
|
-
|
|
7643
|
+
fs15__default.default.mkdirSync(path16__default.default.dirname(this.storePath), { recursive: true });
|
|
7644
|
+
fs15__default.default.writeFileSync(this.storePath, out, { mode: 384 });
|
|
6608
7645
|
}
|
|
6609
7646
|
writeWithSalt(data, salt) {
|
|
6610
7647
|
if (!this.masterKey) throw new Error("writeWithSalt called before masterKey was set");
|
|
@@ -6614,11 +7651,11 @@ var Keystore = class {
|
|
|
6614
7651
|
const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
|
|
6615
7652
|
const tag = cipher.getAuthTag();
|
|
6616
7653
|
const out = Buffer.concat([salt, iv, tag, ciphertext]);
|
|
6617
|
-
|
|
6618
|
-
|
|
7654
|
+
fs15__default.default.mkdirSync(path16__default.default.dirname(this.storePath), { recursive: true });
|
|
7655
|
+
fs15__default.default.writeFileSync(this.storePath, out, { mode: 384 });
|
|
6619
7656
|
}
|
|
6620
7657
|
readRaw() {
|
|
6621
|
-
const buf =
|
|
7658
|
+
const buf = fs15__default.default.readFileSync(this.storePath);
|
|
6622
7659
|
let offset = 0;
|
|
6623
7660
|
const salt = buf.subarray(offset, offset + SALT_LEN);
|
|
6624
7661
|
offset += SALT_LEN;
|
|
@@ -6651,9 +7688,9 @@ var CascadeIgnore = class {
|
|
|
6651
7688
|
]);
|
|
6652
7689
|
}
|
|
6653
7690
|
async load(workspacePath) {
|
|
6654
|
-
const filePath =
|
|
7691
|
+
const filePath = path16__default.default.join(workspacePath, ".cascadeignore");
|
|
6655
7692
|
try {
|
|
6656
|
-
const content = await
|
|
7693
|
+
const content = await fs3__default.default.readFile(filePath, "utf-8");
|
|
6657
7694
|
const lines = content.split("\n").filter((l) => l.trim() && !l.startsWith("#"));
|
|
6658
7695
|
this.ig.add(lines);
|
|
6659
7696
|
this.loaded = true;
|
|
@@ -6662,7 +7699,7 @@ var CascadeIgnore = class {
|
|
|
6662
7699
|
}
|
|
6663
7700
|
isIgnored(filePath, workspacePath) {
|
|
6664
7701
|
try {
|
|
6665
|
-
const relative = workspacePath ?
|
|
7702
|
+
const relative = workspacePath ? path16__default.default.relative(workspacePath, filePath) : filePath;
|
|
6666
7703
|
return this.ig.ignores(relative);
|
|
6667
7704
|
} catch {
|
|
6668
7705
|
return false;
|
|
@@ -6673,9 +7710,9 @@ var CascadeIgnore = class {
|
|
|
6673
7710
|
}
|
|
6674
7711
|
};
|
|
6675
7712
|
async function loadCascadeMd(workspacePath) {
|
|
6676
|
-
const filePath =
|
|
7713
|
+
const filePath = path16__default.default.join(workspacePath, "CASCADE.md");
|
|
6677
7714
|
try {
|
|
6678
|
-
const raw = await
|
|
7715
|
+
const raw = await fs3__default.default.readFile(filePath, "utf-8");
|
|
6679
7716
|
return parseCascadeMd(raw);
|
|
6680
7717
|
} catch {
|
|
6681
7718
|
return null;
|
|
@@ -6704,7 +7741,7 @@ ${raw.trim()}`;
|
|
|
6704
7741
|
var MemoryStore = class _MemoryStore {
|
|
6705
7742
|
db;
|
|
6706
7743
|
constructor(dbPath) {
|
|
6707
|
-
|
|
7744
|
+
fs15__default.default.mkdirSync(path16__default.default.dirname(dbPath), { recursive: true });
|
|
6708
7745
|
try {
|
|
6709
7746
|
this.db = new Database__default.default(dbPath, { timeout: 5e3 });
|
|
6710
7747
|
this.db.pragma("journal_mode = WAL");
|
|
@@ -7187,6 +8224,27 @@ Original error: ${err.message}`
|
|
|
7187
8224
|
if (!row.oldest) return Infinity;
|
|
7188
8225
|
return Date.now() - new Date(row.oldest).getTime();
|
|
7189
8226
|
}
|
|
8227
|
+
saveModelProfile(modelId, provider, specializations) {
|
|
8228
|
+
const cacheKey = `${provider}:${modelId}`;
|
|
8229
|
+
const existing = this.db.prepare("SELECT metadata FROM model_cache WHERE id = ?").get(cacheKey);
|
|
8230
|
+
const meta = existing ? JSON.parse(existing.metadata) : { id: modelId, provider, name: modelId, contextWindow: 0, isVisionCapable: false, inputCostPer1kTokens: 0, outputCostPer1kTokens: 0, maxOutputTokens: 0, supportsStreaming: false, isLocal: false };
|
|
8231
|
+
meta.specializations = specializations;
|
|
8232
|
+
this.db.prepare(`
|
|
8233
|
+
INSERT INTO model_cache (id, provider, model_id, name, metadata, updated_at)
|
|
8234
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
8235
|
+
ON CONFLICT(id) DO UPDATE SET metadata = excluded.metadata, updated_at = excluded.updated_at
|
|
8236
|
+
`).run(cacheKey, provider, modelId, meta.name ?? modelId, JSON.stringify(meta), (/* @__PURE__ */ new Date()).toISOString());
|
|
8237
|
+
}
|
|
8238
|
+
getModelProfile(modelId, provider) {
|
|
8239
|
+
const row = this.db.prepare("SELECT metadata FROM model_cache WHERE id = ?").get(`${provider}:${modelId}`);
|
|
8240
|
+
return row ? JSON.parse(row.metadata) : void 0;
|
|
8241
|
+
}
|
|
8242
|
+
getProfiledModelIds() {
|
|
8243
|
+
const rows = this.db.prepare(
|
|
8244
|
+
"SELECT model_id FROM model_cache WHERE json_extract(metadata, '$.specializations') IS NOT NULL"
|
|
8245
|
+
).all();
|
|
8246
|
+
return rows.map((r) => r.model_id);
|
|
8247
|
+
}
|
|
7190
8248
|
// ── Tool Result Cache (in-memory, TTL-based) ──────────────────────────
|
|
7191
8249
|
// Avoids redundant calls for read-only tools within a short window.
|
|
7192
8250
|
// Not persisted to DB — cleared on process restart.
|
|
@@ -7441,15 +8499,15 @@ var ConfigManager = class {
|
|
|
7441
8499
|
globalDir;
|
|
7442
8500
|
constructor(workspacePath = process.cwd()) {
|
|
7443
8501
|
this.workspacePath = workspacePath;
|
|
7444
|
-
this.globalDir =
|
|
8502
|
+
this.globalDir = path16__default.default.join(os3__default.default.homedir(), GLOBAL_CONFIG_DIR);
|
|
7445
8503
|
}
|
|
7446
8504
|
async load() {
|
|
7447
8505
|
this.config = await this.loadConfig();
|
|
7448
8506
|
this.ignore = new CascadeIgnore();
|
|
7449
8507
|
await this.ignore.load(this.workspacePath);
|
|
7450
8508
|
this.cascadeMd = await loadCascadeMd(this.workspacePath);
|
|
7451
|
-
this.keystore = new Keystore(
|
|
7452
|
-
this.store = new MemoryStore(
|
|
8509
|
+
this.keystore = new Keystore(path16__default.default.join(this.globalDir, GLOBAL_KEYSTORE_FILE));
|
|
8510
|
+
this.store = new MemoryStore(path16__default.default.join(this.workspacePath, CASCADE_DB_FILE));
|
|
7453
8511
|
await this.injectEnvKeys();
|
|
7454
8512
|
await this.ensureDefaultIdentity();
|
|
7455
8513
|
}
|
|
@@ -7472,9 +8530,9 @@ var ConfigManager = class {
|
|
|
7472
8530
|
return this.workspacePath;
|
|
7473
8531
|
}
|
|
7474
8532
|
async save() {
|
|
7475
|
-
const configPath =
|
|
7476
|
-
await
|
|
7477
|
-
await
|
|
8533
|
+
const configPath = path16__default.default.join(this.workspacePath, CASCADE_CONFIG_FILE);
|
|
8534
|
+
await fs3__default.default.mkdir(path16__default.default.dirname(configPath), { recursive: true });
|
|
8535
|
+
await fs3__default.default.writeFile(configPath, JSON.stringify(this.config, null, 2), "utf-8");
|
|
7478
8536
|
}
|
|
7479
8537
|
async updateConfig(updates) {
|
|
7480
8538
|
this.config = validateConfig({ ...this.config, ...updates });
|
|
@@ -7497,9 +8555,9 @@ var ConfigManager = class {
|
|
|
7497
8555
|
return configProvider?.apiKey;
|
|
7498
8556
|
}
|
|
7499
8557
|
async loadConfig() {
|
|
7500
|
-
const configPath =
|
|
8558
|
+
const configPath = path16__default.default.join(this.workspacePath, CASCADE_CONFIG_FILE);
|
|
7501
8559
|
try {
|
|
7502
|
-
const raw = await
|
|
8560
|
+
const raw = await fs3__default.default.readFile(configPath, "utf-8");
|
|
7503
8561
|
return validateConfig(JSON.parse(raw));
|
|
7504
8562
|
} catch (err) {
|
|
7505
8563
|
if (err.code === "ENOENT") {
|
|
@@ -7663,6 +8721,9 @@ var DashboardSocket = class {
|
|
|
7663
8721
|
emitStreamToken(tierId, text, sessionId) {
|
|
7664
8722
|
this.io.to(`session:${sessionId}`).emit("stream:token", { tierId, text, sessionId });
|
|
7665
8723
|
}
|
|
8724
|
+
emitPeerMessage(event) {
|
|
8725
|
+
this.io.to(`session:${event.sessionId}`).emit("peer:message", event);
|
|
8726
|
+
}
|
|
7666
8727
|
emitApprovalRequest(request) {
|
|
7667
8728
|
this.io.emit("permission:user-required", request);
|
|
7668
8729
|
}
|
|
@@ -7705,16 +8766,13 @@ var DashboardSocket = class {
|
|
|
7705
8766
|
const { sessionId } = normalizeSessionSubscriptionPayload(payload);
|
|
7706
8767
|
socket.leave(`session:${sessionId}`);
|
|
7707
8768
|
});
|
|
7708
|
-
socket.on("join:tenant", (tenantId) => {
|
|
7709
|
-
socket.join(`tenant:${tenantId}`);
|
|
7710
|
-
});
|
|
7711
8769
|
});
|
|
7712
8770
|
}
|
|
7713
8771
|
close() {
|
|
7714
8772
|
this.io.close();
|
|
7715
8773
|
}
|
|
7716
8774
|
};
|
|
7717
|
-
var __dirname$1 =
|
|
8775
|
+
var __dirname$1 = path16__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))));
|
|
7718
8776
|
var DashboardServer = class {
|
|
7719
8777
|
app;
|
|
7720
8778
|
httpServer;
|
|
@@ -7780,15 +8838,15 @@ var DashboardServer = class {
|
|
|
7780
8838
|
resolveDashboardSecret() {
|
|
7781
8839
|
const fromConfig = this.config.dashboard.secret ?? process.env["CASCADE_DASHBOARD_SECRET"];
|
|
7782
8840
|
if (fromConfig) return fromConfig;
|
|
7783
|
-
const secretPath =
|
|
8841
|
+
const secretPath = path16__default.default.join(this.workspacePath, CASCADE_DASHBOARD_SECRET_FILE);
|
|
7784
8842
|
try {
|
|
7785
|
-
if (
|
|
7786
|
-
const existing =
|
|
8843
|
+
if (fs15__default.default.existsSync(secretPath)) {
|
|
8844
|
+
const existing = fs15__default.default.readFileSync(secretPath, "utf-8").trim();
|
|
7787
8845
|
if (existing.length >= 16) return existing;
|
|
7788
8846
|
}
|
|
7789
8847
|
const generated = crypto.randomUUID();
|
|
7790
|
-
|
|
7791
|
-
|
|
8848
|
+
fs15__default.default.mkdirSync(path16__default.default.dirname(secretPath), { recursive: true });
|
|
8849
|
+
fs15__default.default.writeFileSync(secretPath, generated, { encoding: "utf-8", mode: 384 });
|
|
7792
8850
|
if (this.config.dashboard.auth) {
|
|
7793
8851
|
console.warn(
|
|
7794
8852
|
`Dashboard auth enabled with no secret configured; persisted a generated secret to ${secretPath}. Set CASCADE_DASHBOARD_SECRET or config.dashboard.secret to override.`
|
|
@@ -7815,7 +8873,7 @@ var DashboardServer = class {
|
|
|
7815
8873
|
// ── Setup ─────────────────────────────────────
|
|
7816
8874
|
getGlobalStore() {
|
|
7817
8875
|
if (!this.globalStore) {
|
|
7818
|
-
const globalDbPath =
|
|
8876
|
+
const globalDbPath = path16__default.default.join(os3__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
|
|
7819
8877
|
this.globalStore = new MemoryStore(globalDbPath);
|
|
7820
8878
|
}
|
|
7821
8879
|
return this.globalStore;
|
|
@@ -7876,12 +8934,12 @@ var DashboardServer = class {
|
|
|
7876
8934
|
}
|
|
7877
8935
|
}
|
|
7878
8936
|
watchRuntimeChanges() {
|
|
7879
|
-
const workspaceDbPath =
|
|
7880
|
-
const globalDbPath =
|
|
8937
|
+
const workspaceDbPath = path16__default.default.join(this.workspacePath, CASCADE_DB_FILE);
|
|
8938
|
+
const globalDbPath = path16__default.default.join(os3__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
|
|
7881
8939
|
const watchPaths = [workspaceDbPath, globalDbPath].filter((p, index, arr) => arr.indexOf(p) === index);
|
|
7882
8940
|
for (const watchPath of watchPaths) {
|
|
7883
|
-
if (!
|
|
7884
|
-
|
|
8941
|
+
if (!fs15__default.default.existsSync(watchPath)) continue;
|
|
8942
|
+
fs15__default.default.watchFile(watchPath, { interval: 3e3 }, () => {
|
|
7885
8943
|
this.throttledBroadcast(watchPath === globalDbPath ? "global" : "workspace");
|
|
7886
8944
|
});
|
|
7887
8945
|
}
|
|
@@ -7912,6 +8970,21 @@ var DashboardServer = class {
|
|
|
7912
8970
|
legacyHeaders: false,
|
|
7913
8971
|
message: { error: "Too many login attempts. Try again in 15 minutes." }
|
|
7914
8972
|
});
|
|
8973
|
+
const apiLimiter = rateLimit__default.default({
|
|
8974
|
+
windowMs: 60 * 1e3,
|
|
8975
|
+
limit: 60,
|
|
8976
|
+
standardHeaders: "draft-7",
|
|
8977
|
+
legacyHeaders: false,
|
|
8978
|
+
message: { error: "Too many requests. Slow down." }
|
|
8979
|
+
});
|
|
8980
|
+
this.app.use("/api", apiLimiter);
|
|
8981
|
+
const mutationLimiter = rateLimit__default.default({
|
|
8982
|
+
windowMs: 60 * 1e3,
|
|
8983
|
+
limit: 10,
|
|
8984
|
+
standardHeaders: "draft-7",
|
|
8985
|
+
legacyHeaders: false,
|
|
8986
|
+
message: { error: "Too many requests on this endpoint." }
|
|
8987
|
+
});
|
|
7915
8988
|
this.app.post("/api/auth/login", loginLimiter, (req, res) => {
|
|
7916
8989
|
const { username, password } = req.body ?? {};
|
|
7917
8990
|
if (!authRequired) {
|
|
@@ -7947,22 +9020,33 @@ var DashboardServer = class {
|
|
|
7947
9020
|
res.status(401).json({ error: "Invalid credentials" });
|
|
7948
9021
|
}
|
|
7949
9022
|
});
|
|
7950
|
-
this.app.post("/api/force-halt", auth, (req, res) => {
|
|
7951
|
-
const
|
|
9023
|
+
this.app.post("/api/force-halt", auth, mutationLimiter, (req, res) => {
|
|
9024
|
+
const body = req.body;
|
|
9025
|
+
const sessionId = typeof body["sessionId"] === "string" ? body["sessionId"] : void 0;
|
|
9026
|
+
const nodeId = typeof body["nodeId"] === "string" ? body["nodeId"] : void 0;
|
|
7952
9027
|
const payload = { sessionId, nodeId, requestedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
7953
9028
|
this.socket.broadcast("session:halt", payload);
|
|
7954
9029
|
if (sessionId) this.socket.broadcastToRoom(`session:${sessionId}`, "session:halt", payload);
|
|
7955
9030
|
res.json({ success: true, ...payload });
|
|
7956
9031
|
});
|
|
7957
|
-
this.app.post("/api/approve", auth, (req, res) => {
|
|
7958
|
-
const
|
|
9032
|
+
this.app.post("/api/approve", auth, mutationLimiter, (req, res) => {
|
|
9033
|
+
const body = req.body;
|
|
9034
|
+
const sessionId = typeof body["sessionId"] === "string" ? body["sessionId"] : void 0;
|
|
9035
|
+
const nodeId = typeof body["nodeId"] === "string" ? body["nodeId"] : void 0;
|
|
7959
9036
|
const payload = { sessionId, nodeId, requestedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
7960
9037
|
this.socket.broadcast("session:approve", payload);
|
|
7961
9038
|
if (sessionId) this.socket.broadcastToRoom(`session:${sessionId}`, "session:approve", payload);
|
|
7962
9039
|
res.json({ success: true, ...payload });
|
|
7963
9040
|
});
|
|
7964
|
-
this.app.post("/api/inject", auth, (req, res) => {
|
|
7965
|
-
const
|
|
9041
|
+
this.app.post("/api/inject", auth, mutationLimiter, (req, res) => {
|
|
9042
|
+
const body = req.body;
|
|
9043
|
+
const message = typeof body["message"] === "string" ? body["message"] : void 0;
|
|
9044
|
+
const sessionId = typeof body["sessionId"] === "string" ? body["sessionId"] : void 0;
|
|
9045
|
+
const nodeId = typeof body["nodeId"] === "string" ? body["nodeId"] : void 0;
|
|
9046
|
+
if (!message) {
|
|
9047
|
+
res.status(400).json({ error: "message is required and must be a string" });
|
|
9048
|
+
return;
|
|
9049
|
+
}
|
|
7966
9050
|
const payload = { sessionId, nodeId, message, requestedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
7967
9051
|
this.socket.broadcast("session:message-injected", payload);
|
|
7968
9052
|
if (sessionId) this.socket.broadcastToRoom(`session:${sessionId}`, "session:message-injected", payload);
|
|
@@ -7985,7 +9069,7 @@ var DashboardServer = class {
|
|
|
7985
9069
|
const sessionId = req.params.id;
|
|
7986
9070
|
this.store.deleteSession(sessionId);
|
|
7987
9071
|
this.store.deleteRuntimeSession(sessionId);
|
|
7988
|
-
const globalDbPath =
|
|
9072
|
+
const globalDbPath = path16__default.default.join(os3__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
|
|
7989
9073
|
const globalStore = new MemoryStore(globalDbPath);
|
|
7990
9074
|
try {
|
|
7991
9075
|
globalStore.deleteRuntimeSession(sessionId);
|
|
@@ -7999,7 +9083,7 @@ var DashboardServer = class {
|
|
|
7999
9083
|
});
|
|
8000
9084
|
this.app.delete("/api/sessions", auth, (req, res) => {
|
|
8001
9085
|
const body = req.body;
|
|
8002
|
-
const globalDbPath =
|
|
9086
|
+
const globalDbPath = path16__default.default.join(os3__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
|
|
8003
9087
|
if (body?.ids && Array.isArray(body.ids) && body.ids.length > 0) {
|
|
8004
9088
|
const globalStore = new MemoryStore(globalDbPath);
|
|
8005
9089
|
try {
|
|
@@ -8022,7 +9106,7 @@ var DashboardServer = class {
|
|
|
8022
9106
|
});
|
|
8023
9107
|
this.app.delete("/api/runtime", auth, (_req, res) => {
|
|
8024
9108
|
this.store.deleteAllRuntimeNodes();
|
|
8025
|
-
const globalDbPath =
|
|
9109
|
+
const globalDbPath = path16__default.default.join(os3__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
|
|
8026
9110
|
const globalStore = new MemoryStore(globalDbPath);
|
|
8027
9111
|
try {
|
|
8028
9112
|
globalStore.deleteAllRuntimeNodes();
|
|
@@ -8084,16 +9168,26 @@ var DashboardServer = class {
|
|
|
8084
9168
|
});
|
|
8085
9169
|
this.app.put("/api/config", auth, async (req, res) => {
|
|
8086
9170
|
const body = req.body;
|
|
8087
|
-
if (body
|
|
8088
|
-
|
|
9171
|
+
if (body["tierLimits"] !== void 0 && (typeof body["tierLimits"] !== "object" || Array.isArray(body["tierLimits"]))) {
|
|
9172
|
+
res.status(400).json({ error: "tierLimits must be an object" });
|
|
9173
|
+
return;
|
|
9174
|
+
}
|
|
9175
|
+
if (body["budget"] !== void 0 && (typeof body["budget"] !== "object" || Array.isArray(body["budget"]))) {
|
|
9176
|
+
res.status(400).json({ error: "budget must be an object" });
|
|
9177
|
+
return;
|
|
9178
|
+
}
|
|
9179
|
+
if (body["tierLimits"]) this.config.tierLimits = { ...this.config.tierLimits, ...body["tierLimits"] };
|
|
9180
|
+
if (body["budget"]) this.config.budget = { ...this.config.budget, ...body["budget"] };
|
|
8089
9181
|
try {
|
|
8090
|
-
const configPath =
|
|
8091
|
-
const existing =
|
|
9182
|
+
const configPath = path16__default.default.join(this.workspacePath, CASCADE_CONFIG_FILE);
|
|
9183
|
+
const existing = fs15__default.default.existsSync(configPath) ? JSON.parse(fs15__default.default.readFileSync(configPath, "utf-8")) : {};
|
|
8092
9184
|
const updated = { ...existing, tierLimits: this.config.tierLimits, budget: this.config.budget };
|
|
8093
|
-
|
|
9185
|
+
const tmp = configPath + ".tmp";
|
|
9186
|
+
fs15__default.default.writeFileSync(tmp, JSON.stringify(updated, null, 2), "utf-8");
|
|
9187
|
+
fs15__default.default.renameSync(tmp, configPath);
|
|
8094
9188
|
res.json({ ok: true });
|
|
8095
9189
|
} catch (err) {
|
|
8096
|
-
res.status(500).json({ error: `Failed to save config: ${String(err)}` });
|
|
9190
|
+
res.status(500).json({ error: `Failed to save config: ${err instanceof Error ? err.message : String(err)}` });
|
|
8097
9191
|
}
|
|
8098
9192
|
});
|
|
8099
9193
|
this.app.get("/api/runtime/logs/:sessionId", auth, (req, res) => {
|
|
@@ -8118,7 +9212,7 @@ var DashboardServer = class {
|
|
|
8118
9212
|
this.app.get("/api/runtime", auth, (req, res) => {
|
|
8119
9213
|
const scope = req.query["scope"] ?? "workspace";
|
|
8120
9214
|
if (scope === "global") {
|
|
8121
|
-
const globalDbPath =
|
|
9215
|
+
const globalDbPath = path16__default.default.join(os3__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
|
|
8122
9216
|
const globalStore = new MemoryStore(globalDbPath);
|
|
8123
9217
|
try {
|
|
8124
9218
|
res.json({
|
|
@@ -8139,7 +9233,7 @@ var DashboardServer = class {
|
|
|
8139
9233
|
logs: this.store.listRuntimeNodeLogs(void 0, void 0, 500)
|
|
8140
9234
|
});
|
|
8141
9235
|
});
|
|
8142
|
-
this.app.post("/api/run", auth, (req, res) => {
|
|
9236
|
+
this.app.post("/api/run", auth, mutationLimiter, (req, res) => {
|
|
8143
9237
|
const body = req.body;
|
|
8144
9238
|
if (!body.prompt || typeof body.prompt !== "string") {
|
|
8145
9239
|
res.status(400).json({ error: "prompt is required" });
|
|
@@ -8160,12 +9254,15 @@ var DashboardServer = class {
|
|
|
8160
9254
|
cascade.on("permission:user-required", (e) => {
|
|
8161
9255
|
this.socket.broadcastToRoom(`session:${sessionId}`, "permission:user-required", { sessionId, ...e });
|
|
8162
9256
|
});
|
|
9257
|
+
cascade.on("peer:message", (e) => {
|
|
9258
|
+
this.socket.emitPeerMessage(e);
|
|
9259
|
+
});
|
|
8163
9260
|
try {
|
|
8164
9261
|
const result = await cascade.run({ prompt: body.prompt, identityId: body.identityId });
|
|
8165
9262
|
this.socket.broadcast("cost:update", {
|
|
8166
9263
|
sessionId,
|
|
8167
|
-
|
|
8168
|
-
|
|
9264
|
+
totalTokens: result.usage.totalTokens,
|
|
9265
|
+
totalCostUsd: result.usage.estimatedCostUsd
|
|
8169
9266
|
});
|
|
8170
9267
|
this.socket.broadcastToRoom(`session:${sessionId}`, "session:complete", { sessionId, result });
|
|
8171
9268
|
this.throttledBroadcast("workspace");
|
|
@@ -8188,13 +9285,13 @@ var DashboardServer = class {
|
|
|
8188
9285
|
}))
|
|
8189
9286
|
});
|
|
8190
9287
|
});
|
|
8191
|
-
const prodPath =
|
|
8192
|
-
const devPath =
|
|
8193
|
-
const webDistPath =
|
|
8194
|
-
if (
|
|
9288
|
+
const prodPath = path16__default.default.resolve(__dirname$1, "../web/dist");
|
|
9289
|
+
const devPath = path16__default.default.resolve(__dirname$1, "../../web/dist");
|
|
9290
|
+
const webDistPath = fs15__default.default.existsSync(prodPath) ? prodPath : devPath;
|
|
9291
|
+
if (fs15__default.default.existsSync(webDistPath)) {
|
|
8195
9292
|
this.app.use(express__default.default.static(webDistPath));
|
|
8196
9293
|
this.app.get("*", (_req, res) => {
|
|
8197
|
-
res.sendFile(
|
|
9294
|
+
res.sendFile(path16__default.default.join(webDistPath, "index.html"));
|
|
8198
9295
|
});
|
|
8199
9296
|
} else {
|
|
8200
9297
|
this.app.get("/", (_req, res) => {
|
|
@@ -8271,7 +9368,7 @@ var TaskScheduler = class {
|
|
|
8271
9368
|
return cron__default.default.validate(expression);
|
|
8272
9369
|
}
|
|
8273
9370
|
};
|
|
8274
|
-
var
|
|
9371
|
+
var execFileAsync2 = util.promisify(child_process.execFile);
|
|
8275
9372
|
var SAFE_ENV_NAME = /^[A-Z][A-Z0-9_]*$/;
|
|
8276
9373
|
function sanitizeEnvValue(v) {
|
|
8277
9374
|
const raw = typeof v === "string" ? v : JSON.stringify(v);
|
|
@@ -8310,7 +9407,7 @@ var HooksRunner = class {
|
|
|
8310
9407
|
const isWin = process.platform === "win32";
|
|
8311
9408
|
const shell = isWin ? "cmd.exe" : "/bin/sh";
|
|
8312
9409
|
const shellArgs = isWin ? ["/d", "/s", "/c", hook.command] : ["-c", hook.command];
|
|
8313
|
-
const { stdout } = await
|
|
9410
|
+
const { stdout } = await execFileAsync2(shell, shellArgs, {
|
|
8314
9411
|
timeout: hook.timeout ?? 1e4,
|
|
8315
9412
|
env: { ...process.env, ...envVars },
|
|
8316
9413
|
windowsHide: true
|