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.js
CHANGED
|
@@ -1845,7 +1845,7 @@ function reconcilePrice(model, live) {
|
|
|
1845
1845
|
}
|
|
1846
1846
|
|
|
1847
1847
|
// src/constants.ts
|
|
1848
|
-
var CASCADE_VERSION = "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";
|
|
@@ -2358,46 +2358,37 @@ var BaseProvider = class {
|
|
|
2358
2358
|
};
|
|
2359
2359
|
|
|
2360
2360
|
// src/providers/model-filter.ts
|
|
2361
|
-
var
|
|
2362
|
-
"embed",
|
|
2361
|
+
var MODALITY_PATTERNS = [
|
|
2362
|
+
["embedding", ["embed"]],
|
|
2363
2363
|
// text-embedding-3, nomic-embed, mxbai-embed, snowflake-arctic-embed
|
|
2364
|
-
"(?:^|[-_/])
|
|
2365
|
-
"text-to-speech",
|
|
2366
|
-
"
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
"
|
|
2378
|
-
"
|
|
2379
|
-
"
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
"veo",
|
|
2390
|
-
"sora",
|
|
2391
|
-
"(?:^|[-_/])video(?:[-_/]|$)",
|
|
2392
|
-
// video
|
|
2393
|
-
"(?:^|[-_/])aqa(?:[-_/]|$)",
|
|
2394
|
-
// attributed QA
|
|
2395
|
-
"(?:^|[-_/])live(?:[-_/]|$)",
|
|
2396
|
-
// live-audio variants
|
|
2397
|
-
"(?:^|[-_/])(?:davinci|babbage)(?:[-_/]|$)"
|
|
2398
|
-
// legacy completion-only base models
|
|
2364
|
+
["transcription", ["whisper", "transcrib", "(?:^|[-_/])stt(?:[-_/]|$)"]],
|
|
2365
|
+
["speech", ["(?:^|[-_/])tts(?:[-_/]|$)", "text-to-speech", "speech"]],
|
|
2366
|
+
["video", ["veo", "sora", "(?:^|[-_/])video(?:[-_/]|$)"]],
|
|
2367
|
+
["image", [
|
|
2368
|
+
"dall[-]?e",
|
|
2369
|
+
"imagen",
|
|
2370
|
+
"image-generation",
|
|
2371
|
+
"gpt-image",
|
|
2372
|
+
"(?:^|[-_/])image(?:[-_/]|$)",
|
|
2373
|
+
"stable-diffusion",
|
|
2374
|
+
"sdxl",
|
|
2375
|
+
"flux"
|
|
2376
|
+
]],
|
|
2377
|
+
["moderation", ["moderation", "(?:^|[-_/])guard(?:[-_/]|$)"]],
|
|
2378
|
+
["rerank", ["rerank"]],
|
|
2379
|
+
["realtime", [
|
|
2380
|
+
"(?:^|[-_/])audio(?:[-_/]|$)",
|
|
2381
|
+
"native-audio",
|
|
2382
|
+
"realtime",
|
|
2383
|
+
"(?:^|[-_/])live(?:[-_/]|$)",
|
|
2384
|
+
// live-audio variants
|
|
2385
|
+
"(?:^|[-_/])aqa(?:[-_/]|$)"
|
|
2386
|
+
// attributed QA
|
|
2387
|
+
]],
|
|
2388
|
+
["legacy-completion", ["(?:^|[-_/])(?:davinci|babbage)(?:[-_/]|$)"]]
|
|
2399
2389
|
];
|
|
2400
|
-
|
|
2390
|
+
MODALITY_PATTERNS.map(([m, pats]) => [m, new RegExp(pats.join("|"), "i")]);
|
|
2391
|
+
var NON_CHAT = new RegExp(MODALITY_PATTERNS.flatMap(([, p]) => p).join("|"), "i");
|
|
2401
2392
|
function isChatModel(id, methods) {
|
|
2402
2393
|
if (methods && methods.length > 0) {
|
|
2403
2394
|
const canGenerate = methods.some((m) => /generatecontent|generatemessage|chat|completion/i.test(m));
|
|
@@ -2925,6 +2916,115 @@ var AzureOpenAIProvider = class extends OpenAIProvider {
|
|
|
2925
2916
|
}
|
|
2926
2917
|
}
|
|
2927
2918
|
};
|
|
2919
|
+
|
|
2920
|
+
// src/providers/gemini-schema.ts
|
|
2921
|
+
var ALLOWED_KEYS = /* @__PURE__ */ new Set([
|
|
2922
|
+
"type",
|
|
2923
|
+
"format",
|
|
2924
|
+
"title",
|
|
2925
|
+
"description",
|
|
2926
|
+
"nullable",
|
|
2927
|
+
"enum",
|
|
2928
|
+
"items",
|
|
2929
|
+
"properties",
|
|
2930
|
+
"required",
|
|
2931
|
+
"propertyOrdering",
|
|
2932
|
+
"minItems",
|
|
2933
|
+
"maxItems",
|
|
2934
|
+
"minProperties",
|
|
2935
|
+
"maxProperties",
|
|
2936
|
+
"minimum",
|
|
2937
|
+
"maximum",
|
|
2938
|
+
"minLength",
|
|
2939
|
+
"maxLength",
|
|
2940
|
+
"pattern",
|
|
2941
|
+
"example",
|
|
2942
|
+
"default",
|
|
2943
|
+
"anyOf"
|
|
2944
|
+
]);
|
|
2945
|
+
var ALLOWED_TYPES = /* @__PURE__ */ new Set(["string", "number", "integer", "boolean", "array", "object"]);
|
|
2946
|
+
function isPlainObject(v) {
|
|
2947
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
2948
|
+
}
|
|
2949
|
+
function normalizeType(value) {
|
|
2950
|
+
if (typeof value === "string") {
|
|
2951
|
+
return ALLOWED_TYPES.has(value) ? { type: value } : {};
|
|
2952
|
+
}
|
|
2953
|
+
if (Array.isArray(value)) {
|
|
2954
|
+
const types = value.filter((t) => typeof t === "string");
|
|
2955
|
+
const nullable = types.includes("null");
|
|
2956
|
+
const concrete = types.find((t) => t !== "null" && ALLOWED_TYPES.has(t));
|
|
2957
|
+
return {
|
|
2958
|
+
...concrete ? { type: concrete } : {},
|
|
2959
|
+
...nullable ? { nullable: true } : {}
|
|
2960
|
+
};
|
|
2961
|
+
}
|
|
2962
|
+
return {};
|
|
2963
|
+
}
|
|
2964
|
+
function normalizeEnum(value) {
|
|
2965
|
+
if (!Array.isArray(value) || value.length === 0) return void 0;
|
|
2966
|
+
return value.map((v) => typeof v === "string" ? v : JSON.stringify(v));
|
|
2967
|
+
}
|
|
2968
|
+
function sanitizeGeminiSchema(schema) {
|
|
2969
|
+
if (!isPlainObject(schema)) return void 0;
|
|
2970
|
+
const out = {};
|
|
2971
|
+
for (const [key, value] of Object.entries(schema)) {
|
|
2972
|
+
if (!ALLOWED_KEYS.has(key)) continue;
|
|
2973
|
+
switch (key) {
|
|
2974
|
+
case "type": {
|
|
2975
|
+
Object.assign(out, normalizeType(value));
|
|
2976
|
+
break;
|
|
2977
|
+
}
|
|
2978
|
+
case "enum": {
|
|
2979
|
+
const e = normalizeEnum(value);
|
|
2980
|
+
if (e) out["enum"] = e;
|
|
2981
|
+
break;
|
|
2982
|
+
}
|
|
2983
|
+
case "properties": {
|
|
2984
|
+
if (!isPlainObject(value)) break;
|
|
2985
|
+
const props = {};
|
|
2986
|
+
for (const [propName, propSchema] of Object.entries(value)) {
|
|
2987
|
+
const cleaned = sanitizeGeminiSchema(propSchema);
|
|
2988
|
+
props[propName] = cleaned && Object.keys(cleaned).length ? cleaned : { type: "string" };
|
|
2989
|
+
}
|
|
2990
|
+
out["properties"] = props;
|
|
2991
|
+
break;
|
|
2992
|
+
}
|
|
2993
|
+
case "items": {
|
|
2994
|
+
const cleaned = sanitizeGeminiSchema(value);
|
|
2995
|
+
if (cleaned) out["items"] = cleaned;
|
|
2996
|
+
break;
|
|
2997
|
+
}
|
|
2998
|
+
case "anyOf": {
|
|
2999
|
+
if (!Array.isArray(value)) break;
|
|
3000
|
+
const variants = value.map((v) => sanitizeGeminiSchema(v)).filter((v) => !!v && Object.keys(v).length > 0);
|
|
3001
|
+
if (variants.length) out["anyOf"] = variants;
|
|
3002
|
+
break;
|
|
3003
|
+
}
|
|
3004
|
+
case "required": {
|
|
3005
|
+
if (Array.isArray(value)) {
|
|
3006
|
+
out["required"] = value.filter((r) => typeof r === "string");
|
|
3007
|
+
}
|
|
3008
|
+
break;
|
|
3009
|
+
}
|
|
3010
|
+
default:
|
|
3011
|
+
out[key] = value;
|
|
3012
|
+
}
|
|
3013
|
+
}
|
|
3014
|
+
if (!out["type"] && out["properties"]) out["type"] = "object";
|
|
3015
|
+
if (!out["type"] && out["items"]) out["type"] = "array";
|
|
3016
|
+
return out;
|
|
3017
|
+
}
|
|
3018
|
+
function toGeminiParameters(inputSchema) {
|
|
3019
|
+
const cleaned = sanitizeGeminiSchema(inputSchema);
|
|
3020
|
+
if (!cleaned) return void 0;
|
|
3021
|
+
const props = cleaned["properties"];
|
|
3022
|
+
if (isPlainObject(props) && Object.keys(props).length === 0) return void 0;
|
|
3023
|
+
if (Object.keys(cleaned).length === 0) return void 0;
|
|
3024
|
+
return cleaned;
|
|
3025
|
+
}
|
|
3026
|
+
|
|
3027
|
+
// src/providers/gemini.ts
|
|
2928
3028
|
var GeminiProvider = class extends BaseProvider {
|
|
2929
3029
|
client;
|
|
2930
3030
|
constructor(config, model) {
|
|
@@ -3145,7 +3245,12 @@ var GeminiProvider = class extends BaseProvider {
|
|
|
3145
3245
|
return {
|
|
3146
3246
|
name: tool.name,
|
|
3147
3247
|
description: tool.description,
|
|
3148
|
-
parameters
|
|
3248
|
+
// Convert, never cast. Gemini's parameters field is an OpenAPI subset
|
|
3249
|
+
// that 400s on unknown keys, and MCP servers ship schemas carrying their
|
|
3250
|
+
// own extensions (GitHub's adds `x-mcp-header` to every header-bound
|
|
3251
|
+
// property). Casting sent those through verbatim and every request failed
|
|
3252
|
+
// before the model saw it. See providers/gemini-schema.ts.
|
|
3253
|
+
parameters: toGeminiParameters(tool.inputSchema)
|
|
3149
3254
|
};
|
|
3150
3255
|
}
|
|
3151
3256
|
};
|
|
@@ -3173,7 +3278,7 @@ async function nodeHttpFetch(input, init = {}, redirectCount = 0) {
|
|
|
3173
3278
|
headers["accept-encoding"] = "gzip, deflate, br";
|
|
3174
3279
|
}
|
|
3175
3280
|
const body = init.body == null ? void 0 : typeof init.body === "string" ? init.body : Buffer.from(init.body);
|
|
3176
|
-
return new Promise((
|
|
3281
|
+
return new Promise((resolve2, reject) => {
|
|
3177
3282
|
const req = lib.request(
|
|
3178
3283
|
{
|
|
3179
3284
|
hostname: u.hostname,
|
|
@@ -3190,7 +3295,7 @@ async function nodeHttpFetch(input, init = {}, redirectCount = 0) {
|
|
|
3190
3295
|
const nextUrl = new URL(location, u).href;
|
|
3191
3296
|
const downgrade = status === 303 || (status === 301 || status === 302) && method !== "GET" && method !== "HEAD";
|
|
3192
3297
|
const nextInit = downgrade ? { ...init, method: "GET", body: void 0 } : init;
|
|
3193
|
-
|
|
3298
|
+
resolve2(nodeHttpFetch(nextUrl, nextInit, redirectCount + 1));
|
|
3194
3299
|
return;
|
|
3195
3300
|
}
|
|
3196
3301
|
const encoding = (res.headers["content-encoding"] ?? "").toLowerCase();
|
|
@@ -3205,7 +3310,7 @@ async function nodeHttpFetch(input, init = {}, redirectCount = 0) {
|
|
|
3205
3310
|
if (Array.isArray(v)) respHeaders.set(k, v.join(", "));
|
|
3206
3311
|
else if (typeof v === "string") respHeaders.set(k, v);
|
|
3207
3312
|
}
|
|
3208
|
-
|
|
3313
|
+
resolve2(new Response(stream, {
|
|
3209
3314
|
status,
|
|
3210
3315
|
statusText: res.statusMessage ?? "",
|
|
3211
3316
|
headers: respHeaders
|
|
@@ -3850,7 +3955,7 @@ var TpmLimiter = class {
|
|
|
3850
3955
|
}
|
|
3851
3956
|
const deficit = want - bucket.available;
|
|
3852
3957
|
const waitMs = Math.max(50, Math.ceil(deficit / bucket.tokensPerMinute * 6e4));
|
|
3853
|
-
await new Promise((
|
|
3958
|
+
await new Promise((resolve2) => setTimeout(resolve2, waitMs));
|
|
3854
3959
|
}
|
|
3855
3960
|
}
|
|
3856
3961
|
/**
|
|
@@ -3913,14 +4018,14 @@ var LocalRequestQueue = class {
|
|
|
3913
4018
|
this.active++;
|
|
3914
4019
|
return this.makeRelease();
|
|
3915
4020
|
}
|
|
3916
|
-
return new Promise((
|
|
4021
|
+
return new Promise((resolve2, reject) => {
|
|
3917
4022
|
let settled = false;
|
|
3918
4023
|
let timer;
|
|
3919
4024
|
const resolver = (release) => {
|
|
3920
4025
|
if (settled) return;
|
|
3921
4026
|
settled = true;
|
|
3922
4027
|
if (timer !== void 0) clearTimeout(timer);
|
|
3923
|
-
|
|
4028
|
+
resolve2(release);
|
|
3924
4029
|
};
|
|
3925
4030
|
if (timeoutMs !== void 0 && timeoutMs > 0) {
|
|
3926
4031
|
timer = setTimeout(() => {
|
|
@@ -4551,6 +4656,7 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter {
|
|
|
4551
4656
|
tpmLimiter;
|
|
4552
4657
|
localQueue;
|
|
4553
4658
|
taskAnalyzer;
|
|
4659
|
+
pendingFeedbackSource;
|
|
4554
4660
|
worldStateDB;
|
|
4555
4661
|
privacyPaths;
|
|
4556
4662
|
guidanceQueue;
|
|
@@ -5005,8 +5111,18 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter {
|
|
|
5005
5111
|
return windows.length ? Math.min(...windows) : void 0;
|
|
5006
5112
|
}
|
|
5007
5113
|
/** Wire the Cascade Auto task analyzer used for per-subtask model routing. */
|
|
5114
|
+
/**
|
|
5115
|
+
* Feed per-model thumbs counts into Auto routing. Forwarded to the analyzer
|
|
5116
|
+
* because that is where the public benchmark score is combined; the prior
|
|
5117
|
+
* adjusts that number rather than sitting anywhere else in the pipeline.
|
|
5118
|
+
*/
|
|
5119
|
+
setFeedbackSource(source) {
|
|
5120
|
+
this.taskAnalyzer?.setFeedbackSource(source);
|
|
5121
|
+
this.pendingFeedbackSource = source;
|
|
5122
|
+
}
|
|
5008
5123
|
setTaskAnalyzer(analyzer) {
|
|
5009
5124
|
this.taskAnalyzer = analyzer;
|
|
5125
|
+
if (this.pendingFeedbackSource) analyzer.setFeedbackSource(this.pendingFeedbackSource);
|
|
5010
5126
|
}
|
|
5011
5127
|
setWorldStateDB(db) {
|
|
5012
5128
|
this.worldStateDB = db;
|
|
@@ -6100,8 +6216,11 @@ Now execute your subtask using this context where relevant.`
|
|
|
6100
6216
|
const result = await this.runAgentLoop(systemPrompt, this.tools);
|
|
6101
6217
|
output = result.output;
|
|
6102
6218
|
toolCalls = result.toolCalls;
|
|
6103
|
-
this.
|
|
6104
|
-
|
|
6219
|
+
const mustProduceArtifact = this.requiresArtifact();
|
|
6220
|
+
if (mustProduceArtifact) {
|
|
6221
|
+
this.sendStatusUpdate({ progressPct: 65, currentAction: "Verifying required artifacts", status: "IN_PROGRESS" });
|
|
6222
|
+
}
|
|
6223
|
+
const artifactCheck = mustProduceArtifact ? await this.verifyArtifacts(assignment) : { ok: true, issues: [] };
|
|
6105
6224
|
if (!artifactCheck.ok) {
|
|
6106
6225
|
correctionAttempts = 1;
|
|
6107
6226
|
issues.push(...artifactCheck.issues);
|
|
@@ -6381,11 +6500,11 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : "") + textToolSuffix,
|
|
|
6381
6500
|
if (!wasApproved) return `Tool ${tc.name} was denied by user.`;
|
|
6382
6501
|
} else {
|
|
6383
6502
|
const LEGACY_APPROVAL_TIMEOUT_MS = 6e5;
|
|
6384
|
-
const legacyDecision = await new Promise((
|
|
6503
|
+
const legacyDecision = await new Promise((resolve2) => {
|
|
6385
6504
|
const eventName = `tool:approval-response:${this.id}-${tc.id}`;
|
|
6386
6505
|
const timer = setTimeout(() => {
|
|
6387
6506
|
this.removeAllListeners(eventName);
|
|
6388
|
-
|
|
6507
|
+
resolve2({ approved: false });
|
|
6389
6508
|
}, LEGACY_APPROVAL_TIMEOUT_MS);
|
|
6390
6509
|
timer.unref?.();
|
|
6391
6510
|
this.emit("tool:approval-request", {
|
|
@@ -6398,7 +6517,7 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : "") + textToolSuffix,
|
|
|
6398
6517
|
});
|
|
6399
6518
|
this.once(eventName, (d) => {
|
|
6400
6519
|
clearTimeout(timer);
|
|
6401
|
-
|
|
6520
|
+
resolve2(d);
|
|
6402
6521
|
});
|
|
6403
6522
|
});
|
|
6404
6523
|
if (legacyDecision.always) this.sessionApprovals.set(tc.name, legacyDecision.approved);
|
|
@@ -6418,6 +6537,9 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : "") + textToolSuffix,
|
|
|
6418
6537
|
tierId: this.id,
|
|
6419
6538
|
sessionId: this.taskId,
|
|
6420
6539
|
requireApproval: false,
|
|
6540
|
+
// Media generation can run for a minute; without this a cancelled run
|
|
6541
|
+
// still pays for an image nobody will see.
|
|
6542
|
+
...this.signal ? { signal: this.signal } : {},
|
|
6421
6543
|
saveSnapshot: async (path26, content) => {
|
|
6422
6544
|
this.store?.addFileSnapshot(this.taskId, path26, content);
|
|
6423
6545
|
},
|
|
@@ -6465,7 +6587,8 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : "") + textToolSuffix,
|
|
|
6465
6587
|
const result = await this.toolRegistry.execute(altTool, tc.input, {
|
|
6466
6588
|
tierId: this.id,
|
|
6467
6589
|
sessionId: this.taskId,
|
|
6468
|
-
requireApproval: false
|
|
6590
|
+
requireApproval: false,
|
|
6591
|
+
...this.signal ? { signal: this.signal } : {}
|
|
6469
6592
|
});
|
|
6470
6593
|
const str = typeof result === "string" ? result : JSON.stringify(result);
|
|
6471
6594
|
if (!str.startsWith("Tool error:") && !str.startsWith("Error:")) {
|
|
@@ -6487,7 +6610,8 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : "") + textToolSuffix,
|
|
|
6487
6610
|
const result = await this.toolRegistry.execute(newToolName, tc.input, {
|
|
6488
6611
|
tierId: this.id,
|
|
6489
6612
|
sessionId: this.taskId,
|
|
6490
|
-
requireApproval: false
|
|
6613
|
+
requireApproval: false,
|
|
6614
|
+
...this.signal ? { signal: this.signal } : {}
|
|
6491
6615
|
});
|
|
6492
6616
|
const str = typeof result === "string" ? result : JSON.stringify(result);
|
|
6493
6617
|
if (!str.startsWith("Tool error:")) return `[Synthesized ${newToolName}]: ${str}`;
|
|
@@ -6905,7 +7029,7 @@ var PeerBus = class extends EventEmitter {
|
|
|
6905
7029
|
this.outputs.set(subtaskId, entry);
|
|
6906
7030
|
this.emit("output:ready", entry);
|
|
6907
7031
|
const waiting = this.waiters.get(subtaskId) ?? [];
|
|
6908
|
-
for (const
|
|
7032
|
+
for (const resolve2 of waiting) resolve2(entry);
|
|
6909
7033
|
this.waiters.delete(subtaskId);
|
|
6910
7034
|
}
|
|
6911
7035
|
/**
|
|
@@ -6936,10 +7060,10 @@ var PeerBus = class extends EventEmitter {
|
|
|
6936
7060
|
waitFor(subtaskId, timeoutMs = 12e4) {
|
|
6937
7061
|
const existing = this.outputs.get(subtaskId);
|
|
6938
7062
|
if (existing && !this.retryPending.has(subtaskId)) return Promise.resolve(existing);
|
|
6939
|
-
return new Promise((
|
|
7063
|
+
return new Promise((resolve2, reject) => {
|
|
6940
7064
|
const resolver = (output) => {
|
|
6941
7065
|
clearTimeout(timer);
|
|
6942
|
-
|
|
7066
|
+
resolve2(output);
|
|
6943
7067
|
};
|
|
6944
7068
|
const timer = setTimeout(() => {
|
|
6945
7069
|
const waiting = this.waiters.get(subtaskId);
|
|
@@ -6993,10 +7117,10 @@ var PeerBus = class extends EventEmitter {
|
|
|
6993
7117
|
*/
|
|
6994
7118
|
collect(timeoutMs) {
|
|
6995
7119
|
const collected = [...this.broadcastLog];
|
|
6996
|
-
return new Promise((
|
|
7120
|
+
return new Promise((resolve2) => {
|
|
6997
7121
|
const timer = setTimeout(() => {
|
|
6998
7122
|
off();
|
|
6999
|
-
|
|
7123
|
+
resolve2(collected);
|
|
7000
7124
|
}, timeoutMs);
|
|
7001
7125
|
const handler = (msg) => {
|
|
7002
7126
|
collected.push({ fromId: msg.fromId, payload: msg.payload, timestamp: msg.timestamp });
|
|
@@ -7010,7 +7134,7 @@ var PeerBus = class extends EventEmitter {
|
|
|
7010
7134
|
const broadcasters = new Set(collected.map((m) => m.fromId));
|
|
7011
7135
|
if (broadcasters.size >= this.members.size) {
|
|
7012
7136
|
off();
|
|
7013
|
-
|
|
7137
|
+
resolve2(collected);
|
|
7014
7138
|
}
|
|
7015
7139
|
};
|
|
7016
7140
|
this.on("broadcast", checkComplete);
|
|
@@ -7028,7 +7152,7 @@ var PeerBus = class extends EventEmitter {
|
|
|
7028
7152
|
return;
|
|
7029
7153
|
}
|
|
7030
7154
|
this.emitCoordination(tierId, `\u23F3 waiting for ${filePath} (held by ${existing.holderId})`);
|
|
7031
|
-
return new Promise((
|
|
7155
|
+
return new Promise((resolve2, reject) => {
|
|
7032
7156
|
const timer = setTimeout(() => {
|
|
7033
7157
|
reject(new Error(`File lock timeout for ${filePath} (held by ${existing.holderId})`));
|
|
7034
7158
|
}, timeoutMs);
|
|
@@ -7036,7 +7160,7 @@ var PeerBus = class extends EventEmitter {
|
|
|
7036
7160
|
clearTimeout(timer);
|
|
7037
7161
|
this.fileLocks.set(filePath, { holderId: tierId, lockedAt: (/* @__PURE__ */ new Date()).toISOString(), waiters: [] });
|
|
7038
7162
|
this.emitCoordination(tierId, `\u{1F512} locked ${filePath}`);
|
|
7039
|
-
|
|
7163
|
+
resolve2();
|
|
7040
7164
|
});
|
|
7041
7165
|
});
|
|
7042
7166
|
}
|
|
@@ -7061,11 +7185,11 @@ var PeerBus = class extends EventEmitter {
|
|
|
7061
7185
|
*/
|
|
7062
7186
|
waitForFileRelease(filePath, timeoutMs = 3e4) {
|
|
7063
7187
|
if (!this.fileLocks.has(filePath)) return Promise.resolve();
|
|
7064
|
-
return new Promise((
|
|
7188
|
+
return new Promise((resolve2, reject) => {
|
|
7065
7189
|
const timer = setTimeout(() => reject(new Error(`Timeout waiting for file release: ${filePath}`)), timeoutMs);
|
|
7066
7190
|
this.once(`file:released:${filePath}`, () => {
|
|
7067
7191
|
clearTimeout(timer);
|
|
7068
|
-
|
|
7192
|
+
resolve2();
|
|
7069
7193
|
});
|
|
7070
7194
|
});
|
|
7071
7195
|
}
|
|
@@ -7131,13 +7255,13 @@ var PeerBus = class extends EventEmitter {
|
|
|
7131
7255
|
this.emit(`barrier:${barrierName}`);
|
|
7132
7256
|
return;
|
|
7133
7257
|
}
|
|
7134
|
-
return new Promise((
|
|
7258
|
+
return new Promise((resolve2, reject) => {
|
|
7135
7259
|
const timer = setTimeout(() => {
|
|
7136
7260
|
reject(new Error(`Barrier timeout: ${barrierName} (${bar.arrived.size}/${bar.total} arrived)`));
|
|
7137
7261
|
}, 12e4);
|
|
7138
7262
|
this.once(`barrier:${barrierName}`, () => {
|
|
7139
7263
|
clearTimeout(timer);
|
|
7140
|
-
|
|
7264
|
+
resolve2();
|
|
7141
7265
|
});
|
|
7142
7266
|
});
|
|
7143
7267
|
}
|
|
@@ -8682,6 +8806,170 @@ Reply with exactly one word: YES, NO, or UNSURE.
|
|
|
8682
8806
|
}
|
|
8683
8807
|
};
|
|
8684
8808
|
|
|
8809
|
+
// src/core/multimodal/registry.ts
|
|
8810
|
+
var CAPABILITIES = [
|
|
8811
|
+
// ── Image ──
|
|
8812
|
+
{
|
|
8813
|
+
modality: "image",
|
|
8814
|
+
provider: "openai",
|
|
8815
|
+
modelId: "dall-e-3",
|
|
8816
|
+
api: "openai-images"
|
|
8817
|
+
},
|
|
8818
|
+
{
|
|
8819
|
+
modality: "image",
|
|
8820
|
+
provider: "gemini",
|
|
8821
|
+
modelId: "gemini-3-pro-image",
|
|
8822
|
+
api: "gemini-predict"
|
|
8823
|
+
},
|
|
8824
|
+
// ── Speech (text → audio) ──
|
|
8825
|
+
{
|
|
8826
|
+
modality: "speech",
|
|
8827
|
+
provider: "openai",
|
|
8828
|
+
modelId: "tts-1",
|
|
8829
|
+
api: "openai-speech"
|
|
8830
|
+
},
|
|
8831
|
+
{
|
|
8832
|
+
// The only quality claim in this table that needs no leaderboard: OpenAI
|
|
8833
|
+
// ships these as the same voice at two fidelities and charges double for
|
|
8834
|
+
// this one. That is the vendor's own ordering, not an inference.
|
|
8835
|
+
modality: "speech",
|
|
8836
|
+
provider: "openai",
|
|
8837
|
+
modelId: "tts-1-hd",
|
|
8838
|
+
api: "openai-speech",
|
|
8839
|
+
quality: 70,
|
|
8840
|
+
qualityBasis: "OpenAI's own hd variant of tts-1, priced 2x"
|
|
8841
|
+
},
|
|
8842
|
+
// ── Transcription (audio → text) ──
|
|
8843
|
+
{
|
|
8844
|
+
modality: "transcription",
|
|
8845
|
+
provider: "openai",
|
|
8846
|
+
modelId: "whisper-1",
|
|
8847
|
+
api: "openai-transcriptions"
|
|
8848
|
+
},
|
|
8849
|
+
// ── Video ──
|
|
8850
|
+
{
|
|
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`.
|
|
8854
|
+
modality: "video",
|
|
8855
|
+
provider: "gemini",
|
|
8856
|
+
modelId: "veo-3.1-generate-001",
|
|
8857
|
+
api: "gemini-predict-lro"
|
|
8858
|
+
}
|
|
8859
|
+
];
|
|
8860
|
+
function capabilityCost(cap) {
|
|
8861
|
+
const match = findPricing(cap.modelId, cap.provider, {
|
|
8862
|
+
fallbackProviders: cap.provider === "gemini" ? ["vertex"] : []
|
|
8863
|
+
});
|
|
8864
|
+
const rate = match?.entry.rates?.[0];
|
|
8865
|
+
if (!rate) return null;
|
|
8866
|
+
const unitLabel = {
|
|
8867
|
+
per_image: "per image",
|
|
8868
|
+
per_second: "per second of video",
|
|
8869
|
+
per_character: "per character",
|
|
8870
|
+
per_audio_minute: "per audio minute"
|
|
8871
|
+
};
|
|
8872
|
+
const unit = unitLabel[rate.unit] ?? rate.unit;
|
|
8873
|
+
const amount = rate.amount;
|
|
8874
|
+
const shown = amount < 1e-3 ? amount.toExponential(2) : amount.toFixed(4);
|
|
8875
|
+
return { label: `$${shown} ${unit}`, amount, unit };
|
|
8876
|
+
}
|
|
8877
|
+
var MultimodalRegistry = class {
|
|
8878
|
+
/** Provider types the user has actually configured a key for. */
|
|
8879
|
+
available;
|
|
8880
|
+
constructor(availableProviders) {
|
|
8881
|
+
this.available = new Set(availableProviders);
|
|
8882
|
+
}
|
|
8883
|
+
/** Every capability whose provider is configured AND which Cascade can call. */
|
|
8884
|
+
usable(modality) {
|
|
8885
|
+
return CAPABILITIES.filter(
|
|
8886
|
+
(c) => !c.unsupported && this.available.has(c.provider) && (!modality || c.modality === modality)
|
|
8887
|
+
);
|
|
8888
|
+
}
|
|
8889
|
+
/** Modalities this account can actually generate right now. */
|
|
8890
|
+
availableModalities() {
|
|
8891
|
+
return [...new Set(this.usable().map((c) => c.modality))];
|
|
8892
|
+
}
|
|
8893
|
+
/**
|
|
8894
|
+
* Pick a model for a modality.
|
|
8895
|
+
*
|
|
8896
|
+
* Ranked models beat unranked ones, then higher quality, then lower unit
|
|
8897
|
+
* price. An explicit `preferModel` wins outright when it is usable — the user
|
|
8898
|
+
* naming a model is a stronger signal than any prior.
|
|
8899
|
+
*/
|
|
8900
|
+
select(modality, opts = {}) {
|
|
8901
|
+
const pool = this.usable(modality);
|
|
8902
|
+
if (!pool.length) return null;
|
|
8903
|
+
if (opts.preferModel) {
|
|
8904
|
+
const wanted = pool.find((c) => c.modelId === opts.preferModel);
|
|
8905
|
+
if (wanted) {
|
|
8906
|
+
return { capability: wanted, cost: capabilityCost(wanted), reason: `Requested explicitly: ${wanted.modelId}.` };
|
|
8907
|
+
}
|
|
8908
|
+
}
|
|
8909
|
+
const scored = pool.map((c) => ({ c, cost: capabilityCost(c) })).sort((a, b) => {
|
|
8910
|
+
const qa = a.c.quality ?? -1;
|
|
8911
|
+
const qb = b.c.quality ?? -1;
|
|
8912
|
+
if (qa !== qb) return qb - qa;
|
|
8913
|
+
const pa = a.cost?.amount ?? Number.POSITIVE_INFINITY;
|
|
8914
|
+
const pb = b.cost?.amount ?? Number.POSITIVE_INFINITY;
|
|
8915
|
+
return pa - pb;
|
|
8916
|
+
});
|
|
8917
|
+
const best = scored[0];
|
|
8918
|
+
const ranked = best.c.quality !== void 0;
|
|
8919
|
+
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.";
|
|
8920
|
+
return { capability: best.c, cost: best.cost, reason };
|
|
8921
|
+
}
|
|
8922
|
+
/**
|
|
8923
|
+
* A plain-language account of what this account can and cannot generate —
|
|
8924
|
+
* including the things it cannot, and why. The planner sees this, so being
|
|
8925
|
+
* candid here is what stops it inventing a `generate_music` call.
|
|
8926
|
+
*/
|
|
8927
|
+
describe() {
|
|
8928
|
+
const lines = [];
|
|
8929
|
+
const usable = this.usable();
|
|
8930
|
+
if (usable.length) {
|
|
8931
|
+
lines.push("Generation models available on your configured providers:");
|
|
8932
|
+
for (const modality of ["image", "speech", "transcription", "video"]) {
|
|
8933
|
+
const forModality = usable.filter((c) => c.modality === modality);
|
|
8934
|
+
if (!forModality.length) continue;
|
|
8935
|
+
const shown = forModality.map((c) => {
|
|
8936
|
+
const cost = capabilityCost(c);
|
|
8937
|
+
return `${c.modelId}${cost ? ` (${cost.label})` : ""}`;
|
|
8938
|
+
});
|
|
8939
|
+
lines.push(` ${modality}: ${shown.join(", ")}`);
|
|
8940
|
+
}
|
|
8941
|
+
} else {
|
|
8942
|
+
lines.push("No generation models are available \u2014 none of your configured providers offers one.");
|
|
8943
|
+
}
|
|
8944
|
+
const blocked = CAPABILITIES.filter((c) => c.unsupported && this.available.has(c.provider));
|
|
8945
|
+
for (const c of blocked) {
|
|
8946
|
+
lines.push(` ${c.modality}: ${c.modelId} exists on your account but is not callable. ${c.unsupported}`);
|
|
8947
|
+
}
|
|
8948
|
+
lines.push(
|
|
8949
|
+
" music: no supported provider exposes a music-generation API, so Cascade cannot generate music."
|
|
8950
|
+
);
|
|
8951
|
+
return lines.join("\n");
|
|
8952
|
+
}
|
|
8953
|
+
};
|
|
8954
|
+
|
|
8955
|
+
// src/tools/base.ts
|
|
8956
|
+
var BaseTool = class {
|
|
8957
|
+
workspaceRoot = process.cwd();
|
|
8958
|
+
setWorkspaceRoot(root) {
|
|
8959
|
+
this.workspaceRoot = root;
|
|
8960
|
+
}
|
|
8961
|
+
isDangerous() {
|
|
8962
|
+
return false;
|
|
8963
|
+
}
|
|
8964
|
+
getDefinition() {
|
|
8965
|
+
return {
|
|
8966
|
+
name: this.name,
|
|
8967
|
+
description: this.description,
|
|
8968
|
+
inputSchema: this.inputSchema
|
|
8969
|
+
};
|
|
8970
|
+
}
|
|
8971
|
+
};
|
|
8972
|
+
|
|
8685
8973
|
// src/core/router/provider-errors.ts
|
|
8686
8974
|
var SYSTEMIC = /* @__PURE__ */ new Set([
|
|
8687
8975
|
"rate_limit",
|
|
@@ -8746,7 +9034,7 @@ function classifyProviderError(err) {
|
|
|
8746
9034
|
return { kind, systemic: isSystemicKind(kind), status, raw };
|
|
8747
9035
|
}
|
|
8748
9036
|
function describeProviderError(c, modelId) {
|
|
8749
|
-
const on = ` on ${modelId}` ;
|
|
9037
|
+
const on = modelId ? ` on ${modelId}` : "";
|
|
8750
9038
|
const detail = c.raw ? ` Provider said: ${c.raw}` : "";
|
|
8751
9039
|
switch (c.kind) {
|
|
8752
9040
|
case "rate_limit":
|
|
@@ -8768,6 +9056,417 @@ function describeProviderError(c, modelId) {
|
|
|
8768
9056
|
}
|
|
8769
9057
|
}
|
|
8770
9058
|
|
|
9059
|
+
// src/core/multimodal/generate.ts
|
|
9060
|
+
var DEFAULT_TIMEOUT_MS = 12e4;
|
|
9061
|
+
async function callProvider(fn, modelId) {
|
|
9062
|
+
try {
|
|
9063
|
+
return await fn();
|
|
9064
|
+
} catch (err) {
|
|
9065
|
+
const classified = classifyProviderError(err);
|
|
9066
|
+
throw new Error(describeProviderError(classified, modelId));
|
|
9067
|
+
}
|
|
9068
|
+
}
|
|
9069
|
+
async function postJson(url, body, headers, signal) {
|
|
9070
|
+
const ac = new AbortController();
|
|
9071
|
+
const timer = setTimeout(() => ac.abort(), DEFAULT_TIMEOUT_MS);
|
|
9072
|
+
const onAbort = () => ac.abort();
|
|
9073
|
+
signal?.addEventListener("abort", onAbort);
|
|
9074
|
+
try {
|
|
9075
|
+
const res = await fetch(url, {
|
|
9076
|
+
method: "POST",
|
|
9077
|
+
headers: { "Content-Type": "application/json", ...headers },
|
|
9078
|
+
body: JSON.stringify(body),
|
|
9079
|
+
signal: ac.signal
|
|
9080
|
+
});
|
|
9081
|
+
if (!res.ok) {
|
|
9082
|
+
const text = await res.text().catch(() => "");
|
|
9083
|
+
throw Object.assign(new Error(text || res.statusText), { status: res.status });
|
|
9084
|
+
}
|
|
9085
|
+
return res;
|
|
9086
|
+
} finally {
|
|
9087
|
+
clearTimeout(timer);
|
|
9088
|
+
signal?.removeEventListener("abort", onAbort);
|
|
9089
|
+
}
|
|
9090
|
+
}
|
|
9091
|
+
function baseUrlFor(cfg, fallback) {
|
|
9092
|
+
return (cfg.baseUrl ?? fallback).replace(/\/+$/, "");
|
|
9093
|
+
}
|
|
9094
|
+
function stamp(ext) {
|
|
9095
|
+
return `cascade-${Date.now()}.${ext}`;
|
|
9096
|
+
}
|
|
9097
|
+
async function generateImage(cap, cfg, req, signal) {
|
|
9098
|
+
return callProvider(async () => {
|
|
9099
|
+
if (cap.api === "openai-images") {
|
|
9100
|
+
const url = `${baseUrlFor(cfg, "https://api.openai.com/v1")}/images/generations`;
|
|
9101
|
+
const res = await postJson(url, {
|
|
9102
|
+
model: cap.modelId,
|
|
9103
|
+
prompt: req.prompt,
|
|
9104
|
+
size: req.size ?? "1024x1024",
|
|
9105
|
+
n: 1,
|
|
9106
|
+
// b64 rather than a URL: see the file header — provider URLs expire.
|
|
9107
|
+
response_format: "b64_json"
|
|
9108
|
+
}, { Authorization: `Bearer ${cfg.apiKey ?? ""}` }, signal);
|
|
9109
|
+
const body = await res.json();
|
|
9110
|
+
const first = body.data?.[0];
|
|
9111
|
+
if (!first?.b64_json) throw new Error("Image response contained no image data.");
|
|
9112
|
+
return {
|
|
9113
|
+
data: Buffer.from(first.b64_json, "base64"),
|
|
9114
|
+
mimeType: "image/png",
|
|
9115
|
+
filename: stamp("png"),
|
|
9116
|
+
modelId: cap.modelId,
|
|
9117
|
+
provider: cap.provider,
|
|
9118
|
+
...first.revised_prompt ? { revisedPrompt: first.revised_prompt } : {}
|
|
9119
|
+
};
|
|
9120
|
+
}
|
|
9121
|
+
if (cap.api === "gemini-predict") {
|
|
9122
|
+
const base = baseUrlFor(cfg, "https://generativelanguage.googleapis.com/v1beta");
|
|
9123
|
+
const url = `${base}/models/${encodeURIComponent(cap.modelId)}:predict`;
|
|
9124
|
+
const res = await postJson(url, {
|
|
9125
|
+
instances: [{ prompt: req.prompt }],
|
|
9126
|
+
parameters: { sampleCount: 1 }
|
|
9127
|
+
}, { "x-goog-api-key": cfg.apiKey ?? "" }, signal);
|
|
9128
|
+
const body = await res.json();
|
|
9129
|
+
const first = body.predictions?.[0];
|
|
9130
|
+
if (!first?.bytesBase64Encoded) throw new Error("Image response contained no image data.");
|
|
9131
|
+
const mimeType = first.mimeType ?? "image/png";
|
|
9132
|
+
return {
|
|
9133
|
+
data: Buffer.from(first.bytesBase64Encoded, "base64"),
|
|
9134
|
+
mimeType,
|
|
9135
|
+
filename: stamp(mimeType.includes("jpeg") ? "jpg" : "png"),
|
|
9136
|
+
modelId: cap.modelId,
|
|
9137
|
+
provider: cap.provider
|
|
9138
|
+
};
|
|
9139
|
+
}
|
|
9140
|
+
throw new Error(`${cap.modelId} is not an image generator.`);
|
|
9141
|
+
}, cap.modelId);
|
|
9142
|
+
}
|
|
9143
|
+
async function generateSpeech(cap, cfg, req, signal) {
|
|
9144
|
+
return callProvider(async () => {
|
|
9145
|
+
if (cap.api !== "openai-speech") throw new Error(`${cap.modelId} is not a speech model.`);
|
|
9146
|
+
const format = req.format ?? "mp3";
|
|
9147
|
+
const url = `${baseUrlFor(cfg, "https://api.openai.com/v1")}/audio/speech`;
|
|
9148
|
+
const res = await postJson(url, {
|
|
9149
|
+
model: cap.modelId,
|
|
9150
|
+
input: req.text,
|
|
9151
|
+
voice: req.voice ?? "alloy",
|
|
9152
|
+
response_format: format
|
|
9153
|
+
}, { Authorization: `Bearer ${cfg.apiKey ?? ""}` }, signal);
|
|
9154
|
+
const mime = {
|
|
9155
|
+
mp3: "audio/mpeg",
|
|
9156
|
+
opus: "audio/opus",
|
|
9157
|
+
aac: "audio/aac",
|
|
9158
|
+
flac: "audio/flac",
|
|
9159
|
+
wav: "audio/wav"
|
|
9160
|
+
};
|
|
9161
|
+
return {
|
|
9162
|
+
data: Buffer.from(await res.arrayBuffer()),
|
|
9163
|
+
mimeType: mime[format] ?? "application/octet-stream",
|
|
9164
|
+
filename: stamp(format),
|
|
9165
|
+
modelId: cap.modelId,
|
|
9166
|
+
provider: cap.provider
|
|
9167
|
+
};
|
|
9168
|
+
}, cap.modelId);
|
|
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
|
+
}
|
|
9239
|
+
async function transcribeAudio(cap, cfg, req, signal) {
|
|
9240
|
+
return callProvider(async () => {
|
|
9241
|
+
if (cap.api !== "openai-transcriptions") throw new Error(`${cap.modelId} is not a transcription model.`);
|
|
9242
|
+
const url = `${baseUrlFor(cfg, "https://api.openai.com/v1")}/audio/transcriptions`;
|
|
9243
|
+
const form = new FormData();
|
|
9244
|
+
form.append("model", cap.modelId);
|
|
9245
|
+
if (req.language) form.append("language", req.language);
|
|
9246
|
+
form.append("file", new Blob([new Uint8Array(req.audio)]), req.filename);
|
|
9247
|
+
const ac = new AbortController();
|
|
9248
|
+
const timer = setTimeout(() => ac.abort(), DEFAULT_TIMEOUT_MS);
|
|
9249
|
+
const onAbort = () => ac.abort();
|
|
9250
|
+
signal?.addEventListener("abort", onAbort);
|
|
9251
|
+
try {
|
|
9252
|
+
const res = await fetch(url, {
|
|
9253
|
+
method: "POST",
|
|
9254
|
+
headers: { Authorization: `Bearer ${cfg.apiKey ?? ""}` },
|
|
9255
|
+
body: form,
|
|
9256
|
+
signal: ac.signal
|
|
9257
|
+
});
|
|
9258
|
+
if (!res.ok) {
|
|
9259
|
+
const text = await res.text().catch(() => "");
|
|
9260
|
+
throw Object.assign(new Error(text || res.statusText), { status: res.status });
|
|
9261
|
+
}
|
|
9262
|
+
const body = await res.json();
|
|
9263
|
+
return { text: body.text ?? "", modelId: cap.modelId };
|
|
9264
|
+
} finally {
|
|
9265
|
+
clearTimeout(timer);
|
|
9266
|
+
signal?.removeEventListener("abort", onAbort);
|
|
9267
|
+
}
|
|
9268
|
+
}, cap.modelId);
|
|
9269
|
+
}
|
|
9270
|
+
|
|
9271
|
+
// src/tools/generate-media.ts
|
|
9272
|
+
function resolve(deps, modality, preferModel) {
|
|
9273
|
+
const selected = deps.registry.select(modality, preferModel ? { preferModel } : {});
|
|
9274
|
+
if (!selected) {
|
|
9275
|
+
return { ok: false, error: `No ${modality} model is available on your configured providers.` };
|
|
9276
|
+
}
|
|
9277
|
+
const cfg = deps.lookupProvider(selected.capability.provider);
|
|
9278
|
+
if (!cfg) {
|
|
9279
|
+
return {
|
|
9280
|
+
ok: false,
|
|
9281
|
+
error: `${selected.capability.modelId} needs the ${selected.capability.provider} provider, which is not configured.`
|
|
9282
|
+
};
|
|
9283
|
+
}
|
|
9284
|
+
return { ok: true, selected, cfg };
|
|
9285
|
+
}
|
|
9286
|
+
var GenerateImageTool = class extends BaseTool {
|
|
9287
|
+
constructor(deps) {
|
|
9288
|
+
super();
|
|
9289
|
+
this.deps = deps;
|
|
9290
|
+
}
|
|
9291
|
+
deps;
|
|
9292
|
+
name = "generate_image";
|
|
9293
|
+
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.";
|
|
9294
|
+
inputSchema = {
|
|
9295
|
+
type: "object",
|
|
9296
|
+
properties: {
|
|
9297
|
+
prompt: { type: "string", description: "What the image should show. Be specific about subject, style and composition." },
|
|
9298
|
+
size: { type: "string", description: 'Pixel size, e.g. "1024x1024" (default), "1792x1024", "1024x1792".' },
|
|
9299
|
+
model: { type: "string", description: "Optional: force a specific image model instead of the automatic choice." }
|
|
9300
|
+
},
|
|
9301
|
+
required: ["prompt"]
|
|
9302
|
+
};
|
|
9303
|
+
async execute(input, options) {
|
|
9304
|
+
const prompt = String(input["prompt"] ?? "").trim();
|
|
9305
|
+
if (!prompt) return 'Provide a "prompt" describing the image.';
|
|
9306
|
+
const r = resolve(this.deps, "image", input["model"] ? String(input["model"]) : void 0);
|
|
9307
|
+
if (!r.ok) return r.error;
|
|
9308
|
+
const size = input["size"] ? String(input["size"]) : void 0;
|
|
9309
|
+
const asset = await generateImage(
|
|
9310
|
+
r.selected.capability,
|
|
9311
|
+
r.cfg,
|
|
9312
|
+
{ prompt, ...size ? { size } : {} },
|
|
9313
|
+
options.signal
|
|
9314
|
+
);
|
|
9315
|
+
const location = await this.deps.sink(asset);
|
|
9316
|
+
return [
|
|
9317
|
+
`Image generated and saved to ${location}.`,
|
|
9318
|
+
`Model: ${asset.modelId} \u2014 ${r.selected.reason}`,
|
|
9319
|
+
// DALL·E rewrites the prompt before drawing; the model needs to know what
|
|
9320
|
+
// was actually rendered, not what it asked for, or its description of the
|
|
9321
|
+
// result will not match the image.
|
|
9322
|
+
asset.revisedPrompt ? `The provider rewrote the prompt to: ${asset.revisedPrompt}` : ""
|
|
9323
|
+
].filter(Boolean).join("\n");
|
|
9324
|
+
}
|
|
9325
|
+
};
|
|
9326
|
+
var GenerateSpeechTool = class extends BaseTool {
|
|
9327
|
+
constructor(deps) {
|
|
9328
|
+
super();
|
|
9329
|
+
this.deps = deps;
|
|
9330
|
+
}
|
|
9331
|
+
deps;
|
|
9332
|
+
name = "generate_speech";
|
|
9333
|
+
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.";
|
|
9334
|
+
inputSchema = {
|
|
9335
|
+
type: "object",
|
|
9336
|
+
properties: {
|
|
9337
|
+
text: { type: "string", description: "The text to speak." },
|
|
9338
|
+
voice: { type: "string", description: "Voice id: alloy, echo, fable, onyx, nova, shimmer. Default alloy." },
|
|
9339
|
+
format: { type: "string", description: "Audio format: mp3 (default), opus, aac, flac, wav." },
|
|
9340
|
+
model: { type: "string", description: "Optional: force a specific speech model." }
|
|
9341
|
+
},
|
|
9342
|
+
required: ["text"]
|
|
9343
|
+
};
|
|
9344
|
+
async execute(input, options) {
|
|
9345
|
+
const text = String(input["text"] ?? "").trim();
|
|
9346
|
+
if (!text) return 'Provide "text" to speak.';
|
|
9347
|
+
const r = resolve(this.deps, "speech", input["model"] ? String(input["model"]) : void 0);
|
|
9348
|
+
if (!r.ok) return r.error;
|
|
9349
|
+
const fmt = String(input["format"] ?? "mp3");
|
|
9350
|
+
const allowed = ["mp3", "opus", "aac", "flac", "wav"];
|
|
9351
|
+
const format = allowed.includes(fmt) ? fmt : "mp3";
|
|
9352
|
+
const asset = await generateSpeech(
|
|
9353
|
+
r.selected.capability,
|
|
9354
|
+
r.cfg,
|
|
9355
|
+
{ text, voice: input["voice"] ? String(input["voice"]) : void 0, format },
|
|
9356
|
+
options.signal
|
|
9357
|
+
);
|
|
9358
|
+
const location = await this.deps.sink(asset);
|
|
9359
|
+
return `Speech generated and saved to ${location}.
|
|
9360
|
+
Model: ${asset.modelId} \u2014 ${r.selected.reason}`;
|
|
9361
|
+
}
|
|
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
|
+
};
|
|
9415
|
+
var TranscribeAudioTool = class extends BaseTool {
|
|
9416
|
+
constructor(deps, readFile) {
|
|
9417
|
+
super();
|
|
9418
|
+
this.deps = deps;
|
|
9419
|
+
this.readFile = readFile;
|
|
9420
|
+
}
|
|
9421
|
+
deps;
|
|
9422
|
+
readFile;
|
|
9423
|
+
name = "transcribe_audio";
|
|
9424
|
+
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.";
|
|
9425
|
+
inputSchema = {
|
|
9426
|
+
type: "object",
|
|
9427
|
+
properties: {
|
|
9428
|
+
path: { type: "string", description: "Path to the audio file to transcribe." },
|
|
9429
|
+
language: { type: "string", description: 'Optional ISO-639-1 language hint, e.g. "en", improves accuracy.' },
|
|
9430
|
+
model: { type: "string", description: "Optional: force a specific transcription model." }
|
|
9431
|
+
},
|
|
9432
|
+
required: ["path"]
|
|
9433
|
+
};
|
|
9434
|
+
async execute(input, options) {
|
|
9435
|
+
const filePath = String(input["path"] ?? "").trim();
|
|
9436
|
+
if (!filePath) return 'Provide a "path" to the audio file.';
|
|
9437
|
+
const r = resolve(this.deps, "transcription", input["model"] ? String(input["model"]) : void 0);
|
|
9438
|
+
if (!r.ok) return r.error;
|
|
9439
|
+
let audio;
|
|
9440
|
+
try {
|
|
9441
|
+
audio = await this.readFile(filePath);
|
|
9442
|
+
} catch {
|
|
9443
|
+
return `Could not read the audio file at ${filePath}.`;
|
|
9444
|
+
}
|
|
9445
|
+
const { text, modelId } = await transcribeAudio(
|
|
9446
|
+
r.selected.capability,
|
|
9447
|
+
r.cfg,
|
|
9448
|
+
{
|
|
9449
|
+
audio,
|
|
9450
|
+
filename: filePath.split(/[\\/]/).pop() || "audio",
|
|
9451
|
+
...input["language"] ? { language: String(input["language"]) } : {}
|
|
9452
|
+
},
|
|
9453
|
+
options.signal
|
|
9454
|
+
);
|
|
9455
|
+
return text ? `Transcript (${modelId}):
|
|
9456
|
+
|
|
9457
|
+
${text}` : `${modelId} returned an empty transcript \u2014 the file may be silent or in an unsupported format.`;
|
|
9458
|
+
}
|
|
9459
|
+
};
|
|
9460
|
+
function buildMediaTools(deps, readFile) {
|
|
9461
|
+
const tools = [];
|
|
9462
|
+
const modalities = new Set(deps.registry.availableModalities());
|
|
9463
|
+
if (modalities.has("image")) tools.push(new GenerateImageTool(deps));
|
|
9464
|
+
if (modalities.has("speech")) tools.push(new GenerateSpeechTool(deps));
|
|
9465
|
+
if (modalities.has("video")) tools.push(new GenerateVideoTool(deps));
|
|
9466
|
+
if (modalities.has("transcription") && readFile) tools.push(new TranscribeAudioTool(deps, readFile));
|
|
9467
|
+
return tools;
|
|
9468
|
+
}
|
|
9469
|
+
|
|
8771
9470
|
// src/core/run-breaker.ts
|
|
8772
9471
|
var DEFAULT_FAILURE_THRESHOLD = 3;
|
|
8773
9472
|
var RunBreaker = class {
|
|
@@ -8833,26 +9532,6 @@ var RunBreaker = class {
|
|
|
8833
9532
|
return out;
|
|
8834
9533
|
}
|
|
8835
9534
|
};
|
|
8836
|
-
|
|
8837
|
-
// src/tools/base.ts
|
|
8838
|
-
var BaseTool = class {
|
|
8839
|
-
workspaceRoot = process.cwd();
|
|
8840
|
-
setWorkspaceRoot(root) {
|
|
8841
|
-
this.workspaceRoot = root;
|
|
8842
|
-
}
|
|
8843
|
-
isDangerous() {
|
|
8844
|
-
return false;
|
|
8845
|
-
}
|
|
8846
|
-
getDefinition() {
|
|
8847
|
-
return {
|
|
8848
|
-
name: this.name,
|
|
8849
|
-
description: this.description,
|
|
8850
|
-
inputSchema: this.inputSchema
|
|
8851
|
-
};
|
|
8852
|
-
}
|
|
8853
|
-
};
|
|
8854
|
-
|
|
8855
|
-
// src/tools/shell.ts
|
|
8856
9535
|
var execAsync = promisify(exec);
|
|
8857
9536
|
var ShellTool = class extends BaseTool {
|
|
8858
9537
|
name = "shell";
|
|
@@ -9548,7 +10227,7 @@ var PDFCreateTool = class extends BaseTool {
|
|
|
9548
10227
|
if (!fs23.existsSync(dir)) {
|
|
9549
10228
|
fs23.mkdirSync(dir, { recursive: true });
|
|
9550
10229
|
}
|
|
9551
|
-
return new Promise((
|
|
10230
|
+
return new Promise((resolve2, reject) => {
|
|
9552
10231
|
try {
|
|
9553
10232
|
const doc = new PDFDocument({ margin: 50 });
|
|
9554
10233
|
const stream = fs23.createWriteStream(filePath);
|
|
@@ -9566,7 +10245,7 @@ var PDFCreateTool = class extends BaseTool {
|
|
|
9566
10245
|
});
|
|
9567
10246
|
doc.end();
|
|
9568
10247
|
stream.on("finish", () => {
|
|
9569
|
-
|
|
10248
|
+
resolve2(`Successfully created PDF at ${filePath} (${content.length} characters)`);
|
|
9570
10249
|
});
|
|
9571
10250
|
stream.on("error", (err) => {
|
|
9572
10251
|
reject(new Error(`Failed to write PDF: ${err.message}`));
|
|
@@ -9638,7 +10317,7 @@ var CodeInterpreterTool = class extends BaseTool {
|
|
|
9638
10317
|
const filePath = path24.join(tmpDir, fileName);
|
|
9639
10318
|
fs23.writeFileSync(filePath, code, "utf-8");
|
|
9640
10319
|
const execArgs = [filePath, ...args];
|
|
9641
|
-
return new Promise((
|
|
10320
|
+
return new Promise((resolve2) => {
|
|
9642
10321
|
const startMs = Date.now();
|
|
9643
10322
|
execFile(cmdPrefix, execArgs, { cwd: this.workspaceRoot, timeout: 3e4 }, (error, stdout, stderr) => {
|
|
9644
10323
|
const duration = Date.now() - startMs;
|
|
@@ -9652,17 +10331,17 @@ var CodeInterpreterTool = class extends BaseTool {
|
|
|
9652
10331
|
if (error) {
|
|
9653
10332
|
const timedOut = error.killed && duration >= 3e4;
|
|
9654
10333
|
if (timedOut) {
|
|
9655
|
-
|
|
10334
|
+
resolve2(`Execution timed out after 30s. Consider breaking the task into smaller pieces.
|
|
9656
10335
|
Partial stdout: ${stdout}
|
|
9657
10336
|
Stderr: ${stderr}`);
|
|
9658
10337
|
} else {
|
|
9659
|
-
|
|
10338
|
+
resolve2(`Execution failed (${duration}ms):
|
|
9660
10339
|
Error: ${error.message}
|
|
9661
10340
|
Stderr: ${stderr}
|
|
9662
10341
|
Stdout: ${stdout}`);
|
|
9663
10342
|
}
|
|
9664
10343
|
} else {
|
|
9665
|
-
|
|
10344
|
+
resolve2(`Execution successful (${duration}ms):
|
|
9666
10345
|
Stdout: ${stdout}
|
|
9667
10346
|
Stderr: ${stderr}`);
|
|
9668
10347
|
}
|
|
@@ -10316,7 +10995,7 @@ var ToolRegistry = class extends EventEmitter {
|
|
|
10316
10995
|
*/
|
|
10317
10996
|
waitForTool(toolName, timeoutMs = 6e4) {
|
|
10318
10997
|
if (this.tools.has(toolName)) return Promise.resolve();
|
|
10319
|
-
return new Promise((
|
|
10998
|
+
return new Promise((resolve2, reject) => {
|
|
10320
10999
|
const timer = setTimeout(() => {
|
|
10321
11000
|
this.off("tool:added", handler);
|
|
10322
11001
|
reject(new Error(`Timeout waiting for tool: ${toolName}`));
|
|
@@ -10325,7 +11004,7 @@ var ToolRegistry = class extends EventEmitter {
|
|
|
10325
11004
|
if (name === toolName) {
|
|
10326
11005
|
clearTimeout(timer);
|
|
10327
11006
|
this.off("tool:added", handler);
|
|
10328
|
-
|
|
11007
|
+
resolve2();
|
|
10329
11008
|
}
|
|
10330
11009
|
};
|
|
10331
11010
|
this.on("tool:added", handler);
|
|
@@ -11000,20 +11679,20 @@ var PermissionEscalator = class extends EventEmitter {
|
|
|
11000
11679
|
resolver(decision);
|
|
11001
11680
|
}
|
|
11002
11681
|
waitForUserDecision(req) {
|
|
11003
|
-
return new Promise((
|
|
11682
|
+
return new Promise((resolve2) => {
|
|
11004
11683
|
let timer;
|
|
11005
11684
|
const wrappedResolver = (decision) => {
|
|
11006
11685
|
if (timer) clearTimeout(timer);
|
|
11007
11686
|
if (decision.always) {
|
|
11008
11687
|
this.taskWideCache.set(req.toolName, decision.approved);
|
|
11009
11688
|
}
|
|
11010
|
-
|
|
11689
|
+
resolve2(decision);
|
|
11011
11690
|
};
|
|
11012
11691
|
this.pendingUserDecisions.set(req.id, wrappedResolver);
|
|
11013
11692
|
if (this.approvalTimeoutMs > 0 && Number.isFinite(this.approvalTimeoutMs)) {
|
|
11014
11693
|
timer = setTimeout(() => {
|
|
11015
11694
|
if (this.pendingUserDecisions.delete(req.id)) {
|
|
11016
|
-
|
|
11695
|
+
resolve2({
|
|
11017
11696
|
requestId: req.id,
|
|
11018
11697
|
approved: false,
|
|
11019
11698
|
decidedBy: "USER",
|
|
@@ -11112,7 +11791,14 @@ var ToolsConfigSchema = z.object({
|
|
|
11112
11791
|
* built-in tool (shell/file/git/…) from existing at all, rather than just
|
|
11113
11792
|
* gating it behind approval. Omitted = full default set (unchanged).
|
|
11114
11793
|
*/
|
|
11115
|
-
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()
|
|
11116
11802
|
});
|
|
11117
11803
|
var HookDefinitionSchema = z.object({
|
|
11118
11804
|
name: z.string().optional(),
|
|
@@ -11180,6 +11866,14 @@ var BudgetConfigSchema = z.object({
|
|
|
11180
11866
|
maxTokensPerRun: z.number().int().positive().default(2e5),
|
|
11181
11867
|
/** Optional hard per-task cost ceiling (USD). Unset = only the token cap applies. */
|
|
11182
11868
|
maxCostPerRunUsd: z.number().positive().optional(),
|
|
11869
|
+
/**
|
|
11870
|
+
* Consecutive systemic failures against ONE model before the run stops
|
|
11871
|
+
* launching work that would fail the same way (dead key, wrong model id,
|
|
11872
|
+
* exhausted quota). Lower reacts sooner and wastes less; higher absorbs a
|
|
11873
|
+
* longer transient before giving up. Only failures that will certainly repeat
|
|
11874
|
+
* are counted — see core/run-breaker.ts.
|
|
11875
|
+
*/
|
|
11876
|
+
failureThreshold: z.number().int().min(1).max(20).default(3),
|
|
11183
11877
|
warnAtPct: z.number().default(80)
|
|
11184
11878
|
});
|
|
11185
11879
|
var WorkspaceConfigSchema = z.object({
|
|
@@ -11467,6 +12161,23 @@ var noopTelemetry = {
|
|
|
11467
12161
|
}
|
|
11468
12162
|
};
|
|
11469
12163
|
|
|
12164
|
+
// src/core/router/feedback-prior.ts
|
|
12165
|
+
var MAX_FEEDBACK_ADJUSTMENT = 0.05;
|
|
12166
|
+
var PRIOR_STRENGTH = 10;
|
|
12167
|
+
function feedbackAdjustment(counts) {
|
|
12168
|
+
const good = Math.max(0, Math.floor(counts.good));
|
|
12169
|
+
const bad = Math.max(0, Math.floor(counts.bad));
|
|
12170
|
+
const n = good + bad;
|
|
12171
|
+
if (n === 0) return 0;
|
|
12172
|
+
const half = PRIOR_STRENGTH / 2;
|
|
12173
|
+
const shrunk = (good + half) / (n + PRIOR_STRENGTH);
|
|
12174
|
+
return (shrunk - 0.5) * 2 * MAX_FEEDBACK_ADJUSTMENT;
|
|
12175
|
+
}
|
|
12176
|
+
function applyFeedback(score01, counts) {
|
|
12177
|
+
const adjusted = score01 + feedbackAdjustment(counts);
|
|
12178
|
+
return Math.max(0, Math.min(1, adjusted));
|
|
12179
|
+
}
|
|
12180
|
+
|
|
11470
12181
|
// src/core/router/task-analyzer.ts
|
|
11471
12182
|
var CODE_SIGNALS = [
|
|
11472
12183
|
/\b(?:function|class|interface|async|await|import|export|const|let|var|def|return|if|else|for|while|try|catch)\b/,
|
|
@@ -11541,6 +12252,8 @@ var TASK_TYPE_TAGS = {
|
|
|
11541
12252
|
};
|
|
11542
12253
|
var TaskAnalyzer = class {
|
|
11543
12254
|
tracker;
|
|
12255
|
+
/** Per-model thumbs counts, when the host collects them. */
|
|
12256
|
+
feedback;
|
|
11544
12257
|
bias;
|
|
11545
12258
|
lastProfile = null;
|
|
11546
12259
|
lastSelectedModels = /* @__PURE__ */ new Map();
|
|
@@ -11548,6 +12261,14 @@ var TaskAnalyzer = class {
|
|
|
11548
12261
|
this.tracker = tracker;
|
|
11549
12262
|
this.bias = bias;
|
|
11550
12263
|
}
|
|
12264
|
+
/**
|
|
12265
|
+
* Supply per-model rating counts. Optional by design: a host that collects no
|
|
12266
|
+
* feedback (the CLI, a fresh install) scores exactly as it did before, since
|
|
12267
|
+
* feedbackAdjustment returns 0 for an empty record.
|
|
12268
|
+
*/
|
|
12269
|
+
setFeedbackSource(source) {
|
|
12270
|
+
this.feedback = source;
|
|
12271
|
+
}
|
|
11551
12272
|
setTracker(tracker) {
|
|
11552
12273
|
this.tracker = tracker;
|
|
11553
12274
|
}
|
|
@@ -11631,7 +12352,10 @@ var TaskAnalyzer = class {
|
|
|
11631
12352
|
const perf = this.tracker?.performanceScore(model.id, profile.type) ?? 0.5;
|
|
11632
12353
|
const costEff = this.costEfficiency(model, profile.complexity);
|
|
11633
12354
|
const match = this.taskMatchScore(model, profile);
|
|
11634
|
-
const
|
|
12355
|
+
const rated = this.feedback?.(model.id);
|
|
12356
|
+
const publicScore = benchmarkScore01(model, profile.type);
|
|
12357
|
+
const adjusted = rated ? applyFeedback(publicScore, rated) : publicScore;
|
|
12358
|
+
const benchmark = 0.3 + 0.7 * adjusted;
|
|
11635
12359
|
switch (this.bias) {
|
|
11636
12360
|
case "quality":
|
|
11637
12361
|
return perf * match * benchmark ** 2 * (0.85 + 0.15 * costEff);
|
|
@@ -12033,10 +12757,10 @@ var DynamicTool = class extends BaseTool {
|
|
|
12033
12757
|
`;
|
|
12034
12758
|
const script = await isolate.compileScript(bootstrap);
|
|
12035
12759
|
const runPromise = script.run(context, { timeout: timeoutMs, promise: true }).then((v) => String(v ?? ""));
|
|
12036
|
-
const timeoutPromise = new Promise((
|
|
12760
|
+
const timeoutPromise = new Promise((resolve2) => {
|
|
12037
12761
|
const t = setTimeout(() => {
|
|
12038
12762
|
dispose();
|
|
12039
|
-
|
|
12763
|
+
resolve2(`Dynamic tool "${this.name}" timed out after ${timeoutMs}ms and was terminated.`);
|
|
12040
12764
|
}, timeoutMs + 500);
|
|
12041
12765
|
t.unref?.();
|
|
12042
12766
|
});
|
|
@@ -12054,7 +12778,7 @@ var DynamicTool = class extends BaseTool {
|
|
|
12054
12778
|
/** Spawn the worker, service its callTool/fetch bridge, enforce the kill timeout. */
|
|
12055
12779
|
runInWorker(input, callTool) {
|
|
12056
12780
|
const timeoutMs = Math.max(200, Number(process.env["CASCADE_DYNAMIC_TOOL_TIMEOUT_MS"]) || DYNAMIC_TOOL_TIMEOUT_MS);
|
|
12057
|
-
return new Promise((
|
|
12781
|
+
return new Promise((resolve2) => {
|
|
12058
12782
|
let settled = false;
|
|
12059
12783
|
const worker = new Worker(HARNESS_SRC, {
|
|
12060
12784
|
eval: true,
|
|
@@ -12066,7 +12790,7 @@ var DynamicTool = class extends BaseTool {
|
|
|
12066
12790
|
settled = true;
|
|
12067
12791
|
clearTimeout(timer);
|
|
12068
12792
|
void worker.terminate();
|
|
12069
|
-
|
|
12793
|
+
resolve2(value);
|
|
12070
12794
|
};
|
|
12071
12795
|
const timer = setTimeout(
|
|
12072
12796
|
() => finish(`Dynamic tool "${this.name}" timed out after ${timeoutMs}ms and was terminated.`),
|
|
@@ -13612,6 +14336,8 @@ ${prompt}`;
|
|
|
13612
14336
|
var Cascade = class _Cascade extends EventEmitter {
|
|
13613
14337
|
router;
|
|
13614
14338
|
toolRegistry;
|
|
14339
|
+
multimodal;
|
|
14340
|
+
mediaSink;
|
|
13615
14341
|
mcpClient;
|
|
13616
14342
|
codeIndex;
|
|
13617
14343
|
config;
|
|
@@ -13650,6 +14376,10 @@ var Cascade = class _Cascade extends EventEmitter {
|
|
|
13650
14376
|
}
|
|
13651
14377
|
});
|
|
13652
14378
|
this.toolRegistry = new ToolRegistry(this.config.tools, workspacePath);
|
|
14379
|
+
this.multimodal = new MultimodalRegistry(
|
|
14380
|
+
(this.config.providers ?? []).map((p) => p.type)
|
|
14381
|
+
);
|
|
14382
|
+
this.registerMediaTools(workspacePath);
|
|
13653
14383
|
this.telemetry = config.telemetry?.enabled ? new Telemetry(config.telemetry, config.telemetry.distinctId ?? "anonymous") : noopTelemetry;
|
|
13654
14384
|
this.worldStateDB = new WorldStateDB(this.workspacePath, this.config.workspace?.debugWorldState ?? false);
|
|
13655
14385
|
this.router.setWorldStateDB(this.worldStateDB);
|
|
@@ -13703,14 +14433,14 @@ var Cascade = class _Cascade extends EventEmitter {
|
|
|
13703
14433
|
if (this.listenerCount("mcp:approval-required") === 0) {
|
|
13704
14434
|
return false;
|
|
13705
14435
|
}
|
|
13706
|
-
return await new Promise((
|
|
13707
|
-
this.pendingMcpApprovals.set(server.name,
|
|
14436
|
+
return await new Promise((resolve2) => {
|
|
14437
|
+
this.pendingMcpApprovals.set(server.name, resolve2);
|
|
13708
14438
|
const timeout = setTimeout(() => {
|
|
13709
|
-
if (this.pendingMcpApprovals.delete(server.name))
|
|
14439
|
+
if (this.pendingMcpApprovals.delete(server.name)) resolve2(false);
|
|
13710
14440
|
}, 3e4);
|
|
13711
14441
|
const wrap = (approved) => {
|
|
13712
14442
|
clearTimeout(timeout);
|
|
13713
|
-
|
|
14443
|
+
resolve2(approved);
|
|
13714
14444
|
};
|
|
13715
14445
|
this.pendingMcpApprovals.set(server.name, wrap);
|
|
13716
14446
|
this.emit("mcp:approval-required", { server });
|
|
@@ -13784,17 +14514,17 @@ var Cascade = class _Cascade extends EventEmitter {
|
|
|
13784
14514
|
}
|
|
13785
14515
|
const t2Count = plan.sections.length;
|
|
13786
14516
|
const t3Count = plan.sections.reduce((sum, s) => sum + (s.t3Subtasks?.length ?? 0), 0);
|
|
13787
|
-
return await new Promise((
|
|
14517
|
+
return await new Promise((resolve2) => {
|
|
13788
14518
|
const timeout = setTimeout(() => {
|
|
13789
14519
|
if (this.pendingPlanApproval) {
|
|
13790
14520
|
this.pendingPlanApproval = void 0;
|
|
13791
|
-
|
|
14521
|
+
resolve2({ approved: true });
|
|
13792
14522
|
}
|
|
13793
14523
|
}, 12e4);
|
|
13794
14524
|
this.pendingPlanApproval = (decision) => {
|
|
13795
14525
|
clearTimeout(timeout);
|
|
13796
14526
|
this.pendingPlanApproval = void 0;
|
|
13797
|
-
|
|
14527
|
+
resolve2(decision);
|
|
13798
14528
|
};
|
|
13799
14529
|
this.emit("plan:approval-required", {
|
|
13800
14530
|
taskId,
|
|
@@ -13827,17 +14557,17 @@ var Cascade = class _Cascade extends EventEmitter {
|
|
|
13827
14557
|
}
|
|
13828
14558
|
async requestContextApproval(info) {
|
|
13829
14559
|
if (this.listenerCount("context:approval-required") === 0) return true;
|
|
13830
|
-
return await new Promise((
|
|
14560
|
+
return await new Promise((resolve2) => {
|
|
13831
14561
|
const timeout = setTimeout(() => {
|
|
13832
14562
|
if (this.pendingContextApproval) {
|
|
13833
14563
|
this.pendingContextApproval = void 0;
|
|
13834
|
-
|
|
14564
|
+
resolve2(true);
|
|
13835
14565
|
}
|
|
13836
14566
|
}, 12e4);
|
|
13837
14567
|
this.pendingContextApproval = (approved) => {
|
|
13838
14568
|
clearTimeout(timeout);
|
|
13839
14569
|
this.pendingContextApproval = void 0;
|
|
13840
|
-
|
|
14570
|
+
resolve2(approved);
|
|
13841
14571
|
};
|
|
13842
14572
|
this.emit("context:approval-required", info);
|
|
13843
14573
|
});
|
|
@@ -13917,6 +14647,68 @@ ${input}` }],
|
|
|
13917
14647
|
if (this.store) t1.setStore(this.store);
|
|
13918
14648
|
return t1.previewPlan(prompt);
|
|
13919
14649
|
}
|
|
14650
|
+
/**
|
|
14651
|
+
* Register image / speech / transcription tools for the modalities this
|
|
14652
|
+
* config can actually serve.
|
|
14653
|
+
*
|
|
14654
|
+
* The sink writes bytes into the workspace, because that is what a desktop or
|
|
14655
|
+
* CLI run wants and it keeps a generated asset indistinguishable from any
|
|
14656
|
+
* other file the run produced. A host with different storage (the cloud
|
|
14657
|
+
* server stores a file row) constructs the tools itself via buildMediaTools
|
|
14658
|
+
* with its own sink.
|
|
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
|
+
}
|
|
14673
|
+
registerMediaTools(workspacePath) {
|
|
14674
|
+
const denied = new Set(this.config.tools?.disabledTools ?? []);
|
|
14675
|
+
const permitted = (name) => !denied.has(name);
|
|
14676
|
+
const tools = buildMediaTools(
|
|
14677
|
+
{
|
|
14678
|
+
registry: this.multimodal,
|
|
14679
|
+
lookupProvider: (provider) => (this.config.providers ?? []).find((p) => p.type === provider),
|
|
14680
|
+
sink: this.mediaSink ?? (async (asset) => {
|
|
14681
|
+
const fsp = await import('fs/promises');
|
|
14682
|
+
const nodePath = await import('path');
|
|
14683
|
+
const dir = nodePath.join(workspacePath, "generated");
|
|
14684
|
+
await fsp.mkdir(dir, { recursive: true });
|
|
14685
|
+
const dest = nodePath.join(dir, asset.filename);
|
|
14686
|
+
await fsp.writeFile(dest, asset.data);
|
|
14687
|
+
return nodePath.relative(workspacePath, dest);
|
|
14688
|
+
})
|
|
14689
|
+
},
|
|
14690
|
+
async (p) => {
|
|
14691
|
+
const fsp = await import('fs/promises');
|
|
14692
|
+
return fsp.readFile(p);
|
|
14693
|
+
}
|
|
14694
|
+
);
|
|
14695
|
+
for (const tool of tools) {
|
|
14696
|
+
if (!permitted(tool.name)) continue;
|
|
14697
|
+
tool.setWorkspaceRoot(workspacePath);
|
|
14698
|
+
this.toolRegistry.register(tool);
|
|
14699
|
+
}
|
|
14700
|
+
}
|
|
14701
|
+
/**
|
|
14702
|
+
* Supply per-model thumbs-up/down counts so Auto routing can weigh how models
|
|
14703
|
+
* have actually performed for this user, on top of public benchmarks.
|
|
14704
|
+
*
|
|
14705
|
+
* Optional everywhere: with no source, or an empty record, scoring is
|
|
14706
|
+
* byte-identical to before. The adjustment is capped and sample-size shrunk —
|
|
14707
|
+
* see core/router/feedback-prior.ts.
|
|
14708
|
+
*/
|
|
14709
|
+
setFeedbackSource(source) {
|
|
14710
|
+
this.router.setFeedbackSource(source);
|
|
14711
|
+
}
|
|
13920
14712
|
/** True when a task stopped at the budget cap and can be resumed via /continue. */
|
|
13921
14713
|
hasResumableRun() {
|
|
13922
14714
|
return this.lastInterruptedRun != null;
|
|
@@ -14406,7 +15198,7 @@ ${prompt}` : prompt;
|
|
|
14406
15198
|
if (toolCreator) toolCreator.setPermissionEscalator(escalator);
|
|
14407
15199
|
const rootPrompt = buildContextualPrompt(options.prompt, options.conversationHistory);
|
|
14408
15200
|
let finalOutput = "";
|
|
14409
|
-
const runBreaker = new RunBreaker();
|
|
15201
|
+
const runBreaker = new RunBreaker(this.config.budget?.failureThreshold);
|
|
14410
15202
|
let t2Results = [];
|
|
14411
15203
|
let runError = null;
|
|
14412
15204
|
let identityPrompt = "";
|
|
@@ -16579,12 +17371,12 @@ var DashboardServer = class {
|
|
|
16579
17371
|
console.warn("\u26A0 Dashboard auth is DISABLED while bound to a non-loopback interface \u2014 this allows unauthenticated remote task execution.");
|
|
16580
17372
|
}
|
|
16581
17373
|
}
|
|
16582
|
-
await new Promise((
|
|
17374
|
+
await new Promise((resolve2, reject) => {
|
|
16583
17375
|
const onError = (err) => reject(err);
|
|
16584
17376
|
this.httpServer.once("error", onError);
|
|
16585
17377
|
this.httpServer.listen(this.port, this.host, () => {
|
|
16586
17378
|
this.httpServer.off("error", onError);
|
|
16587
|
-
|
|
17379
|
+
resolve2();
|
|
16588
17380
|
});
|
|
16589
17381
|
});
|
|
16590
17382
|
try {
|
|
@@ -16608,8 +17400,8 @@ var DashboardServer = class {
|
|
|
16608
17400
|
} catch {
|
|
16609
17401
|
}
|
|
16610
17402
|
this.globalStore = null;
|
|
16611
|
-
await new Promise((
|
|
16612
|
-
this.httpServer.close((err) => err ? reject(err) :
|
|
17403
|
+
await new Promise((resolve2, reject) => {
|
|
17404
|
+
this.httpServer.close((err) => err ? reject(err) : resolve2());
|
|
16613
17405
|
});
|
|
16614
17406
|
}
|
|
16615
17407
|
getSocket() {
|
|
@@ -16806,8 +17598,8 @@ var DashboardServer = class {
|
|
|
16806
17598
|
* timeout denies it.
|
|
16807
17599
|
*/
|
|
16808
17600
|
makeApprovalCallback(sessionId) {
|
|
16809
|
-
return (request) => new Promise((
|
|
16810
|
-
this.pendingApprovals.set(request.id, { resolve, sessionId });
|
|
17601
|
+
return (request) => new Promise((resolve2) => {
|
|
17602
|
+
this.pendingApprovals.set(request.id, { resolve: resolve2, sessionId });
|
|
16811
17603
|
});
|
|
16812
17604
|
}
|
|
16813
17605
|
/**
|
|
@@ -17907,7 +18699,7 @@ var CloudClient = class _CloudClient {
|
|
|
17907
18699
|
const deadline = Date.now() + start.expiresIn * 1e3;
|
|
17908
18700
|
while (Date.now() < deadline) {
|
|
17909
18701
|
if (opts.signal?.aborted) throw new Error("Sign-in cancelled.");
|
|
17910
|
-
await
|
|
18702
|
+
await sleep2(interval * 1e3, opts.signal);
|
|
17911
18703
|
const r = await this.pollDevice(start.deviceCode);
|
|
17912
18704
|
if (r === "slow_down") {
|
|
17913
18705
|
interval += 5;
|
|
@@ -18118,9 +18910,9 @@ var CloudClient = class _CloudClient {
|
|
|
18118
18910
|
this.store.clear();
|
|
18119
18911
|
}
|
|
18120
18912
|
};
|
|
18121
|
-
function
|
|
18122
|
-
return new Promise((
|
|
18123
|
-
const t = setTimeout(
|
|
18913
|
+
function sleep2(ms, signal) {
|
|
18914
|
+
return new Promise((resolve2, reject) => {
|
|
18915
|
+
const t = setTimeout(resolve2, ms);
|
|
18124
18916
|
signal?.addEventListener("abort", () => {
|
|
18125
18917
|
clearTimeout(t);
|
|
18126
18918
|
reject(new Error("aborted"));
|