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/desktop-core.cjs
CHANGED
|
@@ -23507,7 +23507,7 @@ var require_verify_stream = __commonJS({
|
|
|
23507
23507
|
function isObject4(thing) {
|
|
23508
23508
|
return Object.prototype.toString.call(thing) === "[object Object]";
|
|
23509
23509
|
}
|
|
23510
|
-
function
|
|
23510
|
+
function safeJsonParse2(thing) {
|
|
23511
23511
|
if (isObject4(thing))
|
|
23512
23512
|
return thing;
|
|
23513
23513
|
try {
|
|
@@ -23518,7 +23518,7 @@ var require_verify_stream = __commonJS({
|
|
|
23518
23518
|
}
|
|
23519
23519
|
function headerFromJWS(jwsSig) {
|
|
23520
23520
|
var encodedHeader = jwsSig.split(".", 1)[0];
|
|
23521
|
-
return
|
|
23521
|
+
return safeJsonParse2(Buffer5.from(encodedHeader, "base64").toString("binary"));
|
|
23522
23522
|
}
|
|
23523
23523
|
function securedInputFromJWS(jwsSig) {
|
|
23524
23524
|
return jwsSig.split(".", 2).join(".");
|
|
@@ -222595,7 +222595,7 @@ Anthropic.Models = Models2;
|
|
|
222595
222595
|
Anthropic.Beta = Beta;
|
|
222596
222596
|
|
|
222597
222597
|
// src/constants.ts
|
|
222598
|
-
var CASCADE_VERSION = "0.
|
|
222598
|
+
var CASCADE_VERSION = "0.15.1";
|
|
222599
222599
|
var CASCADE_CONFIG_DIR = ".cascade";
|
|
222600
222600
|
var CASCADE_MD_FILE = "CASCADE.md";
|
|
222601
222601
|
var CASCADE_IGNORE_FILE = ".cascadeignore";
|
|
@@ -249744,29 +249744,71 @@ var OllamaProvider = class extends BaseProvider {
|
|
|
249744
249744
|
async countTokens(text) {
|
|
249745
249745
|
return Math.ceil(text.length / 4);
|
|
249746
249746
|
}
|
|
249747
|
+
/**
|
|
249748
|
+
* Ask the Ollama server what a model can actually do. Modern Ollama returns a
|
|
249749
|
+
* `capabilities` array ("completion", "tools", "vision", …) and the model's
|
|
249750
|
+
* real context length in `model_info` — authoritative, unlike the hardcoded
|
|
249751
|
+
* family-name allowlist, which silently misclassifies any unlisted family.
|
|
249752
|
+
* Best-effort: null on old servers / errors, callers fall back to heuristics.
|
|
249753
|
+
*/
|
|
249754
|
+
async showModel(name) {
|
|
249755
|
+
const ac = new AbortController();
|
|
249756
|
+
const timer = setTimeout(() => ac.abort(), 2500);
|
|
249757
|
+
try {
|
|
249758
|
+
const resp = await fetch(`${this.baseUrl}/api/show`, {
|
|
249759
|
+
method: "POST",
|
|
249760
|
+
headers: { "Content-Type": "application/json" },
|
|
249761
|
+
body: JSON.stringify({ model: name }),
|
|
249762
|
+
signal: ac.signal
|
|
249763
|
+
});
|
|
249764
|
+
if (!resp.ok) return null;
|
|
249765
|
+
const data = await resp.json();
|
|
249766
|
+
const out = {};
|
|
249767
|
+
if (Array.isArray(data.capabilities)) {
|
|
249768
|
+
out.tools = data.capabilities.includes("tools");
|
|
249769
|
+
out.vision = data.capabilities.includes("vision");
|
|
249770
|
+
}
|
|
249771
|
+
for (const [k4, v6] of Object.entries(data.model_info ?? {})) {
|
|
249772
|
+
if (k4.endsWith(".context_length") && typeof v6 === "number" && v6 > 0) {
|
|
249773
|
+
out.contextWindow = v6;
|
|
249774
|
+
break;
|
|
249775
|
+
}
|
|
249776
|
+
}
|
|
249777
|
+
return out.tools !== void 0 || out.contextWindow ? out : null;
|
|
249778
|
+
} catch {
|
|
249779
|
+
return null;
|
|
249780
|
+
} finally {
|
|
249781
|
+
clearTimeout(timer);
|
|
249782
|
+
}
|
|
249783
|
+
}
|
|
249747
249784
|
async listModels() {
|
|
249748
249785
|
try {
|
|
249749
249786
|
const response = await fetch(`${this.baseUrl}/api/tags`);
|
|
249750
249787
|
if (!response.ok) return [];
|
|
249751
249788
|
const data = await response.json();
|
|
249752
249789
|
const supportedKeywords = ["llama3", "llama2", "gemma", "mistral", "mixtral", "qwen", "phi3", "codellama", "deepseek", "llava", "starcoder", "stable-code", "nomic-embed"];
|
|
249753
|
-
|
|
249790
|
+
const entries = data.models.filter((m3) => {
|
|
249754
249791
|
const name = m3.name.toLowerCase();
|
|
249755
249792
|
return supportedKeywords.some((k4) => name.includes(k4));
|
|
249756
|
-
})
|
|
249757
|
-
|
|
249758
|
-
|
|
249759
|
-
|
|
249760
|
-
|
|
249761
|
-
|
|
249762
|
-
|
|
249763
|
-
|
|
249764
|
-
|
|
249765
|
-
|
|
249766
|
-
|
|
249767
|
-
|
|
249768
|
-
|
|
249769
|
-
|
|
249793
|
+
});
|
|
249794
|
+
const shows = await Promise.all(entries.map((m3) => this.showModel(m3.name)));
|
|
249795
|
+
return entries.map((m3, i4) => {
|
|
249796
|
+
const show = shows[i4];
|
|
249797
|
+
return {
|
|
249798
|
+
id: m3.name,
|
|
249799
|
+
name: m3.name,
|
|
249800
|
+
provider: "ollama",
|
|
249801
|
+
contextWindow: show?.contextWindow ?? 128e3,
|
|
249802
|
+
isVisionCapable: show?.vision ?? (m3.name.includes("llava") || m3.name.includes("vision")),
|
|
249803
|
+
inputCostPer1kTokens: 0,
|
|
249804
|
+
outputCostPer1kTokens: 0,
|
|
249805
|
+
maxOutputTokens: 4e3,
|
|
249806
|
+
supportsStreaming: true,
|
|
249807
|
+
isLocal: true,
|
|
249808
|
+
supportsToolUse: show?.tools ?? isToolCapable(m3.name),
|
|
249809
|
+
minSizeB: this.parseSizeB(m3.details?.parameter_size)
|
|
249810
|
+
};
|
|
249811
|
+
});
|
|
249770
249812
|
} catch {
|
|
249771
249813
|
return [];
|
|
249772
249814
|
}
|
|
@@ -250445,6 +250487,7 @@ function normalizeModelId(id) {
|
|
|
250445
250487
|
var LiveDataProvider = class {
|
|
250446
250488
|
snapshot = null;
|
|
250447
250489
|
prices = /* @__PURE__ */ new Map();
|
|
250490
|
+
capabilities = /* @__PURE__ */ new Map();
|
|
250448
250491
|
source = "bundled";
|
|
250449
250492
|
fetchedAt = 0;
|
|
250450
250493
|
loaded = false;
|
|
@@ -250473,6 +250516,9 @@ var LiveDataProvider = class {
|
|
|
250473
250516
|
if (cache.prices) {
|
|
250474
250517
|
for (const [id, p3] of Object.entries(cache.prices)) this.prices.set(id, p3);
|
|
250475
250518
|
}
|
|
250519
|
+
if (cache.capabilities) {
|
|
250520
|
+
for (const [id, c4] of Object.entries(cache.capabilities)) this.capabilities.set(id, c4);
|
|
250521
|
+
}
|
|
250476
250522
|
this.fetchedAt = cache.fetchedAt ?? 0;
|
|
250477
250523
|
} catch {
|
|
250478
250524
|
}
|
|
@@ -250493,9 +250539,9 @@ var LiveDataProvider = class {
|
|
|
250493
250539
|
const ttlMs = this.opts.refreshHours * 36e5;
|
|
250494
250540
|
const fresh = ttlMs > 0 && Date.now() - this.fetchedAt < ttlMs;
|
|
250495
250541
|
if (!force && fresh && this.source !== "bundled") return;
|
|
250496
|
-
const [snap,
|
|
250542
|
+
const [snap, catalog] = await Promise.all([
|
|
250497
250543
|
this.opts.live ? this.fetchSnapshot() : Promise.resolve(null),
|
|
250498
|
-
this.opts.pricingLive ? this.
|
|
250544
|
+
this.opts.pricingLive ? this.fetchCatalog() : Promise.resolve(null)
|
|
250499
250545
|
]);
|
|
250500
250546
|
let changed = false;
|
|
250501
250547
|
if (snap) {
|
|
@@ -250503,8 +250549,12 @@ var LiveDataProvider = class {
|
|
|
250503
250549
|
this.source = "live";
|
|
250504
250550
|
changed = true;
|
|
250505
250551
|
}
|
|
250506
|
-
if (
|
|
250507
|
-
this.prices = prices;
|
|
250552
|
+
if (catalog && catalog.prices.size > 0) {
|
|
250553
|
+
this.prices = catalog.prices;
|
|
250554
|
+
changed = true;
|
|
250555
|
+
}
|
|
250556
|
+
if (catalog && catalog.capabilities.size > 0) {
|
|
250557
|
+
this.capabilities = catalog.capabilities;
|
|
250508
250558
|
changed = true;
|
|
250509
250559
|
}
|
|
250510
250560
|
if (changed) {
|
|
@@ -250526,21 +250576,43 @@ var LiveDataProvider = class {
|
|
|
250526
250576
|
return null;
|
|
250527
250577
|
}
|
|
250528
250578
|
}
|
|
250529
|
-
|
|
250579
|
+
/**
|
|
250580
|
+
* One fetch of the OpenRouter catalog yields BOTH pricing and capability
|
|
250581
|
+
* facts (context window, native tool support, input modalities) — the
|
|
250582
|
+
* capability fields used to be discarded while providers guessed/hardcoded
|
|
250583
|
+
* them in their listModels() stubs.
|
|
250584
|
+
*/
|
|
250585
|
+
async fetchCatalog() {
|
|
250530
250586
|
try {
|
|
250531
250587
|
const resp = await withTimeout(fetch(OPENROUTER_MODELS_URL), FETCH_TIMEOUT_MS, "pricing fetch timed out");
|
|
250532
250588
|
if (!resp.ok) return null;
|
|
250533
250589
|
const data = await resp.json();
|
|
250534
250590
|
if (!Array.isArray(data?.data)) return null;
|
|
250535
|
-
const
|
|
250591
|
+
const prices = /* @__PURE__ */ new Map();
|
|
250592
|
+
const capabilities = /* @__PURE__ */ new Map();
|
|
250536
250593
|
for (const m3 of data.data) {
|
|
250537
|
-
if (!m3?.id
|
|
250538
|
-
const
|
|
250539
|
-
|
|
250540
|
-
|
|
250541
|
-
|
|
250594
|
+
if (!m3?.id) continue;
|
|
250595
|
+
const key = normalizeModelId(m3.id);
|
|
250596
|
+
if (m3.pricing) {
|
|
250597
|
+
const input = Number(m3.pricing.prompt) * 1e3;
|
|
250598
|
+
const output = Number(m3.pricing.completion) * 1e3;
|
|
250599
|
+
if (Number.isFinite(input) && Number.isFinite(output)) {
|
|
250600
|
+
prices.set(key, { input, output });
|
|
250601
|
+
}
|
|
250602
|
+
}
|
|
250603
|
+
const cap = {};
|
|
250604
|
+
if (typeof m3.context_length === "number" && m3.context_length > 0) {
|
|
250605
|
+
cap.contextWindow = m3.context_length;
|
|
250606
|
+
}
|
|
250607
|
+
if (Array.isArray(m3.supported_parameters)) {
|
|
250608
|
+
cap.supportsTools = m3.supported_parameters.includes("tools");
|
|
250609
|
+
}
|
|
250610
|
+
if (Array.isArray(m3.architecture?.input_modalities)) {
|
|
250611
|
+
cap.inputModalities = m3.architecture.input_modalities;
|
|
250612
|
+
}
|
|
250613
|
+
if (Object.keys(cap).length > 0) capabilities.set(key, cap);
|
|
250542
250614
|
}
|
|
250543
|
-
return
|
|
250615
|
+
return { prices, capabilities };
|
|
250544
250616
|
} catch {
|
|
250545
250617
|
return null;
|
|
250546
250618
|
}
|
|
@@ -250551,7 +250623,8 @@ var LiveDataProvider = class {
|
|
|
250551
250623
|
const cache = {
|
|
250552
250624
|
fetchedAt: this.fetchedAt,
|
|
250553
250625
|
snapshot: this.snapshot ?? void 0,
|
|
250554
|
-
prices: Object.fromEntries(this.prices)
|
|
250626
|
+
prices: Object.fromEntries(this.prices),
|
|
250627
|
+
capabilities: Object.fromEntries(this.capabilities)
|
|
250555
250628
|
};
|
|
250556
250629
|
await fs8__namespace.default.writeFile(this.opts.cacheFile, JSON.stringify(cache, null, 2), "utf-8");
|
|
250557
250630
|
} catch {
|
|
@@ -250576,6 +250649,27 @@ var LiveDataProvider = class {
|
|
|
250576
250649
|
return { ...m3, inputCostPer1kTokens: p3.input, outputCostPer1kTokens: p3.output };
|
|
250577
250650
|
});
|
|
250578
250651
|
}
|
|
250652
|
+
/** Current capability facts for a model id, or null when unknown. */
|
|
250653
|
+
getCapability(modelId) {
|
|
250654
|
+
return this.capabilities.get(normalizeModelId(modelId)) ?? null;
|
|
250655
|
+
}
|
|
250656
|
+
/**
|
|
250657
|
+
* Returns capability-corrected copies of each model (originals untouched):
|
|
250658
|
+
* real context windows replace the providers' hardcoded guesses, native
|
|
250659
|
+
* tool support replaces the assume-by-provider default, and vision is set
|
|
250660
|
+
* from the declared input modalities.
|
|
250661
|
+
*/
|
|
250662
|
+
applyLiveCapabilities(models) {
|
|
250663
|
+
return models.map((m3) => {
|
|
250664
|
+
const c4 = this.getCapability(m3.id);
|
|
250665
|
+
if (!c4) return m3;
|
|
250666
|
+
const next = { ...m3 };
|
|
250667
|
+
if (c4.contextWindow) next.contextWindow = c4.contextWindow;
|
|
250668
|
+
if (c4.supportsTools !== void 0) next.supportsToolUse = c4.supportsTools;
|
|
250669
|
+
if (c4.inputModalities) next.isVisionCapable = c4.inputModalities.includes("image");
|
|
250670
|
+
return next;
|
|
250671
|
+
});
|
|
250672
|
+
}
|
|
250579
250673
|
/** Where the active quality data came from — for /why and `cascade models`. */
|
|
250580
250674
|
getDataSource() {
|
|
250581
250675
|
return this.source;
|
|
@@ -250586,6 +250680,9 @@ var LiveDataProvider = class {
|
|
|
250586
250680
|
hasLivePricing() {
|
|
250587
250681
|
return this.prices.size > 0;
|
|
250588
250682
|
}
|
|
250683
|
+
hasCapabilities() {
|
|
250684
|
+
return this.capabilities.size > 0;
|
|
250685
|
+
}
|
|
250589
250686
|
};
|
|
250590
250687
|
|
|
250591
250688
|
// src/core/router/benchmarks.ts
|
|
@@ -250774,6 +250871,57 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
|
|
|
250774
250871
|
profiler.profileAll(allModels).catch(() => {
|
|
250775
250872
|
});
|
|
250776
250873
|
}
|
|
250874
|
+
/**
|
|
250875
|
+
* One-time native tool-call probe for local/compat models with NO capability
|
|
250876
|
+
* metadata. llama.cpp / LM Studio "/models" endpoints return ids only, so
|
|
250877
|
+
* `supportsToolUse` stays undefined and the T3 tool gate ASSUMES native
|
|
250878
|
+
* support — wrong for many local builds, which then fumble tool-format
|
|
250879
|
+
* output. Each unknown model is probed once with a trivial tool; the verdict
|
|
250880
|
+
* persists in the MemoryStore's model_cache (via the previously-dormant
|
|
250881
|
+
* getModelProfile read-back), so a model is probed once EVER, not per run.
|
|
250882
|
+
* Cloud providers are never probed — their metadata is authoritative.
|
|
250883
|
+
*/
|
|
250884
|
+
async probeLocalToolSupport(store) {
|
|
250885
|
+
const unknown2 = this.selector.getAllAvailableModels().filter(
|
|
250886
|
+
(m3) => m3.provider === "openai-compatible" && m3.supportsToolUse === void 0
|
|
250887
|
+
);
|
|
250888
|
+
for (const model of unknown2) {
|
|
250889
|
+
const cached2 = store.getModelProfile(model.id, model.provider);
|
|
250890
|
+
let verdict = cached2?.supportsToolUse;
|
|
250891
|
+
if (verdict === void 0) {
|
|
250892
|
+
const probed = await this.probeNativeToolCall(model);
|
|
250893
|
+
if (probed === null) continue;
|
|
250894
|
+
verdict = probed;
|
|
250895
|
+
store.saveModelCapability(model.id, model.provider, { supportsToolUse: verdict });
|
|
250896
|
+
}
|
|
250897
|
+
this.selector.addDynamicModel({ ...model, supportsToolUse: verdict });
|
|
250898
|
+
}
|
|
250899
|
+
for (const tier of ["T1", "T2", "T3"]) {
|
|
250900
|
+
const cur = this.tierModels.get(tier);
|
|
250901
|
+
if (!cur) continue;
|
|
250902
|
+
const fresh = this.selector.getModelById(cur.id);
|
|
250903
|
+
if (fresh) this.tierModels.set(tier, fresh);
|
|
250904
|
+
}
|
|
250905
|
+
}
|
|
250906
|
+
async probeNativeToolCall(model) {
|
|
250907
|
+
try {
|
|
250908
|
+
const cfg = this.config.providers.find((p3) => p3.type === model.provider) ?? { type: model.provider };
|
|
250909
|
+
const provider = this.createProvider(cfg, model);
|
|
250910
|
+
const result = await withTimeout(provider.generate({
|
|
250911
|
+
messages: [{ role: "user", content: "Call the echo tool with text set to 'ping'. Use the tool; do not answer in prose." }],
|
|
250912
|
+
tools: [{
|
|
250913
|
+
name: "echo",
|
|
250914
|
+
description: "Echo the given text back to the caller.",
|
|
250915
|
+
inputSchema: { type: "object", properties: { text: { type: "string", description: "Text to echo" } }, required: ["text"] }
|
|
250916
|
+
}],
|
|
250917
|
+
maxTokens: 80,
|
|
250918
|
+
temperature: 0
|
|
250919
|
+
}), 3e4, "tool-support probe timed out");
|
|
250920
|
+
return (result.toolCalls?.length ?? 0) > 0;
|
|
250921
|
+
} catch {
|
|
250922
|
+
return null;
|
|
250923
|
+
}
|
|
250924
|
+
}
|
|
250777
250925
|
/**
|
|
250778
250926
|
* Cascade Auto live data: discover/validate real model ids from each cloud
|
|
250779
250927
|
* provider, then fetch current public quality scores + per-token prices and
|
|
@@ -250823,12 +250971,17 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
|
|
|
250823
250971
|
await Promise.allSettled(tasks);
|
|
250824
250972
|
}
|
|
250825
250973
|
/**
|
|
250826
|
-
* Replace available models with live-priced
|
|
250827
|
-
*
|
|
250974
|
+
* Replace available models with live-priced AND capability-corrected copies
|
|
250975
|
+
* (real context windows, native tool support, vision from modalities), then
|
|
250976
|
+
* refresh the already resolved tier models so shared-tier cost accounting and
|
|
250977
|
+
* the tool-use gate both see current data.
|
|
250828
250978
|
*/
|
|
250829
250979
|
applyLivePricing() {
|
|
250830
|
-
if (!this.liveData
|
|
250831
|
-
|
|
250980
|
+
if (!this.liveData) return;
|
|
250981
|
+
if (!this.liveData.hasLivePricing() && !this.liveData.hasCapabilities()) return;
|
|
250982
|
+
let updated = this.selector.getAllAvailableModels();
|
|
250983
|
+
if (this.liveData.hasLivePricing()) updated = this.liveData.applyLivePricing(updated);
|
|
250984
|
+
if (this.liveData.hasCapabilities()) updated = this.liveData.applyLiveCapabilities(updated);
|
|
250832
250985
|
for (const m3 of updated) this.selector.addDynamicModel(m3);
|
|
250833
250986
|
for (const tier of ["T1", "T2", "T3"]) {
|
|
250834
250987
|
const cur = this.tierModels.get(tier);
|
|
@@ -251003,6 +251156,10 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
|
|
|
251003
251156
|
const r4 = this.config?.reinforcements;
|
|
251004
251157
|
return { enabled: r4?.enabled === true, maxPerSection: r4?.maxPerSection ?? 4 };
|
|
251005
251158
|
}
|
|
251159
|
+
/** Project-knowledge settings (config.knowledge). Facts extraction on by default. */
|
|
251160
|
+
getKnowledgeConfig() {
|
|
251161
|
+
return { factsExtraction: this.config?.knowledge?.factsExtraction !== false };
|
|
251162
|
+
}
|
|
251006
251163
|
/**
|
|
251007
251164
|
* Resolved T3 wave execution mode. 'auto' becomes 'sequential' when the T3
|
|
251008
251165
|
* tier resolves to a LOCAL model (the single-GPU queue serializes anyway, so
|
|
@@ -251089,10 +251246,10 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
|
|
|
251089
251246
|
* null when Cascade Auto is off (callers then use the shared tier model).
|
|
251090
251247
|
* Pure heuristic — no extra LLM call.
|
|
251091
251248
|
*/
|
|
251092
|
-
async selectModelForSubtask(tier, text) {
|
|
251249
|
+
async selectModelForSubtask(tier, text, opts) {
|
|
251093
251250
|
if (!this.config?.cascadeAuto || !this.taskAnalyzer || !text.trim()) return null;
|
|
251094
251251
|
try {
|
|
251095
|
-
return await this.taskAnalyzer.selectModel(text, tier, this.selector);
|
|
251252
|
+
return await this.taskAnalyzer.selectModel(text, tier, this.selector, opts);
|
|
251096
251253
|
} catch {
|
|
251097
251254
|
return null;
|
|
251098
251255
|
}
|
|
@@ -251847,6 +252004,13 @@ EXAMPLE \u2014 calling the "shell" tool to list files:
|
|
|
251847
252004
|
{"name": "shell", "input": {"command": "ls -la"}}
|
|
251848
252005
|
</tool_call>
|
|
251849
252006
|
|
|
252007
|
+
When you have enough information, stop calling tools and write your final answer.`;
|
|
252008
|
+
}
|
|
252009
|
+
function buildTextToolReminder(tools) {
|
|
252010
|
+
return `
|
|
252011
|
+
TOOL USE REMINDER:
|
|
252012
|
+
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.
|
|
252013
|
+
Available tools: ${tools.map((t4) => t4.name).join(", ")}.
|
|
251850
252014
|
When you have enough information, stop calling tools and write your final answer.`;
|
|
251851
252015
|
}
|
|
251852
252016
|
|
|
@@ -252086,6 +252250,9 @@ Now execute your subtask using this context where relevant.`
|
|
|
252086
252250
|
} catch (e3) {
|
|
252087
252251
|
this.log("Failed to write to World State DB");
|
|
252088
252252
|
}
|
|
252253
|
+
if (this.router.getKnowledgeConfig?.().factsExtraction !== false) {
|
|
252254
|
+
await this.extractAndStoreFacts(db, assignment, output);
|
|
252255
|
+
}
|
|
252089
252256
|
}
|
|
252090
252257
|
this.setStatus("COMPLETED", output);
|
|
252091
252258
|
this.sendStatusUpdate({ progressPct: 100, currentAction: "Subtask complete", status: "IN_PROGRESS", output });
|
|
@@ -252147,7 +252314,7 @@ Now execute your subtask using this context where relevant.`
|
|
|
252147
252314
|
let subtaskModel;
|
|
252148
252315
|
try {
|
|
252149
252316
|
const subtaskText = `${this.assignment?.subtaskTitle ?? ""} ${this.assignment?.description ?? ""} ${this.assignment?.expectedOutput ?? ""}`;
|
|
252150
|
-
subtaskModel = await this.router.selectModelForSubtask("T3", subtaskText) ?? void 0;
|
|
252317
|
+
subtaskModel = await this.router.selectModelForSubtask("T3", subtaskText, { requiresToolUse: tools.length > 0 }) ?? void 0;
|
|
252151
252318
|
if (subtaskModel) {
|
|
252152
252319
|
this.log(`Cascade Auto: routing this subtask to ${subtaskModel.provider}:${subtaskModel.id}`);
|
|
252153
252320
|
}
|
|
@@ -252155,7 +252322,8 @@ Now execute your subtask using this context where relevant.`
|
|
|
252155
252322
|
}
|
|
252156
252323
|
const effectiveModel = subtaskModel ?? this.router.getModelForTier("T3");
|
|
252157
252324
|
const useTextTools = effectiveModel?.supportsToolUse === false && tools.length > 0;
|
|
252158
|
-
|
|
252325
|
+
let sentFullTextContract = false;
|
|
252326
|
+
let textContractSignature = "";
|
|
252159
252327
|
while (iterations < MAX_ITERATIONS) {
|
|
252160
252328
|
iterations++;
|
|
252161
252329
|
this.throwIfCancelled();
|
|
@@ -252169,6 +252337,17 @@ Now execute your subtask using this context where relevant.`
|
|
|
252169
252337
|
${g2.text}`
|
|
252170
252338
|
});
|
|
252171
252339
|
}
|
|
252340
|
+
let textToolSuffix = "";
|
|
252341
|
+
if (useTextTools) {
|
|
252342
|
+
const signature = tools.map((t4) => t4.name).join(",");
|
|
252343
|
+
if (!sentFullTextContract || signature !== textContractSignature) {
|
|
252344
|
+
textToolSuffix = buildTextToolSystemPrompt(tools);
|
|
252345
|
+
sentFullTextContract = true;
|
|
252346
|
+
textContractSignature = signature;
|
|
252347
|
+
} else {
|
|
252348
|
+
textToolSuffix = buildTextToolReminder(tools);
|
|
252349
|
+
}
|
|
252350
|
+
}
|
|
252172
252351
|
const options = {
|
|
252173
252352
|
messages: this.context.getMessages(),
|
|
252174
252353
|
systemPrompt: this.systemPromptOverride + systemPrompt + (this.hierarchyContext ? `
|
|
@@ -252658,6 +252837,41 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
|
252658
252837
|
};
|
|
252659
252838
|
}
|
|
252660
252839
|
}
|
|
252840
|
+
/**
|
|
252841
|
+
* world-state v2: distill a completed subtask's output into durable
|
|
252842
|
+
* `(entity, relation, value)` facts and upsert them into the knowledge graph.
|
|
252843
|
+
* A bounded, cheap T3-tier call; entirely best-effort — any failure is swallowed
|
|
252844
|
+
* so it never blocks or fails the subtask. Respects the subtask's privacy tier
|
|
252845
|
+
* (a local-only subtask extracts on a local model too, never leaking to cloud).
|
|
252846
|
+
*/
|
|
252847
|
+
async extractAndStoreFacts(db, assignment, output) {
|
|
252848
|
+
try {
|
|
252849
|
+
const prompt = `Extract durable project facts from this completed subtask.
|
|
252850
|
+
Return ONLY a JSON array of {"entity","relation","value"} triples describing lasting
|
|
252851
|
+
facts about the codebase/project \u2014 e.g. {"entity":"auth module","relation":"uses","value":"JWT"}.
|
|
252852
|
+
Ignore transient step-by-step details. At most 6 triples. If nothing durable, return [].
|
|
252853
|
+
|
|
252854
|
+
Subtask: ${assignment.subtaskTitle}
|
|
252855
|
+
Output:
|
|
252856
|
+
${output.slice(0, 4e3)}`;
|
|
252857
|
+
const result = await this.router.generate("T3", {
|
|
252858
|
+
messages: [{ role: "user", content: prompt }],
|
|
252859
|
+
maxTokens: 300,
|
|
252860
|
+
temperature: 0,
|
|
252861
|
+
...this.localOnlyMatch ? { forceLocal: true } : {}
|
|
252862
|
+
});
|
|
252863
|
+
const match = /\[[\s\S]*\]/.exec(result.content);
|
|
252864
|
+
if (!match) return;
|
|
252865
|
+
const facts = JSON.parse(match[0]);
|
|
252866
|
+
if (!Array.isArray(facts)) return;
|
|
252867
|
+
for (const f4 of facts.slice(0, 8)) {
|
|
252868
|
+
if (f4 && typeof f4.entity === "string" && typeof f4.relation === "string" && typeof f4.value === "string") {
|
|
252869
|
+
db.upsertFact(f4.entity, f4.relation, f4.value, this.id);
|
|
252870
|
+
}
|
|
252871
|
+
}
|
|
252872
|
+
} catch {
|
|
252873
|
+
}
|
|
252874
|
+
}
|
|
252661
252875
|
async correctOutput(originalOutput, failures) {
|
|
252662
252876
|
const correctionPrompt = `The following output failed these checks: ${failures.join(", ")}.
|
|
252663
252877
|
|
|
@@ -254060,10 +254274,24 @@ In 3-5 terse bullets, flag the most important RISKS, GAPS, or over-/under-decomp
|
|
|
254060
254274
|
}
|
|
254061
254275
|
async decomposeTask(prompt, systemContext) {
|
|
254062
254276
|
const db = this.router.getWorldStateDB?.();
|
|
254063
|
-
|
|
254277
|
+
let worldStateContext = "";
|
|
254278
|
+
if (db) {
|
|
254279
|
+
const knowledge = db.getFormattedKnowledge(prompt);
|
|
254280
|
+
if (knowledge) {
|
|
254281
|
+
worldStateContext = `
|
|
254282
|
+
|
|
254283
|
+
PROJECT KNOWLEDGE (relevant facts about this codebase):
|
|
254284
|
+
${knowledge}`;
|
|
254285
|
+
} else {
|
|
254286
|
+
const log = db.getFormattedState();
|
|
254287
|
+
if (log && log !== "World State is currently empty.") {
|
|
254288
|
+
worldStateContext = `
|
|
254064
254289
|
|
|
254065
254290
|
PROJECT WORLD STATE (Current architecture and recent changes):
|
|
254066
|
-
${
|
|
254291
|
+
${log}`;
|
|
254292
|
+
}
|
|
254293
|
+
}
|
|
254294
|
+
}
|
|
254067
254295
|
const contextSection = systemContext ? `
|
|
254068
254296
|
Project context:
|
|
254069
254297
|
${systemContext}` : "";
|
|
@@ -294499,7 +294727,15 @@ var ToolsConfigSchema = external_exports.object({
|
|
|
294499
294727
|
mcpServers: external_exports.array(McpServerConfigSchema).optional(),
|
|
294500
294728
|
mcpTrusted: external_exports.array(external_exports.string()).optional(),
|
|
294501
294729
|
/** Web search backends — at least one should be configured for best results */
|
|
294502
|
-
webSearch: WebSearchConfigSchema.optional()
|
|
294730
|
+
webSearch: WebSearchConfigSchema.optional(),
|
|
294731
|
+
/**
|
|
294732
|
+
* Sandbox runtime for LLM-authored dynamic tools:
|
|
294733
|
+
* - 'isolate': hard V8 isolate (isolated-vm) — no Node globals, true capability
|
|
294734
|
+
* confinement. Requires the optional native `isolated-vm` dependency.
|
|
294735
|
+
* - 'worker': node:worker_threads (resource/kill limits, but not capability-confined).
|
|
294736
|
+
* - 'auto' (default): use the isolate when `isolated-vm` loads, else fall back to worker.
|
|
294737
|
+
*/
|
|
294738
|
+
dynamicToolSandbox: external_exports.enum(["isolate", "worker", "auto"]).default("auto")
|
|
294503
294739
|
});
|
|
294504
294740
|
var HookDefinitionSchema = external_exports.object({
|
|
294505
294741
|
name: external_exports.string().optional(),
|
|
@@ -294611,10 +294847,20 @@ var CascadeConfigSchema = external_exports.object({
|
|
|
294611
294847
|
/**
|
|
294612
294848
|
* Runtime Tool Creation: when true, T3 workers can generate and register new tools
|
|
294613
294849
|
* at runtime via the ToolCreator when no existing tool can handle a required operation.
|
|
294614
|
-
* Generated tools are session-scoped and sandboxed
|
|
294850
|
+
* Generated tools are session-scoped and sandboxed (see tools.dynamicToolSandbox).
|
|
294615
294851
|
* HTTP calls from generated tools require approval.
|
|
294616
294852
|
*/
|
|
294617
294853
|
enableToolCreation: external_exports.boolean().default(true),
|
|
294854
|
+
/**
|
|
294855
|
+
* Project knowledge (world state). `factsExtraction`: after each worker
|
|
294856
|
+
* completes, run a cheap extraction pass that distills its output into
|
|
294857
|
+
* queryable entity/relation/value facts (superseding older facts), which T1
|
|
294858
|
+
* queries during planning instead of replaying the whole linear log. Best-effort
|
|
294859
|
+
* and non-blocking; set false to keep only the raw linear log.
|
|
294860
|
+
*/
|
|
294861
|
+
knowledge: external_exports.object({
|
|
294862
|
+
factsExtraction: external_exports.boolean().default(true)
|
|
294863
|
+
}).default({}),
|
|
294618
294864
|
/**
|
|
294619
294865
|
* Persist runtime-generated tools to .cascade/dynamic-tools.json and reload them
|
|
294620
294866
|
* on startup for cross-run dedup. Reloaded (and peer-received) tools are always
|
|
@@ -294907,13 +295153,17 @@ var TaskAnalyzer = class {
|
|
|
294907
295153
|
* Scores tier-eligible models using cost efficiency + historical performance.
|
|
294908
295154
|
* Falls back to the priority-list default when no candidates have history.
|
|
294909
295155
|
*/
|
|
294910
|
-
async selectModel(prompt, tier, selector) {
|
|
295156
|
+
async selectModel(prompt, tier, selector, opts) {
|
|
294911
295157
|
const profile = await this.analyze(prompt);
|
|
294912
295158
|
if (profile.requiresVision) {
|
|
294913
295159
|
return selector.selectVisionModel();
|
|
294914
295160
|
}
|
|
294915
|
-
|
|
295161
|
+
let candidates = selector.getCandidatesForTier(tier);
|
|
294916
295162
|
if (candidates.length === 0) return selector.selectForTier(tier);
|
|
295163
|
+
if (opts?.requiresToolUse) {
|
|
295164
|
+
const toolCapable = candidates.filter((m3) => m3.supportsToolUse !== false);
|
|
295165
|
+
if (toolCapable.length > 0) candidates = toolCapable;
|
|
295166
|
+
}
|
|
294917
295167
|
const scored = candidates.map((m3) => ({
|
|
294918
295168
|
model: m3,
|
|
294919
295169
|
score: this.scoreModel(m3, profile)
|
|
@@ -295092,6 +295342,28 @@ var ModelPerformanceTracker = class {
|
|
|
295092
295342
|
return Math.max(0.1, 1 - normalised * complexityWeight);
|
|
295093
295343
|
}
|
|
295094
295344
|
};
|
|
295345
|
+
var ivmCache;
|
|
295346
|
+
var ivmWarned = false;
|
|
295347
|
+
async function loadIsolatedVm() {
|
|
295348
|
+
if (ivmCache !== void 0) return ivmCache;
|
|
295349
|
+
try {
|
|
295350
|
+
const specifier = "isolated-vm";
|
|
295351
|
+
const mod = await import(specifier);
|
|
295352
|
+
ivmCache = mod.default ?? mod;
|
|
295353
|
+
} catch {
|
|
295354
|
+
ivmCache = null;
|
|
295355
|
+
}
|
|
295356
|
+
return ivmCache;
|
|
295357
|
+
}
|
|
295358
|
+
function safeJsonParse(text) {
|
|
295359
|
+
if (typeof text !== "string") return {};
|
|
295360
|
+
try {
|
|
295361
|
+
const v6 = JSON.parse(text);
|
|
295362
|
+
return v6 && typeof v6 === "object" ? v6 : {};
|
|
295363
|
+
} catch {
|
|
295364
|
+
return {};
|
|
295365
|
+
}
|
|
295366
|
+
}
|
|
295095
295367
|
var DYNAMIC_TOOLS_FILE = "dynamic-tools.json";
|
|
295096
295368
|
function normalizeToolSchema(schema) {
|
|
295097
295369
|
if (schema && schema["type"] === "object" && typeof schema["properties"] === "object") {
|
|
@@ -295194,7 +295466,12 @@ var DynamicTool = class extends BaseTool {
|
|
|
295194
295466
|
getEscalator;
|
|
295195
295467
|
/** Untrusted = loaded from disk / a peer; its dangerous calls always re-prompt. */
|
|
295196
295468
|
trusted;
|
|
295197
|
-
|
|
295469
|
+
/** Resolve the configured sandbox mode at call time (default 'auto'). */
|
|
295470
|
+
getSandboxMode;
|
|
295471
|
+
/** Optional diagnostics sink (routed through the host so it survives the Ink TUI). */
|
|
295472
|
+
log;
|
|
295473
|
+
constructor(spec, registry2, getEscalator, trusted, getSandboxMode = () => "auto", log = () => {
|
|
295474
|
+
}) {
|
|
295198
295475
|
super();
|
|
295199
295476
|
this.name = spec.name;
|
|
295200
295477
|
this.description = spec.description;
|
|
@@ -295204,6 +295481,8 @@ var DynamicTool = class extends BaseTool {
|
|
|
295204
295481
|
this.registry = registry2;
|
|
295205
295482
|
this.getEscalator = getEscalator;
|
|
295206
295483
|
this.trusted = trusted;
|
|
295484
|
+
this.getSandboxMode = getSandboxMode;
|
|
295485
|
+
this.log = log;
|
|
295207
295486
|
}
|
|
295208
295487
|
isDangerous() {
|
|
295209
295488
|
return this._isDangerous;
|
|
@@ -295240,8 +295519,94 @@ var DynamicTool = class extends BaseTool {
|
|
|
295240
295519
|
return `Error calling ${toolName}: ${err instanceof Error ? err.message : String(err)}`;
|
|
295241
295520
|
}
|
|
295242
295521
|
};
|
|
295522
|
+
const mode = this.getSandboxMode();
|
|
295523
|
+
if (mode !== "worker") {
|
|
295524
|
+
const ivm = await loadIsolatedVm();
|
|
295525
|
+
if (ivm) return this.runInIsolate(ivm, input, callTool);
|
|
295526
|
+
if (mode === "isolate" && !ivmWarned) {
|
|
295527
|
+
ivmWarned = true;
|
|
295528
|
+
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.");
|
|
295529
|
+
}
|
|
295530
|
+
}
|
|
295243
295531
|
return this.runInWorker(input, callTool);
|
|
295244
295532
|
}
|
|
295533
|
+
/**
|
|
295534
|
+
* Run the generated code in a hard V8 isolate (isolated-vm). The isolate global
|
|
295535
|
+
* has no Node built-ins, so the code cannot see `process`, `require`, the
|
|
295536
|
+
* filesystem, or the network — it reaches the host ONLY through the injected
|
|
295537
|
+
* `callTool` (escalator-gated on the main thread) and `fetch` (SSRF-guarded via
|
|
295538
|
+
* bridgeFetch). `script.run({ timeout })` bounds synchronous CPU; an outer
|
|
295539
|
+
* wall-clock race + `isolate.dispose()` bounds async runaway (a never-resolving
|
|
295540
|
+
* await), mirroring the worker's terminate().
|
|
295541
|
+
*/
|
|
295542
|
+
async runInIsolate(ivm, input, callTool) {
|
|
295543
|
+
const timeoutMs = Math.max(200, Number(process.env["CASCADE_DYNAMIC_TOOL_TIMEOUT_MS"]) || DYNAMIC_TOOL_TIMEOUT_MS);
|
|
295544
|
+
const isolate = new ivm.Isolate({ memoryLimit: 128 });
|
|
295545
|
+
let disposed = false;
|
|
295546
|
+
const dispose = () => {
|
|
295547
|
+
if (!disposed) {
|
|
295548
|
+
disposed = true;
|
|
295549
|
+
try {
|
|
295550
|
+
isolate.dispose();
|
|
295551
|
+
} catch {
|
|
295552
|
+
}
|
|
295553
|
+
}
|
|
295554
|
+
};
|
|
295555
|
+
try {
|
|
295556
|
+
const context = await isolate.createContext();
|
|
295557
|
+
const jail = context.global;
|
|
295558
|
+
await jail.set("_callTool", new ivm.Reference(async (name, inputJson) => {
|
|
295559
|
+
const out = await callTool(String(name), safeJsonParse(inputJson));
|
|
295560
|
+
return String(out);
|
|
295561
|
+
}));
|
|
295562
|
+
await jail.set("_fetch", new ivm.Reference(async (url, initJson) => {
|
|
295563
|
+
const r4 = await bridgeFetch(String(url), safeJsonParse(initJson));
|
|
295564
|
+
return JSON.stringify(r4);
|
|
295565
|
+
}));
|
|
295566
|
+
await jail.set("_input", new ivm.ExternalCopy(input).copyInto());
|
|
295567
|
+
await jail.set("_code", this.executeCode);
|
|
295568
|
+
const bootstrap = `
|
|
295569
|
+
const callTool = (name, toolInput) => _callTool.apply(undefined,
|
|
295570
|
+
[String(name), JSON.stringify(toolInput || {})],
|
|
295571
|
+
{ result: { promise: true }, arguments: { copy: true } });
|
|
295572
|
+
const fetch = async (url, init) => {
|
|
295573
|
+
const raw = await _fetch.apply(undefined,
|
|
295574
|
+
[String(url), JSON.stringify(init || null)],
|
|
295575
|
+
{ result: { promise: true }, arguments: { copy: true } });
|
|
295576
|
+
const r = JSON.parse(raw);
|
|
295577
|
+
if (r && r.__error) throw new Error(r.__error);
|
|
295578
|
+
return {
|
|
295579
|
+
ok: r.ok, status: r.status, statusText: r.statusText,
|
|
295580
|
+
headers: { get: (k) => (String(k).toLowerCase() === 'content-type' ? r.contentType : null) },
|
|
295581
|
+
text: async () => r.body,
|
|
295582
|
+
json: async () => JSON.parse(r.body),
|
|
295583
|
+
};
|
|
295584
|
+
};
|
|
295585
|
+
const console = { log() {}, error() {} };
|
|
295586
|
+
const AsyncFunction = Object.getPrototypeOf(async function () {}).constructor;
|
|
295587
|
+
const fn = new AsyncFunction('input', 'callTool', 'fetch', 'console', _code);
|
|
295588
|
+
(async () => { const r = await fn(_input, callTool, fetch, console); return String(r == null ? '' : r); })();
|
|
295589
|
+
`;
|
|
295590
|
+
const script = await isolate.compileScript(bootstrap);
|
|
295591
|
+
const runPromise = script.run(context, { timeout: timeoutMs, promise: true }).then((v6) => String(v6 ?? ""));
|
|
295592
|
+
const timeoutPromise = new Promise((resolve) => {
|
|
295593
|
+
const t4 = setTimeout(() => {
|
|
295594
|
+
dispose();
|
|
295595
|
+
resolve(`Dynamic tool "${this.name}" timed out after ${timeoutMs}ms and was terminated.`);
|
|
295596
|
+
}, timeoutMs + 500);
|
|
295597
|
+
t4.unref?.();
|
|
295598
|
+
});
|
|
295599
|
+
return await Promise.race([runPromise, timeoutPromise]);
|
|
295600
|
+
} catch (err) {
|
|
295601
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
295602
|
+
if (/timed? out/i.test(msg)) {
|
|
295603
|
+
return `Dynamic tool "${this.name}" timed out after ${timeoutMs}ms and was terminated.`;
|
|
295604
|
+
}
|
|
295605
|
+
return `Tool error: ${msg}`;
|
|
295606
|
+
} finally {
|
|
295607
|
+
dispose();
|
|
295608
|
+
}
|
|
295609
|
+
}
|
|
295245
295610
|
/** Spawn the worker, service its callTool/fetch bridge, enforce the kill timeout. */
|
|
295246
295611
|
runInWorker(input, callTool) {
|
|
295247
295612
|
const timeoutMs = Math.max(200, Number(process.env["CASCADE_DYNAMIC_TOOL_TIMEOUT_MS"]) || DYNAMIC_TOOL_TIMEOUT_MS);
|
|
@@ -295321,16 +295686,19 @@ var ToolCreator = class {
|
|
|
295321
295686
|
workspacePath;
|
|
295322
295687
|
/** When false, persisted tools are neither loaded nor written. */
|
|
295323
295688
|
persistEnabled;
|
|
295689
|
+
/** Sandbox runtime for generated tools; passed to each DynamicTool. */
|
|
295690
|
+
sandboxMode;
|
|
295324
295691
|
logger;
|
|
295325
295692
|
/** name → spec, for persistence, broadcast, and re-registration. */
|
|
295326
295693
|
specs = /* @__PURE__ */ new Map();
|
|
295327
295694
|
/** capability fingerprint → tool name, so the same need isn't re-generated. */
|
|
295328
295695
|
capabilityIndex = /* @__PURE__ */ new Map();
|
|
295329
|
-
constructor(router, registry2, workspacePath, persistEnabled = true) {
|
|
295696
|
+
constructor(router, registry2, workspacePath, persistEnabled = true, sandboxMode = "auto") {
|
|
295330
295697
|
this.router = router;
|
|
295331
295698
|
this.registry = registry2;
|
|
295332
295699
|
this.workspacePath = workspacePath;
|
|
295333
295700
|
this.persistEnabled = persistEnabled;
|
|
295701
|
+
this.sandboxMode = sandboxMode;
|
|
295334
295702
|
}
|
|
295335
295703
|
setPermissionEscalator(escalator) {
|
|
295336
295704
|
this.escalator = escalator;
|
|
@@ -295418,7 +295786,14 @@ Required capability: ${description.slice(0, 300)}`;
|
|
|
295418
295786
|
this.specs.set(spec.name, spec);
|
|
295419
295787
|
return;
|
|
295420
295788
|
}
|
|
295421
|
-
const tool = new DynamicTool(
|
|
295789
|
+
const tool = new DynamicTool(
|
|
295790
|
+
spec,
|
|
295791
|
+
this.registry,
|
|
295792
|
+
() => this.escalator,
|
|
295793
|
+
trusted,
|
|
295794
|
+
() => this.sandboxMode,
|
|
295795
|
+
(msg) => this.log(msg)
|
|
295796
|
+
);
|
|
295422
295797
|
this.registry.register(tool);
|
|
295423
295798
|
this.specs.set(spec.name, spec);
|
|
295424
295799
|
this.capabilityIndex.set(capabilityKey(`${spec.description}`), spec.name);
|
|
@@ -295467,6 +295842,9 @@ Required capability: ${description.slice(0, 300)}`;
|
|
|
295467
295842
|
return Array.from(this.specs.keys());
|
|
295468
295843
|
}
|
|
295469
295844
|
};
|
|
295845
|
+
function normalizeKey(text) {
|
|
295846
|
+
return text.trim().toLowerCase().replace(/\s+/g, " ");
|
|
295847
|
+
}
|
|
295470
295848
|
var WorldStateDB = class {
|
|
295471
295849
|
constructor(workspacePath, debugMode = false) {
|
|
295472
295850
|
this.workspacePath = workspacePath;
|
|
@@ -295488,6 +295866,16 @@ var WorldStateDB = class {
|
|
|
295488
295866
|
encrypted_payload TEXT NOT NULL
|
|
295489
295867
|
)
|
|
295490
295868
|
`);
|
|
295869
|
+
this.db.exec(`
|
|
295870
|
+
CREATE TABLE IF NOT EXISTS facts (
|
|
295871
|
+
entity TEXT NOT NULL,
|
|
295872
|
+
relation TEXT NOT NULL,
|
|
295873
|
+
encrypted_value TEXT NOT NULL,
|
|
295874
|
+
source_worker TEXT NOT NULL,
|
|
295875
|
+
timestamp TEXT NOT NULL,
|
|
295876
|
+
PRIMARY KEY (entity, relation)
|
|
295877
|
+
)
|
|
295878
|
+
`);
|
|
295491
295879
|
}
|
|
295492
295880
|
workspacePath;
|
|
295493
295881
|
debugMode;
|
|
@@ -295561,6 +295949,131 @@ var WorldStateDB = class {
|
|
|
295561
295949
|
if (entries.length === 0) return "World State is currently empty.";
|
|
295562
295950
|
return entries.map((e3, idx) => `[${e3.timestamp}] Step ${idx + 1} (${e3.workerId}): ${e3.summary}`).join("\n");
|
|
295563
295951
|
}
|
|
295952
|
+
// ── world-state v2: queryable facts ──────────────
|
|
295953
|
+
/**
|
|
295954
|
+
* Upsert a fact. `(entity, relation)` is normalized so casing/whitespace don't
|
|
295955
|
+
* fragment the key; an existing pair is superseded (value + provenance updated)
|
|
295956
|
+
* rather than duplicated. Empty entity/relation/value are ignored.
|
|
295957
|
+
*/
|
|
295958
|
+
upsertFact(entity, relation, value, sourceWorker, timestamp) {
|
|
295959
|
+
const e3 = normalizeKey(entity);
|
|
295960
|
+
const r4 = normalizeKey(relation);
|
|
295961
|
+
const v6 = value.trim();
|
|
295962
|
+
if (!e3 || !r4 || !v6) return;
|
|
295963
|
+
const encrypted = this.encrypt(JSON.stringify({ value: v6 }));
|
|
295964
|
+
const stmt = this.db.prepare(`
|
|
295965
|
+
INSERT INTO facts (entity, relation, encrypted_value, source_worker, timestamp)
|
|
295966
|
+
VALUES (?, ?, ?, ?, ?)
|
|
295967
|
+
ON CONFLICT(entity, relation) DO UPDATE SET
|
|
295968
|
+
encrypted_value = excluded.encrypted_value,
|
|
295969
|
+
source_worker = excluded.source_worker,
|
|
295970
|
+
timestamp = excluded.timestamp
|
|
295971
|
+
`);
|
|
295972
|
+
stmt.run(e3, r4, encrypted, sourceWorker, timestamp ?? (/* @__PURE__ */ new Date()).toISOString());
|
|
295973
|
+
this.dumpDebugIfNeeded();
|
|
295974
|
+
}
|
|
295975
|
+
rowToFact(row) {
|
|
295976
|
+
let value;
|
|
295977
|
+
try {
|
|
295978
|
+
value = JSON.parse(this.decrypt(row.encrypted_value)).value;
|
|
295979
|
+
} catch {
|
|
295980
|
+
value = "[Decryption Failed - Payload Corrupted]";
|
|
295981
|
+
}
|
|
295982
|
+
return { entity: row.entity, relation: row.relation, value, sourceWorker: row.source_worker, timestamp: row.timestamp };
|
|
295983
|
+
}
|
|
295984
|
+
/** All facts whose entity matches one of the (normalized) query entities. */
|
|
295985
|
+
getFactsForEntities(entities) {
|
|
295986
|
+
const keys = Array.from(new Set(entities.map(normalizeKey).filter(Boolean)));
|
|
295987
|
+
if (keys.length === 0) return [];
|
|
295988
|
+
const placeholders = keys.map(() => "?").join(", ");
|
|
295989
|
+
const stmt = this.db.prepare(
|
|
295990
|
+
`SELECT entity, relation, encrypted_value, source_worker, timestamp FROM facts WHERE entity IN (${placeholders}) ORDER BY entity ASC, relation ASC`
|
|
295991
|
+
);
|
|
295992
|
+
return stmt.all(...keys).map((row) => this.rowToFact(row));
|
|
295993
|
+
}
|
|
295994
|
+
/** Every fact (used for tests / debug / whole-graph views). */
|
|
295995
|
+
getAllFacts() {
|
|
295996
|
+
const stmt = this.db.prepare("SELECT entity, relation, encrypted_value, source_worker, timestamp FROM facts ORDER BY entity ASC, relation ASC");
|
|
295997
|
+
return stmt.all().map((row) => this.rowToFact(row));
|
|
295998
|
+
}
|
|
295999
|
+
/**
|
|
296000
|
+
* A compact, human/LLM-readable fact block for T1/T2 planning. When `entities`
|
|
296001
|
+
* is given, only facts about those entities are included; otherwise all facts.
|
|
296002
|
+
* Returns '' when there are none so callers can fall back to the linear log.
|
|
296003
|
+
*/
|
|
296004
|
+
getFormattedFacts(entities) {
|
|
296005
|
+
const facts = entities && entities.length > 0 ? this.getFactsForEntities(entities) : this.getAllFacts();
|
|
296006
|
+
if (facts.length === 0) return "";
|
|
296007
|
+
return facts.map((f4) => `- ${f4.entity} ${f4.relation} ${f4.value}`).join("\n");
|
|
296008
|
+
}
|
|
296009
|
+
/**
|
|
296010
|
+
* A compact knowledge block for T1/T2 planning. When `prompt` is given, facts
|
|
296011
|
+
* whose entity/relation/value mention a prompt token are preferred (relevance
|
|
296012
|
+
* filter); otherwise, or when nothing matches, all facts are used (capped at
|
|
296013
|
+
* `limit`). Returns '' when there are no facts, so the caller can fall back to
|
|
296014
|
+
* the raw linear log — this replaces replaying the whole log during planning.
|
|
296015
|
+
*/
|
|
296016
|
+
getFormattedKnowledge(prompt, limit2 = 40) {
|
|
296017
|
+
const all = this.getAllFacts();
|
|
296018
|
+
if (all.length === 0) return "";
|
|
296019
|
+
let selected = all;
|
|
296020
|
+
if (prompt && prompt.trim()) {
|
|
296021
|
+
const tokens = Array.from(new Set(prompt.toLowerCase().match(/[a-z0-9_./-]{3,}/g) ?? []));
|
|
296022
|
+
if (tokens.length > 0) {
|
|
296023
|
+
const matched = all.filter((f4) => {
|
|
296024
|
+
const hay = `${f4.entity} ${f4.relation} ${f4.value}`.toLowerCase();
|
|
296025
|
+
return tokens.some((t4) => hay.includes(t4));
|
|
296026
|
+
});
|
|
296027
|
+
if (matched.length > 0) selected = matched;
|
|
296028
|
+
}
|
|
296029
|
+
}
|
|
296030
|
+
return selected.slice(0, limit2).map((f4) => `- ${f4.entity} ${f4.relation} ${f4.value}`).join("\n");
|
|
296031
|
+
}
|
|
296032
|
+
// ── Export / Import ──────────────────────────
|
|
296033
|
+
//
|
|
296034
|
+
// Knowledge travels DECRYPTED in the export bundle (a portable plaintext
|
|
296035
|
+
// JSON file — the encryption key never leaves this machine) and re-encrypts
|
|
296036
|
+
// with the LOCAL key on import.
|
|
296037
|
+
exportKnowledge() {
|
|
296038
|
+
return { facts: this.getAllFacts(), worldLog: this.getAllEntries() };
|
|
296039
|
+
}
|
|
296040
|
+
/**
|
|
296041
|
+
* Merge imported knowledge. Facts upsert on (entity, relation) with
|
|
296042
|
+
* newer-timestamp-wins (a local fact newer than the imported one is kept);
|
|
296043
|
+
* log entries append with fresh ids, skipping exact duplicates
|
|
296044
|
+
* (worker + timestamp + summary). Returns counts of what actually landed.
|
|
296045
|
+
*/
|
|
296046
|
+
importKnowledge(data) {
|
|
296047
|
+
let facts = 0;
|
|
296048
|
+
let logEntries = 0;
|
|
296049
|
+
if (Array.isArray(data.facts)) {
|
|
296050
|
+
const local = new Map(this.getAllFacts().map((f4) => [`${f4.entity}\0${f4.relation}`, f4.timestamp]));
|
|
296051
|
+
for (const f4 of data.facts) {
|
|
296052
|
+
if (!f4 || typeof f4.entity !== "string" || typeof f4.relation !== "string" || typeof f4.value !== "string") continue;
|
|
296053
|
+
const key = `${normalizeKey(f4.entity)}\0${normalizeKey(f4.relation)}`;
|
|
296054
|
+
const localTs = local.get(key);
|
|
296055
|
+
const importTs = f4.timestamp ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
296056
|
+
if (localTs && localTs >= importTs) continue;
|
|
296057
|
+
this.upsertFact(f4.entity, f4.relation, f4.value, f4.sourceWorker ?? "imported", importTs);
|
|
296058
|
+
facts++;
|
|
296059
|
+
}
|
|
296060
|
+
}
|
|
296061
|
+
if (Array.isArray(data.worldLog)) {
|
|
296062
|
+
const seen = new Set(this.getAllEntries().map((e3) => `${e3.workerId}\0${e3.timestamp}\0${e3.summary}`));
|
|
296063
|
+
const stmt = this.db.prepare("INSERT INTO world_state (id, timestamp, worker_id, encrypted_payload) VALUES (?, ?, ?, ?)");
|
|
296064
|
+
for (const e3 of data.worldLog) {
|
|
296065
|
+
if (!e3 || typeof e3.summary !== "string") continue;
|
|
296066
|
+
const workerId = e3.workerId ?? "imported";
|
|
296067
|
+
const timestamp = e3.timestamp ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
296068
|
+
const key = `${workerId}\0${timestamp}\0${e3.summary}`;
|
|
296069
|
+
if (seen.has(key)) continue;
|
|
296070
|
+
stmt.run(crypto4__default.default.randomUUID(), timestamp, workerId, this.encrypt(JSON.stringify({ summary: e3.summary })));
|
|
296071
|
+
seen.add(key);
|
|
296072
|
+
logEntries++;
|
|
296073
|
+
}
|
|
296074
|
+
}
|
|
296075
|
+
return { facts, logEntries };
|
|
296076
|
+
}
|
|
295564
296077
|
dumpDebugIfNeeded() {
|
|
295565
296078
|
if (!this.debugMode) return;
|
|
295566
296079
|
try {
|
|
@@ -295700,7 +296213,8 @@ var Cascade = class _Cascade extends EventEmitter__default.default {
|
|
|
295700
296213
|
}
|
|
295701
296214
|
const cfg = this.config;
|
|
295702
296215
|
if (cfg["enableToolCreation"] === true) {
|
|
295703
|
-
|
|
296216
|
+
const sandboxMode = this.config.tools?.dynamicToolSandbox ?? "auto";
|
|
296217
|
+
this.toolCreator = new ToolCreator(this.router, this.toolRegistry, this.workspacePath, cfg["persistDynamicTools"] !== false, sandboxMode);
|
|
295704
296218
|
this.toolCreator.setLogger((m3) => {
|
|
295705
296219
|
if (this.listenerCount("log") > 0) this.emit("log", { level: "info", message: m3 });
|
|
295706
296220
|
});
|
|
@@ -295938,6 +296452,10 @@ ${last3.partialOutput}` : "");
|
|
|
295938
296452
|
this.router.profileModels(this.store).catch(() => {
|
|
295939
296453
|
});
|
|
295940
296454
|
}
|
|
296455
|
+
if (this.store) {
|
|
296456
|
+
this.router.probeLocalToolSupport(this.store).catch(() => {
|
|
296457
|
+
});
|
|
296458
|
+
}
|
|
295941
296459
|
if (this.config.cascadeAuto) {
|
|
295942
296460
|
this.router.refreshLiveData().catch(() => {
|
|
295943
296461
|
});
|
|
@@ -297012,6 +297530,78 @@ Original error: ${err.message}`
|
|
|
297012
297530
|
`).all(`%${query}%`, limit2);
|
|
297013
297531
|
return rows.map(this.deserializeMessage);
|
|
297014
297532
|
}
|
|
297533
|
+
// ── Export / Import ──────────────────────────
|
|
297534
|
+
//
|
|
297535
|
+
// Chats travel as full Session objects (messages included) inside a
|
|
297536
|
+
// plaintext-JSON bundle. Import NEVER overwrites: every imported session
|
|
297537
|
+
// gets a fresh id (title suffixed "(imported)") and fresh message ids, so
|
|
297538
|
+
// re-importing the same bundle can duplicate but can never clobber.
|
|
297539
|
+
/** Full sessions (with messages) for the given ids, or ALL sessions. */
|
|
297540
|
+
exportSessions(ids) {
|
|
297541
|
+
const targets = ids && ids.length > 0 ? ids : this.db.prepare("SELECT id FROM sessions ORDER BY updated_at DESC").all().map((r4) => r4.id);
|
|
297542
|
+
const out = [];
|
|
297543
|
+
for (const id of targets) {
|
|
297544
|
+
const s3 = this.getSession(id);
|
|
297545
|
+
if (s3) out.push(s3);
|
|
297546
|
+
}
|
|
297547
|
+
return out;
|
|
297548
|
+
}
|
|
297549
|
+
/**
|
|
297550
|
+
* Import sessions from a bundle. Returns the created `{id, title}` rows so
|
|
297551
|
+
* the caller can mirror them into the runtime session list (the sidebar
|
|
297552
|
+
* reads runtime_sessions, not sessions). Malformed entries are skipped.
|
|
297553
|
+
*/
|
|
297554
|
+
importSessions(sessions) {
|
|
297555
|
+
const out = [];
|
|
297556
|
+
const msgStmt = this.db.prepare(`
|
|
297557
|
+
INSERT INTO messages (id, session_id, role, content, timestamp, tokens, agent_messages)
|
|
297558
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
297559
|
+
`);
|
|
297560
|
+
for (const s3 of sessions) {
|
|
297561
|
+
if (!s3 || typeof s3 !== "object") continue;
|
|
297562
|
+
const newId = crypto4.randomUUID();
|
|
297563
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
297564
|
+
const title = `${String(s3.title || "Untitled").slice(0, 200)} (imported)`;
|
|
297565
|
+
const metadata = s3.metadata && typeof s3.metadata === "object" ? s3.metadata : { totalTokens: 0, totalCostUsd: 0, modelsUsed: [], toolsUsed: [], taskCount: 0 };
|
|
297566
|
+
this.db.prepare(`
|
|
297567
|
+
INSERT INTO sessions (id, title, created_at, updated_at, identity_id, workspace_path, metadata)
|
|
297568
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
297569
|
+
`).run(newId, title, s3.createdAt ?? now, s3.updatedAt ?? now, s3.identityId ?? "default", s3.workspacePath ?? "", JSON.stringify(metadata));
|
|
297570
|
+
for (const m3 of Array.isArray(s3.messages) ? s3.messages : []) {
|
|
297571
|
+
if (!m3 || m3.role !== "user" && m3.role !== "assistant" && m3.role !== "system") continue;
|
|
297572
|
+
msgStmt.run(
|
|
297573
|
+
crypto4.randomUUID(),
|
|
297574
|
+
newId,
|
|
297575
|
+
m3.role,
|
|
297576
|
+
typeof m3.content === "string" ? m3.content : JSON.stringify(m3.content),
|
|
297577
|
+
m3.timestamp ?? now,
|
|
297578
|
+
m3.tokens ? JSON.stringify(m3.tokens) : null,
|
|
297579
|
+
m3.agentMessages ? JSON.stringify(m3.agentMessages) : null
|
|
297580
|
+
);
|
|
297581
|
+
}
|
|
297582
|
+
out.push({ id: newId, title });
|
|
297583
|
+
}
|
|
297584
|
+
return out;
|
|
297585
|
+
}
|
|
297586
|
+
/** Import identities/personas, deduped by (case-insensitive) name — an
|
|
297587
|
+
* existing name is skipped, never replaced. Imports are never the default. */
|
|
297588
|
+
importIdentities(identities) {
|
|
297589
|
+
const existing = new Set(this.listIdentities().map((i4) => i4.name.toLowerCase()));
|
|
297590
|
+
let imported = 0;
|
|
297591
|
+
for (const ident of identities ?? []) {
|
|
297592
|
+
if (!ident?.name || typeof ident.name !== "string") continue;
|
|
297593
|
+
if (existing.has(ident.name.toLowerCase())) continue;
|
|
297594
|
+
this.createIdentity({
|
|
297595
|
+
...ident,
|
|
297596
|
+
id: crypto4.randomUUID(),
|
|
297597
|
+
isDefault: false,
|
|
297598
|
+
createdAt: ident.createdAt ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
297599
|
+
});
|
|
297600
|
+
existing.add(ident.name.toLowerCase());
|
|
297601
|
+
imported++;
|
|
297602
|
+
}
|
|
297603
|
+
return imported;
|
|
297604
|
+
}
|
|
297015
297605
|
// ── Identities ────────────────────────────────
|
|
297016
297606
|
createIdentity(identity) {
|
|
297017
297607
|
this.db.prepare(`
|
|
@@ -297189,6 +297779,23 @@ Original error: ${err.message}`
|
|
|
297189
297779
|
const row = this.db.prepare("SELECT metadata FROM model_cache WHERE id = ?").get(`${provider}:${modelId}`);
|
|
297190
297780
|
return row ? JSON.parse(row.metadata) : void 0;
|
|
297191
297781
|
}
|
|
297782
|
+
/**
|
|
297783
|
+
* Persist a probed capability verdict (currently: native tool support) into
|
|
297784
|
+
* the model's cached profile, merging with whatever is already recorded.
|
|
297785
|
+
* Read back via getModelProfile — so a model is probed once ever, not once
|
|
297786
|
+
* per run.
|
|
297787
|
+
*/
|
|
297788
|
+
saveModelCapability(modelId, provider, caps) {
|
|
297789
|
+
const cacheKey = `${provider}:${modelId}`;
|
|
297790
|
+
const existing = this.db.prepare("SELECT metadata FROM model_cache WHERE id = ?").get(cacheKey);
|
|
297791
|
+
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 };
|
|
297792
|
+
if (caps.supportsToolUse !== void 0) meta.supportsToolUse = caps.supportsToolUse;
|
|
297793
|
+
this.db.prepare(`
|
|
297794
|
+
INSERT INTO model_cache (id, provider, model_id, name, metadata, updated_at)
|
|
297795
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
297796
|
+
ON CONFLICT(id) DO UPDATE SET metadata = excluded.metadata, updated_at = excluded.updated_at
|
|
297797
|
+
`).run(cacheKey, provider, modelId, meta.name ?? modelId, JSON.stringify(meta), (/* @__PURE__ */ new Date()).toISOString());
|
|
297798
|
+
}
|
|
297192
297799
|
getProfiledModelIds() {
|
|
297193
297800
|
const rows = this.db.prepare(
|
|
297194
297801
|
"SELECT model_id FROM model_cache WHERE json_extract(metadata, '$.specializations') IS NOT NULL"
|
|
@@ -299129,6 +299736,60 @@ ${prompt}`;
|
|
|
299129
299736
|
this.socket.broadcast("runtime:refresh", { scope: "global" });
|
|
299130
299737
|
res.json({ ok: true });
|
|
299131
299738
|
});
|
|
299739
|
+
this.app.get("/api/export", auth, (req, res) => {
|
|
299740
|
+
try {
|
|
299741
|
+
const sessionsParam = typeof req.query["sessions"] === "string" ? req.query["sessions"] : "all";
|
|
299742
|
+
const includeMemories = req.query["memories"] === "1" || req.query["memories"] === "true";
|
|
299743
|
+
const ids = sessionsParam === "all" ? void 0 : sessionsParam.split(",").map((s3) => s3.trim()).filter(Boolean);
|
|
299744
|
+
const bundle = {
|
|
299745
|
+
format: "cascade-export@1",
|
|
299746
|
+
exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
299747
|
+
sessions: this.store.exportSessions(ids)
|
|
299748
|
+
};
|
|
299749
|
+
if (includeMemories) {
|
|
299750
|
+
const ws2 = new WorldStateDB(this.workspacePath);
|
|
299751
|
+
try {
|
|
299752
|
+
bundle["memories"] = { ...ws2.exportKnowledge(), identities: this.store.listIdentities() };
|
|
299753
|
+
} finally {
|
|
299754
|
+
ws2.close();
|
|
299755
|
+
}
|
|
299756
|
+
}
|
|
299757
|
+
res.json(bundle);
|
|
299758
|
+
} catch (err) {
|
|
299759
|
+
res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
|
|
299760
|
+
}
|
|
299761
|
+
});
|
|
299762
|
+
this.app.post("/api/import", auth, mutationLimiter, (req, res) => {
|
|
299763
|
+
try {
|
|
299764
|
+
const bundle = req.body;
|
|
299765
|
+
if (!bundle || bundle.format !== "cascade-export@1") {
|
|
299766
|
+
res.status(400).json({ error: "Not a cascade-export@1 bundle." });
|
|
299767
|
+
return;
|
|
299768
|
+
}
|
|
299769
|
+
const importedSessions = Array.isArray(bundle.sessions) ? this.store.importSessions(bundle.sessions) : [];
|
|
299770
|
+
for (const s3 of importedSessions) this.persistRuntimeRow(s3.id, s3.title, "COMPLETED");
|
|
299771
|
+
let facts = 0;
|
|
299772
|
+
let logEntries = 0;
|
|
299773
|
+
let identities = 0;
|
|
299774
|
+
if (bundle.memories) {
|
|
299775
|
+
const ws2 = new WorldStateDB(this.workspacePath);
|
|
299776
|
+
try {
|
|
299777
|
+
const counts = ws2.importKnowledge(bundle.memories);
|
|
299778
|
+
facts = counts.facts;
|
|
299779
|
+
logEntries = counts.logEntries;
|
|
299780
|
+
} finally {
|
|
299781
|
+
ws2.close();
|
|
299782
|
+
}
|
|
299783
|
+
if (Array.isArray(bundle.memories.identities)) {
|
|
299784
|
+
identities = this.store.importIdentities(bundle.memories.identities);
|
|
299785
|
+
}
|
|
299786
|
+
}
|
|
299787
|
+
this.socket.broadcast("runtime:refresh", { scope: "workspace" });
|
|
299788
|
+
res.json({ ok: true, imported: { sessions: importedSessions.length, facts, logEntries, identities } });
|
|
299789
|
+
} catch (err) {
|
|
299790
|
+
res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
|
|
299791
|
+
}
|
|
299792
|
+
});
|
|
299132
299793
|
this.app.post("/api/sessions/:id/rollback", auth, mutationLimiter, async (req, res) => {
|
|
299133
299794
|
const sessionId = req.params.id;
|
|
299134
299795
|
const taskIds = this.sessionTaskIds.get(sessionId) ?? [];
|