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.js CHANGED
@@ -59,7 +59,7 @@ var __export = (target, all) => {
59
59
  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;
60
60
  var init_constants = __esm({
61
61
  "src/constants.ts"() {
62
- CASCADE_VERSION = "0.18.0";
62
+ CASCADE_VERSION = "0.19.1";
63
63
  CASCADE_CONFIG_FILE = ".cascade/config.json";
64
64
  CASCADE_DB_FILE = ".cascade/memory.db";
65
65
  CASCADE_DASHBOARD_SECRET_FILE = ".cascade/dashboard-secret";
@@ -885,8 +885,26 @@ var init_openai = __esm({
885
885
  // src/providers/azure.ts
886
886
  var azure_exports = {};
887
887
  __export(azure_exports, {
888
- AzureOpenAIProvider: () => AzureOpenAIProvider
888
+ AzureOpenAIProvider: () => AzureOpenAIProvider,
889
+ azureModelForDeployment: () => azureModelForDeployment
889
890
  });
891
+ function azureModelForDeployment(cfg) {
892
+ if (cfg.type !== "azure" || !cfg.deploymentName?.trim()) return null;
893
+ const id = cfg.deploymentName.trim();
894
+ return {
895
+ id,
896
+ name: cfg.label?.trim() || id,
897
+ provider: "azure",
898
+ contextWindow: 128e3,
899
+ isVisionCapable: false,
900
+ inputCostPer1kTokens: 25e-4,
901
+ outputCostPer1kTokens: 0.01,
902
+ maxOutputTokens: 16e3,
903
+ supportsStreaming: true,
904
+ isLocal: false,
905
+ supportsToolUse: true
906
+ };
907
+ }
890
908
  var AzureOpenAIProvider;
891
909
  var init_azure = __esm({
892
910
  "src/providers/azure.ts"() {
@@ -912,17 +930,31 @@ var init_azure = __esm({
912
930
  });
913
931
  }
914
932
  async listModels() {
915
- return [this.model];
933
+ const fromDeployment = azureModelForDeployment(this.config);
934
+ return [fromDeployment ?? this.model];
916
935
  }
917
936
  async isAvailable() {
937
+ const params = {
938
+ model: this.model.id,
939
+ messages: [{ role: "user", content: "ping" }],
940
+ max_tokens: 1
941
+ };
918
942
  try {
919
- await this.client.chat.completions.create({
920
- model: this.model.id,
921
- messages: [{ role: "user", content: "ping" }],
922
- max_tokens: 1
923
- });
943
+ await this.client.chat.completions.create(params);
924
944
  return true;
925
- } catch {
945
+ } catch (err) {
946
+ if (err?.message && String(err.message).includes("max_completion_tokens")) {
947
+ try {
948
+ await this.client.chat.completions.create({
949
+ model: this.model.id,
950
+ messages: [{ role: "user", content: "ping" }],
951
+ max_completion_tokens: 1
952
+ });
953
+ return true;
954
+ } catch {
955
+ return false;
956
+ }
957
+ }
926
958
  return false;
927
959
  }
928
960
  }
@@ -2997,8 +3029,11 @@ var CascadeConfigSchema = z.object({
2997
3029
  * Cascade Auto: when true, the TaskAnalyzer selects the optimal model for each
2998
3030
  * tier based on task type and complexity, overriding the static priority lists.
2999
3031
  * Heuristic-first with AI inference fallback (adds ~0–500ms per task).
3032
+ * ON by default since v0.19.0 — "Auto" without it was just a static priority
3033
+ * list, not the benchmark-value routing the docs describe. Explicit per-tier
3034
+ * model pins are unaffected; disable via config/Settings → Advanced.
3000
3035
  */
3001
- cascadeAuto: z.boolean().default(false),
3036
+ cascadeAuto: z.boolean().default(true),
3002
3037
  /**
3003
3038
  * Cascade Auto trade-off bias when picking a model for a task:
3004
3039
  * - 'balanced' (default): quality × cost-efficiency — cheap models win
@@ -3800,6 +3835,10 @@ var ModelSelector = class {
3800
3835
  actualId = parts.slice(1).join(":");
3801
3836
  }
3802
3837
  }
3838
+ const registered = this.availableModels.get(actualId);
3839
+ if (registered && (!providerStr || registered.provider === providerStr)) {
3840
+ return registered;
3841
+ }
3803
3842
  if (!providerStr) {
3804
3843
  const lower = actualId.toLowerCase();
3805
3844
  if (lower.includes("claude")) providerStr = "anthropic";
@@ -4552,6 +4591,12 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter {
4552
4591
  const results = await Promise.all(ocConfigs.map((cfg) => this.discoverOpenAICompatibleModels(cfg)));
4553
4592
  if (results.some(Boolean)) this.selector.markProviderAvailable("openai-compatible");
4554
4593
  }
4594
+ if (availableProviders.has("azure")) {
4595
+ for (const cfg of config.providers) {
4596
+ const model = azureModelForDeployment(cfg);
4597
+ if (model) this.selector.addDynamicModel(model);
4598
+ }
4599
+ }
4555
4600
  for (const tier of ["T1", "T2", "T3"]) {
4556
4601
  const override = tier === "T1" ? config.models.t1 : tier === "T2" ? config.models.t2 : config.models.t3;
4557
4602
  if (!override || override === "auto") continue;
@@ -5158,7 +5203,7 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter {
5158
5203
  ensureProvider(model, configs) {
5159
5204
  const key = `${model.provider}:${model.id}`;
5160
5205
  if (this.providers.has(key)) return;
5161
- const cfg = configs.find((c) => c.type === model.provider) ?? { type: model.provider };
5206
+ 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 };
5162
5207
  const provider = this.createProvider(cfg, model);
5163
5208
  this.providers.set(key, provider);
5164
5209
  }
@@ -5318,6 +5363,12 @@ var BaseTier = class extends EventEmitter {
5318
5363
  * which would interleave, are not tagged.
5319
5364
  */
5320
5365
  isPresenter = false;
5366
+ /**
5367
+ * The model actually serving this tier (`provider:id`), once resolved —
5368
+ * rides on every tier:status event so the desktop can show which model ran
5369
+ * which node (Cockpit node panel / Why panel).
5370
+ */
5371
+ servingModel;
5321
5372
  constructor(role, id, parentId) {
5322
5373
  super();
5323
5374
  this.role = role;
@@ -5342,11 +5393,16 @@ var BaseTier = class extends EventEmitter {
5342
5393
  label: this.label,
5343
5394
  status,
5344
5395
  timestamp,
5345
- output
5396
+ output,
5397
+ model: this.servingModel
5346
5398
  };
5347
5399
  this.emit("status", event);
5348
5400
  this.emit("tier:status", event);
5349
5401
  }
5402
+ /** Record the model serving this tier; future status events carry it. */
5403
+ setServingModel(model) {
5404
+ this.servingModel = model || void 0;
5405
+ }
5350
5406
  setLabel(label) {
5351
5407
  this.label = label;
5352
5408
  }
@@ -5369,7 +5425,8 @@ var BaseTier = class extends EventEmitter {
5369
5425
  currentAction: update.currentAction,
5370
5426
  progressPct: update.progressPct,
5371
5427
  timestamp,
5372
- output: update.output
5428
+ output: update.output,
5429
+ model: this.servingModel
5373
5430
  });
5374
5431
  }
5375
5432
  buildMessage(type, to, payload) {
@@ -5736,6 +5793,21 @@ Available tools: ${tools.map((t) => t.name).join(", ")}.
5736
5793
  When you have enough information, stop calling tools and write your final answer.`;
5737
5794
  }
5738
5795
 
5796
+ // src/utils/truncate.ts
5797
+ function truncateForContext(text, maxChars = 12e3) {
5798
+ if (text.length <= maxChars) return text;
5799
+ const headLen = Math.floor(maxChars * 0.75);
5800
+ const tailLen = maxChars - headLen;
5801
+ const head = text.slice(0, headLen);
5802
+ const tail = text.slice(-tailLen);
5803
+ const elided = text.length - headLen - tailLen;
5804
+ return `${head}
5805
+
5806
+ [... ${elided.toLocaleString()} characters elided to keep context small \u2014 re-read the file with a line range if you need the middle ...]
5807
+
5808
+ ${tail}`;
5809
+ }
5810
+
5739
5811
  // src/core/tiers/t3-worker.ts
5740
5812
  var CriticalToolError = class extends Error {
5741
5813
  constructor(message, toolName) {
@@ -6043,6 +6115,7 @@ Now execute your subtask using this context where relevant.`
6043
6115
  } catch {
6044
6116
  }
6045
6117
  const effectiveModel = subtaskModel ?? this.router.getModelForTier("T3");
6118
+ if (effectiveModel) this.setServingModel(`${effectiveModel.provider}:${effectiveModel.id}`);
6046
6119
  const useTextTools = effectiveModel?.supportsToolUse === false && tools.length > 0;
6047
6120
  let sentFullTextContract = false;
6048
6121
  let textContractSignature = "";
@@ -6140,7 +6213,7 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : "") + textToolSuffix,
6140
6213
  const toolResult = await this.executeTool(tc);
6141
6214
  await this.context.addMessage({
6142
6215
  role: "tool",
6143
- content: toolResult,
6216
+ content: truncateForContext(toolResult),
6144
6217
  toolCallId: tc.id
6145
6218
  });
6146
6219
  }
@@ -6397,15 +6470,17 @@ ${assignment.expectedOutput}`;
6397
6470
  };
6398
6471
  }
6399
6472
  requiresArtifact() {
6473
+ if (this.assignment?.files?.length) return true;
6400
6474
  const haystack = `${this.assignment?.description ?? ""}
6401
6475
  ${this.assignment?.expectedOutput ?? ""}`;
6402
6476
  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);
6403
6477
  }
6404
6478
  extractArtifactPaths(assignment) {
6479
+ const declared = (assignment.files ?? []).map((f) => f.trim()).filter((f) => f.includes("."));
6405
6480
  const haystack = `${assignment.description}
6406
6481
  ${assignment.expectedOutput}`;
6407
6482
  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) ?? [];
6408
- return [...new Set(matches.map((m) => m.trim()))];
6483
+ return [.../* @__PURE__ */ new Set([...declared, ...matches.map((m) => m.trim())])];
6409
6484
  }
6410
6485
  async verifyArtifacts(assignment) {
6411
6486
  const artifactPaths = this.extractArtifactPaths(assignment);
@@ -6528,7 +6603,9 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
6528
6603
  Assignment: ${assignment.description}
6529
6604
  Expected output: ${assignment.expectedOutput}
6530
6605
  Constraints: ${assignment.constraints.join("; ")}
6531
-
6606
+ ${assignment.acceptance?.length ? `Acceptance criteria \u2014 ALL must be satisfied for "completeness" to pass:
6607
+ ${assignment.acceptance.map((a) => `- ${a}`).join("\n")}
6608
+ ` : ""}
6532
6609
  Output to test:
6533
6610
  ${output}
6534
6611
 
@@ -6617,17 +6694,27 @@ Your subtask:
6617
6694
  - Title: ${assignment.subtaskTitle}
6618
6695
  - Description: ${assignment.description}
6619
6696
  - Expected output: ${assignment.expectedOutput}
6620
- - Constraints: ${assignment.constraints.join("; ")}`;
6697
+ - Constraints: ${assignment.constraints.join("; ")}${assignment.files?.length ? `
6698
+ - Files you own (create/edit ONLY these): ${assignment.files.join(", ")}` : ""}${assignment.acceptance?.length ? `
6699
+ - Definition of done: ${assignment.acceptance.join("; ")}` : ""}`;
6621
6700
  }
6622
6701
  buildInitialPrompt(assignment) {
6623
6702
  return `Execute the following subtask completely:
6624
6703
 
6625
6704
  **${assignment.subtaskTitle}**
6626
-
6705
+ ${assignment.contextBrief ? `
6706
+ Context: ${assignment.contextBrief}
6707
+ ` : ""}
6627
6708
  ${assignment.description}
6628
6709
 
6629
6710
  Expected output: ${assignment.expectedOutput}
6630
-
6711
+ ${assignment.files?.length ? `
6712
+ Files you own (create or edit exactly these paths):
6713
+ ${assignment.files.map((f) => `- ${f}`).join("\n")}
6714
+ ` : ""}${assignment.acceptance?.length ? `
6715
+ Definition of done (your output must satisfy ALL of these):
6716
+ ${assignment.acceptance.map((a) => `- ${a}`).join("\n")}
6717
+ ` : ""}
6631
6718
  Constraints:
6632
6719
  ${assignment.constraints.map((c) => `- ${c}`).join("\n")}
6633
6720
 
@@ -7108,6 +7195,8 @@ var T2Manager = class extends BaseTier {
7108
7195
  this.assignment = assignment;
7109
7196
  this.taskId = taskId;
7110
7197
  this.setLabel(assignment.sectionTitle);
7198
+ const m = this.router.getModelForTier("T2");
7199
+ if (m) this.setServingModel(`${m.provider}:${m.id}`);
7111
7200
  this.setStatus("ACTIVE");
7112
7201
  this.sendStatusUpdate({
7113
7202
  progressPct: 0,
@@ -7194,7 +7283,7 @@ Guidance (must be followed): ${decision.note}`
7194
7283
  // ── Private ──────────────────────────────────
7195
7284
  async decomposeSection(assignment) {
7196
7285
  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");
7197
- const prompt = `Decompose this section into 2-5 concrete subtasks for T3 workers.
7286
+ 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).
7198
7287
 
7199
7288
  Section: ${assignment.sectionTitle}
7200
7289
  Description: ${assignment.description}
@@ -7213,6 +7302,9 @@ Return a JSON array of subtask objects, each with:
7213
7302
  - peerT3Ids: string[] (empty for now)
7214
7303
  - dependsOn: string[] (array of subtaskIds this task depends on to start)
7215
7304
  - executionMode: "parallel|sequential" (default is parallel)
7305
+ - files: string[] (the EXACT relative paths this subtask creates or edits)
7306
+ - acceptance: string[] (1-3 mechanically checkable done-criteria: file exists / contains X / command exits 0)
7307
+ - contextBrief: string (1-3 short sentences with ALL the background the worker needs \u2014 it sees nothing else)
7216
7308
 
7217
7309
  Return ONLY the JSON array.`;
7218
7310
  const messages = [{ role: "user", content: prompt }];
@@ -7813,6 +7905,8 @@ var T1Administrator = class extends BaseTier {
7813
7905
  this.signal = signal;
7814
7906
  this.taskId = randomUUID();
7815
7907
  this.setLabel("Administrator");
7908
+ const m = this.router.getModelForTier("T1");
7909
+ if (m) this.setServingModel(`${m.provider}:${m.id}`);
7816
7910
  this.setStatus("ACTIVE");
7817
7911
  this.taskGoal = userPrompt;
7818
7912
  this.sendStatusUpdate({
@@ -8059,10 +8153,13 @@ Return JSON where SECTIONS can declare dependencies on other SECTIONS:
8059
8153
  "description": "Run npm init",
8060
8154
  "expectedOutput": "package.json created",
8061
8155
  "constraints": [],
8062
- "dependsOn": []
8156
+ "dependsOn": [],
8157
+ "files": ["package.json"], // \u2190 exact paths this subtask owns
8158
+ "acceptance": ["package.json exists and parses as JSON"], // \u2190 objectively checkable
8159
+ "contextBrief": "Fresh Node 20 project; npm available." // \u2190 ALL the background the worker gets
8063
8160
  }]
8064
8161
  }, {
8065
- "sectionId": "s2",
8162
+ "sectionId": "s2",
8066
8163
  "sectionTitle": "Write Tests",
8067
8164
  "description": "Write tests for the project",
8068
8165
  "expectedOutput": "Tests passing",
@@ -8072,7 +8169,13 @@ Return JSON where SECTIONS can declare dependencies on other SECTIONS:
8072
8169
  }]
8073
8170
  }
8074
8171
  Use dependsOn at the SECTION level when a whole T2 Manager needs the output of a previous T2 Manager.
8075
- Leave dependsOn empty for sections that can run immediately in parallel.`;
8172
+ Leave dependsOn empty for sections that can run immediately in parallel.
8173
+
8174
+ SPEC RULES \u2014 each subtask is a self-contained spec slice (workers execute from their slice ALONE):
8175
+ - "files": the exact relative paths the subtask creates or edits. Never vague ("some files"); always concrete.
8176
+ - "acceptance": 1-3 checks a reviewer could verify mechanically (file exists / contains X / command exits 0). These define done.
8177
+ - "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.
8178
+ - 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.`;
8076
8179
  const messages = [{ role: "user", content: decompositionPrompt }];
8077
8180
  const result = await this.router.generate("T1", {
8078
8181
  messages,
@@ -9380,30 +9483,56 @@ async function searchTavily(query, apiKey, maxResults) {
9380
9483
  engine: "tavily"
9381
9484
  }));
9382
9485
  }
9383
- async function searchDuckDuckGoLite(query, maxResults) {
9384
- const resp = await fetch(`https://lite.duckduckgo.com/lite/?q=${encodeURIComponent(query)}`, {
9385
- headers: { "User-Agent": "Mozilla/5.0 (compatible; Cascade-AI/1.0)" },
9386
- signal: AbortSignal.timeout(1e4)
9387
- });
9388
- if (!resp.ok) throw new Error(`DuckDuckGo Lite returned HTTP ${resp.status}`);
9389
- const html = await resp.text();
9390
- const linkPattern = /<a[^>]+class="result-link"[^>]+href="([^"]+)"[^>]*>([^<]+)<\/a>/g;
9391
- const snippetPattern = /<td[^>]+class="result-snippet"[^>]*>([\s\S]*?)<\/td>/g;
9392
- const links = [];
9393
- const snippets = [];
9486
+ var BROWSER_UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0 Safari/537.36";
9487
+ function unwrapDdgRedirect(href) {
9488
+ try {
9489
+ const url = new URL(href.startsWith("//") ? `https:${href}` : href, "https://duckduckgo.com");
9490
+ if (/(^|\.)duckduckgo\.com$/i.test(url.hostname) && url.pathname.startsWith("/l/")) {
9491
+ const target = url.searchParams.get("uddg");
9492
+ if (target) return decodeURIComponent(target);
9493
+ }
9494
+ return url.protocol === "http:" || url.protocol === "https:" ? url.toString() : href;
9495
+ } catch {
9496
+ return href;
9497
+ }
9498
+ }
9499
+ function stripTags(html) {
9500
+ return html.replace(/<[^>]+>/g, "").replace(/\s+/g, " ").trim();
9501
+ }
9502
+ function decodeEntities(text) {
9503
+ return text.replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&#x27;|&#39;/g, "'").replace(/&nbsp;/g, " ");
9504
+ }
9505
+ function parseDdgAnchors(html, anchorClass, snippetClass) {
9506
+ const anchorRe = new RegExp(`<a\\b[^>]*class=["']?[^"'>]*\\b${anchorClass}\\b[^"'>]*["']?[^>]*>([\\s\\S]*?)<\\/a>`, "gi");
9507
+ const snippetRe = new RegExp(`class=["']?[^"'>]*\\b${snippetClass}\\b[^"'>]*["']?[^>]*>([\\s\\S]*?)<\\/(?:td|a|div|span)>`, "gi");
9508
+ const results = [];
9394
9509
  let m;
9395
- while ((m = linkPattern.exec(html)) !== null) {
9396
- links.push({ url: m[1], title: m[2].trim() });
9510
+ while ((m = anchorRe.exec(html)) !== null) {
9511
+ const tag = m[0];
9512
+ const href = /href=["']([^"']+)["']/i.exec(tag)?.[1];
9513
+ const title = decodeEntities(stripTags(m[1] ?? ""));
9514
+ if (!href || !title) continue;
9515
+ results.push({ title, url: unwrapDdgRedirect(decodeEntities(href)), snippet: "" });
9397
9516
  }
9398
- while ((m = snippetPattern.exec(html)) !== null) {
9399
- snippets.push(m[1].replace(/<[^>]+>/g, "").trim());
9517
+ const snippets = [];
9518
+ while ((m = snippetRe.exec(html)) !== null) {
9519
+ snippets.push(decodeEntities(stripTags(m[1] ?? "")));
9400
9520
  }
9401
- return links.slice(0, maxResults).map((link, i) => ({
9402
- title: link.title,
9403
- url: link.url,
9404
- snippet: snippets[i] ?? "",
9405
- engine: "duckduckgo-lite"
9406
- }));
9521
+ for (let i = 0; i < results.length; i++) {
9522
+ if (snippets[i]) results[i].snippet = snippets[i];
9523
+ }
9524
+ return results;
9525
+ }
9526
+ async function searchDuckDuckGo(query, maxResults, variant) {
9527
+ const base = variant === "html" ? "https://html.duckduckgo.com/html/?q=" : "https://lite.duckduckgo.com/lite/?q=";
9528
+ const resp = await fetch(`${base}${encodeURIComponent(query)}`, {
9529
+ headers: { "User-Agent": BROWSER_UA, Accept: "text/html" },
9530
+ signal: AbortSignal.timeout(1e4)
9531
+ });
9532
+ if (!resp.ok) throw new Error(`DuckDuckGo ${variant} returned HTTP ${resp.status}`);
9533
+ const html = await resp.text();
9534
+ const parsed = variant === "html" ? parseDdgAnchors(html, "result__a", "result__snippet") : parseDdgAnchors(html, "result-link", "result-snippet");
9535
+ return parsed.slice(0, maxResults).map((r) => ({ ...r, engine: `duckduckgo-${variant}` }));
9407
9536
  }
9408
9537
  var WebSearchTool = class extends BaseTool {
9409
9538
  name = "web_search";
@@ -9462,12 +9591,14 @@ var WebSearchTool = class extends BaseTool {
9462
9591
  errors.push(`Tavily: ${err instanceof Error ? err.message : String(err)}`);
9463
9592
  }
9464
9593
  }
9465
- try {
9466
- results = await searchDuckDuckGoLite(query, maxResults);
9467
- if (results.length > 0) return this.formatResults(query, results);
9468
- errors.push("DuckDuckGo Lite: returned 0 results");
9469
- } catch (err) {
9470
- errors.push(`DuckDuckGo Lite: ${err instanceof Error ? err.message : String(err)}`);
9594
+ for (const variant of ["html", "lite"]) {
9595
+ try {
9596
+ results = await searchDuckDuckGo(query, maxResults, variant);
9597
+ if (results.length > 0) return this.formatResults(query, results);
9598
+ errors.push(`DuckDuckGo ${variant}: returned 0 results`);
9599
+ } catch (err) {
9600
+ errors.push(`DuckDuckGo ${variant}: ${err instanceof Error ? err.message : String(err)}`);
9601
+ }
9471
9602
  }
9472
9603
  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" : "";
9473
9604
  return [
@@ -11814,13 +11945,30 @@ ${last.partialOutput}` : "");
11814
11945
  * explicit multi-part structure, so ordinary single-file asks (handled as
11815
11946
  * Simple/Moderate) don't get over-escalated.
11816
11947
  */
11817
- looksClearlyComplex(prompt) {
11948
+ /** Shared build/scale signals for the complexity floors below. */
11949
+ buildSignals(prompt) {
11818
11950
  const p = prompt.trim();
11819
- if (p.length < 24) return false;
11951
+ if (p.length < 24) return { buildVerb: false, scaleCount: 0, multiPart: false };
11820
11952
  const buildVerb = /\b(?:build|implement|create|develop|design|scaffold|refactor|migrate|architect|set up|integrate)\b/i.test(p);
11821
- 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);
11953
+ 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;
11822
11954
  const multiPart = /(?:\b(?:and|then|also|plus|as well as)\b.*\b(?:and|then|also)\b)|(?:^|\n)\s*(?:[-*]|\d+[.)])\s+/i.test(p);
11823
- return buildVerb && (scaleNoun || multiPart);
11955
+ return { buildVerb, scaleCount, multiPart };
11956
+ }
11957
+ /**
11958
+ * A build prompt with REAL scale: multiple system-level deliverables, or a
11959
+ * deliverable plus explicitly multi-part phrasing. Only these floor to the
11960
+ * full T1→T2→T3 hierarchy — "create a todo app" is a build prompt too, but
11961
+ * flooring every small build to Complex was the #1 token bomb (3-5 managers
11962
+ * × workers for a task one worker handles).
11963
+ */
11964
+ looksClearlyComplex(prompt) {
11965
+ const s = this.buildSignals(prompt);
11966
+ return s.buildVerb && (s.scaleCount >= 2 || s.scaleCount >= 1 && s.multiPart);
11967
+ }
11968
+ /** A small single-deliverable build — real work, but one manager's worth. */
11969
+ looksLikeModerateBuild(prompt) {
11970
+ const s = this.buildSignals(prompt);
11971
+ return s.buildVerb && (s.scaleCount >= 1 || s.multiPart);
11824
11972
  }
11825
11973
  // Cache glob scan results per workspace path to avoid repeated I/O.
11826
11974
  static globCache = /* @__PURE__ */ new Map();
@@ -11908,6 +12056,9 @@ ${prompt}` : prompt;
11908
12056
  if (verdict !== "Complex" && this.looksClearlyComplex(prompt)) {
11909
12057
  this.recordDecision("complexity", `Complex \u2014 heuristic floor over classifier "${verdict}": explicit multi-step build/implementation signals (T1 engaged)`);
11910
12058
  verdict = "Complex";
12059
+ } else if (verdict === "Simple" && this.looksLikeModerateBuild(prompt)) {
12060
+ this.recordDecision("complexity", 'Moderate \u2014 heuristic floor over classifier "Simple": build signals without multi-system scale (single manager)');
12061
+ verdict = "Moderate";
11911
12062
  } else {
11912
12063
  this.recordDecision("complexity", `${verdict} \u2014 classifier: ${reason || "no reason given"}`);
11913
12064
  }