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