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/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.1";
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,17 +1030,31 @@ 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() {
1037
+ const params = {
1038
+ model: this.model.id,
1039
+ messages: [{ role: "user", content: "ping" }],
1040
+ max_tokens: 1
1041
+ };
1019
1042
  try {
1020
- await this.client.chat.completions.create({
1021
- model: this.model.id,
1022
- messages: [{ role: "user", content: "ping" }],
1023
- max_tokens: 1
1024
- });
1043
+ await this.client.chat.completions.create(params);
1025
1044
  return true;
1026
- } catch {
1045
+ } catch (err) {
1046
+ if (err?.message && String(err.message).includes("max_completion_tokens")) {
1047
+ try {
1048
+ await this.client.chat.completions.create({
1049
+ model: this.model.id,
1050
+ messages: [{ role: "user", content: "ping" }],
1051
+ max_completion_tokens: 1
1052
+ });
1053
+ return true;
1054
+ } catch {
1055
+ return false;
1056
+ }
1057
+ }
1027
1058
  return false;
1028
1059
  }
1029
1060
  }
@@ -1765,6 +1796,10 @@ var ModelSelector = class {
1765
1796
  actualId = parts.slice(1).join(":");
1766
1797
  }
1767
1798
  }
1799
+ const registered = this.availableModels.get(actualId);
1800
+ if (registered && (!providerStr || registered.provider === providerStr)) {
1801
+ return registered;
1802
+ }
1768
1803
  if (!providerStr) {
1769
1804
  const lower = actualId.toLowerCase();
1770
1805
  if (lower.includes("claude")) providerStr = "anthropic";
@@ -2522,6 +2557,12 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter {
2522
2557
  const results = await Promise.all(ocConfigs.map((cfg) => this.discoverOpenAICompatibleModels(cfg)));
2523
2558
  if (results.some(Boolean)) this.selector.markProviderAvailable("openai-compatible");
2524
2559
  }
2560
+ if (availableProviders.has("azure")) {
2561
+ for (const cfg of config.providers) {
2562
+ const model = azureModelForDeployment(cfg);
2563
+ if (model) this.selector.addDynamicModel(model);
2564
+ }
2565
+ }
2525
2566
  for (const tier of ["T1", "T2", "T3"]) {
2526
2567
  const override = tier === "T1" ? config.models.t1 : tier === "T2" ? config.models.t2 : config.models.t3;
2527
2568
  if (!override || override === "auto") continue;
@@ -3128,7 +3169,7 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter {
3128
3169
  ensureProvider(model, configs) {
3129
3170
  const key = `${model.provider}:${model.id}`;
3130
3171
  if (this.providers.has(key)) return;
3131
- const cfg = configs.find((c) => c.type === model.provider) ?? { type: model.provider };
3172
+ 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
3173
  const provider = this.createProvider(cfg, model);
3133
3174
  this.providers.set(key, provider);
3134
3175
  }
@@ -3288,6 +3329,12 @@ var BaseTier = class extends EventEmitter {
3288
3329
  * which would interleave, are not tagged.
3289
3330
  */
3290
3331
  isPresenter = false;
3332
+ /**
3333
+ * The model actually serving this tier (`provider:id`), once resolved —
3334
+ * rides on every tier:status event so the desktop can show which model ran
3335
+ * which node (Cockpit node panel / Why panel).
3336
+ */
3337
+ servingModel;
3291
3338
  constructor(role, id, parentId) {
3292
3339
  super();
3293
3340
  this.role = role;
@@ -3312,11 +3359,16 @@ var BaseTier = class extends EventEmitter {
3312
3359
  label: this.label,
3313
3360
  status,
3314
3361
  timestamp,
3315
- output
3362
+ output,
3363
+ model: this.servingModel
3316
3364
  };
3317
3365
  this.emit("status", event);
3318
3366
  this.emit("tier:status", event);
3319
3367
  }
3368
+ /** Record the model serving this tier; future status events carry it. */
3369
+ setServingModel(model) {
3370
+ this.servingModel = model || void 0;
3371
+ }
3320
3372
  setLabel(label) {
3321
3373
  this.label = label;
3322
3374
  }
@@ -3339,7 +3391,8 @@ var BaseTier = class extends EventEmitter {
3339
3391
  currentAction: update.currentAction,
3340
3392
  progressPct: update.progressPct,
3341
3393
  timestamp,
3342
- output: update.output
3394
+ output: update.output,
3395
+ model: this.servingModel
3343
3396
  });
3344
3397
  }
3345
3398
  buildMessage(type, to, payload) {
@@ -3705,6 +3758,21 @@ Available tools: ${tools.map((t) => t.name).join(", ")}.
3705
3758
  When you have enough information, stop calling tools and write your final answer.`;
3706
3759
  }
3707
3760
 
3761
+ // src/utils/truncate.ts
3762
+ function truncateForContext(text, maxChars = 12e3) {
3763
+ if (text.length <= maxChars) return text;
3764
+ const headLen = Math.floor(maxChars * 0.75);
3765
+ const tailLen = maxChars - headLen;
3766
+ const head = text.slice(0, headLen);
3767
+ const tail = text.slice(-tailLen);
3768
+ const elided = text.length - headLen - tailLen;
3769
+ return `${head}
3770
+
3771
+ [... ${elided.toLocaleString()} characters elided to keep context small \u2014 re-read the file with a line range if you need the middle ...]
3772
+
3773
+ ${tail}`;
3774
+ }
3775
+
3708
3776
  // src/core/tiers/t3-worker.ts
3709
3777
  var CriticalToolError = class extends Error {
3710
3778
  constructor(message, toolName) {
@@ -4012,6 +4080,7 @@ Now execute your subtask using this context where relevant.`
4012
4080
  } catch {
4013
4081
  }
4014
4082
  const effectiveModel = subtaskModel ?? this.router.getModelForTier("T3");
4083
+ if (effectiveModel) this.setServingModel(`${effectiveModel.provider}:${effectiveModel.id}`);
4015
4084
  const useTextTools = effectiveModel?.supportsToolUse === false && tools.length > 0;
4016
4085
  let sentFullTextContract = false;
4017
4086
  let textContractSignature = "";
@@ -4109,7 +4178,7 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : "") + textToolSuffix,
4109
4178
  const toolResult = await this.executeTool(tc);
4110
4179
  await this.context.addMessage({
4111
4180
  role: "tool",
4112
- content: toolResult,
4181
+ content: truncateForContext(toolResult),
4113
4182
  toolCallId: tc.id
4114
4183
  });
4115
4184
  }
