cascade-ai 0.49.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 +1007 -113
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +1007 -113
- package/dist/cli.js.map +1 -1
- package/dist/desktop-core.cjs +1214 -327
- package/dist/index.cjs +991 -102
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +133 -0
- package/dist/index.d.ts +133 -0
- package/dist/index.js +991 -102
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1845,7 +1845,7 @@ function reconcilePrice(model, live) {
|
|
|
1845
1845
|
}
|
|
1846
1846
|
|
|
1847
1847
|
// src/constants.ts
|
|
1848
|
-
var CASCADE_VERSION = "0.
|
|
1848
|
+
var CASCADE_VERSION = "0.51.0";
|
|
1849
1849
|
var CASCADE_CONFIG_DIR = ".cascade";
|
|
1850
1850
|
var CASCADE_MD_FILE = "CASCADE.md";
|
|
1851
1851
|
var CASCADE_IGNORE_FILE = ".cascadeignore";
|
|
@@ -2358,46 +2358,37 @@ var BaseProvider = class {
|
|
|
2358
2358
|
};
|
|
2359
2359
|
|
|
2360
2360
|
// src/providers/model-filter.ts
|
|
2361
|
-
var
|
|
2362
|
-
"embed",
|
|
2361
|
+
var MODALITY_PATTERNS = [
|
|
2362
|
+
["embedding", ["embed"]],
|
|
2363
2363
|
// text-embedding-3, nomic-embed, mxbai-embed, snowflake-arctic-embed
|
|
2364
|
-
"(?:^|[-_/])
|
|
2365
|
-
"text-to-speech",
|
|
2366
|
-
"
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
"
|
|
2378
|
-
"
|
|
2379
|
-
"
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
"veo",
|
|
2390
|
-
"sora",
|
|
2391
|
-
"(?:^|[-_/])video(?:[-_/]|$)",
|
|
2392
|
-
// video
|
|
2393
|
-
"(?:^|[-_/])aqa(?:[-_/]|$)",
|
|
2394
|
-
// attributed QA
|
|
2395
|
-
"(?:^|[-_/])live(?:[-_/]|$)",
|
|
2396
|
-
// live-audio variants
|
|
2397
|
-
"(?:^|[-_/])(?:davinci|babbage)(?:[-_/]|$)"
|
|
2398
|
-
// legacy completion-only base models
|
|
2364
|
+
["transcription", ["whisper", "transcrib", "(?:^|[-_/])stt(?:[-_/]|$)"]],
|
|
2365
|
+
["speech", ["(?:^|[-_/])tts(?:[-_/]|$)", "text-to-speech", "speech"]],
|
|
2366
|
+
["video", ["veo", "sora", "(?:^|[-_/])video(?:[-_/]|$)"]],
|
|
2367
|
+
["image", [
|
|
2368
|
+
"dall[-]?e",
|
|
2369
|
+
"imagen",
|
|
2370
|
+
"image-generation",
|
|
2371
|
+
"gpt-image",
|
|
2372
|
+
"(?:^|[-_/])image(?:[-_/]|$)",
|
|
2373
|
+
"stable-diffusion",
|
|
2374
|
+
"sdxl",
|
|
2375
|
+
"flux"
|
|
2376
|
+
]],
|
|
2377
|
+
["moderation", ["moderation", "(?:^|[-_/])guard(?:[-_/]|$)"]],
|
|
2378
|
+
["rerank", ["rerank"]],
|
|
2379
|
+
["realtime", [
|
|
2380
|
+
"(?:^|[-_/])audio(?:[-_/]|$)",
|
|
2381
|
+
"native-audio",
|
|
2382
|
+
"realtime",
|
|
2383
|
+
"(?:^|[-_/])live(?:[-_/]|$)",
|
|
2384
|
+
// live-audio variants
|
|
2385
|
+
"(?:^|[-_/])aqa(?:[-_/]|$)"
|
|
2386
|
+
// attributed QA
|
|
2387
|
+
]],
|
|
2388
|
+
["legacy-completion", ["(?:^|[-_/])(?:davinci|babbage)(?:[-_/]|$)"]]
|
|
2399
2389
|
];
|
|
2400
|
-
|
|
2390
|
+
MODALITY_PATTERNS.map(([m, pats]) => [m, new RegExp(pats.join("|"), "i")]);
|
|
2391
|
+
var NON_CHAT = new RegExp(MODALITY_PATTERNS.flatMap(([, p]) => p).join("|"), "i");
|
|
2401
2392
|
function isChatModel(id, methods) {
|
|
2402
2393
|
if (methods && methods.length > 0) {
|
|
2403
2394
|
const canGenerate = methods.some((m) => /generatecontent|generatemessage|chat|completion/i.test(m));
|
|
@@ -2925,6 +2916,115 @@ var AzureOpenAIProvider = class extends OpenAIProvider {
|
|
|
2925
2916
|
}
|
|
2926
2917
|
}
|
|
2927
2918
|
};
|
|
2919
|
+
|
|
2920
|
+
// src/providers/gemini-schema.ts
|
|
2921
|
+
var ALLOWED_KEYS = /* @__PURE__ */ new Set([
|
|
2922
|
+
"type",
|
|
2923
|
+
"format",
|
|
2924
|
+
"title",
|
|
2925
|
+
"description",
|
|
2926
|
+
"nullable",
|
|
2927
|
+
"enum",
|
|
2928
|
+
"items",
|
|
2929
|
+
"properties",
|
|
2930
|
+
"required",
|
|
2931
|
+
"propertyOrdering",
|
|
2932
|
+
"minItems",
|
|
2933
|
+
"maxItems",
|
|
2934
|
+
"minProperties",
|
|
2935
|
+
"maxProperties",
|
|
2936
|
+
"minimum",
|
|
2937
|
+
"maximum",
|
|
2938
|
+
"minLength",
|
|
2939
|
+
"maxLength",
|
|
2940
|
+
"pattern",
|
|
2941
|
+
"example",
|
|
2942
|
+
"default",
|
|
2943
|
+
"anyOf"
|
|
2944
|
+
]);
|
|
2945
|
+
var ALLOWED_TYPES = /* @__PURE__ */ new Set(["string", "number", "integer", "boolean", "array", "object"]);
|
|
2946
|
+
function isPlainObject(v) {
|
|
2947
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
2948
|
+
}
|
|
2949
|
+
function normalizeType(value) {
|
|
2950
|
+
if (typeof value === "string") {
|
|
2951
|
+
return ALLOWED_TYPES.has(value) ? { type: value } : {};
|
|
2952
|
+
}
|
|
2953
|
+
if (Array.isArray(value)) {
|
|
2954
|
+
const types = value.filter((t) => typeof t === "string");
|
|
2955
|
+
const nullable = types.includes("null");
|
|
2956
|
+
const concrete = types.find((t) => t !== "null" && ALLOWED_TYPES.has(t));
|
|
2957
|
+
return {
|
|
2958
|
+
...concrete ? { type: concrete } : {},
|
|
2959
|
+
...nullable ? { nullable: true } : {}
|
|
2960
|
+
};
|
|
2961
|
+
}
|
|
2962
|
+
return {};
|
|
2963
|
+
}
|
|
2964
|
+
function normalizeEnum(value) {
|
|
2965
|
+
if (!Array.isArray(value) || value.length === 0) return void 0;
|
|
2966
|
+
return value.map((v) => typeof v === "string" ? v : JSON.stringify(v));
|
|
2967
|
+
}
|
|
2968
|
+
function sanitizeGeminiSchema(schema) {
|
|
2969
|
+
if (!isPlainObject(schema)) return void 0;
|
|
2970
|
+
const out = {};
|
|
2971
|
+
for (const [key, value] of Object.entries(schema)) {
|
|
2972
|
+
if (!ALLOWED_KEYS.has(key)) continue;
|
|
2973
|
+
switch (key) {
|
|
2974
|
+
case "type": {
|
|
2975
|
+
Object.assign(out, normalizeType(value));
|
|
2976
|
+
break;
|
|
2977
|
+
}
|
|
2978
|
+
case "enum": {
|
|
2979
|
+
const e = normalizeEnum(value);
|
|
2980
|
+
if (e) out["enum"] = e;
|
|
2981
|
+
break;
|
|
2982
|
+
}
|
|
2983
|
+
case "properties": {
|
|
2984
|
+
if (!isPlainObject(value)) break;
|
|
2985
|
+
const props = {};
|
|
2986
|
+
for (const [propName, propSchema] of Object.entries(value)) {
|
|
2987
|
+
const cleaned = sanitizeGeminiSchema(propSchema);
|
|
2988
|
+
props[propName] = cleaned && Object.keys(cleaned).length ? cleaned : { type: "string" };
|
|
2989
|
+
}
|
|
2990
|
+
out["properties"] = props;
|
|
2991
|
+
break;
|
|
2992
|
+
}
|
|
2993
|
+
case "items": {
|
|
2994
|
+
const cleaned = sanitizeGeminiSchema(value);
|
|
2995
|
+
if (cleaned) out["items"] = cleaned;
|
|
2996
|
+
break;
|
|
2997
|
+
}
|
|
2998
|
+
case "anyOf": {
|
|
2999
|
+
if (!Array.isArray(value)) break;
|
|
3000
|
+
const variants = value.map((v) => sanitizeGeminiSchema(v)).filter((v) => !!v && Object.keys(v).length > 0);
|
|
3001
|
+
if (variants.length) out["anyOf"] = variants;
|
|
3002
|
+
break;
|
|
3003
|
+
}
|
|
3004
|
+
case "required": {
|
|
3005
|
+
if (Array.isArray(value)) {
|
|
3006
|
+
out["required"] = value.filter((r) => typeof r === "string");
|
|
3007
|
+
}
|
|
3008
|
+
break;
|
|
3009
|
+
}
|
|
3010
|
+
default:
|
|
3011
|
+
out[key] = value;
|
|
3012
|
+
}
|
|
3013
|
+
}
|
|
3014
|
+
if (!out["type"] && out["properties"]) out["type"] = "object";
|
|
3015
|
+
if (!out["type"] && out["items"]) out["type"] = "array";
|
|
3016
|
+
return out;
|
|
3017
|
+
}
|
|
3018
|
+
function toGeminiParameters(inputSchema) {
|
|
3019
|
+
const cleaned = sanitizeGeminiSchema(inputSchema);
|
|
3020
|
+
if (!cleaned) return void 0;
|
|
3021
|
+
const props = cleaned["properties"];
|
|
3022
|
+
if (isPlainObject(props) && Object.keys(props).length === 0) return void 0;
|
|
3023
|
+
if (Object.keys(cleaned).length === 0) return void 0;
|
|
3024
|
+
return cleaned;
|
|
3025
|
+
}
|
|
3026
|
+
|
|
3027
|
+
// src/providers/gemini.ts
|
|
2928
3028
|
var GeminiProvider = class extends BaseProvider {
|
|
2929
3029
|
client;
|
|
2930
3030
|
constructor(config, model) {
|
|
@@ -3145,7 +3245,12 @@ var GeminiProvider = class extends BaseProvider {
|
|
|
3145
3245
|
return {
|
|
3146
3246
|
name: tool.name,
|
|
3147
3247
|
description: tool.description,
|
|
3148
|
-
parameters
|
|
3248
|
+
// Convert, never cast. Gemini's parameters field is an OpenAPI subset
|
|
3249
|
+
// that 400s on unknown keys, and MCP servers ship schemas carrying their
|
|
3250
|
+
// own extensions (GitHub's adds `x-mcp-header` to every header-bound
|
|
3251
|
+
// property). Casting sent those through verbatim and every request failed
|
|
3252
|
+
// before the model saw it. See providers/gemini-schema.ts.
|
|
3253
|
+
parameters: toGeminiParameters(tool.inputSchema)
|
|
3149
3254
|
};
|
|
3150
3255
|
}
|
|
3151
3256
|
};
|
|
@@ -3173,7 +3278,7 @@ async function nodeHttpFetch(input, init = {}, redirectCount = 0) {
|
|
|
3173
3278
|
headers["accept-encoding"] = "gzip, deflate, br";
|
|
3174
3279
|
}
|
|
3175
3280
|
const body = init.body == null ? void 0 : typeof init.body === "string" ? init.body : Buffer.from(init.body);
|
|
3176
|
-
return new Promise((
|
|
3281
|
+
return new Promise((resolve2, reject) => {
|
|
3177
3282
|
const req = lib.request(
|
|
3178
3283
|
{
|
|
3179
3284
|
hostname: u.hostname,
|
|
@@ -3190,7 +3295,7 @@ async function nodeHttpFetch(input, init = {}, redirectCount = 0) {
|
|
|
3190
3295
|
const nextUrl = new URL(location, u).href;
|
|
3191
3296
|
const downgrade = status === 303 || (status === 301 || status === 302) && method !== "GET" && method !== "HEAD";
|
|
3192
3297
|
const nextInit = downgrade ? { ...init, method: "GET", body: void 0 } : init;
|
|
3193
|
-
|
|
3298
|
+
resolve2(nodeHttpFetch(nextUrl, nextInit, redirectCount + 1));
|
|
3194
3299
|
return;
|
|
3195
3300
|
}
|
|
3196
3301
|
const encoding = (res.headers["content-encoding"] ?? "").toLowerCase();
|
|
@@ -3205,7 +3310,7 @@ async function nodeHttpFetch(input, init = {}, redirectCount = 0) {
|
|
|
3205
3310
|
if (Array.isArray(v)) respHeaders.set(k, v.join(", "));
|
|
3206
3311
|
else if (typeof v === "string") respHeaders.set(k, v);
|
|
3207
3312
|
}
|
|
3208
|
-
|
|
3313
|
+
resolve2(new Response(stream, {
|
|
3209
3314
|
status,
|
|
3210
3315
|
statusText: res.statusMessage ?? "",
|
|
3211
3316
|
headers: respHeaders
|
|
@@ -3850,7 +3955,7 @@ var TpmLimiter = class {
|
|
|
3850
3955
|
}
|
|
3851
3956
|
const deficit = want - bucket.available;
|
|
3852
3957
|
const waitMs = Math.max(50, Math.ceil(deficit / bucket.tokensPerMinute * 6e4));
|
|
3853
|
-
await new Promise((
|
|
3958
|
+
await new Promise((resolve2) => setTimeout(resolve2, waitMs));
|
|
3854
3959
|
}
|
|
3855
3960
|
}
|
|
3856
3961
|
/**
|
|
@@ -3913,14 +4018,14 @@ var LocalRequestQueue = class {
|
|
|
3913
4018
|
this.active++;
|
|
3914
4019
|
return this.makeRelease();
|
|
3915
4020
|
}
|
|
3916
|
-
return new Promise((
|
|
4021
|
+
return new Promise((resolve2, reject) => {
|
|
3917
4022
|
let settled = false;
|
|
3918
4023
|
let timer;
|
|
3919
4024
|
const resolver = (release) => {
|
|
3920
4025
|
if (settled) return;
|
|
3921
4026
|
settled = true;
|
|
3922
4027
|
if (timer !== void 0) clearTimeout(timer);
|
|
3923
|
-
|
|
4028
|
+
resolve2(release);
|
|
3924
4029
|
};
|
|
3925
4030
|
if (timeoutMs !== void 0 && timeoutMs > 0) {
|
|
3926
4031
|
timer = setTimeout(() => {
|
|
@@ -4551,6 +4656,7 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter {
|
|
|
4551
4656
|
tpmLimiter;
|
|
4552
4657
|
localQueue;
|
|
4553
4658
|
taskAnalyzer;
|
|
4659
|
+
pendingFeedbackSource;
|
|
4554
4660
|
worldStateDB;
|
|
4555
4661
|
privacyPaths;
|
|
4556
4662
|
guidanceQueue;
|
|
@@ -5005,8 +5111,18 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter {
|
|
|
5005
5111
|
return windows.length ? Math.min(...windows) : void 0;
|
|
5006
5112
|
}
|
|
5007
5113
|
/** Wire the Cascade Auto task analyzer used for per-subtask model routing. */
|
|
5114
|
+
/**
|
|
5115
|
+
* Feed per-model thumbs counts into Auto routing. Forwarded to the analyzer
|
|
5116
|
+
* because that is where the public benchmark score is combined; the prior
|
|
5117
|
+
* adjusts that number rather than sitting anywhere else in the pipeline.
|
|
5118
|
+
*/
|
|
5119
|
+
setFeedbackSource(source) {
|
|
5120
|
+
this.taskAnalyzer?.setFeedbackSource(source);
|
|
5121
|
+
this.pendingFeedbackSource = source;
|
|
5122
|
+
}
|
|
5008
5123
|
setTaskAnalyzer(analyzer) {
|
|
5009
5124
|
this.taskAnalyzer = analyzer;
|
|
5125
|
+
if (this.pendingFeedbackSource) analyzer.setFeedbackSource(this.pendingFeedbackSource);
|
|
5010
5126
|
}
|
|
5011
5127
|
setWorldStateDB(db) {
|
|
5012
5128
|
this.worldStateDB = db;
|
|
@@ -5461,6 +5577,14 @@ var BaseTier = class extends EventEmitter {
|
|
|
5461
5577
|
this.emit("tier:status", event);
|
|
5462
5578
|
}
|
|
5463
5579
|
/** Record the model serving this tier; future status events carry it. */
|
|
5580
|
+
/**
|
|
5581
|
+
* The model actually serving this tier ("provider:id"), once one has been
|
|
5582
|
+
* chosen. The run breaker reads it to attribute a failure to a model rather
|
|
5583
|
+
* than to the subtask that happened to hit it.
|
|
5584
|
+
*/
|
|
5585
|
+
getServingModel() {
|
|
5586
|
+
return this.servingModel;
|
|
5587
|
+
}
|
|
5464
5588
|
setServingModel(model) {
|
|
5465
5589
|
this.servingModel = model || void 0;
|
|
5466
5590
|
}
|
|
@@ -6092,8 +6216,11 @@ Now execute your subtask using this context where relevant.`
|
|
|
6092
6216
|
const result = await this.runAgentLoop(systemPrompt, this.tools);
|
|
6093
6217
|
output = result.output;
|
|
6094
6218
|
toolCalls = result.toolCalls;
|
|
6095
|
-
this.
|
|
6096
|
-
|
|
6219
|
+
const mustProduceArtifact = this.requiresArtifact();
|
|
6220
|
+
if (mustProduceArtifact) {
|
|
6221
|
+
this.sendStatusUpdate({ progressPct: 65, currentAction: "Verifying required artifacts", status: "IN_PROGRESS" });
|
|
6222
|
+
}
|
|
6223
|
+
const artifactCheck = mustProduceArtifact ? await this.verifyArtifacts(assignment) : { ok: true, issues: [] };
|
|
6097
6224
|
if (!artifactCheck.ok) {
|
|
6098
6225
|
correctionAttempts = 1;
|
|
6099
6226
|
issues.push(...artifactCheck.issues);
|
|
@@ -6373,11 +6500,11 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : "") + textToolSuffix,
|
|
|
6373
6500
|
if (!wasApproved) return `Tool ${tc.name} was denied by user.`;
|
|
6374
6501
|
} else {
|
|
6375
6502
|
const LEGACY_APPROVAL_TIMEOUT_MS = 6e5;
|
|
6376
|
-
const legacyDecision = await new Promise((
|
|
6503
|
+
const legacyDecision = await new Promise((resolve2) => {
|
|
6377
6504
|
const eventName = `tool:approval-response:${this.id}-${tc.id}`;
|
|
6378
6505
|
const timer = setTimeout(() => {
|
|
6379
6506
|
this.removeAllListeners(eventName);
|
|
6380
|
-
|
|
6507
|
+
resolve2({ approved: false });
|
|
6381
6508
|
}, LEGACY_APPROVAL_TIMEOUT_MS);
|
|
6382
6509
|
timer.unref?.();
|
|
6383
6510
|
this.emit("tool:approval-request", {
|
|
@@ -6390,7 +6517,7 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : "") + textToolSuffix,
|
|
|
6390
6517
|
});
|
|
6391
6518
|
this.once(eventName, (d) => {
|
|
6392
6519
|
clearTimeout(timer);
|
|
6393
|
-
|
|
6520
|
+
resolve2(d);
|
|
6394
6521
|
});
|
|
6395
6522
|
});
|
|
6396
6523
|
if (legacyDecision.always) this.sessionApprovals.set(tc.name, legacyDecision.approved);
|
|
@@ -6410,6 +6537,9 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : "") + textToolSuffix,
|
|
|
6410
6537
|
tierId: this.id,
|
|
6411
6538
|
sessionId: this.taskId,
|
|
6412
6539
|
requireApproval: false,
|
|
6540
|
+
// Media generation can run for a minute; without this a cancelled run
|
|
6541
|
+
// still pays for an image nobody will see.
|
|
6542
|
+
...this.signal ? { signal: this.signal } : {},
|
|
6413
6543
|
saveSnapshot: async (path26, content) => {
|
|
6414
6544
|
this.store?.addFileSnapshot(this.taskId, path26, content);
|
|
6415
6545
|
},
|
|
@@ -6457,7 +6587,8 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : "") + textToolSuffix,
|
|
|
6457
6587
|
const result = await this.toolRegistry.execute(altTool, tc.input, {
|
|
6458
6588
|
tierId: this.id,
|
|
6459
6589
|
sessionId: this.taskId,
|
|
6460
|
-
requireApproval: false
|
|
6590
|
+
requireApproval: false,
|
|
6591
|
+
...this.signal ? { signal: this.signal } : {}
|
|
6461
6592
|
});
|
|
6462
6593
|
const str = typeof result === "string" ? result : JSON.stringify(result);
|
|
6463
6594
|
if (!str.startsWith("Tool error:") && !str.startsWith("Error:")) {
|
|
@@ -6479,7 +6610,8 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : "") + textToolSuffix,
|
|
|
6479
6610
|
const result = await this.toolRegistry.execute(newToolName, tc.input, {
|
|
6480
6611
|
tierId: this.id,
|
|
6481
6612
|
sessionId: this.taskId,
|
|
6482
|
-
requireApproval: false
|
|
6613
|
+
requireApproval: false,
|
|
6614
|
+
...this.signal ? { signal: this.signal } : {}
|
|
6483
6615
|
});
|
|
6484
6616
|
const str = typeof result === "string" ? result : JSON.stringify(result);
|
|
6485
6617
|
if (!str.startsWith("Tool error:")) return `[Synthesized ${newToolName}]: ${str}`;
|
|
@@ -6897,7 +7029,7 @@ var PeerBus = class extends EventEmitter {
|
|
|
6897
7029
|
this.outputs.set(subtaskId, entry);
|
|
6898
7030
|
this.emit("output:ready", entry);
|
|
6899
7031
|
const waiting = this.waiters.get(subtaskId) ?? [];
|
|
6900
|
-
for (const
|
|
7032
|
+
for (const resolve2 of waiting) resolve2(entry);
|
|
6901
7033
|
this.waiters.delete(subtaskId);
|
|
6902
7034
|
}
|
|
6903
7035
|
/**
|
|
@@ -6928,10 +7060,10 @@ var PeerBus = class extends EventEmitter {
|
|
|
6928
7060
|
waitFor(subtaskId, timeoutMs = 12e4) {
|
|
6929
7061
|
const existing = this.outputs.get(subtaskId);
|
|
6930
7062
|
if (existing && !this.retryPending.has(subtaskId)) return Promise.resolve(existing);
|
|
6931
|
-
return new Promise((
|
|
7063
|
+
return new Promise((resolve2, reject) => {
|
|
6932
7064
|
const resolver = (output) => {
|
|
6933
7065
|
clearTimeout(timer);
|
|
6934
|
-
|
|
7066
|
+
resolve2(output);
|
|
6935
7067
|
};
|
|
6936
7068
|
const timer = setTimeout(() => {
|
|
6937
7069
|
const waiting = this.waiters.get(subtaskId);
|
|
@@ -6985,10 +7117,10 @@ var PeerBus = class extends EventEmitter {
|
|
|
6985
7117
|
*/
|
|
6986
7118
|
collect(timeoutMs) {
|
|
6987
7119
|
const collected = [...this.broadcastLog];
|
|
6988
|
-
return new Promise((
|
|
7120
|
+
return new Promise((resolve2) => {
|
|
6989
7121
|
const timer = setTimeout(() => {
|
|
6990
7122
|
off();
|
|
6991
|
-
|
|
7123
|
+
resolve2(collected);
|
|
6992
7124
|
}, timeoutMs);
|
|
6993
7125
|
const handler = (msg) => {
|
|
6994
7126
|
collected.push({ fromId: msg.fromId, payload: msg.payload, timestamp: msg.timestamp });
|
|
@@ -7002,7 +7134,7 @@ var PeerBus = class extends EventEmitter {
|
|
|
7002
7134
|
const broadcasters = new Set(collected.map((m) => m.fromId));
|
|
7003
7135
|
if (broadcasters.size >= this.members.size) {
|
|
7004
7136
|
off();
|
|
7005
|
-
|
|
7137
|
+
resolve2(collected);
|
|
7006
7138
|
}
|
|
7007
7139
|
};
|
|
7008
7140
|
this.on("broadcast", checkComplete);
|
|
@@ -7020,7 +7152,7 @@ var PeerBus = class extends EventEmitter {
|
|
|
7020
7152
|
return;
|
|
7021
7153
|
}
|
|
7022
7154
|
this.emitCoordination(tierId, `\u23F3 waiting for ${filePath} (held by ${existing.holderId})`);
|
|
7023
|
-
return new Promise((
|
|
7155
|
+
return new Promise((resolve2, reject) => {
|
|
7024
7156
|
const timer = setTimeout(() => {
|
|
7025
7157
|
reject(new Error(`File lock timeout for ${filePath} (held by ${existing.holderId})`));
|
|
7026
7158
|
}, timeoutMs);
|
|
@@ -7028,7 +7160,7 @@ var PeerBus = class extends EventEmitter {
|
|
|
7028
7160
|
clearTimeout(timer);
|
|
7029
7161
|
this.fileLocks.set(filePath, { holderId: tierId, lockedAt: (/* @__PURE__ */ new Date()).toISOString(), waiters: [] });
|
|
7030
7162
|
this.emitCoordination(tierId, `\u{1F512} locked ${filePath}`);
|
|
7031
|
-
|
|
7163
|
+
resolve2();
|
|
7032
7164
|
});
|
|
7033
7165
|
});
|
|
7034
7166
|
}
|
|
@@ -7053,11 +7185,11 @@ var PeerBus = class extends EventEmitter {
|
|
|
7053
7185
|
*/
|
|
7054
7186
|
waitForFileRelease(filePath, timeoutMs = 3e4) {
|
|
7055
7187
|
if (!this.fileLocks.has(filePath)) return Promise.resolve();
|
|
7056
|
-
return new Promise((
|
|
7188
|
+
return new Promise((resolve2, reject) => {
|
|
7057
7189
|
const timer = setTimeout(() => reject(new Error(`Timeout waiting for file release: ${filePath}`)), timeoutMs);
|
|
7058
7190
|
this.once(`file:released:${filePath}`, () => {
|
|
7059
7191
|
clearTimeout(timer);
|
|
7060
|
-
|
|
7192
|
+
resolve2();
|
|
7061
7193
|
});
|
|
7062
7194
|
});
|
|
7063
7195
|
}
|
|
@@ -7123,13 +7255,13 @@ var PeerBus = class extends EventEmitter {
|
|
|
7123
7255
|
this.emit(`barrier:${barrierName}`);
|
|
7124
7256
|
return;
|
|
7125
7257
|
}
|
|
7126
|
-
return new Promise((
|
|
7258
|
+
return new Promise((resolve2, reject) => {
|
|
7127
7259
|
const timer = setTimeout(() => {
|
|
7128
7260
|
reject(new Error(`Barrier timeout: ${barrierName} (${bar.arrived.size}/${bar.total} arrived)`));
|
|
7129
7261
|
}, 12e4);
|
|
7130
7262
|
this.once(`barrier:${barrierName}`, () => {
|
|
7131
7263
|
clearTimeout(timer);
|
|
7132
|
-
|
|
7264
|
+
resolve2();
|
|
7133
7265
|
});
|
|
7134
7266
|
});
|
|
7135
7267
|
}
|
|
@@ -7196,6 +7328,7 @@ var T2Manager = class extends BaseTier {
|
|
|
7196
7328
|
escalations = [];
|
|
7197
7329
|
peerSyncBuffer = [];
|
|
7198
7330
|
store;
|
|
7331
|
+
runBreaker;
|
|
7199
7332
|
t3PeerBus = new PeerBus();
|
|
7200
7333
|
// ← T3↔T3 bus (local to this T2)
|
|
7201
7334
|
t2PeerBus;
|
|
@@ -7226,6 +7359,15 @@ var T2Manager = class extends BaseTier {
|
|
|
7226
7359
|
this.router = router;
|
|
7227
7360
|
this.toolRegistry = toolRegistry;
|
|
7228
7361
|
}
|
|
7362
|
+
/**
|
|
7363
|
+
* Share the run-wide circuit breaker. Every T2 in a run gets the SAME
|
|
7364
|
+
* instance: three sections each failing once against a dead model is the
|
|
7365
|
+
* same fact as one section failing three times, and a per-section breaker
|
|
7366
|
+
* would never see it.
|
|
7367
|
+
*/
|
|
7368
|
+
setRunBreaker(breaker) {
|
|
7369
|
+
this.runBreaker = breaker;
|
|
7370
|
+
}
|
|
7229
7371
|
setStore(store) {
|
|
7230
7372
|
this.store = store;
|
|
7231
7373
|
}
|
|
@@ -7349,7 +7491,8 @@ Guidance (must be followed): ${decision.note}`
|
|
|
7349
7491
|
const overallStatus = this.determineStatus(t3Results);
|
|
7350
7492
|
const isOk = overallStatus === "COMPLETED" || overallStatus === "PARTIAL";
|
|
7351
7493
|
this.setStatus(isOk ? "COMPLETED" : "FAILED", summary);
|
|
7352
|
-
|
|
7494
|
+
const sectionAction = overallStatus === "COMPLETED" ? "Section complete" : overallStatus === "PARTIAL" ? "Section partially complete \u2014 some subtasks failed" : overallStatus === "ESCALATED" ? "Section escalated \u2014 needs a decision" : "Section failed \u2014 no subtask completed";
|
|
7495
|
+
this.sendStatusUpdate({ progressPct: 100, currentAction: sectionAction, status: "IN_PROGRESS", output: summary });
|
|
7353
7496
|
const result = {
|
|
7354
7497
|
sectionId: assignment.sectionId,
|
|
7355
7498
|
sectionTitle: assignment.sectionTitle,
|
|
@@ -7519,6 +7662,24 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
|
7519
7662
|
const reinforceCfg = this.router.getReinforcementsConfig?.() ?? { enabled: false, maxPerSection: 4 };
|
|
7520
7663
|
let reinforcementsAdded = 0;
|
|
7521
7664
|
while (remaining.size > 0) {
|
|
7665
|
+
if (this.runBreaker?.isOpen()) {
|
|
7666
|
+
const why = this.runBreaker.skipMessage();
|
|
7667
|
+
this.log(`Run breaker open \u2014 skipping ${remaining.size} remaining subtask(s)`);
|
|
7668
|
+
for (const id of remaining) {
|
|
7669
|
+
this.t3PeerBus.publish(this.id, id, why, "FAILED");
|
|
7670
|
+
resultMap.set(id, {
|
|
7671
|
+
subtaskId: id,
|
|
7672
|
+
status: "FAILED",
|
|
7673
|
+
output: why,
|
|
7674
|
+
testResults: { checksRun: [], passed: [], failed: [] },
|
|
7675
|
+
issues: [why],
|
|
7676
|
+
peerSyncsUsed: [],
|
|
7677
|
+
correctionAttempts: 0
|
|
7678
|
+
});
|
|
7679
|
+
}
|
|
7680
|
+
remaining.clear();
|
|
7681
|
+
break;
|
|
7682
|
+
}
|
|
7522
7683
|
const runnableIds = [...remaining].filter((id) => (inDegree.get(id) ?? 0) === 0);
|
|
7523
7684
|
if (runnableIds.length === 0) {
|
|
7524
7685
|
const fallbackId = [...remaining].sort(
|
|
@@ -7621,8 +7782,32 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
|
7621
7782
|
remaining.delete(id);
|
|
7622
7783
|
const r = waveResults[i];
|
|
7623
7784
|
if (r.status === "rejected") {
|
|
7624
|
-
this.log(`T3 worker ${id} failed: ${r.reason instanceof Error ? r.reason.message : String(r.reason)} \u2014 retrying once`);
|
|
7625
7785
|
const assignment = sanitizedAssignments.find((a) => a.subtaskId === id);
|
|
7786
|
+
const failedModel = workerMap.get(id)?.getServingModel();
|
|
7787
|
+
const tripped = this.runBreaker?.record(r.reason, failedModel);
|
|
7788
|
+
if (tripped) {
|
|
7789
|
+
this.log(`Run breaker opened: ${tripped.failures} consecutive ${tripped.kind} failures on ${tripped.modelId} \u2014 stopping`);
|
|
7790
|
+
this.emit("run:breaker", tripped);
|
|
7791
|
+
}
|
|
7792
|
+
if (this.runBreaker?.isOpen()) {
|
|
7793
|
+
const why = this.runBreaker.skipMessage();
|
|
7794
|
+
this.log(`T3 worker ${id} failed and the run breaker is open \u2014 not retrying`);
|
|
7795
|
+
this.t3PeerBus.publish(this.id, id, why, "FAILED");
|
|
7796
|
+
resultMap.set(id, {
|
|
7797
|
+
subtaskId: id,
|
|
7798
|
+
status: "FAILED",
|
|
7799
|
+
output: why,
|
|
7800
|
+
testResults: { checksRun: [], passed: [], failed: [] },
|
|
7801
|
+
issues: [why],
|
|
7802
|
+
peerSyncsUsed: [],
|
|
7803
|
+
correctionAttempts: 0
|
|
7804
|
+
});
|
|
7805
|
+
for (const dependent of adj.get(id) ?? []) {
|
|
7806
|
+
inDegree.set(dependent, Math.max(0, (inDegree.get(dependent) ?? 0) - 1));
|
|
7807
|
+
}
|
|
7808
|
+
continue;
|
|
7809
|
+
}
|
|
7810
|
+
this.log(`T3 worker ${id} failed: ${r.reason instanceof Error ? r.reason.message : String(r.reason)} \u2014 retrying once`);
|
|
7626
7811
|
try {
|
|
7627
7812
|
const retried = await this.retryT3(assignment, taskId);
|
|
7628
7813
|
resultMap.set(id, retried);
|
|
@@ -7954,6 +8139,7 @@ var T1Administrator = class extends BaseTier {
|
|
|
7954
8139
|
t2Managers = /* @__PURE__ */ new Map();
|
|
7955
8140
|
escalations = [];
|
|
7956
8141
|
store;
|
|
8142
|
+
runBreaker;
|
|
7957
8143
|
t2PeerBus = new PeerBus();
|
|
7958
8144
|
permissionEscalator;
|
|
7959
8145
|
toolCreator;
|
|
@@ -7968,6 +8154,10 @@ var T1Administrator = class extends BaseTier {
|
|
|
7968
8154
|
this.toolRegistry = toolRegistry;
|
|
7969
8155
|
this.config = config;
|
|
7970
8156
|
}
|
|
8157
|
+
/** Share the run-wide circuit breaker with every section manager. */
|
|
8158
|
+
setRunBreaker(breaker) {
|
|
8159
|
+
this.runBreaker = breaker;
|
|
8160
|
+
}
|
|
7971
8161
|
setStore(store) {
|
|
7972
8162
|
this.store = store;
|
|
7973
8163
|
}
|
|
@@ -8330,6 +8520,7 @@ SPEC RULES \u2014 each subtask is a self-contained spec slice (workers execute f
|
|
|
8330
8520
|
};
|
|
8331
8521
|
const managers = sections.map((section) => {
|
|
8332
8522
|
const manager = new T2Manager(this.router, this.toolRegistry, this.id);
|
|
8523
|
+
if (this.runBreaker) manager.setRunBreaker(this.runBreaker);
|
|
8333
8524
|
manager.setHierarchyContext(`You are a T2 Manager for the section "${section.sectionTitle}". You are part of a COMPLEX task overseen by T1 Administrator.`);
|
|
8334
8525
|
if (this.store) {
|
|
8335
8526
|
manager.setStore(this.store);
|
|
@@ -8615,6 +8806,156 @@ Reply with exactly one word: YES, NO, or UNSURE.
|
|
|
8615
8806
|
}
|
|
8616
8807
|
};
|
|
8617
8808
|
|
|
8809
|
+
// src/core/multimodal/registry.ts
|
|
8810
|
+
var CAPABILITIES = [
|
|
8811
|
+
// ── Image ──
|
|
8812
|
+
{
|
|
8813
|
+
modality: "image",
|
|
8814
|
+
provider: "openai",
|
|
8815
|
+
modelId: "dall-e-3",
|
|
8816
|
+
api: "openai-images"
|
|
8817
|
+
},
|
|
8818
|
+
{
|
|
8819
|
+
modality: "image",
|
|
8820
|
+
provider: "gemini",
|
|
8821
|
+
modelId: "gemini-3-pro-image",
|
|
8822
|
+
api: "gemini-predict"
|
|
8823
|
+
},
|
|
8824
|
+
// ── Speech (text → audio) ──
|
|
8825
|
+
{
|
|
8826
|
+
modality: "speech",
|
|
8827
|
+
provider: "openai",
|
|
8828
|
+
modelId: "tts-1",
|
|
8829
|
+
api: "openai-speech"
|
|
8830
|
+
},
|
|
8831
|
+
{
|
|
8832
|
+
// The only quality claim in this table that needs no leaderboard: OpenAI
|
|
8833
|
+
// ships these as the same voice at two fidelities and charges double for
|
|
8834
|
+
// this one. That is the vendor's own ordering, not an inference.
|
|
8835
|
+
modality: "speech",
|
|
8836
|
+
provider: "openai",
|
|
8837
|
+
modelId: "tts-1-hd",
|
|
8838
|
+
api: "openai-speech",
|
|
8839
|
+
quality: 70,
|
|
8840
|
+
qualityBasis: "OpenAI's own hd variant of tts-1, priced 2x"
|
|
8841
|
+
},
|
|
8842
|
+
// ── Transcription (audio → text) ──
|
|
8843
|
+
{
|
|
8844
|
+
modality: "transcription",
|
|
8845
|
+
provider: "openai",
|
|
8846
|
+
modelId: "whisper-1",
|
|
8847
|
+
api: "openai-transcriptions"
|
|
8848
|
+
},
|
|
8849
|
+
// ── Video ──
|
|
8850
|
+
{
|
|
8851
|
+
// Listed so `describe()` can tell the truth about what exists, and so the
|
|
8852
|
+
// planner is told video is out of reach rather than left to invent a tool.
|
|
8853
|
+
// Veo is a long-running operation: submit, poll, then fetch bytes from a
|
|
8854
|
+
// signed URL. That is a different execution shape from every other entry
|
|
8855
|
+
// here and is not wired up — claiming otherwise would produce a tool that
|
|
8856
|
+
// fails at call time instead of an honest "not yet".
|
|
8857
|
+
modality: "video",
|
|
8858
|
+
provider: "gemini",
|
|
8859
|
+
modelId: "veo-3.1-generate-001",
|
|
8860
|
+
api: "gemini-predict",
|
|
8861
|
+
unsupported: "Veo runs as a long-running operation (submit \u2192 poll \u2192 fetch). Not wired up yet."
|
|
8862
|
+
}
|
|
8863
|
+
];
|
|
8864
|
+
function capabilityCost(cap) {
|
|
8865
|
+
const match = findPricing(cap.modelId, cap.provider, {
|
|
8866
|
+
fallbackProviders: cap.provider === "gemini" ? ["vertex"] : []
|
|
8867
|
+
});
|
|
8868
|
+
const rate = match?.entry.rates?.[0];
|
|
8869
|
+
if (!rate) return null;
|
|
8870
|
+
const unitLabel = {
|
|
8871
|
+
per_image: "per image",
|
|
8872
|
+
per_second: "per second of video",
|
|
8873
|
+
per_character: "per character",
|
|
8874
|
+
per_audio_minute: "per audio minute"
|
|
8875
|
+
};
|
|
8876
|
+
const unit = unitLabel[rate.unit] ?? rate.unit;
|
|
8877
|
+
const amount = rate.amount;
|
|
8878
|
+
const shown = amount < 1e-3 ? amount.toExponential(2) : amount.toFixed(4);
|
|
8879
|
+
return { label: `$${shown} ${unit}`, amount, unit };
|
|
8880
|
+
}
|
|
8881
|
+
var MultimodalRegistry = class {
|
|
8882
|
+
/** Provider types the user has actually configured a key for. */
|
|
8883
|
+
available;
|
|
8884
|
+
constructor(availableProviders) {
|
|
8885
|
+
this.available = new Set(availableProviders);
|
|
8886
|
+
}
|
|
8887
|
+
/** Every capability whose provider is configured AND which Cascade can call. */
|
|
8888
|
+
usable(modality) {
|
|
8889
|
+
return CAPABILITIES.filter(
|
|
8890
|
+
(c) => !c.unsupported && this.available.has(c.provider) && (!modality || c.modality === modality)
|
|
8891
|
+
);
|
|
8892
|
+
}
|
|
8893
|
+
/** Modalities this account can actually generate right now. */
|
|
8894
|
+
availableModalities() {
|
|
8895
|
+
return [...new Set(this.usable().map((c) => c.modality))];
|
|
8896
|
+
}
|
|
8897
|
+
/**
|
|
8898
|
+
* Pick a model for a modality.
|
|
8899
|
+
*
|
|
8900
|
+
* Ranked models beat unranked ones, then higher quality, then lower unit
|
|
8901
|
+
* price. An explicit `preferModel` wins outright when it is usable — the user
|
|
8902
|
+
* naming a model is a stronger signal than any prior.
|
|
8903
|
+
*/
|
|
8904
|
+
select(modality, opts = {}) {
|
|
8905
|
+
const pool = this.usable(modality);
|
|
8906
|
+
if (!pool.length) return null;
|
|
8907
|
+
if (opts.preferModel) {
|
|
8908
|
+
const wanted = pool.find((c) => c.modelId === opts.preferModel);
|
|
8909
|
+
if (wanted) {
|
|
8910
|
+
return { capability: wanted, cost: capabilityCost(wanted), reason: `Requested explicitly: ${wanted.modelId}.` };
|
|
8911
|
+
}
|
|
8912
|
+
}
|
|
8913
|
+
const scored = pool.map((c) => ({ c, cost: capabilityCost(c) })).sort((a, b) => {
|
|
8914
|
+
const qa = a.c.quality ?? -1;
|
|
8915
|
+
const qb = b.c.quality ?? -1;
|
|
8916
|
+
if (qa !== qb) return qb - qa;
|
|
8917
|
+
const pa = a.cost?.amount ?? Number.POSITIVE_INFINITY;
|
|
8918
|
+
const pb = b.cost?.amount ?? Number.POSITIVE_INFINITY;
|
|
8919
|
+
return pa - pb;
|
|
8920
|
+
});
|
|
8921
|
+
const best = scored[0];
|
|
8922
|
+
const ranked = best.c.quality !== void 0;
|
|
8923
|
+
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.";
|
|
8924
|
+
return { capability: best.c, cost: best.cost, reason };
|
|
8925
|
+
}
|
|
8926
|
+
/**
|
|
8927
|
+
* A plain-language account of what this account can and cannot generate —
|
|
8928
|
+
* including the things it cannot, and why. The planner sees this, so being
|
|
8929
|
+
* candid here is what stops it inventing a `generate_music` call.
|
|
8930
|
+
*/
|
|
8931
|
+
describe() {
|
|
8932
|
+
const lines = [];
|
|
8933
|
+
const usable = this.usable();
|
|
8934
|
+
if (usable.length) {
|
|
8935
|
+
lines.push("Generation models available on your configured providers:");
|
|
8936
|
+
for (const modality of ["image", "speech", "transcription", "video"]) {
|
|
8937
|
+
const forModality = usable.filter((c) => c.modality === modality);
|
|
8938
|
+
if (!forModality.length) continue;
|
|
8939
|
+
const shown = forModality.map((c) => {
|
|
8940
|
+
const cost = capabilityCost(c);
|
|
8941
|
+
return `${c.modelId}${cost ? ` (${cost.label})` : ""}`;
|
|
8942
|
+
});
|
|
8943
|
+
lines.push(` ${modality}: ${shown.join(", ")}`);
|
|
8944
|
+
}
|
|
8945
|
+
} else {
|
|
8946
|
+
lines.push("No generation models are available \u2014 none of your configured providers offers one.");
|
|
8947
|
+
}
|
|
8948
|
+
const blocked = CAPABILITIES.filter((c) => c.unsupported && this.available.has(c.provider));
|
|
8949
|
+
for (const c of blocked) {
|
|
8950
|
+
lines.push(` ${c.modality}: ${c.modelId} exists on your account but is not callable. ${c.unsupported}`);
|
|
8951
|
+
}
|
|
8952
|
+
lines.push(
|
|
8953
|
+
" music: no supported provider exposes a music-generation API, so Cascade cannot generate music."
|
|
8954
|
+
);
|
|
8955
|
+
return lines.join("\n");
|
|
8956
|
+
}
|
|
8957
|
+
};
|
|
8958
|
+
|
|
8618
8959
|
// src/tools/base.ts
|
|
8619
8960
|
var BaseTool = class {
|
|
8620
8961
|
workspaceRoot = process.cwd();
|
|
@@ -8633,7 +8974,446 @@ var BaseTool = class {
|
|
|
8633
8974
|
}
|
|
8634
8975
|
};
|
|
8635
8976
|
|
|
8636
|
-
// src/
|
|
8977
|
+
// src/core/router/provider-errors.ts
|
|
8978
|
+
var SYSTEMIC = /* @__PURE__ */ new Set([
|
|
8979
|
+
"rate_limit",
|
|
8980
|
+
"quota_exhausted",
|
|
8981
|
+
"auth",
|
|
8982
|
+
"model_unavailable"
|
|
8983
|
+
]);
|
|
8984
|
+
function isSystemicKind(kind) {
|
|
8985
|
+
return SYSTEMIC.has(kind);
|
|
8986
|
+
}
|
|
8987
|
+
function statusOf(err) {
|
|
8988
|
+
if (!err || typeof err !== "object") return void 0;
|
|
8989
|
+
const e = err;
|
|
8990
|
+
for (const key of ["status", "statusCode", "code"]) {
|
|
8991
|
+
const v = e[key];
|
|
8992
|
+
if (typeof v === "number" && v >= 100 && v < 600) return v;
|
|
8993
|
+
}
|
|
8994
|
+
const resp = e["response"];
|
|
8995
|
+
if (resp && typeof resp["status"] === "number") return resp["status"];
|
|
8996
|
+
const inner = e["error"];
|
|
8997
|
+
if (inner && typeof inner["code"] === "number") return inner["code"];
|
|
8998
|
+
return void 0;
|
|
8999
|
+
}
|
|
9000
|
+
function messageOf(err) {
|
|
9001
|
+
if (err instanceof Error) return err.message;
|
|
9002
|
+
if (typeof err === "string") return err;
|
|
9003
|
+
if (err && typeof err === "object") {
|
|
9004
|
+
const e = err;
|
|
9005
|
+
if (typeof e["message"] === "string") return e["message"];
|
|
9006
|
+
const inner = e["error"];
|
|
9007
|
+
if (inner && typeof inner["message"] === "string") return inner["message"];
|
|
9008
|
+
try {
|
|
9009
|
+
return JSON.stringify(err);
|
|
9010
|
+
} catch {
|
|
9011
|
+
}
|
|
9012
|
+
}
|
|
9013
|
+
return String(err);
|
|
9014
|
+
}
|
|
9015
|
+
function classifyProviderError(err) {
|
|
9016
|
+
const raw = messageOf(err).trim();
|
|
9017
|
+
const status = statusOf(err);
|
|
9018
|
+
const m = raw.toLowerCase();
|
|
9019
|
+
const kind = (() => {
|
|
9020
|
+
if (status === 429) {
|
|
9021
|
+
return /quota|billing|credit|exceeded your current quota|insufficient/.test(m) ? "quota_exhausted" : "rate_limit";
|
|
9022
|
+
}
|
|
9023
|
+
if (status === 401 || status === 403) return "auth";
|
|
9024
|
+
if (status === 404) return "model_unavailable";
|
|
9025
|
+
if (/rate limit|too many requests|resource_exhausted/.test(m)) {
|
|
9026
|
+
return /quota|billing|credit|insufficient/.test(m) ? "quota_exhausted" : "rate_limit";
|
|
9027
|
+
}
|
|
9028
|
+
if (/quota|billing|insufficient_quota|insufficient credit|payment required/.test(m)) return "quota_exhausted";
|
|
9029
|
+
if (/api key|unauthorized|authentication|permission denied|invalid[_ ]api/.test(m)) return "auth";
|
|
9030
|
+
if (/model .*(not found|does not exist|unavailable)|no such model|unknown model|not supported for/.test(m)) {
|
|
9031
|
+
return "model_unavailable";
|
|
9032
|
+
}
|
|
9033
|
+
if (/context length|too many tokens|maximum context|prompt is too long|input too long/.test(m)) return "context_length";
|
|
9034
|
+
if (/safety|content filter|blocked|recitation|prohibited/.test(m)) return "content_filter";
|
|
9035
|
+
if (/econnreset|etimedout|enotfound|socket hang up|network|fetch failed|aborted/.test(m)) return "network";
|
|
9036
|
+
return "unknown";
|
|
9037
|
+
})();
|
|
9038
|
+
return { kind, systemic: isSystemicKind(kind), status, raw };
|
|
9039
|
+
}
|
|
9040
|
+
function describeProviderError(c, modelId) {
|
|
9041
|
+
const on = modelId ? ` on ${modelId}` : "";
|
|
9042
|
+
const detail = c.raw ? ` Provider said: ${c.raw}` : "";
|
|
9043
|
+
switch (c.kind) {
|
|
9044
|
+
case "rate_limit":
|
|
9045
|
+
return `Rate limit hit${on}. The provider is throttling requests \u2014 lower the number of parallel workers, or use a model with a higher requests-per-minute allowance.${detail}`;
|
|
9046
|
+
case "quota_exhausted":
|
|
9047
|
+
return `Quota or billing limit reached${on}. This will not recover on its own \u2014 check the provider's billing page, or switch this tier to a different provider.${detail}`;
|
|
9048
|
+
case "auth":
|
|
9049
|
+
return `Authentication failed${on}. The API key is missing, expired, or lacks access to this model \u2014 re-check the key in Settings > Providers.${detail}`;
|
|
9050
|
+
case "model_unavailable":
|
|
9051
|
+
return `Model unavailable${on}. The id may be wrong, retired, or not enabled for this API key \u2014 pick a different model for this tier.${detail}`;
|
|
9052
|
+
case "context_length":
|
|
9053
|
+
return `The prompt was too long${on}. Reduce the attached context, or use a model with a larger context window for this tier.${detail}`;
|
|
9054
|
+
case "content_filter":
|
|
9055
|
+
return `The provider's safety filter refused this request${on}.${detail}`;
|
|
9056
|
+
case "network":
|
|
9057
|
+
return `Network error reaching the provider${on}. This is usually transient.${detail}`;
|
|
9058
|
+
default:
|
|
9059
|
+
return `The model call failed${on}.${detail}`;
|
|
9060
|
+
}
|
|
9061
|
+
}
|
|
9062
|
+
|
|
9063
|
+
// src/core/multimodal/generate.ts
|
|
9064
|
+
var DEFAULT_TIMEOUT_MS = 12e4;
|
|
9065
|
+
async function callProvider(fn, modelId) {
|
|
9066
|
+
try {
|
|
9067
|
+
return await fn();
|
|
9068
|
+
} catch (err) {
|
|
9069
|
+
const classified = classifyProviderError(err);
|
|
9070
|
+
throw new Error(describeProviderError(classified, modelId));
|
|
9071
|
+
}
|
|
9072
|
+
}
|
|
9073
|
+
async function postJson(url, body, headers, signal) {
|
|
9074
|
+
const ac = new AbortController();
|
|
9075
|
+
const timer = setTimeout(() => ac.abort(), DEFAULT_TIMEOUT_MS);
|
|
9076
|
+
const onAbort = () => ac.abort();
|
|
9077
|
+
signal?.addEventListener("abort", onAbort);
|
|
9078
|
+
try {
|
|
9079
|
+
const res = await fetch(url, {
|
|
9080
|
+
method: "POST",
|
|
9081
|
+
headers: { "Content-Type": "application/json", ...headers },
|
|
9082
|
+
body: JSON.stringify(body),
|
|
9083
|
+
signal: ac.signal
|
|
9084
|
+
});
|
|
9085
|
+
if (!res.ok) {
|
|
9086
|
+
const text = await res.text().catch(() => "");
|
|
9087
|
+
throw Object.assign(new Error(text || res.statusText), { status: res.status });
|
|
9088
|
+
}
|
|
9089
|
+
return res;
|
|
9090
|
+
} finally {
|
|
9091
|
+
clearTimeout(timer);
|
|
9092
|
+
signal?.removeEventListener("abort", onAbort);
|
|
9093
|
+
}
|
|
9094
|
+
}
|
|
9095
|
+
function baseUrlFor(cfg, fallback) {
|
|
9096
|
+
return (cfg.baseUrl ?? fallback).replace(/\/+$/, "");
|
|
9097
|
+
}
|
|
9098
|
+
function stamp(ext) {
|
|
9099
|
+
return `cascade-${Date.now()}.${ext}`;
|
|
9100
|
+
}
|
|
9101
|
+
async function generateImage(cap, cfg, req, signal) {
|
|
9102
|
+
return callProvider(async () => {
|
|
9103
|
+
if (cap.api === "openai-images") {
|
|
9104
|
+
const url = `${baseUrlFor(cfg, "https://api.openai.com/v1")}/images/generations`;
|
|
9105
|
+
const res = await postJson(url, {
|
|
9106
|
+
model: cap.modelId,
|
|
9107
|
+
prompt: req.prompt,
|
|
9108
|
+
size: req.size ?? "1024x1024",
|
|
9109
|
+
n: 1,
|
|
9110
|
+
// b64 rather than a URL: see the file header — provider URLs expire.
|
|
9111
|
+
response_format: "b64_json"
|
|
9112
|
+
}, { Authorization: `Bearer ${cfg.apiKey ?? ""}` }, signal);
|
|
9113
|
+
const body = await res.json();
|
|
9114
|
+
const first = body.data?.[0];
|
|
9115
|
+
if (!first?.b64_json) throw new Error("Image response contained no image data.");
|
|
9116
|
+
return {
|
|
9117
|
+
data: Buffer.from(first.b64_json, "base64"),
|
|
9118
|
+
mimeType: "image/png",
|
|
9119
|
+
filename: stamp("png"),
|
|
9120
|
+
modelId: cap.modelId,
|
|
9121
|
+
provider: cap.provider,
|
|
9122
|
+
...first.revised_prompt ? { revisedPrompt: first.revised_prompt } : {}
|
|
9123
|
+
};
|
|
9124
|
+
}
|
|
9125
|
+
if (cap.api === "gemini-predict") {
|
|
9126
|
+
const base = baseUrlFor(cfg, "https://generativelanguage.googleapis.com/v1beta");
|
|
9127
|
+
const url = `${base}/models/${encodeURIComponent(cap.modelId)}:predict`;
|
|
9128
|
+
const res = await postJson(url, {
|
|
9129
|
+
instances: [{ prompt: req.prompt }],
|
|
9130
|
+
parameters: { sampleCount: 1 }
|
|
9131
|
+
}, { "x-goog-api-key": cfg.apiKey ?? "" }, signal);
|
|
9132
|
+
const body = await res.json();
|
|
9133
|
+
const first = body.predictions?.[0];
|
|
9134
|
+
if (!first?.bytesBase64Encoded) throw new Error("Image response contained no image data.");
|
|
9135
|
+
const mimeType = first.mimeType ?? "image/png";
|
|
9136
|
+
return {
|
|
9137
|
+
data: Buffer.from(first.bytesBase64Encoded, "base64"),
|
|
9138
|
+
mimeType,
|
|
9139
|
+
filename: stamp(mimeType.includes("jpeg") ? "jpg" : "png"),
|
|
9140
|
+
modelId: cap.modelId,
|
|
9141
|
+
provider: cap.provider
|
|
9142
|
+
};
|
|
9143
|
+
}
|
|
9144
|
+
throw new Error(`${cap.modelId} is not an image generator.`);
|
|
9145
|
+
}, cap.modelId);
|
|
9146
|
+
}
|
|
9147
|
+
async function generateSpeech(cap, cfg, req, signal) {
|
|
9148
|
+
return callProvider(async () => {
|
|
9149
|
+
if (cap.api !== "openai-speech") throw new Error(`${cap.modelId} is not a speech model.`);
|
|
9150
|
+
const format = req.format ?? "mp3";
|
|
9151
|
+
const url = `${baseUrlFor(cfg, "https://api.openai.com/v1")}/audio/speech`;
|
|
9152
|
+
const res = await postJson(url, {
|
|
9153
|
+
model: cap.modelId,
|
|
9154
|
+
input: req.text,
|
|
9155
|
+
voice: req.voice ?? "alloy",
|
|
9156
|
+
response_format: format
|
|
9157
|
+
}, { Authorization: `Bearer ${cfg.apiKey ?? ""}` }, signal);
|
|
9158
|
+
const mime = {
|
|
9159
|
+
mp3: "audio/mpeg",
|
|
9160
|
+
opus: "audio/opus",
|
|
9161
|
+
aac: "audio/aac",
|
|
9162
|
+
flac: "audio/flac",
|
|
9163
|
+
wav: "audio/wav"
|
|
9164
|
+
};
|
|
9165
|
+
return {
|
|
9166
|
+
data: Buffer.from(await res.arrayBuffer()),
|
|
9167
|
+
mimeType: mime[format] ?? "application/octet-stream",
|
|
9168
|
+
filename: stamp(format),
|
|
9169
|
+
modelId: cap.modelId,
|
|
9170
|
+
provider: cap.provider
|
|
9171
|
+
};
|
|
9172
|
+
}, cap.modelId);
|
|
9173
|
+
}
|
|
9174
|
+
async function transcribeAudio(cap, cfg, req, signal) {
|
|
9175
|
+
return callProvider(async () => {
|
|
9176
|
+
if (cap.api !== "openai-transcriptions") throw new Error(`${cap.modelId} is not a transcription model.`);
|
|
9177
|
+
const url = `${baseUrlFor(cfg, "https://api.openai.com/v1")}/audio/transcriptions`;
|
|
9178
|
+
const form = new FormData();
|
|
9179
|
+
form.append("model", cap.modelId);
|
|
9180
|
+
if (req.language) form.append("language", req.language);
|
|
9181
|
+
form.append("file", new Blob([new Uint8Array(req.audio)]), req.filename);
|
|
9182
|
+
const ac = new AbortController();
|
|
9183
|
+
const timer = setTimeout(() => ac.abort(), DEFAULT_TIMEOUT_MS);
|
|
9184
|
+
const onAbort = () => ac.abort();
|
|
9185
|
+
signal?.addEventListener("abort", onAbort);
|
|
9186
|
+
try {
|
|
9187
|
+
const res = await fetch(url, {
|
|
9188
|
+
method: "POST",
|
|
9189
|
+
headers: { Authorization: `Bearer ${cfg.apiKey ?? ""}` },
|
|
9190
|
+
body: form,
|
|
9191
|
+
signal: ac.signal
|
|
9192
|
+
});
|
|
9193
|
+
if (!res.ok) {
|
|
9194
|
+
const text = await res.text().catch(() => "");
|
|
9195
|
+
throw Object.assign(new Error(text || res.statusText), { status: res.status });
|
|
9196
|
+
}
|
|
9197
|
+
const body = await res.json();
|
|
9198
|
+
return { text: body.text ?? "", modelId: cap.modelId };
|
|
9199
|
+
} finally {
|
|
9200
|
+
clearTimeout(timer);
|
|
9201
|
+
signal?.removeEventListener("abort", onAbort);
|
|
9202
|
+
}
|
|
9203
|
+
}, cap.modelId);
|
|
9204
|
+
}
|
|
9205
|
+
|
|
9206
|
+
// src/tools/generate-media.ts
|
|
9207
|
+
function resolve(deps, modality, preferModel) {
|
|
9208
|
+
const selected = deps.registry.select(modality, preferModel ? { preferModel } : {});
|
|
9209
|
+
if (!selected) {
|
|
9210
|
+
return { ok: false, error: `No ${modality} model is available on your configured providers.` };
|
|
9211
|
+
}
|
|
9212
|
+
const cfg = deps.lookupProvider(selected.capability.provider);
|
|
9213
|
+
if (!cfg) {
|
|
9214
|
+
return {
|
|
9215
|
+
ok: false,
|
|
9216
|
+
error: `${selected.capability.modelId} needs the ${selected.capability.provider} provider, which is not configured.`
|
|
9217
|
+
};
|
|
9218
|
+
}
|
|
9219
|
+
return { ok: true, selected, cfg };
|
|
9220
|
+
}
|
|
9221
|
+
var GenerateImageTool = class extends BaseTool {
|
|
9222
|
+
constructor(deps) {
|
|
9223
|
+
super();
|
|
9224
|
+
this.deps = deps;
|
|
9225
|
+
}
|
|
9226
|
+
deps;
|
|
9227
|
+
name = "generate_image";
|
|
9228
|
+
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.";
|
|
9229
|
+
inputSchema = {
|
|
9230
|
+
type: "object",
|
|
9231
|
+
properties: {
|
|
9232
|
+
prompt: { type: "string", description: "What the image should show. Be specific about subject, style and composition." },
|
|
9233
|
+
size: { type: "string", description: 'Pixel size, e.g. "1024x1024" (default), "1792x1024", "1024x1792".' },
|
|
9234
|
+
model: { type: "string", description: "Optional: force a specific image model instead of the automatic choice." }
|
|
9235
|
+
},
|
|
9236
|
+
required: ["prompt"]
|
|
9237
|
+
};
|
|
9238
|
+
async execute(input, options) {
|
|
9239
|
+
const prompt = String(input["prompt"] ?? "").trim();
|
|
9240
|
+
if (!prompt) return 'Provide a "prompt" describing the image.';
|
|
9241
|
+
const r = resolve(this.deps, "image", input["model"] ? String(input["model"]) : void 0);
|
|
9242
|
+
if (!r.ok) return r.error;
|
|
9243
|
+
const size = input["size"] ? String(input["size"]) : void 0;
|
|
9244
|
+
const asset = await generateImage(
|
|
9245
|
+
r.selected.capability,
|
|
9246
|
+
r.cfg,
|
|
9247
|
+
{ prompt, ...size ? { size } : {} },
|
|
9248
|
+
options.signal
|
|
9249
|
+
);
|
|
9250
|
+
const location = await this.deps.sink(asset);
|
|
9251
|
+
return [
|
|
9252
|
+
`Image generated and saved to ${location}.`,
|
|
9253
|
+
`Model: ${asset.modelId} \u2014 ${r.selected.reason}`,
|
|
9254
|
+
// DALL·E rewrites the prompt before drawing; the model needs to know what
|
|
9255
|
+
// was actually rendered, not what it asked for, or its description of the
|
|
9256
|
+
// result will not match the image.
|
|
9257
|
+
asset.revisedPrompt ? `The provider rewrote the prompt to: ${asset.revisedPrompt}` : ""
|
|
9258
|
+
].filter(Boolean).join("\n");
|
|
9259
|
+
}
|
|
9260
|
+
};
|
|
9261
|
+
var GenerateSpeechTool = class extends BaseTool {
|
|
9262
|
+
constructor(deps) {
|
|
9263
|
+
super();
|
|
9264
|
+
this.deps = deps;
|
|
9265
|
+
}
|
|
9266
|
+
deps;
|
|
9267
|
+
name = "generate_speech";
|
|
9268
|
+
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.";
|
|
9269
|
+
inputSchema = {
|
|
9270
|
+
type: "object",
|
|
9271
|
+
properties: {
|
|
9272
|
+
text: { type: "string", description: "The text to speak." },
|
|
9273
|
+
voice: { type: "string", description: "Voice id: alloy, echo, fable, onyx, nova, shimmer. Default alloy." },
|
|
9274
|
+
format: { type: "string", description: "Audio format: mp3 (default), opus, aac, flac, wav." },
|
|
9275
|
+
model: { type: "string", description: "Optional: force a specific speech model." }
|
|
9276
|
+
},
|
|
9277
|
+
required: ["text"]
|
|
9278
|
+
};
|
|
9279
|
+
async execute(input, options) {
|
|
9280
|
+
const text = String(input["text"] ?? "").trim();
|
|
9281
|
+
if (!text) return 'Provide "text" to speak.';
|
|
9282
|
+
const r = resolve(this.deps, "speech", input["model"] ? String(input["model"]) : void 0);
|
|
9283
|
+
if (!r.ok) return r.error;
|
|
9284
|
+
const fmt = String(input["format"] ?? "mp3");
|
|
9285
|
+
const allowed = ["mp3", "opus", "aac", "flac", "wav"];
|
|
9286
|
+
const format = allowed.includes(fmt) ? fmt : "mp3";
|
|
9287
|
+
const asset = await generateSpeech(
|
|
9288
|
+
r.selected.capability,
|
|
9289
|
+
r.cfg,
|
|
9290
|
+
{ text, voice: input["voice"] ? String(input["voice"]) : void 0, format },
|
|
9291
|
+
options.signal
|
|
9292
|
+
);
|
|
9293
|
+
const location = await this.deps.sink(asset);
|
|
9294
|
+
return `Speech generated and saved to ${location}.
|
|
9295
|
+
Model: ${asset.modelId} \u2014 ${r.selected.reason}`;
|
|
9296
|
+
}
|
|
9297
|
+
};
|
|
9298
|
+
var TranscribeAudioTool = class extends BaseTool {
|
|
9299
|
+
constructor(deps, readFile) {
|
|
9300
|
+
super();
|
|
9301
|
+
this.deps = deps;
|
|
9302
|
+
this.readFile = readFile;
|
|
9303
|
+
}
|
|
9304
|
+
deps;
|
|
9305
|
+
readFile;
|
|
9306
|
+
name = "transcribe_audio";
|
|
9307
|
+
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.";
|
|
9308
|
+
inputSchema = {
|
|
9309
|
+
type: "object",
|
|
9310
|
+
properties: {
|
|
9311
|
+
path: { type: "string", description: "Path to the audio file to transcribe." },
|
|
9312
|
+
language: { type: "string", description: 'Optional ISO-639-1 language hint, e.g. "en", improves accuracy.' },
|
|
9313
|
+
model: { type: "string", description: "Optional: force a specific transcription model." }
|
|
9314
|
+
},
|
|
9315
|
+
required: ["path"]
|
|
9316
|
+
};
|
|
9317
|
+
async execute(input, options) {
|
|
9318
|
+
const filePath = String(input["path"] ?? "").trim();
|
|
9319
|
+
if (!filePath) return 'Provide a "path" to the audio file.';
|
|
9320
|
+
const r = resolve(this.deps, "transcription", input["model"] ? String(input["model"]) : void 0);
|
|
9321
|
+
if (!r.ok) return r.error;
|
|
9322
|
+
let audio;
|
|
9323
|
+
try {
|
|
9324
|
+
audio = await this.readFile(filePath);
|
|
9325
|
+
} catch {
|
|
9326
|
+
return `Could not read the audio file at ${filePath}.`;
|
|
9327
|
+
}
|
|
9328
|
+
const { text, modelId } = await transcribeAudio(
|
|
9329
|
+
r.selected.capability,
|
|
9330
|
+
r.cfg,
|
|
9331
|
+
{
|
|
9332
|
+
audio,
|
|
9333
|
+
filename: filePath.split(/[\\/]/).pop() || "audio",
|
|
9334
|
+
...input["language"] ? { language: String(input["language"]) } : {}
|
|
9335
|
+
},
|
|
9336
|
+
options.signal
|
|
9337
|
+
);
|
|
9338
|
+
return text ? `Transcript (${modelId}):
|
|
9339
|
+
|
|
9340
|
+
${text}` : `${modelId} returned an empty transcript \u2014 the file may be silent or in an unsupported format.`;
|
|
9341
|
+
}
|
|
9342
|
+
};
|
|
9343
|
+
function buildMediaTools(deps, readFile) {
|
|
9344
|
+
const tools = [];
|
|
9345
|
+
const modalities = new Set(deps.registry.availableModalities());
|
|
9346
|
+
if (modalities.has("image")) tools.push(new GenerateImageTool(deps));
|
|
9347
|
+
if (modalities.has("speech")) tools.push(new GenerateSpeechTool(deps));
|
|
9348
|
+
if (modalities.has("transcription") && readFile) tools.push(new TranscribeAudioTool(deps, readFile));
|
|
9349
|
+
return tools;
|
|
9350
|
+
}
|
|
9351
|
+
|
|
9352
|
+
// src/core/run-breaker.ts
|
|
9353
|
+
var DEFAULT_FAILURE_THRESHOLD = 3;
|
|
9354
|
+
var RunBreaker = class {
|
|
9355
|
+
threshold;
|
|
9356
|
+
/** modelId → consecutive systemic failures of the SAME kind. */
|
|
9357
|
+
counts = /* @__PURE__ */ new Map();
|
|
9358
|
+
trip = null;
|
|
9359
|
+
constructor(threshold = DEFAULT_FAILURE_THRESHOLD) {
|
|
9360
|
+
this.threshold = Math.max(1, threshold);
|
|
9361
|
+
}
|
|
9362
|
+
/**
|
|
9363
|
+
* Record a failed model call. Returns the trip if this failure is the one
|
|
9364
|
+
* that opened the breaker, otherwise null.
|
|
9365
|
+
*
|
|
9366
|
+
* A non-systemic failure RESETS the model's counter: it is evidence the model
|
|
9367
|
+
* is reachable and answering, which is exactly what a systemic streak claims
|
|
9368
|
+
* it is not.
|
|
9369
|
+
*/
|
|
9370
|
+
record(err, modelId) {
|
|
9371
|
+
if (this.trip) return null;
|
|
9372
|
+
const id = modelId?.trim() || "unknown-model";
|
|
9373
|
+
const c = classifyProviderError(err);
|
|
9374
|
+
if (!c.systemic) {
|
|
9375
|
+
this.counts.delete(id);
|
|
9376
|
+
return null;
|
|
9377
|
+
}
|
|
9378
|
+
const prev = this.counts.get(id);
|
|
9379
|
+
const n = prev && prev.kind === c.kind ? prev.n + 1 : 1;
|
|
9380
|
+
this.counts.set(id, { kind: c.kind, n, last: c });
|
|
9381
|
+
if (n >= this.threshold) {
|
|
9382
|
+
this.trip = {
|
|
9383
|
+
modelId: id,
|
|
9384
|
+
kind: c.kind,
|
|
9385
|
+
failures: n,
|
|
9386
|
+
message: describeProviderError(c, id)
|
|
9387
|
+
};
|
|
9388
|
+
return this.trip;
|
|
9389
|
+
}
|
|
9390
|
+
return null;
|
|
9391
|
+
}
|
|
9392
|
+
/** True once the run should stop launching work that would fail identically. */
|
|
9393
|
+
isOpen() {
|
|
9394
|
+
return this.trip !== null;
|
|
9395
|
+
}
|
|
9396
|
+
/** Why the breaker opened, or null while it is closed. */
|
|
9397
|
+
reason() {
|
|
9398
|
+
return this.trip;
|
|
9399
|
+
}
|
|
9400
|
+
/**
|
|
9401
|
+
* One line for a worker/section that never ran because the breaker was
|
|
9402
|
+
* already open. Says the work was skipped deliberately — not that it failed
|
|
9403
|
+
* on its own merits, which is what an empty FAILED status would imply.
|
|
9404
|
+
*/
|
|
9405
|
+
skipMessage() {
|
|
9406
|
+
const t = this.trip;
|
|
9407
|
+
if (!t) return "Skipped.";
|
|
9408
|
+
return `Skipped \u2014 the run stopped after ${t.failures} consecutive failures on ${t.modelId}. ${t.message}`;
|
|
9409
|
+
}
|
|
9410
|
+
/** Test/diagnostic view of the per-model streaks. */
|
|
9411
|
+
streaks() {
|
|
9412
|
+
const out = {};
|
|
9413
|
+
for (const [k, v] of this.counts) out[k] = { kind: v.kind, n: v.n };
|
|
9414
|
+
return out;
|
|
9415
|
+
}
|
|
9416
|
+
};
|
|
8637
9417
|
var execAsync = promisify(exec);
|
|
8638
9418
|
var ShellTool = class extends BaseTool {
|
|
8639
9419
|
name = "shell";
|
|
@@ -9329,7 +10109,7 @@ var PDFCreateTool = class extends BaseTool {
|
|
|
9329
10109
|
if (!fs23.existsSync(dir)) {
|
|
9330
10110
|
fs23.mkdirSync(dir, { recursive: true });
|
|
9331
10111
|
}
|
|
9332
|
-
return new Promise((
|
|
10112
|
+
return new Promise((resolve2, reject) => {
|
|
9333
10113
|
try {
|
|
9334
10114
|
const doc = new PDFDocument({ margin: 50 });
|
|
9335
10115
|
const stream = fs23.createWriteStream(filePath);
|
|
@@ -9347,7 +10127,7 @@ var PDFCreateTool = class extends BaseTool {
|
|
|
9347
10127
|
});
|
|
9348
10128
|
doc.end();
|
|
9349
10129
|
stream.on("finish", () => {
|
|
9350
|
-
|
|
10130
|
+
resolve2(`Successfully created PDF at ${filePath} (${content.length} characters)`);
|
|
9351
10131
|
});
|
|
9352
10132
|
stream.on("error", (err) => {
|
|
9353
10133
|
reject(new Error(`Failed to write PDF: ${err.message}`));
|
|
@@ -9419,7 +10199,7 @@ var CodeInterpreterTool = class extends BaseTool {
|
|
|
9419
10199
|
const filePath = path24.join(tmpDir, fileName);
|
|
9420
10200
|
fs23.writeFileSync(filePath, code, "utf-8");
|
|
9421
10201
|
const execArgs = [filePath, ...args];
|
|
9422
|
-
return new Promise((
|
|
10202
|
+
return new Promise((resolve2) => {
|
|
9423
10203
|
const startMs = Date.now();
|
|
9424
10204
|
execFile(cmdPrefix, execArgs, { cwd: this.workspaceRoot, timeout: 3e4 }, (error, stdout, stderr) => {
|
|
9425
10205
|
const duration = Date.now() - startMs;
|
|
@@ -9433,17 +10213,17 @@ var CodeInterpreterTool = class extends BaseTool {
|
|
|
9433
10213
|
if (error) {
|
|
9434
10214
|
const timedOut = error.killed && duration >= 3e4;
|
|
9435
10215
|
if (timedOut) {
|
|
9436
|
-
|
|
10216
|
+
resolve2(`Execution timed out after 30s. Consider breaking the task into smaller pieces.
|
|
9437
10217
|
Partial stdout: ${stdout}
|
|
9438
10218
|
Stderr: ${stderr}`);
|
|
9439
10219
|
} else {
|
|
9440
|
-
|
|
10220
|
+
resolve2(`Execution failed (${duration}ms):
|
|
9441
10221
|
Error: ${error.message}
|
|
9442
10222
|
Stderr: ${stderr}
|
|
9443
10223
|
Stdout: ${stdout}`);
|
|
9444
10224
|
}
|
|
9445
10225
|
} else {
|
|
9446
|
-
|
|
10226
|
+
resolve2(`Execution successful (${duration}ms):
|
|
9447
10227
|
Stdout: ${stdout}
|
|
9448
10228
|
Stderr: ${stderr}`);
|
|
9449
10229
|
}
|
|
@@ -10097,7 +10877,7 @@ var ToolRegistry = class extends EventEmitter {
|
|
|
10097
10877
|
*/
|
|
10098
10878
|
waitForTool(toolName, timeoutMs = 6e4) {
|
|
10099
10879
|
if (this.tools.has(toolName)) return Promise.resolve();
|
|
10100
|
-
return new Promise((
|
|
10880
|
+
return new Promise((resolve2, reject) => {
|
|
10101
10881
|
const timer = setTimeout(() => {
|
|
10102
10882
|
this.off("tool:added", handler);
|
|
10103
10883
|
reject(new Error(`Timeout waiting for tool: ${toolName}`));
|
|
@@ -10106,7 +10886,7 @@ var ToolRegistry = class extends EventEmitter {
|
|
|
10106
10886
|
if (name === toolName) {
|
|
10107
10887
|
clearTimeout(timer);
|
|
10108
10888
|
this.off("tool:added", handler);
|
|
10109
|
-
|
|
10889
|
+
resolve2();
|
|
10110
10890
|
}
|
|
10111
10891
|
};
|
|
10112
10892
|
this.on("tool:added", handler);
|
|
@@ -10781,20 +11561,20 @@ var PermissionEscalator = class extends EventEmitter {
|
|
|
10781
11561
|
resolver(decision);
|
|
10782
11562
|
}
|
|
10783
11563
|
waitForUserDecision(req) {
|
|
10784
|
-
return new Promise((
|
|
11564
|
+
return new Promise((resolve2) => {
|
|
10785
11565
|
let timer;
|
|
10786
11566
|
const wrappedResolver = (decision) => {
|
|
10787
11567
|
if (timer) clearTimeout(timer);
|
|
10788
11568
|
if (decision.always) {
|
|
10789
11569
|
this.taskWideCache.set(req.toolName, decision.approved);
|
|
10790
11570
|
}
|
|
10791
|
-
|
|
11571
|
+
resolve2(decision);
|
|
10792
11572
|
};
|
|
10793
11573
|
this.pendingUserDecisions.set(req.id, wrappedResolver);
|
|
10794
11574
|
if (this.approvalTimeoutMs > 0 && Number.isFinite(this.approvalTimeoutMs)) {
|
|
10795
11575
|
timer = setTimeout(() => {
|
|
10796
11576
|
if (this.pendingUserDecisions.delete(req.id)) {
|
|
10797
|
-
|
|
11577
|
+
resolve2({
|
|
10798
11578
|
requestId: req.id,
|
|
10799
11579
|
approved: false,
|
|
10800
11580
|
decidedBy: "USER",
|
|
@@ -10961,6 +11741,14 @@ var BudgetConfigSchema = z.object({
|
|
|
10961
11741
|
maxTokensPerRun: z.number().int().positive().default(2e5),
|
|
10962
11742
|
/** Optional hard per-task cost ceiling (USD). Unset = only the token cap applies. */
|
|
10963
11743
|
maxCostPerRunUsd: z.number().positive().optional(),
|
|
11744
|
+
/**
|
|
11745
|
+
* Consecutive systemic failures against ONE model before the run stops
|
|
11746
|
+
* launching work that would fail the same way (dead key, wrong model id,
|
|
11747
|
+
* exhausted quota). Lower reacts sooner and wastes less; higher absorbs a
|
|
11748
|
+
* longer transient before giving up. Only failures that will certainly repeat
|
|
11749
|
+
* are counted — see core/run-breaker.ts.
|
|
11750
|
+
*/
|
|
11751
|
+
failureThreshold: z.number().int().min(1).max(20).default(3),
|
|
10964
11752
|
warnAtPct: z.number().default(80)
|
|
10965
11753
|
});
|
|
10966
11754
|
var WorkspaceConfigSchema = z.object({
|
|
@@ -11248,6 +12036,23 @@ var noopTelemetry = {
|
|
|
11248
12036
|
}
|
|
11249
12037
|
};
|
|
11250
12038
|
|
|
12039
|
+
// src/core/router/feedback-prior.ts
|
|
12040
|
+
var MAX_FEEDBACK_ADJUSTMENT = 0.05;
|
|
12041
|
+
var PRIOR_STRENGTH = 10;
|
|
12042
|
+
function feedbackAdjustment(counts) {
|
|
12043
|
+
const good = Math.max(0, Math.floor(counts.good));
|
|
12044
|
+
const bad = Math.max(0, Math.floor(counts.bad));
|
|
12045
|
+
const n = good + bad;
|
|
12046
|
+
if (n === 0) return 0;
|
|
12047
|
+
const half = PRIOR_STRENGTH / 2;
|
|
12048
|
+
const shrunk = (good + half) / (n + PRIOR_STRENGTH);
|
|
12049
|
+
return (shrunk - 0.5) * 2 * MAX_FEEDBACK_ADJUSTMENT;
|
|
12050
|
+
}
|
|
12051
|
+
function applyFeedback(score01, counts) {
|
|
12052
|
+
const adjusted = score01 + feedbackAdjustment(counts);
|
|
12053
|
+
return Math.max(0, Math.min(1, adjusted));
|
|
12054
|
+
}
|
|
12055
|
+
|
|
11251
12056
|
// src/core/router/task-analyzer.ts
|
|
11252
12057
|
var CODE_SIGNALS = [
|
|
11253
12058
|
/\b(?:function|class|interface|async|await|import|export|const|let|var|def|return|if|else|for|while|try|catch)\b/,
|
|
@@ -11322,6 +12127,8 @@ var TASK_TYPE_TAGS = {
|
|
|
11322
12127
|
};
|
|
11323
12128
|
var TaskAnalyzer = class {
|
|
11324
12129
|
tracker;
|
|
12130
|
+
/** Per-model thumbs counts, when the host collects them. */
|
|
12131
|
+
feedback;
|
|
11325
12132
|
bias;
|
|
11326
12133
|
lastProfile = null;
|
|
11327
12134
|
lastSelectedModels = /* @__PURE__ */ new Map();
|
|
@@ -11329,6 +12136,14 @@ var TaskAnalyzer = class {
|
|
|
11329
12136
|
this.tracker = tracker;
|
|
11330
12137
|
this.bias = bias;
|
|
11331
12138
|
}
|
|
12139
|
+
/**
|
|
12140
|
+
* Supply per-model rating counts. Optional by design: a host that collects no
|
|
12141
|
+
* feedback (the CLI, a fresh install) scores exactly as it did before, since
|
|
12142
|
+
* feedbackAdjustment returns 0 for an empty record.
|
|
12143
|
+
*/
|
|
12144
|
+
setFeedbackSource(source) {
|
|
12145
|
+
this.feedback = source;
|
|
12146
|
+
}
|
|
11332
12147
|
setTracker(tracker) {
|
|
11333
12148
|
this.tracker = tracker;
|
|
11334
12149
|
}
|
|
@@ -11412,7 +12227,10 @@ var TaskAnalyzer = class {
|
|
|
11412
12227
|
const perf = this.tracker?.performanceScore(model.id, profile.type) ?? 0.5;
|
|
11413
12228
|
const costEff = this.costEfficiency(model, profile.complexity);
|
|
11414
12229
|
const match = this.taskMatchScore(model, profile);
|
|
11415
|
-
const
|
|
12230
|
+
const rated = this.feedback?.(model.id);
|
|
12231
|
+
const publicScore = benchmarkScore01(model, profile.type);
|
|
12232
|
+
const adjusted = rated ? applyFeedback(publicScore, rated) : publicScore;
|
|
12233
|
+
const benchmark = 0.3 + 0.7 * adjusted;
|
|
11416
12234
|
switch (this.bias) {
|
|
11417
12235
|
case "quality":
|
|
11418
12236
|
return perf * match * benchmark ** 2 * (0.85 + 0.15 * costEff);
|
|
@@ -11814,10 +12632,10 @@ var DynamicTool = class extends BaseTool {
|
|
|
11814
12632
|
`;
|
|
11815
12633
|
const script = await isolate.compileScript(bootstrap);
|
|
11816
12634
|
const runPromise = script.run(context, { timeout: timeoutMs, promise: true }).then((v) => String(v ?? ""));
|
|
11817
|
-
const timeoutPromise = new Promise((
|
|
12635
|
+
const timeoutPromise = new Promise((resolve2) => {
|
|
11818
12636
|
const t = setTimeout(() => {
|
|
11819
12637
|
dispose();
|
|
11820
|
-
|
|
12638
|
+
resolve2(`Dynamic tool "${this.name}" timed out after ${timeoutMs}ms and was terminated.`);
|
|
11821
12639
|
}, timeoutMs + 500);
|
|
11822
12640
|
t.unref?.();
|
|
11823
12641
|
});
|
|
@@ -11835,7 +12653,7 @@ var DynamicTool = class extends BaseTool {
|
|
|
11835
12653
|
/** Spawn the worker, service its callTool/fetch bridge, enforce the kill timeout. */
|
|
11836
12654
|
runInWorker(input, callTool) {
|
|
11837
12655
|
const timeoutMs = Math.max(200, Number(process.env["CASCADE_DYNAMIC_TOOL_TIMEOUT_MS"]) || DYNAMIC_TOOL_TIMEOUT_MS);
|
|
11838
|
-
return new Promise((
|
|
12656
|
+
return new Promise((resolve2) => {
|
|
11839
12657
|
let settled = false;
|
|
11840
12658
|
const worker = new Worker(HARNESS_SRC, {
|
|
11841
12659
|
eval: true,
|
|
@@ -11847,7 +12665,7 @@ var DynamicTool = class extends BaseTool {
|
|
|
11847
12665
|
settled = true;
|
|
11848
12666
|
clearTimeout(timer);
|
|
11849
12667
|
void worker.terminate();
|
|
11850
|
-
|
|
12668
|
+
resolve2(value);
|
|
11851
12669
|
};
|
|
11852
12670
|
const timer = setTimeout(
|
|
11853
12671
|
() => finish(`Dynamic tool "${this.name}" timed out after ${timeoutMs}ms and was terminated.`),
|
|
@@ -13393,6 +14211,7 @@ ${prompt}`;
|
|
|
13393
14211
|
var Cascade = class _Cascade extends EventEmitter {
|
|
13394
14212
|
router;
|
|
13395
14213
|
toolRegistry;
|
|
14214
|
+
multimodal;
|
|
13396
14215
|
mcpClient;
|
|
13397
14216
|
codeIndex;
|
|
13398
14217
|
config;
|
|
@@ -13431,6 +14250,10 @@ var Cascade = class _Cascade extends EventEmitter {
|
|
|
13431
14250
|
}
|
|
13432
14251
|
});
|
|
13433
14252
|
this.toolRegistry = new ToolRegistry(this.config.tools, workspacePath);
|
|
14253
|
+
this.multimodal = new MultimodalRegistry(
|
|
14254
|
+
(this.config.providers ?? []).map((p) => p.type)
|
|
14255
|
+
);
|
|
14256
|
+
this.registerMediaTools(workspacePath);
|
|
13434
14257
|
this.telemetry = config.telemetry?.enabled ? new Telemetry(config.telemetry, config.telemetry.distinctId ?? "anonymous") : noopTelemetry;
|
|
13435
14258
|
this.worldStateDB = new WorldStateDB(this.workspacePath, this.config.workspace?.debugWorldState ?? false);
|
|
13436
14259
|
this.router.setWorldStateDB(this.worldStateDB);
|
|
@@ -13484,14 +14307,14 @@ var Cascade = class _Cascade extends EventEmitter {
|
|
|
13484
14307
|
if (this.listenerCount("mcp:approval-required") === 0) {
|
|
13485
14308
|
return false;
|
|
13486
14309
|
}
|
|
13487
|
-
return await new Promise((
|
|
13488
|
-
this.pendingMcpApprovals.set(server.name,
|
|
14310
|
+
return await new Promise((resolve2) => {
|
|
14311
|
+
this.pendingMcpApprovals.set(server.name, resolve2);
|
|
13489
14312
|
const timeout = setTimeout(() => {
|
|
13490
|
-
if (this.pendingMcpApprovals.delete(server.name))
|
|
14313
|
+
if (this.pendingMcpApprovals.delete(server.name)) resolve2(false);
|
|
13491
14314
|
}, 3e4);
|
|
13492
14315
|
const wrap = (approved) => {
|
|
13493
14316
|
clearTimeout(timeout);
|
|
13494
|
-
|
|
14317
|
+
resolve2(approved);
|
|
13495
14318
|
};
|
|
13496
14319
|
this.pendingMcpApprovals.set(server.name, wrap);
|
|
13497
14320
|
this.emit("mcp:approval-required", { server });
|
|
@@ -13565,17 +14388,17 @@ var Cascade = class _Cascade extends EventEmitter {
|
|
|
13565
14388
|
}
|
|
13566
14389
|
const t2Count = plan.sections.length;
|
|
13567
14390
|
const t3Count = plan.sections.reduce((sum, s) => sum + (s.t3Subtasks?.length ?? 0), 0);
|
|
13568
|
-
return await new Promise((
|
|
14391
|
+
return await new Promise((resolve2) => {
|
|
13569
14392
|
const timeout = setTimeout(() => {
|
|
13570
14393
|
if (this.pendingPlanApproval) {
|
|
13571
14394
|
this.pendingPlanApproval = void 0;
|
|
13572
|
-
|
|
14395
|
+
resolve2({ approved: true });
|
|
13573
14396
|
}
|
|
13574
14397
|
}, 12e4);
|
|
13575
14398
|
this.pendingPlanApproval = (decision) => {
|
|
13576
14399
|
clearTimeout(timeout);
|
|
13577
14400
|
this.pendingPlanApproval = void 0;
|
|
13578
|
-
|
|
14401
|
+
resolve2(decision);
|
|
13579
14402
|
};
|
|
13580
14403
|
this.emit("plan:approval-required", {
|
|
13581
14404
|
taskId,
|
|
@@ -13608,17 +14431,17 @@ var Cascade = class _Cascade extends EventEmitter {
|
|
|
13608
14431
|
}
|
|
13609
14432
|
async requestContextApproval(info) {
|
|
13610
14433
|
if (this.listenerCount("context:approval-required") === 0) return true;
|
|
13611
|
-
return await new Promise((
|
|
14434
|
+
return await new Promise((resolve2) => {
|
|
13612
14435
|
const timeout = setTimeout(() => {
|
|
13613
14436
|
if (this.pendingContextApproval) {
|
|
13614
14437
|
this.pendingContextApproval = void 0;
|
|
13615
|
-
|
|
14438
|
+
resolve2(true);
|
|
13616
14439
|
}
|
|
13617
14440
|
}, 12e4);
|
|
13618
14441
|
this.pendingContextApproval = (approved) => {
|
|
13619
14442
|
clearTimeout(timeout);
|
|
13620
14443
|
this.pendingContextApproval = void 0;
|
|
13621
|
-
|
|
14444
|
+
resolve2(approved);
|
|
13622
14445
|
};
|
|
13623
14446
|
this.emit("context:approval-required", info);
|
|
13624
14447
|
});
|
|
@@ -13698,6 +14521,55 @@ ${input}` }],
|
|
|
13698
14521
|
if (this.store) t1.setStore(this.store);
|
|
13699
14522
|
return t1.previewPlan(prompt);
|
|
13700
14523
|
}
|
|
14524
|
+
/**
|
|
14525
|
+
* Register image / speech / transcription tools for the modalities this
|
|
14526
|
+
* config can actually serve.
|
|
14527
|
+
*
|
|
14528
|
+
* The sink writes bytes into the workspace, because that is what a desktop or
|
|
14529
|
+
* CLI run wants and it keeps a generated asset indistinguishable from any
|
|
14530
|
+
* other file the run produced. A host with different storage (the cloud
|
|
14531
|
+
* server stores a file row) constructs the tools itself via buildMediaTools
|
|
14532
|
+
* with its own sink.
|
|
14533
|
+
*/
|
|
14534
|
+
registerMediaTools(workspacePath) {
|
|
14535
|
+
const allowlist = this.config.tools?.enabledTools;
|
|
14536
|
+
const permitted = (name) => !allowlist || allowlist.includes(name);
|
|
14537
|
+
const tools = buildMediaTools(
|
|
14538
|
+
{
|
|
14539
|
+
registry: this.multimodal,
|
|
14540
|
+
lookupProvider: (provider) => (this.config.providers ?? []).find((p) => p.type === provider),
|
|
14541
|
+
sink: async (asset) => {
|
|
14542
|
+
const fsp = await import('fs/promises');
|
|
14543
|
+
const nodePath = await import('path');
|
|
14544
|
+
const dir = nodePath.join(workspacePath, "generated");
|
|
14545
|
+
await fsp.mkdir(dir, { recursive: true });
|
|
14546
|
+
const dest = nodePath.join(dir, asset.filename);
|
|
14547
|
+
await fsp.writeFile(dest, asset.data);
|
|
14548
|
+
return nodePath.relative(workspacePath, dest);
|
|
14549
|
+
}
|
|
14550
|
+
},
|
|
14551
|
+
async (p) => {
|
|
14552
|
+
const fsp = await import('fs/promises');
|
|
14553
|
+
return fsp.readFile(p);
|
|
14554
|
+
}
|
|
14555
|
+
);
|
|
14556
|
+
for (const tool of tools) {
|
|
14557
|
+
if (!permitted(tool.name)) continue;
|
|
14558
|
+
tool.setWorkspaceRoot(workspacePath);
|
|
14559
|
+
this.toolRegistry.register(tool);
|
|
14560
|
+
}
|
|
14561
|
+
}
|
|
14562
|
+
/**
|
|
14563
|
+
* Supply per-model thumbs-up/down counts so Auto routing can weigh how models
|
|
14564
|
+
* have actually performed for this user, on top of public benchmarks.
|
|
14565
|
+
*
|
|
14566
|
+
* Optional everywhere: with no source, or an empty record, scoring is
|
|
14567
|
+
* byte-identical to before. The adjustment is capped and sample-size shrunk —
|
|
14568
|
+
* see core/router/feedback-prior.ts.
|
|
14569
|
+
*/
|
|
14570
|
+
setFeedbackSource(source) {
|
|
14571
|
+
this.router.setFeedbackSource(source);
|
|
14572
|
+
}
|
|
13701
14573
|
/** True when a task stopped at the budget cap and can be resumed via /continue. */
|
|
13702
14574
|
hasResumableRun() {
|
|
13703
14575
|
return this.lastInterruptedRun != null;
|
|
@@ -14187,6 +15059,7 @@ ${prompt}` : prompt;
|
|
|
14187
15059
|
if (toolCreator) toolCreator.setPermissionEscalator(escalator);
|
|
14188
15060
|
const rootPrompt = buildContextualPrompt(options.prompt, options.conversationHistory);
|
|
14189
15061
|
let finalOutput = "";
|
|
15062
|
+
const runBreaker = new RunBreaker(this.config.budget?.failureThreshold);
|
|
14190
15063
|
let t2Results = [];
|
|
14191
15064
|
let runError = null;
|
|
14192
15065
|
let identityPrompt = "";
|
|
@@ -14255,6 +15128,7 @@ ${prompt}` : prompt;
|
|
|
14255
15128
|
this.emit("tier:status", { tierId: "t3-root", status: "COMPLETED", role: "T3" });
|
|
14256
15129
|
} else if (complexity === "Moderate") {
|
|
14257
15130
|
const t2 = new T2Manager(this.router, this.toolRegistry, "root");
|
|
15131
|
+
t2.setRunBreaker(runBreaker);
|
|
14258
15132
|
t2.setPresenter(true);
|
|
14259
15133
|
t2.setHierarchyContext("You are the ROOT Manager for this task. There is no T1 Administrator involved in this run. You are responsible for decomposing the task and managing T3 workers directly.");
|
|
14260
15134
|
if (identityPrompt) {
|
|
@@ -14315,6 +15189,7 @@ _(incomplete: ${reasons.join("; ")})_` : partial;
|
|
|
14315
15189
|
}
|
|
14316
15190
|
} else {
|
|
14317
15191
|
const t1 = new T1Administrator(this.router, this.toolRegistry, this.config);
|
|
15192
|
+
t1.setRunBreaker(runBreaker);
|
|
14318
15193
|
t1.setPresenter(true);
|
|
14319
15194
|
t1.setHierarchyContext("You are the top-level Administrator. You are responsible for the overall plan and supervising multiple T2 Managers.");
|
|
14320
15195
|
if (identityPrompt) {
|
|
@@ -14339,6 +15214,20 @@ _(incomplete: ${reasons.join("; ")})_` : partial;
|
|
|
14339
15214
|
finalOutput = result.output;
|
|
14340
15215
|
t2Results = result.t2Results;
|
|
14341
15216
|
}
|
|
15217
|
+
const trip = runBreaker.reason();
|
|
15218
|
+
if (trip) {
|
|
15219
|
+
finalOutput = [
|
|
15220
|
+
`\u26A0 **Run stopped early \u2014 ${trip.modelId} was failing every request.**`,
|
|
15221
|
+
"",
|
|
15222
|
+
trip.message,
|
|
15223
|
+
"",
|
|
15224
|
+
`Cascade stopped after ${trip.failures} consecutive failures rather than retrying every remaining subtask against the same model, which would have cost more and produced the same result.`,
|
|
15225
|
+
finalOutput ? `
|
|
15226
|
+
---
|
|
15227
|
+
|
|
15228
|
+
${finalOutput}` : ""
|
|
15229
|
+
].join("\n").trimEnd();
|
|
15230
|
+
}
|
|
14342
15231
|
} catch (err) {
|
|
14343
15232
|
if (err instanceof CascadeCancelledError || err instanceof Error && err.name === "AbortError" || options.signal?.aborted) {
|
|
14344
15233
|
this.emit("run:cancelled", {
|
|
@@ -16343,12 +17232,12 @@ var DashboardServer = class {
|
|
|
16343
17232
|
console.warn("\u26A0 Dashboard auth is DISABLED while bound to a non-loopback interface \u2014 this allows unauthenticated remote task execution.");
|
|
16344
17233
|
}
|
|
16345
17234
|
}
|
|
16346
|
-
await new Promise((
|
|
17235
|
+
await new Promise((resolve2, reject) => {
|
|
16347
17236
|
const onError = (err) => reject(err);
|
|
16348
17237
|
this.httpServer.once("error", onError);
|
|
16349
17238
|
this.httpServer.listen(this.port, this.host, () => {
|
|
16350
17239
|
this.httpServer.off("error", onError);
|
|
16351
|
-
|
|
17240
|
+
resolve2();
|
|
16352
17241
|
});
|
|
16353
17242
|
});
|
|
16354
17243
|
try {
|
|
@@ -16372,8 +17261,8 @@ var DashboardServer = class {
|
|
|
16372
17261
|
} catch {
|
|
16373
17262
|
}
|
|
16374
17263
|
this.globalStore = null;
|
|
16375
|
-
await new Promise((
|
|
16376
|
-
this.httpServer.close((err) => err ? reject(err) :
|
|
17264
|
+
await new Promise((resolve2, reject) => {
|
|
17265
|
+
this.httpServer.close((err) => err ? reject(err) : resolve2());
|
|
16377
17266
|
});
|
|
16378
17267
|
}
|
|
16379
17268
|
getSocket() {
|
|
@@ -16570,8 +17459,8 @@ var DashboardServer = class {
|
|
|
16570
17459
|
* timeout denies it.
|
|
16571
17460
|
*/
|
|
16572
17461
|
makeApprovalCallback(sessionId) {
|
|
16573
|
-
return (request) => new Promise((
|
|
16574
|
-
this.pendingApprovals.set(request.id, { resolve, sessionId });
|
|
17462
|
+
return (request) => new Promise((resolve2) => {
|
|
17463
|
+
this.pendingApprovals.set(request.id, { resolve: resolve2, sessionId });
|
|
16575
17464
|
});
|
|
16576
17465
|
}
|
|
16577
17466
|
/**
|
|
@@ -17883,8 +18772,8 @@ var CloudClient = class _CloudClient {
|
|
|
17883
18772
|
}
|
|
17884
18773
|
};
|
|
17885
18774
|
function sleep(ms, signal) {
|
|
17886
|
-
return new Promise((
|
|
17887
|
-
const t = setTimeout(
|
|
18775
|
+
return new Promise((resolve2, reject) => {
|
|
18776
|
+
const t = setTimeout(resolve2, ms);
|
|
17888
18777
|
signal?.addEventListener("abort", () => {
|
|
17889
18778
|
clearTimeout(t);
|
|
17890
18779
|
reject(new Error("aborted"));
|