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/cli.js CHANGED
@@ -58,7 +58,7 @@ var __export = (target, all) => {
58
58
  var CASCADE_VERSION, CASCADE_CONFIG_FILE, CASCADE_DB_FILE, CASCADE_DASHBOARD_SECRET_FILE, GLOBAL_CONFIG_DIR, GLOBAL_DB_FILE, GLOBAL_KEYSTORE_FILE, GLOBAL_RUNTIME_DB_FILE, DEFAULT_DASHBOARD_PORT, DEFAULT_CONTEXT_LIMIT, DEFAULT_AUTO_SUMMARIZE_AT, MODELS, T1_MODEL_PRIORITY, T2_MODEL_PRIORITY, T3_MODEL_PRIORITY, VISION_MODEL_PRIORITY, COMPLEXITY_T2_COUNT, THEME_NAMES, DEFAULT_THEME, OLLAMA_BASE_URL, LM_STUDIO_BASE_URL, AZURE_BASE_URL_TEMPLATE, TOOL_NAMES, DEFAULT_APPROVAL_REQUIRED;
59
59
  var init_constants = __esm({
60
60
  "src/constants.ts"() {
61
- CASCADE_VERSION = "0.13.1";
61
+ CASCADE_VERSION = "0.13.2";
62
62
  CASCADE_CONFIG_FILE = ".cascade/config.json";
63
63
  CASCADE_DB_FILE = ".cascade/memory.db";
64
64
  CASCADE_DASHBOARD_SECRET_FILE = ".cascade/dashboard-secret";
@@ -2989,6 +2989,8 @@ var CascadeConfigSchema = z.object({
2989
2989
  privacy: z.object({
2990
2990
  paths: z.array(z.object({ pattern: z.string().min(1), policy: z.enum(["local-only"]) })).default([])
2991
2991
  }).optional(),
2992
+ /** Routing controls — forceTier pins the root tier, bypassing the classifier. */
2993
+ routing: z.object({ forceTier: z.enum(["auto", "T1", "T2", "T3"]).default("auto") }).optional(),
2992
2994
  /**
2993
2995
  * T3→T2 reinforcement: when enabled, a worker that discovers its subtask should
2994
2996
  * fan out can call the `request_workers` tool to have its T2 manager spawn
@@ -4974,6 +4976,13 @@ var BaseTier = class extends EventEmitter {
4974
4976
  hierarchyContext = "";
4975
4977
  /** Propagated AbortSignal — set by the tier's `execute()` before work begins. */
4976
4978
  signal;
4979
+ /**
4980
+ * True for the run's ROOT tier (T3 for Simple, T2 for Moderate, T1 for
4981
+ * Complex). Its own synthesis stream is the user-facing answer and is
4982
+ * tagged `primary` so the desktop renders it live — background workers,
4983
+ * which would interleave, are not tagged.
4984
+ */
4985
+ isPresenter = false;
4977
4986
  constructor(role, id, parentId) {
4978
4987
  super();
4979
4988
  this.role = role;
@@ -4981,6 +4990,10 @@ var BaseTier = class extends EventEmitter {
4981
4990
  this.parentId = parentId;
4982
4991
  this.label = this.id;
4983
4992
  }
4993
+ /** Mark this tier as the run's presenter (root tier). */
4994
+ setPresenter(on = true) {
4995
+ this.isPresenter = on;
4996
+ }
4984
4997
  getStatus() {
4985
4998
  return this.status;
4986
4999
  }
@@ -5716,7 +5729,7 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : "") + textToolSuffix,
5716
5729
  "T3",
5717
5730
  options,
5718
5731
  (chunk) => {
5719
- this.emit("stream:token", { tierId: this.id, text: chunk.text });
5732
+ this.emit("stream:token", { tierId: this.id, text: chunk.text, primary: this.isPresenter });
5720
5733
  }
5721
5734
  );
5722
5735
  let effectiveToolCalls = result.toolCalls ?? [];
@@ -7177,6 +7190,7 @@ ${peerOutputs}` : "";
7177
7190
  chunkEnd++;
7178
7191
  }
7179
7192
  i = chunkEnd;
7193
+ const isLastChunk = chunkEnd >= completed.length;
7180
7194
  const prompt = `Summarize these T3 worker outputs for section "${assignment.sectionTitle}" in 2-3 sentences.
7181
7195
  ${currentSummary ? `
7182
7196
  PREVIOUS SUMMARY SO FAR:
@@ -7186,6 +7200,7 @@ NEW OUTPUTS TO INTEGRATE:
7186
7200
  ` : "\nOUTPUTS:\n"}${chunkText}${peerContext}`;
7187
7201
  const messages = [{ role: "user", content: prompt }];
7188
7202
  try {
7203
+ const streamFinal = isLastChunk && this.isPresenter ? (chunk) => this.emit("stream:token", { tierId: this.id, text: chunk.text, primary: true }) : void 0;
7189
7204
  const result = await this.router.generate("T2", {
7190
7205
  messages,
7191
7206
  systemPrompt: this.systemPromptOverride + "You are a T2 Manager. Summarize the work of your T3 workers succinctly." + (this.hierarchyContext ? `
@@ -7193,7 +7208,7 @@ NEW OUTPUTS TO INTEGRATE:
7193
7208
  HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
7194
7209
  maxTokens: 500,
7195
7210
  ...this.sectionModel ? { model: this.sectionModel } : {}
7196
- });
7211
+ }, streamFinal);
7197
7212
  currentSummary = result.content;
7198
7213
  } catch (err) {
7199
7214
  this.log(`aggregateResults: LLM summarization failed at chunk \u2014 returning raw T3 outputs. Error: ${err instanceof Error ? err.message : String(err)}`);
@@ -7245,14 +7260,11 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
7245
7260
  ...this.sectionModel ? { model: this.sectionModel } : {}
7246
7261
  });
