cascade-ai 0.15.2 → 0.16.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
@@ -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.15.2";
61
+ CASCADE_VERSION = "0.16.0";
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";
@@ -3179,6 +3179,11 @@ function validateConfig(raw) {
3179
3179
 
3180
3180
  // src/config/index.ts
3181
3181
  init_constants();
3182
+ var KEY_OPTIONAL_PROVIDER_TYPES = /* @__PURE__ */ new Set(["ollama", "openai-compatible"]);
3183
+ function hasUsableProvider(providers) {
3184
+ if (!providers?.length) return false;
3185
+ return providers.some((p) => KEY_OPTIONAL_PROVIDER_TYPES.has(p.type) || !!p.apiKey);
3186
+ }
3182
3187
  var ConfigManager = class {
3183
3188
  config;
3184
3189
  keystore;
@@ -7467,6 +7472,10 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
7467
7472
  const worker = new T3Worker(this.router, this.toolRegistry, this.id);
7468
7473
  if (this.store) worker.setStore(this.store, taskId);
7469
7474
  worker.setPeerBus(this.t3PeerBus);
7475
+ if (this.permissionEscalator) worker.setPermissionEscalator(this.permissionEscalator);
7476
+ if (this.toolCreator) worker.setToolCreator(this.toolCreator);
7477
+ worker.on("log", (e) => this.emit("log", e));
7478
+ worker.on("tier:status", (e) => this.emit("tier:status", e));
7470
7479
  worker.on("stream:token", (e) => this.emit("stream:token", e));
7471
7480
  worker.on("tool:approval-request", (e) => this.emit("tool:approval-request", {
7472
7481
  ...e,
@@ -7818,12 +7827,14 @@ Board guidance (must be followed in the plan): ${decision.note}`,
7818
7827
  status: "IN_PROGRESS"
7819
7828
  });
7820
7829
  const okBefore = okCount(allT2Results);
7830
+ const completedSummary = this.summarizeCompletedSections(allT2Results);
7831
+ const correctionContext = [systemContext, completedSummary].filter(Boolean).join("\n\n") || void 0;
7821
7832
  const correctionPlan = await this.decomposeTask(`The previous execution plan failed to fully satisfy the original goal or encountered errors.
7822
7833
  Review reason: ${reviewResult.reason}
7823
7834
 
7824
7835
  Original goal: ${enrichedPrompt}
7825
7836
 
7826
- Create a CORRECTION PLAN that contains only the new sections needed to fix the issues. Do not repeat successful sections.`);
7837
+ Create a CORRECTION PLAN that contains only the new sections needed to fix the issues. Do not repeat successful sections.`, correctionContext);
7827
7838
  const correctionResults = await this.dispatchT2Managers(correctionPlan.sections);
7828
7839
  allT2Results = [...allT2Results, ...correctionResults];
7829
7840
  if (okCount(allT2Results) <= okBefore) {
@@ -7926,6 +7937,15 @@ In 3-5 terse bullets, flag the most important RISKS, GAPS, or over-/under-decomp
7926
7937
  return null;
7927
7938
  }
7928
7939
  }
7940
+ /** Structured, grounded summary of what's already done — used to keep
7941
+ * corrective replan passes from re-emitting completed sections. */
7942
+ summarizeCompletedSections(results) {
7943
+ const done = results.filter((r) => r.status === "COMPLETED" || r.status === "PARTIAL");
7944
+ if (done.length === 0) return "";
7945
+ const lines = done.map((r) => `- "${r.sectionTitle}" [${r.status}]: ${(r.sectionSummary || "(no summary)").slice(0, 300)}`);
7946
+ return `ALREADY COMPLETED SECTIONS (do not redo these \u2014 plan only what's still missing):
7947
+ ${lines.join("\n")}`;
7948
+ }
7929
7949
  async decomposeTask(prompt, systemContext) {
7930
7950
  const db = this.router.getWorldStateDB?.();
7931
7951
  let worldStateContext = "";
@@ -8193,7 +8213,7 @@ Leave dependsOn empty for sections that can run immediately in parallel.`;
8193
8213
  }
8194
8214
  }
8195
8215
  if (readyIds.length === 0) return;
8196
- await Promise.all(readyIds.map(async (id) => {
8216
+ const runOne = async (id) => {
8197
8217
  resultMap.set(id, null);
8198
8218
  const index = sections.findIndex((s) => s.sectionId === id);
8199
8219
  const section = sections[index];
@@ -8233,7 +8253,14 @@ Leave dependsOn empty for sections that can run immediately in parallel.`;
8233
8253
  for (const dependentId of adj.get(id) ?? /* @__PURE__ */ new Set()) {
8234
8254
  inDegree.set(dependentId, Math.max(0, (inDegree.get(dependentId) ?? 1) - 1));
8235
8255
  }
8236
- }));
8256
+ };
8257
+ if (this.router.getT3ExecutionMode?.() === "sequential") {
8258
+ for (const id of readyIds) {
8259
+ await runOne(id);
8260
+ }
8261
+ } else {
8262
+ await Promise.all(readyIds.map(runOne));
8263
+ }
8237
8264
  if (Array.from(inDegree.values()).some((deg) => deg === 0) && resultMap.size < totalSections) {
8238
8265
  await executeWave();
8239
8266
  }
