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/cli.cjs +182 -48
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +182 -48
- package/dist/cli.js.map +1 -1
- package/dist/desktop-core.cjs +180 -47
- package/dist/index.cjs +180 -47
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +30 -0
- package/dist/index.d.ts +30 -0
- package/dist/index.js +180 -47
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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.
|
|
62
|
+
CASCADE_VERSION = "0.19.0";
|
|
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,7 +930,8 @@ var init_azure = __esm({
|
|
|
912
930
|
});
|
|
913
931
|
}
|
|
914
932
|
async listModels() {
|
|
915
|
-
|
|
933
|
+
const fromDeployment = azureModelForDeployment(this.config);
|
|
934
|
+
return [fromDeployment ?? this.model];
|
|
916
935
|
}
|
|
917
936
|
async isAvailable() {
|
|
918
937
|
try {
|
|
@@ -2997,8 +3016,11 @@ var CascadeConfigSchema = z.object({
|
|
|
2997
3016
|
* Cascade Auto: when true, the TaskAnalyzer selects the optimal model for each
|
|
2998
3017
|
* tier based on task type and complexity, overriding the static priority lists.
|
|
2999
3018
|
* Heuristic-first with AI inference fallback (adds ~0–500ms per task).
|
|
3019
|
+
* ON by default since v0.19.0 — "Auto" without it was just a static priority
|
|
3020
|
+
* list, not the benchmark-value routing the docs describe. Explicit per-tier
|
|
3021
|
+
* model pins are unaffected; disable via config/Settings → Advanced.
|
|
3000
3022
|
*/
|
|
3001
|
-
cascadeAuto: z.boolean().default(
|
|
3023
|
+
cascadeAuto: z.boolean().default(true),
|
|
3002
3024
|
/**
|
|
3003
3025
|
* Cascade Auto trade-off bias when picking a model for a task:
|
|
3004
3026
|
* - 'balanced' (default): quality × cost-efficiency — cheap models win
|
|
@@ -4552,6 +4574,12 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter {
|
|
|
4552
4574
|
const results = await Promise.all(ocConfigs.map((cfg) => this.discoverOpenAICompatibleModels(cfg)));
|
|
4553
4575
|
if (results.some(Boolean)) this.selector.markProviderAvailable("openai-compatible");
|
|
4554
4576
|
}
|
|
4577
|
+
if (availableProviders.has("azure")) {
|
|
4578
|
+
for (const cfg of config.providers) {
|
|
4579
|
+
const model = azureModelForDeployment(cfg);
|
|
4580
|
+
if (model) this.selector.addDynamicModel(model);
|
|
4581
|
+
}
|
|
4582
|
+
}
|
|
4555
4583
|
for (const tier of ["T1", "T2", "T3"]) {
|
|
4556
4584
|
const override = tier === "T1" ? config.models.t1 : tier === "T2" ? config.models.t2 : config.models.t3;
|
|
4557
4585
|
if (!override || override === "auto") continue;
|
|
@@ -5158,7 +5186,7 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter {
|
|
|
5158
5186
|
ensureProvider(model, configs) {
|
|
5159
5187
|
const key = `${model.provider}:${model.id}`;
|
|
5160
5188
|
if (this.providers.has(key)) return;
|
|
5161
|
-
const cfg = configs.find((c) => c.type === model.provider) ?? { type: model.provider };
|
|
5189
|
+
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
5190
|
const provider = this.createProvider(cfg, model);
|
|
5163
5191
|
this.providers.set(key, provider);
|
|
5164
5192
|
}
|
|
@@ -5318,6 +5346,12 @@ var BaseTier = class extends EventEmitter {
|
|
|
5318
5346
|
* which would interleave, are not tagged.
|
|
5319
5347
|
*/
|
|
5320
5348
|
isPresenter = false;
|
|
5349
|
+
/**
|
|
5350
|
+
* The model actually serving this tier (`provider:id`), once resolved —
|
|
5351
|
+
* rides on every tier:status event so the desktop can show which model ran
|
|
5352
|
+
* which node (Cockpit node panel / Why panel).
|
|
5353
|
+
*/
|
|
5354
|
+
servingModel;
|
|
5321
5355
|
constructor(role, id, parentId) {
|
|
5322
5356
|
super();
|
|
5323
5357
|
this.role = role;
|
|
@@ -5342,11 +5376,16 @@ var BaseTier = class extends EventEmitter {
|
|
|
5342
5376
|
label: this.label,
|
|
5343
5377
|
status,
|
|
5344
5378
|
timestamp,
|
|
5345
|
-
output
|
|
5379
|
+
output,
|
|
5380
|
+
model: this.servingModel
|
|
5346
5381
|
};
|
|
5347
5382
|
this.emit("status", event);
|
|
5348
5383
|
this.emit("tier:status", event);
|
|
5349
5384
|
}
|
|
5385
|
+
/** Record the model serving this tier; future status events carry it. */
|
|
5386
|
+
setServingModel(model) {
|
|
5387
|
+
this.servingModel = model || void 0;
|
|
5388
|
+
}
|
|
5350
5389
|
setLabel(label) {
|
|
5351
5390
|
this.label = label;
|
|
5352
5391
|
}
|
|
@@ -5369,7 +5408,8 @@ var BaseTier = class extends EventEmitter {
|
|
|
5369
5408
|
currentAction: update.currentAction,
|
|
5370
5409
|
progressPct: update.progressPct,
|
|
5371
5410
|
timestamp,
|
|
5372
|
-
output: update.output
|
|
5411
|
+
output: update.output,
|
|
5412
|
+
model: this.servingModel
|
|
5373
5413
|
});
|
|
5374
5414
|
}
|
|
5375
5415
|
buildMessage(type, to, payload) {
|
|
@@ -5736,6 +5776,21 @@ Available tools: ${tools.map((t) => t.name).join(", ")}.
|
|
|
5736
5776
|
When you have enough information, stop calling tools and write your final answer.`;
|
|
5737
5777
|
}
|
|
5738
5778
|
|
|
5779
|
+
// src/utils/truncate.ts
|
|
5780
|
+
function truncateForContext(text, maxChars = 12e3) {
|
|
5781
|
+
if (text.length <= maxChars) return text;
|
|
5782
|
+
const headLen = Math.floor(maxChars * 0.75);
|
|
5783
|
+
const tailLen = maxChars - headLen;
|
|
5784
|
+
const head = text.slice(0, headLen);
|
|
5785
|
+
const tail = text.slice(-tailLen);
|
|
5786
|
+
const elided = text.length - headLen - tailLen;
|
|
5787
|
+
return `${head}
|
|
5788
|
+
|
|
5789
|
+
[... ${elided.toLocaleString()} characters elided to keep context small \u2014 re-read the file with a line range if you need the middle ...]
|
|
5790
|
+
|
|
5791
|
+
${tail}`;
|
|
5792
|
+
}
|
|
5793
|
+
|
|
5739
5794
|
// src/core/tiers/t3-worker.ts
|
|
5740
5795
|
var CriticalToolError = class extends Error {
|
|
5741
5796
|
constructor(message, toolName) {
|
|
@@ -6043,6 +6098,7 @@ Now execute your subtask using this context where relevant.`
|
|
|
6043
6098
|
} catch {
|
|
6044
6099
|
}
|
|
6045
6100
|
const effectiveModel = subtaskModel ?? this.router.getModelForTier("T3");
|
|
6101
|
+
if (effectiveModel) this.setServingModel(`${effectiveModel.provider}:${effectiveModel.id}`);
|
|
6046
6102
|
const useTextTools = effectiveModel?.supportsToolUse === false && tools.length > 0;
|
|
6047
6103
|
let sentFullTextContract = false;
|
|
6048
6104
|
let textContractSignature = "";
|
|
@@ -6140,7 +6196,7 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : "") + textToolSuffix,
|
|
|
6140
6196
|
const toolResult = await this.executeTool(tc);
|
|
6141
6197
|
await this.context.addMessage({
|
|
6142
6198
|
role: "tool",
|
|
6143
|
-
content: toolResult,
|
|
6199
|
+
content: truncateForContext(toolResult),
|
|
6144
6200
|
toolCallId: tc.id
|
|
6145
6201
|
});
|
|
6146
6202
|
}
|
|
@@ -6397,15 +6453,17 @@ ${assignment.expectedOutput}`;
|
|
|
6397
6453
|
};
|
|
6398
6454
|
}
|
|
6399
6455
|
requiresArtifact() {
|
|
6456
|
+
if (this.assignment?.files?.length) return true;
|
|
6400
6457
|
const haystack = `${this.assignment?.description ?? ""}
|
|
6401
6458
|
${this.assignment?.expectedOutput ?? ""}`;
|
|
6402
6459
|
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
6460
|
}
|
|
6404
6461
|
extractArtifactPaths(assignment) {
|
|
6462
|
+
const declared = (assignment.files ?? []).map((f) => f.trim()).filter((f) => f.includes("."));
|
|
6405
6463
|
const haystack = `${assignment.description}
|
|
6406
6464
|
${assignment.expectedOutput}`;
|
|
6407
6465
|
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 [
|
|
6466
|
+
return [.../* @__PURE__ */ new Set([...declared, ...matches.map((m) => m.trim())])];
|
|
6409
6467
|
}
|
|
6410
6468
|
async verifyArtifacts(assignment) {
|
|
6411
6469
|
const artifactPaths = this.extractArtifactPaths(assignment);
|
|
@@ -6528,7 +6586,9 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
|
6528
6586
|
Assignment: ${assignment.description}
|
|
6529
6587
|
Expected output: ${assignment.expectedOutput}
|
|
6530
6588
|
Constraints: ${assignment.constraints.join("; ")}
|
|
6531
|
-
|
|
6589
|
+
${assignment.acceptance?.length ? `Acceptance criteria \u2014 ALL must be satisfied for "completeness" to pass:
|
|
6590
|
+
${assignment.acceptance.map((a) => `- ${a}`).join("\n")}
|
|
6591
|
+
` : ""}
|
|
6532
6592
|
Output to test:
|
|
6533
6593
|
${output}
|
|
6534
6594
|
|
|
@@ -6617,17 +6677,27 @@ Your subtask:
|
|
|
6617
6677
|
- Title: ${assignment.subtaskTitle}
|
|
6618
6678
|
- Description: ${assignment.description}
|
|
6619
6679
|
- Expected output: ${assignment.expectedOutput}
|
|
6620
|
-
- Constraints: ${assignment.constraints.join("; ")}
|
|
6680
|
+
- Constraints: ${assignment.constraints.join("; ")}${assignment.files?.length ? `
|
|
6681
|
+
- Files you own (create/edit ONLY these): ${assignment.files.join(", ")}` : ""}${assignment.acceptance?.length ? `
|
|
6682
|
+
- Definition of done: ${assignment.acceptance.join("; ")}` : ""}`;
|
|
6621
6683
|
}
|
|
6622
6684
|
buildInitialPrompt(assignment) {
|
|
6623
6685
|
return `Execute the following subtask completely:
|
|
6624
6686
|
|
|
6625
6687
|
**${assignment.subtaskTitle}**
|
|
6626
|
-
|
|
6688
|
+
${assignment.contextBrief ? `
|
|
6689
|
+
Context: ${assignment.contextBrief}
|
|
6690
|
+
` : ""}
|
|
6627
6691
|
${assignment.description}
|
|
6628
6692
|
|
|
6629
6693
|
Expected output: ${assignment.expectedOutput}
|
|
6630
|
-
|
|
6694
|
+
${assignment.files?.length ? `
|
|
6695
|
+
Files you own (create or edit exactly these paths):
|
|
6696
|
+
${assignment.files.map((f) => `- ${f}`).join("\n")}
|
|
6697
|
+
` : ""}${assignment.acceptance?.length ? `
|
|
6698
|
+
Definition of done (your output must satisfy ALL of these):
|
|
6699
|
+
${assignment.acceptance.map((a) => `- ${a}`).join("\n")}
|
|
6700
|
+
` : ""}
|
|
6631
6701
|
Constraints:
|
|
6632
6702
|
${assignment.constraints.map((c) => `- ${c}`).join("\n")}
|
|
6633
6703
|
|
|
@@ -7108,6 +7178,8 @@ var T2Manager = class extends BaseTier {
|
|
|
7108
7178
|
this.assignment = assignment;
|
|
7109
7179
|
this.taskId = taskId;
|
|
7110
7180
|
this.setLabel(assignment.sectionTitle);
|
|
7181
|
+
const m = this.router.getModelForTier("T2");
|
|
7182
|
+
if (m) this.setServingModel(`${m.provider}:${m.id}`);
|
|
7111
7183
|
this.setStatus("ACTIVE");
|
|
7112
7184
|
this.sendStatusUpdate({
|
|
7113
7185
|
progressPct: 0,
|
|
@@ -7194,7 +7266,7 @@ Guidance (must be followed): ${decision.note}`
|
|
|
7194
7266
|
// ── Private ──────────────────────────────────
|
|
7195
7267
|
async decomposeSection(assignment) {
|
|
7196
7268
|
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
|
|
7269
|
+
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
7270
|
|
|
7199
7271
|
Section: ${assignment.sectionTitle}
|
|
7200
7272
|
Description: ${assignment.description}
|
|
@@ -7213,6 +7285,9 @@ Return a JSON array of subtask objects, each with:
|
|
|
7213
7285
|
- peerT3Ids: string[] (empty for now)
|
|
7214
7286
|
- dependsOn: string[] (array of subtaskIds this task depends on to start)
|
|
7215
7287
|
- executionMode: "parallel|sequential" (default is parallel)
|
|
7288
|
+
- files: string[] (the EXACT relative paths this subtask creates or edits)
|
|
7289
|
+
- acceptance: string[] (1-3 mechanically checkable done-criteria: file exists / contains X / command exits 0)
|
|
7290
|
+
- contextBrief: string (1-3 short sentences with ALL the background the worker needs \u2014 it sees nothing else)
|
|
7216
7291
|
|
|
7217
7292
|
Return ONLY the JSON array.`;
|
|
7218
7293
|
const messages = [{ role: "user", content: prompt }];
|
|
@@ -7813,6 +7888,8 @@ var T1Administrator = class extends BaseTier {
|
|
|
7813
7888
|
this.signal = signal;
|
|
7814
7889
|
this.taskId = randomUUID();
|
|
7815
7890
|
this.setLabel("Administrator");
|
|
7891
|
+
const m = this.router.getModelForTier("T1");
|
|
7892
|
+
if (m) this.setServingModel(`${m.provider}:${m.id}`);
|
|
7816
7893
|
this.setStatus("ACTIVE");
|
|
7817
7894
|
this.taskGoal = userPrompt;
|
|
7818
7895
|
this.sendStatusUpdate({
|
|
@@ -8059,10 +8136,13 @@ Return JSON where SECTIONS can declare dependencies on other SECTIONS:
|
|
|
8059
8136
|
"description": "Run npm init",
|
|
8060
8137
|
"expectedOutput": "package.json created",
|
|
8061
8138
|
"constraints": [],
|
|
8062
|
-
"dependsOn": []
|
|
8139
|
+
"dependsOn": [],
|
|
8140
|
+
"files": ["package.json"], // \u2190 exact paths this subtask owns
|
|
8141
|
+
"acceptance": ["package.json exists and parses as JSON"], // \u2190 objectively checkable
|
|
8142
|
+
"contextBrief": "Fresh Node 20 project; npm available." // \u2190 ALL the background the worker gets
|
|
8063
8143
|
}]
|
|
8064
8144
|
}, {
|
|
8065
|
-
"sectionId": "s2",
|
|
8145
|
+
"sectionId": "s2",
|
|
8066
8146
|
"sectionTitle": "Write Tests",
|
|
8067
8147
|
"description": "Write tests for the project",
|
|
8068
8148
|
"expectedOutput": "Tests passing",
|
|
@@ -8072,7 +8152,13 @@ Return JSON where SECTIONS can declare dependencies on other SECTIONS:
|
|
|
8072
8152
|
}]
|
|
8073
8153
|
}
|
|
8074
8154
|
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
|
|
8155
|
+
Leave dependsOn empty for sections that can run immediately in parallel.
|
|
8156
|
+
|
|
8157
|
+
SPEC RULES \u2014 each subtask is a self-contained spec slice (workers execute from their slice ALONE):
|
|
8158
|
+
- "files": the exact relative paths the subtask creates or edits. Never vague ("some files"); always concrete.
|
|
8159
|
+
- "acceptance": 1-3 checks a reviewer could verify mechanically (file exists / contains X / command exits 0). These define done.
|
|
8160
|
+
- "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.
|
|
8161
|
+
- 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
8162
|
const messages = [{ role: "user", content: decompositionPrompt }];
|
|
8077
8163
|
const result = await this.router.generate("T1", {
|
|
8078
8164
|
messages,
|
|
@@ -9380,30 +9466,56 @@ async function searchTavily(query, apiKey, maxResults) {
|
|
|
9380
9466
|
engine: "tavily"
|
|
9381
9467
|
}));
|
|
9382
9468
|
}
|
|
9383
|
-
|
|
9384
|
-
|
|
9385
|
-
|
|
9386
|
-
|
|
9387
|
-
|
|
9388
|
-
|
|
9389
|
-
|
|
9390
|
-
|
|
9391
|
-
|
|
9392
|
-
|
|
9393
|
-
|
|
9469
|
+
var BROWSER_UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0 Safari/537.36";
|
|
9470
|
+
function unwrapDdgRedirect(href) {
|
|
9471
|
+
try {
|
|
9472
|
+
const url = new URL(href.startsWith("//") ? `https:${href}` : href, "https://duckduckgo.com");
|
|
9473
|
+
if (/(^|\.)duckduckgo\.com$/i.test(url.hostname) && url.pathname.startsWith("/l/")) {
|
|
9474
|
+
const target = url.searchParams.get("uddg");
|
|
9475
|
+
if (target) return decodeURIComponent(target);
|
|
9476
|
+
}
|
|
9477
|
+
return url.protocol === "http:" || url.protocol === "https:" ? url.toString() : href;
|
|
9478
|
+
} catch {
|
|
9479
|
+
return href;
|
|
9480
|
+
}
|
|
9481
|
+
}
|
|
9482
|
+
function stripTags(html) {
|
|
9483
|
+
return html.replace(/<[^>]+>/g, "").replace(/\s+/g, " ").trim();
|
|
9484
|
+
}
|
|
9485
|
+
function decodeEntities(text) {
|
|
9486
|
+
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'|'/g, "'").replace(/ /g, " ");
|
|
9487
|
+
}
|
|
9488
|
+
function parseDdgAnchors(html, anchorClass, snippetClass) {
|
|
9489
|
+
const anchorRe = new RegExp(`<a\\b[^>]*class=["']?[^"'>]*\\b${anchorClass}\\b[^"'>]*["']?[^>]*>([\\s\\S]*?)<\\/a>`, "gi");
|
|
9490
|
+
const snippetRe = new RegExp(`class=["']?[^"'>]*\\b${snippetClass}\\b[^"'>]*["']?[^>]*>([\\s\\S]*?)<\\/(?:td|a|div|span)>`, "gi");
|
|
9491
|
+
const results = [];
|
|
9394
9492
|
let m;
|
|
9395
|
-
while ((m =
|
|
9396
|
-
|
|
9493
|
+
while ((m = anchorRe.exec(html)) !== null) {
|
|
9494
|
+
const tag = m[0];
|
|
9495
|
+
const href = /href=["']([^"']+)["']/i.exec(tag)?.[1];
|
|
9496
|
+
const title = decodeEntities(stripTags(m[1] ?? ""));
|
|
9497
|
+
if (!href || !title) continue;
|
|
9498
|
+
results.push({ title, url: unwrapDdgRedirect(decodeEntities(href)), snippet: "" });
|
|
9397
9499
|
}
|
|
9398
|
-
|
|
9399
|
-
|
|
9500
|
+
const snippets = [];
|
|
9501
|
+
while ((m = snippetRe.exec(html)) !== null) {
|
|
9502
|
+
snippets.push(decodeEntities(stripTags(m[1] ?? "")));
|
|
9400
9503
|
}
|
|
9401
|
-
|
|
9402
|
-
|
|
9403
|
-
|
|
9404
|
-
|
|
9405
|
-
|
|
9406
|
-
|
|
9504
|
+
for (let i = 0; i < results.length; i++) {
|
|
9505
|
+
if (snippets[i]) results[i].snippet = snippets[i];
|
|
9506
|
+
}
|
|
9507
|
+
return results;
|
|
9508
|
+
}
|
|
9509
|
+
async function searchDuckDuckGo(query, maxResults, variant) {
|
|
9510
|
+
const base = variant === "html" ? "https://html.duckduckgo.com/html/?q=" : "https://lite.duckduckgo.com/lite/?q=";
|
|
9511
|
+
const resp = await fetch(`${base}${encodeURIComponent(query)}`, {
|
|
9512
|
+
headers: { "User-Agent": BROWSER_UA, Accept: "text/html" },
|
|
9513
|
+
signal: AbortSignal.timeout(1e4)
|
|
9514
|
+
});
|
|
9515
|
+
if (!resp.ok) throw new Error(`DuckDuckGo ${variant} returned HTTP ${resp.status}`);
|
|
9516
|
+
const html = await resp.text();
|
|
9517
|
+
const parsed = variant === "html" ? parseDdgAnchors(html, "result__a", "result__snippet") : parseDdgAnchors(html, "result-link", "result-snippet");
|
|
9518
|
+
return parsed.slice(0, maxResults).map((r) => ({ ...r, engine: `duckduckgo-${variant}` }));
|
|
9407
9519
|
}
|
|
9408
9520
|
var WebSearchTool = class extends BaseTool {
|
|
9409
9521
|
name = "web_search";
|
|
@@ -9462,12 +9574,14 @@ var WebSearchTool = class extends BaseTool {
|
|
|
9462
9574
|
errors.push(`Tavily: ${err instanceof Error ? err.message : String(err)}`);
|
|
9463
9575
|
}
|
|
9464
9576
|
}
|
|
9465
|
-
|
|
9466
|
-
|
|
9467
|
-
|
|
9468
|
-
|
|
9469
|
-
|
|
9470
|
-
|
|
9577
|
+
for (const variant of ["html", "lite"]) {
|
|
9578
|
+
try {
|
|
9579
|
+
results = await searchDuckDuckGo(query, maxResults, variant);
|
|
9580
|
+
if (results.length > 0) return this.formatResults(query, results);
|
|
9581
|
+
errors.push(`DuckDuckGo ${variant}: returned 0 results`);
|
|
9582
|
+
} catch (err) {
|
|
9583
|
+
errors.push(`DuckDuckGo ${variant}: ${err instanceof Error ? err.message : String(err)}`);
|
|
9584
|
+
}
|
|
9471
9585
|
}
|
|
9472
9586
|
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
9587
|
return [
|
|
@@ -11814,13 +11928,30 @@ ${last.partialOutput}` : "");
|
|
|
11814
11928
|
* explicit multi-part structure, so ordinary single-file asks (handled as
|
|
11815
11929
|
* Simple/Moderate) don't get over-escalated.
|
|
11816
11930
|
*/
|
|
11817
|
-
|
|
11931
|
+
/** Shared build/scale signals for the complexity floors below. */
|
|
11932
|
+
buildSignals(prompt) {
|
|
11818
11933
|
const p = prompt.trim();
|
|
11819
|
-
if (p.length < 24) return false;
|
|
11934
|
+
if (p.length < 24) return { buildVerb: false, scaleCount: 0, multiPart: false };
|
|
11820
11935
|
const buildVerb = /\b(?:build|implement|create|develop|design|scaffold|refactor|migrate|architect|set up|integrate)\b/i.test(p);
|
|
11821
|
-
const
|
|
11936
|
+
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
11937
|
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
|
|
11938
|
+
return { buildVerb, scaleCount, multiPart };
|
|
11939
|
+
}
|
|
11940
|
+
/**
|
|
11941
|
+
* A build prompt with REAL scale: multiple system-level deliverables, or a
|
|
11942
|
+
* deliverable plus explicitly multi-part phrasing. Only these floor to the
|
|
11943
|
+
* full T1→T2→T3 hierarchy — "create a todo app" is a build prompt too, but
|
|
11944
|
+
* flooring every small build to Complex was the #1 token bomb (3-5 managers
|
|
11945
|
+
* × workers for a task one worker handles).
|
|
11946
|
+
*/
|
|
11947
|
+
looksClearlyComplex(prompt) {
|
|
11948
|
+
const s = this.buildSignals(prompt);
|
|
11949
|
+
return s.buildVerb && (s.scaleCount >= 2 || s.scaleCount >= 1 && s.multiPart);
|
|
11950
|
+
}
|
|
11951
|
+
/** A small single-deliverable build — real work, but one manager's worth. */
|
|
11952
|
+
looksLikeModerateBuild(prompt) {
|
|
11953
|
+
const s = this.buildSignals(prompt);
|
|
11954
|
+
return s.buildVerb && (s.scaleCount >= 1 || s.multiPart);
|
|
11824
11955
|
}
|
|
11825
11956
|
// Cache glob scan results per workspace path to avoid repeated I/O.
|
|
11826
11957
|
static globCache = /* @__PURE__ */ new Map();
|
|
@@ -11908,6 +12039,9 @@ ${prompt}` : prompt;
|
|
|
11908
12039
|
if (verdict !== "Complex" && this.looksClearlyComplex(prompt)) {
|
|
11909
12040
|
this.recordDecision("complexity", `Complex \u2014 heuristic floor over classifier "${verdict}": explicit multi-step build/implementation signals (T1 engaged)`);
|
|
11910
12041
|
verdict = "Complex";
|
|
12042
|
+
} else if (verdict === "Simple" && this.looksLikeModerateBuild(prompt)) {
|
|
12043
|
+
this.recordDecision("complexity", 'Moderate \u2014 heuristic floor over classifier "Simple": build signals without multi-system scale (single manager)');
|
|
12044
|
+
verdict = "Moderate";
|
|
11911
12045
|
} else {
|
|
11912
12046
|
this.recordDecision("complexity", `${verdict} \u2014 classifier: ${reason || "no reason given"}`);
|
|
11913
12047
|
}
|