7247
7262
  const answer = result.content.trim().toUpperCase();
7248
- if (answer.includes("YES")) {
7249
- return { requestId: req.id, approved: true, always: true, decidedBy: "T2", reasoning: "T2 LLM evaluated: consistent with section goal" };
7250
- }
7251
- if (answer.includes("NO")) {
7252
- return { requestId: req.id, approved: false, always: true, decidedBy: "T2", reasoning: "T2 LLM evaluated: inconsistent with section goal" };
7253
- }
7263
+ const verdict = answer.includes("YES") ? "approve" : answer.includes("NO") ? "deny" : "unsure";
7264
+ (req.trail ??= []).push({ tier: "T2", verdict, reason: `T2: ${verdict === "approve" ? "consistent with section goal" : verdict === "deny" ? "inconsistent with section goal" : "unsure"}` });
7254
7265
  return null;
7255
7266
  } catch {
7267
+ (req.trail ??= []).push({ tier: "T2", verdict: "unsure", reason: "T2 evaluation failed" });
7256
7268
  return null;
7257
7269
  }
7258
7270
  }
@@ -7934,7 +7946,7 @@ Instructions:
7934
7946
  systemPrompt: this.systemPromptOverride + "You are a final output compiler. Summarize and format the task results clearly.",
7935
7947
  maxTokens: 8e3
7936
7948
  }, (chunk) => {
7937
- this.emit("stream:token", { tierId: this.id, text: chunk.text });
7949
+ this.emit("stream:token", { tierId: this.id, text: chunk.text, primary: this.isPresenter });
7938
7950
  });
7939
7951
  return result.content;
7940
7952
  }
@@ -7963,14 +7975,11 @@ Reply with exactly one word: YES, NO, or UNSURE.
7963
7975
  temperature: 0
7964
7976
  });
7965
7977
  const answer = result.content.trim().toUpperCase();
