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/index.cjs CHANGED
@@ -1891,7 +1891,7 @@ function reconcilePrice(model, live) {
1891
1891
  }
1892
1892
 
1893
1893
  // src/constants.ts
1894
- var CASCADE_VERSION = "0.50.0";
1894
+ var CASCADE_VERSION = "0.51.0";
1895
1895
  var CASCADE_CONFIG_DIR = ".cascade";
1896
1896
  var CASCADE_MD_FILE = "CASCADE.md";
1897
1897
  var CASCADE_IGNORE_FILE = ".cascadeignore";
@@ -2404,46 +2404,37 @@ var BaseProvider = class {
2404
2404
  };
2405
2405
 
2406
2406
  // src/providers/model-filter.ts
2407
- var NON_CHAT_PATTERNS = [
2408
- "embed",
2407
+ var MODALITY_PATTERNS = [
2408
+ ["embedding", ["embed"]],
2409
2409
  // text-embedding-3, nomic-embed, mxbai-embed, snowflake-arctic-embed
2410
- "(?:^|[-_/])tts(?:[-_/]|$)",
2411
- "text-to-speech",
2412
- "speech",
2413
- // text-to-speech
2414
- "whisper",
2415
- "transcrib",
2416
- "(?:^|[-_/])stt(?:[-_/]|$)",
2417
- // speech-to-text
2418
- "dall[-]?e",
2419
- "imagen",
2420
- "image-generation",
2421
- "gpt-image",
2422
- "(?:^|[-_/])image(?:[-_/]|$)",
2423
- "stable-diffusion",
2424
- "sdxl",
2425
- "flux",
2426
- // image gen
2427
- "moderation",
2428
- "(?:^|[-_/])guard(?:[-_/]|$)",
2429
- // moderation / safety
2430
- "rerank",
2431
- "(?:^|[-_/])audio(?:[-_/]|$)",
2432
- "native-audio",
2433
- "realtime",
2434
- // audio / realtime
2435
- "veo",
2436
- "sora",
2437
- "(?:^|[-_/])video(?:[-_/]|$)",
2438
- // video
2439
- "(?:^|[-_/])aqa(?:[-_/]|$)",
2440
- // attributed QA
2441
- "(?:^|[-_/])live(?:[-_/]|$)",
2442
- // live-audio variants
2443
- "(?:^|[-_/])(?:davinci|babbage)(?:[-_/]|$)"
2444
- // legacy completion-only base models
2410
+ ["transcription", ["whisper", "transcrib", "(?:^|[-_/])stt(?:[-_/]|$)"]],
2411
+ ["speech", ["(?:^|[-_/])tts(?:[-_/]|$)", "text-to-speech", "speech"]],
2412
+ ["video", ["veo", "sora", "(?:^|[-_/])video(?:[-_/]|$)"]],
2413
+ ["image", [
2414
+ "dall[-]?e",
2415
+ "imagen",
2416
+ "image-generation",
2417
+ "gpt-image",
2418
+ "(?:^|[-_/])image(?:[-_/]|$)",
2419
+ "stable-diffusion",
2420
+ "sdxl",
2421
+ "flux"
2422
+ ]],
2423
+ ["moderation", ["moderation", "(?:^|[-_/])guard(?:[-_/]|$)"]],
2424
+ ["rerank", ["rerank"]],
2425
+ ["realtime", [
2426
+ "(?:^|[-_/])audio(?:[-_/]|$)",
2427
+ "native-audio",
2428
+ "realtime",
2429
+ "(?:^|[-_/])live(?:[-_/]|$)",
2430
+ // live-audio variants
2431
+ "(?:^|[-_/])aqa(?:[-_/]|$)"
2432
+ // attributed QA
2433
+ ]],
2434
+ ["legacy-completion", ["(?:^|[-_/])(?:davinci|babbage)(?:[-_/]|$)"]]
2445
2435
  ];
2446
- var NON_CHAT = new RegExp(NON_CHAT_PATTERNS.join("|"), "i");
2436
+ MODALITY_PATTERNS.map(([m, pats]) => [m, new RegExp(pats.join("|"), "i")]);
2437
+ var NON_CHAT = new RegExp(MODALITY_PATTERNS.flatMap(([, p]) => p).join("|"), "i");
2447
2438
  function isChatModel(id, methods) {
2448
2439
  if (methods && methods.length > 0) {
2449
2440
  const canGenerate = methods.some((m) => /generatecontent|generatemessage|chat|completion/i.test(m));
@@ -2971,6 +2962,115 @@ var AzureOpenAIProvider = class extends OpenAIProvider {
2971
2962
  }
2972
2963
  }
2973
2964
  };
2965
+
2966
+ // src/providers/gemini-schema.ts
2967
+ var ALLOWED_KEYS = /* @__PURE__ */ new Set([
2968
+ "type",
2969
+ "format",
2970
+ "title",
2971
+ "description",
2972
+ "nullable",
2973
+ "enum",
2974
+ "items",
2975
+ "properties",
2976
+ "required",
2977
+ "propertyOrdering",
2978
+ "minItems",
2979
+ "maxItems",
2980
+ "minProperties",
2981
+ "maxProperties",
2982
+ "minimum",
2983
+ "maximum",
2984
+ "minLength",
2985
+ "maxLength",
2986
+ "pattern",
2987
+ "example",
2988
+ "default",
2989
+ "anyOf"
2990
+ ]);
2991
+ var ALLOWED_TYPES = /* @__PURE__ */ new Set(["string", "number", "integer", "boolean", "array", "object"]);
2992
+ function isPlainObject(v) {
2993
+ return typeof v === "object" && v !== null && !Array.isArray(v);
2994
+ }
2995
+ function normalizeType(value) {
2996
+ if (typeof value === "string") {
2997
+ return ALLOWED_TYPES.has(value) ? { type: value } : {};
2998
+ }
2999
+ if (Array.isArray(value)) {
3000
+ const types = value.filter((t) => typeof t === "string");
3001
+ const nullable = types.includes("null");
3002
+ const concrete = types.find((t) => t !== "null" && ALLOWED_TYPES.has(t));
3003
+ return {
3004
+ ...concrete ? { type: concrete } : {},
3005
+ ...nullable ? { nullable: true } : {}
3006
+ };
3007
+ }
3008
+ return {};
3009
+ }
3010
+ function normalizeEnum(value) {
3011
+ if (!Array.isArray(value) || value.length === 0) return void 0;
3012
+ return value.map((v) => typeof v === "string" ? v : JSON.stringify(v));
3013
+ }
3014
+ function sanitizeGeminiSchema(schema) {
3015
+ if (!isPlainObject(schema)) return void 0;
3016
+ const out = {};
3017
+ for (const [key, value] of Object.entries(schema)) {
3018
+ if (!ALLOWED_KEYS.has(key)) continue;
3019
+ switch (key) {
3020
+ case "type": {
3021
+ Object.assign(out, normalizeType(value));
3022
+ break;
3023
+ }
3024
+ case "enum": {
3025
+ const e = normalizeEnum(value);
3026
+ if (e) out["enum"] = e;
3027
+ break;
3028
+ }
3029
+ case "properties": {
3030
+ if (!isPlainObject(value)) break;
3031
+ const props = {};
3032
+ for (const [propName, propSchema] of Object.entries(value)) {
3033
+ const cleaned = sanitizeGeminiSchema(propSchema);
3034
+ props[propName] = cleaned && Object.keys(cleaned).length ? cleaned : { type: "string" };
3035
+ }
3036
+ out["properties"] = props;
3037
+ break;
3038
+ }
3039
+ case "items": {
3040
+ const cleaned = sanitizeGeminiSchema(value);
3041
+ if (cleaned) out["items"] = cleaned;
3042
+ break;
3043
+ }
3044
+ case "anyOf": {
3045
+ if (!Array.isArray(value)) break;
3046
+ const variants = value.map((v) => sanitizeGeminiSchema(v)).filter((v) => !!v && Object.keys(v).length > 0);
3047
+ if (variants.length) out["anyOf"] = variants;
3048
+ break;
3049
+ }
3050
+ case "required": {
3051
+ if (Array.isArray(value)) {
3052
+ out["required"] = value.filter((r) => typeof r === "string");
3053
+ }
3054
+ break;
3055
+ }
3056
+ default:
3057
+ out[key] = value;
3058
+ }
3059
+ }
3060
+ if (!out["type"] && out["properties"]) out["type"] = "object";
3061
+ if (!out["type"] && out["items"]) out["type"] = "array";
3062
+ return out;
3063
+ }
3064
+ function toGeminiParameters(inputSchema) {
3065
+ const cleaned = sanitizeGeminiSchema(inputSchema);
3066
+ if (!cleaned) return void 0;
3067
+ const props = cleaned["properties"];
3068
+ if (isPlainObject(props) && Object.keys(props).length === 0) return void 0;
3069
+ if (Object.keys(cleaned).length === 0) return void 0;
3070
+ return cleaned;
3071
+ }
3072
+
3073
+ // src/providers/gemini.ts
2974
3074
  var GeminiProvider = class extends BaseProvider {
2975
3075
  client;
2976
3076
  constructor(config, model) {
@@ -3191,7 +3291,12 @@ var GeminiProvider = class extends BaseProvider {
3191
3291
  return {
3192
3292
  name: tool.name,
3193
3293
  description: tool.description,
3194
- parameters: tool.inputSchema
3294
+ // Convert, never cast. Gemini's parameters field is an OpenAPI subset
3295
+ // that 400s on unknown keys, and MCP servers ship schemas carrying their
3296
+ // own extensions (GitHub's adds `x-mcp-header` to every header-bound
3297
+ // property). Casting sent those through verbatim and every request failed
3298
+ // before the model saw it. See providers/gemini-schema.ts.
3299
+ parameters: toGeminiParameters(tool.inputSchema)
3195
3300
  };
3196
3301
  }
3197
3302
  };
@@ -3219,7 +3324,7 @@ async function nodeHttpFetch(input, init = {}, redirectCount = 0) {
3219
3324
  headers["accept-encoding"] = "gzip, deflate, br";
3220
3325
  }
3221
3326
  const body = init.body == null ? void 0 : typeof init.body === "string" ? init.body : Buffer.from(init.body);
3222
- return new Promise((resolve, reject) => {
3327
+ return new Promise((resolve2, reject) => {
3223
3328
  const req = lib.request(
3224
3329
  {
3225
3330
  hostname: u.hostname,
@@ -3236,7 +3341,7 @@ async function nodeHttpFetch(input, init = {}, redirectCount = 0) {
3236
3341
  const nextUrl = new URL(location, u).href;
3237
3342
  const downgrade = status === 303 || (status === 301 || status === 302) && method !== "GET" && method !== "HEAD";
3238
3343
  const nextInit = downgrade ? { ...init, method: "GET", body: void 0 } : init;
3239
- resolve(nodeHttpFetch(nextUrl, nextInit, redirectCount + 1));
3344
+ resolve2(nodeHttpFetch(nextUrl, nextInit, redirectCount + 1));
3240
3345
  return;
3241
3346
  }
3242
3347
  const encoding = (res.headers["content-encoding"] ?? "").toLowerCase();
@@ -3251,7 +3356,7 @@ async function nodeHttpFetch(input, init = {}, redirectCount = 0) {
3251
3356
  if (Array.isArray(v)) respHeaders.set(k, v.join(", "));
3252
3357
  else if (typeof v === "string") respHeaders.set(k, v);
3253
3358
  }
3254
- resolve(new Response(stream$1, {
3359
+ resolve2(new Response(stream$1, {
3255
3360
  status,
3256
3361
  statusText: res.statusMessage ?? "",
3257
3362
  headers: respHeaders
@@ -3896,7 +4001,7 @@ var TpmLimiter = class {
3896
4001
  }
3897
4002
  const deficit = want - bucket.available;
3898
4003
  const waitMs = Math.max(50, Math.ceil(deficit / bucket.tokensPerMinute * 6e4));
3899
- await new Promise((resolve) => setTimeout(resolve, waitMs));
4004
+ await new Promise((resolve2) => setTimeout(resolve2, waitMs));
3900
4005
  }
3901
4006
  }
3902
4007
  /**
@@ -3959,14 +4064,14 @@ var LocalRequestQueue = class {
3959
4064
  this.active++;
3960
4065
  return this.makeRelease();
3961
4066
  }
3962
- return new Promise((resolve, reject) => {
4067
+ return new Promise((resolve2, reject) => {
3963
4068
  let settled = false;
3964
4069
  let timer;
3965
4070
  const resolver = (release) => {
3966
4071
  if (settled) return;
3967
4072
  settled = true;
3968
4073
  if (timer !== void 0) clearTimeout(timer);
3969
- resolve(release);
4074
+ resolve2(release);
3970
4075
  };
3971
4076
  if (timeoutMs !== void 0 && timeoutMs > 0) {
3972
4077
  timer = setTimeout(() => {
@@ -4597,6 +4702,7 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
4597
4702
  tpmLimiter;
4598
4703
  localQueue;
4599
4704
  taskAnalyzer;
4705
+ pendingFeedbackSource;
4600
4706
  worldStateDB;
4601
4707
  privacyPaths;
4602
4708
  guidanceQueue;
@@ -5051,8 +5157,18 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
5051
5157
  return windows.length ? Math.min(...windows) : void 0;
5052
5158
  }
5053
5159
  /** Wire the Cascade Auto task analyzer used for per-subtask model routing. */
5160
+ /**
5161
+ * Feed per-model thumbs counts into Auto routing. Forwarded to the analyzer
5162
+ * because that is where the public benchmark score is combined; the prior
5163
+ * adjusts that number rather than sitting anywhere else in the pipeline.
5164
+ */
5165
+ setFeedbackSource(source) {
5166
+ this.taskAnalyzer?.setFeedbackSource(source);
5167
+ this.pendingFeedbackSource = source;
5168
+ }
5054
5169
  setTaskAnalyzer(analyzer) {
5055
5170
  this.taskAnalyzer = analyzer;
5171
+ if (this.pendingFeedbackSource) analyzer.setFeedbackSource(this.pendingFeedbackSource);
5056
5172
  }
5057
5173
  setWorldStateDB(db) {
5058
5174
  this.worldStateDB = db;
@@ -6146,8 +6262,11 @@ Now execute your subtask using this context where relevant.`
6146
6262
  const result = await this.runAgentLoop(systemPrompt, this.tools);
6147
6263
  output = result.output;
6148
6264
  toolCalls = result.toolCalls;
6149
- this.sendStatusUpdate({ progressPct: 65, currentAction: "Verifying required artifacts", status: "IN_PROGRESS" });
6150
- const artifactCheck = await this.verifyArtifacts(assignment);
6265
+ const mustProduceArtifact = this.requiresArtifact();
6266
+ if (mustProduceArtifact) {
6267
+ this.sendStatusUpdate({ progressPct: 65, currentAction: "Verifying required artifacts", status: "IN_PROGRESS" });
6268
+ }
6269
+ const artifactCheck = mustProduceArtifact ? await this.verifyArtifacts(assignment) : { ok: true, issues: [] };
6151
6270
  if (!artifactCheck.ok) {
6152
6271
  correctionAttempts = 1;
6153
6272
  issues.push(...artifactCheck.issues);
@@ -6427,11 +6546,11 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : "") + textToolSuffix,
6427
6546
  if (!wasApproved) return `Tool ${tc.name} was denied by user.`;
6428
6547
  } else {
6429
6548
  const LEGACY_APPROVAL_TIMEOUT_MS = 6e5;
6430
- const legacyDecision = await new Promise((resolve) => {
6549
+ const legacyDecision = await new Promise((resolve2) => {
6431
6550
  const eventName = `tool:approval-response:${this.id}-${tc.id}`;
6432
6551
  const timer = setTimeout(() => {
6433
6552
  this.removeAllListeners(eventName);
6434
- resolve({ approved: false });
6553
+ resolve2({ approved: false });
6435
6554
  }, LEGACY_APPROVAL_TIMEOUT_MS);
6436
6555
  timer.unref?.();
6437
6556
  this.emit("tool:approval-request", {
@@ -6444,7 +6563,7 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : "") + textToolSuffix,
6444
6563
  });
6445
6564
  this.once(eventName, (d) => {
6446
6565
  clearTimeout(timer);
6447
- resolve(d);
6566
+ resolve2(d);
6448
6567
  });
6449
6568
  });
6450
6569
  if (legacyDecision.always) this.sessionApprovals.set(tc.name, legacyDecision.approved);
@@ -6464,6 +6583,9 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : "") + textToolSuffix,
6464
6583
  tierId: this.id,
6465
6584
  sessionId: this.taskId,
6466
6585
  requireApproval: false,
6586
+ // Media generation can run for a minute; without this a cancelled run
6587
+ // still pays for an image nobody will see.
6588
+ ...this.signal ? { signal: this.signal } : {},
6467
6589
  saveSnapshot: async (path26, content) => {
6468
6590
  this.store?.addFileSnapshot(this.taskId, path26, content);
6469
6591
  },
@@ -6511,7 +6633,8 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : "") + textToolSuffix,
6511
6633
  const result = await this.toolRegistry.execute(altTool, tc.input, {
6512
6634
  tierId: this.id,
6513
6635
  sessionId: this.taskId,
6514
- requireApproval: false
6636
+ requireApproval: false,
6637
+ ...this.signal ? { signal: this.signal } : {}
6515
6638
  });
6516
6639
  const str = typeof result === "string" ? result : JSON.stringify(result);
6517
6640
  if (!str.startsWith("Tool error:") && !str.startsWith("Error:")) {
@@ -6533,7 +6656,8 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : "") + textToolSuffix,
6533
6656
  const result = await this.toolRegistry.execute(newToolName, tc.input, {
6534
6657
  tierId: this.id,
6535
6658
  sessionId: this.taskId,
6536
- requireApproval: false
6659
+ requireApproval: false,
6660
+ ...this.signal ? { signal: this.signal } : {}
6537
6661
  });
6538
6662
  const str = typeof result === "string" ? result : JSON.stringify(result);
6539
6663
  if (!str.startsWith("Tool error:")) return `[Synthesized ${newToolName}]: ${str}`;
@@ -6951,7 +7075,7 @@ var PeerBus = class extends EventEmitter__default.default {
6951
7075
  this.outputs.set(subtaskId, entry);
6952
7076
  this.emit("output:ready", entry);
6953
7077
  const waiting = this.waiters.get(subtaskId) ?? [];
6954
- for (const resolve of waiting) resolve(entry);
7078
+ for (const resolve2 of waiting) resolve2(entry);
6955
7079
  this.waiters.delete(subtaskId);
6956
7080
  }
6957
7081
  /**
@@ -6982,10 +7106,10 @@ var PeerBus = class extends EventEmitter__default.default {
6982
7106
  waitFor(subtaskId, timeoutMs = 12e4) {
6983
7107
  const existing = this.outputs.get(subtaskId);
6984
7108
  if (existing && !this.retryPending.has(subtaskId)) return Promise.resolve(existing);
6985
- return new Promise((resolve, reject) => {
7109
+ return new Promise((resolve2, reject) => {
6986
7110
  const resolver = (output) => {
6987
7111
  clearTimeout(timer);
6988
- resolve(output);
7112
+ resolve2(output);
6989
7113
  };
6990
7114
  const timer = setTimeout(() => {
6991
7115
  const waiting = this.waiters.get(subtaskId);
@@ -7039,10 +7163,10 @@ var PeerBus = class extends EventEmitter__default.default {
7039
7163
  */
7040
7164
  collect(timeoutMs) {
7041
7165
  const collected = [...this.broadcastLog];
7042
- return new Promise((resolve) => {
7166
+ return new Promise((resolve2) => {
7043
7167
  const timer = setTimeout(() => {
7044
7168
  off();
7045
- resolve(collected);
7169
+ resolve2(collected);
7046
7170
  }, timeoutMs);
7047
7171
  const handler = (msg) => {
7048
7172
  collected.push({ fromId: msg.fromId, payload: msg.payload, timestamp: msg.timestamp });
@@ -7056,7 +7180,7 @@ var PeerBus = class extends EventEmitter__default.default {
7056
7180
  const broadcasters = new Set(collected.map((m) => m.fromId));
7057
7181
  if (broadcasters.size >= this.members.size) {
7058
7182
  off();
7059
- resolve(collected);
7183
+ resolve2(collected);
7060
7184
  }
7061
7185
  };
7062
7186
  this.on("broadcast", checkComplete);
@@ -7074,7 +7198,7 @@ var PeerBus = class extends EventEmitter__default.default {
7074
7198
  return;
7075
7199
  }
7076
7200
  this.emitCoordination(tierId, `\u23F3 waiting for ${filePath} (held by ${existing.holderId})`);
7077
- return new Promise((resolve, reject) => {
7201
+ return new Promise((resolve2, reject) => {
7078
7202
  const timer = setTimeout(() => {
7079
7203
  reject(new Error(`File lock timeout for ${filePath} (held by ${existing.holderId})`));
7080
7204
  }, timeoutMs);
@@ -7082,7 +7206,7 @@ var PeerBus = class extends EventEmitter__default.default {
7082
7206
  clearTimeout(timer);
7083
7207
  this.fileLocks.set(filePath, { holderId: tierId, lockedAt: (/* @__PURE__ */ new Date()).toISOString(), waiters: [] });
7084
7208
  this.emitCoordination(tierId, `\u{1F512} locked ${filePath}`);
7085
- resolve();
7209
+ resolve2();
7086
7210
  });
7087
7211
  });
7088
7212
  }
@@ -7107,11 +7231,11 @@ var PeerBus = class extends EventEmitter__default.default {
7107
7231
  */
7108
7232
  waitForFileRelease(filePath, timeoutMs = 3e4) {
7109
7233
  if (!this.fileLocks.has(filePath)) return Promise.resolve();
7110
- return new Promise((resolve, reject) => {
7234
+ return new Promise((resolve2, reject) => {
7111
7235
  const timer = setTimeout(() => reject(new Error(`Timeout waiting for file release: ${filePath}`)), timeoutMs);
7112
7236
  this.once(`file:released:${filePath}`, () => {
7113
7237
  clearTimeout(timer);
7114
- resolve();
7238
+ resolve2();
7115
7239
  });
7116
7240
  });
7117
7241
  }
@@ -7177,13 +7301,13 @@ var PeerBus = class extends EventEmitter__default.default {
7177
7301
  this.emit(`barrier:${barrierName}`);
7178
7302
  return;
7179
7303
  }
7180
- return new Promise((resolve, reject) => {
7304
+ return new Promise((resolve2, reject) => {
7181
7305
  const timer = setTimeout(() => {
7182
7306
  reject(new Error(`Barrier timeout: ${barrierName} (${bar.arrived.size}/${bar.total} arrived)`));
7183
7307
  }, 12e4);
7184
7308
  this.once(`barrier:${barrierName}`, () => {
7185
7309
  clearTimeout(timer);
7186
- resolve();
7310
+ resolve2();
7187
7311
  });
7188
7312
  });
7189
7313
  }
@@ -8728,6 +8852,174 @@ Reply with exactly one word: YES, NO, or UNSURE.
8728
8852
  }
8729
8853
  };
8730
8854
 
8855
+ // src/core/multimodal/registry.ts
8856
+ var CAPABILITIES = [
8857
+ // ── Image ──
8858
+ {
8859
+ modality: "image",
8860
+ provider: "openai",
8861
+ modelId: "dall-e-3",
8862
+ api: "openai-images"
8863
+ },
8864
+ {
8865
+ modality: "image",
8866
+ provider: "gemini",
8867
+ modelId: "gemini-3-pro-image",
8868
+ api: "gemini-predict"
8869
+ },
8870
+ // ── Speech (text → audio) ──
8871
+ {
8872
+ modality: "speech",
8873
+ provider: "openai",
8874
+ modelId: "tts-1",
8875
+ api: "openai-speech"
8876
+ },
8877
+ {
8878
+ // The only quality claim in this table that needs no leaderboard: OpenAI
8879
+ // ships these as the same voice at two fidelities and charges double for
8880
+ // this one. That is the vendor's own ordering, not an inference.
8881
+ modality: "speech",
8882
+ provider: "openai",
8883
+ modelId: "tts-1-hd",
8884
+ api: "openai-speech",
8885
+ quality: 70,
8886
+ qualityBasis: "OpenAI's own hd variant of tts-1, priced 2x"
8887
+ },
8888
+ // ── Transcription (audio → text) ──
8889
+ {
8890
+ modality: "transcription",
8891
+ provider: "openai",
8892
+ modelId: "whisper-1",
8893
+ api: "openai-transcriptions"
8894
+ },
8895
+ // ── Video ──
8896
+ {
8897
+ // Listed so `describe()` can tell the truth about what exists, and so the
8898
+ // planner is told video is out of reach rather than left to invent a tool.
8899
+ // Veo is a long-running operation: submit, poll, then fetch bytes from a
8900
+ // signed URL. That is a different execution shape from every other entry
8901
+ // here and is not wired up — claiming otherwise would produce a tool that
8902
+ // fails at call time instead of an honest "not yet".
8903
+ modality: "video",
8904
+ provider: "gemini",
8905
+ modelId: "veo-3.1-generate-001",
8906
+ api: "gemini-predict",
8907
+ unsupported: "Veo runs as a long-running operation (submit \u2192 poll \u2192 fetch). Not wired up yet."
8908
+ }
8909
+ ];
8910
+ function capabilityCost(cap) {
8911
+ const match = findPricing(cap.modelId, cap.provider, {
8912
+ fallbackProviders: cap.provider === "gemini" ? ["vertex"] : []
8913
+ });
8914
+ const rate = match?.entry.rates?.[0];
8915
+ if (!rate) return null;
8916
+ const unitLabel = {
8917
+ per_image: "per image",
8918
+ per_second: "per second of video",
8919
+ per_character: "per character",
8920
+ per_audio_minute: "per audio minute"
8921
+ };
8922
+ const unit = unitLabel[rate.unit] ?? rate.unit;
8923
+ const amount = rate.amount;
8924
+ const shown = amount < 1e-3 ? amount.toExponential(2) : amount.toFixed(4);
8925
+ return { label: `$${shown} ${unit}`, amount, unit };
8926
+ }
8927
+ var MultimodalRegistry = class {
8928
+ /** Provider types the user has actually configured a key for. */
8929
+ available;
8930
+ constructor(availableProviders) {
8931
+ this.available = new Set(availableProviders);
8932
+ }
8933
+ /** Every capability whose provider is configured AND which Cascade can call. */
8934
+ usable(modality) {
8935
+ return CAPABILITIES.filter(
8936
+ (c) => !c.unsupported && this.available.has(c.provider) && (!modality || c.modality === modality)
8937
+ );
8938
+ }
8939
+ /** Modalities this account can actually generate right now. */
8940
+ availableModalities() {
8941
+ return [...new Set(this.usable().map((c) => c.modality))];
8942
+ }
8943
+ /**
8944
+ * Pick a model for a modality.
8945
+ *
8946
+ * Ranked models beat unranked ones, then higher quality, then lower unit
8947
+ * price. An explicit `preferModel` wins outright when it is usable — the user
8948
+ * naming a model is a stronger signal than any prior.
8949
+ */
8950
+ select(modality, opts = {}) {
8951
+ const pool = this.usable(modality);
8952
+ if (!pool.length) return null;
8953
+ if (opts.preferModel) {
8954
+ const wanted = pool.find((c) => c.modelId === opts.preferModel);
8955
+ if (wanted) {
8956
+ return { capability: wanted, cost: capabilityCost(wanted), reason: `Requested explicitly: ${wanted.modelId}.` };
8957
+ }
8958
+ }
8959
+ const scored = pool.map((c) => ({ c, cost: capabilityCost(c) })).sort((a, b) => {
8960
+ const qa = a.c.quality ?? -1;
8961
+ const qb = b.c.quality ?? -1;
8962
+ if (qa !== qb) return qb - qa;
8963
+ const pa = a.cost?.amount ?? Number.POSITIVE_INFINITY;
8964
+ const pb = b.cost?.amount ?? Number.POSITIVE_INFINITY;
8965
+ return pa - pb;
8966
+ });
8967
+ const best = scored[0];
8968
+ const ranked = best.c.quality !== void 0;
8969
+ 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.";
8970
+ return { capability: best.c, cost: best.cost, reason };
8971
+ }
8972
+ /**
8973
+ * A plain-language account of what this account can and cannot generate —
8974
+ * including the things it cannot, and why. The planner sees this, so being
8975
+ * candid here is what stops it inventing a `generate_music` call.
8976
+ */
8977
+ describe() {
8978
+ const lines = [];
8979
+ const usable = this.usable();
8980
+ if (usable.length) {
8981
+ lines.push("Generation models available on your configured providers:");
8982
+ for (const modality of ["image", "speech", "transcription", "video"]) {
8983
+ const forModality = usable.filter((c) => c.modality === modality);
8984
+ if (!forModality.length) continue;
8985
+ const shown = forModality.map((c) => {
8986
+ const cost = capabilityCost(c);
8987
+ return `${c.modelId}${cost ? ` (${cost.label})` : ""}`;
8988
+ });
8989
+ lines.push(` ${modality}: ${shown.join(", ")}`);
8990
+ }
8991
+ } else {
8992
+ lines.push("No generation models are available \u2014 none of your configured providers offers one.");
8993
+ }
8994
+ const blocked = CAPABILITIES.filter((c) => c.unsupported && this.available.has(c.provider));
8995
+ for (const c of blocked) {
8996
+ lines.push(` ${c.modality}: ${c.modelId} exists on your account but is not callable. ${c.unsupported}`);
8997
+ }
8998
+ lines.push(
8999
+ " music: no supported provider exposes a music-generation API, so Cascade cannot generate music."
9000
+ );
9001
+ return lines.join("\n");
9002
+ }
9003
+ };
9004
+
9005
+ // src/tools/base.ts
9006
+ var BaseTool = class {
9007
+ workspaceRoot = process.cwd();
9008
+ setWorkspaceRoot(root) {
9009
+ this.workspaceRoot = root;
9010
+ }
9011
+ isDangerous() {
9012
+ return false;
9013
+ }
9014
+ getDefinition() {
9015
+ return {
9016
+ name: this.name,
9017
+ description: this.description,
9018
+ inputSchema: this.inputSchema
9019
+ };
9020
+ }
9021
+ };
9022
+
8731
9023
  // src/core/router/provider-errors.ts
8732
9024
  var SYSTEMIC = /* @__PURE__ */ new Set([
8733
9025
  "rate_limit",
@@ -8792,7 +9084,7 @@ function classifyProviderError(err) {
8792
9084
  return { kind, systemic: isSystemicKind(kind), status, raw };
8793
9085
  }
8794
9086
  function describeProviderError(c, modelId) {
8795
- const on = ` on ${modelId}` ;
9087
+ const on = modelId ? ` on ${modelId}` : "";
8796
9088
  const detail = c.raw ? ` Provider said: ${c.raw}` : "";
8797
9089
  switch (c.kind) {
8798
9090
  case "rate_limit":
@@ -8814,6 +9106,295 @@ function describeProviderError(c, modelId) {
8814
9106
  }
8815
9107
  }
8816
9108
 
9109
+ // src/core/multimodal/generate.ts
9110
+ var DEFAULT_TIMEOUT_MS = 12e4;
9111
+ async function callProvider(fn, modelId) {
9112
+ try {
9113
+ return await fn();
9114
+ } catch (err) {
9115
+ const classified = classifyProviderError(err);
9116
+ throw new Error(describeProviderError(classified, modelId));
9117
+ }
9118
+ }
9119
+ async function postJson(url, body, headers, signal) {
9120
+ const ac = new AbortController();
9121
+ const timer = setTimeout(() => ac.abort(), DEFAULT_TIMEOUT_MS);
9122
+ const onAbort = () => ac.abort();
9123
+ signal?.addEventListener("abort", onAbort);
9124
+ try {
9125
+ const res = await fetch(url, {
9126
+ method: "POST",
9127
+ headers: { "Content-Type": "application/json", ...headers },
9128
+ body: JSON.stringify(body),
9129
+ signal: ac.signal
9130
+ });
9131
+ if (!res.ok) {
9132
+ const text = await res.text().catch(() => "");
9133
+ throw Object.assign(new Error(text || res.statusText), { status: res.status });
9134
+ }
9135
+ return res;
9136
+ } finally {
9137
+ clearTimeout(timer);
9138
+ signal?.removeEventListener("abort", onAbort);
9139
+ }
9140
+ }
9141
+ function baseUrlFor(cfg, fallback) {
9142
+ return (cfg.baseUrl ?? fallback).replace(/\/+$/, "");
9143
+ }
9144
+ function stamp(ext) {
9145
+ return `cascade-${Date.now()}.${ext}`;
9146
+ }
9147
+ async function generateImage(cap, cfg, req, signal) {
9148
+ return callProvider(async () => {
9149
+ if (cap.api === "openai-images") {
9150
+ const url = `${baseUrlFor(cfg, "https://api.openai.com/v1")}/images/generations`;
9151
+ const res = await postJson(url, {
9152
+ model: cap.modelId,
9153
+ prompt: req.prompt,
9154
+ size: req.size ?? "1024x1024",
9155
+ n: 1,
9156
+ // b64 rather than a URL: see the file header — provider URLs expire.
9157
+ response_format: "b64_json"
9158
+ }, { Authorization: `Bearer ${cfg.apiKey ?? ""}` }, signal);
9159
+ const body = await res.json();
9160
+ const first = body.data?.[0];
9161
+ if (!first?.b64_json) throw new Error("Image response contained no image data.");
9162
+ return {
9163
+ data: Buffer.from(first.b64_json, "base64"),
9164
+ mimeType: "image/png",
9165
+ filename: stamp("png"),
9166
+ modelId: cap.modelId,
9167
+ provider: cap.provider,
9168
+ ...first.revised_prompt ? { revisedPrompt: first.revised_prompt } : {}
9169
+ };
9170
+ }
9171
+ if (cap.api === "gemini-predict") {
9172
+ const base = baseUrlFor(cfg, "https://generativelanguage.googleapis.com/v1beta");
9173
+ const url = `${base}/models/${encodeURIComponent(cap.modelId)}:predict`;
9174
+ const res = await postJson(url, {
9175
+ instances: [{ prompt: req.prompt }],
9176
+ parameters: { sampleCount: 1 }
9177
+ }, { "x-goog-api-key": cfg.apiKey ?? "" }, signal);
9178
+ const body = await res.json();
9179
+ const first = body.predictions?.[0];
9180
+ if (!first?.bytesBase64Encoded) throw new Error("Image response contained no image data.");
9181
+ const mimeType = first.mimeType ?? "image/png";
9182
+ return {
9183
+ data: Buffer.from(first.bytesBase64Encoded, "base64"),
9184
+ mimeType,
9185
+ filename: stamp(mimeType.includes("jpeg") ? "jpg" : "png"),
9186
+ modelId: cap.modelId,
9187
+ provider: cap.provider
9188
+ };
9189
+ }
9190
+ throw new Error(`${cap.modelId} is not an image generator.`);
9191
+ }, cap.modelId);
9192
+ }
9193
+ async function generateSpeech(cap, cfg, req, signal) {
9194
+ return callProvider(async () => {
9195
+ if (cap.api !== "openai-speech") throw new Error(`${cap.modelId} is not a speech model.`);
9196
+ const format = req.format ?? "mp3";
9197
+ const url = `${baseUrlFor(cfg, "https://api.openai.com/v1")}/audio/speech`;
9198
+ const res = await postJson(url, {
9199
+ model: cap.modelId,
9200
+ input: req.text,
9201
+ voice: req.voice ?? "alloy",
9202
+ response_format: format
9203
+ }, { Authorization: `Bearer ${cfg.apiKey ?? ""}` }, signal);
9204
+ const mime = {
9205
+ mp3: "audio/mpeg",
9206
+ opus: "audio/opus",
9207
+ aac: "audio/aac",
9208
+ flac: "audio/flac",
9209
+ wav: "audio/wav"
9210
+ };
9211
+ return {
9212
+ data: Buffer.from(await res.arrayBuffer()),
9213
+ mimeType: mime[format] ?? "application/octet-stream",
9214
+ filename: stamp(format),
9215
+ modelId: cap.modelId,
9216
+ provider: cap.provider
9217
+ };
9218
+ }, cap.modelId);
9219
+ }
9220
+ async function transcribeAudio(cap, cfg, req, signal) {
9221
+ return callProvider(async () => {
9222
+ if (cap.api !== "openai-transcriptions") throw new Error(`${cap.modelId} is not a transcription model.`);
9223
+ const url = `${baseUrlFor(cfg, "https://api.openai.com/v1")}/audio/transcriptions`;
9224
+ const form = new FormData();
9225
+ form.append("model", cap.modelId);
9226
+ if (req.language) form.append("language", req.language);
9227
+ form.append("file", new Blob([new Uint8Array(req.audio)]), req.filename);
9228
+ const ac = new AbortController();
9229
+ const timer = setTimeout(() => ac.abort(), DEFAULT_TIMEOUT_MS);
9230
+ const onAbort = () => ac.abort();
9231
+ signal?.addEventListener("abort", onAbort);
9232
+ try {
9233
+ const res = await fetch(url, {
9234
+ method: "POST",
9235
+ headers: { Authorization: `Bearer ${cfg.apiKey ?? ""}` },
9236
+ body: form,
9237
+ signal: ac.signal
9238
+ });
9239
+ if (!res.ok) {
9240
+ const text = await res.text().catch(() => "");
9241
+ throw Object.assign(new Error(text || res.statusText), { status: res.status });
9242
+ }
9243
+ const body = await res.json();
9244
+ return { text: body.text ?? "", modelId: cap.modelId };
9245
+ } finally {
9246
+ clearTimeout(timer);
9247
+ signal?.removeEventListener("abort", onAbort);
9248
+ }
9249
+ }, cap.modelId);
9250
+ }
9251
+
9252
+ // src/tools/generate-media.ts
9253
+ function resolve(deps, modality, preferModel) {
9254
+ const selected = deps.registry.select(modality, preferModel ? { preferModel } : {});
9255
+ if (!selected) {
9256
+ return { ok: false, error: `No ${modality} model is available on your configured providers.` };
9257
+ }
9258
+ const cfg = deps.lookupProvider(selected.capability.provider);
9259
+ if (!cfg) {
9260
+ return {
9261
+ ok: false,
9262
+ error: `${selected.capability.modelId} needs the ${selected.capability.provider} provider, which is not configured.`
9263
+ };
9264
+ }
9265
+ return { ok: true, selected, cfg };
9266
+ }
9267
+ var GenerateImageTool = class extends BaseTool {
9268
+ constructor(deps) {
9269
+ super();
9270
+ this.deps = deps;
9271
+ }
9272
+ deps;
9273
+ name = "generate_image";
9274
+ 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.";
9275
+ inputSchema = {
9276
+ type: "object",
9277
+ properties: {
9278
+ prompt: { type: "string", description: "What the image should show. Be specific about subject, style and composition." },
9279
+ size: { type: "string", description: 'Pixel size, e.g. "1024x1024" (default), "1792x1024", "1024x1792".' },
9280
+ model: { type: "string", description: "Optional: force a specific image model instead of the automatic choice." }
9281
+ },
9282
+ required: ["prompt"]
9283
+ };
9284
+ async execute(input, options) {
9285
+ const prompt = String(input["prompt"] ?? "").trim();
9286
+ if (!prompt) return 'Provide a "prompt" describing the image.';
9287
+ const r = resolve(this.deps, "image", input["model"] ? String(input["model"]) : void 0);
9288
+ if (!r.ok) return r.error;
9289
+ const size = input["size"] ? String(input["size"]) : void 0;
9290
+ const asset = await generateImage(
9291
+ r.selected.capability,
9292
+ r.cfg,
9293
+ { prompt, ...size ? { size } : {} },
9294
+ options.signal
9295
+ );
9296
+ const location = await this.deps.sink(asset);
9297
+ return [
9298
+ `Image generated and saved to ${location}.`,
9299
+ `Model: ${asset.modelId} \u2014 ${r.selected.reason}`,
9300
+ // DALL·E rewrites the prompt before drawing; the model needs to know what
9301
+ // was actually rendered, not what it asked for, or its description of the
9302
+ // result will not match the image.
9303
+ asset.revisedPrompt ? `The provider rewrote the prompt to: ${asset.revisedPrompt}` : ""
9304
+ ].filter(Boolean).join("\n");
9305
+ }
9306
+ };
9307
+ var GenerateSpeechTool = class extends BaseTool {
9308
+ constructor(deps) {
9309
+ super();
9310
+ this.deps = deps;
9311
+ }
9312
+ deps;
9313
+ name = "generate_speech";
9314
+ 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.";
9315
+ inputSchema = {
9316
+ type: "object",
9317
+ properties: {
9318
+ text: { type: "string", description: "The text to speak." },
9319
+ voice: { type: "string", description: "Voice id: alloy, echo, fable, onyx, nova, shimmer. Default alloy." },
9320
+ format: { type: "string", description: "Audio format: mp3 (default), opus, aac, flac, wav." },
9321
+ model: { type: "string", description: "Optional: force a specific speech model." }
9322
+ },
9323
+ required: ["text"]
9324
+ };
9325
+ async execute(input, options) {
9326
+ const text = String(input["text"] ?? "").trim();
9327
+ if (!text) return 'Provide "text" to speak.';
9328
+ const r = resolve(this.deps, "speech", input["model"] ? String(input["model"]) : void 0);
9329
+ if (!r.ok) return r.error;
9330
+ const fmt = String(input["format"] ?? "mp3");
9331
+ const allowed = ["mp3", "opus", "aac", "flac", "wav"];
9332
+ const format = allowed.includes(fmt) ? fmt : "mp3";
9333
+ const asset = await generateSpeech(
9334
+ r.selected.capability,
9335
+ r.cfg,
9336
+ { text, voice: input["voice"] ? String(input["voice"]) : void 0, format },
9337
+ options.signal
9338
+ );
9339
+ const location = await this.deps.sink(asset);
9340
+ return `Speech generated and saved to ${location}.
9341
+ Model: ${asset.modelId} \u2014 ${r.selected.reason}`;
9342
+ }
9343
+ };
9344
+ var TranscribeAudioTool = class extends BaseTool {
9345
+ constructor(deps, readFile) {
9346
+ super();
9347
+ this.deps = deps;
9348
+ this.readFile = readFile;
9349
+ }
9350
+ deps;
9351
+ readFile;
9352
+ name = "transcribe_audio";
9353
+ 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.";
9354
+ inputSchema = {
9355
+ type: "object",
9356
+ properties: {
9357
+ path: { type: "string", description: "Path to the audio file to transcribe." },
9358
+ language: { type: "string", description: 'Optional ISO-639-1 language hint, e.g. "en", improves accuracy.' },
9359
+ model: { type: "string", description: "Optional: force a specific transcription model." }
9360
+ },
9361
+ required: ["path"]
9362
+ };
9363
+ async execute(input, options) {
9364
+ const filePath = String(input["path"] ?? "").trim();
9365
+ if (!filePath) return 'Provide a "path" to the audio file.';
9366
+ const r = resolve(this.deps, "transcription", input["model"] ? String(input["model"]) : void 0);
9367
+ if (!r.ok) return r.error;
9368
+ let audio;
9369
+ try {
9370
+ audio = await this.readFile(filePath);
9371
+ } catch {
9372
+ return `Could not read the audio file at ${filePath}.`;
9373
+ }
9374
+ const { text, modelId } = await transcribeAudio(
9375
+ r.selected.capability,
9376
+ r.cfg,
9377
+ {
9378
+ audio,
9379
+ filename: filePath.split(/[\\/]/).pop() || "audio",
9380
+ ...input["language"] ? { language: String(input["language"]) } : {}
9381
+ },
9382
+ options.signal
9383
+ );
9384
+ return text ? `Transcript (${modelId}):
9385
+
9386
+ ${text}` : `${modelId} returned an empty transcript \u2014 the file may be silent or in an unsupported format.`;
9387
+ }
9388
+ };
9389
+ function buildMediaTools(deps, readFile) {
9390
+ const tools = [];
9391
+ const modalities = new Set(deps.registry.availableModalities());
9392
+ if (modalities.has("image")) tools.push(new GenerateImageTool(deps));
9393
+ if (modalities.has("speech")) tools.push(new GenerateSpeechTool(deps));
9394
+ if (modalities.has("transcription") && readFile) tools.push(new TranscribeAudioTool(deps, readFile));
9395
+ return tools;
9396
+ }
9397
+
8817
9398
  // src/core/run-breaker.ts
8818
9399
  var DEFAULT_FAILURE_THRESHOLD = 3;
8819
9400
  var RunBreaker = class {
@@ -8879,26 +9460,6 @@ var RunBreaker = class {
8879
9460
  return out;
8880
9461
  }
8881
9462
  };
8882
-
8883
- // src/tools/base.ts
8884
- var BaseTool = class {
8885
- workspaceRoot = process.cwd();
8886
- setWorkspaceRoot(root) {
8887
- this.workspaceRoot = root;
8888
- }
8889
- isDangerous() {
8890
- return false;
8891
- }
8892
- getDefinition() {
8893
- return {
8894
- name: this.name,
8895
- description: this.description,
8896
- inputSchema: this.inputSchema
8897
- };
8898
- }
8899
- };
8900
-
8901
- // src/tools/shell.ts
8902
9463
  var execAsync = util.promisify(child_process.exec);
8903
9464
  var ShellTool = class extends BaseTool {
8904
9465
  name = "shell";
@@ -9594,7 +10155,7 @@ var PDFCreateTool = class extends BaseTool {
9594
10155
  if (!fs23__default.default.existsSync(dir)) {
9595
10156
  fs23__default.default.mkdirSync(dir, { recursive: true });
9596
10157
  }
9597
- return new Promise((resolve, reject) => {
10158
+ return new Promise((resolve2, reject) => {
9598
10159
  try {
9599
10160
  const doc = new PDFDocument__default.default({ margin: 50 });
9600
10161
  const stream = fs23__default.default.createWriteStream(filePath);
@@ -9612,7 +10173,7 @@ var PDFCreateTool = class extends BaseTool {
9612
10173
  });
9613
10174
  doc.end();
9614
10175
  stream.on("finish", () => {
9615
- resolve(`Successfully created PDF at ${filePath} (${content.length} characters)`);
10176
+ resolve2(`Successfully created PDF at ${filePath} (${content.length} characters)`);
9616
10177
  });
9617
10178
  stream.on("error", (err) => {
9618
10179
  reject(new Error(`Failed to write PDF: ${err.message}`));
@@ -9684,7 +10245,7 @@ var CodeInterpreterTool = class extends BaseTool {
9684
10245
  const filePath = path24__default.default.join(tmpDir, fileName);
9685
10246
  fs23__default.default.writeFileSync(filePath, code, "utf-8");
9686
10247
  const execArgs = [filePath, ...args];
9687
- return new Promise((resolve) => {
10248
+ return new Promise((resolve2) => {
9688
10249
  const startMs = Date.now();
9689
10250
  child_process.execFile(cmdPrefix, execArgs, { cwd: this.workspaceRoot, timeout: 3e4 }, (error, stdout, stderr) => {
9690
10251
  const duration = Date.now() - startMs;
@@ -9698,17 +10259,17 @@ var CodeInterpreterTool = class extends BaseTool {
9698
10259
  if (error) {
9699
10260
  const timedOut = error.killed && duration >= 3e4;
9700
10261
  if (timedOut) {
9701
- resolve(`Execution timed out after 30s. Consider breaking the task into smaller pieces.
10262
+ resolve2(`Execution timed out after 30s. Consider breaking the task into smaller pieces.
9702
10263
  Partial stdout: ${stdout}
9703
10264
  Stderr: ${stderr}`);
9704
10265
  } else {
9705
- resolve(`Execution failed (${duration}ms):
10266
+ resolve2(`Execution failed (${duration}ms):
9706
10267
  Error: ${error.message}
9707
10268
  Stderr: ${stderr}
9708
10269
  Stdout: ${stdout}`);
9709
10270
  }
9710
10271
  } else {
9711
- resolve(`Execution successful (${duration}ms):
10272
+ resolve2(`Execution successful (${duration}ms):
9712
10273
  Stdout: ${stdout}
9713
10274
  Stderr: ${stderr}`);
9714
10275
  }
@@ -10362,7 +10923,7 @@ var ToolRegistry = class extends EventEmitter__default.default {
10362
10923
  */
10363
10924
  waitForTool(toolName, timeoutMs = 6e4) {
10364
10925
  if (this.tools.has(toolName)) return Promise.resolve();
10365
- return new Promise((resolve, reject) => {
10926
+ return new Promise((resolve2, reject) => {
10366
10927
  const timer = setTimeout(() => {
10367
10928
  this.off("tool:added", handler);
10368
10929
  reject(new Error(`Timeout waiting for tool: ${toolName}`));
@@ -10371,7 +10932,7 @@ var ToolRegistry = class extends EventEmitter__default.default {
10371
10932
  if (name === toolName) {
10372
10933
  clearTimeout(timer);
10373
10934
  this.off("tool:added", handler);
10374
- resolve();
10935
+ resolve2();
10375
10936
  }
10376
10937
  };
10377
10938
  this.on("tool:added", handler);
@@ -11046,20 +11607,20 @@ var PermissionEscalator = class extends EventEmitter__default.default {
11046
11607
  resolver(decision);
11047
11608
  }
11048
11609
  waitForUserDecision(req) {
11049
- return new Promise((resolve) => {
11610
+ return new Promise((resolve2) => {
11050
11611
  let timer;
11051
11612
  const wrappedResolver = (decision) => {
11052
11613
  if (timer) clearTimeout(timer);
11053
11614
  if (decision.always) {
11054
11615
  this.taskWideCache.set(req.toolName, decision.approved);
11055
11616
  }
11056
- resolve(decision);
11617
+ resolve2(decision);
11057
11618
  };
11058
11619
  this.pendingUserDecisions.set(req.id, wrappedResolver);
11059
11620
  if (this.approvalTimeoutMs > 0 && Number.isFinite(this.approvalTimeoutMs)) {
11060
11621
  timer = setTimeout(() => {
11061
11622
  if (this.pendingUserDecisions.delete(req.id)) {
11062
- resolve({
11623
+ resolve2({
11063
11624
  requestId: req.id,
11064
11625
  approved: false,
11065
11626
  decidedBy: "USER",
@@ -11226,6 +11787,14 @@ var BudgetConfigSchema = zod.z.object({
11226
11787
  maxTokensPerRun: zod.z.number().int().positive().default(2e5),
11227
11788
  /** Optional hard per-task cost ceiling (USD). Unset = only the token cap applies. */
11228
11789
  maxCostPerRunUsd: zod.z.number().positive().optional(),
11790
+ /**
11791
+ * Consecutive systemic failures against ONE model before the run stops
11792
+ * launching work that would fail the same way (dead key, wrong model id,
11793
+ * exhausted quota). Lower reacts sooner and wastes less; higher absorbs a
11794
+ * longer transient before giving up. Only failures that will certainly repeat
11795
+ * are counted — see core/run-breaker.ts.
11796
+ */
11797
+ failureThreshold: zod.z.number().int().min(1).max(20).default(3),
11229
11798
  warnAtPct: zod.z.number().default(80)
11230
11799
  });
11231
11800
  var WorkspaceConfigSchema = zod.z.object({
@@ -11513,6 +12082,23 @@ var noopTelemetry = {
11513
12082
  }
11514
12083
  };
11515
12084
 
12085
+ // src/core/router/feedback-prior.ts
12086
+ var MAX_FEEDBACK_ADJUSTMENT = 0.05;
12087
+ var PRIOR_STRENGTH = 10;
12088
+ function feedbackAdjustment(counts) {
12089
+ const good = Math.max(0, Math.floor(counts.good));
12090
+ const bad = Math.max(0, Math.floor(counts.bad));
12091
+ const n = good + bad;
12092
+ if (n === 0) return 0;
12093
+ const half = PRIOR_STRENGTH / 2;
12094
+ const shrunk = (good + half) / (n + PRIOR_STRENGTH);
12095
+ return (shrunk - 0.5) * 2 * MAX_FEEDBACK_ADJUSTMENT;
12096
+ }
12097
+ function applyFeedback(score01, counts) {
12098
+ const adjusted = score01 + feedbackAdjustment(counts);
12099
+ return Math.max(0, Math.min(1, adjusted));
12100
+ }
12101
+
11516
12102
  // src/core/router/task-analyzer.ts
11517
12103
  var CODE_SIGNALS = [
11518
12104
  /\b(?:function|class|interface|async|await|import|export|const|let|var|def|return|if|else|for|while|try|catch)\b/,
@@ -11587,6 +12173,8 @@ var TASK_TYPE_TAGS = {
11587
12173
  };
11588
12174
  var TaskAnalyzer = class {
11589
12175
  tracker;
12176
+ /** Per-model thumbs counts, when the host collects them. */
12177
+ feedback;
11590
12178
  bias;
11591
12179
  lastProfile = null;
11592
12180
  lastSelectedModels = /* @__PURE__ */ new Map();
@@ -11594,6 +12182,14 @@ var TaskAnalyzer = class {
11594
12182
  this.tracker = tracker;
11595
12183
  this.bias = bias;
11596
12184
  }
12185
+ /**
12186
+ * Supply per-model rating counts. Optional by design: a host that collects no
12187
+ * feedback (the CLI, a fresh install) scores exactly as it did before, since
12188
+ * feedbackAdjustment returns 0 for an empty record.
12189
+ */
12190
+ setFeedbackSource(source) {
12191
+ this.feedback = source;
12192
+ }
11597
12193
  setTracker(tracker) {
11598
12194
  this.tracker = tracker;
11599
12195
  }
@@ -11677,7 +12273,10 @@ var TaskAnalyzer = class {
11677
12273
  const perf = this.tracker?.performanceScore(model.id, profile.type) ?? 0.5;
11678
12274
  const costEff = this.costEfficiency(model, profile.complexity);
11679
12275
  const match = this.taskMatchScore(model, profile);
11680
- const benchmark = 0.3 + 0.7 * benchmarkScore01(model, profile.type);
12276
+ const rated = this.feedback?.(model.id);
12277
+ const publicScore = benchmarkScore01(model, profile.type);
12278
+ const adjusted = rated ? applyFeedback(publicScore, rated) : publicScore;
12279
+ const benchmark = 0.3 + 0.7 * adjusted;
11681
12280
  switch (this.bias) {
11682
12281
  case "quality":
11683
12282
  return perf * match * benchmark ** 2 * (0.85 + 0.15 * costEff);
@@ -12079,10 +12678,10 @@ var DynamicTool = class extends BaseTool {
12079
12678
  `;
12080
12679
  const script = await isolate.compileScript(bootstrap);
12081
12680
  const runPromise = script.run(context, { timeout: timeoutMs, promise: true }).then((v) => String(v ?? ""));
12082
- const timeoutPromise = new Promise((resolve) => {
12681
+ const timeoutPromise = new Promise((resolve2) => {
12083
12682
  const t = setTimeout(() => {
12084
12683
  dispose();
12085
- resolve(`Dynamic tool "${this.name}" timed out after ${timeoutMs}ms and was terminated.`);
12684
+ resolve2(`Dynamic tool "${this.name}" timed out after ${timeoutMs}ms and was terminated.`);
12086
12685
  }, timeoutMs + 500);
12087
12686
  t.unref?.();
12088
12687
  });
@@ -12100,7 +12699,7 @@ var DynamicTool = class extends BaseTool {
12100
12699
  /** Spawn the worker, service its callTool/fetch bridge, enforce the kill timeout. */
12101
12700
  runInWorker(input, callTool) {
12102
12701
  const timeoutMs = Math.max(200, Number(process.env["CASCADE_DYNAMIC_TOOL_TIMEOUT_MS"]) || DYNAMIC_TOOL_TIMEOUT_MS);
12103
- return new Promise((resolve) => {
12702
+ return new Promise((resolve2) => {
12104
12703
  let settled = false;
12105
12704
  const worker = new worker_threads.Worker(HARNESS_SRC, {
12106
12705
  eval: true,
@@ -12112,7 +12711,7 @@ var DynamicTool = class extends BaseTool {
12112
12711
  settled = true;
12113
12712
  clearTimeout(timer);
12114
12713
  void worker.terminate();
12115
- resolve(value);
12714
+ resolve2(value);
12116
12715
  };
12117
12716
  const timer = setTimeout(
12118
12717
  () => finish(`Dynamic tool "${this.name}" timed out after ${timeoutMs}ms and was terminated.`),
@@ -13658,6 +14257,7 @@ ${prompt}`;
13658
14257
  var Cascade = class _Cascade extends EventEmitter__default.default {
13659
14258
  router;
13660
14259
  toolRegistry;
14260
+ multimodal;
13661
14261
  mcpClient;
13662
14262
  codeIndex;
13663
14263
  config;
@@ -13696,6 +14296,10 @@ var Cascade = class _Cascade extends EventEmitter__default.default {
13696
14296
  }
13697
14297
  });
13698
14298
  this.toolRegistry = new ToolRegistry(this.config.tools, workspacePath);
14299
+ this.multimodal = new MultimodalRegistry(
14300
+ (this.config.providers ?? []).map((p) => p.type)
14301
+ );
14302
+ this.registerMediaTools(workspacePath);
13699
14303
  this.telemetry = config.telemetry?.enabled ? new Telemetry(config.telemetry, config.telemetry.distinctId ?? "anonymous") : noopTelemetry;
13700
14304
  this.worldStateDB = new WorldStateDB(this.workspacePath, this.config.workspace?.debugWorldState ?? false);
13701
14305
  this.router.setWorldStateDB(this.worldStateDB);
@@ -13749,14 +14353,14 @@ var Cascade = class _Cascade extends EventEmitter__default.default {
13749
14353
  if (this.listenerCount("mcp:approval-required") === 0) {
13750
14354
  return false;
13751
14355
  }
13752
- return await new Promise((resolve) => {
13753
- this.pendingMcpApprovals.set(server.name, resolve);
14356
+ return await new Promise((resolve2) => {
14357
+ this.pendingMcpApprovals.set(server.name, resolve2);
13754
14358
  const timeout = setTimeout(() => {
13755
- if (this.pendingMcpApprovals.delete(server.name)) resolve(false);
14359
+ if (this.pendingMcpApprovals.delete(server.name)) resolve2(false);
13756
14360
  }, 3e4);
13757
14361
  const wrap = (approved) => {
13758
14362
  clearTimeout(timeout);
13759
- resolve(approved);
14363
+ resolve2(approved);
13760
14364
  };
13761
14365
  this.pendingMcpApprovals.set(server.name, wrap);
13762
14366
  this.emit("mcp:approval-required", { server });
@@ -13830,17 +14434,17 @@ var Cascade = class _Cascade extends EventEmitter__default.default {
13830
14434
  }
13831
14435
  const t2Count = plan.sections.length;
13832
14436
  const t3Count = plan.sections.reduce((sum, s) => sum + (s.t3Subtasks?.length ?? 0), 0);
13833
- return await new Promise((resolve) => {
14437
+ return await new Promise((resolve2) => {
13834
14438
  const timeout = setTimeout(() => {
13835
14439
  if (this.pendingPlanApproval) {
13836
14440
  this.pendingPlanApproval = void 0;
13837
- resolve({ approved: true });
14441
+ resolve2({ approved: true });
13838
14442
  }
13839
14443
  }, 12e4);
13840
14444
  this.pendingPlanApproval = (decision) => {
13841
14445
  clearTimeout(timeout);
13842
14446
  this.pendingPlanApproval = void 0;
13843
- resolve(decision);
14447
+ resolve2(decision);
13844
14448
  };
13845
14449
  this.emit("plan:approval-required", {
13846
14450
  taskId,
@@ -13873,17 +14477,17 @@ var Cascade = class _Cascade extends EventEmitter__default.default {
13873
14477
  }
13874
14478
  async requestContextApproval(info) {
13875
14479
  if (this.listenerCount("context:approval-required") === 0) return true;
13876
- return await new Promise((resolve) => {
14480
+ return await new Promise((resolve2) => {
13877
14481
  const timeout = setTimeout(() => {
13878
14482
  if (this.pendingContextApproval) {
13879
14483
  this.pendingContextApproval = void 0;
13880
- resolve(true);
14484
+ resolve2(true);
13881
14485
  }
13882
14486
  }, 12e4);
13883
14487
  this.pendingContextApproval = (approved) => {
13884
14488
  clearTimeout(timeout);
13885
14489
  this.pendingContextApproval = void 0;
13886
- resolve(approved);
14490
+ resolve2(approved);
13887
14491
  };
13888
14492
  this.emit("context:approval-required", info);
13889
14493
  });
@@ -13963,6 +14567,55 @@ ${input}` }],
13963
14567
  if (this.store) t1.setStore(this.store);
13964
14568
  return t1.previewPlan(prompt);
13965
14569
  }
14570
+ /**
14571
+ * Register image / speech / transcription tools for the modalities this
14572
+ * config can actually serve.
14573
+ *
14574
+ * The sink writes bytes into the workspace, because that is what a desktop or
14575
+ * CLI run wants and it keeps a generated asset indistinguishable from any
14576
+ * other file the run produced. A host with different storage (the cloud
14577
+ * server stores a file row) constructs the tools itself via buildMediaTools
14578
+ * with its own sink.
14579
+ */
14580
+ registerMediaTools(workspacePath) {
14581
+ const allowlist = this.config.tools?.enabledTools;
14582
+ const permitted = (name) => !allowlist || allowlist.includes(name);
14583
+ const tools = buildMediaTools(
14584
+ {
14585
+ registry: this.multimodal,
14586
+ lookupProvider: (provider) => (this.config.providers ?? []).find((p) => p.type === provider),
14587
+ sink: async (asset) => {
14588
+ const fsp = await import('fs/promises');
14589
+ const nodePath = await import('path');
14590
+ const dir = nodePath.join(workspacePath, "generated");
14591
+ await fsp.mkdir(dir, { recursive: true });
14592
+ const dest = nodePath.join(dir, asset.filename);
14593
+ await fsp.writeFile(dest, asset.data);
14594
+ return nodePath.relative(workspacePath, dest);
14595
+ }
14596
+ },
14597
+ async (p) => {
14598
+ const fsp = await import('fs/promises');
14599
+ return fsp.readFile(p);
14600
+ }
14601
+ );
14602
+ for (const tool of tools) {
14603
+ if (!permitted(tool.name)) continue;
14604
+ tool.setWorkspaceRoot(workspacePath);
14605
+ this.toolRegistry.register(tool);
14606
+ }
14607
+ }
14608
+ /**
14609
+ * Supply per-model thumbs-up/down counts so Auto routing can weigh how models
14610
+ * have actually performed for this user, on top of public benchmarks.
14611
+ *
14612
+ * Optional everywhere: with no source, or an empty record, scoring is
14613
+ * byte-identical to before. The adjustment is capped and sample-size shrunk —
14614
+ * see core/router/feedback-prior.ts.
14615
+ */
14616
+ setFeedbackSource(source) {
14617
+ this.router.setFeedbackSource(source);
14618
+ }
13966
14619
  /** True when a task stopped at the budget cap and can be resumed via /continue. */
13967
14620
  hasResumableRun() {
13968
14621
  return this.lastInterruptedRun != null;
@@ -14452,7 +15105,7 @@ ${prompt}` : prompt;
14452
15105
  if (toolCreator) toolCreator.setPermissionEscalator(escalator);
14453
15106
  const rootPrompt = buildContextualPrompt(options.prompt, options.conversationHistory);
14454
15107
  let finalOutput = "";
14455
- const runBreaker = new RunBreaker();
15108
+ const runBreaker = new RunBreaker(this.config.budget?.failureThreshold);
14456
15109
  let t2Results = [];
14457
15110
  let runError = null;
14458
15111
  let identityPrompt = "";
@@ -16625,12 +17278,12 @@ var DashboardServer = class {
16625
17278
  console.warn("\u26A0 Dashboard auth is DISABLED while bound to a non-loopback interface \u2014 this allows unauthenticated remote task execution.");
16626
17279
  }
16627
17280
  }
16628
- await new Promise((resolve, reject) => {
17281
+ await new Promise((resolve2, reject) => {
16629
17282
  const onError = (err) => reject(err);
16630
17283
  this.httpServer.once("error", onError);
16631
17284
  this.httpServer.listen(this.port, this.host, () => {
16632
17285
  this.httpServer.off("error", onError);
16633
- resolve();
17286
+ resolve2();
16634
17287
  });
16635
17288
  });
16636
17289
  try {
@@ -16654,8 +17307,8 @@ var DashboardServer = class {
16654
17307
  } catch {
16655
17308
  }
16656
17309
  this.globalStore = null;
16657
- await new Promise((resolve, reject) => {
16658
- this.httpServer.close((err) => err ? reject(err) : resolve());
17310
+ await new Promise((resolve2, reject) => {
17311
+ this.httpServer.close((err) => err ? reject(err) : resolve2());
16659
17312
  });
16660
17313
  }
16661
17314
  getSocket() {
@@ -16852,8 +17505,8 @@ var DashboardServer = class {
16852
17505
  * timeout denies it.
16853
17506
  */
16854
17507
  makeApprovalCallback(sessionId) {
16855
- return (request) => new Promise((resolve) => {
16856
- this.pendingApprovals.set(request.id, { resolve, sessionId });
17508
+ return (request) => new Promise((resolve2) => {
17509
+ this.pendingApprovals.set(request.id, { resolve: resolve2, sessionId });
16857
17510
  });
16858
17511
  }
16859
17512
  /**
@@ -18165,8 +18818,8 @@ var CloudClient = class _CloudClient {
18165
18818
  }
18166
18819
  };
18167
18820
  function sleep(ms, signal) {
18168
- return new Promise((resolve, reject) => {
18169
- const t = setTimeout(resolve, ms);
18821
+ return new Promise((resolve2, reject) => {
18822
+ const t = setTimeout(resolve2, ms);
18170
18823
  signal?.addEventListener("abort", () => {
18171
18824
  clearTimeout(t);
18172
18825
  reject(new Error("aborted"));