@@ -4366,15 +4435,17 @@ ${assignment.expectedOutput}`;
4366
4435
  };
4367
4436
  }
4368
4437
  requiresArtifact() {
4438
+ if (this.assignment?.files?.length) return true;
4369
4439
  const haystack = `${this.assignment?.description ?? ""}
4370
4440
  ${this.assignment?.expectedOutput ?? ""}`;
4371
4441
  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
4442
  }
4373
4443
  extractArtifactPaths(assignment) {
4444
+ const declared = (assignment.files ?? []).map((f) => f.trim()).filter((f) => f.includes("."));
4374
4445
  const haystack = `${assignment.description}
4375
4446
  ${assignment.expectedOutput}`;
4376
4447
  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()))];
4448
+ return [.../* @__PURE__ */ new Set([...declared, ...matches.map((m) => m.trim())])];
4378
4449
  }
4379
4450
  async verifyArtifacts(assignment) {
4380
4451
  const artifactPaths = this.extractArtifactPaths(assignment);
@@ -4497,7 +4568,9 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
4497
4568
  Assignment: ${assignment.description}
4498
4569
  Expected output: ${assignment.expectedOutput}
4499
4570
  Constraints: ${assignment.constraints.join("; ")}
4500
-
4571
+ ${assignment.acceptance?.length ? `Acceptance criteria \u2014 ALL must be satisfied for "completeness" to pass:
4572
+ ${assignment.acceptance.map((a) => `- ${a}`).join("\n")}
4573
+ ` : ""}
4501
4574
  Output to test:
4502
4575
  ${output}
4503
4576
 
@@ -4586,17 +4659,27 @@ Your subtask:
4586
4659
  - Title: ${assignment.subtaskTitle}
4587
4660
  - Description: ${assignment.description}
4588
4661
  - Expected output: ${assignment.expectedOutput}
4589
- - Constraints: ${assignment.constraints.join("; ")}`;
4662
+ - Constraints: ${assignment.constraints.join("; ")}${assignment.files?.length ? `
4663
+ - Files you own (create/edit ONLY these): ${assignment.files.join(", ")}` : ""}${assignment.acceptance?.length ? `
4664
+ - Definition of done: ${assignment.acceptance.join("; ")}` : ""}`;
4590
4665
  }
4591
4666
  buildInitialPrompt(assignment) {
4592
4667
  return `Execute the following subtask completely:
