cascade-ai 0.4.0 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +35 -2
- package/dist/cli.cjs +2318 -1019
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +2316 -1016
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +1426 -329
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +177 -74
- package/dist/index.d.ts +177 -74
- package/dist/index.js +1422 -325
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/web/dist/assets/index-BFrwdYDg.js +225 -0
- package/web/dist/assets/index-C6Nd1mOj.css +1 -0
- package/web/dist/index.html +2 -2
- package/web/dist/assets/index-BvxaBI9b.js +0 -216
- package/web/dist/assets/index-DO_ICahS.css +0 -1
package/dist/index.js
CHANGED
|
@@ -5,20 +5,20 @@ import Anthropic from '@anthropic-ai/sdk';
|
|
|
5
5
|
import OpenAI, { AzureOpenAI } from 'openai';
|
|
6
6
|
import { GoogleGenAI, HarmBlockThreshold, HarmCategory } from '@google/genai';
|
|
7
7
|
import axios2 from 'axios';
|
|
8
|
-
import
|
|
9
|
-
import
|
|
8
|
+
import fs3 from 'fs/promises';
|
|
9
|
+
import path16 from 'path';
|
|
10
10
|
import * as ignoreFactory from 'ignore';
|
|
11
11
|
import ignoreFactory__default from 'ignore';
|
|
12
12
|
import { exec, execFile, execSync } from 'child_process';
|
|
13
13
|
import { promisify } from 'util';
|
|
14
|
+
import fs15 from 'fs';
|
|
14
15
|
import { simpleGit } from 'simple-git';
|
|
15
|
-
import fs11 from 'fs';
|
|
16
16
|
import PDFDocument from 'pdfkit';
|
|
17
17
|
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
18
18
|
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
|
19
19
|
import { z } from 'zod';
|
|
20
|
+
import os3 from 'os';
|
|
20
21
|
import { createContext, runInContext } from 'vm';
|
|
21
|
-
import os2 from 'os';
|
|
22
22
|
import Database from 'better-sqlite3';
|
|
23
23
|
import { createServer } from 'http';
|
|
24
24
|
import { fileURLToPath } from 'url';
|
|
@@ -124,7 +124,7 @@ var require_keytar2 = __commonJS({
|
|
|
124
124
|
});
|
|
125
125
|
|
|
126
126
|
// src/constants.ts
|
|
127
|
-
var CASCADE_VERSION = "0.
|
|
127
|
+
var CASCADE_VERSION = "0.5.1";
|
|
128
128
|
var CASCADE_CONFIG_DIR = ".cascade";
|
|
129
129
|
var CASCADE_MD_FILE = "CASCADE.md";
|
|
130
130
|
var CASCADE_IGNORE_FILE = ".cascadeignore";
|
|
@@ -1118,6 +1118,22 @@ var GeminiProvider = class extends BaseProvider {
|
|
|
1118
1118
|
};
|
|
1119
1119
|
}
|
|
1120
1120
|
};
|
|
1121
|
+
var TOOL_CAPABLE_FAMILIES = [
|
|
1122
|
+
"llama3.1",
|
|
1123
|
+
"llama3.2",
|
|
1124
|
+
"llama3.3",
|
|
1125
|
+
"qwen2",
|
|
1126
|
+
"qwen2.5",
|
|
1127
|
+
"qwen3",
|
|
1128
|
+
"mistral-nemo",
|
|
1129
|
+
"mistral-small",
|
|
1130
|
+
"command-r",
|
|
1131
|
+
"firefunction"
|
|
1132
|
+
];
|
|
1133
|
+
function isToolCapable(modelName) {
|
|
1134
|
+
const name = modelName.toLowerCase();
|
|
1135
|
+
return TOOL_CAPABLE_FAMILIES.some((family) => name.includes(family));
|
|
1136
|
+
}
|
|
1121
1137
|
var OllamaProvider = class extends BaseProvider {
|
|
1122
1138
|
baseUrl;
|
|
1123
1139
|
constructor(config, model) {
|
|
@@ -1130,12 +1146,21 @@ var OllamaProvider = class extends BaseProvider {
|
|
|
1130
1146
|
}
|
|
1131
1147
|
async generateStream(options, onChunk) {
|
|
1132
1148
|
const messages = this.convertMessages(options.messages, options.systemPrompt);
|
|
1149
|
+
const ollamaTools = options.tools?.map((t) => ({
|
|
1150
|
+
type: "function",
|
|
1151
|
+
function: {
|
|
1152
|
+
name: t.name,
|
|
1153
|
+
description: t.description,
|
|
1154
|
+
parameters: t.inputSchema
|
|
1155
|
+
}
|
|
1156
|
+
}));
|
|
1133
1157
|
const response = await axios2.post(
|
|
1134
1158
|
`${this.baseUrl}/api/chat`,
|
|
1135
1159
|
{
|
|
1136
1160
|
model: this.model.id,
|
|
1137
1161
|
messages,
|
|
1138
1162
|
stream: true,
|
|
1163
|
+
tools: ollamaTools?.length ? ollamaTools : void 0,
|
|
1139
1164
|
options: {
|
|
1140
1165
|
num_predict: options.maxTokens ?? this.model.maxOutputTokens,
|
|
1141
1166
|
temperature: options.temperature ?? 0.7
|
|
@@ -1146,6 +1171,7 @@ var OllamaProvider = class extends BaseProvider {
|
|
|
1146
1171
|
let fullContent = "";
|
|
1147
1172
|
let inputTokens = 0;
|
|
1148
1173
|
let outputTokens = 0;
|
|
1174
|
+
const pendingToolCalls = [];
|
|
1149
1175
|
await new Promise((resolve, reject) => {
|
|
1150
1176
|
let buffer = "";
|
|
1151
1177
|
response.data.on("data", (chunk) => {
|
|
@@ -1160,6 +1186,9 @@ var OllamaProvider = class extends BaseProvider {
|
|
|
1160
1186
|
fullContent += parsed.message.content;
|
|
1161
1187
|
onChunk({ text: parsed.message.content, finishReason: null });
|
|
1162
1188
|
}
|
|
1189
|
+
if (parsed.message?.tool_calls?.length) {
|
|
1190
|
+
pendingToolCalls.push(...parsed.message.tool_calls);
|
|
1191
|
+
}
|
|
1163
1192
|
if (parsed.done) {
|
|
1164
1193
|
inputTokens = parsed.prompt_eval_count ?? 0;
|
|
1165
1194
|
outputTokens = parsed.eval_count ?? 0;
|
|
@@ -1177,6 +1206,9 @@ var OllamaProvider = class extends BaseProvider {
|
|
|
1177
1206
|
fullContent += parsed.message.content;
|
|
1178
1207
|
onChunk({ text: parsed.message.content, finishReason: null });
|
|
1179
1208
|
}
|
|
1209
|
+
if (parsed.message?.tool_calls?.length) {
|
|
1210
|
+
pendingToolCalls.push(...parsed.message.tool_calls);
|
|
1211
|
+
}
|
|
1180
1212
|
if (parsed.done) {
|
|
1181
1213
|
inputTokens = parsed.prompt_eval_count ?? inputTokens;
|
|
1182
1214
|
outputTokens = parsed.eval_count ?? outputTokens;
|
|
@@ -1188,11 +1220,30 @@ var OllamaProvider = class extends BaseProvider {
|
|
|
1188
1220
|
});
|
|
1189
1221
|
response.data.on("error", reject);
|
|
1190
1222
|
});
|
|
1191
|
-
|
|
1223
|
+
const toolCalls = pendingToolCalls.map((tc, i) => {
|
|
1224
|
+
let input;
|
|
1225
|
+
if (typeof tc.function.arguments === "string") {
|
|
1226
|
+
try {
|
|
1227
|
+
input = JSON.parse(tc.function.arguments);
|
|
1228
|
+
} catch {
|
|
1229
|
+
input = { __rawArguments: tc.function.arguments };
|
|
1230
|
+
}
|
|
1231
|
+
} else {
|
|
1232
|
+
input = tc.function.arguments;
|
|
1233
|
+
}
|
|
1234
|
+
return {
|
|
1235
|
+
id: `ollama-tool-${Date.now()}-${i}`,
|
|
1236
|
+
name: tc.function.name,
|
|
1237
|
+
input
|
|
1238
|
+
};
|
|
1239
|
+
});
|
|
1240
|
+
const finishReason = toolCalls.length ? "tool_use" : "stop";
|
|
1241
|
+
onChunk({ text: "", finishReason });
|
|
1192
1242
|
return {
|
|
1193
1243
|
content: fullContent,
|
|
1194
1244
|
usage: this.makeUsage(inputTokens, outputTokens),
|
|
1195
|
-
|
|
1245
|
+
toolCalls: toolCalls.length ? toolCalls : void 0,
|
|
1246
|
+
finishReason
|
|
1196
1247
|
};
|
|
1197
1248
|
}
|
|
1198
1249
|
async countTokens(text) {
|
|
@@ -1216,6 +1267,7 @@ var OllamaProvider = class extends BaseProvider {
|
|
|
1216
1267
|
maxOutputTokens: 4e3,
|
|
1217
1268
|
supportsStreaming: true,
|
|
1218
1269
|
isLocal: true,
|
|
1270
|
+
supportsToolUse: isToolCapable(m.name),
|
|
1219
1271
|
minSizeB: this.parseSizeB(m.details?.parameter_size)
|
|
1220
1272
|
}));
|
|
1221
1273
|
} catch {
|
|
@@ -1238,6 +1290,26 @@ var OllamaProvider = class extends BaseProvider {
|
|
|
1238
1290
|
result.push({ role: "system", content: typeof m.content === "string" ? m.content : "" });
|
|
1239
1291
|
continue;
|
|
1240
1292
|
}
|
|
1293
|
+
if (m.role === "tool") {
|
|
1294
|
+
result.push({
|
|
1295
|
+
role: "tool",
|
|
1296
|
+
content: typeof m.content === "string" ? m.content : JSON.stringify(m.content)
|
|
1297
|
+
});
|
|
1298
|
+
continue;
|
|
1299
|
+
}
|
|
1300
|
+
if (m.role === "assistant" && m.toolCalls?.length) {
|
|
1301
|
+
result.push({
|
|
1302
|
+
role: "assistant",
|
|
1303
|
+
content: typeof m.content === "string" ? m.content : "",
|
|
1304
|
+
tool_calls: m.toolCalls.map((tc) => ({
|
|
1305
|
+
function: {
|
|
1306
|
+
name: tc.name,
|
|
1307
|
+
arguments: tc.input
|
|
1308
|
+
}
|
|
1309
|
+
}))
|
|
1310
|
+
});
|
|
1311
|
+
continue;
|
|
1312
|
+
}
|
|
1241
1313
|
if (typeof m.content === "string") {
|
|
1242
1314
|
result.push({ role: m.role, content: m.content });
|
|
1243
1315
|
continue;
|
|
@@ -1370,6 +1442,26 @@ var ModelSelector = class {
|
|
|
1370
1442
|
return T3_MODEL_PRIORITY;
|
|
1371
1443
|
}
|
|
1372
1444
|
}
|
|
1445
|
+
getAllAvailableModels() {
|
|
1446
|
+
return Array.from(this.availableModels.values()).filter(
|
|
1447
|
+
(m) => this.availableProviders.has(m.provider)
|
|
1448
|
+
);
|
|
1449
|
+
}
|
|
1450
|
+
/**
|
|
1451
|
+
* Returns all available models eligible for the given tier, ordered by the
|
|
1452
|
+
* tier's priority chain. Use this as the candidate set for scored selection.
|
|
1453
|
+
*/
|
|
1454
|
+
getCandidatesForTier(tier) {
|
|
1455
|
+
const priority = this.getPriorityList(tier);
|
|
1456
|
+
const candidates = [];
|
|
1457
|
+
for (const key of priority) {
|
|
1458
|
+
const model = this.availableModels.get(key);
|
|
1459
|
+
if (model && this.availableProviders.has(model.provider)) {
|
|
1460
|
+
candidates.push(model);
|
|
1461
|
+
}
|
|
1462
|
+
}
|
|
1463
|
+
return candidates;
|
|
1464
|
+
}
|
|
1373
1465
|
isProviderAvailable(provider) {
|
|
1374
1466
|
return this.availableProviders.has(provider);
|
|
1375
1467
|
}
|
|
@@ -1575,11 +1667,203 @@ var TpmLimiter = class {
|
|
|
1575
1667
|
}
|
|
1576
1668
|
};
|
|
1577
1669
|
|
|
1670
|
+
// src/core/router/local-queue.ts
|
|
1671
|
+
var LocalRequestQueue = class {
|
|
1672
|
+
maxConcurrent;
|
|
1673
|
+
active = 0;
|
|
1674
|
+
queue = [];
|
|
1675
|
+
constructor(maxConcurrent = 1) {
|
|
1676
|
+
this.maxConcurrent = Math.max(1, maxConcurrent);
|
|
1677
|
+
}
|
|
1678
|
+
/**
|
|
1679
|
+
* Acquire a queue slot. Returns a `release` function that MUST be called
|
|
1680
|
+
* when the inference call is done (even on error). Rejects if the slot
|
|
1681
|
+
* cannot be acquired within `timeoutMs`.
|
|
1682
|
+
*/
|
|
1683
|
+
async acquire(timeoutMs) {
|
|
1684
|
+
if (this.active < this.maxConcurrent) {
|
|
1685
|
+
this.active++;
|
|
1686
|
+
return this.makeRelease();
|
|
1687
|
+
}
|
|
1688
|
+
return new Promise((resolve, reject) => {
|
|
1689
|
+
let settled = false;
|
|
1690
|
+
let timer;
|
|
1691
|
+
const resolver = (release) => {
|
|
1692
|
+
if (settled) return;
|
|
1693
|
+
settled = true;
|
|
1694
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
1695
|
+
resolve(release);
|
|
1696
|
+
};
|
|
1697
|
+
if (timeoutMs !== void 0 && timeoutMs > 0) {
|
|
1698
|
+
timer = setTimeout(() => {
|
|
1699
|
+
if (settled) return;
|
|
1700
|
+
settled = true;
|
|
1701
|
+
const idx = this.queue.indexOf(resolver);
|
|
1702
|
+
if (idx !== -1) this.queue.splice(idx, 1);
|
|
1703
|
+
reject(new Error(
|
|
1704
|
+
`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.`
|
|
1705
|
+
));
|
|
1706
|
+
}, timeoutMs);
|
|
1707
|
+
}
|
|
1708
|
+
this.queue.push(resolver);
|
|
1709
|
+
});
|
|
1710
|
+
}
|
|
1711
|
+
/** Number of in-flight requests. */
|
|
1712
|
+
get activeCount() {
|
|
1713
|
+
return this.active;
|
|
1714
|
+
}
|
|
1715
|
+
/** Number of requests waiting for a slot. */
|
|
1716
|
+
get queueDepth() {
|
|
1717
|
+
return this.queue.length;
|
|
1718
|
+
}
|
|
1719
|
+
makeRelease() {
|
|
1720
|
+
let called = false;
|
|
1721
|
+
return () => {
|
|
1722
|
+
if (called) return;
|
|
1723
|
+
called = true;
|
|
1724
|
+
this.active--;
|
|
1725
|
+
const next = this.queue.shift();
|
|
1726
|
+
if (next) {
|
|
1727
|
+
this.active++;
|
|
1728
|
+
next(this.makeRelease());
|
|
1729
|
+
}
|
|
1730
|
+
};
|
|
1731
|
+
}
|
|
1732
|
+
};
|
|
1733
|
+
|
|
1578
1734
|
// src/utils/cost.ts
|
|
1579
1735
|
function calculateCost(inputTokens, outputTokens, model) {
|
|
1580
1736
|
return inputTokens / 1e3 * model.inputCostPer1kTokens + outputTokens / 1e3 * model.outputCostPer1kTokens;
|
|
1581
1737
|
}
|
|
1582
1738
|
|
|
1739
|
+
// src/utils/retry.ts
|
|
1740
|
+
var CascadeCancelledError = class extends Error {
|
|
1741
|
+
constructor(reason) {
|
|
1742
|
+
super(reason ?? "Run was cancelled via AbortSignal");
|
|
1743
|
+
this.name = "CascadeCancelledError";
|
|
1744
|
+
}
|
|
1745
|
+
};
|
|
1746
|
+
var CascadeToolError = class extends Error {
|
|
1747
|
+
/** A friendly message to show the user / T3 */
|
|
1748
|
+
userMessage;
|
|
1749
|
+
/** Whether this error class is retryable by default */
|
|
1750
|
+
retryable;
|
|
1751
|
+
constructor(userMessage, cause, retryable = false) {
|
|
1752
|
+
const causeMsg = cause instanceof Error ? cause.message : String(cause);
|
|
1753
|
+
super(`${userMessage}: ${causeMsg}`);
|
|
1754
|
+
this.name = "CascadeToolError";
|
|
1755
|
+
this.userMessage = userMessage;
|
|
1756
|
+
this.retryable = retryable;
|
|
1757
|
+
}
|
|
1758
|
+
};
|
|
1759
|
+
async function withTimeout(promise, timeoutMs, errorMessage = "Operation timed out") {
|
|
1760
|
+
let timer;
|
|
1761
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
1762
|
+
timer = setTimeout(
|
|
1763
|
+
() => reject(new Error(errorMessage)),
|
|
1764
|
+
timeoutMs
|
|
1765
|
+
);
|
|
1766
|
+
});
|
|
1767
|
+
try {
|
|
1768
|
+
return await Promise.race([promise, timeoutPromise]);
|
|
1769
|
+
} finally {
|
|
1770
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
1771
|
+
}
|
|
1772
|
+
}
|
|
1773
|
+
|
|
1774
|
+
// src/core/router/model-profiler.ts
|
|
1775
|
+
var SKIP_PATTERN = /embed|dall-e|whisper|tts|vision|instruct-vision|rerank/i;
|
|
1776
|
+
var SPECIALIZATION_KEYWORDS = {
|
|
1777
|
+
code: ["code", "coding", "programming", "developer", "software", "function", "debug", "typescript", "python", "javascript"],
|
|
1778
|
+
analysis: ["analysis", "analytical", "reasoning", "logic", "research", "evaluate", "assess", "explain"],
|
|
1779
|
+
creative: ["creative", "writing", "story", "poetry", "content", "blog", "essay", "narrative"],
|
|
1780
|
+
data: ["data", "sql", "statistics", "chart", "csv", "json", "excel", "spreadsheet", "math", "mathematical"],
|
|
1781
|
+
instruction: ["instruction", "instruction-following", "accurate", "precise", "factual"],
|
|
1782
|
+
multilingual: ["multilingual", "language", "translation", "linguistic"],
|
|
1783
|
+
long_context: ["long", "context", "document", "book", "summarize", "large"]
|
|
1784
|
+
};
|
|
1785
|
+
function extractSpecializations(description) {
|
|
1786
|
+
const lower = description.toLowerCase();
|
|
1787
|
+
const found = [];
|
|
1788
|
+
for (const [key, terms] of Object.entries(SPECIALIZATION_KEYWORDS)) {
|
|
1789
|
+
if (terms.some((t) => lower.includes(t))) {
|
|
1790
|
+
found.push(key);
|
|
1791
|
+
}
|
|
1792
|
+
}
|
|
1793
|
+
return found;
|
|
1794
|
+
}
|
|
1795
|
+
async function fetchOpenRouterModels() {
|
|
1796
|
+
try {
|
|
1797
|
+
const resp = await fetch("https://openrouter.ai/api/v1/models", {
|
|
1798
|
+
headers: { "User-Agent": "Cascade-AI/0.4.0" },
|
|
1799
|
+
signal: AbortSignal.timeout(8e3)
|
|
1800
|
+
});
|
|
1801
|
+
if (!resp.ok) return [];
|
|
1802
|
+
const data = await resp.json();
|
|
1803
|
+
return data.data ?? [];
|
|
1804
|
+
} catch {
|
|
1805
|
+
return [];
|
|
1806
|
+
}
|
|
1807
|
+
}
|
|
1808
|
+
async function queryModelDirectly(router, model) {
|
|
1809
|
+
try {
|
|
1810
|
+
const result = await router.generate("T3", {
|
|
1811
|
+
messages: [{
|
|
1812
|
+
role: "user",
|
|
1813
|
+
content: 'What are your top 3 task specializations? Reply with valid JSON only: {"specializations": ["<area1>", "<area2>", "<area3>"]}'
|
|
1814
|
+
}],
|
|
1815
|
+
maxTokens: 60
|
|
1816
|
+
});
|
|
1817
|
+
const match = /\{[\s\S]*?\}/.exec(result.content);
|
|
1818
|
+
if (!match) return [];
|
|
1819
|
+
const parsed = JSON.parse(match[0]);
|
|
1820
|
+
const specs = parsed.specializations;
|
|
1821
|
+
if (!Array.isArray(specs)) return [];
|
|
1822
|
+
return specs.filter((s) => typeof s === "string").slice(0, 5);
|
|
1823
|
+
} catch {
|
|
1824
|
+
return [];
|
|
1825
|
+
}
|
|
1826
|
+
}
|
|
1827
|
+
var ModelProfiler = class {
|
|
1828
|
+
store;
|
|
1829
|
+
router;
|
|
1830
|
+
constructor(store, router) {
|
|
1831
|
+
this.store = store;
|
|
1832
|
+
this.router = router;
|
|
1833
|
+
}
|
|
1834
|
+
/**
|
|
1835
|
+
* Profile all models that haven't been profiled yet.
|
|
1836
|
+
* Safe to call concurrently — SQLite upsert handles races.
|
|
1837
|
+
*/
|
|
1838
|
+
async profileAll(models) {
|
|
1839
|
+
const alreadyProfiled = new Set(this.store.getProfiledModelIds());
|
|
1840
|
+
const toProfile = models.filter(
|
|
1841
|
+
(m) => !alreadyProfiled.has(m.id) && !SKIP_PATTERN.test(m.id) && !SKIP_PATTERN.test(m.name)
|
|
1842
|
+
);
|
|
1843
|
+
if (toProfile.length === 0) return;
|
|
1844
|
+
const openRouterModels = await fetchOpenRouterModels();
|
|
1845
|
+
const orByNormalizedId = /* @__PURE__ */ new Map();
|
|
1846
|
+
for (const m of openRouterModels) {
|
|
1847
|
+
orByNormalizedId.set(m.id.toLowerCase(), m);
|
|
1848
|
+
const short = m.id.split("/").pop();
|
|
1849
|
+
if (short) orByNormalizedId.set(short.toLowerCase(), m);
|
|
1850
|
+
}
|
|
1851
|
+
await Promise.allSettled(
|
|
1852
|
+
toProfile.map(async (model) => {
|
|
1853
|
+
let specializations = [];
|
|
1854
|
+
const orMatch = orByNormalizedId.get(model.id.toLowerCase()) ?? orByNormalizedId.get(model.id.split("/").pop()?.toLowerCase() ?? "");
|
|
1855
|
+
if (orMatch?.description) {
|
|
1856
|
+
specializations = extractSpecializations(orMatch.description);
|
|
1857
|
+
}
|
|
1858
|
+
if (specializations.length === 0 && this.router) {
|
|
1859
|
+
specializations = await queryModelDirectly(this.router);
|
|
1860
|
+
}
|
|
1861
|
+
this.store.saveModelProfile(model.id, model.provider, specializations);
|
|
1862
|
+
})
|
|
1863
|
+
);
|
|
1864
|
+
}
|
|
1865
|
+
};
|
|
1866
|
+
|
|
1583
1867
|
// src/core/router/index.ts
|
|
1584
1868
|
var CascadeRouter = class _CascadeRouter extends EventEmitter {
|
|
1585
1869
|
selector;
|
|
@@ -1607,6 +1891,7 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter {
|
|
|
1607
1891
|
budgetState = "ok";
|
|
1608
1892
|
budgetExceededReason;
|
|
1609
1893
|
tpmLimiter;
|
|
1894
|
+
localQueue;
|
|
1610
1895
|
/** Thrown when the configured budget is exceeded. */
|
|
1611
1896
|
static BudgetExceededError = class extends Error {
|
|
1612
1897
|
constructor(msg) {
|
|
@@ -1623,6 +1908,7 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter {
|
|
|
1623
1908
|
this.selector = new ModelSelector(availableProviders);
|
|
1624
1909
|
this.failover = new FailoverManager(this.selector);
|
|
1625
1910
|
this.tpmLimiter = new TpmLimiter(config.rateLimits?.providerTpm ?? {});
|
|
1911
|
+
this.localQueue = new LocalRequestQueue(config.localConcurrency ?? 1);
|
|
1626
1912
|
const ollamaCfg = config.providers.find((p) => p.type === "ollama");
|
|
1627
1913
|
if (availableProviders.has("ollama")) {
|
|
1628
1914
|
await this.discoverOllamaModels(ollamaCfg);
|
|
@@ -1649,6 +1935,17 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter {
|
|
|
1649
1935
|
}
|
|
1650
1936
|
}
|
|
1651
1937
|
}
|
|
1938
|
+
/**
|
|
1939
|
+
* Run model specialization profiling in the background.
|
|
1940
|
+
* Only profiles models that haven't been profiled yet (cache-first).
|
|
1941
|
+
* No-op if store is not provided.
|
|
1942
|
+
*/
|
|
1943
|
+
async profileModels(store) {
|
|
1944
|
+
const allModels = this.selector.getAllAvailableModels();
|
|
1945
|
+
const profiler = new ModelProfiler(store, this);
|
|
1946
|
+
profiler.profileAll(allModels).catch(() => {
|
|
1947
|
+
});
|
|
1948
|
+
}
|
|
1652
1949
|
async generate(tier, options, onChunk, requireVision = false) {
|
|
1653
1950
|
if (this.budgetState === "exceeded") {
|
|
1654
1951
|
throw new _CascadeRouter.BudgetExceededError(
|
|
@@ -1670,9 +1967,26 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter {
|
|
|
1670
1967
|
await this.tpmLimiter.acquire(model.provider, estimatedTokens);
|
|
1671
1968
|
}
|
|
1672
1969
|
const useStream = Boolean(onChunk) && model.supportsStreaming && typeof provider.generateStream === "function";
|
|
1970
|
+
let releaseLocalSlot;
|
|
1971
|
+
if (model.isLocal) {
|
|
1972
|
+
const inferenceTimeoutMs = this.config.localInferenceTimeoutMs ?? 3e5;
|
|
1973
|
+
const queueWaitMs = Math.round(inferenceTimeoutMs / 2);
|
|
1974
|
+
releaseLocalSlot = await this.localQueue.acquire(queueWaitMs);
|
|
1975
|
+
}
|
|
1673
1976
|
try {
|
|
1674
1977
|
let result;
|
|
1675
|
-
if (
|
|
1978
|
+
if (model.isLocal) {
|
|
1979
|
+
const inferenceTimeoutMs = this.config.localInferenceTimeoutMs ?? 3e5;
|
|
1980
|
+
const inferencePromise = useStream && onChunk ? provider.generateStream(options, (chunk) => {
|
|
1981
|
+
const text = typeof chunk?.text === "string" ? chunk.text : "";
|
|
1982
|
+
if (text) onChunk({ ...chunk, text });
|
|
1983
|
+
}) : provider.generate(options);
|
|
1984
|
+
result = await withTimeout(
|
|
1985
|
+
inferencePromise,
|
|
1986
|
+
inferenceTimeoutMs,
|
|
1987
|
+
`Local model ${model.id} inference timed out after ${inferenceTimeoutMs}ms`
|
|
1988
|
+
);
|
|
1989
|
+
} else if (useStream && onChunk) {
|
|
1676
1990
|
try {
|
|
1677
1991
|
result = await provider.generateStream(options, (chunk) => {
|
|
1678
1992
|
const text = typeof chunk?.text === "string" ? chunk.text : "";
|
|
@@ -1710,10 +2024,14 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter {
|
|
|
1710
2024
|
if (fallback) {
|
|
1711
2025
|
this.tierModels.set(tier, fallback);
|
|
1712
2026
|
this.ensureProvider(fallback, this.config.providers);
|
|
2027
|
+
releaseLocalSlot?.();
|
|
2028
|
+
releaseLocalSlot = void 0;
|
|
1713
2029
|
return this.generate(tier, options, onChunk, requireVision);
|
|
1714
2030
|
}
|
|
1715
2031
|
}
|
|
1716
2032
|
throw err;
|
|
2033
|
+
} finally {
|
|
2034
|
+
releaseLocalSlot?.();
|
|
1717
2035
|
}
|
|
1718
2036
|
}
|
|
1719
2037
|
getModelForTier(tier) {
|
|
@@ -1953,29 +2271,6 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter {
|
|
|
1953
2271
|
return /rate.?limit|429|too.?many.?requests|quota/i.test(msg);
|
|
1954
2272
|
}
|
|
1955
2273
|
};
|
|
1956
|
-
|
|
1957
|
-
// src/utils/retry.ts
|
|
1958
|
-
var CascadeCancelledError = class extends Error {
|
|
1959
|
-
constructor(reason) {
|
|
1960
|
-
super(reason ?? "Run was cancelled via AbortSignal");
|
|
1961
|
-
this.name = "CascadeCancelledError";
|
|
1962
|
-
}
|
|
1963
|
-
};
|
|
1964
|
-
var CascadeToolError = class extends Error {
|
|
1965
|
-
/** A friendly message to show the user / T3 */
|
|
1966
|
-
userMessage;
|
|
1967
|
-
/** Whether this error class is retryable by default */
|
|
1968
|
-
retryable;
|
|
1969
|
-
constructor(userMessage, cause, retryable = false) {
|
|
1970
|
-
const causeMsg = cause instanceof Error ? cause.message : String(cause);
|
|
1971
|
-
super(`${userMessage}: ${causeMsg}`);
|
|
1972
|
-
this.name = "CascadeToolError";
|
|
1973
|
-
this.userMessage = userMessage;
|
|
1974
|
-
this.retryable = retryable;
|
|
1975
|
-
}
|
|
1976
|
-
};
|
|
1977
|
-
|
|
1978
|
-
// src/core/tiers/base.ts
|
|
1979
2274
|
var BaseTier = class extends EventEmitter {
|
|
1980
2275
|
id;
|
|
1981
2276
|
role;
|
|
@@ -2256,6 +2551,97 @@ var AuditLogger = class {
|
|
|
2256
2551
|
}
|
|
2257
2552
|
};
|
|
2258
2553
|
|
|
2554
|
+
// src/tools/text-tool-parser.ts
|
|
2555
|
+
var TOOL_CALL_RE = /<tool_call>\s*([\s\S]*?)\s*<\/tool_call>/g;
|
|
2556
|
+
var JSON_BLOCK_RE = /```json\s*([\s\S]*?)\s*```/g;
|
|
2557
|
+
var FUNCTION_OBJ_RE = /\{\s*"function"\s*:\s*\{[^}]*"name"\s*:[^}]*\}\s*\}/g;
|
|
2558
|
+
function parseTextToolCalls(text) {
|
|
2559
|
+
const results = tryXmlBlocks(text);
|
|
2560
|
+
if (results.length > 0) return results;
|
|
2561
|
+
const jsonBlockResults = tryJsonCodeBlocks(text);
|
|
2562
|
+
if (jsonBlockResults.length > 0) return jsonBlockResults;
|
|
2563
|
+
return tryFunctionCallObjects(text);
|
|
2564
|
+
}
|
|
2565
|
+
function tryXmlBlocks(text) {
|
|
2566
|
+
const results = [];
|
|
2567
|
+
let match;
|
|
2568
|
+
TOOL_CALL_RE.lastIndex = 0;
|
|
2569
|
+
while ((match = TOOL_CALL_RE.exec(text)) !== null) {
|
|
2570
|
+
try {
|
|
2571
|
+
const raw = JSON.parse(match[1]);
|
|
2572
|
+
if (typeof raw.name !== "string") continue;
|
|
2573
|
+
const input = typeof raw.input === "object" && raw.input !== null ? raw.input : {};
|
|
2574
|
+
results.push({ name: raw.name, input });
|
|
2575
|
+
} catch {
|
|
2576
|
+
}
|
|
2577
|
+
}
|
|
2578
|
+
return results;
|
|
2579
|
+
}
|
|
2580
|
+
function tryJsonCodeBlocks(text) {
|
|
2581
|
+
const results = [];
|
|
2582
|
+
let match;
|
|
2583
|
+
JSON_BLOCK_RE.lastIndex = 0;
|
|
2584
|
+
while ((match = JSON_BLOCK_RE.exec(text)) !== null) {
|
|
2585
|
+
try {
|
|
2586
|
+
const raw = JSON.parse(match[1]);
|
|
2587
|
+
if (typeof raw.name !== "string") continue;
|
|
2588
|
+
const input = typeof raw.input === "object" && raw.input !== null ? raw.input : {};
|
|
2589
|
+
results.push({ name: raw.name, input });
|
|
2590
|
+
} catch {
|
|
2591
|
+
}
|
|
2592
|
+
}
|
|
2593
|
+
return results;
|
|
2594
|
+
}
|
|
2595
|
+
function tryFunctionCallObjects(text) {
|
|
2596
|
+
const results = [];
|
|
2597
|
+
let match;
|
|
2598
|
+
FUNCTION_OBJ_RE.lastIndex = 0;
|
|
2599
|
+
while ((match = FUNCTION_OBJ_RE.exec(text)) !== null) {
|
|
2600
|
+
try {
|
|
2601
|
+
const raw = JSON.parse(match[0]);
|
|
2602
|
+
const fn = raw.function;
|
|
2603
|
+
if (!fn || typeof fn.name !== "string") continue;
|
|
2604
|
+
const input = typeof fn.arguments === "object" && fn.arguments !== null ? fn.arguments : {};
|
|
2605
|
+
results.push({ name: fn.name, input });
|
|
2606
|
+
} catch {
|
|
2607
|
+
}
|
|
2608
|
+
}
|
|
2609
|
+
return results;
|
|
2610
|
+
}
|
|
2611
|
+
function toToolCall(parsed, index) {
|
|
2612
|
+
return {
|
|
2613
|
+
id: `text-tool-${Date.now()}-${index}`,
|
|
2614
|
+
name: parsed.name,
|
|
2615
|
+
input: parsed.input
|
|
2616
|
+
};
|
|
2617
|
+
}
|
|
2618
|
+
function buildTextToolSystemPrompt(tools) {
|
|
2619
|
+
const toolDefs = tools.map((t) => {
|
|
2620
|
+
const props = t.inputSchema?.properties ?? {};
|
|
2621
|
+
const paramLines = Object.entries(props).map(([k, v]) => ` "${k}": "<${v.description ?? k}>"`);
|
|
2622
|
+
return `\u2022 ${t.name}: ${t.description}
|
|
2623
|
+
Input: {${paramLines.length ? "\n" + paramLines.join(",\n") + "\n " : ""}}`;
|
|
2624
|
+
}).join("\n");
|
|
2625
|
+
return `
|
|
2626
|
+
TOOL USE INSTRUCTIONS:
|
|
2627
|
+
You do not have native tool-use capability. To call a tool, write a <tool_call> block:
|
|
2628
|
+
|
|
2629
|
+
<tool_call>
|
|
2630
|
+
{"name": "<tool_name>", "input": {<parameters>}}
|
|
2631
|
+
</tool_call>
|
|
2632
|
+
|
|
2633
|
+
Available tools:
|
|
2634
|
+
${toolDefs}
|
|
2635
|
+
|
|
2636
|
+
EXAMPLE \u2014 calling the "shell" tool to list files:
|
|
2637
|
+
<tool_call>
|
|
2638
|
+
{"name": "shell", "input": {"command": "ls -la /workspace"}}
|
|
2639
|
+
</tool_call>
|
|
2640
|
+
|
|
2641
|
+
You will then receive a user message with the result, then continue your work.
|
|
2642
|
+
Only call one tool at a time. When you have enough information, provide your final answer.`;
|
|
2643
|
+
}
|
|
2644
|
+
|
|
2259
2645
|
// src/core/tiers/t3-worker.ts
|
|
2260
2646
|
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.
|
|
2261
2647
|
|
|
@@ -2457,6 +2843,9 @@ Now execute your subtask using this context where relevant.`
|
|
|
2457
2843
|
const MAX_ITERATIONS = 15;
|
|
2458
2844
|
const requiresArtifact = this.requiresArtifact();
|
|
2459
2845
|
tools = [...tools];
|
|
2846
|
+
const t3Model = this.router.getModelForTier("T3");
|
|
2847
|
+
const useTextTools = t3Model?.supportsToolUse === false && tools.length > 0;
|
|
2848
|
+
const textToolSuffix = useTextTools ? buildTextToolSystemPrompt(tools) : "";
|
|
2460
2849
|
while (iterations < MAX_ITERATIONS) {
|
|
2461
2850
|
iterations++;
|
|
2462
2851
|
this.throwIfCancelled();
|
|
@@ -2464,8 +2853,9 @@ Now execute your subtask using this context where relevant.`
|
|
|
2464
2853
|
messages: this.context.getMessages(),
|
|
2465
2854
|
systemPrompt: this.systemPromptOverride + systemPrompt + (this.hierarchyContext ? `
|
|
2466
2855
|
|
|
2467
|
-
HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
2468
|
-
|
|
2856
|
+
HIERARCHY CONTEXT: ${this.hierarchyContext}` : "") + textToolSuffix,
|
|
2857
|
+
// Don't pass tools array when model can't use them natively
|
|
2858
|
+
tools: useTextTools ? void 0 : tools.length ? tools : void 0,
|
|
2469
2859
|
maxTokens: 4096
|
|
2470
2860
|
};
|
|
2471
2861
|
const result = await this.router.generate(
|
|
@@ -2475,8 +2865,14 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
|
2475
2865
|
this.emit("stream:token", { tierId: this.id, text: chunk.text });
|
|
2476
2866
|
}
|
|
2477
2867
|
);
|
|
2478
|
-
|
|
2479
|
-
if (
|
|
2868
|
+
let effectiveToolCalls = result.toolCalls ?? [];
|
|
2869
|
+
if (useTextTools && effectiveToolCalls.length === 0) {
|
|
2870
|
+
const textCalls = parseTextToolCalls(result.content);
|
|
2871
|
+
effectiveToolCalls = textCalls.map((tc, i) => toToolCall(tc, i));
|
|
2872
|
+
}
|
|
2873
|
+
const effectiveResult = { ...result, toolCalls: effectiveToolCalls };
|
|
2874
|
+
await this.context.addMessage({ role: "assistant", content: result.content, toolCalls: effectiveToolCalls });
|
|
2875
|
+
if (!effectiveResult.toolCalls?.length) {
|
|
2480
2876
|
if (requiresArtifact) {
|
|
2481
2877
|
const artifactCheck = await this.verifyArtifacts(this.assignment);
|
|
2482
2878
|
if (artifactCheck.ok) {
|
|
@@ -2498,7 +2894,7 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
|
2498
2894
|
return { output: result.content, toolCalls: allToolCalls };
|
|
2499
2895
|
}
|
|
2500
2896
|
stalledArtifactIterations = 0;
|
|
2501
|
-
if (
|
|
2897
|
+
if (effectiveResult.finishReason === "stop" && effectiveResult.toolCalls.length === 0) {
|
|
2502
2898
|
if (requiresArtifact) {
|
|
2503
2899
|
const artifactCheck = await this.verifyArtifacts(this.assignment);
|
|
2504
2900
|
if (artifactCheck.ok) {
|
|
@@ -2508,7 +2904,7 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
|
2508
2904
|
return { output: result.content, toolCalls: allToolCalls };
|
|
2509
2905
|
}
|
|
2510
2906
|
}
|
|
2511
|
-
for (const tc of
|
|
2907
|
+
for (const tc of effectiveResult.toolCalls) {
|
|
2512
2908
|
allToolCalls.push(tc);
|
|
2513
2909
|
const toolResult = await this.executeTool(tc);
|
|
2514
2910
|
await this.context.addMessage({
|
|
@@ -2566,13 +2962,15 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
|
2566
2962
|
currentAction: `Using tool: ${tc.name}`,
|
|
2567
2963
|
status: "IN_PROGRESS"
|
|
2568
2964
|
});
|
|
2965
|
+
this.emit("tool:call", { id: tc.id, tierId: this.id, toolName: tc.name, input: tc.input });
|
|
2966
|
+
const toolStartMs = Date.now();
|
|
2569
2967
|
try {
|
|
2570
2968
|
const result = await this.toolRegistry.execute(tc.name, tc.input, {
|
|
2571
2969
|
tierId: this.id,
|
|
2572
2970
|
sessionId: this.taskId,
|
|
2573
2971
|
requireApproval: false,
|
|
2574
|
-
saveSnapshot: async (
|
|
2575
|
-
this.store?.addFileSnapshot(this.taskId,
|
|
2972
|
+
saveSnapshot: async (path17, content) => {
|
|
2973
|
+
this.store?.addFileSnapshot(this.taskId, path17, content);
|
|
2576
2974
|
},
|
|
2577
2975
|
sendPeerSync: (to, syncType, content) => {
|
|
2578
2976
|
this.peerBus?.send(this.id, to, syncType, this.assignment?.subtaskId ?? "", content);
|
|
@@ -2589,12 +2987,84 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
|
2589
2987
|
this.audit.fileChange(this.id, tc.input["path"] ?? "unknown", tc.name);
|
|
2590
2988
|
}
|
|
2591
2989
|
}
|
|
2592
|
-
|
|
2990
|
+
const durationMs = Date.now() - toolStartMs;
|
|
2991
|
+
this.emit("tool:result", { id: tc.id, tierId: this.id, toolName: tc.name, output: typeof result === "string" ? result : JSON.stringify(result), durationMs });
|
|
2593
2992
|
return typeof result === "string" ? result : JSON.stringify(result);
|
|
2594
2993
|
} catch (err) {
|
|
2595
|
-
|
|
2994
|
+
const durationMs = Date.now() - toolStartMs;
|
|
2995
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
2996
|
+
this.emit("tool:result", { id: tc.id, tierId: this.id, toolName: tc.name, error: errMsg, durationMs });
|
|
2997
|
+
return `Tool error: ${errMsg}`;
|
|
2596
2998
|
}
|
|
2597
2999
|
}
|
|
3000
|
+
/**
|
|
3001
|
+
* Adaptive fallback cascade — invoked when executeTool() fails.
|
|
3002
|
+
* Strategy order:
|
|
3003
|
+
* 1. Find a semantically similar registered tool and retry with same input
|
|
3004
|
+
* 2. Synthesize a new tool via ToolCreator (if available) and run it
|
|
3005
|
+
* 3. Return the original error so the agent loop can decide what to do next
|
|
3006
|
+
*/
|
|
3007
|
+
async adaptiveFallback(tc, originalError) {
|
|
3008
|
+
const altTool = this.findAlternativeTool(tc.name);
|
|
3009
|
+
if (altTool) {
|
|
3010
|
+
this.log(`Adaptive fallback: trying alternative tool "${altTool}" for failed "${tc.name}"`);
|
|
3011
|
+
this.sendStatusUpdate({ progressPct: 50, currentAction: `Fallback: trying ${altTool}`, status: "IN_PROGRESS" });
|
|
3012
|
+
try {
|
|
3013
|
+
const result = await this.toolRegistry.execute(altTool, tc.input, {
|
|
3014
|
+
tierId: this.id,
|
|
3015
|
+
sessionId: this.taskId,
|
|
3016
|
+
requireApproval: false
|
|
3017
|
+
});
|
|
3018
|
+
const str = typeof result === "string" ? result : JSON.stringify(result);
|
|
3019
|
+
if (!str.startsWith("Tool error:") && !str.startsWith("Error:")) {
|
|
3020
|
+
return `[Fallback via ${altTool}]: ${str}`;
|
|
3021
|
+
}
|
|
3022
|
+
} catch {
|
|
3023
|
+
}
|
|
3024
|
+
}
|
|
3025
|
+
if (this.toolCreator) {
|
|
3026
|
+
this.log(`Adaptive fallback: requesting dynamic tool synthesis for "${tc.name}"`);
|
|
3027
|
+
this.sendStatusUpdate({ progressPct: 55, currentAction: `Synthesizing fallback tool for: ${tc.name}`, status: "IN_PROGRESS" });
|
|
3028
|
+
try {
|
|
3029
|
+
const newToolName = await this.toolCreator.createTool(
|
|
3030
|
+
`Replacement for "${tc.name}" \u2014 original failed with: ${originalError.slice(0, 150)}`,
|
|
3031
|
+
this.assignment?.subtaskTitle ?? tc.name
|
|
3032
|
+
);
|
|
3033
|
+
if (newToolName) {
|
|
3034
|
+
this.log(`Adaptive fallback: synthesized "${newToolName}", retrying`);
|
|
3035
|
+
const result = await this.toolRegistry.execute(newToolName, tc.input, {
|
|
3036
|
+
tierId: this.id,
|
|
3037
|
+
sessionId: this.taskId,
|
|
3038
|
+
requireApproval: false
|
|
3039
|
+
});
|
|
3040
|
+
const str = typeof result === "string" ? result : JSON.stringify(result);
|
|
3041
|
+
if (!str.startsWith("Tool error:")) return `[Synthesized ${newToolName}]: ${str}`;
|
|
3042
|
+
}
|
|
3043
|
+
} catch {
|
|
3044
|
+
}
|
|
3045
|
+
}
|
|
3046
|
+
return originalError;
|
|
3047
|
+
}
|
|
3048
|
+
/**
|
|
3049
|
+
* Find a registered tool whose name/description semantically overlaps with
|
|
3050
|
+
* the failing tool. Returns the best candidate name, or null if none found.
|
|
3051
|
+
*/
|
|
3052
|
+
findAlternativeTool(failedToolName) {
|
|
3053
|
+
const failedKeywords = failedToolName.toLowerCase().split(/[_\-\s]+/);
|
|
3054
|
+
const allTools = this.toolRegistry.getToolDefinitions();
|
|
3055
|
+
let bestScore = 0;
|
|
3056
|
+
let bestName = null;
|
|
3057
|
+
for (const tool of allTools) {
|
|
3058
|
+
if (tool.name === failedToolName) continue;
|
|
3059
|
+
const toolWords = tool.name.toLowerCase().split(/[_\-\s]+/);
|
|
3060
|
+
const score = failedKeywords.filter((k) => toolWords.includes(k)).length;
|
|
3061
|
+
if (score > bestScore && score >= 1) {
|
|
3062
|
+
bestScore = score;
|
|
3063
|
+
bestName = tool.name;
|
|
3064
|
+
}
|
|
3065
|
+
}
|
|
3066
|
+
return bestName;
|
|
3067
|
+
}
|
|
2598
3068
|
/**
|
|
2599
3069
|
* Announce which files this T3 plans to edit, then acquire locks on them
|
|
2600
3070
|
* before competing siblings can claim them. T3s working on different files
|
|
@@ -2653,12 +3123,12 @@ ${assignment.expectedOutput}`;
|
|
|
2653
3123
|
if (!artifactPaths.length) return { ok: true, issues: [] };
|
|
2654
3124
|
const issues = [];
|
|
2655
3125
|
const { exec: exec3 } = await import('child_process');
|
|
2656
|
-
const { promisify:
|
|
2657
|
-
const execAsync2 =
|
|
3126
|
+
const { promisify: promisify4 } = await import('util');
|
|
3127
|
+
const execAsync2 = promisify4(exec3);
|
|
2658
3128
|
for (const artifactPath of artifactPaths) {
|
|
2659
|
-
const absolutePath =
|
|
3129
|
+
const absolutePath = path16.resolve(process.cwd(), artifactPath);
|
|
2660
3130
|
try {
|
|
2661
|
-
const stat = await
|
|
3131
|
+
const stat = await fs3.stat(absolutePath);
|
|
2662
3132
|
if (!stat.isFile()) {
|
|
2663
3133
|
issues.push(`Expected artifact is not a file: ${artifactPath}`);
|
|
2664
3134
|
continue;
|
|
@@ -2668,7 +3138,7 @@ ${assignment.expectedOutput}`;
|
|
|
2668
3138
|
continue;
|
|
2669
3139
|
}
|
|
2670
3140
|
if (!/\.pdf$/i.test(artifactPath)) {
|
|
2671
|
-
const content = await
|
|
3141
|
+
const content = await fs3.readFile(absolutePath, "utf-8");
|
|
2672
3142
|
if (!content.trim()) {
|
|
2673
3143
|
issues.push(`Artifact content is empty: ${artifactPath}`);
|
|
2674
3144
|
continue;
|
|
@@ -2677,7 +3147,7 @@ ${assignment.expectedOutput}`;
|
|
|
2677
3147
|
issues.push(`PDF artifact looks too small to be valid: ${artifactPath}`);
|
|
2678
3148
|
continue;
|
|
2679
3149
|
}
|
|
2680
|
-
const ext =
|
|
3150
|
+
const ext = path16.extname(absolutePath).toLowerCase();
|
|
2681
3151
|
try {
|
|
2682
3152
|
if (ext === ".ts" || ext === ".tsx") {
|
|
2683
3153
|
await execAsync2(`npx tsc --noEmit ${absolutePath}`, { timeout: 1e4 });
|
|
@@ -2795,6 +3265,11 @@ var PeerBus = class extends EventEmitter {
|
|
|
2795
3265
|
barriers = /* @__PURE__ */ new Map();
|
|
2796
3266
|
broadcastLog = [];
|
|
2797
3267
|
fileLocks = /* @__PURE__ */ new Map();
|
|
3268
|
+
/** subtaskIds whose T3 is being retried by T2 — dependents should re-wait rather than fail fast */
|
|
3269
|
+
retryPending = /* @__PURE__ */ new Set();
|
|
3270
|
+
/** Called when any peer message or broadcast is sent — used for dashboard visibility. */
|
|
3271
|
+
onPeerMessage;
|
|
3272
|
+
sessionId = "";
|
|
2798
3273
|
register(peerId) {
|
|
2799
3274
|
this.members.add(peerId);
|
|
2800
3275
|
}
|
|
@@ -2816,11 +3291,33 @@ var PeerBus = class extends EventEmitter {
|
|
|
2816
3291
|
this.waiters.delete(subtaskId);
|
|
2817
3292
|
}
|
|
2818
3293
|
/**
|
|
2819
|
-
*
|
|
3294
|
+
* Mark a subtask as retry-pending so dependents re-wait instead of failing fast
|
|
3295
|
+
* when they see an ESCALATED status.
|
|
3296
|
+
*/
|
|
3297
|
+
markRetryPending(subtaskId) {
|
|
3298
|
+
this.retryPending.add(subtaskId);
|
|
3299
|
+
this.outputs.delete(subtaskId);
|
|
3300
|
+
}
|
|
3301
|
+
/** Called by T2 after retry resolves (success or final failure). */
|
|
3302
|
+
clearRetryPending(subtaskId) {
|
|
3303
|
+
this.retryPending.delete(subtaskId);
|
|
3304
|
+
}
|
|
3305
|
+
/** Remove a single output entry so a respawned worker can republish without clearing prior-wave outputs. */
|
|
3306
|
+
clearOutput(subtaskId) {
|
|
3307
|
+
this.outputs.delete(subtaskId);
|
|
3308
|
+
this.waiters.delete(subtaskId);
|
|
3309
|
+
this.retryPending.delete(subtaskId);
|
|
3310
|
+
}
|
|
3311
|
+
isRetryPending(subtaskId) {
|
|
3312
|
+
return this.retryPending.has(subtaskId);
|
|
3313
|
+
}
|
|
3314
|
+
/**
|
|
3315
|
+
* Wait for a specific subtask's output — resolves immediately if already available.
|
|
3316
|
+
* If the output is ESCALATED but a retry is pending, waits for the retry result.
|
|
2820
3317
|
*/
|
|
2821
3318
|
waitFor(subtaskId, timeoutMs = 12e4) {
|
|
2822
3319
|
const existing = this.outputs.get(subtaskId);
|
|
2823
|
-
if (existing) return Promise.resolve(existing);
|
|
3320
|
+
if (existing && !this.retryPending.has(subtaskId)) return Promise.resolve(existing);
|
|
2824
3321
|
return new Promise((resolve, reject) => {
|
|
2825
3322
|
const resolver = (output) => {
|
|
2826
3323
|
clearTimeout(timer);
|
|
@@ -2851,6 +3348,7 @@ var PeerBus = class extends EventEmitter {
|
|
|
2851
3348
|
* Also logs to broadcastLog so collect() can retrieve recent broadcasts.
|
|
2852
3349
|
*/
|
|
2853
3350
|
broadcast(fromId, payload) {
|
|
3351
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
2854
3352
|
const msg = {
|
|
2855
3353
|
fromId,
|
|
2856
3354
|
toId: "*",
|
|
@@ -2858,10 +3356,18 @@ var PeerBus = class extends EventEmitter {
|
|
|
2858
3356
|
subtaskId: "",
|
|
2859
3357
|
syncType: "SHARE_OUTPUT",
|
|
2860
3358
|
payload,
|
|
2861
|
-
timestamp
|
|
3359
|
+
timestamp
|
|
2862
3360
|
};
|
|
2863
|
-
this.broadcastLog.push({ fromId, payload, timestamp
|
|
3361
|
+
this.broadcastLog.push({ fromId, payload, timestamp });
|
|
2864
3362
|
this.emit("broadcast", msg);
|
|
3363
|
+
this.onPeerMessage?.({
|
|
3364
|
+
fromId,
|
|
3365
|
+
toId: void 0,
|
|
3366
|
+
syncType: "SHARE_OUTPUT",
|
|
3367
|
+
payload: typeof payload === "string" ? payload : JSON.stringify(payload),
|
|
3368
|
+
timestamp,
|
|
3369
|
+
sessionId: this.sessionId
|
|
3370
|
+
});
|
|
2865
3371
|
}
|
|
2866
3372
|
/**
|
|
2867
3373
|
* Collect all broadcast messages received within a time window.
|
|
@@ -2947,6 +3453,16 @@ var PeerBus = class extends EventEmitter {
|
|
|
2947
3453
|
isFileLocked(filePath) {
|
|
2948
3454
|
return this.fileLocks.has(filePath);
|
|
2949
3455
|
}
|
|
3456
|
+
/**
|
|
3457
|
+
* Reset all runtime output/waiter state for a fresh T3 respawn wave.
|
|
3458
|
+
* Preserves member registrations and barrier definitions.
|
|
3459
|
+
*/
|
|
3460
|
+
reset() {
|
|
3461
|
+
this.outputs.clear();
|
|
3462
|
+
this.waiters.clear();
|
|
3463
|
+
this.retryPending.clear();
|
|
3464
|
+
this.broadcastLog = [];
|
|
3465
|
+
}
|
|
2950
3466
|
/**
|
|
2951
3467
|
* Clear broadcast log — call between phases to avoid stale announcements.
|
|
2952
3468
|
*/
|
|
@@ -2957,6 +3473,7 @@ var PeerBus = class extends EventEmitter {
|
|
|
2957
3473
|
* Send a targeted message to a specific peer
|
|
2958
3474
|
*/
|
|
2959
3475
|
send(fromId, toId, syncType, subtaskId, payload) {
|
|
3476
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
2960
3477
|
const msg = {
|
|
2961
3478
|
fromId,
|
|
2962
3479
|
toId,
|
|
@@ -2964,10 +3481,18 @@ var PeerBus = class extends EventEmitter {
|
|
|
2964
3481
|
subtaskId,
|
|
2965
3482
|
syncType,
|
|
2966
3483
|
payload,
|
|
2967
|
-
timestamp
|
|
3484
|
+
timestamp
|
|
2968
3485
|
};
|
|
2969
3486
|
this.emit(`message:${toId}`, msg);
|
|
2970
3487
|
this.emit("message", msg);
|
|
3488
|
+
this.onPeerMessage?.({
|
|
3489
|
+
fromId,
|
|
3490
|
+
toId,
|
|
3491
|
+
syncType: syncType ?? "SHARE_OUTPUT",
|
|
3492
|
+
payload: typeof payload === "string" ? payload : JSON.stringify(payload),
|
|
3493
|
+
timestamp,
|
|
3494
|
+
sessionId: this.sessionId
|
|
3495
|
+
});
|
|
2971
3496
|
}
|
|
2972
3497
|
/**
|
|
2973
3498
|
* Barrier — wait until N peers have all reached this point
|
|
@@ -3020,6 +3545,8 @@ var T2Manager = class extends BaseTier {
|
|
|
3020
3545
|
t2PeerBus;
|
|
3021
3546
|
permissionEscalator;
|
|
3022
3547
|
toolCreator;
|
|
3548
|
+
/** AbortController for the current T3 wave — aborted on cancel-and-respawn */
|
|
3549
|
+
waveAbortController = null;
|
|
3023
3550
|
setPeerBus(bus) {
|
|
3024
3551
|
this.t2PeerBus = bus;
|
|
3025
3552
|
this.t2PeerBus.register(this.id);
|
|
@@ -3028,6 +3555,14 @@ var T2Manager = class extends BaseTier {
|
|
|
3028
3555
|
this.receivePeerSync(msg.fromId, msg.payload);
|
|
3029
3556
|
});
|
|
3030
3557
|
}
|
|
3558
|
+
setPeerMessageCallback(cb, sessionId) {
|
|
3559
|
+
this.t3PeerBus.onPeerMessage = cb;
|
|
3560
|
+
this.t3PeerBus.sessionId = sessionId;
|
|
3561
|
+
if (this.t2PeerBus) {
|
|
3562
|
+
this.t2PeerBus.onPeerMessage = cb;
|
|
3563
|
+
this.t2PeerBus.sessionId = sessionId;
|
|
3564
|
+
}
|
|
3565
|
+
}
|
|
3031
3566
|
constructor(router, toolRegistry, parentId) {
|
|
3032
3567
|
super("T2", void 0, parentId);
|
|
3033
3568
|
this.router = router;
|
|
@@ -3195,6 +3730,26 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
|
3195
3730
|
}];
|
|
3196
3731
|
}
|
|
3197
3732
|
}
|
|
3733
|
+
buildWorkerMap(assignments, taskId) {
|
|
3734
|
+
const workerMap = /* @__PURE__ */ new Map();
|
|
3735
|
+
for (const a of assignments) {
|
|
3736
|
+
const worker = new T3Worker(this.router, this.toolRegistry, this.id);
|
|
3737
|
+
if (this.store) worker.setStore(this.store, taskId);
|
|
3738
|
+
worker.setPeerBus(this.t3PeerBus);
|
|
3739
|
+
if (this.permissionEscalator) worker.setPermissionEscalator(this.permissionEscalator);
|
|
3740
|
+
if (this.toolCreator) worker.setToolCreator(this.toolCreator);
|
|
3741
|
+
workerMap.set(a.subtaskId, worker);
|
|
3742
|
+
this.t3Workers.set(a.subtaskId, worker);
|
|
3743
|
+
worker.on("stream:token", (e) => this.emit("stream:token", e));
|
|
3744
|
+
worker.on("log", (e) => this.emit("log", e));
|
|
3745
|
+
worker.on("tier:status", (e) => this.emit("tier:status", e));
|
|
3746
|
+
worker.on("tool:approval-request", (e) => this.emit("tool:approval-request", {
|
|
3747
|
+
...e,
|
|
3748
|
+
__cascadeResponder: (decision) => worker.emit(`tool:approval-response:${e.id}`, decision)
|
|
3749
|
+
}));
|
|
3750
|
+
}
|
|
3751
|
+
return workerMap;
|
|
3752
|
+
}
|
|
3198
3753
|
async executeSubtasks(subtasks, taskId) {
|
|
3199
3754
|
const assignments = subtasks.map((s) => ({
|
|
3200
3755
|
...s,
|
|
@@ -3221,6 +3776,8 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
|
3221
3776
|
worker.on("stream:token", (e) => this.emit("stream:token", e));
|
|
3222
3777
|
worker.on("log", (e) => this.emit("log", e));
|
|
3223
3778
|
worker.on("tier:status", (e) => this.emit("tier:status", e));
|
|
3779
|
+
worker.on("tool:call", (e) => this.emit("tool:call", e));
|
|
3780
|
+
worker.on("tool:result", (e) => this.emit("tool:result", e));
|
|
3224
3781
|
worker.on("tool:approval-request", (e) => this.emit("tool:approval-request", {
|
|
3225
3782
|
...e,
|
|
3226
3783
|
__cascadeResponder: (decision) => worker.emit(`tool:approval-response:${e.id}`, decision)
|
|
@@ -3257,6 +3814,7 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
|
3257
3814
|
const sanitizedAssignments = this.breakCycles(assignments, adj, inDegree);
|
|
3258
3815
|
let remaining = new Set(sanitizedAssignments.map((a) => a.subtaskId));
|
|
3259
3816
|
let wave = 0;
|
|
3817
|
+
let respawnBudget = 1;
|
|
3260
3818
|
while (remaining.size > 0) {
|
|
3261
3819
|
const runnableIds = [...remaining].filter((id) => (inDegree.get(id) ?? 0) === 0);
|
|
3262
3820
|
if (runnableIds.length === 0) {
|
|
@@ -3277,15 +3835,62 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
|
3277
3835
|
status: "IN_PROGRESS"
|
|
3278
3836
|
});
|
|
3279
3837
|
this.throwIfCancelled();
|
|
3838
|
+
this.waveAbortController = new AbortController();
|
|
3839
|
+
const waveSignal = AbortSignal.any(
|
|
3840
|
+
[this.signal, this.waveAbortController.signal].filter(Boolean)
|
|
3841
|
+
);
|
|
3280
3842
|
const waveResults = await Promise.allSettled(
|
|
3281
3843
|
runnableIds.map(async (id) => {
|
|
3282
3844
|
const assignment = sanitizedAssignments.find((a) => a.subtaskId === id);
|
|
3283
3845
|
const worker = workerMap.get(id);
|
|
3284
|
-
const result = await worker.execute(assignment, taskId,
|
|
3846
|
+
const result = await worker.execute(assignment, taskId, waveSignal);
|
|
3285
3847
|
resultMap.set(id, result);
|
|
3286
3848
|
return result;
|
|
3287
3849
|
})
|
|
3288
3850
|
);
|
|
3851
|
+
const escalatedToolIdx = respawnBudget > 0 ? waveResults.findIndex(
|
|
3852
|
+
(r) => r.status === "fulfilled" && r.value.status === "ESCALATED" && r.value.issues.some((iss) => iss.includes("dynamic tool generation"))
|
|
3853
|
+
) : -1;
|
|
3854
|
+
if (escalatedToolIdx !== -1 && this.toolCreator) {
|
|
3855
|
+
respawnBudget--;
|
|
3856
|
+
this.waveAbortController.abort();
|
|
3857
|
+
const escalatedId = runnableIds[escalatedToolIdx];
|
|
3858
|
+
const escalatedAssignment = sanitizedAssignments.find((a) => a.subtaskId === escalatedId);
|
|
3859
|
+
this.log(`Wave ${wave}: tool escalation detected \u2014 synthesizing tool then respawning all ${runnableIds.length} worker(s)`);
|
|
3860
|
+
this.sendStatusUpdate({
|
|
3861
|
+
progressPct: 50,
|
|
3862
|
+
currentAction: `Synthesizing dynamic tool for: ${escalatedAssignment.subtaskTitle}`,
|
|
3863
|
+
status: "IN_PROGRESS"
|
|
3864
|
+
});
|
|
3865
|
+
const toolName = await this.toolCreator.createTool(
|
|
3866
|
+
`Help complete: ${escalatedAssignment.subtaskTitle}`,
|
|
3867
|
+
escalatedAssignment.description
|
|
3868
|
+
);
|
|
3869
|
+
if (toolName) {
|
|
3870
|
+
this.log(`Tool "${toolName}" created \u2014 respawning wave ${wave} workers`);
|
|
3871
|
+
for (const a of sanitizedAssignments) {
|
|
3872
|
+
if (runnableIds.includes(a.subtaskId)) {
|
|
3873
|
+
a.description += `
|
|
3874
|
+
|
|
3875
|
+
[SYSTEM]: Dynamic tool "${toolName}" is now available \u2014 use it to complete your task.`;
|
|
3876
|
+
}
|
|
3877
|
+
}
|
|
3878
|
+
}
|
|
3879
|
+
for (const id of runnableIds) {
|
|
3880
|
+
this.t3PeerBus.clearOutput(id);
|
|
3881
|
+
}
|
|
3882
|
+
const freshMap = this.buildWorkerMap(
|
|
3883
|
+
sanitizedAssignments.filter((a) => runnableIds.includes(a.subtaskId)),
|
|
3884
|
+
taskId
|
|
3885
|
+
);
|
|
3886
|
+
for (const [k, v] of freshMap) workerMap.set(k, v);
|
|
3887
|
+
for (const id of runnableIds) {
|
|
3888
|
+
remaining.add(id);
|
|
3889
|
+
inDegree.set(id, 0);
|
|
3890
|
+
}
|
|
3891
|
+
wave--;
|
|
3892
|
+
continue;
|
|
3893
|
+
}
|
|
3289
3894
|
for (let i = 0; i < runnableIds.length; i++) {
|
|
3290
3895
|
const id = runnableIds[i];
|
|
3291
3896
|
remaining.delete(id);
|
|
@@ -3293,61 +3898,22 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
|
3293
3898
|
if (r.status === "rejected") {
|
|
3294
3899
|
this.log(`T3 worker ${id} failed: ${r.reason instanceof Error ? r.reason.message : String(r.reason)} \u2014 retrying once`);
|
|
3295
3900
|
const assignment = sanitizedAssignments.find((a) => a.subtaskId === id);
|
|
3296
|
-
|
|
3297
|
-
|
|
3298
|
-
|
|
3299
|
-
|
|
3300
|
-
|
|
3301
|
-
this.log(`T3
|
|
3302
|
-
this.
|
|
3303
|
-
|
|
3304
|
-
|
|
3305
|
-
status: "
|
|
3901
|
+
try {
|
|
3902
|
+
const retried = await this.retryT3(assignment, taskId);
|
|
3903
|
+
resultMap.set(id, retried);
|
|
3904
|
+
} catch (retryErr) {
|
|
3905
|
+
const msg = retryErr instanceof Error ? retryErr.message : String(retryErr);
|
|
3906
|
+
this.log(`T3 retry for ${id} threw before publishing \u2014 unblocking dependents with FAILED`);
|
|
3907
|
+
this.t3PeerBus.publish(this.id, id, `Retry failed: ${msg}`, "FAILED");
|
|
3908
|
+
resultMap.set(id, {
|
|
3909
|
+
subtaskId: id,
|
|
3910
|
+
status: "FAILED",
|
|
3911
|
+
output: `Retry threw: ${msg}`,
|
|
3912
|
+
testResults: { checksRun: [], passed: [], failed: [] },
|
|
3913
|
+
issues: [msg],
|
|
3914
|
+
peerSyncsUsed: [],
|
|
3915
|
+
correctionAttempts: 1
|
|
3306
3916
|
});
|
|
3307
|
-
const toolName = await this.toolCreator.createTool(
|
|
3308
|
-
`Help complete: ${assignment.subtaskTitle}`,
|
|
3309
|
-
assignment.description
|
|
3310
|
-
);
|
|
3311
|
-
if (toolName) {
|
|
3312
|
-
this.log(`T2 verifying new tool: ${toolName}`);
|
|
3313
|
-
this.sendStatusUpdate({
|
|
3314
|
-
progressPct: 60,
|
|
3315
|
-
currentAction: `T2 Verifying new tool: ${toolName}`,
|
|
3316
|
-
status: "IN_PROGRESS"
|
|
3317
|
-
});
|
|
3318
|
-
try {
|
|
3319
|
-
const verifyResult = await this.router.generate("T2", {
|
|
3320
|
-
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".` }],
|
|
3321
|
-
systemPrompt: this.systemPromptOverride + "You are T2 Manager verifying a dynamic tool.",
|
|
3322
|
-
maxTokens: 50
|
|
3323
|
-
});
|
|
3324
|
-
if (!verifyResult.content.toUpperCase().includes("REJECTED")) {
|
|
3325
|
-
this.log(`T2 verification passed for ${toolName}. Restarting original T3.`);
|
|
3326
|
-
const retried = await this.retryT3({
|
|
3327
|
-
...assignment,
|
|
3328
|
-
description: `${assignment.description}
|
|
3329
|
-
|
|
3330
|
-
[SYSTEM NOTIFICATION]: A new dynamic tool "${toolName}" has been built and verified for you. Use it to complete your task.`
|
|
3331
|
-
}, taskId);
|
|
3332
|
-
resultMap.set(id, retried);
|
|
3333
|
-
} else {
|
|
3334
|
-
this.log(`T2 rejected the dynamic tool: ${toolName}`);
|
|
3335
|
-
resultMap.set(id, r.value);
|
|
3336
|
-
}
|
|
3337
|
-
} catch {
|
|
3338
|
-
const retried = await this.retryT3({
|
|
3339
|
-
...assignment,
|
|
3340
|
-
description: `${assignment.description}
|
|
3341
|
-
|
|
3342
|
-
[SYSTEM NOTIFICATION]: A new dynamic tool "${toolName}" has been built for you. Use it to complete your task.`
|
|
3343
|
-
}, taskId);
|
|
3344
|
-
resultMap.set(id, retried);
|
|
3345
|
-
}
|
|
3346
|
-
} else {
|
|
3347
|
-
resultMap.set(id, r.value);
|
|
3348
|
-
}
|
|
3349
|
-
} else {
|
|
3350
|
-
resultMap.set(id, r.value);
|
|
3351
3917
|
}
|
|
3352
3918
|
}
|
|
3353
3919
|
for (const dependent of adj.get(id) ?? []) {
|
|
@@ -3618,6 +4184,8 @@ var T1Administrator = class extends BaseTier {
|
|
|
3618
4184
|
toolCreator;
|
|
3619
4185
|
/** Stored overall task goal — used when evaluating escalated permissions */
|
|
3620
4186
|
taskGoal = "";
|
|
4187
|
+
peerMessageCallback;
|
|
4188
|
+
peerMessageSessionId = "";
|
|
3621
4189
|
constructor(router, toolRegistry, config) {
|
|
3622
4190
|
super("T1", "T1");
|
|
3623
4191
|
this.router = router;
|
|
@@ -3638,6 +4206,12 @@ var T1Administrator = class extends BaseTier {
|
|
|
3638
4206
|
setToolCreator(creator) {
|
|
3639
4207
|
this.toolCreator = creator;
|
|
3640
4208
|
}
|
|
4209
|
+
setPeerMessageCallback(cb, sessionId) {
|
|
4210
|
+
this.peerMessageCallback = cb;
|
|
4211
|
+
this.peerMessageSessionId = sessionId;
|
|
4212
|
+
this.t2PeerBus.onPeerMessage = cb;
|
|
4213
|
+
this.t2PeerBus.sessionId = sessionId;
|
|
4214
|
+
}
|
|
3641
4215
|
async execute(userPrompt, images, systemContext, signal) {
|
|
3642
4216
|
this.signal = signal;
|
|
3643
4217
|
this.taskId = randomUUID();
|
|
@@ -3857,6 +4431,9 @@ Leave dependsOn empty for sections that can run immediately in parallel.`;
|
|
|
3857
4431
|
manager.setStore(this.store);
|
|
3858
4432
|
}
|
|
3859
4433
|
manager.setPeerBus(this.t2PeerBus);
|
|
4434
|
+
if (this.peerMessageCallback) {
|
|
4435
|
+
manager.setPeerMessageCallback(this.peerMessageCallback, this.peerMessageSessionId);
|
|
4436
|
+
}
|
|
3860
4437
|
if (this.permissionEscalator) {
|
|
3861
4438
|
manager.setPermissionEscalator(this.permissionEscalator);
|
|
3862
4439
|
}
|
|
@@ -3867,6 +4444,8 @@ Leave dependsOn empty for sections that can run immediately in parallel.`;
|
|
|
3867
4444
|
bind(manager, "stream:token", (e) => this.emit("stream:token", e));
|
|
3868
4445
|
bind(manager, "log", (e) => this.emit("log", e));
|
|
3869
4446
|
bind(manager, "tier:status", (e) => this.emit("tier:status", e));
|
|
4447
|
+
bind(manager, "tool:call", (e) => this.emit("tool:call", e));
|
|
4448
|
+
bind(manager, "tool:result", (e) => this.emit("tool:result", e));
|
|
3870
4449
|
bind(manager, "tool:approval-request", (e) => this.emit("tool:approval-request", e));
|
|
3871
4450
|
bind(manager, "message", (msg) => {
|
|
3872
4451
|
if (msg.type === "PEER_SYNC") {
|
|
@@ -4226,13 +4805,21 @@ function resolveInWorkspace(workspaceRoot, input) {
|
|
|
4226
4805
|
if (typeof input !== "string" || input.length === 0) {
|
|
4227
4806
|
throw new WorkspaceSandboxError(String(input), workspaceRoot);
|
|
4228
4807
|
}
|
|
4229
|
-
const root =
|
|
4230
|
-
const abs =
|
|
4231
|
-
const rel =
|
|
4232
|
-
if (rel === "" || rel === ".")
|
|
4233
|
-
if (rel.startsWith("..") || path13.isAbsolute(rel)) {
|
|
4808
|
+
const root = path16.resolve(workspaceRoot);
|
|
4809
|
+
const abs = path16.isAbsolute(input) ? path16.resolve(input) : path16.resolve(root, input);
|
|
4810
|
+
const rel = path16.relative(root, abs);
|
|
4811
|
+
if (rel === "" || rel === ".") ; else if (rel.startsWith("..") || path16.isAbsolute(rel)) {
|
|
4234
4812
|
throw new WorkspaceSandboxError(input, root);
|
|
4235
4813
|
}
|
|
4814
|
+
try {
|
|
4815
|
+
const real = fs15.realpathSync(abs);
|
|
4816
|
+
const realRel = path16.relative(root, real);
|
|
4817
|
+
if (realRel !== "" && realRel !== "." && (realRel.startsWith("..") || path16.isAbsolute(realRel))) {
|
|
4818
|
+
throw new WorkspaceSandboxError(input, root);
|
|
4819
|
+
}
|
|
4820
|
+
} catch (e) {
|
|
4821
|
+
if (e instanceof WorkspaceSandboxError) throw e;
|
|
4822
|
+
}
|
|
4236
4823
|
return abs;
|
|
4237
4824
|
}
|
|
4238
4825
|
|
|
@@ -4254,7 +4841,7 @@ var FileReadTool = class extends BaseTool {
|
|
|
4254
4841
|
const absPath = resolveInWorkspace(this.workspaceRoot, filePath);
|
|
4255
4842
|
const offset = input["offset"] ?? 1;
|
|
4256
4843
|
const limit = input["limit"];
|
|
4257
|
-
const content = await
|
|
4844
|
+
const content = await fs3.readFile(absPath, "utf-8");
|
|
4258
4845
|
const lines = content.split("\n");
|
|
4259
4846
|
const start = Math.max(0, offset - 1);
|
|
4260
4847
|
const end = limit ? start + limit : lines.length;
|
|
@@ -4283,13 +4870,13 @@ var FileWriteTool = class extends BaseTool {
|
|
|
4283
4870
|
const content = input["content"];
|
|
4284
4871
|
if (options.saveSnapshot) {
|
|
4285
4872
|
try {
|
|
4286
|
-
const oldContent = await
|
|
4873
|
+
const oldContent = await fs3.readFile(absPath, "utf-8");
|
|
4287
4874
|
await options.saveSnapshot(absPath, oldContent);
|
|
4288
4875
|
} catch {
|
|
4289
4876
|
}
|
|
4290
4877
|
}
|
|
4291
|
-
await
|
|
4292
|
-
await
|
|
4878
|
+
await fs3.mkdir(path16.dirname(absPath), { recursive: true });
|
|
4879
|
+
await fs3.writeFile(absPath, content, "utf-8");
|
|
4293
4880
|
return `Written ${content.length} characters to ${filePath}`;
|
|
4294
4881
|
}
|
|
4295
4882
|
};
|
|
@@ -4315,7 +4902,7 @@ var FileEditTool = class extends BaseTool {
|
|
|
4315
4902
|
const oldString = input["old_string"];
|
|
4316
4903
|
const newString = input["new_string"];
|
|
4317
4904
|
const replaceAll = input["replace_all"] ?? false;
|
|
4318
|
-
const rawContent = await
|
|
4905
|
+
const rawContent = await fs3.readFile(absPath, "utf-8");
|
|
4319
4906
|
if (options.saveSnapshot) {
|
|
4320
4907
|
await options.saveSnapshot(absPath, rawContent);
|
|
4321
4908
|
}
|
|
@@ -4327,7 +4914,7 @@ var FileEditTool = class extends BaseTool {
|
|
|
4327
4914
|
);
|
|
4328
4915
|
}
|
|
4329
4916
|
const updated = replaceAll ? content.split(normalizedOld).join(newString) : content.replace(normalizedOld, newString);
|
|
4330
|
-
await
|
|
4917
|
+
await fs3.writeFile(absPath, updated, "utf-8");
|
|
4331
4918
|
const count = replaceAll ? content.split(normalizedOld).length - 1 : 1;
|
|
4332
4919
|
return `Replaced ${count} occurrence(s) in ${filePath}`;
|
|
4333
4920
|
}
|
|
@@ -4350,12 +4937,12 @@ var FileDeleteTool = class extends BaseTool {
|
|
|
4350
4937
|
const absPath = resolveInWorkspace(this.workspaceRoot, filePath);
|
|
4351
4938
|
if (options.saveSnapshot) {
|
|
4352
4939
|
try {
|
|
4353
|
-
const oldContent = await
|
|
4940
|
+
const oldContent = await fs3.readFile(absPath, "utf-8");
|
|
4354
4941
|
await options.saveSnapshot(absPath, oldContent);
|
|
4355
4942
|
} catch {
|
|
4356
4943
|
}
|
|
4357
4944
|
}
|
|
4358
|
-
await
|
|
4945
|
+
await fs3.rm(absPath, { recursive: false });
|
|
4359
4946
|
return `Deleted ${filePath}`;
|
|
4360
4947
|
}
|
|
4361
4948
|
};
|
|
@@ -4372,7 +4959,7 @@ var FileListTool = class extends BaseTool {
|
|
|
4372
4959
|
async execute(input, _options) {
|
|
4373
4960
|
const inputPath = input["path"] || ".";
|
|
4374
4961
|
const absPath = resolveInWorkspace(this.workspaceRoot, inputPath);
|
|
4375
|
-
const entries = await
|
|
4962
|
+
const entries = await fs3.readdir(absPath, { withFileTypes: true });
|
|
4376
4963
|
return entries.map((e) => `${e.isDirectory() ? "[DIR] " : " "}${e.name}`).join("\n") || "(empty directory)";
|
|
4377
4964
|
}
|
|
4378
4965
|
};
|
|
@@ -4755,8 +5342,8 @@ var ImageAnalyzeTool = class extends BaseTool {
|
|
|
4755
5342
|
}
|
|
4756
5343
|
};
|
|
4757
5344
|
async function fileToImageAttachment(filePath) {
|
|
4758
|
-
const data = await
|
|
4759
|
-
const ext =
|
|
5345
|
+
const data = await fs3.readFile(filePath);
|
|
5346
|
+
const ext = path16.extname(filePath).toLowerCase();
|
|
4760
5347
|
const mimeMap = {
|
|
4761
5348
|
".jpg": "image/jpeg",
|
|
4762
5349
|
".jpeg": "image/jpeg",
|
|
@@ -4790,14 +5377,14 @@ var PDFCreateTool = class extends BaseTool {
|
|
|
4790
5377
|
const filePath = input["path"];
|
|
4791
5378
|
const content = input["content"];
|
|
4792
5379
|
const title = input["title"];
|
|
4793
|
-
const dir =
|
|
4794
|
-
if (!
|
|
4795
|
-
|
|
5380
|
+
const dir = path16.dirname(filePath);
|
|
5381
|
+
if (!fs15.existsSync(dir)) {
|
|
5382
|
+
fs15.mkdirSync(dir, { recursive: true });
|
|
4796
5383
|
}
|
|
4797
5384
|
return new Promise((resolve, reject) => {
|
|
4798
5385
|
try {
|
|
4799
5386
|
const doc = new PDFDocument({ margin: 50 });
|
|
4800
|
-
const stream =
|
|
5387
|
+
const stream = fs15.createWriteStream(filePath);
|
|
4801
5388
|
doc.pipe(stream);
|
|
4802
5389
|
if (title) {
|
|
4803
5390
|
doc.info["Title"] = title;
|
|
@@ -4875,14 +5462,14 @@ var CodeInterpreterTool = class extends BaseTool {
|
|
|
4875
5462
|
}
|
|
4876
5463
|
cmdPrefix = NODE_CMD;
|
|
4877
5464
|
}
|
|
4878
|
-
const tmpDir =
|
|
4879
|
-
if (!
|
|
4880
|
-
|
|
5465
|
+
const tmpDir = path16.join(process.cwd(), ".cascade", "tmp");
|
|
5466
|
+
if (!fs15.existsSync(tmpDir)) {
|
|
5467
|
+
fs15.mkdirSync(tmpDir, { recursive: true });
|
|
4881
5468
|
}
|
|
4882
5469
|
const extension = language === "python" ? "py" : "js";
|
|
4883
5470
|
const fileName = `intp_${randomUUID().slice(0, 8)}.${extension}`;
|
|
4884
|
-
const filePath =
|
|
4885
|
-
|
|
5471
|
+
const filePath = path16.join(tmpDir, fileName);
|
|
5472
|
+
fs15.writeFileSync(filePath, code, "utf-8");
|
|
4886
5473
|
const quotedPath = `"${filePath}"`;
|
|
4887
5474
|
const quotedArgs = args.map((a) => `"${a}"`).join(" ");
|
|
4888
5475
|
const fullCmd = `${cmdPrefix} ${quotedPath}${quotedArgs ? " " + quotedArgs : ""}`;
|
|
@@ -4891,8 +5478,8 @@ var CodeInterpreterTool = class extends BaseTool {
|
|
|
4891
5478
|
exec(fullCmd, { cwd: process.cwd(), timeout: 3e4 }, (error, stdout, stderr) => {
|
|
4892
5479
|
const duration = Date.now() - startMs;
|
|
4893
5480
|
try {
|
|
4894
|
-
if (
|
|
4895
|
-
|
|
5481
|
+
if (fs15.existsSync(filePath)) {
|
|
5482
|
+
fs15.unlinkSync(filePath);
|
|
4896
5483
|
}
|
|
4897
5484
|
} catch (cleanupErr) {
|
|
4898
5485
|
console.error(`Failed to cleanup interpreter script ${filePath}:`, cleanupErr);
|
|
@@ -5152,6 +5739,253 @@ var WebSearchTool = class extends BaseTool {
|
|
|
5152
5739
|
return lines.join("\n");
|
|
5153
5740
|
}
|
|
5154
5741
|
};
|
|
5742
|
+
var GlobTool = class extends BaseTool {
|
|
5743
|
+
name = "glob";
|
|
5744
|
+
description = "Fast file pattern matching. Returns file paths matching a glob pattern, sorted by modification time. Use this to find files by name patterns.";
|
|
5745
|
+
inputSchema = {
|
|
5746
|
+
type: "object",
|
|
5747
|
+
properties: {
|
|
5748
|
+
pattern: {
|
|
5749
|
+
type: "string",
|
|
5750
|
+
description: 'Glob pattern to match files against, e.g. "**/*.ts", "src/**/*.tsx"'
|
|
5751
|
+
},
|
|
5752
|
+
path: {
|
|
5753
|
+
type: "string",
|
|
5754
|
+
description: "Directory to search in. Defaults to the workspace root."
|
|
5755
|
+
}
|
|
5756
|
+
},
|
|
5757
|
+
required: ["pattern"]
|
|
5758
|
+
};
|
|
5759
|
+
async execute(input, _options) {
|
|
5760
|
+
const pattern = input["pattern"];
|
|
5761
|
+
const searchPath = input["path"] ? path16.resolve(this.workspaceRoot, input["path"]) : this.workspaceRoot;
|
|
5762
|
+
const matches = await glob(pattern, {
|
|
5763
|
+
cwd: searchPath,
|
|
5764
|
+
ignore: ["node_modules/**", ".git/**", "dist/**", "build/**"],
|
|
5765
|
+
nodir: true,
|
|
5766
|
+
dot: false
|
|
5767
|
+
});
|
|
5768
|
+
if (matches.length === 0) {
|
|
5769
|
+
return `No files matched pattern: ${pattern}`;
|
|
5770
|
+
}
|
|
5771
|
+
const withMtime = await Promise.all(
|
|
5772
|
+
matches.map(async (rel) => {
|
|
5773
|
+
try {
|
|
5774
|
+
const stat = await fs3.stat(path16.join(searchPath, rel));
|
|
5775
|
+
return { rel, mtime: stat.mtimeMs };
|
|
5776
|
+
} catch {
|
|
5777
|
+
return { rel, mtime: 0 };
|
|
5778
|
+
}
|
|
5779
|
+
})
|
|
5780
|
+
);
|
|
5781
|
+
withMtime.sort((a, b) => b.mtime - a.mtime);
|
|
5782
|
+
const lines = withMtime.map((f) => f.rel);
|
|
5783
|
+
return lines.join("\n");
|
|
5784
|
+
}
|
|
5785
|
+
};
|
|
5786
|
+
var execFileAsync = promisify(execFile);
|
|
5787
|
+
var GrepTool = class extends BaseTool {
|
|
5788
|
+
name = "grep";
|
|
5789
|
+
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.";
|
|
5790
|
+
inputSchema = {
|
|
5791
|
+
type: "object",
|
|
5792
|
+
properties: {
|
|
5793
|
+
pattern: {
|
|
5794
|
+
type: "string",
|
|
5795
|
+
description: "Regular expression pattern to search for in file contents"
|
|
5796
|
+
},
|
|
5797
|
+
path: {
|
|
5798
|
+
type: "string",
|
|
5799
|
+
description: "File or directory to search in. Defaults to workspace root."
|
|
5800
|
+
},
|
|
5801
|
+
glob: {
|
|
5802
|
+
type: "string",
|
|
5803
|
+
description: 'Glob pattern to filter files, e.g. "*.ts", "**/*.tsx"'
|
|
5804
|
+
},
|
|
5805
|
+
output_mode: {
|
|
5806
|
+
type: "string",
|
|
5807
|
+
enum: ["content", "files_with_matches", "count"],
|
|
5808
|
+
description: '"content" shows matching lines (default), "files_with_matches" shows file paths only, "count" shows match counts'
|
|
5809
|
+
},
|
|
5810
|
+
context: {
|
|
5811
|
+
type: "number",
|
|
5812
|
+
description: "Lines of context around each match (content mode only). Default: 0."
|
|
5813
|
+
},
|
|
5814
|
+
case_insensitive: {
|
|
5815
|
+
type: "boolean",
|
|
5816
|
+
description: "Case-insensitive search. Default: false."
|
|
5817
|
+
}
|
|
5818
|
+
},
|
|
5819
|
+
required: ["pattern"]
|
|
5820
|
+
};
|
|
5821
|
+
async execute(input, _options) {
|
|
5822
|
+
const pattern = input["pattern"];
|
|
5823
|
+
const searchPath = input["path"] ? path16.resolve(this.workspaceRoot, input["path"]) : this.workspaceRoot;
|
|
5824
|
+
const globPattern = input["glob"];
|
|
5825
|
+
const outputMode = input["output_mode"] ?? "content";
|
|
5826
|
+
const context = input["context"] ?? 0;
|
|
5827
|
+
const caseInsensitive = input["case_insensitive"] ?? false;
|
|
5828
|
+
try {
|
|
5829
|
+
const result = await this.runRipgrep(
|
|
5830
|
+
pattern,
|
|
5831
|
+
searchPath,
|
|
5832
|
+
globPattern,
|
|
5833
|
+
outputMode,
|
|
5834
|
+
context,
|
|
5835
|
+
caseInsensitive
|
|
5836
|
+
);
|
|
5837
|
+
return result;
|
|
5838
|
+
} catch {
|
|
5839
|
+
}
|
|
5840
|
+
return this.nodeScan(pattern, searchPath, globPattern, outputMode, context, caseInsensitive);
|
|
5841
|
+
}
|
|
5842
|
+
async runRipgrep(pattern, searchPath, globPattern, outputMode, context, caseInsensitive) {
|
|
5843
|
+
const args = ["--no-heading"];
|
|
5844
|
+
if (caseInsensitive) args.push("-i");
|
|
5845
|
+
if (outputMode === "files_with_matches") args.push("-l");
|
|
5846
|
+
else if (outputMode === "count") args.push("-c");
|
|
5847
|
+
else {
|
|
5848
|
+
args.push("-n");
|
|
5849
|
+
if (context > 0) args.push(`-C${context}`);
|
|
5850
|
+
}
|
|
5851
|
+
if (globPattern) args.push("--glob", globPattern);
|
|
5852
|
+
args.push("--", pattern, searchPath);
|
|
5853
|
+
const { stdout } = await execFileAsync("rg", args, {
|
|
5854
|
+
timeout: 15e3,
|
|
5855
|
+
maxBuffer: 2 * 1024 * 1024
|
|
5856
|
+
});
|
|
5857
|
+
const trimmed = stdout.trim();
|
|
5858
|
+
return trimmed || `No matches found for: ${pattern}`;
|
|
5859
|
+
}
|
|
5860
|
+
async nodeScan(pattern, searchPath, globPattern, outputMode, context, caseInsensitive) {
|
|
5861
|
+
const flags = caseInsensitive ? "gi" : "g";
|
|
5862
|
+
let regex;
|
|
5863
|
+
try {
|
|
5864
|
+
regex = new RegExp(pattern, flags);
|
|
5865
|
+
} catch {
|
|
5866
|
+
return `Invalid regex pattern: ${pattern}`;
|
|
5867
|
+
}
|
|
5868
|
+
const fileGlob = globPattern ?? "**/*";
|
|
5869
|
+
let files;
|
|
5870
|
+
try {
|
|
5871
|
+
files = await glob(fileGlob, {
|
|
5872
|
+
cwd: searchPath,
|
|
5873
|
+
ignore: ["node_modules/**", ".git/**", "dist/**", "build/**"],
|
|
5874
|
+
nodir: true
|
|
5875
|
+
});
|
|
5876
|
+
} catch {
|
|
5877
|
+
files = [path16.relative(searchPath, searchPath) || "."];
|
|
5878
|
+
}
|
|
5879
|
+
const results = [];
|
|
5880
|
+
let totalCount = 0;
|
|
5881
|
+
for (const rel of files) {
|
|
5882
|
+
const abs = path16.join(searchPath, rel);
|
|
5883
|
+
let content;
|
|
5884
|
+
try {
|
|
5885
|
+
content = await fs3.readFile(abs, "utf-8");
|
|
5886
|
+
} catch {
|
|
5887
|
+
continue;
|
|
5888
|
+
}
|
|
5889
|
+
const lines = content.split("\n");
|
|
5890
|
+
const matchingLines = [];
|
|
5891
|
+
for (let i = 0; i < lines.length; i++) {
|
|
5892
|
+
if (regex.test(lines[i])) matchingLines.push(i);
|
|
5893
|
+
regex.lastIndex = 0;
|
|
5894
|
+
}
|
|
5895
|
+
if (matchingLines.length === 0) continue;
|
|
5896
|
+
totalCount += matchingLines.length;
|
|
5897
|
+
if (outputMode === "files_with_matches") {
|
|
5898
|
+
results.push(rel);
|
|
5899
|
+
} else if (outputMode === "count") {
|
|
5900
|
+
results.push(`${rel}: ${matchingLines.length}`);
|
|
5901
|
+
} else {
|
|
5902
|
+
const shown = /* @__PURE__ */ new Set();
|
|
5903
|
+
for (const lineIdx of matchingLines) {
|
|
5904
|
+
const start = Math.max(0, lineIdx - context);
|
|
5905
|
+
const end = Math.min(lines.length - 1, lineIdx + context);
|
|
5906
|
+
for (let i = start; i <= end; i++) shown.add(i);
|
|
5907
|
+
}
|
|
5908
|
+
const sortedIdxs = [...shown].sort((a, b) => a - b);
|
|
5909
|
+
for (const i of sortedIdxs) {
|
|
5910
|
+
const marker = matchingLines.includes(i) ? ":" : "-";
|
|
5911
|
+
results.push(`${rel}:${i + 1}${marker}${lines[i]}`);
|
|
5912
|
+
}
|
|
5913
|
+
}
|
|
5914
|
+
}
|
|
5915
|
+
if (results.length === 0) return `No matches found for: ${pattern}`;
|
|
5916
|
+
if (outputMode === "count") {
|
|
5917
|
+
results.push(`
|
|
5918
|
+
Total: ${totalCount} matches`);
|
|
5919
|
+
}
|
|
5920
|
+
return results.join("\n");
|
|
5921
|
+
}
|
|
5922
|
+
};
|
|
5923
|
+
|
|
5924
|
+
// src/tools/web-fetch.ts
|
|
5925
|
+
var MAX_CHARS = 5e4;
|
|
5926
|
+
var TIMEOUT_MS = 15e3;
|
|
5927
|
+
function stripHtml(html) {
|
|
5928
|
+
let text = html.replace(/<script[\s\S]*?<\/script>/gi, "").replace(/<style[\s\S]*?<\/style>/gi, "").replace(/<noscript[\s\S]*?<\/noscript>/gi, "");
|
|
5929
|
+
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, " ");
|
|
5930
|
+
text = text.replace(/<[^>]+>/g, "");
|
|
5931
|
+
text = text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'").replace(/ /g, " ").replace(/&#(\d+);/g, (_, n) => String.fromCharCode(Number(n)));
|
|
5932
|
+
text = text.split("\n").map((l) => l.trim()).filter((l) => l.length > 0).join("\n");
|
|
5933
|
+
return text;
|
|
5934
|
+
}
|
|
5935
|
+
var WebFetchTool = class extends BaseTool {
|
|
5936
|
+
name = "web_fetch";
|
|
5937
|
+
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.";
|
|
5938
|
+
inputSchema = {
|
|
5939
|
+
type: "object",
|
|
5940
|
+
properties: {
|
|
5941
|
+
url: {
|
|
5942
|
+
type: "string",
|
|
5943
|
+
description: "The URL to fetch"
|
|
5944
|
+
},
|
|
5945
|
+
prompt: {
|
|
5946
|
+
type: "string",
|
|
5947
|
+
description: "Optional hint for what information to extract from the page (not used for filtering, just context)"
|
|
5948
|
+
}
|
|
5949
|
+
},
|
|
5950
|
+
required: ["url"]
|
|
5951
|
+
};
|
|
5952
|
+
async execute(input, _options) {
|
|
5953
|
+
const url = input["url"];
|
|
5954
|
+
let resp;
|
|
5955
|
+
try {
|
|
5956
|
+
resp = await fetch(url, {
|
|
5957
|
+
headers: {
|
|
5958
|
+
"User-Agent": "Cascade-AI/1.0 WebFetchTool",
|
|
5959
|
+
Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,text/plain;q=0.8,*/*;q=0.5"
|
|
5960
|
+
},
|
|
5961
|
+
signal: AbortSignal.timeout(TIMEOUT_MS),
|
|
5962
|
+
redirect: "follow"
|
|
5963
|
+
});
|
|
5964
|
+
} catch (err) {
|
|
5965
|
+
return `Failed to fetch ${url}: ${err instanceof Error ? err.message : String(err)}`;
|
|
5966
|
+
}
|
|
5967
|
+
if (!resp.ok) {
|
|
5968
|
+
return `HTTP ${resp.status} ${resp.statusText} from ${url}`;
|
|
5969
|
+
}
|
|
5970
|
+
const contentType = resp.headers.get("content-type") ?? "";
|
|
5971
|
+
let text;
|
|
5972
|
+
try {
|
|
5973
|
+
const raw = await resp.text();
|
|
5974
|
+
text = contentType.includes("html") ? stripHtml(raw) : raw;
|
|
5975
|
+
} catch (err) {
|
|
5976
|
+
return `Failed to read response body: ${err instanceof Error ? err.message : String(err)}`;
|
|
5977
|
+
}
|
|
5978
|
+
if (text.length > MAX_CHARS) {
|
|
5979
|
+
text = text.slice(0, MAX_CHARS) + `
|
|
5980
|
+
|
|
5981
|
+
[Content truncated at ${MAX_CHARS} characters]`;
|
|
5982
|
+
}
|
|
5983
|
+
return `URL: ${url}
|
|
5984
|
+
Content-Type: ${contentType}
|
|
5985
|
+
|
|
5986
|
+
${text}`;
|
|
5987
|
+
}
|
|
5988
|
+
};
|
|
5155
5989
|
|
|
5156
5990
|
// src/tools/mcp.ts
|
|
5157
5991
|
var McpToolWrapper = class extends BaseTool {
|
|
@@ -5177,7 +6011,7 @@ var McpToolWrapper = class extends BaseTool {
|
|
|
5177
6011
|
|
|
5178
6012
|
// src/tools/registry.ts
|
|
5179
6013
|
var ignore = ignoreFactory__default.default ?? ignoreFactory__default;
|
|
5180
|
-
var ToolRegistry = class {
|
|
6014
|
+
var ToolRegistry = class extends EventEmitter {
|
|
5181
6015
|
tools = /* @__PURE__ */ new Map();
|
|
5182
6016
|
config;
|
|
5183
6017
|
ignoreMatcher = ignore();
|
|
@@ -5185,12 +6019,36 @@ var ToolRegistry = class {
|
|
|
5185
6019
|
/** Loaded plugins, keyed by plugin name */
|
|
5186
6020
|
plugins = /* @__PURE__ */ new Map();
|
|
5187
6021
|
constructor(config, workspaceRoot = process.cwd()) {
|
|
6022
|
+
super();
|
|
5188
6023
|
this.config = config;
|
|
5189
6024
|
this.workspaceRoot = workspaceRoot;
|
|
5190
6025
|
this.registerDefaults();
|
|
5191
6026
|
}
|
|
5192
6027
|
register(tool) {
|
|
5193
6028
|
this.tools.set(tool.name, tool);
|
|
6029
|
+
this.emit("tool:added", tool.name);
|
|
6030
|
+
}
|
|
6031
|
+
/**
|
|
6032
|
+
* Wait until a named tool is registered, resolving immediately if it already exists.
|
|
6033
|
+
* T3 workers can call this after encountering a missing-tool error to resume
|
|
6034
|
+
* automatically once T2 synthesizes the tool.
|
|
6035
|
+
*/
|
|
6036
|
+
waitForTool(toolName, timeoutMs = 6e4) {
|
|
6037
|
+
if (this.tools.has(toolName)) return Promise.resolve();
|
|
6038
|
+
return new Promise((resolve, reject) => {
|
|
6039
|
+
const timer = setTimeout(() => {
|
|
6040
|
+
this.off("tool:added", handler);
|
|
6041
|
+
reject(new Error(`Timeout waiting for tool: ${toolName}`));
|
|
6042
|
+
}, timeoutMs);
|
|
6043
|
+
const handler = (name) => {
|
|
6044
|
+
if (name === toolName) {
|
|
6045
|
+
clearTimeout(timer);
|
|
6046
|
+
this.off("tool:added", handler);
|
|
6047
|
+
resolve();
|
|
6048
|
+
}
|
|
6049
|
+
};
|
|
6050
|
+
this.on("tool:added", handler);
|
|
6051
|
+
});
|
|
5194
6052
|
}
|
|
5195
6053
|
/**
|
|
5196
6054
|
* Register a ToolPlugin, loading all its tools into the registry.
|
|
@@ -5275,7 +6133,10 @@ var ToolRegistry = class {
|
|
|
5275
6133
|
new PDFCreateTool(),
|
|
5276
6134
|
new CodeInterpreterTool(),
|
|
5277
6135
|
new PeerCommunicationTool(),
|
|
5278
|
-
new WebSearchTool(this.config.webSearch)
|
|
6136
|
+
new WebSearchTool(this.config.webSearch),
|
|
6137
|
+
new GlobTool(),
|
|
6138
|
+
new GrepTool(),
|
|
6139
|
+
new WebFetchTool()
|
|
5279
6140
|
];
|
|
5280
6141
|
for (const tool of tools) {
|
|
5281
6142
|
tool.setWorkspaceRoot(this.workspaceRoot);
|
|
@@ -5292,10 +6153,10 @@ var ToolRegistry = class {
|
|
|
5292
6153
|
}
|
|
5293
6154
|
isIgnored(filePath) {
|
|
5294
6155
|
if (!filePath) return false;
|
|
5295
|
-
const abs =
|
|
5296
|
-
const rel =
|
|
5297
|
-
if (!rel || rel.startsWith("..") ||
|
|
5298
|
-
const posixRel = rel.split(
|
|
6156
|
+
const abs = path16.resolve(this.workspaceRoot, filePath);
|
|
6157
|
+
const rel = path16.relative(this.workspaceRoot, abs);
|
|
6158
|
+
if (!rel || rel.startsWith("..") || path16.isAbsolute(rel)) return true;
|
|
6159
|
+
const posixRel = rel.split(path16.sep).join("/");
|
|
5299
6160
|
return this.ignoreMatcher.ignores(posixRel);
|
|
5300
6161
|
}
|
|
5301
6162
|
};
|
|
@@ -5632,7 +6493,24 @@ var CascadeConfigSchema = z.object({
|
|
|
5632
6493
|
* Generated tools are session-scoped and sandboxed in node:vm.
|
|
5633
6494
|
* HTTP calls from generated tools require approval.
|
|
5634
6495
|
*/
|
|
5635
|
-
enableToolCreation: z.boolean().default(
|
|
6496
|
+
enableToolCreation: z.boolean().default(true),
|
|
6497
|
+
/**
|
|
6498
|
+
* External plugin paths or npm package names to load at startup.
|
|
6499
|
+
* Each entry must export a default ToolPlugin object.
|
|
6500
|
+
* Example: ["./plugins/my-tool.js", "cascade-plugin-slack"]
|
|
6501
|
+
*/
|
|
6502
|
+
plugins: z.array(z.string()).default([]),
|
|
6503
|
+
/**
|
|
6504
|
+
* Maximum number of concurrent inference requests to any local model provider
|
|
6505
|
+
* (e.g. Ollama). Defaults to 1 to prevent GPU memory pressure when multiple
|
|
6506
|
+
* T3 workers run in parallel on a single-GPU machine.
|
|
6507
|
+
*/
|
|
6508
|
+
localConcurrency: z.number().int().min(1).default(1),
|
|
6509
|
+
/**
|
|
6510
|
+
* Timeout in milliseconds for a single local model inference call.
|
|
6511
|
+
* Local models can take minutes for large parameter counts. Default: 5 minutes.
|
|
6512
|
+
*/
|
|
6513
|
+
localInferenceTimeoutMs: z.number().int().min(1e3).default(3e5)
|
|
5636
6514
|
});
|
|
5637
6515
|
|
|
5638
6516
|
// src/config/validate.ts
|
|
@@ -5760,139 +6638,237 @@ function heuristicAnalyze(prompt) {
|
|
|
5760
6638
|
const estimatedTokens = wordCount * 5;
|
|
5761
6639
|
return { type, complexity, requiresReasoning, requiresVision, estimatedTokens, confidence };
|
|
5762
6640
|
}
|
|
5763
|
-
function selectModelFromProfile(profile, tier, selector) {
|
|
5764
|
-
if (profile.requiresVision) {
|
|
5765
|
-
return selector.selectVisionModel();
|
|
5766
|
-
}
|
|
5767
|
-
if (tier === "T1") {
|
|
5768
|
-
if (profile.complexity >= 4) {
|
|
5769
|
-
return selector.selectForTier("T1");
|
|
5770
|
-
} else {
|
|
5771
|
-
return selector.selectForTier("T2");
|
|
5772
|
-
}
|
|
5773
|
-
}
|
|
5774
|
-
if (tier === "T2") {
|
|
5775
|
-
if (profile.type === "code" || profile.type === "data") {
|
|
5776
|
-
return selector.selectForTier("T2");
|
|
5777
|
-
} else if (profile.complexity <= 2) {
|
|
5778
|
-
return selector.selectForTier("T3");
|
|
5779
|
-
}
|
|
5780
|
-
return selector.selectForTier("T2");
|
|
5781
|
-
}
|
|
5782
|
-
if (tier === "T3") {
|
|
5783
|
-
if (profile.complexity >= 4 || profile.requiresReasoning) {
|
|
5784
|
-
return selector.selectForTier("T2");
|
|
5785
|
-
} else if (profile.type === "creative") {
|
|
5786
|
-
return selector.selectForTier("T2");
|
|
5787
|
-
} else {
|
|
5788
|
-
return selector.selectForTier("T3");
|
|
5789
|
-
}
|
|
5790
|
-
}
|
|
5791
|
-
return selector.selectForTier(tier);
|
|
5792
|
-
}
|
|
5793
6641
|
var analysisCache = /* @__PURE__ */ new Map();
|
|
6642
|
+
var TASK_TYPE_TAGS = {
|
|
6643
|
+
code: ["code", "instruction"],
|
|
6644
|
+
analysis: ["analysis", "instruction"],
|
|
6645
|
+
creative: ["creative", "multilingual"],
|
|
6646
|
+
data: ["data", "code"],
|
|
6647
|
+
mixed: []
|
|
6648
|
+
};
|
|
5794
6649
|
var TaskAnalyzer = class {
|
|
5795
|
-
|
|
5796
|
-
|
|
5797
|
-
|
|
6650
|
+
tracker;
|
|
6651
|
+
lastProfile = null;
|
|
6652
|
+
lastSelectedModels = /* @__PURE__ */ new Map();
|
|
6653
|
+
constructor(tracker) {
|
|
6654
|
+
this.tracker = tracker;
|
|
6655
|
+
}
|
|
6656
|
+
setTracker(tracker) {
|
|
6657
|
+
this.tracker = tracker;
|
|
6658
|
+
}
|
|
6659
|
+
/** Returns the TaskProfile from the most recent analyze() call — used for outcome recording. */
|
|
6660
|
+
getLastProfile() {
|
|
6661
|
+
return this.lastProfile;
|
|
5798
6662
|
}
|
|
5799
6663
|
/**
|
|
5800
|
-
* Analyze a prompt and return a TaskProfile.
|
|
5801
|
-
*
|
|
6664
|
+
* Analyze a prompt and return a TaskProfile using pure heuristics.
|
|
6665
|
+
* Low confidence prompts fall back to a conservative mixed/moderate profile.
|
|
5802
6666
|
*/
|
|
5803
6667
|
async analyze(prompt) {
|
|
5804
6668
|
const cacheKey = prompt.slice(0, 200);
|
|
5805
6669
|
const cached = analysisCache.get(cacheKey);
|
|
5806
|
-
if (cached)
|
|
5807
|
-
|
|
5808
|
-
|
|
5809
|
-
try {
|
|
5810
|
-
const aiProfile = await this.aiInference(prompt);
|
|
5811
|
-
const merged = {
|
|
5812
|
-
type: aiProfile.type,
|
|
5813
|
-
complexity: aiProfile.complexity,
|
|
5814
|
-
requiresReasoning: aiProfile.requiresReasoning,
|
|
5815
|
-
requiresVision: heuristic.requiresVision || aiProfile.requiresVision,
|
|
5816
|
-
estimatedTokens: heuristic.estimatedTokens,
|
|
5817
|
-
confidence: 0.9
|
|
5818
|
-
// AI-backed
|
|
5819
|
-
};
|
|
5820
|
-
analysisCache.set(cacheKey, merged);
|
|
5821
|
-
return merged;
|
|
5822
|
-
} catch {
|
|
5823
|
-
}
|
|
6670
|
+
if (cached) {
|
|
6671
|
+
this.lastProfile = cached;
|
|
6672
|
+
return cached;
|
|
5824
6673
|
}
|
|
5825
|
-
|
|
5826
|
-
|
|
6674
|
+
const profile = heuristicAnalyze(prompt);
|
|
6675
|
+
analysisCache.set(cacheKey, profile);
|
|
6676
|
+
this.lastProfile = profile;
|
|
6677
|
+
return profile;
|
|
5827
6678
|
}
|
|
5828
6679
|
/**
|
|
5829
|
-
* Select the optimal model for a given tier
|
|
6680
|
+
* Select the optimal model for a given tier.
|
|
6681
|
+
* Scores tier-eligible models using cost efficiency + historical performance.
|
|
6682
|
+
* Falls back to the priority-list default when no candidates have history.
|
|
5830
6683
|
*/
|
|
5831
6684
|
async selectModel(prompt, tier, selector) {
|
|
5832
6685
|
const profile = await this.analyze(prompt);
|
|
5833
|
-
|
|
6686
|
+
if (profile.requiresVision) {
|
|
6687
|
+
return selector.selectVisionModel();
|
|
6688
|
+
}
|
|
6689
|
+
const candidates = selector.getCandidatesForTier(tier);
|
|
6690
|
+
if (candidates.length === 0) return selector.selectForTier(tier);
|
|
6691
|
+
const scored = candidates.map((m) => ({
|
|
6692
|
+
model: m,
|
|
6693
|
+
score: this.scoreModel(m, profile)
|
|
6694
|
+
}));
|
|
6695
|
+
scored.sort((a, b) => b.score - a.score);
|
|
6696
|
+
const best = scored[0]?.model ?? selector.selectForTier(tier);
|
|
6697
|
+
if (best) this.lastSelectedModels.set(tier, best);
|
|
6698
|
+
return best;
|
|
5834
6699
|
}
|
|
5835
|
-
|
|
5836
|
-
|
|
5837
|
-
|
|
5838
|
-
|
|
5839
|
-
|
|
5840
|
-
|
|
5841
|
-
|
|
5842
|
-
|
|
5843
|
-
|
|
5844
|
-
|
|
5845
|
-
|
|
5846
|
-
|
|
5847
|
-
|
|
5848
|
-
|
|
5849
|
-
|
|
5850
|
-
const
|
|
5851
|
-
const
|
|
5852
|
-
const
|
|
5853
|
-
|
|
5854
|
-
|
|
5855
|
-
|
|
5856
|
-
|
|
5857
|
-
|
|
5858
|
-
|
|
5859
|
-
|
|
5860
|
-
|
|
5861
|
-
|
|
6700
|
+
/**
|
|
6701
|
+
* Record the outcome of a completed run across all tiers that were selected
|
|
6702
|
+
* during this session and persist stats to disk.
|
|
6703
|
+
*/
|
|
6704
|
+
recordRunOutcome(outcome, costByTier) {
|
|
6705
|
+
if (!this.tracker || !this.lastProfile) return;
|
|
6706
|
+
const taskType = this.lastProfile.type;
|
|
6707
|
+
for (const [tier, model] of this.lastSelectedModels) {
|
|
6708
|
+
const cost = costByTier[tier] ?? 0;
|
|
6709
|
+
this.tracker.record(model.id, taskType, outcome, 0, cost);
|
|
6710
|
+
}
|
|
6711
|
+
this.lastSelectedModels.clear();
|
|
6712
|
+
void this.tracker.save();
|
|
6713
|
+
}
|
|
6714
|
+
scoreModel(model, profile) {
|
|
6715
|
+
const perf = this.tracker?.performanceScore(model.id, profile.type) ?? 0.5;
|
|
6716
|
+
const costEff = this.costEfficiency(model, profile.complexity);
|
|
6717
|
+
const match = this.taskMatchScore(model, profile);
|
|
6718
|
+
return perf * costEff * match;
|
|
6719
|
+
}
|
|
6720
|
+
costEfficiency(model, complexity) {
|
|
6721
|
+
if (this.tracker) return this.tracker.costEfficiencyScore(model, complexity);
|
|
6722
|
+
const blended = model.inputCostPer1kTokens + model.outputCostPer1kTokens * 2;
|
|
6723
|
+
const normalised = Math.min(1, blended / 0.05);
|
|
6724
|
+
const complexityWeight = (6 - complexity) / 5;
|
|
6725
|
+
return Math.max(0.1, 1 - normalised * complexityWeight);
|
|
6726
|
+
}
|
|
6727
|
+
taskMatchScore(model, profile) {
|
|
6728
|
+
const expected = TASK_TYPE_TAGS[profile.type];
|
|
6729
|
+
if (!model.specializations?.length || expected.length === 0) return 1;
|
|
6730
|
+
const matches = expected.filter((tag) => model.specializations.includes(tag)).length;
|
|
6731
|
+
return matches > 0 ? 1 + matches / expected.length * 0.3 : 0.8;
|
|
5862
6732
|
}
|
|
5863
6733
|
/** Clear the analysis cache (call between sessions). */
|
|
5864
6734
|
static clearCache() {
|
|
5865
6735
|
analysisCache.clear();
|
|
5866
6736
|
}
|
|
5867
6737
|
};
|
|
6738
|
+
var DEFAULT_STATS_FILE = path16.join(os3.homedir(), ".cascade", "model-perf.json");
|
|
6739
|
+
var ModelPerformanceTracker = class {
|
|
6740
|
+
stats = /* @__PURE__ */ new Map();
|
|
6741
|
+
statsFile;
|
|
6742
|
+
loaded = false;
|
|
6743
|
+
constructor(statsFile = DEFAULT_STATS_FILE) {
|
|
6744
|
+
this.statsFile = statsFile;
|
|
6745
|
+
}
|
|
6746
|
+
async load() {
|
|
6747
|
+
if (this.loaded) return;
|
|
6748
|
+
this.loaded = true;
|
|
6749
|
+
try {
|
|
6750
|
+
const raw = await fs3.readFile(this.statsFile, "utf-8");
|
|
6751
|
+
const parsed = JSON.parse(raw);
|
|
6752
|
+
for (const [key, stat] of Object.entries(parsed)) {
|
|
6753
|
+
this.stats.set(key, stat);
|
|
6754
|
+
}
|
|
6755
|
+
} catch {
|
|
6756
|
+
}
|
|
6757
|
+
}
|
|
6758
|
+
async save() {
|
|
6759
|
+
try {
|
|
6760
|
+
await fs3.mkdir(path16.dirname(this.statsFile), { recursive: true });
|
|
6761
|
+
const obj = {};
|
|
6762
|
+
for (const [key, stat] of this.stats) obj[key] = stat;
|
|
6763
|
+
await fs3.writeFile(this.statsFile, JSON.stringify(obj, null, 2), "utf-8");
|
|
6764
|
+
} catch {
|
|
6765
|
+
}
|
|
6766
|
+
}
|
|
6767
|
+
record(modelId, taskType, outcome, retries = 0, costUsd = 0) {
|
|
6768
|
+
const key = `${modelId}:${taskType}`;
|
|
6769
|
+
const s = this.stats.get(key) ?? {
|
|
6770
|
+
successCount: 0,
|
|
6771
|
+
failureCount: 0,
|
|
6772
|
+
totalRetries: 0,
|
|
6773
|
+
totalCostUsd: 0,
|
|
6774
|
+
sampleCount: 0
|
|
6775
|
+
};
|
|
6776
|
+
this.stats.set(key, {
|
|
6777
|
+
successCount: s.successCount + (outcome === "success" ? 1 : 0),
|
|
6778
|
+
failureCount: s.failureCount + (outcome === "failure" ? 1 : 0),
|
|
6779
|
+
totalRetries: s.totalRetries + retries,
|
|
6780
|
+
totalCostUsd: s.totalCostUsd + costUsd,
|
|
6781
|
+
sampleCount: s.sampleCount + 1
|
|
6782
|
+
});
|
|
6783
|
+
}
|
|
6784
|
+
/**
|
|
6785
|
+
* Returns 0.05–1.0; defaults to 0.5 (neutral prior) when no history exists.
|
|
6786
|
+
* High retry counts penalise the score.
|
|
6787
|
+
*/
|
|
6788
|
+
performanceScore(modelId, taskType) {
|
|
6789
|
+
const key = `${modelId}:${taskType}`;
|
|
6790
|
+
const s = this.stats.get(key);
|
|
6791
|
+
if (!s || s.sampleCount === 0) return 0.5;
|
|
6792
|
+
const successRate = s.successCount / s.sampleCount;
|
|
6793
|
+
const avgRetries = s.totalRetries / s.sampleCount;
|
|
6794
|
+
const retryPenalty = Math.min(0.4, avgRetries / 3);
|
|
6795
|
+
return Math.max(0.05, successRate * (1 - retryPenalty));
|
|
6796
|
+
}
|
|
6797
|
+
/**
|
|
6798
|
+
* Returns 0.1–1.0. Cheaper models score higher, with the penalty scaled
|
|
6799
|
+
* down for complex tasks (where capability matters more than cost).
|
|
6800
|
+
*
|
|
6801
|
+
* blended cost = input + 2 × output (output tokens are typically pricier).
|
|
6802
|
+
* normalised over $0.05 blended as the "expensive" ceiling.
|
|
6803
|
+
*/
|
|
6804
|
+
costEfficiencyScore(model, complexity) {
|
|
6805
|
+
const blended = model.inputCostPer1kTokens + model.outputCostPer1kTokens * 2;
|
|
6806
|
+
const normalised = Math.min(1, blended / 0.05);
|
|
6807
|
+
const complexityWeight = (6 - complexity) / 5;
|
|
6808
|
+
return Math.max(0.1, 1 - normalised * complexityWeight);
|
|
6809
|
+
}
|
|
6810
|
+
};
|
|
5868
6811
|
var DynamicTool = class extends BaseTool {
|
|
5869
6812
|
name;
|
|
5870
6813
|
description;
|
|
5871
6814
|
inputSchema;
|
|
5872
6815
|
executeCode;
|
|
5873
6816
|
_isDangerous;
|
|
5874
|
-
|
|
6817
|
+
registry;
|
|
6818
|
+
escalator;
|
|
6819
|
+
constructor(spec, registry, escalator) {
|
|
5875
6820
|
super();
|
|
5876
6821
|
this.name = spec.name;
|
|
5877
6822
|
this.description = spec.description;
|
|
5878
6823
|
this.inputSchema = spec.inputSchema;
|
|
5879
6824
|
this.executeCode = spec.executeCode;
|
|
5880
6825
|
this._isDangerous = spec.isDangerous;
|
|
6826
|
+
this.registry = registry;
|
|
6827
|
+
this.escalator = escalator;
|
|
5881
6828
|
}
|
|
5882
6829
|
isDangerous() {
|
|
5883
6830
|
return this._isDangerous;
|
|
5884
6831
|
}
|
|
5885
|
-
async execute(input,
|
|
6832
|
+
async execute(input, options) {
|
|
6833
|
+
const registry = this.registry;
|
|
6834
|
+
const escalator = this.escalator;
|
|
6835
|
+
const callTool = async (toolName, toolInput) => {
|
|
6836
|
+
if (!registry.hasTool(toolName)) return `Tool not found: ${toolName}`;
|
|
6837
|
+
if (registry.isDangerous(toolName)) {
|
|
6838
|
+
if (escalator) {
|
|
6839
|
+
const req = {
|
|
6840
|
+
id: `dynamic-${this.name}-${toolName}-${Date.now()}`,
|
|
6841
|
+
requestedBy: `dynamic_tool:${this.name}`,
|
|
6842
|
+
parentT2Id: options.tierId,
|
|
6843
|
+
toolName,
|
|
6844
|
+
input: toolInput,
|
|
6845
|
+
isDangerous: true,
|
|
6846
|
+
subtaskContext: `Dynamic tool "${this.name}" requesting access to "${toolName}"`,
|
|
6847
|
+
sectionContext: `Dynamic tool "${this.name}"`
|
|
6848
|
+
};
|
|
6849
|
+
const decision = await escalator.requestPermission(req);
|
|
6850
|
+
if (!decision.approved) {
|
|
6851
|
+
return `Permission denied for ${toolName} (decided by ${decision.decidedBy}).`;
|
|
6852
|
+
}
|
|
6853
|
+
}
|
|
6854
|
+
}
|
|
6855
|
+
try {
|
|
6856
|
+
const result = await registry.execute(toolName, toolInput, options);
|
|
6857
|
+
return typeof result === "string" ? result : JSON.stringify(result);
|
|
6858
|
+
} catch (err) {
|
|
6859
|
+
return `Error calling ${toolName}: ${err instanceof Error ? err.message : String(err)}`;
|
|
6860
|
+
}
|
|
6861
|
+
};
|
|
5886
6862
|
const sandbox = {
|
|
5887
6863
|
input,
|
|
5888
6864
|
fetch: globalThis.fetch,
|
|
6865
|
+
callTool,
|
|
5889
6866
|
JSON,
|
|
5890
6867
|
Math,
|
|
5891
6868
|
Date,
|
|
5892
6869
|
console: { log: () => {
|
|
5893
6870
|
}, error: () => {
|
|
5894
6871
|
} },
|
|
5895
|
-
// Silenced
|
|
5896
6872
|
setTimeout,
|
|
5897
6873
|
clearTimeout,
|
|
5898
6874
|
Promise,
|
|
@@ -5925,29 +6901,42 @@ Generate a minimal, safe JavaScript tool function for the described operation.
|
|
|
5925
6901
|
|
|
5926
6902
|
Rules:
|
|
5927
6903
|
- Return ONLY a JSON object with these fields: name, description, inputSchema, executeCode, isDangerous
|
|
5928
|
-
- executeCode is a self-contained JavaScript function body that:
|
|
5929
|
-
- Receives: input (object), fetch (
|
|
6904
|
+
- executeCode is a self-contained JavaScript async function body that:
|
|
6905
|
+
- Receives: input (object), fetch (for HTTP), callTool(toolName, input) (to call any registered cascade tool)
|
|
5930
6906
|
- Returns: a string result
|
|
5931
|
-
-
|
|
6907
|
+
- For file operations, prefer: await callTool('file_read', { path: input.path })
|
|
6908
|
+
- For shell commands, prefer: await callTool('shell', { command: 'ls -la' })
|
|
6909
|
+
- For pure computation / HTTP: use fetch or built-ins (JSON, Math, Date, String, Number, Array, Object)
|
|
5932
6910
|
- Must complete in under 15 seconds
|
|
5933
|
-
- isDangerous
|
|
6911
|
+
- isDangerous: true if the tool calls dangerous cascade tools (shell, file_write, file_delete, git) or makes HTTP calls that write data
|
|
5934
6912
|
- name must be snake_case, start with "dynamic_", max 40 chars
|
|
5935
6913
|
- description must be \u2264 120 chars
|
|
5936
6914
|
|
|
5937
|
-
Example
|
|
5938
|
-
|
|
6915
|
+
Example for a file-summary tool:
|
|
6916
|
+
{
|
|
6917
|
+
"name": "dynamic_summarize_file",
|
|
6918
|
+
"description": "Read a file and return a one-paragraph summary",
|
|
6919
|
+
"inputSchema": { "path": { "type": "string", "description": "File path to summarize" } },
|
|
6920
|
+
"executeCode": "const content = await callTool('file_read', { path: input.path }); return content.slice(0, 500);",
|
|
6921
|
+
"isDangerous": false
|
|
6922
|
+
}
|
|
5939
6923
|
|
|
5940
6924
|
Return ONLY valid JSON \u2014 no other text.`;
|
|
5941
6925
|
var ToolCreator = class {
|
|
5942
6926
|
router;
|
|
5943
6927
|
registry;
|
|
6928
|
+
escalator;
|
|
5944
6929
|
createdTools = /* @__PURE__ */ new Set();
|
|
5945
6930
|
constructor(router, registry) {
|
|
5946
6931
|
this.router = router;
|
|
5947
6932
|
this.registry = registry;
|
|
5948
6933
|
}
|
|
6934
|
+
setPermissionEscalator(escalator) {
|
|
6935
|
+
this.escalator = escalator;
|
|
6936
|
+
}
|
|
5949
6937
|
/**
|
|
5950
6938
|
* Generate a new tool from a description and register it with the ToolRegistry.
|
|
6939
|
+
* The generated tool has access to all registered cascade tools via callTool().
|
|
5951
6940
|
* Returns the tool name if successful, null if generation failed.
|
|
5952
6941
|
*/
|
|
5953
6942
|
async createTool(description, context) {
|
|
@@ -5958,26 +6947,21 @@ Required capability: ${description.slice(0, 300)}`;
|
|
|
5958
6947
|
try {
|
|
5959
6948
|
const result = await this.router.generate("T3", {
|
|
5960
6949
|
messages: [{ role: "user", content: prompt }],
|
|
5961
|
-
maxTokens:
|
|
6950
|
+
maxTokens: 800
|
|
5962
6951
|
});
|
|
5963
6952
|
const jsonMatch = /\{[\s\S]*\}/.exec(result.content);
|
|
5964
|
-
if (!jsonMatch)
|
|
5965
|
-
return null;
|
|
5966
|
-
}
|
|
6953
|
+
if (!jsonMatch) return null;
|
|
5967
6954
|
const spec = JSON.parse(jsonMatch[0]);
|
|
5968
|
-
if (!spec.name || !spec.description || !spec.executeCode || !spec.inputSchema)
|
|
5969
|
-
return null;
|
|
5970
|
-
}
|
|
6955
|
+
if (!spec.name || !spec.description || !spec.executeCode || !spec.inputSchema) return null;
|
|
5971
6956
|
if (this.createdTools.has(spec.name) || this.registry.hasTool(spec.name)) {
|
|
5972
6957
|
spec.name = `${spec.name}_${Date.now() % 1e4}`;
|
|
5973
6958
|
}
|
|
5974
6959
|
try {
|
|
5975
|
-
|
|
5976
|
-
|
|
5977
|
-
} catch (err) {
|
|
6960
|
+
new Function("input", "fetch", "callTool", spec.executeCode);
|
|
6961
|
+
} catch {
|
|
5978
6962
|
return null;
|
|
5979
6963
|
}
|
|
5980
|
-
const tool = new DynamicTool(spec);
|
|
6964
|
+
const tool = new DynamicTool(spec, this.registry, this.escalator);
|
|
5981
6965
|
this.registry.register(tool);
|
|
5982
6966
|
this.createdTools.add(spec.name);
|
|
5983
6967
|
return spec.name;
|
|
@@ -5985,16 +6969,14 @@ Required capability: ${description.slice(0, 300)}`;
|
|
|
5985
6969
|
return null;
|
|
5986
6970
|
}
|
|
5987
6971
|
}
|
|
5988
|
-
/**
|
|
5989
|
-
* Returns the names of all tools created in this session.
|
|
5990
|
-
*/
|
|
6972
|
+
/** Returns the names of all tools created in this session. */
|
|
5991
6973
|
getCreatedTools() {
|
|
5992
6974
|
return Array.from(this.createdTools);
|
|
5993
6975
|
}
|
|
5994
6976
|
};
|
|
5995
6977
|
|
|
5996
6978
|
// src/core/cascade.ts
|
|
5997
|
-
var Cascade = class extends EventEmitter {
|
|
6979
|
+
var Cascade = class _Cascade extends EventEmitter {
|
|
5998
6980
|
router;
|
|
5999
6981
|
toolRegistry;
|
|
6000
6982
|
mcpClient;
|
|
@@ -6005,6 +6987,7 @@ var Cascade = class extends EventEmitter {
|
|
|
6005
6987
|
audit;
|
|
6006
6988
|
telemetry;
|
|
6007
6989
|
taskAnalyzer;
|
|
6990
|
+
perfTracker;
|
|
6008
6991
|
toolCreator;
|
|
6009
6992
|
constructor(config, workspacePath, store) {
|
|
6010
6993
|
super();
|
|
@@ -6021,10 +7004,12 @@ var Cascade = class extends EventEmitter {
|
|
|
6021
7004
|
this.telemetry = config.telemetry?.enabled ? new Telemetry(config.telemetry, config.telemetry.distinctId ?? "anonymous") : noopTelemetry;
|
|
6022
7005
|
}
|
|
6023
7006
|
initOptionalFeatures() {
|
|
6024
|
-
|
|
6025
|
-
|
|
6026
|
-
this.
|
|
7007
|
+
if (this.config.cascadeAuto === true) {
|
|
7008
|
+
this.perfTracker = new ModelPerformanceTracker();
|
|
7009
|
+
void this.perfTracker.load();
|
|
7010
|
+
this.taskAnalyzer = new TaskAnalyzer(this.perfTracker);
|
|
6027
7011
|
}
|
|
7012
|
+
const cfg = this.config;
|
|
6028
7013
|
if (cfg["enableToolCreation"] === true) {
|
|
6029
7014
|
this.toolCreator = new ToolCreator(this.router, this.toolRegistry);
|
|
6030
7015
|
}
|
|
@@ -6090,6 +7075,26 @@ var Cascade = class extends EventEmitter {
|
|
|
6090
7075
|
}
|
|
6091
7076
|
}
|
|
6092
7077
|
}
|
|
7078
|
+
const pluginPaths = this.config["plugins"];
|
|
7079
|
+
if (pluginPaths?.length) {
|
|
7080
|
+
for (const pluginPath of pluginPaths) {
|
|
7081
|
+
try {
|
|
7082
|
+
const mod = await import(pluginPath);
|
|
7083
|
+
const plugin = mod.default ?? mod;
|
|
7084
|
+
if (plugin && Array.isArray(plugin.tools)) {
|
|
7085
|
+
this.toolRegistry.registerPlugin(plugin);
|
|
7086
|
+
} else {
|
|
7087
|
+
console.warn(`[cascade] Plugin "${pluginPath}" does not export a valid ToolPlugin.`);
|
|
7088
|
+
}
|
|
7089
|
+
} catch (err) {
|
|
7090
|
+
console.warn(`[cascade] Failed to load plugin "${pluginPath}":`, err);
|
|
7091
|
+
}
|
|
7092
|
+
}
|
|
7093
|
+
}
|
|
7094
|
+
if (this.config.cascadeAuto && this.store) {
|
|
7095
|
+
this.router.profileModels(this.store).catch(() => {
|
|
7096
|
+
});
|
|
7097
|
+
}
|
|
6093
7098
|
this.initOptionalFeatures();
|
|
6094
7099
|
this.initialized = true;
|
|
6095
7100
|
})();
|
|
@@ -6107,21 +7112,41 @@ var Cascade = class extends EventEmitter {
|
|
|
6107
7112
|
looksLikeSimpleArtifactTask(prompt) {
|
|
6108
7113
|
return /create .*\.(txt|md|json|csv)\b/i.test(prompt) && !/(research|compare|thorough|pdf|report|analy[sz]e|architecture|multi-agent)/i.test(prompt);
|
|
6109
7114
|
}
|
|
6110
|
-
|
|
6111
|
-
|
|
6112
|
-
|
|
6113
|
-
|
|
6114
|
-
|
|
6115
|
-
|
|
6116
|
-
|
|
6117
|
-
|
|
7115
|
+
looksLikeConversational(prompt) {
|
|
7116
|
+
const LOW_COMPLEXITY = [
|
|
7117
|
+
/^(?:hi|hello|hey|thanks|thank you|ok|okay|yes|no|sure|got it|sounds good)\b/i,
|
|
7118
|
+
/^(?:what is|what are|list|show me|tell me|who is|where is|when is|how do i)\b/i,
|
|
7119
|
+
/\b(?:simple|quick|brief|small|single|one-line|typo|rename)\b/i
|
|
7120
|
+
];
|
|
7121
|
+
const wordCount = prompt.trim().split(/\s+/).length;
|
|
7122
|
+
return wordCount <= 12 && LOW_COMPLEXITY.some((re) => re.test(prompt.trim()));
|
|
7123
|
+
}
|
|
7124
|
+
// Cache glob scan results per workspace path to avoid repeated I/O.
|
|
7125
|
+
static globCache = /* @__PURE__ */ new Map();
|
|
7126
|
+
async countWorkspaceFiles(workspacePath) {
|
|
7127
|
+
const now = Date.now();
|
|
7128
|
+
const cached = _Cascade.globCache.get(workspacePath);
|
|
7129
|
+
if (cached && cached.expiresAt > now) return cached.count;
|
|
6118
7130
|
try {
|
|
6119
7131
|
const files = await glob("**/*.*", {
|
|
6120
7132
|
cwd: workspacePath,
|
|
6121
7133
|
ignore: ["node_modules/**", ".git/**", "dist/**", "build/**"],
|
|
6122
7134
|
nodir: true
|
|
6123
7135
|
});
|
|
6124
|
-
|
|
7136
|
+
_Cascade.globCache.set(workspacePath, { count: files.length, expiresAt: now + 3e4 });
|
|
7137
|
+
return files.length;
|
|
7138
|
+
} catch {
|
|
7139
|
+
return 0;
|
|
7140
|
+
}
|
|
7141
|
+
}
|
|
7142
|
+
async determineComplexity(prompt, workspacePath, conversationHistory = []) {
|
|
7143
|
+
if (this.isCasualGreeting(prompt)) return "Simple";
|
|
7144
|
+
if (this.looksLikeSimpleArtifactTask(prompt)) return "Simple";
|
|
7145
|
+
if (this.looksLikeConversational(prompt)) return "Simple";
|
|
7146
|
+
let workspaceContext = "";
|
|
7147
|
+
try {
|
|
7148
|
+
const count = await this.countWorkspaceFiles(workspacePath);
|
|
7149
|
+
workspaceContext = `Workspace Scout: Found ~${count} source files in the project.`;
|
|
6125
7150
|
} catch {
|
|
6126
7151
|
workspaceContext = "Workspace Scout: Could not scan workspace.";
|
|
6127
7152
|
}
|
|
@@ -6207,7 +7232,7 @@ ${prompt}` : prompt;
|
|
|
6207
7232
|
this.telemetry.capture("cascade:session_start", {
|
|
6208
7233
|
complexity,
|
|
6209
7234
|
providerCount: this.config.providers.length,
|
|
6210
|
-
cascadeAutoEnabled: this.config
|
|
7235
|
+
cascadeAutoEnabled: this.config.cascadeAuto === true,
|
|
6211
7236
|
toolCreationEnabled: this.config["enableToolCreation"] === true
|
|
6212
7237
|
});
|
|
6213
7238
|
this.emit("tier:root", { role: complexity === "Simple" ? "T3" : complexity === "Moderate" ? "T2" : "T1" });
|
|
@@ -6222,6 +7247,7 @@ ${prompt}` : prompt;
|
|
|
6222
7247
|
}));
|
|
6223
7248
|
}
|
|
6224
7249
|
const toolCreator = this.toolCreator;
|
|
7250
|
+
if (toolCreator) toolCreator.setPermissionEscalator(escalator);
|
|
6225
7251
|
let finalOutput = "";
|
|
6226
7252
|
let t2Results = [];
|
|
6227
7253
|
let runError = null;
|
|
@@ -6243,6 +7269,8 @@ ${prompt}` : prompt;
|
|
|
6243
7269
|
});
|
|
6244
7270
|
tier.on("log", (e) => this.emit("log", e));
|
|
6245
7271
|
tier.on("tier:status", (e) => this.emit("tier:status", e));
|
|
7272
|
+
tier.on("tool:call", (e) => this.emit("tool:call", e));
|
|
7273
|
+
tier.on("tool:result", (e) => this.emit("tool:result", e));
|
|
6246
7274
|
tier.on("tool:approval-request", async (request) => {
|
|
6247
7275
|
this.emit("tool:approval-request", request);
|
|
6248
7276
|
let decision = { approved: false };
|
|
@@ -6297,6 +7325,7 @@ ${prompt}` : prompt;
|
|
|
6297
7325
|
}
|
|
6298
7326
|
t2.setPermissionEscalator(escalator);
|
|
6299
7327
|
if (toolCreator) t2.setToolCreator(toolCreator);
|
|
7328
|
+
t2.setPeerMessageCallback((e) => this.emit("peer:message", e), options.sessionId ?? "");
|
|
6300
7329
|
bindTierEvents(t2);
|
|
6301
7330
|
const assignment = {
|
|
6302
7331
|
sectionId: taskId,
|
|
@@ -6326,6 +7355,7 @@ ${prompt}` : prompt;
|
|
|
6326
7355
|
}
|
|
6327
7356
|
t1.setPermissionEscalator(escalator);
|
|
6328
7357
|
if (toolCreator) t1.setToolCreator(toolCreator);
|
|
7358
|
+
t1.setPeerMessageCallback((e) => this.emit("peer:message", e), options.sessionId ?? "");
|
|
6329
7359
|
bindTierEvents(t1);
|
|
6330
7360
|
t1.on("plan", (e) => this.emit("plan", e));
|
|
6331
7361
|
const result = await t1.execute(options.prompt, options.images, void 0, options.signal);
|
|
@@ -6349,6 +7379,13 @@ ${prompt}` : prompt;
|
|
|
6349
7379
|
escalator.cancelAllPending();
|
|
6350
7380
|
} catch {
|
|
6351
7381
|
}
|
|
7382
|
+
if (this.taskAnalyzer) {
|
|
7383
|
+
try {
|
|
7384
|
+
const stats2 = this.router.getStats();
|
|
7385
|
+
this.taskAnalyzer.recordRunOutcome(runError ? "failure" : "success", stats2.costByTier);
|
|
7386
|
+
} catch {
|
|
7387
|
+
}
|
|
7388
|
+
}
|
|
6352
7389
|
try {
|
|
6353
7390
|
const stats2 = this.router.getStats();
|
|
6354
7391
|
const durationMs2 = Date.now() - startMs;
|
|
@@ -6449,7 +7486,7 @@ var Keystore = class {
|
|
|
6449
7486
|
const creds = await this.keytar.findCredentials(KEYTAR_SERVICE);
|
|
6450
7487
|
this.cache = Object.fromEntries(creds.map((c) => [c.account, c.password]));
|
|
6451
7488
|
this.backend = "keytar";
|
|
6452
|
-
if (password &&
|
|
7489
|
+
if (password && fs15.existsSync(this.storePath)) {
|
|
6453
7490
|
try {
|
|
6454
7491
|
const fileEntries = this.decryptFile(password);
|
|
6455
7492
|
for (const [k, v] of Object.entries(fileEntries)) {
|
|
@@ -6468,7 +7505,7 @@ var Keystore = class {
|
|
|
6468
7505
|
"Keystore unlock requires a password because the OS keychain (keytar) is not available on this system."
|
|
6469
7506
|
);
|
|
6470
7507
|
}
|
|
6471
|
-
if (!
|
|
7508
|
+
if (!fs15.existsSync(this.storePath)) {
|
|
6472
7509
|
const salt = crypto.randomBytes(SALT_LEN);
|
|
6473
7510
|
this.masterKey = this.deriveKey(password, salt);
|
|
6474
7511
|
this.writeWithSalt({}, salt);
|
|
@@ -6482,7 +7519,7 @@ var Keystore = class {
|
|
|
6482
7519
|
}
|
|
6483
7520
|
/** Synchronous legacy unlock kept for AES-only environments. */
|
|
6484
7521
|
unlockSync(password) {
|
|
6485
|
-
if (!
|
|
7522
|
+
if (!fs15.existsSync(this.storePath)) {
|
|
6486
7523
|
const salt = crypto.randomBytes(SALT_LEN);
|
|
6487
7524
|
this.masterKey = this.deriveKey(password, salt);
|
|
6488
7525
|
this.writeWithSalt({}, salt);
|
|
@@ -6540,7 +7577,7 @@ var Keystore = class {
|
|
|
6540
7577
|
}
|
|
6541
7578
|
}
|
|
6542
7579
|
decryptFile(password, knownSalt) {
|
|
6543
|
-
if (!
|
|
7580
|
+
if (!fs15.existsSync(this.storePath)) return {};
|
|
6544
7581
|
try {
|
|
6545
7582
|
const { salt, ciphertext, iv, tag } = this.readRaw();
|
|
6546
7583
|
const useSalt = knownSalt ?? salt;
|
|
@@ -6562,8 +7599,8 @@ var Keystore = class {
|
|
|
6562
7599
|
const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
|
|
6563
7600
|
const tag = cipher.getAuthTag();
|
|
6564
7601
|
const out = Buffer.concat([raw.salt, iv, tag, ciphertext]);
|
|
6565
|
-
|
|
6566
|
-
|
|
7602
|
+
fs15.mkdirSync(path16.dirname(this.storePath), { recursive: true });
|
|
7603
|
+
fs15.writeFileSync(this.storePath, out, { mode: 384 });
|
|
6567
7604
|
}
|
|
6568
7605
|
writeWithSalt(data, salt) {
|
|
6569
7606
|
if (!this.masterKey) throw new Error("writeWithSalt called before masterKey was set");
|
|
@@ -6573,11 +7610,11 @@ var Keystore = class {
|
|
|
6573
7610
|
const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
|
|
6574
7611
|
const tag = cipher.getAuthTag();
|
|
6575
7612
|
const out = Buffer.concat([salt, iv, tag, ciphertext]);
|
|
6576
|
-
|
|
6577
|
-
|
|
7613
|
+
fs15.mkdirSync(path16.dirname(this.storePath), { recursive: true });
|
|
7614
|
+
fs15.writeFileSync(this.storePath, out, { mode: 384 });
|
|
6578
7615
|
}
|
|
6579
7616
|
readRaw() {
|
|
6580
|
-
const buf =
|
|
7617
|
+
const buf = fs15.readFileSync(this.storePath);
|
|
6581
7618
|
let offset = 0;
|
|
6582
7619
|
const salt = buf.subarray(offset, offset + SALT_LEN);
|
|
6583
7620
|
offset += SALT_LEN;
|
|
@@ -6610,9 +7647,9 @@ var CascadeIgnore = class {
|
|
|
6610
7647
|
]);
|
|
6611
7648
|
}
|
|
6612
7649
|
async load(workspacePath) {
|
|
6613
|
-
const filePath =
|
|
7650
|
+
const filePath = path16.join(workspacePath, ".cascadeignore");
|
|
6614
7651
|
try {
|
|
6615
|
-
const content = await
|
|
7652
|
+
const content = await fs3.readFile(filePath, "utf-8");
|
|
6616
7653
|
const lines = content.split("\n").filter((l) => l.trim() && !l.startsWith("#"));
|
|
6617
7654
|
this.ig.add(lines);
|
|
6618
7655
|
this.loaded = true;
|
|
@@ -6621,7 +7658,7 @@ var CascadeIgnore = class {
|
|
|
6621
7658
|
}
|
|
6622
7659
|
isIgnored(filePath, workspacePath) {
|
|
6623
7660
|
try {
|
|
6624
|
-
const relative = workspacePath ?
|
|
7661
|
+
const relative = workspacePath ? path16.relative(workspacePath, filePath) : filePath;
|
|
6625
7662
|
return this.ig.ignores(relative);
|
|
6626
7663
|
} catch {
|
|
6627
7664
|
return false;
|
|
@@ -6632,9 +7669,9 @@ var CascadeIgnore = class {
|
|
|
6632
7669
|
}
|
|
6633
7670
|
};
|
|
6634
7671
|
async function loadCascadeMd(workspacePath) {
|
|
6635
|
-
const filePath =
|
|
7672
|
+
const filePath = path16.join(workspacePath, "CASCADE.md");
|
|
6636
7673
|
try {
|
|
6637
|
-
const raw = await
|
|
7674
|
+
const raw = await fs3.readFile(filePath, "utf-8");
|
|
6638
7675
|
return parseCascadeMd(raw);
|
|
6639
7676
|
} catch {
|
|
6640
7677
|
return null;
|
|
@@ -6663,7 +7700,7 @@ ${raw.trim()}`;
|
|
|
6663
7700
|
var MemoryStore = class _MemoryStore {
|
|
6664
7701
|
db;
|
|
6665
7702
|
constructor(dbPath) {
|
|
6666
|
-
|
|
7703
|
+
fs15.mkdirSync(path16.dirname(dbPath), { recursive: true });
|
|
6667
7704
|
try {
|
|
6668
7705
|
this.db = new Database(dbPath, { timeout: 5e3 });
|
|
6669
7706
|
this.db.pragma("journal_mode = WAL");
|
|
@@ -7146,6 +8183,27 @@ Original error: ${err.message}`
|
|
|
7146
8183
|
if (!row.oldest) return Infinity;
|
|
7147
8184
|
return Date.now() - new Date(row.oldest).getTime();
|
|
7148
8185
|
}
|
|
8186
|
+
saveModelProfile(modelId, provider, specializations) {
|
|
8187
|
+
const cacheKey = `${provider}:${modelId}`;
|
|
8188
|
+
const existing = this.db.prepare("SELECT metadata FROM model_cache WHERE id = ?").get(cacheKey);
|
|
8189
|
+
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 };
|
|
8190
|
+
meta.specializations = specializations;
|
|
8191
|
+
this.db.prepare(`
|
|
8192
|
+
INSERT INTO model_cache (id, provider, model_id, name, metadata, updated_at)
|
|
8193
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
8194
|
+
ON CONFLICT(id) DO UPDATE SET metadata = excluded.metadata, updated_at = excluded.updated_at
|
|
8195
|
+
`).run(cacheKey, provider, modelId, meta.name ?? modelId, JSON.stringify(meta), (/* @__PURE__ */ new Date()).toISOString());
|
|
8196
|
+
}
|
|
8197
|
+
getModelProfile(modelId, provider) {
|
|
8198
|
+
const row = this.db.prepare("SELECT metadata FROM model_cache WHERE id = ?").get(`${provider}:${modelId}`);
|
|
8199
|
+
return row ? JSON.parse(row.metadata) : void 0;
|
|
8200
|
+
}
|
|
8201
|
+
getProfiledModelIds() {
|
|
8202
|
+
const rows = this.db.prepare(
|
|
8203
|
+
"SELECT model_id FROM model_cache WHERE json_extract(metadata, '$.specializations') IS NOT NULL"
|
|
8204
|
+
).all();
|
|
8205
|
+
return rows.map((r) => r.model_id);
|
|
8206
|
+
}
|
|
7149
8207
|
// ── Tool Result Cache (in-memory, TTL-based) ──────────────────────────
|
|
7150
8208
|
// Avoids redundant calls for read-only tools within a short window.
|
|
7151
8209
|
// Not persisted to DB — cleared on process restart.
|
|
@@ -7400,15 +8458,15 @@ var ConfigManager = class {
|
|
|
7400
8458
|
globalDir;
|
|
7401
8459
|
constructor(workspacePath = process.cwd()) {
|
|
7402
8460
|
this.workspacePath = workspacePath;
|
|
7403
|
-
this.globalDir =
|
|
8461
|
+
this.globalDir = path16.join(os3.homedir(), GLOBAL_CONFIG_DIR);
|
|
7404
8462
|
}
|
|
7405
8463
|
async load() {
|
|
7406
8464
|
this.config = await this.loadConfig();
|
|
7407
8465
|
this.ignore = new CascadeIgnore();
|
|
7408
8466
|
await this.ignore.load(this.workspacePath);
|
|
7409
8467
|
this.cascadeMd = await loadCascadeMd(this.workspacePath);
|
|
7410
|
-
this.keystore = new Keystore(
|
|
7411
|
-
this.store = new MemoryStore(
|
|
8468
|
+
this.keystore = new Keystore(path16.join(this.globalDir, GLOBAL_KEYSTORE_FILE));
|
|
8469
|
+
this.store = new MemoryStore(path16.join(this.workspacePath, CASCADE_DB_FILE));
|
|
7412
8470
|
await this.injectEnvKeys();
|
|
7413
8471
|
await this.ensureDefaultIdentity();
|
|
7414
8472
|
}
|
|
@@ -7431,9 +8489,9 @@ var ConfigManager = class {
|
|
|
7431
8489
|
return this.workspacePath;
|
|
7432
8490
|
}
|
|
7433
8491
|
async save() {
|
|
7434
|
-
const configPath =
|
|
7435
|
-
await
|
|
7436
|
-
await
|
|
8492
|
+
const configPath = path16.join(this.workspacePath, CASCADE_CONFIG_FILE);
|
|
8493
|
+
await fs3.mkdir(path16.dirname(configPath), { recursive: true });
|
|
8494
|
+
await fs3.writeFile(configPath, JSON.stringify(this.config, null, 2), "utf-8");
|
|
7437
8495
|
}
|
|
7438
8496
|
async updateConfig(updates) {
|
|
7439
8497
|
this.config = validateConfig({ ...this.config, ...updates });
|
|
@@ -7456,9 +8514,9 @@ var ConfigManager = class {
|
|
|
7456
8514
|
return configProvider?.apiKey;
|
|
7457
8515
|
}
|
|
7458
8516
|
async loadConfig() {
|
|
7459
|
-
const configPath =
|
|
8517
|
+
const configPath = path16.join(this.workspacePath, CASCADE_CONFIG_FILE);
|
|
7460
8518
|
try {
|
|
7461
|
-
const raw = await
|
|
8519
|
+
const raw = await fs3.readFile(configPath, "utf-8");
|
|
7462
8520
|
return validateConfig(JSON.parse(raw));
|
|
7463
8521
|
} catch (err) {
|
|
7464
8522
|
if (err.code === "ENOENT") {
|
|
@@ -7622,6 +8680,9 @@ var DashboardSocket = class {
|
|
|
7622
8680
|
emitStreamToken(tierId, text, sessionId) {
|
|
7623
8681
|
this.io.to(`session:${sessionId}`).emit("stream:token", { tierId, text, sessionId });
|
|
7624
8682
|
}
|
|
8683
|
+
emitPeerMessage(event) {
|
|
8684
|
+
this.io.to(`session:${event.sessionId}`).emit("peer:message", event);
|
|
8685
|
+
}
|
|
7625
8686
|
emitApprovalRequest(request) {
|
|
7626
8687
|
this.io.emit("permission:user-required", request);
|
|
7627
8688
|
}
|
|
@@ -7664,16 +8725,13 @@ var DashboardSocket = class {
|
|
|
7664
8725
|
const { sessionId } = normalizeSessionSubscriptionPayload(payload);
|
|
7665
8726
|
socket.leave(`session:${sessionId}`);
|
|
7666
8727
|
});
|
|
7667
|
-
socket.on("join:tenant", (tenantId) => {
|
|
7668
|
-
socket.join(`tenant:${tenantId}`);
|
|
7669
|
-
});
|
|
7670
8728
|
});
|
|
7671
8729
|
}
|
|
7672
8730
|
close() {
|
|
7673
8731
|
this.io.close();
|
|
7674
8732
|
}
|
|
7675
8733
|
};
|
|
7676
|
-
var __dirname$1 =
|
|
8734
|
+
var __dirname$1 = path16.dirname(fileURLToPath(import.meta.url));
|
|
7677
8735
|
var DashboardServer = class {
|
|
7678
8736
|
app;
|
|
7679
8737
|
httpServer;
|
|
@@ -7739,15 +8797,15 @@ var DashboardServer = class {
|
|
|
7739
8797
|
resolveDashboardSecret() {
|
|
7740
8798
|
const fromConfig = this.config.dashboard.secret ?? process.env["CASCADE_DASHBOARD_SECRET"];
|
|
7741
8799
|
if (fromConfig) return fromConfig;
|
|
7742
|
-
const secretPath =
|
|
8800
|
+
const secretPath = path16.join(this.workspacePath, CASCADE_DASHBOARD_SECRET_FILE);
|
|
7743
8801
|
try {
|
|
7744
|
-
if (
|
|
7745
|
-
const existing =
|
|
8802
|
+
if (fs15.existsSync(secretPath)) {
|
|
8803
|
+
const existing = fs15.readFileSync(secretPath, "utf-8").trim();
|
|
7746
8804
|
if (existing.length >= 16) return existing;
|
|
7747
8805
|
}
|
|
7748
8806
|
const generated = randomUUID();
|
|
7749
|
-
|
|
7750
|
-
|
|
8807
|
+
fs15.mkdirSync(path16.dirname(secretPath), { recursive: true });
|
|
8808
|
+
fs15.writeFileSync(secretPath, generated, { encoding: "utf-8", mode: 384 });
|
|
7751
8809
|
if (this.config.dashboard.auth) {
|
|
7752
8810
|
console.warn(
|
|
7753
8811
|
`Dashboard auth enabled with no secret configured; persisted a generated secret to ${secretPath}. Set CASCADE_DASHBOARD_SECRET or config.dashboard.secret to override.`
|
|
@@ -7774,7 +8832,7 @@ var DashboardServer = class {
|
|
|
7774
8832
|
// ── Setup ─────────────────────────────────────
|
|
7775
8833
|
getGlobalStore() {
|
|
7776
8834
|
if (!this.globalStore) {
|
|
7777
|
-
const globalDbPath =
|
|
8835
|
+
const globalDbPath = path16.join(os3.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
|
|
7778
8836
|
this.globalStore = new MemoryStore(globalDbPath);
|
|
7779
8837
|
}
|
|
7780
8838
|
return this.globalStore;
|
|
@@ -7835,12 +8893,12 @@ var DashboardServer = class {
|
|
|
7835
8893
|
}
|
|
7836
8894
|
}
|
|
7837
8895
|
watchRuntimeChanges() {
|
|
7838
|
-
const workspaceDbPath =
|
|
7839
|
-
const globalDbPath =
|
|
8896
|
+
const workspaceDbPath = path16.join(this.workspacePath, CASCADE_DB_FILE);
|
|
8897
|
+
const globalDbPath = path16.join(os3.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
|
|
7840
8898
|
const watchPaths = [workspaceDbPath, globalDbPath].filter((p, index, arr) => arr.indexOf(p) === index);
|
|
7841
8899
|
for (const watchPath of watchPaths) {
|
|
7842
|
-
if (!
|
|
7843
|
-
|
|
8900
|
+
if (!fs15.existsSync(watchPath)) continue;
|
|
8901
|
+
fs15.watchFile(watchPath, { interval: 3e3 }, () => {
|
|
7844
8902
|
this.throttledBroadcast(watchPath === globalDbPath ? "global" : "workspace");
|
|
7845
8903
|
});
|
|
7846
8904
|
}
|
|
@@ -7871,6 +8929,21 @@ var DashboardServer = class {
|
|
|
7871
8929
|
legacyHeaders: false,
|
|
7872
8930
|
message: { error: "Too many login attempts. Try again in 15 minutes." }
|
|
7873
8931
|
});
|
|
8932
|
+
const apiLimiter = rateLimit({
|
|
8933
|
+
windowMs: 60 * 1e3,
|
|
8934
|
+
limit: 60,
|
|
8935
|
+
standardHeaders: "draft-7",
|
|
8936
|
+
legacyHeaders: false,
|
|
8937
|
+
message: { error: "Too many requests. Slow down." }
|
|
8938
|
+
});
|
|
8939
|
+
this.app.use("/api", apiLimiter);
|
|
8940
|
+
const mutationLimiter = rateLimit({
|
|
8941
|
+
windowMs: 60 * 1e3,
|
|
8942
|
+
limit: 10,
|
|
8943
|
+
standardHeaders: "draft-7",
|
|
8944
|
+
legacyHeaders: false,
|
|
8945
|
+
message: { error: "Too many requests on this endpoint." }
|
|
8946
|
+
});
|
|
7874
8947
|
this.app.post("/api/auth/login", loginLimiter, (req, res) => {
|
|
7875
8948
|
const { username, password } = req.body ?? {};
|
|
7876
8949
|
if (!authRequired) {
|
|
@@ -7906,22 +8979,33 @@ var DashboardServer = class {
|
|
|
7906
8979
|
res.status(401).json({ error: "Invalid credentials" });
|
|
7907
8980
|
}
|
|
7908
8981
|
});
|
|
7909
|
-
this.app.post("/api/force-halt", auth, (req, res) => {
|
|
7910
|
-
const
|
|
8982
|
+
this.app.post("/api/force-halt", auth, mutationLimiter, (req, res) => {
|
|
8983
|
+
const body = req.body;
|
|
8984
|
+
const sessionId = typeof body["sessionId"] === "string" ? body["sessionId"] : void 0;
|
|
8985
|
+
const nodeId = typeof body["nodeId"] === "string" ? body["nodeId"] : void 0;
|
|
7911
8986
|
const payload = { sessionId, nodeId, requestedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
7912
8987
|
this.socket.broadcast("session:halt", payload);
|
|
7913
8988
|
if (sessionId) this.socket.broadcastToRoom(`session:${sessionId}`, "session:halt", payload);
|
|
7914
8989
|
res.json({ success: true, ...payload });
|
|
7915
8990
|
});
|
|
7916
|
-
this.app.post("/api/approve", auth, (req, res) => {
|
|
7917
|
-
const
|
|
8991
|
+
this.app.post("/api/approve", auth, mutationLimiter, (req, res) => {
|
|
8992
|
+
const body = req.body;
|
|
8993
|
+
const sessionId = typeof body["sessionId"] === "string" ? body["sessionId"] : void 0;
|
|
8994
|
+
const nodeId = typeof body["nodeId"] === "string" ? body["nodeId"] : void 0;
|
|
7918
8995
|
const payload = { sessionId, nodeId, requestedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
7919
8996
|
this.socket.broadcast("session:approve", payload);
|
|
7920
8997
|
if (sessionId) this.socket.broadcastToRoom(`session:${sessionId}`, "session:approve", payload);
|
|
7921
8998
|
res.json({ success: true, ...payload });
|
|
7922
8999
|
});
|
|
7923
|
-
this.app.post("/api/inject", auth, (req, res) => {
|
|
7924
|
-
const
|
|
9000
|
+
this.app.post("/api/inject", auth, mutationLimiter, (req, res) => {
|
|
9001
|
+
const body = req.body;
|
|
9002
|
+
const message = typeof body["message"] === "string" ? body["message"] : void 0;
|
|
9003
|
+
const sessionId = typeof body["sessionId"] === "string" ? body["sessionId"] : void 0;
|
|
9004
|
+
const nodeId = typeof body["nodeId"] === "string" ? body["nodeId"] : void 0;
|
|
9005
|
+
if (!message) {
|
|
9006
|
+
res.status(400).json({ error: "message is required and must be a string" });
|
|
9007
|
+
return;
|
|
9008
|
+
}
|
|
7925
9009
|
const payload = { sessionId, nodeId, message, requestedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
7926
9010
|
this.socket.broadcast("session:message-injected", payload);
|
|
7927
9011
|
if (sessionId) this.socket.broadcastToRoom(`session:${sessionId}`, "session:message-injected", payload);
|
|
@@ -7944,7 +9028,7 @@ var DashboardServer = class {
|
|
|
7944
9028
|
const sessionId = req.params.id;
|
|
7945
9029
|
this.store.deleteSession(sessionId);
|
|
7946
9030
|
this.store.deleteRuntimeSession(sessionId);
|
|
7947
|
-
const globalDbPath =
|
|
9031
|
+
const globalDbPath = path16.join(os3.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
|
|
7948
9032
|
const globalStore = new MemoryStore(globalDbPath);
|
|
7949
9033
|
try {
|
|
7950
9034
|
globalStore.deleteRuntimeSession(sessionId);
|
|
@@ -7958,7 +9042,7 @@ var DashboardServer = class {
|
|
|
7958
9042
|
});
|
|
7959
9043
|
this.app.delete("/api/sessions", auth, (req, res) => {
|
|
7960
9044
|
const body = req.body;
|
|
7961
|
-
const globalDbPath =
|
|
9045
|
+
const globalDbPath = path16.join(os3.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
|
|
7962
9046
|
if (body?.ids && Array.isArray(body.ids) && body.ids.length > 0) {
|
|
7963
9047
|
const globalStore = new MemoryStore(globalDbPath);
|
|
7964
9048
|
try {
|
|
@@ -7981,7 +9065,7 @@ var DashboardServer = class {
|
|
|
7981
9065
|
});
|
|
7982
9066
|
this.app.delete("/api/runtime", auth, (_req, res) => {
|
|
7983
9067
|
this.store.deleteAllRuntimeNodes();
|
|
7984
|
-
const globalDbPath =
|
|
9068
|
+
const globalDbPath = path16.join(os3.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
|
|
7985
9069
|
const globalStore = new MemoryStore(globalDbPath);
|
|
7986
9070
|
try {
|
|
7987
9071
|
globalStore.deleteAllRuntimeNodes();
|
|
@@ -8043,16 +9127,26 @@ var DashboardServer = class {
|
|
|
8043
9127
|
});
|
|
8044
9128
|
this.app.put("/api/config", auth, async (req, res) => {
|
|
8045
9129
|
const body = req.body;
|
|
8046
|
-
if (body
|
|
8047
|
-
|
|
9130
|
+
if (body["tierLimits"] !== void 0 && (typeof body["tierLimits"] !== "object" || Array.isArray(body["tierLimits"]))) {
|
|
9131
|
+
res.status(400).json({ error: "tierLimits must be an object" });
|
|
9132
|
+
return;
|
|
9133
|
+
}
|
|
9134
|
+
if (body["budget"] !== void 0 && (typeof body["budget"] !== "object" || Array.isArray(body["budget"]))) {
|
|
9135
|
+
res.status(400).json({ error: "budget must be an object" });
|
|
9136
|
+
return;
|
|
9137
|
+
}
|
|
9138
|
+
if (body["tierLimits"]) this.config.tierLimits = { ...this.config.tierLimits, ...body["tierLimits"] };
|
|
9139
|
+
if (body["budget"]) this.config.budget = { ...this.config.budget, ...body["budget"] };
|
|
8048
9140
|
try {
|
|
8049
|
-
const configPath =
|
|
8050
|
-
const existing =
|
|
9141
|
+
const configPath = path16.join(this.workspacePath, CASCADE_CONFIG_FILE);
|
|
9142
|
+
const existing = fs15.existsSync(configPath) ? JSON.parse(fs15.readFileSync(configPath, "utf-8")) : {};
|
|
8051
9143
|
const updated = { ...existing, tierLimits: this.config.tierLimits, budget: this.config.budget };
|
|
8052
|
-
|
|
9144
|
+
const tmp = configPath + ".tmp";
|
|
9145
|
+
fs15.writeFileSync(tmp, JSON.stringify(updated, null, 2), "utf-8");
|
|
9146
|
+
fs15.renameSync(tmp, configPath);
|
|
8053
9147
|
res.json({ ok: true });
|
|
8054
9148
|
} catch (err) {
|
|
8055
|
-
res.status(500).json({ error: `Failed to save config: ${String(err)}` });
|
|
9149
|
+
res.status(500).json({ error: `Failed to save config: ${err instanceof Error ? err.message : String(err)}` });
|
|
8056
9150
|
}
|
|
8057
9151
|
});
|
|
8058
9152
|
this.app.get("/api/runtime/logs/:sessionId", auth, (req, res) => {
|
|
@@ -8077,7 +9171,7 @@ var DashboardServer = class {
|
|
|
8077
9171
|
this.app.get("/api/runtime", auth, (req, res) => {
|
|
8078
9172
|
const scope = req.query["scope"] ?? "workspace";
|
|
8079
9173
|
if (scope === "global") {
|
|
8080
|
-
const globalDbPath =
|
|
9174
|
+
const globalDbPath = path16.join(os3.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
|
|
8081
9175
|
const globalStore = new MemoryStore(globalDbPath);
|
|
8082
9176
|
try {
|
|
8083
9177
|
res.json({
|
|
@@ -8098,7 +9192,7 @@ var DashboardServer = class {
|
|
|
8098
9192
|
logs: this.store.listRuntimeNodeLogs(void 0, void 0, 500)
|
|
8099
9193
|
});
|
|
8100
9194
|
});
|
|
8101
|
-
this.app.post("/api/run", auth, (req, res) => {
|
|
9195
|
+
this.app.post("/api/run", auth, mutationLimiter, (req, res) => {
|
|
8102
9196
|
const body = req.body;
|
|
8103
9197
|
if (!body.prompt || typeof body.prompt !== "string") {
|
|
8104
9198
|
res.status(400).json({ error: "prompt is required" });
|
|
@@ -8119,12 +9213,15 @@ var DashboardServer = class {
|
|
|
8119
9213
|
cascade.on("permission:user-required", (e) => {
|
|
8120
9214
|
this.socket.broadcastToRoom(`session:${sessionId}`, "permission:user-required", { sessionId, ...e });
|
|
8121
9215
|
});
|
|
9216
|
+
cascade.on("peer:message", (e) => {
|
|
9217
|
+
this.socket.emitPeerMessage(e);
|
|
9218
|
+
});
|
|
8122
9219
|
try {
|
|
8123
9220
|
const result = await cascade.run({ prompt: body.prompt, identityId: body.identityId });
|
|
8124
9221
|
this.socket.broadcast("cost:update", {
|
|
8125
9222
|
sessionId,
|
|
8126
|
-
|
|
8127
|
-
|
|
9223
|
+
totalTokens: result.usage.totalTokens,
|
|
9224
|
+
totalCostUsd: result.usage.estimatedCostUsd
|
|
8128
9225
|
});
|
|
8129
9226
|
this.socket.broadcastToRoom(`session:${sessionId}`, "session:complete", { sessionId, result });
|
|
8130
9227
|
this.throttledBroadcast("workspace");
|
|
@@ -8147,13 +9244,13 @@ var DashboardServer = class {
|
|
|
8147
9244
|
}))
|
|
8148
9245
|
});
|
|
8149
9246
|
});
|
|
8150
|
-
const prodPath =
|
|
8151
|
-
const devPath =
|
|
8152
|
-
const webDistPath =
|
|
8153
|
-
if (
|
|
9247
|
+
const prodPath = path16.resolve(__dirname$1, "../web/dist");
|
|
9248
|
+
const devPath = path16.resolve(__dirname$1, "../../web/dist");
|
|
9249
|
+
const webDistPath = fs15.existsSync(prodPath) ? prodPath : devPath;
|
|
9250
|
+
if (fs15.existsSync(webDistPath)) {
|
|
8154
9251
|
this.app.use(express.static(webDistPath));
|
|
8155
9252
|
this.app.get("*", (_req, res) => {
|
|
8156
|
-
res.sendFile(
|
|
9253
|
+
res.sendFile(path16.join(webDistPath, "index.html"));
|
|
8157
9254
|
});
|
|
8158
9255
|
} else {
|
|
8159
9256
|
this.app.get("/", (_req, res) => {
|
|
@@ -8230,7 +9327,7 @@ var TaskScheduler = class {
|
|
|
8230
9327
|
return cron.validate(expression);
|
|
8231
9328
|
}
|
|
8232
9329
|
};
|
|
8233
|
-
var
|
|
9330
|
+
var execFileAsync2 = promisify(execFile);
|
|
8234
9331
|
var SAFE_ENV_NAME = /^[A-Z][A-Z0-9_]*$/;
|
|
8235
9332
|
function sanitizeEnvValue(v) {
|
|
8236
9333
|
const raw = typeof v === "string" ? v : JSON.stringify(v);
|
|
@@ -8269,7 +9366,7 @@ var HooksRunner = class {
|
|
|
8269
9366
|
const isWin = process.platform === "win32";
|
|
8270
9367
|
const shell = isWin ? "cmd.exe" : "/bin/sh";
|
|
8271
9368
|
const shellArgs = isWin ? ["/d", "/s", "/c", hook.command] : ["-c", hook.command];
|
|
8272
|
-
const { stdout } = await
|
|
9369
|
+
const { stdout } = await execFileAsync2(shell, shellArgs, {
|
|
8273
9370
|
timeout: hook.timeout ?? 1e4,
|
|
8274
9371
|
env: { ...process.env, ...envVars },
|
|
8275
9372
|
windowsHide: true
|