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.cjs CHANGED
@@ -1793,7 +1793,7 @@ var CASCADE_VERSION, CASCADE_CONFIG_FILE, CASCADE_DB_FILE, CASCADE_DASHBOARD_SEC
1793
1793
  var init_constants = __esm({
1794
1794
  "src/constants.ts"() {
1795
1795
  init_pricing();
1796
- CASCADE_VERSION = "0.50.0";
1796
+ CASCADE_VERSION = "0.51.0";
1797
1797
  CASCADE_CONFIG_FILE = ".cascade/config.json";
1798
1798
  CASCADE_DB_FILE = ".cascade/memory.db";
1799
1799
  CASCADE_DASHBOARD_SECRET_FILE = ".cascade/dashboard-secret";
@@ -2311,49 +2311,40 @@ function isChatModel(id, methods) {
2311
2311
  }
2312
2312
  return !NON_CHAT.test(id);
2313
2313
  }
2314
- var NON_CHAT_PATTERNS, NON_CHAT;
2314
+ var MODALITY_PATTERNS, NON_CHAT;
2315
2315
  var init_model_filter = __esm({
2316
2316
  "src/providers/model-filter.ts"() {
2317
- NON_CHAT_PATTERNS = [
2318
- "embed",
2317
+ MODALITY_PATTERNS = [
2318
+ ["embedding", ["embed"]],
2319
2319
  // text-embedding-3, nomic-embed, mxbai-embed, snowflake-arctic-embed
2320
- "(?:^|[-_/])tts(?:[-_/]|$)",
2321
- "text-to-speech",
2322
- "speech",
2323
- // text-to-speech
2324
- "whisper",
2325
- "transcrib",
2326
- "(?:^|[-_/])stt(?:[-_/]|$)",
2327
- // speech-to-text
2328
- "dall[-]?e",
2329
- "imagen",
2330
- "image-generation",
2331
- "gpt-image",
2332
- "(?:^|[-_/])image(?:[-_/]|$)",
2333
- "stable-diffusion",
2334
- "sdxl",
2335
- "flux",
2336
- // image gen
2337
- "moderation",
2338
- "(?:^|[-_/])guard(?:[-_/]|$)",
2339
- // moderation / safety
2340
- "rerank",
2341
- "(?:^|[-_/])audio(?:[-_/]|$)",
2342
- "native-audio",
2343
- "realtime",
2344
- // audio / realtime
2345
- "veo",
2346
- "sora",
2347
- "(?:^|[-_/])video(?:[-_/]|$)",
2348
- // video
2349
- "(?:^|[-_/])aqa(?:[-_/]|$)",
2350
- // attributed QA
2351
- "(?:^|[-_/])live(?:[-_/]|$)",
2352
- // live-audio variants
2353
- "(?:^|[-_/])(?:davinci|babbage)(?:[-_/]|$)"
2354
- // legacy completion-only base models
2320
+ ["transcription", ["whisper", "transcrib", "(?:^|[-_/])stt(?:[-_/]|$)"]],
2321
+ ["speech", ["(?:^|[-_/])tts(?:[-_/]|$)", "text-to-speech", "speech"]],
2322
+ ["video", ["veo", "sora", "(?:^|[-_/])video(?:[-_/]|$)"]],
2323
+ ["image", [
2324
+ "dall[-]?e",
2325
+ "imagen",
2326
+ "image-generation",
2327
+ "gpt-image",
2328
+ "(?:^|[-_/])image(?:[-_/]|$)",
2329
+ "stable-diffusion",
2330
+ "sdxl",
2331
+ "flux"
2332
+ ]],
2333
+ ["moderation", ["moderation", "(?:^|[-_/])guard(?:[-_/]|$)"]],
2334
+ ["rerank", ["rerank"]],
2335
+ ["realtime", [
2336
+ "(?:^|[-_/])audio(?:[-_/]|$)",
2337
+ "native-audio",
2338
+ "realtime",
2339
+ "(?:^|[-_/])live(?:[-_/]|$)",
2340
+ // live-audio variants
2341
+ "(?:^|[-_/])aqa(?:[-_/]|$)"
2342
+ // attributed QA
2343
+ ]],
2344
+ ["legacy-completion", ["(?:^|[-_/])(?:davinci|babbage)(?:[-_/]|$)"]]
2355
2345
  ];
2356
- NON_CHAT = new RegExp(NON_CHAT_PATTERNS.join("|"), "i");
2346
+ MODALITY_PATTERNS.map(([m, pats]) => [m, new RegExp(pats.join("|"), "i")]);
2347
+ NON_CHAT = new RegExp(MODALITY_PATTERNS.flatMap(([, p]) => p).join("|"), "i");
2357
2348
  }
2358
2349
  });
2359
2350
 
@@ -2921,6 +2912,118 @@ var init_azure = __esm({
2921
2912
  }
2922
2913
  });
2923
2914
 
2915
+ // src/providers/gemini-schema.ts
2916
+ function isPlainObject(v) {
2917
+ return typeof v === "object" && v !== null && !Array.isArray(v);
2918
+ }
2919
+ function normalizeType(value) {
2920
+ if (typeof value === "string") {
2921
+ return ALLOWED_TYPES.has(value) ? { type: value } : {};
2922
+ }
2923
+ if (Array.isArray(value)) {
2924
+ const types = value.filter((t) => typeof t === "string");
2925
+ const nullable = types.includes("null");
2926
+ const concrete = types.find((t) => t !== "null" && ALLOWED_TYPES.has(t));
2927
+ return {
2928
+ ...concrete ? { type: concrete } : {},
2929
+ ...nullable ? { nullable: true } : {}
2930
+ };
2931
+ }
2932
+ return {};
2933
+ }
2934
+ function normalizeEnum(value) {
2935
+ if (!Array.isArray(value) || value.length === 0) return void 0;
2936
+ return value.map((v) => typeof v === "string" ? v : JSON.stringify(v));
2937
+ }
2938
+ function sanitizeGeminiSchema(schema) {
2939
+ if (!isPlainObject(schema)) return void 0;
2940
+ const out = {};
2941
+ for (const [key, value] of Object.entries(schema)) {
2942
+ if (!ALLOWED_KEYS.has(key)) continue;
2943
+ switch (key) {
2944
+ case "type": {
2945
+ Object.assign(out, normalizeType(value));
2946
+ break;
2947
+ }
2948
+ case "enum": {
2949
+ const e = normalizeEnum(value);
2950
+ if (e) out["enum"] = e;
2951
+ break;
2952
+ }
2953
+ case "properties": {
2954
+ if (!isPlainObject(value)) break;
2955
+ const props = {};
2956
+ for (const [propName, propSchema] of Object.entries(value)) {
2957
+ const cleaned = sanitizeGeminiSchema(propSchema);
2958
+ props[propName] = cleaned && Object.keys(cleaned).length ? cleaned : { type: "string" };
2959
+ }
2960
+ out["properties"] = props;
2961
+ break;
2962
+ }
2963
+ case "items": {
2964
+ const cleaned = sanitizeGeminiSchema(value);
2965
+ if (cleaned) out["items"] = cleaned;
2966
+ break;
2967
+ }
2968
+ case "anyOf": {
2969
+ if (!Array.isArray(value)) break;
2970
+ const variants = value.map((v) => sanitizeGeminiSchema(v)).filter((v) => !!v && Object.keys(v).length > 0);
2971
+ if (variants.length) out["anyOf"] = variants;
2972
+ break;
2973
+ }
2974
+ case "required": {
2975
+ if (Array.isArray(value)) {
2976
+ out["required"] = value.filter((r) => typeof r === "string");
2977
+ }
2978
+ break;
2979
+ }
2980
+ default:
2981
+ out[key] = value;
2982
+ }
2983
+ }
2984
+ if (!out["type"] && out["properties"]) out["type"] = "object";
2985
+ if (!out["type"] && out["items"]) out["type"] = "array";
2986
+ return out;
2987
+ }
2988
+ function toGeminiParameters(inputSchema) {
2989
+ const cleaned = sanitizeGeminiSchema(inputSchema);
2990
+ if (!cleaned) return void 0;
2991
+ const props = cleaned["properties"];
2992
+ if (isPlainObject(props) && Object.keys(props).length === 0) return void 0;
2993
+ if (Object.keys(cleaned).length === 0) return void 0;
2994
+ return cleaned;
2995
+ }
2996
+ var ALLOWED_KEYS, ALLOWED_TYPES;
2997
+ var init_gemini_schema = __esm({
2998
+ "src/providers/gemini-schema.ts"() {
2999
+ ALLOWED_KEYS = /* @__PURE__ */ new Set([
3000
+ "type",
3001
+ "format",
3002
+ "title",
3003
+ "description",
3004
+ "nullable",
3005
+ "enum",
3006
+ "items",
3007
+ "properties",
3008
+ "required",
3009
+ "propertyOrdering",
3010
+ "minItems",
3011
+ "maxItems",
3012
+ "minProperties",
3013
+ "maxProperties",
3014
+ "minimum",
3015
+ "maximum",
3016
+ "minLength",
3017
+ "maxLength",
3018
+ "pattern",
3019
+ "example",
3020
+ "default",
3021
+ "anyOf"
3022
+ ]);
3023
+ ALLOWED_TYPES = /* @__PURE__ */ new Set(["string", "number", "integer", "boolean", "array", "object"]);
3024
+ }
3025
+ });
3026
+
2924
3027
  // src/providers/gemini.ts
2925
3028
  var gemini_exports = {};
2926
3029
  __export(gemini_exports, {
@@ -2933,6 +3036,7 @@ var init_gemini = __esm({
2933
3036
  init_base();
2934
3037
  init_pricing();
2935
3038
  init_model_filter();
3039
+ init_gemini_schema();
2936
3040
  GeminiProvider = class extends BaseProvider {
2937
3041
  client;
2938
3042
  constructor(config, model) {
@@ -3153,7 +3257,12 @@ var init_gemini = __esm({
3153
3257
  return {
3154
3258
  name: tool.name,
3155
3259
  description: tool.description,
3156
- parameters: tool.inputSchema
3260
+ // Convert, never cast. Gemini's parameters field is an OpenAPI subset
3261
+ // that 400s on unknown keys, and MCP servers ship schemas carrying their
3262
+ // own extensions (GitHub's adds `x-mcp-header` to every header-bound
3263
+ // property). Casting sent those through verbatim and every request failed
3264
+ // before the model saw it. See providers/gemini-schema.ts.
3265
+ parameters: toGeminiParameters(tool.inputSchema)
3157
3266
  };
3158
3267
  }
3159
3268
  };
@@ -3178,7 +3287,7 @@ async function nodeHttpFetch(input, init = {}, redirectCount = 0) {
3178
3287
  headers["accept-encoding"] = "gzip, deflate, br";
3179
3288
  }
3180
3289
  const body = init.body == null ? void 0 : typeof init.body === "string" ? init.body : Buffer.from(init.body);
3181
- return new Promise((resolve, reject) => {
3290
+ return new Promise((resolve2, reject) => {
3182
3291
  const req = lib.request(
3183
3292
  {
3184
3293
  hostname: u.hostname,
@@ -3195,7 +3304,7 @@ async function nodeHttpFetch(input, init = {}, redirectCount = 0) {
3195
3304
  const nextUrl = new URL(location, u).href;
3196
3305
  const downgrade = status === 303 || (status === 301 || status === 302) && method !== "GET" && method !== "HEAD";
3197
3306
  const nextInit = downgrade ? { ...init, method: "GET", body: void 0 } : init;
3198
- resolve(nodeHttpFetch(nextUrl, nextInit, redirectCount + 1));
3307
+ resolve2(nodeHttpFetch(nextUrl, nextInit, redirectCount + 1));
3199
3308
  return;
3200
3309
  }
3201
3310
  const encoding = (res.headers["content-encoding"] ?? "").toLowerCase();
@@ -3210,7 +3319,7 @@ async function nodeHttpFetch(input, init = {}, redirectCount = 0) {
3210
3319
  if (Array.isArray(v)) respHeaders.set(k, v.join(", "));
3211
3320
  else if (typeof v === "string") respHeaders.set(k, v);
3212
3321
  }
3213
- resolve(new Response(stream$1, {
3322
+ resolve2(new Response(stream$1, {
3214
3323
  status,
3215
3324
  statusText: res.statusMessage ?? "",
3216
3325
  headers: respHeaders
@@ -5009,6 +5118,14 @@ var BudgetConfigSchema = zod.z.object({
5009
5118
  maxTokensPerRun: zod.z.number().int().positive().default(2e5),
5010
5119
  /** Optional hard per-task cost ceiling (USD). Unset = only the token cap applies. */
5011
5120
  maxCostPerRunUsd: zod.z.number().positive().optional(),
5121
+ /**
5122
+ * Consecutive systemic failures against ONE model before the run stops
5123
+ * launching work that would fail the same way (dead key, wrong model id,
5124
+ * exhausted quota). Lower reacts sooner and wastes less; higher absorbs a
5125
+ * longer transient before giving up. Only failures that will certainly repeat
5126
+ * are counted — see core/run-breaker.ts.
5127
+ */
5128
+ failureThreshold: zod.z.number().int().min(1).max(20).default(3),
5012
5129
  warnAtPct: zod.z.number().default(80)
5013
5130
  });
5014
5131
  var WorkspaceConfigSchema = zod.z.object({
@@ -6061,7 +6178,7 @@ var TpmLimiter = class {
6061
6178
  }
6062
6179
  const deficit = want - bucket.available;
6063
6180
  const waitMs = Math.max(50, Math.ceil(deficit / bucket.tokensPerMinute * 6e4));
6064
- await new Promise((resolve) => setTimeout(resolve, waitMs));
6181
+ await new Promise((resolve2) => setTimeout(resolve2, waitMs));
6065
6182
  }
6066
6183
  }
6067
6184
  /**
@@ -6124,14 +6241,14 @@ var LocalRequestQueue = class {
6124
6241
  this.active++;
6125
6242
  return this.makeRelease();
6126
6243
  }
6127
- return new Promise((resolve, reject) => {
6244
+ return new Promise((resolve2, reject) => {
6128
6245
  let settled = false;
6129
6246
  let timer;
6130
6247
  const resolver = (release) => {
6131
6248
  if (settled) return;
6132
6249
  settled = true;
6133
6250
  if (timer !== void 0) clearTimeout(timer);
6134
- resolve(release);
6251
+ resolve2(release);
6135
6252
  };
6136
6253
  if (timeoutMs !== void 0 && timeoutMs > 0) {
6137
6254
  timer = setTimeout(() => {
@@ -6758,6 +6875,7 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
6758
6875
  tpmLimiter;
6759
6876
  localQueue;
6760
6877
  taskAnalyzer;
6878
+ pendingFeedbackSource;
6761
6879
  worldStateDB;
6762
6880
  privacyPaths;
6763
6881
  guidanceQueue;
@@ -7212,8 +7330,18 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
7212
7330
  return windows.length ? Math.min(...windows) : void 0;
7213
7331
  }
7214
7332
  /** Wire the Cascade Auto task analyzer used for per-subtask model routing. */
7333
+ /**
7334
+ * Feed per-model thumbs counts into Auto routing. Forwarded to the analyzer
7335
+ * because that is where the public benchmark score is combined; the prior
7336
+ * adjusts that number rather than sitting anywhere else in the pipeline.
7337
+ */
7338
+ setFeedbackSource(source) {
7339
+ this.taskAnalyzer?.setFeedbackSource(source);
7340
+ this.pendingFeedbackSource = source;
7341
+ }
7215
7342
  setTaskAnalyzer(analyzer) {
7216
7343
  this.taskAnalyzer = analyzer;
7344
+ if (this.pendingFeedbackSource) analyzer.setFeedbackSource(this.pendingFeedbackSource);
7217
7345
  }
7218
7346
  setWorldStateDB(db) {
7219
7347
  this.worldStateDB = db;
@@ -8308,8 +8436,11 @@ Now execute your subtask using this context where relevant.`
8308
8436
  const result = await this.runAgentLoop(systemPrompt, this.tools);
8309
8437
  output = result.output;
8310
8438
  toolCalls = result.toolCalls;
8311
- this.sendStatusUpdate({ progressPct: 65, currentAction: "Verifying required artifacts", status: "IN_PROGRESS" });
8312
- const artifactCheck = await this.verifyArtifacts(assignment);
8439
+ const mustProduceArtifact = this.requiresArtifact();
8440
+ if (mustProduceArtifact) {
8441
+ this.sendStatusUpdate({ progressPct: 65, currentAction: "Verifying required artifacts", status: "IN_PROGRESS" });
8442
+ }
8443
+ const artifactCheck = mustProduceArtifact ? await this.verifyArtifacts(assignment) : { ok: true, issues: [] };
8313
8444
  if (!artifactCheck.ok) {
8314
8445
  correctionAttempts = 1;
8315
8446
  issues.push(...artifactCheck.issues);
@@ -8589,11 +8720,11 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : "") + textToolSuffix,
8589
8720
  if (!wasApproved) return `Tool ${tc.name} was denied by user.`;
8590
8721
  } else {
8591
8722
  const LEGACY_APPROVAL_TIMEOUT_MS = 6e5;
8592
- const legacyDecision = await new Promise((resolve) => {
8723
+ const legacyDecision = await new Promise((resolve2) => {
8593
8724
  const eventName = `tool:approval-response:${this.id}-${tc.id}`;
8594
8725
  const timer = setTimeout(() => {
8595
8726
  this.removeAllListeners(eventName);
8596
- resolve({ approved: false });
8727
+ resolve2({ approved: false });
8597
8728
  }, LEGACY_APPROVAL_TIMEOUT_MS);
8598
8729
  timer.unref?.();
8599
8730
  this.emit("tool:approval-request", {
@@ -8606,7 +8737,7 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : "") + textToolSuffix,
8606
8737
  });
8607
8738
  this.once(eventName, (d) => {
8608
8739
  clearTimeout(timer);
8609
- resolve(d);
8740
+ resolve2(d);
8610
8741
  });
8611
8742
  });
8612
8743
  if (legacyDecision.always) this.sessionApprovals.set(tc.name, legacyDecision.approved);
@@ -8626,6 +8757,9 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : "") + textToolSuffix,
8626
8757
  tierId: this.id,
8627
8758
  sessionId: this.taskId,
8628
8759
  requireApproval: false,
8760
+ // Media generation can run for a minute; without this a cancelled run
8761
+ // still pays for an image nobody will see.
8762
+ ...this.signal ? { signal: this.signal } : {},
8629
8763
  saveSnapshot: async (path37, content) => {
8630
8764
  this.store?.addFileSnapshot(this.taskId, path37, content);
8631
8765
  },
@@ -8673,7 +8807,8 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : "") + textToolSuffix,
8673
8807
  const result = await this.toolRegistry.execute(altTool, tc.input, {
8674
8808
  tierId: this.id,
8675
8809
  sessionId: this.taskId,
8676
- requireApproval: false
8810
+ requireApproval: false,
8811
+ ...this.signal ? { signal: this.signal } : {}
8677
8812
  });
8678
8813
  const str2 = typeof result === "string" ? result : JSON.stringify(result);
8679
8814
  if (!str2.startsWith("Tool error:") && !str2.startsWith("Error:")) {
@@ -8695,7 +8830,8 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : "") + textToolSuffix,
8695
8830
  const result = await this.toolRegistry.execute(newToolName, tc.input, {
8696
8831
  tierId: this.id,
8697
8832
  sessionId: this.taskId,
8698
- requireApproval: false
8833
+ requireApproval: false,
8834
+ ...this.signal ? { signal: this.signal } : {}
8699
8835
  });
8700
8836
  const str2 = typeof result === "string" ? result : JSON.stringify(result);
8701
8837
  if (!str2.startsWith("Tool error:")) return `[Synthesized ${newToolName}]: ${str2}`;
@@ -9113,7 +9249,7 @@ var PeerBus = class extends EventEmitter__default.default {
9113
9249
  this.outputs.set(subtaskId, entry);
9114
9250
  this.emit("output:ready", entry);
9115
9251
  const waiting = this.waiters.get(subtaskId) ?? [];
9116
- for (const resolve of waiting) resolve(entry);
9252
+ for (const resolve2 of waiting) resolve2(entry);
9117
9253
  this.waiters.delete(subtaskId);
9118
9254
  }
9119
9255
  /**
@@ -9144,10 +9280,10 @@ var PeerBus = class extends EventEmitter__default.default {
9144
9280
  waitFor(subtaskId, timeoutMs = 12e4) {
9145
9281
  const existing = this.outputs.get(subtaskId);
9146
9282
  if (existing && !this.retryPending.has(subtaskId)) return Promise.resolve(existing);
9147
- return new Promise((resolve, reject) => {
9283
+ return new Promise((resolve2, reject) => {
9148
9284
  const resolver = (output) => {
9149
9285
  clearTimeout(timer);
9150
- resolve(output);
9286
+ resolve2(output);
9151
9287
  };
9152
9288
  const timer = setTimeout(() => {
9153
9289
  const waiting = this.waiters.get(subtaskId);
@@ -9201,10 +9337,10 @@ var PeerBus = class extends EventEmitter__default.default {
9201
9337
  */
9202
9338
  collect(timeoutMs) {
9203
9339
  const collected = [...this.broadcastLog];
9204
- return new Promise((resolve) => {
9340
+ return new Promise((resolve2) => {
9205
9341
  const timer = setTimeout(() => {
9206
9342
  off();
9207
- resolve(collected);
9343
+ resolve2(collected);
9208
9344
  }, timeoutMs);
9209
9345
  const handler = (msg) => {
9210
9346
  collected.push({ fromId: msg.fromId, payload: msg.payload, timestamp: msg.timestamp });
@@ -9218,7 +9354,7 @@ var PeerBus = class extends EventEmitter__default.default {
9218
9354
  const broadcasters = new Set(collected.map((m) => m.fromId));
9219
9355
  if (broadcasters.size >= this.members.size) {
9220
9356
  off();
9221
- resolve(collected);
9357
+ resolve2(collected);
9222
9358
  }
9223
9359
  };
9224
9360
  this.on("broadcast", checkComplete);
@@ -9236,7 +9372,7 @@ var PeerBus = class extends EventEmitter__default.default {
9236
9372
  return;
9237
9373
  }
9238
9374
  this.emitCoordination(tierId, `\u23F3 waiting for ${filePath} (held by ${existing.holderId})`);
9239
- return new Promise((resolve, reject) => {
9375
+ return new Promise((resolve2, reject) => {
9240
9376
  const timer = setTimeout(() => {
9241
9377
  reject(new Error(`File lock timeout for ${filePath} (held by ${existing.holderId})`));
9242
9378
  }, timeoutMs);
@@ -9244,7 +9380,7 @@ var PeerBus = class extends EventEmitter__default.default {
9244
9380
  clearTimeout(timer);
9245
9381
  this.fileLocks.set(filePath, { holderId: tierId, lockedAt: (/* @__PURE__ */ new Date()).toISOString(), waiters: [] });
9246
9382
  this.emitCoordination(tierId, `\u{1F512} locked ${filePath}`);
9247
- resolve();
9383
+ resolve2();
9248
9384
  });
9249
9385
  });
9250
9386
  }
@@ -9269,11 +9405,11 @@ var PeerBus = class extends EventEmitter__default.default {
9269
9405
  */
9270
9406
  waitForFileRelease(filePath, timeoutMs = 3e4) {
9271
9407
  if (!this.fileLocks.has(filePath)) return Promise.resolve();
9272
- return new Promise((resolve, reject) => {
9408
+ return new Promise((resolve2, reject) => {
9273
9409
  const timer = setTimeout(() => reject(new Error(`Timeout waiting for file release: ${filePath}`)), timeoutMs);
9274
9410
  this.once(`file:released:${filePath}`, () => {
9275
9411
  clearTimeout(timer);
9276
- resolve();
9412
+ resolve2();
9277
9413
  });
9278
9414
  });
9279
9415
  }
@@ -9339,13 +9475,13 @@ var PeerBus = class extends EventEmitter__default.default {
9339
9475
  this.emit(`barrier:${barrierName}`);
9340
9476
  return;
9341
9477
  }
9342
- return new Promise((resolve, reject) => {
9478
+ return new Promise((resolve2, reject) => {
9343
9479
  const timer = setTimeout(() => {
9344
9480
  reject(new Error(`Barrier timeout: ${barrierName} (${bar.arrived.size}/${bar.total} arrived)`));
9345
9481
  }, 12e4);
9346
9482
  this.once(`barrier:${barrierName}`, () => {
9347
9483
  clearTimeout(timer);
9348
- resolve();
9484
+ resolve2();
9349
9485
  });
9350
9486
  });
9351
9487
  }
@@ -10896,6 +11032,175 @@ Reply with exactly one word: YES, NO, or UNSURE.
10896
11032
  // src/core/cascade.ts
10897
11033
  init_cost();
10898
11034
 
11035
+ // src/core/multimodal/registry.ts
11036
+ init_pricing();
11037
+ var CAPABILITIES = [
11038
+ // ── Image ──
11039
+ {
11040
+ modality: "image",
11041
+ provider: "openai",
11042
+ modelId: "dall-e-3",
11043
+ api: "openai-images"
11044
+ },
11045
+ {
11046
+ modality: "image",
11047
+ provider: "gemini",
11048
+ modelId: "gemini-3-pro-image",
11049
+ api: "gemini-predict"
11050
+ },
11051
+ // ── Speech (text → audio) ──
11052
+ {
11053
+ modality: "speech",
11054
+ provider: "openai",
11055
+ modelId: "tts-1",
11056
+ api: "openai-speech"
11057
+ },
11058
+ {
11059
+ // The only quality claim in this table that needs no leaderboard: OpenAI
11060
+ // ships these as the same voice at two fidelities and charges double for
11061
+ // this one. That is the vendor's own ordering, not an inference.
11062
+ modality: "speech",
11063
+ provider: "openai",
11064
+ modelId: "tts-1-hd",
11065
+ api: "openai-speech",
11066
+ quality: 70,
11067
+ qualityBasis: "OpenAI's own hd variant of tts-1, priced 2x"
11068
+ },
11069
+ // ── Transcription (audio → text) ──
11070
+ {
11071
+ modality: "transcription",
11072
+ provider: "openai",
11073
+ modelId: "whisper-1",
11074
+ api: "openai-transcriptions"
11075
+ },
11076
+ // ── Video ──
11077
+ {
11078
+ // Listed so `describe()` can tell the truth about what exists, and so the
11079
+ // planner is told video is out of reach rather than left to invent a tool.
11080
+ // Veo is a long-running operation: submit, poll, then fetch bytes from a
11081
+ // signed URL. That is a different execution shape from every other entry
11082
+ // here and is not wired up — claiming otherwise would produce a tool that
11083
+ // fails at call time instead of an honest "not yet".
11084
+ modality: "video",
11085
+ provider: "gemini",
11086
+ modelId: "veo-3.1-generate-001",
11087
+ api: "gemini-predict",
11088
+ unsupported: "Veo runs as a long-running operation (submit \u2192 poll \u2192 fetch). Not wired up yet."
11089
+ }
11090
+ ];
11091
+ function capabilityCost(cap) {
11092
+ const match = findPricing(cap.modelId, cap.provider, {
11093
+ fallbackProviders: cap.provider === "gemini" ? ["vertex"] : []
11094
+ });
11095
+ const rate = match?.entry.rates?.[0];
11096
+ if (!rate) return null;
11097
+ const unitLabel = {
11098
+ per_image: "per image",
11099
+ per_second: "per second of video",
11100
+ per_character: "per character",
11101
+ per_audio_minute: "per audio minute"
11102
+ };
11103
+ const unit = unitLabel[rate.unit] ?? rate.unit;
11104
+ const amount = rate.amount;
11105
+ const shown = amount < 1e-3 ? amount.toExponential(2) : amount.toFixed(4);
11106
+ return { label: `$${shown} ${unit}`, amount, unit };
11107
+ }
11108
+ var MultimodalRegistry = class {
11109
+ /** Provider types the user has actually configured a key for. */
11110
+ available;
11111
+ constructor(availableProviders) {
11112
+ this.available = new Set(availableProviders);
11113
+ }
11114
+ /** Every capability whose provider is configured AND which Cascade can call. */
11115
+ usable(modality) {
11116
+ return CAPABILITIES.filter(
11117
+ (c) => !c.unsupported && this.available.has(c.provider) && (!modality || c.modality === modality)
11118
+ );
11119
+ }
11120
+ /** Modalities this account can actually generate right now. */
11121
+ availableModalities() {
11122
+ return [...new Set(this.usable().map((c) => c.modality))];
11123
+ }
11124
+ /**
11125
+ * Pick a model for a modality.
11126
+ *
11127
+ * Ranked models beat unranked ones, then higher quality, then lower unit
11128
+ * price. An explicit `preferModel` wins outright when it is usable — the user
11129
+ * naming a model is a stronger signal than any prior.
11130
+ */
11131
+ select(modality, opts = {}) {
11132
+ const pool = this.usable(modality);
11133
+ if (!pool.length) return null;
11134
+ if (opts.preferModel) {
11135
+ const wanted = pool.find((c) => c.modelId === opts.preferModel);
11136
+ if (wanted) {
11137
+ return { capability: wanted, cost: capabilityCost(wanted), reason: `Requested explicitly: ${wanted.modelId}.` };
11138
+ }
11139
+ }
11140
+ const scored = pool.map((c) => ({ c, cost: capabilityCost(c) })).sort((a, b) => {
11141
+ const qa = a.c.quality ?? -1;
11142
+ const qb = b.c.quality ?? -1;
11143
+ if (qa !== qb) return qb - qa;
11144
+ const pa = a.cost?.amount ?? Number.POSITIVE_INFINITY;
11145
+ const pb = b.cost?.amount ?? Number.POSITIVE_INFINITY;
11146
+ return pa - pb;
11147
+ });
11148
+ const best = scored[0];
11149
+ const ranked = best.c.quality !== void 0;
11150
+ 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.";
11151
+ return { capability: best.c, cost: best.cost, reason };
11152
+ }
11153
+ /**
11154
+ * A plain-language account of what this account can and cannot generate —
11155
+ * including the things it cannot, and why. The planner sees this, so being
11156
+ * candid here is what stops it inventing a `generate_music` call.
11157
+ */
11158
+ describe() {
11159
+ const lines = [];
11160
+ const usable = this.usable();
11161
+ if (usable.length) {
11162
+ lines.push("Generation models available on your configured providers:");
11163
+ for (const modality of ["image", "speech", "transcription", "video"]) {
11164
+ const forModality = usable.filter((c) => c.modality === modality);
11165
+ if (!forModality.length) continue;
11166
+ const shown = forModality.map((c) => {
11167
+ const cost = capabilityCost(c);
11168
+ return `${c.modelId}${cost ? ` (${cost.label})` : ""}`;
11169
+ });
11170
+ lines.push(` ${modality}: ${shown.join(", ")}`);
11171
+ }
11172
+ } else {
11173
+ lines.push("No generation models are available \u2014 none of your configured providers offers one.");
11174
+ }
11175
+ const blocked = CAPABILITIES.filter((c) => c.unsupported && this.available.has(c.provider));
11176
+ for (const c of blocked) {
11177
+ lines.push(` ${c.modality}: ${c.modelId} exists on your account but is not callable. ${c.unsupported}`);
11178
+ }
11179
+ lines.push(
11180
+ " music: no supported provider exposes a music-generation API, so Cascade cannot generate music."
11181
+ );
11182
+ return lines.join("\n");
11183
+ }
11184
+ };
11185
+
11186
+ // src/tools/base.ts
11187
+ var BaseTool = class {
11188
+ workspaceRoot = process.cwd();
11189
+ setWorkspaceRoot(root) {
11190
+ this.workspaceRoot = root;
11191
+ }
11192
+ isDangerous() {
11193
+ return false;
11194
+ }
11195
+ getDefinition() {
11196
+ return {
11197
+ name: this.name,
11198
+ description: this.description,
11199
+ inputSchema: this.inputSchema
11200
+ };
11201
+ }
11202
+ };
11203
+
10899
11204
  // src/core/router/provider-errors.ts
10900
11205
  var SYSTEMIC = /* @__PURE__ */ new Set([
10901
11206
  "rate_limit",
@@ -10960,7 +11265,7 @@ function classifyProviderError(err) {
10960
11265
  return { kind, systemic: isSystemicKind(kind), status, raw };
10961
11266
  }
10962
11267
  function describeProviderError(c, modelId) {
10963
- const on = ` on ${modelId}` ;
11268
+ const on = modelId ? ` on ${modelId}` : "";
10964
11269
  const detail = c.raw ? ` Provider said: ${c.raw}` : "";
10965
11270
  switch (c.kind) {
10966
11271
  case "rate_limit":
@@ -10982,6 +11287,295 @@ function describeProviderError(c, modelId) {
10982
11287
  }
10983
11288
  }
10984
11289
 
11290
+ // src/core/multimodal/generate.ts
11291
+ var DEFAULT_TIMEOUT_MS = 12e4;
11292
+ async function callProvider(fn, modelId) {
11293
+ try {
11294
+ return await fn();
11295
+ } catch (err) {
11296
+ const classified = classifyProviderError(err);
11297
+ throw new Error(describeProviderError(classified, modelId));
11298
+ }
11299
+ }
11300
+ async function postJson(url, body, headers, signal) {
11301
+ const ac = new AbortController();
11302
+ const timer = setTimeout(() => ac.abort(), DEFAULT_TIMEOUT_MS);
11303
+ const onAbort = () => ac.abort();
11304
+ signal?.addEventListener("abort", onAbort);
11305
+ try {
11306
+ const res = await fetch(url, {
11307
+ method: "POST",
11308
+ headers: { "Content-Type": "application/json", ...headers },
11309
+ body: JSON.stringify(body),
11310
+ signal: ac.signal
11311
+ });
11312
+ if (!res.ok) {
11313
+ const text = await res.text().catch(() => "");
11314
+ throw Object.assign(new Error(text || res.statusText), { status: res.status });
11315
+ }
11316
+ return res;
11317
+ } finally {
11318
+ clearTimeout(timer);
11319
+ signal?.removeEventListener("abort", onAbort);
11320
+ }
11321
+ }
11322
+ function baseUrlFor(cfg, fallback) {
11323
+ return (cfg.baseUrl ?? fallback).replace(/\/+$/, "");
11324
+ }
11325
+ function stamp(ext) {
11326
+ return `cascade-${Date.now()}.${ext}`;
11327
+ }
11328
+ async function generateImage(cap, cfg, req, signal) {
11329
+ return callProvider(async () => {
11330
+ if (cap.api === "openai-images") {
11331
+ const url = `${baseUrlFor(cfg, "https://api.openai.com/v1")}/images/generations`;
11332
+ const res = await postJson(url, {
11333
+ model: cap.modelId,
11334
+ prompt: req.prompt,
11335
+ size: req.size ?? "1024x1024",
11336
+ n: 1,
11337
+ // b64 rather than a URL: see the file header — provider URLs expire.
11338
+ response_format: "b64_json"
11339
+ }, { Authorization: `Bearer ${cfg.apiKey ?? ""}` }, signal);
11340
+ const body = await res.json();
11341
+ const first = body.data?.[0];
11342
+ if (!first?.b64_json) throw new Error("Image response contained no image data.");
11343
+ return {
11344
+ data: Buffer.from(first.b64_json, "base64"),
11345
+ mimeType: "image/png",
11346
+ filename: stamp("png"),
11347
+ modelId: cap.modelId,
11348
+ provider: cap.provider,
11349
+ ...first.revised_prompt ? { revisedPrompt: first.revised_prompt } : {}
11350
+ };
11351
+ }
11352
+ if (cap.api === "gemini-predict") {
11353
+ const base = baseUrlFor(cfg, "https://generativelanguage.googleapis.com/v1beta");
11354
+ const url = `${base}/models/${encodeURIComponent(cap.modelId)}:predict`;
11355
+ const res = await postJson(url, {
11356
+ instances: [{ prompt: req.prompt }],
11357
+ parameters: { sampleCount: 1 }
11358
+ }, { "x-goog-api-key": cfg.apiKey ?? "" }, signal);
11359
+ const body = await res.json();
11360
+ const first = body.predictions?.[0];
11361
+ if (!first?.bytesBase64Encoded) throw new Error("Image response contained no image data.");
11362
+ const mimeType = first.mimeType ?? "image/png";
11363
+ return {
11364
+ data: Buffer.from(first.bytesBase64Encoded, "base64"),
11365
+ mimeType,
11366
+ filename: stamp(mimeType.includes("jpeg") ? "jpg" : "png"),
11367
+ modelId: cap.modelId,
11368
+ provider: cap.provider
11369
+ };
11370
+ }
11371
+ throw new Error(`${cap.modelId} is not an image generator.`);
11372
+ }, cap.modelId);
11373
+ }
11374
+ async function generateSpeech(cap, cfg, req, signal) {
11375
+ return callProvider(async () => {
11376
+ if (cap.api !== "openai-speech") throw new Error(`${cap.modelId} is not a speech model.`);
11377
+ const format = req.format ?? "mp3";
11378
+ const url = `${baseUrlFor(cfg, "https://api.openai.com/v1")}/audio/speech`;
11379
+ const res = await postJson(url, {
11380
+ model: cap.modelId,
11381
+ input: req.text,
11382
+ voice: req.voice ?? "alloy",
11383
+ response_format: format
11384
+ }, { Authorization: `Bearer ${cfg.apiKey ?? ""}` }, signal);
11385
+ const mime = {
11386
+ mp3: "audio/mpeg",
11387
+ opus: "audio/opus",
11388
+ aac: "audio/aac",
11389
+ flac: "audio/flac",
11390
+ wav: "audio/wav"
11391
+ };
11392
+ return {
11393
+ data: Buffer.from(await res.arrayBuffer()),
11394
+ mimeType: mime[format] ?? "application/octet-stream",
11395
+ filename: stamp(format),
11396
+ modelId: cap.modelId,
11397
+ provider: cap.provider
11398
+ };
11399
+ }, cap.modelId);
11400
+ }
11401
+ async function transcribeAudio(cap, cfg, req, signal) {
11402
+ return callProvider(async () => {
11403
+ if (cap.api !== "openai-transcriptions") throw new Error(`${cap.modelId} is not a transcription model.`);
11404
+ const url = `${baseUrlFor(cfg, "https://api.openai.com/v1")}/audio/transcriptions`;
11405
+ const form = new FormData();
11406
+ form.append("model", cap.modelId);
11407
+ if (req.language) form.append("language", req.language);
11408
+ form.append("file", new Blob([new Uint8Array(req.audio)]), req.filename);
11409
+ const ac = new AbortController();
11410
+ const timer = setTimeout(() => ac.abort(), DEFAULT_TIMEOUT_MS);
11411
+ const onAbort = () => ac.abort();
11412
+ signal?.addEventListener("abort", onAbort);
11413
+ try {
11414
+ const res = await fetch(url, {
11415
+ method: "POST",
11416
+ headers: { Authorization: `Bearer ${cfg.apiKey ?? ""}` },
11417
+ body: form,
11418
+ signal: ac.signal
11419
+ });
11420
+ if (!res.ok) {
11421
+ const text = await res.text().catch(() => "");
11422
+ throw Object.assign(new Error(text || res.statusText), { status: res.status });
11423
+ }
11424
+ const body = await res.json();
11425
+ return { text: body.text ?? "", modelId: cap.modelId };
11426
+ } finally {
11427
+ clearTimeout(timer);
11428
+ signal?.removeEventListener("abort", onAbort);
11429
+ }
11430
+ }, cap.modelId);
11431
+ }
11432
+
11433
+ // src/tools/generate-media.ts
11434
+ function resolve(deps, modality, preferModel) {
11435
+ const selected = deps.registry.select(modality, preferModel ? { preferModel } : {});
11436
+ if (!selected) {
11437
+ return { ok: false, error: `No ${modality} model is available on your configured providers.` };
11438
+ }
11439
+ const cfg = deps.lookupProvider(selected.capability.provider);
11440
+ if (!cfg) {
11441
+ return {
11442
+ ok: false,
11443
+ error: `${selected.capability.modelId} needs the ${selected.capability.provider} provider, which is not configured.`
11444
+ };
11445
+ }
11446
+ return { ok: true, selected, cfg };
11447
+ }
11448
+ var GenerateImageTool = class extends BaseTool {
11449
+ constructor(deps) {
11450
+ super();
11451
+ this.deps = deps;
11452
+ }
11453
+ deps;
11454
+ name = "generate_image";
11455
+ 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.";
11456
+ inputSchema = {
11457
+ type: "object",
11458
+ properties: {
11459
+ prompt: { type: "string", description: "What the image should show. Be specific about subject, style and composition." },
11460
+ size: { type: "string", description: 'Pixel size, e.g. "1024x1024" (default), "1792x1024", "1024x1792".' },
11461
+ model: { type: "string", description: "Optional: force a specific image model instead of the automatic choice." }
11462
+ },
11463
+ required: ["prompt"]
11464
+ };
11465
+ async execute(input, options) {
11466
+ const prompt = String(input["prompt"] ?? "").trim();
11467
+ if (!prompt) return 'Provide a "prompt" describing the image.';
11468
+ const r = resolve(this.deps, "image", input["model"] ? String(input["model"]) : void 0);
11469
+ if (!r.ok) return r.error;
11470
+ const size = input["size"] ? String(input["size"]) : void 0;
11471
+ const asset = await generateImage(
11472
+ r.selected.capability,
11473
+ r.cfg,
11474
+ { prompt, ...size ? { size } : {} },
11475
+ options.signal
11476
+ );
11477
+ const location = await this.deps.sink(asset);
11478
+ return [
11479
+ `Image generated and saved to ${location}.`,
11480
+ `Model: ${asset.modelId} \u2014 ${r.selected.reason}`,
11481
+ // DALL·E rewrites the prompt before drawing; the model needs to know what
11482
+ // was actually rendered, not what it asked for, or its description of the
11483
+ // result will not match the image.
11484
+ asset.revisedPrompt ? `The provider rewrote the prompt to: ${asset.revisedPrompt}` : ""
11485
+ ].filter(Boolean).join("\n");
11486
+ }
11487
+ };
11488
+ var GenerateSpeechTool = class extends BaseTool {
11489
+ constructor(deps) {
11490
+ super();
11491
+ this.deps = deps;
11492
+ }
11493
+ deps;
11494
+ name = "generate_speech";
11495
+ 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.";
11496
+ inputSchema = {
11497
+ type: "object",
11498
+ properties: {
11499
+ text: { type: "string", description: "The text to speak." },
11500
+ voice: { type: "string", description: "Voice id: alloy, echo, fable, onyx, nova, shimmer. Default alloy." },
11501
+ format: { type: "string", description: "Audio format: mp3 (default), opus, aac, flac, wav." },
11502
+ model: { type: "string", description: "Optional: force a specific speech model." }
11503
+ },
11504
+ required: ["text"]
11505
+ };
11506
+ async execute(input, options) {
11507
+ const text = String(input["text"] ?? "").trim();
11508
+ if (!text) return 'Provide "text" to speak.';
11509
+ const r = resolve(this.deps, "speech", input["model"] ? String(input["model"]) : void 0);
11510
+ if (!r.ok) return r.error;
11511
+ const fmt = String(input["format"] ?? "mp3");
11512
+ const allowed = ["mp3", "opus", "aac", "flac", "wav"];
11513
+ const format = allowed.includes(fmt) ? fmt : "mp3";
11514
+ const asset = await generateSpeech(
11515
+ r.selected.capability,
11516
+ r.cfg,
11517
+ { text, voice: input["voice"] ? String(input["voice"]) : void 0, format },
11518
+ options.signal
11519
+ );
11520
+ const location = await this.deps.sink(asset);
11521
+ return `Speech generated and saved to ${location}.
11522
+ Model: ${asset.modelId} \u2014 ${r.selected.reason}`;
11523
+ }
11524
+ };
11525
+ var TranscribeAudioTool = class extends BaseTool {
11526
+ constructor(deps, readFile) {
11527
+ super();
11528
+ this.deps = deps;
11529
+ this.readFile = readFile;
11530
+ }
11531
+ deps;
11532
+ readFile;
11533
+ name = "transcribe_audio";
11534
+ 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.";
11535
+ inputSchema = {
11536
+ type: "object",
11537
+ properties: {
11538
+ path: { type: "string", description: "Path to the audio file to transcribe." },
11539
+ language: { type: "string", description: 'Optional ISO-639-1 language hint, e.g. "en", improves accuracy.' },
11540
+ model: { type: "string", description: "Optional: force a specific transcription model." }
11541
+ },
11542
+ required: ["path"]
11543
+ };
11544
+ async execute(input, options) {
11545
+ const filePath = String(input["path"] ?? "").trim();
11546
+ if (!filePath) return 'Provide a "path" to the audio file.';
11547
+ const r = resolve(this.deps, "transcription", input["model"] ? String(input["model"]) : void 0);
11548
+ if (!r.ok) return r.error;
11549
+ let audio;
11550
+ try {
11551
+ audio = await this.readFile(filePath);
11552
+ } catch {
11553
+ return `Could not read the audio file at ${filePath}.`;
11554
+ }
11555
+ const { text, modelId } = await transcribeAudio(
11556
+ r.selected.capability,
11557
+ r.cfg,
11558
+ {
11559
+ audio,
11560
+ filename: filePath.split(/[\\/]/).pop() || "audio",
11561
+ ...input["language"] ? { language: String(input["language"]) } : {}
11562
+ },
11563
+ options.signal
11564
+ );
11565
+ return text ? `Transcript (${modelId}):
11566
+
11567
+ ${text}` : `${modelId} returned an empty transcript \u2014 the file may be silent or in an unsupported format.`;
11568
+ }
11569
+ };
11570
+ function buildMediaTools(deps, readFile) {
11571
+ const tools = [];
11572
+ const modalities = new Set(deps.registry.availableModalities());
11573
+ if (modalities.has("image")) tools.push(new GenerateImageTool(deps));
11574
+ if (modalities.has("speech")) tools.push(new GenerateSpeechTool(deps));
11575
+ if (modalities.has("transcription") && readFile) tools.push(new TranscribeAudioTool(deps, readFile));
11576
+ return tools;
11577
+ }
11578
+
10985
11579
  // src/core/run-breaker.ts
10986
11580
  var DEFAULT_FAILURE_THRESHOLD = 3;
10987
11581
  var RunBreaker = class {
@@ -11050,26 +11644,6 @@ var RunBreaker = class {
11050
11644
 
11051
11645
  // src/tools/registry.ts
11052
11646
  init_constants();
11053
-
11054
- // src/tools/base.ts
11055
- var BaseTool = class {
11056
- workspaceRoot = process.cwd();
11057
- setWorkspaceRoot(root) {
11058
- this.workspaceRoot = root;
11059
- }
11060
- isDangerous() {
11061
- return false;
11062
- }
11063
- getDefinition() {
11064
- return {
11065
- name: this.name,
11066
- description: this.description,
11067
- inputSchema: this.inputSchema
11068
- };
11069
- }
11070
- };
11071
-
11072
- // src/tools/shell.ts
11073
11647
  var execAsync = util.promisify(child_process.exec);
11074
11648
  var ShellTool = class extends BaseTool {
11075
11649
  name = "shell";
@@ -11765,7 +12339,7 @@ var PDFCreateTool = class extends BaseTool {
11765
12339
  if (!fs29__default.default.existsSync(dir)) {
11766
12340
  fs29__default.default.mkdirSync(dir, { recursive: true });
11767
12341
  }
11768
- return new Promise((resolve, reject) => {
12342
+ return new Promise((resolve2, reject) => {
11769
12343
  try {
11770
12344
  const doc = new PDFDocument__default.default({ margin: 50 });
11771
12345
  const stream = fs29__default.default.createWriteStream(filePath);
@@ -11783,7 +12357,7 @@ var PDFCreateTool = class extends BaseTool {
11783
12357
  });
11784
12358
  doc.end();
11785
12359
  stream.on("finish", () => {
11786
- resolve(`Successfully created PDF at ${filePath} (${content.length} characters)`);
12360
+ resolve2(`Successfully created PDF at ${filePath} (${content.length} characters)`);
11787
12361
  });
11788
12362
  stream.on("error", (err) => {
11789
12363
  reject(new Error(`Failed to write PDF: ${err.message}`));
@@ -11855,7 +12429,7 @@ var CodeInterpreterTool = class extends BaseTool {
11855
12429
  const filePath = path32__default.default.join(tmpDir, fileName);
11856
12430
  fs29__default.default.writeFileSync(filePath, code, "utf-8");
11857
12431
  const execArgs = [filePath, ...args];
11858
- return new Promise((resolve) => {
12432
+ return new Promise((resolve2) => {
11859
12433
  const startMs = Date.now();
11860
12434
  child_process.execFile(cmdPrefix, execArgs, { cwd: this.workspaceRoot, timeout: 3e4 }, (error, stdout, stderr) => {
11861
12435
  const duration = Date.now() - startMs;
@@ -11869,17 +12443,17 @@ var CodeInterpreterTool = class extends BaseTool {
11869
12443
  if (error) {
11870
12444
  const timedOut = error.killed && duration >= 3e4;
11871
12445
  if (timedOut) {
11872
- resolve(`Execution timed out after 30s. Consider breaking the task into smaller pieces.
12446
+ resolve2(`Execution timed out after 30s. Consider breaking the task into smaller pieces.
11873
12447
  Partial stdout: ${stdout}
11874
12448
  Stderr: ${stderr}`);
11875
12449
  } else {
11876
- resolve(`Execution failed (${duration}ms):
12450
+ resolve2(`Execution failed (${duration}ms):
11877
12451
  Error: ${error.message}
11878
12452
  Stderr: ${stderr}
11879
12453
  Stdout: ${stdout}`);
11880
12454
  }
11881
12455
  } else {
11882
- resolve(`Execution successful (${duration}ms):
12456
+ resolve2(`Execution successful (${duration}ms):
11883
12457
  Stdout: ${stdout}
11884
12458
  Stderr: ${stderr}`);
11885
12459
  }
@@ -12533,7 +13107,7 @@ var ToolRegistry = class extends EventEmitter__default.default {
12533
13107
  */
12534
13108
  waitForTool(toolName, timeoutMs = 6e4) {
12535
13109
  if (this.tools.has(toolName)) return Promise.resolve();
12536
- return new Promise((resolve, reject) => {
13110
+ return new Promise((resolve2, reject) => {
12537
13111
  const timer = setTimeout(() => {
12538
13112
  this.off("tool:added", handler);
12539
13113
  reject(new Error(`Timeout waiting for tool: ${toolName}`));
@@ -12542,7 +13116,7 @@ var ToolRegistry = class extends EventEmitter__default.default {
12542
13116
  if (name === toolName) {
12543
13117
  clearTimeout(timer);
12544
13118
  this.off("tool:added", handler);
12545
- resolve();
13119
+ resolve2();
12546
13120
  }
12547
13121
  };
12548
13122
  this.on("tool:added", handler);
@@ -13195,20 +13769,20 @@ var PermissionEscalator = class extends EventEmitter__default.default {
13195
13769
  resolver(decision);
13196
13770
  }
13197
13771
  waitForUserDecision(req) {
13198
- return new Promise((resolve) => {
13772
+ return new Promise((resolve2) => {
13199
13773
  let timer;
13200
13774
  const wrappedResolver = (decision) => {
13201
13775
  if (timer) clearTimeout(timer);
13202
13776
  if (decision.always) {
13203
13777
  this.taskWideCache.set(req.toolName, decision.approved);
13204
13778
  }
13205
- resolve(decision);
13779
+ resolve2(decision);
13206
13780
  };
13207
13781
  this.pendingUserDecisions.set(req.id, wrappedResolver);
13208
13782
  if (this.approvalTimeoutMs > 0 && Number.isFinite(this.approvalTimeoutMs)) {
13209
13783
  timer = setTimeout(() => {
13210
13784
  if (this.pendingUserDecisions.delete(req.id)) {
13211
- resolve({
13785
+ resolve2({
13212
13786
  requestId: req.id,
13213
13787
  approved: false,
13214
13788
  decidedBy: "USER",
@@ -13276,6 +13850,23 @@ var noopTelemetry = {
13276
13850
  }
13277
13851
  };
13278
13852
 
13853
+ // src/core/router/feedback-prior.ts
13854
+ var MAX_FEEDBACK_ADJUSTMENT = 0.05;
13855
+ var PRIOR_STRENGTH = 10;
13856
+ function feedbackAdjustment(counts) {
13857
+ const good = Math.max(0, Math.floor(counts.good));
13858
+ const bad = Math.max(0, Math.floor(counts.bad));
13859
+ const n = good + bad;
13860
+ if (n === 0) return 0;
13861
+ const half = PRIOR_STRENGTH / 2;
13862
+ const shrunk = (good + half) / (n + PRIOR_STRENGTH);
13863
+ return (shrunk - 0.5) * 2 * MAX_FEEDBACK_ADJUSTMENT;
13864
+ }
13865
+ function applyFeedback(score01, counts) {
13866
+ const adjusted = score01 + feedbackAdjustment(counts);
13867
+ return Math.max(0, Math.min(1, adjusted));
13868
+ }
13869
+
13279
13870
  // src/core/router/task-analyzer.ts
13280
13871
  init_pricing();
13281
13872
  var CODE_SIGNALS = [
@@ -13351,6 +13942,8 @@ var TASK_TYPE_TAGS = {
13351
13942
  };
13352
13943
  var TaskAnalyzer = class {
13353
13944
  tracker;
13945
+ /** Per-model thumbs counts, when the host collects them. */
13946
+ feedback;
13354
13947
  bias;
13355
13948
  lastProfile = null;
13356
13949
  lastSelectedModels = /* @__PURE__ */ new Map();
@@ -13358,6 +13951,14 @@ var TaskAnalyzer = class {
13358
13951
  this.tracker = tracker;
13359
13952
  this.bias = bias;
13360
13953
  }
13954
+ /**
13955
+ * Supply per-model rating counts. Optional by design: a host that collects no
13956
+ * feedback (the CLI, a fresh install) scores exactly as it did before, since
13957
+ * feedbackAdjustment returns 0 for an empty record.
13958
+ */
13959
+ setFeedbackSource(source) {
13960
+ this.feedback = source;
13961
+ }
13361
13962
  setTracker(tracker) {
13362
13963
  this.tracker = tracker;
13363
13964
  }
@@ -13441,7 +14042,10 @@ var TaskAnalyzer = class {
13441
14042
  const perf = this.tracker?.performanceScore(model.id, profile.type) ?? 0.5;
13442
14043
  const costEff = this.costEfficiency(model, profile.complexity);
13443
14044
  const match = this.taskMatchScore(model, profile);
13444
- const benchmark = 0.3 + 0.7 * benchmarkScore01(model, profile.type);
14045
+ const rated = this.feedback?.(model.id);
14046
+ const publicScore = benchmarkScore01(model, profile.type);
14047
+ const adjusted = rated ? applyFeedback(publicScore, rated) : publicScore;
14048
+ const benchmark = 0.3 + 0.7 * adjusted;
13445
14049
  switch (this.bias) {
13446
14050
  case "quality":
13447
14051
  return perf * match * benchmark ** 2 * (0.85 + 0.15 * costEff);
@@ -13846,10 +14450,10 @@ var DynamicTool = class extends BaseTool {
13846
14450
  `;
13847
14451
  const script = await isolate.compileScript(bootstrap);
13848
14452
  const runPromise = script.run(context, { timeout: timeoutMs, promise: true }).then((v) => String(v ?? ""));
13849
- const timeoutPromise = new Promise((resolve) => {
14453
+ const timeoutPromise = new Promise((resolve2) => {
13850
14454
  const t = setTimeout(() => {
13851
14455
  dispose();
13852
- resolve(`Dynamic tool "${this.name}" timed out after ${timeoutMs}ms and was terminated.`);
14456
+ resolve2(`Dynamic tool "${this.name}" timed out after ${timeoutMs}ms and was terminated.`);
13853
14457
  }, timeoutMs + 500);
13854
14458
  t.unref?.();
13855
14459
  });
@@ -13867,7 +14471,7 @@ var DynamicTool = class extends BaseTool {
13867
14471
  /** Spawn the worker, service its callTool/fetch bridge, enforce the kill timeout. */
13868
14472
  runInWorker(input, callTool) {
13869
14473
  const timeoutMs = Math.max(200, Number(process.env["CASCADE_DYNAMIC_TOOL_TIMEOUT_MS"]) || DYNAMIC_TOOL_TIMEOUT_MS);
13870
- return new Promise((resolve) => {
14474
+ return new Promise((resolve2) => {
13871
14475
  let settled = false;
13872
14476
  const worker = new worker_threads.Worker(HARNESS_SRC, {
13873
14477
  eval: true,
@@ -13879,7 +14483,7 @@ var DynamicTool = class extends BaseTool {
13879
14483
  settled = true;
13880
14484
  clearTimeout(timer);
13881
14485
  void worker.terminate();
13882
- resolve(value);
14486
+ resolve2(value);
13883
14487
  };
13884
14488
  const timer = setTimeout(
13885
14489
  () => finish(`Dynamic tool "${this.name}" timed out after ${timeoutMs}ms and was terminated.`),
@@ -15386,6 +15990,7 @@ ${prompt}`;
15386
15990
  var Cascade = class _Cascade extends EventEmitter__default.default {
15387
15991
  router;
15388
15992
  toolRegistry;
15993
+ multimodal;
15389
15994
  mcpClient;
15390
15995
  codeIndex;
15391
15996
  config;
@@ -15424,6 +16029,10 @@ var Cascade = class _Cascade extends EventEmitter__default.default {
15424
16029
  }
15425
16030
  });
15426
16031
  this.toolRegistry = new ToolRegistry(this.config.tools, workspacePath);
16032
+ this.multimodal = new MultimodalRegistry(
16033
+ (this.config.providers ?? []).map((p) => p.type)
16034
+ );
16035
+ this.registerMediaTools(workspacePath);
15427
16036
  this.telemetry = config.telemetry?.enabled ? new Telemetry(config.telemetry, config.telemetry.distinctId ?? "anonymous") : noopTelemetry;
15428
16037
  this.worldStateDB = new WorldStateDB(this.workspacePath, this.config.workspace?.debugWorldState ?? false);
15429
16038
  this.router.setWorldStateDB(this.worldStateDB);
@@ -15477,14 +16086,14 @@ var Cascade = class _Cascade extends EventEmitter__default.default {
15477
16086
  if (this.listenerCount("mcp:approval-required") === 0) {
15478
16087
  return false;
15479
16088
  }
15480
- return await new Promise((resolve) => {
15481
- this.pendingMcpApprovals.set(server.name, resolve);
16089
+ return await new Promise((resolve2) => {
16090
+ this.pendingMcpApprovals.set(server.name, resolve2);
15482
16091
  const timeout = setTimeout(() => {
15483
- if (this.pendingMcpApprovals.delete(server.name)) resolve(false);
16092
+ if (this.pendingMcpApprovals.delete(server.name)) resolve2(false);
15484
16093
  }, 3e4);
15485
16094
  const wrap = (approved) => {
15486
16095
  clearTimeout(timeout);
15487
- resolve(approved);
16096
+ resolve2(approved);
15488
16097
  };
15489
16098
  this.pendingMcpApprovals.set(server.name, wrap);
15490
16099
  this.emit("mcp:approval-required", { server });
@@ -15558,17 +16167,17 @@ var Cascade = class _Cascade extends EventEmitter__default.default {
15558
16167
  }
15559
16168
  const t2Count = plan.sections.length;
15560
16169
  const t3Count = plan.sections.reduce((sum, s) => sum + (s.t3Subtasks?.length ?? 0), 0);
15561
- return await new Promise((resolve) => {
16170
+ return await new Promise((resolve2) => {
15562
16171
  const timeout = setTimeout(() => {
15563
16172
  if (this.pendingPlanApproval) {
15564
16173
  this.pendingPlanApproval = void 0;
15565
- resolve({ approved: true });
16174
+ resolve2({ approved: true });
15566
16175
  }
15567
16176
  }, 12e4);
15568
16177
  this.pendingPlanApproval = (decision) => {
15569
16178
  clearTimeout(timeout);
15570
16179
  this.pendingPlanApproval = void 0;
15571
- resolve(decision);
16180
+ resolve2(decision);
15572
16181
  };
15573
16182
  this.emit("plan:approval-required", {
15574
16183
  taskId,
@@ -15601,17 +16210,17 @@ var Cascade = class _Cascade extends EventEmitter__default.default {
15601
16210
  }
15602
16211
  async requestContextApproval(info) {
15603
16212
  if (this.listenerCount("context:approval-required") === 0) return true;
15604
- return await new Promise((resolve) => {
16213
+ return await new Promise((resolve2) => {
15605
16214
  const timeout = setTimeout(() => {
15606
16215
  if (this.pendingContextApproval) {
15607
16216
  this.pendingContextApproval = void 0;
15608
- resolve(true);
16217
+ resolve2(true);
15609
16218
  }
15610
16219
  }, 12e4);
15611
16220
  this.pendingContextApproval = (approved) => {
15612
16221
  clearTimeout(timeout);
15613
16222
  this.pendingContextApproval = void 0;
15614
- resolve(approved);
16223
+ resolve2(approved);
15615
16224
  };
15616
16225
  this.emit("context:approval-required", info);
15617
16226
  });
@@ -15691,6 +16300,55 @@ ${input}` }],
15691
16300
  if (this.store) t1.setStore(this.store);
15692
16301
  return t1.previewPlan(prompt);
15693
16302
  }
16303
+ /**
16304
+ * Register image / speech / transcription tools for the modalities this
16305
+ * config can actually serve.
16306
+ *
16307
+ * The sink writes bytes into the workspace, because that is what a desktop or
16308
+ * CLI run wants and it keeps a generated asset indistinguishable from any
16309
+ * other file the run produced. A host with different storage (the cloud
16310
+ * server stores a file row) constructs the tools itself via buildMediaTools
16311
+ * with its own sink.
16312
+ */
16313
+ registerMediaTools(workspacePath) {
16314
+ const allowlist = this.config.tools?.enabledTools;
16315
+ const permitted = (name) => !allowlist || allowlist.includes(name);
16316
+ const tools = buildMediaTools(
16317
+ {
16318
+ registry: this.multimodal,
16319
+ lookupProvider: (provider) => (this.config.providers ?? []).find((p) => p.type === provider),
16320
+ sink: async (asset) => {
16321
+ const fsp = await import('fs/promises');
16322
+ const nodePath = await import('path');
16323
+ const dir = nodePath.join(workspacePath, "generated");
16324
+ await fsp.mkdir(dir, { recursive: true });
16325
+ const dest = nodePath.join(dir, asset.filename);
16326
+ await fsp.writeFile(dest, asset.data);
16327
+ return nodePath.relative(workspacePath, dest);
16328
+ }
16329
+ },
16330
+ async (p) => {
16331
+ const fsp = await import('fs/promises');
16332
+ return fsp.readFile(p);
16333
+ }
16334
+ );
16335
+ for (const tool of tools) {
16336
+ if (!permitted(tool.name)) continue;
16337
+ tool.setWorkspaceRoot(workspacePath);
16338
+ this.toolRegistry.register(tool);
16339
+ }
16340
+ }
16341
+ /**
16342
+ * Supply per-model thumbs-up/down counts so Auto routing can weigh how models
16343
+ * have actually performed for this user, on top of public benchmarks.
16344
+ *
16345
+ * Optional everywhere: with no source, or an empty record, scoring is
16346
+ * byte-identical to before. The adjustment is capped and sample-size shrunk —
16347
+ * see core/router/feedback-prior.ts.
16348
+ */
16349
+ setFeedbackSource(source) {
16350
+ this.router.setFeedbackSource(source);
16351
+ }
15694
16352
  /** True when a task stopped at the budget cap and can be resumed via /continue. */
15695
16353
  hasResumableRun() {
15696
16354
  return this.lastInterruptedRun != null;
@@ -16180,7 +16838,7 @@ ${prompt}` : prompt;
16180
16838
  if (toolCreator) toolCreator.setPermissionEscalator(escalator);
16181
16839
  const rootPrompt = buildContextualPrompt(options.prompt, options.conversationHistory);
16182
16840
  let finalOutput = "";
16183
- const runBreaker = new RunBreaker();
16841
+ const runBreaker = new RunBreaker(this.config.budget?.failureThreshold);
16184
16842
  let t2Results = [];
16185
16843
  let runError = null;
16186
16844
  let identityPrompt = "";
@@ -16883,8 +17541,8 @@ var CloudClient = class _CloudClient {
16883
17541
  }
16884
17542
  };
16885
17543
  function sleep(ms, signal) {
16886
- return new Promise((resolve, reject) => {
16887
- const t = setTimeout(resolve, ms);
17544
+ return new Promise((resolve2, reject) => {
17545
+ const t = setTimeout(resolve2, ms);
16888
17546
  signal?.addEventListener("abort", () => {
16889
17547
  clearTimeout(t);
16890
17548
  reject(new Error("aborted"));
@@ -18676,8 +19334,8 @@ function Repl({ config, workspacePath, themeName, initialPrompt, identityName, a
18676
19334
  onExit: () => exit(),
18677
19335
  onThemeChange: (name) => setTheme(getTheme(name)),
18678
19336
  onExport: async (fmt) => {
18679
- const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
18680
- const exportPath = path32__default.default.join(workspacePath, `cascade-export-${stamp}.${fmt === "json" ? "json" : "md"}`);
19337
+ const stamp2 = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
19338
+ const exportPath = path32__default.default.join(workspacePath, `cascade-export-${stamp2}.${fmt === "json" ? "json" : "md"}`);
18681
19339
  if (fmt === "json") {
18682
19340
  await fs10__default.default.writeFile(exportPath, JSON.stringify({ sessionId: sessionIdRef.current, messages: state.messages }, null, 2), "utf-8");
18683
19341
  } else {
@@ -19109,8 +19767,8 @@ ${lastUser.content}`;
19109
19767
  conversationHistory: toConversationHistory(state.messages),
19110
19768
  approvalCallback: async (req) => {
19111
19769
  dispatch({ type: "SET_APPROVAL", request: req });
19112
- return new Promise((resolve) => {
19113
- approvalResolverRef.current = resolve;
19770
+ return new Promise((resolve2) => {
19771
+ approvalResolverRef.current = resolve2;
19114
19772
  });
19115
19773
  }
19116
19774
  });
@@ -20376,13 +21034,13 @@ function SetupWizard({ workspacePath, onComplete }) {
20376
21034
  }
20377
21035
  async function runSetupWizard(workspacePath) {
20378
21036
  printTelemetryBanner();
20379
- return new Promise((resolve) => {
21037
+ return new Promise((resolve2) => {
20380
21038
  const { unmount } = ink.render(
20381
21039
  React2__default.default.createElement(SetupWizard, {
20382
21040
  workspacePath,
20383
21041
  onComplete: (config) => {
20384
21042
  unmount();
20385
- resolve(config);
21043
+ resolve2(config);
20386
21044
  }
20387
21045
  }),
20388
21046
  { exitOnCtrlC: true }
@@ -20850,7 +21508,7 @@ function notSignedIn() {
20850
21508
  process.exitCode = 1;
20851
21509
  }
20852
21510
  function promptHidden(question) {
20853
- return new Promise((resolve) => {
21511
+ return new Promise((resolve2) => {
20854
21512
  const rl = readline__default.default.createInterface({ input: process.stdin, output: process.stdout, terminal: true });
20855
21513
  let masked = false;
20856
21514
  rl._writeToOutput = (s) => {
@@ -20859,7 +21517,7 @@ function promptHidden(question) {
20859
21517
  rl.question(question, (answer) => {
20860
21518
  rl.close();
20861
21519
  process.stdout.write("\n");
20862
- resolve(answer.trim());
21520
+ resolve2(answer.trim());
20863
21521
  });
20864
21522
  masked = true;
20865
21523
  });
@@ -21786,12 +22444,12 @@ var DashboardServer = class {
21786
22444
  console.warn("\u26A0 Dashboard auth is DISABLED while bound to a non-loopback interface \u2014 this allows unauthenticated remote task execution.");
21787
22445
  }
21788
22446
  }
21789
- await new Promise((resolve, reject) => {
22447
+ await new Promise((resolve2, reject) => {
21790
22448
  const onError = (err) => reject(err);
21791
22449
  this.httpServer.once("error", onError);
21792
22450
  this.httpServer.listen(this.port, this.host, () => {
21793
22451
  this.httpServer.off("error", onError);
21794
- resolve();
22452
+ resolve2();
21795
22453
  });
21796
22454
  });
21797
22455
  try {
@@ -21815,8 +22473,8 @@ var DashboardServer = class {
21815
22473
  } catch {
21816
22474
  }
21817
22475
  this.globalStore = null;
21818
- await new Promise((resolve, reject) => {
21819
- this.httpServer.close((err) => err ? reject(err) : resolve());
22476
+ await new Promise((resolve2, reject) => {
22477
+ this.httpServer.close((err) => err ? reject(err) : resolve2());
21820
22478
  });
21821
22479
  }
21822
22480
  getSocket() {
@@ -22013,8 +22671,8 @@ var DashboardServer = class {
22013
22671
  * timeout denies it.
22014
22672
  */
22015
22673
  makeApprovalCallback(sessionId) {
22016
- return (request) => new Promise((resolve) => {
22017
- this.pendingApprovals.set(request.id, { resolve, sessionId });
22674
+ return (request) => new Promise((resolve2) => {
22675
+ this.pendingApprovals.set(request.id, { resolve: resolve2, sessionId });
22018
22676
  });
22019
22677
  }
22020
22678
  /**