cascade-ai 0.50.0 → 0.51.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.50.0";
1743
+ CASCADE_VERSION = "0.51.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";
@@ -2258,49 +2258,40 @@ function isChatModel(id, methods) {
2258
2258
  }
2259
2259
  return !NON_CHAT.test(id);
2260
2260
  }
2261
- var NON_CHAT_PATTERNS, NON_CHAT;
2261
+ var MODALITY_PATTERNS, NON_CHAT;
2262
2262
  var init_model_filter = __esm({
2263
2263
  "src/providers/model-filter.ts"() {
2264
- NON_CHAT_PATTERNS = [
2265
- "embed",
2264
+ MODALITY_PATTERNS = [
2265
+ ["embedding", ["embed"]],
2266
2266
  // text-embedding-3, nomic-embed, mxbai-embed, snowflake-arctic-embed
2267
- "(?:^|[-_/])tts(?:[-_/]|$)",
2268
- "text-to-speech",
2269
- "speech",
2270
- // text-to-speech
2271
- "whisper",
2272
- "transcrib",
2273
- "(?:^|[-_/])stt(?:[-_/]|$)",
2274
- // speech-to-text
2275
- "dall[-]?e",
2276
- "imagen",
2277
- "image-generation",
2278
- "gpt-image",
2279
- "(?:^|[-_/])image(?:[-_/]|$)",
2280
- "stable-diffusion",
2281
- "sdxl",
2282
- "flux",
2283
- // image gen
2284
- "moderation",
2285
- "(?:^|[-_/])guard(?:[-_/]|$)",
2286
- // moderation / safety
2287
- "rerank",
2288
- "(?:^|[-_/])audio(?:[-_/]|$)",
2289
- "native-audio",
2290
- "realtime",
2291
- // audio / realtime
2292
- "veo",
2293
- "sora",
2294
- "(?:^|[-_/])video(?:[-_/]|$)",
2295
- // video
2296
- "(?:^|[-_/])aqa(?:[-_/]|$)",
2297
- // attributed QA
2298
- "(?:^|[-_/])live(?:[-_/]|$)",
2299
- // live-audio variants
2300
- "(?:^|[-_/])(?:davinci|babbage)(?:[-_/]|$)"
2301
- // legacy completion-only base models
2267
+ ["transcription", ["whisper", "transcrib", "(?:^|[-_/])stt(?:[-_/]|$)"]],
2268
+ ["speech", ["(?:^|[-_/])tts(?:[-_/]|$)", "text-to-speech", "speech"]],
2269
+ ["video", ["veo", "sora", "(?:^|[-_/])video(?:[-_/]|$)"]],
2270
+ ["image", [
2271
+ "dall[-]?e",
2272
+ "imagen",
2273
+ "image-generation",
2274
+ "gpt-image",
2275
+ "(?:^|[-_/])image(?:[-_/]|$)",
2276
+ "stable-diffusion",
2277
+ "sdxl",
2278
+ "flux"
2279
+ ]],
2280
+ ["moderation", ["moderation", "(?:^|[-_/])guard(?:[-_/]|$)"]],
2281
+ ["rerank", ["rerank"]],
2282
+ ["realtime", [
2283
+ "(?:^|[-_/])audio(?:[-_/]|$)",
2284
+ "native-audio",
2285
+ "realtime",
2286
+ "(?:^|[-_/])live(?:[-_/]|$)",
2287
+ // live-audio variants
2288
+ "(?:^|[-_/])aqa(?:[-_/]|$)"
2289
+ // attributed QA
2290
+ ]],
2291
+ ["legacy-completion", ["(?:^|[-_/])(?:davinci|babbage)(?:[-_/]|$)"]]
2302
2292
  ];
2303
- NON_CHAT = new RegExp(NON_CHAT_PATTERNS.join("|"), "i");
2293
+ MODALITY_PATTERNS.map(([m, pats]) => [m, new RegExp(pats.join("|"), "i")]);
2294
+ NON_CHAT = new RegExp(MODALITY_PATTERNS.flatMap(([, p]) => p).join("|"), "i");
2304
2295
  }
2305
2296
  });
2306
2297
 
@@ -2868,6 +2859,118 @@ var init_azure = __esm({
2868
2859
  }
2869
2860
  });
2870
2861
 
2862
+ // src/providers/gemini-schema.ts
2863
+ function isPlainObject(v) {
2864
+ return typeof v === "object" && v !== null && !Array.isArray(v);
2865
+ }
2866
+ function normalizeType(value) {
2867
+ if (typeof value === "string") {
2868
+ return ALLOWED_TYPES.has(value) ? { type: value } : {};
2869
+ }
2870
+ if (Array.isArray(value)) {
2871
+ const types = value.filter((t) => typeof t === "string");
2872
+ const nullable = types.includes("null");
2873
+ const concrete = types.find((t) => t !== "null" && ALLOWED_TYPES.has(t));
2874
+ return {
2875
+ ...concrete ? { type: concrete } : {},
2876
+ ...nullable ? { nullable: true } : {}
2877
+ };
2878
+ }
2879
+ return {};
2880
+ }
2881
+ function normalizeEnum(value) {
2882
+ if (!Array.isArray(value) || value.length === 0) return void 0;
2883
+ return value.map((v) => typeof v === "string" ? v : JSON.stringify(v));
2884
+ }
2885
+ function sanitizeGeminiSchema(schema) {
2886
+ if (!isPlainObject(schema)) return void 0;
2887
+ const out = {};
2888
+ for (const [key, value] of Object.entries(schema)) {
2889
+ if (!ALLOWED_KEYS.has(key)) continue;
2890
+ switch (key) {
2891
+ case "type": {
2892
+ Object.assign(out, normalizeType(value));
2893
+ break;
2894
+ }
2895
+ case "enum": {
2896
+ const e = normalizeEnum(value);
2897
+ if (e) out["enum"] = e;
2898
+ break;
2899
+ }
2900
+ case "properties": {
2901
+ if (!isPlainObject(value)) break;
2902
+ const props = {};
2903
+ for (const [propName, propSchema] of Object.entries(value)) {
2904
+ const cleaned = sanitizeGeminiSchema(propSchema);
2905
+ props[propName] = cleaned && Object.keys(cleaned).length ? cleaned : { type: "string" };
2906
+ }
2907
+ out["properties"] = props;
2908
+ break;
2909
+ }
2910
+ case "items": {
2911
+ const cleaned = sanitizeGeminiSchema(value);
2912
+ if (cleaned) out["items"] = cleaned;
2913
+ break;
2914
+ }
2915
+ case "anyOf": {
2916
+ if (!Array.isArray(value)) break;
2917
+ const variants = value.map((v) => sanitizeGeminiSchema(v)).filter((v) => !!v && Object.keys(v).length > 0);
2918
+ if (variants.length) out["anyOf"] = variants;
2919
+ break;
2920
+ }
2921
+ case "required": {
2922
+ if (Array.isArray(value)) {
2923
+ out["required"] = value.filter((r) => typeof r === "string");
2924
+ }
2925
+ break;
2926
+ }
2927
+ default:
2928
+ out[key] = value;
2929
+ }
2930
+ }
2931
+ if (!out["type"] && out["properties"]) out["type"] = "object";
2932
+ if (!out["type"] && out["items"]) out["type"] = "array";
2933
+ return out;
2934
+ }
2935
+ function toGeminiParameters(inputSchema) {
2936
+ const cleaned = sanitizeGeminiSchema(inputSchema);
2937
+ if (!cleaned) return void 0;
2938
+ const props = cleaned["properties"];
2939
+ if (isPlainObject(props) && Object.keys(props).length === 0) return void 0;
2940
+ if (Object.keys(cleaned).length === 0) return void 0;
2941
+ return cleaned;
2942
+ }
2943
+ var ALLOWED_KEYS, ALLOWED_TYPES;
2944
+ var init_gemini_schema = __esm({
2945
+ "src/providers/gemini-schema.ts"() {
2946
+ ALLOWED_KEYS = /* @__PURE__ */ new Set([
2947
+ "type",
2948
+ "format",
2949
+ "title",
2950
+ "description",
2951
+ "nullable",
2952
+ "enum",
2953
+ "items",
2954
+ "properties",
2955
+ "required",
2956
+ "propertyOrdering",
2957
+ "minItems",
2958
+ "maxItems",
2959
+ "minProperties",
2960
+ "maxProperties",
2961
+ "minimum",
2962
+ "maximum",
2963
+ "minLength",
2964
+ "maxLength",
2965
+ "pattern",
2966
+ "example",
2967
+ "default",
2968
+ "anyOf"
2969
+ ]);
2970
+ ALLOWED_TYPES = /* @__PURE__ */ new Set(["string", "number", "integer", "boolean", "array", "object"]);
2971
+ }
2972
+ });
2973
+
2871
2974
  // src/providers/gemini.ts
2872
2975
  var gemini_exports = {};
