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.js
CHANGED
|
@@ -1740,7 +1740,7 @@ var CASCADE_VERSION, CASCADE_CONFIG_FILE, CASCADE_DB_FILE, CASCADE_DASHBOARD_SEC
|
|
|
1740
1740
|
var init_constants = __esm({
|
|
1741
1741
|
"src/constants.ts"() {
|
|
1742
1742
|
init_pricing();
|
|
1743
|
-
CASCADE_VERSION = "0.
|
|
1743
|
+
CASCADE_VERSION = "0.50.0";
|
|
1744
1744
|
CASCADE_CONFIG_FILE = ".cascade/config.json";
|
|
1745
1745
|
CASCADE_DB_FILE = ".cascade/memory.db";
|
|
1746
1746
|
CASCADE_DASHBOARD_SECRET_FILE = ".cascade/dashboard-secret";
|
|
@@ -7615,6 +7615,14 @@ var BaseTier = class extends EventEmitter {
|
|
|
7615
7615
|
this.emit("tier:status", event);
|
|
7616
7616
|
}
|
|
7617
7617
|
/** Record the model serving this tier; future status events carry it. */
|
|
7618
|
+
/**
|
|
7619
|
+
* The model actually serving this tier ("provider:id"), once one has been
|
|
7620
|
+
* chosen. The run breaker reads it to attribute a failure to a model rather
|
|
7621
|
+
* than to the subtask that happened to hit it.
|
|
7622
|
+
*/
|
|
7623
|
+
getServingModel() {
|
|
7624
|
+
return this.servingModel;
|
|
7625
|
+
}
|
|
7618
7626
|
setServingModel(model) {
|
|
7619
7627
|
this.servingModel = model || void 0;
|
|
7620
7628
|
}
|
|
@@ -9351,6 +9359,7 @@ var T2Manager = class extends BaseTier {
|
|
|
9351
9359
|
escalations = [];
|
|
9352
9360
|
peerSyncBuffer = [];
|
|
9353
9361
|
store;
|
|
9362
|
+
runBreaker;
|
|
9354
9363
|
t3PeerBus = new PeerBus();
|
|
9355
9364
|
// ← T3↔T3 bus (local to this T2)
|
|
9356
9365
|
t2PeerBus;
|
|
@@ -9381,6 +9390,15 @@ var T2Manager = class extends BaseTier {
|
|
|
9381
9390
|
this.router = router;
|
|
9382
9391
|
this.toolRegistry = toolRegistry;
|
|
9383
9392
|
}
|
|
9393
|
+
/**
|
|
9394
|
+
* Share the run-wide circuit breaker. Every T2 in a run gets the SAME
|
|
9395
|
+
* instance: three sections each failing once against a dead model is the
|
|
9396
|
+
* same fact as one section failing three times, and a per-section breaker
|
|
9397
|
+
* would never see it.
|
|
9398
|
+
*/
|
|
9399
|
+
setRunBreaker(breaker) {
|
|
9400
|
+
this.runBreaker = breaker;
|
|
9401
|
+
}
|
|
9384
9402
|
setStore(store) {
|
|
9385
9403
|
this.store = store;
|
|
9386
9404
|
}
|
|
@@ -9504,7 +9522,8 @@ Guidance (must be followed): ${decision.note}`
|
|
|
9504
9522
|
const overallStatus = this.determineStatus(t3Results);
|
|
9505
9523
|
const isOk = overallStatus === "COMPLETED" || overallStatus === "PARTIAL";
|
|
9506
9524
|
this.setStatus(isOk ? "COMPLETED" : "FAILED", summary);
|
|
9507
|
-
|
|
9525
|
+
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";
|
|
9526
|
+
this.sendStatusUpdate({ progressPct: 100, currentAction: sectionAction, status: "IN_PROGRESS", output: summary });
|
|
9508
9527
|
const result = {
|
|
9509
9528
|
sectionId: assignment.sectionId,
|
|
9510
9529
|
sectionTitle: assignment.sectionTitle,
|
|
@@ -9674,6 +9693,24 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
|
9674
9693
|
const reinforceCfg = this.router.getReinforcementsConfig?.() ?? { enabled: false, maxPerSection: 4 };
|
|
9675
9694
|
let reinforcementsAdded = 0;
|
|
9676
9695
|
while (remaining.size > 0) {
|
|
9696
|
+
if (this.runBreaker?.isOpen()) {
|
|
9697
|
+
const why = this.runBreaker.skipMessage();
|
|
9698
|
+
this.log(`Run breaker open \u2014 skipping ${remaining.size} remaining subtask(s)`);
|
|
9699
|
+
for (const id of remaining) {
|
|
9700
|
+
this.t3PeerBus.publish(this.id, id, why, "FAILED");
|
|
9701
|
+
resultMap.set(id, {
|
|
9702
|
+
subtaskId: id,
|
|
9703
|
+
status: "FAILED",
|
|
9704
|
+
output: why,
|
|
9705
|
+
testResults: { checksRun: [], passed: [], failed: [] },
|
|
9706
|
+
issues: [why],
|
|
9707
|
+
peerSyncsUsed: [],
|
|
9708
|
+
correctionAttempts: 0
|
|
9709
|
+
});
|
|
9710
|
+
}
|
|
9711
|
+
remaining.clear();
|
|
9712
|
+
break;
|
|
9713
|
+
}
|
|
9677
9714
|
const runnableIds = [...remaining].filter((id) => (inDegree.get(id) ?? 0) === 0);
|
|
9678
9715
|
if (runnableIds.length === 0) {
|
|
9679
9716
|
const fallbackId = [...remaining].sort(
|
|
@@ -9776,8 +9813,32 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
|
9776
9813
|
remaining.delete(id);
|
|
9777
9814
|
const r = waveResults[i];
|
|
9778
9815
|
if (r.status === "rejected") {
|
|
9779
|
-
this.log(`T3 worker ${id} failed: ${r.reason instanceof Error ? r.reason.message : String(r.reason)} \u2014 retrying once`);
|
|
9780
9816
|
const assignment = sanitizedAssignments.find((a) => a.subtaskId === id);
|
|
9817
|
+
const failedModel = workerMap.get(id)?.getServingModel();
|
|
9818
|
+
const tripped = this.runBreaker?.record(r.reason, failedModel);
|
|
9819
|
+
if (tripped) {
|
|
9820
|
+
this.log(`Run breaker opened: ${tripped.failures} consecutive ${tripped.kind} failures on ${tripped.modelId} \u2014 stopping`);
|
|
9821
|
+
this.emit("run:breaker", tripped);
|
|
9822
|
+
}
|
|
9823
|
+
if (this.runBreaker?.isOpen()) {
|
|
9824
|
+
const why = this.runBreaker.skipMessage();
|
|
9825
|
+
this.log(`T3 worker ${id} failed and the run breaker is open \u2014 not retrying`);
|
|
9826
|
+
this.t3PeerBus.publish(this.id, id, why, "FAILED");
|
|
9827
|
+
resultMap.set(id, {
|
|
9828
|
+
subtaskId: id,
|
|
9829
|
+
status: "FAILED",
|
|
9830
|
+
output: why,
|
|
9831
|
+
testResults: { checksRun: [], passed: [], failed: [] },
|
|
9832
|
+
issues: [why],
|
|
9833
|
+
peerSyncsUsed: [],
|
|
9834
|
+
correctionAttempts: 0
|
|
9835
|
+
});
|
|
9836
|
+
for (const dependent of adj.get(id) ?? []) {
|
|
9837
|
+
inDegree.set(dependent, Math.max(0, (inDegree.get(dependent) ?? 0) - 1));
|
|
9838
|
+
}
|
|
9839
|
+
continue;
|
|
9840
|
+
}
|
|
9841
|
+
this.log(`T3 worker ${id} failed: ${r.reason instanceof Error ? r.reason.message : String(r.reason)} \u2014 retrying once`);
|
|
9781
9842
|
try {
|
|
9782
9843
|
const retried = await this.retryT3(assignment, taskId);
|
|
9783
9844
|
resultMap.set(id, retried);
|
|
@@ -10112,6 +10173,7 @@ var T1Administrator = class extends BaseTier {
|
|
|
10112
10173
|
t2Managers = /* @__PURE__ */ new Map();
|
|
10113
10174
|
escalations = [];
|
|
10114
10175
|
store;
|
|
10176
|
+
runBreaker;
|
|
10115
10177
|
t2PeerBus = new PeerBus();
|
|
10116
10178
|
permissionEscalator;
|
|
10117
10179
|
toolCreator;
|
|
@@ -10126,6 +10188,10 @@ var T1Administrator = class extends BaseTier {
|
|
|
10126
10188
|
this.toolRegistry = toolRegistry;
|
|
10127
10189
|
this.config = config;
|
|
10128
10190
|
}
|
|
10191
|
+
/** Share the run-wide circuit breaker with every section manager. */
|
|
10192
|
+
setRunBreaker(breaker) {
|
|
10193
|
+
this.runBreaker = breaker;
|
|
10194
|
+
}
|
|
10129
10195
|
setStore(store) {
|
|
10130
10196
|
this.store = store;
|
|
10131
10197
|
}
|
|
@@ -10488,6 +10554,7 @@ SPEC RULES \u2014 each subtask is a self-contained spec slice (workers execute f
|
|
|
10488
10554
|
};
|
|
10489
10555
|
const managers = sections.map((section) => {
|
|
10490
10556
|
const manager = new T2Manager(this.router, this.toolRegistry, this.id);
|
|
10557
|
+
if (this.runBreaker) manager.setRunBreaker(this.runBreaker);
|
|
10491
10558
|
manager.setHierarchyContext(`You are a T2 Manager for the section "${section.sectionTitle}". You are part of a COMPLEX task overseen by T1 Administrator.`);
|
|
10492
10559
|
if (this.store) {
|
|
10493
10560
|
manager.setStore(this.store);
|
|
@@ -10776,6 +10843,158 @@ Reply with exactly one word: YES, NO, or UNSURE.
|
|
|
10776
10843
|
// src/core/cascade.ts
|
|
10777
10844
|
init_cost();
|
|
10778
10845
|
|
|
10846
|
+
// src/core/router/provider-errors.ts
|
|
10847
|
+
var SYSTEMIC = /* @__PURE__ */ new Set([
|
|
10848
|
+
"rate_limit",
|
|
10849
|
+
"quota_exhausted",
|
|
10850
|
+
"auth",
|
|
10851
|
+
"model_unavailable"
|
|
10852
|
+
]);
|
|
10853
|
+
function isSystemicKind(kind) {
|
|
10854
|
+
return SYSTEMIC.has(kind);
|
|
10855
|
+
}
|
|
10856
|
+
function statusOf(err) {
|
|
10857
|
+
if (!err || typeof err !== "object") return void 0;
|
|
10858
|
+
const e = err;
|
|
10859
|
+
for (const key of ["status", "statusCode", "code"]) {
|
|
10860
|
+
const v = e[key];
|
|
10861
|
+
if (typeof v === "number" && v >= 100 && v < 600) return v;
|
|
10862
|
+
}
|
|
10863
|
+
const resp = e["response"];
|
|
10864
|
+
if (resp && typeof resp["status"] === "number") return resp["status"];
|
|
10865
|
+
const inner = e["error"];
|
|
10866
|
+
if (inner && typeof inner["code"] === "number") return inner["code"];
|
|
10867
|
+
return void 0;
|
|
10868
|
+
}
|
|
10869
|
+
function messageOf(err) {
|
|
10870
|
+
if (err instanceof Error) return err.message;
|
|
10871
|
+
if (typeof err === "string") return err;
|
|
10872
|
+
if (err && typeof err === "object") {
|
|
10873
|
+
const e = err;
|
|
10874
|
+
if (typeof e["message"] === "string") return e["message"];
|
|
10875
|
+
const inner = e["error"];
|
|
10876
|
+
if (inner && typeof inner["message"] === "string") return inner["message"];
|
|
10877
|
+
try {
|
|
10878
|
+
return JSON.stringify(err);
|
|
10879
|
+
} catch {
|
|
10880
|
+
}
|
|
10881
|
+
}
|
|
10882
|
+
return String(err);
|
|
10883
|
+
}
|
|
10884
|
+
function classifyProviderError(err) {
|
|
10885
|
+
const raw = messageOf(err).trim();
|
|
10886
|
+
const status = statusOf(err);
|
|
10887
|
+
const m = raw.toLowerCase();
|
|
10888
|
+
const kind = (() => {
|
|
10889
|
+
if (status === 429) {
|
|
10890
|
+
return /quota|billing|credit|exceeded your current quota|insufficient/.test(m) ? "quota_exhausted" : "rate_limit";
|
|
10891
|
+
}
|
|
10892
|
+
if (status === 401 || status === 403) return "auth";
|
|
10893
|
+
if (status === 404) return "model_unavailable";
|
|
10894
|
+
if (/rate limit|too many requests|resource_exhausted/.test(m)) {
|
|
10895
|
+
return /quota|billing|credit|insufficient/.test(m) ? "quota_exhausted" : "rate_limit";
|
|
10896
|
+
}
|
|
10897
|
+
if (/quota|billing|insufficient_quota|insufficient credit|payment required/.test(m)) return "quota_exhausted";
|
|
10898
|
+
if (/api key|unauthorized|authentication|permission denied|invalid[_ ]api/.test(m)) return "auth";
|
|
10899
|
+
if (/model .*(not found|does not exist|unavailable)|no such model|unknown model|not supported for/.test(m)) {
|
|
10900
|
+
return "model_unavailable";
|
|
10901
|
+
}
|
|
10902
|
+
if (/context length|too many tokens|maximum context|prompt is too long|input too long/.test(m)) return "context_length";
|
|
10903
|
+
if (/safety|content filter|blocked|recitation|prohibited/.test(m)) return "content_filter";
|
|
10904
|
+
if (/econnreset|etimedout|enotfound|socket hang up|network|fetch failed|aborted/.test(m)) return "network";
|
|
10905
|
+
return "unknown";
|
|
10906
|
+
})();
|
|
10907
|
+
return { kind, systemic: isSystemicKind(kind), status, raw };
|
|
10908
|
+
}
|
|
10909
|
+
function describeProviderError(c, modelId) {
|
|
10910
|
+
const on = ` on ${modelId}` ;
|
|
10911
|
+
const detail = c.raw ? ` Provider said: ${c.raw}` : "";
|
|
10912
|
+
switch (c.kind) {
|
|
10913
|
+
case "rate_limit":
|
|
10914
|
+
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}`;
|
|
10915
|
+
case "quota_exhausted":
|
|
10916
|
+
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}`;
|
|
10917
|
+
case "auth":
|
|
10918
|
+
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}`;
|
|
10919
|
+
case "model_unavailable":
|
|
10920
|
+
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}`;
|
|
10921
|
+
case "context_length":
|
|
10922
|
+
return `The prompt was too long${on}. Reduce the attached context, or use a model with a larger context window for this tier.${detail}`;
|
|
10923
|
+
case "content_filter":
|
|
10924
|
+
return `The provider's safety filter refused this request${on}.${detail}`;
|
|
10925
|
+
case "network":
|
|
10926
|
+
return `Network error reaching the provider${on}. This is usually transient.${detail}`;
|
|
10927
|
+
default:
|
|
10928
|
+
return `The model call failed${on}.${detail}`;
|
|
10929
|
+
}
|
|
10930
|
+
}
|
|
10931
|
+
|
|
10932
|
+
// src/core/run-breaker.ts
|
|
10933
|
+
var DEFAULT_FAILURE_THRESHOLD = 3;
|
|
10934
|
+
var RunBreaker = class {
|
|
10935
|
+
threshold;
|
|
10936
|
+
/** modelId → consecutive systemic failures of the SAME kind. */
|
|
10937
|
+
counts = /* @__PURE__ */ new Map();
|
|
10938
|
+
trip = null;
|
|
10939
|
+
constructor(threshold = DEFAULT_FAILURE_THRESHOLD) {
|
|
10940
|
+
this.threshold = Math.max(1, threshold);
|
|
10941
|
+
}
|
|
10942
|
+
/**
|
|
10943
|
+
* Record a failed model call. Returns the trip if this failure is the one
|
|
10944
|
+
* that opened the breaker, otherwise null.
|
|
10945
|
+
*
|
|
10946
|
+
* A non-systemic failure RESETS the model's counter: it is evidence the model
|
|
10947
|
+
* is reachable and answering, which is exactly what a systemic streak claims
|
|
10948
|
+
* it is not.
|
|
10949
|
+
*/
|
|
10950
|
+
record(err, modelId) {
|
|
10951
|
+
if (this.trip) return null;
|
|
10952
|
+
const id = modelId?.trim() || "unknown-model";
|
|
10953
|
+
const c = classifyProviderError(err);
|
|
10954
|
+
if (!c.systemic) {
|
|
10955
|
+
this.counts.delete(id);
|
|
10956
|
+
return null;
|
|
10957
|
+
}
|
|
10958
|
+
const prev = this.counts.get(id);
|
|
10959
|
+
const n = prev && prev.kind === c.kind ? prev.n + 1 : 1;
|
|
10960
|
+
this.counts.set(id, { kind: c.kind, n, last: c });
|
|
10961
|
+
if (n >= this.threshold) {
|
|
10962
|
+
this.trip = {
|
|
10963
|
+
modelId: id,
|
|
10964
|
+
kind: c.kind,
|
|
10965
|
+
failures: n,
|
|
10966
|
+
message: describeProviderError(c, id)
|
|
10967
|
+
};
|
|
10968
|
+
return this.trip;
|
|
10969
|
+
}
|
|
10970
|
+
return null;
|
|
10971
|
+
}
|
|
10972
|
+
/** True once the run should stop launching work that would fail identically. */
|
|
10973
|
+
isOpen() {
|
|
10974
|
+
return this.trip !== null;
|
|
10975
|
+
}
|
|
10976
|
+
/** Why the breaker opened, or null while it is closed. */
|
|
10977
|
+
reason() {
|
|
10978
|
+
return this.trip;
|
|
10979
|
+
}
|
|
10980
|
+
/**
|
|
10981
|
+
* One line for a worker/section that never ran because the breaker was
|
|
10982
|
+
* already open. Says the work was skipped deliberately — not that it failed
|
|
10983
|
+
* on its own merits, which is what an empty FAILED status would imply.
|
|
10984
|
+
*/
|
|
10985
|
+
skipMessage() {
|
|
10986
|
+
const t = this.trip;
|
|
10987
|
+
if (!t) return "Skipped.";
|
|
10988
|
+
return `Skipped \u2014 the run stopped after ${t.failures} consecutive failures on ${t.modelId}. ${t.message}`;
|
|
10989
|
+
}
|
|
10990
|
+
/** Test/diagnostic view of the per-model streaks. */
|
|
10991
|
+
streaks() {
|
|
10992
|
+
const out = {};
|
|
10993
|
+
for (const [k, v] of this.counts) out[k] = { kind: v.kind, n: v.n };
|
|
10994
|
+
return out;
|
|
10995
|
+
}
|
|
10996
|
+
};
|
|
10997
|
+
|
|
10779
10998
|
// src/tools/registry.ts
|
|
10780
10999
|
init_constants();
|
|
10781
11000
|
|
|
@@ -15908,6 +16127,7 @@ ${prompt}` : prompt;
|
|
|
15908
16127
|
if (toolCreator) toolCreator.setPermissionEscalator(escalator);
|
|
15909
16128
|
const rootPrompt = buildContextualPrompt(options.prompt, options.conversationHistory);
|
|
15910
16129
|
let finalOutput = "";
|
|
16130
|
+
const runBreaker = new RunBreaker();
|
|
15911
16131
|
let t2Results = [];
|
|
15912
16132
|
let runError = null;
|
|
15913
16133
|
let identityPrompt = "";
|
|
@@ -15976,6 +16196,7 @@ ${prompt}` : prompt;
|
|
|
15976
16196
|
this.emit("tier:status", { tierId: "t3-root", status: "COMPLETED", role: "T3" });
|
|
15977
16197
|
} else if (complexity === "Moderate") {
|
|
15978
16198
|
const t2 = new T2Manager(this.router, this.toolRegistry, "root");
|
|
16199
|
+
t2.setRunBreaker(runBreaker);
|
|
15979
16200
|
t2.setPresenter(true);
|
|
15980
16201
|
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.");
|
|
15981
16202
|
if (identityPrompt) {
|
|
@@ -16036,6 +16257,7 @@ _(incomplete: ${reasons.join("; ")})_` : partial;
|
|
|
16036
16257
|
}
|
|
16037
16258
|
} else {
|
|
16038
16259
|
const t1 = new T1Administrator(this.router, this.toolRegistry, this.config);
|
|
16260
|
+
t1.setRunBreaker(runBreaker);
|
|
16039
16261
|
t1.setPresenter(true);
|
|
16040
16262
|
t1.setHierarchyContext("You are the top-level Administrator. You are responsible for the overall plan and supervising multiple T2 Managers.");
|
|
16041
16263
|
if (identityPrompt) {
|
|
@@ -16060,6 +16282,20 @@ _(incomplete: ${reasons.join("; ")})_` : partial;
|
|
|
16060
16282
|
finalOutput = result.output;
|
|
16061
16283
|
t2Results = result.t2Results;
|
|
16062
16284
|
}
|
|
16285
|
+
const trip = runBreaker.reason();
|
|
16286
|
+
if (trip) {
|
|
16287
|
+
finalOutput = [
|
|
16288
|
+
`\u26A0 **Run stopped early \u2014 ${trip.modelId} was failing every request.**`,
|
|
16289
|
+
"",
|
|
16290
|
+
trip.message,
|
|
16291
|
+
"",
|
|
16292
|
+
`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.`,
|
|
16293
|
+
finalOutput ? `
|
|
16294
|
+
---
|
|
16295
|
+
|
|
16296
|
+
${finalOutput}` : ""
|
|
16297
|
+
].join("\n").trimEnd();
|
|
16298
|
+
}
|
|
16063
16299
|
} catch (err) {
|
|
16064
16300
|
if (err instanceof CascadeCancelledError || err instanceof Error && err.name === "AbortError" || options.signal?.aborted) {
|
|
16065
16301
|
this.emit("run:cancelled", {
|