7966
- if (answer.includes("YES")) {
7967
- return { requestId: req.id, approved: true, always: true, decidedBy: "T1", reasoning: "T1 evaluated: consistent with overall task goal" };
7968
- }
7969
- if (answer.includes("NO")) {
7970
- return { requestId: req.id, approved: false, always: true, decidedBy: "T1", reasoning: "T1 evaluated: not consistent with overall task goal" };
7971
- }
7978
+ const verdict = answer.includes("YES") ? "approve" : answer.includes("NO") ? "deny" : "unsure";
7979
+ (req.trail ??= []).push({ tier: "T1", verdict, reason: `T1: ${verdict === "approve" ? "consistent with overall task goal" : verdict === "deny" ? "not consistent with overall task goal" : "unsure"}` });
7972
7980
  return null;
7973
7981
  } catch {
7982
+ (req.trail ??= []).push({ tier: "T1", verdict: "unsure", reason: "T1 evaluation failed" });
7974
7983
  return null;
7975
7984
  }
7976
7985
  }
@@ -11066,6 +11075,21 @@ ${last.partialOutput}` : "");
11066
11075
  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);
11067
11076
  return inquiry && !producesArtifact;
11068
11077
  }
11078
+ /**
11079
+ * Strong, explicit signals that a task needs the full hierarchy (planning +
11080
+ * multiple sections/workers). Deliberately conservative: requires a
11081
+ * build/implementation verb AND either an app/system-scale noun or an
11082
+ * explicit multi-part structure, so ordinary single-file asks (handled as
11083
+ * Simple/Moderate) don't get over-escalated.
11084
+ */
11085
+ looksClearlyComplex(prompt) {
11086
+ const p = prompt.trim();
11087
+ if (p.length < 24) return false;
11088
+ const buildVerb = /\b(?:build|implement|create|develop|design|scaffold|refactor|migrate|architect|set up|integrate)\b/i.test(p);
11089
+ 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);
11090
+ const multiPart = /(?:\b(?:and|then|also|plus|as well as)\b.*\b(?:and|then|also)\b)|(?:^|\n)\s*(?:[-*]|\d+[.)])\s+/i.test(p);
11091
+ return buildVerb && (scaleNoun || multiPart);
11092
+ }
11069
11093
  // Cache glob scan results per workspace path to avoid repeated I/O.
11070
11094
  static globCache = /* @__PURE__ */ new Map();
11071
11095
  async countWorkspaceFiles(workspacePath) {
@@ -11149,10 +11173,16 @@ ${prompt}` : prompt;
11149
11173
  let verdict;
11150
11174
  if (match) {
11151
11175
  verdict = match[1] === "simple" ? "Simple" : match[1] === "moderate" ? "Moderate" : "Complex";
11152
- this.recordDecision("complexity", `${verdict} \u2014 classifier: ${reason || "no reason given"}`);
11176
+ if (verdict !== "Complex" && this.looksClearlyComplex(prompt)) {
11177
+ this.recordDecision("complexity", `Complex \u2014 heuristic floor over classifier "${verdict}": explicit multi-step build/implementation signals (T1 engaged)`);
11178
+ verdict = "Complex";
11179
+ } else {
11180
+ this.recordDecision("complexity", `${verdict} \u2014 classifier: ${reason || "no reason given"}`);
11181
+ }
11153
11182
  } else {
11154
- verdict = prompt.trim().split(/\s+/).length <= 12 ? "Simple" : "Moderate";
11155
- this.recordDecision("complexity", `${verdict} \u2014 classifier output unparseable; defaulted by length`);
11183
+ const words = prompt.trim().split(/\s+/).length;
11184
+ verdict = words <= 12 ? "Simple" : words >= 40 || this.looksClearlyComplex(prompt) ? "Complex" : "Moderate";
11185
+ this.recordDecision("complexity", `${verdict} \u2014 classifier output unparseable; defaulted by length/signals`);
11156
11186
  }
11157
11187
  return verdict;
