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.
package/dist/index.cjs CHANGED
@@ -236,7 +236,7 @@ var init_audit_logger = __esm({
236
236
  });
237
237
 
238
238
  // src/constants.ts
239
- var CASCADE_VERSION = "0.13.1";
239
+ var CASCADE_VERSION = "0.13.2";
240
240
  var CASCADE_CONFIG_DIR = ".cascade";
241
241
  var CASCADE_MD_FILE = "CASCADE.md";
242
242
  var CASCADE_IGNORE_FILE = ".cascadeignore";
@@ -3169,6 +3169,13 @@ var BaseTier = class extends EventEmitter__default.default {
3169
3169
  hierarchyContext = "";
3170
3170
  /** Propagated AbortSignal — set by the tier's `execute()` before work begins. */
3171
3171
  signal;
3172
+ /**
3173
+ * True for the run's ROOT tier (T3 for Simple, T2 for Moderate, T1 for
3174
+ * Complex). Its own synthesis stream is the user-facing answer and is
3175
+ * tagged `primary` so the desktop renders it live — background workers,
3176
+ * which would interleave, are not tagged.
3177
+ */
3178
+ isPresenter = false;
3172
3179
  constructor(role, id, parentId) {
3173
3180
  super();
3174
3181
  this.role = role;
@@ -3176,6 +3183,10 @@ var BaseTier = class extends EventEmitter__default.default {
3176
3183
  this.parentId = parentId;
3177
3184
  this.label = this.id;
3178
3185
  }
3186
+ /** Mark this tier as the run's presenter (root tier). */
3187
+ setPresenter(on = true) {
3188
+ this.isPresenter = on;
3189
+ }
3179
3190
  getStatus() {
3180
3191
  return this.status;
3181
3192
  }
@@ -3910,7 +3921,7 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : "") + textToolSuffix,
3910
3921
  "T3",
3911
3922
  options,
3912
3923
  (chunk) => {
3913
- this.emit("stream:token", { tierId: this.id, text: chunk.text });
3924
+ this.emit("stream:token", { tierId: this.id, text: chunk.text, primary: this.isPresenter });
3914
3925
  }
3915
3926
  );
3916
3927
  let effectiveToolCalls = result.toolCalls ?? [];
@@ -5371,6 +5382,7 @@ ${peerOutputs}` : "";
5371
5382
  chunkEnd++;
5372
5383
  }
5373
5384
  i = chunkEnd;
5385
+ const isLastChunk = chunkEnd >= completed.length;
5374
5386
  const prompt = `Summarize these T3 worker outputs for section "${assignment.sectionTitle}" in 2-3 sentences.
5375
5387
  ${currentSummary ? `
5376
5388
  PREVIOUS SUMMARY SO FAR:
@@ -5380,6 +5392,7 @@ NEW OUTPUTS TO INTEGRATE:
5380
5392
  ` : "\nOUTPUTS:\n"}${chunkText}${peerContext}`;
5381
5393
  const messages = [{ role: "user", content: prompt }];
5382
5394
  try {
5395
+ const streamFinal = isLastChunk && this.isPresenter ? (chunk) => this.emit("stream:token", { tierId: this.id, text: chunk.text, primary: true }) : void 0;
5383
5396
  const result = await this.router.generate("T2", {
5384
5397
  messages,
5385
5398
  systemPrompt: this.systemPromptOverride + "You are a T2 Manager. Summarize the work of your T3 workers succinctly." + (this.hierarchyContext ? `
@@ -5387,7 +5400,7 @@ NEW OUTPUTS TO INTEGRATE:
5387
5400
  HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
5388
5401
  maxTokens: 500,
5389
5402
  ...this.sectionModel ? { model: this.sectionModel } : {}
5390
- });
5403
+ }, streamFinal);
5391
5404
  currentSummary = result.content;
