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 +205 -54
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +205 -54
- package/dist/cli.js.map +1 -1
- package/dist/desktop-core.cjs +203 -53
- package/dist/index.cjs +203 -53
- 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 +203 -53
- 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.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
|
-
|
|
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(
|
|
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 [
|
|
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
|
|
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
|
-
|
|
9436
|
-
|
|
9437
|
-
|
|
9438
|
-
|
|
9439
|
-
|
|
9440
|
-
|
|
9441
|
-
|
|
9442
|
-
|
|
9443
|
-
|
|
9444
|
-
|
|
9445
|
-
|
|
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(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'|'/g, "'").replace(/ /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 =
|
|
9448
|
-
|
|
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
|
-
|
|
9451
|
-
|
|
9569
|
+
const snippets = [];
|
|
9570
|
+
while ((m = snippetRe.exec(html)) !== null) {
|
|
9571
|
+
snippets.push(decodeEntities(stripTags(m[1] ?? "")));
|
|
9452
9572
|
}
|
|
9453
|
-
|
|
9454
|
-
|
|
9455
|
-
|
|
9456
|
-
|
|
9457
|
-
|
|
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
|
-
|
|
9518
|
-
|
|
9519
|
-
|
|
9520
|
-
|
|
9521
|
-
|
|
9522
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
}
|