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 CHANGED
@@ -111,7 +111,7 @@ var __export = (target, all) => {
111
111
  var CASCADE_VERSION, CASCADE_CONFIG_FILE, CASCADE_DB_FILE, CASCADE_DASHBOARD_SECRET_FILE, GLOBAL_CONFIG_DIR, GLOBAL_DB_FILE, GLOBAL_KEYSTORE_FILE, GLOBAL_CREDENTIALS_FILE, GLOBAL_RUNTIME_DB_FILE, DEFAULT_DASHBOARD_PORT, DEFAULT_CONTEXT_LIMIT, DEFAULT_AUTO_SUMMARIZE_AT, MODELS, T1_MODEL_PRIORITY, T2_MODEL_PRIORITY, T3_MODEL_PRIORITY, VISION_MODEL_PRIORITY, COMPLEXITY_T2_COUNT, THEME_NAMES, DEFAULT_THEME, OLLAMA_BASE_URL, LM_STUDIO_BASE_URL, AZURE_BASE_URL_TEMPLATE, TOOL_NAMES, DEFAULT_APPROVAL_REQUIRED;
112
112
  var init_constants = __esm({
113
113
  "src/constants.ts"() {
114
- CASCADE_VERSION = "0.18.0";
114
+ CASCADE_VERSION = "0.19.1";
115
115
  CASCADE_CONFIG_FILE = ".cascade/config.json";
116
116
  CASCADE_DB_FILE = ".cascade/memory.db";
117
117
  CASCADE_DASHBOARD_SECRET_FILE = ".cascade/dashboard-secret";
@@ -937,8 +937,26 @@ var init_openai = __esm({
937
937
  // src/providers/azure.ts
938
938
  var azure_exports = {};
939
939
  __export(azure_exports, {
940
- AzureOpenAIProvider: () => AzureOpenAIProvider
940
+ AzureOpenAIProvider: () => AzureOpenAIProvider,
941
+ azureModelForDeployment: () => azureModelForDeployment
941
942
  });
943
+ function azureModelForDeployment(cfg) {
944
+ if (cfg.type !== "azure" || !cfg.deploymentName?.trim()) return null;
945
+ const id = cfg.deploymentName.trim();
946
+ return {
947
+ id,
948
+ name: cfg.label?.trim() || id,
949
+ provider: "azure",
950
+ contextWindow: 128e3,
951
+ isVisionCapable: false,
952
+ inputCostPer1kTokens: 25e-4,
953
+ outputCostPer1kTokens: 0.01,
954
+ maxOutputTokens: 16e3,
955
+ supportsStreaming: true,
956
+ isLocal: false,
957
+ supportsToolUse: true
958
+ };
959
+ }
942
960
  var AzureOpenAIProvider;
943
961
  var init_azure = __esm({
944
962
  "src/providers/azure.ts"() {
@@ -964,17 +982,31 @@ var init_azure = __esm({
964
982
  });
965
983
  }
966
984
  async listModels() {
967
- return [this.model];
985
+ const fromDeployment = azureModelForDeployment(this.config);
986
+ return [fromDeployment ?? this.model];
968
987
  }
969
988
  async isAvailable() {
989
+ const params = {
990
+ model: this.model.id,
991
+ messages: [{ role: "user", content: "ping" }],
992
+ max_tokens: 1
993
+ };
970
994
  try {
971
- await this.client.chat.completions.create({
972
- model: this.model.id,
973
- messages: [{ role: "user", content: "ping" }],
974
- max_tokens: 1
975
- });
995
+ await this.client.chat.completions.create(params);
976
996
  return true;
977
- } catch {
997
+ } catch (err) {
998
+ if (err?.message && String(err.message).includes("max_completion_tokens")) {
999
+ try {
1000
+ await this.client.chat.completions.create({
1001
+ model: this.model.id,
1002
+ messages: [{ role: "user", content: "ping" }],
1003
+ max_completion_tokens: 1
1004
+ });
1005
+ return true;
1006
+ } catch {
1007
+ return false;
1008
+ }
1009
+ }
978
1010
  return false;
979
1011
  }
980
1012
  }
@@ -3049,8 +3081,11 @@ var CascadeConfigSchema = zod.z.object({
3049
3081
  * Cascade Auto: when true, the TaskAnalyzer selects the optimal model for each
3050
3082
  * tier based on task type and complexity, overriding the static priority lists.
3051
3083
  * Heuristic-first with AI inference fallback (adds ~0–500ms per task).
3084
+ * ON by default since v0.19.0 — "Auto" without it was just a static priority
3085
+ * list, not the benchmark-value routing the docs describe. Explicit per-tier
3086
+ * model pins are unaffected; disable via config/Settings → Advanced.
3052
3087
  */
3053
- cascadeAuto: zod.z.boolean().default(false),
3088
+ cascadeAuto: zod.z.boolean().default(true),
3054
3089
  /**
3055
3090
  * Cascade Auto trade-off bias when picking a model for a task:
3056
3091
  * - 'balanced' (default): quality × cost-efficiency — cheap models win
@@ -3852,6 +3887,10 @@ var ModelSelector = class {
3852
3887
  actualId = parts.slice(1).join(":");
3853
3888
  }
3854
3889
  }
3890
+ const registered = this.availableModels.get(actualId);
3891
+ if (registered && (!providerStr || registered.provider === providerStr)) {
3892
+ return registered;
3893
+ }
3855
3894
  if (!providerStr) {
3856
3895
  const lower = actualId.toLowerCase();
3857
3896
  if (lower.includes("claude")) providerStr = "anthropic";
@@ -4604,6 +4643,12 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
4604
4643
  const results = await Promise.all(ocConfigs.map((cfg) => this.discoverOpenAICompatibleModels(cfg)));
4605
4644
  if (results.some(Boolean)) this.selector.markProviderAvailable("openai-compatible");
4606
4645
  }
4646
+ if (availableProviders.has("azure")) {
4647
+ for (const cfg of config.providers) {
4648
+ const model = azureModelForDeployment(cfg);
4649
+ if (model) this.selector.addDynamicModel(model);
4650
+ }
4651
+ }
4607
4652
  for (const tier of ["T1", "T2", "T3"]) {
4608
4653
  const override = tier === "T1" ? config.models.t1 : tier === "T2" ? config.models.t2 : config.models.t3;
4609
4654
  if (!override || override === "auto") continue;
@@ -5210,7 +5255,7 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
5210
5255
  ensureProvider(model, configs) {
5211
5256
  const key = `${model.provider}:${model.id}`;
5212
5257
  if (this.providers.has(key)) return;
5213
- const cfg = configs.find((c) => c.type === model.provider) ?? { type: model.provider };
5258
+ 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 };
5214
5259
  const provider = this.createProvider(cfg, model);
5215
5260
  this.providers.set(key, provider);
5216
5261
  }
@@ -5370,6 +5415,12 @@ var BaseTier = class extends EventEmitter__default.default {
5370
5415
  * which would interleave, are not tagged.
5371
5416
  */
5372
5417
  isPresenter = false;
5418
+ /**
5419
+ * The model actually serving this tier (`provider:id`), once resolved —
5420
+ * rides on every tier:status event so the desktop can show which model ran
5421
+ * which node (Cockpit node panel / Why panel).
5422
+ */
5423
+ servingModel;
5373
5424
  constructor(role, id, parentId) {
5374
5425
  super();
5375
5426
  this.role = role;
@@ -5394,11 +5445,16 @@ var BaseTier = class extends EventEmitter__default.default {
5394
5445
  label: this.label,
5395
5446
  status,
5396
5447
  timestamp,
5397
- output
5448
+ output,
5449
+ model: this.servingModel
5398
5450
  };
5399
5451
  this.emit("status", event);
5400
5452
  this.emit("tier:status", event);
5401
5453
  }
5454
+ /** Record the model serving this tier; future status events carry it. */
5455
+ setServingModel(model) {
5456
+ this.servingModel = model || void 0;
5457
+ }
5402
5458
  setLabel(label) {
5403
5459
  this.label = label;
5404
5460
  }
@@ -5421,7 +5477,8 @@ var BaseTier = class extends EventEmitter__default.default {
5421
5477
  currentAction: update.currentAction,
5422
5478
  progressPct: update.progressPct,
5423
5479
  timestamp,
5424
- output: update.output
5480
+ output: update.output,
5481
+ model: this.servingModel
5425
5482
  });
5426
5483
  }
5427
5484
  buildMessage(type, to, payload) {
@@ -5788,6 +5845,21 @@ Available tools: ${tools.map((t) => t.name).join(", ")}.
5788
5845
  When you have enough information, stop calling tools and write your final answer.`;
5789
5846
  }
5790
5847
 
5848
+ // src/utils/truncate.ts
5849
+ function truncateForContext(text, maxChars = 12e3) {
5850
+ if (text.length <= maxChars) return text;
5851
+ const headLen = Math.floor(maxChars * 0.75);
5852
+ const tailLen = maxChars - headLen;
5853
+ const head = text.slice(0, headLen);
5854
+ const tail = text.slice(-tailLen);
5855
+ const elided = text.length - headLen - tailLen;
5856
+ return `${head}
5857
+
5858
+ [... ${elided.toLocaleString()} characters elided to keep context small \u2014 re-read the file with a line range if you need the middle ...]
5859
+
5860
+ ${tail}`;
5861
+ }
5862
+
5791
5863
  // src/core/tiers/t3-worker.ts
5792
5864
  var CriticalToolError = class extends Error {
5793
5865
  constructor(message, toolName) {
@@ -6095,6 +6167,7 @@ Now execute your subtask using this context where relevant.`
6095
6167
  } catch {
6096
6168
  }
6097
6169
  const effectiveModel = subtaskModel ?? this.router.getModelForTier("T3");
6170
+ if (effectiveModel) this.setServingModel(`${effectiveModel.provider}:${effectiveModel.id}`);
6098
6171
  const useTextTools = effectiveModel?.supportsToolUse === false && tools.length > 0;
6099
6172
  let sentFullTextContract = false;
6100
6173
  let textContractSignature = "";
@@ -6192,7 +6265,7 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : "") + textToolSuffix,
6192
6265
  const toolResult = await this.executeTool(tc);
6193
6266
  await this.context.addMessage({
6194
6267
  role: "tool",
6195
- content: toolResult,
6268
+ content: truncateForContext(toolResult),
6196
6269
  toolCallId: tc.id
6197
6270
  });
6198
6271
  }
@@ -6449,15 +6522,17 @@ ${assignment.expectedOutput}`;
6449
6522
  };
6450
6523
  }
6451
6524
  requiresArtifact() {
6525
+ if (this.assignment?.files?.length) return true;
6452
6526
  const haystack = `${this.assignment?.description ?? ""}
6453
6527
  ${this.assignment?.expectedOutput ?? ""}`;
6454
6528
  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);
6455
6529
  }
6456
6530
  extractArtifactPaths(assignment) {
6531
+ const declared = (assignment.files ?? []).map((f) => f.trim()).filter((f) => f.includes("."));
6457
6532
  const haystack = `${assignment.description}
6458
6533
  ${assignment.expectedOutput}`;
6459
6534
  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) ?? [];
6460
- return [...new Set(matches.map((m) => m.trim()))];
6535
+ return [.../* @__PURE__ */ new Set([...declared, ...matches.map((m) => m.trim())])];
6461
6536
  }
6462
6537
  async verifyArtifacts(assignment) {
6463
6538
  const artifactPaths = this.extractArtifactPaths(assignment);
@@ -6580,7 +6655,9 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
6580
6655
  Assignment: ${assignment.description}
6581
6656
  Expected output: ${assignment.expectedOutput}
6582
6657
  Constraints: ${assignment.constraints.join("; ")}
6583
-
6658
+ ${assignment.acceptance?.length ? `Acceptance criteria \u2014 ALL must be satisfied for "completeness" to pass:
6659
+ ${assignment.acceptance.map((a) => `- ${a}`).join("\n")}
6660
+ ` : ""}
6584
6661
  Output to test:
6585
6662
  ${output}
6586
6663
 
@@ -6669,17 +6746,27 @@ Your subtask:
6669
6746
  - Title: ${assignment.subtaskTitle}
6670
6747
  - Description: ${assignment.description}
6671
6748
  - Expected output: ${assignment.expectedOutput}
6672
- - Constraints: ${assignment.constraints.join("; ")}`;
6749
+ - Constraints: ${assignment.constraints.join("; ")}${assignment.files?.length ? `
6750
+ - Files you own (create/edit ONLY these): ${assignment.files.join(", ")}` : ""}${assignment.acceptance?.length ? `
6751
+ - Definition of done: ${assignment.acceptance.join("; ")}` : ""}`;
6673
6752
  }
6674
6753
  buildInitialPrompt(assignment) {
6675
6754
  return `Execute the following subtask completely:
6676
6755
 
6677
6756
  **${assignment.subtaskTitle}**
6678
-
6757
+ ${assignment.contextBrief ? `
6758
+ Context: ${assignment.contextBrief}
6759
+ ` : ""}
6679
6760
  ${assignment.description}
6680
6761
 
6681
6762
  Expected output: ${assignment.expectedOutput}
6682
-
6763
+ ${assignment.files?.length ? `
6764
+ Files you own (create or edit exactly these paths):
6765
+ ${assignment.files.map((f) => `- ${f}`).join("\n")}
6766
+ ` : ""}${assignment.acceptance?.length ? `
6767
+ Definition of done (your output must satisfy ALL of these):
6768
+ ${assignment.acceptance.map((a) => `- ${a}`).join("\n")}
6769
+ ` : ""}
6683
6770
  Constraints:
6684
6771
  ${assignment.constraints.map((c) => `- ${c}`).join("\n")}
6685
6772
 
@@ -7160,6 +7247,8 @@ var T2Manager = class extends BaseTier {
7160
7247
  this.assignment = assignment;
7161
7248
  this.taskId = taskId;
7162
7249
  this.setLabel(assignment.sectionTitle);
7250
+ const m = this.router.getModelForTier("T2");
7251
+ if (m) this.setServingModel(`${m.provider}:${m.id}`);
7163
7252
  this.setStatus("ACTIVE");
7164
7253
  this.sendStatusUpdate({
7165
7254
  progressPct: 0,
@@ -7246,7 +7335,7 @@ Guidance (must be followed): ${decision.note}`
7246
7335
  // ── Private ──────────────────────────────────
7247
7336
  async decomposeSection(assignment) {
7248
7337
  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");
7249
- const prompt = `Decompose this section into 2-5 concrete subtasks for T3 workers.
7338
+ 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).
7250
7339
 
7251
7340
  Section: ${assignment.sectionTitle}
7252
7341
  Description: ${assignment.description}
@@ -7265,6 +7354,9 @@ Return a JSON array of subtask objects, each with:
7265
7354
  - peerT3Ids: string[] (empty for now)
7266
7355
  - dependsOn: string[] (array of subtaskIds this task depends on to start)
7267
7356
  - executionMode: "parallel|sequential" (default is parallel)
7357
+ - files: string[] (the EXACT relative paths this subtask creates or edits)
7358
+ - acceptance: string[] (1-3 mechanically checkable done-criteria: file exists / contains X / command exits 0)
7359
+ - contextBrief: string (1-3 short sentences with ALL the background the worker needs \u2014 it sees nothing else)
7268
7360
 
7269
7361
  Return ONLY the JSON array.`;
7270
7362
  const messages = [{ role: "user", content: prompt }];
@@ -7865,6 +7957,8 @@ var T1Administrator = class extends BaseTier {
7865
7957
  this.signal = signal;
7866
7958
  this.taskId = crypto.randomUUID();
7867
7959
  this.setLabel("Administrator");
7960
+ const m = this.router.getModelForTier("T1");
7961
+ if (m) this.setServingModel(`${m.provider}:${m.id}`);
7868
7962
  this.setStatus("ACTIVE");
7869
7963
  this.taskGoal = userPrompt;
7870
7964
  this.sendStatusUpdate({
@@ -8111,10 +8205,13 @@ Return JSON where SECTIONS can declare dependencies on other SECTIONS:
8111
8205
  "description": "Run npm init",
8112
8206
  "expectedOutput": "package.json created",
8113
8207
  "constraints": [],
8114
- "dependsOn": []
8208
+ "dependsOn": [],
8209
+ "files": ["package.json"], // \u2190 exact paths this subtask owns
8210
+ "acceptance": ["package.json exists and parses as JSON"], // \u2190 objectively checkable
8211
+ "contextBrief": "Fresh Node 20 project; npm available." // \u2190 ALL the background the worker gets
8115
8212
  }]
8116
8213
  }, {
8117
- "sectionId": "s2",
8214
+ "sectionId": "s2",
8118
8215
  "sectionTitle": "Write Tests",
8119
8216
  "description": "Write tests for the project",
8120
8217
  "expectedOutput": "Tests passing",
@@ -8124,7 +8221,13 @@ Return JSON where SECTIONS can declare dependencies on other SECTIONS:
8124
8221
  }]
8125
8222
  }
8126
8223
  Use dependsOn at the SECTION level when a whole T2 Manager needs the output of a previous T2 Manager.
8127
- Leave dependsOn empty for sections that can run immediately in parallel.`;
8224
+ Leave dependsOn empty for sections that can run immediately in parallel.
8225
+
8226
+ SPEC RULES \u2014 each subtask is a self-contained spec slice (workers execute from their slice ALONE):
8227
+ - "files": the exact relative paths the subtask creates or edits. Never vague ("some files"); always concrete.
8228
+ - "acceptance": 1-3 checks a reviewer could verify mechanically (file exists / contains X / command exits 0). These define done.
8229
+ - "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.
8230
+ - 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.`;
8128
8231
  const messages = [{ role: "user", content: decompositionPrompt }];
8129
8232
  const result = await this.router.generate("T1", {
8130
8233
  messages,
@@ -9432,30 +9535,56 @@ async function searchTavily(query, apiKey, maxResults) {
9432
9535
  engine: "tavily"
9433
9536
  }));
9434
9537
  }
9435
- async function searchDuckDuckGoLite(query, maxResults) {
9436
- const resp = await fetch(`https://lite.duckduckgo.com/lite/?q=${encodeURIComponent(query)}`, {
9437
- headers: { "User-Agent": "Mozilla/5.0 (compatible; Cascade-AI/1.0)" },
9438
- signal: AbortSignal.timeout(1e4)
9439
- });
9440
- if (!resp.ok) throw new Error(`DuckDuckGo Lite returned HTTP ${resp.status}`);
9441
- const html = await resp.text();
9442
- const linkPattern = /<a[^>]+class="result-link"[^>]+href="([^"]+)"[^>]*>([^<]+)<\/a>/g;
9443
- const snippetPattern = /<td[^>]+class="result-snippet"[^>]*>([\s\S]*?)<\/td>/g;
9444
- const links = [];
9445
- const snippets = [];
9538
+ var BROWSER_UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0 Safari/537.36";
9539
+ function unwrapDdgRedirect(href) {
9540
+ try {
9541
+ const url = new URL(href.startsWith("//") ? `https:${href}` : href, "https://duckduckgo.com");
9542
+ if (/(^|\.)duckduckgo\.com$/i.test(url.hostname) && url.pathname.startsWith("/l/")) {
9543
+ const target = url.searchParams.get("uddg");
9544
+ if (target) return decodeURIComponent(target);
9545
+ }
9546
+ return url.protocol === "http:" || url.protocol === "https:" ? url.toString() : href;
9547
+ } catch {
9548
+ return href;
9549
+ }
9550
+ }
9551
+ function stripTags(html) {
9552
+ return html.replace(/<[^>]+>/g, "").replace(/\s+/g, " ").trim();
9553
+ }
9554
+ function decodeEntities(text) {
9555
+ return text.replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&#x27;|&#39;/g, "'").replace(/&nbsp;/g, " ");
9556
+ }
9557
+ function parseDdgAnchors(html, anchorClass, snippetClass) {
9558
+ const anchorRe = new RegExp(`<a\\b[^>]*class=["']?[^"'>]*\\b${anchorClass}\\b[^"'>]*["']?[^>]*>([\\s\\S]*?)<\\/a>`, "gi");
9559
+ const snippetRe = new RegExp(`class=["']?[^"'>]*\\b${snippetClass}\\b[^"'>]*["']?[^>]*>([\\s\\S]*?)<\\/(?:td|a|div|span)>`, "gi");
9560
+ const results = [];
9446
9561
  let m;
9447
- while ((m = linkPattern.exec(html)) !== null) {
9448
- links.push({ url: m[1], title: m[2].trim() });
9562
+ while ((m = anchorRe.exec(html)) !== null) {
9563
+ const tag = m[0];
9564
+ const href = /href=["']([^"']+)["']/i.exec(tag)?.[1];
9565
+ const title = decodeEntities(stripTags(m[1] ?? ""));
9566
+ if (!href || !title) continue;
9567
+ results.push({ title, url: unwrapDdgRedirect(decodeEntities(href)), snippet: "" });
9449
9568
  }
9450
- while ((m = snippetPattern.exec(html)) !== null) {
9451
- snippets.push(m[1].replace(/<[^>]+>/g, "").trim());
9569
+ const snippets = [];
9570
+ while ((m = snippetRe.exec(html)) !== null) {
9571
+ snippets.push(decodeEntities(stripTags(m[1] ?? "")));
9452
9572
  }
9453
- return links.slice(0, maxResults).map((link, i) => ({
9454
- title: link.title,
9455
- url: link.url,
9456
- snippet: snippets[i] ?? "",
9457
- engine: "duckduckgo-lite"
9458
- }));
9573
+ for (let i = 0; i < results.length; i++) {
9574
+ if (snippets[i]) results[i].snippet = snippets[i];
9575
+ }
9576
+ return results;
9577
+ }
9578
+ async function searchDuckDuckGo(query, maxResults, variant) {
9579
+ const base = variant === "html" ? "https://html.duckduckgo.com/html/?q=" : "https://lite.duckduckgo.com/lite/?q=";
9580
+ const resp = await fetch(`${base}${encodeURIComponent(query)}`, {
9581
+ headers: { "User-Agent": BROWSER_UA, Accept: "text/html" },
9582
+ signal: AbortSignal.timeout(1e4)
9583
+ });
9584
+ if (!resp.ok) throw new Error(`DuckDuckGo ${variant} returned HTTP ${resp.status}`);
9585
+ const html = await resp.text();
9586
+ const parsed = variant === "html" ? parseDdgAnchors(html, "result__a", "result__snippet") : parseDdgAnchors(html, "result-link", "result-snippet");
9587
+ return parsed.slice(0, maxResults).map((r) => ({ ...r, engine: `duckduckgo-${variant}` }));
9459
9588
  }
9460
9589
  var WebSearchTool = class extends BaseTool {
9461
9590
  name = "web_search";
@@ -9514,12 +9643,14 @@ var WebSearchTool = class extends BaseTool {
9514
9643
  errors.push(`Tavily: ${err instanceof Error ? err.message : String(err)}`);
9515
9644
  }
9516
9645
  }
9517
- try {
9518
- results = await searchDuckDuckGoLite(query, maxResults);
9519
- if (results.length > 0) return this.formatResults(query, results);
9520
- errors.push("DuckDuckGo Lite: returned 0 results");
9521
- } catch (err) {
9522
- errors.push(`DuckDuckGo Lite: ${err instanceof Error ? err.message : String(err)}`);
9646
+ for (const variant of ["html", "lite"]) {
9647
+ try {
9648
+ results = await searchDuckDuckGo(query, maxResults, variant);
9649
+ if (results.length > 0) return this.formatResults(query, results);
9650
+ errors.push(`DuckDuckGo ${variant}: returned 0 results`);
9651
+ } catch (err) {
9652
+ errors.push(`DuckDuckGo ${variant}: ${err instanceof Error ? err.message : String(err)}`);
9653
+ }
9523
9654
  }
9524
9655
  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" : "";
9525
9656
  return [
@@ -11866,13 +11997,30 @@ ${last.partialOutput}` : "");
11866
11997
  * explicit multi-part structure, so ordinary single-file asks (handled as
11867
11998
  * Simple/Moderate) don't get over-escalated.
11868
11999
  */
11869
- looksClearlyComplex(prompt) {
12000
+ /** Shared build/scale signals for the complexity floors below. */
12001
+ buildSignals(prompt) {
11870
12002
  const p = prompt.trim();
11871
- if (p.length < 24) return false;
12003
+ if (p.length < 24) return { buildVerb: false, scaleCount: 0, multiPart: false };
11872
12004
  const buildVerb = /\b(?:build|implement|create|develop|design|scaffold|refactor|migrate|architect|set up|integrate)\b/i.test(p);
11873
- 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);
12005
+ 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;
11874
12006
  const multiPart = /(?:\b(?:and|then|also|plus|as well as)\b.*\b(?:and|then|also)\b)|(?:^|\n)\s*(?:[-*]|\d+[.)])\s+/i.test(p);
11875
- return buildVerb && (scaleNoun || multiPart);
12007
+ return { buildVerb, scaleCount, multiPart };
12008
+ }
12009
+ /**
12010
+ * A build prompt with REAL scale: multiple system-level deliverables, or a
12011
+ * deliverable plus explicitly multi-part phrasing. Only these floor to the
12012
+ * full T1→T2→T3 hierarchy — "create a todo app" is a build prompt too, but
12013
+ * flooring every small build to Complex was the #1 token bomb (3-5 managers
12014
+ * × workers for a task one worker handles).
12015
+ */
12016
+ looksClearlyComplex(prompt) {
12017
+ const s = this.buildSignals(prompt);
12018
+ return s.buildVerb && (s.scaleCount >= 2 || s.scaleCount >= 1 && s.multiPart);
12019
+ }
12020
+ /** A small single-deliverable build — real work, but one manager's worth. */
12021
+ looksLikeModerateBuild(prompt) {
12022
+ const s = this.buildSignals(prompt);
12023
+ return s.buildVerb && (s.scaleCount >= 1 || s.multiPart);
11876
12024
  }
11877
12025
  // Cache glob scan results per workspace path to avoid repeated I/O.
11878
12026
  static globCache = /* @__PURE__ */ new Map();
@@ -11960,6 +12108,9 @@ ${prompt}` : prompt;
11960
12108
  if (verdict !== "Complex" && this.looksClearlyComplex(prompt)) {
11961
12109
  this.recordDecision("complexity", `Complex \u2014 heuristic floor over classifier "${verdict}": explicit multi-step build/implementation signals (T1 engaged)`);
11962
12110
  verdict = "Complex";
12111
+ } else if (verdict === "Simple" && this.looksLikeModerateBuild(prompt)) {
12112
+ this.recordDecision("complexity", 'Moderate \u2014 heuristic floor over classifier "Simple": build signals without multi-system scale (single manager)');
12113
+ verdict = "Moderate";
11963
12114
  } else {
11964
12115
  this.recordDecision("complexity", `${verdict} \u2014 classifier: ${reason || "no reason given"}`);
11965
12116
  }