cascade-ai 0.13.1 → 0.13.2

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.
@@ -222595,7 +222595,7 @@ Anthropic.Models = Models2;
222595
222595
  Anthropic.Beta = Beta;
222596
222596
 
222597
222597
  // src/constants.ts
222598
- var CASCADE_VERSION = "0.13.1";
222598
+ var CASCADE_VERSION = "0.13.2";
222599
222599
  var CASCADE_CONFIG_DIR = ".cascade";
222600
222600
  var CASCADE_MD_FILE = "CASCADE.md";
222601
222601
  var CASCADE_IGNORE_FILE = ".cascadeignore";
@@ -251433,6 +251433,13 @@ var BaseTier = class extends EventEmitter__default.default {
251433
251433
  hierarchyContext = "";
251434
251434
  /** Propagated AbortSignal — set by the tier's `execute()` before work begins. */
251435
251435
  signal;
251436
+ /**
251437
+ * True for the run's ROOT tier (T3 for Simple, T2 for Moderate, T1 for
251438
+ * Complex). Its own synthesis stream is the user-facing answer and is
251439
+ * tagged `primary` so the desktop renders it live — background workers,
251440
+ * which would interleave, are not tagged.
251441
+ */
251442
+ isPresenter = false;
251436
251443
  constructor(role, id, parentId) {
251437
251444
  super();
251438
251445
  this.role = role;
@@ -251440,6 +251447,10 @@ var BaseTier = class extends EventEmitter__default.default {
251440
251447
  this.parentId = parentId;
251441
251448
  this.label = this.id;
251442
251449
  }
251450
+ /** Mark this tier as the run's presenter (root tier). */
251451
+ setPresenter(on = true) {
251452
+ this.isPresenter = on;
251453
+ }
251443
251454
  getStatus() {
251444
251455
  return this.status;
251445
251456
  }
@@ -252174,7 +252185,7 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : "") + textToolSuffix,
252174
252185
  "T3",
252175
252186
  options,
252176
252187
  (chunk) => {
252177
- this.emit("stream:token", { tierId: this.id, text: chunk.text });
252188
+ this.emit("stream:token", { tierId: this.id, text: chunk.text, primary: this.isPresenter });
252178
252189
  }
252179
252190
  );
252180
252191
  let effectiveToolCalls = result.toolCalls ?? [];
@@ -253635,6 +253646,7 @@ ${peerOutputs}` : "";
253635
253646
  chunkEnd++;
253636
253647
  }
253637
253648
  i4 = chunkEnd;
253649
+ const isLastChunk = chunkEnd >= completed.length;
253638
253650
  const prompt = `Summarize these T3 worker outputs for section "${assignment.sectionTitle}" in 2-3 sentences.
253639
253651
  ${currentSummary ? `
253640
253652
  PREVIOUS SUMMARY SO FAR:
@@ -253644,6 +253656,7 @@ NEW OUTPUTS TO INTEGRATE:
253644
253656
  ` : "\nOUTPUTS:\n"}${chunkText}${peerContext}`;
253645
253657
  const messages = [{ role: "user", content: prompt }];
253646
253658
  try {
253659
+ const streamFinal = isLastChunk && this.isPresenter ? (chunk) => this.emit("stream:token", { tierId: this.id, text: chunk.text, primary: true }) : void 0;
253647
253660
  const result = await this.router.generate("T2", {
253648
253661
  messages,
253649
253662
  systemPrompt: this.systemPromptOverride + "You are a T2 Manager. Summarize the work of your T3 workers succinctly." + (this.hierarchyContext ? `
@@ -253651,7 +253664,7 @@ NEW OUTPUTS TO INTEGRATE:
253651
253664
  HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
253652
253665
  maxTokens: 500,
253653
253666
  ...this.sectionModel ? { model: this.sectionModel } : {}
253654
- });
253667
+ }, streamFinal);
253655
253668
  currentSummary = result.content;