4593
4668
 
4594
4669
  **${assignment.subtaskTitle}**
4595
-
4670
+ ${assignment.contextBrief ? `
4671
+ Context: ${assignment.contextBrief}
4672
+ ` : ""}
4596
4673
  ${assignment.description}
4597
4674
 
4598
4675
  Expected output: ${assignment.expectedOutput}
4599
-
4676
+ ${assignment.files?.length ? `
4677
+ Files you own (create or edit exactly these paths):
4678
+ ${assignment.files.map((f) => `- ${f}`).join("\n")}
4679
+ ` : ""}${assignment.acceptance?.length ? `
4680
+ Definition of done (your output must satisfy ALL of these):
4681
+ ${assignment.acceptance.map((a) => `- ${a}`).join("\n")}
4682
+ ` : ""}
4600
4683
  Constraints:
4601
4684
  ${assignment.constraints.map((c) => `- ${c}`).join("\n")}
4602
4685
 
@@ -5077,6 +5160,8 @@ var T2Manager = class extends BaseTier {
5077
5160
  this.assignment = assignment;
5078
5161
  this.taskId = taskId;
5079
5162
  this.setLabel(assignment.sectionTitle);
5163
+ const m = this.router.getModelForTier("T2");
5164
+ if (m) this.setServingModel(`${m.provider}:${m.id}`);
5080
5165
  this.setStatus("ACTIVE");
5081
5166
  this.sendStatusUpdate({
5082
5167
  progressPct: 0,
@@ -5163,7 +5248,7 @@ Guidance (must be followed): ${decision.note}`
5163
5248
  // ── Private ──────────────────────────────────
5164
5249
  async decomposeSection(assignment) {
5165
5250
  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.
5251
+ 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
5252
 
5168
5253
  Section: ${assignment.sectionTitle}
5169
5254
  Description: ${assignment.description}
@@ -5182,6 +5267,9 @@ Return a JSON array of subtask objects, each with:
5182
5267
  - peerT3Ids: string[] (empty for now)
5183
5268
  - dependsOn: string[] (array of subtaskIds this task depends on to start)
5184
5269
  - executionMode: "parallel|sequential" (default is parallel)
5270
+ - files: string[] (the EXACT relative paths this subtask creates or edits)
5271
+ - acceptance: string[] (1-3 mechanically checkable done-criteria: file exists / contains X / command exits 0)
5272
+ - contextBrief: string (1-3 short sentences with ALL the background the worker needs \u2014 it sees nothing else)
5185
5273
 
5186
5274
  Return ONLY the JSON array.`;
5187
5275
  const messages = [{ role: "user", content: prompt }];
@@ -5779,6 +5867,8 @@ var T1Administrator = class extends BaseTier {
5779
5867
  this.signal = signal;
5780
5868
  this.taskId = randomUUID();
5781
5869
  this.setLabel("Administrator");
5870
+ const m = this.router.getModelForTier("T1");
5871
+ if (m) this.setServingModel(`${m.provider}:${m.id}`);
5782
5872
  this.setStatus("ACTIVE");
5783
5873
  this.taskGoal = userPrompt;
5784
5874
  this.sendStatusUpdate({
@@ -6025,10 +6115,13 @@ Return JSON where SECTIONS can declare dependencies on other SECTIONS:
6025
6115
  "description": "Run npm init",
6026
6116
  "expectedOutput": "package.json created",
6027
6117
  "constraints": [],
6028
- "dependsOn": []
6118
+ "dependsOn": [],
6119
+ "files": ["package.json"], // \u2190 exact paths this subtask owns
6120
+ "acceptance": ["package.json exists and parses as JSON"], // \u2190 objectively checkable
6121
+ "contextBrief": "Fresh Node 20 project; npm available." // \u2190 ALL the background the worker gets
6029
6122
  }]
6030
6123
  }, {
6031
- "sectionId": "s2",
6124
+ "sectionId": "s2",
6032
6125
  "sectionTitle": "Write Tests",
6033
6126
  "description": "Write tests for the project",
6034
6127
  "expectedOutput": "Tests passing",
@@ -6038,7 +6131,13 @@ Return JSON where SECTIONS can declare dependencies on other SECTIONS:
6038
6131
  }]
6039
6132
  }
6040
6133
  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.`;
6134
+ Leave dependsOn empty for sections that can run immediately in parallel.
6135
+
6136
+ SPEC RULES \u2014 each subtask is a self-contained spec slice (workers execute from their slice ALONE):
6137
+ - "files": the exact relative paths the subtask creates or edits. Never vague ("some files"); always concrete.
6138
+ - "acceptance": 1-3 checks a reviewer could verify mechanically (file exists / contains X / command exits 0). These define done.
6139
+ - "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.
6140
+ - 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
6141
  const messages = [{ role: "user", content: decompositionPrompt }];
6043
6142
  const result = await this.router.generate("T1", {
6044
6143
  messages,
@@ -7340,30 +7439,56 @@ async function searchTavily(query, apiKey, maxResults) {
7340
7439
  engine: "tavily"
7341
7440
  }));
7342
7441
  }
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 = [];
7442
+ var BROWSER_UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0 Safari/537.36";
7443
+ function unwrapDdgRedirect(href) {
7444
+ try {
7445
+ const url = new URL(href.startsWith("//") ? `https:${href}` : href, "https://duckduckgo.com");
7446
+ if (/(^|\.)duckduckgo\.com$/i.test(url.hostname) && url.pathname.startsWith("/l/")) {
7447
+ const target = url.searchParams.get("uddg");
7448
+ if (target) return decodeURIComponent(target);
7449
+ }
7450
+ return url.protocol === "http:" || url.protocol === "https:" ? url.toString() : href;
7451
+ } catch {
7452
+ return href;
7453
+ }
7454
+ }
7455
+ function stripTags(html) {
7456
+ return html.replace(/<[^>]+>/g, "").replace(/\s+/g, " ").trim();
7457
+ }
7458
+ function decodeEntities(text) {
7459
+ return text.replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&#x27;|&#39;/g, "'").replace(/&nbsp;/g, " ");
7460
+ }
7461
+ function parseDdgAnchors(html, anchorClass, snippetClass) {
7462
+ const anchorRe = new RegExp(`<a\\b[^>]*class=["']?[^"'>]*\\b${anchorClass}\\b[^"'>]*["']?[^>]*>([\\s\\S]*?)<\\/a>`, "gi");
7463
+ const snippetRe = new RegExp(`class=["']?[^"'>]*\\b${snippetClass}\\b[^"'>]*["']?[^>]*>([\\s\\S]*?)<\\/(?:td|a|div|span)>`, "gi");
7464
+ const results = [];
7354
7465
  let m;
7355
- while ((m = linkPattern.exec(html)) !== null) {
7356
- links.push({ url: m[1], title: m[2].trim() });
7466
+ while ((m = anchorRe.exec(html)) !== null) {
7467
+ const tag = m[0];
7468
+ const href = /href=["']([^"']+)["']/i.exec(tag)?.[1];
7469
+ const title = decodeEntities(stripTags(m[1] ?? ""));
7470
+ if (!href || !title) continue;
7471
+ results.push({ title, url: unwrapDdgRedirect(decodeEntities(href)), snippet: "" });
7472
+ }
7473
+ const snippets = [];
7474
+ while ((m = snippetRe.exec(html)) !== null) {
7475
+ snippets.push(decodeEntities(stripTags(m[1] ?? "")));
7357
7476
  }
7358
- while ((m = snippetPattern.exec(html)) !== null) {
7359
- snippets.push(m[1].replace(/<[^>]+>/g, "").trim());
7477
+ for (let i = 0; i < results.length; i++) {
7478
+ if (snippets[i]) results[i].snippet = snippets[i];
7360
7479
  }
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
- }));
7480
+ return results;
7481
+ }
7482
+ async function searchDuckDuckGo(query, maxResults, variant) {
7483
+ const base = variant === "html" ? "https://html.duckduckgo.com/html/?q=" : "https://lite.duckduckgo.com/lite/?q=";
7484
+ const resp = await fetch(`${base}${encodeURIComponent(query)}`, {
7485
+ headers: { "User-Agent": BROWSER_UA, Accept: "text/html" },
7486
+ signal: AbortSignal.timeout(1e4)
7487
+ });
7488
+ if (!resp.ok) throw new Error(`DuckDuckGo ${variant} returned HTTP ${resp.status}`);
7489
+ const html = await resp.text();
7490
+ const parsed = variant === "html" ? parseDdgAnchors(html, "result__a", "result__snippet") : parseDdgAnchors(html, "result-link", "result-snippet");
7491
+ return parsed.slice(0, maxResults).map((r) => ({ ...r, engine: `duckduckgo-${variant}` }));
7367
7492
  }
