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.js
CHANGED
|
@@ -1740,7 +1740,7 @@ var CASCADE_VERSION, CASCADE_CONFIG_FILE, CASCADE_DB_FILE, CASCADE_DASHBOARD_SEC
|
|
|
1740
1740
|
var init_constants = __esm({
|
|
1741
1741
|
"src/constants.ts"() {
|
|
1742
1742
|
init_pricing();
|
|
1743
|
-
CASCADE_VERSION = "0.
|
|
1743
|
+
CASCADE_VERSION = "0.51.0";
|
|
1744
1744
|
CASCADE_CONFIG_FILE = ".cascade/config.json";
|
|
1745
1745
|
CASCADE_DB_FILE = ".cascade/memory.db";
|
|
1746
1746
|
CASCADE_DASHBOARD_SECRET_FILE = ".cascade/dashboard-secret";
|
|
@@ -2258,49 +2258,40 @@ function isChatModel(id, methods) {
|
|
|
2258
2258
|
}
|
|
2259
2259
|
return !NON_CHAT.test(id);
|
|
2260
2260
|
}
|
|
2261
|
-
var
|
|
2261
|
+
var MODALITY_PATTERNS, NON_CHAT;
|
|
2262
2262
|
var init_model_filter = __esm({
|
|
2263
2263
|
"src/providers/model-filter.ts"() {
|
|
2264
|
-
|
|
2265
|
-
"embed",
|
|
2264
|
+
MODALITY_PATTERNS = [
|
|
2265
|
+
["embedding", ["embed"]],
|
|
2266
2266
|
// text-embedding-3, nomic-embed, mxbai-embed, snowflake-arctic-embed
|
|
2267
|
-
"(?:^|[-_/])
|
|
2268
|
-
"text-to-speech",
|
|
2269
|
-
"
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
"
|
|
2281
|
-
"
|
|
2282
|
-
"
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
"veo",
|
|
2293
|
-
"sora",
|
|
2294
|
-
"(?:^|[-_/])video(?:[-_/]|$)",
|
|
2295
|
-
// video
|
|
2296
|
-
"(?:^|[-_/])aqa(?:[-_/]|$)",
|
|
2297
|
-
// attributed QA
|
|
2298
|
-
"(?:^|[-_/])live(?:[-_/]|$)",
|
|
2299
|
-
// live-audio variants
|
|
2300
|
-
"(?:^|[-_/])(?:davinci|babbage)(?:[-_/]|$)"
|
|
2301
|
-
// legacy completion-only base models
|
|
2267
|
+
["transcription", ["whisper", "transcrib", "(?:^|[-_/])stt(?:[-_/]|$)"]],
|
|
2268
|
+
["speech", ["(?:^|[-_/])tts(?:[-_/]|$)", "text-to-speech", "speech"]],
|
|
2269
|
+
["video", ["veo", "sora", "(?:^|[-_/])video(?:[-_/]|$)"]],
|
|
2270
|
+
["image", [
|
|
2271
|
+
"dall[-]?e",
|
|
2272
|
+
"imagen",
|
|
2273
|
+
"image-generation",
|
|
2274
|
+
"gpt-image",
|
|
2275
|
+
"(?:^|[-_/])image(?:[-_/]|$)",
|
|
2276
|
+
"stable-diffusion",
|
|
2277
|
+
"sdxl",
|
|
2278
|
+
"flux"
|
|
2279
|
+
]],
|
|
2280
|
+
["moderation", ["moderation", "(?:^|[-_/])guard(?:[-_/]|$)"]],
|
|
2281
|
+
["rerank", ["rerank"]],
|
|
2282
|
+
["realtime", [
|
|
2283
|
+
"(?:^|[-_/])audio(?:[-_/]|$)",
|
|
2284
|
+
"native-audio",
|
|
2285
|
+
"realtime",
|
|
2286
|
+
"(?:^|[-_/])live(?:[-_/]|$)",
|
|
2287
|
+
// live-audio variants
|
|
2288
|
+
"(?:^|[-_/])aqa(?:[-_/]|$)"
|
|
2289
|
+
// attributed QA
|
|
2290
|
+
]],
|
|
2291
|
+
["legacy-completion", ["(?:^|[-_/])(?:davinci|babbage)(?:[-_/]|$)"]]
|
|
2302
2292
|
];
|
|
2303
|
-
|
|
2293
|
+
MODALITY_PATTERNS.map(([m, pats]) => [m, new RegExp(pats.join("|"), "i")]);
|
|
2294
|
+
NON_CHAT = new RegExp(MODALITY_PATTERNS.flatMap(([, p]) => p).join("|"), "i");
|
|
2304
2295
|
}
|
|
2305
2296
|
});
|
|
2306
2297
|
|
|
@@ -2868,6 +2859,118 @@ var init_azure = __esm({
|
|
|
2868
2859
|
}
|
|
2869
2860
|
});
|
|
2870
2861
|
|
|
2862
|
+
// src/providers/gemini-schema.ts
|
|
2863
|
+
function isPlainObject(v) {
|
|
2864
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
2865
|
+
}
|
|
2866
|
+
function normalizeType(value) {
|
|
2867
|
+
if (typeof value === "string") {
|
|
2868
|
+
return ALLOWED_TYPES.has(value) ? { type: value } : {};
|
|
2869
|
+
}
|
|
2870
|
+
if (Array.isArray(value)) {
|
|
2871
|
+
const types = value.filter((t) => typeof t === "string");
|
|
2872
|
+
const nullable = types.includes("null");
|
|
2873
|
+
const concrete = types.find((t) => t !== "null" && ALLOWED_TYPES.has(t));
|
|
2874
|
+
return {
|
|
2875
|
+
...concrete ? { type: concrete } : {},
|
|
2876
|
+
...nullable ? { nullable: true } : {}
|
|
2877
|
+
};
|
|
2878
|
+
}
|
|
2879
|
+
return {};
|
|
2880
|
+
}
|
|
2881
|
+
function normalizeEnum(value) {
|
|
2882
|
+
if (!Array.isArray(value) || value.length === 0) return void 0;
|
|
2883
|
+
return value.map((v) => typeof v === "string" ? v : JSON.stringify(v));
|
|
2884
|
+
}
|
|
2885
|
+
function sanitizeGeminiSchema(schema) {
|
|
2886
|
+
if (!isPlainObject(schema)) return void 0;
|
|
2887
|
+
const out = {};
|
|
2888
|
+
for (const [key, value] of Object.entries(schema)) {
|
|
2889
|
+
if (!ALLOWED_KEYS.has(key)) continue;
|
|
2890
|
+
switch (key) {
|
|
2891
|
+
case "type": {
|
|
2892
|
+
Object.assign(out, normalizeType(value));
|
|
2893
|
+
break;
|
|
2894
|
+
}
|
|
2895
|
+
case "enum": {
|
|
2896
|
+
const e = normalizeEnum(value);
|
|
2897
|
+
if (e) out["enum"] = e;
|
|
2898
|
+
break;
|
|
2899
|
+
}
|
|
2900
|
+
case "properties": {
|
|
2901
|
+
if (!isPlainObject(value)) break;
|
|
2902
|
+
const props = {};
|
|
2903
|
+
for (const [propName, propSchema] of Object.entries(value)) {
|
|
2904
|
+
const cleaned = sanitizeGeminiSchema(propSchema);
|
|
2905
|
+
props[propName] = cleaned && Object.keys(cleaned).length ? cleaned : { type: "string" };
|
|
2906
|
+
}
|
|
2907
|
+
out["properties"] = props;
|
|
2908
|
+
break;
|
|
2909
|
+
}
|
|
2910
|
+
case "items": {
|
|
2911
|
+
const cleaned = sanitizeGeminiSchema(value);
|
|
2912
|
+
if (cleaned) out["items"] = cleaned;
|
|
2913
|
+
break;
|
|
2914
|
+
}
|
|
2915
|
+
case "anyOf": {
|
|
2916
|
+
if (!Array.isArray(value)) break;
|
|
2917
|
+
const variants = value.map((v) => sanitizeGeminiSchema(v)).filter((v) => !!v && Object.keys(v).length > 0);
|
|
2918
|
+
if (variants.length) out["anyOf"] = variants;
|
|
2919
|
+
break;
|
|
2920
|
+
}
|
|
2921
|
+
case "required": {
|
|
2922
|
+
if (Array.isArray(value)) {
|
|
2923
|
+
out["required"] = value.filter((r) => typeof r === "string");
|
|
2924
|
+
}
|
|
2925
|
+
break;
|
|
2926
|
+
}
|
|
2927
|
+
default:
|
|
2928
|
+
out[key] = value;
|
|
2929
|
+
}
|
|
2930
|
+
}
|
|
2931
|
+
if (!out["type"] && out["properties"]) out["type"] = "object";
|
|
2932
|
+
if (!out["type"] && out["items"]) out["type"] = "array";
|
|
2933
|
+
return out;
|
|
2934
|
+
}
|
|
2935
|
+
function toGeminiParameters(inputSchema) {
|
|
2936
|
+
const cleaned = sanitizeGeminiSchema(inputSchema);
|
|
2937
|
+
if (!cleaned) return void 0;
|
|
2938
|
+
const props = cleaned["properties"];
|
|
2939
|
+
if (isPlainObject(props) && Object.keys(props).length === 0) return void 0;
|
|
2940
|
+
if (Object.keys(cleaned).length === 0) return void 0;
|
|
2941
|
+
return cleaned;
|
|
2942
|
+
}
|
|
2943
|
+
var ALLOWED_KEYS, ALLOWED_TYPES;
|
|
2944
|
+
var init_gemini_schema = __esm({
|
|
2945
|
+
"src/providers/gemini-schema.ts"() {
|
|
2946
|
+
ALLOWED_KEYS = /* @__PURE__ */ new Set([
|
|
2947
|
+
"type",
|
|
2948
|
+
"format",
|
|
2949
|
+
"title",
|
|
2950
|
+
"description",
|
|
2951
|
+
"nullable",
|
|
2952
|
+
"enum",
|
|
2953
|
+
"items",
|
|
2954
|
+
"properties",
|
|
2955
|
+
"required",
|
|
2956
|
+
"propertyOrdering",
|
|
2957
|
+
"minItems",
|
|
2958
|
+
"maxItems",
|
|
2959
|
+
"minProperties",
|
|
2960
|
+
"maxProperties",
|
|
2961
|
+
"minimum",
|
|
2962
|
+
"maximum",
|
|
2963
|
+
"minLength",
|
|
2964
|
+
"maxLength",
|
|
2965
|
+
"pattern",
|
|
2966
|
+
"example",
|
|
2967
|
+
"default",
|
|
2968
|
+
"anyOf"
|
|
2969
|
+
]);
|
|
2970
|
+
ALLOWED_TYPES = /* @__PURE__ */ new Set(["string", "number", "integer", "boolean", "array", "object"]);
|
|
2971
|
+
}
|
|
2972
|
+
});
|
|
2973
|
+
|
|
2871
2974
|
// src/providers/gemini.ts
|
|
2872
2975
|
var gemini_exports = {};
|
|
2873
2976
|
__export(gemini_exports, {
|
|
@@ -2880,6 +2983,7 @@ var init_gemini = __esm({
|
|
|
2880
2983
|
init_base();
|
|
2881
2984
|
init_pricing();
|
|
2882
2985
|
init_model_filter();
|
|
2986
|
+
init_gemini_schema();
|
|
2883
2987
|
GeminiProvider = class extends BaseProvider {
|
|
2884
2988
|
client;
|
|
2885
2989
|
constructor(config, model) {
|
|
@@ -3100,7 +3204,12 @@ var init_gemini = __esm({
|
|
|
3100
3204
|
return {
|
|
3101
3205
|
name: tool.name,
|
|
3102
3206
|
description: tool.description,
|
|
3103
|
-
parameters
|
|
3207
|
+
// Convert, never cast. Gemini's parameters field is an OpenAPI subset
|
|
3208
|
+
// that 400s on unknown keys, and MCP servers ship schemas carrying their
|
|
3209
|
+
// own extensions (GitHub's adds `x-mcp-header` to every header-bound
|
|
3210
|
+
// property). Casting sent those through verbatim and every request failed
|
|
3211
|
+
// before the model saw it. See providers/gemini-schema.ts.
|
|
3212
|
+
parameters: toGeminiParameters(tool.inputSchema)
|
|
3104
3213
|
};
|
|
3105
3214
|
}
|
|
3106
3215
|
};
|
|
@@ -3125,7 +3234,7 @@ async function nodeHttpFetch(input, init = {}, redirectCount = 0) {
|
|
|
3125
3234
|
headers["accept-encoding"] = "gzip, deflate, br";
|
|
3126
3235
|
}
|
|
3127
3236
|
const body = init.body == null ? void 0 : typeof init.body === "string" ? init.body : Buffer.from(init.body);
|
|
3128
|
-
return new Promise((
|
|
3237
|
+
return new Promise((resolve2, reject) => {
|
|
3129
3238
|
const req = lib.request(
|
|
3130
3239
|
{
|
|
3131
3240
|
hostname: u.hostname,
|
|
@@ -3142,7 +3251,7 @@ async function nodeHttpFetch(input, init = {}, redirectCount = 0) {
|
|
|
3142
3251
|
const nextUrl = new URL(location, u).href;
|
|
3143
3252
|
const downgrade = status === 303 || (status === 301 || status === 302) && method !== "GET" && method !== "HEAD";
|
|
3144
3253
|
const nextInit = downgrade ? { ...init, method: "GET", body: void 0 } : init;
|
|
3145
|
-
|
|
3254
|
+
resolve2(nodeHttpFetch(nextUrl, nextInit, redirectCount + 1));
|
|
3146
3255
|
return;
|
|
3147
3256
|
}
|
|
3148
3257
|
const encoding = (res.headers["content-encoding"] ?? "").toLowerCase();
|
|
@@ -3157,7 +3266,7 @@ async function nodeHttpFetch(input, init = {}, redirectCount = 0) {
|
|
|
3157
3266
|
if (Array.isArray(v)) respHeaders.set(k, v.join(", "));
|
|
3158
3267
|
else if (typeof v === "string") respHeaders.set(k, v);
|
|
3159
3268
|
}
|
|
3160
|
-
|
|
3269
|
+
resolve2(new Response(stream, {
|
|
3161
3270
|
status,
|
|
3162
3271
|
statusText: res.statusMessage ?? "",
|
|
3163
3272
|
headers: respHeaders
|
|
@@ -4956,6 +5065,14 @@ var BudgetConfigSchema = z.object({
|
|
|
4956
5065
|
maxTokensPerRun: z.number().int().positive().default(2e5),
|
|
4957
5066
|
/** Optional hard per-task cost ceiling (USD). Unset = only the token cap applies. */
|
|
4958
5067
|
maxCostPerRunUsd: z.number().positive().optional(),
|
|
5068
|
+
/**
|
|
5069
|
+
* Consecutive systemic failures against ONE model before the run stops
|
|
5070
|
+
* launching work that would fail the same way (dead key, wrong model id,
|
|
5071
|
+
* exhausted quota). Lower reacts sooner and wastes less; higher absorbs a
|
|
5072
|
+
* longer transient before giving up. Only failures that will certainly repeat
|
|
5073
|
+
* are counted — see core/run-breaker.ts.
|
|
5074
|
+
*/
|
|
5075
|
+
failureThreshold: z.number().int().min(1).max(20).default(3),
|
|
4959
5076
|
warnAtPct: z.number().default(80)
|
|
4960
5077
|
});
|
|
4961
5078
|
var WorkspaceConfigSchema = z.object({
|
|
@@ -6008,7 +6125,7 @@ var TpmLimiter = class {
|
|
|
6008
6125
|
}
|
|
6009
6126
|
const deficit = want - bucket.available;
|
|
6010
6127
|
const waitMs = Math.max(50, Math.ceil(deficit / bucket.tokensPerMinute * 6e4));
|
|
6011
|
-
await new Promise((
|
|
6128
|
+
await new Promise((resolve2) => setTimeout(resolve2, waitMs));
|
|
6012
6129
|
}
|
|
6013
6130
|
}
|
|
6014
6131
|
/**
|
|
@@ -6071,14 +6188,14 @@ var LocalRequestQueue = class {
|
|
|
6071
6188
|
this.active++;
|
|
6072
6189
|
return this.makeRelease();
|
|
6073
6190
|
}
|
|
6074
|
-
return new Promise((
|
|
6191
|
+
return new Promise((resolve2, reject) => {
|
|
6075
6192
|
let settled = false;
|
|
6076
6193
|
let timer;
|
|
6077
6194
|
const resolver = (release) => {
|
|
6078
6195
|
if (settled) return;
|
|
6079
6196
|
settled = true;
|
|
6080
6197
|
if (timer !== void 0) clearTimeout(timer);
|
|
6081
|
-
|
|
6198
|
+
resolve2(release);
|
|
6082
6199
|
};
|
|
6083
6200
|
if (timeoutMs !== void 0 && timeoutMs > 0) {
|
|
6084
6201
|
timer = setTimeout(() => {
|
|
@@ -6705,6 +6822,7 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter {
|
|
|
6705
6822
|
tpmLimiter;
|
|
6706
6823
|
localQueue;
|
|
6707
6824
|
taskAnalyzer;
|
|
6825
|
+
pendingFeedbackSource;
|
|
6708
6826
|
worldStateDB;
|
|
6709
6827
|
privacyPaths;
|
|
6710
6828
|
guidanceQueue;
|
|
@@ -7159,8 +7277,18 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter {
|
|
|
7159
7277
|
return windows.length ? Math.min(...windows) : void 0;
|
|
7160
7278
|
}
|
|
7161
7279
|
/** Wire the Cascade Auto task analyzer used for per-subtask model routing. */
|
|
7280
|
+
/**
|
|
7281
|
+
* Feed per-model thumbs counts into Auto routing. Forwarded to the analyzer
|
|
7282
|
+
* because that is where the public benchmark score is combined; the prior
|
|
7283
|
+
* adjusts that number rather than sitting anywhere else in the pipeline.
|
|
7284
|
+
*/
|
|
7285
|
+
setFeedbackSource(source) {
|
|
7286
|
+
this.taskAnalyzer?.setFeedbackSource(source);
|
|
7287
|
+
this.pendingFeedbackSource = source;
|
|
7288
|
+
}
|
|
7162
7289
|
setTaskAnalyzer(analyzer) {
|
|
7163
7290
|
this.taskAnalyzer = analyzer;
|
|
7291
|
+
if (this.pendingFeedbackSource) analyzer.setFeedbackSource(this.pendingFeedbackSource);
|
|
7164
7292
|
}
|
|
7165
7293
|
setWorldStateDB(db) {
|
|
7166
7294
|
this.worldStateDB = db;
|
|
@@ -8255,8 +8383,11 @@ Now execute your subtask using this context where relevant.`
|
|
|
8255
8383
|
const result = await this.runAgentLoop(systemPrompt, this.tools);
|
|
8256
8384
|
output = result.output;
|
|
8257
8385
|
toolCalls = result.toolCalls;
|
|
8258
|
-
this.
|
|
8259
|
-
|
|
8386
|
+
const mustProduceArtifact = this.requiresArtifact();
|
|
8387
|
+
if (mustProduceArtifact) {
|
|
8388
|
+
this.sendStatusUpdate({ progressPct: 65, currentAction: "Verifying required artifacts", status: "IN_PROGRESS" });
|
|
8389
|
+
}
|
|
8390
|
+
const artifactCheck = mustProduceArtifact ? await this.verifyArtifacts(assignment) : { ok: true, issues: [] };
|
|
8260
8391
|
if (!artifactCheck.ok) {
|
|
8261
8392
|
correctionAttempts = 1;
|
|
8262
8393
|
issues.push(...artifactCheck.issues);
|
|
@@ -8536,11 +8667,11 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : "") + textToolSuffix,
|
|
|
8536
8667
|
if (!wasApproved) return `Tool ${tc.name} was denied by user.`;
|
|
8537
8668
|
} else {
|
|
8538
8669
|
const LEGACY_APPROVAL_TIMEOUT_MS = 6e5;
|
|
8539
|
-
const legacyDecision = await new Promise((
|
|
8670
|
+
const legacyDecision = await new Promise((resolve2) => {
|
|
8540
8671
|
const eventName = `tool:approval-response:${this.id}-${tc.id}`;
|
|
8541
8672
|
const timer = setTimeout(() => {
|
|
8542
8673
|
this.removeAllListeners(eventName);
|
|
8543
|
-
|
|
8674
|
+
resolve2({ approved: false });
|
|
8544
8675
|
}, LEGACY_APPROVAL_TIMEOUT_MS);
|
|
8545
8676
|
timer.unref?.();
|
|
8546
8677
|
this.emit("tool:approval-request", {
|
|
@@ -8553,7 +8684,7 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : "") + textToolSuffix,
|
|
|
8553
8684
|
});
|
|
8554
8685
|
this.once(eventName, (d) => {
|
|
8555
8686
|
clearTimeout(timer);
|
|
8556
|
-
|
|
8687
|
+
resolve2(d);
|
|
8557
8688
|
});
|
|
8558
8689
|
});
|
|
8559
8690
|
if (legacyDecision.always) this.sessionApprovals.set(tc.name, legacyDecision.approved);
|
|
@@ -8573,6 +8704,9 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : "") + textToolSuffix,
|
|
|
8573
8704
|
tierId: this.id,
|
|
8574
8705
|
sessionId: this.taskId,
|
|
8575
8706
|
requireApproval: false,
|
|
8707
|
+
// Media generation can run for a minute; without this a cancelled run
|
|
8708
|
+
// still pays for an image nobody will see.
|
|
8709
|
+
...this.signal ? { signal: this.signal } : {},
|
|
8576
8710
|
saveSnapshot: async (path37, content) => {
|
|
8577
8711
|
this.store?.addFileSnapshot(this.taskId, path37, content);
|
|
8578
8712
|
},
|
|
@@ -8620,7 +8754,8 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : "") + textToolSuffix,
|
|
|
8620
8754
|
const result = await this.toolRegistry.execute(altTool, tc.input, {
|
|
8621
8755
|
tierId: this.id,
|
|
8622
8756
|
sessionId: this.taskId,
|
|
8623
|
-
requireApproval: false
|
|
8757
|
+
requireApproval: false,
|
|
8758
|
+
...this.signal ? { signal: this.signal } : {}
|
|
8624
8759
|
});
|
|
8625
8760
|
const str2 = typeof result === "string" ? result : JSON.stringify(result);
|
|
8626
8761
|
if (!str2.startsWith("Tool error:") && !str2.startsWith("Error:")) {
|
|
@@ -8642,7 +8777,8 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : "") + textToolSuffix,
|
|
|
8642
8777
|
const result = await this.toolRegistry.execute(newToolName, tc.input, {
|
|
8643
8778
|
tierId: this.id,
|
|
8644
8779
|
sessionId: this.taskId,
|
|
8645
|
-
requireApproval: false
|
|
8780
|
+
requireApproval: false,
|
|
8781
|
+
...this.signal ? { signal: this.signal } : {}
|
|
8646
8782
|
});
|
|
8647
8783
|
const str2 = typeof result === "string" ? result : JSON.stringify(result);
|
|
8648
8784
|
if (!str2.startsWith("Tool error:")) return `[Synthesized ${newToolName}]: ${str2}`;
|
|
@@ -9060,7 +9196,7 @@ var PeerBus = class extends EventEmitter {
|
|
|
9060
9196
|
this.outputs.set(subtaskId, entry);
|
|
9061
9197
|
this.emit("output:ready", entry);
|
|
9062
9198
|
const waiting = this.waiters.get(subtaskId) ?? [];
|
|
9063
|
-
for (const
|
|
9199
|
+
for (const resolve2 of waiting) resolve2(entry);
|
|
9064
9200
|
this.waiters.delete(subtaskId);
|
|
9065
9201
|
}
|
|
9066
9202
|
/**
|
|
@@ -9091,10 +9227,10 @@ var PeerBus = class extends EventEmitter {
|
|
|
9091
9227
|
waitFor(subtaskId, timeoutMs = 12e4) {
|
|
9092
9228
|
const existing = this.outputs.get(subtaskId);
|
|
9093
9229
|
if (existing && !this.retryPending.has(subtaskId)) return Promise.resolve(existing);
|
|
9094
|
-
return new Promise((
|
|
9230
|
+
return new Promise((resolve2, reject) => {
|
|
9095
9231
|
const resolver = (output) => {
|
|
9096
9232
|
clearTimeout(timer);
|
|
9097
|
-
|
|
9233
|
+
resolve2(output);
|
|
9098
9234
|
};
|
|
9099
9235
|
const timer = setTimeout(() => {
|
|
9100
9236
|
const waiting = this.waiters.get(subtaskId);
|
|
@@ -9148,10 +9284,10 @@ var PeerBus = class extends EventEmitter {
|
|
|
9148
9284
|
*/
|
|
9149
9285
|
collect(timeoutMs) {
|
|
9150
9286
|
const collected = [...this.broadcastLog];
|
|
9151
|
-
return new Promise((
|
|
9287
|
+
return new Promise((resolve2) => {
|
|
9152
9288
|
const timer = setTimeout(() => {
|
|
9153
9289
|
off();
|
|
9154
|
-
|
|
9290
|
+
resolve2(collected);
|
|
9155
9291
|
}, timeoutMs);
|
|
9156
9292
|
const handler = (msg) => {
|
|
9157
9293
|
collected.push({ fromId: msg.fromId, payload: msg.payload, timestamp: msg.timestamp });
|
|
@@ -9165,7 +9301,7 @@ var PeerBus = class extends EventEmitter {
|
|
|
9165
9301
|
const broadcasters = new Set(collected.map((m) => m.fromId));
|
|
9166
9302
|
if (broadcasters.size >= this.members.size) {
|
|
9167
9303
|
off();
|
|
9168
|
-
|
|
9304
|
+
resolve2(collected);
|
|
9169
9305
|
}
|
|
9170
9306
|
};
|
|
9171
9307
|
this.on("broadcast", checkComplete);
|
|
@@ -9183,7 +9319,7 @@ var PeerBus = class extends EventEmitter {
|
|
|
9183
9319
|
return;
|
|
9184
9320
|
}
|
|
9185
9321
|
this.emitCoordination(tierId, `\u23F3 waiting for ${filePath} (held by ${existing.holderId})`);
|
|
9186
|
-
return new Promise((
|
|
9322
|
+
return new Promise((resolve2, reject) => {
|
|
9187
9323
|
const timer = setTimeout(() => {
|
|
9188
9324
|
reject(new Error(`File lock timeout for ${filePath} (held by ${existing.holderId})`));
|
|
9189
9325
|
}, timeoutMs);
|
|
@@ -9191,7 +9327,7 @@ var PeerBus = class extends EventEmitter {
|
|
|
9191
9327
|
clearTimeout(timer);
|
|
9192
9328
|
this.fileLocks.set(filePath, { holderId: tierId, lockedAt: (/* @__PURE__ */ new Date()).toISOString(), waiters: [] });
|
|
9193
9329
|
this.emitCoordination(tierId, `\u{1F512} locked ${filePath}`);
|
|
9194
|
-
|
|
9330
|
+
resolve2();
|
|
9195
9331
|
});
|
|
9196
9332
|
});
|
|
9197
9333
|
}
|
|
@@ -9216,11 +9352,11 @@ var PeerBus = class extends EventEmitter {
|
|
|
9216
9352
|
*/
|
|
9217
9353
|
waitForFileRelease(filePath, timeoutMs = 3e4) {
|
|
9218
9354
|
if (!this.fileLocks.has(filePath)) return Promise.resolve();
|
|
9219
|
-
return new Promise((
|
|
9355
|
+
return new Promise((resolve2, reject) => {
|
|
9220
9356
|
const timer = setTimeout(() => reject(new Error(`Timeout waiting for file release: ${filePath}`)), timeoutMs);
|
|
9221
9357
|
this.once(`file:released:${filePath}`, () => {
|
|
9222
9358
|
clearTimeout(timer);
|
|
9223
|
-
|
|
9359
|
+
resolve2();
|
|
9224
9360
|
});
|
|
9225
9361
|
});
|
|
9226
9362
|
}
|
|
@@ -9286,13 +9422,13 @@ var PeerBus = class extends EventEmitter {
|
|
|
9286
9422
|
this.emit(`barrier:${barrierName}`);
|
|
9287
9423
|
return;
|
|
9288
9424
|
}
|
|
9289
|
-
return new Promise((
|
|
9425
|
+
return new Promise((resolve2, reject) => {
|
|
9290
9426
|
const timer = setTimeout(() => {
|
|
9291
9427
|
reject(new Error(`Barrier timeout: ${barrierName} (${bar.arrived.size}/${bar.total} arrived)`));
|
|
9292
9428
|
}, 12e4);
|
|
9293
9429
|
this.once(`barrier:${barrierName}`, () => {
|
|
9294
9430
|
clearTimeout(timer);
|
|
9295
|
-
|
|
9431
|
+
resolve2();
|
|
9296
9432
|
});
|
|
9297
9433
|
});
|
|
9298
9434
|
}
|
|
@@ -10843,6 +10979,175 @@ Reply with exactly one word: YES, NO, or UNSURE.
|
|
|
10843
10979
|
// src/core/cascade.ts
|
|
10844
10980
|
init_cost();
|
|
10845
10981
|
|
|
10982
|
+
// src/core/multimodal/registry.ts
|
|
10983
|
+
init_pricing();
|
|
10984
|
+
var CAPABILITIES = [
|
|
10985
|
+
// ── Image ──
|
|
10986
|
+
{
|
|
10987
|
+
modality: "image",
|
|
10988
|
+
provider: "openai",
|
|
10989
|
+
modelId: "dall-e-3",
|
|
10990
|
+
api: "openai-images"
|
|
10991
|
+
},
|
|
10992
|
+
{
|
|
10993
|
+
modality: "image",
|
|
10994
|
+
provider: "gemini",
|
|
10995
|
+
modelId: "gemini-3-pro-image",
|
|
10996
|
+
api: "gemini-predict"
|
|
10997
|
+
},
|
|
10998
|
+
// ── Speech (text → audio) ──
|
|
10999
|
+
{
|
|
11000
|
+
modality: "speech",
|
|
11001
|
+
provider: "openai",
|
|
11002
|
+
modelId: "tts-1",
|
|
11003
|
+
api: "openai-speech"
|
|
11004
|
+
},
|
|
11005
|
+
{
|
|
11006
|
+
// The only quality claim in this table that needs no leaderboard: OpenAI
|
|
11007
|
+
// ships these as the same voice at two fidelities and charges double for
|
|
11008
|
+
// this one. That is the vendor's own ordering, not an inference.
|
|
11009
|
+
modality: "speech",
|
|
11010
|
+
provider: "openai",
|
|
11011
|
+
modelId: "tts-1-hd",
|
|
11012
|
+
api: "openai-speech",
|
|
11013
|
+
quality: 70,
|
|
11014
|
+
qualityBasis: "OpenAI's own hd variant of tts-1, priced 2x"
|
|
11015
|
+
},
|
|
11016
|
+
// ── Transcription (audio → text) ──
|
|
11017
|
+
{
|
|
11018
|
+
modality: "transcription",
|
|
11019
|
+
provider: "openai",
|
|
11020
|
+
modelId: "whisper-1",
|
|
11021
|
+
api: "openai-transcriptions"
|
|
11022
|
+
},
|
|
11023
|
+
// ── Video ──
|
|
11024
|
+
{
|
|
11025
|
+
// Listed so `describe()` can tell the truth about what exists, and so the
|
|
11026
|
+
// planner is told video is out of reach rather than left to invent a tool.
|
|
11027
|
+
// Veo is a long-running operation: submit, poll, then fetch bytes from a
|
|
11028
|
+
// signed URL. That is a different execution shape from every other entry
|
|
11029
|
+
// here and is not wired up — claiming otherwise would produce a tool that
|
|
11030
|
+
// fails at call time instead of an honest "not yet".
|
|
11031
|
+
modality: "video",
|
|
11032
|
+
provider: "gemini",
|
|
11033
|
+
modelId: "veo-3.1-generate-001",
|
|
11034
|
+
api: "gemini-predict",
|
|
11035
|
+
unsupported: "Veo runs as a long-running operation (submit \u2192 poll \u2192 fetch). Not wired up yet."
|
|
11036
|
+
}
|
|
11037
|
+
];
|
|
11038
|
+
function capabilityCost(cap) {
|
|
11039
|
+
const match = findPricing(cap.modelId, cap.provider, {
|
|
11040
|
+
fallbackProviders: cap.provider === "gemini" ? ["vertex"] : []
|
|
11041
|
+
});
|
|
11042
|
+
const rate = match?.entry.rates?.[0];
|
|
11043
|
+
if (!rate) return null;
|
|
11044
|
+
const unitLabel = {
|
|
11045
|
+
per_image: "per image",
|
|
11046
|
+
per_second: "per second of video",
|
|
11047
|
+
per_character: "per character",
|
|
11048
|
+
per_audio_minute: "per audio minute"
|
|
11049
|
+
};
|
|
11050
|
+
const unit = unitLabel[rate.unit] ?? rate.unit;
|
|
11051
|
+
const amount = rate.amount;
|
|
11052
|
+
const shown = amount < 1e-3 ? amount.toExponential(2) : amount.toFixed(4);
|
|
11053
|
+
return { label: `$${shown} ${unit}`, amount, unit };
|
|
11054
|
+
}
|
|
11055
|
+
var MultimodalRegistry = class {
|
|
11056
|
+
/** Provider types the user has actually configured a key for. */
|
|
11057
|
+
available;
|
|
11058
|
+
constructor(availableProviders) {
|
|
11059
|
+
this.available = new Set(availableProviders);
|
|
11060
|
+
}
|
|
11061
|
+
/** Every capability whose provider is configured AND which Cascade can call. */
|
|
11062
|
+
usable(modality) {
|
|
11063
|
+
return CAPABILITIES.filter(
|
|
11064
|
+
(c) => !c.unsupported && this.available.has(c.provider) && (!modality || c.modality === modality)
|
|
11065
|
+
);
|
|
11066
|
+
}
|
|
11067
|
+
/** Modalities this account can actually generate right now. */
|
|
11068
|
+
availableModalities() {
|
|
11069
|
+
return [...new Set(this.usable().map((c) => c.modality))];
|
|
11070
|
+
}
|
|
11071
|
+
/**
|
|
11072
|
+
* Pick a model for a modality.
|
|
11073
|
+
*
|
|
11074
|
+
* Ranked models beat unranked ones, then higher quality, then lower unit
|
|
11075
|
+
* price. An explicit `preferModel` wins outright when it is usable — the user
|
|
11076
|
+
* naming a model is a stronger signal than any prior.
|
|
11077
|
+
*/
|
|
11078
|
+
select(modality, opts = {}) {
|
|
11079
|
+
const pool = this.usable(modality);
|
|
11080
|
+
if (!pool.length) return null;
|
|
11081
|
+
if (opts.preferModel) {
|
|
11082
|
+
const wanted = pool.find((c) => c.modelId === opts.preferModel);
|
|
11083
|
+
if (wanted) {
|
|
11084
|
+
return { capability: wanted, cost: capabilityCost(wanted), reason: `Requested explicitly: ${wanted.modelId}.` };
|
|
11085
|
+
}
|
|
11086
|
+
}
|
|
11087
|
+
const scored = pool.map((c) => ({ c, cost: capabilityCost(c) })).sort((a, b) => {
|
|
11088
|
+
const qa = a.c.quality ?? -1;
|
|
11089
|
+
const qb = b.c.quality ?? -1;
|
|
11090
|
+
if (qa !== qb) return qb - qa;
|
|
11091
|
+
const pa = a.cost?.amount ?? Number.POSITIVE_INFINITY;
|
|
11092
|
+
const pb = b.cost?.amount ?? Number.POSITIVE_INFINITY;
|
|
11093
|
+
return pa - pb;
|
|
11094
|
+
});
|
|
11095
|
+
const best = scored[0];
|
|
11096
|
+
const ranked = best.c.quality !== void 0;
|
|
11097
|
+
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.";
|
|
11098
|
+
return { capability: best.c, cost: best.cost, reason };
|
|
11099
|
+
}
|
|
11100
|
+
/**
|
|
11101
|
+
* A plain-language account of what this account can and cannot generate —
|
|
11102
|
+
* including the things it cannot, and why. The planner sees this, so being
|
|
11103
|
+
* candid here is what stops it inventing a `generate_music` call.
|
|
11104
|
+
*/
|
|
11105
|
+
describe() {
|
|
11106
|
+
const lines = [];
|
|
11107
|
+
const usable = this.usable();
|
|
11108
|
+
if (usable.length) {
|
|
11109
|
+
lines.push("Generation models available on your configured providers:");
|
|
11110
|
+
for (const modality of ["image", "speech", "transcription", "video"]) {
|
|
11111
|
+
const forModality = usable.filter((c) => c.modality === modality);
|
|
11112
|
+
if (!forModality.length) continue;
|
|
11113
|
+
const shown = forModality.map((c) => {
|
|
11114
|
+
const cost = capabilityCost(c);
|
|
11115
|
+
return `${c.modelId}${cost ? ` (${cost.label})` : ""}`;
|
|
11116
|
+
});
|
|
11117
|
+
lines.push(` ${modality}: ${shown.join(", ")}`);
|
|
11118
|
+
}
|
|
11119
|
+
} else {
|
|
11120
|
+
lines.push("No generation models are available \u2014 none of your configured providers offers one.");
|
|
11121
|
+
}
|
|
11122
|
+
const blocked = CAPABILITIES.filter((c) => c.unsupported && this.available.has(c.provider));
|
|
11123
|
+
for (const c of blocked) {
|
|
11124
|
+
lines.push(` ${c.modality}: ${c.modelId} exists on your account but is not callable. ${c.unsupported}`);
|
|
11125
|
+
}
|
|
11126
|
+
lines.push(
|
|
11127
|
+
" music: no supported provider exposes a music-generation API, so Cascade cannot generate music."
|
|
11128
|
+
);
|
|
11129
|
+
return lines.join("\n");
|
|
11130
|
+
}
|
|
11131
|
+
};
|
|
11132
|
+
|
|
11133
|
+
// src/tools/base.ts
|
|
11134
|
+
var BaseTool = class {
|
|
11135
|
+
workspaceRoot = process.cwd();
|
|
11136
|
+
setWorkspaceRoot(root) {
|
|
11137
|
+
this.workspaceRoot = root;
|
|
11138
|
+
}
|
|
11139
|
+
isDangerous() {
|
|
11140
|
+
return false;
|
|
11141
|
+
}
|
|
11142
|
+
getDefinition() {
|
|
11143
|
+
return {
|
|
11144
|
+
name: this.name,
|
|
11145
|
+
description: this.description,
|
|
11146
|
+
inputSchema: this.inputSchema
|
|
11147
|
+
};
|
|
11148
|
+
}
|
|
11149
|
+
};
|
|
11150
|
+
|
|
10846
11151
|
// src/core/router/provider-errors.ts
|
|
10847
11152
|
var SYSTEMIC = /* @__PURE__ */ new Set([
|
|
10848
11153
|
"rate_limit",
|
|
@@ -10907,7 +11212,7 @@ function classifyProviderError(err) {
|
|
|
10907
11212
|
return { kind, systemic: isSystemicKind(kind), status, raw };
|
|
10908
11213
|
}
|
|
10909
11214
|
function describeProviderError(c, modelId) {
|
|
10910
|
-
const on = ` on ${modelId}` ;
|
|
11215
|
+
const on = modelId ? ` on ${modelId}` : "";
|
|
10911
11216
|
const detail = c.raw ? ` Provider said: ${c.raw}` : "";
|
|
10912
11217
|
switch (c.kind) {
|
|
10913
11218
|
case "rate_limit":
|
|
@@ -10929,6 +11234,295 @@ function describeProviderError(c, modelId) {
|
|
|
10929
11234
|
}
|
|
10930
11235
|
}
|
|
10931
11236
|
|
|
11237
|
+
// src/core/multimodal/generate.ts
|
|
11238
|
+
var DEFAULT_TIMEOUT_MS = 12e4;
|
|
11239
|
+
async function callProvider(fn, modelId) {
|
|
11240
|
+
try {
|
|
11241
|
+
return await fn();
|
|
11242
|
+
} catch (err) {
|
|
11243
|
+
const classified = classifyProviderError(err);
|
|
11244
|
+
throw new Error(describeProviderError(classified, modelId));
|
|
11245
|
+
}
|
|
11246
|
+
}
|
|
11247
|
+
async function postJson(url, body, headers, signal) {
|
|
11248
|
+
const ac = new AbortController();
|
|
11249
|
+
const timer = setTimeout(() => ac.abort(), DEFAULT_TIMEOUT_MS);
|
|
11250
|
+
const onAbort = () => ac.abort();
|
|
11251
|
+
signal?.addEventListener("abort", onAbort);
|
|
11252
|
+
try {
|
|
11253
|
+
const res = await fetch(url, {
|
|
11254
|
+
method: "POST",
|
|
11255
|
+
headers: { "Content-Type": "application/json", ...headers },
|
|
11256
|
+
body: JSON.stringify(body),
|
|
11257
|
+
signal: ac.signal
|
|
11258
|
+
});
|
|
11259
|
+
if (!res.ok) {
|
|
11260
|
+
const text = await res.text().catch(() => "");
|
|
11261
|
+
throw Object.assign(new Error(text || res.statusText), { status: res.status });
|
|
11262
|
+
}
|
|
11263
|
+
return res;
|
|
11264
|
+
} finally {
|
|
11265
|
+
clearTimeout(timer);
|
|
11266
|
+
signal?.removeEventListener("abort", onAbort);
|
|
11267
|
+
}
|
|
11268
|
+
}
|
|
11269
|
+
function baseUrlFor(cfg, fallback) {
|
|
11270
|
+
return (cfg.baseUrl ?? fallback).replace(/\/+$/, "");
|
|
11271
|
+
}
|
|
11272
|
+
function stamp(ext) {
|
|
11273
|
+
return `cascade-${Date.now()}.${ext}`;
|
|
11274
|
+
}
|
|
11275
|
+
async function generateImage(cap, cfg, req, signal) {
|
|
11276
|
+
return callProvider(async () => {
|
|
11277
|
+
if (cap.api === "openai-images") {
|
|
11278
|
+
const url = `${baseUrlFor(cfg, "https://api.openai.com/v1")}/images/generations`;
|
|
11279
|
+
const res = await postJson(url, {
|
|
11280
|
+
model: cap.modelId,
|
|
11281
|
+
prompt: req.prompt,
|
|
11282
|
+
size: req.size ?? "1024x1024",
|
|
11283
|
+
n: 1,
|
|
11284
|
+
// b64 rather than a URL: see the file header — provider URLs expire.
|
|
11285
|
+
response_format: "b64_json"
|
|
11286
|
+
}, { Authorization: `Bearer ${cfg.apiKey ?? ""}` }, signal);
|
|
11287
|
+
const body = await res.json();
|
|
11288
|
+
const first = body.data?.[0];
|
|
11289
|
+
if (!first?.b64_json) throw new Error("Image response contained no image data.");
|
|
11290
|
+
return {
|
|
11291
|
+
data: Buffer.from(first.b64_json, "base64"),
|
|
11292
|
+
mimeType: "image/png",
|
|
11293
|
+
filename: stamp("png"),
|
|
11294
|
+
modelId: cap.modelId,
|
|
11295
|
+
provider: cap.provider,
|
|
11296
|
+
...first.revised_prompt ? { revisedPrompt: first.revised_prompt } : {}
|
|
11297
|
+
};
|
|
11298
|
+
}
|
|
11299
|
+
if (cap.api === "gemini-predict") {
|
|
11300
|
+
const base = baseUrlFor(cfg, "https://generativelanguage.googleapis.com/v1beta");
|
|
11301
|
+
const url = `${base}/models/${encodeURIComponent(cap.modelId)}:predict`;
|
|
11302
|
+
const res = await postJson(url, {
|
|
11303
|
+
instances: [{ prompt: req.prompt }],
|
|
11304
|
+
parameters: { sampleCount: 1 }
|
|
11305
|
+
}, { "x-goog-api-key": cfg.apiKey ?? "" }, signal);
|
|
11306
|
+
const body = await res.json();
|
|
11307
|
+
const first = body.predictions?.[0];
|
|
11308
|
+
if (!first?.bytesBase64Encoded) throw new Error("Image response contained no image data.");
|
|
11309
|
+
const mimeType = first.mimeType ?? "image/png";
|
|
11310
|
+
return {
|
|
11311
|
+
data: Buffer.from(first.bytesBase64Encoded, "base64"),
|
|
11312
|
+
mimeType,
|
|
11313
|
+
filename: stamp(mimeType.includes("jpeg") ? "jpg" : "png"),
|
|
11314
|
+
modelId: cap.modelId,
|
|
11315
|
+
provider: cap.provider
|
|
11316
|
+
};
|
|
11317
|
+
}
|
|
11318
|
+
throw new Error(`${cap.modelId} is not an image generator.`);
|
|
11319
|
+
}, cap.modelId);
|
|
11320
|
+
}
|
|
11321
|
+
async function generateSpeech(cap, cfg, req, signal) {
|
|
11322
|
+
return callProvider(async () => {
|
|
11323
|
+
if (cap.api !== "openai-speech") throw new Error(`${cap.modelId} is not a speech model.`);
|
|
11324
|
+
const format = req.format ?? "mp3";
|
|
11325
|
+
const url = `${baseUrlFor(cfg, "https://api.openai.com/v1")}/audio/speech`;
|
|
11326
|
+
const res = await postJson(url, {
|
|
11327
|
+
model: cap.modelId,
|
|
11328
|
+
input: req.text,
|
|
11329
|
+
voice: req.voice ?? "alloy",
|
|
11330
|
+
response_format: format
|
|
11331
|
+
}, { Authorization: `Bearer ${cfg.apiKey ?? ""}` }, signal);
|
|
11332
|
+
const mime = {
|
|
11333
|
+
mp3: "audio/mpeg",
|
|
11334
|
+
opus: "audio/opus",
|
|
11335
|
+
aac: "audio/aac",
|
|
11336
|
+
flac: "audio/flac",
|
|
11337
|
+
wav: "audio/wav"
|
|
11338
|
+
};
|
|
11339
|
+
return {
|
|
11340
|
+
data: Buffer.from(await res.arrayBuffer()),
|
|
11341
|
+
mimeType: mime[format] ?? "application/octet-stream",
|
|
11342
|
+
filename: stamp(format),
|
|
11343
|
+
modelId: cap.modelId,
|
|
11344
|
+
provider: cap.provider
|
|
11345
|
+
};
|
|
11346
|
+
}, cap.modelId);
|
|
11347
|
+
}
|
|
11348
|
+
async function transcribeAudio(cap, cfg, req, signal) {
|
|
11349
|
+
return callProvider(async () => {
|
|
11350
|
+
if (cap.api !== "openai-transcriptions") throw new Error(`${cap.modelId} is not a transcription model.`);
|
|
11351
|
+
const url = `${baseUrlFor(cfg, "https://api.openai.com/v1")}/audio/transcriptions`;
|
|
11352
|
+
const form = new FormData();
|
|
11353
|
+
form.append("model", cap.modelId);
|
|
11354
|
+
if (req.language) form.append("language", req.language);
|
|
11355
|
+
form.append("file", new Blob([new Uint8Array(req.audio)]), req.filename);
|
|
11356
|
+
const ac = new AbortController();
|
|
11357
|
+
const timer = setTimeout(() => ac.abort(), DEFAULT_TIMEOUT_MS);
|
|
11358
|
+
const onAbort = () => ac.abort();
|
|
11359
|
+
signal?.addEventListener("abort", onAbort);
|
|
11360
|
+
try {
|
|
11361
|
+
const res = await fetch(url, {
|
|
11362
|
+
method: "POST",
|
|
11363
|
+
headers: { Authorization: `Bearer ${cfg.apiKey ?? ""}` },
|
|
11364
|
+
body: form,
|
|
11365
|
+
signal: ac.signal
|
|
11366
|
+
});
|
|
11367
|
+
if (!res.ok) {
|
|
11368
|
+
const text = await res.text().catch(() => "");
|
|
11369
|
+
throw Object.assign(new Error(text || res.statusText), { status: res.status });
|
|
11370
|
+
}
|
|
11371
|
+
const body = await res.json();
|
|
11372
|
+
return { text: body.text ?? "", modelId: cap.modelId };
|
|
11373
|
+
} finally {
|
|
11374
|
+
clearTimeout(timer);
|
|
11375
|
+
signal?.removeEventListener("abort", onAbort);
|
|
11376
|
+
}
|
|
11377
|
+
}, cap.modelId);
|
|
11378
|
+
}
|
|
11379
|
+
|
|
11380
|
+
// src/tools/generate-media.ts
|
|
11381
|
+
function resolve(deps, modality, preferModel) {
|
|
11382
|
+
const selected = deps.registry.select(modality, preferModel ? { preferModel } : {});
|
|
11383
|
+
if (!selected) {
|
|
11384
|
+
return { ok: false, error: `No ${modality} model is available on your configured providers.` };
|
|
11385
|
+
}
|
|
11386
|
+
const cfg = deps.lookupProvider(selected.capability.provider);
|
|
11387
|
+
if (!cfg) {
|
|
11388
|
+
return {
|
|
11389
|
+
ok: false,
|
|
11390
|
+
error: `${selected.capability.modelId} needs the ${selected.capability.provider} provider, which is not configured.`
|
|
11391
|
+
};
|
|
11392
|
+
}
|
|
11393
|
+
return { ok: true, selected, cfg };
|
|
11394
|
+
}
|
|
11395
|
+
var GenerateImageTool = class extends BaseTool {
|
|
11396
|
+
constructor(deps) {
|
|
11397
|
+
super();
|
|
11398
|
+
this.deps = deps;
|
|
11399
|
+
}
|
|
11400
|
+
deps;
|
|
11401
|
+
name = "generate_image";
|
|
11402
|
+
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.";
|
|
11403
|
+
inputSchema = {
|
|
11404
|
+
type: "object",
|
|
11405
|
+
properties: {
|
|
11406
|
+
prompt: { type: "string", description: "What the image should show. Be specific about subject, style and composition." },
|
|
11407
|
+
size: { type: "string", description: 'Pixel size, e.g. "1024x1024" (default), "1792x1024", "1024x1792".' },
|
|
11408
|
+
model: { type: "string", description: "Optional: force a specific image model instead of the automatic choice." }
|
|
11409
|
+
},
|
|
11410
|
+
required: ["prompt"]
|
|
11411
|
+
};
|
|
11412
|
+
async execute(input, options) {
|
|
11413
|
+
const prompt = String(input["prompt"] ?? "").trim();
|
|
11414
|
+
if (!prompt) return 'Provide a "prompt" describing the image.';
|
|
11415
|
+
const r = resolve(this.deps, "image", input["model"] ? String(input["model"]) : void 0);
|
|
11416
|
+
if (!r.ok) return r.error;
|
|
11417
|
+
const size = input["size"] ? String(input["size"]) : void 0;
|
|
11418
|
+
const asset = await generateImage(
|
|
11419
|
+
r.selected.capability,
|
|
11420
|
+
r.cfg,
|
|
11421
|
+
{ prompt, ...size ? { size } : {} },
|
|
11422
|
+
options.signal
|
|
11423
|
+
);
|
|
11424
|
+
const location = await this.deps.sink(asset);
|
|
11425
|
+
return [
|
|
11426
|
+
`Image generated and saved to ${location}.`,
|
|
11427
|
+
`Model: ${asset.modelId} \u2014 ${r.selected.reason}`,
|
|
11428
|
+
// DALL·E rewrites the prompt before drawing; the model needs to know what
|
|
11429
|
+
// was actually rendered, not what it asked for, or its description of the
|
|
11430
|
+
// result will not match the image.
|
|
11431
|
+
asset.revisedPrompt ? `The provider rewrote the prompt to: ${asset.revisedPrompt}` : ""
|
|
11432
|
+
].filter(Boolean).join("\n");
|
|
11433
|
+
}
|
|
11434
|
+
};
|
|
11435
|
+
var GenerateSpeechTool = class extends BaseTool {
|
|
11436
|
+
constructor(deps) {
|
|
11437
|
+
super();
|
|
11438
|
+
this.deps = deps;
|
|
11439
|
+
}
|
|
11440
|
+
deps;
|
|
11441
|
+
name = "generate_speech";
|
|
11442
|
+
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.";
|
|
11443
|
+
inputSchema = {
|
|
11444
|
+
type: "object",
|
|
11445
|
+
properties: {
|
|
11446
|
+
text: { type: "string", description: "The text to speak." },
|
|
11447
|
+
voice: { type: "string", description: "Voice id: alloy, echo, fable, onyx, nova, shimmer. Default alloy." },
|
|
11448
|
+
format: { type: "string", description: "Audio format: mp3 (default), opus, aac, flac, wav." },
|
|
11449
|
+
model: { type: "string", description: "Optional: force a specific speech model." }
|
|
11450
|
+
},
|
|
11451
|
+
required: ["text"]
|
|
11452
|
+
};
|
|
11453
|
+
async execute(input, options) {
|
|
11454
|
+
const text = String(input["text"] ?? "").trim();
|
|
11455
|
+
if (!text) return 'Provide "text" to speak.';
|
|
11456
|
+
const r = resolve(this.deps, "speech", input["model"] ? String(input["model"]) : void 0);
|
|
11457
|
+
if (!r.ok) return r.error;
|
|
11458
|
+
const fmt = String(input["format"] ?? "mp3");
|
|
11459
|
+
const allowed = ["mp3", "opus", "aac", "flac", "wav"];
|
|
11460
|
+
const format = allowed.includes(fmt) ? fmt : "mp3";
|
|
11461
|
+
const asset = await generateSpeech(
|
|
11462
|
+
r.selected.capability,
|
|
11463
|
+
r.cfg,
|
|
11464
|
+
{ text, voice: input["voice"] ? String(input["voice"]) : void 0, format },
|
|
11465
|
+
options.signal
|
|
11466
|
+
);
|
|
11467
|
+
const location = await this.deps.sink(asset);
|
|
11468
|
+
return `Speech generated and saved to ${location}.
|
|
11469
|
+
Model: ${asset.modelId} \u2014 ${r.selected.reason}`;
|
|
11470
|
+
}
|
|
11471
|
+
};
|
|
11472
|
+
var TranscribeAudioTool = class extends BaseTool {
|
|
11473
|
+
constructor(deps, readFile) {
|
|
11474
|
+
super();
|
|
11475
|
+
this.deps = deps;
|
|
11476
|
+
this.readFile = readFile;
|
|
11477
|
+
}
|
|
11478
|
+
deps;
|
|
11479
|
+
readFile;
|
|
11480
|
+
name = "transcribe_audio";
|
|
11481
|
+
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.";
|
|
11482
|
+
inputSchema = {
|
|
11483
|
+
type: "object",
|
|
11484
|
+
properties: {
|
|
11485
|
+
path: { type: "string", description: "Path to the audio file to transcribe." },
|
|
11486
|
+
language: { type: "string", description: 'Optional ISO-639-1 language hint, e.g. "en", improves accuracy.' },
|
|
11487
|
+
model: { type: "string", description: "Optional: force a specific transcription model." }
|
|
11488
|
+
},
|
|
11489
|
+
required: ["path"]
|
|
11490
|
+
};
|
|
11491
|
+
async execute(input, options) {
|
|
11492
|
+
const filePath = String(input["path"] ?? "").trim();
|
|
11493
|
+
if (!filePath) return 'Provide a "path" to the audio file.';
|
|
11494
|
+
const r = resolve(this.deps, "transcription", input["model"] ? String(input["model"]) : void 0);
|
|
11495
|
+
if (!r.ok) return r.error;
|
|
11496
|
+
let audio;
|
|
11497
|
+
try {
|
|
11498
|
+
audio = await this.readFile(filePath);
|
|
11499
|
+
} catch {
|
|
11500
|
+
return `Could not read the audio file at ${filePath}.`;
|
|
11501
|
+
}
|
|
11502
|
+
const { text, modelId } = await transcribeAudio(
|
|
11503
|
+
r.selected.capability,
|
|
11504
|
+
r.cfg,
|
|
11505
|
+
{
|
|
11506
|
+
audio,
|
|
11507
|
+
filename: filePath.split(/[\\/]/).pop() || "audio",
|
|
11508
|
+
...input["language"] ? { language: String(input["language"]) } : {}
|
|
11509
|
+
},
|
|
11510
|
+
options.signal
|
|
11511
|
+
);
|
|
11512
|
+
return text ? `Transcript (${modelId}):
|
|
11513
|
+
|
|
11514
|
+
${text}` : `${modelId} returned an empty transcript \u2014 the file may be silent or in an unsupported format.`;
|
|
11515
|
+
}
|
|
11516
|
+
};
|
|
11517
|
+
function buildMediaTools(deps, readFile) {
|
|
11518
|
+
const tools = [];
|
|
11519
|
+
const modalities = new Set(deps.registry.availableModalities());
|
|
11520
|
+
if (modalities.has("image")) tools.push(new GenerateImageTool(deps));
|
|
11521
|
+
if (modalities.has("speech")) tools.push(new GenerateSpeechTool(deps));
|
|
11522
|
+
if (modalities.has("transcription") && readFile) tools.push(new TranscribeAudioTool(deps, readFile));
|
|
11523
|
+
return tools;
|
|
11524
|
+
}
|
|
11525
|
+
|
|
10932
11526
|
// src/core/run-breaker.ts
|
|
10933
11527
|
var DEFAULT_FAILURE_THRESHOLD = 3;
|
|
10934
11528
|
var RunBreaker = class {
|
|
@@ -10997,26 +11591,6 @@ var RunBreaker = class {
|
|
|
10997
11591
|
|
|
10998
11592
|
// src/tools/registry.ts
|
|
10999
11593
|
init_constants();
|
|
11000
|
-
|
|
11001
|
-
// src/tools/base.ts
|
|
11002
|
-
var BaseTool = class {
|
|
11003
|
-
workspaceRoot = process.cwd();
|
|
11004
|
-
setWorkspaceRoot(root) {
|
|
11005
|
-
this.workspaceRoot = root;
|
|
11006
|
-
}
|
|
11007
|
-
isDangerous() {
|
|
11008
|
-
return false;
|
|
11009
|
-
}
|
|
11010
|
-
getDefinition() {
|
|
11011
|
-
return {
|
|
11012
|
-
name: this.name,
|
|
11013
|
-
description: this.description,
|
|
11014
|
-
inputSchema: this.inputSchema
|
|
11015
|
-
};
|
|
11016
|
-
}
|
|
11017
|
-
};
|
|
11018
|
-
|
|
11019
|
-
// src/tools/shell.ts
|
|
11020
11594
|
var execAsync = promisify(exec);
|
|
11021
11595
|
var ShellTool = class extends BaseTool {
|
|
11022
11596
|
name = "shell";
|
|
@@ -11712,7 +12286,7 @@ var PDFCreateTool = class extends BaseTool {
|
|
|
11712
12286
|
if (!fs29.existsSync(dir)) {
|
|
11713
12287
|
fs29.mkdirSync(dir, { recursive: true });
|
|
11714
12288
|
}
|
|
11715
|
-
return new Promise((
|
|
12289
|
+
return new Promise((resolve2, reject) => {
|
|
11716
12290
|
try {
|
|
11717
12291
|
const doc = new PDFDocument({ margin: 50 });
|
|
11718
12292
|
const stream = fs29.createWriteStream(filePath);
|
|
@@ -11730,7 +12304,7 @@ var PDFCreateTool = class extends BaseTool {
|
|
|
11730
12304
|
});
|
|
11731
12305
|
doc.end();
|
|
11732
12306
|
stream.on("finish", () => {
|
|
11733
|
-
|
|
12307
|
+
resolve2(`Successfully created PDF at ${filePath} (${content.length} characters)`);
|
|
11734
12308
|
});
|
|
11735
12309
|
stream.on("error", (err) => {
|
|
11736
12310
|
reject(new Error(`Failed to write PDF: ${err.message}`));
|
|
@@ -11802,7 +12376,7 @@ var CodeInterpreterTool = class extends BaseTool {
|
|
|
11802
12376
|
const filePath = path32.join(tmpDir, fileName);
|
|
11803
12377
|
fs29.writeFileSync(filePath, code, "utf-8");
|
|
11804
12378
|
const execArgs = [filePath, ...args];
|
|
11805
|
-
return new Promise((
|
|
12379
|
+
return new Promise((resolve2) => {
|
|
11806
12380
|
const startMs = Date.now();
|
|
11807
12381
|
execFile(cmdPrefix, execArgs, { cwd: this.workspaceRoot, timeout: 3e4 }, (error, stdout, stderr) => {
|
|
11808
12382
|
const duration = Date.now() - startMs;
|
|
@@ -11816,17 +12390,17 @@ var CodeInterpreterTool = class extends BaseTool {
|
|
|
11816
12390
|
if (error) {
|
|
11817
12391
|
const timedOut = error.killed && duration >= 3e4;
|
|
11818
12392
|
if (timedOut) {
|
|
11819
|
-
|
|
12393
|
+
resolve2(`Execution timed out after 30s. Consider breaking the task into smaller pieces.
|
|
11820
12394
|
Partial stdout: ${stdout}
|
|
11821
12395
|
Stderr: ${stderr}`);
|
|
11822
12396
|
} else {
|
|
11823
|
-
|
|
12397
|
+
resolve2(`Execution failed (${duration}ms):
|
|
11824
12398
|
Error: ${error.message}
|
|
11825
12399
|
Stderr: ${stderr}
|
|
11826
12400
|
Stdout: ${stdout}`);
|
|
11827
12401
|
}
|
|
11828
12402
|
} else {
|
|
11829
|
-
|
|
12403
|
+
resolve2(`Execution successful (${duration}ms):
|
|
11830
12404
|
Stdout: ${stdout}
|
|
11831
12405
|
Stderr: ${stderr}`);
|
|
11832
12406
|
}
|
|
@@ -12480,7 +13054,7 @@ var ToolRegistry = class extends EventEmitter {
|
|
|
12480
13054
|
*/
|
|
12481
13055
|
waitForTool(toolName, timeoutMs = 6e4) {
|
|
12482
13056
|
if (this.tools.has(toolName)) return Promise.resolve();
|
|
12483
|
-
return new Promise((
|
|
13057
|
+
return new Promise((resolve2, reject) => {
|
|
12484
13058
|
const timer = setTimeout(() => {
|
|
12485
13059
|
this.off("tool:added", handler);
|
|
12486
13060
|
reject(new Error(`Timeout waiting for tool: ${toolName}`));
|
|
@@ -12489,7 +13063,7 @@ var ToolRegistry = class extends EventEmitter {
|
|
|
12489
13063
|
if (name === toolName) {
|
|
12490
13064
|
clearTimeout(timer);
|
|
12491
13065
|
this.off("tool:added", handler);
|
|
12492
|
-
|
|
13066
|
+
resolve2();
|
|
12493
13067
|
}
|
|
12494
13068
|
};
|
|
12495
13069
|
this.on("tool:added", handler);
|
|
@@ -13142,20 +13716,20 @@ var PermissionEscalator = class extends EventEmitter {
|
|
|
13142
13716
|
resolver(decision);
|
|
13143
13717
|
}
|
|
13144
13718
|
waitForUserDecision(req) {
|
|
13145
|
-
return new Promise((
|
|
13719
|
+
return new Promise((resolve2) => {
|
|
13146
13720
|
let timer;
|
|
13147
13721
|
const wrappedResolver = (decision) => {
|
|
13148
13722
|
if (timer) clearTimeout(timer);
|
|
13149
13723
|
if (decision.always) {
|
|
13150
13724
|
this.taskWideCache.set(req.toolName, decision.approved);
|
|
13151
13725
|
}
|
|
13152
|
-
|
|
13726
|
+
resolve2(decision);
|
|
13153
13727
|
};
|
|
13154
13728
|
this.pendingUserDecisions.set(req.id, wrappedResolver);
|
|
13155
13729
|
if (this.approvalTimeoutMs > 0 && Number.isFinite(this.approvalTimeoutMs)) {
|
|
13156
13730
|
timer = setTimeout(() => {
|
|
13157
13731
|
if (this.pendingUserDecisions.delete(req.id)) {
|
|
13158
|
-
|
|
13732
|
+
resolve2({
|
|
13159
13733
|
requestId: req.id,
|
|
13160
13734
|
approved: false,
|
|
13161
13735
|
decidedBy: "USER",
|
|
@@ -13223,6 +13797,23 @@ var noopTelemetry = {
|
|
|
13223
13797
|
}
|
|
13224
13798
|
};
|
|
13225
13799
|
|
|
13800
|
+
// src/core/router/feedback-prior.ts
|
|
13801
|
+
var MAX_FEEDBACK_ADJUSTMENT = 0.05;
|
|
13802
|
+
var PRIOR_STRENGTH = 10;
|
|
13803
|
+
function feedbackAdjustment(counts) {
|
|
13804
|
+
const good = Math.max(0, Math.floor(counts.good));
|
|
13805
|
+
const bad = Math.max(0, Math.floor(counts.bad));
|
|
13806
|
+
const n = good + bad;
|
|
13807
|
+
if (n === 0) return 0;
|
|
13808
|
+
const half = PRIOR_STRENGTH / 2;
|
|
13809
|
+
const shrunk = (good + half) / (n + PRIOR_STRENGTH);
|
|
13810
|
+
return (shrunk - 0.5) * 2 * MAX_FEEDBACK_ADJUSTMENT;
|
|
13811
|
+
}
|
|
13812
|
+
function applyFeedback(score01, counts) {
|
|
13813
|
+
const adjusted = score01 + feedbackAdjustment(counts);
|
|
13814
|
+
return Math.max(0, Math.min(1, adjusted));
|
|
13815
|
+
}
|
|
13816
|
+
|
|
13226
13817
|
// src/core/router/task-analyzer.ts
|
|
13227
13818
|
init_pricing();
|
|
13228
13819
|
var CODE_SIGNALS = [
|
|
@@ -13298,6 +13889,8 @@ var TASK_TYPE_TAGS = {
|
|
|
13298
13889
|
};
|
|
13299
13890
|
var TaskAnalyzer = class {
|
|
13300
13891
|
tracker;
|
|
13892
|
+
/** Per-model thumbs counts, when the host collects them. */
|
|
13893
|
+
feedback;
|
|
13301
13894
|
bias;
|
|
13302
13895
|
lastProfile = null;
|
|
13303
13896
|
lastSelectedModels = /* @__PURE__ */ new Map();
|
|
@@ -13305,6 +13898,14 @@ var TaskAnalyzer = class {
|
|
|
13305
13898
|
this.tracker = tracker;
|
|
13306
13899
|
this.bias = bias;
|
|
13307
13900
|
}
|
|
13901
|
+
/**
|
|
13902
|
+
* Supply per-model rating counts. Optional by design: a host that collects no
|
|
13903
|
+
* feedback (the CLI, a fresh install) scores exactly as it did before, since
|
|
13904
|
+
* feedbackAdjustment returns 0 for an empty record.
|
|
13905
|
+
*/
|
|
13906
|
+
setFeedbackSource(source) {
|
|
13907
|
+
this.feedback = source;
|
|
13908
|
+
}
|
|
13308
13909
|
setTracker(tracker) {
|
|
13309
13910
|
this.tracker = tracker;
|
|
13310
13911
|
}
|
|
@@ -13388,7 +13989,10 @@ var TaskAnalyzer = class {
|
|
|
13388
13989
|
const perf = this.tracker?.performanceScore(model.id, profile.type) ?? 0.5;
|
|
13389
13990
|
const costEff = this.costEfficiency(model, profile.complexity);
|
|
13390
13991
|
const match = this.taskMatchScore(model, profile);
|
|
13391
|
-
const
|
|
13992
|
+
const rated = this.feedback?.(model.id);
|
|
13993
|
+
const publicScore = benchmarkScore01(model, profile.type);
|
|
13994
|
+
const adjusted = rated ? applyFeedback(publicScore, rated) : publicScore;
|
|
13995
|
+
const benchmark = 0.3 + 0.7 * adjusted;
|
|
13392
13996
|
switch (this.bias) {
|
|
13393
13997
|
case "quality":
|
|
13394
13998
|
return perf * match * benchmark ** 2 * (0.85 + 0.15 * costEff);
|
|
@@ -13793,10 +14397,10 @@ var DynamicTool = class extends BaseTool {
|
|
|
13793
14397
|
`;
|
|
13794
14398
|
const script = await isolate.compileScript(bootstrap);
|
|
13795
14399
|
const runPromise = script.run(context, { timeout: timeoutMs, promise: true }).then((v) => String(v ?? ""));
|
|
13796
|
-
const timeoutPromise = new Promise((
|
|
14400
|
+
const timeoutPromise = new Promise((resolve2) => {
|
|
13797
14401
|
const t = setTimeout(() => {
|
|
13798
14402
|
dispose();
|
|
13799
|
-
|
|
14403
|
+
resolve2(`Dynamic tool "${this.name}" timed out after ${timeoutMs}ms and was terminated.`);
|
|
13800
14404
|
}, timeoutMs + 500);
|
|
13801
14405
|
t.unref?.();
|
|
13802
14406
|
});
|
|
@@ -13814,7 +14418,7 @@ var DynamicTool = class extends BaseTool {
|
|
|
13814
14418
|
/** Spawn the worker, service its callTool/fetch bridge, enforce the kill timeout. */
|
|
13815
14419
|
runInWorker(input, callTool) {
|
|
13816
14420
|
const timeoutMs = Math.max(200, Number(process.env["CASCADE_DYNAMIC_TOOL_TIMEOUT_MS"]) || DYNAMIC_TOOL_TIMEOUT_MS);
|
|
13817
|
-
return new Promise((
|
|
14421
|
+
return new Promise((resolve2) => {
|
|
13818
14422
|
let settled = false;
|
|
13819
14423
|
const worker = new Worker(HARNESS_SRC, {
|
|
13820
14424
|
eval: true,
|
|
@@ -13826,7 +14430,7 @@ var DynamicTool = class extends BaseTool {
|
|
|
13826
14430
|
settled = true;
|
|
13827
14431
|
clearTimeout(timer);
|
|
13828
14432
|
void worker.terminate();
|
|
13829
|
-
|
|
14433
|
+
resolve2(value);
|
|
13830
14434
|
};
|
|
13831
14435
|
const timer = setTimeout(
|
|
13832
14436
|
() => finish(`Dynamic tool "${this.name}" timed out after ${timeoutMs}ms and was terminated.`),
|
|
@@ -15333,6 +15937,7 @@ ${prompt}`;
|
|
|
15333
15937
|
var Cascade = class _Cascade extends EventEmitter {
|
|
15334
15938
|
router;
|
|
15335
15939
|
toolRegistry;
|
|
15940
|
+
multimodal;
|
|
15336
15941
|
mcpClient;
|
|
15337
15942
|
codeIndex;
|
|
15338
15943
|
config;
|
|
@@ -15371,6 +15976,10 @@ var Cascade = class _Cascade extends EventEmitter {
|
|
|
15371
15976
|
}
|
|
15372
15977
|
});
|
|
15373
15978
|
this.toolRegistry = new ToolRegistry(this.config.tools, workspacePath);
|
|
15979
|
+
this.multimodal = new MultimodalRegistry(
|
|
15980
|
+
(this.config.providers ?? []).map((p) => p.type)
|
|
15981
|
+
);
|
|
15982
|
+
this.registerMediaTools(workspacePath);
|
|
15374
15983
|
this.telemetry = config.telemetry?.enabled ? new Telemetry(config.telemetry, config.telemetry.distinctId ?? "anonymous") : noopTelemetry;
|
|
15375
15984
|
this.worldStateDB = new WorldStateDB(this.workspacePath, this.config.workspace?.debugWorldState ?? false);
|
|
15376
15985
|
this.router.setWorldStateDB(this.worldStateDB);
|
|
@@ -15424,14 +16033,14 @@ var Cascade = class _Cascade extends EventEmitter {
|
|
|
15424
16033
|
if (this.listenerCount("mcp:approval-required") === 0) {
|
|
15425
16034
|
return false;
|
|
15426
16035
|
}
|
|
15427
|
-
return await new Promise((
|
|
15428
|
-
this.pendingMcpApprovals.set(server.name,
|
|
16036
|
+
return await new Promise((resolve2) => {
|
|
16037
|
+
this.pendingMcpApprovals.set(server.name, resolve2);
|
|
15429
16038
|
const timeout = setTimeout(() => {
|
|
15430
|
-
if (this.pendingMcpApprovals.delete(server.name))
|
|
16039
|
+
if (this.pendingMcpApprovals.delete(server.name)) resolve2(false);
|
|
15431
16040
|
}, 3e4);
|
|
15432
16041
|
const wrap = (approved) => {
|
|
15433
16042
|
clearTimeout(timeout);
|
|
15434
|
-
|
|
16043
|
+
resolve2(approved);
|
|
15435
16044
|
};
|
|
15436
16045
|
this.pendingMcpApprovals.set(server.name, wrap);
|
|
15437
16046
|
this.emit("mcp:approval-required", { server });
|
|
@@ -15505,17 +16114,17 @@ var Cascade = class _Cascade extends EventEmitter {
|
|
|
15505
16114
|
}
|
|
15506
16115
|
const t2Count = plan.sections.length;
|
|
15507
16116
|
const t3Count = plan.sections.reduce((sum, s) => sum + (s.t3Subtasks?.length ?? 0), 0);
|
|
15508
|
-
return await new Promise((
|
|
16117
|
+
return await new Promise((resolve2) => {
|
|
15509
16118
|
const timeout = setTimeout(() => {
|
|
15510
16119
|
if (this.pendingPlanApproval) {
|
|
15511
16120
|
this.pendingPlanApproval = void 0;
|
|
15512
|
-
|
|
16121
|
+
resolve2({ approved: true });
|
|
15513
16122
|
}
|
|
15514
16123
|
}, 12e4);
|
|
15515
16124
|
this.pendingPlanApproval = (decision) => {
|
|
15516
16125
|
clearTimeout(timeout);
|
|
15517
16126
|
this.pendingPlanApproval = void 0;
|
|
15518
|
-
|
|
16127
|
+
resolve2(decision);
|
|
15519
16128
|
};
|
|
15520
16129
|
this.emit("plan:approval-required", {
|
|
15521
16130
|
taskId,
|
|
@@ -15548,17 +16157,17 @@ var Cascade = class _Cascade extends EventEmitter {
|
|
|
15548
16157
|
}
|
|
15549
16158
|
async requestContextApproval(info) {
|
|
15550
16159
|
if (this.listenerCount("context:approval-required") === 0) return true;
|
|
15551
|
-
return await new Promise((
|
|
16160
|
+
return await new Promise((resolve2) => {
|
|
15552
16161
|
const timeout = setTimeout(() => {
|
|
15553
16162
|
if (this.pendingContextApproval) {
|
|
15554
16163
|
this.pendingContextApproval = void 0;
|
|
15555
|
-
|
|
16164
|
+
resolve2(true);
|
|
15556
16165
|
}
|
|
15557
16166
|
}, 12e4);
|
|
15558
16167
|
this.pendingContextApproval = (approved) => {
|
|
15559
16168
|
clearTimeout(timeout);
|
|
15560
16169
|
this.pendingContextApproval = void 0;
|
|
15561
|
-
|
|
16170
|
+
resolve2(approved);
|
|
15562
16171
|
};
|
|
15563
16172
|
this.emit("context:approval-required", info);
|
|
15564
16173
|
});
|
|
@@ -15638,6 +16247,55 @@ ${input}` }],
|
|
|
15638
16247
|
if (this.store) t1.setStore(this.store);
|
|
15639
16248
|
return t1.previewPlan(prompt);
|
|
15640
16249
|
}
|
|
16250
|
+
/**
|
|
16251
|
+
* Register image / speech / transcription tools for the modalities this
|
|
16252
|
+
* config can actually serve.
|
|
16253
|
+
*
|
|
16254
|
+
* The sink writes bytes into the workspace, because that is what a desktop or
|
|
16255
|
+
* CLI run wants and it keeps a generated asset indistinguishable from any
|
|
16256
|
+
* other file the run produced. A host with different storage (the cloud
|
|
16257
|
+
* server stores a file row) constructs the tools itself via buildMediaTools
|
|
16258
|
+
* with its own sink.
|
|
16259
|
+
*/
|
|
16260
|
+
registerMediaTools(workspacePath) {
|
|
16261
|
+
const allowlist = this.config.tools?.enabledTools;
|
|
16262
|
+
const permitted = (name) => !allowlist || allowlist.includes(name);
|
|
16263
|
+
const tools = buildMediaTools(
|
|
16264
|
+
{
|
|
16265
|
+
registry: this.multimodal,
|
|
16266
|
+
lookupProvider: (provider) => (this.config.providers ?? []).find((p) => p.type === provider),
|
|
16267
|
+
sink: async (asset) => {
|
|
16268
|
+
const fsp = await import('fs/promises');
|
|
16269
|
+
const nodePath = await import('path');
|
|
16270
|
+
const dir = nodePath.join(workspacePath, "generated");
|
|
16271
|
+
await fsp.mkdir(dir, { recursive: true });
|
|
16272
|
+
const dest = nodePath.join(dir, asset.filename);
|
|
16273
|
+
await fsp.writeFile(dest, asset.data);
|
|
16274
|
+
return nodePath.relative(workspacePath, dest);
|
|
16275
|
+
}
|
|
16276
|
+
},
|
|
16277
|
+
async (p) => {
|
|
16278
|
+
const fsp = await import('fs/promises');
|
|
16279
|
+
return fsp.readFile(p);
|
|
16280
|
+
}
|
|
16281
|
+
);
|
|
16282
|
+
for (const tool of tools) {
|
|
16283
|
+
if (!permitted(tool.name)) continue;
|
|
16284
|
+
tool.setWorkspaceRoot(workspacePath);
|
|
16285
|
+
this.toolRegistry.register(tool);
|
|
16286
|
+
}
|
|
16287
|
+
}
|
|
16288
|
+
/**
|
|
16289
|
+
* Supply per-model thumbs-up/down counts so Auto routing can weigh how models
|
|
16290
|
+
* have actually performed for this user, on top of public benchmarks.
|
|
16291
|
+
*
|
|
16292
|
+
* Optional everywhere: with no source, or an empty record, scoring is
|
|
16293
|
+
* byte-identical to before. The adjustment is capped and sample-size shrunk —
|
|
16294
|
+
* see core/router/feedback-prior.ts.
|
|
16295
|
+
*/
|
|
16296
|
+
setFeedbackSource(source) {
|
|
16297
|
+
this.router.setFeedbackSource(source);
|
|
16298
|
+
}
|
|
15641
16299
|
/** True when a task stopped at the budget cap and can be resumed via /continue. */
|
|
15642
16300
|
hasResumableRun() {
|
|
15643
16301
|
return this.lastInterruptedRun != null;
|
|
@@ -16127,7 +16785,7 @@ ${prompt}` : prompt;
|
|
|
16127
16785
|
if (toolCreator) toolCreator.setPermissionEscalator(escalator);
|
|
16128
16786
|
const rootPrompt = buildContextualPrompt(options.prompt, options.conversationHistory);
|
|
16129
16787
|
let finalOutput = "";
|
|
16130
|
-
const runBreaker = new RunBreaker();
|
|
16788
|
+
const runBreaker = new RunBreaker(this.config.budget?.failureThreshold);
|
|
16131
16789
|
let t2Results = [];
|
|
16132
16790
|
let runError = null;
|
|
16133
16791
|
let identityPrompt = "";
|
|
@@ -16830,8 +17488,8 @@ var CloudClient = class _CloudClient {
|
|
|
16830
17488
|
}
|
|
16831
17489
|
};
|
|
16832
17490
|
function sleep(ms, signal) {
|
|
16833
|
-
return new Promise((
|
|
16834
|
-
const t = setTimeout(
|
|
17491
|
+
return new Promise((resolve2, reject) => {
|
|
17492
|
+
const t = setTimeout(resolve2, ms);
|
|
16835
17493
|
signal?.addEventListener("abort", () => {
|
|
16836
17494
|
clearTimeout(t);
|
|
16837
17495
|
reject(new Error("aborted"));
|
|
@@ -18623,8 +19281,8 @@ function Repl({ config, workspacePath, themeName, initialPrompt, identityName, a
|
|
|
18623
19281
|
onExit: () => exit(),
|
|
18624
19282
|
onThemeChange: (name) => setTheme(getTheme(name)),
|
|
18625
19283
|
onExport: async (fmt) => {
|
|
18626
|
-
const
|
|
18627
|
-
const exportPath = path32.join(workspacePath, `cascade-export-${
|
|
19284
|
+
const stamp2 = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
19285
|
+
const exportPath = path32.join(workspacePath, `cascade-export-${stamp2}.${fmt === "json" ? "json" : "md"}`);
|
|
18628
19286
|
if (fmt === "json") {
|
|
18629
19287
|
await fs10.writeFile(exportPath, JSON.stringify({ sessionId: sessionIdRef.current, messages: state.messages }, null, 2), "utf-8");
|
|
18630
19288
|
} else {
|
|
@@ -19056,8 +19714,8 @@ ${lastUser.content}`;
|
|
|
19056
19714
|
conversationHistory: toConversationHistory(state.messages),
|
|
19057
19715
|
approvalCallback: async (req) => {
|
|
19058
19716
|
dispatch({ type: "SET_APPROVAL", request: req });
|
|
19059
|
-
return new Promise((
|
|
19060
|
-
approvalResolverRef.current =
|
|
19717
|
+
return new Promise((resolve2) => {
|
|
19718
|
+
approvalResolverRef.current = resolve2;
|
|
19061
19719
|
});
|
|
19062
19720
|
}
|
|
19063
19721
|
});
|
|
@@ -20323,13 +20981,13 @@ function SetupWizard({ workspacePath, onComplete }) {
|
|
|
20323
20981
|
}
|
|
20324
20982
|
async function runSetupWizard(workspacePath) {
|
|
20325
20983
|
printTelemetryBanner();
|
|
20326
|
-
return new Promise((
|
|
20984
|
+
return new Promise((resolve2) => {
|
|
20327
20985
|
const { unmount } = render(
|
|
20328
20986
|
React2.createElement(SetupWizard, {
|
|
20329
20987
|
workspacePath,
|
|
20330
20988
|
onComplete: (config) => {
|
|
20331
20989
|
unmount();
|
|
20332
|
-
|
|
20990
|
+
resolve2(config);
|
|
20333
20991
|
}
|
|
20334
20992
|
}),
|
|
20335
20993
|
{ exitOnCtrlC: true }
|
|
@@ -20797,7 +21455,7 @@ function notSignedIn() {
|
|
|
20797
21455
|
process.exitCode = 1;
|
|
20798
21456
|
}
|
|
20799
21457
|
function promptHidden(question) {
|
|
20800
|
-
return new Promise((
|
|
21458
|
+
return new Promise((resolve2) => {
|
|
20801
21459
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout, terminal: true });
|
|
20802
21460
|
let masked = false;
|
|
20803
21461
|
rl._writeToOutput = (s) => {
|
|
@@ -20806,7 +21464,7 @@ function promptHidden(question) {
|
|
|
20806
21464
|
rl.question(question, (answer) => {
|
|
20807
21465
|
rl.close();
|
|
20808
21466
|
process.stdout.write("\n");
|
|
20809
|
-
|
|
21467
|
+
resolve2(answer.trim());
|
|
20810
21468
|
});
|
|
20811
21469
|
masked = true;
|
|
20812
21470
|
});
|
|
@@ -21733,12 +22391,12 @@ var DashboardServer = class {
|
|
|
21733
22391
|
console.warn("\u26A0 Dashboard auth is DISABLED while bound to a non-loopback interface \u2014 this allows unauthenticated remote task execution.");
|
|
21734
22392
|
}
|
|
21735
22393
|
}
|
|
21736
|
-
await new Promise((
|
|
22394
|
+
await new Promise((resolve2, reject) => {
|
|
21737
22395
|
const onError = (err) => reject(err);
|
|
21738
22396
|
this.httpServer.once("error", onError);
|
|
21739
22397
|
this.httpServer.listen(this.port, this.host, () => {
|
|
21740
22398
|
this.httpServer.off("error", onError);
|
|
21741
|
-
|
|
22399
|
+
resolve2();
|
|
21742
22400
|
});
|
|
21743
22401
|
});
|
|
21744
22402
|
try {
|
|
@@ -21762,8 +22420,8 @@ var DashboardServer = class {
|
|
|
21762
22420
|
} catch {
|
|
21763
22421
|
}
|
|
21764
22422
|
this.globalStore = null;
|
|
21765
|
-
await new Promise((
|
|
21766
|
-
this.httpServer.close((err) => err ? reject(err) :
|
|
22423
|
+
await new Promise((resolve2, reject) => {
|
|
22424
|
+
this.httpServer.close((err) => err ? reject(err) : resolve2());
|
|
21767
22425
|
});
|
|
21768
22426
|
}
|
|
21769
22427
|
getSocket() {
|
|
@@ -21960,8 +22618,8 @@ var DashboardServer = class {
|
|
|
21960
22618
|
* timeout denies it.
|
|
21961
22619
|
*/
|
|
21962
22620
|
makeApprovalCallback(sessionId) {
|
|
21963
|
-
return (request) => new Promise((
|
|
21964
|
-
this.pendingApprovals.set(request.id, { resolve, sessionId });
|
|
22621
|
+
return (request) => new Promise((resolve2) => {
|
|
22622
|
+
this.pendingApprovals.set(request.id, { resolve: resolve2, sessionId });
|
|
21965
22623
|
});
|
|
21966
22624
|
}
|
|
21967
22625
|
/**
|