5392
5405
  } catch (err) {
5393
5406
  this.log(`aggregateResults: LLM summarization failed at chunk \u2014 returning raw T3 outputs. Error: ${err instanceof Error ? err.message : String(err)}`);
@@ -5439,14 +5452,11 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
5439
5452
  ...this.sectionModel ? { model: this.sectionModel } : {}
5440
5453
  });
5441
5454
  const answer = result.content.trim().toUpperCase();
5442
- if (answer.includes("YES")) {
5443
- return { requestId: req.id, approved: true, always: true, decidedBy: "T2", reasoning: "T2 LLM evaluated: consistent with section goal" };
5444
- }
5445
- if (answer.includes("NO")) {
5446
- return { requestId: req.id, approved: false, always: true, decidedBy: "T2", reasoning: "T2 LLM evaluated: inconsistent with section goal" };
5447
- }
5455
+ const verdict = answer.includes("YES") ? "approve" : answer.includes("NO") ? "deny" : "unsure";
5456
+ (req.trail ??= []).push({ tier: "T2", verdict, reason: `T2: ${verdict === "approve" ? "consistent with section goal" : verdict === "deny" ? "inconsistent with section goal" : "unsure"}` });
5448
5457
  return null;
5449
5458
  } catch {
5459
+ (req.trail ??= []).push({ tier: "T2", verdict: "unsure", reason: "T2 evaluation failed" });
5450
5460
  return null;
5451
5461
  }
5452
5462
  }
@@ -6125,7 +6135,7 @@ Instructions:
6125
6135
  systemPrompt: this.systemPromptOverride + "You are a final output compiler. Summarize and format the task results clearly.",
6126
6136
  maxTokens: 8e3
6127
6137
  }, (chunk) => {
6128
- this.emit("stream:token", { tierId: this.id, text: chunk.text });
6138
+ this.emit("stream:token", { tierId: this.id, text: chunk.text, primary: this.isPresenter });
6129
6139
  });
6130
6140
  return result.content;
6131
6141
  }
@@ -6154,14 +6164,11 @@ Reply with exactly one word: YES, NO, or UNSURE.
6154
6164
  temperature: 0
6155
6165
  });
6156
6166
  const answer = result.content.trim().toUpperCase();
6157
- if (answer.includes("YES")) {
6158
- return { requestId: req.id, approved: true, always: true, decidedBy: "T1", reasoning: "T1 evaluated: consistent with overall task goal" };
6159
- }
6160
- if (answer.includes("NO")) {
6161
- return { requestId: req.id, approved: false, always: true, decidedBy: "T1", reasoning: "T1 evaluated: not consistent with overall task goal" };
6162
- }
6167
+ const verdict = answer.includes("YES") ? "approve" : answer.includes("NO") ? "deny" : "unsure";
6168
+ (req.trail ??= []).push({ tier: "T1", verdict, reason: `T1: ${verdict === "approve" ? "consistent with overall task goal" : verdict === "deny" ? "not consistent with overall task goal" : "unsure"}` });
6163
6169
  return null;
6164
6170
  } catch {
6171
+ (req.trail ??= []).push({ tier: "T1", verdict: "unsure", reason: "T1 evaluation failed" });
6165
6172
  return null;
6166
6173
  }
6167
6174
  }
