cascade-ai 0.51.0 → 0.52.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.d.cts CHANGED
@@ -202,6 +202,12 @@ interface ToolExecuteOptions {
202
202
  * paying it out and discarding the result.
203
203
  */
204
204
  signal?: AbortSignal;
205
+ /**
206
+ * Progress notes from a long-running tool. Video generation polls for minutes;
207
+ * without a channel back, a working render is indistinguishable from a hang —
208
+ * which is the most likely reason someone kills one.
209
+ */
210
+ onProgress?: (note: string) => void;
205
211
  saveSnapshot?: (filePath: string, content: string) => Promise<void>;
206
212
  sendPeerSync?: (to: string, syncType: PeerSyncType, content: string | Record<string, unknown>) => void;
207
213
  getPeerMessages?: () => Array<{
@@ -592,6 +598,12 @@ interface ToolsConfig {
592
598
  * behavior). An empty array registers no tools at all.
593
599
  */
594
600
  enabledTools?: string[];
601
+ /**
602
+ * Tools to remove even when they would otherwise be registered. Reaches
603
+ * tools registered outside `enabledTools` (MCP, media generation), which is
604
+ * the only way to switch those off.
605
+ */
606
+ disabledTools?: string[];
595
607
  }
596
608
  interface HooksConfig {
597
609
  preToolUse?: HookDefinition[];
@@ -2182,6 +2194,24 @@ declare class T1Administrator extends BaseTier {
2182
2194
  private evaluatePermissionAtT1;
2183
2195
  }
2184
2196
 
2197
+ interface GeneratedAsset {
2198
+ /** Raw bytes of the asset. */
2199
+ data: Buffer;
2200
+ mimeType: string;
2201
+ /** Suggested filename, extension included. */
2202
+ filename: string;
2203
+ modelId: string;
2204
+ provider: string;
2205
+ /** Provider-side revision of the prompt, when it reports one (DALL·E does). */
2206
+ revisedPrompt?: string;
2207
+ }
2208
+
2209
+ /**
2210
+ * Where finished bytes go. Returns a short human-readable locator (a path, a
2211
+ * file id, a URL) that the tool reports back to the model.
2212
+ */
2213
+ type AssetSink = (asset: GeneratedAsset) => Promise<string>;
2214
+
2185
2215
  /** One entry in the per-run orchestration decision trail (see /why). */
2186
2216
  interface DecisionLogEntry {
2187
2217
  at: string;
@@ -2192,6 +2222,7 @@ declare class Cascade extends EventEmitter {
2192
2222
  private router;
2193
2223
  private toolRegistry;
2194
2224
  private multimodal;
2225
+ private mediaSink?;
2195
2226
  private mcpClient;
2196
2227
  private codeIndex?;
2197
2228
  private config;
@@ -2287,6 +2318,16 @@ declare class Cascade extends EventEmitter {
2287
2318
  * server stores a file row) constructs the tools itself via buildMediaTools
2288
2319
  * with its own sink.
2289
2320
  */
2321
+ /**
2322
+ * Replace where generated media is written.
2323
+ *
2324
+ * The default sink writes into the workspace, which is what a desktop or CLI
2325
+ * run wants. A host with different storage — the cloud server keeps a file
2326
+ * row plus bytes under the tenant's directory — supplies its own here, and
2327
+ * the media tools are rebuilt around it. Re-registering by name replaces the
2328
+ * existing tools rather than duplicating them.
2329
+ */
2330
+ setMediaSink(sink: AssetSink): void;
2290
2331
  private registerMediaTools;
2291
2332
  /**
2292
2333
  * Supply per-model thumbs-up/down counts so Auto routing can weigh how models
package/dist/index.d.ts CHANGED
@@ -202,6 +202,12 @@ interface ToolExecuteOptions {
202
202
  * paying it out and discarding the result.
203
203
  */
204
204
  signal?: AbortSignal;
205
+ /**
206
+ * Progress notes from a long-running tool. Video generation polls for minutes;
207
+ * without a channel back, a working render is indistinguishable from a hang —
208
+ * which is the most likely reason someone kills one.
209
+ */
210
+ onProgress?: (note: string) => void;
205
211
  saveSnapshot?: (filePath: string, content: string) => Promise<void>;
206
212
  sendPeerSync?: (to: string, syncType: PeerSyncType, content: string | Record<string, unknown>) => void;
207
213
  getPeerMessages?: () => Array<{
@@ -592,6 +598,12 @@ interface ToolsConfig {
592
598
  * behavior). An empty array registers no tools at all.
593
599
  */
594
600
  enabledTools?: string[];
601
+ /**
602
+ * Tools to remove even when they would otherwise be registered. Reaches
603
+ * tools registered outside `enabledTools` (MCP, media generation), which is
604
+ * the only way to switch those off.
605
+ */
606
+ disabledTools?: string[];
595
607
  }
596
608
  interface HooksConfig {
597
609
  preToolUse?: HookDefinition[];
@@ -2182,6 +2194,24 @@ declare class T1Administrator extends BaseTier {
2182
2194
  private evaluatePermissionAtT1;
2183
2195
  }
2184
2196
 
2197
+ interface GeneratedAsset {
2198
+ /** Raw bytes of the asset. */
2199
+ data: Buffer;
2200
+ mimeType: string;
2201
+ /** Suggested filename, extension included. */
2202
+ filename: string;
2203
+ modelId: string;
2204
+ provider: string;
2205
+ /** Provider-side revision of the prompt, when it reports one (DALL·E does). */
2206
+ revisedPrompt?: string;
2207
+ }
2208
+
2209
+ /**
2210
+ * Where finished bytes go. Returns a short human-readable locator (a path, a
2211
+ * file id, a URL) that the tool reports back to the model.
2212
+ */
2213
+ type AssetSink = (asset: GeneratedAsset) => Promise<string>;
2214
+
2185
2215
  /** One entry in the per-run orchestration decision trail (see /why). */
2186
2216
  interface DecisionLogEntry {
2187
2217
  at: string;
@@ -2192,6 +2222,7 @@ declare class Cascade extends EventEmitter {
2192
2222
  private router;
2193
2223
  private toolRegistry;
2194
2224
  private multimodal;
2225
+ private mediaSink?;
2195
2226
  private mcpClient;
2196
2227
  private codeIndex?;
2197
2228
  private config;
@@ -2287,6 +2318,16 @@ declare class Cascade extends EventEmitter {
2287
2318
  * server stores a file row) constructs the tools itself via buildMediaTools
2288
2319
  * with its own sink.
2289
2320
  */
2321
+ /**
2322
+ * Replace where generated media is written.
2323
+ *
2324
+ * The default sink writes into the workspace, which is what a desktop or CLI
2325
+ * run wants. A host with different storage — the cloud server keeps a file
2326
+ * row plus bytes under the tenant's directory — supplies its own here, and
2327
+ * the media tools are rebuilt around it. Re-registering by name replaces the
2328
+ * existing tools rather than duplicating them.
2329
+ */
2330
+ setMediaSink(sink: AssetSink): void;
2290
2331
  private registerMediaTools;
2291
2332
  /**
2292
2333
  * Supply per-model thumbs-up/down counts so Auto routing can weigh how models
package/dist/index.js CHANGED
@@ -1845,7 +1845,7 @@ function reconcilePrice(model, live) {
1845
1845
  }
1846
1846
 
1847
1847
  // src/constants.ts
1848
- var CASCADE_VERSION = "0.51.0";
1848
+ var CASCADE_VERSION = "0.52.0";
1849
1849
  var CASCADE_CONFIG_DIR = ".cascade";
1850
1850
  var CASCADE_MD_FILE = "CASCADE.md";
1851
1851
  var CASCADE_IGNORE_FILE = ".cascadeignore";
@@ -8848,17 +8848,13 @@ var CAPABILITIES = [
8848
8848
  },
8849
8849
  // ── Video ──
8850
8850
  {
8851
- // Listed so `describe()` can tell the truth about what exists, and so the
8852
- // planner is told video is out of reach rather than left to invent a tool.
8853
- // Veo is a long-running operation: submit, poll, then fetch bytes from a
8854
- // signed URL. That is a different execution shape from every other entry
8855
- // here and is not wired up — claiming otherwise would produce a tool that
8856
- // fails at call time instead of an honest "not yet".
8851
+ // Long-running: submit returns an operation name, the video lands minutes
8852
+ // later, and the bytes come from a short-lived signed URL. generate.ts owns
8853
+ // that polling loop; the registry only records the shape via `api`.
8857
8854
  modality: "video",
8858
8855
  provider: "gemini",
8859
8856
  modelId: "veo-3.1-generate-001",
8860
- api: "gemini-predict",
8861
- unsupported: "Veo runs as a long-running operation (submit \u2192 poll \u2192 fetch). Not wired up yet."
8857
+ api: "gemini-predict-lro"
8862
8858
  }
8863
8859
  ];
8864
8860
  function capabilityCost(cap) {
@@ -9171,6 +9167,75 @@ async function generateSpeech(cap, cfg, req, signal) {
9171
9167
  };
9172
9168
  }, cap.modelId);
9173
9169
  }
9170
+ var VIDEO_POLL_INTERVAL_MS = 1e4;
9171
+ var VIDEO_TIMEOUT_MS = 8 * 6e4;
9172
+ function sleep(ms, signal) {
9173
+ return new Promise((resolve2, reject) => {
9174
+ if (signal?.aborted) return reject(new Error("Cancelled."));
9175
+ const timer = setTimeout(() => {
9176
+ signal?.removeEventListener("abort", onAbort);
9177
+ resolve2();
9178
+ }, ms);
9179
+ function onAbort() {
9180
+ clearTimeout(timer);
9181
+ reject(new Error("Cancelled."));
9182
+ }
9183
+ signal?.addEventListener("abort", onAbort, { once: true });
9184
+ });
9185
+ }
9186
+ async function generateVideo(cap, cfg, req, signal, onProgress) {
9187
+ return callProvider(async () => {
9188
+ if (cap.api !== "gemini-predict-lro") throw new Error(`${cap.modelId} is not a video generator.`);
9189
+ const base = baseUrlFor(cfg, "https://generativelanguage.googleapis.com/v1beta");
9190
+ const key = cfg.apiKey ?? "";
9191
+ const headers = { "x-goog-api-key": key };
9192
+ const submit = await postJson(
9193
+ `${base}/models/${encodeURIComponent(cap.modelId)}:predictLongRunning`,
9194
+ {
9195
+ instances: [{ prompt: req.prompt }],
9196
+ parameters: {
9197
+ aspectRatio: req.aspectRatio ?? "16:9",
9198
+ ...req.seconds ? { durationSeconds: req.seconds } : {}
9199
+ }
9200
+ },
9201
+ headers,
9202
+ signal
9203
+ );
9204
+ const started = await submit.json();
9205
+ if (!started.name) throw new Error("Video request was accepted but returned no operation to poll.");
9206
+ onProgress?.(`Video generation started (${cap.modelId}). This usually takes 1-3 minutes.`);
9207
+ const deadline = Date.now() + VIDEO_TIMEOUT_MS;
9208
+ let operation = {};
9209
+ while (Date.now() < deadline) {
9210
+ await sleep(VIDEO_POLL_INTERVAL_MS, signal);
9211
+ const poll = await fetch(`${base}/${started.name}`, { headers, signal });
9212
+ if (!poll.ok) {
9213
+ const text = await poll.text().catch(() => "");
9214
+ throw Object.assign(new Error(text || poll.statusText), { status: poll.status });
9215
+ }
9216
+ operation = await poll.json();
9217
+ if (operation.done) break;
9218
+ onProgress?.("Still rendering\u2026");
9219
+ }
9220
+ if (!operation.done) {
9221
+ throw new Error(
9222
+ `Video was still rendering after ${Math.round(VIDEO_TIMEOUT_MS / 6e4)} minutes. It may still complete on the provider side, but Cascade stopped waiting.`
9223
+ );
9224
+ }
9225
+ if (operation.error?.message) throw new Error(operation.error.message);
9226
+ const uri = operation.response?.generateVideoResponse?.generatedSamples?.[0]?.video?.uri;
9227
+ if (!uri) throw new Error("Video finished but the response contained no video.");
9228
+ const media = await fetch(uri, { headers, signal });
9229
+ if (!media.ok) throw new Error(`Video finished but could not be downloaded (HTTP ${media.status}).`);
9230
+ return {
9231
+ data: Buffer.from(await media.arrayBuffer()),
9232
+ mimeType: "video/mp4",
9233
+ filename: stamp("mp4"),
9234
+ modelId: cap.modelId,
9235
+ provider: cap.provider
9236
+ };
9237
+ }, cap.modelId);
9238
+ }
9174
9239
  async function transcribeAudio(cap, cfg, req, signal) {
9175
9240
  return callProvider(async () => {
9176
9241
  if (cap.api !== "openai-transcriptions") throw new Error(`${cap.modelId} is not a transcription model.`);
@@ -9295,6 +9360,58 @@ var GenerateSpeechTool = class extends BaseTool {
9295
9360
  Model: ${asset.modelId} \u2014 ${r.selected.reason}`;
9296
9361
  }
9297
9362
  };
9363
+ var DEFAULT_VIDEO_SECONDS = 5;
9364
+ var MAX_VIDEO_SECONDS = 8;
9365
+ var GenerateVideoTool = class extends BaseTool {
9366
+ constructor(deps) {
9367
+ super();
9368
+ this.deps = deps;
9369
+ }
9370
+ deps;
9371
+ name = "generate_video";
9372
+ // The price is in the description on purpose. Video is ~1000x the cost of an
9373
+ // image per call, it is billed per SECOND, and the model choosing the
9374
+ // duration is the one deciding the bill — so the number has to be where the
9375
+ // model reads before calling, not only in the result afterwards.
9376
+ description = "Generate a short video clip from a text description. EXPENSIVE and slow: billed per second of output (about $0.40/second, so a 5-second clip costs roughly $2) and takes 1-3 minutes. Only use when the user explicitly asks for a video, and prefer the shortest duration that satisfies the request.";
9377
+ inputSchema = {
9378
+ type: "object",
9379
+ properties: {
9380
+ prompt: { type: "string", description: "What the video should show: subject, action, camera movement, style." },
9381
+ seconds: { type: "number", description: `Clip length, 1-${MAX_VIDEO_SECONDS} seconds (default ${DEFAULT_VIDEO_SECONDS}). Billed per second \u2014 keep it short.` },
9382
+ aspectRatio: { type: "string", description: 'Frame shape: "16:9" (default) or "9:16" for vertical.' },
9383
+ model: { type: "string", description: "Optional: force a specific video model." }
9384
+ },
9385
+ required: ["prompt"]
9386
+ };
9387
+ async execute(input, options) {
9388
+ const prompt = String(input["prompt"] ?? "").trim();
9389
+ if (!prompt) return 'Provide a "prompt" describing the video.';
9390
+ const r = resolve(this.deps, "video", input["model"] ? String(input["model"]) : void 0);
9391
+ if (!r.ok) return r.error;
9392
+ const asked = typeof input["seconds"] === "number" ? input["seconds"] : DEFAULT_VIDEO_SECONDS;
9393
+ const seconds = Math.max(1, Math.min(MAX_VIDEO_SECONDS, Math.round(asked)));
9394
+ const asset = await generateVideo(
9395
+ r.selected.capability,
9396
+ r.cfg,
9397
+ {
9398
+ prompt,
9399
+ seconds,
9400
+ ...input["aspectRatio"] ? { aspectRatio: String(input["aspectRatio"]) } : {}
9401
+ },
9402
+ options.signal,
9403
+ // Progress goes to the run log so a two-minute wait doesn't look like a
9404
+ // hang — the single most likely reason someone kills a working render.
9405
+ (note) => options.onProgress?.(note)
9406
+ );
9407
+ const location = await this.deps.sink(asset);
9408
+ const unit = r.selected.cost;
9409
+ const estimate = unit ? ` Estimated cost: $${(unit.amount * seconds).toFixed(2)} (${seconds}s x ${unit.label}).` : "";
9410
+ const clamped = seconds !== Math.round(asked) ? ` Duration was clamped to ${seconds}s (max ${MAX_VIDEO_SECONDS}).` : "";
9411
+ return `Video generated and saved to ${location}.
9412
+ Model: ${asset.modelId}.${estimate}${clamped}`;
9413
+ }
9414
+ };
9298
9415
  var TranscribeAudioTool = class extends BaseTool {
9299
9416
  constructor(deps, readFile) {
9300
9417
  super();
@@ -9345,6 +9462,7 @@ function buildMediaTools(deps, readFile) {
9345
9462
  const modalities = new Set(deps.registry.availableModalities());
9346
9463
  if (modalities.has("image")) tools.push(new GenerateImageTool(deps));
9347
9464
  if (modalities.has("speech")) tools.push(new GenerateSpeechTool(deps));
9465
+ if (modalities.has("video")) tools.push(new GenerateVideoTool(deps));
9348
9466
  if (modalities.has("transcription") && readFile) tools.push(new TranscribeAudioTool(deps, readFile));
9349
9467
  return tools;
9350
9468
  }
@@ -11673,7 +11791,14 @@ var ToolsConfigSchema = z.object({
11673
11791
  * built-in tool (shell/file/git/…) from existing at all, rather than just
11674
11792
  * gating it behind approval. Omitted = full default set (unchanged).
11675
11793
  */
11676
- enabledTools: z.array(z.string()).optional()
11794
+ enabledTools: z.array(z.string()).optional(),
11795
+ /**
11796
+ * Tools to remove even when they would otherwise be registered. Unlike
11797
+ * `enabledTools` (an allowlist over the built-in set), this also reaches
11798
+ * tools registered outside it — MCP tools and media generation — which is
11799
+ * the only way to turn those off.
11800
+ */
11801
+ disabledTools: z.array(z.string()).optional()
11677
11802
  });
11678
11803
  var HookDefinitionSchema = z.object({
11679
11804
  name: z.string().optional(),
@@ -14212,6 +14337,7 @@ var Cascade = class _Cascade extends EventEmitter {
14212
14337
  router;
14213
14338
  toolRegistry;
14214
14339
  multimodal;
14340
+ mediaSink;
14215
14341
  mcpClient;
14216
14342
  codeIndex;
14217
14343
  config;
@@ -14531,14 +14657,27 @@ ${input}` }],
14531
14657
  * server stores a file row) constructs the tools itself via buildMediaTools
14532
14658
  * with its own sink.
14533
14659
  */
14660
+ /**
14661
+ * Replace where generated media is written.
14662
+ *
14663
+ * The default sink writes into the workspace, which is what a desktop or CLI
14664
+ * run wants. A host with different storage — the cloud server keeps a file
14665
+ * row plus bytes under the tenant's directory — supplies its own here, and
14666
+ * the media tools are rebuilt around it. Re-registering by name replaces the
14667
+ * existing tools rather than duplicating them.
14668
+ */
14669
+ setMediaSink(sink) {
14670
+ this.mediaSink = sink;
14671
+ this.registerMediaTools(this.workspacePath);
14672
+ }
14534
14673
  registerMediaTools(workspacePath) {
14535
- const allowlist = this.config.tools?.enabledTools;
14536
- const permitted = (name) => !allowlist || allowlist.includes(name);
14674
+ const denied = new Set(this.config.tools?.disabledTools ?? []);
14675
+ const permitted = (name) => !denied.has(name);
14537
14676
  const tools = buildMediaTools(
14538
14677
  {
14539
14678
  registry: this.multimodal,
14540
14679
  lookupProvider: (provider) => (this.config.providers ?? []).find((p) => p.type === provider),
14541
- sink: async (asset) => {
14680
+ sink: this.mediaSink ?? (async (asset) => {
14542
14681
  const fsp = await import('fs/promises');
14543
14682
  const nodePath = await import('path');
14544
14683
  const dir = nodePath.join(workspacePath, "generated");
@@ -14546,7 +14685,7 @@ ${input}` }],
14546
14685
  const dest = nodePath.join(dir, asset.filename);
14547
14686
  await fsp.writeFile(dest, asset.data);
14548
14687
  return nodePath.relative(workspacePath, dest);
14549
- }
14688
+ })
14550
14689
  },
14551
14690
  async (p) => {
14552
14691
  const fsp = await import('fs/promises');
@@ -18560,7 +18699,7 @@ var CloudClient = class _CloudClient {
18560
18699
  const deadline = Date.now() + start.expiresIn * 1e3;
18561
18700
  while (Date.now() < deadline) {
18562
18701
  if (opts.signal?.aborted) throw new Error("Sign-in cancelled.");
18563
- await sleep(interval * 1e3, opts.signal);
18702
+ await sleep2(interval * 1e3, opts.signal);
18564
18703
  const r = await this.pollDevice(start.deviceCode);
18565
18704
  if (r === "slow_down") {
18566
18705
  interval += 5;
@@ -18771,7 +18910,7 @@ var CloudClient = class _CloudClient {
18771
18910
  this.store.clear();
18772
18911
  }
18773
18912
  };
18774
- function sleep(ms, signal) {
18913
+ function sleep2(ms, signal) {
18775
18914
  return new Promise((resolve2, reject) => {
18776
18915
  const t = setTimeout(resolve2, ms);
18777
18916
  signal?.addEventListener("abort", () => {