253656
253669
  } catch (err) {
253657
253670
  this.log(`aggregateResults: LLM summarization failed at chunk \u2014 returning raw T3 outputs. Error: ${err instanceof Error ? err.message : String(err)}`);
@@ -253703,14 +253716,11 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
253703
253716
  ...this.sectionModel ? { model: this.sectionModel } : {}
253704
253717
  });
253705
253718
  const answer = result.content.trim().toUpperCase();
253706
- if (answer.includes("YES")) {
253707
- return { requestId: req.id, approved: true, always: true, decidedBy: "T2", reasoning: "T2 LLM evaluated: consistent with section goal" };
253708
- }
253709
- if (answer.includes("NO")) {
253710
- return { requestId: req.id, approved: false, always: true, decidedBy: "T2", reasoning: "T2 LLM evaluated: inconsistent with section goal" };
253711
- }
253719
+ const verdict = answer.includes("YES") ? "approve" : answer.includes("NO") ? "deny" : "unsure";
253720
+ (req.trail ??= []).push({ tier: "T2", verdict, reason: `T2: ${verdict === "approve" ? "consistent with section goal" : verdict === "deny" ? "inconsistent with section goal" : "unsure"}` });
253712
253721
  return null;
253713
253722
  } catch {
253723
+ (req.trail ??= []).push({ tier: "T2", verdict: "unsure", reason: "T2 evaluation failed" });
253714
253724
  return null;
253715
253725
  }
253716
253726
  }
@@ -254389,7 +254399,7 @@ Instructions:
254389
254399
  systemPrompt: this.systemPromptOverride + "You are a final output compiler. Summarize and format the task results clearly.",
254390
254400
  maxTokens: 8e3
254391
254401
  }, (chunk) => {
254392
- this.emit("stream:token", { tierId: this.id, text: chunk.text });
254402
+ this.emit("stream:token", { tierId: this.id, text: chunk.text, primary: this.isPresenter });
254393
254403
  });
254394
254404
  return result.content;
254395
254405
  }
@@ -254418,14 +254428,11 @@ Reply with exactly one word: YES, NO, or UNSURE.
254418
254428
  temperature: 0
254419
254429
  });
254420
254430
  const answer = result.content.trim().toUpperCase();
254421
- if (answer.includes("YES")) {
254422
- return { requestId: req.id, approved: true, always: true, decidedBy: "T1", reasoning: "T1 evaluated: consistent with overall task goal" };
254423
- }
254424
- if (answer.includes("NO")) {
254425
- return { requestId: req.id, approved: false, always: true, decidedBy: "T1", reasoning: "T1 evaluated: not consistent with overall task goal" };
254426
- }
254431
+ const verdict = answer.includes("YES") ? "approve" : answer.includes("NO") ? "deny" : "unsure";
254432
+ (req.trail ??= []).push({ tier: "T1", verdict, reason: `T1: ${verdict === "approve" ? "consistent with overall task goal" : verdict === "deny" ? "not consistent with overall task goal" : "unsure"}` });
254427
254433
  return null;
254428
254434
  } catch {
254435
+ (req.trail ??= []).push({ tier: "T1", verdict: "unsure", reason: "T1 evaluation failed" });
254429
254436
  return null;
254430
254437
  }
254431
254438
  }