7368
7493
  var WebSearchTool = class extends BaseTool {
7369
7494
  name = "web_search";
@@ -7422,12 +7547,14 @@ var WebSearchTool = class extends BaseTool {
7422
7547
  errors.push(`Tavily: ${err instanceof Error ? err.message : String(err)}`);
7423
7548
  }
7424
7549
  }
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)}`);
7550
+ for (const variant of ["html", "lite"]) {
7551
+ try {
7552
+ results = await searchDuckDuckGo(query, maxResults, variant);
7553
+ if (results.length > 0) return this.formatResults(query, results);
7554
+ errors.push(`DuckDuckGo ${variant}: returned 0 results`);
7555
+ } catch (err) {
7556
+ errors.push(`DuckDuckGo ${variant}: ${err instanceof Error ? err.message : String(err)}`);
7557
+ }
7431
7558
  }
7432
7559
  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
7560
  return [
@@ -8371,8 +8498,11 @@ var CascadeConfigSchema = z.object({
8371
8498
  * Cascade Auto: when true, the TaskAnalyzer selects the optimal model for each
8372
8499
  * tier based on task type and complexity, overriding the static priority lists.
8373
8500
  * Heuristic-first with AI inference fallback (adds ~0–500ms per task).
8501
+ * ON by default since v0.19.0 — "Auto" without it was just a static priority
8502
+ * list, not the benchmark-value routing the docs describe. Explicit per-tier
8503
+ * model pins are unaffected; disable via config/Settings → Advanced.
8374
8504
  */
8375
- cascadeAuto: z.boolean().default(false),
8505
+ cascadeAuto: z.boolean().default(true),
8376
8506
  /**
8377
8507
  * Cascade Auto trade-off bias when picking a model for a task:
8378
8508
  * - 'balanced' (default): quality × cost-efficiency — cheap models win
@@ -10085,13 +10215,30 @@ ${last.partialOutput}` : "");
10085
10215
  * explicit multi-part structure, so ordinary single-file asks (handled as
10086
10216
  * Simple/Moderate) don't get over-escalated.
