cascade-ai 0.50.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/cli.cjs +930 -133
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +930 -133
- package/dist/cli.js.map +1 -1
- package/dist/desktop-core.cjs +1137 -347
- package/dist/index.cjs +916 -124
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +102 -0
- package/dist/index.d.ts +102 -0
- package/dist/index.js +916 -124
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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.
|
|
1894
|
+
var CASCADE_VERSION = "0.52.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
|
|
2408
|
-
"embed",
|
|
2407
|
+
var MODALITY_PATTERNS = [
|
|
2408
|
+
["embedding", ["embed"]],
|
|
2409
2409
|
// text-embedding-3, nomic-embed, mxbai-embed, snowflake-arctic-embed
|
|
2410
|
-
"(?:^|[-_/])
|
|
2411
|
-
"text-to-speech",
|
|
2412
|
-
"
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
"
|
|
2424
|
-
"
|
|
2425
|
-
"
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
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
|
-
|
|
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
|
|
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((
|
|
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
|
-
|
|
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
|
-
|
|
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((
|
|
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((
|
|
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
|
-
|
|
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.
|
|
6150
|
-
|
|
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((
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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((
|
|
7109
|
+
return new Promise((resolve2, reject) => {
|
|
6986
7110
|
const resolver = (output) => {
|
|
6987
7111
|
clearTimeout(timer);
|
|
6988
|
-
|
|
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((
|
|
7166
|
+
return new Promise((resolve2) => {
|
|
7043
7167
|
const timer = setTimeout(() => {
|
|
7044
7168
|
off();
|
|
7045
|
-
|
|
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
|
-
|
|
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((
|
|
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
|
-
|
|
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((
|
|
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
|
-
|
|
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((
|
|
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
|
-
|
|
7310
|
+
resolve2();
|
|
7187
7311
|
});
|
|
7188
7312
|
});
|
|
7189
7313
|
}
|
|
@@ -8728,6 +8852,170 @@ 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
|
+
// Long-running: submit returns an operation name, the video lands minutes
|
|
8898
|
+
// later, and the bytes come from a short-lived signed URL. generate.ts owns
|
|
8899
|
+
// that polling loop; the registry only records the shape via `api`.
|
|
8900
|
+
modality: "video",
|
|
8901
|
+
provider: "gemini",
|
|
8902
|
+
modelId: "veo-3.1-generate-001",
|
|
8903
|
+
api: "gemini-predict-lro"
|
|
8904
|
+
}
|
|
8905
|
+
];
|
|
8906
|
+
function capabilityCost(cap) {
|
|
8907
|
+
const match = findPricing(cap.modelId, cap.provider, {
|
|
8908
|
+
fallbackProviders: cap.provider === "gemini" ? ["vertex"] : []
|
|
8909
|
+
});
|
|
8910
|
+
const rate = match?.entry.rates?.[0];
|
|
8911
|
+
if (!rate) return null;
|
|
8912
|
+
const unitLabel = {
|
|
8913
|
+
per_image: "per image",
|
|
8914
|
+
per_second: "per second of video",
|
|
8915
|
+
per_character: "per character",
|
|
8916
|
+
per_audio_minute: "per audio minute"
|
|
8917
|
+
};
|
|
8918
|
+
const unit = unitLabel[rate.unit] ?? rate.unit;
|
|
8919
|
+
const amount = rate.amount;
|
|
8920
|
+
const shown = amount < 1e-3 ? amount.toExponential(2) : amount.toFixed(4);
|
|
8921
|
+
return { label: `$${shown} ${unit}`, amount, unit };
|
|
8922
|
+
}
|
|
8923
|
+
var MultimodalRegistry = class {
|
|
8924
|
+
/** Provider types the user has actually configured a key for. */
|
|
8925
|
+
available;
|
|
8926
|
+
constructor(availableProviders) {
|
|
8927
|
+
this.available = new Set(availableProviders);
|
|
8928
|
+
}
|
|
8929
|
+
/** Every capability whose provider is configured AND which Cascade can call. */
|
|
8930
|
+
usable(modality) {
|
|
8931
|
+
return CAPABILITIES.filter(
|
|
8932
|
+
(c) => !c.unsupported && this.available.has(c.provider) && (!modality || c.modality === modality)
|
|
8933
|
+
);
|
|
8934
|
+
}
|
|
8935
|
+
/** Modalities this account can actually generate right now. */
|
|
8936
|
+
availableModalities() {
|
|
8937
|
+
return [...new Set(this.usable().map((c) => c.modality))];
|
|
8938
|
+
}
|
|
8939
|
+
/**
|
|
8940
|
+
* Pick a model for a modality.
|
|
8941
|
+
*
|
|
8942
|
+
* Ranked models beat unranked ones, then higher quality, then lower unit
|
|
8943
|
+
* price. An explicit `preferModel` wins outright when it is usable — the user
|
|
8944
|
+
* naming a model is a stronger signal than any prior.
|
|
8945
|
+
*/
|
|
8946
|
+
select(modality, opts = {}) {
|
|
8947
|
+
const pool = this.usable(modality);
|
|
8948
|
+
if (!pool.length) return null;
|
|
8949
|
+
if (opts.preferModel) {
|
|
8950
|
+
const wanted = pool.find((c) => c.modelId === opts.preferModel);
|
|
8951
|
+
if (wanted) {
|
|
8952
|
+
return { capability: wanted, cost: capabilityCost(wanted), reason: `Requested explicitly: ${wanted.modelId}.` };
|
|
8953
|
+
}
|
|
8954
|
+
}
|
|
8955
|
+
const scored = pool.map((c) => ({ c, cost: capabilityCost(c) })).sort((a, b) => {
|
|
8956
|
+
const qa = a.c.quality ?? -1;
|
|
8957
|
+
const qb = b.c.quality ?? -1;
|
|
8958
|
+
if (qa !== qb) return qb - qa;
|
|
8959
|
+
const pa = a.cost?.amount ?? Number.POSITIVE_INFINITY;
|
|
8960
|
+
const pb = b.cost?.amount ?? Number.POSITIVE_INFINITY;
|
|
8961
|
+
return pa - pb;
|
|
8962
|
+
});
|
|
8963
|
+
const best = scored[0];
|
|
8964
|
+
const ranked = best.c.quality !== void 0;
|
|
8965
|
+
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.";
|
|
8966
|
+
return { capability: best.c, cost: best.cost, reason };
|
|
8967
|
+
}
|
|
8968
|
+
/**
|
|
8969
|
+
* A plain-language account of what this account can and cannot generate —
|
|
8970
|
+
* including the things it cannot, and why. The planner sees this, so being
|
|
8971
|
+
* candid here is what stops it inventing a `generate_music` call.
|
|
8972
|
+
*/
|
|
8973
|
+
describe() {
|
|
8974
|
+
const lines = [];
|
|
8975
|
+
const usable = this.usable();
|
|
8976
|
+
if (usable.length) {
|
|
8977
|
+
lines.push("Generation models available on your configured providers:");
|
|
8978
|
+
for (const modality of ["image", "speech", "transcription", "video"]) {
|
|
8979
|
+
const forModality = usable.filter((c) => c.modality === modality);
|
|
8980
|
+
if (!forModality.length) continue;
|
|
8981
|
+
const shown = forModality.map((c) => {
|
|
8982
|
+
const cost = capabilityCost(c);
|
|
8983
|
+
return `${c.modelId}${cost ? ` (${cost.label})` : ""}`;
|
|
8984
|
+
});
|
|
8985
|
+
lines.push(` ${modality}: ${shown.join(", ")}`);
|
|
8986
|
+
}
|
|
8987
|
+
} else {
|
|
8988
|
+
lines.push("No generation models are available \u2014 none of your configured providers offers one.");
|
|
8989
|
+
}
|
|
8990
|
+
const blocked = CAPABILITIES.filter((c) => c.unsupported && this.available.has(c.provider));
|
|
8991
|
+
for (const c of blocked) {
|
|
8992
|
+
lines.push(` ${c.modality}: ${c.modelId} exists on your account but is not callable. ${c.unsupported}`);
|
|
8993
|
+
}
|
|
8994
|
+
lines.push(
|
|
8995
|
+
" music: no supported provider exposes a music-generation API, so Cascade cannot generate music."
|
|
8996
|
+
);
|
|
8997
|
+
return lines.join("\n");
|
|
8998
|
+
}
|
|
8999
|
+
};
|
|
9000
|
+
|
|
9001
|
+
// src/tools/base.ts
|
|
9002
|
+
var BaseTool = class {
|
|
9003
|
+
workspaceRoot = process.cwd();
|
|
9004
|
+
setWorkspaceRoot(root) {
|
|
9005
|
+
this.workspaceRoot = root;
|
|
9006
|
+
}
|
|
9007
|
+
isDangerous() {
|
|
9008
|
+
return false;
|
|
9009
|
+
}
|
|
9010
|
+
getDefinition() {
|
|
9011
|
+
return {
|
|
9012
|
+
name: this.name,
|
|
9013
|
+
description: this.description,
|
|
9014
|
+
inputSchema: this.inputSchema
|
|
9015
|
+
};
|
|
9016
|
+
}
|
|
9017
|
+
};
|
|
9018
|
+
|
|
8731
9019
|
// src/core/router/provider-errors.ts
|
|
8732
9020
|
var SYSTEMIC = /* @__PURE__ */ new Set([
|
|
8733
9021
|
"rate_limit",
|
|
@@ -8792,7 +9080,7 @@ function classifyProviderError(err) {
|
|
|
8792
9080
|
return { kind, systemic: isSystemicKind(kind), status, raw };
|
|
8793
9081
|
}
|
|
8794
9082
|
function describeProviderError(c, modelId) {
|
|
8795
|
-
const on = ` on ${modelId}` ;
|
|
9083
|
+
const on = modelId ? ` on ${modelId}` : "";
|
|
8796
9084
|
const detail = c.raw ? ` Provider said: ${c.raw}` : "";
|
|
8797
9085
|
switch (c.kind) {
|
|
8798
9086
|
case "rate_limit":
|
|
@@ -8814,6 +9102,417 @@ function describeProviderError(c, modelId) {
|
|
|
8814
9102
|
}
|
|
8815
9103
|
}
|
|
8816
9104
|
|
|
9105
|
+
// src/core/multimodal/generate.ts
|
|
9106
|
+
var DEFAULT_TIMEOUT_MS = 12e4;
|
|
9107
|
+
async function callProvider(fn, modelId) {
|
|
9108
|
+
try {
|
|
9109
|
+
return await fn();
|
|
9110
|
+
} catch (err) {
|
|
9111
|
+
const classified = classifyProviderError(err);
|
|
9112
|
+
throw new Error(describeProviderError(classified, modelId));
|
|
9113
|
+
}
|
|
9114
|
+
}
|
|
9115
|
+
async function postJson(url, body, headers, signal) {
|
|
9116
|
+
const ac = new AbortController();
|
|
9117
|
+
const timer = setTimeout(() => ac.abort(), DEFAULT_TIMEOUT_MS);
|
|
9118
|
+
const onAbort = () => ac.abort();
|
|
9119
|
+
signal?.addEventListener("abort", onAbort);
|
|
9120
|
+
try {
|
|
9121
|
+
const res = await fetch(url, {
|
|
9122
|
+
method: "POST",
|
|
9123
|
+
headers: { "Content-Type": "application/json", ...headers },
|
|
9124
|
+
body: JSON.stringify(body),
|
|
9125
|
+
signal: ac.signal
|
|
9126
|
+
});
|
|
9127
|
+
if (!res.ok) {
|
|
9128
|
+
const text = await res.text().catch(() => "");
|
|
9129
|
+
throw Object.assign(new Error(text || res.statusText), { status: res.status });
|
|
9130
|
+
}
|
|
9131
|
+
return res;
|
|
9132
|
+
} finally {
|
|
9133
|
+
clearTimeout(timer);
|
|
9134
|
+
signal?.removeEventListener("abort", onAbort);
|
|
9135
|
+
}
|
|
9136
|
+
}
|
|
9137
|
+
function baseUrlFor(cfg, fallback) {
|
|
9138
|
+
return (cfg.baseUrl ?? fallback).replace(/\/+$/, "");
|
|
9139
|
+
}
|
|
9140
|
+
function stamp(ext) {
|
|
9141
|
+
return `cascade-${Date.now()}.${ext}`;
|
|
9142
|
+
}
|
|
9143
|
+
async function generateImage(cap, cfg, req, signal) {
|
|
9144
|
+
return callProvider(async () => {
|
|
9145
|
+
if (cap.api === "openai-images") {
|
|
9146
|
+
const url = `${baseUrlFor(cfg, "https://api.openai.com/v1")}/images/generations`;
|
|
9147
|
+
const res = await postJson(url, {
|
|
9148
|
+
model: cap.modelId,
|
|
9149
|
+
prompt: req.prompt,
|
|
9150
|
+
size: req.size ?? "1024x1024",
|
|
9151
|
+
n: 1,
|
|
9152
|
+
// b64 rather than a URL: see the file header — provider URLs expire.
|
|
9153
|
+
response_format: "b64_json"
|
|
9154
|
+
}, { Authorization: `Bearer ${cfg.apiKey ?? ""}` }, signal);
|
|
9155
|
+
const body = await res.json();
|
|
9156
|
+
const first = body.data?.[0];
|
|
9157
|
+
if (!first?.b64_json) throw new Error("Image response contained no image data.");
|
|
9158
|
+
return {
|
|
9159
|
+
data: Buffer.from(first.b64_json, "base64"),
|
|
9160
|
+
mimeType: "image/png",
|
|
9161
|
+
filename: stamp("png"),
|
|
9162
|
+
modelId: cap.modelId,
|
|
9163
|
+
provider: cap.provider,
|
|
9164
|
+
...first.revised_prompt ? { revisedPrompt: first.revised_prompt } : {}
|
|
9165
|
+
};
|
|
9166
|
+
}
|
|
9167
|
+
if (cap.api === "gemini-predict") {
|
|
9168
|
+
const base = baseUrlFor(cfg, "https://generativelanguage.googleapis.com/v1beta");
|
|
9169
|
+
const url = `${base}/models/${encodeURIComponent(cap.modelId)}:predict`;
|
|
9170
|
+
const res = await postJson(url, {
|
|
9171
|
+
instances: [{ prompt: req.prompt }],
|
|
9172
|
+
parameters: { sampleCount: 1 }
|
|
9173
|
+
}, { "x-goog-api-key": cfg.apiKey ?? "" }, signal);
|
|
9174
|
+
const body = await res.json();
|
|
9175
|
+
const first = body.predictions?.[0];
|
|
9176
|
+
if (!first?.bytesBase64Encoded) throw new Error("Image response contained no image data.");
|
|
9177
|
+
const mimeType = first.mimeType ?? "image/png";
|
|
9178
|
+
return {
|
|
9179
|
+
data: Buffer.from(first.bytesBase64Encoded, "base64"),
|
|
9180
|
+
mimeType,
|
|
9181
|
+
filename: stamp(mimeType.includes("jpeg") ? "jpg" : "png"),
|
|
9182
|
+
modelId: cap.modelId,
|
|
9183
|
+
provider: cap.provider
|
|
9184
|
+
};
|
|
9185
|
+
}
|
|
9186
|
+
throw new Error(`${cap.modelId} is not an image generator.`);
|
|
9187
|
+
}, cap.modelId);
|
|
9188
|
+
}
|
|
9189
|
+
async function generateSpeech(cap, cfg, req, signal) {
|
|
9190
|
+
return callProvider(async () => {
|
|
9191
|
+
if (cap.api !== "openai-speech") throw new Error(`${cap.modelId} is not a speech model.`);
|
|
9192
|
+
const format = req.format ?? "mp3";
|
|
9193
|
+
const url = `${baseUrlFor(cfg, "https://api.openai.com/v1")}/audio/speech`;
|
|
9194
|
+
const res = await postJson(url, {
|
|
9195
|
+
model: cap.modelId,
|
|
9196
|
+
input: req.text,
|
|
9197
|
+
voice: req.voice ?? "alloy",
|
|
9198
|
+
response_format: format
|
|
9199
|
+
}, { Authorization: `Bearer ${cfg.apiKey ?? ""}` }, signal);
|
|
9200
|
+
const mime = {
|
|
9201
|
+
mp3: "audio/mpeg",
|
|
9202
|
+
opus: "audio/opus",
|
|
9203
|
+
aac: "audio/aac",
|
|
9204
|
+
flac: "audio/flac",
|
|
9205
|
+
wav: "audio/wav"
|
|
9206
|
+
};
|
|
9207
|
+
return {
|
|
9208
|
+
data: Buffer.from(await res.arrayBuffer()),
|
|
9209
|
+
mimeType: mime[format] ?? "application/octet-stream",
|
|
9210
|
+
filename: stamp(format),
|
|
9211
|
+
modelId: cap.modelId,
|
|
9212
|
+
provider: cap.provider
|
|
9213
|
+
};
|
|
9214
|
+
}, cap.modelId);
|
|
9215
|
+
}
|
|
9216
|
+
var VIDEO_POLL_INTERVAL_MS = 1e4;
|
|
9217
|
+
var VIDEO_TIMEOUT_MS = 8 * 6e4;
|
|
9218
|
+
function sleep(ms, signal) {
|
|
9219
|
+
return new Promise((resolve2, reject) => {
|
|
9220
|
+
if (signal?.aborted) return reject(new Error("Cancelled."));
|
|
9221
|
+
const timer = setTimeout(() => {
|
|
9222
|
+
signal?.removeEventListener("abort", onAbort);
|
|
9223
|
+
resolve2();
|
|
9224
|
+
}, ms);
|
|
9225
|
+
function onAbort() {
|
|
9226
|
+
clearTimeout(timer);
|
|
9227
|
+
reject(new Error("Cancelled."));
|
|
9228
|
+
}
|
|
9229
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
9230
|
+
});
|
|
9231
|
+
}
|
|
9232
|
+
async function generateVideo(cap, cfg, req, signal, onProgress) {
|
|
9233
|
+
return callProvider(async () => {
|
|
9234
|
+
if (cap.api !== "gemini-predict-lro") throw new Error(`${cap.modelId} is not a video generator.`);
|
|
9235
|
+
const base = baseUrlFor(cfg, "https://generativelanguage.googleapis.com/v1beta");
|
|
9236
|
+
const key = cfg.apiKey ?? "";
|
|
9237
|
+
const headers = { "x-goog-api-key": key };
|
|
9238
|
+
const submit = await postJson(
|
|
9239
|
+
`${base}/models/${encodeURIComponent(cap.modelId)}:predictLongRunning`,
|
|
9240
|
+
{
|
|
9241
|
+
instances: [{ prompt: req.prompt }],
|
|
9242
|
+
parameters: {
|
|
9243
|
+
aspectRatio: req.aspectRatio ?? "16:9",
|
|
9244
|
+
...req.seconds ? { durationSeconds: req.seconds } : {}
|
|
9245
|
+
}
|
|
9246
|
+
},
|
|
9247
|
+
headers,
|
|
9248
|
+
signal
|
|
9249
|
+
);
|
|
9250
|
+
const started = await submit.json();
|
|
9251
|
+
if (!started.name) throw new Error("Video request was accepted but returned no operation to poll.");
|
|
9252
|
+
onProgress?.(`Video generation started (${cap.modelId}). This usually takes 1-3 minutes.`);
|
|
9253
|
+
const deadline = Date.now() + VIDEO_TIMEOUT_MS;
|
|
9254
|
+
let operation = {};
|
|
9255
|
+
while (Date.now() < deadline) {
|
|
9256
|
+
await sleep(VIDEO_POLL_INTERVAL_MS, signal);
|
|
9257
|
+
const poll = await fetch(`${base}/${started.name}`, { headers, signal });
|
|
9258
|
+
if (!poll.ok) {
|
|
9259
|
+
const text = await poll.text().catch(() => "");
|
|
9260
|
+
throw Object.assign(new Error(text || poll.statusText), { status: poll.status });
|
|
9261
|
+
}
|
|
9262
|
+
operation = await poll.json();
|
|
9263
|
+
if (operation.done) break;
|
|
9264
|
+
onProgress?.("Still rendering\u2026");
|
|
9265
|
+
}
|
|
9266
|
+
if (!operation.done) {
|
|
9267
|
+
throw new Error(
|
|
9268
|
+
`Video was still rendering after ${Math.round(VIDEO_TIMEOUT_MS / 6e4)} minutes. It may still complete on the provider side, but Cascade stopped waiting.`
|
|
9269
|
+
);
|
|
9270
|
+
}
|
|
9271
|
+
if (operation.error?.message) throw new Error(operation.error.message);
|
|
9272
|
+
const uri = operation.response?.generateVideoResponse?.generatedSamples?.[0]?.video?.uri;
|
|
9273
|
+
if (!uri) throw new Error("Video finished but the response contained no video.");
|
|
9274
|
+
const media = await fetch(uri, { headers, signal });
|
|
9275
|
+
if (!media.ok) throw new Error(`Video finished but could not be downloaded (HTTP ${media.status}).`);
|
|
9276
|
+
return {
|
|
9277
|
+
data: Buffer.from(await media.arrayBuffer()),
|
|
9278
|
+
mimeType: "video/mp4",
|
|
9279
|
+
filename: stamp("mp4"),
|
|
9280
|
+
modelId: cap.modelId,
|
|
9281
|
+
provider: cap.provider
|
|
9282
|
+
};
|
|
9283
|
+
}, cap.modelId);
|
|
9284
|
+
}
|
|
9285
|
+
async function transcribeAudio(cap, cfg, req, signal) {
|
|
9286
|
+
return callProvider(async () => {
|
|
9287
|
+
if (cap.api !== "openai-transcriptions") throw new Error(`${cap.modelId} is not a transcription model.`);
|
|
9288
|
+
const url = `${baseUrlFor(cfg, "https://api.openai.com/v1")}/audio/transcriptions`;
|
|
9289
|
+
const form = new FormData();
|
|
9290
|
+
form.append("model", cap.modelId);
|
|
9291
|
+
if (req.language) form.append("language", req.language);
|
|
9292
|
+
form.append("file", new Blob([new Uint8Array(req.audio)]), req.filename);
|
|
9293
|
+
const ac = new AbortController();
|
|
9294
|
+
const timer = setTimeout(() => ac.abort(), DEFAULT_TIMEOUT_MS);
|
|
9295
|
+
const onAbort = () => ac.abort();
|
|
9296
|
+
signal?.addEventListener("abort", onAbort);
|
|
9297
|
+
try {
|
|
9298
|
+
const res = await fetch(url, {
|
|
9299
|
+
method: "POST",
|
|
9300
|
+
headers: { Authorization: `Bearer ${cfg.apiKey ?? ""}` },
|
|
9301
|
+
body: form,
|
|
9302
|
+
signal: ac.signal
|
|
9303
|
+
});
|
|
9304
|
+
if (!res.ok) {
|
|
9305
|
+
const text = await res.text().catch(() => "");
|
|
9306
|
+
throw Object.assign(new Error(text || res.statusText), { status: res.status });
|
|
9307
|
+
}
|
|
9308
|
+
const body = await res.json();
|
|
9309
|
+
return { text: body.text ?? "", modelId: cap.modelId };
|
|
9310
|
+
} finally {
|
|
9311
|
+
clearTimeout(timer);
|
|
9312
|
+
signal?.removeEventListener("abort", onAbort);
|
|
9313
|
+
}
|
|
9314
|
+
}, cap.modelId);
|
|
9315
|
+
}
|
|
9316
|
+
|
|
9317
|
+
// src/tools/generate-media.ts
|
|
9318
|
+
function resolve(deps, modality, preferModel) {
|
|
9319
|
+
const selected = deps.registry.select(modality, preferModel ? { preferModel } : {});
|
|
9320
|
+
if (!selected) {
|
|
9321
|
+
return { ok: false, error: `No ${modality} model is available on your configured providers.` };
|
|
9322
|
+
}
|
|
9323
|
+
const cfg = deps.lookupProvider(selected.capability.provider);
|
|
9324
|
+
if (!cfg) {
|
|
9325
|
+
return {
|
|
9326
|
+
ok: false,
|
|
9327
|
+
error: `${selected.capability.modelId} needs the ${selected.capability.provider} provider, which is not configured.`
|
|
9328
|
+
};
|
|
9329
|
+
}
|
|
9330
|
+
return { ok: true, selected, cfg };
|
|
9331
|
+
}
|
|
9332
|
+
var GenerateImageTool = class extends BaseTool {
|
|
9333
|
+
constructor(deps) {
|
|
9334
|
+
super();
|
|
9335
|
+
this.deps = deps;
|
|
9336
|
+
}
|
|
9337
|
+
deps;
|
|
9338
|
+
name = "generate_image";
|
|
9339
|
+
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.";
|
|
9340
|
+
inputSchema = {
|
|
9341
|
+
type: "object",
|
|
9342
|
+
properties: {
|
|
9343
|
+
prompt: { type: "string", description: "What the image should show. Be specific about subject, style and composition." },
|
|
9344
|
+
size: { type: "string", description: 'Pixel size, e.g. "1024x1024" (default), "1792x1024", "1024x1792".' },
|
|
9345
|
+
model: { type: "string", description: "Optional: force a specific image model instead of the automatic choice." }
|
|
9346
|
+
},
|
|
9347
|
+
required: ["prompt"]
|
|
9348
|
+
};
|
|
9349
|
+
async execute(input, options) {
|
|
9350
|
+
const prompt = String(input["prompt"] ?? "").trim();
|
|
9351
|
+
if (!prompt) return 'Provide a "prompt" describing the image.';
|
|
9352
|
+
const r = resolve(this.deps, "image", input["model"] ? String(input["model"]) : void 0);
|
|
9353
|
+
if (!r.ok) return r.error;
|
|
9354
|
+
const size = input["size"] ? String(input["size"]) : void 0;
|
|
9355
|
+
const asset = await generateImage(
|
|
9356
|
+
r.selected.capability,
|
|
9357
|
+
r.cfg,
|
|
9358
|
+
{ prompt, ...size ? { size } : {} },
|
|
9359
|
+
options.signal
|
|
9360
|
+
);
|
|
9361
|
+
const location = await this.deps.sink(asset);
|
|
9362
|
+
return [
|
|
9363
|
+
`Image generated and saved to ${location}.`,
|
|
9364
|
+
`Model: ${asset.modelId} \u2014 ${r.selected.reason}`,
|
|
9365
|
+
// DALL·E rewrites the prompt before drawing; the model needs to know what
|
|
9366
|
+
// was actually rendered, not what it asked for, or its description of the
|
|
9367
|
+
// result will not match the image.
|
|
9368
|
+
asset.revisedPrompt ? `The provider rewrote the prompt to: ${asset.revisedPrompt}` : ""
|
|
9369
|
+
].filter(Boolean).join("\n");
|
|
9370
|
+
}
|
|
9371
|
+
};
|
|
9372
|
+
var GenerateSpeechTool = class extends BaseTool {
|
|
9373
|
+
constructor(deps) {
|
|
9374
|
+
super();
|
|
9375
|
+
this.deps = deps;
|
|
9376
|
+
}
|
|
9377
|
+
deps;
|
|
9378
|
+
name = "generate_speech";
|
|
9379
|
+
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.";
|
|
9380
|
+
inputSchema = {
|
|
9381
|
+
type: "object",
|
|
9382
|
+
properties: {
|
|
9383
|
+
text: { type: "string", description: "The text to speak." },
|
|
9384
|
+
voice: { type: "string", description: "Voice id: alloy, echo, fable, onyx, nova, shimmer. Default alloy." },
|
|
9385
|
+
format: { type: "string", description: "Audio format: mp3 (default), opus, aac, flac, wav." },
|
|
9386
|
+
model: { type: "string", description: "Optional: force a specific speech model." }
|
|
9387
|
+
},
|
|
9388
|
+
required: ["text"]
|
|
9389
|
+
};
|
|
9390
|
+
async execute(input, options) {
|
|
9391
|
+
const text = String(input["text"] ?? "").trim();
|
|
9392
|
+
if (!text) return 'Provide "text" to speak.';
|
|
9393
|
+
const r = resolve(this.deps, "speech", input["model"] ? String(input["model"]) : void 0);
|
|
9394
|
+
if (!r.ok) return r.error;
|
|
9395
|
+
const fmt = String(input["format"] ?? "mp3");
|
|
9396
|
+
const allowed = ["mp3", "opus", "aac", "flac", "wav"];
|
|
9397
|
+
const format = allowed.includes(fmt) ? fmt : "mp3";
|
|
9398
|
+
const asset = await generateSpeech(
|
|
9399
|
+
r.selected.capability,
|
|
9400
|
+
r.cfg,
|
|
9401
|
+
{ text, voice: input["voice"] ? String(input["voice"]) : void 0, format },
|
|
9402
|
+
options.signal
|
|
9403
|
+
);
|
|
9404
|
+
const location = await this.deps.sink(asset);
|
|
9405
|
+
return `Speech generated and saved to ${location}.
|
|
9406
|
+
Model: ${asset.modelId} \u2014 ${r.selected.reason}`;
|
|
9407
|
+
}
|
|
9408
|
+
};
|
|
9409
|
+
var DEFAULT_VIDEO_SECONDS = 5;
|
|
9410
|
+
var MAX_VIDEO_SECONDS = 8;
|
|
9411
|
+
var GenerateVideoTool = class extends BaseTool {
|
|
9412
|
+
constructor(deps) {
|
|
9413
|
+
super();
|
|
9414
|
+
this.deps = deps;
|
|
9415
|
+
}
|
|
9416
|
+
deps;
|
|
9417
|
+
name = "generate_video";
|
|
9418
|
+
// The price is in the description on purpose. Video is ~1000x the cost of an
|
|
9419
|
+
// image per call, it is billed per SECOND, and the model choosing the
|
|
9420
|
+
// duration is the one deciding the bill — so the number has to be where the
|
|
9421
|
+
// model reads before calling, not only in the result afterwards.
|
|
9422
|
+
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.";
|
|
9423
|
+
inputSchema = {
|
|
9424
|
+
type: "object",
|
|
9425
|
+
properties: {
|
|
9426
|
+
prompt: { type: "string", description: "What the video should show: subject, action, camera movement, style." },
|
|
9427
|
+
seconds: { type: "number", description: `Clip length, 1-${MAX_VIDEO_SECONDS} seconds (default ${DEFAULT_VIDEO_SECONDS}). Billed per second \u2014 keep it short.` },
|
|
9428
|
+
aspectRatio: { type: "string", description: 'Frame shape: "16:9" (default) or "9:16" for vertical.' },
|
|
9429
|
+
model: { type: "string", description: "Optional: force a specific video model." }
|
|
9430
|
+
},
|
|
9431
|
+
required: ["prompt"]
|
|
9432
|
+
};
|
|
9433
|
+
async execute(input, options) {
|
|
9434
|
+
const prompt = String(input["prompt"] ?? "").trim();
|
|
9435
|
+
if (!prompt) return 'Provide a "prompt" describing the video.';
|
|
9436
|
+
const r = resolve(this.deps, "video", input["model"] ? String(input["model"]) : void 0);
|
|
9437
|
+
if (!r.ok) return r.error;
|
|
9438
|
+
const asked = typeof input["seconds"] === "number" ? input["seconds"] : DEFAULT_VIDEO_SECONDS;
|
|
9439
|
+
const seconds = Math.max(1, Math.min(MAX_VIDEO_SECONDS, Math.round(asked)));
|
|
9440
|
+
const asset = await generateVideo(
|
|
9441
|
+
r.selected.capability,
|
|
9442
|
+
r.cfg,
|
|
9443
|
+
{
|
|
9444
|
+
prompt,
|
|
9445
|
+
seconds,
|
|
9446
|
+
...input["aspectRatio"] ? { aspectRatio: String(input["aspectRatio"]) } : {}
|
|
9447
|
+
},
|
|
9448
|
+
options.signal,
|
|
9449
|
+
// Progress goes to the run log so a two-minute wait doesn't look like a
|
|
9450
|
+
// hang — the single most likely reason someone kills a working render.
|
|
9451
|
+
(note) => options.onProgress?.(note)
|
|
9452
|
+
);
|
|
9453
|
+
const location = await this.deps.sink(asset);
|
|
9454
|
+
const unit = r.selected.cost;
|
|
9455
|
+
const estimate = unit ? ` Estimated cost: $${(unit.amount * seconds).toFixed(2)} (${seconds}s x ${unit.label}).` : "";
|
|
9456
|
+
const clamped = seconds !== Math.round(asked) ? ` Duration was clamped to ${seconds}s (max ${MAX_VIDEO_SECONDS}).` : "";
|
|
9457
|
+
return `Video generated and saved to ${location}.
|
|
9458
|
+
Model: ${asset.modelId}.${estimate}${clamped}`;
|
|
9459
|
+
}
|
|
9460
|
+
};
|
|
9461
|
+
var TranscribeAudioTool = class extends BaseTool {
|
|
9462
|
+
constructor(deps, readFile) {
|
|
9463
|
+
super();
|
|
9464
|
+
this.deps = deps;
|
|
9465
|
+
this.readFile = readFile;
|
|
9466
|
+
}
|
|
9467
|
+
deps;
|
|
9468
|
+
readFile;
|
|
9469
|
+
name = "transcribe_audio";
|
|
9470
|
+
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.";
|
|
9471
|
+
inputSchema = {
|
|
9472
|
+
type: "object",
|
|
9473
|
+
properties: {
|
|
9474
|
+
path: { type: "string", description: "Path to the audio file to transcribe." },
|
|
9475
|
+
language: { type: "string", description: 'Optional ISO-639-1 language hint, e.g. "en", improves accuracy.' },
|
|
9476
|
+
model: { type: "string", description: "Optional: force a specific transcription model." }
|
|
9477
|
+
},
|
|
9478
|
+
required: ["path"]
|
|
9479
|
+
};
|
|
9480
|
+
async execute(input, options) {
|
|
9481
|
+
const filePath = String(input["path"] ?? "").trim();
|
|
9482
|
+
if (!filePath) return 'Provide a "path" to the audio file.';
|
|
9483
|
+
const r = resolve(this.deps, "transcription", input["model"] ? String(input["model"]) : void 0);
|
|
9484
|
+
if (!r.ok) return r.error;
|
|
9485
|
+
let audio;
|
|
9486
|
+
try {
|
|
9487
|
+
audio = await this.readFile(filePath);
|
|
9488
|
+
} catch {
|
|
9489
|
+
return `Could not read the audio file at ${filePath}.`;
|
|
9490
|
+
}
|
|
9491
|
+
const { text, modelId } = await transcribeAudio(
|
|
9492
|
+
r.selected.capability,
|
|
9493
|
+
r.cfg,
|
|
9494
|
+
{
|
|
9495
|
+
audio,
|
|
9496
|
+
filename: filePath.split(/[\\/]/).pop() || "audio",
|
|
9497
|
+
...input["language"] ? { language: String(input["language"]) } : {}
|
|
9498
|
+
},
|
|
9499
|
+
options.signal
|
|
9500
|
+
);
|
|
9501
|
+
return text ? `Transcript (${modelId}):
|
|
9502
|
+
|
|
9503
|
+
${text}` : `${modelId} returned an empty transcript \u2014 the file may be silent or in an unsupported format.`;
|
|
9504
|
+
}
|
|
9505
|
+
};
|
|
9506
|
+
function buildMediaTools(deps, readFile) {
|
|
9507
|
+
const tools = [];
|
|
9508
|
+
const modalities = new Set(deps.registry.availableModalities());
|
|
9509
|
+
if (modalities.has("image")) tools.push(new GenerateImageTool(deps));
|
|
9510
|
+
if (modalities.has("speech")) tools.push(new GenerateSpeechTool(deps));
|
|
9511
|
+
if (modalities.has("video")) tools.push(new GenerateVideoTool(deps));
|
|
9512
|
+
if (modalities.has("transcription") && readFile) tools.push(new TranscribeAudioTool(deps, readFile));
|
|
9513
|
+
return tools;
|
|
9514
|
+
}
|
|
9515
|
+
|
|
8817
9516
|
// src/core/run-breaker.ts
|
|
8818
9517
|
var DEFAULT_FAILURE_THRESHOLD = 3;
|
|
8819
9518
|
var RunBreaker = class {
|
|
@@ -8879,26 +9578,6 @@ var RunBreaker = class {
|
|
|
8879
9578
|
return out;
|
|
8880
9579
|
}
|
|
8881
9580
|
};
|
|
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
9581
|
var execAsync = util.promisify(child_process.exec);
|
|
8903
9582
|
var ShellTool = class extends BaseTool {
|
|
8904
9583
|
name = "shell";
|
|
@@ -9594,7 +10273,7 @@ var PDFCreateTool = class extends BaseTool {
|
|
|
9594
10273
|
if (!fs23__default.default.existsSync(dir)) {
|
|
9595
10274
|
fs23__default.default.mkdirSync(dir, { recursive: true });
|
|
9596
10275
|
}
|
|
9597
|
-
return new Promise((
|
|
10276
|
+
return new Promise((resolve2, reject) => {
|
|
9598
10277
|
try {
|
|
9599
10278
|
const doc = new PDFDocument__default.default({ margin: 50 });
|
|
9600
10279
|
const stream = fs23__default.default.createWriteStream(filePath);
|
|
@@ -9612,7 +10291,7 @@ var PDFCreateTool = class extends BaseTool {
|
|
|
9612
10291
|
});
|
|
9613
10292
|
doc.end();
|
|
9614
10293
|
stream.on("finish", () => {
|
|
9615
|
-
|
|
10294
|
+
resolve2(`Successfully created PDF at ${filePath} (${content.length} characters)`);
|
|
9616
10295
|
});
|
|
9617
10296
|
stream.on("error", (err) => {
|
|
9618
10297
|
reject(new Error(`Failed to write PDF: ${err.message}`));
|
|
@@ -9684,7 +10363,7 @@ var CodeInterpreterTool = class extends BaseTool {
|
|
|
9684
10363
|
const filePath = path24__default.default.join(tmpDir, fileName);
|
|
9685
10364
|
fs23__default.default.writeFileSync(filePath, code, "utf-8");
|
|
9686
10365
|
const execArgs = [filePath, ...args];
|
|
9687
|
-
return new Promise((
|
|
10366
|
+
return new Promise((resolve2) => {
|
|
9688
10367
|
const startMs = Date.now();
|
|
9689
10368
|
child_process.execFile(cmdPrefix, execArgs, { cwd: this.workspaceRoot, timeout: 3e4 }, (error, stdout, stderr) => {
|
|
9690
10369
|
const duration = Date.now() - startMs;
|
|
@@ -9698,17 +10377,17 @@ var CodeInterpreterTool = class extends BaseTool {
|
|
|
9698
10377
|
if (error) {
|
|
9699
10378
|
const timedOut = error.killed && duration >= 3e4;
|
|
9700
10379
|
if (timedOut) {
|
|
9701
|
-
|
|
10380
|
+
resolve2(`Execution timed out after 30s. Consider breaking the task into smaller pieces.
|
|
9702
10381
|
Partial stdout: ${stdout}
|
|
9703
10382
|
Stderr: ${stderr}`);
|
|
9704
10383
|
} else {
|
|
9705
|
-
|
|
10384
|
+
resolve2(`Execution failed (${duration}ms):
|
|
9706
10385
|
Error: ${error.message}
|
|
9707
10386
|
Stderr: ${stderr}
|
|
9708
10387
|
Stdout: ${stdout}`);
|
|
9709
10388
|
}
|
|
9710
10389
|
} else {
|
|
9711
|
-
|
|
10390
|
+
resolve2(`Execution successful (${duration}ms):
|
|
9712
10391
|
Stdout: ${stdout}
|
|
9713
10392
|
Stderr: ${stderr}`);
|
|
9714
10393
|
}
|
|
@@ -10362,7 +11041,7 @@ var ToolRegistry = class extends EventEmitter__default.default {
|
|
|
10362
11041
|
*/
|
|
10363
11042
|
waitForTool(toolName, timeoutMs = 6e4) {
|
|
10364
11043
|
if (this.tools.has(toolName)) return Promise.resolve();
|
|
10365
|
-
return new Promise((
|
|
11044
|
+
return new Promise((resolve2, reject) => {
|
|
10366
11045
|
const timer = setTimeout(() => {
|
|
10367
11046
|
this.off("tool:added", handler);
|
|
10368
11047
|
reject(new Error(`Timeout waiting for tool: ${toolName}`));
|
|
@@ -10371,7 +11050,7 @@ var ToolRegistry = class extends EventEmitter__default.default {
|
|
|
10371
11050
|
if (name === toolName) {
|
|
10372
11051
|
clearTimeout(timer);
|
|
10373
11052
|
this.off("tool:added", handler);
|
|
10374
|
-
|
|
11053
|
+
resolve2();
|
|
10375
11054
|
}
|
|
10376
11055
|
};
|
|
10377
11056
|
this.on("tool:added", handler);
|
|
@@ -11046,20 +11725,20 @@ var PermissionEscalator = class extends EventEmitter__default.default {
|
|
|
11046
11725
|
resolver(decision);
|
|
11047
11726
|
}
|
|
11048
11727
|
waitForUserDecision(req) {
|
|
11049
|
-
return new Promise((
|
|
11728
|
+
return new Promise((resolve2) => {
|
|
11050
11729
|
let timer;
|
|
11051
11730
|
const wrappedResolver = (decision) => {
|
|
11052
11731
|
if (timer) clearTimeout(timer);
|
|
11053
11732
|
if (decision.always) {
|
|
11054
11733
|
this.taskWideCache.set(req.toolName, decision.approved);
|
|
11055
11734
|
}
|
|
11056
|
-
|
|
11735
|
+
resolve2(decision);
|
|
11057
11736
|
};
|
|
11058
11737
|
this.pendingUserDecisions.set(req.id, wrappedResolver);
|
|
11059
11738
|
if (this.approvalTimeoutMs > 0 && Number.isFinite(this.approvalTimeoutMs)) {
|
|
11060
11739
|
timer = setTimeout(() => {
|
|
11061
11740
|
if (this.pendingUserDecisions.delete(req.id)) {
|
|
11062
|
-
|
|
11741
|
+
resolve2({
|
|
11063
11742
|
requestId: req.id,
|
|
11064
11743
|
approved: false,
|
|
11065
11744
|
decidedBy: "USER",
|
|
@@ -11158,7 +11837,14 @@ var ToolsConfigSchema = zod.z.object({
|
|
|
11158
11837
|
* built-in tool (shell/file/git/…) from existing at all, rather than just
|
|
11159
11838
|
* gating it behind approval. Omitted = full default set (unchanged).
|
|
11160
11839
|
*/
|
|
11161
|
-
enabledTools: zod.z.array(zod.z.string()).optional()
|
|
11840
|
+
enabledTools: zod.z.array(zod.z.string()).optional(),
|
|
11841
|
+
/**
|
|
11842
|
+
* Tools to remove even when they would otherwise be registered. Unlike
|
|
11843
|
+
* `enabledTools` (an allowlist over the built-in set), this also reaches
|
|
11844
|
+
* tools registered outside it — MCP tools and media generation — which is
|
|
11845
|
+
* the only way to turn those off.
|
|
11846
|
+
*/
|
|
11847
|
+
disabledTools: zod.z.array(zod.z.string()).optional()
|
|
11162
11848
|
});
|
|
11163
11849
|
var HookDefinitionSchema = zod.z.object({
|
|
11164
11850
|
name: zod.z.string().optional(),
|
|
@@ -11226,6 +11912,14 @@ var BudgetConfigSchema = zod.z.object({
|
|
|
11226
11912
|
maxTokensPerRun: zod.z.number().int().positive().default(2e5),
|
|
11227
11913
|
/** Optional hard per-task cost ceiling (USD). Unset = only the token cap applies. */
|
|
11228
11914
|
maxCostPerRunUsd: zod.z.number().positive().optional(),
|
|
11915
|
+
/**
|
|
11916
|
+
* Consecutive systemic failures against ONE model before the run stops
|
|
11917
|
+
* launching work that would fail the same way (dead key, wrong model id,
|
|
11918
|
+
* exhausted quota). Lower reacts sooner and wastes less; higher absorbs a
|
|
11919
|
+
* longer transient before giving up. Only failures that will certainly repeat
|
|
11920
|
+
* are counted — see core/run-breaker.ts.
|
|
11921
|
+
*/
|
|
11922
|
+
failureThreshold: zod.z.number().int().min(1).max(20).default(3),
|
|
11229
11923
|
warnAtPct: zod.z.number().default(80)
|
|
11230
11924
|
});
|
|
11231
11925
|
var WorkspaceConfigSchema = zod.z.object({
|
|
@@ -11513,6 +12207,23 @@ var noopTelemetry = {
|
|
|
11513
12207
|
}
|
|
11514
12208
|
};
|
|
11515
12209
|
|
|
12210
|
+
// src/core/router/feedback-prior.ts
|
|
12211
|
+
var MAX_FEEDBACK_ADJUSTMENT = 0.05;
|
|
12212
|
+
var PRIOR_STRENGTH = 10;
|
|
12213
|
+
function feedbackAdjustment(counts) {
|
|
12214
|
+
const good = Math.max(0, Math.floor(counts.good));
|
|
12215
|
+
const bad = Math.max(0, Math.floor(counts.bad));
|
|
12216
|
+
const n = good + bad;
|
|
12217
|
+
if (n === 0) return 0;
|
|
12218
|
+
const half = PRIOR_STRENGTH / 2;
|
|
12219
|
+
const shrunk = (good + half) / (n + PRIOR_STRENGTH);
|
|
12220
|
+
return (shrunk - 0.5) * 2 * MAX_FEEDBACK_ADJUSTMENT;
|
|
12221
|
+
}
|
|
12222
|
+
function applyFeedback(score01, counts) {
|
|
12223
|
+
const adjusted = score01 + feedbackAdjustment(counts);
|
|
12224
|
+
return Math.max(0, Math.min(1, adjusted));
|
|
12225
|
+
}
|
|
12226
|
+
|
|
11516
12227
|
// src/core/router/task-analyzer.ts
|
|
11517
12228
|
var CODE_SIGNALS = [
|
|
11518
12229
|
/\b(?:function|class|interface|async|await|import|export|const|let|var|def|return|if|else|for|while|try|catch)\b/,
|
|
@@ -11587,6 +12298,8 @@ var TASK_TYPE_TAGS = {
|
|
|
11587
12298
|
};
|
|
11588
12299
|
var TaskAnalyzer = class {
|
|
11589
12300
|
tracker;
|
|
12301
|
+
/** Per-model thumbs counts, when the host collects them. */
|
|
12302
|
+
feedback;
|
|
11590
12303
|
bias;
|
|
11591
12304
|
lastProfile = null;
|
|
11592
12305
|
lastSelectedModels = /* @__PURE__ */ new Map();
|
|
@@ -11594,6 +12307,14 @@ var TaskAnalyzer = class {
|
|
|
11594
12307
|
this.tracker = tracker;
|
|
11595
12308
|
this.bias = bias;
|
|
11596
12309
|
}
|
|
12310
|
+
/**
|
|
12311
|
+
* Supply per-model rating counts. Optional by design: a host that collects no
|
|
12312
|
+
* feedback (the CLI, a fresh install) scores exactly as it did before, since
|
|
12313
|
+
* feedbackAdjustment returns 0 for an empty record.
|
|
12314
|
+
*/
|
|
12315
|
+
setFeedbackSource(source) {
|
|
12316
|
+
this.feedback = source;
|
|
12317
|
+
}
|
|
11597
12318
|
setTracker(tracker) {
|
|
11598
12319
|
this.tracker = tracker;
|
|
11599
12320
|
}
|
|
@@ -11677,7 +12398,10 @@ var TaskAnalyzer = class {
|
|
|
11677
12398
|
const perf = this.tracker?.performanceScore(model.id, profile.type) ?? 0.5;
|
|
11678
12399
|
const costEff = this.costEfficiency(model, profile.complexity);
|
|
11679
12400
|
const match = this.taskMatchScore(model, profile);
|
|
11680
|
-
const
|
|
12401
|
+
const rated = this.feedback?.(model.id);
|
|
12402
|
+
const publicScore = benchmarkScore01(model, profile.type);
|
|
12403
|
+
const adjusted = rated ? applyFeedback(publicScore, rated) : publicScore;
|
|
12404
|
+
const benchmark = 0.3 + 0.7 * adjusted;
|
|
11681
12405
|
switch (this.bias) {
|
|
11682
12406
|
case "quality":
|
|
11683
12407
|
return perf * match * benchmark ** 2 * (0.85 + 0.15 * costEff);
|
|
@@ -12079,10 +12803,10 @@ var DynamicTool = class extends BaseTool {
|
|
|
12079
12803
|
`;
|
|
12080
12804
|
const script = await isolate.compileScript(bootstrap);
|
|
12081
12805
|
const runPromise = script.run(context, { timeout: timeoutMs, promise: true }).then((v) => String(v ?? ""));
|
|
12082
|
-
const timeoutPromise = new Promise((
|
|
12806
|
+
const timeoutPromise = new Promise((resolve2) => {
|
|
12083
12807
|
const t = setTimeout(() => {
|
|
12084
12808
|
dispose();
|
|
12085
|
-
|
|
12809
|
+
resolve2(`Dynamic tool "${this.name}" timed out after ${timeoutMs}ms and was terminated.`);
|
|
12086
12810
|
}, timeoutMs + 500);
|
|
12087
12811
|
t.unref?.();
|
|
12088
12812
|
});
|
|
@@ -12100,7 +12824,7 @@ var DynamicTool = class extends BaseTool {
|
|
|
12100
12824
|
/** Spawn the worker, service its callTool/fetch bridge, enforce the kill timeout. */
|
|
12101
12825
|
runInWorker(input, callTool) {
|
|
12102
12826
|
const timeoutMs = Math.max(200, Number(process.env["CASCADE_DYNAMIC_TOOL_TIMEOUT_MS"]) || DYNAMIC_TOOL_TIMEOUT_MS);
|
|
12103
|
-
return new Promise((
|
|
12827
|
+
return new Promise((resolve2) => {
|
|
12104
12828
|
let settled = false;
|
|
12105
12829
|
const worker = new worker_threads.Worker(HARNESS_SRC, {
|
|
12106
12830
|
eval: true,
|
|
@@ -12112,7 +12836,7 @@ var DynamicTool = class extends BaseTool {
|
|
|
12112
12836
|
settled = true;
|
|
12113
12837
|
clearTimeout(timer);
|
|
12114
12838
|
void worker.terminate();
|
|
12115
|
-
|
|
12839
|
+
resolve2(value);
|
|
12116
12840
|
};
|
|
12117
12841
|
const timer = setTimeout(
|
|
12118
12842
|
() => finish(`Dynamic tool "${this.name}" timed out after ${timeoutMs}ms and was terminated.`),
|
|
@@ -13658,6 +14382,8 @@ ${prompt}`;
|
|
|
13658
14382
|
var Cascade = class _Cascade extends EventEmitter__default.default {
|
|
13659
14383
|
router;
|
|
13660
14384
|
toolRegistry;
|
|
14385
|
+
multimodal;
|
|
14386
|
+
mediaSink;
|
|
13661
14387
|
mcpClient;
|
|
13662
14388
|
codeIndex;
|
|
13663
14389
|
config;
|
|
@@ -13696,6 +14422,10 @@ var Cascade = class _Cascade extends EventEmitter__default.default {
|
|
|
13696
14422
|
}
|
|
13697
14423
|
});
|
|
13698
14424
|
this.toolRegistry = new ToolRegistry(this.config.tools, workspacePath);
|
|
14425
|
+
this.multimodal = new MultimodalRegistry(
|
|
14426
|
+
(this.config.providers ?? []).map((p) => p.type)
|
|
14427
|
+
);
|
|
14428
|
+
this.registerMediaTools(workspacePath);
|
|
13699
14429
|
this.telemetry = config.telemetry?.enabled ? new Telemetry(config.telemetry, config.telemetry.distinctId ?? "anonymous") : noopTelemetry;
|
|
13700
14430
|
this.worldStateDB = new WorldStateDB(this.workspacePath, this.config.workspace?.debugWorldState ?? false);
|
|
13701
14431
|
this.router.setWorldStateDB(this.worldStateDB);
|
|
@@ -13749,14 +14479,14 @@ var Cascade = class _Cascade extends EventEmitter__default.default {
|
|
|
13749
14479
|
if (this.listenerCount("mcp:approval-required") === 0) {
|
|
13750
14480
|
return false;
|
|
13751
14481
|
}
|
|
13752
|
-
return await new Promise((
|
|
13753
|
-
this.pendingMcpApprovals.set(server.name,
|
|
14482
|
+
return await new Promise((resolve2) => {
|
|
14483
|
+
this.pendingMcpApprovals.set(server.name, resolve2);
|
|
13754
14484
|
const timeout = setTimeout(() => {
|
|
13755
|
-
if (this.pendingMcpApprovals.delete(server.name))
|
|
14485
|
+
if (this.pendingMcpApprovals.delete(server.name)) resolve2(false);
|
|
13756
14486
|
}, 3e4);
|
|
13757
14487
|
const wrap = (approved) => {
|
|
13758
14488
|
clearTimeout(timeout);
|
|
13759
|
-
|
|
14489
|
+
resolve2(approved);
|
|
13760
14490
|
};
|
|
13761
14491
|
this.pendingMcpApprovals.set(server.name, wrap);
|
|
13762
14492
|
this.emit("mcp:approval-required", { server });
|
|
@@ -13830,17 +14560,17 @@ var Cascade = class _Cascade extends EventEmitter__default.default {
|
|
|
13830
14560
|
}
|
|
13831
14561
|
const t2Count = plan.sections.length;
|
|
13832
14562
|
const t3Count = plan.sections.reduce((sum, s) => sum + (s.t3Subtasks?.length ?? 0), 0);
|
|
13833
|
-
return await new Promise((
|
|
14563
|
+
return await new Promise((resolve2) => {
|
|
13834
14564
|
const timeout = setTimeout(() => {
|
|
13835
14565
|
if (this.pendingPlanApproval) {
|
|
13836
14566
|
this.pendingPlanApproval = void 0;
|
|
13837
|
-
|
|
14567
|
+
resolve2({ approved: true });
|
|
13838
14568
|
}
|
|
13839
14569
|
}, 12e4);
|
|
13840
14570
|
this.pendingPlanApproval = (decision) => {
|
|
13841
14571
|
clearTimeout(timeout);
|
|
13842
14572
|
this.pendingPlanApproval = void 0;
|
|
13843
|
-
|
|
14573
|
+
resolve2(decision);
|
|
13844
14574
|
};
|
|
13845
14575
|
this.emit("plan:approval-required", {
|
|
13846
14576
|
taskId,
|
|
@@ -13873,17 +14603,17 @@ var Cascade = class _Cascade extends EventEmitter__default.default {
|
|
|
13873
14603
|
}
|
|
13874
14604
|
async requestContextApproval(info) {
|
|
13875
14605
|
if (this.listenerCount("context:approval-required") === 0) return true;
|
|
13876
|
-
return await new Promise((
|
|
14606
|
+
return await new Promise((resolve2) => {
|
|
13877
14607
|
const timeout = setTimeout(() => {
|
|
13878
14608
|
if (this.pendingContextApproval) {
|
|
13879
14609
|
this.pendingContextApproval = void 0;
|
|
13880
|
-
|
|
14610
|
+
resolve2(true);
|
|
13881
14611
|
}
|
|
13882
14612
|
}, 12e4);
|
|
13883
14613
|
this.pendingContextApproval = (approved) => {
|
|
13884
14614
|
clearTimeout(timeout);
|
|
13885
14615
|
this.pendingContextApproval = void 0;
|
|
13886
|
-
|
|
14616
|
+
resolve2(approved);
|
|
13887
14617
|
};
|
|
13888
14618
|
this.emit("context:approval-required", info);
|
|
13889
14619
|
});
|
|
@@ -13963,6 +14693,68 @@ ${input}` }],
|
|
|
13963
14693
|
if (this.store) t1.setStore(this.store);
|
|
13964
14694
|
return t1.previewPlan(prompt);
|
|
13965
14695
|
}
|
|
14696
|
+
/**
|
|
14697
|
+
* Register image / speech / transcription tools for the modalities this
|
|
14698
|
+
* config can actually serve.
|
|
14699
|
+
*
|
|
14700
|
+
* The sink writes bytes into the workspace, because that is what a desktop or
|
|
14701
|
+
* CLI run wants and it keeps a generated asset indistinguishable from any
|
|
14702
|
+
* other file the run produced. A host with different storage (the cloud
|
|
14703
|
+
* server stores a file row) constructs the tools itself via buildMediaTools
|
|
14704
|
+
* with its own sink.
|
|
14705
|
+
*/
|
|
14706
|
+
/**
|
|
14707
|
+
* Replace where generated media is written.
|
|
14708
|
+
*
|
|
14709
|
+
* The default sink writes into the workspace, which is what a desktop or CLI
|
|
14710
|
+
* run wants. A host with different storage — the cloud server keeps a file
|
|
14711
|
+
* row plus bytes under the tenant's directory — supplies its own here, and
|
|
14712
|
+
* the media tools are rebuilt around it. Re-registering by name replaces the
|
|
14713
|
+
* existing tools rather than duplicating them.
|
|
14714
|
+
*/
|
|
14715
|
+
setMediaSink(sink) {
|
|
14716
|
+
this.mediaSink = sink;
|
|
14717
|
+
this.registerMediaTools(this.workspacePath);
|
|
14718
|
+
}
|
|
14719
|
+
registerMediaTools(workspacePath) {
|
|
14720
|
+
const denied = new Set(this.config.tools?.disabledTools ?? []);
|
|
14721
|
+
const permitted = (name) => !denied.has(name);
|
|
14722
|
+
const tools = buildMediaTools(
|
|
14723
|
+
{
|
|
14724
|
+
registry: this.multimodal,
|
|
14725
|
+
lookupProvider: (provider) => (this.config.providers ?? []).find((p) => p.type === provider),
|
|
14726
|
+
sink: this.mediaSink ?? (async (asset) => {
|
|
14727
|
+
const fsp = await import('fs/promises');
|
|
14728
|
+
const nodePath = await import('path');
|
|
14729
|
+
const dir = nodePath.join(workspacePath, "generated");
|
|
14730
|
+
await fsp.mkdir(dir, { recursive: true });
|
|
14731
|
+
const dest = nodePath.join(dir, asset.filename);
|
|
14732
|
+
await fsp.writeFile(dest, asset.data);
|
|
14733
|
+
return nodePath.relative(workspacePath, dest);
|
|
14734
|
+
})
|
|
14735
|
+
},
|
|
14736
|
+
async (p) => {
|
|
14737
|
+
const fsp = await import('fs/promises');
|
|
14738
|
+
return fsp.readFile(p);
|
|
14739
|
+
}
|
|
14740
|
+
);
|
|
14741
|
+
for (const tool of tools) {
|
|
14742
|
+
if (!permitted(tool.name)) continue;
|
|
14743
|
+
tool.setWorkspaceRoot(workspacePath);
|
|
14744
|
+
this.toolRegistry.register(tool);
|
|
14745
|
+
}
|
|
14746
|
+
}
|
|
14747
|
+
/**
|
|
14748
|
+
* Supply per-model thumbs-up/down counts so Auto routing can weigh how models
|
|
14749
|
+
* have actually performed for this user, on top of public benchmarks.
|
|
14750
|
+
*
|
|
14751
|
+
* Optional everywhere: with no source, or an empty record, scoring is
|
|
14752
|
+
* byte-identical to before. The adjustment is capped and sample-size shrunk —
|
|
14753
|
+
* see core/router/feedback-prior.ts.
|
|
14754
|
+
*/
|
|
14755
|
+
setFeedbackSource(source) {
|
|
14756
|
+
this.router.setFeedbackSource(source);
|
|
14757
|
+
}
|
|
13966
14758
|
/** True when a task stopped at the budget cap and can be resumed via /continue. */
|
|
13967
14759
|
hasResumableRun() {
|
|
13968
14760
|
return this.lastInterruptedRun != null;
|
|
@@ -14452,7 +15244,7 @@ ${prompt}` : prompt;
|
|
|
14452
15244
|
if (toolCreator) toolCreator.setPermissionEscalator(escalator);
|
|
14453
15245
|
const rootPrompt = buildContextualPrompt(options.prompt, options.conversationHistory);
|
|
14454
15246
|
let finalOutput = "";
|
|
14455
|
-
const runBreaker = new RunBreaker();
|
|
15247
|
+
const runBreaker = new RunBreaker(this.config.budget?.failureThreshold);
|
|
14456
15248
|
let t2Results = [];
|
|
14457
15249
|
let runError = null;
|
|
14458
15250
|
let identityPrompt = "";
|
|
@@ -16625,12 +17417,12 @@ var DashboardServer = class {
|
|
|
16625
17417
|
console.warn("\u26A0 Dashboard auth is DISABLED while bound to a non-loopback interface \u2014 this allows unauthenticated remote task execution.");
|
|
16626
17418
|
}
|
|
16627
17419
|
}
|
|
16628
|
-
await new Promise((
|
|
17420
|
+
await new Promise((resolve2, reject) => {
|
|
16629
17421
|
const onError = (err) => reject(err);
|
|
16630
17422
|
this.httpServer.once("error", onError);
|
|
16631
17423
|
this.httpServer.listen(this.port, this.host, () => {
|
|
16632
17424
|
this.httpServer.off("error", onError);
|
|
16633
|
-
|
|
17425
|
+
resolve2();
|
|
16634
17426
|
});
|
|
16635
17427
|
});
|
|
16636
17428
|
try {
|
|
@@ -16654,8 +17446,8 @@ var DashboardServer = class {
|
|
|
16654
17446
|
} catch {
|
|
16655
17447
|
}
|
|
16656
17448
|
this.globalStore = null;
|
|
16657
|
-
await new Promise((
|
|
16658
|
-
this.httpServer.close((err) => err ? reject(err) :
|
|
17449
|
+
await new Promise((resolve2, reject) => {
|
|
17450
|
+
this.httpServer.close((err) => err ? reject(err) : resolve2());
|
|
16659
17451
|
});
|
|
16660
17452
|
}
|
|
16661
17453
|
getSocket() {
|
|
@@ -16852,8 +17644,8 @@ var DashboardServer = class {
|
|
|
16852
17644
|
* timeout denies it.
|
|
16853
17645
|
*/
|
|
16854
17646
|
makeApprovalCallback(sessionId) {
|
|
16855
|
-
return (request) => new Promise((
|
|
16856
|
-
this.pendingApprovals.set(request.id, { resolve, sessionId });
|
|
17647
|
+
return (request) => new Promise((resolve2) => {
|
|
17648
|
+
this.pendingApprovals.set(request.id, { resolve: resolve2, sessionId });
|
|
16857
17649
|
});
|
|
16858
17650
|
}
|
|
16859
17651
|
/**
|
|
@@ -17953,7 +18745,7 @@ var CloudClient = class _CloudClient {
|
|
|
17953
18745
|
const deadline = Date.now() + start.expiresIn * 1e3;
|
|
17954
18746
|
while (Date.now() < deadline) {
|
|
17955
18747
|
if (opts.signal?.aborted) throw new Error("Sign-in cancelled.");
|
|
17956
|
-
await
|
|
18748
|
+
await sleep2(interval * 1e3, opts.signal);
|
|
17957
18749
|
const r = await this.pollDevice(start.deviceCode);
|
|
17958
18750
|
if (r === "slow_down") {
|
|
17959
18751
|
interval += 5;
|
|
@@ -18164,9 +18956,9 @@ var CloudClient = class _CloudClient {
|
|
|
18164
18956
|
this.store.clear();
|
|
18165
18957
|
}
|
|
18166
18958
|
};
|
|
18167
|
-
function
|
|
18168
|
-
return new Promise((
|
|
18169
|
-
const t = setTimeout(
|
|
18959
|
+
function sleep2(ms, signal) {
|
|
18960
|
+
return new Promise((resolve2, reject) => {
|
|
18961
|
+
const t = setTimeout(resolve2, ms);
|
|
18170
18962
|
signal?.addEventListener("abort", () => {
|
|
18171
18963
|
clearTimeout(t);
|
|
18172
18964
|
reject(new Error("aborted"));
|