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.cjs
CHANGED
|
@@ -109,7 +109,7 @@ var __export = (target, all) => {
|
|
|
109
109
|
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;
|
|
110
110
|
var init_constants = __esm({
|
|
111
111
|
"src/constants.ts"() {
|
|
112
|
-
CASCADE_VERSION = "0.
|
|
112
|
+
CASCADE_VERSION = "0.15.1";
|
|
113
113
|
CASCADE_CONFIG_FILE = ".cascade/config.json";
|
|
114
114
|
CASCADE_DB_FILE = ".cascade/memory.db";
|
|
115
115
|
CASCADE_DASHBOARD_SECRET_FILE = ".cascade/dashboard-secret";
|
|
@@ -1415,29 +1415,71 @@ var init_ollama = __esm({
|
|
|
1415
1415
|
async countTokens(text) {
|
|
1416
1416
|
return Math.ceil(text.length / 4);
|
|
1417
1417
|
}
|
|
1418
|
+
/**
|
|
1419
|
+
* Ask the Ollama server what a model can actually do. Modern Ollama returns a
|
|
1420
|
+
* `capabilities` array ("completion", "tools", "vision", …) and the model's
|
|
1421
|
+
* real context length in `model_info` — authoritative, unlike the hardcoded
|
|
1422
|
+
* family-name allowlist, which silently misclassifies any unlisted family.
|
|
1423
|
+
* Best-effort: null on old servers / errors, callers fall back to heuristics.
|
|
1424
|
+
*/
|
|
1425
|
+
async showModel(name) {
|
|
1426
|
+
const ac = new AbortController();
|
|
1427
|
+
const timer = setTimeout(() => ac.abort(), 2500);
|
|
1428
|
+
try {
|
|
1429
|
+
const resp = await fetch(`${this.baseUrl}/api/show`, {
|
|
1430
|
+
method: "POST",
|
|
1431
|
+
headers: { "Content-Type": "application/json" },
|
|
1432
|
+
body: JSON.stringify({ model: name }),
|
|
1433
|
+
signal: ac.signal
|
|
1434
|
+
});
|
|
1435
|
+
if (!resp.ok) return null;
|
|
1436
|
+
const data = await resp.json();
|
|
1437
|
+
const out = {};
|
|
1438
|
+
if (Array.isArray(data.capabilities)) {
|
|
1439
|
+
out.tools = data.capabilities.includes("tools");
|
|
1440
|
+
out.vision = data.capabilities.includes("vision");
|
|
1441
|
+
}
|
|
1442
|
+
for (const [k, v] of Object.entries(data.model_info ?? {})) {
|
|
1443
|
+
if (k.endsWith(".context_length") && typeof v === "number" && v > 0) {
|
|
1444
|
+
out.contextWindow = v;
|
|
1445
|
+
break;
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
return out.tools !== void 0 || out.contextWindow ? out : null;
|
|
1449
|
+
} catch {
|
|
1450
|
+
return null;
|
|
1451
|
+
} finally {
|
|
1452
|
+
clearTimeout(timer);
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1418
1455
|
async listModels() {
|
|
1419
1456
|
try {
|
|
1420
1457
|
const response = await fetch(`${this.baseUrl}/api/tags`);
|
|
1421
1458
|
if (!response.ok) return [];
|
|
1422
1459
|
const data = await response.json();
|
|
1423
1460
|
const supportedKeywords = ["llama3", "llama2", "gemma", "mistral", "mixtral", "qwen", "phi3", "codellama", "deepseek", "llava", "starcoder", "stable-code", "nomic-embed"];
|
|
1424
|
-
|
|
1461
|
+
const entries = data.models.filter((m) => {
|
|
1425
1462
|
const name = m.name.toLowerCase();
|
|
1426
1463
|
return supportedKeywords.some((k) => name.includes(k));
|
|
1427
|
-
})
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1464
|
+
});
|
|
1465
|
+
const shows = await Promise.all(entries.map((m) => this.showModel(m.name)));
|
|
1466
|
+
return entries.map((m, i) => {
|
|
1467
|
+
const show = shows[i];
|
|
1468
|
+
return {
|
|
1469
|
+
id: m.name,
|
|
1470
|
+
name: m.name,
|
|
1471
|
+
provider: "ollama",
|
|
1472
|
+
contextWindow: show?.contextWindow ?? 128e3,
|
|
1473
|
+
isVisionCapable: show?.vision ?? (m.name.includes("llava") || m.name.includes("vision")),
|
|
1474
|
+
inputCostPer1kTokens: 0,
|
|
1475
|
+
outputCostPer1kTokens: 0,
|
|
1476
|
+
maxOutputTokens: 4e3,
|
|
1477
|
+
supportsStreaming: true,
|
|
1478
|
+
isLocal: true,
|
|
1479
|
+
supportsToolUse: show?.tools ?? isToolCapable(m.name),
|
|
1480
|
+
minSizeB: this.parseSizeB(m.details?.parameter_size)
|
|
1481
|
+
};
|
|
1482
|
+
});
|
|
1441
1483
|
} catch {
|
|
1442
1484
|
return [];
|
|
1443
1485
|
}
|
|
@@ -2360,6 +2402,78 @@ Original error: ${err.message}`
|
|
|
2360
2402
|
`).all(`%${query}%`, limit);
|
|
2361
2403
|
return rows.map(this.deserializeMessage);
|
|
2362
2404
|
}
|
|
2405
|
+
// ── Export / Import ──────────────────────────
|
|
2406
|
+
//
|
|
2407
|
+
// Chats travel as full Session objects (messages included) inside a
|
|
2408
|
+
// plaintext-JSON bundle. Import NEVER overwrites: every imported session
|
|
2409
|
+
// gets a fresh id (title suffixed "(imported)") and fresh message ids, so
|
|
2410
|
+
// re-importing the same bundle can duplicate but can never clobber.
|
|
2411
|
+
/** Full sessions (with messages) for the given ids, or ALL sessions. */
|
|
2412
|
+
exportSessions(ids) {
|
|
2413
|
+
const targets = ids && ids.length > 0 ? ids : this.db.prepare("SELECT id FROM sessions ORDER BY updated_at DESC").all().map((r) => r.id);
|
|
2414
|
+
const out = [];
|
|
2415
|
+
for (const id of targets) {
|
|
2416
|
+
const s = this.getSession(id);
|
|
2417
|
+
if (s) out.push(s);
|
|
2418
|
+
}
|
|
2419
|
+
return out;
|
|
2420
|
+
}
|
|
2421
|
+
/**
|
|
2422
|
+
* Import sessions from a bundle. Returns the created `{id, title}` rows so
|
|
2423
|
+
* the caller can mirror them into the runtime session list (the sidebar
|
|
2424
|
+
* reads runtime_sessions, not sessions). Malformed entries are skipped.
|
|
2425
|
+
*/
|
|
2426
|
+
importSessions(sessions) {
|
|
2427
|
+
const out = [];
|
|
2428
|
+
const msgStmt = this.db.prepare(`
|
|
2429
|
+
INSERT INTO messages (id, session_id, role, content, timestamp, tokens, agent_messages)
|
|
2430
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
2431
|
+
`);
|
|
2432
|
+
for (const s of sessions) {
|
|
2433
|
+
if (!s || typeof s !== "object") continue;
|
|
2434
|
+
const newId = crypto.randomUUID();
|
|
2435
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2436
|
+
const title = `${String(s.title || "Untitled").slice(0, 200)} (imported)`;
|
|
2437
|
+
const metadata = s.metadata && typeof s.metadata === "object" ? s.metadata : { totalTokens: 0, totalCostUsd: 0, modelsUsed: [], toolsUsed: [], taskCount: 0 };
|
|
2438
|
+
this.db.prepare(`
|
|
2439
|
+
INSERT INTO sessions (id, title, created_at, updated_at, identity_id, workspace_path, metadata)
|
|
2440
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
2441
|
+
`).run(newId, title, s.createdAt ?? now, s.updatedAt ?? now, s.identityId ?? "default", s.workspacePath ?? "", JSON.stringify(metadata));
|
|
2442
|
+
for (const m of Array.isArray(s.messages) ? s.messages : []) {
|
|
2443
|
+
if (!m || m.role !== "user" && m.role !== "assistant" && m.role !== "system") continue;
|
|
2444
|
+
msgStmt.run(
|
|
2445
|
+
crypto.randomUUID(),
|
|
2446
|
+
newId,
|
|
2447
|
+
m.role,
|
|
2448
|
+
typeof m.content === "string" ? m.content : JSON.stringify(m.content),
|
|
2449
|
+
m.timestamp ?? now,
|
|
2450
|
+
m.tokens ? JSON.stringify(m.tokens) : null,
|
|
2451
|
+
m.agentMessages ? JSON.stringify(m.agentMessages) : null
|
|
2452
|
+
);
|
|
2453
|
+
}
|
|
2454
|
+
out.push({ id: newId, title });
|
|
2455
|
+
}
|
|
2456
|
+
return out;
|
|
2457
|
+
}
|
|
2458
|
+
/** Import identities/personas, deduped by (case-insensitive) name — an
|
|
2459
|
+
* existing name is skipped, never replaced. Imports are never the default. */
|
|
2460
|
+
importIdentities(identities) {
|
|
2461
|
+
const existing = new Set(this.listIdentities().map((i) => i.name.toLowerCase()));
|
|
2462
|
+
let imported = 0;
|
|
2463
|
+
for (const ident of identities ?? []) {
|
|
2464
|
+
if (!ident?.name || typeof ident.name !== "string") continue;
|
|
2465
|
+
if (existing.has(ident.name.toLowerCase())) continue;
|
|
2466
|
+
this.createIdentity({
|
|
2467
|
+
...ident,
|
|
2468
|
+
id: crypto.randomUUID(),
|
|
2469
|
+
isDefault: false,
|
|
2470
|
+
createdAt: ident.createdAt ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
2471
|
+
});
|
|
2472
|
+
existing.add(ident.name.toLowerCase());
|
|
2473
|
+
imported++;
|
|
2474
|
+
}
|
|
2475
|
+
return imported;
|
|
2476
|
+
}
|
|
2363
2477
|
// ── Identities ────────────────────────────────
|
|
2364
2478
|
createIdentity(identity) {
|
|
2365
2479
|
this.db.prepare(`
|
|
@@ -2537,6 +2651,23 @@ Original error: ${err.message}`
|
|
|
2537
2651
|
const row = this.db.prepare("SELECT metadata FROM model_cache WHERE id = ?").get(`${provider}:${modelId}`);
|
|
2538
2652
|
return row ? JSON.parse(row.metadata) : void 0;
|
|
2539
2653
|
}
|
|
2654
|
+
/**
|
|
2655
|
+
* Persist a probed capability verdict (currently: native tool support) into
|
|
2656
|
+
* the model's cached profile, merging with whatever is already recorded.
|
|
2657
|
+
* Read back via getModelProfile — so a model is probed once ever, not once
|
|
2658
|
+
* per run.
|
|
2659
|
+
*/
|
|
2660
|
+
saveModelCapability(modelId, provider, caps) {
|
|
2661
|
+
const cacheKey = `${provider}:${modelId}`;
|
|
2662
|
+
const existing = this.db.prepare("SELECT metadata FROM model_cache WHERE id = ?").get(cacheKey);
|
|
2663
|
+
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 };
|
|
2664
|
+
if (caps.supportsToolUse !== void 0) meta.supportsToolUse = caps.supportsToolUse;
|
|
2665
|
+
this.db.prepare(`
|
|
2666
|
+
INSERT INTO model_cache (id, provider, model_id, name, metadata, updated_at)
|
|
2667
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
2668
|
+
ON CONFLICT(id) DO UPDATE SET metadata = excluded.metadata, updated_at = excluded.updated_at
|
|
2669
|
+
`).run(cacheKey, provider, modelId, meta.name ?? modelId, JSON.stringify(meta), (/* @__PURE__ */ new Date()).toISOString());
|
|
2670
|
+
}
|
|
2540
2671
|
getProfiledModelIds() {
|
|
2541
2672
|
const rows = this.db.prepare(
|
|
2542
2673
|
"SELECT model_id FROM model_cache WHERE json_extract(metadata, '$.specializations') IS NOT NULL"
|
|
@@ -2826,7 +2957,15 @@ var ToolsConfigSchema = zod.z.object({
|
|
|
2826
2957
|
mcpServers: zod.z.array(McpServerConfigSchema).optional(),
|
|
2827
2958
|
mcpTrusted: zod.z.array(zod.z.string()).optional(),
|
|
2828
2959
|
/** Web search backends — at least one should be configured for best results */
|
|
2829
|
-
webSearch: WebSearchConfigSchema.optional()
|
|
2960
|
+
webSearch: WebSearchConfigSchema.optional(),
|
|
2961
|
+
/**
|
|
2962
|
+
* Sandbox runtime for LLM-authored dynamic tools:
|
|
2963
|
+
* - 'isolate': hard V8 isolate (isolated-vm) — no Node globals, true capability
|
|
2964
|
+
* confinement. Requires the optional native `isolated-vm` dependency.
|
|
2965
|
+
* - 'worker': node:worker_threads (resource/kill limits, but not capability-confined).
|
|
2966
|
+
* - 'auto' (default): use the isolate when `isolated-vm` loads, else fall back to worker.
|
|
2967
|
+
*/
|
|
2968
|
+
dynamicToolSandbox: zod.z.enum(["isolate", "worker", "auto"]).default("auto")
|
|
2830
2969
|
});
|
|
2831
2970
|
var HookDefinitionSchema = zod.z.object({
|
|
2832
2971
|
name: zod.z.string().optional(),
|
|
@@ -2938,10 +3077,20 @@ var CascadeConfigSchema = zod.z.object({
|
|
|
2938
3077
|
/**
|
|
2939
3078
|
* Runtime Tool Creation: when true, T3 workers can generate and register new tools
|
|
2940
3079
|
* at runtime via the ToolCreator when no existing tool can handle a required operation.
|
|
2941
|
-
* Generated tools are session-scoped and sandboxed
|
|
3080
|
+
* Generated tools are session-scoped and sandboxed (see tools.dynamicToolSandbox).
|
|
2942
3081
|
* HTTP calls from generated tools require approval.
|
|
2943
3082
|
*/
|
|
2944
3083
|
enableToolCreation: zod.z.boolean().default(true),
|
|
3084
|
+
/**
|
|
3085
|
+
* Project knowledge (world state). `factsExtraction`: after each worker
|
|
3086
|
+
* completes, run a cheap extraction pass that distills its output into
|
|
3087
|
+
* queryable entity/relation/value facts (superseding older facts), which T1
|
|
3088
|
+
* queries during planning instead of replaying the whole linear log. Best-effort
|
|
3089
|
+
* and non-blocking; set false to keep only the raw linear log.
|
|
3090
|
+
*/
|
|
3091
|
+
knowledge: zod.z.object({
|
|
3092
|
+
factsExtraction: zod.z.boolean().default(true)
|
|
3093
|
+
}).default({}),
|
|
2945
3094
|
/**
|
|
2946
3095
|
* Persist runtime-generated tools to .cascade/dynamic-tools.json and reload them
|
|
2947
3096
|
* on startup for cross-run dedup. Reloaded (and peer-received) tools are always
|
|
@@ -4039,6 +4188,7 @@ function normalizeModelId(id) {
|
|
|
4039
4188
|
var LiveDataProvider = class {
|
|
4040
4189
|
snapshot = null;
|
|
4041
4190
|
prices = /* @__PURE__ */ new Map();
|
|
4191
|
+
capabilities = /* @__PURE__ */ new Map();
|
|
4042
4192
|
source = "bundled";
|
|
4043
4193
|
fetchedAt = 0;
|
|
4044
4194
|
loaded = false;
|
|
@@ -4067,6 +4217,9 @@ var LiveDataProvider = class {
|
|
|
4067
4217
|
if (cache.prices) {
|
|
4068
4218
|
for (const [id, p] of Object.entries(cache.prices)) this.prices.set(id, p);
|
|
4069
4219
|
}
|
|
4220
|
+
if (cache.capabilities) {
|
|
4221
|
+
for (const [id, c] of Object.entries(cache.capabilities)) this.capabilities.set(id, c);
|
|
4222
|
+
}
|
|
4070
4223
|
this.fetchedAt = cache.fetchedAt ?? 0;
|
|
4071
4224
|
} catch {
|
|
4072
4225
|
}
|
|
@@ -4087,9 +4240,9 @@ var LiveDataProvider = class {
|
|
|
4087
4240
|
const ttlMs = this.opts.refreshHours * 36e5;
|
|
4088
4241
|
const fresh = ttlMs > 0 && Date.now() - this.fetchedAt < ttlMs;
|
|
4089
4242
|
if (!force && fresh && this.source !== "bundled") return;
|
|
4090
|
-
const [snap,
|
|
4243
|
+
const [snap, catalog] = await Promise.all([
|
|
4091
4244
|
this.opts.live ? this.fetchSnapshot() : Promise.resolve(null),
|
|
4092
|
-
this.opts.pricingLive ? this.
|
|
4245
|
+
this.opts.pricingLive ? this.fetchCatalog() : Promise.resolve(null)
|
|
4093
4246
|
]);
|
|
4094
4247
|
let changed = false;
|
|
4095
4248
|
if (snap) {
|
|
@@ -4097,8 +4250,12 @@ var LiveDataProvider = class {
|
|
|
4097
4250
|
this.source = "live";
|
|
4098
4251
|
changed = true;
|
|
4099
4252
|
}
|
|
4100
|
-
if (
|
|
4101
|
-
this.prices = prices;
|
|
4253
|
+
if (catalog && catalog.prices.size > 0) {
|
|
4254
|
+
this.prices = catalog.prices;
|
|
4255
|
+
changed = true;
|
|
4256
|
+
}
|
|
4257
|
+
if (catalog && catalog.capabilities.size > 0) {
|
|
4258
|
+
this.capabilities = catalog.capabilities;
|
|
4102
4259
|
changed = true;
|
|
4103
4260
|
}
|
|
4104
4261
|
if (changed) {
|
|
@@ -4120,21 +4277,43 @@ var LiveDataProvider = class {
|
|
|
4120
4277
|
return null;
|
|
4121
4278
|
}
|
|
4122
4279
|
}
|
|
4123
|
-
|
|
4280
|
+
/**
|
|
4281
|
+
* One fetch of the OpenRouter catalog yields BOTH pricing and capability
|
|
4282
|
+
* facts (context window, native tool support, input modalities) — the
|
|
4283
|
+
* capability fields used to be discarded while providers guessed/hardcoded
|
|
4284
|
+
* them in their listModels() stubs.
|
|
4285
|
+
*/
|
|
4286
|
+
async fetchCatalog() {
|
|
4124
4287
|
try {
|
|
4125
4288
|
const resp = await withTimeout(fetch(OPENROUTER_MODELS_URL), FETCH_TIMEOUT_MS, "pricing fetch timed out");
|
|
4126
4289
|
if (!resp.ok) return null;
|
|
4127
4290
|
const data = await resp.json();
|
|
4128
4291
|
if (!Array.isArray(data?.data)) return null;
|
|
4129
|
-
const
|
|
4292
|
+
const prices = /* @__PURE__ */ new Map();
|
|
4293
|
+
const capabilities = /* @__PURE__ */ new Map();
|
|
4130
4294
|
for (const m of data.data) {
|
|
4131
|
-
if (!m?.id
|
|
4132
|
-
const
|
|
4133
|
-
|
|
4134
|
-
|
|
4135
|
-
|
|
4295
|
+
if (!m?.id) continue;
|
|
4296
|
+
const key = normalizeModelId(m.id);
|
|
4297
|
+
if (m.pricing) {
|
|
4298
|
+
const input = Number(m.pricing.prompt) * 1e3;
|
|
4299
|
+
const output = Number(m.pricing.completion) * 1e3;
|
|
4300
|
+
if (Number.isFinite(input) && Number.isFinite(output)) {
|
|
4301
|
+
prices.set(key, { input, output });
|
|
4302
|
+
}
|
|
4303
|
+
}
|
|
4304
|
+
const cap = {};
|
|
4305
|
+
if (typeof m.context_length === "number" && m.context_length > 0) {
|
|
4306
|
+
cap.contextWindow = m.context_length;
|
|
4307
|
+
}
|
|
4308
|
+
if (Array.isArray(m.supported_parameters)) {
|
|
4309
|
+
cap.supportsTools = m.supported_parameters.includes("tools");
|
|
4310
|
+
}
|
|
4311
|
+
if (Array.isArray(m.architecture?.input_modalities)) {
|
|
4312
|
+
cap.inputModalities = m.architecture.input_modalities;
|
|
4313
|
+
}
|
|
4314
|
+
if (Object.keys(cap).length > 0) capabilities.set(key, cap);
|
|
4136
4315
|
}
|
|
4137
|
-
return
|
|
4316
|
+
return { prices, capabilities };
|
|
4138
4317
|
} catch {
|
|
4139
4318
|
return null;
|
|
4140
4319
|
}
|
|
@@ -4145,7 +4324,8 @@ var LiveDataProvider = class {
|
|
|
4145
4324
|
const cache = {
|
|
4146
4325
|
fetchedAt: this.fetchedAt,
|
|
4147
4326
|
snapshot: this.snapshot ?? void 0,
|
|
4148
|
-
prices: Object.fromEntries(this.prices)
|
|
4327
|
+
prices: Object.fromEntries(this.prices),
|
|
4328
|
+
capabilities: Object.fromEntries(this.capabilities)
|
|
4149
4329
|
};
|
|
4150
4330
|
await fs9__default.default.writeFile(this.opts.cacheFile, JSON.stringify(cache, null, 2), "utf-8");
|
|
4151
4331
|
} catch {
|
|
@@ -4170,6 +4350,27 @@ var LiveDataProvider = class {
|
|
|
4170
4350
|
return { ...m, inputCostPer1kTokens: p.input, outputCostPer1kTokens: p.output };
|
|
4171
4351
|
});
|
|
4172
4352
|
}
|
|
4353
|
+
/** Current capability facts for a model id, or null when unknown. */
|
|
4354
|
+
getCapability(modelId) {
|
|
4355
|
+
return this.capabilities.get(normalizeModelId(modelId)) ?? null;
|
|
4356
|
+
}
|
|
4357
|
+
/**
|
|
4358
|
+
* Returns capability-corrected copies of each model (originals untouched):
|
|
4359
|
+
* real context windows replace the providers' hardcoded guesses, native
|
|
4360
|
+
* tool support replaces the assume-by-provider default, and vision is set
|
|
4361
|
+
* from the declared input modalities.
|
|
4362
|
+
*/
|
|
4363
|
+
applyLiveCapabilities(models) {
|
|
4364
|
+
return models.map((m) => {
|
|
4365
|
+
const c = this.getCapability(m.id);
|
|
4366
|
+
if (!c) return m;
|
|
4367
|
+
const next = { ...m };
|
|
4368
|
+
if (c.contextWindow) next.contextWindow = c.contextWindow;
|
|
4369
|
+
if (c.supportsTools !== void 0) next.supportsToolUse = c.supportsTools;
|
|
4370
|
+
if (c.inputModalities) next.isVisionCapable = c.inputModalities.includes("image");
|
|
4371
|
+
return next;
|
|
4372
|
+
});
|
|
4373
|
+
}
|
|
4173
4374
|
/** Where the active quality data came from — for /why and `cascade models`. */
|
|
4174
4375
|
getDataSource() {
|
|
4175
4376
|
return this.source;
|
|
@@ -4180,6 +4381,9 @@ var LiveDataProvider = class {
|
|
|
4180
4381
|
hasLivePricing() {
|
|
4181
4382
|
return this.prices.size > 0;
|
|
4182
4383
|
}
|
|
4384
|
+
hasCapabilities() {
|
|
4385
|
+
return this.capabilities.size > 0;
|
|
4386
|
+
}
|
|
4183
4387
|
};
|
|
4184
4388
|
|
|
4185
4389
|
// src/core/router/benchmarks.ts
|
|
@@ -4368,6 +4572,57 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
|
|
|
4368
4572
|
profiler.profileAll(allModels).catch(() => {
|
|
4369
4573
|
});
|
|
4370
4574
|
}
|
|
4575
|
+
/**
|
|
4576
|
+
* One-time native tool-call probe for local/compat models with NO capability
|
|
4577
|
+
* metadata. llama.cpp / LM Studio "/models" endpoints return ids only, so
|
|
4578
|
+
* `supportsToolUse` stays undefined and the T3 tool gate ASSUMES native
|
|
4579
|
+
* support — wrong for many local builds, which then fumble tool-format
|
|
4580
|
+
* output. Each unknown model is probed once with a trivial tool; the verdict
|
|
4581
|
+
* persists in the MemoryStore's model_cache (via the previously-dormant
|
|
4582
|
+
* getModelProfile read-back), so a model is probed once EVER, not per run.
|
|
4583
|
+
* Cloud providers are never probed — their metadata is authoritative.
|
|
4584
|
+
*/
|
|
4585
|
+
async probeLocalToolSupport(store) {
|
|
4586
|
+
const unknown = this.selector.getAllAvailableModels().filter(
|
|
4587
|
+
(m) => m.provider === "openai-compatible" && m.supportsToolUse === void 0
|
|
4588
|
+
);
|
|
4589
|
+
for (const model of unknown) {
|
|
4590
|
+
const cached = store.getModelProfile(model.id, model.provider);
|
|
4591
|
+
let verdict = cached?.supportsToolUse;
|
|
4592
|
+
if (verdict === void 0) {
|
|
4593
|
+
const probed = await this.probeNativeToolCall(model);
|
|
4594
|
+
if (probed === null) continue;
|
|
4595
|
+
verdict = probed;
|
|
4596
|
+
store.saveModelCapability(model.id, model.provider, { supportsToolUse: verdict });
|
|
4597
|
+
}
|
|
4598
|
+
this.selector.addDynamicModel({ ...model, supportsToolUse: verdict });
|
|
4599
|
+
}
|
|
4600
|
+
for (const tier of ["T1", "T2", "T3"]) {
|
|
4601
|
+
const cur = this.tierModels.get(tier);
|
|
4602
|
+
if (!cur) continue;
|
|
4603
|
+
const fresh = this.selector.getModelById(cur.id);
|
|
4604
|
+
if (fresh) this.tierModels.set(tier, fresh);
|
|
4605
|
+
}
|
|
4606
|
+
}
|
|
4607
|
+
async probeNativeToolCall(model) {
|
|
4608
|
+
try {
|
|
4609
|
+
const cfg = this.config.providers.find((p) => p.type === model.provider) ?? { type: model.provider };
|
|
4610
|
+
const provider = this.createProvider(cfg, model);
|
|
4611
|
+
const result = await withTimeout(provider.generate({
|
|
4612
|
+
messages: [{ role: "user", content: "Call the echo tool with text set to 'ping'. Use the tool; do not answer in prose." }],
|
|
4613
|
+
tools: [{
|
|
4614
|
+
name: "echo",
|
|
4615
|
+
description: "Echo the given text back to the caller.",
|
|
4616
|
+
inputSchema: { type: "object", properties: { text: { type: "string", description: "Text to echo" } }, required: ["text"] }
|
|
4617
|
+
}],
|
|
4618
|
+
maxTokens: 80,
|
|
4619
|
+
temperature: 0
|
|
4620
|
+
}), 3e4, "tool-support probe timed out");
|
|
4621
|
+
return (result.toolCalls?.length ?? 0) > 0;
|
|
4622
|
+
} catch {
|
|
4623
|
+
return null;
|
|
4624
|
+
}
|
|
4625
|
+
}
|
|
4371
4626
|
/**
|
|
4372
4627
|
* Cascade Auto live data: discover/validate real model ids from each cloud
|
|
4373
4628
|
* provider, then fetch current public quality scores + per-token prices and
|
|
@@ -4417,12 +4672,17 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
|
|
|
4417
4672
|
await Promise.allSettled(tasks);
|
|
4418
4673
|
}
|
|
4419
4674
|
/**
|
|
4420
|
-
* Replace available models with live-priced
|
|
4421
|
-
*
|
|
4675
|
+
* Replace available models with live-priced AND capability-corrected copies
|
|
4676
|
+
* (real context windows, native tool support, vision from modalities), then
|
|
4677
|
+
* refresh the already resolved tier models so shared-tier cost accounting and
|
|
4678
|
+
* the tool-use gate both see current data.
|
|
4422
4679
|
*/
|
|
4423
4680
|
applyLivePricing() {
|
|
4424
|
-
if (!this.liveData
|
|
4425
|
-
|
|
4681
|
+
if (!this.liveData) return;
|
|
4682
|
+
if (!this.liveData.hasLivePricing() && !this.liveData.hasCapabilities()) return;
|
|
4683
|
+
let updated = this.selector.getAllAvailableModels();
|
|
4684
|
+
if (this.liveData.hasLivePricing()) updated = this.liveData.applyLivePricing(updated);
|
|
4685
|
+
if (this.liveData.hasCapabilities()) updated = this.liveData.applyLiveCapabilities(updated);
|
|
4426
4686
|
for (const m of updated) this.selector.addDynamicModel(m);
|
|
4427
4687
|
for (const tier of ["T1", "T2", "T3"]) {
|
|
4428
4688
|
const cur = this.tierModels.get(tier);
|
|
@@ -4597,6 +4857,10 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
|
|
|
4597
4857
|
const r = this.config?.reinforcements;
|
|
4598
4858
|
return { enabled: r?.enabled === true, maxPerSection: r?.maxPerSection ?? 4 };
|
|
4599
4859
|
}
|
|
4860
|
+
/** Project-knowledge settings (config.knowledge). Facts extraction on by default. */
|
|
4861
|
+
getKnowledgeConfig() {
|
|
4862
|
+
return { factsExtraction: this.config?.knowledge?.factsExtraction !== false };
|
|
4863
|
+
}
|
|
4600
4864
|
/**
|
|
4601
4865
|
* Resolved T3 wave execution mode. 'auto' becomes 'sequential' when the T3
|
|
4602
4866
|
* tier resolves to a LOCAL model (the single-GPU queue serializes anyway, so
|
|
@@ -4683,10 +4947,10 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
|
|
|
4683
4947
|
* null when Cascade Auto is off (callers then use the shared tier model).
|
|
4684
4948
|
* Pure heuristic — no extra LLM call.
|
|
4685
4949
|
*/
|
|
4686
|
-
async selectModelForSubtask(tier, text) {
|
|
4950
|
+
async selectModelForSubtask(tier, text, opts) {
|
|
4687
4951
|
if (!this.config?.cascadeAuto || !this.taskAnalyzer || !text.trim()) return null;
|
|
4688
4952
|
try {
|
|
4689
|
-
return await this.taskAnalyzer.selectModel(text, tier, this.selector);
|
|
4953
|
+
return await this.taskAnalyzer.selectModel(text, tier, this.selector, opts);
|
|
4690
4954
|
} catch {
|
|
4691
4955
|
return null;
|
|
4692
4956
|
}
|
|
@@ -5442,6 +5706,13 @@ EXAMPLE \u2014 calling the "shell" tool to list files:
|
|
|
5442
5706
|
{"name": "shell", "input": {"command": "ls -la"}}
|
|
5443
5707
|
</tool_call>
|
|
5444
5708
|
|
|
5709
|
+
When you have enough information, stop calling tools and write your final answer.`;
|
|
5710
|
+
}
|
|
5711
|
+
function buildTextToolReminder(tools) {
|
|
5712
|
+
return `
|
|
5713
|
+
TOOL USE REMINDER:
|
|
5714
|
+
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.
|
|
5715
|
+
Available tools: ${tools.map((t) => t.name).join(", ")}.
|
|
5445
5716
|
When you have enough information, stop calling tools and write your final answer.`;
|
|
5446
5717
|
}
|
|
5447
5718
|
|
|
@@ -5681,6 +5952,9 @@ Now execute your subtask using this context where relevant.`
|
|
|
5681
5952
|
} catch (e) {
|
|
5682
5953
|
this.log("Failed to write to World State DB");
|
|
5683
5954
|
}
|
|
5955
|
+
if (this.router.getKnowledgeConfig?.().factsExtraction !== false) {
|
|
5956
|
+
await this.extractAndStoreFacts(db, assignment, output);
|
|
5957
|
+
}
|
|
5684
5958
|
}
|
|
5685
5959
|
this.setStatus("COMPLETED", output);
|
|
5686
5960
|
this.sendStatusUpdate({ progressPct: 100, currentAction: "Subtask complete", status: "IN_PROGRESS", output });
|
|
@@ -5742,7 +6016,7 @@ Now execute your subtask using this context where relevant.`
|
|
|
5742
6016
|
let subtaskModel;
|
|
5743
6017
|
try {
|
|
5744
6018
|
const subtaskText = `${this.assignment?.subtaskTitle ?? ""} ${this.assignment?.description ?? ""} ${this.assignment?.expectedOutput ?? ""}`;
|
|
5745
|
-
subtaskModel = await this.router.selectModelForSubtask("T3", subtaskText) ?? void 0;
|
|
6019
|
+
subtaskModel = await this.router.selectModelForSubtask("T3", subtaskText, { requiresToolUse: tools.length > 0 }) ?? void 0;
|
|
5746
6020
|
if (subtaskModel) {
|
|
5747
6021
|
this.log(`Cascade Auto: routing this subtask to ${subtaskModel.provider}:${subtaskModel.id}`);
|
|
5748
6022
|
}
|
|
@@ -5750,7 +6024,8 @@ Now execute your subtask using this context where relevant.`
|
|
|
5750
6024
|
}
|
|
5751
6025
|
const effectiveModel = subtaskModel ?? this.router.getModelForTier("T3");
|
|
5752
6026
|
const useTextTools = effectiveModel?.supportsToolUse === false && tools.length > 0;
|
|
5753
|
-
|
|
6027
|
+
let sentFullTextContract = false;
|
|
6028
|
+
let textContractSignature = "";
|
|
5754
6029
|
while (iterations < MAX_ITERATIONS) {
|
|
5755
6030
|
iterations++;
|
|
5756
6031
|
this.throwIfCancelled();
|
|
@@ -5764,6 +6039,17 @@ Now execute your subtask using this context where relevant.`
|
|
|
5764
6039
|
${g.text}`
|
|
5765
6040
|
});
|
|
5766
6041
|
}
|
|
6042
|
+
let textToolSuffix = "";
|
|
6043
|
+
if (useTextTools) {
|
|
6044
|
+
const signature = tools.map((t) => t.name).join(",");
|
|
6045
|
+
if (!sentFullTextContract || signature !== textContractSignature) {
|
|
6046
|
+
textToolSuffix = buildTextToolSystemPrompt(tools);
|
|
6047
|
+
sentFullTextContract = true;
|
|
6048
|
+
textContractSignature = signature;
|
|
6049
|
+
} else {
|
|
6050
|
+
textToolSuffix = buildTextToolReminder(tools);
|
|
6051
|
+
}
|
|
6052
|
+
}
|
|
5767
6053
|
const options = {
|
|
5768
6054
|
messages: this.context.getMessages(),
|
|
5769
6055
|
systemPrompt: this.systemPromptOverride + systemPrompt + (this.hierarchyContext ? `
|
|
@@ -6253,6 +6539,41 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
|
6253
6539
|
};
|
|
6254
6540
|
}
|
|
6255
6541
|
}
|
|
6542
|
+
/**
|
|
6543
|
+
* world-state v2: distill a completed subtask's output into durable
|
|
6544
|
+
* `(entity, relation, value)` facts and upsert them into the knowledge graph.
|
|
6545
|
+
* A bounded, cheap T3-tier call; entirely best-effort — any failure is swallowed
|
|
6546
|
+
* so it never blocks or fails the subtask. Respects the subtask's privacy tier
|
|
6547
|
+
* (a local-only subtask extracts on a local model too, never leaking to cloud).
|
|
6548
|
+
*/
|
|
6549
|
+
async extractAndStoreFacts(db, assignment, output) {
|
|
6550
|
+
try {
|
|
6551
|
+
const prompt = `Extract durable project facts from this completed subtask.
|
|
6552
|
+
Return ONLY a JSON array of {"entity","relation","value"} triples describing lasting
|
|
6553
|
+
facts about the codebase/project \u2014 e.g. {"entity":"auth module","relation":"uses","value":"JWT"}.
|
|
6554
|
+
Ignore transient step-by-step details. At most 6 triples. If nothing durable, return [].
|
|
6555
|
+
|
|
6556
|
+
Subtask: ${assignment.subtaskTitle}
|
|
6557
|
+
Output:
|
|
6558
|
+
${output.slice(0, 4e3)}`;
|
|
6559
|
+
const result = await this.router.generate("T3", {
|
|
6560
|
+
messages: [{ role: "user", content: prompt }],
|
|
6561
|
+
maxTokens: 300,
|
|
6562
|
+
temperature: 0,
|
|
6563
|
+
...this.localOnlyMatch ? { forceLocal: true } : {}
|
|
6564
|
+
});
|
|
6565
|
+
const match = /\[[\s\S]*\]/.exec(result.content);
|
|
6566
|
+
if (!match) return;
|
|
6567
|
+
const facts = JSON.parse(match[0]);
|
|
6568
|
+
if (!Array.isArray(facts)) return;
|
|
6569
|
+
for (const f of facts.slice(0, 8)) {
|
|
6570
|
+
if (f && typeof f.entity === "string" && typeof f.relation === "string" && typeof f.value === "string") {
|
|
6571
|
+
db.upsertFact(f.entity, f.relation, f.value, this.id);
|
|
6572
|
+
}
|
|
6573
|
+
}
|
|
6574
|
+
} catch {
|
|
6575
|
+
}
|
|
6576
|
+
}
|
|
6256
6577
|
async correctOutput(originalOutput, failures) {
|
|
6257
6578
|
const correctionPrompt = `The following output failed these checks: ${failures.join(", ")}.
|
|
6258
6579
|
|
|
@@ -7658,10 +7979,24 @@ In 3-5 terse bullets, flag the most important RISKS, GAPS, or over-/under-decomp
|
|
|
7658
7979
|
}
|
|
7659
7980
|
async decomposeTask(prompt, systemContext) {
|
|
7660
7981
|
const db = this.router.getWorldStateDB?.();
|
|
7661
|
-
|
|
7982
|
+
let worldStateContext = "";
|
|
7983
|
+
if (db) {
|
|
7984
|
+
const knowledge = db.getFormattedKnowledge(prompt);
|
|
7985
|
+
if (knowledge) {
|
|
7986
|
+
worldStateContext = `
|
|
7987
|
+
|
|
7988
|
+
PROJECT KNOWLEDGE (relevant facts about this codebase):
|
|
7989
|
+
${knowledge}`;
|
|
7990
|
+
} else {
|
|
7991
|
+
const log = db.getFormattedState();
|
|
7992
|
+
if (log && log !== "World State is currently empty.") {
|
|
7993
|
+
worldStateContext = `
|
|
7662
7994
|
|
|
7663
7995
|
PROJECT WORLD STATE (Current architecture and recent changes):
|
|
7664
|
-
${
|
|
7996
|
+
${log}`;
|
|
7997
|
+
}
|
|
7998
|
+
}
|
|
7999
|
+
}
|
|
7665
8000
|
const contextSection = systemContext ? `
|
|
7666
8001
|
Project context:
|
|
7667
8002
|
${systemContext}` : "";
|
|
@@ -10044,13 +10379,17 @@ var TaskAnalyzer = class {
|
|
|
10044
10379
|
* Scores tier-eligible models using cost efficiency + historical performance.
|
|
10045
10380
|
* Falls back to the priority-list default when no candidates have history.
|
|
10046
10381
|
*/
|
|
10047
|
-
async selectModel(prompt, tier, selector) {
|
|
10382
|
+
async selectModel(prompt, tier, selector, opts) {
|
|
10048
10383
|
const profile = await this.analyze(prompt);
|
|
10049
10384
|
if (profile.requiresVision) {
|
|
10050
10385
|
return selector.selectVisionModel();
|
|
10051
10386
|
}
|
|
10052
|
-
|
|
10387
|
+
let candidates2 = selector.getCandidatesForTier(tier);
|
|
10053
10388
|
if (candidates2.length === 0) return selector.selectForTier(tier);
|
|
10389
|
+
if (opts?.requiresToolUse) {
|
|
10390
|
+
const toolCapable = candidates2.filter((m) => m.supportsToolUse !== false);
|
|
10391
|
+
if (toolCapable.length > 0) candidates2 = toolCapable;
|
|
10392
|
+
}
|
|
10054
10393
|
const scored = candidates2.map((m) => ({
|
|
10055
10394
|
model: m,
|
|
10056
10395
|
score: this.scoreModel(m, profile)
|
|
@@ -10229,6 +10568,28 @@ var ModelPerformanceTracker = class {
|
|
|
10229
10568
|
return Math.max(0.1, 1 - normalised * complexityWeight);
|
|
10230
10569
|
}
|
|
10231
10570
|
};
|
|
10571
|
+
var ivmCache;
|
|
10572
|
+
var ivmWarned = false;
|
|
10573
|
+
async function loadIsolatedVm() {
|
|
10574
|
+
if (ivmCache !== void 0) return ivmCache;
|
|
10575
|
+
try {
|
|
10576
|
+
const specifier = "isolated-vm";
|
|
10577
|
+
const mod = await import(specifier);
|
|
10578
|
+
ivmCache = mod.default ?? mod;
|
|
10579
|
+
} catch {
|
|
10580
|
+
ivmCache = null;
|
|
10581
|
+
}
|
|
10582
|
+
return ivmCache;
|
|
10583
|
+
}
|
|
10584
|
+
function safeJsonParse(text) {
|
|
10585
|
+
if (typeof text !== "string") return {};
|
|
10586
|
+
try {
|
|
10587
|
+
const v = JSON.parse(text);
|
|
10588
|
+
return v && typeof v === "object" ? v : {};
|
|
10589
|
+
} catch {
|
|
10590
|
+
return {};
|
|
10591
|
+
}
|
|
10592
|
+
}
|
|
10232
10593
|
var DYNAMIC_TOOLS_FILE = "dynamic-tools.json";
|
|
10233
10594
|
function normalizeToolSchema(schema) {
|
|
10234
10595
|
if (schema && schema["type"] === "object" && typeof schema["properties"] === "object") {
|
|
@@ -10331,7 +10692,12 @@ var DynamicTool = class extends BaseTool {
|
|
|
10331
10692
|
getEscalator;
|
|
10332
10693
|
/** Untrusted = loaded from disk / a peer; its dangerous calls always re-prompt. */
|
|
10333
10694
|
trusted;
|
|
10334
|
-
|
|
10695
|
+
/** Resolve the configured sandbox mode at call time (default 'auto'). */
|
|
10696
|
+
getSandboxMode;
|
|
10697
|
+
/** Optional diagnostics sink (routed through the host so it survives the Ink TUI). */
|
|
10698
|
+
log;
|
|
10699
|
+
constructor(spec, registry, getEscalator, trusted, getSandboxMode = () => "auto", log = () => {
|
|
10700
|
+
}) {
|
|
10335
10701
|
super();
|
|
10336
10702
|
this.name = spec.name;
|
|
10337
10703
|
this.description = spec.description;
|
|
@@ -10341,6 +10707,8 @@ var DynamicTool = class extends BaseTool {
|
|
|
10341
10707
|
this.registry = registry;
|
|
10342
10708
|
this.getEscalator = getEscalator;
|
|
10343
10709
|
this.trusted = trusted;
|
|
10710
|
+
this.getSandboxMode = getSandboxMode;
|
|
10711
|
+
this.log = log;
|
|
10344
10712
|
}
|
|
10345
10713
|
isDangerous() {
|
|
10346
10714
|
return this._isDangerous;
|
|
@@ -10377,8 +10745,94 @@ var DynamicTool = class extends BaseTool {
|
|
|
10377
10745
|
return `Error calling ${toolName}: ${err instanceof Error ? err.message : String(err)}`;
|
|
10378
10746
|
}
|
|
10379
10747
|
};
|
|
10748
|
+
const mode = this.getSandboxMode();
|
|
10749
|
+
if (mode !== "worker") {
|
|
10750
|
+
const ivm = await loadIsolatedVm();
|
|
10751
|
+
if (ivm) return this.runInIsolate(ivm, input, callTool);
|
|
10752
|
+
if (mode === "isolate" && !ivmWarned) {
|
|
10753
|
+
ivmWarned = true;
|
|
10754
|
+
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.");
|
|
10755
|
+
}
|
|
10756
|
+
}
|
|
10380
10757
|
return this.runInWorker(input, callTool);
|
|
10381
10758
|
}
|
|
10759
|
+
/**
|
|
10760
|
+
* Run the generated code in a hard V8 isolate (isolated-vm). The isolate global
|
|
10761
|
+
* has no Node built-ins, so the code cannot see `process`, `require`, the
|
|
10762
|
+
* filesystem, or the network — it reaches the host ONLY through the injected
|
|
10763
|
+
* `callTool` (escalator-gated on the main thread) and `fetch` (SSRF-guarded via
|
|
10764
|
+
* bridgeFetch). `script.run({ timeout })` bounds synchronous CPU; an outer
|
|
10765
|
+
* wall-clock race + `isolate.dispose()` bounds async runaway (a never-resolving
|
|
10766
|
+
* await), mirroring the worker's terminate().
|
|
10767
|
+
*/
|
|
10768
|
+
async runInIsolate(ivm, input, callTool) {
|
|
10769
|
+
const timeoutMs = Math.max(200, Number(process.env["CASCADE_DYNAMIC_TOOL_TIMEOUT_MS"]) || DYNAMIC_TOOL_TIMEOUT_MS);
|
|
10770
|
+
const isolate = new ivm.Isolate({ memoryLimit: 128 });
|
|
10771
|
+
let disposed = false;
|
|
10772
|
+
const dispose = () => {
|
|
10773
|
+
if (!disposed) {
|
|
10774
|
+
disposed = true;
|
|
10775
|
+
try {
|
|
10776
|
+
isolate.dispose();
|
|
10777
|
+
} catch {
|
|
10778
|
+
}
|
|
10779
|
+
}
|
|
10780
|
+
};
|
|
10781
|
+
try {
|
|
10782
|
+
const context = await isolate.createContext();
|
|
10783
|
+
const jail = context.global;
|
|
10784
|
+
await jail.set("_callTool", new ivm.Reference(async (name, inputJson) => {
|
|
10785
|
+
const out = await callTool(String(name), safeJsonParse(inputJson));
|
|
10786
|
+
return String(out);
|
|
10787
|
+
}));
|
|
10788
|
+
await jail.set("_fetch", new ivm.Reference(async (url, initJson) => {
|
|
10789
|
+
const r = await bridgeFetch(String(url), safeJsonParse(initJson));
|
|
10790
|
+
return JSON.stringify(r);
|
|
10791
|
+
}));
|
|
10792
|
+
await jail.set("_input", new ivm.ExternalCopy(input).copyInto());
|
|
10793
|
+
await jail.set("_code", this.executeCode);
|
|
10794
|
+
const bootstrap = `
|
|
10795
|
+
const callTool = (name, toolInput) => _callTool.apply(undefined,
|
|
10796
|
+
[String(name), JSON.stringify(toolInput || {})],
|
|
10797
|
+
{ result: { promise: true }, arguments: { copy: true } });
|
|
10798
|
+
const fetch = async (url, init) => {
|
|
10799
|
+
const raw = await _fetch.apply(undefined,
|
|
10800
|
+
[String(url), JSON.stringify(init || null)],
|
|
10801
|
+
{ result: { promise: true }, arguments: { copy: true } });
|
|
10802
|
+
const r = JSON.parse(raw);
|
|
10803
|
+
if (r && r.__error) throw new Error(r.__error);
|
|
10804
|
+
return {
|
|
10805
|
+
ok: r.ok, status: r.status, statusText: r.statusText,
|
|
10806
|
+
headers: { get: (k) => (String(k).toLowerCase() === 'content-type' ? r.contentType : null) },
|
|
10807
|
+
text: async () => r.body,
|
|
10808
|
+
json: async () => JSON.parse(r.body),
|
|
10809
|
+
};
|
|
10810
|
+
};
|
|
10811
|
+
const console = { log() {}, error() {} };
|
|
10812
|
+
const AsyncFunction = Object.getPrototypeOf(async function () {}).constructor;
|
|
10813
|
+
const fn = new AsyncFunction('input', 'callTool', 'fetch', 'console', _code);
|
|
10814
|
+
(async () => { const r = await fn(_input, callTool, fetch, console); return String(r == null ? '' : r); })();
|
|
10815
|
+
`;
|
|
10816
|
+
const script = await isolate.compileScript(bootstrap);
|
|
10817
|
+
const runPromise = script.run(context, { timeout: timeoutMs, promise: true }).then((v) => String(v ?? ""));
|
|
10818
|
+
const timeoutPromise = new Promise((resolve) => {
|
|
10819
|
+
const t = setTimeout(() => {
|
|
10820
|
+
dispose();
|
|
10821
|
+
resolve(`Dynamic tool "${this.name}" timed out after ${timeoutMs}ms and was terminated.`);
|
|
10822
|
+
}, timeoutMs + 500);
|
|
10823
|
+
t.unref?.();
|
|
10824
|
+
});
|
|
10825
|
+
return await Promise.race([runPromise, timeoutPromise]);
|
|
10826
|
+
} catch (err) {
|
|
10827
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
10828
|
+
if (/timed? out/i.test(msg)) {
|
|
10829
|
+
return `Dynamic tool "${this.name}" timed out after ${timeoutMs}ms and was terminated.`;
|
|
10830
|
+
}
|
|
10831
|
+
return `Tool error: ${msg}`;
|
|
10832
|
+
} finally {
|
|
10833
|
+
dispose();
|
|
10834
|
+
}
|
|
10835
|
+
}
|
|
10382
10836
|
/** Spawn the worker, service its callTool/fetch bridge, enforce the kill timeout. */
|
|
10383
10837
|
runInWorker(input, callTool) {
|
|
10384
10838
|
const timeoutMs = Math.max(200, Number(process.env["CASCADE_DYNAMIC_TOOL_TIMEOUT_MS"]) || DYNAMIC_TOOL_TIMEOUT_MS);
|
|
@@ -10458,16 +10912,19 @@ var ToolCreator = class {
|
|
|
10458
10912
|
workspacePath;
|
|
10459
10913
|
/** When false, persisted tools are neither loaded nor written. */
|
|
10460
10914
|
persistEnabled;
|
|
10915
|
+
/** Sandbox runtime for generated tools; passed to each DynamicTool. */
|
|
10916
|
+
sandboxMode;
|
|
10461
10917
|
logger;
|
|
10462
10918
|
/** name → spec, for persistence, broadcast, and re-registration. */
|
|
10463
10919
|
specs = /* @__PURE__ */ new Map();
|
|
10464
10920
|
/** capability fingerprint → tool name, so the same need isn't re-generated. */
|
|
10465
10921
|
capabilityIndex = /* @__PURE__ */ new Map();
|
|
10466
|
-
constructor(router, registry, workspacePath, persistEnabled = true) {
|
|
10922
|
+
constructor(router, registry, workspacePath, persistEnabled = true, sandboxMode = "auto") {
|
|
10467
10923
|
this.router = router;
|
|
10468
10924
|
this.registry = registry;
|
|
10469
10925
|
this.workspacePath = workspacePath;
|
|
10470
10926
|
this.persistEnabled = persistEnabled;
|
|
10927
|
+
this.sandboxMode = sandboxMode;
|
|
10471
10928
|
}
|
|
10472
10929
|
setPermissionEscalator(escalator) {
|
|
10473
10930
|
this.escalator = escalator;
|
|
@@ -10555,7 +11012,14 @@ Required capability: ${description.slice(0, 300)}`;
|
|
|
10555
11012
|
this.specs.set(spec.name, spec);
|
|
10556
11013
|
return;
|
|
10557
11014
|
}
|
|
10558
|
-
const tool = new DynamicTool(
|
|
11015
|
+
const tool = new DynamicTool(
|
|
11016
|
+
spec,
|
|
11017
|
+
this.registry,
|
|
11018
|
+
() => this.escalator,
|
|
11019
|
+
trusted,
|
|
11020
|
+
() => this.sandboxMode,
|
|
11021
|
+
(msg) => this.log(msg)
|
|
11022
|
+
);
|
|
10559
11023
|
this.registry.register(tool);
|
|
10560
11024
|
this.specs.set(spec.name, spec);
|
|
10561
11025
|
this.capabilityIndex.set(capabilityKey(`${spec.description}`), spec.name);
|
|
@@ -10604,6 +11068,9 @@ Required capability: ${description.slice(0, 300)}`;
|
|
|
10604
11068
|
return Array.from(this.specs.keys());
|
|
10605
11069
|
}
|
|
10606
11070
|
};
|
|
11071
|
+
function normalizeKey(text) {
|
|
11072
|
+
return text.trim().toLowerCase().replace(/\s+/g, " ");
|
|
11073
|
+
}
|
|
10607
11074
|
var WorldStateDB = class {
|
|
10608
11075
|
constructor(workspacePath, debugMode = false) {
|
|
10609
11076
|
this.workspacePath = workspacePath;
|
|
@@ -10625,6 +11092,16 @@ var WorldStateDB = class {
|
|
|
10625
11092
|
encrypted_payload TEXT NOT NULL
|
|
10626
11093
|
)
|
|
10627
11094
|
`);
|
|
11095
|
+
this.db.exec(`
|
|
11096
|
+
CREATE TABLE IF NOT EXISTS facts (
|
|
11097
|
+
entity TEXT NOT NULL,
|
|
11098
|
+
relation TEXT NOT NULL,
|
|
11099
|
+
encrypted_value TEXT NOT NULL,
|
|
11100
|
+
source_worker TEXT NOT NULL,
|
|
11101
|
+
timestamp TEXT NOT NULL,
|
|
11102
|
+
PRIMARY KEY (entity, relation)
|
|
11103
|
+
)
|
|
11104
|
+
`);
|
|
10628
11105
|
}
|
|
10629
11106
|
workspacePath;
|
|
10630
11107
|
debugMode;
|
|
@@ -10698,6 +11175,131 @@ var WorldStateDB = class {
|
|
|
10698
11175
|
if (entries.length === 0) return "World State is currently empty.";
|
|
10699
11176
|
return entries.map((e, idx) => `[${e.timestamp}] Step ${idx + 1} (${e.workerId}): ${e.summary}`).join("\n");
|
|
10700
11177
|
}
|
|
11178
|
+
// ── world-state v2: queryable facts ──────────────
|
|
11179
|
+
/**
|
|
11180
|
+
* Upsert a fact. `(entity, relation)` is normalized so casing/whitespace don't
|
|
11181
|
+
* fragment the key; an existing pair is superseded (value + provenance updated)
|
|
11182
|
+
* rather than duplicated. Empty entity/relation/value are ignored.
|
|
11183
|
+
*/
|
|
11184
|
+
upsertFact(entity, relation, value, sourceWorker, timestamp) {
|
|
11185
|
+
const e = normalizeKey(entity);
|
|
11186
|
+
const r = normalizeKey(relation);
|
|
11187
|
+
const v = value.trim();
|
|
11188
|
+
if (!e || !r || !v) return;
|
|
11189
|
+
const encrypted = this.encrypt(JSON.stringify({ value: v }));
|
|
11190
|
+
const stmt = this.db.prepare(`
|
|
11191
|
+
INSERT INTO facts (entity, relation, encrypted_value, source_worker, timestamp)
|
|
11192
|
+
VALUES (?, ?, ?, ?, ?)
|
|
11193
|
+
ON CONFLICT(entity, relation) DO UPDATE SET
|
|
11194
|
+
encrypted_value = excluded.encrypted_value,
|
|
11195
|
+
source_worker = excluded.source_worker,
|
|
11196
|
+
timestamp = excluded.timestamp
|
|
11197
|
+
`);
|
|
11198
|
+
stmt.run(e, r, encrypted, sourceWorker, timestamp ?? (/* @__PURE__ */ new Date()).toISOString());
|
|
11199
|
+
this.dumpDebugIfNeeded();
|
|
11200
|
+
}
|
|
11201
|
+
rowToFact(row) {
|
|
11202
|
+
let value;
|
|
11203
|
+
try {
|
|
11204
|
+
value = JSON.parse(this.decrypt(row.encrypted_value)).value;
|
|
11205
|
+
} catch {
|
|
11206
|
+
value = "[Decryption Failed - Payload Corrupted]";
|
|
11207
|
+
}
|
|
11208
|
+
return { entity: row.entity, relation: row.relation, value, sourceWorker: row.source_worker, timestamp: row.timestamp };
|
|
11209
|
+
}
|
|
11210
|
+
/** All facts whose entity matches one of the (normalized) query entities. */
|
|
11211
|
+
getFactsForEntities(entities) {
|
|
11212
|
+
const keys = Array.from(new Set(entities.map(normalizeKey).filter(Boolean)));
|
|
11213
|
+
if (keys.length === 0) return [];
|
|
11214
|
+
const placeholders = keys.map(() => "?").join(", ");
|
|
11215
|
+
const stmt = this.db.prepare(
|
|
11216
|
+
`SELECT entity, relation, encrypted_value, source_worker, timestamp FROM facts WHERE entity IN (${placeholders}) ORDER BY entity ASC, relation ASC`
|
|
11217
|
+
);
|
|
11218
|
+
return stmt.all(...keys).map((row) => this.rowToFact(row));
|
|
11219
|
+
}
|
|
11220
|
+
/** Every fact (used for tests / debug / whole-graph views). */
|
|
11221
|
+
getAllFacts() {
|
|
11222
|
+
const stmt = this.db.prepare("SELECT entity, relation, encrypted_value, source_worker, timestamp FROM facts ORDER BY entity ASC, relation ASC");
|
|
11223
|
+
return stmt.all().map((row) => this.rowToFact(row));
|
|
11224
|
+
}
|
|
11225
|
+
/**
|
|
11226
|
+
* A compact, human/LLM-readable fact block for T1/T2 planning. When `entities`
|
|
11227
|
+
* is given, only facts about those entities are included; otherwise all facts.
|
|
11228
|
+
* Returns '' when there are none so callers can fall back to the linear log.
|
|
11229
|
+
*/
|
|
11230
|
+
getFormattedFacts(entities) {
|
|
11231
|
+
const facts = entities && entities.length > 0 ? this.getFactsForEntities(entities) : this.getAllFacts();
|
|
11232
|
+
if (facts.length === 0) return "";
|
|
11233
|
+
return facts.map((f) => `- ${f.entity} ${f.relation} ${f.value}`).join("\n");
|
|
11234
|
+
}
|
|
11235
|
+
/**
|
|
11236
|
+
* A compact knowledge block for T1/T2 planning. When `prompt` is given, facts
|
|
11237
|
+
* whose entity/relation/value mention a prompt token are preferred (relevance
|
|
11238
|
+
* filter); otherwise, or when nothing matches, all facts are used (capped at
|
|
11239
|
+
* `limit`). Returns '' when there are no facts, so the caller can fall back to
|
|
11240
|
+
* the raw linear log — this replaces replaying the whole log during planning.
|
|
11241
|
+
*/
|
|
11242
|
+
getFormattedKnowledge(prompt, limit = 40) {
|
|
11243
|
+
const all = this.getAllFacts();
|
|
11244
|
+
if (all.length === 0) return "";
|
|
11245
|
+
let selected = all;
|
|
11246
|
+
if (prompt && prompt.trim()) {
|
|
11247
|
+
const tokens = Array.from(new Set(prompt.toLowerCase().match(/[a-z0-9_./-]{3,}/g) ?? []));
|
|
11248
|
+
if (tokens.length > 0) {
|
|
11249
|
+
const matched = all.filter((f) => {
|
|
11250
|
+
const hay = `${f.entity} ${f.relation} ${f.value}`.toLowerCase();
|
|
11251
|
+
return tokens.some((t) => hay.includes(t));
|
|
11252
|
+
});
|
|
11253
|
+
if (matched.length > 0) selected = matched;
|
|
11254
|
+
}
|
|
11255
|
+
}
|
|
11256
|
+
return selected.slice(0, limit).map((f) => `- ${f.entity} ${f.relation} ${f.value}`).join("\n");
|
|
11257
|
+
}
|
|
11258
|
+
// ── Export / Import ──────────────────────────
|
|
11259
|
+
//
|
|
11260
|
+
// Knowledge travels DECRYPTED in the export bundle (a portable plaintext
|
|
11261
|
+
// JSON file — the encryption key never leaves this machine) and re-encrypts
|
|
11262
|
+
// with the LOCAL key on import.
|
|
11263
|
+
exportKnowledge() {
|
|
11264
|
+
return { facts: this.getAllFacts(), worldLog: this.getAllEntries() };
|
|
11265
|
+
}
|
|
11266
|
+
/**
|
|
11267
|
+
* Merge imported knowledge. Facts upsert on (entity, relation) with
|
|
11268
|
+
* newer-timestamp-wins (a local fact newer than the imported one is kept);
|
|
11269
|
+
* log entries append with fresh ids, skipping exact duplicates
|
|
11270
|
+
* (worker + timestamp + summary). Returns counts of what actually landed.
|
|
11271
|
+
*/
|
|
11272
|
+
importKnowledge(data) {
|
|
11273
|
+
let facts = 0;
|
|
11274
|
+
let logEntries = 0;
|
|
11275
|
+
if (Array.isArray(data.facts)) {
|
|
11276
|
+
const local = new Map(this.getAllFacts().map((f) => [`${f.entity}\0${f.relation}`, f.timestamp]));
|
|
11277
|
+
for (const f of data.facts) {
|
|
11278
|
+
if (!f || typeof f.entity !== "string" || typeof f.relation !== "string" || typeof f.value !== "string") continue;
|
|
11279
|
+
const key = `${normalizeKey(f.entity)}\0${normalizeKey(f.relation)}`;
|
|
11280
|
+
const localTs = local.get(key);
|
|
11281
|
+
const importTs = f.timestamp ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
11282
|
+
if (localTs && localTs >= importTs) continue;
|
|
11283
|
+
this.upsertFact(f.entity, f.relation, f.value, f.sourceWorker ?? "imported", importTs);
|
|
11284
|
+
facts++;
|
|
11285
|
+
}
|
|
11286
|
+
}
|
|
11287
|
+
if (Array.isArray(data.worldLog)) {
|
|
11288
|
+
const seen = new Set(this.getAllEntries().map((e) => `${e.workerId}\0${e.timestamp}\0${e.summary}`));
|
|
11289
|
+
const stmt = this.db.prepare("INSERT INTO world_state (id, timestamp, worker_id, encrypted_payload) VALUES (?, ?, ?, ?)");
|
|
11290
|
+
for (const e of data.worldLog) {
|
|
11291
|
+
if (!e || typeof e.summary !== "string") continue;
|
|
11292
|
+
const workerId = e.workerId ?? "imported";
|
|
11293
|
+
const timestamp = e.timestamp ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
11294
|
+
const key = `${workerId}\0${timestamp}\0${e.summary}`;
|
|
11295
|
+
if (seen.has(key)) continue;
|
|
11296
|
+
stmt.run(crypto__default.default.randomUUID(), timestamp, workerId, this.encrypt(JSON.stringify({ summary: e.summary })));
|
|
11297
|
+
seen.add(key);
|
|
11298
|
+
logEntries++;
|
|
11299
|
+
}
|
|
11300
|
+
}
|
|
11301
|
+
return { facts, logEntries };
|
|
11302
|
+
}
|
|
10701
11303
|
dumpDebugIfNeeded() {
|
|
10702
11304
|
if (!this.debugMode) return;
|
|
10703
11305
|
try {
|
|
@@ -10834,7 +11436,8 @@ var Cascade = class _Cascade extends EventEmitter__default.default {
|
|
|
10834
11436
|
}
|
|
10835
11437
|
const cfg = this.config;
|
|
10836
11438
|
if (cfg["enableToolCreation"] === true) {
|
|
10837
|
-
|
|
11439
|
+
const sandboxMode = this.config.tools?.dynamicToolSandbox ?? "auto";
|
|
11440
|
+
this.toolCreator = new ToolCreator(this.router, this.toolRegistry, this.workspacePath, cfg["persistDynamicTools"] !== false, sandboxMode);
|
|
10838
11441
|
this.toolCreator.setLogger((m) => {
|
|
10839
11442
|
if (this.listenerCount("log") > 0) this.emit("log", { level: "info", message: m });
|
|
10840
11443
|
});
|
|
@@ -11072,6 +11675,10 @@ ${last.partialOutput}` : "");
|
|
|
11072
11675
|
this.router.profileModels(this.store).catch(() => {
|
|
11073
11676
|
});
|
|
11074
11677
|
}
|
|
11678
|
+
if (this.store) {
|
|
11679
|
+
this.router.probeLocalToolSupport(this.store).catch(() => {
|
|
11680
|
+
});
|
|
11681
|
+
}
|
|
11075
11682
|
if (this.config.cascadeAuto) {
|
|
11076
11683
|
this.router.refreshLiveData().catch(() => {
|
|
11077
11684
|
});
|
|
@@ -16025,6 +16632,60 @@ ${prompt}`;
|
|
|
16025
16632
|
this.socket.broadcast("runtime:refresh", { scope: "global" });
|
|
16026
16633
|
res.json({ ok: true });
|
|
16027
16634
|
});
|
|
16635
|
+
this.app.get("/api/export", auth, (req, res) => {
|
|
16636
|
+
try {
|
|
16637
|
+
const sessionsParam = typeof req.query["sessions"] === "string" ? req.query["sessions"] : "all";
|
|
16638
|
+
const includeMemories = req.query["memories"] === "1" || req.query["memories"] === "true";
|
|
16639
|
+
const ids = sessionsParam === "all" ? void 0 : sessionsParam.split(",").map((s) => s.trim()).filter(Boolean);
|
|
16640
|
+
const bundle = {
|
|
16641
|
+
format: "cascade-export@1",
|
|
16642
|
+
exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
16643
|
+
sessions: this.store.exportSessions(ids)
|
|
16644
|
+
};
|
|
16645
|
+
if (includeMemories) {
|
|
16646
|
+
const ws = new WorldStateDB(this.workspacePath);
|
|
16647
|
+
try {
|
|
16648
|
+
bundle["memories"] = { ...ws.exportKnowledge(), identities: this.store.listIdentities() };
|
|
16649
|
+
} finally {
|
|
16650
|
+
ws.close();
|
|
16651
|
+
}
|
|
16652
|
+
}
|
|
16653
|
+
res.json(bundle);
|
|
16654
|
+
} catch (err) {
|
|
16655
|
+
res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
|
|
16656
|
+
}
|
|
16657
|
+
});
|
|
16658
|
+
this.app.post("/api/import", auth, mutationLimiter, (req, res) => {
|
|
16659
|
+
try {
|
|
16660
|
+
const bundle = req.body;
|
|
16661
|
+
if (!bundle || bundle.format !== "cascade-export@1") {
|
|
16662
|
+
res.status(400).json({ error: "Not a cascade-export@1 bundle." });
|
|
16663
|
+
return;
|
|
16664
|
+
}
|
|
16665
|
+
const importedSessions = Array.isArray(bundle.sessions) ? this.store.importSessions(bundle.sessions) : [];
|
|
16666
|
+
for (const s of importedSessions) this.persistRuntimeRow(s.id, s.title, "COMPLETED");
|
|
16667
|
+
let facts = 0;
|
|
16668
|
+
let logEntries = 0;
|
|
16669
|
+
let identities = 0;
|
|
16670
|
+
if (bundle.memories) {
|
|
16671
|
+
const ws = new WorldStateDB(this.workspacePath);
|
|
16672
|
+
try {
|
|
16673
|
+
const counts = ws.importKnowledge(bundle.memories);
|
|
16674
|
+
facts = counts.facts;
|
|
16675
|
+
logEntries = counts.logEntries;
|
|
16676
|
+
} finally {
|
|
16677
|
+
ws.close();
|
|
16678
|
+
}
|
|
16679
|
+
if (Array.isArray(bundle.memories.identities)) {
|
|
16680
|
+
identities = this.store.importIdentities(bundle.memories.identities);
|
|
16681
|
+
}
|
|
16682
|
+
}
|
|
16683
|
+
this.socket.broadcast("runtime:refresh", { scope: "workspace" });
|
|
16684
|
+
res.json({ ok: true, imported: { sessions: importedSessions.length, facts, logEntries, identities } });
|
|
16685
|
+
} catch (err) {
|
|
16686
|
+
res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
|
|
16687
|
+
}
|
|
16688
|
+
});
|
|
16028
16689
|
this.app.post("/api/sessions/:id/rollback", auth, mutationLimiter, async (req, res) => {
|
|
16029
16690
|
const sessionId = req.params.id;
|
|
16030
16691
|
const taskIds = this.sessionTaskIds.get(sessionId) ?? [];
|