@@ -10037,6 +10064,13 @@ var PermissionEscalator = class extends EventEmitter {
10037
10064
  * All T3 workers under the same T2 share cached decisions for the same tool.
10038
10065
  */
10039
10066
  sessionCache = /* @__PURE__ */ new Map();
10067
+ /**
10068
+ * Task-wide cache keyed by `toolName` alone, for USER- and T1-level
10069
+ * "always" decisions — these are meant to cover every sibling T2/T3 in the
10070
+ * run, not just the one that happened to ask first (see PermissionDecision
10071
+ * doc comment: "task-wide for T1").
10072
+ */
10073
+ taskWideCache = /* @__PURE__ */ new Map();
10040
10074
  t2Evaluator;
10041
10075
  t1Evaluator;
10042
10076
  /** Pending user-decision resolvers keyed by request ID */
@@ -10065,6 +10099,15 @@ var PermissionEscalator = class extends EventEmitter {
10065
10099
  * Returns a PermissionDecision from whichever tier was able to decide.
10066
10100
  */
10067
10101
  async requestPermission(req) {
10102
+ if (!req.forceReprompt && this.taskWideCache.has(req.toolName)) {
10103
+ return {
10104
+ requestId: req.id,
10105
+ approved: this.taskWideCache.get(req.toolName),
10106
+ always: true,
10107
+ decidedBy: "T1",
10108
+ reasoning: "Cached from a previous task-wide decision in this session"
10109
+ };
10110
+ }
10068
10111
  const cacheKey = `${req.parentT2Id}:${req.toolName}`;
10069
10112
  if (!req.forceReprompt && this.sessionCache.has(cacheKey)) {
10070
10113
  return {
@@ -10109,7 +10152,7 @@ var PermissionEscalator = class extends EventEmitter {
10109
10152
  try {
10110
10153
  const t1Decision = await this.t1Evaluator(req);
10111
10154
  if (t1Decision !== null) {
10112
- if (t1Decision.always) this.sessionCache.set(cacheKey, t1Decision.approved);
10155
+ if (t1Decision.always) this.taskWideCache.set(req.toolName, t1Decision.approved);
10113
10156
  return t1Decision;
10114
10157
  }
10115
10158
  } catch {
@@ -10139,7 +10182,7 @@ var PermissionEscalator = class extends EventEmitter {
10139
10182
  const wrappedResolver = (decision) => {
10140
10183
  if (timer) clearTimeout(timer);
10141
10184
  if (decision.always) {
10142
- this.sessionCache.set(`${req.parentT2Id}:${req.toolName}`, decision.approved);
10185
+ this.taskWideCache.set(req.toolName, decision.approved);
10143
10186
  }
10144
10187
  resolve(decision);
10145
10188
  };
@@ -15218,11 +15261,18 @@ function SetupWizard({ workspacePath, onComplete }) {
15218
15261
  }
15219
15262
  });
15220
15263
  const currentEntry = state.entries[state.currentEntryIdx];
15264
+ const rejectIfBlank = useCallback((val, message) => {
15265
+ if (val.trim()) return false;
15266
+ dispatch({ type: "SET_ERROR", error: message });
15267
+ return true;
15268
+ }, []);
15221
15269
  const handleFieldSubmit = useCallback((val) => {
15222
15270
  if (!currentEntry) return;
15223
15271
  if (currentEntry.type === "azure") {
15224
15272
  if (fieldStage === "deploymentName") {
15273
+ if (rejectIfBlank(val, "Deployment name is required.")) return;
15225
15274
  dispatch({ type: "SET_ENTRY_FIELD", field: "deploymentName", value: val });
15275
+ dispatch({ type: "SET_ERROR", error: "" });
15226
15276
  setFieldBuffer("");
15227
15277
  if (currentEntry.baseUrl && currentEntry.apiKey && currentEntry.apiVersion) {
15228
15278
  setFieldStage("askMore");
@@ -15230,38 +15280,50 @@ function SetupWizard({ workspacePath, onComplete }) {
15230
15280
  setFieldStage("baseUrl");
15231
15281
  }
15232
15282
  } else if (fieldStage === "baseUrl") {
15283
+ if (rejectIfBlank(val, "Azure endpoint URL is required.")) return;
15233
15284
  dispatch({ type: "SET_ENTRY_FIELD", field: "baseUrl", value: val });
15285
+ dispatch({ type: "SET_ERROR", error: "" });
15234
15286
  setFieldBuffer("");
15235
15287
  setFieldStage("apiKey");
15236
15288
  } else if (fieldStage === "apiKey") {
15289
+ if (rejectIfBlank(val, "API key is required.")) return;
15237
15290
  dispatch({ type: "SET_ENTRY_FIELD", field: "apiKey", value: val });
15291
+ dispatch({ type: "SET_ERROR", error: "" });
15238
15292
  setFieldBuffer("");
15239
15293
  setFieldStage("apiVersion");
15240
15294
  } else if (fieldStage === "apiVersion") {
15241
15295
  dispatch({ type: "SET_ENTRY_FIELD", field: "apiVersion", value: val || "2024-08-01-preview" });
15296
+ dispatch({ type: "SET_ERROR", error: "" });
15242
15297
  setFieldBuffer("");
15243
15298
  setFieldStage("askMore");
15244
15299
  }
15245
15300
  } else if (currentEntry.type === "openai-compatible") {
15246
15301
  if (fieldStage === "label") {
15247
15302
  dispatch({ type: "SET_ENTRY_FIELD", field: "label", value: val || currentEntry.label });
15303
+ dispatch({ type: "SET_ERROR", error: "" });
15248
15304
  setFieldBuffer("");
15249
15305
  setFieldStage("baseUrl");
15250
15306
  } else if (fieldStage === "baseUrl") {
15307
+ if (rejectIfBlank(val, "Base URL is required.")) return;
15251
15308
  dispatch({ type: "SET_ENTRY_FIELD", field: "baseUrl", value: val });
15309
+ dispatch({ type: "SET_ERROR", error: "" });
15252
15310
  setFieldBuffer("");
15253
15311
  setFieldStage("apiKey");
15254
15312
  } else if (fieldStage === "apiKey") {
15255
15313
  dispatch({ type: "SET_ENTRY_FIELD", field: "apiKey", value: val });
15314
+ dispatch({ type: "SET_ERROR", error: "" });
15256
15315
  setFieldBuffer("");
15257
15316
  setFieldStage("askMore");
15258
15317
  }
15259
15318
  } else if (currentEntry.type === "ollama") {
15260
15319
  dispatch({ type: "SET_ENTRY_FIELD", field: "baseUrl", value: val || "http://localhost:11434" });
15320
+ dispatch({ type: "SET_ERROR", error: "" });
15261
15321
  setFieldBuffer("");
15262
15322
  setFieldStage("askMore");
15263
15323
  } else {
15324
+ if (rejectIfBlank(val, "API key is required.")) return;
15264
15325
  dispatch({ type: "SET_ENTRY_FIELD", field: "apiKey", value: val });
15326
+ dispatch({ type: "SET_ERROR", error: "" });
15265
15327
  setFieldBuffer("");
15266
15328
  const nextEntry = state.entries[state.currentEntryIdx + 1];
15267
15329
  if (nextEntry) {
@@ -15269,7 +15331,7 @@ function SetupWizard({ workspacePath, onComplete }) {
15269
15331
  }
15270
15332
  dispatch({ type: "NEXT_ENTRY" });
15271
15333
  }
15272
- }, [currentEntry, fieldStage]);
15334
+ }, [currentEntry, fieldStage, rejectIfBlank, state.entries, state.currentEntryIdx]);
15273
15335
  if (state.step === "PROVIDER_SELECT") {
15274
15336
  return /* @__PURE__ */ jsxs(Frame, { theme, phase: "keys", children: [
15275
15337
  /* @__PURE__ */ jsxs(Box, { marginBottom: 1, children: [
@@ -16204,6 +16266,16 @@ var DashboardServer = class {
16204
16266
  getSocket() {
16205
16267
  return this.socket;
16206
16268
  }
16269
+ /**
16270
+ * Rebind the workspace tasks execute in — e.g. the desktop app's Code view
16271
+ * opening a different project folder — without tearing down the socket
16272
+ * server (which would drop the port/auth token/connection mid-session).
16273
+ * The next `cascade:run` picks this up immediately since `this.workspacePath`
16274
+ * is read live per-run (see onCascadeRun below).
16275
+ */
16276
+ setWorkspacePath(workspacePath) {
16277
+ this.workspacePath = workspacePath;
16278
+ }
16207
16279
  /**
16208
16280
  * Write the in-memory config back to the workspace config file so mutations
16209
16281
  * made over the socket (Settings → Save) persist across restarts. Best-effort:
@@ -17507,7 +17579,7 @@ async function startRepl(options) {
17507
17579
  process.exit(1);
17508
17580
  }
17509
17581
  let config = cm.getConfig();
17510
- const needsSetup = !config.providers?.length || config.providers.every((p) => p.type !== "ollama" && !p.apiKey);
17582
+ const needsSetup = !hasUsableProvider(config.providers);
17511
17583
  if (needsSetup) {
17512
17584
  console.log(chalk9.magenta(" \u25C8 No providers configured \u2014 launching setup wizard\u2026"));
17513
17585
  console.log();
@@ -17548,7 +17620,7 @@ async function runHeadless(prompt, options) {
17548
17620
  process.exit(1);
17549
17621
  }
17550
17622
  const config = cm.getConfig();
17551
- const needsSetup = !config.providers?.length || config.providers.every((p) => p.type !== "ollama" && !p.apiKey);
17623
+ const needsSetup = !hasUsableProvider(config.providers);
17552
17624
  if (needsSetup) {
17553
17625
  console.error(chalk9.red("No providers configured. Run `cascade init` first."));
17554
17626
  process.exit(1);