cascade-ai 0.3.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 +2340 -1008
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +2339 -1006
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +1452 -335
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +178 -74
- package/dist/index.d.ts +178 -74
- package/dist/index.js +1449 -332
- 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";
|
|
@@ -903,19 +903,21 @@ var OpenAIProvider = class extends BaseProvider {
|
|
|
903
903
|
// src/providers/azure.ts
|
|
904
904
|
var AzureOpenAIProvider = class extends OpenAIProvider {
|
|
905
905
|
constructor(config, model) {
|
|
906
|
-
const
|
|
906
|
+
const rawUrl = config.baseUrl ?? AZURE_BASE_URL_TEMPLATE.replace("{resource}", "YOUR_RESOURCE");
|
|
907
|
+
const endpoint = rawUrl.replace(/\/+$/, "");
|
|
907
908
|
super(
|
|
908
909
|
{
|
|
909
910
|
...config,
|
|
910
|
-
baseUrl:
|
|
911
|
+
baseUrl: endpoint
|
|
912
|
+
// Kept for superclass compatibility if it reads it
|
|
911
913
|
},
|
|
912
914
|
model
|
|
913
915
|
);
|
|
914
|
-
this.client = new
|
|
916
|
+
this.client = new OpenAI.AzureOpenAI({
|
|
915
917
|
apiKey: config.apiKey,
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
918
|
+
endpoint,
|
|
919
|
+
deployment: config.deploymentName ?? model.id,
|
|
920
|
+
apiVersion: config.apiVersion ?? "2024-08-01-preview"
|
|
919
921
|
});
|
|
920
922
|
}
|
|
921
923
|
async listModels() {
|
|
@@ -1157,6 +1159,22 @@ var GeminiProvider = class extends BaseProvider {
|
|
|
1157
1159
|
};
|
|
1158
1160
|
}
|
|
1159
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
|
+
}
|
|
1160
1178
|
var OllamaProvider = class extends BaseProvider {
|
|
1161
1179
|
baseUrl;
|
|
1162
1180
|
constructor(config, model) {
|
|
@@ -1169,12 +1187,21 @@ var OllamaProvider = class extends BaseProvider {
|
|
|
1169
1187
|
}
|
|
1170
1188
|
async generateStream(options, onChunk) {
|
|
1171
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
|
+
}));
|
|
1172
1198
|
const response = await axios2__default.default.post(
|
|
1173
1199
|
`${this.baseUrl}/api/chat`,
|
|
1174
1200
|
{
|
|
1175
1201
|
model: this.model.id,
|
|
1176
1202
|
messages,
|
|
1177
1203
|
stream: true,
|
|
1204
|
+
tools: ollamaTools?.length ? ollamaTools : void 0,
|
|
1178
1205
|
options: {
|
|
1179
1206
|
num_predict: options.maxTokens ?? this.model.maxOutputTokens,
|
|
1180
1207
|
temperature: options.temperature ?? 0.7
|
|
@@ -1185,6 +1212,7 @@ var OllamaProvider = class extends BaseProvider {
|
|
|
1185
1212
|
let fullContent = "";
|
|
1186
1213
|
let inputTokens = 0;
|
|
1187
1214
|
let outputTokens = 0;
|
|
1215
|
+
const pendingToolCalls = [];
|
|
1188
1216
|
await new Promise((resolve, reject) => {
|
|
1189
1217
|
let buffer = "";
|
|
1190
1218
|
response.data.on("data", (chunk) => {
|
|
@@ -1199,6 +1227,9 @@ var OllamaProvider = class extends BaseProvider {
|
|
|
1199
1227
|
fullContent += parsed.message.content;
|
|
1200
1228
|
onChunk({ text: parsed.message.content, finishReason: null });
|
|
1201
1229
|
}
|
|
1230
|
+
if (parsed.message?.tool_calls?.length) {
|
|
1231
|
+
pendingToolCalls.push(...parsed.message.tool_calls);
|
|
1232
|
+
}
|
|
1202
1233
|
if (parsed.done) {
|
|
1203
1234
|
inputTokens = parsed.prompt_eval_count ?? 0;
|
|
1204
1235
|
outputTokens = parsed.eval_count ?? 0;
|
|
@@ -1216,6 +1247,9 @@ var OllamaProvider = class extends BaseProvider {
|
|
|
1216
1247
|
fullContent += parsed.message.content;
|
|
1217
1248
|
onChunk({ text: parsed.message.content, finishReason: null });
|
|
1218
1249
|
}
|
|
1250
|
+
if (parsed.message?.tool_calls?.length) {
|
|
1251
|
+
pendingToolCalls.push(...parsed.message.tool_calls);
|
|
1252
|
+
}
|
|
1219
1253
|
if (parsed.done) {
|
|
1220
1254
|
inputTokens = parsed.prompt_eval_count ?? inputTokens;
|
|
1221
1255
|
outputTokens = parsed.eval_count ?? outputTokens;
|
|
@@ -1227,11 +1261,30 @@ var OllamaProvider = class extends BaseProvider {
|
|
|
1227
1261
|
});
|
|
1228
1262
|
response.data.on("error", reject);
|
|
1229
1263
|
});
|
|
1230
|
-
|
|
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 });
|
|
1231
1283
|
return {
|
|
1232
1284
|
content: fullContent,
|
|
1233
1285
|
usage: this.makeUsage(inputTokens, outputTokens),
|
|
1234
|
-
|
|
1286
|
+
toolCalls: toolCalls.length ? toolCalls : void 0,
|
|
1287
|
+
finishReason
|
|
1235
1288
|
};
|
|
1236
1289
|
}
|
|
1237
1290
|
async countTokens(text) {
|
|
@@ -1255,6 +1308,7 @@ var OllamaProvider = class extends BaseProvider {
|
|
|
1255
1308
|
maxOutputTokens: 4e3,
|
|
1256
1309
|
supportsStreaming: true,
|
|
1257
1310
|
isLocal: true,
|
|
1311
|
+
supportsToolUse: isToolCapable(m.name),
|
|
1258
1312
|
minSizeB: this.parseSizeB(m.details?.parameter_size)
|
|
1259
1313
|
}));
|
|
1260
1314
|
} catch {
|
|
@@ -1277,6 +1331,26 @@ var OllamaProvider = class extends BaseProvider {
|
|
|
1277
1331
|
result.push({ role: "system", content: typeof m.content === "string" ? m.content : "" });
|
|
1278
1332
|
continue;
|
|
1279
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
|
+
}
|
|
1280
1354
|
if (typeof m.content === "string") {
|
|
1281
1355
|
result.push({ role: m.role, content: m.content });
|
|
1282
1356
|
continue;
|
|
@@ -1409,6 +1483,26 @@ var ModelSelector = class {
|
|
|
1409
1483
|
return T3_MODEL_PRIORITY;
|
|
1410
1484
|
}
|
|
1411
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
|
+
}
|
|
1412
1506
|
isProviderAvailable(provider) {
|
|
1413
1507
|
return this.availableProviders.has(provider);
|
|
1414
1508
|
}
|
|
@@ -1614,11 +1708,203 @@ var TpmLimiter = class {
|
|
|
1614
1708
|
}
|
|
1615
1709
|
};
|
|
1616
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
|
+
|
|
1617
1775
|
// src/utils/cost.ts
|
|
1618
1776
|
function calculateCost(inputTokens, outputTokens, model) {
|
|
1619
1777
|
return inputTokens / 1e3 * model.inputCostPer1kTokens + outputTokens / 1e3 * model.outputCostPer1kTokens;
|
|
1620
1778
|
}
|
|
1621
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
|
+
|
|
1622
1908
|
// src/core/router/index.ts
|
|
1623
1909
|
var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
|
|
1624
1910
|
selector;
|
|
@@ -1646,6 +1932,7 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
|
|
|
1646
1932
|
budgetState = "ok";
|
|
1647
1933
|
budgetExceededReason;
|
|
1648
1934
|
tpmLimiter;
|
|
1935
|
+
localQueue;
|
|
1649
1936
|
/** Thrown when the configured budget is exceeded. */
|
|
1650
1937
|
static BudgetExceededError = class extends Error {
|
|
1651
1938
|
constructor(msg) {
|
|
@@ -1662,6 +1949,7 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
|
|
|
1662
1949
|
this.selector = new ModelSelector(availableProviders);
|
|
1663
1950
|
this.failover = new FailoverManager(this.selector);
|
|
1664
1951
|
this.tpmLimiter = new TpmLimiter(config.rateLimits?.providerTpm ?? {});
|
|
1952
|
+
this.localQueue = new LocalRequestQueue(config.localConcurrency ?? 1);
|
|
1665
1953
|
const ollamaCfg = config.providers.find((p) => p.type === "ollama");
|
|
1666
1954
|
if (availableProviders.has("ollama")) {
|
|
1667
1955
|
await this.discoverOllamaModels(ollamaCfg);
|
|
@@ -1673,7 +1961,7 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
|
|
|
1673
1961
|
if (!model) {
|
|
1674
1962
|
throw new Error(`Configured model "${override}" for ${tier} could not be loaded. Check provider availability and exact model name.`);
|
|
1675
1963
|
}
|
|
1676
|
-
if (model.id !== override) {
|
|
1964
|
+
if (model.id !== override && `${model.provider}:${model.id}` !== override) {
|
|
1677
1965
|
throw new Error(`Configured model "${override}" for ${tier} resolved to "${model.id}". Use the exact provider model ID or prefix the provider (e.g. gemini:${override}).`);
|
|
1678
1966
|
}
|
|
1679
1967
|
this.tierModels.set(tier, model);
|
|
@@ -1688,6 +1976,17 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
|
|
|
1688
1976
|
}
|
|
1689
1977
|
}
|
|
1690
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
|
+
}
|
|
1691
1990
|
async generate(tier, options, onChunk, requireVision = false) {
|
|
1692
1991
|
if (this.budgetState === "exceeded") {
|
|
1693
1992
|
throw new _CascadeRouter.BudgetExceededError(
|
|
@@ -1709,9 +2008,26 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
|
|
|
1709
2008
|
await this.tpmLimiter.acquire(model.provider, estimatedTokens);
|
|
1710
2009
|
}
|
|
1711
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
|
+
}
|
|
1712
2017
|
try {
|
|
1713
2018
|
let result;
|
|
1714
|
-
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) {
|
|
1715
2031
|
try {
|
|
1716
2032
|
result = await provider.generateStream(options, (chunk) => {
|
|
1717
2033
|
const text = typeof chunk?.text === "string" ? chunk.text : "";
|
|
@@ -1749,10 +2065,14 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
|
|
|
1749
2065
|
if (fallback) {
|
|
1750
2066
|
this.tierModels.set(tier, fallback);
|
|
1751
2067
|
this.ensureProvider(fallback, this.config.providers);
|
|
2068
|
+
releaseLocalSlot?.();
|
|
2069
|
+
releaseLocalSlot = void 0;
|
|
1752
2070
|
return this.generate(tier, options, onChunk, requireVision);
|
|
1753
2071
|
}
|
|
1754
2072
|
}
|
|
1755
2073
|
throw err;
|
|
2074
|
+
} finally {
|
|
2075
|
+
releaseLocalSlot?.();
|
|
1756
2076
|
}
|
|
1757
2077
|
}
|
|
1758
2078
|
getModelForTier(tier) {
|
|
@@ -1992,29 +2312,6 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
|
|
|
1992
2312
|
return /rate.?limit|429|too.?many.?requests|quota/i.test(msg);
|
|
1993
2313
|
}
|
|
1994
2314
|
};
|
|
1995
|
-
|
|
1996
|
-
// src/utils/retry.ts
|
|
1997
|
-
var CascadeCancelledError = class extends Error {
|
|
1998
|
-
constructor(reason) {
|
|
1999
|
-
super(reason ?? "Run was cancelled via AbortSignal");
|
|
2000
|
-
this.name = "CascadeCancelledError";
|
|
2001
|
-
}
|
|
2002
|
-
};
|
|
2003
|
-
var CascadeToolError = class extends Error {
|
|
2004
|
-
/** A friendly message to show the user / T3 */
|
|
2005
|
-
userMessage;
|
|
2006
|
-
/** Whether this error class is retryable by default */
|
|
2007
|
-
retryable;
|
|
2008
|
-
constructor(userMessage, cause, retryable = false) {
|
|
2009
|
-
const causeMsg = cause instanceof Error ? cause.message : String(cause);
|
|
2010
|
-
super(`${userMessage}: ${causeMsg}`);
|
|
2011
|
-
this.name = "CascadeToolError";
|
|
2012
|
-
this.userMessage = userMessage;
|
|
2013
|
-
this.retryable = retryable;
|
|
2014
|
-
}
|
|
2015
|
-
};
|
|
2016
|
-
|
|
2017
|
-
// src/core/tiers/base.ts
|
|
2018
2315
|
var BaseTier = class extends EventEmitter__default.default {
|
|
2019
2316
|
id;
|
|
2020
2317
|
role;
|
|
@@ -2295,6 +2592,97 @@ var AuditLogger = class {
|
|
|
2295
2592
|
}
|
|
2296
2593
|
};
|
|
2297
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
|
+
|
|
2298
2686
|
// src/core/tiers/t3-worker.ts
|
|
2299
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.
|
|
2300
2688
|
|
|
@@ -2496,6 +2884,9 @@ Now execute your subtask using this context where relevant.`
|
|
|
2496
2884
|
const MAX_ITERATIONS = 15;
|
|
2497
2885
|
const requiresArtifact = this.requiresArtifact();
|
|
2498
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) : "";
|
|
2499
2890
|
while (iterations < MAX_ITERATIONS) {
|
|
2500
2891
|
iterations++;
|
|
2501
2892
|
this.throwIfCancelled();
|
|
@@ -2503,8 +2894,9 @@ Now execute your subtask using this context where relevant.`
|
|
|
2503
2894
|
messages: this.context.getMessages(),
|
|
2504
2895
|
systemPrompt: this.systemPromptOverride + systemPrompt + (this.hierarchyContext ? `
|
|
2505
2896
|
|
|
2506
|
-
HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
2507
|
-
|
|
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,
|
|
2508
2900
|
maxTokens: 4096
|
|
2509
2901
|
};
|
|
2510
2902
|
const result = await this.router.generate(
|
|
@@ -2514,9 +2906,19 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
|
2514
2906
|
this.emit("stream:token", { tierId: this.id, text: chunk.text });
|
|
2515
2907
|
}
|
|
2516
2908
|
);
|
|
2517
|
-
|
|
2518
|
-
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) {
|
|
2519
2917
|
if (requiresArtifact) {
|
|
2918
|
+
const artifactCheck = await this.verifyArtifacts(this.assignment);
|
|
2919
|
+
if (artifactCheck.ok) {
|
|
2920
|
+
return { output: result.content, toolCalls: allToolCalls };
|
|
2921
|
+
}
|
|
2520
2922
|
stalledArtifactIterations += 1;
|
|
2521
2923
|
if (stalledArtifactIterations >= 2) {
|
|
2522
2924
|
if (stalledArtifactIterations === 2) {
|
|
@@ -2526,17 +2928,24 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
|
2526
2928
|
}
|
|
2527
2929
|
await this.context.addMessage({
|
|
2528
2930
|
role: "user",
|
|
2529
|
-
content:
|
|
2931
|
+
content: `You have not yet created and verified the required artifact. Issues: ${artifactCheck.issues.join("; ")}. Use tools to create the file in the workspace, verify it exists, and inspect the result before concluding.`
|
|
2530
2932
|
});
|
|
2531
2933
|
continue;
|
|
2532
2934
|
}
|
|
2533
2935
|
return { output: result.content, toolCalls: allToolCalls };
|
|
2534
2936
|
}
|
|
2535
2937
|
stalledArtifactIterations = 0;
|
|
2536
|
-
if (
|
|
2537
|
-
|
|
2938
|
+
if (effectiveResult.finishReason === "stop" && effectiveResult.toolCalls.length === 0) {
|
|
2939
|
+
if (requiresArtifact) {
|
|
2940
|
+
const artifactCheck = await this.verifyArtifacts(this.assignment);
|
|
2941
|
+
if (artifactCheck.ok) {
|
|
2942
|
+
return { output: result.content, toolCalls: allToolCalls };
|
|
2943
|
+
}
|
|
2944
|
+
} else {
|
|
2945
|
+
return { output: result.content, toolCalls: allToolCalls };
|
|
2946
|
+
}
|
|
2538
2947
|
}
|
|
2539
|
-
for (const tc of
|
|
2948
|
+
for (const tc of effectiveResult.toolCalls) {
|
|
2540
2949
|
allToolCalls.push(tc);
|
|
2541
2950
|
const toolResult = await this.executeTool(tc);
|
|
2542
2951
|
await this.context.addMessage({
|
|
@@ -2594,13 +3003,15 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
|
2594
3003
|
currentAction: `Using tool: ${tc.name}`,
|
|
2595
3004
|
status: "IN_PROGRESS"
|
|
2596
3005
|
});
|
|
3006
|
+
this.emit("tool:call", { id: tc.id, tierId: this.id, toolName: tc.name, input: tc.input });
|
|
3007
|
+
const toolStartMs = Date.now();
|
|
2597
3008
|
try {
|
|
2598
3009
|
const result = await this.toolRegistry.execute(tc.name, tc.input, {
|
|
2599
3010
|
tierId: this.id,
|
|
2600
3011
|
sessionId: this.taskId,
|
|
2601
3012
|
requireApproval: false,
|
|
2602
|
-
saveSnapshot: async (
|
|
2603
|
-
this.store?.addFileSnapshot(this.taskId,
|
|
3013
|
+
saveSnapshot: async (path17, content) => {
|
|
3014
|
+
this.store?.addFileSnapshot(this.taskId, path17, content);
|
|
2604
3015
|
},
|
|
2605
3016
|
sendPeerSync: (to, syncType, content) => {
|
|
2606
3017
|
this.peerBus?.send(this.id, to, syncType, this.assignment?.subtaskId ?? "", content);
|
|
@@ -2617,11 +3028,83 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
|
2617
3028
|
this.audit.fileChange(this.id, tc.input["path"] ?? "unknown", tc.name);
|
|
2618
3029
|
}
|
|
2619
3030
|
}
|
|
2620
|
-
|
|
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 });
|
|
2621
3033
|
return typeof result === "string" ? result : JSON.stringify(result);
|
|
2622
3034
|
} catch (err) {
|
|
2623
|
-
|
|
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}`;
|
|
3039
|
+
}
|
|
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
|
+
}
|
|
2624
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;
|
|
2625
3108
|
}
|
|
2626
3109
|
/**
|
|
2627
3110
|
* Announce which files this T3 plans to edit, then acquire locks on them
|
|
@@ -2681,12 +3164,12 @@ ${assignment.expectedOutput}`;
|
|
|
2681
3164
|
if (!artifactPaths.length) return { ok: true, issues: [] };
|
|
2682
3165
|
const issues = [];
|
|
2683
3166
|
const { exec: exec3 } = await import('child_process');
|
|
2684
|
-
const { promisify:
|
|
2685
|
-
const execAsync2 =
|
|
3167
|
+
const { promisify: promisify4 } = await import('util');
|
|
3168
|
+
const execAsync2 = promisify4(exec3);
|
|
2686
3169
|
for (const artifactPath of artifactPaths) {
|
|
2687
|
-
const absolutePath =
|
|
3170
|
+
const absolutePath = path16__default.default.resolve(process.cwd(), artifactPath);
|
|
2688
3171
|
try {
|
|
2689
|
-
const stat = await
|
|
3172
|
+
const stat = await fs3__default.default.stat(absolutePath);
|
|
2690
3173
|
if (!stat.isFile()) {
|
|
2691
3174
|
issues.push(`Expected artifact is not a file: ${artifactPath}`);
|
|
2692
3175
|
continue;
|
|
@@ -2696,7 +3179,7 @@ ${assignment.expectedOutput}`;
|
|
|
2696
3179
|
continue;
|
|
2697
3180
|
}
|
|
2698
3181
|
if (!/\.pdf$/i.test(artifactPath)) {
|
|
2699
|
-
const content = await
|
|
3182
|
+
const content = await fs3__default.default.readFile(absolutePath, "utf-8");
|
|
2700
3183
|
if (!content.trim()) {
|
|
2701
3184
|
issues.push(`Artifact content is empty: ${artifactPath}`);
|
|
2702
3185
|
continue;
|
|
@@ -2705,7 +3188,7 @@ ${assignment.expectedOutput}`;
|
|
|
2705
3188
|
issues.push(`PDF artifact looks too small to be valid: ${artifactPath}`);
|
|
2706
3189
|
continue;
|
|
2707
3190
|
}
|
|
2708
|
-
const ext =
|
|
3191
|
+
const ext = path16__default.default.extname(absolutePath).toLowerCase();
|
|
2709
3192
|
try {
|
|
2710
3193
|
if (ext === ".ts" || ext === ".tsx") {
|
|
2711
3194
|
await execAsync2(`npx tsc --noEmit ${absolutePath}`, { timeout: 1e4 });
|
|
@@ -2823,6 +3306,11 @@ var PeerBus = class extends EventEmitter__default.default {
|
|
|
2823
3306
|
barriers = /* @__PURE__ */ new Map();
|
|
2824
3307
|
broadcastLog = [];
|
|
2825
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 = "";
|
|
2826
3314
|
register(peerId) {
|
|
2827
3315
|
this.members.add(peerId);
|
|
2828
3316
|
}
|
|
@@ -2844,11 +3332,33 @@ var PeerBus = class extends EventEmitter__default.default {
|
|
|
2844
3332
|
this.waiters.delete(subtaskId);
|
|
2845
3333
|
}
|
|
2846
3334
|
/**
|
|
2847
|
-
*
|
|
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.
|
|
2848
3358
|
*/
|
|
2849
3359
|
waitFor(subtaskId, timeoutMs = 12e4) {
|
|
2850
3360
|
const existing = this.outputs.get(subtaskId);
|
|
2851
|
-
if (existing) return Promise.resolve(existing);
|
|
3361
|
+
if (existing && !this.retryPending.has(subtaskId)) return Promise.resolve(existing);
|
|
2852
3362
|
return new Promise((resolve, reject) => {
|
|
2853
3363
|
const resolver = (output) => {
|
|
2854
3364
|
clearTimeout(timer);
|
|
@@ -2879,6 +3389,7 @@ var PeerBus = class extends EventEmitter__default.default {
|
|
|
2879
3389
|
* Also logs to broadcastLog so collect() can retrieve recent broadcasts.
|
|
2880
3390
|
*/
|
|
2881
3391
|
broadcast(fromId, payload) {
|
|
3392
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
2882
3393
|
const msg = {
|
|
2883
3394
|
fromId,
|
|
2884
3395
|
toId: "*",
|
|
@@ -2886,10 +3397,18 @@ var PeerBus = class extends EventEmitter__default.default {
|
|
|
2886
3397
|
subtaskId: "",
|
|
2887
3398
|
syncType: "SHARE_OUTPUT",
|
|
2888
3399
|
payload,
|
|
2889
|
-
timestamp
|
|
3400
|
+
timestamp
|
|
2890
3401
|
};
|
|
2891
|
-
this.broadcastLog.push({ fromId, payload, timestamp
|
|
3402
|
+
this.broadcastLog.push({ fromId, payload, timestamp });
|
|
2892
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
|
+
});
|
|
2893
3412
|
}
|
|
2894
3413
|
/**
|
|
2895
3414
|
* Collect all broadcast messages received within a time window.
|
|
@@ -2975,6 +3494,16 @@ var PeerBus = class extends EventEmitter__default.default {
|
|
|
2975
3494
|
isFileLocked(filePath) {
|
|
2976
3495
|
return this.fileLocks.has(filePath);
|
|
2977
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
|
+
}
|
|
2978
3507
|
/**
|
|
2979
3508
|
* Clear broadcast log — call between phases to avoid stale announcements.
|
|
2980
3509
|
*/
|
|
@@ -2985,6 +3514,7 @@ var PeerBus = class extends EventEmitter__default.default {
|
|
|
2985
3514
|
* Send a targeted message to a specific peer
|
|
2986
3515
|
*/
|
|
2987
3516
|
send(fromId, toId, syncType, subtaskId, payload) {
|
|
3517
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
2988
3518
|
const msg = {
|
|
2989
3519
|
fromId,
|
|
2990
3520
|
toId,
|
|
@@ -2992,10 +3522,18 @@ var PeerBus = class extends EventEmitter__default.default {
|
|
|
2992
3522
|
subtaskId,
|
|
2993
3523
|
syncType,
|
|
2994
3524
|
payload,
|
|
2995
|
-
timestamp
|
|
3525
|
+
timestamp
|
|
2996
3526
|
};
|
|
2997
3527
|
this.emit(`message:${toId}`, msg);
|
|
2998
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
|
+
});
|
|
2999
3537
|
}
|
|
3000
3538
|
/**
|
|
3001
3539
|
* Barrier — wait until N peers have all reached this point
|
|
@@ -3048,6 +3586,8 @@ var T2Manager = class extends BaseTier {
|
|
|
3048
3586
|
t2PeerBus;
|
|
3049
3587
|
permissionEscalator;
|
|
3050
3588
|
toolCreator;
|
|
3589
|
+
/** AbortController for the current T3 wave — aborted on cancel-and-respawn */
|
|
3590
|
+
waveAbortController = null;
|
|
3051
3591
|
setPeerBus(bus) {
|
|
3052
3592
|
this.t2PeerBus = bus;
|
|
3053
3593
|
this.t2PeerBus.register(this.id);
|
|
@@ -3056,6 +3596,14 @@ var T2Manager = class extends BaseTier {
|
|
|
3056
3596
|
this.receivePeerSync(msg.fromId, msg.payload);
|
|
3057
3597
|
});
|
|
3058
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
|
+
}
|
|
3059
3607
|
constructor(router, toolRegistry, parentId) {
|
|
3060
3608
|
super("T2", void 0, parentId);
|
|
3061
3609
|
this.router = router;
|
|
@@ -3223,6 +3771,26 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
|
3223
3771
|
}];
|
|
3224
3772
|
}
|
|
3225
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
|
+
}
|
|
3226
3794
|
async executeSubtasks(subtasks, taskId) {
|
|
3227
3795
|
const assignments = subtasks.map((s) => ({
|
|
3228
3796
|
...s,
|
|
@@ -3249,6 +3817,8 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
|
3249
3817
|
worker.on("stream:token", (e) => this.emit("stream:token", e));
|
|
3250
3818
|
worker.on("log", (e) => this.emit("log", e));
|
|
3251
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));
|
|
3252
3822
|
worker.on("tool:approval-request", (e) => this.emit("tool:approval-request", {
|
|
3253
3823
|
...e,
|
|
3254
3824
|
__cascadeResponder: (decision) => worker.emit(`tool:approval-response:${e.id}`, decision)
|
|
@@ -3285,6 +3855,7 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
|
3285
3855
|
const sanitizedAssignments = this.breakCycles(assignments, adj, inDegree);
|
|
3286
3856
|
let remaining = new Set(sanitizedAssignments.map((a) => a.subtaskId));
|
|
3287
3857
|
let wave = 0;
|
|
3858
|
+
let respawnBudget = 1;
|
|
3288
3859
|
while (remaining.size > 0) {
|
|
3289
3860
|
const runnableIds = [...remaining].filter((id) => (inDegree.get(id) ?? 0) === 0);
|
|
3290
3861
|
if (runnableIds.length === 0) {
|
|
@@ -3305,15 +3876,62 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
|
3305
3876
|
status: "IN_PROGRESS"
|
|
3306
3877
|
});
|
|
3307
3878
|
this.throwIfCancelled();
|
|
3879
|
+
this.waveAbortController = new AbortController();
|
|
3880
|
+
const waveSignal = AbortSignal.any(
|
|
3881
|
+
[this.signal, this.waveAbortController.signal].filter(Boolean)
|
|
3882
|
+
);
|
|
3308
3883
|
const waveResults = await Promise.allSettled(
|
|
3309
3884
|
runnableIds.map(async (id) => {
|
|
3310
3885
|
const assignment = sanitizedAssignments.find((a) => a.subtaskId === id);
|
|
3311
3886
|
const worker = workerMap.get(id);
|
|
3312
|
-
const result = await worker.execute(assignment, taskId,
|
|
3887
|
+
const result = await worker.execute(assignment, taskId, waveSignal);
|
|
3313
3888
|
resultMap.set(id, result);
|
|
3314
3889
|
return result;
|
|
3315
3890
|
})
|
|
3316
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
|
+
}
|
|
3317
3935
|
for (let i = 0; i < runnableIds.length; i++) {
|
|
3318
3936
|
const id = runnableIds[i];
|
|
3319
3937
|
remaining.delete(id);
|
|
@@ -3321,61 +3939,22 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
|
3321
3939
|
if (r.status === "rejected") {
|
|
3322
3940
|
this.log(`T3 worker ${id} failed: ${r.reason instanceof Error ? r.reason.message : String(r.reason)} \u2014 retrying once`);
|
|
3323
3941
|
const assignment = sanitizedAssignments.find((a) => a.subtaskId === id);
|
|
3324
|
-
|
|
3325
|
-
|
|
3326
|
-
|
|
3327
|
-
|
|
3328
|
-
|
|
3329
|
-
this.log(`T3
|
|
3330
|
-
this.
|
|
3331
|
-
|
|
3332
|
-
|
|
3333
|
-
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
|
|
3334
3957
|
});
|
|
3335
|
-
const toolName = await this.toolCreator.createTool(
|
|
3336
|
-
`Help complete: ${assignment.subtaskTitle}`,
|
|
3337
|
-
assignment.description
|
|
3338
|
-
);
|
|
3339
|
-
if (toolName) {
|
|
3340
|
-
this.log(`T2 verifying new tool: ${toolName}`);
|
|
3341
|
-
this.sendStatusUpdate({
|
|
3342
|
-
progressPct: 60,
|
|
3343
|
-
currentAction: `T2 Verifying new tool: ${toolName}`,
|
|
3344
|
-
status: "IN_PROGRESS"
|
|
3345
|
-
});
|
|
3346
|
-
try {
|
|
3347
|
-
const verifyResult = await this.router.generate("T2", {
|
|
3348
|
-
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".` }],
|
|
3349
|
-
systemPrompt: this.systemPromptOverride + "You are T2 Manager verifying a dynamic tool.",
|
|
3350
|
-
maxTokens: 50
|
|
3351
|
-
});
|
|
3352
|
-
if (!verifyResult.content.toUpperCase().includes("REJECTED")) {
|
|
3353
|
-
this.log(`T2 verification passed for ${toolName}. Restarting original T3.`);
|
|
3354
|
-
const retried = await this.retryT3({
|
|
3355
|
-
...assignment,
|
|
3356
|
-
description: `${assignment.description}
|
|
3357
|
-
|
|
3358
|
-
[SYSTEM NOTIFICATION]: A new dynamic tool "${toolName}" has been built and verified for you. Use it to complete your task.`
|
|
3359
|
-
}, taskId);
|
|
3360
|
-
resultMap.set(id, retried);
|
|
3361
|
-
} else {
|
|
3362
|
-
this.log(`T2 rejected the dynamic tool: ${toolName}`);
|
|
3363
|
-
resultMap.set(id, r.value);
|
|
3364
|
-
}
|
|
3365
|
-
} catch {
|
|
3366
|
-
const retried = await this.retryT3({
|
|
3367
|
-
...assignment,
|
|
3368
|
-
description: `${assignment.description}
|
|
3369
|
-
|
|
3370
|
-
[SYSTEM NOTIFICATION]: A new dynamic tool "${toolName}" has been built for you. Use it to complete your task.`
|
|
3371
|
-
}, taskId);
|
|
3372
|
-
resultMap.set(id, retried);
|
|
3373
|
-
}
|
|
3374
|
-
} else {
|
|
3375
|
-
resultMap.set(id, r.value);
|
|
3376
|
-
}
|
|
3377
|
-
} else {
|
|
3378
|
-
resultMap.set(id, r.value);
|
|
3379
3958
|
}
|
|
3380
3959
|
}
|
|
3381
3960
|
for (const dependent of adj.get(id) ?? []) {
|
|
@@ -3646,6 +4225,8 @@ var T1Administrator = class extends BaseTier {
|
|
|
3646
4225
|
toolCreator;
|
|
3647
4226
|
/** Stored overall task goal — used when evaluating escalated permissions */
|
|
3648
4227
|
taskGoal = "";
|
|
4228
|
+
peerMessageCallback;
|
|
4229
|
+
peerMessageSessionId = "";
|
|
3649
4230
|
constructor(router, toolRegistry, config) {
|
|
3650
4231
|
super("T1", "T1");
|
|
3651
4232
|
this.router = router;
|
|
@@ -3666,6 +4247,12 @@ var T1Administrator = class extends BaseTier {
|
|
|
3666
4247
|
setToolCreator(creator) {
|
|
3667
4248
|
this.toolCreator = creator;
|
|
3668
4249
|
}
|
|
4250
|
+
setPeerMessageCallback(cb, sessionId) {
|
|
4251
|
+
this.peerMessageCallback = cb;
|
|
4252
|
+
this.peerMessageSessionId = sessionId;
|
|
4253
|
+
this.t2PeerBus.onPeerMessage = cb;
|
|
4254
|
+
this.t2PeerBus.sessionId = sessionId;
|
|
4255
|
+
}
|
|
3669
4256
|
async execute(userPrompt, images, systemContext, signal) {
|
|
3670
4257
|
this.signal = signal;
|
|
3671
4258
|
this.taskId = crypto.randomUUID();
|
|
@@ -3885,6 +4472,9 @@ Leave dependsOn empty for sections that can run immediately in parallel.`;
|
|
|
3885
4472
|
manager.setStore(this.store);
|
|
3886
4473
|
}
|
|
3887
4474
|
manager.setPeerBus(this.t2PeerBus);
|
|
4475
|
+
if (this.peerMessageCallback) {
|
|
4476
|
+
manager.setPeerMessageCallback(this.peerMessageCallback, this.peerMessageSessionId);
|
|
4477
|
+
}
|
|
3888
4478
|
if (this.permissionEscalator) {
|
|
3889
4479
|
manager.setPermissionEscalator(this.permissionEscalator);
|
|
3890
4480
|
}
|
|
@@ -3895,6 +4485,8 @@ Leave dependsOn empty for sections that can run immediately in parallel.`;
|
|
|
3895
4485
|
bind(manager, "stream:token", (e) => this.emit("stream:token", e));
|
|
3896
4486
|
bind(manager, "log", (e) => this.emit("log", e));
|
|
3897
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));
|
|
3898
4490
|
bind(manager, "tool:approval-request", (e) => this.emit("tool:approval-request", e));
|
|
3899
4491
|
bind(manager, "message", (msg) => {
|
|
3900
4492
|
if (msg.type === "PEER_SYNC") {
|
|
@@ -4254,13 +4846,21 @@ function resolveInWorkspace(workspaceRoot, input) {
|
|
|
4254
4846
|
if (typeof input !== "string" || input.length === 0) {
|
|
4255
4847
|
throw new WorkspaceSandboxError(String(input), workspaceRoot);
|
|
4256
4848
|
}
|
|
4257
|
-
const root =
|
|
4258
|
-
const abs =
|
|
4259
|
-
const rel =
|
|
4260
|
-
if (rel === "" || rel === ".")
|
|
4261
|
-
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)) {
|
|
4262
4853
|
throw new WorkspaceSandboxError(input, root);
|
|
4263
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
|
+
}
|
|
4264
4864
|
return abs;
|
|
4265
4865
|
}
|
|
4266
4866
|
|
|
@@ -4282,7 +4882,7 @@ var FileReadTool = class extends BaseTool {
|
|
|
4282
4882
|
const absPath = resolveInWorkspace(this.workspaceRoot, filePath);
|
|
4283
4883
|
const offset = input["offset"] ?? 1;
|
|
4284
4884
|
const limit = input["limit"];
|
|
4285
|
-
const content = await
|
|
4885
|
+
const content = await fs3__default.default.readFile(absPath, "utf-8");
|
|
4286
4886
|
const lines = content.split("\n");
|
|
4287
4887
|
const start = Math.max(0, offset - 1);
|
|
4288
4888
|
const end = limit ? start + limit : lines.length;
|
|
@@ -4311,13 +4911,13 @@ var FileWriteTool = class extends BaseTool {
|
|
|
4311
4911
|
const content = input["content"];
|
|
4312
4912
|
if (options.saveSnapshot) {
|
|
4313
4913
|
try {
|
|
4314
|
-
const oldContent = await
|
|
4914
|
+
const oldContent = await fs3__default.default.readFile(absPath, "utf-8");
|
|
4315
4915
|
await options.saveSnapshot(absPath, oldContent);
|
|
4316
4916
|
} catch {
|
|
4317
4917
|
}
|
|
4318
4918
|
}
|
|
4319
|
-
await
|
|
4320
|
-
await
|
|
4919
|
+
await fs3__default.default.mkdir(path16__default.default.dirname(absPath), { recursive: true });
|
|
4920
|
+
await fs3__default.default.writeFile(absPath, content, "utf-8");
|
|
4321
4921
|
return `Written ${content.length} characters to ${filePath}`;
|
|
4322
4922
|
}
|
|
4323
4923
|
};
|
|
@@ -4343,7 +4943,7 @@ var FileEditTool = class extends BaseTool {
|
|
|
4343
4943
|
const oldString = input["old_string"];
|
|
4344
4944
|
const newString = input["new_string"];
|
|
4345
4945
|
const replaceAll = input["replace_all"] ?? false;
|
|
4346
|
-
const rawContent = await
|
|
4946
|
+
const rawContent = await fs3__default.default.readFile(absPath, "utf-8");
|
|
4347
4947
|
if (options.saveSnapshot) {
|
|
4348
4948
|
await options.saveSnapshot(absPath, rawContent);
|
|
4349
4949
|
}
|
|
@@ -4355,7 +4955,7 @@ var FileEditTool = class extends BaseTool {
|
|
|
4355
4955
|
);
|
|
4356
4956
|
}
|
|
4357
4957
|
const updated = replaceAll ? content.split(normalizedOld).join(newString) : content.replace(normalizedOld, newString);
|
|
4358
|
-
await
|
|
4958
|
+
await fs3__default.default.writeFile(absPath, updated, "utf-8");
|
|
4359
4959
|
const count = replaceAll ? content.split(normalizedOld).length - 1 : 1;
|
|
4360
4960
|
return `Replaced ${count} occurrence(s) in ${filePath}`;
|
|
4361
4961
|
}
|
|
@@ -4378,12 +4978,12 @@ var FileDeleteTool = class extends BaseTool {
|
|
|
4378
4978
|
const absPath = resolveInWorkspace(this.workspaceRoot, filePath);
|
|
4379
4979
|
if (options.saveSnapshot) {
|
|
4380
4980
|
try {
|
|
4381
|
-
const oldContent = await
|
|
4981
|
+
const oldContent = await fs3__default.default.readFile(absPath, "utf-8");
|
|
4382
4982
|
await options.saveSnapshot(absPath, oldContent);
|
|
4383
4983
|
} catch {
|
|
4384
4984
|
}
|
|
4385
4985
|
}
|
|
4386
|
-
await
|
|
4986
|
+
await fs3__default.default.rm(absPath, { recursive: false });
|
|
4387
4987
|
return `Deleted ${filePath}`;
|
|
4388
4988
|
}
|
|
4389
4989
|
};
|
|
@@ -4400,7 +5000,7 @@ var FileListTool = class extends BaseTool {
|
|
|
4400
5000
|
async execute(input, _options) {
|
|
4401
5001
|
const inputPath = input["path"] || ".";
|
|
4402
5002
|
const absPath = resolveInWorkspace(this.workspaceRoot, inputPath);
|
|
4403
|
-
const entries = await
|
|
5003
|
+
const entries = await fs3__default.default.readdir(absPath, { withFileTypes: true });
|
|
4404
5004
|
return entries.map((e) => `${e.isDirectory() ? "[DIR] " : " "}${e.name}`).join("\n") || "(empty directory)";
|
|
4405
5005
|
}
|
|
4406
5006
|
};
|
|
@@ -4783,8 +5383,8 @@ var ImageAnalyzeTool = class extends BaseTool {
|
|
|
4783
5383
|
}
|
|
4784
5384
|
};
|
|
4785
5385
|
async function fileToImageAttachment(filePath) {
|
|
4786
|
-
const data = await
|
|
4787
|
-
const ext =
|
|
5386
|
+
const data = await fs3__default.default.readFile(filePath);
|
|
5387
|
+
const ext = path16__default.default.extname(filePath).toLowerCase();
|
|
4788
5388
|
const mimeMap = {
|
|
4789
5389
|
".jpg": "image/jpeg",
|
|
4790
5390
|
".jpeg": "image/jpeg",
|
|
@@ -4818,14 +5418,14 @@ var PDFCreateTool = class extends BaseTool {
|
|
|
4818
5418
|
const filePath = input["path"];
|
|
4819
5419
|
const content = input["content"];
|
|
4820
5420
|
const title = input["title"];
|
|
4821
|
-
const dir =
|
|
4822
|
-
if (!
|
|
4823
|
-
|
|
5421
|
+
const dir = path16__default.default.dirname(filePath);
|
|
5422
|
+
if (!fs15__default.default.existsSync(dir)) {
|
|
5423
|
+
fs15__default.default.mkdirSync(dir, { recursive: true });
|
|
4824
5424
|
}
|
|
4825
5425
|
return new Promise((resolve, reject) => {
|
|
4826
5426
|
try {
|
|
4827
5427
|
const doc = new PDFDocument__default.default({ margin: 50 });
|
|
4828
|
-
const stream =
|
|
5428
|
+
const stream = fs15__default.default.createWriteStream(filePath);
|
|
4829
5429
|
doc.pipe(stream);
|
|
4830
5430
|
if (title) {
|
|
4831
5431
|
doc.info["Title"] = title;
|
|
@@ -4903,14 +5503,14 @@ var CodeInterpreterTool = class extends BaseTool {
|
|
|
4903
5503
|
}
|
|
4904
5504
|
cmdPrefix = NODE_CMD;
|
|
4905
5505
|
}
|
|
4906
|
-
const tmpDir =
|
|
4907
|
-
if (!
|
|
4908
|
-
|
|
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 });
|
|
4909
5509
|
}
|
|
4910
5510
|
const extension = language === "python" ? "py" : "js";
|
|
4911
5511
|
const fileName = `intp_${crypto.randomUUID().slice(0, 8)}.${extension}`;
|
|
4912
|
-
const filePath =
|
|
4913
|
-
|
|
5512
|
+
const filePath = path16__default.default.join(tmpDir, fileName);
|
|
5513
|
+
fs15__default.default.writeFileSync(filePath, code, "utf-8");
|
|
4914
5514
|
const quotedPath = `"${filePath}"`;
|
|
4915
5515
|
const quotedArgs = args.map((a) => `"${a}"`).join(" ");
|
|
4916
5516
|
const fullCmd = `${cmdPrefix} ${quotedPath}${quotedArgs ? " " + quotedArgs : ""}`;
|
|
@@ -4919,8 +5519,8 @@ var CodeInterpreterTool = class extends BaseTool {
|
|
|
4919
5519
|
child_process.exec(fullCmd, { cwd: process.cwd(), timeout: 3e4 }, (error, stdout, stderr) => {
|
|
4920
5520
|
const duration = Date.now() - startMs;
|
|
4921
5521
|
try {
|
|
4922
|
-
if (
|
|
4923
|
-
|
|
5522
|
+
if (fs15__default.default.existsSync(filePath)) {
|
|
5523
|
+
fs15__default.default.unlinkSync(filePath);
|
|
4924
5524
|
}
|
|
4925
5525
|
} catch (cleanupErr) {
|
|
4926
5526
|
console.error(`Failed to cleanup interpreter script ${filePath}:`, cleanupErr);
|
|
@@ -5180,6 +5780,253 @@ var WebSearchTool = class extends BaseTool {
|
|
|
5180
5780
|
return lines.join("\n");
|
|
5181
5781
|
}
|
|
5182
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
|
+
};
|
|
5183
6030
|
|
|
5184
6031
|
// src/tools/mcp.ts
|
|
5185
6032
|
var McpToolWrapper = class extends BaseTool {
|
|
@@ -5205,7 +6052,7 @@ var McpToolWrapper = class extends BaseTool {
|
|
|
5205
6052
|
|
|
5206
6053
|
// src/tools/registry.ts
|
|
5207
6054
|
var ignore = ignoreFactory__namespace.default.default ?? ignoreFactory__namespace.default;
|
|
5208
|
-
var ToolRegistry = class {
|
|
6055
|
+
var ToolRegistry = class extends EventEmitter__default.default {
|
|
5209
6056
|
tools = /* @__PURE__ */ new Map();
|
|
5210
6057
|
config;
|
|
5211
6058
|
ignoreMatcher = ignore();
|
|
@@ -5213,12 +6060,36 @@ var ToolRegistry = class {
|
|
|
5213
6060
|
/** Loaded plugins, keyed by plugin name */
|
|
5214
6061
|
plugins = /* @__PURE__ */ new Map();
|
|
5215
6062
|
constructor(config, workspaceRoot = process.cwd()) {
|
|
6063
|
+
super();
|
|
5216
6064
|
this.config = config;
|
|
5217
6065
|
this.workspaceRoot = workspaceRoot;
|
|
5218
6066
|
this.registerDefaults();
|
|
5219
6067
|
}
|
|
5220
6068
|
register(tool) {
|
|
5221
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
|
+
});
|
|
5222
6093
|
}
|
|
5223
6094
|
/**
|
|
5224
6095
|
* Register a ToolPlugin, loading all its tools into the registry.
|
|
@@ -5303,7 +6174,10 @@ var ToolRegistry = class {
|
|
|
5303
6174
|
new PDFCreateTool(),
|
|
5304
6175
|
new CodeInterpreterTool(),
|
|
5305
6176
|
new PeerCommunicationTool(),
|
|
5306
|
-
new WebSearchTool(this.config.webSearch)
|
|
6177
|
+
new WebSearchTool(this.config.webSearch),
|
|
6178
|
+
new GlobTool(),
|
|
6179
|
+
new GrepTool(),
|
|
6180
|
+
new WebFetchTool()
|
|
5307
6181
|
];
|
|
5308
6182
|
for (const tool of tools) {
|
|
5309
6183
|
tool.setWorkspaceRoot(this.workspaceRoot);
|
|
@@ -5320,10 +6194,10 @@ var ToolRegistry = class {
|
|
|
5320
6194
|
}
|
|
5321
6195
|
isIgnored(filePath) {
|
|
5322
6196
|
if (!filePath) return false;
|
|
5323
|
-
const abs =
|
|
5324
|
-
const rel =
|
|
5325
|
-
if (!rel || rel.startsWith("..") ||
|
|
5326
|
-
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("/");
|
|
5327
6201
|
return this.ignoreMatcher.ignores(posixRel);
|
|
5328
6202
|
}
|
|
5329
6203
|
};
|
|
@@ -5660,7 +6534,24 @@ var CascadeConfigSchema = zod.z.object({
|
|
|
5660
6534
|
* Generated tools are session-scoped and sandboxed in node:vm.
|
|
5661
6535
|
* HTTP calls from generated tools require approval.
|
|
5662
6536
|
*/
|
|
5663
|
-
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)
|
|
5664
6555
|
});
|
|
5665
6556
|
|
|
5666
6557
|
// src/config/validate.ts
|
|
@@ -5788,139 +6679,237 @@ function heuristicAnalyze(prompt) {
|
|
|
5788
6679
|
const estimatedTokens = wordCount * 5;
|
|
5789
6680
|
return { type, complexity, requiresReasoning, requiresVision, estimatedTokens, confidence };
|
|
5790
6681
|
}
|
|
5791
|
-
function selectModelFromProfile(profile, tier, selector) {
|
|
5792
|
-
if (profile.requiresVision) {
|
|
5793
|
-
return selector.selectVisionModel();
|
|
5794
|
-
}
|
|
5795
|
-
if (tier === "T1") {
|
|
5796
|
-
if (profile.complexity >= 4) {
|
|
5797
|
-
return selector.selectForTier("T1");
|
|
5798
|
-
} else {
|
|
5799
|
-
return selector.selectForTier("T2");
|
|
5800
|
-
}
|
|
5801
|
-
}
|
|
5802
|
-
if (tier === "T2") {
|
|
5803
|
-
if (profile.type === "code" || profile.type === "data") {
|
|
5804
|
-
return selector.selectForTier("T2");
|
|
5805
|
-
} else if (profile.complexity <= 2) {
|
|
5806
|
-
return selector.selectForTier("T3");
|
|
5807
|
-
}
|
|
5808
|
-
return selector.selectForTier("T2");
|
|
5809
|
-
}
|
|
5810
|
-
if (tier === "T3") {
|
|
5811
|
-
if (profile.complexity >= 4 || profile.requiresReasoning) {
|
|
5812
|
-
return selector.selectForTier("T2");
|
|
5813
|
-
} else if (profile.type === "creative") {
|
|
5814
|
-
return selector.selectForTier("T2");
|
|
5815
|
-
} else {
|
|
5816
|
-
return selector.selectForTier("T3");
|
|
5817
|
-
}
|
|
5818
|
-
}
|
|
5819
|
-
return selector.selectForTier(tier);
|
|
5820
|
-
}
|
|
5821
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
|
+
};
|
|
5822
6690
|
var TaskAnalyzer = class {
|
|
5823
|
-
|
|
5824
|
-
|
|
5825
|
-
|
|
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;
|
|
5826
6703
|
}
|
|
5827
6704
|
/**
|
|
5828
|
-
* Analyze a prompt and return a TaskProfile.
|
|
5829
|
-
*
|
|
6705
|
+
* Analyze a prompt and return a TaskProfile using pure heuristics.
|
|
6706
|
+
* Low confidence prompts fall back to a conservative mixed/moderate profile.
|
|
5830
6707
|
*/
|
|
5831
6708
|
async analyze(prompt) {
|
|
5832
6709
|
const cacheKey = prompt.slice(0, 200);
|
|
5833
6710
|
const cached = analysisCache.get(cacheKey);
|
|
5834
|
-
if (cached)
|
|
5835
|
-
|
|
5836
|
-
|
|
5837
|
-
try {
|
|
5838
|
-
const aiProfile = await this.aiInference(prompt);
|
|
5839
|
-
const merged = {
|
|
5840
|
-
type: aiProfile.type,
|
|
5841
|
-
complexity: aiProfile.complexity,
|
|
5842
|
-
requiresReasoning: aiProfile.requiresReasoning,
|
|
5843
|
-
requiresVision: heuristic.requiresVision || aiProfile.requiresVision,
|
|
5844
|
-
estimatedTokens: heuristic.estimatedTokens,
|
|
5845
|
-
confidence: 0.9
|
|
5846
|
-
// AI-backed
|
|
5847
|
-
};
|
|
5848
|
-
analysisCache.set(cacheKey, merged);
|
|
5849
|
-
return merged;
|
|
5850
|
-
} catch {
|
|
5851
|
-
}
|
|
6711
|
+
if (cached) {
|
|
6712
|
+
this.lastProfile = cached;
|
|
6713
|
+
return cached;
|
|
5852
6714
|
}
|
|
5853
|
-
|
|
5854
|
-
|
|
6715
|
+
const profile = heuristicAnalyze(prompt);
|
|
6716
|
+
analysisCache.set(cacheKey, profile);
|
|
6717
|
+
this.lastProfile = profile;
|
|
6718
|
+
return profile;
|
|
5855
6719
|
}
|
|
5856
6720
|
/**
|
|
5857
|
-
* 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.
|
|
5858
6724
|
*/
|
|
5859
6725
|
async selectModel(prompt, tier, selector) {
|
|
5860
6726
|
const profile = await this.analyze(prompt);
|
|
5861
|
-
|
|
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;
|
|
5862
6740
|
}
|
|
5863
|
-
|
|
5864
|
-
|
|
5865
|
-
|
|
5866
|
-
|
|
5867
|
-
|
|
5868
|
-
|
|
5869
|
-
|
|
5870
|
-
|
|
5871
|
-
|
|
5872
|
-
|
|
5873
|
-
|
|
5874
|
-
|
|
5875
|
-
|
|
5876
|
-
|
|
5877
|
-
|
|
5878
|
-
const
|
|
5879
|
-
const
|
|
5880
|
-
const
|
|
5881
|
-
|
|
5882
|
-
|
|
5883
|
-
|
|
5884
|
-
|
|
5885
|
-
|
|
5886
|
-
|
|
5887
|
-
|
|
5888
|
-
|
|
5889
|
-
|
|
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;
|
|
5890
6773
|
}
|
|
5891
6774
|
/** Clear the analysis cache (call between sessions). */
|
|
5892
6775
|
static clearCache() {
|
|
5893
6776
|
analysisCache.clear();
|
|
5894
6777
|
}
|
|
5895
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
|
+
};
|
|
5896
6852
|
var DynamicTool = class extends BaseTool {
|
|
5897
6853
|
name;
|
|
5898
6854
|
description;
|
|
5899
6855
|
inputSchema;
|
|
5900
6856
|
executeCode;
|
|
5901
6857
|
_isDangerous;
|
|
5902
|
-
|
|
6858
|
+
registry;
|
|
6859
|
+
escalator;
|
|
6860
|
+
constructor(spec, registry, escalator) {
|
|
5903
6861
|
super();
|
|
5904
6862
|
this.name = spec.name;
|
|
5905
6863
|
this.description = spec.description;
|
|
5906
6864
|
this.inputSchema = spec.inputSchema;
|
|
5907
6865
|
this.executeCode = spec.executeCode;
|
|
5908
6866
|
this._isDangerous = spec.isDangerous;
|
|
6867
|
+
this.registry = registry;
|
|
6868
|
+
this.escalator = escalator;
|
|
5909
6869
|
}
|
|
5910
6870
|
isDangerous() {
|
|
5911
6871
|
return this._isDangerous;
|
|
5912
6872
|
}
|
|
5913
|
-
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
|
+
};
|
|
5914
6903
|
const sandbox = {
|
|
5915
6904
|
input,
|
|
5916
6905
|
fetch: globalThis.fetch,
|
|
6906
|
+
callTool,
|
|
5917
6907
|
JSON,
|
|
5918
6908
|
Math,
|
|
5919
6909
|
Date,
|
|
5920
6910
|
console: { log: () => {
|
|
5921
6911
|
}, error: () => {
|
|
5922
6912
|
} },
|
|
5923
|
-
// Silenced
|
|
5924
6913
|
setTimeout,
|
|
5925
6914
|
clearTimeout,
|
|
5926
6915
|
Promise,
|
|
@@ -5953,29 +6942,42 @@ Generate a minimal, safe JavaScript tool function for the described operation.
|
|
|
5953
6942
|
|
|
5954
6943
|
Rules:
|
|
5955
6944
|
- Return ONLY a JSON object with these fields: name, description, inputSchema, executeCode, isDangerous
|
|
5956
|
-
- executeCode is a self-contained JavaScript function body that:
|
|
5957
|
-
- 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)
|
|
5958
6947
|
- Returns: a string result
|
|
5959
|
-
-
|
|
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)
|
|
5960
6951
|
- Must complete in under 15 seconds
|
|
5961
|
-
- isDangerous
|
|
6952
|
+
- isDangerous: true if the tool calls dangerous cascade tools (shell, file_write, file_delete, git) or makes HTTP calls that write data
|
|
5962
6953
|
- name must be snake_case, start with "dynamic_", max 40 chars
|
|
5963
6954
|
- description must be \u2264 120 chars
|
|
5964
6955
|
|
|
5965
|
-
Example
|
|
5966
|
-
|
|
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
|
+
}
|
|
5967
6964
|
|
|
5968
6965
|
Return ONLY valid JSON \u2014 no other text.`;
|
|
5969
6966
|
var ToolCreator = class {
|
|
5970
6967
|
router;
|
|
5971
6968
|
registry;
|
|
6969
|
+
escalator;
|
|
5972
6970
|
createdTools = /* @__PURE__ */ new Set();
|
|
5973
6971
|
constructor(router, registry) {
|
|
5974
6972
|
this.router = router;
|
|
5975
6973
|
this.registry = registry;
|
|
5976
6974
|
}
|
|
6975
|
+
setPermissionEscalator(escalator) {
|
|
6976
|
+
this.escalator = escalator;
|
|
6977
|
+
}
|
|
5977
6978
|
/**
|
|
5978
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().
|
|
5979
6981
|
* Returns the tool name if successful, null if generation failed.
|
|
5980
6982
|
*/
|
|
5981
6983
|
async createTool(description, context) {
|
|
@@ -5986,26 +6988,21 @@ Required capability: ${description.slice(0, 300)}`;
|
|
|
5986
6988
|
try {
|
|
5987
6989
|
const result = await this.router.generate("T3", {
|
|
5988
6990
|
messages: [{ role: "user", content: prompt }],
|
|
5989
|
-
maxTokens:
|
|
6991
|
+
maxTokens: 800
|
|
5990
6992
|
});
|
|
5991
6993
|
const jsonMatch = /\{[\s\S]*\}/.exec(result.content);
|
|
5992
|
-
if (!jsonMatch)
|
|
5993
|
-
return null;
|
|
5994
|
-
}
|
|
6994
|
+
if (!jsonMatch) return null;
|
|
5995
6995
|
const spec = JSON.parse(jsonMatch[0]);
|
|
5996
|
-
if (!spec.name || !spec.description || !spec.executeCode || !spec.inputSchema)
|
|
5997
|
-
return null;
|
|
5998
|
-
}
|
|
6996
|
+
if (!spec.name || !spec.description || !spec.executeCode || !spec.inputSchema) return null;
|
|
5999
6997
|
if (this.createdTools.has(spec.name) || this.registry.hasTool(spec.name)) {
|
|
6000
6998
|
spec.name = `${spec.name}_${Date.now() % 1e4}`;
|
|
6001
6999
|
}
|
|
6002
7000
|
try {
|
|
6003
|
-
|
|
6004
|
-
|
|
6005
|
-
} catch (err) {
|
|
7001
|
+
new Function("input", "fetch", "callTool", spec.executeCode);
|
|
7002
|
+
} catch {
|
|
6006
7003
|
return null;
|
|
6007
7004
|
}
|
|
6008
|
-
const tool = new DynamicTool(spec);
|
|
7005
|
+
const tool = new DynamicTool(spec, this.registry, this.escalator);
|
|
6009
7006
|
this.registry.register(tool);
|
|
6010
7007
|
this.createdTools.add(spec.name);
|
|
6011
7008
|
return spec.name;
|
|
@@ -6013,16 +7010,14 @@ Required capability: ${description.slice(0, 300)}`;
|
|
|
6013
7010
|
return null;
|
|
6014
7011
|
}
|
|
6015
7012
|
}
|
|
6016
|
-
/**
|
|
6017
|
-
* Returns the names of all tools created in this session.
|
|
6018
|
-
*/
|
|
7013
|
+
/** Returns the names of all tools created in this session. */
|
|
6019
7014
|
getCreatedTools() {
|
|
6020
7015
|
return Array.from(this.createdTools);
|
|
6021
7016
|
}
|
|
6022
7017
|
};
|
|
6023
7018
|
|
|
6024
7019
|
// src/core/cascade.ts
|
|
6025
|
-
var Cascade = class extends EventEmitter__default.default {
|
|
7020
|
+
var Cascade = class _Cascade extends EventEmitter__default.default {
|
|
6026
7021
|
router;
|
|
6027
7022
|
toolRegistry;
|
|
6028
7023
|
mcpClient;
|
|
@@ -6033,6 +7028,7 @@ var Cascade = class extends EventEmitter__default.default {
|
|
|
6033
7028
|
audit;
|
|
6034
7029
|
telemetry;
|
|
6035
7030
|
taskAnalyzer;
|
|
7031
|
+
perfTracker;
|
|
6036
7032
|
toolCreator;
|
|
6037
7033
|
constructor(config, workspacePath, store) {
|
|
6038
7034
|
super();
|
|
@@ -6049,10 +7045,12 @@ var Cascade = class extends EventEmitter__default.default {
|
|
|
6049
7045
|
this.telemetry = config.telemetry?.enabled ? new Telemetry(config.telemetry, config.telemetry.distinctId ?? "anonymous") : noopTelemetry;
|
|
6050
7046
|
}
|
|
6051
7047
|
initOptionalFeatures() {
|
|
6052
|
-
|
|
6053
|
-
|
|
6054
|
-
this.
|
|
7048
|
+
if (this.config.cascadeAuto === true) {
|
|
7049
|
+
this.perfTracker = new ModelPerformanceTracker();
|
|
7050
|
+
void this.perfTracker.load();
|
|
7051
|
+
this.taskAnalyzer = new TaskAnalyzer(this.perfTracker);
|
|
6055
7052
|
}
|
|
7053
|
+
const cfg = this.config;
|
|
6056
7054
|
if (cfg["enableToolCreation"] === true) {
|
|
6057
7055
|
this.toolCreator = new ToolCreator(this.router, this.toolRegistry);
|
|
6058
7056
|
}
|
|
@@ -6118,6 +7116,26 @@ var Cascade = class extends EventEmitter__default.default {
|
|
|
6118
7116
|
}
|
|
6119
7117
|
}
|
|
6120
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
|
+
}
|
|
6121
7139
|
this.initOptionalFeatures();
|
|
6122
7140
|
this.initialized = true;
|
|
6123
7141
|
})();
|
|
@@ -6128,21 +7146,48 @@ var Cascade = class extends EventEmitter__default.default {
|
|
|
6128
7146
|
throw err;
|
|
6129
7147
|
}
|
|
6130
7148
|
}
|
|
7149
|
+
isCasualGreeting(prompt) {
|
|
7150
|
+
const casual = /^(hi|hello|hey|greetings|thanks|thank you|thx|bye|goodbye|cya)$/i.test(prompt.trim().replace(/[!?.]+$/, ""));
|
|
7151
|
+
return casual;
|
|
7152
|
+
}
|
|
6131
7153
|
looksLikeSimpleArtifactTask(prompt) {
|
|
6132
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);
|
|
6133
7155
|
}
|
|
6134
|
-
|
|
6135
|
-
|
|
6136
|
-
|
|
6137
|
-
|
|
6138
|
-
|
|
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;
|
|
6139
7171
|
try {
|
|
6140
7172
|
const files = await glob.glob("**/*.*", {
|
|
6141
7173
|
cwd: workspacePath,
|
|
6142
7174
|
ignore: ["node_modules/**", ".git/**", "dist/**", "build/**"],
|
|
6143
7175
|
nodir: true
|
|
6144
7176
|
});
|
|
6145
|
-
|
|
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.`;
|
|
6146
7191
|
} catch {
|
|
6147
7192
|
workspaceContext = "Workspace Scout: Could not scan workspace.";
|
|
6148
7193
|
}
|
|
@@ -6228,7 +7273,7 @@ ${prompt}` : prompt;
|
|
|
6228
7273
|
this.telemetry.capture("cascade:session_start", {
|
|
6229
7274
|
complexity,
|
|
6230
7275
|
providerCount: this.config.providers.length,
|
|
6231
|
-
cascadeAutoEnabled: this.config
|
|
7276
|
+
cascadeAutoEnabled: this.config.cascadeAuto === true,
|
|
6232
7277
|
toolCreationEnabled: this.config["enableToolCreation"] === true
|
|
6233
7278
|
});
|
|
6234
7279
|
this.emit("tier:root", { role: complexity === "Simple" ? "T3" : complexity === "Moderate" ? "T2" : "T1" });
|
|
@@ -6243,6 +7288,7 @@ ${prompt}` : prompt;
|
|
|
6243
7288
|
}));
|
|
6244
7289
|
}
|
|
6245
7290
|
const toolCreator = this.toolCreator;
|
|
7291
|
+
if (toolCreator) toolCreator.setPermissionEscalator(escalator);
|
|
6246
7292
|
let finalOutput = "";
|
|
6247
7293
|
let t2Results = [];
|
|
6248
7294
|
let runError = null;
|
|
@@ -6264,6 +7310,8 @@ ${prompt}` : prompt;
|
|
|
6264
7310
|
});
|
|
6265
7311
|
tier.on("log", (e) => this.emit("log", e));
|
|
6266
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));
|
|
6267
7315
|
tier.on("tool:approval-request", async (request) => {
|
|
6268
7316
|
this.emit("tool:approval-request", request);
|
|
6269
7317
|
let decision = { approved: false };
|
|
@@ -6318,6 +7366,7 @@ ${prompt}` : prompt;
|
|
|
6318
7366
|
}
|
|
6319
7367
|
t2.setPermissionEscalator(escalator);
|
|
6320
7368
|
if (toolCreator) t2.setToolCreator(toolCreator);
|
|
7369
|
+
t2.setPeerMessageCallback((e) => this.emit("peer:message", e), options.sessionId ?? "");
|
|
6321
7370
|
bindTierEvents(t2);
|
|
6322
7371
|
const assignment = {
|
|
6323
7372
|
sectionId: taskId,
|
|
@@ -6347,6 +7396,7 @@ ${prompt}` : prompt;
|
|
|
6347
7396
|
}
|
|
6348
7397
|
t1.setPermissionEscalator(escalator);
|
|
6349
7398
|
if (toolCreator) t1.setToolCreator(toolCreator);
|
|
7399
|
+
t1.setPeerMessageCallback((e) => this.emit("peer:message", e), options.sessionId ?? "");
|
|
6350
7400
|
bindTierEvents(t1);
|
|
6351
7401
|
t1.on("plan", (e) => this.emit("plan", e));
|
|
6352
7402
|
const result = await t1.execute(options.prompt, options.images, void 0, options.signal);
|
|
@@ -6370,6 +7420,13 @@ ${prompt}` : prompt;
|
|
|
6370
7420
|
escalator.cancelAllPending();
|
|
6371
7421
|
} catch {
|
|
6372
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
|
+
}
|
|
6373
7430
|
try {
|
|
6374
7431
|
const stats2 = this.router.getStats();
|
|
6375
7432
|
const durationMs2 = Date.now() - startMs;
|
|
@@ -6470,7 +7527,7 @@ var Keystore = class {
|
|
|
6470
7527
|
const creds = await this.keytar.findCredentials(KEYTAR_SERVICE);
|
|
6471
7528
|
this.cache = Object.fromEntries(creds.map((c) => [c.account, c.password]));
|
|
6472
7529
|
this.backend = "keytar";
|
|
6473
|
-
if (password &&
|
|
7530
|
+
if (password && fs15__default.default.existsSync(this.storePath)) {
|
|
6474
7531
|
try {
|
|
6475
7532
|
const fileEntries = this.decryptFile(password);
|
|
6476
7533
|
for (const [k, v] of Object.entries(fileEntries)) {
|
|
@@ -6489,7 +7546,7 @@ var Keystore = class {
|
|
|
6489
7546
|
"Keystore unlock requires a password because the OS keychain (keytar) is not available on this system."
|
|
6490
7547
|
);
|
|
6491
7548
|
}
|
|
6492
|
-
if (!
|
|
7549
|
+
if (!fs15__default.default.existsSync(this.storePath)) {
|
|
6493
7550
|
const salt = crypto__default.default.randomBytes(SALT_LEN);
|
|
6494
7551
|
this.masterKey = this.deriveKey(password, salt);
|
|
6495
7552
|
this.writeWithSalt({}, salt);
|
|
@@ -6503,7 +7560,7 @@ var Keystore = class {
|
|
|
6503
7560
|
}
|
|
6504
7561
|
/** Synchronous legacy unlock kept for AES-only environments. */
|
|
6505
7562
|
unlockSync(password) {
|
|
6506
|
-
if (!
|
|
7563
|
+
if (!fs15__default.default.existsSync(this.storePath)) {
|
|
6507
7564
|
const salt = crypto__default.default.randomBytes(SALT_LEN);
|
|
6508
7565
|
this.masterKey = this.deriveKey(password, salt);
|
|
6509
7566
|
this.writeWithSalt({}, salt);
|
|
@@ -6561,7 +7618,7 @@ var Keystore = class {
|
|
|
6561
7618
|
}
|
|
6562
7619
|
}
|
|
6563
7620
|
decryptFile(password, knownSalt) {
|
|
6564
|
-
if (!
|
|
7621
|
+
if (!fs15__default.default.existsSync(this.storePath)) return {};
|
|
6565
7622
|
try {
|
|
6566
7623
|
const { salt, ciphertext, iv, tag } = this.readRaw();
|
|
6567
7624
|
const useSalt = knownSalt ?? salt;
|
|
@@ -6583,8 +7640,8 @@ var Keystore = class {
|
|
|
6583
7640
|
const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
|
|
6584
7641
|
const tag = cipher.getAuthTag();
|
|
6585
7642
|
const out = Buffer.concat([raw.salt, iv, tag, ciphertext]);
|
|
6586
|
-
|
|
6587
|
-
|
|
7643
|
+
fs15__default.default.mkdirSync(path16__default.default.dirname(this.storePath), { recursive: true });
|
|
7644
|
+
fs15__default.default.writeFileSync(this.storePath, out, { mode: 384 });
|
|
6588
7645
|
}
|
|
6589
7646
|
writeWithSalt(data, salt) {
|
|
6590
7647
|
if (!this.masterKey) throw new Error("writeWithSalt called before masterKey was set");
|
|
@@ -6594,11 +7651,11 @@ var Keystore = class {
|
|
|
6594
7651
|
const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
|
|
6595
7652
|
const tag = cipher.getAuthTag();
|
|
6596
7653
|
const out = Buffer.concat([salt, iv, tag, ciphertext]);
|
|
6597
|
-
|
|
6598
|
-
|
|
7654
|
+
fs15__default.default.mkdirSync(path16__default.default.dirname(this.storePath), { recursive: true });
|
|
7655
|
+
fs15__default.default.writeFileSync(this.storePath, out, { mode: 384 });
|
|
6599
7656
|
}
|
|
6600
7657
|
readRaw() {
|
|
6601
|
-
const buf =
|
|
7658
|
+
const buf = fs15__default.default.readFileSync(this.storePath);
|
|
6602
7659
|
let offset = 0;
|
|
6603
7660
|
const salt = buf.subarray(offset, offset + SALT_LEN);
|
|
6604
7661
|
offset += SALT_LEN;
|
|
@@ -6631,9 +7688,9 @@ var CascadeIgnore = class {
|
|
|
6631
7688
|
]);
|
|
6632
7689
|
}
|
|
6633
7690
|
async load(workspacePath) {
|
|
6634
|
-
const filePath =
|
|
7691
|
+
const filePath = path16__default.default.join(workspacePath, ".cascadeignore");
|
|
6635
7692
|
try {
|
|
6636
|
-
const content = await
|
|
7693
|
+
const content = await fs3__default.default.readFile(filePath, "utf-8");
|
|
6637
7694
|
const lines = content.split("\n").filter((l) => l.trim() && !l.startsWith("#"));
|
|
6638
7695
|
this.ig.add(lines);
|
|
6639
7696
|
this.loaded = true;
|
|
@@ -6642,7 +7699,7 @@ var CascadeIgnore = class {
|
|
|
6642
7699
|
}
|
|
6643
7700
|
isIgnored(filePath, workspacePath) {
|
|
6644
7701
|
try {
|
|
6645
|
-
const relative = workspacePath ?
|
|
7702
|
+
const relative = workspacePath ? path16__default.default.relative(workspacePath, filePath) : filePath;
|
|
6646
7703
|
return this.ig.ignores(relative);
|
|
6647
7704
|
} catch {
|
|
6648
7705
|
return false;
|
|
@@ -6653,9 +7710,9 @@ var CascadeIgnore = class {
|
|
|
6653
7710
|
}
|
|
6654
7711
|
};
|
|
6655
7712
|
async function loadCascadeMd(workspacePath) {
|
|
6656
|
-
const filePath =
|
|
7713
|
+
const filePath = path16__default.default.join(workspacePath, "CASCADE.md");
|
|
6657
7714
|
try {
|
|
6658
|
-
const raw = await
|
|
7715
|
+
const raw = await fs3__default.default.readFile(filePath, "utf-8");
|
|
6659
7716
|
return parseCascadeMd(raw);
|
|
6660
7717
|
} catch {
|
|
6661
7718
|
return null;
|
|
@@ -6684,7 +7741,7 @@ ${raw.trim()}`;
|
|
|
6684
7741
|
var MemoryStore = class _MemoryStore {
|
|
6685
7742
|
db;
|
|
6686
7743
|
constructor(dbPath) {
|
|
6687
|
-
|
|
7744
|
+
fs15__default.default.mkdirSync(path16__default.default.dirname(dbPath), { recursive: true });
|
|
6688
7745
|
try {
|
|
6689
7746
|
this.db = new Database__default.default(dbPath, { timeout: 5e3 });
|
|
6690
7747
|
this.db.pragma("journal_mode = WAL");
|
|
@@ -7167,6 +8224,27 @@ Original error: ${err.message}`
|
|
|
7167
8224
|
if (!row.oldest) return Infinity;
|
|
7168
8225
|
return Date.now() - new Date(row.oldest).getTime();
|
|
7169
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
|
+
}
|
|
7170
8248
|
// ── Tool Result Cache (in-memory, TTL-based) ──────────────────────────
|
|
7171
8249
|
// Avoids redundant calls for read-only tools within a short window.
|
|
7172
8250
|
// Not persisted to DB — cleared on process restart.
|
|
@@ -7421,15 +8499,15 @@ var ConfigManager = class {
|
|
|
7421
8499
|
globalDir;
|
|
7422
8500
|
constructor(workspacePath = process.cwd()) {
|
|
7423
8501
|
this.workspacePath = workspacePath;
|
|
7424
|
-
this.globalDir =
|
|
8502
|
+
this.globalDir = path16__default.default.join(os3__default.default.homedir(), GLOBAL_CONFIG_DIR);
|
|
7425
8503
|
}
|
|
7426
8504
|
async load() {
|
|
7427
8505
|
this.config = await this.loadConfig();
|
|
7428
8506
|
this.ignore = new CascadeIgnore();
|
|
7429
8507
|
await this.ignore.load(this.workspacePath);
|
|
7430
8508
|
this.cascadeMd = await loadCascadeMd(this.workspacePath);
|
|
7431
|
-
this.keystore = new Keystore(
|
|
7432
|
-
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));
|
|
7433
8511
|
await this.injectEnvKeys();
|
|
7434
8512
|
await this.ensureDefaultIdentity();
|
|
7435
8513
|
}
|
|
@@ -7452,9 +8530,9 @@ var ConfigManager = class {
|
|
|
7452
8530
|
return this.workspacePath;
|
|
7453
8531
|
}
|
|
7454
8532
|
async save() {
|
|
7455
|
-
const configPath =
|
|
7456
|
-
await
|
|
7457
|
-
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");
|
|
7458
8536
|
}
|
|
7459
8537
|
async updateConfig(updates) {
|
|
7460
8538
|
this.config = validateConfig({ ...this.config, ...updates });
|
|
@@ -7477,9 +8555,9 @@ var ConfigManager = class {
|
|
|
7477
8555
|
return configProvider?.apiKey;
|
|
7478
8556
|
}
|
|
7479
8557
|
async loadConfig() {
|
|
7480
|
-
const configPath =
|
|
8558
|
+
const configPath = path16__default.default.join(this.workspacePath, CASCADE_CONFIG_FILE);
|
|
7481
8559
|
try {
|
|
7482
|
-
const raw = await
|
|
8560
|
+
const raw = await fs3__default.default.readFile(configPath, "utf-8");
|
|
7483
8561
|
return validateConfig(JSON.parse(raw));
|
|
7484
8562
|
} catch (err) {
|
|
7485
8563
|
if (err.code === "ENOENT") {
|
|
@@ -7643,6 +8721,9 @@ var DashboardSocket = class {
|
|
|
7643
8721
|
emitStreamToken(tierId, text, sessionId) {
|
|
7644
8722
|
this.io.to(`session:${sessionId}`).emit("stream:token", { tierId, text, sessionId });
|
|
7645
8723
|
}
|
|
8724
|
+
emitPeerMessage(event) {
|
|
8725
|
+
this.io.to(`session:${event.sessionId}`).emit("peer:message", event);
|
|
8726
|
+
}
|
|
7646
8727
|
emitApprovalRequest(request) {
|
|
7647
8728
|
this.io.emit("permission:user-required", request);
|
|
7648
8729
|
}
|
|
@@ -7685,16 +8766,13 @@ var DashboardSocket = class {
|
|
|
7685
8766
|
const { sessionId } = normalizeSessionSubscriptionPayload(payload);
|
|
7686
8767
|
socket.leave(`session:${sessionId}`);
|
|
7687
8768
|
});
|
|
7688
|
-
socket.on("join:tenant", (tenantId) => {
|
|
7689
|
-
socket.join(`tenant:${tenantId}`);
|
|
7690
|
-
});
|
|
7691
8769
|
});
|
|
7692
8770
|
}
|
|
7693
8771
|
close() {
|
|
7694
8772
|
this.io.close();
|
|
7695
8773
|
}
|
|
7696
8774
|
};
|
|
7697
|
-
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))));
|
|
7698
8776
|
var DashboardServer = class {
|
|
7699
8777
|
app;
|
|
7700
8778
|
httpServer;
|
|
@@ -7760,15 +8838,15 @@ var DashboardServer = class {
|
|
|
7760
8838
|
resolveDashboardSecret() {
|
|
7761
8839
|
const fromConfig = this.config.dashboard.secret ?? process.env["CASCADE_DASHBOARD_SECRET"];
|
|
7762
8840
|
if (fromConfig) return fromConfig;
|
|
7763
|
-
const secretPath =
|
|
8841
|
+
const secretPath = path16__default.default.join(this.workspacePath, CASCADE_DASHBOARD_SECRET_FILE);
|
|
7764
8842
|
try {
|
|
7765
|
-
if (
|
|
7766
|
-
const existing =
|
|
8843
|
+
if (fs15__default.default.existsSync(secretPath)) {
|
|
8844
|
+
const existing = fs15__default.default.readFileSync(secretPath, "utf-8").trim();
|
|
7767
8845
|
if (existing.length >= 16) return existing;
|
|
7768
8846
|
}
|
|
7769
8847
|
const generated = crypto.randomUUID();
|
|
7770
|
-
|
|
7771
|
-
|
|
8848
|
+
fs15__default.default.mkdirSync(path16__default.default.dirname(secretPath), { recursive: true });
|
|
8849
|
+
fs15__default.default.writeFileSync(secretPath, generated, { encoding: "utf-8", mode: 384 });
|
|
7772
8850
|
if (this.config.dashboard.auth) {
|
|
7773
8851
|
console.warn(
|
|
7774
8852
|
`Dashboard auth enabled with no secret configured; persisted a generated secret to ${secretPath}. Set CASCADE_DASHBOARD_SECRET or config.dashboard.secret to override.`
|
|
@@ -7795,7 +8873,7 @@ var DashboardServer = class {
|
|
|
7795
8873
|
// ── Setup ─────────────────────────────────────
|
|
7796
8874
|
getGlobalStore() {
|
|
7797
8875
|
if (!this.globalStore) {
|
|
7798
|
-
const globalDbPath =
|
|
8876
|
+
const globalDbPath = path16__default.default.join(os3__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
|
|
7799
8877
|
this.globalStore = new MemoryStore(globalDbPath);
|
|
7800
8878
|
}
|
|
7801
8879
|
return this.globalStore;
|
|
@@ -7856,12 +8934,12 @@ var DashboardServer = class {
|
|
|
7856
8934
|
}
|
|
7857
8935
|
}
|
|
7858
8936
|
watchRuntimeChanges() {
|
|
7859
|
-
const workspaceDbPath =
|
|
7860
|
-
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);
|
|
7861
8939
|
const watchPaths = [workspaceDbPath, globalDbPath].filter((p, index, arr) => arr.indexOf(p) === index);
|
|
7862
8940
|
for (const watchPath of watchPaths) {
|
|
7863
|
-
if (!
|
|
7864
|
-
|
|
8941
|
+
if (!fs15__default.default.existsSync(watchPath)) continue;
|
|
8942
|
+
fs15__default.default.watchFile(watchPath, { interval: 3e3 }, () => {
|
|
7865
8943
|
this.throttledBroadcast(watchPath === globalDbPath ? "global" : "workspace");
|
|
7866
8944
|
});
|
|
7867
8945
|
}
|
|
@@ -7892,6 +8970,21 @@ var DashboardServer = class {
|
|
|
7892
8970
|
legacyHeaders: false,
|
|
7893
8971
|
message: { error: "Too many login attempts. Try again in 15 minutes." }
|
|
7894
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
|
+
});
|
|
7895
8988
|
this.app.post("/api/auth/login", loginLimiter, (req, res) => {
|
|
7896
8989
|
const { username, password } = req.body ?? {};
|
|
7897
8990
|
if (!authRequired) {
|
|
@@ -7927,22 +9020,33 @@ var DashboardServer = class {
|
|
|
7927
9020
|
res.status(401).json({ error: "Invalid credentials" });
|
|
7928
9021
|
}
|
|
7929
9022
|
});
|
|
7930
|
-
this.app.post("/api/force-halt", auth, (req, res) => {
|
|
7931
|
-
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;
|
|
7932
9027
|
const payload = { sessionId, nodeId, requestedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
7933
9028
|
this.socket.broadcast("session:halt", payload);
|
|
7934
9029
|
if (sessionId) this.socket.broadcastToRoom(`session:${sessionId}`, "session:halt", payload);
|
|
7935
9030
|
res.json({ success: true, ...payload });
|
|
7936
9031
|
});
|
|
7937
|
-
this.app.post("/api/approve", auth, (req, res) => {
|
|
7938
|
-
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;
|
|
7939
9036
|
const payload = { sessionId, nodeId, requestedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
7940
9037
|
this.socket.broadcast("session:approve", payload);
|
|
7941
9038
|
if (sessionId) this.socket.broadcastToRoom(`session:${sessionId}`, "session:approve", payload);
|
|
7942
9039
|
res.json({ success: true, ...payload });
|
|
7943
9040
|
});
|
|
7944
|
-
this.app.post("/api/inject", auth, (req, res) => {
|
|
7945
|
-
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
|
+
}
|
|
7946
9050
|
const payload = { sessionId, nodeId, message, requestedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
7947
9051
|
this.socket.broadcast("session:message-injected", payload);
|
|
7948
9052
|
if (sessionId) this.socket.broadcastToRoom(`session:${sessionId}`, "session:message-injected", payload);
|
|
@@ -7965,7 +9069,7 @@ var DashboardServer = class {
|
|
|
7965
9069
|
const sessionId = req.params.id;
|
|
7966
9070
|
this.store.deleteSession(sessionId);
|
|
7967
9071
|
this.store.deleteRuntimeSession(sessionId);
|
|
7968
|
-
const globalDbPath =
|
|
9072
|
+
const globalDbPath = path16__default.default.join(os3__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
|
|
7969
9073
|
const globalStore = new MemoryStore(globalDbPath);
|
|
7970
9074
|
try {
|
|
7971
9075
|
globalStore.deleteRuntimeSession(sessionId);
|
|
@@ -7979,7 +9083,7 @@ var DashboardServer = class {
|
|
|
7979
9083
|
});
|
|
7980
9084
|
this.app.delete("/api/sessions", auth, (req, res) => {
|
|
7981
9085
|
const body = req.body;
|
|
7982
|
-
const globalDbPath =
|
|
9086
|
+
const globalDbPath = path16__default.default.join(os3__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
|
|
7983
9087
|
if (body?.ids && Array.isArray(body.ids) && body.ids.length > 0) {
|
|
7984
9088
|
const globalStore = new MemoryStore(globalDbPath);
|
|
7985
9089
|
try {
|
|
@@ -8002,7 +9106,7 @@ var DashboardServer = class {
|
|
|
8002
9106
|
});
|
|
8003
9107
|
this.app.delete("/api/runtime", auth, (_req, res) => {
|
|
8004
9108
|
this.store.deleteAllRuntimeNodes();
|
|
8005
|
-
const globalDbPath =
|
|
9109
|
+
const globalDbPath = path16__default.default.join(os3__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
|
|
8006
9110
|
const globalStore = new MemoryStore(globalDbPath);
|
|
8007
9111
|
try {
|
|
8008
9112
|
globalStore.deleteAllRuntimeNodes();
|
|
@@ -8064,16 +9168,26 @@ var DashboardServer = class {
|
|
|
8064
9168
|
});
|
|
8065
9169
|
this.app.put("/api/config", auth, async (req, res) => {
|
|
8066
9170
|
const body = req.body;
|
|
8067
|
-
if (body
|
|
8068
|
-
|
|
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"] };
|
|
8069
9181
|
try {
|
|
8070
|
-
const configPath =
|
|
8071
|
-
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")) : {};
|
|
8072
9184
|
const updated = { ...existing, tierLimits: this.config.tierLimits, budget: this.config.budget };
|
|
8073
|
-
|
|
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);
|
|
8074
9188
|
res.json({ ok: true });
|
|
8075
9189
|
} catch (err) {
|
|
8076
|
-
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)}` });
|
|
8077
9191
|
}
|
|
8078
9192
|
});
|
|
8079
9193
|
this.app.get("/api/runtime/logs/:sessionId", auth, (req, res) => {
|
|
@@ -8098,7 +9212,7 @@ var DashboardServer = class {
|
|
|
8098
9212
|
this.app.get("/api/runtime", auth, (req, res) => {
|
|
8099
9213
|
const scope = req.query["scope"] ?? "workspace";
|
|
8100
9214
|
if (scope === "global") {
|
|
8101
|
-
const globalDbPath =
|
|
9215
|
+
const globalDbPath = path16__default.default.join(os3__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
|
|
8102
9216
|
const globalStore = new MemoryStore(globalDbPath);
|
|
8103
9217
|
try {
|
|
8104
9218
|
res.json({
|
|
@@ -8119,7 +9233,7 @@ var DashboardServer = class {
|
|
|
8119
9233
|
logs: this.store.listRuntimeNodeLogs(void 0, void 0, 500)
|
|
8120
9234
|
});
|
|
8121
9235
|
});
|
|
8122
|
-
this.app.post("/api/run", auth, (req, res) => {
|
|
9236
|
+
this.app.post("/api/run", auth, mutationLimiter, (req, res) => {
|
|
8123
9237
|
const body = req.body;
|
|
8124
9238
|
if (!body.prompt || typeof body.prompt !== "string") {
|
|
8125
9239
|
res.status(400).json({ error: "prompt is required" });
|
|
@@ -8140,12 +9254,15 @@ var DashboardServer = class {
|
|
|
8140
9254
|
cascade.on("permission:user-required", (e) => {
|
|
8141
9255
|
this.socket.broadcastToRoom(`session:${sessionId}`, "permission:user-required", { sessionId, ...e });
|
|
8142
9256
|
});
|
|
9257
|
+
cascade.on("peer:message", (e) => {
|
|
9258
|
+
this.socket.emitPeerMessage(e);
|
|
9259
|
+
});
|
|
8143
9260
|
try {
|
|
8144
9261
|
const result = await cascade.run({ prompt: body.prompt, identityId: body.identityId });
|
|
8145
9262
|
this.socket.broadcast("cost:update", {
|
|
8146
9263
|
sessionId,
|
|
8147
|
-
|
|
8148
|
-
|
|
9264
|
+
totalTokens: result.usage.totalTokens,
|
|
9265
|
+
totalCostUsd: result.usage.estimatedCostUsd
|
|
8149
9266
|
});
|
|
8150
9267
|
this.socket.broadcastToRoom(`session:${sessionId}`, "session:complete", { sessionId, result });
|
|
8151
9268
|
this.throttledBroadcast("workspace");
|
|
@@ -8168,13 +9285,13 @@ var DashboardServer = class {
|
|
|
8168
9285
|
}))
|
|
8169
9286
|
});
|
|
8170
9287
|
});
|
|
8171
|
-
const prodPath =
|
|
8172
|
-
const devPath =
|
|
8173
|
-
const webDistPath =
|
|
8174
|
-
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)) {
|
|
8175
9292
|
this.app.use(express__default.default.static(webDistPath));
|
|
8176
9293
|
this.app.get("*", (_req, res) => {
|
|
8177
|
-
res.sendFile(
|
|
9294
|
+
res.sendFile(path16__default.default.join(webDistPath, "index.html"));
|
|
8178
9295
|
});
|
|
8179
9296
|
} else {
|
|
8180
9297
|
this.app.get("/", (_req, res) => {
|
|
@@ -8251,7 +9368,7 @@ var TaskScheduler = class {
|
|
|
8251
9368
|
return cron__default.default.validate(expression);
|
|
8252
9369
|
}
|
|
8253
9370
|
};
|
|
8254
|
-
var
|
|
9371
|
+
var execFileAsync2 = util.promisify(child_process.execFile);
|
|
8255
9372
|
var SAFE_ENV_NAME = /^[A-Z][A-Z0-9_]*$/;
|
|
8256
9373
|
function sanitizeEnvValue(v) {
|
|
8257
9374
|
const raw = typeof v === "string" ? v : JSON.stringify(v);
|
|
@@ -8290,7 +9407,7 @@ var HooksRunner = class {
|
|
|
8290
9407
|
const isWin = process.platform === "win32";
|
|
8291
9408
|
const shell = isWin ? "cmd.exe" : "/bin/sh";
|
|
8292
9409
|
const shellArgs = isWin ? ["/d", "/s", "/c", hook.command] : ["-c", hook.command];
|
|
8293
|
-
const { stdout } = await
|
|
9410
|
+
const { stdout } = await execFileAsync2(shell, shellArgs, {
|
|
8294
9411
|
timeout: hook.timeout ?? 1e4,
|
|
8295
9412
|
env: { ...process.env, ...envVars },
|
|
8296
9413
|
windowsHide: true
|