cascade-ai 0.18.0 → 0.19.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 +205 -54
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +205 -54
- package/dist/cli.js.map +1 -1
- package/dist/desktop-core.cjs +203 -53
- package/dist/index.cjs +203 -53
- 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 +203 -53
- 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.1";
|
|
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,17 +1076,31 @@ 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() {
|
|
1083
|
+
const params = {
|
|
1084
|
+
model: this.model.id,
|
|
1085
|
+
messages: [{ role: "user", content: "ping" }],
|
|
1086
|
+
max_tokens: 1
|
|
1087
|
+
};
|
|
1065
1088
|
try {
|
|
1066
|
-
await this.client.chat.completions.create(
|
|
1067
|
-
model: this.model.id,
|
|
1068
|
-
messages: [{ role: "user", content: "ping" }],
|
|
1069
|
-
max_tokens: 1
|
|
1070
|
-
});
|
|
1089
|
+
await this.client.chat.completions.create(params);
|
|
1071
1090
|
return true;
|
|
1072
|
-
} catch {
|
|
1091
|
+
} catch (err) {
|
|
1092
|
+
if (err?.message && String(err.message).includes("max_completion_tokens")) {
|
|
1093
|
+
try {
|
|
1094
|
+
await this.client.chat.completions.create({
|
|
1095
|
+
model: this.model.id,
|
|
1096
|
+
messages: [{ role: "user", content: "ping" }],
|
|
1097
|
+
max_completion_tokens: 1
|
|
1098
|
+
});
|
|
1099
|
+
return true;
|
|
1100
|
+
} catch {
|
|
1101
|
+
return false;
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1073
1104
|
return false;
|
|
1074
1105
|
}
|
|
1075
1106
|
}
|
|
@@ -1811,6 +1842,10 @@ var ModelSelector = class {
|
|
|
1811
1842
|
actualId = parts.slice(1).join(":");
|
|
1812
1843
|
}
|
|
1813
1844
|
}
|
|
1845
|
+
const registered = this.availableModels.get(actualId);
|
|
1846
|
+
if (registered && (!providerStr || registered.provider === providerStr)) {
|
|
1847
|
+
return registered;
|
|
1848
|
+
}
|
|
1814
1849
|
if (!providerStr) {
|
|
1815
1850
|
const lower = actualId.toLowerCase();
|
|
1816
1851
|
if (lower.includes("claude")) providerStr = "anthropic";
|
|
@@ -2568,6 +2603,12 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
|
|
|
2568
2603
|
const results = await Promise.all(ocConfigs.map((cfg) => this.discoverOpenAICompatibleModels(cfg)));
|
|
2569
2604
|
if (results.some(Boolean)) this.selector.markProviderAvailable("openai-compatible");
|
|
2570
2605
|
}
|
|
2606
|
+
if (availableProviders.has("azure")) {
|
|
2607
|
+
for (const cfg of config.providers) {
|
|
2608
|
+
const model = azureModelForDeployment(cfg);
|
|
2609
|
+
if (model) this.selector.addDynamicModel(model);
|
|
2610
|
+
}
|
|
2611
|
+
}
|
|
2571
2612
|
for (const tier of ["T1", "T2", "T3"]) {
|
|
2572
2613
|
const override = tier === "T1" ? config.models.t1 : tier === "T2" ? config.models.t2 : config.models.t3;
|
|
2573
2614
|
if (!override || override === "auto") continue;
|
|
@@ -3174,7 +3215,7 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
|
|
|
3174
3215
|
ensureProvider(model, configs) {
|
|
3175
3216
|
const key = `${model.provider}:${model.id}`;
|
|
3176
3217
|
if (this.providers.has(key)) return;
|
|
3177
|
-
const cfg = configs.find((c) => c.type === model.provider) ?? { type: model.provider };
|
|
3218
|
+
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
3219
|
const provider = this.createProvider(cfg, model);
|
|
3179
3220
|
this.providers.set(key, provider);
|
|
3180
3221
|
}
|
|
@@ -3334,6 +3375,12 @@ var BaseTier = class extends EventEmitter__default.default {
|
|
|
3334
3375
|
* which would interleave, are not tagged.
|
|
3335
3376
|
*/
|
|
3336
3377
|
isPresenter = false;
|
|
3378
|
+
/**
|
|
3379
|
+
* The model actually serving this tier (`provider:id`), once resolved —
|
|
3380
|
+
* rides on every tier:status event so the desktop can show which model ran
|
|
3381
|
+
* which node (Cockpit node panel / Why panel).
|
|
3382
|
+
*/
|
|
3383
|
+
servingModel;
|
|
3337
3384
|
constructor(role, id, parentId) {
|
|
3338
3385
|
super();
|
|
3339
3386
|
this.role = role;
|
|
@@ -3358,11 +3405,16 @@ var BaseTier = class extends EventEmitter__default.default {
|
|
|
3358
3405
|
label: this.label,
|
|
3359
3406
|
status,
|
|
3360
3407
|
timestamp,
|
|
3361
|
-
output
|
|
3408
|
+
output,
|
|
3409
|
+
model: this.servingModel
|
|
3362
3410
|
};
|
|
3363
3411
|
this.emit("status", event);
|
|
3364
3412
|
this.emit("tier:status", event);
|
|
3365
3413
|
}
|
|
3414
|
+
/** Record the model serving this tier; future status events carry it. */
|
|
3415
|
+
setServingModel(model) {
|
|
3416
|
+
this.servingModel = model || void 0;
|
|
3417
|
+
}
|
|
3366
3418
|
setLabel(label) {
|
|
3367
3419
|
this.label = label;
|
|
3368
3420
|
}
|
|
@@ -3385,7 +3437,8 @@ var BaseTier = class extends EventEmitter__default.default {
|
|
|
3385
3437
|
currentAction: update.currentAction,
|
|
3386
3438
|
progressPct: update.progressPct,
|
|
3387
3439
|
timestamp,
|
|
3388
|
-
output: update.output
|
|
3440
|
+
output: update.output,
|
|
3441
|
+
model: this.servingModel
|
|
3389
3442
|
});
|
|
3390
3443
|
}
|
|
3391
3444
|
buildMessage(type, to, payload) {
|
|
@@ -3751,6 +3804,21 @@ Available tools: ${tools.map((t) => t.name).join(", ")}.
|
|
|
3751
3804
|
When you have enough information, stop calling tools and write your final answer.`;
|
|
3752
3805
|
}
|
|
3753
3806
|
|
|
3807
|
+
// src/utils/truncate.ts
|
|
3808
|
+
function truncateForContext(text, maxChars = 12e3) {
|
|
3809
|
+
if (text.length <= maxChars) return text;
|
|
3810
|
+
const headLen = Math.floor(maxChars * 0.75);
|
|
3811
|
+
const tailLen = maxChars - headLen;
|
|
3812
|
+
const head = text.slice(0, headLen);
|
|
3813
|
+
const tail = text.slice(-tailLen);
|
|
3814
|
+
const elided = text.length - headLen - tailLen;
|
|
3815
|
+
return `${head}
|
|
3816
|
+
|
|
3817
|
+
[... ${elided.toLocaleString()} characters elided to keep context small \u2014 re-read the file with a line range if you need the middle ...]
|
|
3818
|
+
|
|
3819
|
+
${tail}`;
|
|
3820
|
+
}
|
|
3821
|
+
|
|
3754
3822
|
// src/core/tiers/t3-worker.ts
|
|
3755
3823
|
var CriticalToolError = class extends Error {
|
|
3756
3824
|
constructor(message, toolName) {
|
|
@@ -4058,6 +4126,7 @@ Now execute your subtask using this context where relevant.`
|
|
|
4058
4126
|
} catch {
|
|
4059
4127
|
}
|
|
4060
4128
|
const effectiveModel = subtaskModel ?? this.router.getModelForTier("T3");
|
|
4129
|
+
if (effectiveModel) this.setServingModel(`${effectiveModel.provider}:${effectiveModel.id}`);
|
|
4061
4130
|
const useTextTools = effectiveModel?.supportsToolUse === false && tools.length > 0;
|
|
4062
4131
|
let sentFullTextContract = false;
|
|
4063
4132
|
let textContractSignature = "";
|
|
@@ -4155,7 +4224,7 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : "") + textToolSuffix,
|
|
|
4155
4224
|
const toolResult = await this.executeTool(tc);
|
|
4156
4225
|
await this.context.addMessage({
|
|
4157
4226
|
role: "tool",
|
|
4158
|
-
content: toolResult,
|
|
4227
|
+
content: truncateForContext(toolResult),
|
|
4159
4228
|
toolCallId: tc.id
|
|
4160
4229
|
});
|
|
4161
4230
|
}
|
|
@@ -4412,15 +4481,17 @@ ${assignment.expectedOutput}`;
|
|
|
4412
4481
|
};
|
|
4413
4482
|
}
|
|
4414
4483
|
requiresArtifact() {
|
|
4484
|
+
if (this.assignment?.files?.length) return true;
|
|
4415
4485
|
const haystack = `${this.assignment?.description ?? ""}
|
|
4416
4486
|
${this.assignment?.expectedOutput ?? ""}`;
|
|
4417
4487
|
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
4488
|
}
|
|
4419
4489
|
extractArtifactPaths(assignment) {
|
|
4490
|
+
const declared = (assignment.files ?? []).map((f) => f.trim()).filter((f) => f.includes("."));
|
|
4420
4491
|
const haystack = `${assignment.description}
|
|
4421
4492
|
${assignment.expectedOutput}`;
|
|
4422
4493
|
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 [
|
|
4494
|
+
return [.../* @__PURE__ */ new Set([...declared, ...matches.map((m) => m.trim())])];
|
|
4424
4495
|
}
|
|
4425
4496
|
async verifyArtifacts(assignment) {
|
|
4426
4497
|
const artifactPaths = this.extractArtifactPaths(assignment);
|
|
@@ -4543,7 +4614,9 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
|
4543
4614
|
Assignment: ${assignment.description}
|
|
4544
4615
|
Expected output: ${assignment.expectedOutput}
|
|
4545
4616
|
Constraints: ${assignment.constraints.join("; ")}
|
|
4546
|
-
|
|
4617
|
+
${assignment.acceptance?.length ? `Acceptance criteria \u2014 ALL must be satisfied for "completeness" to pass:
|
|
4618
|
+
${assignment.acceptance.map((a) => `- ${a}`).join("\n")}
|
|
4619
|
+
` : ""}
|
|
4547
4620
|
Output to test:
|
|
4548
4621
|
${output}
|
|
4549
4622
|
|
|
@@ -4632,17 +4705,27 @@ Your subtask:
|
|
|
4632
4705
|
- Title: ${assignment.subtaskTitle}
|
|
4633
4706
|
- Description: ${assignment.description}
|
|
4634
4707
|
- Expected output: ${assignment.expectedOutput}
|
|
4635
|
-
- Constraints: ${assignment.constraints.join("; ")}
|
|
4708
|
+
- Constraints: ${assignment.constraints.join("; ")}${assignment.files?.length ? `
|
|
4709
|
+
- Files you own (create/edit ONLY these): ${assignment.files.join(", ")}` : ""}${assignment.acceptance?.length ? `
|
|
4710
|
+
- Definition of done: ${assignment.acceptance.join("; ")}` : ""}`;
|
|
4636
4711
|
}
|
|
4637
4712
|
buildInitialPrompt(assignment) {
|
|
4638
4713
|
return `Execute the following subtask completely:
|
|
4639
4714
|
|
|
4640
4715
|
**${assignment.subtaskTitle}**
|
|
4641
|
-
|
|
4716
|
+
${assignment.contextBrief ? `
|
|
4717
|
+
Context: ${assignment.contextBrief}
|
|
4718
|
+
` : ""}
|
|
4642
4719
|
${assignment.description}
|
|
4643
4720
|
|
|
4644
4721
|
Expected output: ${assignment.expectedOutput}
|
|
4645
|
-
|
|
4722
|
+
${assignment.files?.length ? `
|
|
4723
|
+
Files you own (create or edit exactly these paths):
|
|
4724
|
+
${assignment.files.map((f) => `- ${f}`).join("\n")}
|
|
4725
|
+
` : ""}${assignment.acceptance?.length ? `
|
|
4726
|
+
Definition of done (your output must satisfy ALL of these):
|
|
4727
|
+
${assignment.acceptance.map((a) => `- ${a}`).join("\n")}
|
|
4728
|
+
` : ""}
|
|
4646
4729
|
Constraints:
|
|
4647
4730
|
${assignment.constraints.map((c) => `- ${c}`).join("\n")}
|
|
4648
4731
|
|
|
@@ -5123,6 +5206,8 @@ var T2Manager = class extends BaseTier {
|
|
|
5123
5206
|
this.assignment = assignment;
|
|
5124
5207
|
this.taskId = taskId;
|
|
5125
5208
|
this.setLabel(assignment.sectionTitle);
|
|
5209
|
+
const m = this.router.getModelForTier("T2");
|
|
5210
|
+
if (m) this.setServingModel(`${m.provider}:${m.id}`);
|
|
5126
5211
|
this.setStatus("ACTIVE");
|
|
5127
5212
|
this.sendStatusUpdate({
|
|
5128
5213
|
progressPct: 0,
|
|
@@ -5209,7 +5294,7 @@ Guidance (must be followed): ${decision.note}`
|
|
|
5209
5294
|
// ── Private ──────────────────────────────────
|
|
5210
5295
|
async decomposeSection(assignment) {
|
|
5211
5296
|
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
|
|
5297
|
+
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
5298
|
|
|
5214
5299
|
Section: ${assignment.sectionTitle}
|
|
5215
5300
|
Description: ${assignment.description}
|
|
@@ -5228,6 +5313,9 @@ Return a JSON array of subtask objects, each with:
|
|
|
5228
5313
|
- peerT3Ids: string[] (empty for now)
|
|
5229
5314
|
- dependsOn: string[] (array of subtaskIds this task depends on to start)
|
|
5230
5315
|
- executionMode: "parallel|sequential" (default is parallel)
|
|
5316
|
+
- files: string[] (the EXACT relative paths this subtask creates or edits)
|
|
5317
|
+
- acceptance: string[] (1-3 mechanically checkable done-criteria: file exists / contains X / command exits 0)
|
|
5318
|
+
- contextBrief: string (1-3 short sentences with ALL the background the worker needs \u2014 it sees nothing else)
|
|
5231
5319
|
|
|
5232
5320
|
Return ONLY the JSON array.`;
|
|
5233
5321
|
const messages = [{ role: "user", content: prompt }];
|
|
@@ -5825,6 +5913,8 @@ var T1Administrator = class extends BaseTier {
|
|
|
5825
5913
|
this.signal = signal;
|
|
5826
5914
|
this.taskId = crypto3.randomUUID();
|
|
5827
5915
|
this.setLabel("Administrator");
|
|
5916
|
+
const m = this.router.getModelForTier("T1");
|
|
5917
|
+
if (m) this.setServingModel(`${m.provider}:${m.id}`);
|
|
5828
5918
|
this.setStatus("ACTIVE");
|
|
5829
5919
|
this.taskGoal = userPrompt;
|
|
5830
5920
|
this.sendStatusUpdate({
|
|
@@ -6071,10 +6161,13 @@ Return JSON where SECTIONS can declare dependencies on other SECTIONS:
|
|
|
6071
6161
|
"description": "Run npm init",
|
|
6072
6162
|
"expectedOutput": "package.json created",
|
|
6073
6163
|
"constraints": [],
|
|
6074
|
-
"dependsOn": []
|
|
6164
|
+
"dependsOn": [],
|
|
6165
|
+
"files": ["package.json"], // \u2190 exact paths this subtask owns
|
|
6166
|
+
"acceptance": ["package.json exists and parses as JSON"], // \u2190 objectively checkable
|
|
6167
|
+
"contextBrief": "Fresh Node 20 project; npm available." // \u2190 ALL the background the worker gets
|
|
6075
6168
|
}]
|
|
6076
6169
|
}, {
|
|
6077
|
-
"sectionId": "s2",
|
|
6170
|
+
"sectionId": "s2",
|
|
6078
6171
|
"sectionTitle": "Write Tests",
|
|
6079
6172
|
"description": "Write tests for the project",
|
|
6080
6173
|
"expectedOutput": "Tests passing",
|
|
@@ -6084,7 +6177,13 @@ Return JSON where SECTIONS can declare dependencies on other SECTIONS:
|
|
|
6084
6177
|
}]
|
|
6085
6178
|
}
|
|
6086
6179
|
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
|
|
6180
|
+
Leave dependsOn empty for sections that can run immediately in parallel.
|
|
6181
|
+
|
|
6182
|
+
SPEC RULES \u2014 each subtask is a self-contained spec slice (workers execute from their slice ALONE):
|
|
6183
|
+
- "files": the exact relative paths the subtask creates or edits. Never vague ("some files"); always concrete.
|
|
6184
|
+
- "acceptance": 1-3 checks a reviewer could verify mechanically (file exists / contains X / command exits 0). These define done.
|
|
6185
|
+
- "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.
|
|
6186
|
+
- 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
6187
|
const messages = [{ role: "user", content: decompositionPrompt }];
|
|
6089
6188
|
const result = await this.router.generate("T1", {
|
|
6090
6189
|
messages,
|
|
@@ -7386,30 +7485,56 @@ async function searchTavily(query, apiKey, maxResults) {
|
|
|
7386
7485
|
engine: "tavily"
|
|
7387
7486
|
}));
|
|
7388
7487
|
}
|
|
7389
|
-
|
|
7390
|
-
|
|
7391
|
-
|
|
7392
|
-
|
|
7393
|
-
|
|
7394
|
-
|
|
7395
|
-
|
|
7396
|
-
|
|
7397
|
-
|
|
7398
|
-
|
|
7399
|
-
|
|
7488
|
+
var BROWSER_UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0 Safari/537.36";
|
|
7489
|
+
function unwrapDdgRedirect(href) {
|
|
7490
|
+
try {
|
|
7491
|
+
const url = new URL(href.startsWith("//") ? `https:${href}` : href, "https://duckduckgo.com");
|
|
7492
|
+
if (/(^|\.)duckduckgo\.com$/i.test(url.hostname) && url.pathname.startsWith("/l/")) {
|
|
7493
|
+
const target = url.searchParams.get("uddg");
|
|
7494
|
+
if (target) return decodeURIComponent(target);
|
|
7495
|
+
}
|
|
7496
|
+
return url.protocol === "http:" || url.protocol === "https:" ? url.toString() : href;
|
|
7497
|
+
} catch {
|
|
7498
|
+
return href;
|
|
7499
|
+
}
|
|
7500
|
+
}
|
|
7501
|
+
function stripTags(html) {
|
|
7502
|
+
return html.replace(/<[^>]+>/g, "").replace(/\s+/g, " ").trim();
|
|
7503
|
+
}
|
|
7504
|
+
function decodeEntities(text) {
|
|
7505
|
+
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'|'/g, "'").replace(/ /g, " ");
|
|
7506
|
+
}
|
|
7507
|
+
function parseDdgAnchors(html, anchorClass, snippetClass) {
|
|
7508
|
+
const anchorRe = new RegExp(`<a\\b[^>]*class=["']?[^"'>]*\\b${anchorClass}\\b[^"'>]*["']?[^>]*>([\\s\\S]*?)<\\/a>`, "gi");
|
|
7509
|
+
const snippetRe = new RegExp(`class=["']?[^"'>]*\\b${snippetClass}\\b[^"'>]*["']?[^>]*>([\\s\\S]*?)<\\/(?:td|a|div|span)>`, "gi");
|
|
7510
|
+
const results = [];
|
|
7400
7511
|
let m;
|
|
7401
|
-
while ((m =
|
|
7402
|
-
|
|
7512
|
+
while ((m = anchorRe.exec(html)) !== null) {
|
|
7513
|
+
const tag = m[0];
|
|
7514
|
+
const href = /href=["']([^"']+)["']/i.exec(tag)?.[1];
|
|
7515
|
+
const title = decodeEntities(stripTags(m[1] ?? ""));
|
|
7516
|
+
if (!href || !title) continue;
|
|
7517
|
+
results.push({ title, url: unwrapDdgRedirect(decodeEntities(href)), snippet: "" });
|
|
7518
|
+
}
|
|
7519
|
+
const snippets = [];
|
|
7520
|
+
while ((m = snippetRe.exec(html)) !== null) {
|
|
7521
|
+
snippets.push(decodeEntities(stripTags(m[1] ?? "")));
|
|
7403
7522
|
}
|
|
7404
|
-
|
|
7405
|
-
snippets
|
|
7523
|
+
for (let i = 0; i < results.length; i++) {
|
|
7524
|
+
if (snippets[i]) results[i].snippet = snippets[i];
|
|
7406
7525
|
}
|
|
7407
|
-
return
|
|
7408
|
-
|
|
7409
|
-
|
|
7410
|
-
|
|
7411
|
-
|
|
7412
|
-
|
|
7526
|
+
return results;
|
|
7527
|
+
}
|
|
7528
|
+
async function searchDuckDuckGo(query, maxResults, variant) {
|
|
7529
|
+
const base = variant === "html" ? "https://html.duckduckgo.com/html/?q=" : "https://lite.duckduckgo.com/lite/?q=";
|
|
7530
|
+
const resp = await fetch(`${base}${encodeURIComponent(query)}`, {
|
|
7531
|
+
headers: { "User-Agent": BROWSER_UA, Accept: "text/html" },
|
|
7532
|
+
signal: AbortSignal.timeout(1e4)
|
|
7533
|
+
});
|
|
7534
|
+
if (!resp.ok) throw new Error(`DuckDuckGo ${variant} returned HTTP ${resp.status}`);
|
|
7535
|
+
const html = await resp.text();
|
|
7536
|
+
const parsed = variant === "html" ? parseDdgAnchors(html, "result__a", "result__snippet") : parseDdgAnchors(html, "result-link", "result-snippet");
|
|
7537
|
+
return parsed.slice(0, maxResults).map((r) => ({ ...r, engine: `duckduckgo-${variant}` }));
|
|
7413
7538
|
}
|
|
7414
7539
|
var WebSearchTool = class extends BaseTool {
|
|
7415
7540
|
name = "web_search";
|
|
@@ -7468,12 +7593,14 @@ var WebSearchTool = class extends BaseTool {
|
|
|
7468
7593
|
errors.push(`Tavily: ${err instanceof Error ? err.message : String(err)}`);
|
|
7469
7594
|
}
|
|
7470
7595
|
}
|
|
7471
|
-
|
|
7472
|
-
|
|
7473
|
-
|
|
7474
|
-
|
|
7475
|
-
|
|
7476
|
-
|
|
7596
|
+
for (const variant of ["html", "lite"]) {
|
|
7597
|
+
try {
|
|
7598
|
+
results = await searchDuckDuckGo(query, maxResults, variant);
|
|
7599
|
+
if (results.length > 0) return this.formatResults(query, results);
|
|
7600
|
+
errors.push(`DuckDuckGo ${variant}: returned 0 results`);
|
|
7601
|
+
} catch (err) {
|
|
7602
|
+
errors.push(`DuckDuckGo ${variant}: ${err instanceof Error ? err.message : String(err)}`);
|
|
7603
|
+
}
|
|
7477
7604
|
}
|
|
7478
7605
|
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
7606
|
return [
|
|
@@ -8417,8 +8544,11 @@ var CascadeConfigSchema = zod.z.object({
|
|
|
8417
8544
|
* Cascade Auto: when true, the TaskAnalyzer selects the optimal model for each
|
|
8418
8545
|
* tier based on task type and complexity, overriding the static priority lists.
|
|
8419
8546
|
* Heuristic-first with AI inference fallback (adds ~0–500ms per task).
|
|
8547
|
+
* ON by default since v0.19.0 — "Auto" without it was just a static priority
|
|
8548
|
+
* list, not the benchmark-value routing the docs describe. Explicit per-tier
|
|
8549
|
+
* model pins are unaffected; disable via config/Settings → Advanced.
|
|
8420
8550
|
*/
|
|
8421
|
-
cascadeAuto: zod.z.boolean().default(
|
|
8551
|
+
cascadeAuto: zod.z.boolean().default(true),
|
|
8422
8552
|
/**
|
|
8423
8553
|
* Cascade Auto trade-off bias when picking a model for a task:
|
|
8424
8554
|
* - 'balanced' (default): quality × cost-efficiency — cheap models win
|
|
@@ -10131,13 +10261,30 @@ ${last.partialOutput}` : "");
|
|
|
10131
10261
|
* explicit multi-part structure, so ordinary single-file asks (handled as
|
|
10132
10262
|
* Simple/Moderate) don't get over-escalated.
|
|
10133
10263
|
*/
|
|
10134
|
-
|
|
10264
|
+
/** Shared build/scale signals for the complexity floors below. */
|
|
10265
|
+
buildSignals(prompt) {
|
|
10135
10266
|
const p = prompt.trim();
|
|
10136
|
-
if (p.length < 24) return false;
|
|
10267
|
+
if (p.length < 24) return { buildVerb: false, scaleCount: 0, multiPart: false };
|
|
10137
10268
|
const buildVerb = /\b(?:build|implement|create|develop|design|scaffold|refactor|migrate|architect|set up|integrate)\b/i.test(p);
|
|
10138
|
-
const
|
|
10269
|
+
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
10270
|
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
|
|
10271
|
+
return { buildVerb, scaleCount, multiPart };
|
|
10272
|
+
}
|
|
10273
|
+
/**
|
|
10274
|
+
* A build prompt with REAL scale: multiple system-level deliverables, or a
|
|
10275
|
+
* deliverable plus explicitly multi-part phrasing. Only these floor to the
|
|
10276
|
+
* full T1→T2→T3 hierarchy — "create a todo app" is a build prompt too, but
|
|
10277
|
+
* flooring every small build to Complex was the #1 token bomb (3-5 managers
|
|
10278
|
+
* × workers for a task one worker handles).
|
|
10279
|
+
*/
|
|
10280
|
+
looksClearlyComplex(prompt) {
|
|
10281
|
+
const s = this.buildSignals(prompt);
|
|
10282
|
+
return s.buildVerb && (s.scaleCount >= 2 || s.scaleCount >= 1 && s.multiPart);
|
|
10283
|
+
}
|
|
10284
|
+
/** A small single-deliverable build — real work, but one manager's worth. */
|
|
10285
|
+
looksLikeModerateBuild(prompt) {
|
|
10286
|
+
const s = this.buildSignals(prompt);
|
|
10287
|
+
return s.buildVerb && (s.scaleCount >= 1 || s.multiPart);
|
|
10141
10288
|
}
|
|
10142
10289
|
// Cache glob scan results per workspace path to avoid repeated I/O.
|
|
10143
10290
|
static globCache = /* @__PURE__ */ new Map();
|
|
@@ -10225,6 +10372,9 @@ ${prompt}` : prompt;
|
|
|
10225
10372
|
if (verdict !== "Complex" && this.looksClearlyComplex(prompt)) {
|
|
10226
10373
|
this.recordDecision("complexity", `Complex \u2014 heuristic floor over classifier "${verdict}": explicit multi-step build/implementation signals (T1 engaged)`);
|
|
10227
10374
|
verdict = "Complex";
|
|
10375
|
+
} else if (verdict === "Simple" && this.looksLikeModerateBuild(prompt)) {
|
|
10376
|
+
this.recordDecision("complexity", 'Moderate \u2014 heuristic floor over classifier "Simple": build signals without multi-system scale (single manager)');
|
|
10377
|
+
verdict = "Moderate";
|
|
10228
10378
|
} else {
|
|
10229
10379
|
this.recordDecision("complexity", `${verdict} \u2014 classifier: ${reason || "no reason given"}`);
|
|
10230
10380
|
}
|