cascade-ai 0.13.1 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -0
- package/dist/cli.cjs +824 -82
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +824 -82
- package/dist/cli.js.map +1 -1
- package/dist/desktop-core.cjs +826 -84
- package/dist/index.cjs +824 -82
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +223 -8
- package/dist/index.d.ts +223 -8
- package/dist/index.js +824 -82
- 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.0";
|
|
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
|
|
@@ -2989,6 +3138,8 @@ var CascadeConfigSchema = z.object({
|
|
|
2989
3138
|
privacy: z.object({
|
|
2990
3139
|
paths: z.array(z.object({ pattern: z.string().min(1), policy: z.enum(["local-only"]) })).default([])
|
|
2991
3140
|
}).optional(),
|
|
3141
|
+
/** Routing controls — forceTier pins the root tier, bypassing the classifier. */
|
|
3142
|
+
routing: z.object({ forceTier: z.enum(["auto", "T1", "T2", "T3"]).default("auto") }).optional(),
|
|
2992
3143
|
/**
|
|
2993
3144
|
* T3→T2 reinforcement: when enabled, a worker that discovers its subtask should
|
|
2994
3145
|
* fan out can call the `request_workers` tool to have its T2 manager spawn
|
|
@@ -3986,6 +4137,7 @@ function normalizeModelId(id) {
|
|
|
3986
4137
|
var LiveDataProvider = class {
|
|
3987
4138
|
snapshot = null;
|
|
3988
4139
|
prices = /* @__PURE__ */ new Map();
|
|
4140
|
+
capabilities = /* @__PURE__ */ new Map();
|
|
3989
4141
|
source = "bundled";
|
|
3990
4142
|
fetchedAt = 0;
|
|
3991
4143
|
loaded = false;
|
|
@@ -4014,6 +4166,9 @@ var LiveDataProvider = class {
|
|
|
4014
4166
|
if (cache.prices) {
|
|
4015
4167
|
for (const [id, p] of Object.entries(cache.prices)) this.prices.set(id, p);
|
|
4016
4168
|
}
|
|
4169
|
+
if (cache.capabilities) {
|
|
4170
|
+
for (const [id, c] of Object.entries(cache.capabilities)) this.capabilities.set(id, c);
|
|
4171
|
+
}
|
|
4017
4172
|
this.fetchedAt = cache.fetchedAt ?? 0;
|
|
4018
4173
|
} catch {
|
|
4019
4174
|
}
|
|
@@ -4034,9 +4189,9 @@ var LiveDataProvider = class {
|
|
|
4034
4189
|
const ttlMs = this.opts.refreshHours * 36e5;
|
|
4035
4190
|
const fresh = ttlMs > 0 && Date.now() - this.fetchedAt < ttlMs;
|
|
4036
4191
|
if (!force && fresh && this.source !== "bundled") return;
|
|
4037
|
-
const [snap,
|
|
4192
|
+
const [snap, catalog] = await Promise.all([
|
|
4038
4193
|
this.opts.live ? this.fetchSnapshot() : Promise.resolve(null),
|
|
4039
|
-
this.opts.pricingLive ? this.
|
|
4194
|
+
this.opts.pricingLive ? this.fetchCatalog() : Promise.resolve(null)
|
|
4040
4195
|
]);
|
|
4041
4196
|
let changed = false;
|
|
4042
4197
|
if (snap) {
|
|
@@ -4044,8 +4199,12 @@ var LiveDataProvider = class {
|
|
|
4044
4199
|
this.source = "live";
|
|
4045
4200
|
changed = true;
|
|
4046
4201
|
}
|
|
4047
|
-
if (
|
|
4048
|
-
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;
|
|
4049
4208
|
changed = true;
|
|
4050
4209
|
}
|
|
4051
4210
|
if (changed) {
|
|
@@ -4067,21 +4226,43 @@ var LiveDataProvider = class {
|
|
|
4067
4226
|
return null;
|
|
4068
4227
|
}
|
|
4069
4228
|
}
|
|
4070
|
-
|
|
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() {
|
|
4071
4236
|
try {
|
|
4072
4237
|
const resp = await withTimeout(fetch(OPENROUTER_MODELS_URL), FETCH_TIMEOUT_MS, "pricing fetch timed out");
|
|
4073
4238
|
if (!resp.ok) return null;
|
|
4074
4239
|
const data = await resp.json();
|
|
4075
4240
|
if (!Array.isArray(data?.data)) return null;
|
|
4076
|
-
const
|
|
4241
|
+
const prices = /* @__PURE__ */ new Map();
|
|
4242
|
+
const capabilities = /* @__PURE__ */ new Map();
|
|
4077
4243
|
for (const m of data.data) {
|
|
4078
|
-
if (!m?.id
|
|
4079
|
-
const
|
|
4080
|
-
|
|
4081
|
-
|
|
4082
|
-
|
|
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);
|
|
4083
4264
|
}
|
|
4084
|
-
return
|
|
4265
|
+
return { prices, capabilities };
|
|
4085
4266
|
} catch {
|
|
4086
4267
|
return null;
|
|
4087
4268
|
}
|
|
@@ -4092,7 +4273,8 @@ var LiveDataProvider = class {
|
|
|
4092
4273
|
const cache = {
|
|
4093
4274
|
fetchedAt: this.fetchedAt,
|
|
4094
4275
|
snapshot: this.snapshot ?? void 0,
|
|
4095
|
-
prices: Object.fromEntries(this.prices)
|
|
4276
|
+
prices: Object.fromEntries(this.prices),
|
|
4277
|
+
capabilities: Object.fromEntries(this.capabilities)
|
|
4096
4278
|
};
|
|
4097
4279
|
await fs9.writeFile(this.opts.cacheFile, JSON.stringify(cache, null, 2), "utf-8");
|
|
4098
4280
|
} catch {
|
|
@@ -4117,6 +4299,27 @@ var LiveDataProvider = class {
|
|
|
4117
4299
|
return { ...m, inputCostPer1kTokens: p.input, outputCostPer1kTokens: p.output };
|
|
4118
4300
|
});
|
|
4119
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
|
+
}
|
|
4120
4323
|
/** Where the active quality data came from — for /why and `cascade models`. */
|
|
4121
4324
|
getDataSource() {
|
|
4122
4325
|
return this.source;
|
|
@@ -4127,6 +4330,9 @@ var LiveDataProvider = class {
|
|
|
4127
4330
|
hasLivePricing() {
|
|
4128
4331
|
return this.prices.size > 0;
|
|
4129
4332
|
}
|
|
4333
|
+
hasCapabilities() {
|
|
4334
|
+
return this.capabilities.size > 0;
|
|
4335
|
+
}
|
|
4130
4336
|
};
|
|
4131
4337
|
|
|
4132
4338
|
// src/core/router/benchmarks.ts
|
|
@@ -4315,6 +4521,57 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter {
|
|
|
4315
4521
|
profiler.profileAll(allModels).catch(() => {
|
|
4316
4522
|
});
|
|
4317
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
|
+
}
|
|
4318
4575
|
/**
|
|
4319
4576
|
* Cascade Auto live data: discover/validate real model ids from each cloud
|
|
4320
4577
|
* provider, then fetch current public quality scores + per-token prices and
|
|
@@ -4364,12 +4621,17 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter {
|
|
|
4364
4621
|
await Promise.allSettled(tasks);
|
|
4365
4622
|
}
|
|
4366
4623
|
/**
|
|
4367
|
-
* Replace available models with live-priced
|
|
4368
|
-
*
|
|
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.
|
|
4369
4628
|
*/
|
|
4370
4629
|
applyLivePricing() {
|
|
4371
|
-
if (!this.liveData
|
|
4372
|
-
|
|
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);
|
|
4373
4635
|
for (const m of updated) this.selector.addDynamicModel(m);
|
|
4374
4636
|
for (const tier of ["T1", "T2", "T3"]) {
|
|
4375
4637
|
const cur = this.tierModels.get(tier);
|
|
@@ -4544,6 +4806,10 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter {
|
|
|
4544
4806
|
const r = this.config?.reinforcements;
|
|
4545
4807
|
return { enabled: r?.enabled === true, maxPerSection: r?.maxPerSection ?? 4 };
|
|
4546
4808
|
}
|
|
4809
|
+
/** Project-knowledge settings (config.knowledge). Facts extraction on by default. */
|
|
4810
|
+
getKnowledgeConfig() {
|
|
4811
|
+
return { factsExtraction: this.config?.knowledge?.factsExtraction !== false };
|
|
4812
|
+
}
|
|
4547
4813
|
/**
|
|
4548
4814
|
* Resolved T3 wave execution mode. 'auto' becomes 'sequential' when the T3
|
|
4549
4815
|
* tier resolves to a LOCAL model (the single-GPU queue serializes anyway, so
|
|
@@ -4630,10 +4896,10 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter {
|
|
|
4630
4896
|
* null when Cascade Auto is off (callers then use the shared tier model).
|
|
4631
4897
|
* Pure heuristic — no extra LLM call.
|
|
4632
4898
|
*/
|
|
4633
|
-
async selectModelForSubtask(tier, text) {
|
|
4899
|
+
async selectModelForSubtask(tier, text, opts) {
|
|
4634
4900
|
if (!this.config?.cascadeAuto || !this.taskAnalyzer || !text.trim()) return null;
|
|
4635
4901
|
try {
|
|
4636
|
-
return await this.taskAnalyzer.selectModel(text, tier, this.selector);
|
|
4902
|
+
return await this.taskAnalyzer.selectModel(text, tier, this.selector, opts);
|
|
4637
4903
|
} catch {
|
|
4638
4904
|
return null;
|
|
4639
4905
|
}
|
|
@@ -4974,6 +5240,13 @@ var BaseTier = class extends EventEmitter {
|
|
|
4974
5240
|
hierarchyContext = "";
|
|
4975
5241
|
/** Propagated AbortSignal — set by the tier's `execute()` before work begins. */
|
|
4976
5242
|
signal;
|
|
5243
|
+
/**
|
|
5244
|
+
* True for the run's ROOT tier (T3 for Simple, T2 for Moderate, T1 for
|
|
5245
|
+
* Complex). Its own synthesis stream is the user-facing answer and is
|
|
5246
|
+
* tagged `primary` so the desktop renders it live — background workers,
|
|
5247
|
+
* which would interleave, are not tagged.
|
|
5248
|
+
*/
|
|
5249
|
+
isPresenter = false;
|
|
4977
5250
|
constructor(role, id, parentId) {
|
|
4978
5251
|
super();
|
|
4979
5252
|
this.role = role;
|
|
@@ -4981,6 +5254,10 @@ var BaseTier = class extends EventEmitter {
|
|
|
4981
5254
|
this.parentId = parentId;
|
|
4982
5255
|
this.label = this.id;
|
|
4983
5256
|
}
|
|
5257
|
+
/** Mark this tier as the run's presenter (root tier). */
|
|
5258
|
+
setPresenter(on = true) {
|
|
5259
|
+
this.isPresenter = on;
|
|
5260
|
+
}
|
|
4984
5261
|
getStatus() {
|
|
4985
5262
|
return this.status;
|
|
4986
5263
|
}
|
|
@@ -5378,6 +5655,13 @@ EXAMPLE \u2014 calling the "shell" tool to list files:
|
|
|
5378
5655
|
{"name": "shell", "input": {"command": "ls -la"}}
|
|
5379
5656
|
</tool_call>
|
|
5380
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(", ")}.
|
|
5381
5665
|
When you have enough information, stop calling tools and write your final answer.`;
|
|
5382
5666
|
}
|
|
5383
5667
|
|
|
@@ -5617,6 +5901,9 @@ Now execute your subtask using this context where relevant.`
|
|
|
5617
5901
|
} catch (e) {
|
|
5618
5902
|
this.log("Failed to write to World State DB");
|
|
5619
5903
|
}
|
|
5904
|
+
if (this.router.getKnowledgeConfig?.().factsExtraction !== false) {
|
|
5905
|
+
await this.extractAndStoreFacts(db, assignment, output);
|
|
5906
|
+
}
|
|
5620
5907
|
}
|
|
5621
5908
|
this.setStatus("COMPLETED", output);
|
|
5622
5909
|
this.sendStatusUpdate({ progressPct: 100, currentAction: "Subtask complete", status: "IN_PROGRESS", output });
|
|
@@ -5678,7 +5965,7 @@ Now execute your subtask using this context where relevant.`
|
|
|
5678
5965
|
let subtaskModel;
|
|
5679
5966
|
try {
|
|
5680
5967
|
const subtaskText = `${this.assignment?.subtaskTitle ?? ""} ${this.assignment?.description ?? ""} ${this.assignment?.expectedOutput ?? ""}`;
|
|
5681
|
-
subtaskModel = await this.router.selectModelForSubtask("T3", subtaskText) ?? void 0;
|
|
5968
|
+
subtaskModel = await this.router.selectModelForSubtask("T3", subtaskText, { requiresToolUse: tools.length > 0 }) ?? void 0;
|
|
5682
5969
|
if (subtaskModel) {
|
|
5683
5970
|
this.log(`Cascade Auto: routing this subtask to ${subtaskModel.provider}:${subtaskModel.id}`);
|
|
5684
5971
|
}
|
|
@@ -5686,7 +5973,8 @@ Now execute your subtask using this context where relevant.`
|
|
|
5686
5973
|
}
|
|
5687
5974
|
const effectiveModel = subtaskModel ?? this.router.getModelForTier("T3");
|
|
5688
5975
|
const useTextTools = effectiveModel?.supportsToolUse === false && tools.length > 0;
|
|
5689
|
-
|
|
5976
|
+
let sentFullTextContract = false;
|
|
5977
|
+
let textContractSignature = "";
|
|
5690
5978
|
while (iterations < MAX_ITERATIONS) {
|
|
5691
5979
|
iterations++;
|
|
5692
5980
|
this.throwIfCancelled();
|
|
@@ -5700,6 +5988,17 @@ Now execute your subtask using this context where relevant.`
|
|
|
5700
5988
|
${g.text}`
|
|
5701
5989
|
});
|
|
5702
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
|
+
}
|
|
5703
6002
|
const options = {
|
|
5704
6003
|
messages: this.context.getMessages(),
|
|
5705
6004
|
systemPrompt: this.systemPromptOverride + systemPrompt + (this.hierarchyContext ? `
|
|
@@ -5716,7 +6015,7 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : "") + textToolSuffix,
|
|
|
5716
6015
|
"T3",
|
|
5717
6016
|
options,
|
|
5718
6017
|
(chunk) => {
|
|
5719
|
-
this.emit("stream:token", { tierId: this.id, text: chunk.text });
|
|
6018
|
+
this.emit("stream:token", { tierId: this.id, text: chunk.text, primary: this.isPresenter });
|
|
5720
6019
|
}
|
|
5721
6020
|
);
|
|
5722
6021
|
let effectiveToolCalls = result.toolCalls ?? [];
|
|
@@ -6189,6 +6488,41 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
|
6189
6488
|
};
|
|
6190
6489
|
}
|
|
6191
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
|
+
}
|
|
6192
6526
|
async correctOutput(originalOutput, failures) {
|
|
6193
6527
|
const correctionPrompt = `The following output failed these checks: ${failures.join(", ")}.
|
|
6194
6528
|
|
|
@@ -7177,6 +7511,7 @@ ${peerOutputs}` : "";
|
|
|
7177
7511
|
chunkEnd++;
|
|
7178
7512
|
}
|
|
7179
7513
|
i = chunkEnd;
|
|
7514
|
+
const isLastChunk = chunkEnd >= completed.length;
|
|
7180
7515
|
const prompt = `Summarize these T3 worker outputs for section "${assignment.sectionTitle}" in 2-3 sentences.
|
|
7181
7516
|
${currentSummary ? `
|
|
7182
7517
|
PREVIOUS SUMMARY SO FAR:
|
|
@@ -7186,6 +7521,7 @@ NEW OUTPUTS TO INTEGRATE:
|
|
|
7186
7521
|
` : "\nOUTPUTS:\n"}${chunkText}${peerContext}`;
|
|
7187
7522
|
const messages = [{ role: "user", content: prompt }];
|
|
7188
7523
|
try {
|
|
7524
|
+
const streamFinal = isLastChunk && this.isPresenter ? (chunk) => this.emit("stream:token", { tierId: this.id, text: chunk.text, primary: true }) : void 0;
|
|
7189
7525
|
const result = await this.router.generate("T2", {
|
|
7190
7526
|
messages,
|
|
7191
7527
|
systemPrompt: this.systemPromptOverride + "You are a T2 Manager. Summarize the work of your T3 workers succinctly." + (this.hierarchyContext ? `
|
|
@@ -7193,7 +7529,7 @@ NEW OUTPUTS TO INTEGRATE:
|
|
|
7193
7529
|
HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
7194
7530
|
maxTokens: 500,
|
|
7195
7531
|
...this.sectionModel ? { model: this.sectionModel } : {}
|
|
7196
|
-
});
|
|
7532
|
+
}, streamFinal);
|
|
7197
7533
|
currentSummary = result.content;
|
|
7198
7534
|
} catch (err) {
|
|
7199
7535
|
this.log(`aggregateResults: LLM summarization failed at chunk \u2014 returning raw T3 outputs. Error: ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -7245,14 +7581,11 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
|
7245
7581
|
...this.sectionModel ? { model: this.sectionModel } : {}
|
|
7246
7582
|
});
|
|
7247
7583
|
const answer = result.content.trim().toUpperCase();
|
|
7248
|
-
|
|
7249
|
-
|
|
7250
|
-
}
|
|
7251
|
-
if (answer.includes("NO")) {
|
|
7252
|
-
return { requestId: req.id, approved: false, always: true, decidedBy: "T2", reasoning: "T2 LLM evaluated: inconsistent with section goal" };
|
|
7253
|
-
}
|
|
7584
|
+
const verdict = answer.includes("YES") ? "approve" : answer.includes("NO") ? "deny" : "unsure";
|
|
7585
|
+
(req.trail ??= []).push({ tier: "T2", verdict, reason: `T2: ${verdict === "approve" ? "consistent with section goal" : verdict === "deny" ? "inconsistent with section goal" : "unsure"}` });
|
|
7254
7586
|
return null;
|
|
7255
7587
|
} catch {
|
|
7588
|
+
(req.trail ??= []).push({ tier: "T2", verdict: "unsure", reason: "T2 evaluation failed" });
|
|
7256
7589
|
return null;
|
|
7257
7590
|
}
|
|
7258
7591
|
}
|
|
@@ -7595,10 +7928,24 @@ In 3-5 terse bullets, flag the most important RISKS, GAPS, or over-/under-decomp
|
|
|
7595
7928
|
}
|
|
7596
7929
|
async decomposeTask(prompt, systemContext) {
|
|
7597
7930
|
const db = this.router.getWorldStateDB?.();
|
|
7598
|
-
|
|
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 = `
|
|
7599
7943
|
|
|
7600
7944
|
PROJECT WORLD STATE (Current architecture and recent changes):
|
|
7601
|
-
${
|
|
7945
|
+
${log}`;
|
|
7946
|
+
}
|
|
7947
|
+
}
|
|
7948
|
+
}
|
|
7602
7949
|
const contextSection = systemContext ? `
|
|
7603
7950
|
Project context:
|
|
7604
7951
|
${systemContext}` : "";
|
|
@@ -7934,7 +8281,7 @@ Instructions:
|
|
|
7934
8281
|
systemPrompt: this.systemPromptOverride + "You are a final output compiler. Summarize and format the task results clearly.",
|
|
7935
8282
|
maxTokens: 8e3
|
|
7936
8283
|
}, (chunk) => {
|
|
7937
|
-
this.emit("stream:token", { tierId: this.id, text: chunk.text });
|
|
8284
|
+
this.emit("stream:token", { tierId: this.id, text: chunk.text, primary: this.isPresenter });
|
|
7938
8285
|
});
|
|
7939
8286
|
return result.content;
|
|
7940
8287
|
}
|
|
@@ -7963,14 +8310,11 @@ Reply with exactly one word: YES, NO, or UNSURE.
|
|
|
7963
8310
|
temperature: 0
|
|
7964
8311
|
});
|
|
7965
8312
|
const answer = result.content.trim().toUpperCase();
|
|
7966
|
-
|
|
7967
|
-
|
|
7968
|
-
}
|
|
7969
|
-
if (answer.includes("NO")) {
|
|
7970
|
-
return { requestId: req.id, approved: false, always: true, decidedBy: "T1", reasoning: "T1 evaluated: not consistent with overall task goal" };
|
|
7971
|
-
}
|
|
8313
|
+
const verdict = answer.includes("YES") ? "approve" : answer.includes("NO") ? "deny" : "unsure";
|
|
8314
|
+
(req.trail ??= []).push({ tier: "T1", verdict, reason: `T1: ${verdict === "approve" ? "consistent with overall task goal" : verdict === "deny" ? "not consistent with overall task goal" : "unsure"}` });
|
|
7972
8315
|
return null;
|
|
7973
8316
|
} catch {
|
|
8317
|
+
(req.trail ??= []).push({ tier: "T1", verdict: "unsure", reason: "T1 evaluation failed" });
|
|
7974
8318
|
return null;
|
|
7975
8319
|
}
|
|
7976
8320
|
}
|
|
@@ -9984,13 +10328,17 @@ var TaskAnalyzer = class {
|
|
|
9984
10328
|
* Scores tier-eligible models using cost efficiency + historical performance.
|
|
9985
10329
|
* Falls back to the priority-list default when no candidates have history.
|
|
9986
10330
|
*/
|
|
9987
|
-
async selectModel(prompt, tier, selector) {
|
|
10331
|
+
async selectModel(prompt, tier, selector, opts) {
|
|
9988
10332
|
const profile = await this.analyze(prompt);
|
|
9989
10333
|
if (profile.requiresVision) {
|
|
9990
10334
|
return selector.selectVisionModel();
|
|
9991
10335
|
}
|
|
9992
|
-
|
|
10336
|
+
let candidates2 = selector.getCandidatesForTier(tier);
|
|
9993
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
|
+
}
|
|
9994
10342
|
const scored = candidates2.map((m) => ({
|
|
9995
10343
|
model: m,
|
|
9996
10344
|
score: this.scoreModel(m, profile)
|
|
@@ -10169,6 +10517,28 @@ var ModelPerformanceTracker = class {
|
|
|
10169
10517
|
return Math.max(0.1, 1 - normalised * complexityWeight);
|
|
10170
10518
|
}
|
|
10171
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
|
+
}
|
|
10172
10542
|
var DYNAMIC_TOOLS_FILE = "dynamic-tools.json";
|
|
10173
10543
|
function normalizeToolSchema(schema) {
|
|
10174
10544
|
if (schema && schema["type"] === "object" && typeof schema["properties"] === "object") {
|
|
@@ -10271,7 +10641,12 @@ var DynamicTool = class extends BaseTool {
|
|
|
10271
10641
|
getEscalator;
|
|
10272
10642
|
/** Untrusted = loaded from disk / a peer; its dangerous calls always re-prompt. */
|
|
10273
10643
|
trusted;
|
|
10274
|
-
|
|
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
|
+
}) {
|
|
10275
10650
|
super();
|
|
10276
10651
|
this.name = spec.name;
|
|
10277
10652
|
this.description = spec.description;
|
|
@@ -10281,6 +10656,8 @@ var DynamicTool = class extends BaseTool {
|
|
|
10281
10656
|
this.registry = registry;
|
|
10282
10657
|
this.getEscalator = getEscalator;
|
|
10283
10658
|
this.trusted = trusted;
|
|
10659
|
+
this.getSandboxMode = getSandboxMode;
|
|
10660
|
+
this.log = log;
|
|
10284
10661
|
}
|
|
10285
10662
|
isDangerous() {
|
|
10286
10663
|
return this._isDangerous;
|
|
@@ -10317,8 +10694,94 @@ var DynamicTool = class extends BaseTool {
|
|
|
10317
10694
|
return `Error calling ${toolName}: ${err instanceof Error ? err.message : String(err)}`;
|
|
10318
10695
|
}
|
|
10319
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
|
+
}
|
|
10320
10706
|
return this.runInWorker(input, callTool);
|
|
10321
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
|
+
}
|
|
10322
10785
|
/** Spawn the worker, service its callTool/fetch bridge, enforce the kill timeout. */
|
|
10323
10786
|
runInWorker(input, callTool) {
|
|
10324
10787
|
const timeoutMs = Math.max(200, Number(process.env["CASCADE_DYNAMIC_TOOL_TIMEOUT_MS"]) || DYNAMIC_TOOL_TIMEOUT_MS);
|
|
@@ -10398,16 +10861,19 @@ var ToolCreator = class {
|
|
|
10398
10861
|
workspacePath;
|
|
10399
10862
|
/** When false, persisted tools are neither loaded nor written. */
|
|
10400
10863
|
persistEnabled;
|
|
10864
|
+
/** Sandbox runtime for generated tools; passed to each DynamicTool. */
|
|
10865
|
+
sandboxMode;
|
|
10401
10866
|
logger;
|
|
10402
10867
|
/** name → spec, for persistence, broadcast, and re-registration. */
|
|
10403
10868
|
specs = /* @__PURE__ */ new Map();
|
|
10404
10869
|
/** capability fingerprint → tool name, so the same need isn't re-generated. */
|
|
10405
10870
|
capabilityIndex = /* @__PURE__ */ new Map();
|
|
10406
|
-
constructor(router, registry, workspacePath, persistEnabled = true) {
|
|
10871
|
+
constructor(router, registry, workspacePath, persistEnabled = true, sandboxMode = "auto") {
|
|
10407
10872
|
this.router = router;
|
|
10408
10873
|
this.registry = registry;
|
|
10409
10874
|
this.workspacePath = workspacePath;
|
|
10410
10875
|
this.persistEnabled = persistEnabled;
|
|
10876
|
+
this.sandboxMode = sandboxMode;
|
|
10411
10877
|
}
|
|
10412
10878
|
setPermissionEscalator(escalator) {
|
|
10413
10879
|
this.escalator = escalator;
|
|
@@ -10495,7 +10961,14 @@ Required capability: ${description.slice(0, 300)}`;
|
|
|
10495
10961
|
this.specs.set(spec.name, spec);
|
|
10496
10962
|
return;
|
|
10497
10963
|
}
|
|
10498
|
-
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
|
+
);
|
|
10499
10972
|
this.registry.register(tool);
|
|
10500
10973
|
this.specs.set(spec.name, spec);
|
|
10501
10974
|
this.capabilityIndex.set(capabilityKey(`${spec.description}`), spec.name);
|
|
@@ -10544,6 +11017,9 @@ Required capability: ${description.slice(0, 300)}`;
|
|
|
10544
11017
|
return Array.from(this.specs.keys());
|
|
10545
11018
|
}
|
|
10546
11019
|
};
|
|
11020
|
+
function normalizeKey(text) {
|
|
11021
|
+
return text.trim().toLowerCase().replace(/\s+/g, " ");
|
|
11022
|
+
}
|
|
10547
11023
|
var WorldStateDB = class {
|
|
10548
11024
|
constructor(workspacePath, debugMode = false) {
|
|
10549
11025
|
this.workspacePath = workspacePath;
|
|
@@ -10565,6 +11041,16 @@ var WorldStateDB = class {
|
|
|
10565
11041
|
encrypted_payload TEXT NOT NULL
|
|
10566
11042
|
)
|
|
10567
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
|
+
`);
|
|
10568
11054
|
}
|
|
10569
11055
|
workspacePath;
|
|
10570
11056
|
debugMode;
|
|
@@ -10638,6 +11124,131 @@ var WorldStateDB = class {
|
|
|
10638
11124
|
if (entries.length === 0) return "World State is currently empty.";
|
|
10639
11125
|
return entries.map((e, idx) => `[${e.timestamp}] Step ${idx + 1} (${e.workerId}): ${e.summary}`).join("\n");
|
|
10640
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
|
+
}
|
|
10641
11252
|
dumpDebugIfNeeded() {
|
|
10642
11253
|
if (!this.debugMode) return;
|
|
10643
11254
|
try {
|
|
@@ -10774,7 +11385,8 @@ var Cascade = class _Cascade extends EventEmitter {
|
|
|
10774
11385
|
}
|
|
10775
11386
|
const cfg = this.config;
|
|
10776
11387
|
if (cfg["enableToolCreation"] === true) {
|
|
10777
|
-
|
|
11388
|
+
const sandboxMode = this.config.tools?.dynamicToolSandbox ?? "auto";
|
|
11389
|
+
this.toolCreator = new ToolCreator(this.router, this.toolRegistry, this.workspacePath, cfg["persistDynamicTools"] !== false, sandboxMode);
|
|
10778
11390
|
this.toolCreator.setLogger((m) => {
|
|
10779
11391
|
if (this.listenerCount("log") > 0) this.emit("log", { level: "info", message: m });
|
|
10780
11392
|
});
|
|
@@ -11012,6 +11624,10 @@ ${last.partialOutput}` : "");
|
|
|
11012
11624
|
this.router.profileModels(this.store).catch(() => {
|
|
11013
11625
|
});
|
|
11014
11626
|
}
|
|
11627
|
+
if (this.store) {
|
|
11628
|
+
this.router.probeLocalToolSupport(this.store).catch(() => {
|
|
11629
|
+
});
|
|
11630
|
+
}
|
|
11015
11631
|
if (this.config.cascadeAuto) {
|
|
11016
11632
|
this.router.refreshLiveData().catch(() => {
|
|
11017
11633
|
});
|
|
@@ -11066,6 +11682,21 @@ ${last.partialOutput}` : "");
|
|
|
11066
11682
|
const producesArtifact = /\b(?:create|build|implement|generate|write|refactor|rewrite|add|fix|deploy|install|migrate|scaffold|set up|save (?:a|the)|report|\.(?:pdf|md|txt|json|csv|py|js|ts|tsx|jsx|html|docx?))\b/i.test(p);
|
|
11067
11683
|
return inquiry && !producesArtifact;
|
|
11068
11684
|
}
|
|
11685
|
+
/**
|
|
11686
|
+
* Strong, explicit signals that a task needs the full hierarchy (planning +
|
|
11687
|
+
* multiple sections/workers). Deliberately conservative: requires a
|
|
11688
|
+
* build/implementation verb AND either an app/system-scale noun or an
|
|
11689
|
+
* explicit multi-part structure, so ordinary single-file asks (handled as
|
|
11690
|
+
* Simple/Moderate) don't get over-escalated.
|
|
11691
|
+
*/
|
|
11692
|
+
looksClearlyComplex(prompt) {
|
|
11693
|
+
const p = prompt.trim();
|
|
11694
|
+
if (p.length < 24) return false;
|
|
11695
|
+
const buildVerb = /\b(?:build|implement|create|develop|design|scaffold|refactor|migrate|architect|set up|integrate)\b/i.test(p);
|
|
11696
|
+
const scaleNoun = /\b(?:app(?:lication)?|system|platform|service|api|backend|frontend|full[- ]?stack|website|dashboard|pipeline|microservices?|database schema|authentication|end[- ]to[- ]end|codebase|project|multiple files|several (?:files|modules|components)|test suite)\b/i.test(p);
|
|
11697
|
+
const multiPart = /(?:\b(?:and|then|also|plus|as well as)\b.*\b(?:and|then|also)\b)|(?:^|\n)\s*(?:[-*]|\d+[.)])\s+/i.test(p);
|
|
11698
|
+
return buildVerb && (scaleNoun || multiPart);
|
|
11699
|
+
}
|
|
11069
11700
|
// Cache glob scan results per workspace path to avoid repeated I/O.
|
|
11070
11701
|
static globCache = /* @__PURE__ */ new Map();
|
|
11071
11702
|
async countWorkspaceFiles(workspacePath) {
|
|
@@ -11149,10 +11780,16 @@ ${prompt}` : prompt;
|
|
|
11149
11780
|
let verdict;
|
|
11150
11781
|
if (match) {
|
|
11151
11782
|
verdict = match[1] === "simple" ? "Simple" : match[1] === "moderate" ? "Moderate" : "Complex";
|
|
11152
|
-
|
|
11783
|
+
if (verdict !== "Complex" && this.looksClearlyComplex(prompt)) {
|
|
11784
|
+
this.recordDecision("complexity", `Complex \u2014 heuristic floor over classifier "${verdict}": explicit multi-step build/implementation signals (T1 engaged)`);
|
|
11785
|
+
verdict = "Complex";
|
|
11786
|
+
} else {
|
|
11787
|
+
this.recordDecision("complexity", `${verdict} \u2014 classifier: ${reason || "no reason given"}`);
|
|
11788
|
+
}
|
|
11153
11789
|
} else {
|
|
11154
|
-
|
|
11155
|
-
|
|
11790
|
+
const words = prompt.trim().split(/\s+/).length;
|
|
11791
|
+
verdict = words <= 12 ? "Simple" : words >= 40 || this.looksClearlyComplex(prompt) ? "Complex" : "Moderate";
|
|
11792
|
+
this.recordDecision("complexity", `${verdict} \u2014 classifier output unparseable; defaulted by length/signals`);
|
|
11156
11793
|
}
|
|
11157
11794
|
return verdict;
|
|
11158
11795
|
} catch {
|
|
@@ -11204,7 +11841,15 @@ ${prompt}` : prompt;
|
|
|
11204
11841
|
}
|
|
11205
11842
|
escalator.resolveUserDecision(req.id, approved, always);
|
|
11206
11843
|
});
|
|
11207
|
-
const
|
|
11844
|
+
const forceTier = this.config.routing?.forceTier;
|
|
11845
|
+
const forced = forceTier === "T1" ? "Complex" : forceTier === "T2" ? "Moderate" : forceTier === "T3" ? "Simple" : void 0;
|
|
11846
|
+
let complexity;
|
|
11847
|
+
if (forced) {
|
|
11848
|
+
complexity = forced;
|
|
11849
|
+
this.recordDecision("complexity", `${forced} \u2014 manually forced via routing.forceTier=${forceTier}`);
|
|
11850
|
+
} else {
|
|
11851
|
+
complexity = await this.determineComplexity(options.prompt, options.workspacePath || process.cwd(), options.conversationHistory);
|
|
11852
|
+
}
|
|
11208
11853
|
this.telemetry.capture("cascade:session_start", {
|
|
11209
11854
|
complexity,
|
|
11210
11855
|
providerCount: this.config.providers.length,
|
|
@@ -11284,6 +11929,7 @@ ${prompt}` : prompt;
|
|
|
11284
11929
|
try {
|
|
11285
11930
|
if (complexity === "Simple") {
|
|
11286
11931
|
const t3 = new T3Worker(this.router, this.toolRegistry, "root");
|
|
11932
|
+
t3.setPresenter(true);
|
|
11287
11933
|
t3.setHierarchyContext("You are the DIRECT worker for this task. There is no T1 Administrator or T2 Manager involved in this run.");
|
|
11288
11934
|
if (identityPrompt) {
|
|
11289
11935
|
t3.setSystemPromptOverride(identityPrompt);
|
|
@@ -11308,6 +11954,7 @@ ${prompt}` : prompt;
|
|
|
11308
11954
|
this.emit("tier:status", { tierId: "t3-root", status: "COMPLETED", role: "T3" });
|
|
11309
11955
|
} else if (complexity === "Moderate") {
|
|
11310
11956
|
const t2 = new T2Manager(this.router, this.toolRegistry, "root");
|
|
11957
|
+
t2.setPresenter(true);
|
|
11311
11958
|
t2.setHierarchyContext("You are the ROOT Manager for this task. There is no T1 Administrator involved in this run. You are responsible for decomposing the task and managing T3 workers directly.");
|
|
11312
11959
|
if (identityPrompt) {
|
|
11313
11960
|
t2.setSystemPromptOverride(identityPrompt);
|
|
@@ -11357,6 +12004,7 @@ ${prompt}` : prompt;
|
|
|
11357
12004
|
}
|
|
11358
12005
|
} else {
|
|
11359
12006
|
const t1 = new T1Administrator(this.router, this.toolRegistry, this.config);
|
|
12007
|
+
t1.setPresenter(true);
|
|
11360
12008
|
t1.setHierarchyContext("You are the top-level Administrator. You are responsible for the overall plan and supervising multiple T2 Managers.");
|
|
11361
12009
|
if (identityPrompt) {
|
|
11362
12010
|
t1.setSystemPromptOverride(identityPrompt);
|
|
@@ -15357,7 +16005,8 @@ var DashboardSocket = class {
|
|
|
15357
16005
|
socket.on("cascade:run", (payload) => {
|
|
15358
16006
|
if (typeof payload?.prompt === "string" && payload.prompt.trim()) {
|
|
15359
16007
|
const sessionId = typeof payload.sessionId === "string" && payload.sessionId.trim() ? payload.sessionId.trim() : void 0;
|
|
15360
|
-
|
|
16008
|
+
const forceTier = ["T1", "T2", "T3"].includes(payload.forceTier) ? payload.forceTier : void 0;
|
|
16009
|
+
callback(payload.prompt.trim(), payload.model ?? "auto", socket.id, sessionId, forceTier);
|
|
15361
16010
|
}
|
|
15362
16011
|
});
|
|
15363
16012
|
});
|
|
@@ -15387,6 +16036,13 @@ var DashboardServer = class {
|
|
|
15387
16036
|
* of runs the session performed in this server's lifetime.
|
|
15388
16037
|
*/
|
|
15389
16038
|
sessionTaskIds = /* @__PURE__ */ new Map();
|
|
16039
|
+
/**
|
|
16040
|
+
* Tool-approval requests awaiting a user decision from a connected client,
|
|
16041
|
+
* keyed by the request's uuid. The desktop shows a modal on
|
|
16042
|
+
* `permission:user-required` and answers with `permission:decision`; this
|
|
16043
|
+
* map is how that answer reaches the run that's blocked on it.
|
|
16044
|
+
*/
|
|
16045
|
+
pendingApprovals = /* @__PURE__ */ new Map();
|
|
15390
16046
|
port;
|
|
15391
16047
|
host;
|
|
15392
16048
|
workspacePath;
|
|
@@ -15445,17 +16101,18 @@ var DashboardServer = class {
|
|
|
15445
16101
|
}
|
|
15446
16102
|
this.persistConfig();
|
|
15447
16103
|
});
|
|
15448
|
-
this.socket.onCascadeRun(async (prompt, model, socketId, requestedSessionId) => {
|
|
16104
|
+
this.socket.onCascadeRun(async (prompt, model, socketId, requestedSessionId, forceTier) => {
|
|
15449
16105
|
const sessionId = requestedSessionId ?? randomUUID();
|
|
15450
16106
|
const abortController = new AbortController();
|
|
15451
16107
|
this.activeControllers.set(sessionId, abortController);
|
|
15452
16108
|
const runPrompt = requestedSessionId ? this.buildContinuationPrompt(sessionId, prompt) : prompt;
|
|
15453
16109
|
const title = this.persistRunStart(sessionId, prompt);
|
|
15454
|
-
|
|
16110
|
+
let cfg = model !== "auto" ? { ...this.config, models: { ...this.config.models, t1: model } } : this.config;
|
|
16111
|
+
if (forceTier) cfg = { ...cfg, routing: { ...cfg.routing, forceTier } };
|
|
15455
16112
|
const cascade = new Cascade(cfg, this.workspacePath, this.store);
|
|
15456
16113
|
this.activeSessions.set(sessionId, cascade);
|
|
15457
16114
|
cascade.on("stream:token", (e) => {
|
|
15458
|
-
this.socket.emitToSocket(socketId, "stream:token", { sessionId, tierId: e.tierId, text: e.text });
|
|
16115
|
+
this.socket.emitToSocket(socketId, "stream:token", { sessionId, tierId: e.tierId, text: e.text, primary: e.primary });
|
|
15459
16116
|
});
|
|
15460
16117
|
cascade.on("tier:status", (e) => {
|
|
15461
16118
|
this.socket.emitToSocket(socketId, "tier:status", { sessionId, ...e });
|
|
@@ -15467,7 +16124,11 @@ var DashboardServer = class {
|
|
|
15467
16124
|
this.socket.emitPeerMessage(e);
|
|
15468
16125
|
});
|
|
15469
16126
|
try {
|
|
15470
|
-
const result = await cascade.run({
|
|
16127
|
+
const result = await cascade.run({
|
|
16128
|
+
prompt: runPrompt,
|
|
16129
|
+
signal: abortController.signal,
|
|
16130
|
+
approvalCallback: this.makeApprovalCallback(sessionId)
|
|
16131
|
+
});
|
|
15471
16132
|
this.recordSessionTask(sessionId, result.taskId);
|
|
15472
16133
|
this.persistRunEnd(sessionId, title, prompt, result.output, "COMPLETED");
|
|
15473
16134
|
this.socket.emitToSocket(socketId, "session:complete", { sessionId, result });
|
|
@@ -15486,10 +16147,18 @@ var DashboardServer = class {
|
|
|
15486
16147
|
} finally {
|
|
15487
16148
|
this.activeSessions.delete(sessionId);
|
|
15488
16149
|
this.activeControllers.delete(sessionId);
|
|
16150
|
+
this.denyPendingApprovals(sessionId);
|
|
15489
16151
|
}
|
|
15490
16152
|
});
|
|
15491
16153
|
this.socket.onSessionHalt((sessionId) => {
|
|
15492
16154
|
this.activeControllers.get(sessionId)?.abort();
|
|
16155
|
+
this.denyPendingApprovals(sessionId);
|
|
16156
|
+
});
|
|
16157
|
+
this.socket.onApprovalResponse(({ requestId, approved, always }) => {
|
|
16158
|
+
const pending = this.pendingApprovals.get(requestId);
|
|
16159
|
+
if (!pending) return;
|
|
16160
|
+
this.pendingApprovals.delete(requestId);
|
|
16161
|
+
pending.resolve({ approved: !!approved, always: !!always });
|
|
15493
16162
|
});
|
|
15494
16163
|
this.socket.onSessionSteer((message, sessionId, nodeId) => {
|
|
15495
16164
|
const steered = this.steerSessions(message, sessionId, nodeId);
|
|
@@ -15657,6 +16326,29 @@ var DashboardServer = class {
|
|
|
15657
16326
|
list.push(taskId);
|
|
15658
16327
|
this.sessionTaskIds.set(sessionId, list);
|
|
15659
16328
|
}
|
|
16329
|
+
/**
|
|
16330
|
+
* Approval bridge: cascade calls this when a dangerous tool escalates to the
|
|
16331
|
+
* user. The request itself was already pushed to the client via the
|
|
16332
|
+
* `permission:user-required` forward; here we just park a resolver keyed by
|
|
16333
|
+
* the request id and wait for the client's `permission:decision` (handled in
|
|
16334
|
+
* onApprovalResponse). Never auto-approves — an unanswered request stays
|
|
16335
|
+
* pending until the client answers, the run ends, or the escalator's own
|
|
16336
|
+
* timeout denies it.
|
|
16337
|
+
*/
|
|
16338
|
+
makeApprovalCallback(sessionId) {
|
|
16339
|
+
return (request) => new Promise((resolve) => {
|
|
16340
|
+
this.pendingApprovals.set(request.id, { resolve, sessionId });
|
|
16341
|
+
});
|
|
16342
|
+
}
|
|
16343
|
+
/** Deny + clear any approvals still pending for a session (run end / abort). */
|
|
16344
|
+
denyPendingApprovals(sessionId) {
|
|
16345
|
+
for (const [id, pending] of this.pendingApprovals) {
|
|
16346
|
+
if (pending.sessionId === sessionId) {
|
|
16347
|
+
this.pendingApprovals.delete(id);
|
|
16348
|
+
pending.resolve({ approved: false, always: false });
|
|
16349
|
+
}
|
|
16350
|
+
}
|
|
16351
|
+
}
|
|
15660
16352
|
persistRuntimeRow(sessionId, title, status, latestPrompt) {
|
|
15661
16353
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
15662
16354
|
const row = { sessionId, title, workspacePath: this.workspacePath, status, startedAt: now, updatedAt: now, latestPrompt, isGlobal: false };
|
|
@@ -15845,15 +16537,6 @@ ${prompt}`;
|
|
|
15845
16537
|
if (sessionId) this.socket.broadcastToRoom(`session:${sessionId}`, "session:halt", payload);
|
|
15846
16538
|
res.json({ success: true, ...payload });
|
|
15847
16539
|
});
|
|
15848
|
-
this.app.post("/api/approve", auth, mutationLimiter, (req, res) => {
|
|
15849
|
-
const body = req.body;
|
|
15850
|
-
const sessionId = typeof body["sessionId"] === "string" ? body["sessionId"] : void 0;
|
|
15851
|
-
const nodeId = typeof body["nodeId"] === "string" ? body["nodeId"] : void 0;
|
|
15852
|
-
const payload = { sessionId, nodeId, requestedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
15853
|
-
this.socket.broadcast("session:approve", payload);
|
|
15854
|
-
if (sessionId) this.socket.broadcastToRoom(`session:${sessionId}`, "session:approve", payload);
|
|
15855
|
-
res.json({ success: true, ...payload });
|
|
15856
|
-
});
|
|
15857
16540
|
this.app.post("/api/inject", auth, mutationLimiter, (req, res) => {
|
|
15858
16541
|
const body = req.body;
|
|
15859
16542
|
const message = typeof body["message"] === "string" ? body["message"] : void 0;
|
|
@@ -15898,6 +16581,60 @@ ${prompt}`;
|
|
|
15898
16581
|
this.socket.broadcast("runtime:refresh", { scope: "global" });
|
|
15899
16582
|
res.json({ ok: true });
|
|
15900
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
|
+
});
|
|
15901
16638
|
this.app.post("/api/sessions/:id/rollback", auth, mutationLimiter, async (req, res) => {
|
|
15902
16639
|
const sessionId = req.params.id;
|
|
15903
16640
|
const taskIds = this.sessionTaskIds.get(sessionId) ?? [];
|
|
@@ -16104,7 +16841,7 @@ ${prompt}`;
|
|
|
16104
16841
|
const cascade = new Cascade(this.config, this.workspacePath, this.store);
|
|
16105
16842
|
this.activeSessions.set(sessionId, cascade);
|
|
16106
16843
|
cascade.on("stream:token", (e) => {
|
|
16107
|
-
this.socket.broadcastToRoom(`session:${sessionId}`, "stream:token", { sessionId, tierId: e.tierId, text: e.text });
|
|
16844
|
+
this.socket.broadcastToRoom(`session:${sessionId}`, "stream:token", { sessionId, tierId: e.tierId, text: e.text, primary: e.primary });
|
|
16108
16845
|
});
|
|
16109
16846
|
cascade.on("tier:status", (e) => {
|
|
16110
16847
|
this.socket.broadcastToRoom(`session:${sessionId}`, "tier:status", { sessionId, ...e });
|
|
@@ -16116,7 +16853,11 @@ ${prompt}`;
|
|
|
16116
16853
|
this.socket.emitPeerMessage(e);
|
|
16117
16854
|
});
|
|
16118
16855
|
try {
|
|
16119
|
-
const result = await cascade.run({
|
|
16856
|
+
const result = await cascade.run({
|
|
16857
|
+
prompt: runPrompt,
|
|
16858
|
+
identityId: body.identityId,
|
|
16859
|
+
approvalCallback: this.makeApprovalCallback(sessionId)
|
|
16860
|
+
});
|
|
16120
16861
|
this.recordSessionTask(sessionId, result.taskId);
|
|
16121
16862
|
this.persistRunEnd(sessionId, title, prompt, result.output, "COMPLETED");
|
|
16122
16863
|
this.socket.broadcast("cost:update", {
|
|
@@ -16134,6 +16875,7 @@ ${prompt}`;
|
|
|
16134
16875
|
});
|
|
16135
16876
|
} finally {
|
|
16136
16877
|
this.activeSessions.delete(sessionId);
|
|
16878
|
+
this.denyPendingApprovals(sessionId);
|
|
16137
16879
|
}
|
|
16138
16880
|
})();
|
|
16139
16881
|
});
|