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 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.49.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
- this.sendStatusUpdate({ progressPct: 100, currentAction: "Section complete", status: "IN_PROGRESS", output: summary });
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", {