cascade-ai 0.50.0 → 0.52.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.cjs +930 -133
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +930 -133
- package/dist/cli.js.map +1 -1
- package/dist/desktop-core.cjs +1137 -347
- package/dist/index.cjs +916 -124
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +102 -0
- package/dist/index.d.ts +102 -0
- package/dist/index.js +916 -124
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/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.52.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
|
|
@@ -4888,7 +4997,14 @@ var ToolsConfigSchema = z.object({
|
|
|
4888
4997
|
* built-in tool (shell/file/git/…) from existing at all, rather than just
|
|
4889
4998
|
* gating it behind approval. Omitted = full default set (unchanged).
|
|
4890
4999
|
*/
|
|
4891
|
-
enabledTools: z.array(z.string()).optional()
|
|
5000
|
+
enabledTools: z.array(z.string()).optional(),
|
|
5001
|
+
/**
|
|
5002
|
+
* Tools to remove even when they would otherwise be registered. Unlike
|
|
5003
|
+
* `enabledTools` (an allowlist over the built-in set), this also reaches
|
|
5004
|
+
* tools registered outside it — MCP tools and media generation — which is
|
|
5005
|
+
* the only way to turn those off.
|
|
5006
|
+
*/
|
|
5007
|
+
disabledTools: z.array(z.string()).optional()
|
|
4892
5008
|
});
|
|
4893
5009
|
var HookDefinitionSchema = z.object({
|
|
4894
5010
|
name: z.string().optional(),
|
|
@@ -4956,6 +5072,14 @@ var BudgetConfigSchema = z.object({
|
|
|
4956
5072
|
maxTokensPerRun: z.number().int().positive().default(2e5),
|
|
4957
5073
|
/** Optional hard per-task cost ceiling (USD). Unset = only the token cap applies. */
|
|
4958
5074
|
maxCostPerRunUsd: z.number().positive().optional(),
|
|
5075
|
+
/**
|
|
5076
|
+
* Consecutive systemic failures against ONE model before the run stops
|
|
5077
|
+
* launching work that would fail the same way (dead key, wrong model id,
|
|
5078
|
+
* exhausted quota). Lower reacts sooner and wastes less; higher absorbs a
|
|
5079
|
+
* longer transient before giving up. Only failures that will certainly repeat
|
|
5080
|
+
* are counted — see core/run-breaker.ts.
|
|
5081
|
+
*/
|
|
5082
|
+
failureThreshold: z.number().int().min(1).max(20).default(3),
|
|
4959
5083
|
warnAtPct: z.number().default(80)
|
|
4960
5084
|
});
|
|
4961
5085
|
var WorkspaceConfigSchema = z.object({
|
|
@@ -6008,7 +6132,7 @@ var TpmLimiter = class {
|
|
|
6008
6132
|
}
|
|
6009
6133
|
const deficit = want - bucket.available;
|
|
6010
6134
|
const waitMs = Math.max(50, Math.ceil(deficit / bucket.tokensPerMinute * 6e4));
|
|
6011
|
-
await new Promise((
|
|
6135
|
+
await new Promise((resolve2) => setTimeout(resolve2, waitMs));
|
|
6012
6136
|
}
|
|
6013
6137
|
}
|
|
6014
6138
|
/**
|
|
@@ -6071,14 +6195,14 @@ var LocalRequestQueue = class {
|
|
|
6071
6195
|
this.active++;
|
|
6072
6196
|
return this.makeRelease();
|
|
6073
6197
|
}
|
|
6074
|
-
return new Promise((
|
|
6198
|
+
return new Promise((resolve2, reject) => {
|
|
6075
6199
|
let settled = false;
|
|
6076
6200
|
let timer;
|
|
6077
6201
|
const resolver = (release) => {
|
|
6078
6202
|
if (settled) return;
|
|
6079
6203
|
settled = true;
|
|
6080
6204
|
if (timer !== void 0) clearTimeout(timer);
|
|
6081
|
-
|
|
6205
|
+
resolve2(release);
|
|
6082
6206
|
};
|
|
6083
6207
|
if (timeoutMs !== void 0 && timeoutMs > 0) {
|
|
6084
6208
|
timer = setTimeout(() => {
|
|
@@ -6705,6 +6829,7 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter {
|
|
|
6705
6829
|
tpmLimiter;
|
|
6706
6830
|
localQueue;
|
|
6707
6831
|
taskAnalyzer;
|
|
6832
|
+
pendingFeedbackSource;
|
|
6708
6833
|
worldStateDB;
|
|
6709
6834
|
privacyPaths;
|
|
6710
6835
|
guidanceQueue;
|
|
@@ -7159,8 +7284,18 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter {
|
|
|
7159
7284
|
return windows.length ? Math.min(...windows) : void 0;
|
|
7160
7285
|
}
|
|
7161
7286
|
/** Wire the Cascade Auto task analyzer used for per-subtask model routing. */
|
|
7287
|
+
/**
|
|
7288
|
+
* Feed per-model thumbs counts into Auto routing. Forwarded to the analyzer
|
|
7289
|
+
* because that is where the public benchmark score is combined; the prior
|
|
7290
|
+
* adjusts that number rather than sitting anywhere else in the pipeline.
|
|
7291
|
+
*/
|
|
7292
|
+
setFeedbackSource(source) {
|
|
7293
|
+
this.taskAnalyzer?.setFeedbackSource(source);
|
|
7294
|
+
this.pendingFeedbackSource = source;
|
|
7295
|
+
}
|
|
7162
7296
|
setTaskAnalyzer(analyzer) {
|
|
7163
7297
|
this.taskAnalyzer = analyzer;
|
|
7298
|
+
if (this.pendingFeedbackSource) analyzer.setFeedbackSource(this.pendingFeedbackSource);
|
|
7164
7299
|
}
|
|
7165
7300
|
setWorldStateDB(db) {
|
|
7166
7301
|
this.worldStateDB = db;
|
|
@@ -8255,8 +8390,11 @@ Now execute your subtask using this context where relevant.`
|
|
|
8255
8390
|
const result = await this.runAgentLoop(systemPrompt, this.tools);
|
|
8256
8391
|
output = result.output;
|
|
8257
8392
|
toolCalls = result.toolCalls;
|
|
8258
|
-
this.
|
|
8259
|
-
|
|
8393
|
+
const mustProduceArtifact = this.requiresArtifact();
|
|
8394
|
+
if (mustProduceArtifact) {
|
|
8395
|
+
this.sendStatusUpdate({ progressPct: 65, currentAction: "Verifying required artifacts", status: "IN_PROGRESS" });
|
|
8396
|
+
}
|
|
8397
|
+
const artifactCheck = mustProduceArtifact ? await this.verifyArtifacts(assignment) : { ok: true, issues: [] };
|
|
8260
8398
|
if (!artifactCheck.ok) {
|
|
8261
8399
|
correctionAttempts = 1;
|
|
8262
8400
|
issues.push(...artifactCheck.issues);
|
|
@@ -8536,11 +8674,11 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : "") + textToolSuffix,
|
|
|
8536
8674
|
if (!wasApproved) return `Tool ${tc.name} was denied by user.`;
|
|
8537
8675
|
} else {
|
|
8538
8676
|
const LEGACY_APPROVAL_TIMEOUT_MS = 6e5;
|
|
8539
|
-
const legacyDecision = await new Promise((
|
|
8677
|
+
const legacyDecision = await new Promise((resolve2) => {
|
|
8540
8678
|
const eventName = `tool:approval-response:${this.id}-${tc.id}`;
|
|
8541
8679
|
const timer = setTimeout(() => {
|
|
8542
8680
|
this.removeAllListeners(eventName);
|
|
8543
|
-
|
|
8681
|
+
resolve2({ approved: false });
|
|
8544
8682
|
}, LEGACY_APPROVAL_TIMEOUT_MS);
|
|
8545
8683
|
timer.unref?.();
|
|
8546
8684
|
this.emit("tool:approval-request", {
|
|
@@ -8553,7 +8691,7 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : "") + textToolSuffix,
|
|
|
8553
8691
|
});
|
|
8554
8692
|
this.once(eventName, (d) => {
|
|
8555
8693
|
clearTimeout(timer);
|
|
8556
|
-
|
|
8694
|
+
resolve2(d);
|
|
8557
8695
|
});
|
|
8558
8696
|
});
|
|
8559
8697
|
if (legacyDecision.always) this.sessionApprovals.set(tc.name, legacyDecision.approved);
|
|
@@ -8573,6 +8711,9 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : "") + textToolSuffix,
|
|
|
8573
8711
|
tierId: this.id,
|
|
8574
8712
|
sessionId: this.taskId,
|
|
8575
8713
|
requireApproval: false,
|
|
8714
|
+
// Media generation can run for a minute; without this a cancelled run
|
|
8715
|
+
// still pays for an image nobody will see.
|
|
8716
|
+
...this.signal ? { signal: this.signal } : {},
|
|
8576
8717
|
saveSnapshot: async (path37, content) => {
|
|
8577
8718
|
this.store?.addFileSnapshot(this.taskId, path37, content);
|
|
8578
8719
|
},
|
|
@@ -8620,7 +8761,8 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : "") + textToolSuffix,
|
|
|
8620
8761
|
const result = await this.toolRegistry.execute(altTool, tc.input, {
|
|
8621
8762
|
tierId: this.id,
|
|
8622
8763
|
sessionId: this.taskId,
|
|
8623
|
-
requireApproval: false
|
|
8764
|
+
requireApproval: false,
|
|
8765
|
+
...this.signal ? { signal: this.signal } : {}
|
|
8624
8766
|
});
|
|
8625
8767
|
const str2 = typeof result === "string" ? result : JSON.stringify(result);
|
|
8626
8768
|
if (!str2.startsWith("Tool error:") && !str2.startsWith("Error:")) {
|
|
@@ -8642,7 +8784,8 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : "") + textToolSuffix,
|
|
|
8642
8784
|
const result = await this.toolRegistry.execute(newToolName, tc.input, {
|
|
8643
8785
|
tierId: this.id,
|
|
8644
8786
|
sessionId: this.taskId,
|
|
8645
|
-
requireApproval: false
|
|
8787
|
+
requireApproval: false,
|
|
8788
|
+
...this.signal ? { signal: this.signal } : {}
|
|
8646
8789
|
});
|
|
8647
8790
|
const str2 = typeof result === "string" ? result : JSON.stringify(result);
|
|
8648
8791
|
if (!str2.startsWith("Tool error:")) return `[Synthesized ${newToolName}]: ${str2}`;
|
|
@@ -9060,7 +9203,7 @@ var PeerBus = class extends EventEmitter {
|
|
|
9060
9203
|
this.outputs.set(subtaskId, entry);
|
|
9061
9204
|
this.emit("output:ready", entry);
|
|
9062
9205
|
const waiting = this.waiters.get(subtaskId) ?? [];
|
|
9063
|
-
for (const
|
|
9206
|
+
for (const resolve2 of waiting) resolve2(entry);
|
|
9064
9207
|
this.waiters.delete(subtaskId);
|
|
9065
9208
|
}
|
|
9066
9209
|
/**
|
|
@@ -9091,10 +9234,10 @@ var PeerBus = class extends EventEmitter {
|
|
|
9091
9234
|
waitFor(subtaskId, timeoutMs = 12e4) {
|
|
9092
9235
|
const existing = this.outputs.get(subtaskId);
|
|
9093
9236
|
if (existing && !this.retryPending.has(subtaskId)) return Promise.resolve(existing);
|
|
9094
|
-
return new Promise((
|
|
9237
|
+
return new Promise((resolve2, reject) => {
|
|
9095
9238
|
const resolver = (output) => {
|
|
9096
9239
|
clearTimeout(timer);
|
|
9097
|
-
|
|
9240
|
+
resolve2(output);
|
|
9098
9241
|
};
|
|
9099
9242
|
const timer = setTimeout(() => {
|
|
9100
9243
|
const waiting = this.waiters.get(subtaskId);
|
|
@@ -9148,10 +9291,10 @@ var PeerBus = class extends EventEmitter {
|
|
|
9148
9291
|
*/
|
|
9149
9292
|
collect(timeoutMs) {
|
|
9150
9293
|
const collected = [...this.broadcastLog];
|
|
9151
|
-
return new Promise((
|
|
9294
|
+
return new Promise((resolve2) => {
|
|
9152
9295
|
const timer = setTimeout(() => {
|
|
9153
9296
|
off();
|
|
9154
|
-
|
|
9297
|
+
resolve2(collected);
|
|
9155
9298
|
}, timeoutMs);
|
|
9156
9299
|
const handler = (msg) => {
|
|
9157
9300
|
collected.push({ fromId: msg.fromId, payload: msg.payload, timestamp: msg.timestamp });
|
|
@@ -9165,7 +9308,7 @@ var PeerBus = class extends EventEmitter {
|
|
|
9165
9308
|
const broadcasters = new Set(collected.map((m) => m.fromId));
|
|
9166
9309
|
if (broadcasters.size >= this.members.size) {
|
|
9167
9310
|
off();
|
|
9168
|
-
|
|
9311
|
+
resolve2(collected);
|
|
9169
9312
|
}
|
|
9170
9313
|
};
|
|
9171
9314
|
this.on("broadcast", checkComplete);
|
|
@@ -9183,7 +9326,7 @@ var PeerBus = class extends EventEmitter {
|
|
|
9183
9326
|
return;
|
|
9184
9327
|
}
|
|
9185
9328
|
this.emitCoordination(tierId, `\u23F3 waiting for ${filePath} (held by ${existing.holderId})`);
|
|
9186
|
-
return new Promise((
|
|
9329
|
+
return new Promise((resolve2, reject) => {
|
|
9187
9330
|
const timer = setTimeout(() => {
|
|
9188
9331
|
reject(new Error(`File lock timeout for ${filePath} (held by ${existing.holderId})`));
|
|
9189
9332
|
}, timeoutMs);
|
|
@@ -9191,7 +9334,7 @@ var PeerBus = class extends EventEmitter {
|
|
|
9191
9334
|
clearTimeout(timer);
|
|
9192
9335
|
this.fileLocks.set(filePath, { holderId: tierId, lockedAt: (/* @__PURE__ */ new Date()).toISOString(), waiters: [] });
|
|
9193
9336
|
this.emitCoordination(tierId, `\u{1F512} locked ${filePath}`);
|
|
9194
|
-
|
|
9337
|
+
resolve2();
|
|
9195
9338
|
});
|
|
9196
9339
|
});
|
|
9197
9340
|
}
|
|
@@ -9216,11 +9359,11 @@ var PeerBus = class extends EventEmitter {
|
|
|
9216
9359
|
*/
|
|
9217
9360
|
waitForFileRelease(filePath, timeoutMs = 3e4) {
|
|
9218
9361
|
if (!this.fileLocks.has(filePath)) return Promise.resolve();
|
|
9219
|
-
return new Promise((
|
|
9362
|
+
return new Promise((resolve2, reject) => {
|
|
9220
9363
|
const timer = setTimeout(() => reject(new Error(`Timeout waiting for file release: ${filePath}`)), timeoutMs);
|
|
9221
9364
|
this.once(`file:released:${filePath}`, () => {
|
|
9222
9365
|
clearTimeout(timer);
|
|
9223
|
-
|
|
9366
|
+
resolve2();
|
|
9224
9367
|
});
|
|
9225
9368
|
});
|
|
9226
9369
|
}
|
|
@@ -9286,13 +9429,13 @@ var PeerBus = class extends EventEmitter {
|
|
|
9286
9429
|
this.emit(`barrier:${barrierName}`);
|
|
9287
9430
|
return;
|
|
9288
9431
|
}
|
|
9289
|
-
return new Promise((
|
|
9432
|
+
return new Promise((resolve2, reject) => {
|
|
9290
9433
|
const timer = setTimeout(() => {
|
|
9291
9434
|
reject(new Error(`Barrier timeout: ${barrierName} (${bar.arrived.size}/${bar.total} arrived)`));
|
|
9292
9435
|
}, 12e4);
|
|
9293
9436
|
this.once(`barrier:${barrierName}`, () => {
|
|
9294
9437
|
clearTimeout(timer);
|
|
9295
|
-
|
|
9438
|
+
resolve2();
|
|
9296
9439
|
});
|
|
9297
9440
|
});
|
|
9298
9441
|
}
|
|
@@ -10843,6 +10986,171 @@ Reply with exactly one word: YES, NO, or UNSURE.
|
|
|
10843
10986
|
// src/core/cascade.ts
|
|
10844
10987
|
init_cost();
|
|
10845
10988
|
|
|
10989
|
+
// src/core/multimodal/registry.ts
|
|
10990
|
+
init_pricing();
|
|
10991
|
+
var CAPABILITIES = [
|
|
10992
|
+
// ── Image ──
|
|
10993
|
+
{
|
|
10994
|
+
modality: "image",
|
|
10995
|
+
provider: "openai",
|
|
10996
|
+
modelId: "dall-e-3",
|
|
10997
|
+
api: "openai-images"
|
|
10998
|
+
},
|
|
10999
|
+
{
|
|
11000
|
+
modality: "image",
|
|
11001
|
+
provider: "gemini",
|
|
11002
|
+
modelId: "gemini-3-pro-image",
|
|
11003
|
+
api: "gemini-predict"
|
|
11004
|
+
},
|
|
11005
|
+
// ── Speech (text → audio) ──
|
|
11006
|
+
{
|
|
11007
|
+
modality: "speech",
|
|
11008
|
+
provider: "openai",
|
|
11009
|
+
modelId: "tts-1",
|
|
11010
|
+
api: "openai-speech"
|
|
11011
|
+
},
|
|
11012
|
+
{
|
|
11013
|
+
// The only quality claim in this table that needs no leaderboard: OpenAI
|
|
11014
|
+
// ships these as the same voice at two fidelities and charges double for
|
|
11015
|
+
// this one. That is the vendor's own ordering, not an inference.
|
|
11016
|
+
modality: "speech",
|
|
11017
|
+
provider: "openai",
|
|
11018
|
+
modelId: "tts-1-hd",
|
|
11019
|
+
api: "openai-speech",
|
|
11020
|
+
quality: 70,
|
|
11021
|
+
qualityBasis: "OpenAI's own hd variant of tts-1, priced 2x"
|
|
11022
|
+
},
|
|
11023
|
+
// ── Transcription (audio → text) ──
|
|
11024
|
+
{
|
|
11025
|
+
modality: "transcription",
|
|
11026
|
+
provider: "openai",
|
|
11027
|
+
modelId: "whisper-1",
|
|
11028
|
+
api: "openai-transcriptions"
|
|
11029
|
+
},
|
|
11030
|
+
// ── Video ──
|
|
11031
|
+
{
|
|
11032
|
+
// Long-running: submit returns an operation name, the video lands minutes
|
|
11033
|
+
// later, and the bytes come from a short-lived signed URL. generate.ts owns
|
|
11034
|
+
// that polling loop; the registry only records the shape via `api`.
|
|
11035
|
+
modality: "video",
|
|
11036
|
+
provider: "gemini",
|
|
11037
|
+
modelId: "veo-3.1-generate-001",
|
|
11038
|
+
api: "gemini-predict-lro"
|
|
11039
|
+
}
|
|
11040
|
+
];
|
|
11041
|
+
function capabilityCost(cap) {
|
|
11042
|
+
const match = findPricing(cap.modelId, cap.provider, {
|
|
11043
|
+
fallbackProviders: cap.provider === "gemini" ? ["vertex"] : []
|
|
11044
|
+
});
|
|
11045
|
+
const rate = match?.entry.rates?.[0];
|
|
11046
|
+
if (!rate) return null;
|
|
11047
|
+
const unitLabel = {
|
|
11048
|
+
per_image: "per image",
|
|
11049
|
+
per_second: "per second of video",
|
|
11050
|
+
per_character: "per character",
|
|
11051
|
+
per_audio_minute: "per audio minute"
|
|
11052
|
+
};
|
|
11053
|
+
const unit = unitLabel[rate.unit] ?? rate.unit;
|
|
11054
|
+
const amount = rate.amount;
|
|
11055
|
+
const shown = amount < 1e-3 ? amount.toExponential(2) : amount.toFixed(4);
|
|
11056
|
+
return { label: `$${shown} ${unit}`, amount, unit };
|
|
11057
|
+
}
|
|
11058
|
+
var MultimodalRegistry = class {
|
|
11059
|
+
/** Provider types the user has actually configured a key for. */
|
|
11060
|
+
available;
|
|
11061
|
+
constructor(availableProviders) {
|
|
11062
|
+
this.available = new Set(availableProviders);
|
|
11063
|
+
}
|
|
11064
|
+
/** Every capability whose provider is configured AND which Cascade can call. */
|
|
11065
|
+
usable(modality) {
|
|
11066
|
+
return CAPABILITIES.filter(
|
|
11067
|
+
(c) => !c.unsupported && this.available.has(c.provider) && (!modality || c.modality === modality)
|
|
11068
|
+
);
|
|
11069
|
+
}
|
|
11070
|
+
/** Modalities this account can actually generate right now. */
|
|
11071
|
+
availableModalities() {
|
|
11072
|
+
return [...new Set(this.usable().map((c) => c.modality))];
|
|
11073
|
+
}
|
|
11074
|
+
/**
|
|
11075
|
+
* Pick a model for a modality.
|
|
11076
|
+
*
|
|
11077
|
+
* Ranked models beat unranked ones, then higher quality, then lower unit
|
|
11078
|
+
* price. An explicit `preferModel` wins outright when it is usable — the user
|
|
11079
|
+
* naming a model is a stronger signal than any prior.
|
|
11080
|
+
*/
|
|
11081
|
+
select(modality, opts = {}) {
|
|
11082
|
+
const pool = this.usable(modality);
|
|
11083
|
+
if (!pool.length) return null;
|
|
11084
|
+
if (opts.preferModel) {
|
|
11085
|
+
const wanted = pool.find((c) => c.modelId === opts.preferModel);
|
|
11086
|
+
if (wanted) {
|
|
11087
|
+
return { capability: wanted, cost: capabilityCost(wanted), reason: `Requested explicitly: ${wanted.modelId}.` };
|
|
11088
|
+
}
|
|
11089
|
+
}
|
|
11090
|
+
const scored = pool.map((c) => ({ c, cost: capabilityCost(c) })).sort((a, b) => {
|
|
11091
|
+
const qa = a.c.quality ?? -1;
|
|
11092
|
+
const qb = b.c.quality ?? -1;
|
|
11093
|
+
if (qa !== qb) return qb - qa;
|
|
11094
|
+
const pa = a.cost?.amount ?? Number.POSITIVE_INFINITY;
|
|
11095
|
+
const pb = b.cost?.amount ?? Number.POSITIVE_INFINITY;
|
|
11096
|
+
return pa - pb;
|
|
11097
|
+
});
|
|
11098
|
+
const best = scored[0];
|
|
11099
|
+
const ranked = best.c.quality !== void 0;
|
|
11100
|
+
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.";
|
|
11101
|
+
return { capability: best.c, cost: best.cost, reason };
|
|
11102
|
+
}
|
|
11103
|
+
/**
|
|
11104
|
+
* A plain-language account of what this account can and cannot generate —
|
|
11105
|
+
* including the things it cannot, and why. The planner sees this, so being
|
|
11106
|
+
* candid here is what stops it inventing a `generate_music` call.
|
|
11107
|
+
*/
|
|
11108
|
+
describe() {
|
|
11109
|
+
const lines = [];
|
|
11110
|
+
const usable = this.usable();
|
|
11111
|
+
if (usable.length) {
|
|
11112
|
+
lines.push("Generation models available on your configured providers:");
|
|
11113
|
+
for (const modality of ["image", "speech", "transcription", "video"]) {
|
|
11114
|
+
const forModality = usable.filter((c) => c.modality === modality);
|
|
11115
|
+
if (!forModality.length) continue;
|
|
11116
|
+
const shown = forModality.map((c) => {
|
|
11117
|
+
const cost = capabilityCost(c);
|
|
11118
|
+
return `${c.modelId}${cost ? ` (${cost.label})` : ""}`;
|
|
11119
|
+
});
|
|
11120
|
+
lines.push(` ${modality}: ${shown.join(", ")}`);
|
|
11121
|
+
}
|
|
11122
|
+
} else {
|
|
11123
|
+
lines.push("No generation models are available \u2014 none of your configured providers offers one.");
|
|
11124
|
+
}
|
|
11125
|
+
const blocked = CAPABILITIES.filter((c) => c.unsupported && this.available.has(c.provider));
|
|
11126
|
+
for (const c of blocked) {
|
|
11127
|
+
lines.push(` ${c.modality}: ${c.modelId} exists on your account but is not callable. ${c.unsupported}`);
|
|
11128
|
+
}
|
|
11129
|
+
lines.push(
|
|
11130
|
+
" music: no supported provider exposes a music-generation API, so Cascade cannot generate music."
|
|
11131
|
+
);
|
|
11132
|
+
return lines.join("\n");
|
|
11133
|
+
}
|
|
11134
|
+
};
|
|
11135
|
+
|
|
11136
|
+
// src/tools/base.ts
|
|
11137
|
+
var BaseTool = class {
|
|
11138
|
+
workspaceRoot = process.cwd();
|
|
11139
|
+
setWorkspaceRoot(root) {
|
|
11140
|
+
this.workspaceRoot = root;
|
|
11141
|
+
}
|
|
11142
|
+
isDangerous() {
|
|
11143
|
+
return false;
|
|
11144
|
+
}
|
|
11145
|
+
getDefinition() {
|
|
11146
|
+
return {
|
|
11147
|
+
name: this.name,
|
|
11148
|
+
description: this.description,
|
|
11149
|
+
inputSchema: this.inputSchema
|
|
11150
|
+
};
|
|
11151
|
+
}
|
|
11152
|
+
};
|
|
11153
|
+
|
|
10846
11154
|
// src/core/router/provider-errors.ts
|
|
10847
11155
|
var SYSTEMIC = /* @__PURE__ */ new Set([
|
|
10848
11156
|
"rate_limit",
|
|
@@ -10907,7 +11215,7 @@ function classifyProviderError(err) {
|
|
|
10907
11215
|
return { kind, systemic: isSystemicKind(kind), status, raw };
|
|
10908
11216
|
}
|
|
10909
11217
|
function describeProviderError(c, modelId) {
|
|
10910
|
-
const on = ` on ${modelId}` ;
|
|
11218
|
+
const on = modelId ? ` on ${modelId}` : "";
|
|
10911
11219
|
const detail = c.raw ? ` Provider said: ${c.raw}` : "";
|
|
10912
11220
|
switch (c.kind) {
|
|
10913
11221
|
case "rate_limit":
|
|
@@ -10929,6 +11237,417 @@ function describeProviderError(c, modelId) {
|
|
|
10929
11237
|
}
|
|
10930
11238
|
}
|
|
10931
11239
|
|
|
11240
|
+
// src/core/multimodal/generate.ts
|
|
11241
|
+
var DEFAULT_TIMEOUT_MS = 12e4;
|
|
11242
|
+
async function callProvider(fn, modelId) {
|
|
11243
|
+
try {
|
|
11244
|
+
return await fn();
|
|
11245
|
+
} catch (err) {
|
|
11246
|
+
const classified = classifyProviderError(err);
|
|
11247
|
+
throw new Error(describeProviderError(classified, modelId));
|
|
11248
|
+
}
|
|
11249
|
+
}
|
|
11250
|
+
async function postJson(url, body, headers, signal) {
|
|
11251
|
+
const ac = new AbortController();
|
|
11252
|
+
const timer = setTimeout(() => ac.abort(), DEFAULT_TIMEOUT_MS);
|
|
11253
|
+
const onAbort = () => ac.abort();
|
|
11254
|
+
signal?.addEventListener("abort", onAbort);
|
|
11255
|
+
try {
|
|
11256
|
+
const res = await fetch(url, {
|
|
11257
|
+
method: "POST",
|
|
11258
|
+
headers: { "Content-Type": "application/json", ...headers },
|
|
11259
|
+
body: JSON.stringify(body),
|
|
11260
|
+
signal: ac.signal
|
|
11261
|
+
});
|
|
11262
|
+
if (!res.ok) {
|
|
11263
|
+
const text = await res.text().catch(() => "");
|
|
11264
|
+
throw Object.assign(new Error(text || res.statusText), { status: res.status });
|
|
11265
|
+
}
|
|
11266
|
+
return res;
|
|
11267
|
+
} finally {
|
|
11268
|
+
clearTimeout(timer);
|
|
11269
|
+
signal?.removeEventListener("abort", onAbort);
|
|
11270
|
+
}
|
|
11271
|
+
}
|
|
11272
|
+
function baseUrlFor(cfg, fallback) {
|
|
11273
|
+
return (cfg.baseUrl ?? fallback).replace(/\/+$/, "");
|
|
11274
|
+
}
|
|
11275
|
+
function stamp(ext) {
|
|
11276
|
+
return `cascade-${Date.now()}.${ext}`;
|
|
11277
|
+
}
|
|
11278
|
+
async function generateImage(cap, cfg, req, signal) {
|
|
11279
|
+
return callProvider(async () => {
|
|
11280
|
+
if (cap.api === "openai-images") {
|
|
11281
|
+
const url = `${baseUrlFor(cfg, "https://api.openai.com/v1")}/images/generations`;
|
|
11282
|
+
const res = await postJson(url, {
|
|
11283
|
+
model: cap.modelId,
|
|
11284
|
+
prompt: req.prompt,
|
|
11285
|
+
size: req.size ?? "1024x1024",
|
|
11286
|
+
n: 1,
|
|
11287
|
+
// b64 rather than a URL: see the file header — provider URLs expire.
|
|
11288
|
+
response_format: "b64_json"
|
|
11289
|
+
}, { Authorization: `Bearer ${cfg.apiKey ?? ""}` }, signal);
|
|
11290
|
+
const body = await res.json();
|
|
11291
|
+
const first = body.data?.[0];
|
|
11292
|
+
if (!first?.b64_json) throw new Error("Image response contained no image data.");
|
|
11293
|
+
return {
|
|
11294
|
+
data: Buffer.from(first.b64_json, "base64"),
|
|
11295
|
+
mimeType: "image/png",
|
|
11296
|
+
filename: stamp("png"),
|
|
11297
|
+
modelId: cap.modelId,
|
|
11298
|
+
provider: cap.provider,
|
|
11299
|
+
...first.revised_prompt ? { revisedPrompt: first.revised_prompt } : {}
|
|
11300
|
+
};
|
|
11301
|
+
}
|
|
11302
|
+
if (cap.api === "gemini-predict") {
|
|
11303
|
+
const base = baseUrlFor(cfg, "https://generativelanguage.googleapis.com/v1beta");
|
|
11304
|
+
const url = `${base}/models/${encodeURIComponent(cap.modelId)}:predict`;
|
|
11305
|
+
const res = await postJson(url, {
|
|
11306
|
+
instances: [{ prompt: req.prompt }],
|
|
11307
|
+
parameters: { sampleCount: 1 }
|
|
11308
|
+
}, { "x-goog-api-key": cfg.apiKey ?? "" }, signal);
|
|
11309
|
+
const body = await res.json();
|
|
11310
|
+
const first = body.predictions?.[0];
|
|
11311
|
+
if (!first?.bytesBase64Encoded) throw new Error("Image response contained no image data.");
|
|
11312
|
+
const mimeType = first.mimeType ?? "image/png";
|
|
11313
|
+
return {
|
|
11314
|
+
data: Buffer.from(first.bytesBase64Encoded, "base64"),
|
|
11315
|
+
mimeType,
|
|
11316
|
+
filename: stamp(mimeType.includes("jpeg") ? "jpg" : "png"),
|
|
11317
|
+
modelId: cap.modelId,
|
|
11318
|
+
provider: cap.provider
|
|
11319
|
+
};
|
|
11320
|
+
}
|
|
11321
|
+
throw new Error(`${cap.modelId} is not an image generator.`);
|
|
11322
|
+
}, cap.modelId);
|
|
11323
|
+
}
|
|
11324
|
+
async function generateSpeech(cap, cfg, req, signal) {
|
|
11325
|
+
return callProvider(async () => {
|
|
11326
|
+
if (cap.api !== "openai-speech") throw new Error(`${cap.modelId} is not a speech model.`);
|
|
11327
|
+
const format = req.format ?? "mp3";
|
|
11328
|
+
const url = `${baseUrlFor(cfg, "https://api.openai.com/v1")}/audio/speech`;
|
|
11329
|
+
const res = await postJson(url, {
|
|
11330
|
+
model: cap.modelId,
|
|
11331
|
+
input: req.text,
|
|
11332
|
+
voice: req.voice ?? "alloy",
|
|
11333
|
+
response_format: format
|
|
11334
|
+
}, { Authorization: `Bearer ${cfg.apiKey ?? ""}` }, signal);
|
|
11335
|
+
const mime = {
|
|
11336
|
+
mp3: "audio/mpeg",
|
|
11337
|
+
opus: "audio/opus",
|
|
11338
|
+
aac: "audio/aac",
|
|
11339
|
+
flac: "audio/flac",
|
|
11340
|
+
wav: "audio/wav"
|
|
11341
|
+
};
|
|
11342
|
+
return {
|
|
11343
|
+
data: Buffer.from(await res.arrayBuffer()),
|
|
11344
|
+
mimeType: mime[format] ?? "application/octet-stream",
|
|
11345
|
+
filename: stamp(format),
|
|
11346
|
+
modelId: cap.modelId,
|
|
11347
|
+
provider: cap.provider
|
|
11348
|
+
};
|
|
11349
|
+
}, cap.modelId);
|
|
11350
|
+
}
|
|
11351
|
+
var VIDEO_POLL_INTERVAL_MS = 1e4;
|
|
11352
|
+
var VIDEO_TIMEOUT_MS = 8 * 6e4;
|
|
11353
|
+
function sleep(ms, signal) {
|
|
11354
|
+
return new Promise((resolve2, reject) => {
|
|
11355
|
+
if (signal?.aborted) return reject(new Error("Cancelled."));
|
|
11356
|
+
const timer = setTimeout(() => {
|
|
11357
|
+
signal?.removeEventListener("abort", onAbort);
|
|
11358
|
+
resolve2();
|
|
11359
|
+
}, ms);
|
|
11360
|
+
function onAbort() {
|
|
11361
|
+
clearTimeout(timer);
|
|
11362
|
+
reject(new Error("Cancelled."));
|
|
11363
|
+
}
|
|
11364
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
11365
|
+
});
|
|
11366
|
+
}
|
|
11367
|
+
async function generateVideo(cap, cfg, req, signal, onProgress) {
|
|
11368
|
+
return callProvider(async () => {
|
|
11369
|
+
if (cap.api !== "gemini-predict-lro") throw new Error(`${cap.modelId} is not a video generator.`);
|
|
11370
|
+
const base = baseUrlFor(cfg, "https://generativelanguage.googleapis.com/v1beta");
|
|
11371
|
+
const key = cfg.apiKey ?? "";
|
|
11372
|
+
const headers = { "x-goog-api-key": key };
|
|
11373
|
+
const submit = await postJson(
|
|
11374
|
+
`${base}/models/${encodeURIComponent(cap.modelId)}:predictLongRunning`,
|
|
11375
|
+
{
|
|
11376
|
+
instances: [{ prompt: req.prompt }],
|
|
11377
|
+
parameters: {
|
|
11378
|
+
aspectRatio: req.aspectRatio ?? "16:9",
|
|
11379
|
+
...req.seconds ? { durationSeconds: req.seconds } : {}
|
|
11380
|
+
}
|
|
11381
|
+
},
|
|
11382
|
+
headers,
|
|
11383
|
+
signal
|
|
11384
|
+
);
|
|
11385
|
+
const started = await submit.json();
|
|
11386
|
+
if (!started.name) throw new Error("Video request was accepted but returned no operation to poll.");
|
|
11387
|
+
onProgress?.(`Video generation started (${cap.modelId}). This usually takes 1-3 minutes.`);
|
|
11388
|
+
const deadline = Date.now() + VIDEO_TIMEOUT_MS;
|
|
11389
|
+
let operation = {};
|
|
11390
|
+
while (Date.now() < deadline) {
|
|
11391
|
+
await sleep(VIDEO_POLL_INTERVAL_MS, signal);
|
|
11392
|
+
const poll = await fetch(`${base}/${started.name}`, { headers, signal });
|
|
11393
|
+
if (!poll.ok) {
|
|
11394
|
+
const text = await poll.text().catch(() => "");
|
|
11395
|
+
throw Object.assign(new Error(text || poll.statusText), { status: poll.status });
|
|
11396
|
+
}
|
|
11397
|
+
operation = await poll.json();
|
|
11398
|
+
if (operation.done) break;
|
|
11399
|
+
onProgress?.("Still rendering\u2026");
|
|
11400
|
+
}
|
|
11401
|
+
if (!operation.done) {
|
|
11402
|
+
throw new Error(
|
|
11403
|
+
`Video was still rendering after ${Math.round(VIDEO_TIMEOUT_MS / 6e4)} minutes. It may still complete on the provider side, but Cascade stopped waiting.`
|
|
11404
|
+
);
|
|
11405
|
+
}
|
|
11406
|
+
if (operation.error?.message) throw new Error(operation.error.message);
|
|
11407
|
+
const uri = operation.response?.generateVideoResponse?.generatedSamples?.[0]?.video?.uri;
|
|
11408
|
+
if (!uri) throw new Error("Video finished but the response contained no video.");
|
|
11409
|
+
const media = await fetch(uri, { headers, signal });
|
|
11410
|
+
if (!media.ok) throw new Error(`Video finished but could not be downloaded (HTTP ${media.status}).`);
|
|
11411
|
+
return {
|
|
11412
|
+
data: Buffer.from(await media.arrayBuffer()),
|
|
11413
|
+
mimeType: "video/mp4",
|
|
11414
|
+
filename: stamp("mp4"),
|
|
11415
|
+
modelId: cap.modelId,
|
|
11416
|
+
provider: cap.provider
|
|
11417
|
+
};
|
|
11418
|
+
}, cap.modelId);
|
|
11419
|
+
}
|
|
11420
|
+
async function transcribeAudio(cap, cfg, req, signal) {
|
|
11421
|
+
return callProvider(async () => {
|
|
11422
|
+
if (cap.api !== "openai-transcriptions") throw new Error(`${cap.modelId} is not a transcription model.`);
|
|
11423
|
+
const url = `${baseUrlFor(cfg, "https://api.openai.com/v1")}/audio/transcriptions`;
|
|
11424
|
+
const form = new FormData();
|
|
11425
|
+
form.append("model", cap.modelId);
|
|
11426
|
+
if (req.language) form.append("language", req.language);
|
|
11427
|
+
form.append("file", new Blob([new Uint8Array(req.audio)]), req.filename);
|
|
11428
|
+
const ac = new AbortController();
|
|
11429
|
+
const timer = setTimeout(() => ac.abort(), DEFAULT_TIMEOUT_MS);
|
|
11430
|
+
const onAbort = () => ac.abort();
|
|
11431
|
+
signal?.addEventListener("abort", onAbort);
|
|
11432
|
+
try {
|
|
11433
|
+
const res = await fetch(url, {
|
|
11434
|
+
method: "POST",
|
|
11435
|
+
headers: { Authorization: `Bearer ${cfg.apiKey ?? ""}` },
|
|
11436
|
+
body: form,
|
|
11437
|
+
signal: ac.signal
|
|
11438
|
+
});
|
|
11439
|
+
if (!res.ok) {
|
|
11440
|
+
const text = await res.text().catch(() => "");
|
|
11441
|
+
throw Object.assign(new Error(text || res.statusText), { status: res.status });
|
|
11442
|
+
}
|
|
11443
|
+
const body = await res.json();
|
|
11444
|
+
return { text: body.text ?? "", modelId: cap.modelId };
|
|
11445
|
+
} finally {
|
|
11446
|
+
clearTimeout(timer);
|
|
11447
|
+
signal?.removeEventListener("abort", onAbort);
|
|
11448
|
+
}
|
|
11449
|
+
}, cap.modelId);
|
|
11450
|
+
}
|
|
11451
|
+
|
|
11452
|
+
// src/tools/generate-media.ts
|
|
11453
|
+
function resolve(deps, modality, preferModel) {
|
|
11454
|
+
const selected = deps.registry.select(modality, preferModel ? { preferModel } : {});
|
|
11455
|
+
if (!selected) {
|
|
11456
|
+
return { ok: false, error: `No ${modality} model is available on your configured providers.` };
|
|
11457
|
+
}
|
|
11458
|
+
const cfg = deps.lookupProvider(selected.capability.provider);
|
|
11459
|
+
if (!cfg) {
|
|
11460
|
+
return {
|
|
11461
|
+
ok: false,
|
|
11462
|
+
error: `${selected.capability.modelId} needs the ${selected.capability.provider} provider, which is not configured.`
|
|
11463
|
+
};
|
|
11464
|
+
}
|
|
11465
|
+
return { ok: true, selected, cfg };
|
|
11466
|
+
}
|
|
11467
|
+
var GenerateImageTool = class extends BaseTool {
|
|
11468
|
+
constructor(deps) {
|
|
11469
|
+
super();
|
|
11470
|
+
this.deps = deps;
|
|
11471
|
+
}
|
|
11472
|
+
deps;
|
|
11473
|
+
name = "generate_image";
|
|
11474
|
+
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.";
|
|
11475
|
+
inputSchema = {
|
|
11476
|
+
type: "object",
|
|
11477
|
+
properties: {
|
|
11478
|
+
prompt: { type: "string", description: "What the image should show. Be specific about subject, style and composition." },
|
|
11479
|
+
size: { type: "string", description: 'Pixel size, e.g. "1024x1024" (default), "1792x1024", "1024x1792".' },
|
|
11480
|
+
model: { type: "string", description: "Optional: force a specific image model instead of the automatic choice." }
|
|
11481
|
+
},
|
|
11482
|
+
required: ["prompt"]
|
|
11483
|
+
};
|
|
11484
|
+
async execute(input, options) {
|
|
11485
|
+
const prompt = String(input["prompt"] ?? "").trim();
|
|
11486
|
+
if (!prompt) return 'Provide a "prompt" describing the image.';
|
|
11487
|
+
const r = resolve(this.deps, "image", input["model"] ? String(input["model"]) : void 0);
|
|
11488
|
+
if (!r.ok) return r.error;
|
|
11489
|
+
const size = input["size"] ? String(input["size"]) : void 0;
|
|
11490
|
+
const asset = await generateImage(
|
|
11491
|
+
r.selected.capability,
|
|
11492
|
+
r.cfg,
|
|
11493
|
+
{ prompt, ...size ? { size } : {} },
|
|
11494
|
+
options.signal
|
|
11495
|
+
);
|
|
11496
|
+
const location = await this.deps.sink(asset);
|
|
11497
|
+
return [
|
|
11498
|
+
`Image generated and saved to ${location}.`,
|
|
11499
|
+
`Model: ${asset.modelId} \u2014 ${r.selected.reason}`,
|
|
11500
|
+
// DALL·E rewrites the prompt before drawing; the model needs to know what
|
|
11501
|
+
// was actually rendered, not what it asked for, or its description of the
|
|
11502
|
+
// result will not match the image.
|
|
11503
|
+
asset.revisedPrompt ? `The provider rewrote the prompt to: ${asset.revisedPrompt}` : ""
|
|
11504
|
+
].filter(Boolean).join("\n");
|
|
11505
|
+
}
|
|
11506
|
+
};
|
|
11507
|
+
var GenerateSpeechTool = class extends BaseTool {
|
|
11508
|
+
constructor(deps) {
|
|
11509
|
+
super();
|
|
11510
|
+
this.deps = deps;
|
|
11511
|
+
}
|
|
11512
|
+
deps;
|
|
11513
|
+
name = "generate_speech";
|
|
11514
|
+
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.";
|
|
11515
|
+
inputSchema = {
|
|
11516
|
+
type: "object",
|
|
11517
|
+
properties: {
|
|
11518
|
+
text: { type: "string", description: "The text to speak." },
|
|
11519
|
+
voice: { type: "string", description: "Voice id: alloy, echo, fable, onyx, nova, shimmer. Default alloy." },
|
|
11520
|
+
format: { type: "string", description: "Audio format: mp3 (default), opus, aac, flac, wav." },
|
|
11521
|
+
model: { type: "string", description: "Optional: force a specific speech model." }
|
|
11522
|
+
},
|
|
11523
|
+
required: ["text"]
|
|
11524
|
+
};
|
|
11525
|
+
async execute(input, options) {
|
|
11526
|
+
const text = String(input["text"] ?? "").trim();
|
|
11527
|
+
if (!text) return 'Provide "text" to speak.';
|
|
11528
|
+
const r = resolve(this.deps, "speech", input["model"] ? String(input["model"]) : void 0);
|
|
11529
|
+
if (!r.ok) return r.error;
|
|
11530
|
+
const fmt = String(input["format"] ?? "mp3");
|
|
11531
|
+
const allowed = ["mp3", "opus", "aac", "flac", "wav"];
|
|
11532
|
+
const format = allowed.includes(fmt) ? fmt : "mp3";
|
|
11533
|
+
const asset = await generateSpeech(
|
|
11534
|
+
r.selected.capability,
|
|
11535
|
+
r.cfg,
|
|
11536
|
+
{ text, voice: input["voice"] ? String(input["voice"]) : void 0, format },
|
|
11537
|
+
options.signal
|
|
11538
|
+
);
|
|
11539
|
+
const location = await this.deps.sink(asset);
|
|
11540
|
+
return `Speech generated and saved to ${location}.
|
|
11541
|
+
Model: ${asset.modelId} \u2014 ${r.selected.reason}`;
|
|
11542
|
+
}
|
|
11543
|
+
};
|
|
11544
|
+
var DEFAULT_VIDEO_SECONDS = 5;
|
|
11545
|
+
var MAX_VIDEO_SECONDS = 8;
|
|
11546
|
+
var GenerateVideoTool = class extends BaseTool {
|
|
11547
|
+
constructor(deps) {
|
|
11548
|
+
super();
|
|
11549
|
+
this.deps = deps;
|
|
11550
|
+
}
|
|
11551
|
+
deps;
|
|
11552
|
+
name = "generate_video";
|
|
11553
|
+
// The price is in the description on purpose. Video is ~1000x the cost of an
|
|
11554
|
+
// image per call, it is billed per SECOND, and the model choosing the
|
|
11555
|
+
// duration is the one deciding the bill — so the number has to be where the
|
|
11556
|
+
// model reads before calling, not only in the result afterwards.
|
|
11557
|
+
description = "Generate a short video clip from a text description. EXPENSIVE and slow: billed per second of output (about $0.40/second, so a 5-second clip costs roughly $2) and takes 1-3 minutes. Only use when the user explicitly asks for a video, and prefer the shortest duration that satisfies the request.";
|
|
11558
|
+
inputSchema = {
|
|
11559
|
+
type: "object",
|
|
11560
|
+
properties: {
|
|
11561
|
+
prompt: { type: "string", description: "What the video should show: subject, action, camera movement, style." },
|
|
11562
|
+
seconds: { type: "number", description: `Clip length, 1-${MAX_VIDEO_SECONDS} seconds (default ${DEFAULT_VIDEO_SECONDS}). Billed per second \u2014 keep it short.` },
|
|
11563
|
+
aspectRatio: { type: "string", description: 'Frame shape: "16:9" (default) or "9:16" for vertical.' },
|
|
11564
|
+
model: { type: "string", description: "Optional: force a specific video model." }
|
|
11565
|
+
},
|
|
11566
|
+
required: ["prompt"]
|
|
11567
|
+
};
|
|
11568
|
+
async execute(input, options) {
|
|
11569
|
+
const prompt = String(input["prompt"] ?? "").trim();
|
|
11570
|
+
if (!prompt) return 'Provide a "prompt" describing the video.';
|
|
11571
|
+
const r = resolve(this.deps, "video", input["model"] ? String(input["model"]) : void 0);
|
|
11572
|
+
if (!r.ok) return r.error;
|
|
11573
|
+
const asked = typeof input["seconds"] === "number" ? input["seconds"] : DEFAULT_VIDEO_SECONDS;
|
|
11574
|
+
const seconds = Math.max(1, Math.min(MAX_VIDEO_SECONDS, Math.round(asked)));
|
|
11575
|
+
const asset = await generateVideo(
|
|
11576
|
+
r.selected.capability,
|
|
11577
|
+
r.cfg,
|
|
11578
|
+
{
|
|
11579
|
+
prompt,
|
|
11580
|
+
seconds,
|
|
11581
|
+
...input["aspectRatio"] ? { aspectRatio: String(input["aspectRatio"]) } : {}
|
|
11582
|
+
},
|
|
11583
|
+
options.signal,
|
|
11584
|
+
// Progress goes to the run log so a two-minute wait doesn't look like a
|
|
11585
|
+
// hang — the single most likely reason someone kills a working render.
|
|
11586
|
+
(note) => options.onProgress?.(note)
|
|
11587
|
+
);
|
|
11588
|
+
const location = await this.deps.sink(asset);
|
|
11589
|
+
const unit = r.selected.cost;
|
|
11590
|
+
const estimate = unit ? ` Estimated cost: $${(unit.amount * seconds).toFixed(2)} (${seconds}s x ${unit.label}).` : "";
|
|
11591
|
+
const clamped = seconds !== Math.round(asked) ? ` Duration was clamped to ${seconds}s (max ${MAX_VIDEO_SECONDS}).` : "";
|
|
11592
|
+
return `Video generated and saved to ${location}.
|
|
11593
|
+
Model: ${asset.modelId}.${estimate}${clamped}`;
|
|
11594
|
+
}
|
|
11595
|
+
};
|
|
11596
|
+
var TranscribeAudioTool = class extends BaseTool {
|
|
11597
|
+
constructor(deps, readFile) {
|
|
11598
|
+
super();
|
|
11599
|
+
this.deps = deps;
|
|
11600
|
+
this.readFile = readFile;
|
|
11601
|
+
}
|
|
11602
|
+
deps;
|
|
11603
|
+
readFile;
|
|
11604
|
+
name = "transcribe_audio";
|
|
11605
|
+
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.";
|
|
11606
|
+
inputSchema = {
|
|
11607
|
+
type: "object",
|
|
11608
|
+
properties: {
|
|
11609
|
+
path: { type: "string", description: "Path to the audio file to transcribe." },
|
|
11610
|
+
language: { type: "string", description: 'Optional ISO-639-1 language hint, e.g. "en", improves accuracy.' },
|
|
11611
|
+
model: { type: "string", description: "Optional: force a specific transcription model." }
|
|
11612
|
+
},
|
|
11613
|
+
required: ["path"]
|
|
11614
|
+
};
|
|
11615
|
+
async execute(input, options) {
|
|
11616
|
+
const filePath = String(input["path"] ?? "").trim();
|
|
11617
|
+
if (!filePath) return 'Provide a "path" to the audio file.';
|
|
11618
|
+
const r = resolve(this.deps, "transcription", input["model"] ? String(input["model"]) : void 0);
|
|
11619
|
+
if (!r.ok) return r.error;
|
|
11620
|
+
let audio;
|
|
11621
|
+
try {
|
|
11622
|
+
audio = await this.readFile(filePath);
|
|
11623
|
+
} catch {
|
|
11624
|
+
return `Could not read the audio file at ${filePath}.`;
|
|
11625
|
+
}
|
|
11626
|
+
const { text, modelId } = await transcribeAudio(
|
|
11627
|
+
r.selected.capability,
|
|
11628
|
+
r.cfg,
|
|
11629
|
+
{
|
|
11630
|
+
audio,
|
|
11631
|
+
filename: filePath.split(/[\\/]/).pop() || "audio",
|
|
11632
|
+
...input["language"] ? { language: String(input["language"]) } : {}
|
|
11633
|
+
},
|
|
11634
|
+
options.signal
|
|
11635
|
+
);
|
|
11636
|
+
return text ? `Transcript (${modelId}):
|
|
11637
|
+
|
|
11638
|
+
${text}` : `${modelId} returned an empty transcript \u2014 the file may be silent or in an unsupported format.`;
|
|
11639
|
+
}
|
|
11640
|
+
};
|
|
11641
|
+
function buildMediaTools(deps, readFile) {
|
|
11642
|
+
const tools = [];
|
|
11643
|
+
const modalities = new Set(deps.registry.availableModalities());
|
|
11644
|
+
if (modalities.has("image")) tools.push(new GenerateImageTool(deps));
|
|
11645
|
+
if (modalities.has("speech")) tools.push(new GenerateSpeechTool(deps));
|
|
11646
|
+
if (modalities.has("video")) tools.push(new GenerateVideoTool(deps));
|
|
11647
|
+
if (modalities.has("transcription") && readFile) tools.push(new TranscribeAudioTool(deps, readFile));
|
|
11648
|
+
return tools;
|
|
11649
|
+
}
|
|
11650
|
+
|
|
10932
11651
|
// src/core/run-breaker.ts
|
|
10933
11652
|
var DEFAULT_FAILURE_THRESHOLD = 3;
|
|
10934
11653
|
var RunBreaker = class {
|
|
@@ -10997,26 +11716,6 @@ var RunBreaker = class {
|
|
|
10997
11716
|
|
|
10998
11717
|
// src/tools/registry.ts
|
|
10999
11718
|
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
11719
|
var execAsync = promisify(exec);
|
|
11021
11720
|
var ShellTool = class extends BaseTool {
|
|
11022
11721
|
name = "shell";
|
|
@@ -11712,7 +12411,7 @@ var PDFCreateTool = class extends BaseTool {
|
|
|
11712
12411
|
if (!fs29.existsSync(dir)) {
|
|
11713
12412
|
fs29.mkdirSync(dir, { recursive: true });
|
|
11714
12413
|
}
|
|
11715
|
-
return new Promise((
|
|
12414
|
+
return new Promise((resolve2, reject) => {
|
|
11716
12415
|
try {
|
|
11717
12416
|
const doc = new PDFDocument({ margin: 50 });
|
|
11718
12417
|
const stream = fs29.createWriteStream(filePath);
|
|
@@ -11730,7 +12429,7 @@ var PDFCreateTool = class extends BaseTool {
|
|
|
11730
12429
|
});
|
|
11731
12430
|
doc.end();
|
|
11732
12431
|
stream.on("finish", () => {
|
|
11733
|
-
|
|
12432
|
+
resolve2(`Successfully created PDF at ${filePath} (${content.length} characters)`);
|
|
11734
12433
|
});
|
|
11735
12434
|
stream.on("error", (err) => {
|
|
11736
12435
|
reject(new Error(`Failed to write PDF: ${err.message}`));
|
|
@@ -11802,7 +12501,7 @@ var CodeInterpreterTool = class extends BaseTool {
|
|
|
11802
12501
|
const filePath = path32.join(tmpDir, fileName);
|
|
11803
12502
|
fs29.writeFileSync(filePath, code, "utf-8");
|
|
11804
12503
|
const execArgs = [filePath, ...args];
|
|
11805
|
-
return new Promise((
|
|
12504
|
+
return new Promise((resolve2) => {
|
|
11806
12505
|
const startMs = Date.now();
|
|
11807
12506
|
execFile(cmdPrefix, execArgs, { cwd: this.workspaceRoot, timeout: 3e4 }, (error, stdout, stderr) => {
|
|
11808
12507
|
const duration = Date.now() - startMs;
|
|
@@ -11816,17 +12515,17 @@ var CodeInterpreterTool = class extends BaseTool {
|
|
|
11816
12515
|
if (error) {
|
|
11817
12516
|
const timedOut = error.killed && duration >= 3e4;
|
|
11818
12517
|
if (timedOut) {
|
|
11819
|
-
|
|
12518
|
+
resolve2(`Execution timed out after 30s. Consider breaking the task into smaller pieces.
|
|
11820
12519
|
Partial stdout: ${stdout}
|
|
11821
12520
|
Stderr: ${stderr}`);
|
|
11822
12521
|
} else {
|
|
11823
|
-
|
|
12522
|
+
resolve2(`Execution failed (${duration}ms):
|
|
11824
12523
|
Error: ${error.message}
|
|
11825
12524
|
Stderr: ${stderr}
|
|
11826
12525
|
Stdout: ${stdout}`);
|
|
11827
12526
|
}
|
|
11828
12527
|
} else {
|
|
11829
|
-
|
|
12528
|
+
resolve2(`Execution successful (${duration}ms):
|
|
11830
12529
|
Stdout: ${stdout}
|
|
11831
12530
|
Stderr: ${stderr}`);
|
|
11832
12531
|
}
|
|
@@ -12480,7 +13179,7 @@ var ToolRegistry = class extends EventEmitter {
|
|
|
12480
13179
|
*/
|
|
12481
13180
|
waitForTool(toolName, timeoutMs = 6e4) {
|
|
12482
13181
|
if (this.tools.has(toolName)) return Promise.resolve();
|
|
12483
|
-
return new Promise((
|
|
13182
|
+
return new Promise((resolve2, reject) => {
|
|
12484
13183
|
const timer = setTimeout(() => {
|
|
12485
13184
|
this.off("tool:added", handler);
|
|
12486
13185
|
reject(new Error(`Timeout waiting for tool: ${toolName}`));
|
|
@@ -12489,7 +13188,7 @@ var ToolRegistry = class extends EventEmitter {
|
|
|
12489
13188
|
if (name === toolName) {
|
|
12490
13189
|
clearTimeout(timer);
|
|
12491
13190
|
this.off("tool:added", handler);
|
|
12492
|
-
|
|
13191
|
+
resolve2();
|
|
12493
13192
|
}
|
|
12494
13193
|
};
|
|
12495
13194
|
this.on("tool:added", handler);
|
|
@@ -13142,20 +13841,20 @@ var PermissionEscalator = class extends EventEmitter {
|
|
|
13142
13841
|
resolver(decision);
|
|
13143
13842
|
}
|
|
13144
13843
|
waitForUserDecision(req) {
|
|
13145
|
-
return new Promise((
|
|
13844
|
+
return new Promise((resolve2) => {
|
|
13146
13845
|
let timer;
|
|
13147
13846
|
const wrappedResolver = (decision) => {
|
|
13148
13847
|
if (timer) clearTimeout(timer);
|
|
13149
13848
|
if (decision.always) {
|
|
13150
13849
|
this.taskWideCache.set(req.toolName, decision.approved);
|
|
13151
13850
|
}
|
|
13152
|
-
|
|
13851
|
+
resolve2(decision);
|
|
13153
13852
|
};
|
|
13154
13853
|
this.pendingUserDecisions.set(req.id, wrappedResolver);
|
|
13155
13854
|
if (this.approvalTimeoutMs > 0 && Number.isFinite(this.approvalTimeoutMs)) {
|
|
13156
13855
|
timer = setTimeout(() => {
|
|
13157
13856
|
if (this.pendingUserDecisions.delete(req.id)) {
|
|
13158
|
-
|
|
13857
|
+
resolve2({
|
|
13159
13858
|
requestId: req.id,
|
|
13160
13859
|
approved: false,
|
|
13161
13860
|
decidedBy: "USER",
|
|
@@ -13223,6 +13922,23 @@ var noopTelemetry = {
|
|
|
13223
13922
|
}
|
|
13224
13923
|
};
|
|
13225
13924
|
|
|
13925
|
+
// src/core/router/feedback-prior.ts
|
|
13926
|
+
var MAX_FEEDBACK_ADJUSTMENT = 0.05;
|
|
13927
|
+
var PRIOR_STRENGTH = 10;
|
|
13928
|
+
function feedbackAdjustment(counts) {
|
|
13929
|
+
const good = Math.max(0, Math.floor(counts.good));
|
|
13930
|
+
const bad = Math.max(0, Math.floor(counts.bad));
|
|
13931
|
+
const n = good + bad;
|
|
13932
|
+
if (n === 0) return 0;
|
|
13933
|
+
const half = PRIOR_STRENGTH / 2;
|
|
13934
|
+
const shrunk = (good + half) / (n + PRIOR_STRENGTH);
|
|
13935
|
+
return (shrunk - 0.5) * 2 * MAX_FEEDBACK_ADJUSTMENT;
|
|
13936
|
+
}
|
|
13937
|
+
function applyFeedback(score01, counts) {
|
|
13938
|
+
const adjusted = score01 + feedbackAdjustment(counts);
|
|
13939
|
+
return Math.max(0, Math.min(1, adjusted));
|
|
13940
|
+
}
|
|
13941
|
+
|
|
13226
13942
|
// src/core/router/task-analyzer.ts
|
|
13227
13943
|
init_pricing();
|
|
13228
13944
|
var CODE_SIGNALS = [
|
|
@@ -13298,6 +14014,8 @@ var TASK_TYPE_TAGS = {
|
|
|
13298
14014
|
};
|
|
13299
14015
|
var TaskAnalyzer = class {
|
|
13300
14016
|
tracker;
|
|
14017
|
+
/** Per-model thumbs counts, when the host collects them. */
|
|
14018
|
+
feedback;
|
|
13301
14019
|
bias;
|
|
13302
14020
|
lastProfile = null;
|
|
13303
14021
|
lastSelectedModels = /* @__PURE__ */ new Map();
|
|
@@ -13305,6 +14023,14 @@ var TaskAnalyzer = class {
|
|
|
13305
14023
|
this.tracker = tracker;
|
|
13306
14024
|
this.bias = bias;
|
|
13307
14025
|
}
|
|
14026
|
+
/**
|
|
14027
|
+
* Supply per-model rating counts. Optional by design: a host that collects no
|
|
14028
|
+
* feedback (the CLI, a fresh install) scores exactly as it did before, since
|
|
14029
|
+
* feedbackAdjustment returns 0 for an empty record.
|
|
14030
|
+
*/
|
|
14031
|
+
setFeedbackSource(source) {
|
|
14032
|
+
this.feedback = source;
|
|
14033
|
+
}
|
|
13308
14034
|
setTracker(tracker) {
|
|
13309
14035
|
this.tracker = tracker;
|
|
13310
14036
|
}
|
|
@@ -13388,7 +14114,10 @@ var TaskAnalyzer = class {
|
|
|
13388
14114
|
const perf = this.tracker?.performanceScore(model.id, profile.type) ?? 0.5;
|
|
13389
14115
|
const costEff = this.costEfficiency(model, profile.complexity);
|
|
13390
14116
|
const match = this.taskMatchScore(model, profile);
|
|
13391
|
-
const
|
|
14117
|
+
const rated = this.feedback?.(model.id);
|
|
14118
|
+
const publicScore = benchmarkScore01(model, profile.type);
|
|
14119
|
+
const adjusted = rated ? applyFeedback(publicScore, rated) : publicScore;
|
|
14120
|
+
const benchmark = 0.3 + 0.7 * adjusted;
|
|
13392
14121
|
switch (this.bias) {
|
|
13393
14122
|
case "quality":
|
|
13394
14123
|
return perf * match * benchmark ** 2 * (0.85 + 0.15 * costEff);
|
|
@@ -13793,10 +14522,10 @@ var DynamicTool = class extends BaseTool {
|
|
|
13793
14522
|
`;
|
|
13794
14523
|
const script = await isolate.compileScript(bootstrap);
|
|
13795
14524
|
const runPromise = script.run(context, { timeout: timeoutMs, promise: true }).then((v) => String(v ?? ""));
|
|
13796
|
-
const timeoutPromise = new Promise((
|
|
14525
|
+
const timeoutPromise = new Promise((resolve2) => {
|
|
13797
14526
|
const t = setTimeout(() => {
|
|
13798
14527
|
dispose();
|
|
13799
|
-
|
|
14528
|
+
resolve2(`Dynamic tool "${this.name}" timed out after ${timeoutMs}ms and was terminated.`);
|
|
13800
14529
|
}, timeoutMs + 500);
|
|
13801
14530
|
t.unref?.();
|
|
13802
14531
|
});
|
|
@@ -13814,7 +14543,7 @@ var DynamicTool = class extends BaseTool {
|
|
|
13814
14543
|
/** Spawn the worker, service its callTool/fetch bridge, enforce the kill timeout. */
|
|
13815
14544
|
runInWorker(input, callTool) {
|
|
13816
14545
|
const timeoutMs = Math.max(200, Number(process.env["CASCADE_DYNAMIC_TOOL_TIMEOUT_MS"]) || DYNAMIC_TOOL_TIMEOUT_MS);
|
|
13817
|
-
return new Promise((
|
|
14546
|
+
return new Promise((resolve2) => {
|
|
13818
14547
|
let settled = false;
|
|
13819
14548
|
const worker = new Worker(HARNESS_SRC, {
|
|
13820
14549
|
eval: true,
|
|
@@ -13826,7 +14555,7 @@ var DynamicTool = class extends BaseTool {
|
|
|
13826
14555
|
settled = true;
|
|
13827
14556
|
clearTimeout(timer);
|
|
13828
14557
|
void worker.terminate();
|
|
13829
|
-
|
|
14558
|
+
resolve2(value);
|
|
13830
14559
|
};
|
|
13831
14560
|
const timer = setTimeout(
|
|
13832
14561
|
() => finish(`Dynamic tool "${this.name}" timed out after ${timeoutMs}ms and was terminated.`),
|
|
@@ -15333,6 +16062,8 @@ ${prompt}`;
|
|
|
15333
16062
|
var Cascade = class _Cascade extends EventEmitter {
|
|
15334
16063
|
router;
|
|
15335
16064
|
toolRegistry;
|
|
16065
|
+
multimodal;
|
|
16066
|
+
mediaSink;
|
|
15336
16067
|
mcpClient;
|
|
15337
16068
|
codeIndex;
|
|
15338
16069
|
config;
|
|
@@ -15371,6 +16102,10 @@ var Cascade = class _Cascade extends EventEmitter {
|
|
|
15371
16102
|
}
|
|
15372
16103
|
});
|
|
15373
16104
|
this.toolRegistry = new ToolRegistry(this.config.tools, workspacePath);
|
|
16105
|
+
this.multimodal = new MultimodalRegistry(
|
|
16106
|
+
(this.config.providers ?? []).map((p) => p.type)
|
|
16107
|
+
);
|
|
16108
|
+
this.registerMediaTools(workspacePath);
|
|
15374
16109
|
this.telemetry = config.telemetry?.enabled ? new Telemetry(config.telemetry, config.telemetry.distinctId ?? "anonymous") : noopTelemetry;
|
|
15375
16110
|
this.worldStateDB = new WorldStateDB(this.workspacePath, this.config.workspace?.debugWorldState ?? false);
|
|
15376
16111
|
this.router.setWorldStateDB(this.worldStateDB);
|
|
@@ -15424,14 +16159,14 @@ var Cascade = class _Cascade extends EventEmitter {
|
|
|
15424
16159
|
if (this.listenerCount("mcp:approval-required") === 0) {
|
|
15425
16160
|
return false;
|
|
15426
16161
|
}
|
|
15427
|
-
return await new Promise((
|
|
15428
|
-
this.pendingMcpApprovals.set(server.name,
|
|
16162
|
+
return await new Promise((resolve2) => {
|
|
16163
|
+
this.pendingMcpApprovals.set(server.name, resolve2);
|
|
15429
16164
|
const timeout = setTimeout(() => {
|
|
15430
|
-
if (this.pendingMcpApprovals.delete(server.name))
|
|
16165
|
+
if (this.pendingMcpApprovals.delete(server.name)) resolve2(false);
|
|
15431
16166
|
}, 3e4);
|
|
15432
16167
|
const wrap = (approved) => {
|
|
15433
16168
|
clearTimeout(timeout);
|
|
15434
|
-
|
|
16169
|
+
resolve2(approved);
|
|
15435
16170
|
};
|
|
15436
16171
|
this.pendingMcpApprovals.set(server.name, wrap);
|
|
15437
16172
|
this.emit("mcp:approval-required", { server });
|
|
@@ -15505,17 +16240,17 @@ var Cascade = class _Cascade extends EventEmitter {
|
|
|
15505
16240
|
}
|
|
15506
16241
|
const t2Count = plan.sections.length;
|
|
15507
16242
|
const t3Count = plan.sections.reduce((sum, s) => sum + (s.t3Subtasks?.length ?? 0), 0);
|
|
15508
|
-
return await new Promise((
|
|
16243
|
+
return await new Promise((resolve2) => {
|
|
15509
16244
|
const timeout = setTimeout(() => {
|
|
15510
16245
|
if (this.pendingPlanApproval) {
|
|
15511
16246
|
this.pendingPlanApproval = void 0;
|
|
15512
|
-
|
|
16247
|
+
resolve2({ approved: true });
|
|
15513
16248
|
}
|
|
15514
16249
|
}, 12e4);
|
|
15515
16250
|
this.pendingPlanApproval = (decision) => {
|
|
15516
16251
|
clearTimeout(timeout);
|
|
15517
16252
|
this.pendingPlanApproval = void 0;
|
|
15518
|
-
|
|
16253
|
+
resolve2(decision);
|
|
15519
16254
|
};
|
|
15520
16255
|
this.emit("plan:approval-required", {
|
|
15521
16256
|
taskId,
|
|
@@ -15548,17 +16283,17 @@ var Cascade = class _Cascade extends EventEmitter {
|
|
|
15548
16283
|
}
|
|
15549
16284
|
async requestContextApproval(info) {
|
|
15550
16285
|
if (this.listenerCount("context:approval-required") === 0) return true;
|
|
15551
|
-
return await new Promise((
|
|
16286
|
+
return await new Promise((resolve2) => {
|
|
15552
16287
|
const timeout = setTimeout(() => {
|
|
15553
16288
|
if (this.pendingContextApproval) {
|
|
15554
16289
|
this.pendingContextApproval = void 0;
|
|
15555
|
-
|
|
16290
|
+
resolve2(true);
|
|
15556
16291
|
}
|
|
15557
16292
|
}, 12e4);
|
|
15558
16293
|
this.pendingContextApproval = (approved) => {
|
|
15559
16294
|
clearTimeout(timeout);
|
|
15560
16295
|
this.pendingContextApproval = void 0;
|
|
15561
|
-
|
|
16296
|
+
resolve2(approved);
|
|
15562
16297
|
};
|
|
15563
16298
|
this.emit("context:approval-required", info);
|
|
15564
16299
|
});
|
|
@@ -15638,6 +16373,68 @@ ${input}` }],
|
|
|
15638
16373
|
if (this.store) t1.setStore(this.store);
|
|
15639
16374
|
return t1.previewPlan(prompt);
|
|
15640
16375
|
}
|
|
16376
|
+
/**
|
|
16377
|
+
* Register image / speech / transcription tools for the modalities this
|
|
16378
|
+
* config can actually serve.
|
|
16379
|
+
*
|
|
16380
|
+
* The sink writes bytes into the workspace, because that is what a desktop or
|
|
16381
|
+
* CLI run wants and it keeps a generated asset indistinguishable from any
|
|
16382
|
+
* other file the run produced. A host with different storage (the cloud
|
|
16383
|
+
* server stores a file row) constructs the tools itself via buildMediaTools
|
|
16384
|
+
* with its own sink.
|
|
16385
|
+
*/
|
|
16386
|
+
/**
|
|
16387
|
+
* Replace where generated media is written.
|
|
16388
|
+
*
|
|
16389
|
+
* The default sink writes into the workspace, which is what a desktop or CLI
|
|
16390
|
+
* run wants. A host with different storage — the cloud server keeps a file
|
|
16391
|
+
* row plus bytes under the tenant's directory — supplies its own here, and
|
|
16392
|
+
* the media tools are rebuilt around it. Re-registering by name replaces the
|
|
16393
|
+
* existing tools rather than duplicating them.
|
|
16394
|
+
*/
|
|
16395
|
+
setMediaSink(sink) {
|
|
16396
|
+
this.mediaSink = sink;
|
|
16397
|
+
this.registerMediaTools(this.workspacePath);
|
|
16398
|
+
}
|
|
16399
|
+
registerMediaTools(workspacePath) {
|
|
16400
|
+
const denied = new Set(this.config.tools?.disabledTools ?? []);
|
|
16401
|
+
const permitted = (name) => !denied.has(name);
|
|
16402
|
+
const tools = buildMediaTools(
|
|
16403
|
+
{
|
|
16404
|
+
registry: this.multimodal,
|
|
16405
|
+
lookupProvider: (provider) => (this.config.providers ?? []).find((p) => p.type === provider),
|
|
16406
|
+
sink: this.mediaSink ?? (async (asset) => {
|
|
16407
|
+
const fsp = await import('fs/promises');
|
|
16408
|
+
const nodePath = await import('path');
|
|
16409
|
+
const dir = nodePath.join(workspacePath, "generated");
|
|
16410
|
+
await fsp.mkdir(dir, { recursive: true });
|
|
16411
|
+
const dest = nodePath.join(dir, asset.filename);
|
|
16412
|
+
await fsp.writeFile(dest, asset.data);
|
|
16413
|
+
return nodePath.relative(workspacePath, dest);
|
|
16414
|
+
})
|
|
16415
|
+
},
|
|
16416
|
+
async (p) => {
|
|
16417
|
+
const fsp = await import('fs/promises');
|
|
16418
|
+
return fsp.readFile(p);
|
|
16419
|
+
}
|
|
16420
|
+
);
|
|
16421
|
+
for (const tool of tools) {
|
|
16422
|
+
if (!permitted(tool.name)) continue;
|
|
16423
|
+
tool.setWorkspaceRoot(workspacePath);
|
|
16424
|
+
this.toolRegistry.register(tool);
|
|
16425
|
+
}
|
|
16426
|
+
}
|
|
16427
|
+
/**
|
|
16428
|
+
* Supply per-model thumbs-up/down counts so Auto routing can weigh how models
|
|
16429
|
+
* have actually performed for this user, on top of public benchmarks.
|
|
16430
|
+
*
|
|
16431
|
+
* Optional everywhere: with no source, or an empty record, scoring is
|
|
16432
|
+
* byte-identical to before. The adjustment is capped and sample-size shrunk —
|
|
16433
|
+
* see core/router/feedback-prior.ts.
|
|
16434
|
+
*/
|
|
16435
|
+
setFeedbackSource(source) {
|
|
16436
|
+
this.router.setFeedbackSource(source);
|
|
16437
|
+
}
|
|
15641
16438
|
/** True when a task stopped at the budget cap and can be resumed via /continue. */
|
|
15642
16439
|
hasResumableRun() {
|
|
15643
16440
|
return this.lastInterruptedRun != null;
|
|
@@ -16127,7 +16924,7 @@ ${prompt}` : prompt;
|
|
|
16127
16924
|
if (toolCreator) toolCreator.setPermissionEscalator(escalator);
|
|
16128
16925
|
const rootPrompt = buildContextualPrompt(options.prompt, options.conversationHistory);
|
|
16129
16926
|
let finalOutput = "";
|
|
16130
|
-
const runBreaker = new RunBreaker();
|
|
16927
|
+
const runBreaker = new RunBreaker(this.config.budget?.failureThreshold);
|
|
16131
16928
|
let t2Results = [];
|
|
16132
16929
|
let runError = null;
|
|
16133
16930
|
let identityPrompt = "";
|
|
@@ -16618,7 +17415,7 @@ var CloudClient = class _CloudClient {
|
|
|
16618
17415
|
const deadline = Date.now() + start.expiresIn * 1e3;
|
|
16619
17416
|
while (Date.now() < deadline) {
|
|
16620
17417
|
if (opts.signal?.aborted) throw new Error("Sign-in cancelled.");
|
|
16621
|
-
await
|
|
17418
|
+
await sleep2(interval * 1e3, opts.signal);
|
|
16622
17419
|
const r = await this.pollDevice(start.deviceCode);
|
|
16623
17420
|
if (r === "slow_down") {
|
|
16624
17421
|
interval += 5;
|
|
@@ -16829,9 +17626,9 @@ var CloudClient = class _CloudClient {
|
|
|
16829
17626
|
this.store.clear();
|
|
16830
17627
|
}
|
|
16831
17628
|
};
|
|
16832
|
-
function
|
|
16833
|
-
return new Promise((
|
|
16834
|
-
const t = setTimeout(
|
|
17629
|
+
function sleep2(ms, signal) {
|
|
17630
|
+
return new Promise((resolve2, reject) => {
|
|
17631
|
+
const t = setTimeout(resolve2, ms);
|
|
16835
17632
|
signal?.addEventListener("abort", () => {
|
|
16836
17633
|
clearTimeout(t);
|
|
16837
17634
|
reject(new Error("aborted"));
|
|
@@ -18623,8 +19420,8 @@ function Repl({ config, workspacePath, themeName, initialPrompt, identityName, a
|
|
|
18623
19420
|
onExit: () => exit(),
|
|
18624
19421
|
onThemeChange: (name) => setTheme(getTheme(name)),
|
|
18625
19422
|
onExport: async (fmt) => {
|
|
18626
|
-
const
|
|
18627
|
-
const exportPath = path32.join(workspacePath, `cascade-export-${
|
|
19423
|
+
const stamp2 = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
19424
|
+
const exportPath = path32.join(workspacePath, `cascade-export-${stamp2}.${fmt === "json" ? "json" : "md"}`);
|
|
18628
19425
|
if (fmt === "json") {
|
|
18629
19426
|
await fs10.writeFile(exportPath, JSON.stringify({ sessionId: sessionIdRef.current, messages: state.messages }, null, 2), "utf-8");
|
|
18630
19427
|
} else {
|
|
@@ -19056,8 +19853,8 @@ ${lastUser.content}`;
|
|
|
19056
19853
|
conversationHistory: toConversationHistory(state.messages),
|
|
19057
19854
|
approvalCallback: async (req) => {
|
|
19058
19855
|
dispatch({ type: "SET_APPROVAL", request: req });
|
|
19059
|
-
return new Promise((
|
|
19060
|
-
approvalResolverRef.current =
|
|
19856
|
+
return new Promise((resolve2) => {
|
|
19857
|
+
approvalResolverRef.current = resolve2;
|
|
19061
19858
|
});
|
|
19062
19859
|
}
|
|
19063
19860
|
});
|
|
@@ -20323,13 +21120,13 @@ function SetupWizard({ workspacePath, onComplete }) {
|
|
|
20323
21120
|
}
|
|
20324
21121
|
async function runSetupWizard(workspacePath) {
|
|
20325
21122
|
printTelemetryBanner();
|
|
20326
|
-
return new Promise((
|
|
21123
|
+
return new Promise((resolve2) => {
|
|
20327
21124
|
const { unmount } = render(
|
|
20328
21125
|
React2.createElement(SetupWizard, {
|
|
20329
21126
|
workspacePath,
|
|
20330
21127
|
onComplete: (config) => {
|
|
20331
21128
|
unmount();
|
|
20332
|
-
|
|
21129
|
+
resolve2(config);
|
|
20333
21130
|
}
|
|
20334
21131
|
}),
|
|
20335
21132
|
{ exitOnCtrlC: true }
|
|
@@ -20797,7 +21594,7 @@ function notSignedIn() {
|
|
|
20797
21594
|
process.exitCode = 1;
|
|
20798
21595
|
}
|
|
20799
21596
|
function promptHidden(question) {
|
|
20800
|
-
return new Promise((
|
|
21597
|
+
return new Promise((resolve2) => {
|
|
20801
21598
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout, terminal: true });
|
|
20802
21599
|
let masked = false;
|
|
20803
21600
|
rl._writeToOutput = (s) => {
|
|
@@ -20806,7 +21603,7 @@ function promptHidden(question) {
|
|
|
20806
21603
|
rl.question(question, (answer) => {
|
|
20807
21604
|
rl.close();
|
|
20808
21605
|
process.stdout.write("\n");
|
|
20809
|
-
|
|
21606
|
+
resolve2(answer.trim());
|
|
20810
21607
|
});
|
|
20811
21608
|
masked = true;
|
|
20812
21609
|
});
|
|
@@ -21733,12 +22530,12 @@ var DashboardServer = class {
|
|
|
21733
22530
|
console.warn("\u26A0 Dashboard auth is DISABLED while bound to a non-loopback interface \u2014 this allows unauthenticated remote task execution.");
|
|
21734
22531
|
}
|
|
21735
22532
|
}
|
|
21736
|
-
await new Promise((
|
|
22533
|
+
await new Promise((resolve2, reject) => {
|
|
21737
22534
|
const onError = (err) => reject(err);
|
|
21738
22535
|
this.httpServer.once("error", onError);
|
|
21739
22536
|
this.httpServer.listen(this.port, this.host, () => {
|
|
21740
22537
|
this.httpServer.off("error", onError);
|
|
21741
|
-
|
|
22538
|
+
resolve2();
|
|
21742
22539
|
});
|
|
21743
22540
|
});
|
|
21744
22541
|
try {
|
|
@@ -21762,8 +22559,8 @@ var DashboardServer = class {
|
|
|
21762
22559
|
} catch {
|
|
21763
22560
|
}
|
|
21764
22561
|
this.globalStore = null;
|
|
21765
|
-
await new Promise((
|
|
21766
|
-
this.httpServer.close((err) => err ? reject(err) :
|
|
22562
|
+
await new Promise((resolve2, reject) => {
|
|
22563
|
+
this.httpServer.close((err) => err ? reject(err) : resolve2());
|
|
21767
22564
|
});
|
|
21768
22565
|
}
|
|
21769
22566
|
getSocket() {
|
|
@@ -21960,8 +22757,8 @@ var DashboardServer = class {
|
|
|
21960
22757
|
* timeout denies it.
|
|
21961
22758
|
*/
|
|
21962
22759
|
makeApprovalCallback(sessionId) {
|
|
21963
|
-
return (request) => new Promise((
|
|
21964
|
-
this.pendingApprovals.set(request.id, { resolve, sessionId });
|
|
22760
|
+
return (request) => new Promise((resolve2) => {
|
|
22761
|
+
this.pendingApprovals.set(request.id, { resolve: resolve2, sessionId });
|
|
21965
22762
|
});
|
|
21966
22763
|
}
|
|
21967
22764
|
/**
|