cascade-ai 0.49.0 → 0.50.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 +239 -3
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +239 -3
- package/dist/cli.js.map +1 -1
- package/dist/desktop-core.cjs +239 -3
- package/dist/index.cjs +239 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +72 -0
- package/dist/index.d.ts +72 -0
- package/dist/index.js +239 -3
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.cjs
CHANGED
|
@@ -1793,7 +1793,7 @@ var CASCADE_VERSION, CASCADE_CONFIG_FILE, CASCADE_DB_FILE, CASCADE_DASHBOARD_SEC
|
|
|
1793
1793
|
var init_constants = __esm({
|
|
1794
1794
|
"src/constants.ts"() {
|
|
1795
1795
|
init_pricing();
|
|
1796
|
-
CASCADE_VERSION = "0.
|
|
1796
|
+
CASCADE_VERSION = "0.50.0";
|
|
1797
1797
|
CASCADE_CONFIG_FILE = ".cascade/config.json";
|
|
1798
1798
|
CASCADE_DB_FILE = ".cascade/memory.db";
|
|
1799
1799
|
CASCADE_DASHBOARD_SECRET_FILE = ".cascade/dashboard-secret";
|
|
@@ -7668,6 +7668,14 @@ var BaseTier = class extends EventEmitter__default.default {
|
|
|
7668
7668
|
this.emit("tier:status", event);
|
|
7669
7669
|
}
|
|
7670
7670
|
/** Record the model serving this tier; future status events carry it. */
|
|
7671
|
+
/**
|
|
7672
|
+
* The model actually serving this tier ("provider:id"), once one has been
|
|
7673
|
+
* chosen. The run breaker reads it to attribute a failure to a model rather
|
|
7674
|
+
* than to the subtask that happened to hit it.
|
|
7675
|
+
*/
|
|
7676
|
+
getServingModel() {
|
|
7677
|
+
return this.servingModel;
|
|
7678
|
+
}
|
|
7671
7679
|
setServingModel(model) {
|
|
7672
7680
|
this.servingModel = model || void 0;
|
|
7673
7681
|
}
|
|
@@ -9404,6 +9412,7 @@ var T2Manager = class extends BaseTier {
|
|
|
9404
9412
|
escalations = [];
|
|
9405
9413
|
peerSyncBuffer = [];
|
|
9406
9414
|
store;
|
|
9415
|
+
runBreaker;
|
|
9407
9416
|
t3PeerBus = new PeerBus();
|
|
9408
9417
|
// ← T3↔T3 bus (local to this T2)
|
|
9409
9418
|
t2PeerBus;
|
|
@@ -9434,6 +9443,15 @@ var T2Manager = class extends BaseTier {
|
|
|
9434
9443
|
this.router = router;
|
|
9435
9444
|
this.toolRegistry = toolRegistry;
|
|
9436
9445
|
}
|
|
9446
|
+
/**
|
|
9447
|
+
* Share the run-wide circuit breaker. Every T2 in a run gets the SAME
|
|
9448
|
+
* instance: three sections each failing once against a dead model is the
|
|
9449
|
+
* same fact as one section failing three times, and a per-section breaker
|
|
9450
|
+
* would never see it.
|
|
9451
|
+
*/
|
|
9452
|
+
setRunBreaker(breaker) {
|
|
9453
|
+
this.runBreaker = breaker;
|
|
9454
|
+
}
|
|
9437
9455
|
setStore(store) {
|
|
9438
9456
|
this.store = store;
|
|
9439
9457
|
}
|
|
@@ -9557,7 +9575,8 @@ Guidance (must be followed): ${decision.note}`
|
|
|
9557
9575
|
const overallStatus = this.determineStatus(t3Results);
|
|
9558
9576
|
const isOk = overallStatus === "COMPLETED" || overallStatus === "PARTIAL";
|
|
9559
9577
|
this.setStatus(isOk ? "COMPLETED" : "FAILED", summary);
|
|
9560
|
-
|
|
9578
|
+
const sectionAction = overallStatus === "COMPLETED" ? "Section complete" : overallStatus === "PARTIAL" ? "Section partially complete \u2014 some subtasks failed" : overallStatus === "ESCALATED" ? "Section escalated \u2014 needs a decision" : "Section failed \u2014 no subtask completed";
|
|
9579
|
+
this.sendStatusUpdate({ progressPct: 100, currentAction: sectionAction, status: "IN_PROGRESS", output: summary });
|
|
9561
9580
|
const result = {
|
|
9562
9581
|
sectionId: assignment.sectionId,
|
|
9563
9582
|
sectionTitle: assignment.sectionTitle,
|
|
@@ -9727,6 +9746,24 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
|
9727
9746
|
const reinforceCfg = this.router.getReinforcementsConfig?.() ?? { enabled: false, maxPerSection: 4 };
|
|
9728
9747
|
let reinforcementsAdded = 0;
|
|
9729
9748
|
while (remaining.size > 0) {
|
|
9749
|
+
if (this.runBreaker?.isOpen()) {
|
|
9750
|
+
const why = this.runBreaker.skipMessage();
|
|
9751
|
+
this.log(`Run breaker open \u2014 skipping ${remaining.size} remaining subtask(s)`);
|
|
9752
|
+
for (const id of remaining) {
|
|
9753
|
+
this.t3PeerBus.publish(this.id, id, why, "FAILED");
|
|
9754
|
+
resultMap.set(id, {
|
|
9755
|
+
subtaskId: id,
|
|
9756
|
+
status: "FAILED",
|
|
9757
|
+
output: why,
|
|
9758
|
+
testResults: { checksRun: [], passed: [], failed: [] },
|
|
9759
|
+
issues: [why],
|
|
9760
|
+
peerSyncsUsed: [],
|
|
9761
|
+
correctionAttempts: 0
|
|
9762
|
+
});
|
|
9763
|
+
}
|
|
9764
|
+
remaining.clear();
|
|
9765
|
+
break;
|
|
9766
|
+
}
|
|
9730
9767
|
const runnableIds = [...remaining].filter((id) => (inDegree.get(id) ?? 0) === 0);
|
|
9731
9768
|
if (runnableIds.length === 0) {
|
|
9732
9769
|
const fallbackId = [...remaining].sort(
|
|
@@ -9829,8 +9866,32 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
|
9829
9866
|
remaining.delete(id);
|
|
9830
9867
|
const r = waveResults[i];
|
|
9831
9868
|
if (r.status === "rejected") {
|
|
9832
|
-
this.log(`T3 worker ${id} failed: ${r.reason instanceof Error ? r.reason.message : String(r.reason)} \u2014 retrying once`);
|
|
9833
9869
|
const assignment = sanitizedAssignments.find((a) => a.subtaskId === id);
|
|
9870
|
+
const failedModel = workerMap.get(id)?.getServingModel();
|
|
9871
|
+
const tripped = this.runBreaker?.record(r.reason, failedModel);
|
|
9872
|
+
if (tripped) {
|
|
9873
|
+
this.log(`Run breaker opened: ${tripped.failures} consecutive ${tripped.kind} failures on ${tripped.modelId} \u2014 stopping`);
|
|
9874
|
+
this.emit("run:breaker", tripped);
|
|
9875
|
+
}
|
|
9876
|
+
if (this.runBreaker?.isOpen()) {
|
|
9877
|
+
const why = this.runBreaker.skipMessage();
|
|
9878
|
+
this.log(`T3 worker ${id} failed and the run breaker is open \u2014 not retrying`);
|
|
9879
|
+
this.t3PeerBus.publish(this.id, id, why, "FAILED");
|
|
9880
|
+
resultMap.set(id, {
|
|
9881
|
+
subtaskId: id,
|
|
9882
|
+
status: "FAILED",
|
|
9883
|
+
output: why,
|
|
9884
|
+
testResults: { checksRun: [], passed: [], failed: [] },
|
|
9885
|
+
issues: [why],
|
|
9886
|
+
peerSyncsUsed: [],
|
|
9887
|
+
correctionAttempts: 0
|
|
9888
|
+
});
|
|
9889
|
+
for (const dependent of adj.get(id) ?? []) {
|
|
9890
|
+
inDegree.set(dependent, Math.max(0, (inDegree.get(dependent) ?? 0) - 1));
|
|
9891
|
+
}
|
|
9892
|
+
continue;
|
|
9893
|
+
}
|
|
9894
|
+
this.log(`T3 worker ${id} failed: ${r.reason instanceof Error ? r.reason.message : String(r.reason)} \u2014 retrying once`);
|
|
9834
9895
|
try {
|
|
9835
9896
|
const retried = await this.retryT3(assignment, taskId);
|
|
9836
9897
|
resultMap.set(id, retried);
|
|
@@ -10165,6 +10226,7 @@ var T1Administrator = class extends BaseTier {
|
|
|
10165
10226
|
t2Managers = /* @__PURE__ */ new Map();
|
|
10166
10227
|
escalations = [];
|
|
10167
10228
|
store;
|
|
10229
|
+
runBreaker;
|
|
10168
10230
|
t2PeerBus = new PeerBus();
|
|
10169
10231
|
permissionEscalator;
|
|
10170
10232
|
toolCreator;
|
|
@@ -10179,6 +10241,10 @@ var T1Administrator = class extends BaseTier {
|
|
|
10179
10241
|
this.toolRegistry = toolRegistry;
|
|
10180
10242
|
this.config = config;
|
|
10181
10243
|
}
|
|
10244
|
+
/** Share the run-wide circuit breaker with every section manager. */
|
|
10245
|
+
setRunBreaker(breaker) {
|
|
10246
|
+
this.runBreaker = breaker;
|
|
10247
|
+
}
|
|
10182
10248
|
setStore(store) {
|
|
10183
10249
|
this.store = store;
|
|
10184
10250
|
}
|
|
@@ -10541,6 +10607,7 @@ SPEC RULES \u2014 each subtask is a self-contained spec slice (workers execute f
|
|
|
10541
10607
|
};
|
|
10542
10608
|
const managers = sections.map((section) => {
|
|
10543
10609
|
const manager = new T2Manager(this.router, this.toolRegistry, this.id);
|
|
10610
|
+
if (this.runBreaker) manager.setRunBreaker(this.runBreaker);
|
|
10544
10611
|
manager.setHierarchyContext(`You are a T2 Manager for the section "${section.sectionTitle}". You are part of a COMPLEX task overseen by T1 Administrator.`);
|
|
10545
10612
|
if (this.store) {
|
|
10546
10613
|
manager.setStore(this.store);
|
|
@@ -10829,6 +10896,158 @@ Reply with exactly one word: YES, NO, or UNSURE.
|
|
|
10829
10896
|
// src/core/cascade.ts
|
|
10830
10897
|
init_cost();
|
|
10831
10898
|
|
|
10899
|
+
// src/core/router/provider-errors.ts
|
|
10900
|
+
var SYSTEMIC = /* @__PURE__ */ new Set([
|
|
10901
|
+
"rate_limit",
|
|
10902
|
+
"quota_exhausted",
|
|
10903
|
+
"auth",
|
|
10904
|
+
"model_unavailable"
|
|
10905
|
+
]);
|
|
10906
|
+
function isSystemicKind(kind) {
|
|
10907
|
+
return SYSTEMIC.has(kind);
|
|
10908
|
+
}
|
|
10909
|
+
function statusOf(err) {
|
|
10910
|
+
if (!err || typeof err !== "object") return void 0;
|
|
10911
|
+
const e = err;
|
|
10912
|
+
for (const key of ["status", "statusCode", "code"]) {
|
|
10913
|
+
const v = e[key];
|
|
10914
|
+
if (typeof v === "number" && v >= 100 && v < 600) return v;
|
|
10915
|
+
}
|
|
10916
|
+
const resp = e["response"];
|
|
10917
|
+
if (resp && typeof resp["status"] === "number") return resp["status"];
|
|
10918
|
+
const inner = e["error"];
|
|
10919
|
+
if (inner && typeof inner["code"] === "number") return inner["code"];
|
|
10920
|
+
return void 0;
|
|
10921
|
+
}
|
|
10922
|
+
function messageOf(err) {
|
|
10923
|
+
if (err instanceof Error) return err.message;
|
|
10924
|
+
if (typeof err === "string") return err;
|
|
10925
|
+
if (err && typeof err === "object") {
|
|
10926
|
+
const e = err;
|
|
10927
|
+
if (typeof e["message"] === "string") return e["message"];
|
|
10928
|
+
const inner = e["error"];
|
|
10929
|
+
if (inner && typeof inner["message"] === "string") return inner["message"];
|
|
10930
|
+
try {
|
|
10931
|
+
return JSON.stringify(err);
|
|
10932
|
+
} catch {
|
|
10933
|
+
}
|
|
10934
|
+
}
|
|
10935
|
+
return String(err);
|
|
10936
|
+
}
|
|
10937
|
+
function classifyProviderError(err) {
|
|
10938
|
+
const raw = messageOf(err).trim();
|
|
10939
|
+
const status = statusOf(err);
|
|
10940
|
+
const m = raw.toLowerCase();
|
|
10941
|
+
const kind = (() => {
|
|
10942
|
+
if (status === 429) {
|
|
10943
|
+
return /quota|billing|credit|exceeded your current quota|insufficient/.test(m) ? "quota_exhausted" : "rate_limit";
|
|
10944
|
+
}
|
|
10945
|
+
if (status === 401 || status === 403) return "auth";
|
|
10946
|
+
if (status === 404) return "model_unavailable";
|
|
10947
|
+
if (/rate limit|too many requests|resource_exhausted/.test(m)) {
|
|
10948
|
+
return /quota|billing|credit|insufficient/.test(m) ? "quota_exhausted" : "rate_limit";
|
|
10949
|
+
}
|
|
10950
|
+
if (/quota|billing|insufficient_quota|insufficient credit|payment required/.test(m)) return "quota_exhausted";
|
|
10951
|
+
if (/api key|unauthorized|authentication|permission denied|invalid[_ ]api/.test(m)) return "auth";
|
|
10952
|
+
if (/model .*(not found|does not exist|unavailable)|no such model|unknown model|not supported for/.test(m)) {
|
|
10953
|
+
return "model_unavailable";
|
|
10954
|
+
}
|
|
10955
|
+
if (/context length|too many tokens|maximum context|prompt is too long|input too long/.test(m)) return "context_length";
|
|
10956
|
+
if (/safety|content filter|blocked|recitation|prohibited/.test(m)) return "content_filter";
|
|
10957
|
+
if (/econnreset|etimedout|enotfound|socket hang up|network|fetch failed|aborted/.test(m)) return "network";
|
|
10958
|
+
return "unknown";
|
|
10959
|
+
})();
|
|
10960
|
+
return { kind, systemic: isSystemicKind(kind), status, raw };
|
|
10961
|
+
}
|
|
10962
|
+
function describeProviderError(c, modelId) {
|
|
10963
|
+
const on = ` on ${modelId}` ;
|
|
10964
|
+
const detail = c.raw ? ` Provider said: ${c.raw}` : "";
|
|
10965
|
+
switch (c.kind) {
|
|
10966
|
+
case "rate_limit":
|
|
10967
|
+
return `Rate limit hit${on}. The provider is throttling requests \u2014 lower the number of parallel workers, or use a model with a higher requests-per-minute allowance.${detail}`;
|
|
10968
|
+
case "quota_exhausted":
|
|
10969
|
+
return `Quota or billing limit reached${on}. This will not recover on its own \u2014 check the provider's billing page, or switch this tier to a different provider.${detail}`;
|
|
10970
|
+
case "auth":
|
|
10971
|
+
return `Authentication failed${on}. The API key is missing, expired, or lacks access to this model \u2014 re-check the key in Settings > Providers.${detail}`;
|
|
10972
|
+
case "model_unavailable":
|
|
10973
|
+
return `Model unavailable${on}. The id may be wrong, retired, or not enabled for this API key \u2014 pick a different model for this tier.${detail}`;
|
|
10974
|
+
case "context_length":
|
|
10975
|
+
return `The prompt was too long${on}. Reduce the attached context, or use a model with a larger context window for this tier.${detail}`;
|
|
10976
|
+
case "content_filter":
|
|
10977
|
+
return `The provider's safety filter refused this request${on}.${detail}`;
|
|
10978
|
+
case "network":
|
|
10979
|
+
return `Network error reaching the provider${on}. This is usually transient.${detail}`;
|
|
10980
|
+
default:
|
|
10981
|
+
return `The model call failed${on}.${detail}`;
|
|
10982
|
+
}
|
|
10983
|
+
}
|
|
10984
|
+
|
|
10985
|
+
// src/core/run-breaker.ts
|
|
10986
|
+
var DEFAULT_FAILURE_THRESHOLD = 3;
|
|
10987
|
+
var RunBreaker = class {
|
|
10988
|
+
threshold;
|
|
10989
|
+
/** modelId → consecutive systemic failures of the SAME kind. */
|
|
10990
|
+
counts = /* @__PURE__ */ new Map();
|
|
10991
|
+
trip = null;
|
|
10992
|
+
constructor(threshold = DEFAULT_FAILURE_THRESHOLD) {
|
|
10993
|
+
this.threshold = Math.max(1, threshold);
|
|
10994
|
+
}
|
|
10995
|
+
/**
|
|
10996
|
+
* Record a failed model call. Returns the trip if this failure is the one
|
|
10997
|
+
* that opened the breaker, otherwise null.
|
|
10998
|
+
*
|
|
10999
|
+
* A non-systemic failure RESETS the model's counter: it is evidence the model
|
|
11000
|
+
* is reachable and answering, which is exactly what a systemic streak claims
|
|
11001
|
+
* it is not.
|
|
11002
|
+
*/
|
|
11003
|
+
record(err, modelId) {
|
|
11004
|
+
if (this.trip) return null;
|
|
11005
|
+
const id = modelId?.trim() || "unknown-model";
|
|
11006
|
+
const c = classifyProviderError(err);
|
|
11007
|
+
if (!c.systemic) {
|
|
11008
|
+
this.counts.delete(id);
|
|
11009
|
+
return null;
|
|
11010
|
+
}
|
|
11011
|
+
const prev = this.counts.get(id);
|
|
11012
|
+
const n = prev && prev.kind === c.kind ? prev.n + 1 : 1;
|
|
11013
|
+
this.counts.set(id, { kind: c.kind, n, last: c });
|
|
11014
|
+
if (n >= this.threshold) {
|
|
11015
|
+
this.trip = {
|
|
11016
|
+
modelId: id,
|
|
11017
|
+
kind: c.kind,
|
|
11018
|
+
failures: n,
|
|
11019
|
+
message: describeProviderError(c, id)
|
|
11020
|
+
};
|
|
11021
|
+
return this.trip;
|
|
11022
|
+
}
|
|
11023
|
+
return null;
|
|
11024
|
+
}
|
|
11025
|
+
/** True once the run should stop launching work that would fail identically. */
|
|
11026
|
+
isOpen() {
|
|
11027
|
+
return this.trip !== null;
|
|
11028
|
+
}
|
|
11029
|
+
/** Why the breaker opened, or null while it is closed. */
|
|
11030
|
+
reason() {
|
|
11031
|
+
return this.trip;
|
|
11032
|
+
}
|
|
11033
|
+
/**
|
|
11034
|
+
* One line for a worker/section that never ran because the breaker was
|
|
11035
|
+
* already open. Says the work was skipped deliberately — not that it failed
|
|
11036
|
+
* on its own merits, which is what an empty FAILED status would imply.
|
|
11037
|
+
*/
|
|
11038
|
+
skipMessage() {
|
|
11039
|
+
const t = this.trip;
|
|
11040
|
+
if (!t) return "Skipped.";
|
|
11041
|
+
return `Skipped \u2014 the run stopped after ${t.failures} consecutive failures on ${t.modelId}. ${t.message}`;
|
|
11042
|
+
}
|
|
11043
|
+
/** Test/diagnostic view of the per-model streaks. */
|
|
11044
|
+
streaks() {
|
|
11045
|
+
const out = {};
|
|
11046
|
+
for (const [k, v] of this.counts) out[k] = { kind: v.kind, n: v.n };
|
|
11047
|
+
return out;
|
|
11048
|
+
}
|
|
11049
|
+
};
|
|
11050
|
+
|
|
10832
11051
|
// src/tools/registry.ts
|
|
10833
11052
|
init_constants();
|
|
10834
11053
|
|
|
@@ -15961,6 +16180,7 @@ ${prompt}` : prompt;
|
|
|
15961
16180
|
if (toolCreator) toolCreator.setPermissionEscalator(escalator);
|
|
15962
16181
|
const rootPrompt = buildContextualPrompt(options.prompt, options.conversationHistory);
|
|
15963
16182
|
let finalOutput = "";
|
|
16183
|
+
const runBreaker = new RunBreaker();
|
|
15964
16184
|
let t2Results = [];
|
|
15965
16185
|
let runError = null;
|
|
15966
16186
|
let identityPrompt = "";
|
|
@@ -16029,6 +16249,7 @@ ${prompt}` : prompt;
|
|
|
16029
16249
|
this.emit("tier:status", { tierId: "t3-root", status: "COMPLETED", role: "T3" });
|
|
16030
16250
|
} else if (complexity === "Moderate") {
|
|
16031
16251
|
const t2 = new T2Manager(this.router, this.toolRegistry, "root");
|
|
16252
|
+
t2.setRunBreaker(runBreaker);
|
|
16032
16253
|
t2.setPresenter(true);
|
|
16033
16254
|
t2.setHierarchyContext("You are the ROOT Manager for this task. There is no T1 Administrator involved in this run. You are responsible for decomposing the task and managing T3 workers directly.");
|
|
16034
16255
|
if (identityPrompt) {
|
|
@@ -16089,6 +16310,7 @@ _(incomplete: ${reasons.join("; ")})_` : partial;
|
|
|
16089
16310
|
}
|
|
16090
16311
|
} else {
|
|
16091
16312
|
const t1 = new T1Administrator(this.router, this.toolRegistry, this.config);
|
|
16313
|
+
t1.setRunBreaker(runBreaker);
|
|
16092
16314
|
t1.setPresenter(true);
|
|
16093
16315
|
t1.setHierarchyContext("You are the top-level Administrator. You are responsible for the overall plan and supervising multiple T2 Managers.");
|
|
16094
16316
|
if (identityPrompt) {
|
|
@@ -16113,6 +16335,20 @@ _(incomplete: ${reasons.join("; ")})_` : partial;
|
|
|
16113
16335
|
finalOutput = result.output;
|
|
16114
16336
|
t2Results = result.t2Results;
|
|
16115
16337
|
}
|
|
16338
|
+
const trip = runBreaker.reason();
|
|
16339
|
+
if (trip) {
|
|
16340
|
+
finalOutput = [
|
|
16341
|
+
`\u26A0 **Run stopped early \u2014 ${trip.modelId} was failing every request.**`,
|
|
16342
|
+
"",
|
|
16343
|
+
trip.message,
|
|
16344
|
+
"",
|
|
16345
|
+
`Cascade stopped after ${trip.failures} consecutive failures rather than retrying every remaining subtask against the same model, which would have cost more and produced the same result.`,
|
|
16346
|
+
finalOutput ? `
|
|
16347
|
+
---
|
|
16348
|
+
|
|
16349
|
+
${finalOutput}` : ""
|
|
16350
|
+
].join("\n").trimEnd();
|
|
16351
|
+
}
|
|
16116
16352
|
} catch (err) {
|
|
16117
16353
|
if (err instanceof CascadeCancelledError || err instanceof Error && err.name === "AbortError" || options.signal?.aborted) {
|
|
16118
16354
|
this.emit("run:cancelled", {
|