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/index.d.cts CHANGED
@@ -188,6 +188,13 @@ interface T3SubtaskSpec {
188
188
  peerT3Ids: string[];
189
189
  dependsOn?: string[];
190
190
  executionMode?: 'parallel' | 'sequential';
191
+ /** Spec slice: the exact file paths this subtask creates or edits. */
192
+ files?: string[];
193
+ /** Objectively verifiable acceptance checks for the subtask's output. */
194
+ acceptance?: string[];
195
+ /** The ONLY background the worker needs — workers see nothing else, so the
196
+ * planner must make it self-sufficient (keeps worker context minimal). */
197
+ contextBrief?: string;
191
198
  }
192
199
  interface T2ToT3Assignment {
193
200
  subtaskId: string;
@@ -200,6 +207,10 @@ interface T2ToT3Assignment {
200
207
  sectionTitle?: string;
201
208
  dependsOn?: string[];
202
209
  executionMode?: 'parallel' | 'sequential';
210
+ /** Spec slice fields — see T3SubtaskSpec. */
211
+ files?: string[];
212
+ acceptance?: string[];
213
+ contextBrief?: string;
203
214
  }
204
215
  interface StatusUpdate {
205
216
  progressPct: number;
@@ -1557,11 +1568,19 @@ declare abstract class BaseTier extends EventEmitter {
1557
1568
  * which would interleave, are not tagged.
1558
1569
  */
1559
1570
  protected isPresenter: boolean;
1571
+ /**
1572
+ * The model actually serving this tier (`provider:id`), once resolved —
1573
+ * rides on every tier:status event so the desktop can show which model ran
1574
+ * which node (Cockpit node panel / Why panel).
1575
+ */
1576
+ protected servingModel?: string;
1560
1577
  constructor(role: TierRole, id?: string, parentId?: string);
1561
1578
  /** Mark this tier as the run's presenter (root tier). */
1562
1579
  setPresenter(on?: boolean): void;
1563
1580
  getStatus(): TierStatus;
1564
1581
  protected setStatus(status: TierStatus, output?: string): void;
1582
+ /** Record the model serving this tier; future status events carry it. */
1583
+ protected setServingModel(model: string | undefined): void;
1565
1584
  protected setLabel(label: string): void;
1566
1585
  setSystemPromptOverride(prompt: string): void;
1567
1586
  setHierarchyContext(context: string): void;
@@ -1908,7 +1927,18 @@ declare class Cascade extends EventEmitter {
1908
1927
  * explicit multi-part structure, so ordinary single-file asks (handled as
1909
1928
  * Simple/Moderate) don't get over-escalated.
1910
1929
  */
1930
+ /** Shared build/scale signals for the complexity floors below. */
1931
+ private buildSignals;
1932
+ /**
1933
+ * A build prompt with REAL scale: multiple system-level deliverables, or a
1934
+ * deliverable plus explicitly multi-part phrasing. Only these floor to the
1935
+ * full T1→T2→T3 hierarchy — "create a todo app" is a build prompt too, but
1936
+ * flooring every small build to Complex was the #1 token bomb (3-5 managers
1937
+ * × workers for a task one worker handles).
1938
+ */
1911
1939
  private looksClearlyComplex;
1940
+ /** A small single-deliverable build — real work, but one manager's worth. */
1941
+ private looksLikeModerateBuild;
1912
1942
  private static globCache;
1913
1943
  private countWorkspaceFiles;
1914
1944
  private determineComplexity;
package/dist/index.d.ts CHANGED
@@ -188,6 +188,13 @@ interface T3SubtaskSpec {
188
188
  peerT3Ids: string[];
189
189
  dependsOn?: string[];
190
190
  executionMode?: 'parallel' | 'sequential';
191
+ /** Spec slice: the exact file paths this subtask creates or edits. */
192
+ files?: string[];
193
+ /** Objectively verifiable acceptance checks for the subtask's output. */
194
+ acceptance?: string[];
195
+ /** The ONLY background the worker needs — workers see nothing else, so the
196
+ * planner must make it self-sufficient (keeps worker context minimal). */
197
+ contextBrief?: string;
191
198
  }
192
199
  interface T2ToT3Assignment {
193
200
  subtaskId: string;
@@ -200,6 +207,10 @@ interface T2ToT3Assignment {
200
207
  sectionTitle?: string;
201
208
  dependsOn?: string[];
202
209
  executionMode?: 'parallel' | 'sequential';
210
+ /** Spec slice fields — see T3SubtaskSpec. */
211
+ files?: string[];
212
+ acceptance?: string[];
213
+ contextBrief?: string;
203
214
  }
204
215
  interface StatusUpdate {
205
216
  progressPct: number;
@@ -1557,11 +1568,19 @@ declare abstract class BaseTier extends EventEmitter {
1557
1568
  * which would interleave, are not tagged.
1558
1569
  */
1559
1570
  protected isPresenter: boolean;
1571
+ /**
1572
+ * The model actually serving this tier (`provider:id`), once resolved —
1573
+ * rides on every tier:status event so the desktop can show which model ran
1574
+ * which node (Cockpit node panel / Why panel).
1575
+ */
1576
+ protected servingModel?: string;
1560
1577
  constructor(role: TierRole, id?: string, parentId?: string);
1561
1578
  /** Mark this tier as the run's presenter (root tier). */
1562
1579
  setPresenter(on?: boolean): void;
1563
1580
  getStatus(): TierStatus;
1564
1581
  protected setStatus(status: TierStatus, output?: string): void;
1582
+ /** Record the model serving this tier; future status events carry it. */
1583
+ protected setServingModel(model: string | undefined): void;
1565
1584
  protected setLabel(label: string): void;
1566
1585
  setSystemPromptOverride(prompt: string): void;
1567
1586
  setHierarchyContext(context: string): void;
@@ -1908,7 +1927,18 @@ declare class Cascade extends EventEmitter {
1908
1927
  * explicit multi-part structure, so ordinary single-file asks (handled as
1909
1928
  * Simple/Moderate) don't get over-escalated.
1910
1929
  */
1930
+ /** Shared build/scale signals for the complexity floors below. */
1931
+ private buildSignals;
1932
+ /**
1933
+ * A build prompt with REAL scale: multiple system-level deliverables, or a
1934
+ * deliverable plus explicitly multi-part phrasing. Only these floor to the
1935
+ * full T1→T2→T3 hierarchy — "create a todo app" is a build prompt too, but
1936
+ * flooring every small build to Complex was the #1 token bomb (3-5 managers
1937
+ * × workers for a task one worker handles).
1938
+ */
1911
1939
  private looksClearlyComplex;
1940
+ /** A small single-deliverable build — real work, but one manager's worth. */
1941
+ private looksLikeModerateBuild;
1912
1942
  private static globCache;
1913
1943
  private countWorkspaceFiles;
1914
1944
  private determineComplexity;
package/dist/index.js CHANGED
@@ -190,7 +190,7 @@ var init_audit_logger = __esm({
190
190
  });
191
191
 
192
192
  // src/constants.ts
193
- var CASCADE_VERSION = "0.18.0";
193
+ var CASCADE_VERSION = "0.19.0";
194
194
  var CASCADE_CONFIG_DIR = ".cascade";
195
195
  var CASCADE_MD_FILE = "CASCADE.md";
196
196
  var CASCADE_IGNORE_FILE = ".cascadeignore";
@@ -993,6 +993,23 @@ var OpenAIProvider = class extends BaseProvider {
993
993
  };
994
994
 
995
995
  // src/providers/azure.ts
996
+ function azureModelForDeployment(cfg) {
997
+ if (cfg.type !== "azure" || !cfg.deploymentName?.trim()) return null;
998
+ const id = cfg.deploymentName.trim();
999
+ return {
1000
+ id,
1001
+ name: cfg.label?.trim() || id,
1002
+ provider: "azure",
1003
+ contextWindow: 128e3,
1004
+ isVisionCapable: false,
1005
+ inputCostPer1kTokens: 25e-4,
1006
+ outputCostPer1kTokens: 0.01,
1007
+ maxOutputTokens: 16e3,
1008
+ supportsStreaming: true,
1009
+ isLocal: false,
1010
+ supportsToolUse: true
1011
+ };
1012
+ }
996
1013
  var AzureOpenAIProvider = class extends OpenAIProvider {
997
1014
  constructor(config, model) {
998
1015
  const rawUrl = config.baseUrl ?? AZURE_BASE_URL_TEMPLATE.replace("{resource}", "YOUR_RESOURCE");
@@ -1013,7 +1030,8 @@ var AzureOpenAIProvider = class extends OpenAIProvider {
1013
1030
  });
1014
1031
  }
1015
1032
  async listModels() {
1016
- return [this.model];
1033
+ const fromDeployment = azureModelForDeployment(this.config);
1034
+ return [fromDeployment ?? this.model];
1017
1035
  }
1018
1036
  async isAvailable() {
1019
1037
  try {
@@ -2522,6 +2540,12 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter {
2522
2540
  const results = await Promise.all(ocConfigs.map((cfg) => this.discoverOpenAICompatibleModels(cfg)));
2523
2541
  if (results.some(Boolean)) this.selector.markProviderAvailable("openai-compatible");
2524
2542
  }
2543
+ if (availableProviders.has("azure")) {
2544
+ for (const cfg of config.providers) {
2545
+ const model = azureModelForDeployment(cfg);
2546
+ if (model) this.selector.addDynamicModel(model);
2547
+ }
2548
+ }
2525
2549
  for (const tier of ["T1", "T2", "T3"]) {
2526
2550
  const override = tier === "T1" ? config.models.t1 : tier === "T2" ? config.models.t2 : config.models.t3;
2527
2551
  if (!override || override === "auto") continue;
@@ -3128,7 +3152,7 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter {
3128
3152
  ensureProvider(model, configs) {
3129
3153
  const key = `${model.provider}:${model.id}`;
3130
3154
  if (this.providers.has(key)) return;
3131
- const cfg = configs.find((c) => c.type === model.provider) ?? { type: model.provider };
3155
+ 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 };
3132
3156
  const provider = this.createProvider(cfg, model);
3133
3157
  this.providers.set(key, provider);
3134
3158
  }
@@ -3288,6 +3312,12 @@ var BaseTier = class extends EventEmitter {
3288
3312
  * which would interleave, are not tagged.
3289
3313
  */
3290
3314
  isPresenter = false;
3315
+ /**
3316
+ * The model actually serving this tier (`provider:id`), once resolved —
3317
+ * rides on every tier:status event so the desktop can show which model ran
3318
+ * which node (Cockpit node panel / Why panel).
3319
+ */
3320
+ servingModel;
3291
3321
  constructor(role, id, parentId) {
3292
3322
  super();
3293
3323
  this.role = role;
@@ -3312,11 +3342,16 @@ var BaseTier = class extends EventEmitter {
3312
3342
  label: this.label,
3313
3343
  status,
3314
3344
  timestamp,
3315
- output
3345
+ output,
3346
+ model: this.servingModel
3316
3347
  };
3317
3348
  this.emit("status", event);
3318
3349
  this.emit("tier:status", event);
3319
3350
  }
3351
+ /** Record the model serving this tier; future status events carry it. */
3352
+ setServingModel(model) {
3353
+ this.servingModel = model || void 0;
3354
+ }
3320
3355
  setLabel(label) {
3321
3356
  this.label = label;
3322
3357
  }
@@ -3339,7 +3374,8 @@ var BaseTier = class extends EventEmitter {
3339
3374
  currentAction: update.currentAction,
3340
3375
  progressPct: update.progressPct,
3341
3376
  timestamp,
3342
- output: update.output
3377
+ output: update.output,
3378
+ model: this.servingModel
3343
3379
  });
3344
3380
  }
3345
3381
  buildMessage(type, to, payload) {
@@ -3705,6 +3741,21 @@ Available tools: ${tools.map((t) => t.name).join(", ")}.
3705
3741
  When you have enough information, stop calling tools and write your final answer.`;
3706
3742
  }
3707
3743
 
3744
+ // src/utils/truncate.ts
3745
+ function truncateForContext(text, maxChars = 12e3) {
3746
+ if (text.length <= maxChars) return text;
3747
+ const headLen = Math.floor(maxChars * 0.75);
3748
+ const tailLen = maxChars - headLen;
3749
+ const head = text.slice(0, headLen);
3750
+ const tail = text.slice(-tailLen);
3751
+ const elided = text.length - headLen - tailLen;
3752
+ return `${head}
3753
+
3754
+ [... ${elided.toLocaleString()} characters elided to keep context small \u2014 re-read the file with a line range if you need the middle ...]
3755
+
3756
+ ${tail}`;
3757
+ }
3758
+
3708
3759
  // src/core/tiers/t3-worker.ts
3709
3760
  var CriticalToolError = class extends Error {
3710
3761
  constructor(message, toolName) {
@@ -4012,6 +4063,7 @@ Now execute your subtask using this context where relevant.`
4012
4063
  } catch {
4013
4064
  }
4014
4065
  const effectiveModel = subtaskModel ?? this.router.getModelForTier("T3");
4066
+ if (effectiveModel) this.setServingModel(`${effectiveModel.provider}:${effectiveModel.id}`);
4015
4067
  const useTextTools = effectiveModel?.supportsToolUse === false && tools.length > 0;
4016
4068
  let sentFullTextContract = false;
4017
4069
  let textContractSignature = "";
@@ -4109,7 +4161,7 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : "") + textToolSuffix,
4109
4161
  const toolResult = await this.executeTool(tc);
4110
4162
  await this.context.addMessage({
4111
4163
  role: "tool",
4112
- content: toolResult,
4164
+ content: truncateForContext(toolResult),
4113
4165
  toolCallId: tc.id
4114
4166
  });
4115
4167
  }
@@ -4366,15 +4418,17 @@ ${assignment.expectedOutput}`;
4366
4418
  };
4367
4419
  }
4368
4420
  requiresArtifact() {
4421
+ if (this.assignment?.files?.length) return true;
4369
4422
  const haystack = `${this.assignment?.description ?? ""}
4370
4423
  ${this.assignment?.expectedOutput ?? ""}`;
4371
4424
  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);
4372
4425
  }
4373
4426
  extractArtifactPaths(assignment) {
4427
+ const declared = (assignment.files ?? []).map((f) => f.trim()).filter((f) => f.includes("."));
4374
4428
  const haystack = `${assignment.description}
4375
4429
  ${assignment.expectedOutput}`;
4376
4430
  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) ?? [];
4377
- return [...new Set(matches.map((m) => m.trim()))];
4431
+ return [.../* @__PURE__ */ new Set([...declared, ...matches.map((m) => m.trim())])];
4378
4432
  }
4379
4433
  async verifyArtifacts(assignment) {
4380
4434
  const artifactPaths = this.extractArtifactPaths(assignment);
@@ -4497,7 +4551,9 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
4497
4551
  Assignment: ${assignment.description}
4498
4552
  Expected output: ${assignment.expectedOutput}
4499
4553
  Constraints: ${assignment.constraints.join("; ")}
4500
-
4554
+ ${assignment.acceptance?.length ? `Acceptance criteria \u2014 ALL must be satisfied for "completeness" to pass:
4555
+ ${assignment.acceptance.map((a) => `- ${a}`).join("\n")}
4556
+ ` : ""}
4501
4557
  Output to test:
4502
4558
  ${output}
4503
4559
 
@@ -4586,17 +4642,27 @@ Your subtask:
4586
4642
  - Title: ${assignment.subtaskTitle}
4587
4643
  - Description: ${assignment.description}
4588
4644
  - Expected output: ${assignment.expectedOutput}
4589
- - Constraints: ${assignment.constraints.join("; ")}`;
4645
+ - Constraints: ${assignment.constraints.join("; ")}${assignment.files?.length ? `
4646
+ - Files you own (create/edit ONLY these): ${assignment.files.join(", ")}` : ""}${assignment.acceptance?.length ? `
4647
+ - Definition of done: ${assignment.acceptance.join("; ")}` : ""}`;
4590
4648
  }
4591
4649
  buildInitialPrompt(assignment) {
4592
4650
  return `Execute the following subtask completely:
4593
4651
 
4594
4652
  **${assignment.subtaskTitle}**
4595
-
4653
+ ${assignment.contextBrief ? `
4654
+ Context: ${assignment.contextBrief}
4655
+ ` : ""}
4596
4656
  ${assignment.description}
4597
4657
 
4598
4658
  Expected output: ${assignment.expectedOutput}
4599
-
4659
+ ${assignment.files?.length ? `
4660
+ Files you own (create or edit exactly these paths):
4661
+ ${assignment.files.map((f) => `- ${f}`).join("\n")}
4662
+ ` : ""}${assignment.acceptance?.length ? `
4663
+ Definition of done (your output must satisfy ALL of these):
4664
+ ${assignment.acceptance.map((a) => `- ${a}`).join("\n")}
4665
+ ` : ""}
4600
4666
  Constraints:
4601
4667
  ${assignment.constraints.map((c) => `- ${c}`).join("\n")}
4602
4668
 
@@ -5077,6 +5143,8 @@ var T2Manager = class extends BaseTier {
5077
5143
  this.assignment = assignment;
5078
5144
  this.taskId = taskId;
5079
5145
  this.setLabel(assignment.sectionTitle);
5146
+ const m = this.router.getModelForTier("T2");
5147
+ if (m) this.setServingModel(`${m.provider}:${m.id}`);
5080
5148
  this.setStatus("ACTIVE");
5081
5149
  this.sendStatusUpdate({
5082
5150
  progressPct: 0,
@@ -5163,7 +5231,7 @@ Guidance (must be followed): ${decision.note}`
5163
5231
  // ── Private ──────────────────────────────────
5164
5232
  async decomposeSection(assignment) {
5165
5233
  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");
5166
- const prompt = `Decompose this section into 2-5 concrete subtasks for T3 workers.
5234
+ 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).
5167
5235
 
5168
5236
  Section: ${assignment.sectionTitle}
5169
5237
  Description: ${assignment.description}
@@ -5182,6 +5250,9 @@ Return a JSON array of subtask objects, each with:
5182
5250
  - peerT3Ids: string[] (empty for now)
5183
5251
  - dependsOn: string[] (array of subtaskIds this task depends on to start)
5184
5252
  - executionMode: "parallel|sequential" (default is parallel)
5253
+ - files: string[] (the EXACT relative paths this subtask creates or edits)
5254
+ - acceptance: string[] (1-3 mechanically checkable done-criteria: file exists / contains X / command exits 0)
5255
+ - contextBrief: string (1-3 short sentences with ALL the background the worker needs \u2014 it sees nothing else)
5185
5256
 
5186
5257
  Return ONLY the JSON array.`;
5187
5258
  const messages = [{ role: "user", content: prompt }];
@@ -5779,6 +5850,8 @@ var T1Administrator = class extends BaseTier {
5779
5850
  this.signal = signal;
5780
5851
  this.taskId = randomUUID();
5781
5852
  this.setLabel("Administrator");
5853
+ const m = this.router.getModelForTier("T1");
5854
+ if (m) this.setServingModel(`${m.provider}:${m.id}`);
5782
5855
  this.setStatus("ACTIVE");
5783
5856
  this.taskGoal = userPrompt;
5784
5857
  this.sendStatusUpdate({
@@ -6025,10 +6098,13 @@ Return JSON where SECTIONS can declare dependencies on other SECTIONS:
6025
6098
  "description": "Run npm init",
6026
6099
  "expectedOutput": "package.json created",
6027
6100
  "constraints": [],
6028
- "dependsOn": []
6101
+ "dependsOn": [],
6102
+ "files": ["package.json"], // \u2190 exact paths this subtask owns
6103
+ "acceptance": ["package.json exists and parses as JSON"], // \u2190 objectively checkable
6104
+ "contextBrief": "Fresh Node 20 project; npm available." // \u2190 ALL the background the worker gets
6029
6105
  }]
6030
6106
  }, {
6031
- "sectionId": "s2",
6107
+ "sectionId": "s2",
6032
6108
  "sectionTitle": "Write Tests",
6033
6109
  "description": "Write tests for the project",
6034
6110
  "expectedOutput": "Tests passing",
@@ -6038,7 +6114,13 @@ Return JSON where SECTIONS can declare dependencies on other SECTIONS:
6038
6114
  }]
6039
6115
  }
6040
6116
  Use dependsOn at the SECTION level when a whole T2 Manager needs the output of a previous T2 Manager.
6041
- Leave dependsOn empty for sections that can run immediately in parallel.`;
6117
+ Leave dependsOn empty for sections that can run immediately in parallel.
6118
+
6119
+ SPEC RULES \u2014 each subtask is a self-contained spec slice (workers execute from their slice ALONE):
6120
+ - "files": the exact relative paths the subtask creates or edits. Never vague ("some files"); always concrete.
6121
+ - "acceptance": 1-3 checks a reviewer could verify mechanically (file exists / contains X / command exits 0). These define done.
6122
+ - "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.
6123
+ - 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.`;
6042
6124
  const messages = [{ role: "user", content: decompositionPrompt }];
6043
6125
  const result = await this.router.generate("T1", {
6044
6126
  messages,
@@ -7340,30 +7422,56 @@ async function searchTavily(query, apiKey, maxResults) {
7340
7422
  engine: "tavily"
7341
7423
  }));
7342
7424
  }
7343
- async function searchDuckDuckGoLite(query, maxResults) {
7344
- const resp = await fetch(`https://lite.duckduckgo.com/lite/?q=${encodeURIComponent(query)}`, {
7345
- headers: { "User-Agent": "Mozilla/5.0 (compatible; Cascade-AI/1.0)" },
7346
- signal: AbortSignal.timeout(1e4)
7347
- });
7348
- if (!resp.ok) throw new Error(`DuckDuckGo Lite returned HTTP ${resp.status}`);
7349
- const html = await resp.text();
7350
- const linkPattern = /<a[^>]+class="result-link"[^>]+href="([^"]+)"[^>]*>([^<]+)<\/a>/g;
7351
- const snippetPattern = /<td[^>]+class="result-snippet"[^>]*>([\s\S]*?)<\/td>/g;
7352
- const links = [];
7353
- const snippets = [];
7425
+ var BROWSER_UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0 Safari/537.36";
7426
+ function unwrapDdgRedirect(href) {
7427
+ try {
7428
+ const url = new URL(href.startsWith("//") ? `https:${href}` : href, "https://duckduckgo.com");
7429
+ if (/(^|\.)duckduckgo\.com$/i.test(url.hostname) && url.pathname.startsWith("/l/")) {
7430
+ const target = url.searchParams.get("uddg");
7431
+ if (target) return decodeURIComponent(target);
7432
+ }
7433
+ return url.protocol === "http:" || url.protocol === "https:" ? url.toString() : href;
7434
+ } catch {
7435
+ return href;
7436
+ }
7437
+ }
7438
+ function stripTags(html) {
7439
+ return html.replace(/<[^>]+>/g, "").replace(/\s+/g, " ").trim();
7440
+ }
7441
+ function decodeEntities(text) {
7442
+ return text.replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&#x27;|&#39;/g, "'").replace(/&nbsp;/g, " ");
7443
+ }
7444
+ function parseDdgAnchors(html, anchorClass, snippetClass) {
7445
+ const anchorRe = new RegExp(`<a\\b[^>]*class=["']?[^"'>]*\\b${anchorClass}\\b[^"'>]*["']?[^>]*>([\\s\\S]*?)<\\/a>`, "gi");
7446
+ const snippetRe = new RegExp(`class=["']?[^"'>]*\\b${snippetClass}\\b[^"'>]*["']?[^>]*>([\\s\\S]*?)<\\/(?:td|a|div|span)>`, "gi");
7447
+ const results = [];
7354
7448
  let m;
7355
- while ((m = linkPattern.exec(html)) !== null) {
7356
- links.push({ url: m[1], title: m[2].trim() });
7449
+ while ((m = anchorRe.exec(html)) !== null) {
7450
+ const tag = m[0];
7451
+ const href = /href=["']([^"']+)["']/i.exec(tag)?.[1];
7452
+ const title = decodeEntities(stripTags(m[1] ?? ""));
7453
+ if (!href || !title) continue;
7454
+ results.push({ title, url: unwrapDdgRedirect(decodeEntities(href)), snippet: "" });
7455
+ }
7456
+ const snippets = [];
7457
+ while ((m = snippetRe.exec(html)) !== null) {
7458
+ snippets.push(decodeEntities(stripTags(m[1] ?? "")));
7357
7459
  }
7358
- while ((m = snippetPattern.exec(html)) !== null) {
7359
- snippets.push(m[1].replace(/<[^>]+>/g, "").trim());
7460
+ for (let i = 0; i < results.length; i++) {
7461
+ if (snippets[i]) results[i].snippet = snippets[i];
7360
7462
  }
7361
- return links.slice(0, maxResults).map((link, i) => ({
7362
- title: link.title,
7363
- url: link.url,
7364
- snippet: snippets[i] ?? "",
7365
- engine: "duckduckgo-lite"
7366
- }));
7463
+ return results;
7464
+ }
7465
+ async function searchDuckDuckGo(query, maxResults, variant) {
7466
+ const base = variant === "html" ? "https://html.duckduckgo.com/html/?q=" : "https://lite.duckduckgo.com/lite/?q=";
7467
+ const resp = await fetch(`${base}${encodeURIComponent(query)}`, {
7468
+ headers: { "User-Agent": BROWSER_UA, Accept: "text/html" },
7469
+ signal: AbortSignal.timeout(1e4)
7470
+ });
7471
+ if (!resp.ok) throw new Error(`DuckDuckGo ${variant} returned HTTP ${resp.status}`);
7472
+ const html = await resp.text();
7473
+ const parsed = variant === "html" ? parseDdgAnchors(html, "result__a", "result__snippet") : parseDdgAnchors(html, "result-link", "result-snippet");
7474
+ return parsed.slice(0, maxResults).map((r) => ({ ...r, engine: `duckduckgo-${variant}` }));
7367
7475
  }
7368
7476
  var WebSearchTool = class extends BaseTool {
7369
7477
  name = "web_search";
@@ -7422,12 +7530,14 @@ var WebSearchTool = class extends BaseTool {
7422
7530
  errors.push(`Tavily: ${err instanceof Error ? err.message : String(err)}`);
7423
7531
  }
7424
7532
  }
7425
- try {
7426
- results = await searchDuckDuckGoLite(query, maxResults);
7427
- if (results.length > 0) return this.formatResults(query, results);
7428
- errors.push("DuckDuckGo Lite: returned 0 results");
7429
- } catch (err) {
7430
- errors.push(`DuckDuckGo Lite: ${err instanceof Error ? err.message : String(err)}`);
7533
+ for (const variant of ["html", "lite"]) {
7534
+ try {
7535
+ results = await searchDuckDuckGo(query, maxResults, variant);
7536
+ if (results.length > 0) return this.formatResults(query, results);
7537
+ errors.push(`DuckDuckGo ${variant}: returned 0 results`);
7538
+ } catch (err) {
7539
+ errors.push(`DuckDuckGo ${variant}: ${err instanceof Error ? err.message : String(err)}`);
7540
+ }
7431
7541
  }
7432
7542
  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" : "";
7433
7543
  return [
@@ -8371,8 +8481,11 @@ var CascadeConfigSchema = z.object({
8371
8481
  * Cascade Auto: when true, the TaskAnalyzer selects the optimal model for each
8372
8482
  * tier based on task type and complexity, overriding the static priority lists.
8373
8483
  * Heuristic-first with AI inference fallback (adds ~0–500ms per task).
8484
+ * ON by default since v0.19.0 — "Auto" without it was just a static priority
8485
+ * list, not the benchmark-value routing the docs describe. Explicit per-tier
8486
+ * model pins are unaffected; disable via config/Settings → Advanced.
8374
8487
  */
8375
- cascadeAuto: z.boolean().default(false),
8488
+ cascadeAuto: z.boolean().default(true),
8376
8489
  /**
8377
8490
  * Cascade Auto trade-off bias when picking a model for a task:
8378
8491
  * - 'balanced' (default): quality × cost-efficiency — cheap models win
@@ -10085,13 +10198,30 @@ ${last.partialOutput}` : "");
10085
10198
  * explicit multi-part structure, so ordinary single-file asks (handled as
10086
10199
  * Simple/Moderate) don't get over-escalated.
10087
10200
  */
10088
- looksClearlyComplex(prompt) {
10201
+ /** Shared build/scale signals for the complexity floors below. */
10202
+ buildSignals(prompt) {
10089
10203
  const p = prompt.trim();
10090
- if (p.length < 24) return false;
10204
+ if (p.length < 24) return { buildVerb: false, scaleCount: 0, multiPart: false };
10091
10205
  const buildVerb = /\b(?:build|implement|create|develop|design|scaffold|refactor|migrate|architect|set up|integrate)\b/i.test(p);
10092
- const scaleNoun = /\b(?:app(?:lication)?|system|platform|service|api|backend|frontend|full[- ]?stack|website|dashboard|pipeline|microservices?|database schema|authentication|end[- ]to[- ]end|codebase|project|multiple files|several (?:files|modules|components)|test suite)\b/i.test(p);
10206
+ 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;
10093
10207
  const multiPart = /(?:\b(?:and|then|also|plus|as well as)\b.*\b(?:and|then|also)\b)|(?:^|\n)\s*(?:[-*]|\d+[.)])\s+/i.test(p);
10094
- return buildVerb && (scaleNoun || multiPart);
10208
+ return { buildVerb, scaleCount, multiPart };
10209
+ }
10210
+ /**
10211
+ * A build prompt with REAL scale: multiple system-level deliverables, or a
10212
+ * deliverable plus explicitly multi-part phrasing. Only these floor to the
10213
+ * full T1→T2→T3 hierarchy — "create a todo app" is a build prompt too, but
10214
+ * flooring every small build to Complex was the #1 token bomb (3-5 managers
10215
+ * × workers for a task one worker handles).
10216
+ */
10217
+ looksClearlyComplex(prompt) {
10218
+ const s = this.buildSignals(prompt);
10219
+ return s.buildVerb && (s.scaleCount >= 2 || s.scaleCount >= 1 && s.multiPart);
10220
+ }
10221
+ /** A small single-deliverable build — real work, but one manager's worth. */
10222
+ looksLikeModerateBuild(prompt) {
10223
+ const s = this.buildSignals(prompt);
10224
+ return s.buildVerb && (s.scaleCount >= 1 || s.multiPart);
10095
10225
  }
10096
10226
  // Cache glob scan results per workspace path to avoid repeated I/O.
10097
10227
  static globCache = /* @__PURE__ */ new Map();
@@ -10179,6 +10309,9 @@ ${prompt}` : prompt;
10179
10309
  if (verdict !== "Complex" && this.looksClearlyComplex(prompt)) {
10180
10310
  this.recordDecision("complexity", `Complex \u2014 heuristic floor over classifier "${verdict}": explicit multi-step build/implementation signals (T1 engaged)`);
10181
10311
  verdict = "Complex";
10312
+ } else if (verdict === "Simple" && this.looksLikeModerateBuild(prompt)) {
10313
+ this.recordDecision("complexity", 'Moderate \u2014 heuristic floor over classifier "Simple": build signals without multi-system scale (single manager)');
10314
+ verdict = "Moderate";
10182
10315
  } else {
10183
10316
  this.recordDecision("complexity", `${verdict} \u2014 classifier: ${reason || "no reason given"}`);
10184
10317
  }