cascade-ai 0.18.0 → 0.19.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/dist/cli.cjs +182 -48
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +182 -48
- package/dist/cli.js.map +1 -1
- package/dist/desktop-core.cjs +180 -47
- package/dist/index.cjs +180 -47
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +30 -0
- package/dist/index.d.ts +30 -0
- package/dist/index.js +180 -47
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -236,7 +236,7 @@ var init_audit_logger = __esm({
|
|
|
236
236
|
});
|
|
237
237
|
|
|
238
238
|
// src/constants.ts
|
|
239
|
-
var CASCADE_VERSION = "0.
|
|
239
|
+
var CASCADE_VERSION = "0.19.0";
|
|
240
240
|
var CASCADE_CONFIG_DIR = ".cascade";
|
|
241
241
|
var CASCADE_MD_FILE = "CASCADE.md";
|
|
242
242
|
var CASCADE_IGNORE_FILE = ".cascadeignore";
|
|
@@ -1039,6 +1039,23 @@ var OpenAIProvider = class extends BaseProvider {
|
|
|
1039
1039
|
};
|
|
1040
1040
|
|
|
1041
1041
|
// src/providers/azure.ts
|
|
1042
|
+
function azureModelForDeployment(cfg) {
|
|
1043
|
+
if (cfg.type !== "azure" || !cfg.deploymentName?.trim()) return null;
|
|
1044
|
+
const id = cfg.deploymentName.trim();
|
|
1045
|
+
return {
|
|
1046
|
+
id,
|
|
1047
|
+
name: cfg.label?.trim() || id,
|
|
1048
|
+
provider: "azure",
|
|
1049
|
+
contextWindow: 128e3,
|
|
1050
|
+
isVisionCapable: false,
|
|
1051
|
+
inputCostPer1kTokens: 25e-4,
|
|
1052
|
+
outputCostPer1kTokens: 0.01,
|
|
1053
|
+
maxOutputTokens: 16e3,
|
|
1054
|
+
supportsStreaming: true,
|
|
1055
|
+
isLocal: false,
|
|
1056
|
+
supportsToolUse: true
|
|
1057
|
+
};
|
|
1058
|
+
}
|
|
1042
1059
|
var AzureOpenAIProvider = class extends OpenAIProvider {
|
|
1043
1060
|
constructor(config, model) {
|
|
1044
1061
|
const rawUrl = config.baseUrl ?? AZURE_BASE_URL_TEMPLATE.replace("{resource}", "YOUR_RESOURCE");
|
|
@@ -1059,7 +1076,8 @@ var AzureOpenAIProvider = class extends OpenAIProvider {
|
|
|
1059
1076
|
});
|
|
1060
1077
|
}
|
|
1061
1078
|
async listModels() {
|
|
1062
|
-
|
|
1079
|
+
const fromDeployment = azureModelForDeployment(this.config);
|
|
1080
|
+
return [fromDeployment ?? this.model];
|
|
1063
1081
|
}
|
|
1064
1082
|
async isAvailable() {
|
|
1065
1083
|
try {
|
|
@@ -2568,6 +2586,12 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
|
|
|
2568
2586
|
const results = await Promise.all(ocConfigs.map((cfg) => this.discoverOpenAICompatibleModels(cfg)));
|
|
2569
2587
|
if (results.some(Boolean)) this.selector.markProviderAvailable("openai-compatible");
|
|
2570
2588
|
}
|
|
2589
|
+
if (availableProviders.has("azure")) {
|
|
2590
|
+
for (const cfg of config.providers) {
|
|
2591
|
+
const model = azureModelForDeployment(cfg);
|
|
2592
|
+
if (model) this.selector.addDynamicModel(model);
|
|
2593
|
+
}
|
|
2594
|
+
}
|
|
2571
2595
|
for (const tier of ["T1", "T2", "T3"]) {
|
|
2572
2596
|
const override = tier === "T1" ? config.models.t1 : tier === "T2" ? config.models.t2 : config.models.t3;
|
|
2573
2597
|
if (!override || override === "auto") continue;
|
|
@@ -3174,7 +3198,7 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
|
|
|
3174
3198
|
ensureProvider(model, configs) {
|
|
3175
3199
|
const key = `${model.provider}:${model.id}`;
|
|
3176
3200
|
if (this.providers.has(key)) return;
|
|
3177
|
-
const cfg = configs.find((c) => c.type === model.provider) ?? { type: model.provider };
|
|
3201
|
+
const cfg = (model.provider === "azure" ? configs.find((c) => c.type === "azure" && c.deploymentName === model.id) : void 0) ?? configs.find((c) => c.type === model.provider) ?? { type: model.provider };
|
|
3178
3202
|
const provider = this.createProvider(cfg, model);
|
|
3179
3203
|
this.providers.set(key, provider);
|
|
3180
3204
|
}
|
|
@@ -3334,6 +3358,12 @@ var BaseTier = class extends EventEmitter__default.default {
|
|
|
3334
3358
|
* which would interleave, are not tagged.
|
|
3335
3359
|
*/
|
|
3336
3360
|
isPresenter = false;
|
|
3361
|
+
/**
|
|
3362
|
+
* The model actually serving this tier (`provider:id`), once resolved —
|
|
3363
|
+
* rides on every tier:status event so the desktop can show which model ran
|
|
3364
|
+
* which node (Cockpit node panel / Why panel).
|
|
3365
|
+
*/
|
|
3366
|
+
servingModel;
|
|
3337
3367
|
constructor(role, id, parentId) {
|
|
3338
3368
|
super();
|
|
3339
3369
|
this.role = role;
|
|
@@ -3358,11 +3388,16 @@ var BaseTier = class extends EventEmitter__default.default {
|
|
|
3358
3388
|
label: this.label,
|
|
3359
3389
|
status,
|
|
3360
3390
|
timestamp,
|
|
3361
|
-
output
|
|
3391
|
+
output,
|
|
3392
|
+
model: this.servingModel
|
|
3362
3393
|
};
|
|
3363
3394
|
this.emit("status", event);
|
|
3364
3395
|
this.emit("tier:status", event);
|
|
3365
3396
|
}
|
|
3397
|
+
/** Record the model serving this tier; future status events carry it. */
|
|
3398
|
+
setServingModel(model) {
|
|
3399
|
+
this.servingModel = model || void 0;
|
|
3400
|
+
}
|
|
3366
3401
|
setLabel(label) {
|
|
3367
3402
|
this.label = label;
|
|
3368
3403
|
}
|
|
@@ -3385,7 +3420,8 @@ var BaseTier = class extends EventEmitter__default.default {
|
|
|
3385
3420
|
currentAction: update.currentAction,
|
|
3386
3421
|
progressPct: update.progressPct,
|
|
3387
3422
|
timestamp,
|
|
3388
|
-
output: update.output
|
|
3423
|
+
output: update.output,
|
|
3424
|
+
model: this.servingModel
|
|
3389
3425
|
});
|
|
3390
3426
|
}
|
|
3391
3427
|
buildMessage(type, to, payload) {
|
|
@@ -3751,6 +3787,21 @@ Available tools: ${tools.map((t) => t.name).join(", ")}.
|
|
|
3751
3787
|
When you have enough information, stop calling tools and write your final answer.`;
|
|
3752
3788
|
}
|
|
3753
3789
|
|
|
3790
|
+
// src/utils/truncate.ts
|
|
3791
|
+
function truncateForContext(text, maxChars = 12e3) {
|
|
3792
|
+
if (text.length <= maxChars) return text;
|
|
3793
|
+
const headLen = Math.floor(maxChars * 0.75);
|
|
3794
|
+
const tailLen = maxChars - headLen;
|
|
3795
|
+
const head = text.slice(0, headLen);
|
|
3796
|
+
const tail = text.slice(-tailLen);
|
|
3797
|
+
const elided = text.length - headLen - tailLen;
|
|
3798
|
+
return `${head}
|
|
3799
|
+
|
|
3800
|
+
[... ${elided.toLocaleString()} characters elided to keep context small \u2014 re-read the file with a line range if you need the middle ...]
|
|
3801
|
+
|
|
3802
|
+
${tail}`;
|
|
3803
|
+
}
|
|
3804
|
+
|
|
3754
3805
|
// src/core/tiers/t3-worker.ts
|
|
3755
3806
|
var CriticalToolError = class extends Error {
|
|
3756
3807
|
constructor(message, toolName) {
|
|
@@ -4058,6 +4109,7 @@ Now execute your subtask using this context where relevant.`
|
|
|
4058
4109
|
} catch {
|
|
4059
4110
|
}
|
|
4060
4111
|
const effectiveModel = subtaskModel ?? this.router.getModelForTier("T3");
|
|
4112
|
+
if (effectiveModel) this.setServingModel(`${effectiveModel.provider}:${effectiveModel.id}`);
|
|
4061
4113
|
const useTextTools = effectiveModel?.supportsToolUse === false && tools.length > 0;
|
|
4062
4114
|
let sentFullTextContract = false;
|
|
4063
4115
|
let textContractSignature = "";
|
|
@@ -4155,7 +4207,7 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : "") + textToolSuffix,
|
|
|
4155
4207
|
const toolResult = await this.executeTool(tc);
|
|
4156
4208
|
await this.context.addMessage({
|
|
4157
4209
|
role: "tool",
|
|
4158
|
-
content: toolResult,
|
|
4210
|
+
content: truncateForContext(toolResult),
|
|
4159
4211
|
toolCallId: tc.id
|
|
4160
4212
|
});
|
|
4161
4213
|
}
|
|
@@ -4412,15 +4464,17 @@ ${assignment.expectedOutput}`;
|
|
|
4412
4464
|
};
|
|
4413
4465
|
}
|
|
4414
4466
|
requiresArtifact() {
|
|
4467
|
+
if (this.assignment?.files?.length) return true;
|
|
4415
4468
|
const haystack = `${this.assignment?.description ?? ""}
|
|
4416
4469
|
${this.assignment?.expectedOutput ?? ""}`;
|
|
4417
4470
|
return /\b[\w./-]+\.(pdf|md|html|txt|json|csv|py|js|ts|tsx|jsx|docx?|png|jpg|jpeg|svg|gif)\b/i.test(haystack) || /save (?:a|the)? file|create (?:a|the)? file|write (?:a|the)? file/i.test(haystack);
|
|
4418
4471
|
}
|
|
4419
4472
|
extractArtifactPaths(assignment) {
|
|
4473
|
+
const declared = (assignment.files ?? []).map((f) => f.trim()).filter((f) => f.includes("."));
|
|
4420
4474
|
const haystack = `${assignment.description}
|
|
4421
4475
|
${assignment.expectedOutput}`;
|
|
4422
4476
|
const matches = haystack.match(/\b[\w./-]+\.(pdf|md|html|txt|json|csv|py|js|ts|tsx|jsx|docx?|png|jpg|jpeg|svg|gif)\b/gi) ?? [];
|
|
4423
|
-
return [
|
|
4477
|
+
return [.../* @__PURE__ */ new Set([...declared, ...matches.map((m) => m.trim())])];
|
|
4424
4478
|
}
|
|
4425
4479
|
async verifyArtifacts(assignment) {
|
|
4426
4480
|
const artifactPaths = this.extractArtifactPaths(assignment);
|
|
@@ -4543,7 +4597,9 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
|
4543
4597
|
Assignment: ${assignment.description}
|
|
4544
4598
|
Expected output: ${assignment.expectedOutput}
|
|
4545
4599
|
Constraints: ${assignment.constraints.join("; ")}
|
|
4546
|
-
|
|
4600
|
+
${assignment.acceptance?.length ? `Acceptance criteria \u2014 ALL must be satisfied for "completeness" to pass:
|
|
4601
|
+
${assignment.acceptance.map((a) => `- ${a}`).join("\n")}
|
|
4602
|
+
` : ""}
|
|
4547
4603
|
Output to test:
|
|
4548
4604
|
${output}
|
|
4549
4605
|
|
|
@@ -4632,17 +4688,27 @@ Your subtask:
|
|
|
4632
4688
|
- Title: ${assignment.subtaskTitle}
|
|
4633
4689
|
- Description: ${assignment.description}
|
|
4634
4690
|
- Expected output: ${assignment.expectedOutput}
|
|
4635
|
-
- Constraints: ${assignment.constraints.join("; ")}
|
|
4691
|
+
- Constraints: ${assignment.constraints.join("; ")}${assignment.files?.length ? `
|
|
4692
|
+
- Files you own (create/edit ONLY these): ${assignment.files.join(", ")}` : ""}${assignment.acceptance?.length ? `
|
|
4693
|
+
- Definition of done: ${assignment.acceptance.join("; ")}` : ""}`;
|
|
4636
4694
|
}
|
|
4637
4695
|
buildInitialPrompt(assignment) {
|
|
4638
4696
|
return `Execute the following subtask completely:
|
|
4639
4697
|
|
|
4640
4698
|
**${assignment.subtaskTitle}**
|
|
4641
|
-
|
|
4699
|
+
${assignment.contextBrief ? `
|
|
4700
|
+
Context: ${assignment.contextBrief}
|
|
4701
|
+
` : ""}
|
|
4642
4702
|
${assignment.description}
|
|
4643
4703
|
|
|
4644
4704
|
Expected output: ${assignment.expectedOutput}
|
|
4645
|
-
|
|
4705
|
+
${assignment.files?.length ? `
|
|
4706
|
+
Files you own (create or edit exactly these paths):
|
|
4707
|
+
${assignment.files.map((f) => `- ${f}`).join("\n")}
|
|
4708
|
+
` : ""}${assignment.acceptance?.length ? `
|
|
4709
|
+
Definition of done (your output must satisfy ALL of these):
|
|
4710
|
+
${assignment.acceptance.map((a) => `- ${a}`).join("\n")}
|
|
4711
|
+
` : ""}
|
|
4646
4712
|
Constraints:
|
|
4647
4713
|
${assignment.constraints.map((c) => `- ${c}`).join("\n")}
|
|
4648
4714
|
|
|
@@ -5123,6 +5189,8 @@ var T2Manager = class extends BaseTier {
|
|
|
5123
5189
|
this.assignment = assignment;
|
|
5124
5190
|
this.taskId = taskId;
|
|
5125
5191
|
this.setLabel(assignment.sectionTitle);
|
|
5192
|
+
const m = this.router.getModelForTier("T2");
|
|
5193
|
+
if (m) this.setServingModel(`${m.provider}:${m.id}`);
|
|
5126
5194
|
this.setStatus("ACTIVE");
|
|
5127
5195
|
this.sendStatusUpdate({
|
|
5128
5196
|
progressPct: 0,
|
|
@@ -5209,7 +5277,7 @@ Guidance (must be followed): ${decision.note}`
|
|
|
5209
5277
|
// ── Private ──────────────────────────────────
|
|
5210
5278
|
async decomposeSection(assignment) {
|
|
5211
5279
|
const peerPlans = this.peerSyncBuffer.filter((p) => p.content?.type === "T2_PLAN_ANNOUNCEMENT").map((p) => `[Peer ${p.fromId} Plan]: ${p.content.sectionTitle} - ${p.content.subtaskTitles?.join(", ")}`).join("\n");
|
|
5212
|
-
const prompt = `Decompose this section into
|
|
5280
|
+
const prompt = `Decompose this section into 1-4 concrete subtasks for T3 workers \u2014 the FEWEST that fully cover it (one subtask is the correct answer for a small section).
|
|
5213
5281
|
|
|
5214
5282
|
Section: ${assignment.sectionTitle}
|
|
5215
5283
|
Description: ${assignment.description}
|
|
@@ -5228,6 +5296,9 @@ Return a JSON array of subtask objects, each with:
|
|
|
5228
5296
|
- peerT3Ids: string[] (empty for now)
|
|
5229
5297
|
- dependsOn: string[] (array of subtaskIds this task depends on to start)
|
|
5230
5298
|
- executionMode: "parallel|sequential" (default is parallel)
|
|
5299
|
+
- files: string[] (the EXACT relative paths this subtask creates or edits)
|
|
5300
|
+
- acceptance: string[] (1-3 mechanically checkable done-criteria: file exists / contains X / command exits 0)
|
|
5301
|
+
- contextBrief: string (1-3 short sentences with ALL the background the worker needs \u2014 it sees nothing else)
|
|
5231
5302
|
|
|
5232
5303
|
Return ONLY the JSON array.`;
|
|
5233
5304
|
const messages = [{ role: "user", content: prompt }];
|
|
@@ -5825,6 +5896,8 @@ var T1Administrator = class extends BaseTier {
|
|
|
5825
5896
|
this.signal = signal;
|
|
5826
5897
|
this.taskId = crypto3.randomUUID();
|
|
5827
5898
|
this.setLabel("Administrator");
|
|
5899
|
+
const m = this.router.getModelForTier("T1");
|
|
5900
|
+
if (m) this.setServingModel(`${m.provider}:${m.id}`);
|
|
5828
5901
|
this.setStatus("ACTIVE");
|
|
5829
5902
|
this.taskGoal = userPrompt;
|
|
5830
5903
|
this.sendStatusUpdate({
|
|
@@ -6071,10 +6144,13 @@ Return JSON where SECTIONS can declare dependencies on other SECTIONS:
|
|
|
6071
6144
|
"description": "Run npm init",
|
|
6072
6145
|
"expectedOutput": "package.json created",
|
|
6073
6146
|
"constraints": [],
|
|
6074
|
-
"dependsOn": []
|
|
6147
|
+
"dependsOn": [],
|
|
6148
|
+
"files": ["package.json"], // \u2190 exact paths this subtask owns
|
|
6149
|
+
"acceptance": ["package.json exists and parses as JSON"], // \u2190 objectively checkable
|
|
6150
|
+
"contextBrief": "Fresh Node 20 project; npm available." // \u2190 ALL the background the worker gets
|
|
6075
6151
|
}]
|
|
6076
6152
|
}, {
|
|
6077
|
-
"sectionId": "s2",
|
|
6153
|
+
"sectionId": "s2",
|
|
6078
6154
|
"sectionTitle": "Write Tests",
|
|
6079
6155
|
"description": "Write tests for the project",
|
|
6080
6156
|
"expectedOutput": "Tests passing",
|
|
@@ -6084,7 +6160,13 @@ Return JSON where SECTIONS can declare dependencies on other SECTIONS:
|
|
|
6084
6160
|
}]
|
|
6085
6161
|
}
|
|
6086
6162
|
Use dependsOn at the SECTION level when a whole T2 Manager needs the output of a previous T2 Manager.
|
|
6087
|
-
Leave dependsOn empty for sections that can run immediately in parallel
|
|
6163
|
+
Leave dependsOn empty for sections that can run immediately in parallel.
|
|
6164
|
+
|
|
6165
|
+
SPEC RULES \u2014 each subtask is a self-contained spec slice (workers execute from their slice ALONE):
|
|
6166
|
+
- "files": the exact relative paths the subtask creates or edits. Never vague ("some files"); always concrete.
|
|
6167
|
+
- "acceptance": 1-3 checks a reviewer could verify mechanically (file exists / contains X / command exits 0). These define done.
|
|
6168
|
+
- "contextBrief": 1-3 short sentences with the ONLY background the worker needs. It sees nothing else about the task, so make the brief self-sufficient \u2014 but never pad it.
|
|
6169
|
+
- RIGHT-SIZE the plan: use the FEWEST sections and workers that fully cover the task. One section with 1-2 subtasks is the CORRECT plan for a small task; padding a plan with filler sections wastes the user's money.`;
|
|
6088
6170
|
const messages = [{ role: "user", content: decompositionPrompt }];
|
|
6089
6171
|
const result = await this.router.generate("T1", {
|
|
6090
6172
|
messages,
|
|
@@ -7386,30 +7468,56 @@ async function searchTavily(query, apiKey, maxResults) {
|
|
|
7386
7468
|
engine: "tavily"
|
|
7387
7469
|
}));
|
|
7388
7470
|
}
|
|
7389
|
-
|
|
7390
|
-
|
|
7391
|
-
|
|
7392
|
-
|
|
7393
|
-
|
|
7394
|
-
|
|
7395
|
-
|
|
7396
|
-
|
|
7397
|
-
|
|
7398
|
-
|
|
7399
|
-
|
|
7471
|
+
var BROWSER_UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0 Safari/537.36";
|
|
7472
|
+
function unwrapDdgRedirect(href) {
|
|
7473
|
+
try {
|
|
7474
|
+
const url = new URL(href.startsWith("//") ? `https:${href}` : href, "https://duckduckgo.com");
|
|
7475
|
+
if (/(^|\.)duckduckgo\.com$/i.test(url.hostname) && url.pathname.startsWith("/l/")) {
|
|
7476
|
+
const target = url.searchParams.get("uddg");
|
|
7477
|
+
if (target) return decodeURIComponent(target);
|
|
7478
|
+
}
|
|
7479
|
+
return url.protocol === "http:" || url.protocol === "https:" ? url.toString() : href;
|
|
7480
|
+
} catch {
|
|
7481
|
+
return href;
|
|
7482
|
+
}
|
|
7483
|
+
}
|
|
7484
|
+
function stripTags(html) {
|
|
7485
|
+
return html.replace(/<[^>]+>/g, "").replace(/\s+/g, " ").trim();
|
|
7486
|
+
}
|
|
7487
|
+
function decodeEntities(text) {
|
|
7488
|
+
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'|'/g, "'").replace(/ /g, " ");
|
|
7489
|
+
}
|
|
7490
|
+
function parseDdgAnchors(html, anchorClass, snippetClass) {
|
|
7491
|
+
const anchorRe = new RegExp(`<a\\b[^>]*class=["']?[^"'>]*\\b${anchorClass}\\b[^"'>]*["']?[^>]*>([\\s\\S]*?)<\\/a>`, "gi");
|
|
7492
|
+
const snippetRe = new RegExp(`class=["']?[^"'>]*\\b${snippetClass}\\b[^"'>]*["']?[^>]*>([\\s\\S]*?)<\\/(?:td|a|div|span)>`, "gi");
|
|
7493
|
+
const results = [];
|
|
7400
7494
|
let m;
|
|
7401
|
-
while ((m =
|
|
7402
|
-
|
|
7495
|
+
while ((m = anchorRe.exec(html)) !== null) {
|
|
7496
|
+
const tag = m[0];
|
|
7497
|
+
const href = /href=["']([^"']+)["']/i.exec(tag)?.[1];
|
|
7498
|
+
const title = decodeEntities(stripTags(m[1] ?? ""));
|
|
7499
|
+
if (!href || !title) continue;
|
|
7500
|
+
results.push({ title, url: unwrapDdgRedirect(decodeEntities(href)), snippet: "" });
|
|
7501
|
+
}
|
|
7502
|
+
const snippets = [];
|
|
7503
|
+
while ((m = snippetRe.exec(html)) !== null) {
|
|
7504
|
+
snippets.push(decodeEntities(stripTags(m[1] ?? "")));
|
|
7403
7505
|
}
|
|
7404
|
-
|
|
7405
|
-
snippets
|
|
7506
|
+
for (let i = 0; i < results.length; i++) {
|
|
7507
|
+
if (snippets[i]) results[i].snippet = snippets[i];
|
|
7406
7508
|
}
|
|
7407
|
-
return
|
|
7408
|
-
|
|
7409
|
-
|
|
7410
|
-
|
|
7411
|
-
|
|
7412
|
-
|
|
7509
|
+
return results;
|
|
7510
|
+
}
|
|
7511
|
+
async function searchDuckDuckGo(query, maxResults, variant) {
|
|
7512
|
+
const base = variant === "html" ? "https://html.duckduckgo.com/html/?q=" : "https://lite.duckduckgo.com/lite/?q=";
|
|
7513
|
+
const resp = await fetch(`${base}${encodeURIComponent(query)}`, {
|
|
7514
|
+
headers: { "User-Agent": BROWSER_UA, Accept: "text/html" },
|
|
7515
|
+
signal: AbortSignal.timeout(1e4)
|
|
7516
|
+
});
|
|
7517
|
+
if (!resp.ok) throw new Error(`DuckDuckGo ${variant} returned HTTP ${resp.status}`);
|
|
7518
|
+
const html = await resp.text();
|
|
7519
|
+
const parsed = variant === "html" ? parseDdgAnchors(html, "result__a", "result__snippet") : parseDdgAnchors(html, "result-link", "result-snippet");
|
|
7520
|
+
return parsed.slice(0, maxResults).map((r) => ({ ...r, engine: `duckduckgo-${variant}` }));
|
|
7413
7521
|
}
|
|
7414
7522
|
var WebSearchTool = class extends BaseTool {
|
|
7415
7523
|
name = "web_search";
|
|
@@ -7468,12 +7576,14 @@ var WebSearchTool = class extends BaseTool {
|
|
|
7468
7576
|
errors.push(`Tavily: ${err instanceof Error ? err.message : String(err)}`);
|
|
7469
7577
|
}
|
|
7470
7578
|
}
|
|
7471
|
-
|
|
7472
|
-
|
|
7473
|
-
|
|
7474
|
-
|
|
7475
|
-
|
|
7476
|
-
|
|
7579
|
+
for (const variant of ["html", "lite"]) {
|
|
7580
|
+
try {
|
|
7581
|
+
results = await searchDuckDuckGo(query, maxResults, variant);
|
|
7582
|
+
if (results.length > 0) return this.formatResults(query, results);
|
|
7583
|
+
errors.push(`DuckDuckGo ${variant}: returned 0 results`);
|
|
7584
|
+
} catch (err) {
|
|
7585
|
+
errors.push(`DuckDuckGo ${variant}: ${err instanceof Error ? err.message : String(err)}`);
|
|
7586
|
+
}
|
|
7477
7587
|
}
|
|
7478
7588
|
const configHint = !this.config.searxngUrl && !this.config.braveApiKey && !this.config.tavilyApiKey ? "\nTip: Configure a search backend for better results:\n \u2022 Self-hosted: set SEARXNG_URL in your environment\n \u2022 Brave Search API: set BRAVE_SEARCH_API_KEY\n \u2022 Tavily API: set TAVILY_API_KEY" : "";
|
|
7479
7589
|
return [
|
|
@@ -8417,8 +8527,11 @@ var CascadeConfigSchema = zod.z.object({
|
|
|
8417
8527
|
* Cascade Auto: when true, the TaskAnalyzer selects the optimal model for each
|
|
8418
8528
|
* tier based on task type and complexity, overriding the static priority lists.
|
|
8419
8529
|
* Heuristic-first with AI inference fallback (adds ~0–500ms per task).
|
|
8530
|
+
* ON by default since v0.19.0 — "Auto" without it was just a static priority
|
|
8531
|
+
* list, not the benchmark-value routing the docs describe. Explicit per-tier
|
|
8532
|
+
* model pins are unaffected; disable via config/Settings → Advanced.
|
|
8420
8533
|
*/
|
|
8421
|
-
cascadeAuto: zod.z.boolean().default(
|
|
8534
|
+
cascadeAuto: zod.z.boolean().default(true),
|
|
8422
8535
|
/**
|
|
8423
8536
|
* Cascade Auto trade-off bias when picking a model for a task:
|
|
8424
8537
|
* - 'balanced' (default): quality × cost-efficiency — cheap models win
|
|
@@ -10131,13 +10244,30 @@ ${last.partialOutput}` : "");
|
|
|
10131
10244
|
* explicit multi-part structure, so ordinary single-file asks (handled as
|
|
10132
10245
|
* Simple/Moderate) don't get over-escalated.
|
|
10133
10246
|
*/
|
|
10134
|
-
|
|
10247
|
+
/** Shared build/scale signals for the complexity floors below. */
|
|
10248
|
+
buildSignals(prompt) {
|
|
10135
10249
|
const p = prompt.trim();
|
|
10136
|
-
if (p.length < 24) return false;
|
|
10250
|
+
if (p.length < 24) return { buildVerb: false, scaleCount: 0, multiPart: false };
|
|
10137
10251
|
const buildVerb = /\b(?:build|implement|create|develop|design|scaffold|refactor|migrate|architect|set up|integrate)\b/i.test(p);
|
|
10138
|
-
const
|
|
10252
|
+
const scaleCount = (p.match(/\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/gi) ?? []).length;
|
|
10139
10253
|
const multiPart = /(?:\b(?:and|then|also|plus|as well as)\b.*\b(?:and|then|also)\b)|(?:^|\n)\s*(?:[-*]|\d+[.)])\s+/i.test(p);
|
|
10140
|
-
return buildVerb
|
|
10254
|
+
return { buildVerb, scaleCount, multiPart };
|
|
10255
|
+
}
|
|
10256
|
+
/**
|
|
10257
|
+
* A build prompt with REAL scale: multiple system-level deliverables, or a
|
|
10258
|
+
* deliverable plus explicitly multi-part phrasing. Only these floor to the
|
|
10259
|
+
* full T1→T2→T3 hierarchy — "create a todo app" is a build prompt too, but
|
|
10260
|
+
* flooring every small build to Complex was the #1 token bomb (3-5 managers
|
|
10261
|
+
* × workers for a task one worker handles).
|
|
10262
|
+
*/
|
|
10263
|
+
looksClearlyComplex(prompt) {
|
|
10264
|
+
const s = this.buildSignals(prompt);
|
|
10265
|
+
return s.buildVerb && (s.scaleCount >= 2 || s.scaleCount >= 1 && s.multiPart);
|
|
10266
|
+
}
|
|
10267
|
+
/** A small single-deliverable build — real work, but one manager's worth. */
|
|
10268
|
+
looksLikeModerateBuild(prompt) {
|
|
10269
|
+
const s = this.buildSignals(prompt);
|
|
10270
|
+
return s.buildVerb && (s.scaleCount >= 1 || s.multiPart);
|
|
10141
10271
|
}
|
|
10142
10272
|
// Cache glob scan results per workspace path to avoid repeated I/O.
|
|
10143
10273
|
static globCache = /* @__PURE__ */ new Map();
|
|
@@ -10225,6 +10355,9 @@ ${prompt}` : prompt;
|
|
|
10225
10355
|
if (verdict !== "Complex" && this.looksClearlyComplex(prompt)) {
|
|
10226
10356
|
this.recordDecision("complexity", `Complex \u2014 heuristic floor over classifier "${verdict}": explicit multi-step build/implementation signals (T1 engaged)`);
|
|
10227
10357
|
verdict = "Complex";
|
|
10358
|
+
} else if (verdict === "Simple" && this.looksLikeModerateBuild(prompt)) {
|
|
10359
|
+
this.recordDecision("complexity", 'Moderate \u2014 heuristic floor over classifier "Simple": build signals without multi-system scale (single manager)');
|
|
10360
|
+
verdict = "Moderate";
|
|
10228
10361
|
} else {
|
|
10229
10362
|
this.recordDecision("complexity", `${verdict} \u2014 classifier: ${reason || "no reason given"}`);
|
|
10230
10363
|
}
|