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