11158
11188
  } catch {
@@ -11204,7 +11234,15 @@ ${prompt}` : prompt;
11204
11234
  }
11205
11235
  escalator.resolveUserDecision(req.id, approved, always);
11206
11236
  });
11207
- const complexity = await this.determineComplexity(options.prompt, options.workspacePath || process.cwd(), options.conversationHistory);
11237
+ const forceTier = this.config.routing?.forceTier;
11238
+ const forced = forceTier === "T1" ? "Complex" : forceTier === "T2" ? "Moderate" : forceTier === "T3" ? "Simple" : void 0;
11239
+ let complexity;
11240
+ if (forced) {
11241
+ complexity = forced;
11242
+ this.recordDecision("complexity", `${forced} \u2014 manually forced via routing.forceTier=${forceTier}`);
11243
+ } else {
11244
+ complexity = await this.determineComplexity(options.prompt, options.workspacePath || process.cwd(), options.conversationHistory);
11245
+ }
11208
11246
  this.telemetry.capture("cascade:session_start", {
11209
11247
  complexity,
11210
11248
  providerCount: this.config.providers.length,
@@ -11284,6 +11322,7 @@ ${prompt}` : prompt;
11284
11322
  try {
11285
11323
  if (complexity === "Simple") {
11286
11324
  const t3 = new T3Worker(this.router, this.toolRegistry, "root");
11325
+ t3.setPresenter(true);
11287
11326
  t3.setHierarchyContext("You are the DIRECT worker for this task. There is no T1 Administrator or T2 Manager involved in this run.");
11288
11327
  if (identityPrompt) {
11289
11328
  t3.setSystemPromptOverride(identityPrompt);
@@ -11308,6 +11347,7 @@ ${prompt}` : prompt;
11308
11347
  this.emit("tier:status", { tierId: "t3-root", status: "COMPLETED", role: "T3" });
11309
11348
  } else if (complexity === "Moderate") {
11310
11349
  const t2 = new T2Manager(this.router, this.toolRegistry, "root");
11350
+ t2.setPresenter(true);
11311
11351
  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.");
11312
11352
  if (identityPrompt) {
11313
11353
  t2.setSystemPromptOverride(identityPrompt);
@@ -11357,6 +11397,7 @@ ${prompt}` : prompt;
11357
11397
  }
11358
11398
  } else {
11359
11399
  const t1 = new T1Administrator(this.router, this.toolRegistry, this.config);
11400
+ t1.setPresenter(true);
11360
11401
  t1.setHierarchyContext("You are the top-level Administrator. You are responsible for the overall plan and supervising multiple T2 Managers.");
11361
11402
  if (identityPrompt) {
11362
11403
  t1.setSystemPromptOverride(identityPrompt);
@@ -15357,7 +15398,8 @@ var DashboardSocket = class {
15357
15398
  socket.on("cascade:run", (payload) => {
15358
15399
  if (typeof payload?.prompt === "string" && payload.prompt.trim()) {
15359
15400
  const sessionId = typeof payload.sessionId === "string" && payload.sessionId.trim() ? payload.sessionId.trim() : void 0;
15360
- callback(payload.prompt.trim(), payload.model ?? "auto", socket.id, sessionId);
15401
+ const forceTier = ["T1", "T2", "T3"].includes(payload.forceTier) ? payload.forceTier : void 0;
15402
+ callback(payload.prompt.trim(), payload.model ?? "auto", socket.id, sessionId, forceTier);
15361
15403
  }
15362
15404
  });
15363
15405
  });
@@ -15387,6 +15429,13 @@ var DashboardServer = class {
15387
15429
  * of runs the session performed in this server's lifetime.
15388
15430
  */
15389
15431
  sessionTaskIds = /* @__PURE__ */ new Map();
15432
+ /**
15433
+ * Tool-approval requests awaiting a user decision from a connected client,
15434
+ * keyed by the request's uuid. The desktop shows a modal on
15435
+ * `permission:user-required` and answers with `permission:decision`; this
15436
+ * map is how that answer reaches the run that's blocked on it.
15437
+ */
15438
+ pendingApprovals = /* @__PURE__ */ new Map();
15390
15439
  port;
15391
15440
  host;
15392
15441
  workspacePath;
@@ -15445,17 +15494,18 @@ var DashboardServer = class {
15445
15494
  }
15446
15495
  this.persistConfig();
15447
15496
  });
15448
- this.socket.onCascadeRun(async (prompt, model, socketId, requestedSessionId) => {
15497
+ this.socket.onCascadeRun(async (prompt, model, socketId, requestedSessionId, forceTier) => {
15449
15498
  const sessionId = requestedSessionId ?? randomUUID();
15450
15499
  const abortController = new AbortController();
15451
15500
  this.activeControllers.set(sessionId, abortController);
15452
15501
  const runPrompt = requestedSessionId ? this.buildContinuationPrompt(sessionId, prompt) : prompt;
15453
15502
  const title = this.persistRunStart(sessionId, prompt);
15454
- const cfg = model !== "auto" ? { ...this.config, models: { ...this.config.models, t1: model } } : this.config;
15503
+ let cfg = model !== "auto" ? { ...this.config, models: { ...this.config.models, t1: model } } : this.config;
15504
+ if (forceTier) cfg = { ...cfg, routing: { ...cfg.routing, forceTier } };
15455
15505
  const cascade = new Cascade(cfg, this.workspacePath, this.store);
15456
15506
  this.activeSessions.set(sessionId, cascade);
15457
15507
  cascade.on("stream:token", (e) => {
15458
- this.socket.emitToSocket(socketId, "stream:token", { sessionId, tierId: e.tierId, text: e.text });
15508
+ this.socket.emitToSocket(socketId, "stream:token", { sessionId, tierId: e.tierId, text: e.text, primary: e.primary });
15459
15509
  });
15460
15510
  cascade.on("tier:status", (e) => {
15461
15511
  this.socket.emitToSocket(socketId, "tier:status", { sessionId, ...e });
@@ -15467,7 +15517,11 @@ var DashboardServer = class {
15467
15517
  this.socket.emitPeerMessage(e);
15468
15518
  });
15469
15519
  try {
15470
- const result = await cascade.run({ prompt: runPrompt, signal: abortController.signal });
15520
+ const result = await cascade.run({
15521
+ prompt: runPrompt,
15522
+ signal: abortController.signal,
15523
+ approvalCallback: this.makeApprovalCallback(sessionId)
15524
+ });
15471
15525
  this.recordSessionTask(sessionId, result.taskId);
15472
15526
  this.persistRunEnd(sessionId, title, prompt, result.output, "COMPLETED");
15473
15527
  this.socket.emitToSocket(socketId, "session:complete", { sessionId, result });
@@ -15486,10 +15540,18 @@ var DashboardServer = class {
15486
15540
  } finally {
15487
15541
  this.activeSessions.delete(sessionId);
15488
15542
  this.activeControllers.delete(sessionId);
15543
+ this.denyPendingApprovals(sessionId);
15489
15544
  }
15490
15545
  });
15491
15546
  this.socket.onSessionHalt((sessionId) => {
15492
15547
  this.activeControllers.get(sessionId)?.abort();
15548
+ this.denyPendingApprovals(sessionId);
15549
+ });
15550
+ this.socket.onApprovalResponse(({ requestId, approved, always }) => {
15551
+ const pending = this.pendingApprovals.get(requestId);
15552
+ if (!pending) return;
15553
+ this.pendingApprovals.delete(requestId);
15554
+ pending.resolve({ approved: !!approved, always: !!always });
15493
15555
  });
15494
15556
  this.socket.onSessionSteer((message, sessionId, nodeId) => {
15495
15557
  const steered = this.steerSessions(message, sessionId, nodeId);
@@ -15657,6 +15719,29 @@ var DashboardServer = class {
15657
15719
  list.push(taskId);
15658
15720
  this.sessionTaskIds.set(sessionId, list);
15659
15721
  }
15722
+ /**
15723
+ * Approval bridge: cascade calls this when a dangerous tool escalates to the
15724
+ * user. The request itself was already pushed to the client via the
15725
+ * `permission:user-required` forward; here we just park a resolver keyed by
15726
+ * the request id and wait for the client's `permission:decision` (handled in
15727
+ * onApprovalResponse). Never auto-approves — an unanswered request stays
15728
+ * pending until the client answers, the run ends, or the escalator's own
15729
+ * timeout denies it.
15730
+ */
15731
+ makeApprovalCallback(sessionId) {
15732
+ return (request) => new Promise((resolve) => {
15733
+ this.pendingApprovals.set(request.id, { resolve, sessionId });
15734
+ });
15735
+ }
15736
+ /** Deny + clear any approvals still pending for a session (run end / abort). */
15737
+ denyPendingApprovals(sessionId) {
15738
+ for (const [id, pending] of this.pendingApprovals) {
15739
+ if (pending.sessionId === sessionId) {
15740
+ this.pendingApprovals.delete(id);
15741
+ pending.resolve({ approved: false, always: false });
15742
+ }
15743
+ }
15744
+ }
15660
15745
  persistRuntimeRow(sessionId, title, status, latestPrompt) {
15661
15746
  const now = (/* @__PURE__ */ new Date()).toISOString();
15662
15747
  const row = { sessionId, title, workspacePath: this.workspacePath, status, startedAt: now, updatedAt: now, latestPrompt, isGlobal: false };
@@ -15845,15 +15930,6 @@ ${prompt}`;
15845
15930
  if (sessionId) this.socket.broadcastToRoom(`session:${sessionId}`, "session:halt", payload);
15846
15931
  res.json({ success: true, ...payload });
15847
15932
  });
15848
- this.app.post("/api/approve", auth, mutationLimiter, (req, res) => {
15849
- const body = req.body;
15850
- const sessionId = typeof body["sessionId"] === "string" ? body["sessionId"] : void 0;
15851
- const nodeId = typeof body["nodeId"] === "string" ? body["nodeId"] : void 0;
15852
- const payload = { sessionId, nodeId, requestedAt: (/* @__PURE__ */ new Date()).toISOString() };
15853
- this.socket.broadcast("session:approve", payload);
15854
- if (sessionId) this.socket.broadcastToRoom(`session:${sessionId}`, "session:approve", payload);
15855
- res.json({ success: true, ...payload });
15856
- });
15857
15933
  this.app.post("/api/inject", auth, mutationLimiter, (req, res) => {
15858
15934
  const body = req.body;
15859
15935
  const message = typeof body["message"] === "string" ? body["message"] : void 0;
@@ -16104,7 +16180,7 @@ ${prompt}`;
16104
16180
  const cascade = new Cascade(this.config, this.workspacePath, this.store);
16105
16181
  this.activeSessions.set(sessionId, cascade);
16106
16182
  cascade.on("stream:token", (e) => {
16107
- this.socket.broadcastToRoom(`session:${sessionId}`, "stream:token", { sessionId, tierId: e.tierId, text: e.text });
16183
+ this.socket.broadcastToRoom(`session:${sessionId}`, "stream:token", { sessionId, tierId: e.tierId, text: e.text, primary: e.primary });
16108
16184
  });
16109
16185
  cascade.on("tier:status", (e) => {
16110
16186
  this.socket.broadcastToRoom(`session:${sessionId}`, "tier:status", { sessionId, ...e });
@@ -16116,7 +16192,11 @@ ${prompt}`;
16116
16192
  this.socket.emitPeerMessage(e);
16117
16193
  });
16118
16194
  try {
16119
- const result = await cascade.run({ prompt: runPrompt, identityId: body.identityId });
16195
+ const result = await cascade.run({
16196
+ prompt: runPrompt,
16197
+ identityId: body.identityId,
16198
+ approvalCallback: this.makeApprovalCallback(sessionId)
16199
+ });
16120
16200
  this.recordSessionTask(sessionId, result.taskId);
16121
16201
  this.persistRunEnd(sessionId, title, prompt, result.output, "COMPLETED");
16122
16202
  this.socket.broadcast("cost:update", {
@@ -16134,6 +16214,7 @@ ${prompt}`;
16134
16214
  });
16135
16215
  } finally {
16136
16216
  this.activeSessions.delete(sessionId);
16217
+ this.denyPendingApprovals(sessionId);
16137
16218
  }
16138
16219
  })();
16139
16220
  });