@@ -8268,6 +8275,8 @@ var CascadeConfigSchema = zod.z.object({
8268
8275
  privacy: zod.z.object({
8269
8276
  paths: zod.z.array(zod.z.object({ pattern: zod.z.string().min(1), policy: zod.z.enum(["local-only"]) })).default([])
8270
8277
  }).optional(),
8278
+ /** Routing controls — forceTier pins the root tier, bypassing the classifier. */
8279
+ routing: zod.z.object({ forceTier: zod.z.enum(["auto", "T1", "T2", "T3"]).default("auto") }).optional(),
8271
8280
  /**
8272
8281
  * T3→T2 reinforcement: when enabled, a worker that discovers its subtask should
8273
8282
  * fan out can call the `request_workers` tool to have its T2 manager spawn
@@ -9542,6 +9551,21 @@ ${last.partialOutput}` : "");
9542
9551
  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(p);
9543
9552
  return inquiry && !producesArtifact;
9544
9553
  }
9554
+ /**
9555
+ * Strong, explicit signals that a task needs the full hierarchy (planning +
9556
+ * multiple sections/workers). Deliberately conservative: requires a
9557
+ * build/implementation verb AND either an app/system-scale noun or an
9558
+ * explicit multi-part structure, so ordinary single-file asks (handled as
9559
+ * Simple/Moderate) don't get over-escalated.
9560
+ */
9561
+ looksClearlyComplex(prompt) {
9562
+ const p = prompt.trim();
9563
+ if (p.length < 24) return false;
9564
+ const buildVerb = /\b(?:build|implement|create|develop|design|scaffold|refactor|migrate|architect|set up|integrate)\b/i.test(p);
9565
+ 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(p);
9566
+ const multiPart = /(?:\b(?:and|then|also|plus|as well as)\b.*\b(?:and|then|also)\b)|(?:^|\n)\s*(?:[-*]|\d+[.)])\s+/i.test(p);
9567
+ return buildVerb && (scaleNoun || multiPart);
9568
+ }
9545
9569
  // Cache glob scan results per workspace path to avoid repeated I/O.
9546
9570
  static globCache = /* @__PURE__ */ new Map();
9547
9571
  async countWorkspaceFiles(workspacePath) {
@@ -9625,10 +9649,16 @@ ${prompt}` : prompt;
9625
9649
  let verdict;
9626
9650
  if (match) {
9627
9651
  verdict = match[1] === "simple" ? "Simple" : match[1] === "moderate" ? "Moderate" : "Complex";
9628
- this.recordDecision("complexity", `${verdict} \u2014 classifier: ${reason || "no reason given"}`);
9652
+ if (verdict !== "Complex" && this.looksClearlyComplex(prompt)) {
9653
+ this.recordDecision("complexity", `Complex \u2014 heuristic floor over classifier "${verdict}": explicit multi-step build/implementation signals (T1 engaged)`);
9654
+ verdict = "Complex";
9655
+ } else {
9656
+ this.recordDecision("complexity", `${verdict} \u2014 classifier: ${reason || "no reason given"}`);
9657
+ }
9629
9658
  } else {
9630
- verdict = prompt.trim().split(/\s+/).length <= 12 ? "Simple" : "Moderate";
9631
- this.recordDecision("complexity", `${verdict} \u2014 classifier output unparseable; defaulted by length`);
9659
+ const words = prompt.trim().split(/\s+/).length;
9660
+ verdict = words <= 12 ? "Simple" : words >= 40 || this.looksClearlyComplex(prompt) ? "Complex" : "Moderate";
9661
+ this.recordDecision("complexity", `${verdict} \u2014 classifier output unparseable; defaulted by length/signals`);
9632
9662
  }
9633
9663
  return verdict;
9634
9664
  } catch {
@@ -9680,7 +9710,15 @@ ${prompt}` : prompt;
9680
9710
  }
9681
9711
  escalator.resolveUserDecision(req.id, approved, always);
9682
9712
  });
