cascade-ai 0.13.2 → 0.15.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/dist/cli.cjs +708 -47
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +708 -47
- package/dist/cli.js.map +1 -1
- package/dist/desktop-core.cjs +710 -49
- package/dist/index.cjs +708 -47
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +164 -6
- package/dist/index.d.ts +164 -6
- package/dist/index.js +708 -47
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
package/dist/cli.js
CHANGED
|
@@ -58,7 +58,7 @@ var __export = (target, all) => {
|
|
|
58
58
|
var CASCADE_VERSION, CASCADE_CONFIG_FILE, CASCADE_DB_FILE, CASCADE_DASHBOARD_SECRET_FILE, GLOBAL_CONFIG_DIR, GLOBAL_DB_FILE, GLOBAL_KEYSTORE_FILE, GLOBAL_RUNTIME_DB_FILE, DEFAULT_DASHBOARD_PORT, DEFAULT_CONTEXT_LIMIT, DEFAULT_AUTO_SUMMARIZE_AT, MODELS, T1_MODEL_PRIORITY, T2_MODEL_PRIORITY, T3_MODEL_PRIORITY, VISION_MODEL_PRIORITY, COMPLEXITY_T2_COUNT, THEME_NAMES, DEFAULT_THEME, OLLAMA_BASE_URL, LM_STUDIO_BASE_URL, AZURE_BASE_URL_TEMPLATE, TOOL_NAMES, DEFAULT_APPROVAL_REQUIRED;
|
|
59
59
|
var init_constants = __esm({
|
|
60
60
|
"src/constants.ts"() {
|
|
61
|
-
CASCADE_VERSION = "0.
|
|
61
|
+
CASCADE_VERSION = "0.15.1";
|
|
62
62
|
CASCADE_CONFIG_FILE = ".cascade/config.json";
|
|
63
63
|
CASCADE_DB_FILE = ".cascade/memory.db";
|
|
64
64
|
CASCADE_DASHBOARD_SECRET_FILE = ".cascade/dashboard-secret";
|
|
@@ -1364,29 +1364,71 @@ var init_ollama = __esm({
|
|
|
1364
1364
|
async countTokens(text) {
|
|
1365
1365
|
return Math.ceil(text.length / 4);
|
|
1366
1366
|
}
|
|
1367
|
+
/**
|
|
1368
|
+
* Ask the Ollama server what a model can actually do. Modern Ollama returns a
|
|
1369
|
+
* `capabilities` array ("completion", "tools", "vision", …) and the model's
|
|
1370
|
+
* real context length in `model_info` — authoritative, unlike the hardcoded
|
|
1371
|
+
* family-name allowlist, which silently misclassifies any unlisted family.
|
|
1372
|
+
* Best-effort: null on old servers / errors, callers fall back to heuristics.
|
|
1373
|
+
*/
|
|
1374
|
+
async showModel(name) {
|
|
1375
|
+
const ac = new AbortController();
|
|
1376
|
+
const timer = setTimeout(() => ac.abort(), 2500);
|
|
1377
|
+
try {
|
|
1378
|
+
const resp = await fetch(`${this.baseUrl}/api/show`, {
|
|
1379
|
+
method: "POST",
|
|
1380
|
+
headers: { "Content-Type": "application/json" },
|
|
1381
|
+
body: JSON.stringify({ model: name }),
|
|
1382
|
+
signal: ac.signal
|
|
1383
|
+
});
|
|
1384
|
+
if (!resp.ok) return null;
|
|
1385
|
+
const data = await resp.json();
|
|
1386
|
+
const out = {};
|
|
1387
|
+
if (Array.isArray(data.capabilities)) {
|
|
1388
|
+
out.tools = data.capabilities.includes("tools");
|
|
1389
|
+
out.vision = data.capabilities.includes("vision");
|
|
1390
|
+
}
|
|
1391
|
+
for (const [k, v] of Object.entries(data.model_info ?? {})) {
|
|
1392
|
+
if (k.endsWith(".context_length") && typeof v === "number" && v > 0) {
|
|
1393
|
+
out.contextWindow = v;
|
|
1394
|
+
break;
|
|
1395
|
+
}
|
|
1396
|
+
}
|
|
1397
|
+
return out.tools !== void 0 || out.contextWindow ? out : null;
|
|
1398
|
+
} catch {
|
|
1399
|
+
return null;
|
|
1400
|
+
} finally {
|
|
1401
|
+
clearTimeout(timer);
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1367
1404
|
async listModels() {
|
|
1368
1405
|
try {
|
|
1369
1406
|
const response = await fetch(`${this.baseUrl}/api/tags`);
|
|
1370
1407
|
if (!response.ok) return [];
|
|
1371
1408
|
const data = await response.json();
|
|
1372
1409
|
const supportedKeywords = ["llama3", "llama2", "gemma", "mistral", "mixtral", "qwen", "phi3", "codellama", "deepseek", "llava", "starcoder", "stable-code", "nomic-embed"];
|
|
1373
|
-
|
|
1410
|
+
const entries = data.models.filter((m) => {
|
|
1374
1411
|
const name = m.name.toLowerCase();
|
|
1375
1412
|
return supportedKeywords.some((k) => name.includes(k));
|
|
1376
|
-
})
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1413
|
+
});
|
|
1414
|
+
const shows = await Promise.all(entries.map((m) => this.showModel(m.name)));
|
|
1415
|
+
return entries.map((m, i) => {
|
|
1416
|
+
const show = shows[i];
|
|
1417
|
+
return {
|
|
1418
|
+
id: m.name,
|
|
1419
|
+
name: m.name,
|
|
1420
|
+
provider: "ollama",
|
|
1421
|
+
contextWindow: show?.contextWindow ?? 128e3,
|
|
1422
|
+
isVisionCapable: show?.vision ?? (m.name.includes("llava") || m.name.includes("vision")),
|
|
1423
|
+
inputCostPer1kTokens: 0,
|
|
1424
|
+
outputCostPer1kTokens: 0,
|
|
1425
|
+
maxOutputTokens: 4e3,
|
|
1426
|
+
supportsStreaming: true,
|
|
1427
|
+
isLocal: true,
|
|
1428
|
+
supportsToolUse: show?.tools ?? isToolCapable(m.name),
|
|
1429
|
+
minSizeB: this.parseSizeB(m.details?.parameter_size)
|
|
1430
|
+
};
|
|
1431
|
+
});
|
|
1390
1432
|
} catch {
|
|
1391
1433
|
return [];
|
|
1392
1434
|
}
|
|
@@ -2309,6 +2351,78 @@ Original error: ${err.message}`
|
|
|
2309
2351
|
`).all(`%${query}%`, limit);
|
|
2310
2352
|
return rows.map(this.deserializeMessage);
|
|
2311
2353
|
}
|
|
2354
|
+
// ── Export / Import ──────────────────────────
|
|
2355
|
+
//
|
|
2356
|
+
// Chats travel as full Session objects (messages included) inside a
|
|
2357
|
+
// plaintext-JSON bundle. Import NEVER overwrites: every imported session
|
|
2358
|
+
// gets a fresh id (title suffixed "(imported)") and fresh message ids, so
|
|
2359
|
+
// re-importing the same bundle can duplicate but can never clobber.
|
|
2360
|
+
/** Full sessions (with messages) for the given ids, or ALL sessions. */
|
|
2361
|
+
exportSessions(ids) {
|
|
2362
|
+
const targets = ids && ids.length > 0 ? ids : this.db.prepare("SELECT id FROM sessions ORDER BY updated_at DESC").all().map((r) => r.id);
|
|
2363
|
+
const out = [];
|
|
2364
|
+
for (const id of targets) {
|
|
2365
|
+
const s = this.getSession(id);
|
|
2366
|
+
if (s) out.push(s);
|
|
2367
|
+
}
|
|
2368
|
+
return out;
|
|
2369
|
+
}
|
|
2370
|
+
/**
|
|
2371
|
+
* Import sessions from a bundle. Returns the created `{id, title}` rows so
|
|
2372
|
+
* the caller can mirror them into the runtime session list (the sidebar
|
|
2373
|
+
* reads runtime_sessions, not sessions). Malformed entries are skipped.
|
|
2374
|
+
*/
|
|
2375
|
+
importSessions(sessions) {
|
|
2376
|
+
const out = [];
|
|
2377
|
+
const msgStmt = this.db.prepare(`
|
|
2378
|
+
INSERT INTO messages (id, session_id, role, content, timestamp, tokens, agent_messages)
|
|
2379
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
2380
|
+
`);
|
|
2381
|
+
for (const s of sessions) {
|
|
2382
|
+
if (!s || typeof s !== "object") continue;
|
|
2383
|
+
const newId = randomUUID();
|
|
2384
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2385
|
+
const title = `${String(s.title || "Untitled").slice(0, 200)} (imported)`;
|
|
2386
|
+
const metadata = s.metadata && typeof s.metadata === "object" ? s.metadata : { totalTokens: 0, totalCostUsd: 0, modelsUsed: [], toolsUsed: [], taskCount: 0 };
|
|
2387
|
+
this.db.prepare(`
|
|
2388
|
+
INSERT INTO sessions (id, title, created_at, updated_at, identity_id, workspace_path, metadata)
|
|
2389
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
2390
|
+
`).run(newId, title, s.createdAt ?? now, s.updatedAt ?? now, s.identityId ?? "default", s.workspacePath ?? "", JSON.stringify(metadata));
|
|
2391
|
+
for (const m of Array.isArray(s.messages) ? s.messages : []) {
|
|
2392
|
+
if (!m || m.role !== "user" && m.role !== "assistant" && m.role !== "system") continue;
|
|
2393
|
+
msgStmt.run(
|
|
2394
|
+
randomUUID(),
|
|
2395
|
+
newId,
|
|
2396
|
+
m.role,
|
|
2397
|
+
typeof m.content === "string" ? m.content : JSON.stringify(m.content),
|
|
2398
|
+
m.timestamp ?? now,
|
|
2399
|
+
m.tokens ? JSON.stringify(m.tokens) : null,
|
|
2400
|
+
m.agentMessages ? JSON.stringify(m.agentMessages) : null
|
|
2401
|
+
);
|
|
2402
|
+
}
|
|
2403
|
+
out.push({ id: newId, title });
|
|
2404
|
+
}
|
|
2405
|
+
return out;
|
|
2406
|
+
}
|
|
2407
|
+
/** Import identities/personas, deduped by (case-insensitive) name — an
|
|
2408
|
+
* existing name is skipped, never replaced. Imports are never the default. */
|
|
2409
|
+
importIdentities(identities) {
|
|
2410
|
+
const existing = new Set(this.listIdentities().map((i) => i.name.toLowerCase()));
|
|
2411
|
+
let imported = 0;
|
|
2412
|
+
for (const ident of identities ?? []) {
|
|
2413
|
+
if (!ident?.name || typeof ident.name !== "string") continue;
|
|
2414
|
+
if (existing.has(ident.name.toLowerCase())) continue;
|
|
2415
|
+
this.createIdentity({
|
|
2416
|
+
...ident,
|
|
2417
|
+
id: randomUUID(),
|
|
2418
|
+
isDefault: false,
|
|
2419
|
+
createdAt: ident.createdAt ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
2420
|
+
});
|
|
2421
|
+
existing.add(ident.name.toLowerCase());
|
|
2422
|
+
imported++;
|
|
2423
|
+
}
|
|
2424
|
+
return imported;
|
|
2425
|
+
}
|
|
2312
2426
|
// ── Identities ────────────────────────────────
|
|
2313
2427
|
createIdentity(identity) {
|
|
2314
2428
|
this.db.prepare(`
|
|
@@ -2486,6 +2600,23 @@ Original error: ${err.message}`
|
|
|
2486
2600
|
const row = this.db.prepare("SELECT metadata FROM model_cache WHERE id = ?").get(`${provider}:${modelId}`);
|
|
2487
2601
|
return row ? JSON.parse(row.metadata) : void 0;
|
|
2488
2602
|
}
|
|
2603
|
+
/**
|
|
2604
|
+
* Persist a probed capability verdict (currently: native tool support) into
|
|
2605
|
+
* the model's cached profile, merging with whatever is already recorded.
|
|
2606
|
+
* Read back via getModelProfile — so a model is probed once ever, not once
|
|
2607
|
+
* per run.
|
|
2608
|
+
*/
|
|
2609
|
+
saveModelCapability(modelId, provider, caps) {
|
|
2610
|
+
const cacheKey = `${provider}:${modelId}`;
|
|
2611
|
+
const existing = this.db.prepare("SELECT metadata FROM model_cache WHERE id = ?").get(cacheKey);
|
|
2612
|
+
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 };
|
|
2613
|
+
if (caps.supportsToolUse !== void 0) meta.supportsToolUse = caps.supportsToolUse;
|
|
2614
|
+
this.db.prepare(`
|
|
2615
|
+
INSERT INTO model_cache (id, provider, model_id, name, metadata, updated_at)
|
|
2616
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
2617
|
+
ON CONFLICT(id) DO UPDATE SET metadata = excluded.metadata, updated_at = excluded.updated_at
|
|
2618
|
+
`).run(cacheKey, provider, modelId, meta.name ?? modelId, JSON.stringify(meta), (/* @__PURE__ */ new Date()).toISOString());
|
|
2619
|
+
}
|
|
2489
2620
|
getProfiledModelIds() {
|
|
2490
2621
|
const rows = this.db.prepare(
|
|
2491
2622
|
"SELECT model_id FROM model_cache WHERE json_extract(metadata, '$.specializations') IS NOT NULL"
|
|
@@ -2775,7 +2906,15 @@ var ToolsConfigSchema = z.object({
|
|
|
2775
2906
|
mcpServers: z.array(McpServerConfigSchema).optional(),
|
|
2776
2907
|
mcpTrusted: z.array(z.string()).optional(),
|
|
2777
2908
|
/** Web search backends — at least one should be configured for best results */
|
|
2778
|
-
webSearch: WebSearchConfigSchema.optional()
|
|
2909
|
+
webSearch: WebSearchConfigSchema.optional(),
|
|
2910
|
+
/**
|
|
2911
|
+
* Sandbox runtime for LLM-authored dynamic tools:
|
|
2912
|
+
* - 'isolate': hard V8 isolate (isolated-vm) — no Node globals, true capability
|
|
2913
|
+
* confinement. Requires the optional native `isolated-vm` dependency.
|
|
2914
|
+
* - 'worker': node:worker_threads (resource/kill limits, but not capability-confined).
|
|
2915
|
+
* - 'auto' (default): use the isolate when `isolated-vm` loads, else fall back to worker.
|
|
2916
|
+
*/
|
|
2917
|
+
dynamicToolSandbox: z.enum(["isolate", "worker", "auto"]).default("auto")
|
|
2779
2918
|
});
|
|
2780
2919
|
var HookDefinitionSchema = z.object({
|
|
2781
2920
|
name: z.string().optional(),
|
|
@@ -2887,10 +3026,20 @@ var CascadeConfigSchema = z.object({
|
|
|
2887
3026
|
/**
|
|
2888
3027
|
* Runtime Tool Creation: when true, T3 workers can generate and register new tools
|
|
2889
3028
|
* at runtime via the ToolCreator when no existing tool can handle a required operation.
|
|
2890
|
-
* Generated tools are session-scoped and sandboxed
|
|
3029
|
+
* Generated tools are session-scoped and sandboxed (see tools.dynamicToolSandbox).
|
|
2891
3030
|
* HTTP calls from generated tools require approval.
|
|
2892
3031
|
*/
|
|
2893
3032
|
enableToolCreation: z.boolean().default(true),
|
|
3033
|
+
/**
|
|
3034
|
+
* Project knowledge (world state). `factsExtraction`: after each worker
|
|
3035
|
+
* completes, run a cheap extraction pass that distills its output into
|
|
3036
|
+
* queryable entity/relation/value facts (superseding older facts), which T1
|
|
3037
|
+
* queries during planning instead of replaying the whole linear log. Best-effort
|
|
3038
|
+
* and non-blocking; set false to keep only the raw linear log.
|
|
3039
|
+
*/
|
|
3040
|
+
knowledge: z.object({
|
|
3041
|
+
factsExtraction: z.boolean().default(true)
|
|
3042
|
+
}).default({}),
|
|
2894
3043
|
/**
|
|
2895
3044
|
* Persist runtime-generated tools to .cascade/dynamic-tools.json and reload them
|
|
2896
3045
|
* on startup for cross-run dedup. Reloaded (and peer-received) tools are always
|
|
@@ -3988,6 +4137,7 @@ function normalizeModelId(id) {
|
|
|
3988
4137
|
var LiveDataProvider = class {
|
|
3989
4138
|
snapshot = null;
|
|
3990
4139
|
prices = /* @__PURE__ */ new Map();
|
|
4140
|
+
capabilities = /* @__PURE__ */ new Map();
|
|
3991
4141
|
source = "bundled";
|
|
3992
4142
|
fetchedAt = 0;
|
|
3993
4143
|
loaded = false;
|
|
@@ -4016,6 +4166,9 @@ var LiveDataProvider = class {
|
|
|
4016
4166
|
if (cache.prices) {
|
|
4017
4167
|
for (const [id, p] of Object.entries(cache.prices)) this.prices.set(id, p);
|
|
4018
4168
|
}
|
|
4169
|
+
if (cache.capabilities) {
|
|
4170
|
+
for (const [id, c] of Object.entries(cache.capabilities)) this.capabilities.set(id, c);
|
|
4171
|
+
}
|
|
4019
4172
|
this.fetchedAt = cache.fetchedAt ?? 0;
|
|
4020
4173
|
} catch {
|
|
4021
4174
|
}
|
|
@@ -4036,9 +4189,9 @@ var LiveDataProvider = class {
|
|
|
4036
4189
|
const ttlMs = this.opts.refreshHours * 36e5;
|
|
4037
4190
|
const fresh = ttlMs > 0 && Date.now() - this.fetchedAt < ttlMs;
|
|
4038
4191
|
if (!force && fresh && this.source !== "bundled") return;
|
|
4039
|
-
const [snap,
|
|
4192
|
+
const [snap, catalog] = await Promise.all([
|
|
4040
4193
|
this.opts.live ? this.fetchSnapshot() : Promise.resolve(null),
|
|
4041
|
-
this.opts.pricingLive ? this.
|
|
4194
|
+
this.opts.pricingLive ? this.fetchCatalog() : Promise.resolve(null)
|
|
4042
4195
|
]);
|
|
4043
4196
|
let changed = false;
|
|
4044
4197
|
if (snap) {
|
|
@@ -4046,8 +4199,12 @@ var LiveDataProvider = class {
|
|
|
4046
4199
|
this.source = "live";
|
|
4047
4200
|
changed = true;
|
|
4048
4201
|
}
|
|
4049
|
-
if (
|
|
4050
|
-
this.prices = prices;
|
|
4202
|
+
if (catalog && catalog.prices.size > 0) {
|
|
4203
|
+
this.prices = catalog.prices;
|
|
4204
|
+
changed = true;
|
|
4205
|
+
}
|
|
4206
|
+
if (catalog && catalog.capabilities.size > 0) {
|
|
4207
|
+
this.capabilities = catalog.capabilities;
|
|
4051
4208
|
changed = true;
|
|
4052
4209
|
}
|
|
4053
4210
|
if (changed) {
|
|
@@ -4069,21 +4226,43 @@ var LiveDataProvider = class {
|
|
|
4069
4226
|
return null;
|
|
4070
4227
|
}
|
|
4071
4228
|
}
|
|
4072
|
-
|
|
4229
|
+
/**
|
|
4230
|
+
* One fetch of the OpenRouter catalog yields BOTH pricing and capability
|
|
4231
|
+
* facts (context window, native tool support, input modalities) — the
|
|
4232
|
+
* capability fields used to be discarded while providers guessed/hardcoded
|
|
4233
|
+
* them in their listModels() stubs.
|
|
4234
|
+
*/
|
|
4235
|
+
async fetchCatalog() {
|
|
4073
4236
|
try {
|
|
4074
4237
|
const resp = await withTimeout(fetch(OPENROUTER_MODELS_URL), FETCH_TIMEOUT_MS, "pricing fetch timed out");
|
|
4075
4238
|
if (!resp.ok) return null;
|
|
4076
4239
|
const data = await resp.json();
|
|
4077
4240
|
if (!Array.isArray(data?.data)) return null;
|
|
4078
|
-
const
|
|
4241
|
+
const prices = /* @__PURE__ */ new Map();
|
|
4242
|
+
const capabilities = /* @__PURE__ */ new Map();
|
|
4079
4243
|
for (const m of data.data) {
|
|
4080
|
-
if (!m?.id
|
|
4081
|
-
const
|
|
4082
|
-
|
|
4083
|
-
|
|
4084
|
-
|
|
4244
|
+
if (!m?.id) continue;
|
|
4245
|
+
const key = normalizeModelId(m.id);
|
|
4246
|
+
if (m.pricing) {
|
|
4247
|
+
const input = Number(m.pricing.prompt) * 1e3;
|
|
4248
|
+
const output = Number(m.pricing.completion) * 1e3;
|
|
4249
|
+
if (Number.isFinite(input) && Number.isFinite(output)) {
|
|
4250
|
+
prices.set(key, { input, output });
|
|
4251
|
+
}
|
|
4252
|
+
}
|
|
4253
|
+
const cap = {};
|
|
4254
|
+
if (typeof m.context_length === "number" && m.context_length > 0) {
|
|
4255
|
+
cap.contextWindow = m.context_length;
|
|
4256
|
+
}
|
|
4257
|
+
if (Array.isArray(m.supported_parameters)) {
|
|
4258
|
+
cap.supportsTools = m.supported_parameters.includes("tools");
|
|
4259
|
+
}
|
|
4260
|
+
if (Array.isArray(m.architecture?.input_modalities)) {
|
|
4261
|
+
cap.inputModalities = m.architecture.input_modalities;
|
|
4262
|
+
}
|
|
4263
|
+
if (Object.keys(cap).length > 0) capabilities.set(key, cap);
|
|
4085
4264
|
}
|
|
4086
|
-
return
|
|
4265
|
+
return { prices, capabilities };
|
|
4087
4266
|
} catch {
|
|
4088
4267
|
return null;
|
|
4089
4268
|
}
|
|
@@ -4094,7 +4273,8 @@ var LiveDataProvider = class {
|
|
|
4094
4273
|
const cache = {
|
|
4095
4274
|
fetchedAt: this.fetchedAt,
|
|
4096
4275
|
snapshot: this.snapshot ?? void 0,
|
|
4097
|
-
prices: Object.fromEntries(this.prices)
|
|
4276
|
+
prices: Object.fromEntries(this.prices),
|
|
4277
|
+
capabilities: Object.fromEntries(this.capabilities)
|
|
4098
4278
|
};
|
|
4099
4279
|
await fs9.writeFile(this.opts.cacheFile, JSON.stringify(cache, null, 2), "utf-8");
|
|
4100
4280
|
} catch {
|
|
@@ -4119,6 +4299,27 @@ var LiveDataProvider = class {
|
|
|
4119
4299
|
return { ...m, inputCostPer1kTokens: p.input, outputCostPer1kTokens: p.output };
|
|
4120
4300
|
});
|
|
4121
4301
|
}
|
|
4302
|
+
/** Current capability facts for a model id, or null when unknown. */
|
|
4303
|
+
getCapability(modelId) {
|
|
4304
|
+
return this.capabilities.get(normalizeModelId(modelId)) ?? null;
|
|
4305
|
+
}
|
|
4306
|
+
/**
|
|
4307
|
+
* Returns capability-corrected copies of each model (originals untouched):
|
|
4308
|
+
* real context windows replace the providers' hardcoded guesses, native
|
|
4309
|
+
* tool support replaces the assume-by-provider default, and vision is set
|
|
4310
|
+
* from the declared input modalities.
|
|
4311
|
+
*/
|
|
4312
|
+
applyLiveCapabilities(models) {
|
|
4313
|
+
return models.map((m) => {
|
|
4314
|
+
const c = this.getCapability(m.id);
|
|
4315
|
+
if (!c) return m;
|
|
4316
|
+
const next = { ...m };
|
|
4317
|
+
if (c.contextWindow) next.contextWindow = c.contextWindow;
|
|
4318
|
+
if (c.supportsTools !== void 0) next.supportsToolUse = c.supportsTools;
|
|
4319
|
+
if (c.inputModalities) next.isVisionCapable = c.inputModalities.includes("image");
|
|
4320
|
+
return next;
|
|
4321
|
+
});
|
|
4322
|
+
}
|
|
4122
4323
|
/** Where the active quality data came from — for /why and `cascade models`. */
|
|
4123
4324
|
getDataSource() {
|
|
4124
4325
|
return this.source;
|
|
@@ -4129,6 +4330,9 @@ var LiveDataProvider = class {
|
|
|
4129
4330
|
hasLivePricing() {
|
|
4130
4331
|
return this.prices.size > 0;
|
|
4131
4332
|
}
|
|
4333
|
+
hasCapabilities() {
|
|
4334
|
+
return this.capabilities.size > 0;
|
|
4335
|
+
}
|
|
4132
4336
|
};
|
|
4133
4337
|
|
|
4134
4338
|
// src/core/router/benchmarks.ts
|
|
@@ -4317,6 +4521,57 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter {
|
|
|
4317
4521
|
profiler.profileAll(allModels).catch(() => {
|
|
4318
4522
|
});
|
|
4319
4523
|
}
|
|
4524
|
+
/**
|
|
4525
|
+
* One-time native tool-call probe for local/compat models with NO capability
|
|
4526
|
+
* metadata. llama.cpp / LM Studio "/models" endpoints return ids only, so
|
|
4527
|
+
* `supportsToolUse` stays undefined and the T3 tool gate ASSUMES native
|
|
4528
|
+
* support — wrong for many local builds, which then fumble tool-format
|
|
4529
|
+
* output. Each unknown model is probed once with a trivial tool; the verdict
|
|
4530
|
+
* persists in the MemoryStore's model_cache (via the previously-dormant
|
|
4531
|
+
* getModelProfile read-back), so a model is probed once EVER, not per run.
|
|
4532
|
+
* Cloud providers are never probed — their metadata is authoritative.
|
|
4533
|
+
*/
|
|
4534
|
+
async probeLocalToolSupport(store) {
|
|
4535
|
+
const unknown = this.selector.getAllAvailableModels().filter(
|
|
4536
|
+
(m) => m.provider === "openai-compatible" && m.supportsToolUse === void 0
|
|
4537
|
+
);
|
|
4538
|
+
for (const model of unknown) {
|
|
4539
|
+
const cached = store.getModelProfile(model.id, model.provider);
|
|
4540
|
+
let verdict = cached?.supportsToolUse;
|
|
4541
|
+
if (verdict === void 0) {
|
|
4542
|
+
const probed = await this.probeNativeToolCall(model);
|
|
4543
|
+
if (probed === null) continue;
|
|
4544
|
+
verdict = probed;
|
|
4545
|
+
store.saveModelCapability(model.id, model.provider, { supportsToolUse: verdict });
|
|
4546
|
+
}
|
|
4547
|
+
this.selector.addDynamicModel({ ...model, supportsToolUse: verdict });
|
|
4548
|
+
}
|
|
4549
|
+
for (const tier of ["T1", "T2", "T3"]) {
|
|
4550
|
+
const cur = this.tierModels.get(tier);
|
|
4551
|
+
if (!cur) continue;
|
|
4552
|
+
const fresh = this.selector.getModelById(cur.id);
|
|
4553
|
+
if (fresh) this.tierModels.set(tier, fresh);
|
|
4554
|
+
}
|
|
4555
|
+
}
|
|
4556
|
+
async probeNativeToolCall(model) {
|
|
4557
|
+
try {
|
|
4558
|
+
const cfg = this.config.providers.find((p) => p.type === model.provider) ?? { type: model.provider };
|
|
4559
|
+
const provider = this.createProvider(cfg, model);
|
|
4560
|
+
const result = await withTimeout(provider.generate({
|
|
4561
|
+
messages: [{ role: "user", content: "Call the echo tool with text set to 'ping'. Use the tool; do not answer in prose." }],
|
|
4562
|
+
tools: [{
|
|
4563
|
+
name: "echo",
|
|
4564
|
+
description: "Echo the given text back to the caller.",
|
|
4565
|
+
inputSchema: { type: "object", properties: { text: { type: "string", description: "Text to echo" } }, required: ["text"] }
|
|
4566
|
+
}],
|
|
4567
|
+
maxTokens: 80,
|
|
4568
|
+
temperature: 0
|
|
4569
|
+
}), 3e4, "tool-support probe timed out");
|
|
4570
|
+
return (result.toolCalls?.length ?? 0) > 0;
|
|
4571
|
+
} catch {
|
|
4572
|
+
return null;
|
|
4573
|
+
}
|
|
4574
|
+
}
|
|
4320
4575
|
/**
|
|
4321
4576
|
* Cascade Auto live data: discover/validate real model ids from each cloud
|
|
4322
4577
|
* provider, then fetch current public quality scores + per-token prices and
|
|
@@ -4366,12 +4621,17 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter {
|
|
|
4366
4621
|
await Promise.allSettled(tasks);
|
|
4367
4622
|
}
|
|
4368
4623
|
/**
|
|
4369
|
-
* Replace available models with live-priced
|
|
4370
|
-
*
|
|
4624
|
+
* Replace available models with live-priced AND capability-corrected copies
|
|
4625
|
+
* (real context windows, native tool support, vision from modalities), then
|
|
4626
|
+
* refresh the already resolved tier models so shared-tier cost accounting and
|
|
4627
|
+
* the tool-use gate both see current data.
|
|
4371
4628
|
*/
|
|
4372
4629
|
applyLivePricing() {
|
|
4373
|
-
if (!this.liveData
|
|
4374
|
-
|
|
4630
|
+
if (!this.liveData) return;
|
|
4631
|
+
if (!this.liveData.hasLivePricing() && !this.liveData.hasCapabilities()) return;
|
|
4632
|
+
let updated = this.selector.getAllAvailableModels();
|
|
4633
|
+
if (this.liveData.hasLivePricing()) updated = this.liveData.applyLivePricing(updated);
|
|
4634
|
+
if (this.liveData.hasCapabilities()) updated = this.liveData.applyLiveCapabilities(updated);
|
|
4375
4635
|
for (const m of updated) this.selector.addDynamicModel(m);
|
|
4376
4636
|
for (const tier of ["T1", "T2", "T3"]) {
|
|
4377
4637
|
const cur = this.tierModels.get(tier);
|
|
@@ -4546,6 +4806,10 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter {
|
|
|
4546
4806
|
const r = this.config?.reinforcements;
|
|
4547
4807
|
return { enabled: r?.enabled === true, maxPerSection: r?.maxPerSection ?? 4 };
|
|
4548
4808
|
}
|
|
4809
|
+
/** Project-knowledge settings (config.knowledge). Facts extraction on by default. */
|
|
4810
|
+
getKnowledgeConfig() {
|
|
4811
|
+
return { factsExtraction: this.config?.knowledge?.factsExtraction !== false };
|
|
4812
|
+
}
|
|
4549
4813
|
/**
|
|
4550
4814
|
* Resolved T3 wave execution mode. 'auto' becomes 'sequential' when the T3
|
|
4551
4815
|
* tier resolves to a LOCAL model (the single-GPU queue serializes anyway, so
|
|
@@ -4632,10 +4896,10 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter {
|
|
|
4632
4896
|
* null when Cascade Auto is off (callers then use the shared tier model).
|
|
4633
4897
|
* Pure heuristic — no extra LLM call.
|
|
4634
4898
|
*/
|
|
4635
|
-
async selectModelForSubtask(tier, text) {
|
|
4899
|
+
async selectModelForSubtask(tier, text, opts) {
|
|
4636
4900
|
if (!this.config?.cascadeAuto || !this.taskAnalyzer || !text.trim()) return null;
|
|
4637
4901
|
try {
|
|
4638
|
-
return await this.taskAnalyzer.selectModel(text, tier, this.selector);
|
|
4902
|
+
return await this.taskAnalyzer.selectModel(text, tier, this.selector, opts);
|
|
4639
4903
|
} catch {
|
|
4640
4904
|
return null;
|
|
4641
4905
|
}
|
|
@@ -5391,6 +5655,13 @@ EXAMPLE \u2014 calling the "shell" tool to list files:
|
|
|
5391
5655
|
{"name": "shell", "input": {"command": "ls -la"}}
|
|
5392
5656
|
</tool_call>
|
|
5393
5657
|
|
|
5658
|
+
When you have enough information, stop calling tools and write your final answer.`;
|
|
5659
|
+
}
|
|
5660
|
+
function buildTextToolReminder(tools) {
|
|
5661
|
+
return `
|
|
5662
|
+
TOOL USE REMINDER:
|
|
5663
|
+
Call tools with a single <tool_call>{"name": "<tool_name>", "input": { ... }}</tool_call> block (valid JSON, double quotes, one tool per turn), matching the argument shapes of your earlier calls in this conversation.
|
|
5664
|
+
Available tools: ${tools.map((t) => t.name).join(", ")}.
|
|
5394
5665
|
When you have enough information, stop calling tools and write your final answer.`;
|
|
5395
5666
|
}
|
|
5396
5667
|
|
|
@@ -5630,6 +5901,9 @@ Now execute your subtask using this context where relevant.`
|
|
|
5630
5901
|
} catch (e) {
|
|
5631
5902
|
this.log("Failed to write to World State DB");
|
|
5632
5903
|
}
|
|
5904
|
+
if (this.router.getKnowledgeConfig?.().factsExtraction !== false) {
|
|
5905
|
+
await this.extractAndStoreFacts(db, assignment, output);
|
|
5906
|
+
}
|
|
5633
5907
|
}
|
|
5634
5908
|
this.setStatus("COMPLETED", output);
|
|
5635
5909
|
this.sendStatusUpdate({ progressPct: 100, currentAction: "Subtask complete", status: "IN_PROGRESS", output });
|
|
@@ -5691,7 +5965,7 @@ Now execute your subtask using this context where relevant.`
|
|
|
5691
5965
|
let subtaskModel;
|
|
5692
5966
|
try {
|
|
5693
5967
|
const subtaskText = `${this.assignment?.subtaskTitle ?? ""} ${this.assignment?.description ?? ""} ${this.assignment?.expectedOutput ?? ""}`;
|
|
5694
|
-
subtaskModel = await this.router.selectModelForSubtask("T3", subtaskText) ?? void 0;
|
|
5968
|
+
subtaskModel = await this.router.selectModelForSubtask("T3", subtaskText, { requiresToolUse: tools.length > 0 }) ?? void 0;
|
|
5695
5969
|
if (subtaskModel) {
|
|
5696
5970
|
this.log(`Cascade Auto: routing this subtask to ${subtaskModel.provider}:${subtaskModel.id}`);
|
|
5697
5971
|
}
|
|
@@ -5699,7 +5973,8 @@ Now execute your subtask using this context where relevant.`
|
|
|
5699
5973
|
}
|
|
5700
5974
|
const effectiveModel = subtaskModel ?? this.router.getModelForTier("T3");
|
|
5701
5975
|
const useTextTools = effectiveModel?.supportsToolUse === false && tools.length > 0;
|
|
5702
|
-
|
|
5976
|
+
let sentFullTextContract = false;
|
|
5977
|
+
let textContractSignature = "";
|
|
5703
5978
|
while (iterations < MAX_ITERATIONS) {
|
|
5704
5979
|
iterations++;
|
|
5705
5980
|
this.throwIfCancelled();
|
|
@@ -5713,6 +5988,17 @@ Now execute your subtask using this context where relevant.`
|
|
|
5713
5988
|
${g.text}`
|
|
5714
5989
|
});
|
|
5715
5990
|
}
|
|
5991
|
+
let textToolSuffix = "";
|
|
5992
|
+
if (useTextTools) {
|
|
5993
|
+
const signature = tools.map((t) => t.name).join(",");
|
|
5994
|
+
if (!sentFullTextContract || signature !== textContractSignature) {
|
|
5995
|
+
textToolSuffix = buildTextToolSystemPrompt(tools);
|
|
5996
|
+
sentFullTextContract = true;
|
|
5997
|
+
textContractSignature = signature;
|
|
5998
|
+
} else {
|
|
5999
|
+
textToolSuffix = buildTextToolReminder(tools);
|
|
6000
|
+
}
|
|
6001
|
+
}
|
|
5716
6002
|
const options = {
|
|
5717
6003
|
messages: this.context.getMessages(),
|
|
5718
6004
|
systemPrompt: this.systemPromptOverride + systemPrompt + (this.hierarchyContext ? `
|
|
@@ -6202,6 +6488,41 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
|
6202
6488
|
};
|
|
6203
6489
|
}
|
|
6204
6490
|
}
|
|
6491
|
+
/**
|
|
6492
|
+
* world-state v2: distill a completed subtask's output into durable
|
|
6493
|
+
* `(entity, relation, value)` facts and upsert them into the knowledge graph.
|
|
6494
|
+
* A bounded, cheap T3-tier call; entirely best-effort — any failure is swallowed
|
|
6495
|
+
* so it never blocks or fails the subtask. Respects the subtask's privacy tier
|
|
6496
|
+
* (a local-only subtask extracts on a local model too, never leaking to cloud).
|
|
6497
|
+
*/
|
|
6498
|
+
async extractAndStoreFacts(db, assignment, output) {
|
|
6499
|
+
try {
|
|
6500
|
+
const prompt = `Extract durable project facts from this completed subtask.
|
|
6501
|
+
Return ONLY a JSON array of {"entity","relation","value"} triples describing lasting
|
|
6502
|
+
facts about the codebase/project \u2014 e.g. {"entity":"auth module","relation":"uses","value":"JWT"}.
|
|
6503
|
+
Ignore transient step-by-step details. At most 6 triples. If nothing durable, return [].
|
|
6504
|
+
|
|
6505
|
+
Subtask: ${assignment.subtaskTitle}
|
|
6506
|
+
Output:
|
|
6507
|
+
${output.slice(0, 4e3)}`;
|
|
6508
|
+
const result = await this.router.generate("T3", {
|
|
6509
|
+
messages: [{ role: "user", content: prompt }],
|
|
6510
|
+
maxTokens: 300,
|
|
6511
|
+
temperature: 0,
|
|
6512
|
+
...this.localOnlyMatch ? { forceLocal: true } : {}
|
|
6513
|
+
});
|
|
6514
|
+
const match = /\[[\s\S]*\]/.exec(result.content);
|
|
6515
|
+
if (!match) return;
|
|
6516
|
+
const facts = JSON.parse(match[0]);
|
|
6517
|
+
if (!Array.isArray(facts)) return;
|
|
6518
|
+
for (const f of facts.slice(0, 8)) {
|
|
6519
|
+
if (f && typeof f.entity === "string" && typeof f.relation === "string" && typeof f.value === "string") {
|
|
6520
|
+
db.upsertFact(f.entity, f.relation, f.value, this.id);
|
|
6521
|
+
}
|
|
6522
|
+
}
|
|
6523
|
+
} catch {
|
|
6524
|
+
}
|
|
6525
|
+
}
|
|
6205
6526
|
async correctOutput(originalOutput, failures) {
|
|
6206
6527
|
const correctionPrompt = `The following output failed these checks: ${failures.join(", ")}.
|
|
6207
6528
|
|
|
@@ -7607,10 +7928,24 @@ In 3-5 terse bullets, flag the most important RISKS, GAPS, or over-/under-decomp
|
|
|
7607
7928
|
}
|
|
7608
7929
|
async decomposeTask(prompt, systemContext) {
|
|
7609
7930
|
const db = this.router.getWorldStateDB?.();
|
|
7610
|
-
|
|
7931
|
+
let worldStateContext = "";
|
|
7932
|
+
if (db) {
|
|
7933
|
+
const knowledge = db.getFormattedKnowledge(prompt);
|
|
7934
|
+
if (knowledge) {
|
|
7935
|
+
worldStateContext = `
|
|
7936
|
+
|
|
7937
|
+
PROJECT KNOWLEDGE (relevant facts about this codebase):
|
|
7938
|
+
${knowledge}`;
|
|
7939
|
+
} else {
|
|
7940
|
+
const log = db.getFormattedState();
|
|
7941
|
+
if (log && log !== "World State is currently empty.") {
|
|
7942
|
+
worldStateContext = `
|
|
7611
7943
|
|
|
7612
7944
|
PROJECT WORLD STATE (Current architecture and recent changes):
|
|
7613
|
-
${
|
|
7945
|
+
${log}`;
|
|
7946
|
+
}
|
|
7947
|
+
}
|
|
7948
|
+
}
|
|
7614
7949
|
const contextSection = systemContext ? `
|
|
7615
7950
|
Project context:
|
|
7616
7951
|
${systemContext}` : "";
|
|
@@ -9993,13 +10328,17 @@ var TaskAnalyzer = class {
|
|
|
9993
10328
|
* Scores tier-eligible models using cost efficiency + historical performance.
|
|
9994
10329
|
* Falls back to the priority-list default when no candidates have history.
|
|
9995
10330
|
*/
|
|
9996
|
-
async selectModel(prompt, tier, selector) {
|
|
10331
|
+
async selectModel(prompt, tier, selector, opts) {
|
|
9997
10332
|
const profile = await this.analyze(prompt);
|
|
9998
10333
|
if (profile.requiresVision) {
|
|
9999
10334
|
return selector.selectVisionModel();
|
|
10000
10335
|
}
|
|
10001
|
-
|
|
10336
|
+
let candidates2 = selector.getCandidatesForTier(tier);
|
|
10002
10337
|
if (candidates2.length === 0) return selector.selectForTier(tier);
|
|
10338
|
+
if (opts?.requiresToolUse) {
|
|
10339
|
+
const toolCapable = candidates2.filter((m) => m.supportsToolUse !== false);
|
|
10340
|
+
if (toolCapable.length > 0) candidates2 = toolCapable;
|
|
10341
|
+
}
|
|
10003
10342
|
const scored = candidates2.map((m) => ({
|
|
10004
10343
|
model: m,
|
|
10005
10344
|
score: this.scoreModel(m, profile)
|
|
@@ -10178,6 +10517,28 @@ var ModelPerformanceTracker = class {
|
|
|
10178
10517
|
return Math.max(0.1, 1 - normalised * complexityWeight);
|
|
10179
10518
|
}
|
|
10180
10519
|
};
|
|
10520
|
+
var ivmCache;
|
|
10521
|
+
var ivmWarned = false;
|
|
10522
|
+
async function loadIsolatedVm() {
|
|
10523
|
+
if (ivmCache !== void 0) return ivmCache;
|
|
10524
|
+
try {
|
|
10525
|
+
const specifier = "isolated-vm";
|
|
10526
|
+
const mod = await import(specifier);
|
|
10527
|
+
ivmCache = mod.default ?? mod;
|
|
10528
|
+
} catch {
|
|
10529
|
+
ivmCache = null;
|
|
10530
|
+
}
|
|
10531
|
+
return ivmCache;
|
|
10532
|
+
}
|
|
10533
|
+
function safeJsonParse(text) {
|
|
10534
|
+
if (typeof text !== "string") return {};
|
|
10535
|
+
try {
|
|
10536
|
+
const v = JSON.parse(text);
|
|
10537
|
+
return v && typeof v === "object" ? v : {};
|
|
10538
|
+
} catch {
|
|
10539
|
+
return {};
|
|
10540
|
+
}
|
|
10541
|
+
}
|
|
10181
10542
|
var DYNAMIC_TOOLS_FILE = "dynamic-tools.json";
|
|
10182
10543
|
function normalizeToolSchema(schema) {
|
|
10183
10544
|
if (schema && schema["type"] === "object" && typeof schema["properties"] === "object") {
|
|
@@ -10280,7 +10641,12 @@ var DynamicTool = class extends BaseTool {
|
|
|
10280
10641
|
getEscalator;
|
|
10281
10642
|
/** Untrusted = loaded from disk / a peer; its dangerous calls always re-prompt. */
|
|
10282
10643
|
trusted;
|
|
10283
|
-
|
|
10644
|
+
/** Resolve the configured sandbox mode at call time (default 'auto'). */
|
|
10645
|
+
getSandboxMode;
|
|
10646
|
+
/** Optional diagnostics sink (routed through the host so it survives the Ink TUI). */
|
|
10647
|
+
log;
|
|
10648
|
+
constructor(spec, registry, getEscalator, trusted, getSandboxMode = () => "auto", log = () => {
|
|
10649
|
+
}) {
|
|
10284
10650
|
super();
|
|
10285
10651
|
this.name = spec.name;
|
|
10286
10652
|
this.description = spec.description;
|
|
@@ -10290,6 +10656,8 @@ var DynamicTool = class extends BaseTool {
|
|
|
10290
10656
|
this.registry = registry;
|
|
10291
10657
|
this.getEscalator = getEscalator;
|
|
10292
10658
|
this.trusted = trusted;
|
|
10659
|
+
this.getSandboxMode = getSandboxMode;
|
|
10660
|
+
this.log = log;
|
|
10293
10661
|
}
|
|
10294
10662
|
isDangerous() {
|
|
10295
10663
|
return this._isDangerous;
|
|
@@ -10326,8 +10694,94 @@ var DynamicTool = class extends BaseTool {
|
|
|
10326
10694
|
return `Error calling ${toolName}: ${err instanceof Error ? err.message : String(err)}`;
|
|
10327
10695
|
}
|
|
10328
10696
|
};
|
|
10697
|
+
const mode = this.getSandboxMode();
|
|
10698
|
+
if (mode !== "worker") {
|
|
10699
|
+
const ivm = await loadIsolatedVm();
|
|
10700
|
+
if (ivm) return this.runInIsolate(ivm, input, callTool);
|
|
10701
|
+
if (mode === "isolate" && !ivmWarned) {
|
|
10702
|
+
ivmWarned = true;
|
|
10703
|
+
this.log("[tool-creator] isolated-vm is not available (not installed or failed to build) \u2014 dynamic tools fall back to the worker sandbox, which is NOT capability-confined. Install isolated-vm for a hard isolate.");
|
|
10704
|
+
}
|
|
10705
|
+
}
|
|
10329
10706
|
return this.runInWorker(input, callTool);
|
|
10330
10707
|
}
|
|
10708
|
+
/**
|
|
10709
|
+
* Run the generated code in a hard V8 isolate (isolated-vm). The isolate global
|
|
10710
|
+
* has no Node built-ins, so the code cannot see `process`, `require`, the
|
|
10711
|
+
* filesystem, or the network — it reaches the host ONLY through the injected
|
|
10712
|
+
* `callTool` (escalator-gated on the main thread) and `fetch` (SSRF-guarded via
|
|
10713
|
+
* bridgeFetch). `script.run({ timeout })` bounds synchronous CPU; an outer
|
|
10714
|
+
* wall-clock race + `isolate.dispose()` bounds async runaway (a never-resolving
|
|
10715
|
+
* await), mirroring the worker's terminate().
|
|
10716
|
+
*/
|
|
10717
|
+
async runInIsolate(ivm, input, callTool) {
|
|
10718
|
+
const timeoutMs = Math.max(200, Number(process.env["CASCADE_DYNAMIC_TOOL_TIMEOUT_MS"]) || DYNAMIC_TOOL_TIMEOUT_MS);
|
|
10719
|
+
const isolate = new ivm.Isolate({ memoryLimit: 128 });
|
|
10720
|
+
let disposed = false;
|
|
10721
|
+
const dispose = () => {
|
|
10722
|
+
if (!disposed) {
|
|
10723
|
+
disposed = true;
|
|
10724
|
+
try {
|
|
10725
|
+
isolate.dispose();
|
|
10726
|
+
} catch {
|
|
10727
|
+
}
|
|
10728
|
+
}
|
|
10729
|
+
};
|
|
10730
|
+
try {
|
|
10731
|
+
const context = await isolate.createContext();
|
|
10732
|
+
const jail = context.global;
|
|
10733
|
+
await jail.set("_callTool", new ivm.Reference(async (name, inputJson) => {
|
|
10734
|
+
const out = await callTool(String(name), safeJsonParse(inputJson));
|
|
10735
|
+
return String(out);
|
|
10736
|
+
}));
|
|
10737
|
+
await jail.set("_fetch", new ivm.Reference(async (url, initJson) => {
|
|
10738
|
+
const r = await bridgeFetch(String(url), safeJsonParse(initJson));
|
|
10739
|
+
return JSON.stringify(r);
|
|
10740
|
+
}));
|
|
10741
|
+
await jail.set("_input", new ivm.ExternalCopy(input).copyInto());
|
|
10742
|
+
await jail.set("_code", this.executeCode);
|
|
10743
|
+
const bootstrap = `
|
|
10744
|
+
const callTool = (name, toolInput) => _callTool.apply(undefined,
|
|
10745
|
+
[String(name), JSON.stringify(toolInput || {})],
|
|
10746
|
+
{ result: { promise: true }, arguments: { copy: true } });
|
|
10747
|
+
const fetch = async (url, init) => {
|
|
10748
|
+
const raw = await _fetch.apply(undefined,
|
|
10749
|
+
[String(url), JSON.stringify(init || null)],
|
|
10750
|
+
{ result: { promise: true }, arguments: { copy: true } });
|
|
10751
|
+
const r = JSON.parse(raw);
|
|
10752
|
+
if (r && r.__error) throw new Error(r.__error);
|
|
10753
|
+
return {
|
|
10754
|
+
ok: r.ok, status: r.status, statusText: r.statusText,
|
|
10755
|
+
headers: { get: (k) => (String(k).toLowerCase() === 'content-type' ? r.contentType : null) },
|
|
10756
|
+
text: async () => r.body,
|
|
10757
|
+
json: async () => JSON.parse(r.body),
|
|
10758
|
+
};
|
|
10759
|
+
};
|
|
10760
|
+
const console = { log() {}, error() {} };
|
|
10761
|
+
const AsyncFunction = Object.getPrototypeOf(async function () {}).constructor;
|
|
10762
|
+
const fn = new AsyncFunction('input', 'callTool', 'fetch', 'console', _code);
|
|
10763
|
+
(async () => { const r = await fn(_input, callTool, fetch, console); return String(r == null ? '' : r); })();
|
|
10764
|
+
`;
|
|
10765
|
+
const script = await isolate.compileScript(bootstrap);
|
|
10766
|
+
const runPromise = script.run(context, { timeout: timeoutMs, promise: true }).then((v) => String(v ?? ""));
|
|
10767
|
+
const timeoutPromise = new Promise((resolve) => {
|
|
10768
|
+
const t = setTimeout(() => {
|
|
10769
|
+
dispose();
|
|
10770
|
+
resolve(`Dynamic tool "${this.name}" timed out after ${timeoutMs}ms and was terminated.`);
|
|
10771
|
+
}, timeoutMs + 500);
|
|
10772
|
+
t.unref?.();
|
|
10773
|
+
});
|
|
10774
|
+
return await Promise.race([runPromise, timeoutPromise]);
|
|
10775
|
+
} catch (err) {
|
|
10776
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
10777
|
+
if (/timed? out/i.test(msg)) {
|
|
10778
|
+
return `Dynamic tool "${this.name}" timed out after ${timeoutMs}ms and was terminated.`;
|
|
10779
|
+
}
|
|
10780
|
+
return `Tool error: ${msg}`;
|
|
10781
|
+
} finally {
|
|
10782
|
+
dispose();
|
|
10783
|
+
}
|
|
10784
|
+
}
|
|
10331
10785
|
/** Spawn the worker, service its callTool/fetch bridge, enforce the kill timeout. */
|
|
10332
10786
|
runInWorker(input, callTool) {
|
|
10333
10787
|
const timeoutMs = Math.max(200, Number(process.env["CASCADE_DYNAMIC_TOOL_TIMEOUT_MS"]) || DYNAMIC_TOOL_TIMEOUT_MS);
|
|
@@ -10407,16 +10861,19 @@ var ToolCreator = class {
|
|
|
10407
10861
|
workspacePath;
|
|
10408
10862
|
/** When false, persisted tools are neither loaded nor written. */
|
|
10409
10863
|
persistEnabled;
|
|
10864
|
+
/** Sandbox runtime for generated tools; passed to each DynamicTool. */
|
|
10865
|
+
sandboxMode;
|
|
10410
10866
|
logger;
|
|
10411
10867
|
/** name → spec, for persistence, broadcast, and re-registration. */
|
|
10412
10868
|
specs = /* @__PURE__ */ new Map();
|
|
10413
10869
|
/** capability fingerprint → tool name, so the same need isn't re-generated. */
|
|
10414
10870
|
capabilityIndex = /* @__PURE__ */ new Map();
|
|
10415
|
-
constructor(router, registry, workspacePath, persistEnabled = true) {
|
|
10871
|
+
constructor(router, registry, workspacePath, persistEnabled = true, sandboxMode = "auto") {
|
|
10416
10872
|
this.router = router;
|
|
10417
10873
|
this.registry = registry;
|
|
10418
10874
|
this.workspacePath = workspacePath;
|
|
10419
10875
|
this.persistEnabled = persistEnabled;
|
|
10876
|
+
this.sandboxMode = sandboxMode;
|
|
10420
10877
|
}
|
|
10421
10878
|
setPermissionEscalator(escalator) {
|
|
10422
10879
|
this.escalator = escalator;
|
|
@@ -10504,7 +10961,14 @@ Required capability: ${description.slice(0, 300)}`;
|
|
|
10504
10961
|
this.specs.set(spec.name, spec);
|
|
10505
10962
|
return;
|
|
10506
10963
|
}
|
|
10507
|
-
const tool = new DynamicTool(
|
|
10964
|
+
const tool = new DynamicTool(
|
|
10965
|
+
spec,
|
|
10966
|
+
this.registry,
|
|
10967
|
+
() => this.escalator,
|
|
10968
|
+
trusted,
|
|
10969
|
+
() => this.sandboxMode,
|
|
10970
|
+
(msg) => this.log(msg)
|
|
10971
|
+
);
|
|
10508
10972
|
this.registry.register(tool);
|
|
10509
10973
|
this.specs.set(spec.name, spec);
|
|
10510
10974
|
this.capabilityIndex.set(capabilityKey(`${spec.description}`), spec.name);
|
|
@@ -10553,6 +11017,9 @@ Required capability: ${description.slice(0, 300)}`;
|
|
|
10553
11017
|
return Array.from(this.specs.keys());
|
|
10554
11018
|
}
|
|
10555
11019
|
};
|
|
11020
|
+
function normalizeKey(text) {
|
|
11021
|
+
return text.trim().toLowerCase().replace(/\s+/g, " ");
|
|
11022
|
+
}
|
|
10556
11023
|
var WorldStateDB = class {
|
|
10557
11024
|
constructor(workspacePath, debugMode = false) {
|
|
10558
11025
|
this.workspacePath = workspacePath;
|
|
@@ -10574,6 +11041,16 @@ var WorldStateDB = class {
|
|
|
10574
11041
|
encrypted_payload TEXT NOT NULL
|
|
10575
11042
|
)
|
|
10576
11043
|
`);
|
|
11044
|
+
this.db.exec(`
|
|
11045
|
+
CREATE TABLE IF NOT EXISTS facts (
|
|
11046
|
+
entity TEXT NOT NULL,
|
|
11047
|
+
relation TEXT NOT NULL,
|
|
11048
|
+
encrypted_value TEXT NOT NULL,
|
|
11049
|
+
source_worker TEXT NOT NULL,
|
|
11050
|
+
timestamp TEXT NOT NULL,
|
|
11051
|
+
PRIMARY KEY (entity, relation)
|
|
11052
|
+
)
|
|
11053
|
+
`);
|
|
10577
11054
|
}
|
|
10578
11055
|
workspacePath;
|
|
10579
11056
|
debugMode;
|
|
@@ -10647,6 +11124,131 @@ var WorldStateDB = class {
|
|
|
10647
11124
|
if (entries.length === 0) return "World State is currently empty.";
|
|
10648
11125
|
return entries.map((e, idx) => `[${e.timestamp}] Step ${idx + 1} (${e.workerId}): ${e.summary}`).join("\n");
|
|
10649
11126
|
}
|
|
11127
|
+
// ── world-state v2: queryable facts ──────────────
|
|
11128
|
+
/**
|
|
11129
|
+
* Upsert a fact. `(entity, relation)` is normalized so casing/whitespace don't
|
|
11130
|
+
* fragment the key; an existing pair is superseded (value + provenance updated)
|
|
11131
|
+
* rather than duplicated. Empty entity/relation/value are ignored.
|
|
11132
|
+
*/
|
|
11133
|
+
upsertFact(entity, relation, value, sourceWorker, timestamp) {
|
|
11134
|
+
const e = normalizeKey(entity);
|
|
11135
|
+
const r = normalizeKey(relation);
|
|
11136
|
+
const v = value.trim();
|
|
11137
|
+
if (!e || !r || !v) return;
|
|
11138
|
+
const encrypted = this.encrypt(JSON.stringify({ value: v }));
|
|
11139
|
+
const stmt = this.db.prepare(`
|
|
11140
|
+
INSERT INTO facts (entity, relation, encrypted_value, source_worker, timestamp)
|
|
11141
|
+
VALUES (?, ?, ?, ?, ?)
|
|
11142
|
+
ON CONFLICT(entity, relation) DO UPDATE SET
|
|
11143
|
+
encrypted_value = excluded.encrypted_value,
|
|
11144
|
+
source_worker = excluded.source_worker,
|
|
11145
|
+
timestamp = excluded.timestamp
|
|
11146
|
+
`);
|
|
11147
|
+
stmt.run(e, r, encrypted, sourceWorker, timestamp ?? (/* @__PURE__ */ new Date()).toISOString());
|
|
11148
|
+
this.dumpDebugIfNeeded();
|
|
11149
|
+
}
|
|
11150
|
+
rowToFact(row) {
|
|
11151
|
+
let value;
|
|
11152
|
+
try {
|
|
11153
|
+
value = JSON.parse(this.decrypt(row.encrypted_value)).value;
|
|
11154
|
+
} catch {
|
|
11155
|
+
value = "[Decryption Failed - Payload Corrupted]";
|
|
11156
|
+
}
|
|
11157
|
+
return { entity: row.entity, relation: row.relation, value, sourceWorker: row.source_worker, timestamp: row.timestamp };
|
|
11158
|
+
}
|
|
11159
|
+
/** All facts whose entity matches one of the (normalized) query entities. */
|
|
11160
|
+
getFactsForEntities(entities) {
|
|
11161
|
+
const keys = Array.from(new Set(entities.map(normalizeKey).filter(Boolean)));
|
|
11162
|
+
if (keys.length === 0) return [];
|
|
11163
|
+
const placeholders = keys.map(() => "?").join(", ");
|
|
11164
|
+
const stmt = this.db.prepare(
|
|
11165
|
+
`SELECT entity, relation, encrypted_value, source_worker, timestamp FROM facts WHERE entity IN (${placeholders}) ORDER BY entity ASC, relation ASC`
|
|
11166
|
+
);
|
|
11167
|
+
return stmt.all(...keys).map((row) => this.rowToFact(row));
|
|
11168
|
+
}
|
|
11169
|
+
/** Every fact (used for tests / debug / whole-graph views). */
|
|
11170
|
+
getAllFacts() {
|
|
11171
|
+
const stmt = this.db.prepare("SELECT entity, relation, encrypted_value, source_worker, timestamp FROM facts ORDER BY entity ASC, relation ASC");
|
|
11172
|
+
return stmt.all().map((row) => this.rowToFact(row));
|
|
11173
|
+
}
|
|
11174
|
+
/**
|
|
11175
|
+
* A compact, human/LLM-readable fact block for T1/T2 planning. When `entities`
|
|
11176
|
+
* is given, only facts about those entities are included; otherwise all facts.
|
|
11177
|
+
* Returns '' when there are none so callers can fall back to the linear log.
|
|
11178
|
+
*/
|
|
11179
|
+
getFormattedFacts(entities) {
|
|
11180
|
+
const facts = entities && entities.length > 0 ? this.getFactsForEntities(entities) : this.getAllFacts();
|
|
11181
|
+
if (facts.length === 0) return "";
|
|
11182
|
+
return facts.map((f) => `- ${f.entity} ${f.relation} ${f.value}`).join("\n");
|
|
11183
|
+
}
|
|
11184
|
+
/**
|
|
11185
|
+
* A compact knowledge block for T1/T2 planning. When `prompt` is given, facts
|
|
11186
|
+
* whose entity/relation/value mention a prompt token are preferred (relevance
|
|
11187
|
+
* filter); otherwise, or when nothing matches, all facts are used (capped at
|
|
11188
|
+
* `limit`). Returns '' when there are no facts, so the caller can fall back to
|
|
11189
|
+
* the raw linear log — this replaces replaying the whole log during planning.
|
|
11190
|
+
*/
|
|
11191
|
+
getFormattedKnowledge(prompt, limit = 40) {
|
|
11192
|
+
const all = this.getAllFacts();
|
|
11193
|
+
if (all.length === 0) return "";
|
|
11194
|
+
let selected = all;
|
|
11195
|
+
if (prompt && prompt.trim()) {
|
|
11196
|
+
const tokens = Array.from(new Set(prompt.toLowerCase().match(/[a-z0-9_./-]{3,}/g) ?? []));
|
|
11197
|
+
if (tokens.length > 0) {
|
|
11198
|
+
const matched = all.filter((f) => {
|
|
11199
|
+
const hay = `${f.entity} ${f.relation} ${f.value}`.toLowerCase();
|
|
11200
|
+
return tokens.some((t) => hay.includes(t));
|
|
11201
|
+
});
|
|
11202
|
+
if (matched.length > 0) selected = matched;
|
|
11203
|
+
}
|
|
11204
|
+
}
|
|
11205
|
+
return selected.slice(0, limit).map((f) => `- ${f.entity} ${f.relation} ${f.value}`).join("\n");
|
|
11206
|
+
}
|
|
11207
|
+
// ── Export / Import ──────────────────────────
|
|
11208
|
+
//
|
|
11209
|
+
// Knowledge travels DECRYPTED in the export bundle (a portable plaintext
|
|
11210
|
+
// JSON file — the encryption key never leaves this machine) and re-encrypts
|
|
11211
|
+
// with the LOCAL key on import.
|
|
11212
|
+
exportKnowledge() {
|
|
11213
|
+
return { facts: this.getAllFacts(), worldLog: this.getAllEntries() };
|
|
11214
|
+
}
|
|
11215
|
+
/**
|
|
11216
|
+
* Merge imported knowledge. Facts upsert on (entity, relation) with
|
|
11217
|
+
* newer-timestamp-wins (a local fact newer than the imported one is kept);
|
|
11218
|
+
* log entries append with fresh ids, skipping exact duplicates
|
|
11219
|
+
* (worker + timestamp + summary). Returns counts of what actually landed.
|
|
11220
|
+
*/
|
|
11221
|
+
importKnowledge(data) {
|
|
11222
|
+
let facts = 0;
|
|
11223
|
+
let logEntries = 0;
|
|
11224
|
+
if (Array.isArray(data.facts)) {
|
|
11225
|
+
const local = new Map(this.getAllFacts().map((f) => [`${f.entity}\0${f.relation}`, f.timestamp]));
|
|
11226
|
+
for (const f of data.facts) {
|
|
11227
|
+
if (!f || typeof f.entity !== "string" || typeof f.relation !== "string" || typeof f.value !== "string") continue;
|
|
11228
|
+
const key = `${normalizeKey(f.entity)}\0${normalizeKey(f.relation)}`;
|
|
11229
|
+
const localTs = local.get(key);
|
|
11230
|
+
const importTs = f.timestamp ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
11231
|
+
if (localTs && localTs >= importTs) continue;
|
|
11232
|
+
this.upsertFact(f.entity, f.relation, f.value, f.sourceWorker ?? "imported", importTs);
|
|
11233
|
+
facts++;
|
|
11234
|
+
}
|
|
11235
|
+
}
|
|
11236
|
+
if (Array.isArray(data.worldLog)) {
|
|
11237
|
+
const seen = new Set(this.getAllEntries().map((e) => `${e.workerId}\0${e.timestamp}\0${e.summary}`));
|
|
11238
|
+
const stmt = this.db.prepare("INSERT INTO world_state (id, timestamp, worker_id, encrypted_payload) VALUES (?, ?, ?, ?)");
|
|
11239
|
+
for (const e of data.worldLog) {
|
|
11240
|
+
if (!e || typeof e.summary !== "string") continue;
|
|
11241
|
+
const workerId = e.workerId ?? "imported";
|
|
11242
|
+
const timestamp = e.timestamp ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
11243
|
+
const key = `${workerId}\0${timestamp}\0${e.summary}`;
|
|
11244
|
+
if (seen.has(key)) continue;
|
|
11245
|
+
stmt.run(crypto.randomUUID(), timestamp, workerId, this.encrypt(JSON.stringify({ summary: e.summary })));
|
|
11246
|
+
seen.add(key);
|
|
11247
|
+
logEntries++;
|
|
11248
|
+
}
|
|
11249
|
+
}
|
|
11250
|
+
return { facts, logEntries };
|
|
11251
|
+
}
|
|
10650
11252
|
dumpDebugIfNeeded() {
|
|
10651
11253
|
if (!this.debugMode) return;
|
|
10652
11254
|
try {
|
|
@@ -10783,7 +11385,8 @@ var Cascade = class _Cascade extends EventEmitter {
|
|
|
10783
11385
|
}
|
|
10784
11386
|
const cfg = this.config;
|
|
10785
11387
|
if (cfg["enableToolCreation"] === true) {
|
|
10786
|
-
|
|
11388
|
+
const sandboxMode = this.config.tools?.dynamicToolSandbox ?? "auto";
|
|
11389
|
+
this.toolCreator = new ToolCreator(this.router, this.toolRegistry, this.workspacePath, cfg["persistDynamicTools"] !== false, sandboxMode);
|
|
10787
11390
|
this.toolCreator.setLogger((m) => {
|
|
10788
11391
|
if (this.listenerCount("log") > 0) this.emit("log", { level: "info", message: m });
|
|
10789
11392
|
});
|
|
@@ -11021,6 +11624,10 @@ ${last.partialOutput}` : "");
|
|
|
11021
11624
|
this.router.profileModels(this.store).catch(() => {
|
|
11022
11625
|
});
|
|
11023
11626
|
}
|
|
11627
|
+
if (this.store) {
|
|
11628
|
+
this.router.probeLocalToolSupport(this.store).catch(() => {
|
|
11629
|
+
});
|
|
11630
|
+
}
|
|
11024
11631
|
if (this.config.cascadeAuto) {
|
|
11025
11632
|
this.router.refreshLiveData().catch(() => {
|
|
11026
11633
|
});
|
|
@@ -15974,6 +16581,60 @@ ${prompt}`;
|
|
|
15974
16581
|
this.socket.broadcast("runtime:refresh", { scope: "global" });
|
|
15975
16582
|
res.json({ ok: true });
|
|
15976
16583
|
});
|
|
16584
|
+
this.app.get("/api/export", auth, (req, res) => {
|
|
16585
|
+
try {
|
|
16586
|
+
const sessionsParam = typeof req.query["sessions"] === "string" ? req.query["sessions"] : "all";
|
|
16587
|
+
const includeMemories = req.query["memories"] === "1" || req.query["memories"] === "true";
|
|
16588
|
+
const ids = sessionsParam === "all" ? void 0 : sessionsParam.split(",").map((s) => s.trim()).filter(Boolean);
|
|
16589
|
+
const bundle = {
|
|
16590
|
+
format: "cascade-export@1",
|
|
16591
|
+
exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
16592
|
+
sessions: this.store.exportSessions(ids)
|
|
16593
|
+
};
|
|
16594
|
+
if (includeMemories) {
|
|
16595
|
+
const ws = new WorldStateDB(this.workspacePath);
|
|
16596
|
+
try {
|
|
16597
|
+
bundle["memories"] = { ...ws.exportKnowledge(), identities: this.store.listIdentities() };
|
|
16598
|
+
} finally {
|
|
16599
|
+
ws.close();
|
|
16600
|
+
}
|
|
16601
|
+
}
|
|
16602
|
+
res.json(bundle);
|
|
16603
|
+
} catch (err) {
|
|
16604
|
+
res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
|
|
16605
|
+
}
|
|
16606
|
+
});
|
|
16607
|
+
this.app.post("/api/import", auth, mutationLimiter, (req, res) => {
|
|
16608
|
+
try {
|
|
16609
|
+
const bundle = req.body;
|
|
16610
|
+
if (!bundle || bundle.format !== "cascade-export@1") {
|
|
16611
|
+
res.status(400).json({ error: "Not a cascade-export@1 bundle." });
|
|
16612
|
+
return;
|
|
16613
|
+
}
|
|
16614
|
+
const importedSessions = Array.isArray(bundle.sessions) ? this.store.importSessions(bundle.sessions) : [];
|
|
16615
|
+
for (const s of importedSessions) this.persistRuntimeRow(s.id, s.title, "COMPLETED");
|
|
16616
|
+
let facts = 0;
|
|
16617
|
+
let logEntries = 0;
|
|
16618
|
+
let identities = 0;
|
|
16619
|
+
if (bundle.memories) {
|
|
16620
|
+
const ws = new WorldStateDB(this.workspacePath);
|
|
16621
|
+
try {
|
|
16622
|
+
const counts = ws.importKnowledge(bundle.memories);
|
|
16623
|
+
facts = counts.facts;
|
|
16624
|
+
logEntries = counts.logEntries;
|
|
16625
|
+
} finally {
|
|
16626
|
+
ws.close();
|
|
16627
|
+
}
|
|
16628
|
+
if (Array.isArray(bundle.memories.identities)) {
|
|
16629
|
+
identities = this.store.importIdentities(bundle.memories.identities);
|
|
16630
|
+
}
|
|
16631
|
+
}
|
|
16632
|
+
this.socket.broadcast("runtime:refresh", { scope: "workspace" });
|
|
16633
|
+
res.json({ ok: true, imported: { sessions: importedSessions.length, facts, logEntries, identities } });
|
|
16634
|
+
} catch (err) {
|
|
16635
|
+
res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
|
|
16636
|
+
}
|
|
16637
|
+
});
|
|
15977
16638
|
this.app.post("/api/sessions/:id/rollback", auth, mutationLimiter, async (req, res) => {
|
|
15978
16639
|
const sessionId = req.params.id;
|
|
15979
16640
|
const taskIds = this.sessionTaskIds.get(sessionId) ?? [];
|