10087
10217
  */
10088
- looksClearlyComplex(prompt) {
10218
+ /** Shared build/scale signals for the complexity floors below. */
10219
+ buildSignals(prompt) {
10089
10220
  const p = prompt.trim();
10090
- if (p.length < 24) return false;
10221
+ if (p.length < 24) return { buildVerb: false, scaleCount: 0, multiPart: false };
10091
10222
  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);
10223
+ 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
10224
  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);
10225
+ return { buildVerb, scaleCount, multiPart };
10226
+ }
10227
+ /**
10228
+ * A build prompt with REAL scale: multiple system-level deliverables, or a
10229
+ * deliverable plus explicitly multi-part phrasing. Only these floor to the
10230
+ * full T1→T2→T3 hierarchy — "create a todo app" is a build prompt too, but
10231
+ * flooring every small build to Complex was the #1 token bomb (3-5 managers
10232
+ * × workers for a task one worker handles).
10233
+ */
10234
+ looksClearlyComplex(prompt) {
10235
+ const s = this.buildSignals(prompt);
10236
+ return s.buildVerb && (s.scaleCount >= 2 || s.scaleCount >= 1 && s.multiPart);
10237
+ }
10238
+ /** A small single-deliverable build — real work, but one manager's worth. */
10239
+ looksLikeModerateBuild(prompt) {
10240
+ const s = this.buildSignals(prompt);
10241
+ return s.buildVerb && (s.scaleCount >= 1 || s.multiPart);
10095
10242
  }
10096
10243
  // Cache glob scan results per workspace path to avoid repeated I/O.
10097
10244
  static globCache = /* @__PURE__ */ new Map();
@@ -10179,6 +10326,9 @@ ${prompt}` : prompt;
10179
10326
  if (verdict !== "Complex" && this.looksClearlyComplex(prompt)) {
10180
10327
  this.recordDecision("complexity", `Complex \u2014 heuristic floor over classifier "${verdict}": explicit multi-step build/implementation signals (T1 engaged)`);
10181
10328
  verdict = "Complex";
10329
+ } else if (verdict === "Simple" && this.looksLikeModerateBuild(prompt)) {
10330
+ this.recordDecision("complexity", 'Moderate \u2014 heuristic floor over classifier "Simple": build signals without multi-system scale (single manager)');
10331
+ verdict = "Moderate";
10182
10332
  } else {
10183
10333
  this.recordDecision("complexity", `${verdict} \u2014 classifier: ${reason || "no reason given"}`);
10184
10334
  }