9683
- const complexity = await this.determineComplexity(options.prompt, options.workspacePath || process.cwd(), options.conversationHistory);
9713
+ const forceTier = this.config.routing?.forceTier;
9714
+ const forced = forceTier === "T1" ? "Complex" : forceTier === "T2" ? "Moderate" : forceTier === "T3" ? "Simple" : void 0;
9715
+ let complexity;
9716
+ if (forced) {
9717
+ complexity = forced;
9718
+ this.recordDecision("complexity", `${forced} \u2014 manually forced via routing.forceTier=${forceTier}`);
9719
+ } else {
9720
+ complexity = await this.determineComplexity(options.prompt, options.workspacePath || process.cwd(), options.conversationHistory);
9721
+ }
9684
9722
  this.telemetry.capture("cascade:session_start", {
9685
9723
  complexity,
9686
9724
  providerCount: this.config.providers.length,
@@ -9760,6 +9798,7 @@ ${prompt}` : prompt;
9760
9798
  try {
9761
9799
  if (complexity === "Simple") {
9762
9800
  const t3 = new T3Worker(this.router, this.toolRegistry, "root");
9801
+ t3.setPresenter(true);
9763
9802
  t3.setHierarchyContext("You are the DIRECT worker for this task. There is no T1 Administrator or T2 Manager involved in this run.");
9764
9803
  if (identityPrompt) {
9765
9804
  t3.setSystemPromptOverride(identityPrompt);
@@ -9784,6 +9823,7 @@ ${prompt}` : prompt;
9784
9823
  this.emit("tier:status", { tierId: "t3-root", status: "COMPLETED", role: "T3" });
9785
9824
  } else if (complexity === "Moderate") {
9786
9825
  const t2 = new T2Manager(this.router, this.toolRegistry, "root");
9826
+ t2.setPresenter(true);
9787
9827
  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.");
9788
9828
  if (identityPrompt) {
9789
9829
  t2.setSystemPromptOverride(identityPrompt);
@@ -9833,6 +9873,7 @@ ${prompt}` : prompt;
9833
9873
  }
9834
9874
  } else {
9835
9875
  const t1 = new T1Administrator(this.router, this.toolRegistry, this.config);
9876
+ t1.setPresenter(true);
9836
9877
  t1.setHierarchyContext("You are the top-level Administrator. You are responsible for the overall plan and supervising multiple T2 Managers.");
9837
9878
  if (identityPrompt) {
9838
9879
  t1.setSystemPromptOverride(identityPrompt);
@@ -11290,7 +11331,8 @@ var DashboardSocket = class {
11290
11331
  socket.on("cascade:run", (payload) => {
11291
11332
  if (typeof payload?.prompt === "string" && payload.prompt.trim()) {
11292
11333
  const sessionId = typeof payload.sessionId === "string" && payload.sessionId.trim() ? payload.sessionId.trim() : void 0;
11293
- callback(payload.prompt.trim(), payload.model ?? "auto", socket.id, sessionId);
11334
+ const forceTier = ["T1", "T2", "T3"].includes(payload.forceTier) ? payload.forceTier : void 0;
11335
+ callback(payload.prompt.trim(), payload.model ?? "auto", socket.id, sessionId, forceTier);
11294
11336
  }
11295
11337
  });
11296
11338
  });
@@ -11317,6 +11359,13 @@ var DashboardServer = class {
11317
11359
  * of runs the session performed in this server's lifetime.
11318
11360
  */
11319
11361
  sessionTaskIds = /* @__PURE__ */ new Map();
11362
+ /**
11363
+ * Tool-approval requests awaiting a user decision from a connected client,
11364
+ * keyed by the request's uuid. The desktop shows a modal on
11365
+ * `permission:user-required` and answers with `permission:decision`; this
11366
+ * map is how that answer reaches the run that's blocked on it.
11367
+ */
11368
+ pendingApprovals = /* @__PURE__ */ new Map();
11320
11369
  port;
11321
11370
  host;
11322
11371
  workspacePath;
@@ -11375,17 +11424,18 @@ var DashboardServer = class {
11375
11424
  }
11376
11425
  this.persistConfig();
11377
11426
  });
11378
- this.socket.onCascadeRun(async (prompt, model, socketId, requestedSessionId) => {
11427
+ this.socket.onCascadeRun(async (prompt, model, socketId, requestedSessionId, forceTier) => {
11379
11428
  const sessionId = requestedSessionId ?? crypto3.randomUUID();
11380
11429
  const abortController = new AbortController();
11381
11430
  this.activeControllers.set(sessionId, abortController);
11382
11431
  const runPrompt = requestedSessionId ? this.buildContinuationPrompt(sessionId, prompt) : prompt;
11383
11432
  const title = this.persistRunStart(sessionId, prompt);
11384
- const cfg = model !== "auto" ? { ...this.config, models: { ...this.config.models, t1: model } } : this.config;
11433
+ let cfg = model !== "auto" ? { ...this.config, models: { ...this.config.models, t1: model } } : this.config;
11434
+ if (forceTier) cfg = { ...cfg, routing: { ...cfg.routing, forceTier } };
11385
11435
  const cascade = new Cascade(cfg, this.workspacePath, this.store);
11386
11436
  this.activeSessions.set(sessionId, cascade);
11387
11437
  cascade.on("stream:token", (e) => {
11388
- this.socket.emitToSocket(socketId, "stream:token", { sessionId, tierId: e.tierId, text: e.text });
11438
+ this.socket.emitToSocket(socketId, "stream:token", { sessionId, tierId: e.tierId, text: e.text, primary: e.primary });
11389
11439
  });
11390
11440
  cascade.on("tier:status", (e) => {
11391
11441
  this.socket.emitToSocket(socketId, "tier:status", { sessionId, ...e });
@@ -11397,7 +11447,11 @@ var DashboardServer = class {
11397
11447
  this.socket.emitPeerMessage(e);
11398
11448
  });
11399
11449
  try {
11400
- const result = await cascade.run({ prompt: runPrompt, signal: abortController.signal });
11450
+ const result = await cascade.run({
11451
+ prompt: runPrompt,
11452
+ signal: abortController.signal,
11453
+ approvalCallback: this.makeApprovalCallback(sessionId)
11454
+ });
11401
11455
  this.recordSessionTask(sessionId, result.taskId);
11402
11456
  this.persistRunEnd(sessionId, title, prompt, result.output, "COMPLETED");
11403
11457
  this.socket.emitToSocket(socketId, "session:complete", { sessionId, result });
@@ -11416,10 +11470,18 @@ var DashboardServer = class {
11416
11470
  } finally {
11417
11471
  this.activeSessions.delete(sessionId);
11418
11472
  this.activeControllers.delete(sessionId);
11473
+ this.denyPendingApprovals(sessionId);
11419
11474
  }
11420
11475
  });
11421
11476
  this.socket.onSessionHalt((sessionId) => {
11422
11477
  this.activeControllers.get(sessionId)?.abort();
11478
+ this.denyPendingApprovals(sessionId);
11479
+ });
11480
+ this.socket.onApprovalResponse(({ requestId, approved, always }) => {
11481
+ const pending = this.pendingApprovals.get(requestId);
11482
+ if (!pending) return;
11483
+ this.pendingApprovals.delete(requestId);
11484
+ pending.resolve({ approved: !!approved, always: !!always });
11423
11485
  });
11424
11486
  this.socket.onSessionSteer((message, sessionId, nodeId) => {
11425
11487
  const steered = this.steerSessions(message, sessionId, nodeId);
@@ -11587,6 +11649,29 @@ var DashboardServer = class {
11587
11649
  list.push(taskId);
11588
11650
  this.sessionTaskIds.set(sessionId, list);
11589
11651
  }
11652
+ /**
11653
+ * Approval bridge: cascade calls this when a dangerous tool escalates to the
11654
+ * user. The request itself was already pushed to the client via the
11655
+ * `permission:user-required` forward; here we just park a resolver keyed by
11656
+ * the request id and wait for the client's `permission:decision` (handled in
11657
+ * onApprovalResponse). Never auto-approves — an unanswered request stays
11658
+ * pending until the client answers, the run ends, or the escalator's own
11659
+ * timeout denies it.
11660
+ */
11661
+ makeApprovalCallback(sessionId) {
11662
+ return (request) => new Promise((resolve) => {
11663
+ this.pendingApprovals.set(request.id, { resolve, sessionId });
11664
+ });
11665
+ }
11666
+ /** Deny + clear any approvals still pending for a session (run end / abort). */
11667
+ denyPendingApprovals(sessionId) {
11668
+ for (const [id, pending] of this.pendingApprovals) {
11669
+ if (pending.sessionId === sessionId) {
11670
+ this.pendingApprovals.delete(id);
11671
+ pending.resolve({ approved: false, always: false });
11672
+ }
11673
+ }
11674
+ }
11590
11675
  persistRuntimeRow(sessionId, title, status, latestPrompt) {
11591
11676
  const now = (/* @__PURE__ */ new Date()).toISOString();
11592
11677
  const row = { sessionId, title, workspacePath: this.workspacePath, status, startedAt: now, updatedAt: now, latestPrompt, isGlobal: false };
@@ -11775,15 +11860,6 @@ ${prompt}`;
11775
11860
  if (sessionId) this.socket.broadcastToRoom(`session:${sessionId}`, "session:halt", payload);
11776
11861
  res.json({ success: true, ...payload });
11777
11862
  });
11778
- this.app.post("/api/approve", auth, mutationLimiter, (req, res) => {
11779
- const body = req.body;
11780
- const sessionId = typeof body["sessionId"] === "string" ? body["sessionId"] : void 0;
11781
- const nodeId = typeof body["nodeId"] === "string" ? body["nodeId"] : void 0;
11782
- const payload = { sessionId, nodeId, requestedAt: (/* @__PURE__ */ new Date()).toISOString() };
11783
- this.socket.broadcast("session:approve", payload);
11784
- if (sessionId) this.socket.broadcastToRoom(`session:${sessionId}`, "session:approve", payload);
11785
- res.json({ success: true, ...payload });
11786
- });
11787
11863
  this.app.post("/api/inject", auth, mutationLimiter, (req, res) => {
11788
11864
  const body = req.body;
11789
11865
  const message = typeof body["message"] === "string" ? body["message"] : void 0;
@@ -12034,7 +12110,7 @@ ${prompt}`;
12034
12110
  const cascade = new Cascade(this.config, this.workspacePath, this.store);
12035
12111
  this.activeSessions.set(sessionId, cascade);
12036
12112
  cascade.on("stream:token", (e) => {
12037
- this.socket.broadcastToRoom(`session:${sessionId}`, "stream:token", { sessionId, tierId: e.tierId, text: e.text });
12113
+ this.socket.broadcastToRoom(`session:${sessionId}`, "stream:token", { sessionId, tierId: e.tierId, text: e.text, primary: e.primary });
12038
12114
  });
12039
12115
  cascade.on("tier:status", (e) => {
12040
12116
  this.socket.broadcastToRoom(`session:${sessionId}`, "tier:status", { sessionId, ...e });
@@ -12046,7 +12122,11 @@ ${prompt}`;
12046
12122
  this.socket.emitPeerMessage(e);
12047
12123
  });
12048
12124
  try {
12049
- const result = await cascade.run({ prompt: runPrompt, identityId: body.identityId });
12125
+ const result = await cascade.run({
12126
+ prompt: runPrompt,
12127
+ identityId: body.identityId,
12128
+ approvalCallback: this.makeApprovalCallback(sessionId)
12129
+ });
12050
12130
  this.recordSessionTask(sessionId, result.taskId);
12051
12131
  this.persistRunEnd(sessionId, title, prompt, result.output, "COMPLETED");
12052
12132
  this.socket.broadcast("cost:update", {
@@ -12064,6 +12144,7 @@ ${prompt}`;
12064
12144
  });
12065
12145
  } finally {
12066
12146
  this.activeSessions.delete(sessionId);
12147
+ this.denyPendingApprovals(sessionId);
12067
12148
  }
12068
12149
  })();
12069
12150
  });