2873
2976
  __export(gemini_exports, {
@@ -2880,6 +2983,7 @@ var init_gemini = __esm({
2880
2983
  init_base();
2881
2984
  init_pricing();
2882
2985
  init_model_filter();
2986
+ init_gemini_schema();
2883
2987
  GeminiProvider = class extends BaseProvider {
2884
2988
  client;
2885
2989
  constructor(config, model) {
@@ -3100,7 +3204,12 @@ var init_gemini = __esm({
3100
3204
  return {
3101
3205
  name: tool.name,
3102
3206
  description: tool.description,
3103
- parameters: tool.inputSchema
3207
+ // Convert, never cast. Gemini's parameters field is an OpenAPI subset
3208
+ // that 400s on unknown keys, and MCP servers ship schemas carrying their
3209
+ // own extensions (GitHub's adds `x-mcp-header` to every header-bound
3210
+ // property). Casting sent those through verbatim and every request failed
3211
+ // before the model saw it. See providers/gemini-schema.ts.
3212
+ parameters: toGeminiParameters(tool.inputSchema)
3104
3213
  };
3105
3214
  }
3106
3215
  };
@@ -3125,7 +3234,7 @@ async function nodeHttpFetch(input, init = {}, redirectCount = 0) {
3125
3234
  headers["accept-encoding"] = "gzip, deflate, br";
3126
3235
  }
3127
3236
  const body = init.body == null ? void 0 : typeof init.body === "string" ? init.body : Buffer.from(init.body);
3128
- return new Promise((resolve, reject) => {
3237
+ return new Promise((resolve2, reject) => {
3129
3238
  const req = lib.request(
3130
3239
  {
3131
3240
  hostname: u.hostname,
@@ -3142,7 +3251,7 @@ async function nodeHttpFetch(input, init = {}, redirectCount = 0) {
3142
3251
  const nextUrl = new URL(location, u).href;
3143
3252
  const downgrade = status === 303 || (status === 301 || status === 302) && method !== "GET" && method !== "HEAD";
3144
3253
  const nextInit = downgrade ? { ...init, method: "GET", body: void 0 } : init;
3145
- resolve(nodeHttpFetch(nextUrl, nextInit, redirectCount + 1));
3254
+ resolve2(nodeHttpFetch(nextUrl, nextInit, redirectCount + 1));
3146
3255
  return;
3147
3256
  }
3148
3257
  const encoding = (res.headers["content-encoding"] ?? "").toLowerCase();
@@ -3157,7 +3266,7 @@ async function nodeHttpFetch(input, init = {}, redirectCount = 0) {
3157
3266
  if (Array.isArray(v)) respHeaders.set(k, v.join(", "));
3158
3267
  else if (typeof v === "string") respHeaders.set(k, v);
3159
3268
  }
3160
- resolve(new Response(stream, {
3269
+ resolve2(new Response(stream, {
3161
3270
  status,
3162
3271
  statusText: res.statusMessage ?? "",
3163
3272
  headers: respHeaders
@@ -4956,6 +5065,14 @@ var BudgetConfigSchema = z.object({
4956
5065
  maxTokensPerRun: z.number().int().positive().default(2e5),
4957
5066
  /** Optional hard per-task cost ceiling (USD). Unset = only the token cap applies. */
4958
5067
  maxCostPerRunUsd: z.number().positive().optional(),
5068
+ /**
5069
+ * Consecutive systemic failures against ONE model before the run stops
5070
+ * launching work that would fail the same way (dead key, wrong model id,
5071
+ * exhausted quota). Lower reacts sooner and wastes less; higher absorbs a
5072
+ * longer transient before giving up. Only failures that will certainly repeat
5073
+ * are counted — see core/run-breaker.ts.
5074
+ */
5075
+ failureThreshold: z.number().int().min(1).max(20).default(3),
4959
5076
  warnAtPct: z.number().default(80)
4960
5077
  });
4961
5078
  var WorkspaceConfigSchema = z.object({
@@ -6008,7 +6125,7 @@ var TpmLimiter = class {
6008
6125
  }
6009
6126
  const deficit = want - bucket.available;
6010
6127
  const waitMs = Math.max(50, Math.ceil(deficit / bucket.tokensPerMinute * 6e4));
6011
- await new Promise((resolve) => setTimeout(resolve, waitMs));
6128
+ await new Promise((resolve2) => setTimeout(resolve2, waitMs));
6012
6129
  }
6013
6130
  }
6014
6131
  /**
@@ -6071,14 +6188,14 @@ var LocalRequestQueue = class {
6071
6188
  this.active++;
6072
6189
  return this.makeRelease();
6073
6190
  }
6074
- return new Promise((resolve, reject) => {
6191
+ return new Promise((resolve2, reject) => {
6075
6192
  let settled = false;
6076
6193
  let timer;
6077
6194
  const resolver = (release) => {
6078
6195
  if (settled) return;
6079
6196
  settled = true;
6080
6197
  if (timer !== void 0) clearTimeout(timer);
6081
- resolve(release);
6198
+ resolve2(release);
6082
6199
  };
6083
6200
  if (timeoutMs !== void 0 && timeoutMs > 0) {
6084
6201
  timer = setTimeout(() => {
@@ -6705,6 +6822,7 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter {
6705
6822
  tpmLimiter;
6706
6823
  localQueue;
6707
6824
  taskAnalyzer;
6825
+ pendingFeedbackSource;
6708
6826
  worldStateDB;
6709
6827
  privacyPaths;
6710
6828
  guidanceQueue;
@@ -7159,8 +7277,18 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter {
7159
7277
  return windows.length ? Math.min(...windows) : void 0;
7160
7278
  }
7161
7279
  /** Wire the Cascade Auto task analyzer used for per-subtask model routing. */
7280
+ /**
7281
+ * Feed per-model thumbs counts into Auto routing. Forwarded to the analyzer
7282
+ * because that is where the public benchmark score is combined; the prior
7283
+ * adjusts that number rather than sitting anywhere else in the pipeline.
7284
+ */
7285
+ setFeedbackSource(source) {
7286
+ this.taskAnalyzer?.setFeedbackSource(source);
7287
+ this.pendingFeedbackSource = source;
7288
+ }
7162
7289
  setTaskAnalyzer(analyzer) {
7163
7290
  this.taskAnalyzer = analyzer;
7291
+ if (this.pendingFeedbackSource) analyzer.setFeedbackSource(this.pendingFeedbackSource);
7164
7292
  }
7165
7293
  setWorldStateDB(db) {
7166
7294
  this.worldStateDB = db;
@@ -8255,8 +8383,11 @@ Now execute your subtask using this context where relevant.`
8255
8383
  const result = await this.runAgentLoop(systemPrompt, this.tools);
8256
8384
  output = result.output;
8257
8385
  toolCalls = result.toolCalls;
8258
- this.sendStatusUpdate({ progressPct: 65, currentAction: "Verifying required artifacts", status: "IN_PROGRESS" });
8259
- const artifactCheck = await this.verifyArtifacts(assignment);
8386
+ const mustProduceArtifact = this.requiresArtifact();
8387
+ if (mustProduceArtifact) {
8388
+ this.sendStatusUpdate({ progressPct: 65, currentAction: "Verifying required artifacts", status: "IN_PROGRESS" });
8389
+ }
8390
+ const artifactCheck = mustProduceArtifact ? await this.verifyArtifacts(assignment) : { ok: true, issues: [] };
8260
8391
  if (!artifactCheck.ok) {
8261
8392
  correctionAttempts = 1;
8262
8393
  issues.push(...artifactCheck.issues);
@@ -8536,11 +8667,11 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : "") + textToolSuffix,
8536
8667
  if (!wasApproved) return `Tool ${tc.name} was denied by user.`;
8537
8668
  } else {
8538
8669
  const LEGACY_APPROVAL_TIMEOUT_MS = 6e5;
8539
- const legacyDecision = await new Promise((resolve) => {
8670
+ const legacyDecision = await new Promise((resolve2) => {
8540
8671
  const eventName = `tool:approval-response:${this.id}-${tc.id}`;
8541
8672
  const timer = setTimeout(() => {
8542
8673
  this.removeAllListeners(eventName);
8543
- resolve({ approved: false });
8674
+ resolve2({ approved: false });
8544
8675
  }, LEGACY_APPROVAL_TIMEOUT_MS);
8545
8676
  timer.unref?.();
8546
8677
  this.emit("tool:approval-request", {
@@ -8553,7 +8684,7 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : "") + textToolSuffix,
8553
8684
  });
8554
8685
  this.once(eventName, (d) => {
8555
8686
  clearTimeout(timer);
8556
- resolve(d);
8687
+ resolve2(d);
8557
8688
  });
8558
8689
  });
8559
8690
  if (legacyDecision.always) this.sessionApprovals.set(tc.name, legacyDecision.approved);
@@ -8573,6 +8704,9 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : "") + textToolSuffix,
8573
8704
  tierId: this.id,
8574
8705
  sessionId: this.taskId,
8575
8706
  requireApproval: false,
8707
+ // Media generation can run for a minute; without this a cancelled run
8708
+ // still pays for an image nobody will see.
8709
+ ...this.signal ? { signal: this.signal } : {},
8576
8710
  saveSnapshot: async (path37, content) => {
8577
8711
  this.store?.addFileSnapshot(this.taskId, path37, content);
8578
8712
  },
@@ -8620,7 +8754,8 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : "") + textToolSuffix,
8620
8754
  const result = await this.toolRegistry.execute(altTool, tc.input, {
8621
8755
  tierId: this.id,
8622
8756
  sessionId: this.taskId,
8623
- requireApproval: false
8757
+ requireApproval: false,
8758
+ ...this.signal ? { signal: this.signal } : {}
8624
8759
  });
8625
8760
  const str2 = typeof result === "string" ? result : JSON.stringify(result);
8626
8761
  if (!str2.startsWith("Tool error:") && !str2.startsWith("Error:")) {
@@ -8642,7 +8777,8 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : "") + textToolSuffix,
8642
8777
  const result = await this.toolRegistry.execute(newToolName, tc.input, {
8643
8778
  tierId: this.id,
8644
8779
  sessionId: this.taskId,
8645
- requireApproval: false
8780
+ requireApproval: false,
8781
+ ...this.signal ? { signal: this.signal } : {}
8646
8782
  });
8647
8783
  const str2 = typeof result === "string" ? result : JSON.stringify(result);
8648
8784
  if (!str2.startsWith("Tool error:")) return `[Synthesized ${newToolName}]: ${str2}`;
@@ -9060,7 +9196,7 @@ var PeerBus = class extends EventEmitter {
9060
9196
  this.outputs.set(subtaskId, entry);
9061
9197
  this.emit("output:ready", entry);
9062
9198
  const waiting = this.waiters.get(subtaskId) ?? [];
9063
- for (const resolve of waiting) resolve(entry);
9199
+ for (const resolve2 of waiting) resolve2(entry);
9064
9200
  this.waiters.delete(subtaskId);
9065
9201
  }
9066
9202
  /**
@@ -9091,10 +9227,10 @@ var PeerBus = class extends EventEmitter {
9091
9227
  waitFor(subtaskId, timeoutMs = 12e4) {
9092
9228
  const existing = this.outputs.get(subtaskId);
9093
9229
  if (existing && !this.retryPending.has(subtaskId)) return Promise.resolve(existing);
9094
- return new Promise((resolve, reject) => {
9230
+ return new Promise((resolve2, reject) => {
9095
9231
  const resolver = (output) => {
9096
9232
  clearTimeout(timer);
9097
- resolve(output);
9233
+ resolve2(output);
9098
9234
  };
9099
9235
  const timer = setTimeout(() => {
9100
9236
  const waiting = this.waiters.get(subtaskId);
@@ -9148,10 +9284,10 @@ var PeerBus = class extends EventEmitter {
9148
9284
  */
9149
9285
  collect(timeoutMs) {
9150
9286
  const collected = [...this.broadcastLog];
9151
- return new Promise((resolve) => {
9287
+ return new Promise((resolve2) => {
9152
9288
  const timer = setTimeout(() => {
9153
9289
  off();
9154
- resolve(collected);
9290
+ resolve2(collected);
9155
9291
  }, timeoutMs);
9156
9292
  const handler = (msg) => {
9157
9293
  collected.push({ fromId: msg.fromId, payload: msg.payload, timestamp: msg.timestamp });
@@ -9165,7 +9301,7 @@ var PeerBus = class extends EventEmitter {
9165
9301
  const broadcasters = new Set(collected.map((m) => m.fromId));
9166
9302
  if (broadcasters.size >= this.members.size) {
9167
9303
  off();
9168
- resolve(collected);
9304
+ resolve2(collected);
9169
9305
  }
9170
9306
  };
9171
9307
  this.on("broadcast", checkComplete);
@@ -9183,7 +9319,7 @@ var PeerBus = class extends EventEmitter {
9183
9319
  return;
9184
9320
  }
9185
9321
  this.emitCoordination(tierId, `\u23F3 waiting for ${filePath} (held by ${existing.holderId})`);
9186
- return new Promise((resolve, reject) => {
9322
+ return new Promise((resolve2, reject) => {
9187
9323
  const timer = setTimeout(() => {
9188
9324
  reject(new Error(`File lock timeout for ${filePath} (held by ${existing.holderId})`));
9189
9325
  }, timeoutMs);
@@ -9191,7 +9327,7 @@ var PeerBus = class extends EventEmitter {
9191
9327
  clearTimeout(timer);
9192
9328
  this.fileLocks.set(filePath, { holderId: tierId, lockedAt: (/* @__PURE__ */ new Date()).toISOString(), waiters: [] });
9193
9329
  this.emitCoordination(tierId, `\u{1F512} locked ${filePath}`);
9194
- resolve();
9330
+ resolve2();
9195
9331
  });
9196
9332
  });
9197
9333
  }
@@ -9216,11 +9352,11 @@ var PeerBus = class extends EventEmitter {
9216
9352
  */
9217
9353
  waitForFileRelease(filePath, timeoutMs = 3e4) {
9218
9354
  if (!this.fileLocks.has(filePath)) return Promise.resolve();
9219
- return new Promise((resolve, reject) => {
9355
+ return new Promise((resolve2, reject) => {
9220
9356
  const timer = setTimeout(() => reject(new Error(`Timeout waiting for file release: ${filePath}`)), timeoutMs);
9221
9357
  this.once(`file:released:${filePath}`, () => {
9222
9358
  clearTimeout(timer);
9223
- resolve();
9359
+ resolve2();
9224
9360
  });
9225
9361
  });
9226
9362
  }
@@ -9286,13 +9422,13 @@ var PeerBus = class extends EventEmitter {
9286
9422
  this.emit(`barrier:${barrierName}`);
9287
9423
  return;
9288
9424
  }
9289
- return new Promise((resolve, reject) => {
9425
+ return new Promise((resolve2, reject) => {
9290
9426
  const timer = setTimeout(() => {
9291
9427
  reject(new Error(`Barrier timeout: ${barrierName} (${bar.arrived.size}/${bar.total} arrived)`));
9292
9428
  }, 12e4);
9293
9429
  this.once(`barrier:${barrierName}`, () => {
9294
9430
  clearTimeout(timer);
9295
- resolve();
9431
+ resolve2();
9296
9432
  });
9297
9433
  });
9298
9434
  }
@@ -10843,6 +10979,175 @@ Reply with exactly one word: YES, NO, or UNSURE.
10843
10979
  // src/core/cascade.ts
10844
10980
  init_cost();
10845
10981
 
10982
+ // src/core/multimodal/registry.ts
10983
+ init_pricing();
10984
+ var CAPABILITIES = [
10985
+ // ── Image ──
10986
+ {
10987
+ modality: "image",
10988
+ provider: "openai",
10989
+ modelId: "dall-e-3",
10990
+ api: "openai-images"
10991
+ },
10992
+ {
10993
+ modality: "image",
10994
+ provider: "gemini",
10995
+ modelId: "gemini-3-pro-image",
10996
+ api: "gemini-predict"
10997
+ },
10998
+ // ── Speech (text → audio) ──
10999
+ {
11000
+ modality: "speech",
11001
+ provider: "openai",
11002
+ modelId: "tts-1",
11003
+ api: "openai-speech"
11004
+ },
11005
+ {
11006
+ // The only quality claim in this table that needs no leaderboard: OpenAI
11007
+ // ships these as the same voice at two fidelities and charges double for
11008
+ // this one. That is the vendor's own ordering, not an inference.
11009
+ modality: "speech",
11010
+ provider: "openai",
11011
+ modelId: "tts-1-hd",
11012
+ api: "openai-speech",
11013
+ quality: 70,
11014
+ qualityBasis: "OpenAI's own hd variant of tts-1, priced 2x"
11015
+ },
11016
+ // ── Transcription (audio → text) ──
11017
+ {
11018
+ modality: "transcription",
11019
+ provider: "openai",
11020
+ modelId: "whisper-1",
11021
+ api: "openai-transcriptions"
11022
+ },
11023
+ // ── Video ──
11024
+ {
11025
+ // Listed so `describe()` can tell the truth about what exists, and so the
11026
+ // planner is told video is out of reach rather than left to invent a tool.
11027
+ // Veo is a long-running operation: submit, poll, then fetch bytes from a
11028
+ // signed URL. That is a different execution shape from every other entry
11029
+ // here and is not wired up — claiming otherwise would produce a tool that
11030
+ // fails at call time instead of an honest "not yet".
11031
+ modality: "video",
11032
+ provider: "gemini",
11033
+ modelId: "veo-3.1-generate-001",
11034
+ api: "gemini-predict",
11035
+ unsupported: "Veo runs as a long-running operation (submit \u2192 poll \u2192 fetch). Not wired up yet."
11036
+ }
11037
+ ];
11038
+ function capabilityCost(cap) {
11039
+ const match = findPricing(cap.modelId, cap.provider, {
11040
+ fallbackProviders: cap.provider === "gemini" ? ["vertex"] : []
11041
+ });
11042
+ const rate = match?.entry.rates?.[0];
11043
+ if (!rate) return null;
11044
+ const unitLabel = {
11045
+ per_image: "per image",
11046
+ per_second: "per second of video",
11047
+ per_character: "per character",
11048
+ per_audio_minute: "per audio minute"
11049
+ };
11050
+ const unit = unitLabel[rate.unit] ?? rate.unit;
11051
+ const amount = rate.amount;
11052
+ const shown = amount < 1e-3 ? amount.toExponential(2) : amount.toFixed(4);
11053
+ return { label: `$${shown} ${unit}`, amount, unit };
11054
+ }
11055
+ var MultimodalRegistry = class {
11056
+ /** Provider types the user has actually configured a key for. */
11057
+ available;
11058
+ constructor(availableProviders) {
11059
+ this.available = new Set(availableProviders);
11060
+ }
11061
+ /** Every capability whose provider is configured AND which Cascade can call. */
11062
+ usable(modality) {
11063
+ return CAPABILITIES.filter(
11064
+ (c) => !c.unsupported && this.available.has(c.provider) && (!modality || c.modality === modality)
11065
+ );
11066
+ }
11067
+ /** Modalities this account can actually generate right now. */
11068
+ availableModalities() {
11069
+ return [...new Set(this.usable().map((c) => c.modality))];
11070
+ }
11071
+ /**
11072
+ * Pick a model for a modality.
11073
+ *
11074
+ * Ranked models beat unranked ones, then higher quality, then lower unit
11075
+ * price. An explicit `preferModel` wins outright when it is usable — the user
11076
+ * naming a model is a stronger signal than any prior.
11077
+ */
11078
+ select(modality, opts = {}) {
11079
+ const pool = this.usable(modality);
11080
+ if (!pool.length) return null;
11081
+ if (opts.preferModel) {
11082
+ const wanted = pool.find((c) => c.modelId === opts.preferModel);
11083
+ if (wanted) {
11084
+ return { capability: wanted, cost: capabilityCost(wanted), reason: `Requested explicitly: ${wanted.modelId}.` };
11085
+ }
11086
+ }
11087
+ const scored = pool.map((c) => ({ c, cost: capabilityCost(c) })).sort((a, b) => {
11088
+ const qa = a.c.quality ?? -1;
11089
+ const qb = b.c.quality ?? -1;
11090
+ if (qa !== qb) return qb - qa;
11091
+ const pa = a.cost?.amount ?? Number.POSITIVE_INFINITY;
11092
+ const pb = b.cost?.amount ?? Number.POSITIVE_INFINITY;
11093
+ return pa - pb;
11094
+ });
11095
+ const best = scored[0];
11096
+ const ranked = best.c.quality !== void 0;
11097
+ const reason = ranked ? `${best.c.modelId} (${best.c.provider}) \u2014 ranked highest for ${modality}: ${best.c.qualityBasis}.` + (best.cost ? ` ${best.cost.label}.` : "") : `${best.c.modelId} (${best.c.provider}) \u2014 cheapest usable ${modality} model` + (best.cost ? ` at ${best.cost.label}` : "") + ". No published quality ranking is applied for this modality, so this is a cost choice, not a quality one.";
11098
+ return { capability: best.c, cost: best.cost, reason };
11099
+ }
11100
+ /**
11101
+ * A plain-language account of what this account can and cannot generate —
11102
+ * including the things it cannot, and why. The planner sees this, so being
11103
+ * candid here is what stops it inventing a `generate_music` call.
11104
+ */
11105
+ describe() {
11106
+ const lines = [];
11107
+ const usable = this.usable();
11108
+ if (usable.length) {
11109
+ lines.push("Generation models available on your configured providers:");
11110
+ for (const modality of ["image", "speech", "transcription", "video"]) {
11111
+ const forModality = usable.filter((c) => c.modality === modality);
11112
+ if (!forModality.length) continue;
11113
+ const shown = forModality.map((c) => {
11114
+ const cost = capabilityCost(c);
11115
+ return `${c.modelId}${cost ? ` (${cost.label})` : ""}`;
11116
+ });
11117
+ lines.push(` ${modality}: ${shown.join(", ")}`);
11118
+ }
11119
+ } else {
11120
+ lines.push("No generation models are available \u2014 none of your configured providers offers one.");
11121
+ }
11122
+ const blocked = CAPABILITIES.filter((c) => c.unsupported && this.available.has(c.provider));
11123
+ for (const c of blocked) {
11124
+ lines.push(` ${c.modality}: ${c.modelId} exists on your account but is not callable. ${c.unsupported}`);
11125
+ }
11126
+ lines.push(
11127
+ " music: no supported provider exposes a music-generation API, so Cascade cannot generate music."
11128
+ );
11129
+ return lines.join("\n");
11130
+ }
11131
+ };
11132
+
11133
+ // src/tools/base.ts
11134
+ var BaseTool = class {
11135
+ workspaceRoot = process.cwd();
11136
+ setWorkspaceRoot(root) {
11137
+ this.workspaceRoot = root;
11138
+ }
11139
+ isDangerous() {
11140
+ return false;
11141
+ }
11142
+ getDefinition() {
11143
+ return {
11144
+ name: this.name,
11145
+ description: this.description,
11146
+ inputSchema: this.inputSchema
11147
+ };
11148
+ }
11149
+ };
11150
+
10846
11151
  // src/core/router/provider-errors.ts
10847
11152
  var SYSTEMIC = /* @__PURE__ */ new Set([
10848
11153
  "rate_limit",
@@ -10907,7 +11212,7 @@ function classifyProviderError(err) {
10907
11212
  return { kind, systemic: isSystemicKind(kind), status, raw };
10908
11213
  }
10909
11214
  function describeProviderError(c, modelId) {
10910
- const on = ` on ${modelId}` ;
11215
+ const on = modelId ? ` on ${modelId}` : "";
10911
11216
  const detail = c.raw ? ` Provider said: ${c.raw}` : "";
10912
11217
  switch (c.kind) {
10913
11218
  case "rate_limit":
@@ -10929,6 +11234,295 @@ function describeProviderError(c, modelId) {
10929
11234
  }
10930
11235
  }
10931
11236
 
11237
+ // src/core/multimodal/generate.ts
11238
+ var DEFAULT_TIMEOUT_MS = 12e4;
11239
+ async function callProvider(fn, modelId) {
11240
+ try {
11241
+ return await fn();
11242
+ } catch (err) {
11243
+ const classified = classifyProviderError(err);
11244
+ throw new Error(describeProviderError(classified, modelId));
11245
+ }
11246
+ }
11247
+ async function postJson(url, body, headers, signal) {
11248
+ const ac = new AbortController();
11249
+ const timer = setTimeout(() => ac.abort(), DEFAULT_TIMEOUT_MS);
11250
+ const onAbort = () => ac.abort();
11251
+ signal?.addEventListener("abort", onAbort);
11252
+ try {
11253
+ const res = await fetch(url, {
11254
+ method: "POST",
11255
+ headers: { "Content-Type": "application/json", ...headers },
11256
+ body: JSON.stringify(body),
11257
+ signal: ac.signal
11258
+ });
11259
+ if (!res.ok) {
11260
+ const text = await res.text().catch(() => "");
11261
+ throw Object.assign(new Error(text || res.statusText), { status: res.status });
11262
+ }
11263
+ return res;
11264
+ } finally {
11265
+ clearTimeout(timer);
11266
+ signal?.removeEventListener("abort", onAbort);
11267
+ }
11268
+ }
11269
+ function baseUrlFor(cfg, fallback) {
11270
+ return (cfg.baseUrl ?? fallback).replace(/\/+$/, "");
11271
+ }
11272
+ function stamp(ext) {
11273
+ return `cascade-${Date.now()}.${ext}`;
11274
+ }
11275
+ async function generateImage(cap, cfg, req, signal) {
11276
+ return callProvider(async () => {
11277
+ if (cap.api === "openai-images") {
11278
+ const url = `${baseUrlFor(cfg, "https://api.openai.com/v1")}/images/generations`;
11279
+ const res = await postJson(url, {
11280
+ model: cap.modelId,
11281
+ prompt: req.prompt,
11282
+ size: req.size ?? "1024x1024",
11283
+ n: 1,
11284
+ // b64 rather than a URL: see the file header — provider URLs expire.
11285
+ response_format: "b64_json"
11286
+ }, { Authorization: `Bearer ${cfg.apiKey ?? ""}` }, signal);
11287
+ const body = await res.json();
11288
+ const first = body.data?.[0];
11289
+ if (!first?.b64_json) throw new Error("Image response contained no image data.");
11290
+ return {
11291
+ data: Buffer.from(first.b64_json, "base64"),
11292
+ mimeType: "image/png",
11293
+ filename: stamp("png"),
11294
+ modelId: cap.modelId,
11295
+ provider: cap.provider,
11296
+ ...first.revised_prompt ? { revisedPrompt: first.revised_prompt } : {}
11297
+ };
11298
+ }
11299
+ if (cap.api === "gemini-predict") {
11300
+ const base = baseUrlFor(cfg, "https://generativelanguage.googleapis.com/v1beta");
11301
+ const url = `${base}/models/${encodeURIComponent(cap.modelId)}:predict`;
11302
+ const res = await postJson(url, {
11303
+ instances: [{ prompt: req.prompt }],
11304
+ parameters: { sampleCount: 1 }
11305
+ }, { "x-goog-api-key": cfg.apiKey ?? "" }, signal);
11306
+ const body = await res.json();
11307
+ const first = body.predictions?.[0];
11308
+ if (!first?.bytesBase64Encoded) throw new Error("Image response contained no image data.");
11309
+ const mimeType = first.mimeType ?? "image/png";
11310
+ return {
11311
+ data: Buffer.from(first.bytesBase64Encoded, "base64"),
11312
+ mimeType,
11313
+ filename: stamp(mimeType.includes("jpeg") ? "jpg" : "png"),
11314
+ modelId: cap.modelId,
11315
+ provider: cap.provider
11316
+ };
11317
+ }
11318
+ throw new Error(`${cap.modelId} is not an image generator.`);
11319
+ }, cap.modelId);
11320
+ }
11321
+ async function generateSpeech(cap, cfg, req, signal) {
11322
+ return callProvider(async () => {
11323
+ if (cap.api !== "openai-speech") throw new Error(`${cap.modelId} is not a speech model.`);
11324
+ const format = req.format ?? "mp3";
11325
+ const url = `${baseUrlFor(cfg, "https://api.openai.com/v1")}/audio/speech`;
11326
+ const res = await postJson(url, {
11327
+ model: cap.modelId,
11328
+ input: req.text,
11329
+ voice: req.voice ?? "alloy",
11330
+ response_format: format
11331
+ }, { Authorization: `Bearer ${cfg.apiKey ?? ""}` }, signal);
11332
+ const mime = {
11333
+ mp3: "audio/mpeg",
11334
+ opus: "audio/opus",
11335
+ aac: "audio/aac",
11336
+ flac: "audio/flac",
11337
+ wav: "audio/wav"
11338
+ };
11339
+ return {
11340
+ data: Buffer.from(await res.arrayBuffer()),
11341
+ mimeType: mime[format] ?? "application/octet-stream",
11342
+ filename: stamp(format),
11343
+ modelId: cap.modelId,
11344
+ provider: cap.provider
11345
+ };
11346
+ }, cap.modelId);
11347
+ }
11348
+ async function transcribeAudio(cap, cfg, req, signal) {
11349
+ return callProvider(async () => {
11350
+ if (cap.api !== "openai-transcriptions") throw new Error(`${cap.modelId} is not a transcription model.`);
11351
+ const url = `${baseUrlFor(cfg, "https://api.openai.com/v1")}/audio/transcriptions`;
11352
+ const form = new FormData();
11353
+ form.append("model", cap.modelId);
11354
+ if (req.language) form.append("language", req.language);
11355
+ form.append("file", new Blob([new Uint8Array(req.audio)]), req.filename);
11356
+ const ac = new AbortController();
11357
+ const timer = setTimeout(() => ac.abort(), DEFAULT_TIMEOUT_MS);
11358
+ const onAbort = () => ac.abort();
11359
+ signal?.addEventListener("abort", onAbort);
11360
+ try {
11361
+ const res = await fetch(url, {
11362
+ method: "POST",
11363
+ headers: { Authorization: `Bearer ${cfg.apiKey ?? ""}` },
11364
+ body: form,
11365
+ signal: ac.signal
11366
+ });
11367
+ if (!res.ok) {
11368
+ const text = await res.text().catch(() => "");
11369
+ throw Object.assign(new Error(text || res.statusText), { status: res.status });
11370
+ }
11371
+ const body = await res.json();
11372
+ return { text: body.text ?? "", modelId: cap.modelId };
11373
+ } finally {
11374
+ clearTimeout(timer);
11375
+ signal?.removeEventListener("abort", onAbort);
11376
+ }
11377
+ }, cap.modelId);
11378
+ }
11379
+
11380
+ // src/tools/generate-media.ts
11381
+ function resolve(deps, modality, preferModel) {
11382
+ const selected = deps.registry.select(modality, preferModel ? { preferModel } : {});
11383
+ if (!selected) {
11384
+ return { ok: false, error: `No ${modality} model is available on your configured providers.` };
11385
+ }
11386
+ const cfg = deps.lookupProvider(selected.capability.provider);
11387
+ if (!cfg) {
11388
+ return {
11389
+ ok: false,
11390
+ error: `${selected.capability.modelId} needs the ${selected.capability.provider} provider, which is not configured.`
11391
+ };
11392
+ }
11393
+ return { ok: true, selected, cfg };
11394
+ }
11395
+ var GenerateImageTool = class extends BaseTool {
11396
+ constructor(deps) {
11397
+ super();
11398
+ this.deps = deps;
11399
+ }
11400
+ deps;
11401
+ name = "generate_image";
11402
+ description = "Generate an image from a text description using the best available image model. Returns where the image was saved. Use when the user asks for a picture, illustration, diagram render, logo or any visual asset.";
11403
+ inputSchema = {
11404
+ type: "object",
11405
+ properties: {
11406
+ prompt: { type: "string", description: "What the image should show. Be specific about subject, style and composition." },
11407
+ size: { type: "string", description: 'Pixel size, e.g. "1024x1024" (default), "1792x1024", "1024x1792".' },
11408
+ model: { type: "string", description: "Optional: force a specific image model instead of the automatic choice." }
11409
+ },
11410
+ required: ["prompt"]
11411
+ };
11412
+ async execute(input, options) {
11413
+ const prompt = String(input["prompt"] ?? "").trim();
11414
+ if (!prompt) return 'Provide a "prompt" describing the image.';
11415
+ const r = resolve(this.deps, "image", input["model"] ? String(input["model"]) : void 0);
11416
+ if (!r.ok) return r.error;
11417
+ const size = input["size"] ? String(input["size"]) : void 0;
11418
+ const asset = await generateImage(
11419
+ r.selected.capability,
11420
+ r.cfg,
11421
+ { prompt, ...size ? { size } : {} },
11422
+ options.signal
11423
+ );
11424
+ const location = await this.deps.sink(asset);
11425
+ return [
11426
+ `Image generated and saved to ${location}.`,
11427
+ `Model: ${asset.modelId} \u2014 ${r.selected.reason}`,
11428
+ // DALL·E rewrites the prompt before drawing; the model needs to know what
11429
+ // was actually rendered, not what it asked for, or its description of the
11430
+ // result will not match the image.
11431
+ asset.revisedPrompt ? `The provider rewrote the prompt to: ${asset.revisedPrompt}` : ""
11432
+ ].filter(Boolean).join("\n");
11433
+ }
11434
+ };
11435
+ var GenerateSpeechTool = class extends BaseTool {
11436
+ constructor(deps) {
11437
+ super();
11438
+ this.deps = deps;
11439
+ }
11440
+ deps;
11441
+ name = "generate_speech";
11442
+ description = "Turn text into spoken audio using the best available text-to-speech model. Returns where the audio was saved. Use when the user asks for narration, a voiceover or an audio version of some text.";
11443
+ inputSchema = {
11444
+ type: "object",
11445
+ properties: {
11446
+ text: { type: "string", description: "The text to speak." },
11447
+ voice: { type: "string", description: "Voice id: alloy, echo, fable, onyx, nova, shimmer. Default alloy." },
11448
+ format: { type: "string", description: "Audio format: mp3 (default), opus, aac, flac, wav." },
11449
+ model: { type: "string", description: "Optional: force a specific speech model." }
11450
+ },
11451
+ required: ["text"]
11452
+ };
11453
+ async execute(input, options) {
11454
+ const text = String(input["text"] ?? "").trim();
11455
+ if (!text) return 'Provide "text" to speak.';
11456
+ const r = resolve(this.deps, "speech", input["model"] ? String(input["model"]) : void 0);
11457
+ if (!r.ok) return r.error;
11458
+ const fmt = String(input["format"] ?? "mp3");
11459
+ const allowed = ["mp3", "opus", "aac", "flac", "wav"];
11460
+ const format = allowed.includes(fmt) ? fmt : "mp3";
11461
+ const asset = await generateSpeech(
11462
+ r.selected.capability,
11463
+ r.cfg,
11464
+ { text, voice: input["voice"] ? String(input["voice"]) : void 0, format },
11465
+ options.signal
11466
+ );
11467
+ const location = await this.deps.sink(asset);
11468
+ return `Speech generated and saved to ${location}.
11469
+ Model: ${asset.modelId} \u2014 ${r.selected.reason}`;
11470
+ }
11471
+ };
11472
+ var TranscribeAudioTool = class extends BaseTool {
11473
+ constructor(deps, readFile) {
11474
+ super();
11475
+ this.deps = deps;
11476
+ this.readFile = readFile;
11477
+ }
11478
+ deps;
11479
+ readFile;
11480
+ name = "transcribe_audio";
11481
+ description = "Transcribe a local audio file to text using the best available speech-to-text model. Use when the user supplies audio and wants its contents in writing.";
11482
+ inputSchema = {
11483
+ type: "object",
11484
+ properties: {
11485
+ path: { type: "string", description: "Path to the audio file to transcribe." },
11486
+ language: { type: "string", description: 'Optional ISO-639-1 language hint, e.g. "en", improves accuracy.' },
11487
+ model: { type: "string", description: "Optional: force a specific transcription model." }
11488
+ },
11489
+ required: ["path"]
11490
+ };
11491
+ async execute(input, options) {
11492
+ const filePath = String(input["path"] ?? "").trim();
11493
+ if (!filePath) return 'Provide a "path" to the audio file.';
11494
+ const r = resolve(this.deps, "transcription", input["model"] ? String(input["model"]) : void 0);
11495
+ if (!r.ok) return r.error;
11496
+ let audio;
11497
+ try {
11498
+ audio = await this.readFile(filePath);
11499
+ } catch {
11500
+ return `Could not read the audio file at ${filePath}.`;
11501
+ }
11502
+ const { text, modelId } = await transcribeAudio(
11503
+ r.selected.capability,
11504
+ r.cfg,
11505
+ {
11506
+ audio,
11507
+ filename: filePath.split(/[\\/]/).pop() || "audio",
11508
+ ...input["language"] ? { language: String(input["language"]) } : {}
11509
+ },
11510
+ options.signal
11511
+ );
11512
+ return text ? `Transcript (${modelId}):
11513
+
11514
+ ${text}` : `${modelId} returned an empty transcript \u2014 the file may be silent or in an unsupported format.`;
11515
+ }
11516
+ };
11517
+ function buildMediaTools(deps, readFile) {
11518
+ const tools = [];
11519
+ const modalities = new Set(deps.registry.availableModalities());
11520
+ if (modalities.has("image")) tools.push(new GenerateImageTool(deps));
11521
+ if (modalities.has("speech")) tools.push(new GenerateSpeechTool(deps));
11522
+ if (modalities.has("transcription") && readFile) tools.push(new TranscribeAudioTool(deps, readFile));
11523
+ return tools;
11524
+ }
11525
+
10932
11526
  // src/core/run-breaker.ts
10933
11527
  var DEFAULT_FAILURE_THRESHOLD = 3;
10934
11528
  var RunBreaker = class {
@@ -10997,26 +11591,6 @@ var RunBreaker = class {
10997
11591
 
10998
11592
  // src/tools/registry.ts
10999
11593
  init_constants();
11000
-
11001
- // src/tools/base.ts
11002
- var BaseTool = class {
11003
- workspaceRoot = process.cwd();
11004
- setWorkspaceRoot(root) {
11005
- this.workspaceRoot = root;
11006
- }
11007
- isDangerous() {
11008
- return false;
11009
- }
11010
- getDefinition() {
11011
- return {
11012
- name: this.name,
11013
- description: this.description,
11014
- inputSchema: this.inputSchema
11015
- };
11016
- }
11017
- };
11018
-
11019
- // src/tools/shell.ts
11020
11594
  var execAsync = promisify(exec);
11021
11595
  var ShellTool = class extends BaseTool {
11022
11596
  name = "shell";
@@ -11712,7 +12286,7 @@ var PDFCreateTool = class extends BaseTool {
11712
12286
  if (!fs29.existsSync(dir)) {
11713
12287
  fs29.mkdirSync(dir, { recursive: true });
11714
12288
  }
11715
- return new Promise((resolve, reject) => {
12289
+ return new Promise((resolve2, reject) => {
11716
12290
  try {
11717
12291
  const doc = new PDFDocument({ margin: 50 });
11718
12292
  const stream = fs29.createWriteStream(filePath);
@@ -11730,7 +12304,7 @@ var PDFCreateTool = class extends BaseTool {
11730
12304
  });
11731
12305
  doc.end();
11732
12306
  stream.on("finish", () => {
11733
- resolve(`Successfully created PDF at ${filePath} (${content.length} characters)`);
12307
+ resolve2(`Successfully created PDF at ${filePath} (${content.length} characters)`);
11734
12308
  });
11735
12309
  stream.on("error", (err) => {
11736
12310
  reject(new Error(`Failed to write PDF: ${err.message}`));
@@ -11802,7 +12376,7 @@ var CodeInterpreterTool = class extends BaseTool {
11802
12376
  const filePath = path32.join(tmpDir, fileName);
11803
12377
  fs29.writeFileSync(filePath, code, "utf-8");
11804
12378
  const execArgs = [filePath, ...args];
11805
- return new Promise((resolve) => {
12379
+ return new Promise((resolve2) => {
11806
12380
  const startMs = Date.now();
11807
12381
  execFile(cmdPrefix, execArgs, { cwd: this.workspaceRoot, timeout: 3e4 }, (error, stdout, stderr) => {
11808
12382
  const duration = Date.now() - startMs;
@@ -11816,17 +12390,17 @@ var CodeInterpreterTool = class extends BaseTool {
11816
12390
  if (error) {
11817
12391
  const timedOut = error.killed && duration >= 3e4;
11818
12392
  if (timedOut) {
11819
- resolve(`Execution timed out after 30s. Consider breaking the task into smaller pieces.
12393
+ resolve2(`Execution timed out after 30s. Consider breaking the task into smaller pieces.
11820
12394
  Partial stdout: ${stdout}
11821
12395
  Stderr: ${stderr}`);
11822
12396
  } else {
11823
- resolve(`Execution failed (${duration}ms):
12397
+ resolve2(`Execution failed (${duration}ms):
11824
12398
  Error: ${error.message}
11825
12399
  Stderr: ${stderr}
11826
12400
  Stdout: ${stdout}`);
11827
12401
  }
11828
12402
  } else {
11829
- resolve(`Execution successful (${duration}ms):
12403
+ resolve2(`Execution successful (${duration}ms):
11830
12404
  Stdout: ${stdout}
11831
12405
  Stderr: ${stderr}`);
11832
12406
  }
@@ -12480,7 +13054,7 @@ var ToolRegistry = class extends EventEmitter {
12480
13054
  */
12481
13055
  waitForTool(toolName, timeoutMs = 6e4) {
12482
13056
  if (this.tools.has(toolName)) return Promise.resolve();
12483
- return new Promise((resolve, reject) => {
13057
+ return new Promise((resolve2, reject) => {
12484
13058
  const timer = setTimeout(() => {
12485
13059
  this.off("tool:added", handler);
12486
13060
  reject(new Error(`Timeout waiting for tool: ${toolName}`));
@@ -12489,7 +13063,7 @@ var ToolRegistry = class extends EventEmitter {
12489
13063
  if (name === toolName) {
12490
13064
  clearTimeout(timer);
12491
13065
  this.off("tool:added", handler);
12492
- resolve();
13066
+ resolve2();
12493
13067
  }
12494
13068
  };
12495
13069
  this.on("tool:added", handler);
@@ -13142,20 +13716,20 @@ var PermissionEscalator = class extends EventEmitter {
13142
13716
  resolver(decision);
13143
13717
  }
13144
13718
  waitForUserDecision(req) {
13145
- return new Promise((resolve) => {
13719
+ return new Promise((resolve2) => {
13146
13720
  let timer;
13147
13721
  const wrappedResolver = (decision) => {
13148
13722
  if (timer) clearTimeout(timer);
13149
13723
  if (decision.always) {
13150
13724
  this.taskWideCache.set(req.toolName, decision.approved);
13151
13725
  }
13152
- resolve(decision);
13726
+ resolve2(decision);
13153
13727
  };
13154
13728
  this.pendingUserDecisions.set(req.id, wrappedResolver);
13155
13729
  if (this.approvalTimeoutMs > 0 && Number.isFinite(this.approvalTimeoutMs)) {
13156
13730
  timer = setTimeout(() => {
13157
13731
  if (this.pendingUserDecisions.delete(req.id)) {
13158
- resolve({
13732
+ resolve2({
13159
13733
  requestId: req.id,
13160
13734
  approved: false,
13161
13735
  decidedBy: "USER",
@@ -13223,6 +13797,23 @@ var noopTelemetry = {
13223
13797
  }
13224
13798
  };
13225
13799
 
13800
+ // src/core/router/feedback-prior.ts
13801
+ var MAX_FEEDBACK_ADJUSTMENT = 0.05;
13802
+ var PRIOR_STRENGTH = 10;
13803
+ function feedbackAdjustment(counts) {
13804
+ const good = Math.max(0, Math.floor(counts.good));
13805
+ const bad = Math.max(0, Math.floor(counts.bad));
13806
+ const n = good + bad;
13807
+ if (n === 0) return 0;
13808
+ const half = PRIOR_STRENGTH / 2;
13809
+ const shrunk = (good + half) / (n + PRIOR_STRENGTH);
13810
+ return (shrunk - 0.5) * 2 * MAX_FEEDBACK_ADJUSTMENT;
13811
+ }
13812
+ function applyFeedback(score01, counts) {
13813
+ const adjusted = score01 + feedbackAdjustment(counts);
13814
+ return Math.max(0, Math.min(1, adjusted));
13815
+ }
13816
+
13226
13817
  // src/core/router/task-analyzer.ts
13227
13818
  init_pricing();
13228
13819
  var CODE_SIGNALS = [
@@ -13298,6 +13889,8 @@ var TASK_TYPE_TAGS = {
13298
13889
  };
13299
13890
  var TaskAnalyzer = class {
13300
13891
  tracker;
13892
+ /** Per-model thumbs counts, when the host collects them. */
13893
+ feedback;
13301
13894
  bias;
13302
13895
  lastProfile = null;
13303
13896
  lastSelectedModels = /* @__PURE__ */ new Map();
@@ -13305,6 +13898,14 @@ var TaskAnalyzer = class {
13305
13898
  this.tracker = tracker;
13306
13899
  this.bias = bias;
13307
13900
  }
13901
+ /**
13902
+ * Supply per-model rating counts. Optional by design: a host that collects no
13903
+ * feedback (the CLI, a fresh install) scores exactly as it did before, since
13904
+ * feedbackAdjustment returns 0 for an empty record.
13905
+ */
13906
+ setFeedbackSource(source) {
13907
+ this.feedback = source;
13908
+ }
13308
13909
  setTracker(tracker) {
13309
13910
  this.tracker = tracker;
13310
13911
  }
@@ -13388,7 +13989,10 @@ var TaskAnalyzer = class {
13388
13989
  const perf = this.tracker?.performanceScore(model.id, profile.type) ?? 0.5;
13389
13990
  const costEff = this.costEfficiency(model, profile.complexity);
13390
13991
  const match = this.taskMatchScore(model, profile);
13391
- const benchmark = 0.3 + 0.7 * benchmarkScore01(model, profile.type);
13992
+ const rated = this.feedback?.(model.id);
13993
+ const publicScore = benchmarkScore01(model, profile.type);
13994
+ const adjusted = rated ? applyFeedback(publicScore, rated) : publicScore;
13995
+ const benchmark = 0.3 + 0.7 * adjusted;
13392
13996
  switch (this.bias) {
13393
13997
  case "quality":
13394
13998
  return perf * match * benchmark ** 2 * (0.85 + 0.15 * costEff);
@@ -13793,10 +14397,10 @@ var DynamicTool = class extends BaseTool {
13793
14397
  `;
13794
14398
  const script = await isolate.compileScript(bootstrap);
13795
14399
  const runPromise = script.run(context, { timeout: timeoutMs, promise: true }).then((v) => String(v ?? ""));
13796
- const timeoutPromise = new Promise((resolve) => {
14400
+ const timeoutPromise = new Promise((resolve2) => {
13797
14401
  const t = setTimeout(() => {
13798
14402
  dispose();
13799
- resolve(`Dynamic tool "${this.name}" timed out after ${timeoutMs}ms and was terminated.`);
14403
+ resolve2(`Dynamic tool "${this.name}" timed out after ${timeoutMs}ms and was terminated.`);
13800
14404
  }, timeoutMs + 500);
13801
14405
  t.unref?.();
13802
14406
  });
@@ -13814,7 +14418,7 @@ var DynamicTool = class extends BaseTool {
13814
14418
  /** Spawn the worker, service its callTool/fetch bridge, enforce the kill timeout. */
13815
14419
  runInWorker(input, callTool) {
13816
14420
  const timeoutMs = Math.max(200, Number(process.env["CASCADE_DYNAMIC_TOOL_TIMEOUT_MS"]) || DYNAMIC_TOOL_TIMEOUT_MS);
13817
- return new Promise((resolve) => {
14421
+ return new Promise((resolve2) => {
13818
14422
  let settled = false;
13819
14423
  const worker = new Worker(HARNESS_SRC, {
13820
14424
  eval: true,
@@ -13826,7 +14430,7 @@ var DynamicTool = class extends BaseTool {
13826
14430
  settled = true;
13827
14431
  clearTimeout(timer);
13828
14432
  void worker.terminate();
13829
- resolve(value);
14433
+ resolve2(value);
13830
14434
  };
13831
14435
  const timer = setTimeout(
13832
14436
  () => finish(`Dynamic tool "${this.name}" timed out after ${timeoutMs}ms and was terminated.`),
@@ -15333,6 +15937,7 @@ ${prompt}`;
15333
15937
  var Cascade = class _Cascade extends EventEmitter {
15334
15938
  router;
15335
15939
  toolRegistry;
15940
+ multimodal;
15336
15941
  mcpClient;
15337
15942
  codeIndex;
15338
15943
  config;
@@ -15371,6 +15976,10 @@ var Cascade = class _Cascade extends EventEmitter {
15371
15976
  }
15372
15977
  });
15373
15978
  this.toolRegistry = new ToolRegistry(this.config.tools, workspacePath);
15979
+ this.multimodal = new MultimodalRegistry(
15980
+ (this.config.providers ?? []).map((p) => p.type)
15981
+ );
15982
+ this.registerMediaTools(workspacePath);
15374
15983
  this.telemetry = config.telemetry?.enabled ? new Telemetry(config.telemetry, config.telemetry.distinctId ?? "anonymous") : noopTelemetry;
15375
15984
  this.worldStateDB = new WorldStateDB(this.workspacePath, this.config.workspace?.debugWorldState ?? false);
15376
15985
  this.router.setWorldStateDB(this.worldStateDB);
@@ -15424,14 +16033,14 @@ var Cascade = class _Cascade extends EventEmitter {
15424
16033
  if (this.listenerCount("mcp:approval-required") === 0) {
15425
16034
  return false;
15426
16035
  }
15427
- return await new Promise((resolve) => {
15428
- this.pendingMcpApprovals.set(server.name, resolve);
16036
+ return await new Promise((resolve2) => {
16037
+ this.pendingMcpApprovals.set(server.name, resolve2);
15429
16038
  const timeout = setTimeout(() => {
15430
- if (this.pendingMcpApprovals.delete(server.name)) resolve(false);
16039
+ if (this.pendingMcpApprovals.delete(server.name)) resolve2(false);
15431
16040
  }, 3e4);
15432
16041
  const wrap = (approved) => {
15433
16042
  clearTimeout(timeout);
15434
- resolve(approved);
16043
+ resolve2(approved);
15435
16044
  };
15436
16045
  this.pendingMcpApprovals.set(server.name, wrap);
15437
16046
  this.emit("mcp:approval-required", { server });
@@ -15505,17 +16114,17 @@ var Cascade = class _Cascade extends EventEmitter {
15505
16114
  }
15506
16115
  const t2Count = plan.sections.length;
15507
16116
  const t3Count = plan.sections.reduce((sum, s) => sum + (s.t3Subtasks?.length ?? 0), 0);
15508
- return await new Promise((resolve) => {
16117
+ return await new Promise((resolve2) => {
15509
16118
  const timeout = setTimeout(() => {
15510
16119
  if (this.pendingPlanApproval) {
15511
16120
  this.pendingPlanApproval = void 0;
15512
- resolve({ approved: true });
16121
+ resolve2({ approved: true });
15513
16122
  }
15514
16123
  }, 12e4);
15515
16124
  this.pendingPlanApproval = (decision) => {
15516
16125
  clearTimeout(timeout);
15517
16126
  this.pendingPlanApproval = void 0;
15518
- resolve(decision);
16127
+ resolve2(decision);
15519
16128
  };
15520
16129
  this.emit("plan:approval-required", {
15521
16130
  taskId,
@@ -15548,17 +16157,17 @@ var Cascade = class _Cascade extends EventEmitter {
15548
16157
  }
15549
16158
  async requestContextApproval(info) {
15550
16159
  if (this.listenerCount("context:approval-required") === 0) return true;
15551
- return await new Promise((resolve) => {
16160
+ return await new Promise((resolve2) => {
15552
16161
  const timeout = setTimeout(() => {
15553
16162
  if (this.pendingContextApproval) {
15554
16163
  this.pendingContextApproval = void 0;
15555
- resolve(true);
16164
+ resolve2(true);
15556
16165
  }
15557
16166
  }, 12e4);
15558
16167
  this.pendingContextApproval = (approved) => {
15559
16168
  clearTimeout(timeout);
15560
16169
  this.pendingContextApproval = void 0;
15561
- resolve(approved);
16170
+ resolve2(approved);
15562
16171
  };
15563
16172
  this.emit("context:approval-required", info);
15564
16173
  });
@@ -15638,6 +16247,55 @@ ${input}` }],
15638
16247
  if (this.store) t1.setStore(this.store);
15639
16248
  return t1.previewPlan(prompt);
15640
16249
  }
16250
+ /**
16251
+ * Register image / speech / transcription tools for the modalities this
16252
+ * config can actually serve.
16253
+ *
16254
+ * The sink writes bytes into the workspace, because that is what a desktop or
16255
+ * CLI run wants and it keeps a generated asset indistinguishable from any
16256
+ * other file the run produced. A host with different storage (the cloud
16257
+ * server stores a file row) constructs the tools itself via buildMediaTools
16258
+ * with its own sink.
16259
+ */
16260
+ registerMediaTools(workspacePath) {
16261
+ const allowlist = this.config.tools?.enabledTools;
16262
+ const permitted = (name) => !allowlist || allowlist.includes(name);
16263
+ const tools = buildMediaTools(
16264
+ {
16265
+ registry: this.multimodal,
16266
+ lookupProvider: (provider) => (this.config.providers ?? []).find((p) => p.type === provider),
16267
+ sink: async (asset) => {
16268
+ const fsp = await import('fs/promises');
16269
+ const nodePath = await import('path');
16270
+ const dir = nodePath.join(workspacePath, "generated");
16271
+ await fsp.mkdir(dir, { recursive: true });
16272
+ const dest = nodePath.join(dir, asset.filename);
16273
+ await fsp.writeFile(dest, asset.data);
16274
+ return nodePath.relative(workspacePath, dest);
16275
+ }
16276
+ },
16277
+ async (p) => {
16278
+ const fsp = await import('fs/promises');
16279
+ return fsp.readFile(p);
16280
+ }
16281
+ );
16282
+ for (const tool of tools) {
16283
+ if (!permitted(tool.name)) continue;
16284
+ tool.setWorkspaceRoot(workspacePath);
16285
+ this.toolRegistry.register(tool);
16286
+ }
16287
+ }
16288
+ /**
16289
+ * Supply per-model thumbs-up/down counts so Auto routing can weigh how models
16290
+ * have actually performed for this user, on top of public benchmarks.
16291
+ *
16292
+ * Optional everywhere: with no source, or an empty record, scoring is
16293
+ * byte-identical to before. The adjustment is capped and sample-size shrunk —
16294
+ * see core/router/feedback-prior.ts.
16295
+ */
16296
+ setFeedbackSource(source) {
16297
+ this.router.setFeedbackSource(source);
16298
+ }
15641
16299
  /** True when a task stopped at the budget cap and can be resumed via /continue. */
15642
16300
  hasResumableRun() {
15643
16301
  return this.lastInterruptedRun != null;
@@ -16127,7 +16785,7 @@ ${prompt}` : prompt;
16127
16785
  if (toolCreator) toolCreator.setPermissionEscalator(escalator);
16128
16786
  const rootPrompt = buildContextualPrompt(options.prompt, options.conversationHistory);
16129
16787
  let finalOutput = "";
16130
- const runBreaker = new RunBreaker();
16788
+ const runBreaker = new RunBreaker(this.config.budget?.failureThreshold);
16131
16789
  let t2Results = [];
16132
16790
  let runError = null;
16133
16791
  let identityPrompt = "";
@@ -16830,8 +17488,8 @@ var CloudClient = class _CloudClient {
16830
17488
  }
16831
17489
  };
16832
17490
  function sleep(ms, signal) {
16833
- return new Promise((resolve, reject) => {
16834
- const t = setTimeout(resolve, ms);
17491
+ return new Promise((resolve2, reject) => {
17492
+ const t = setTimeout(resolve2, ms);
16835
17493
  signal?.addEventListener("abort", () => {
16836
17494
  clearTimeout(t);
16837
17495
  reject(new Error("aborted"));
@@ -18623,8 +19281,8 @@ function Repl({ config, workspacePath, themeName, initialPrompt, identityName, a
18623
19281
  onExit: () => exit(),
18624
19282
  onThemeChange: (name) => setTheme(getTheme(name)),
18625
19283
  onExport: async (fmt) => {
18626
- const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
18627
- const exportPath = path32.join(workspacePath, `cascade-export-${stamp}.${fmt === "json" ? "json" : "md"}`);
19284
+ const stamp2 = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
19285
+ const exportPath = path32.join(workspacePath, `cascade-export-${stamp2}.${fmt === "json" ? "json" : "md"}`);
18628
19286
  if (fmt === "json") {
18629
19287
  await fs10.writeFile(exportPath, JSON.stringify({ sessionId: sessionIdRef.current, messages: state.messages }, null, 2), "utf-8");
18630
19288
  } else {
@@ -19056,8 +19714,8 @@ ${lastUser.content}`;
19056
19714
  conversationHistory: toConversationHistory(state.messages),
19057
19715
  approvalCallback: async (req) => {
19058
19716
  dispatch({ type: "SET_APPROVAL", request: req });
19059
- return new Promise((resolve) => {
19060
- approvalResolverRef.current = resolve;
19717
+ return new Promise((resolve2) => {
19718
+ approvalResolverRef.current = resolve2;
19061
19719
  });
19062
19720
  }
19063
19721
  });
@@ -20323,13 +20981,13 @@ function SetupWizard({ workspacePath, onComplete }) {
20323
20981
  }
20324
20982
  async function runSetupWizard(workspacePath) {
20325
20983
  printTelemetryBanner();
20326
- return new Promise((resolve) => {
20984
+ return new Promise((resolve2) => {
20327
20985
  const { unmount } = render(
20328
20986
  React2.createElement(SetupWizard, {
20329
20987
  workspacePath,
20330
20988
  onComplete: (config) => {
20331
20989
  unmount();
20332
- resolve(config);
20990
+ resolve2(config);
20333
20991
  }
20334
20992
  }),
20335
20993
  { exitOnCtrlC: true }
@@ -20797,7 +21455,7 @@ function notSignedIn() {
20797
21455
  process.exitCode = 1;
20798
21456
  }
20799
21457
  function promptHidden(question) {
20800
- return new Promise((resolve) => {
21458
+ return new Promise((resolve2) => {
20801
21459
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout, terminal: true });
20802
21460
  let masked = false;
20803
21461
  rl._writeToOutput = (s) => {
@@ -20806,7 +21464,7 @@ function promptHidden(question) {
20806
21464
  rl.question(question, (answer) => {
20807
21465
  rl.close();
20808
21466
  process.stdout.write("\n");
20809
- resolve(answer.trim());
21467
+ resolve2(answer.trim());
20810
21468
  });
20811
21469
  masked = true;
20812
21470
  });
@@ -21733,12 +22391,12 @@ var DashboardServer = class {
21733
22391
  console.warn("\u26A0 Dashboard auth is DISABLED while bound to a non-loopback interface \u2014 this allows unauthenticated remote task execution.");
21734
22392
  }
21735
22393
  }
21736
- await new Promise((resolve, reject) => {
22394
+ await new Promise((resolve2, reject) => {
21737
22395
  const onError = (err) => reject(err);
21738
22396
  this.httpServer.once("error", onError);
21739
22397
  this.httpServer.listen(this.port, this.host, () => {
21740
22398
  this.httpServer.off("error", onError);
21741
- resolve();
22399
+ resolve2();
21742
22400
  });
21743
22401
  });
21744
22402
  try {
@@ -21762,8 +22420,8 @@ var DashboardServer = class {
21762
22420
  } catch {
21763
22421
  }
21764
22422
  this.globalStore = null;
21765
- await new Promise((resolve, reject) => {
21766
- this.httpServer.close((err) => err ? reject(err) : resolve());
22423
+ await new Promise((resolve2, reject) => {
22424
+ this.httpServer.close((err) => err ? reject(err) : resolve2());
21767
22425
  });
21768
22426
  }
21769
22427
  getSocket() {
@@ -21960,8 +22618,8 @@ var DashboardServer = class {
21960
22618
  * timeout denies it.
21961
22619
  */
21962
22620
  makeApprovalCallback(sessionId) {
21963
- return (request) => new Promise((resolve) => {
21964
- this.pendingApprovals.set(request.id, { resolve, sessionId });
22621
+ return (request) => new Promise((resolve2) => {
22622
+ this.pendingApprovals.set(request.id, { resolve: resolve2, sessionId });
21965
22623
  });
21966
22624
  }
21967
22625
  /**