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.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.
|
|
114
|
+
CASCADE_VERSION = "0.19.0";
|
|
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,7 +982,8 @@ var init_azure = __esm({
|
|
|
964
982
|
});
|
|
965
983
|
}
|
|
966
984
|
async listModels() {
|
|
967
|
-
|
|
985
|
+
const fromDeployment = azureModelForDeployment(this.config);
|
|
986
|
+
return [fromDeployment ?? this.model];
|
|
968
987
|
}
|
|
969
988
|
async isAvailable() {
|
|
970
989
|
try {
|
|
@@ -3049,8 +3068,11 @@ var CascadeConfigSchema = zod.z.object({
|
|
|
3049
3068
|
* Cascade Auto: when true, the TaskAnalyzer selects the optimal model for each
|
|
3050
3069
|
* tier based on task type and complexity, overriding the static priority lists.
|
|
3051
3070
|
* Heuristic-first with AI inference fallback (adds ~0–500ms per task).
|
|
3071
|
+
* ON by default since v0.19.0 — "Auto" without it was just a static priority
|
|
3072
|
+
* list, not the benchmark-value routing the docs describe. Explicit per-tier
|
|
3073
|
+
* model pins are unaffected; disable via config/Settings → Advanced.
|
|
3052
3074
|
*/
|
|
3053
|
-
cascadeAuto: zod.z.boolean().default(
|
|
3075
|
+
cascadeAuto: zod.z.boolean().default(true),
|
|
3054
3076
|
/**
|
|
3055
3077
|
* Cascade Auto trade-off bias when picking a model for a task:
|
|
3056
3078
|
* - 'balanced' (default): quality × cost-efficiency — cheap models win
|
|
@@ -4604,6 +4626,12 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
|
|
|
4604
4626
|
const results = await Promise.all(ocConfigs.map((cfg) => this.discoverOpenAICompatibleModels(cfg)));
|
|
4605
4627
|
if (results.some(Boolean)) this.selector.markProviderAvailable("openai-compatible");
|
|
4606
4628
|
}
|
|
4629
|
+
if (availableProviders.has("azure")) {
|
|
4630
|
+
for (const cfg of config.providers) {
|
|
4631
|
+
const model = azureModelForDeployment(cfg);
|
|
4632
|
+
if (model) this.selector.addDynamicModel(model);
|
|
4633
|
+
}
|
|
4634
|
+
}
|
|
4607
4635
|
for (const tier of ["T1", "T2", "T3"]) {
|
|
4608
4636
|
const override = tier === "T1" ? config.models.t1 : tier === "T2" ? config.models.t2 : config.models.t3;
|
|
4609
4637
|
if (!override || override === "auto") continue;
|
|
@@ -5210,7 +5238,7 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
|
|
|
5210
5238
|
ensureProvider(model, configs) {
|
|
5211
5239
|
const key = `${model.provider}:${model.id}`;
|
|
5212
5240
|
if (this.providers.has(key)) return;
|
|
5213
|
-
const cfg = configs.find((c) => c.type === model.provider) ?? { type: model.provider };
|
|
5241
|
+
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
5242
|
const provider = this.createProvider(cfg, model);
|
|
5215
5243
|
this.providers.set(key, provider);
|
|
5216
5244
|
}
|
|
@@ -5370,6 +5398,12 @@ var BaseTier = class extends EventEmitter__default.default {
|
|
|
5370
5398
|
* which would interleave, are not tagged.
|
|
5371
5399
|
*/
|
|
5372
5400
|
isPresenter = false;
|
|
5401
|
+
/**
|
|
5402
|
+
* The model actually serving this tier (`provider:id`), once resolved —
|
|
5403
|
+
* rides on every tier:status event so the desktop can show which model ran
|
|
5404
|
+
* which node (Cockpit node panel / Why panel).
|
|
5405
|
+
*/
|
|
5406
|
+
servingModel;
|
|
5373
5407
|
constructor(role, id, parentId) {
|
|
5374
5408
|
super();
|
|
5375
5409
|
this.role = role;
|
|
@@ -5394,11 +5428,16 @@ var BaseTier = class extends EventEmitter__default.default {
|
|
|
5394
5428
|
label: this.label,
|
|
5395
5429
|
status,
|
|
5396
5430
|
timestamp,
|
|
5397
|
-
output
|
|
5431
|
+
output,
|
|
5432
|
+
model: this.servingModel
|
|
5398
5433
|
};
|
|
5399
5434
|
this.emit("status", event);
|
|
5400
5435
|
this.emit("tier:status", event);
|
|
5401
5436
|
}
|
|
5437
|
+
/** Record the model serving this tier; future status events carry it. */
|
|
5438
|
+
setServingModel(model) {
|
|
5439
|
+
this.servingModel = model || void 0;
|
|
5440
|
+
}
|
|
5402
5441
|
setLabel(label) {
|
|
5403
5442
|
this.label = label;
|
|
5404
5443
|
}
|
|
@@ -5421,7 +5460,8 @@ var BaseTier = class extends EventEmitter__default.default {
|
|
|
5421
5460
|
currentAction: update.currentAction,
|
|
5422
5461
|
progressPct: update.progressPct,
|
|
5423
5462
|
timestamp,
|
|
5424
|
-
output: update.output
|
|
5463
|
+
output: update.output,
|
|
5464
|
+
model: this.servingModel
|
|
5425
5465
|
});
|
|
5426
5466
|
}
|
|
5427
5467
|
buildMessage(type, to, payload) {
|
|
@@ -5788,6 +5828,21 @@ Available tools: ${tools.map((t) => t.name).join(", ")}.
|
|
|
5788
5828
|
When you have enough information, stop calling tools and write your final answer.`;
|
|
5789
5829
|
}
|
|
5790
5830
|
|
|
5831
|
+
// src/utils/truncate.ts
|
|
5832
|
+
function truncateForContext(text, maxChars = 12e3) {
|
|
5833
|
+
if (text.length <= maxChars) return text;
|
|
5834
|
+
const headLen = Math.floor(maxChars * 0.75);
|
|
5835
|
+
const tailLen = maxChars - headLen;
|
|
5836
|
+
const head = text.slice(0, headLen);
|
|
5837
|
+
const tail = text.slice(-tailLen);
|
|
5838
|
+
const elided = text.length - headLen - tailLen;
|
|
5839
|
+
return `${head}
|
|
5840
|
+
|
|
5841
|
+
[... ${elided.toLocaleString()} characters elided to keep context small \u2014 re-read the file with a line range if you need the middle ...]
|
|
5842
|
+
|
|
5843
|
+
${tail}`;
|
|
5844
|
+
}
|
|
5845
|
+
|
|
5791
5846
|
// src/core/tiers/t3-worker.ts
|
|
5792
5847
|
var CriticalToolError = class extends Error {
|
|
5793
5848
|
constructor(message, toolName) {
|
|
@@ -6095,6 +6150,7 @@ Now execute your subtask using this context where relevant.`
|
|
|
6095
6150
|
} catch {
|
|
6096
6151
|
}
|
|
6097
6152
|
const effectiveModel = subtaskModel ?? this.router.getModelForTier("T3");
|
|
6153
|
+
if (effectiveModel) this.setServingModel(`${effectiveModel.provider}:${effectiveModel.id}`);
|
|
6098
6154
|
const useTextTools = effectiveModel?.supportsToolUse === false && tools.length > 0;
|
|
6099
6155
|
let sentFullTextContract = false;
|
|
6100
6156
|
let textContractSignature = "";
|
|
@@ -6192,7 +6248,7 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : "") + textToolSuffix,
|
|
|
6192
6248
|
const toolResult = await this.executeTool(tc);
|
|
6193
6249
|
await this.context.addMessage({
|
|
6194
6250
|
role: "tool",
|
|
6195
|
-
content: toolResult,
|
|
6251
|
+
content: truncateForContext(toolResult),
|
|
6196
6252
|
toolCallId: tc.id
|
|
6197
6253
|
});
|
|
6198
6254
|
}
|
|
@@ -6449,15 +6505,17 @@ ${assignment.expectedOutput}`;
|
|
|
6449
6505
|
};
|
|
6450
6506
|
}
|
|
6451
6507
|
requiresArtifact() {
|
|
6508
|
+
if (this.assignment?.files?.length) return true;
|
|
6452
6509
|
const haystack = `${this.assignment?.description ?? ""}
|
|
6453
6510
|
${this.assignment?.expectedOutput ?? ""}`;
|
|
6454
6511
|
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
6512
|
}
|
|
6456
6513
|
extractArtifactPaths(assignment) {
|
|
6514
|
+
const declared = (assignment.files ?? []).map((f) => f.trim()).filter((f) => f.includes("."));
|
|
6457
6515
|
const haystack = `${assignment.description}
|
|
6458
6516
|
${assignment.expectedOutput}`;
|
|
6459
6517
|
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 [
|
|
6518
|
+
return [.../* @__PURE__ */ new Set([...declared, ...matches.map((m) => m.trim())])];
|
|
6461
6519
|
}
|
|
6462
6520
|
async verifyArtifacts(assignment) {
|
|
6463
6521
|
const artifactPaths = this.extractArtifactPaths(assignment);
|
|
@@ -6580,7 +6638,9 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
|
6580
6638
|
Assignment: ${assignment.description}
|
|
6581
6639
|
Expected output: ${assignment.expectedOutput}
|
|
6582
6640
|
Constraints: ${assignment.constraints.join("; ")}
|
|
6583
|
-
|
|
6641
|
+
${assignment.acceptance?.length ? `Acceptance criteria \u2014 ALL must be satisfied for "completeness" to pass:
|
|
6642
|
+
${assignment.acceptance.map((a) => `- ${a}`).join("\n")}
|
|
6643
|
+
` : ""}
|
|
6584
6644
|
Output to test:
|
|
6585
6645
|
${output}
|
|
6586
6646
|
|
|
@@ -6669,17 +6729,27 @@ Your subtask:
|
|
|
6669
6729
|
- Title: ${assignment.subtaskTitle}
|
|
6670
6730
|
- Description: ${assignment.description}
|
|
6671
6731
|
- Expected output: ${assignment.expectedOutput}
|
|
6672
|
-
- Constraints: ${assignment.constraints.join("; ")}
|
|
6732
|
+
- Constraints: ${assignment.constraints.join("; ")}${assignment.files?.length ? `
|
|
6733
|
+
- Files you own (create/edit ONLY these): ${assignment.files.join(", ")}` : ""}${assignment.acceptance?.length ? `
|
|
6734
|
+
- Definition of done: ${assignment.acceptance.join("; ")}` : ""}`;
|
|
6673
6735
|
}
|
|
6674
6736
|
buildInitialPrompt(assignment) {
|
|
6675
6737
|
return `Execute the following subtask completely:
|
|
6676
6738
|
|
|
6677
6739
|
**${assignment.subtaskTitle}**
|
|
6678
|
-
|
|
6740
|
+
${assignment.contextBrief ? `
|
|
6741
|
+
Context: ${assignment.contextBrief}
|
|
6742
|
+
` : ""}
|
|
6679
6743
|
${assignment.description}
|
|
6680
6744
|
|
|
6681
6745
|
Expected output: ${assignment.expectedOutput}
|
|
6682
|
-
|
|
6746
|
+
${assignment.files?.length ? `
|
|
6747
|
+
Files you own (create or edit exactly these paths):
|
|
6748
|
+
${assignment.files.map((f) => `- ${f}`).join("\n")}
|
|
6749
|
+
` : ""}${assignment.acceptance?.length ? `
|
|
6750
|
+
Definition of done (your output must satisfy ALL of these):
|
|
6751
|
+
${assignment.acceptance.map((a) => `- ${a}`).join("\n")}
|
|
6752
|
+
` : ""}
|
|
6683
6753
|
Constraints:
|
|
6684
6754
|
${assignment.constraints.map((c) => `- ${c}`).join("\n")}
|
|
6685
6755
|
|
|
@@ -7160,6 +7230,8 @@ var T2Manager = class extends BaseTier {
|
|
|
7160
7230
|
this.assignment = assignment;
|
|
7161
7231
|
this.taskId = taskId;
|
|
7162
7232
|
this.setLabel(assignment.sectionTitle);
|
|
7233
|
+
const m = this.router.getModelForTier("T2");
|
|
7234
|
+
if (m) this.setServingModel(`${m.provider}:${m.id}`);
|
|
7163
7235
|
this.setStatus("ACTIVE");
|
|
7164
7236
|
this.sendStatusUpdate({
|
|
7165
7237
|
progressPct: 0,
|
|
@@ -7246,7 +7318,7 @@ Guidance (must be followed): ${decision.note}`
|
|
|
7246
7318
|
// ── Private ──────────────────────────────────
|
|
7247
7319
|
async decomposeSection(assignment) {
|
|
7248
7320
|
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
|
|
7321
|
+
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
7322
|
|
|
7251
7323
|
Section: ${assignment.sectionTitle}
|
|
7252
7324
|
Description: ${assignment.description}
|
|
@@ -7265,6 +7337,9 @@ Return a JSON array of subtask objects, each with:
|
|
|
7265
7337
|
- peerT3Ids: string[] (empty for now)
|
|
7266
7338
|
- dependsOn: string[] (array of subtaskIds this task depends on to start)
|
|
7267
7339
|
- executionMode: "parallel|sequential" (default is parallel)
|
|
7340
|
+
- files: string[] (the EXACT relative paths this subtask creates or edits)
|
|
7341
|
+
- acceptance: string[] (1-3 mechanically checkable done-criteria: file exists / contains X / command exits 0)
|
|
7342
|
+
- contextBrief: string (1-3 short sentences with ALL the background the worker needs \u2014 it sees nothing else)
|
|
7268
7343
|
|
|
7269
7344
|
Return ONLY the JSON array.`;
|
|
7270
7345
|
const messages = [{ role: "user", content: prompt }];
|
|
@@ -7865,6 +7940,8 @@ var T1Administrator = class extends BaseTier {
|
|
|
7865
7940
|
this.signal = signal;
|
|
7866
7941
|
this.taskId = crypto.randomUUID();
|
|
7867
7942
|
this.setLabel("Administrator");
|
|
7943
|
+
const m = this.router.getModelForTier("T1");
|
|
7944
|
+
if (m) this.setServingModel(`${m.provider}:${m.id}`);
|
|
7868
7945
|
this.setStatus("ACTIVE");
|
|
7869
7946
|
this.taskGoal = userPrompt;
|
|
7870
7947
|
this.sendStatusUpdate({
|
|
@@ -8111,10 +8188,13 @@ Return JSON where SECTIONS can declare dependencies on other SECTIONS:
|
|
|
8111
8188
|
"description": "Run npm init",
|
|
8112
8189
|
"expectedOutput": "package.json created",
|
|
8113
8190
|
"constraints": [],
|
|
8114
|
-
"dependsOn": []
|
|
8191
|
+
"dependsOn": [],
|
|
8192
|
+
"files": ["package.json"], // \u2190 exact paths this subtask owns
|
|
8193
|
+
"acceptance": ["package.json exists and parses as JSON"], // \u2190 objectively checkable
|
|
8194
|
+
"contextBrief": "Fresh Node 20 project; npm available." // \u2190 ALL the background the worker gets
|
|
8115
8195
|
}]
|
|
8116
8196
|
}, {
|
|
8117
|
-
"sectionId": "s2",
|
|
8197
|
+
"sectionId": "s2",
|
|
8118
8198
|
"sectionTitle": "Write Tests",
|
|
8119
8199
|
"description": "Write tests for the project",
|
|
8120
8200
|
"expectedOutput": "Tests passing",
|
|
@@ -8124,7 +8204,13 @@ Return JSON where SECTIONS can declare dependencies on other SECTIONS:
|
|
|
8124
8204
|
}]
|
|
8125
8205
|
}
|
|
8126
8206
|
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
|
|
8207
|
+
Leave dependsOn empty for sections that can run immediately in parallel.
|
|
8208
|
+
|
|
8209
|
+
SPEC RULES \u2014 each subtask is a self-contained spec slice (workers execute from their slice ALONE):
|
|
8210
|
+
- "files": the exact relative paths the subtask creates or edits. Never vague ("some files"); always concrete.
|
|
8211
|
+
- "acceptance": 1-3 checks a reviewer could verify mechanically (file exists / contains X / command exits 0). These define done.
|
|
8212
|
+
- "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.
|
|
8213
|
+
- 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
8214
|
const messages = [{ role: "user", content: decompositionPrompt }];
|
|
8129
8215
|
const result = await this.router.generate("T1", {
|
|
8130
8216
|
messages,
|
|
@@ -9432,30 +9518,56 @@ async function searchTavily(query, apiKey, maxResults) {
|
|
|
9432
9518
|
engine: "tavily"
|
|
9433
9519
|
}));
|
|
9434
9520
|
}
|
|
9435
|
-
|
|
9436
|
-
|
|
9437
|
-
|
|
9438
|
-
|
|
9439
|
-
|
|
9440
|
-
|
|
9441
|
-
|
|
9442
|
-
|
|
9443
|
-
|
|
9444
|
-
|
|
9445
|
-
|
|
9521
|
+
var BROWSER_UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0 Safari/537.36";
|
|
9522
|
+
function unwrapDdgRedirect(href) {
|
|
9523
|
+
try {
|
|
9524
|
+
const url = new URL(href.startsWith("//") ? `https:${href}` : href, "https://duckduckgo.com");
|
|
9525
|
+
if (/(^|\.)duckduckgo\.com$/i.test(url.hostname) && url.pathname.startsWith("/l/")) {
|
|
9526
|
+
const target = url.searchParams.get("uddg");
|
|
9527
|
+
if (target) return decodeURIComponent(target);
|
|
9528
|
+
}
|
|
9529
|
+
return url.protocol === "http:" || url.protocol === "https:" ? url.toString() : href;
|
|
9530
|
+
} catch {
|
|
9531
|
+
return href;
|
|
9532
|
+
}
|
|
9533
|
+
}
|
|
9534
|
+
function stripTags(html) {
|
|
9535
|
+
return html.replace(/<[^>]+>/g, "").replace(/\s+/g, " ").trim();
|
|
9536
|
+
}
|
|
9537
|
+
function decodeEntities(text) {
|
|
9538
|
+
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'|'/g, "'").replace(/ /g, " ");
|
|
9539
|
+
}
|
|
9540
|
+
function parseDdgAnchors(html, anchorClass, snippetClass) {
|
|
9541
|
+
const anchorRe = new RegExp(`<a\\b[^>]*class=["']?[^"'>]*\\b${anchorClass}\\b[^"'>]*["']?[^>]*>([\\s\\S]*?)<\\/a>`, "gi");
|
|
9542
|
+
const snippetRe = new RegExp(`class=["']?[^"'>]*\\b${snippetClass}\\b[^"'>]*["']?[^>]*>([\\s\\S]*?)<\\/(?:td|a|div|span)>`, "gi");
|
|
9543
|
+
const results = [];
|
|
9446
9544
|
let m;
|
|
9447
|
-
while ((m =
|
|
9448
|
-
|
|
9545
|
+
while ((m = anchorRe.exec(html)) !== null) {
|
|
9546
|
+
const tag = m[0];
|
|
9547
|
+
const href = /href=["']([^"']+)["']/i.exec(tag)?.[1];
|
|
9548
|
+
const title = decodeEntities(stripTags(m[1] ?? ""));
|
|
9549
|
+
if (!href || !title) continue;
|
|
9550
|
+
results.push({ title, url: unwrapDdgRedirect(decodeEntities(href)), snippet: "" });
|
|
9449
9551
|
}
|
|
9450
|
-
|
|
9451
|
-
|
|
9552
|
+
const snippets = [];
|
|
9553
|
+
while ((m = snippetRe.exec(html)) !== null) {
|
|
9554
|
+
snippets.push(decodeEntities(stripTags(m[1] ?? "")));
|
|
9452
9555
|
}
|
|
9453
|
-
|
|
9454
|
-
|
|
9455
|
-
|
|
9456
|
-
|
|
9457
|
-
|
|
9458
|
-
|
|
9556
|
+
for (let i = 0; i < results.length; i++) {
|
|
9557
|
+
if (snippets[i]) results[i].snippet = snippets[i];
|
|
9558
|
+
}
|
|
9559
|
+
return results;
|
|
9560
|
+
}
|
|
9561
|
+
async function searchDuckDuckGo(query, maxResults, variant) {
|
|
9562
|
+
const base = variant === "html" ? "https://html.duckduckgo.com/html/?q=" : "https://lite.duckduckgo.com/lite/?q=";
|
|
9563
|
+
const resp = await fetch(`${base}${encodeURIComponent(query)}`, {
|
|
9564
|
+
headers: { "User-Agent": BROWSER_UA, Accept: "text/html" },
|
|
9565
|
+
signal: AbortSignal.timeout(1e4)
|
|
9566
|
+
});
|
|
9567
|
+
if (!resp.ok) throw new Error(`DuckDuckGo ${variant} returned HTTP ${resp.status}`);
|
|
9568
|
+
const html = await resp.text();
|
|
9569
|
+
const parsed = variant === "html" ? parseDdgAnchors(html, "result__a", "result__snippet") : parseDdgAnchors(html, "result-link", "result-snippet");
|
|
9570
|
+
return parsed.slice(0, maxResults).map((r) => ({ ...r, engine: `duckduckgo-${variant}` }));
|
|
9459
9571
|
}
|
|
9460
9572
|
var WebSearchTool = class extends BaseTool {
|
|
9461
9573
|
name = "web_search";
|
|
@@ -9514,12 +9626,14 @@ var WebSearchTool = class extends BaseTool {
|
|
|
9514
9626
|
errors.push(`Tavily: ${err instanceof Error ? err.message : String(err)}`);
|
|
9515
9627
|
}
|
|
9516
9628
|
}
|
|
9517
|
-
|
|
9518
|
-
|
|
9519
|
-
|
|
9520
|
-
|
|
9521
|
-
|
|
9522
|
-
|
|
9629
|
+
for (const variant of ["html", "lite"]) {
|
|
9630
|
+
try {
|
|
9631
|
+
results = await searchDuckDuckGo(query, maxResults, variant);
|
|
9632
|
+
if (results.length > 0) return this.formatResults(query, results);
|
|
9633
|
+
errors.push(`DuckDuckGo ${variant}: returned 0 results`);
|
|
9634
|
+
} catch (err) {
|
|
9635
|
+
errors.push(`DuckDuckGo ${variant}: ${err instanceof Error ? err.message : String(err)}`);
|
|
9636
|
+
}
|
|
9523
9637
|
}
|
|
9524
9638
|
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
9639
|
return [
|
|
@@ -11866,13 +11980,30 @@ ${last.partialOutput}` : "");
|
|
|
11866
11980
|
* explicit multi-part structure, so ordinary single-file asks (handled as
|
|
11867
11981
|
* Simple/Moderate) don't get over-escalated.
|
|
11868
11982
|
*/
|
|
11869
|
-
|
|
11983
|
+
/** Shared build/scale signals for the complexity floors below. */
|
|
11984
|
+
buildSignals(prompt) {
|
|
11870
11985
|
const p = prompt.trim();
|
|
11871
|
-
if (p.length < 24) return false;
|
|
11986
|
+
if (p.length < 24) return { buildVerb: false, scaleCount: 0, multiPart: false };
|
|
11872
11987
|
const buildVerb = /\b(?:build|implement|create|develop|design|scaffold|refactor|migrate|architect|set up|integrate)\b/i.test(p);
|
|
11873
|
-
const
|
|
11988
|
+
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
11989
|
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
|
|
11990
|
+
return { buildVerb, scaleCount, multiPart };
|
|
11991
|
+
}
|
|
11992
|
+
/**
|
|
11993
|
+
* A build prompt with REAL scale: multiple system-level deliverables, or a
|
|
11994
|
+
* deliverable plus explicitly multi-part phrasing. Only these floor to the
|
|
11995
|
+
* full T1→T2→T3 hierarchy — "create a todo app" is a build prompt too, but
|
|
11996
|
+
* flooring every small build to Complex was the #1 token bomb (3-5 managers
|
|
11997
|
+
* × workers for a task one worker handles).
|
|
11998
|
+
*/
|
|
11999
|
+
looksClearlyComplex(prompt) {
|
|
12000
|
+
const s = this.buildSignals(prompt);
|
|
12001
|
+
return s.buildVerb && (s.scaleCount >= 2 || s.scaleCount >= 1 && s.multiPart);
|
|
12002
|
+
}
|
|
12003
|
+
/** A small single-deliverable build — real work, but one manager's worth. */
|
|
12004
|
+
looksLikeModerateBuild(prompt) {
|
|
12005
|
+
const s = this.buildSignals(prompt);
|
|
12006
|
+
return s.buildVerb && (s.scaleCount >= 1 || s.multiPart);
|
|
11876
12007
|
}
|
|
11877
12008
|
// Cache glob scan results per workspace path to avoid repeated I/O.
|
|
11878
12009
|
static globCache = /* @__PURE__ */ new Map();
|
|
@@ -11960,6 +12091,9 @@ ${prompt}` : prompt;
|
|
|
11960
12091
|
if (verdict !== "Complex" && this.looksClearlyComplex(prompt)) {
|
|
11961
12092
|
this.recordDecision("complexity", `Complex \u2014 heuristic floor over classifier "${verdict}": explicit multi-step build/implementation signals (T1 engaged)`);
|
|
11962
12093
|
verdict = "Complex";
|
|
12094
|
+
} else if (verdict === "Simple" && this.looksLikeModerateBuild(prompt)) {
|
|
12095
|
+
this.recordDecision("complexity", 'Moderate \u2014 heuristic floor over classifier "Simple": build signals without multi-system scale (single manager)');
|
|
12096
|
+
verdict = "Moderate";
|
|
11963
12097
|
} else {
|
|
11964
12098
|
this.recordDecision("complexity", `${verdict} \u2014 classifier: ${reason || "no reason given"}`);
|
|
11965
12099
|
}
|