@@ -294706,6 +294713,8 @@ var CascadeConfigSchema = external_exports.object({
294706
294713
  privacy: external_exports.object({
294707
294714
  paths: external_exports.array(external_exports.object({ pattern: external_exports.string().min(1), policy: external_exports.enum(["local-only"]) })).default([])
294708
294715
  }).optional(),
294716
+ /** Routing controls — forceTier pins the root tier, bypassing the classifier. */
294717
+ routing: external_exports.object({ forceTier: external_exports.enum(["auto", "T1", "T2", "T3"]).default("auto") }).optional(),
294709
294718
  /**
294710
294719
  * T3→T2 reinforcement: when enabled, a worker that discovers its subtask should
294711
294720
  * fan out can call the `request_workers` tool to have its T2 manager spawn
@@ -295983,6 +295992,21 @@ ${last3.partialOutput}` : "");
295983
295992
  const producesArtifact = /\b(?:create|build|implement|generate|write|refactor|rewrite|add|fix|deploy|install|migrate|scaffold|set up|save (?:a|the)|report|\.(?:pdf|md|txt|json|csv|py|js|ts|tsx|jsx|html|docx?))\b/i.test(p3);
295984
295993
  return inquiry && !producesArtifact;
295985
295994
  }
295995
+ /**
295996
+ * Strong, explicit signals that a task needs the full hierarchy (planning +
295997
+ * multiple sections/workers). Deliberately conservative: requires a
295998
+ * build/implementation verb AND either an app/system-scale noun or an
295999
+ * explicit multi-part structure, so ordinary single-file asks (handled as
296000
+ * Simple/Moderate) don't get over-escalated.
296001
+ */
296002
+ looksClearlyComplex(prompt) {
296003
+ const p3 = prompt.trim();
296004
+ if (p3.length < 24) return false;
296005
+ const buildVerb = /\b(?:build|implement|create|develop|design|scaffold|refactor|migrate|architect|set up|integrate)\b/i.test(p3);
296006
+ const scaleNoun = /\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/i.test(p3);
296007
+ const multiPart = /(?:\b(?:and|then|also|plus|as well as)\b.*\b(?:and|then|also)\b)|(?:^|\n)\s*(?:[-*]|\d+[.)])\s+/i.test(p3);
296008
+ return buildVerb && (scaleNoun || multiPart);
296009
+ }
295986
296010
  // Cache glob scan results per workspace path to avoid repeated I/O.
295987
296011
  static globCache = /* @__PURE__ */ new Map();
295988
296012
  async countWorkspaceFiles(workspacePath) {
@@ -296066,10 +296090,16 @@ ${prompt}` : prompt;
296066
296090
  let verdict;
296067
296091
  if (match) {
296068
296092
  verdict = match[1] === "simple" ? "Simple" : match[1] === "moderate" ? "Moderate" : "Complex";
296069
- this.recordDecision("complexity", `${verdict} \u2014 classifier: ${reason || "no reason given"}`);
296093
+ if (verdict !== "Complex" && this.looksClearlyComplex(prompt)) {
296094
+ this.recordDecision("complexity", `Complex \u2014 heuristic floor over classifier "${verdict}": explicit multi-step build/implementation signals (T1 engaged)`);
296095
+ verdict = "Complex";
296096
+ } else {
296097
+ this.recordDecision("complexity", `${verdict} \u2014 classifier: ${reason || "no reason given"}`);
296098
+ }
296070
296099
  } else {
296071
- verdict = prompt.trim().split(/\s+/).length <= 12 ? "Simple" : "Moderate";
296072
- this.recordDecision("complexity", `${verdict} \u2014 classifier output unparseable; defaulted by length`);
296100
+ const words = prompt.trim().split(/\s+/).length;
296101
+ verdict = words <= 12 ? "Simple" : words >= 40 || this.looksClearlyComplex(prompt) ? "Complex" : "Moderate";
296102
+ this.recordDecision("complexity", `${verdict} \u2014 classifier output unparseable; defaulted by length/signals`);
296073
296103
  }
296074
296104
  return verdict;
296075
296105
  } catch {
@@ -296121,7 +296151,15 @@ ${prompt}` : prompt;
296121
296151
  }
296122
296152
  escalator.resolveUserDecision(req.id, approved, always);
296123
296153
  });
296124
- const complexity = await this.determineComplexity(options.prompt, options.workspacePath || process.cwd(), options.conversationHistory);
296154
+ const forceTier = this.config.routing?.forceTier;
296155
+ const forced = forceTier === "T1" ? "Complex" : forceTier === "T2" ? "Moderate" : forceTier === "T3" ? "Simple" : void 0;
296156
+ let complexity;
296157
+ if (forced) {
296158
+ complexity = forced;
296159
+ this.recordDecision("complexity", `${forced} \u2014 manually forced via routing.forceTier=${forceTier}`);
296160
+ } else {
296161
+ complexity = await this.determineComplexity(options.prompt, options.workspacePath || process.cwd(), options.conversationHistory);
296162
+ }
296125
296163
  this.telemetry.capture("cascade:session_start", {
296126
296164
  complexity,
296127
296165
  providerCount: this.config.providers.length,
@@ -296201,6 +296239,7 @@ ${prompt}` : prompt;
296201
296239
  try {
296202
296240
  if (complexity === "Simple") {
296203
296241
  const t32 = new T3Worker(this.router, this.toolRegistry, "root");
296242
+ t32.setPresenter(true);
296204
296243
  t32.setHierarchyContext("You are the DIRECT worker for this task. There is no T1 Administrator or T2 Manager involved in this run.");
296205
296244
  if (identityPrompt) {
296206
296245
  t32.setSystemPromptOverride(identityPrompt);
@@ -296225,6 +296264,7 @@ ${prompt}` : prompt;
296225
296264
  this.emit("tier:status", { tierId: "t3-root", status: "COMPLETED", role: "T3" });
296226
296265
  } else if (complexity === "Moderate") {
296227
296266
  const t22 = new T2Manager(this.router, this.toolRegistry, "root");
296267
+ t22.setPresenter(true);
296228
296268
  t22.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.");
296229
296269
  if (identityPrompt) {
296230
296270
  t22.setSystemPromptOverride(identityPrompt);
@@ -296274,6 +296314,7 @@ ${prompt}` : prompt;
296274
296314
  }
296275
296315
  } else {
296276
296316
  const t1 = new T1Administrator(this.router, this.toolRegistry, this.config);
296317
+ t1.setPresenter(true);
296277
296318
  t1.setHierarchyContext("You are the top-level Administrator. You are responsible for the overall plan and supervising multiple T2 Managers.");
296278
296319
  if (identityPrompt) {
296279
296320
  t1.setSystemPromptOverride(identityPrompt);
@@ -298515,7 +298556,8 @@ var DashboardSocket = class {
298515
298556
  socket.on("cascade:run", (payload) => {
298516
298557
  if (typeof payload?.prompt === "string" && payload.prompt.trim()) {
298517
298558
  const sessionId = typeof payload.sessionId === "string" && payload.sessionId.trim() ? payload.sessionId.trim() : void 0;
298518
- callback(payload.prompt.trim(), payload.model ?? "auto", socket.id, sessionId);
298559
+ const forceTier = ["T1", "T2", "T3"].includes(payload.forceTier) ? payload.forceTier : void 0;
298560
+ callback(payload.prompt.trim(), payload.model ?? "auto", socket.id, sessionId, forceTier);
298519
298561
  }
298520
298562
  });
298521
298563
  });
@@ -298542,6 +298584,13 @@ var DashboardServer = class {
298542
298584
  * of runs the session performed in this server's lifetime.
298543
298585
  */
298544
298586
  sessionTaskIds = /* @__PURE__ */ new Map();
298587
+ /**
298588
+ * Tool-approval requests awaiting a user decision from a connected client,
298589
+ * keyed by the request's uuid. The desktop shows a modal on
298590
+ * `permission:user-required` and answers with `permission:decision`; this
298591
+ * map is how that answer reaches the run that's blocked on it.
298592
+ */
298593
+ pendingApprovals = /* @__PURE__ */ new Map();
298545
298594
  port;
298546
298595
  host;
298547
298596
  workspacePath;
@@ -298600,17 +298649,18 @@ var DashboardServer = class {
298600
298649
  }
298601
298650
  this.persistConfig();
298602
298651
  });
298603
- this.socket.onCascadeRun(async (prompt, model, socketId, requestedSessionId) => {
298652
+ this.socket.onCascadeRun(async (prompt, model, socketId, requestedSessionId, forceTier) => {
298604
298653
  const sessionId = requestedSessionId ?? crypto4.randomUUID();
298605
298654
  const abortController = new AbortController();
298606
298655
  this.activeControllers.set(sessionId, abortController);
298607
298656
  const runPrompt = requestedSessionId ? this.buildContinuationPrompt(sessionId, prompt) : prompt;
298608
298657
  const title = this.persistRunStart(sessionId, prompt);
298609
- const cfg = model !== "auto" ? { ...this.config, models: { ...this.config.models, t1: model } } : this.config;
298658
+ let cfg = model !== "auto" ? { ...this.config, models: { ...this.config.models, t1: model } } : this.config;
298659
+ if (forceTier) cfg = { ...cfg, routing: { ...cfg.routing, forceTier } };
298610
298660
  const cascade = new Cascade(cfg, this.workspacePath, this.store);
298611
298661
  this.activeSessions.set(sessionId, cascade);
298612
298662
  cascade.on("stream:token", (e3) => {
298613
- this.socket.emitToSocket(socketId, "stream:token", { sessionId, tierId: e3.tierId, text: e3.text });
298663
+ this.socket.emitToSocket(socketId, "stream:token", { sessionId, tierId: e3.tierId, text: e3.text, primary: e3.primary });
298614
298664
  });
298615
298665
  cascade.on("tier:status", (e3) => {
298616
298666
  this.socket.emitToSocket(socketId, "tier:status", { sessionId, ...e3 });
@@ -298622,7 +298672,11 @@ var DashboardServer = class {
298622
298672
  this.socket.emitPeerMessage(e3);
298623
298673
  });
298624
298674
  try {
298625
- const result = await cascade.run({ prompt: runPrompt, signal: abortController.signal });
298675
+ const result = await cascade.run({
298676
+ prompt: runPrompt,
298677
+ signal: abortController.signal,
298678
+ approvalCallback: this.makeApprovalCallback(sessionId)
298679
+ });
298626
298680
  this.recordSessionTask(sessionId, result.taskId);
298627
298681
  this.persistRunEnd(sessionId, title, prompt, result.output, "COMPLETED");
298628
298682
  this.socket.emitToSocket(socketId, "session:complete", { sessionId, result });
@@ -298641,10 +298695,18 @@ var DashboardServer = class {
298641
298695
  } finally {
298642
298696
  this.activeSessions.delete(sessionId);
298643
298697
  this.activeControllers.delete(sessionId);
298698
+ this.denyPendingApprovals(sessionId);
298644
298699
  }
298645
298700
  });
298646
298701
  this.socket.onSessionHalt((sessionId) => {
298647
298702
  this.activeControllers.get(sessionId)?.abort();
298703
+ this.denyPendingApprovals(sessionId);
298704
+ });
298705
+ this.socket.onApprovalResponse(({ requestId, approved, always }) => {
298706
+ const pending = this.pendingApprovals.get(requestId);
298707
+ if (!pending) return;
298708
+ this.pendingApprovals.delete(requestId);
298709
+ pending.resolve({ approved: !!approved, always: !!always });
298648
298710
  });
298649
298711
  this.socket.onSessionSteer((message, sessionId, nodeId) => {
298650
298712
  const steered = this.steerSessions(message, sessionId, nodeId);
@@ -298812,6 +298874,29 @@ var DashboardServer = class {
298812
298874
  list.push(taskId);
298813
298875
  this.sessionTaskIds.set(sessionId, list);
298814
298876
  }
298877
+ /**
298878
+ * Approval bridge: cascade calls this when a dangerous tool escalates to the
298879
+ * user. The request itself was already pushed to the client via the
298880
+ * `permission:user-required` forward; here we just park a resolver keyed by
298881
+ * the request id and wait for the client's `permission:decision` (handled in
298882
+ * onApprovalResponse). Never auto-approves — an unanswered request stays
298883
+ * pending until the client answers, the run ends, or the escalator's own
298884
+ * timeout denies it.
298885
+ */
298886
+ makeApprovalCallback(sessionId) {
298887
+ return (request) => new Promise((resolve) => {
298888
+ this.pendingApprovals.set(request.id, { resolve, sessionId });
298889
+ });
298890
+ }
298891
+ /** Deny + clear any approvals still pending for a session (run end / abort). */
298892
+ denyPendingApprovals(sessionId) {
298893
+ for (const [id, pending] of this.pendingApprovals) {
298894
+ if (pending.sessionId === sessionId) {
298895
+ this.pendingApprovals.delete(id);
298896
+ pending.resolve({ approved: false, always: false });
298897
+ }
298898
+ }
298899
+ }
298815
298900
  persistRuntimeRow(sessionId, title, status, latestPrompt) {
298816
298901
  const now = (/* @__PURE__ */ new Date()).toISOString();
298817
298902
  const row = { sessionId, title, workspacePath: this.workspacePath, status, startedAt: now, updatedAt: now, latestPrompt, isGlobal: false };
@@ -299000,15 +299085,6 @@ ${prompt}`;
299000
299085
  if (sessionId) this.socket.broadcastToRoom(`session:${sessionId}`, "session:halt", payload);
299001
299086
  res.json({ success: true, ...payload });
299002
299087
  });
299003
- this.app.post("/api/approve", auth, mutationLimiter, (req, res) => {
299004
- const body = req.body;
299005
- const sessionId = typeof body["sessionId"] === "string" ? body["sessionId"] : void 0;
299006
- const nodeId = typeof body["nodeId"] === "string" ? body["nodeId"] : void 0;
299007
- const payload = { sessionId, nodeId, requestedAt: (/* @__PURE__ */ new Date()).toISOString() };
299008
- this.socket.broadcast("session:approve", payload);
299009
- if (sessionId) this.socket.broadcastToRoom(`session:${sessionId}`, "session:approve", payload);
299010
- res.json({ success: true, ...payload });
299011
- });
299012
299088
  this.app.post("/api/inject", auth, mutationLimiter, (req, res) => {
299013
299089
  const body = req.body;
299014
299090
  const message = typeof body["message"] === "string" ? body["message"] : void 0;
@@ -299259,7 +299335,7 @@ ${prompt}`;
299259
299335
  const cascade = new Cascade(this.config, this.workspacePath, this.store);
299260
299336
  this.activeSessions.set(sessionId, cascade);
299261
299337
  cascade.on("stream:token", (e3) => {
299262
- this.socket.broadcastToRoom(`session:${sessionId}`, "stream:token", { sessionId, tierId: e3.tierId, text: e3.text });
299338
+ this.socket.broadcastToRoom(`session:${sessionId}`, "stream:token", { sessionId, tierId: e3.tierId, text: e3.text, primary: e3.primary });
299263
299339
  });
299264
299340
  cascade.on("tier:status", (e3) => {
299265
299341
  this.socket.broadcastToRoom(`session:${sessionId}`, "tier:status", { sessionId, ...e3 });
@@ -299271,7 +299347,11 @@ ${prompt}`;
299271
299347
  this.socket.emitPeerMessage(e3);
299272
299348
  });
299273
299349
  try {
299274
- const result = await cascade.run({ prompt: runPrompt, identityId: body.identityId });
299350
+ const result = await cascade.run({
299351
+ prompt: runPrompt,
299352
+ identityId: body.identityId,
299353
+ approvalCallback: this.makeApprovalCallback(sessionId)
299354
+ });
299275
299355
  this.recordSessionTask(sessionId, result.taskId);
299276
299356
  this.persistRunEnd(sessionId, title, prompt, result.output, "COMPLETED");
299277
299357
  this.socket.broadcast("cost:update", {
@@ -299289,6 +299369,7 @@ ${prompt}`;
299289
299369
  });
299290
299370
  } finally {
299291
299371
  this.activeSessions.delete(sessionId);
299372
+ this.denyPendingApprovals(sessionId);
299292
299373
  }
299293
299374
  })();
299294
299375
  });