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.cjs
CHANGED
|
@@ -1891,7 +1891,7 @@ function reconcilePrice(model, live) {
|
|
|
1891
1891
|
}
|
|
1892
1892
|
|
|
1893
1893
|
// src/constants.ts
|
|
1894
|
-
var CASCADE_VERSION = "0.
|
|
1894
|
+
var CASCADE_VERSION = "0.51.0";
|
|
1895
1895
|
var CASCADE_CONFIG_DIR = ".cascade";
|
|
1896
1896
|
var CASCADE_MD_FILE = "CASCADE.md";
|
|
1897
1897
|
var CASCADE_IGNORE_FILE = ".cascadeignore";
|
|
@@ -2404,46 +2404,37 @@ var BaseProvider = class {
|
|
|
2404
2404
|
};
|
|
2405
2405
|
|
|
2406
2406
|
// src/providers/model-filter.ts
|
|
2407
|
-
var
|
|
2408
|
-
"embed",
|
|
2407
|
+
var MODALITY_PATTERNS = [
|
|
2408
|
+
["embedding", ["embed"]],
|
|
2409
2409
|
// text-embedding-3, nomic-embed, mxbai-embed, snowflake-arctic-embed
|
|
2410
|
-
"(?:^|[-_/])
|
|
2411
|
-
"text-to-speech",
|
|
2412
|
-
"
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
"
|
|
2424
|
-
"
|
|
2425
|
-
"
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
"veo",
|
|
2436
|
-
"sora",
|
|
2437
|
-
"(?:^|[-_/])video(?:[-_/]|$)",
|
|
2438
|
-
// video
|
|
2439
|
-
"(?:^|[-_/])aqa(?:[-_/]|$)",
|
|
2440
|
-
// attributed QA
|
|
2441
|
-
"(?:^|[-_/])live(?:[-_/]|$)",
|
|
2442
|
-
// live-audio variants
|
|
2443
|
-
"(?:^|[-_/])(?:davinci|babbage)(?:[-_/]|$)"
|
|
2444
|
-
// legacy completion-only base models
|
|
2410
|
+
["transcription", ["whisper", "transcrib", "(?:^|[-_/])stt(?:[-_/]|$)"]],
|
|
2411
|
+
["speech", ["(?:^|[-_/])tts(?:[-_/]|$)", "text-to-speech", "speech"]],
|
|
2412
|
+
["video", ["veo", "sora", "(?:^|[-_/])video(?:[-_/]|$)"]],
|
|
2413
|
+
["image", [
|
|
2414
|
+
"dall[-]?e",
|
|
2415
|
+
"imagen",
|
|
2416
|
+
"image-generation",
|
|
2417
|
+
"gpt-image",
|
|
2418
|
+
"(?:^|[-_/])image(?:[-_/]|$)",
|
|
2419
|
+
"stable-diffusion",
|
|
2420
|
+
"sdxl",
|
|
2421
|
+
"flux"
|
|
2422
|
+
]],
|
|
2423
|
+
["moderation", ["moderation", "(?:^|[-_/])guard(?:[-_/]|$)"]],
|
|
2424
|
+
["rerank", ["rerank"]],
|
|
2425
|
+
["realtime", [
|
|
2426
|
+
"(?:^|[-_/])audio(?:[-_/]|$)",
|
|
2427
|
+
"native-audio",
|
|
2428
|
+
"realtime",
|
|
2429
|
+
"(?:^|[-_/])live(?:[-_/]|$)",
|
|
2430
|
+
// live-audio variants
|
|
2431
|
+
"(?:^|[-_/])aqa(?:[-_/]|$)"
|
|
2432
|
+
// attributed QA
|
|
2433
|
+
]],
|
|
2434
|
+
["legacy-completion", ["(?:^|[-_/])(?:davinci|babbage)(?:[-_/]|$)"]]
|
|
2445
2435
|
];
|
|
2446
|
-
|
|
2436
|
+
MODALITY_PATTERNS.map(([m, pats]) => [m, new RegExp(pats.join("|"), "i")]);
|
|
2437
|
+
var NON_CHAT = new RegExp(MODALITY_PATTERNS.flatMap(([, p]) => p).join("|"), "i");
|
|
2447
2438
|
function isChatModel(id, methods) {
|
|
2448
2439
|
if (methods && methods.length > 0) {
|
|
2449
2440
|
const canGenerate = methods.some((m) => /generatecontent|generatemessage|chat|completion/i.test(m));
|
|
@@ -2971,6 +2962,115 @@ var AzureOpenAIProvider = class extends OpenAIProvider {
|
|
|
2971
2962
|
}
|
|
2972
2963
|
}
|
|
2973
2964
|
};
|
|
2965
|
+
|
|
2966
|
+
// src/providers/gemini-schema.ts
|
|
2967
|
+
var ALLOWED_KEYS = /* @__PURE__ */ new Set([
|
|
2968
|
+
"type",
|
|
2969
|
+
"format",
|
|
2970
|
+
"title",
|
|
2971
|
+
"description",
|
|
2972
|
+
"nullable",
|
|
2973
|
+
"enum",
|
|
2974
|
+
"items",
|
|
2975
|
+
"properties",
|
|
2976
|
+
"required",
|
|
2977
|
+
"propertyOrdering",
|
|
2978
|
+
"minItems",
|
|
2979
|
+
"maxItems",
|
|
2980
|
+
"minProperties",
|
|
2981
|
+
"maxProperties",
|
|
2982
|
+
"minimum",
|
|
2983
|
+
"maximum",
|
|
2984
|
+
"minLength",
|
|
2985
|
+
"maxLength",
|
|
2986
|
+
"pattern",
|
|
2987
|
+
"example",
|
|
2988
|
+
"default",
|
|
2989
|
+
"anyOf"
|
|
2990
|
+
]);
|
|
2991
|
+
var ALLOWED_TYPES = /* @__PURE__ */ new Set(["string", "number", "integer", "boolean", "array", "object"]);
|
|
2992
|
+
function isPlainObject(v) {
|
|
2993
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
2994
|
+
}
|
|
2995
|
+
function normalizeType(value) {
|
|
2996
|
+
if (typeof value === "string") {
|
|
2997
|
+
return ALLOWED_TYPES.has(value) ? { type: value } : {};
|
|
2998
|
+
}
|
|
2999
|
+
if (Array.isArray(value)) {
|
|
3000
|
+
const types = value.filter((t) => typeof t === "string");
|
|
3001
|
+
const nullable = types.includes("null");
|
|
3002
|
+
const concrete = types.find((t) => t !== "null" && ALLOWED_TYPES.has(t));
|
|
3003
|
+
return {
|
|
3004
|
+
...concrete ? { type: concrete } : {},
|
|
3005
|
+
...nullable ? { nullable: true } : {}
|
|
3006
|
+
};
|
|
3007
|
+
}
|
|
3008
|
+
return {};
|
|
3009
|
+
}
|
|
3010
|
+
function normalizeEnum(value) {
|
|
3011
|
+
if (!Array.isArray(value) || value.length === 0) return void 0;
|
|
3012
|
+
return value.map((v) => typeof v === "string" ? v : JSON.stringify(v));
|
|
3013
|
+
}
|
|
3014
|
+
function sanitizeGeminiSchema(schema) {
|
|
3015
|
+
if (!isPlainObject(schema)) return void 0;
|
|
3016
|
+
const out = {};
|
|
3017
|
+
for (const [key, value] of Object.entries(schema)) {
|
|
3018
|
+
if (!ALLOWED_KEYS.has(key)) continue;
|
|
3019
|
+
switch (key) {
|
|
3020
|
+
case "type": {
|
|
3021
|
+
Object.assign(out, normalizeType(value));
|
|
3022
|
+
break;
|
|
3023
|
+
}
|
|
3024
|
+
case "enum": {
|
|
3025
|
+
const e = normalizeEnum(value);
|
|
3026
|
+
if (e) out["enum"] = e;
|
|
3027
|
+
break;
|
|
3028
|
+
}
|
|
3029
|
+
case "properties": {
|
|
3030
|
+
if (!isPlainObject(value)) break;
|
|
3031
|
+
const props = {};
|
|
3032
|
+
for (const [propName, propSchema] of Object.entries(value)) {
|
|
3033
|
+
const cleaned = sanitizeGeminiSchema(propSchema);
|
|
3034
|
+
props[propName] = cleaned && Object.keys(cleaned).length ? cleaned : { type: "string" };
|
|
3035
|
+
}
|
|
3036
|
+
out["properties"] = props;
|
|
3037
|
+
break;
|
|
3038
|
+
}
|
|
3039
|
+
case "items": {
|
|
3040
|
+
const cleaned = sanitizeGeminiSchema(value);
|
|
3041
|
+
if (cleaned) out["items"] = cleaned;
|
|
3042
|
+
break;
|
|
3043
|
+
}
|
|
3044
|
+
case "anyOf": {
|
|
3045
|
+
if (!Array.isArray(value)) break;
|
|
3046
|
+
const variants = value.map((v) => sanitizeGeminiSchema(v)).filter((v) => !!v && Object.keys(v).length > 0);
|
|
3047
|
+
if (variants.length) out["anyOf"] = variants;
|
|
3048
|
+
break;
|
|
3049
|
+
}
|
|
3050
|
+
case "required": {
|
|
3051
|
+
if (Array.isArray(value)) {
|
|
3052
|
+
out["required"] = value.filter((r) => typeof r === "string");
|
|
3053
|
+
}
|
|
3054
|
+
break;
|
|
3055
|
+
}
|
|
3056
|
+
default:
|
|
3057
|
+
out[key] = value;
|
|
3058
|
+
}
|
|
3059
|
+
}
|
|
3060
|
+
if (!out["type"] && out["properties"]) out["type"] = "object";
|
|
3061
|
+
if (!out["type"] && out["items"]) out["type"] = "array";
|
|
3062
|
+
return out;
|
|
3063
|
+
}
|
|
3064
|
+
function toGeminiParameters(inputSchema) {
|
|
3065
|
+
const cleaned = sanitizeGeminiSchema(inputSchema);
|
|
3066
|
+
if (!cleaned) return void 0;
|
|
3067
|
+
const props = cleaned["properties"];
|
|
3068
|
+
if (isPlainObject(props) && Object.keys(props).length === 0) return void 0;
|
|
3069
|
+
if (Object.keys(cleaned).length === 0) return void 0;
|
|
3070
|
+
return cleaned;
|
|
3071
|
+
}
|
|
3072
|
+
|
|
3073
|
+
// src/providers/gemini.ts
|
|
2974
3074
|
var GeminiProvider = class extends BaseProvider {
|
|
2975
3075
|
client;
|
|
2976
3076
|
constructor(config, model) {
|
|
@@ -3191,7 +3291,12 @@ var GeminiProvider = class extends BaseProvider {
|
|
|
3191
3291
|
return {
|
|
3192
3292
|
name: tool.name,
|
|
3193
3293
|
description: tool.description,
|
|
3194
|
-
parameters
|
|
3294
|
+
// Convert, never cast. Gemini's parameters field is an OpenAPI subset
|
|
3295
|
+
// that 400s on unknown keys, and MCP servers ship schemas carrying their
|
|
3296
|
+
// own extensions (GitHub's adds `x-mcp-header` to every header-bound
|
|
3297
|
+
// property). Casting sent those through verbatim and every request failed
|
|
3298
|
+
// before the model saw it. See providers/gemini-schema.ts.
|
|
3299
|
+
parameters: toGeminiParameters(tool.inputSchema)
|
|
3195
3300
|
};
|
|
3196
3301
|
}
|
|
3197
3302
|
};
|
|
@@ -3219,7 +3324,7 @@ async function nodeHttpFetch(input, init = {}, redirectCount = 0) {
|
|
|
3219
3324
|
headers["accept-encoding"] = "gzip, deflate, br";
|
|
3220
3325
|
}
|
|
3221
3326
|
const body = init.body == null ? void 0 : typeof init.body === "string" ? init.body : Buffer.from(init.body);
|
|
3222
|
-
return new Promise((
|
|
3327
|
+
return new Promise((resolve2, reject) => {
|
|
3223
3328
|
const req = lib.request(
|
|
3224
3329
|
{
|
|
3225
3330
|
hostname: u.hostname,
|
|
@@ -3236,7 +3341,7 @@ async function nodeHttpFetch(input, init = {}, redirectCount = 0) {
|
|
|
3236
3341
|
const nextUrl = new URL(location, u).href;
|
|
3237
3342
|
const downgrade = status === 303 || (status === 301 || status === 302) && method !== "GET" && method !== "HEAD";
|
|
3238
3343
|
const nextInit = downgrade ? { ...init, method: "GET", body: void 0 } : init;
|
|
3239
|
-
|
|
3344
|
+
resolve2(nodeHttpFetch(nextUrl, nextInit, redirectCount + 1));
|
|
3240
3345
|
return;
|
|
3241
3346
|
}
|
|
3242
3347
|
const encoding = (res.headers["content-encoding"] ?? "").toLowerCase();
|
|
@@ -3251,7 +3356,7 @@ async function nodeHttpFetch(input, init = {}, redirectCount = 0) {
|
|
|
3251
3356
|
if (Array.isArray(v)) respHeaders.set(k, v.join(", "));
|
|
3252
3357
|
else if (typeof v === "string") respHeaders.set(k, v);
|
|
3253
3358
|
}
|
|
3254
|
-
|
|
3359
|
+
resolve2(new Response(stream$1, {
|
|
3255
3360
|
status,
|
|
3256
3361
|
statusText: res.statusMessage ?? "",
|
|
3257
3362
|
headers: respHeaders
|
|
@@ -3896,7 +4001,7 @@ var TpmLimiter = class {
|
|
|
3896
4001
|
}
|
|
3897
4002
|
const deficit = want - bucket.available;
|
|
3898
4003
|
const waitMs = Math.max(50, Math.ceil(deficit / bucket.tokensPerMinute * 6e4));
|
|
3899
|
-
await new Promise((
|
|
4004
|
+
await new Promise((resolve2) => setTimeout(resolve2, waitMs));
|
|
3900
4005
|
}
|
|
3901
4006
|
}
|
|
3902
4007
|
/**
|
|
@@ -3959,14 +4064,14 @@ var LocalRequestQueue = class {
|
|
|
3959
4064
|
this.active++;
|
|
3960
4065
|
return this.makeRelease();
|
|
3961
4066
|
}
|
|
3962
|
-
return new Promise((
|
|
4067
|
+
return new Promise((resolve2, reject) => {
|
|
3963
4068
|
let settled = false;
|
|
3964
4069
|
let timer;
|
|
3965
4070
|
const resolver = (release) => {
|
|
3966
4071
|
if (settled) return;
|
|
3967
4072
|
settled = true;
|
|
3968
4073
|
if (timer !== void 0) clearTimeout(timer);
|
|
3969
|
-
|
|
4074
|
+
resolve2(release);
|
|
3970
4075
|
};
|
|
3971
4076
|
if (timeoutMs !== void 0 && timeoutMs > 0) {
|
|
3972
4077
|
timer = setTimeout(() => {
|
|
@@ -4597,6 +4702,7 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
|
|
|
4597
4702
|
tpmLimiter;
|
|
4598
4703
|
localQueue;
|
|
4599
4704
|
taskAnalyzer;
|
|
4705
|
+
pendingFeedbackSource;
|
|
4600
4706
|
worldStateDB;
|
|
4601
4707
|
privacyPaths;
|
|
4602
4708
|
guidanceQueue;
|
|
@@ -5051,8 +5157,18 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
|
|
|
5051
5157
|
return windows.length ? Math.min(...windows) : void 0;
|
|
5052
5158
|
}
|
|
5053
5159
|
/** Wire the Cascade Auto task analyzer used for per-subtask model routing. */
|
|
5160
|
+
/**
|
|
5161
|
+
* Feed per-model thumbs counts into Auto routing. Forwarded to the analyzer
|
|
5162
|
+
* because that is where the public benchmark score is combined; the prior
|
|
5163
|
+
* adjusts that number rather than sitting anywhere else in the pipeline.
|
|
5164
|
+
*/
|
|
5165
|
+
setFeedbackSource(source) {
|
|
5166
|
+
this.taskAnalyzer?.setFeedbackSource(source);
|
|
5167
|
+
this.pendingFeedbackSource = source;
|
|
5168
|
+
}
|
|
5054
5169
|
setTaskAnalyzer(analyzer) {
|
|
5055
5170
|
this.taskAnalyzer = analyzer;
|
|
5171
|
+
if (this.pendingFeedbackSource) analyzer.setFeedbackSource(this.pendingFeedbackSource);
|
|
5056
5172
|
}
|
|
5057
5173
|
setWorldStateDB(db) {
|
|
5058
5174
|
this.worldStateDB = db;
|
|
@@ -5507,6 +5623,14 @@ var BaseTier = class extends EventEmitter__default.default {
|
|
|
5507
5623
|
this.emit("tier:status", event);
|
|
5508
5624
|
}
|
|
5509
5625
|
/** Record the model serving this tier; future status events carry it. */
|
|
5626
|
+
/**
|
|
5627
|
+
* The model actually serving this tier ("provider:id"), once one has been
|
|
5628
|
+
* chosen. The run breaker reads it to attribute a failure to a model rather
|
|
5629
|
+
* than to the subtask that happened to hit it.
|
|
5630
|
+
*/
|
|
5631
|
+
getServingModel() {
|
|
5632
|
+
return this.servingModel;
|
|
5633
|
+
}
|
|
5510
5634
|
setServingModel(model) {
|
|
5511
5635
|
this.servingModel = model || void 0;
|
|
5512
5636
|
}
|
|
@@ -6138,8 +6262,11 @@ Now execute your subtask using this context where relevant.`
|
|
|
6138
6262
|
const result = await this.runAgentLoop(systemPrompt, this.tools);
|
|
6139
6263
|
output = result.output;
|
|
6140
6264
|
toolCalls = result.toolCalls;
|
|
6141
|
-
this.
|
|
6142
|
-
|
|
6265
|
+
const mustProduceArtifact = this.requiresArtifact();
|
|
6266
|
+
if (mustProduceArtifact) {
|
|
6267
|
+
this.sendStatusUpdate({ progressPct: 65, currentAction: "Verifying required artifacts", status: "IN_PROGRESS" });
|
|
6268
|
+
}
|
|
6269
|
+
const artifactCheck = mustProduceArtifact ? await this.verifyArtifacts(assignment) : { ok: true, issues: [] };
|
|
6143
6270
|
if (!artifactCheck.ok) {
|
|
6144
6271
|
correctionAttempts = 1;
|
|
6145
6272
|
issues.push(...artifactCheck.issues);
|
|
@@ -6419,11 +6546,11 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : "") + textToolSuffix,
|
|
|
6419
6546
|
if (!wasApproved) return `Tool ${tc.name} was denied by user.`;
|
|
6420
6547
|
} else {
|
|
6421
6548
|
const LEGACY_APPROVAL_TIMEOUT_MS = 6e5;
|
|
6422
|
-
const legacyDecision = await new Promise((
|
|
6549
|
+
const legacyDecision = await new Promise((resolve2) => {
|
|
6423
6550
|
const eventName = `tool:approval-response:${this.id}-${tc.id}`;
|
|
6424
6551
|
const timer = setTimeout(() => {
|
|
6425
6552
|
this.removeAllListeners(eventName);
|
|
6426
|
-
|
|
6553
|
+
resolve2({ approved: false });
|
|
6427
6554
|
}, LEGACY_APPROVAL_TIMEOUT_MS);
|
|
6428
6555
|
timer.unref?.();
|
|
6429
6556
|
this.emit("tool:approval-request", {
|
|
@@ -6436,7 +6563,7 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : "") + textToolSuffix,
|
|
|
6436
6563
|
});
|
|
6437
6564
|
this.once(eventName, (d) => {
|
|
6438
6565
|
clearTimeout(timer);
|
|
6439
|
-
|
|
6566
|
+
resolve2(d);
|
|
6440
6567
|
});
|
|
6441
6568
|
});
|
|
6442
6569
|
if (legacyDecision.always) this.sessionApprovals.set(tc.name, legacyDecision.approved);
|
|
@@ -6456,6 +6583,9 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : "") + textToolSuffix,
|
|
|
6456
6583
|
tierId: this.id,
|
|
6457
6584
|
sessionId: this.taskId,
|
|
6458
6585
|
requireApproval: false,
|
|
6586
|
+
// Media generation can run for a minute; without this a cancelled run
|
|
6587
|
+
// still pays for an image nobody will see.
|
|
6588
|
+
...this.signal ? { signal: this.signal } : {},
|
|
6459
6589
|
saveSnapshot: async (path26, content) => {
|
|
6460
6590
|
this.store?.addFileSnapshot(this.taskId, path26, content);
|
|
6461
6591
|
},
|
|
@@ -6503,7 +6633,8 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : "") + textToolSuffix,
|
|
|
6503
6633
|
const result = await this.toolRegistry.execute(altTool, tc.input, {
|
|
6504
6634
|
tierId: this.id,
|
|
6505
6635
|
sessionId: this.taskId,
|
|
6506
|
-
requireApproval: false
|
|
6636
|
+
requireApproval: false,
|
|
6637
|
+
...this.signal ? { signal: this.signal } : {}
|
|
6507
6638
|
});
|
|
6508
6639
|
const str = typeof result === "string" ? result : JSON.stringify(result);
|
|
6509
6640
|
if (!str.startsWith("Tool error:") && !str.startsWith("Error:")) {
|
|
@@ -6525,7 +6656,8 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : "") + textToolSuffix,
|
|
|
6525
6656
|
const result = await this.toolRegistry.execute(newToolName, tc.input, {
|
|
6526
6657
|
tierId: this.id,
|
|
6527
6658
|
sessionId: this.taskId,
|
|
6528
|
-
requireApproval: false
|
|
6659
|
+
requireApproval: false,
|
|
6660
|
+
...this.signal ? { signal: this.signal } : {}
|
|
6529
6661
|
});
|
|
6530
6662
|
const str = typeof result === "string" ? result : JSON.stringify(result);
|
|
6531
6663
|
if (!str.startsWith("Tool error:")) return `[Synthesized ${newToolName}]: ${str}`;
|
|
@@ -6943,7 +7075,7 @@ var PeerBus = class extends EventEmitter__default.default {
|
|
|
6943
7075
|
this.outputs.set(subtaskId, entry);
|
|
6944
7076
|
this.emit("output:ready", entry);
|
|
6945
7077
|
const waiting = this.waiters.get(subtaskId) ?? [];
|
|
6946
|
-
for (const
|
|
7078
|
+
for (const resolve2 of waiting) resolve2(entry);
|
|
6947
7079
|
this.waiters.delete(subtaskId);
|
|
6948
7080
|
}
|
|
6949
7081
|
/**
|
|
@@ -6974,10 +7106,10 @@ var PeerBus = class extends EventEmitter__default.default {
|
|
|
6974
7106
|
waitFor(subtaskId, timeoutMs = 12e4) {
|
|
6975
7107
|
const existing = this.outputs.get(subtaskId);
|
|
6976
7108
|
if (existing && !this.retryPending.has(subtaskId)) return Promise.resolve(existing);
|
|
6977
|
-
return new Promise((
|
|
7109
|
+
return new Promise((resolve2, reject) => {
|
|
6978
7110
|
const resolver = (output) => {
|
|
6979
7111
|
clearTimeout(timer);
|
|
6980
|
-
|
|
7112
|
+
resolve2(output);
|
|
6981
7113
|
};
|
|
6982
7114
|
const timer = setTimeout(() => {
|
|
6983
7115
|
const waiting = this.waiters.get(subtaskId);
|
|
@@ -7031,10 +7163,10 @@ var PeerBus = class extends EventEmitter__default.default {
|
|
|
7031
7163
|
*/
|
|
7032
7164
|
collect(timeoutMs) {
|
|
7033
7165
|
const collected = [...this.broadcastLog];
|
|
7034
|
-
return new Promise((
|
|
7166
|
+
return new Promise((resolve2) => {
|
|
7035
7167
|
const timer = setTimeout(() => {
|
|
7036
7168
|
off();
|
|
7037
|
-
|
|
7169
|
+
resolve2(collected);
|
|
7038
7170
|
}, timeoutMs);
|
|
7039
7171
|
const handler = (msg) => {
|
|
7040
7172
|
collected.push({ fromId: msg.fromId, payload: msg.payload, timestamp: msg.timestamp });
|
|
@@ -7048,7 +7180,7 @@ var PeerBus = class extends EventEmitter__default.default {
|
|
|
7048
7180
|
const broadcasters = new Set(collected.map((m) => m.fromId));
|
|
7049
7181
|
if (broadcasters.size >= this.members.size) {
|
|
7050
7182
|
off();
|
|
7051
|
-
|
|
7183
|
+
resolve2(collected);
|
|
7052
7184
|
}
|
|
7053
7185
|
};
|
|
7054
7186
|
this.on("broadcast", checkComplete);
|
|
@@ -7066,7 +7198,7 @@ var PeerBus = class extends EventEmitter__default.default {
|
|
|
7066
7198
|
return;
|
|
7067
7199
|
}
|
|
7068
7200
|
this.emitCoordination(tierId, `\u23F3 waiting for ${filePath} (held by ${existing.holderId})`);
|
|
7069
|
-
return new Promise((
|
|
7201
|
+
return new Promise((resolve2, reject) => {
|
|
7070
7202
|
const timer = setTimeout(() => {
|
|
7071
7203
|
reject(new Error(`File lock timeout for ${filePath} (held by ${existing.holderId})`));
|
|
7072
7204
|
}, timeoutMs);
|
|
@@ -7074,7 +7206,7 @@ var PeerBus = class extends EventEmitter__default.default {
|
|
|
7074
7206
|
clearTimeout(timer);
|
|
7075
7207
|
this.fileLocks.set(filePath, { holderId: tierId, lockedAt: (/* @__PURE__ */ new Date()).toISOString(), waiters: [] });
|
|
7076
7208
|
this.emitCoordination(tierId, `\u{1F512} locked ${filePath}`);
|
|
7077
|
-
|
|
7209
|
+
resolve2();
|
|
7078
7210
|
});
|
|
7079
7211
|
});
|
|
7080
7212
|
}
|
|
@@ -7099,11 +7231,11 @@ var PeerBus = class extends EventEmitter__default.default {
|
|
|
7099
7231
|
*/
|
|
7100
7232
|
waitForFileRelease(filePath, timeoutMs = 3e4) {
|
|
7101
7233
|
if (!this.fileLocks.has(filePath)) return Promise.resolve();
|
|
7102
|
-
return new Promise((
|
|
7234
|
+
return new Promise((resolve2, reject) => {
|
|
7103
7235
|
const timer = setTimeout(() => reject(new Error(`Timeout waiting for file release: ${filePath}`)), timeoutMs);
|
|
7104
7236
|
this.once(`file:released:${filePath}`, () => {
|
|
7105
7237
|
clearTimeout(timer);
|
|
7106
|
-
|
|
7238
|
+
resolve2();
|
|
7107
7239
|
});
|
|
7108
7240
|
});
|
|
7109
7241
|
}
|
|
@@ -7169,13 +7301,13 @@ var PeerBus = class extends EventEmitter__default.default {
|
|
|
7169
7301
|
this.emit(`barrier:${barrierName}`);
|
|
7170
7302
|
return;
|
|
7171
7303
|
}
|
|
7172
|
-
return new Promise((
|
|
7304
|
+
return new Promise((resolve2, reject) => {
|
|
7173
7305
|
const timer = setTimeout(() => {
|
|
7174
7306
|
reject(new Error(`Barrier timeout: ${barrierName} (${bar.arrived.size}/${bar.total} arrived)`));
|
|
7175
7307
|
}, 12e4);
|
|
7176
7308
|
this.once(`barrier:${barrierName}`, () => {
|
|
7177
7309
|
clearTimeout(timer);
|
|
7178
|
-
|
|
7310
|
+
resolve2();
|
|
7179
7311
|
});
|
|
7180
7312
|
});
|
|
7181
7313
|
}
|
|
@@ -7242,6 +7374,7 @@ var T2Manager = class extends BaseTier {
|
|
|
7242
7374
|
escalations = [];
|
|
7243
7375
|
peerSyncBuffer = [];
|
|
7244
7376
|
store;
|
|
7377
|
+
runBreaker;
|
|
7245
7378
|
t3PeerBus = new PeerBus();
|
|
7246
7379
|
// ← T3↔T3 bus (local to this T2)
|
|
7247
7380
|
t2PeerBus;
|
|
@@ -7272,6 +7405,15 @@ var T2Manager = class extends BaseTier {
|
|
|
7272
7405
|
this.router = router;
|
|
7273
7406
|
this.toolRegistry = toolRegistry;
|
|
7274
7407
|
}
|
|
7408
|
+
/**
|
|
7409
|
+
* Share the run-wide circuit breaker. Every T2 in a run gets the SAME
|
|
7410
|
+
* instance: three sections each failing once against a dead model is the
|
|
7411
|
+
* same fact as one section failing three times, and a per-section breaker
|
|
7412
|
+
* would never see it.
|
|
7413
|
+
*/
|
|
7414
|
+
setRunBreaker(breaker) {
|
|
7415
|
+
this.runBreaker = breaker;
|
|
7416
|
+
}
|
|
7275
7417
|
setStore(store) {
|
|
7276
7418
|
this.store = store;
|
|
7277
7419
|
}
|
|
@@ -7395,7 +7537,8 @@ Guidance (must be followed): ${decision.note}`
|
|
|
7395
7537
|
const overallStatus = this.determineStatus(t3Results);
|
|
7396
7538
|
const isOk = overallStatus === "COMPLETED" || overallStatus === "PARTIAL";
|
|
7397
7539
|
this.setStatus(isOk ? "COMPLETED" : "FAILED", summary);
|
|
7398
|
-
|
|
7540
|
+
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";
|
|
7541
|
+
this.sendStatusUpdate({ progressPct: 100, currentAction: sectionAction, status: "IN_PROGRESS", output: summary });
|
|
7399
7542
|
const result = {
|
|
7400
7543
|
sectionId: assignment.sectionId,
|
|
7401
7544
|
sectionTitle: assignment.sectionTitle,
|
|
@@ -7565,6 +7708,24 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
|
7565
7708
|
const reinforceCfg = this.router.getReinforcementsConfig?.() ?? { enabled: false, maxPerSection: 4 };
|
|
7566
7709
|
let reinforcementsAdded = 0;
|
|
7567
7710
|
while (remaining.size > 0) {
|
|
7711
|
+
if (this.runBreaker?.isOpen()) {
|
|
7712
|
+
const why = this.runBreaker.skipMessage();
|
|
7713
|
+
this.log(`Run breaker open \u2014 skipping ${remaining.size} remaining subtask(s)`);
|
|
7714
|
+
for (const id of remaining) {
|
|
7715
|
+
this.t3PeerBus.publish(this.id, id, why, "FAILED");
|
|
7716
|
+
resultMap.set(id, {
|
|
7717
|
+
subtaskId: id,
|
|
7718
|
+
status: "FAILED",
|
|
7719
|
+
output: why,
|
|
7720
|
+
testResults: { checksRun: [], passed: [], failed: [] },
|
|
7721
|
+
issues: [why],
|
|
7722
|
+
peerSyncsUsed: [],
|
|
7723
|
+
correctionAttempts: 0
|
|
7724
|
+
});
|
|
7725
|
+
}
|
|
7726
|
+
remaining.clear();
|
|
7727
|
+
break;
|
|
7728
|
+
}
|
|
7568
7729
|
const runnableIds = [...remaining].filter((id) => (inDegree.get(id) ?? 0) === 0);
|
|
7569
7730
|
if (runnableIds.length === 0) {
|
|
7570
7731
|
const fallbackId = [...remaining].sort(
|
|
@@ -7667,8 +7828,32 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
|
7667
7828
|
remaining.delete(id);
|
|
7668
7829
|
const r = waveResults[i];
|
|
7669
7830
|
if (r.status === "rejected") {
|
|
7670
|
-
this.log(`T3 worker ${id} failed: ${r.reason instanceof Error ? r.reason.message : String(r.reason)} \u2014 retrying once`);
|
|
7671
7831
|
const assignment = sanitizedAssignments.find((a) => a.subtaskId === id);
|
|
7832
|
+
const failedModel = workerMap.get(id)?.getServingModel();
|
|
7833
|
+
const tripped = this.runBreaker?.record(r.reason, failedModel);
|
|
7834
|
+
if (tripped) {
|
|
7835
|
+
this.log(`Run breaker opened: ${tripped.failures} consecutive ${tripped.kind} failures on ${tripped.modelId} \u2014 stopping`);
|
|
7836
|
+
this.emit("run:breaker", tripped);
|
|
7837
|
+
}
|
|
7838
|
+
if (this.runBreaker?.isOpen()) {
|
|
7839
|
+
const why = this.runBreaker.skipMessage();
|
|
7840
|
+
this.log(`T3 worker ${id} failed and the run breaker is open \u2014 not retrying`);
|
|
7841
|
+
this.t3PeerBus.publish(this.id, id, why, "FAILED");
|
|
7842
|
+
resultMap.set(id, {
|
|
7843
|
+
subtaskId: id,
|
|
7844
|
+
status: "FAILED",
|
|
7845
|
+
output: why,
|
|
7846
|
+
testResults: { checksRun: [], passed: [], failed: [] },
|
|
7847
|
+
issues: [why],
|
|
7848
|
+
peerSyncsUsed: [],
|
|
7849
|
+
correctionAttempts: 0
|
|
7850
|
+
});
|
|
7851
|
+
for (const dependent of adj.get(id) ?? []) {
|
|
7852
|
+
inDegree.set(dependent, Math.max(0, (inDegree.get(dependent) ?? 0) - 1));
|
|
7853
|
+
}
|
|
7854
|
+
continue;
|
|
7855
|
+
}
|
|
7856
|
+
this.log(`T3 worker ${id} failed: ${r.reason instanceof Error ? r.reason.message : String(r.reason)} \u2014 retrying once`);
|
|
7672
7857
|
try {
|
|
7673
7858
|
const retried = await this.retryT3(assignment, taskId);
|
|
7674
7859
|
resultMap.set(id, retried);
|
|
@@ -8000,6 +8185,7 @@ var T1Administrator = class extends BaseTier {
|
|
|
8000
8185
|
t2Managers = /* @__PURE__ */ new Map();
|
|
8001
8186
|
escalations = [];
|
|
8002
8187
|
store;
|
|
8188
|
+
runBreaker;
|
|
8003
8189
|
t2PeerBus = new PeerBus();
|
|
8004
8190
|
permissionEscalator;
|
|
8005
8191
|
toolCreator;
|
|
@@ -8014,6 +8200,10 @@ var T1Administrator = class extends BaseTier {
|
|
|
8014
8200
|
this.toolRegistry = toolRegistry;
|
|
8015
8201
|
this.config = config;
|
|
8016
8202
|
}
|
|
8203
|
+
/** Share the run-wide circuit breaker with every section manager. */
|
|
8204
|
+
setRunBreaker(breaker) {
|
|
8205
|
+
this.runBreaker = breaker;
|
|
8206
|
+
}
|
|
8017
8207
|
setStore(store) {
|
|
8018
8208
|
this.store = store;
|
|
8019
8209
|
}
|
|
@@ -8376,6 +8566,7 @@ SPEC RULES \u2014 each subtask is a self-contained spec slice (workers execute f
|
|
|
8376
8566
|
};
|
|
8377
8567
|
const managers = sections.map((section) => {
|
|
8378
8568
|
const manager = new T2Manager(this.router, this.toolRegistry, this.id);
|
|
8569
|
+
if (this.runBreaker) manager.setRunBreaker(this.runBreaker);
|
|
8379
8570
|
manager.setHierarchyContext(`You are a T2 Manager for the section "${section.sectionTitle}". You are part of a COMPLEX task overseen by T1 Administrator.`);
|
|
8380
8571
|
if (this.store) {
|
|
8381
8572
|
manager.setStore(this.store);
|
|
@@ -8661,6 +8852,156 @@ Reply with exactly one word: YES, NO, or UNSURE.
|
|
|
8661
8852
|
}
|
|
8662
8853
|
};
|
|
8663
8854
|
|
|
8855
|
+
// src/core/multimodal/registry.ts
|
|
8856
|
+
var CAPABILITIES = [
|
|
8857
|
+
// ── Image ──
|
|
8858
|
+
{
|
|
8859
|
+
modality: "image",
|
|
8860
|
+
provider: "openai",
|
|
8861
|
+
modelId: "dall-e-3",
|
|
8862
|
+
api: "openai-images"
|
|
8863
|
+
},
|
|
8864
|
+
{
|
|
8865
|
+
modality: "image",
|
|
8866
|
+
provider: "gemini",
|
|
8867
|
+
modelId: "gemini-3-pro-image",
|
|
8868
|
+
api: "gemini-predict"
|
|
8869
|
+
},
|
|
8870
|
+
// ── Speech (text → audio) ──
|
|
8871
|
+
{
|
|
8872
|
+
modality: "speech",
|
|
8873
|
+
provider: "openai",
|
|
8874
|
+
modelId: "tts-1",
|
|
8875
|
+
api: "openai-speech"
|
|
8876
|
+
},
|
|
8877
|
+
{
|
|
8878
|
+
// The only quality claim in this table that needs no leaderboard: OpenAI
|
|
8879
|
+
// ships these as the same voice at two fidelities and charges double for
|
|
8880
|
+
// this one. That is the vendor's own ordering, not an inference.
|
|
8881
|
+
modality: "speech",
|
|
8882
|
+
provider: "openai",
|
|
8883
|
+
modelId: "tts-1-hd",
|
|
8884
|
+
api: "openai-speech",
|
|
8885
|
+
quality: 70,
|
|
8886
|
+
qualityBasis: "OpenAI's own hd variant of tts-1, priced 2x"
|
|
8887
|
+
},
|
|
8888
|
+
// ── Transcription (audio → text) ──
|
|
8889
|
+
{
|
|
8890
|
+
modality: "transcription",
|
|
8891
|
+
provider: "openai",
|
|
8892
|
+
modelId: "whisper-1",
|
|
8893
|
+
api: "openai-transcriptions"
|
|
8894
|
+
},
|
|
8895
|
+
// ── Video ──
|
|
8896
|
+
{
|
|
8897
|
+
// Listed so `describe()` can tell the truth about what exists, and so the
|
|
8898
|
+
// planner is told video is out of reach rather than left to invent a tool.
|
|
8899
|
+
// Veo is a long-running operation: submit, poll, then fetch bytes from a
|
|
8900
|
+
// signed URL. That is a different execution shape from every other entry
|
|
8901
|
+
// here and is not wired up — claiming otherwise would produce a tool that
|
|
8902
|
+
// fails at call time instead of an honest "not yet".
|
|
8903
|
+
modality: "video",
|
|
8904
|
+
provider: "gemini",
|
|
8905
|
+
modelId: "veo-3.1-generate-001",
|
|
8906
|
+
api: "gemini-predict",
|
|
8907
|
+
unsupported: "Veo runs as a long-running operation (submit \u2192 poll \u2192 fetch). Not wired up yet."
|
|
8908
|
+
}
|
|
8909
|
+
];
|
|
8910
|
+
function capabilityCost(cap) {
|
|
8911
|
+
const match = findPricing(cap.modelId, cap.provider, {
|
|
8912
|
+
fallbackProviders: cap.provider === "gemini" ? ["vertex"] : []
|
|
8913
|
+
});
|
|
8914
|
+
const rate = match?.entry.rates?.[0];
|
|
8915
|
+
if (!rate) return null;
|
|
8916
|
+
const unitLabel = {
|
|
8917
|
+
per_image: "per image",
|
|
8918
|
+
per_second: "per second of video",
|
|
8919
|
+
per_character: "per character",
|
|
8920
|
+
per_audio_minute: "per audio minute"
|
|
8921
|
+
};
|
|
8922
|
+
const unit = unitLabel[rate.unit] ?? rate.unit;
|
|
8923
|
+
const amount = rate.amount;
|
|
8924
|
+
const shown = amount < 1e-3 ? amount.toExponential(2) : amount.toFixed(4);
|
|
8925
|
+
return { label: `$${shown} ${unit}`, amount, unit };
|
|
8926
|
+
}
|
|
8927
|
+
var MultimodalRegistry = class {
|
|
8928
|
+
/** Provider types the user has actually configured a key for. */
|
|
8929
|
+
available;
|
|
8930
|
+
constructor(availableProviders) {
|
|
8931
|
+
this.available = new Set(availableProviders);
|
|
8932
|
+
}
|
|
8933
|
+
/** Every capability whose provider is configured AND which Cascade can call. */
|
|
8934
|
+
usable(modality) {
|
|
8935
|
+
return CAPABILITIES.filter(
|
|
8936
|
+
(c) => !c.unsupported && this.available.has(c.provider) && (!modality || c.modality === modality)
|
|
8937
|
+
);
|
|
8938
|
+
}
|
|
8939
|
+
/** Modalities this account can actually generate right now. */
|
|
8940
|
+
availableModalities() {
|
|
8941
|
+
return [...new Set(this.usable().map((c) => c.modality))];
|
|
8942
|
+
}
|
|
8943
|
+
/**
|
|
8944
|
+
* Pick a model for a modality.
|
|
8945
|
+
*
|
|
8946
|
+
* Ranked models beat unranked ones, then higher quality, then lower unit
|
|
8947
|
+
* price. An explicit `preferModel` wins outright when it is usable — the user
|
|
8948
|
+
* naming a model is a stronger signal than any prior.
|
|
8949
|
+
*/
|
|
8950
|
+
select(modality, opts = {}) {
|
|
8951
|
+
const pool = this.usable(modality);
|
|
8952
|
+
if (!pool.length) return null;
|
|
8953
|
+
if (opts.preferModel) {
|
|
8954
|
+
const wanted = pool.find((c) => c.modelId === opts.preferModel);
|
|
8955
|
+
if (wanted) {
|
|
8956
|
+
return { capability: wanted, cost: capabilityCost(wanted), reason: `Requested explicitly: ${wanted.modelId}.` };
|
|
8957
|
+
}
|
|
8958
|
+
}
|
|
8959
|
+
const scored = pool.map((c) => ({ c, cost: capabilityCost(c) })).sort((a, b) => {
|
|
8960
|
+
const qa = a.c.quality ?? -1;
|
|
8961
|
+
const qb = b.c.quality ?? -1;
|
|
8962
|
+
if (qa !== qb) return qb - qa;
|
|
8963
|
+
const pa = a.cost?.amount ?? Number.POSITIVE_INFINITY;
|
|
8964
|
+
const pb = b.cost?.amount ?? Number.POSITIVE_INFINITY;
|
|
8965
|
+
return pa - pb;
|
|
8966
|
+
});
|
|
8967
|
+
const best = scored[0];
|
|
8968
|
+
const ranked = best.c.quality !== void 0;
|
|
8969
|
+
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.";
|
|
8970
|
+
return { capability: best.c, cost: best.cost, reason };
|
|
8971
|
+
}
|
|
8972
|
+
/**
|
|
8973
|
+
* A plain-language account of what this account can and cannot generate —
|
|
8974
|
+
* including the things it cannot, and why. The planner sees this, so being
|
|
8975
|
+
* candid here is what stops it inventing a `generate_music` call.
|
|
8976
|
+
*/
|
|
8977
|
+
describe() {
|
|
8978
|
+
const lines = [];
|
|
8979
|
+
const usable = this.usable();
|
|
8980
|
+
if (usable.length) {
|
|
8981
|
+
lines.push("Generation models available on your configured providers:");
|
|
8982
|
+
for (const modality of ["image", "speech", "transcription", "video"]) {
|
|
8983
|
+
const forModality = usable.filter((c) => c.modality === modality);
|
|
8984
|
+
if (!forModality.length) continue;
|
|
8985
|
+
const shown = forModality.map((c) => {
|
|
8986
|
+
const cost = capabilityCost(c);
|
|
8987
|
+
return `${c.modelId}${cost ? ` (${cost.label})` : ""}`;
|
|
8988
|
+
});
|
|
8989
|
+
lines.push(` ${modality}: ${shown.join(", ")}`);
|
|
8990
|
+
}
|
|
8991
|
+
} else {
|
|
8992
|
+
lines.push("No generation models are available \u2014 none of your configured providers offers one.");
|
|
8993
|
+
}
|
|
8994
|
+
const blocked = CAPABILITIES.filter((c) => c.unsupported && this.available.has(c.provider));
|
|
8995
|
+
for (const c of blocked) {
|
|
8996
|
+
lines.push(` ${c.modality}: ${c.modelId} exists on your account but is not callable. ${c.unsupported}`);
|
|
8997
|
+
}
|
|
8998
|
+
lines.push(
|
|
8999
|
+
" music: no supported provider exposes a music-generation API, so Cascade cannot generate music."
|
|
9000
|
+
);
|
|
9001
|
+
return lines.join("\n");
|
|
9002
|
+
}
|
|
9003
|
+
};
|
|
9004
|
+
|
|
8664
9005
|
// src/tools/base.ts
|
|
8665
9006
|
var BaseTool = class {
|
|
8666
9007
|
workspaceRoot = process.cwd();
|
|
@@ -8679,7 +9020,446 @@ var BaseTool = class {
|
|
|
8679
9020
|
}
|
|
8680
9021
|
};
|
|
8681
9022
|
|
|
8682
|
-
// src/
|
|
9023
|
+
// src/core/router/provider-errors.ts
|
|
9024
|
+
var SYSTEMIC = /* @__PURE__ */ new Set([
|
|
9025
|
+
"rate_limit",
|
|
9026
|
+
"quota_exhausted",
|
|
9027
|
+
"auth",
|
|
9028
|
+
"model_unavailable"
|
|
9029
|
+
]);
|
|
9030
|
+
function isSystemicKind(kind) {
|
|
9031
|
+
return SYSTEMIC.has(kind);
|
|
9032
|
+
}
|
|
9033
|
+
function statusOf(err) {
|
|
9034
|
+
if (!err || typeof err !== "object") return void 0;
|
|
9035
|
+
const e = err;
|
|
9036
|
+
for (const key of ["status", "statusCode", "code"]) {
|
|
9037
|
+
const v = e[key];
|
|
9038
|
+
if (typeof v === "number" && v >= 100 && v < 600) return v;
|
|
9039
|
+
}
|
|
9040
|
+
const resp = e["response"];
|
|
9041
|
+
if (resp && typeof resp["status"] === "number") return resp["status"];
|
|
9042
|
+
const inner = e["error"];
|
|
9043
|
+
if (inner && typeof inner["code"] === "number") return inner["code"];
|
|
9044
|
+
return void 0;
|
|
9045
|
+
}
|
|
9046
|
+
function messageOf(err) {
|
|
9047
|
+
if (err instanceof Error) return err.message;
|
|
9048
|
+
if (typeof err === "string") return err;
|
|
9049
|
+
if (err && typeof err === "object") {
|
|
9050
|
+
const e = err;
|
|
9051
|
+
if (typeof e["message"] === "string") return e["message"];
|
|
9052
|
+
const inner = e["error"];
|
|
9053
|
+
if (inner && typeof inner["message"] === "string") return inner["message"];
|
|
9054
|
+
try {
|
|
9055
|
+
return JSON.stringify(err);
|
|
9056
|
+
} catch {
|
|
9057
|
+
}
|
|
9058
|
+
}
|
|
9059
|
+
return String(err);
|
|
9060
|
+
}
|
|
9061
|
+
function classifyProviderError(err) {
|
|
9062
|
+
const raw = messageOf(err).trim();
|
|
9063
|
+
const status = statusOf(err);
|
|
9064
|
+
const m = raw.toLowerCase();
|
|
9065
|
+
const kind = (() => {
|
|
9066
|
+
if (status === 429) {
|
|
9067
|
+
return /quota|billing|credit|exceeded your current quota|insufficient/.test(m) ? "quota_exhausted" : "rate_limit";
|
|
9068
|
+
}
|
|
9069
|
+
if (status === 401 || status === 403) return "auth";
|
|
9070
|
+
if (status === 404) return "model_unavailable";
|
|
9071
|
+
if (/rate limit|too many requests|resource_exhausted/.test(m)) {
|
|
9072
|
+
return /quota|billing|credit|insufficient/.test(m) ? "quota_exhausted" : "rate_limit";
|
|
9073
|
+
}
|
|
9074
|
+
if (/quota|billing|insufficient_quota|insufficient credit|payment required/.test(m)) return "quota_exhausted";
|
|
9075
|
+
if (/api key|unauthorized|authentication|permission denied|invalid[_ ]api/.test(m)) return "auth";
|
|
9076
|
+
if (/model .*(not found|does not exist|unavailable)|no such model|unknown model|not supported for/.test(m)) {
|
|
9077
|
+
return "model_unavailable";
|
|
9078
|
+
}
|
|
9079
|
+
if (/context length|too many tokens|maximum context|prompt is too long|input too long/.test(m)) return "context_length";
|
|
9080
|
+
if (/safety|content filter|blocked|recitation|prohibited/.test(m)) return "content_filter";
|
|
9081
|
+
if (/econnreset|etimedout|enotfound|socket hang up|network|fetch failed|aborted/.test(m)) return "network";
|
|
9082
|
+
return "unknown";
|
|
9083
|
+
})();
|
|
9084
|
+
return { kind, systemic: isSystemicKind(kind), status, raw };
|
|
9085
|
+
}
|
|
9086
|
+
function describeProviderError(c, modelId) {
|
|
9087
|
+
const on = modelId ? ` on ${modelId}` : "";
|
|
9088
|
+
const detail = c.raw ? ` Provider said: ${c.raw}` : "";
|
|
9089
|
+
switch (c.kind) {
|
|
9090
|
+
case "rate_limit":
|
|
9091
|
+
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}`;
|
|
9092
|
+
case "quota_exhausted":
|
|
9093
|
+
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}`;
|
|
9094
|
+
case "auth":
|
|
9095
|
+
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}`;
|
|
9096
|
+
case "model_unavailable":
|
|
9097
|
+
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}`;
|
|
9098
|
+
case "context_length":
|
|
9099
|
+
return `The prompt was too long${on}. Reduce the attached context, or use a model with a larger context window for this tier.${detail}`;
|
|
9100
|
+
case "content_filter":
|
|
9101
|
+
return `The provider's safety filter refused this request${on}.${detail}`;
|
|
9102
|
+
case "network":
|
|
9103
|
+
return `Network error reaching the provider${on}. This is usually transient.${detail}`;
|
|
9104
|
+
default:
|
|
9105
|
+
return `The model call failed${on}.${detail}`;
|
|
9106
|
+
}
|
|
9107
|
+
}
|
|
9108
|
+
|
|
9109
|
+
// src/core/multimodal/generate.ts
|
|
9110
|
+
var DEFAULT_TIMEOUT_MS = 12e4;
|
|
9111
|
+
async function callProvider(fn, modelId) {
|
|
9112
|
+
try {
|
|
9113
|
+
return await fn();
|
|
9114
|
+
} catch (err) {
|
|
9115
|
+
const classified = classifyProviderError(err);
|
|
9116
|
+
throw new Error(describeProviderError(classified, modelId));
|
|
9117
|
+
}
|
|
9118
|
+
}
|
|
9119
|
+
async function postJson(url, body, headers, signal) {
|
|
9120
|
+
const ac = new AbortController();
|
|
9121
|
+
const timer = setTimeout(() => ac.abort(), DEFAULT_TIMEOUT_MS);
|
|
9122
|
+
const onAbort = () => ac.abort();
|
|
9123
|
+
signal?.addEventListener("abort", onAbort);
|
|
9124
|
+
try {
|
|
9125
|
+
const res = await fetch(url, {
|
|
9126
|
+
method: "POST",
|
|
9127
|
+
headers: { "Content-Type": "application/json", ...headers },
|
|
9128
|
+
body: JSON.stringify(body),
|
|
9129
|
+
signal: ac.signal
|
|
9130
|
+
});
|
|
9131
|
+
if (!res.ok) {
|
|
9132
|
+
const text = await res.text().catch(() => "");
|
|
9133
|
+
throw Object.assign(new Error(text || res.statusText), { status: res.status });
|
|
9134
|
+
}
|
|
9135
|
+
return res;
|
|
9136
|
+
} finally {
|
|
9137
|
+
clearTimeout(timer);
|
|
9138
|
+
signal?.removeEventListener("abort", onAbort);
|
|
9139
|
+
}
|
|
9140
|
+
}
|
|
9141
|
+
function baseUrlFor(cfg, fallback) {
|
|
9142
|
+
return (cfg.baseUrl ?? fallback).replace(/\/+$/, "");
|
|
9143
|
+
}
|
|
9144
|
+
function stamp(ext) {
|
|
9145
|
+
return `cascade-${Date.now()}.${ext}`;
|
|
9146
|
+
}
|
|
9147
|
+
async function generateImage(cap, cfg, req, signal) {
|
|
9148
|
+
return callProvider(async () => {
|
|
9149
|
+
if (cap.api === "openai-images") {
|
|
9150
|
+
const url = `${baseUrlFor(cfg, "https://api.openai.com/v1")}/images/generations`;
|
|
9151
|
+
const res = await postJson(url, {
|
|
9152
|
+
model: cap.modelId,
|
|
9153
|
+
prompt: req.prompt,
|
|
9154
|
+
size: req.size ?? "1024x1024",
|
|
9155
|
+
n: 1,
|
|
9156
|
+
// b64 rather than a URL: see the file header — provider URLs expire.
|
|
9157
|
+
response_format: "b64_json"
|
|
9158
|
+
}, { Authorization: `Bearer ${cfg.apiKey ?? ""}` }, signal);
|
|
9159
|
+
const body = await res.json();
|
|
9160
|
+
const first = body.data?.[0];
|
|
9161
|
+
if (!first?.b64_json) throw new Error("Image response contained no image data.");
|
|
9162
|
+
return {
|
|
9163
|
+
data: Buffer.from(first.b64_json, "base64"),
|
|
9164
|
+
mimeType: "image/png",
|
|
9165
|
+
filename: stamp("png"),
|
|
9166
|
+
modelId: cap.modelId,
|
|
9167
|
+
provider: cap.provider,
|
|
9168
|
+
...first.revised_prompt ? { revisedPrompt: first.revised_prompt } : {}
|
|
9169
|
+
};
|
|
9170
|
+
}
|
|
9171
|
+
if (cap.api === "gemini-predict") {
|
|
9172
|
+
const base = baseUrlFor(cfg, "https://generativelanguage.googleapis.com/v1beta");
|
|
9173
|
+
const url = `${base}/models/${encodeURIComponent(cap.modelId)}:predict`;
|
|
9174
|
+
const res = await postJson(url, {
|
|
9175
|
+
instances: [{ prompt: req.prompt }],
|
|
9176
|
+
parameters: { sampleCount: 1 }
|
|
9177
|
+
}, { "x-goog-api-key": cfg.apiKey ?? "" }, signal);
|
|
9178
|
+
const body = await res.json();
|
|
9179
|
+
const first = body.predictions?.[0];
|
|
9180
|
+
if (!first?.bytesBase64Encoded) throw new Error("Image response contained no image data.");
|
|
9181
|
+
const mimeType = first.mimeType ?? "image/png";
|
|
9182
|
+
return {
|
|
9183
|
+
data: Buffer.from(first.bytesBase64Encoded, "base64"),
|
|
9184
|
+
mimeType,
|
|
9185
|
+
filename: stamp(mimeType.includes("jpeg") ? "jpg" : "png"),
|
|
9186
|
+
modelId: cap.modelId,
|
|
9187
|
+
provider: cap.provider
|
|
9188
|
+
};
|
|
9189
|
+
}
|
|
9190
|
+
throw new Error(`${cap.modelId} is not an image generator.`);
|
|
9191
|
+
}, cap.modelId);
|
|
9192
|
+
}
|
|
9193
|
+
async function generateSpeech(cap, cfg, req, signal) {
|
|
9194
|
+
return callProvider(async () => {
|
|
9195
|
+
if (cap.api !== "openai-speech") throw new Error(`${cap.modelId} is not a speech model.`);
|
|
9196
|
+
const format = req.format ?? "mp3";
|
|
9197
|
+
const url = `${baseUrlFor(cfg, "https://api.openai.com/v1")}/audio/speech`;
|
|
9198
|
+
const res = await postJson(url, {
|
|
9199
|
+
model: cap.modelId,
|
|
9200
|
+
input: req.text,
|
|
9201
|
+
voice: req.voice ?? "alloy",
|
|
9202
|
+
response_format: format
|
|
9203
|
+
}, { Authorization: `Bearer ${cfg.apiKey ?? ""}` }, signal);
|
|
9204
|
+
const mime = {
|
|
9205
|
+
mp3: "audio/mpeg",
|
|
9206
|
+
opus: "audio/opus",
|
|
9207
|
+
aac: "audio/aac",
|
|
9208
|
+
flac: "audio/flac",
|
|
9209
|
+
wav: "audio/wav"
|
|
9210
|
+
};
|
|
9211
|
+
return {
|
|
9212
|
+
data: Buffer.from(await res.arrayBuffer()),
|
|
9213
|
+
mimeType: mime[format] ?? "application/octet-stream",
|
|
9214
|
+
filename: stamp(format),
|
|
9215
|
+
modelId: cap.modelId,
|
|
9216
|
+
provider: cap.provider
|
|
9217
|
+
};
|
|
9218
|
+
}, cap.modelId);
|
|
9219
|
+
}
|
|
9220
|
+
async function transcribeAudio(cap, cfg, req, signal) {
|
|
9221
|
+
return callProvider(async () => {
|
|
9222
|
+
if (cap.api !== "openai-transcriptions") throw new Error(`${cap.modelId} is not a transcription model.`);
|
|
9223
|
+
const url = `${baseUrlFor(cfg, "https://api.openai.com/v1")}/audio/transcriptions`;
|
|
9224
|
+
const form = new FormData();
|
|
9225
|
+
form.append("model", cap.modelId);
|
|
9226
|
+
if (req.language) form.append("language", req.language);
|
|
9227
|
+
form.append("file", new Blob([new Uint8Array(req.audio)]), req.filename);
|
|
9228
|
+
const ac = new AbortController();
|
|
9229
|
+
const timer = setTimeout(() => ac.abort(), DEFAULT_TIMEOUT_MS);
|
|
9230
|
+
const onAbort = () => ac.abort();
|
|
9231
|
+
signal?.addEventListener("abort", onAbort);
|
|
9232
|
+
try {
|
|
9233
|
+
const res = await fetch(url, {
|
|
9234
|
+
method: "POST",
|
|
9235
|
+
headers: { Authorization: `Bearer ${cfg.apiKey ?? ""}` },
|
|
9236
|
+
body: form,
|
|
9237
|
+
signal: ac.signal
|
|
9238
|
+
});
|
|
9239
|
+
if (!res.ok) {
|
|
9240
|
+
const text = await res.text().catch(() => "");
|
|
9241
|
+
throw Object.assign(new Error(text || res.statusText), { status: res.status });
|
|
9242
|
+
}
|
|
9243
|
+
const body = await res.json();
|
|
9244
|
+
return { text: body.text ?? "", modelId: cap.modelId };
|
|
9245
|
+
} finally {
|
|
9246
|
+
clearTimeout(timer);
|
|
9247
|
+
signal?.removeEventListener("abort", onAbort);
|
|
9248
|
+
}
|
|
9249
|
+
}, cap.modelId);
|
|
9250
|
+
}
|
|
9251
|
+
|
|
9252
|
+
// src/tools/generate-media.ts
|
|
9253
|
+
function resolve(deps, modality, preferModel) {
|
|
9254
|
+
const selected = deps.registry.select(modality, preferModel ? { preferModel } : {});
|
|
9255
|
+
if (!selected) {
|
|
9256
|
+
return { ok: false, error: `No ${modality} model is available on your configured providers.` };
|
|
9257
|
+
}
|
|
9258
|
+
const cfg = deps.lookupProvider(selected.capability.provider);
|
|
9259
|
+
if (!cfg) {
|
|
9260
|
+
return {
|
|
9261
|
+
ok: false,
|
|
9262
|
+
error: `${selected.capability.modelId} needs the ${selected.capability.provider} provider, which is not configured.`
|
|
9263
|
+
};
|
|
9264
|
+
}
|
|
9265
|
+
return { ok: true, selected, cfg };
|
|
9266
|
+
}
|
|
9267
|
+
var GenerateImageTool = class extends BaseTool {
|
|
9268
|
+
constructor(deps) {
|
|
9269
|
+
super();
|
|
9270
|
+
this.deps = deps;
|
|
9271
|
+
}
|
|
9272
|
+
deps;
|
|
9273
|
+
name = "generate_image";
|
|
9274
|
+
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.";
|
|
9275
|
+
inputSchema = {
|
|
9276
|
+
type: "object",
|
|
9277
|
+
properties: {
|
|
9278
|
+
prompt: { type: "string", description: "What the image should show. Be specific about subject, style and composition." },
|
|
9279
|
+
size: { type: "string", description: 'Pixel size, e.g. "1024x1024" (default), "1792x1024", "1024x1792".' },
|
|
9280
|
+
model: { type: "string", description: "Optional: force a specific image model instead of the automatic choice." }
|
|
9281
|
+
},
|
|
9282
|
+
required: ["prompt"]
|
|
9283
|
+
};
|
|
9284
|
+
async execute(input, options) {
|
|
9285
|
+
const prompt = String(input["prompt"] ?? "").trim();
|
|
9286
|
+
if (!prompt) return 'Provide a "prompt" describing the image.';
|
|
9287
|
+
const r = resolve(this.deps, "image", input["model"] ? String(input["model"]) : void 0);
|
|
9288
|
+
if (!r.ok) return r.error;
|
|
9289
|
+
const size = input["size"] ? String(input["size"]) : void 0;
|
|
9290
|
+
const asset = await generateImage(
|
|
9291
|
+
r.selected.capability,
|
|
9292
|
+
r.cfg,
|
|
9293
|
+
{ prompt, ...size ? { size } : {} },
|
|
9294
|
+
options.signal
|
|
9295
|
+
);
|
|
9296
|
+
const location = await this.deps.sink(asset);
|
|
9297
|
+
return [
|
|
9298
|
+
`Image generated and saved to ${location}.`,
|
|
9299
|
+
`Model: ${asset.modelId} \u2014 ${r.selected.reason}`,
|
|
9300
|
+
// DALL·E rewrites the prompt before drawing; the model needs to know what
|
|
9301
|
+
// was actually rendered, not what it asked for, or its description of the
|
|
9302
|
+
// result will not match the image.
|
|
9303
|
+
asset.revisedPrompt ? `The provider rewrote the prompt to: ${asset.revisedPrompt}` : ""
|
|
9304
|
+
].filter(Boolean).join("\n");
|
|
9305
|
+
}
|
|
9306
|
+
};
|
|
9307
|
+
var GenerateSpeechTool = class extends BaseTool {
|
|
9308
|
+
constructor(deps) {
|
|
9309
|
+
super();
|
|
9310
|
+
this.deps = deps;
|
|
9311
|
+
}
|
|
9312
|
+
deps;
|
|
9313
|
+
name = "generate_speech";
|
|
9314
|
+
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.";
|
|
9315
|
+
inputSchema = {
|
|
9316
|
+
type: "object",
|
|
9317
|
+
properties: {
|
|
9318
|
+
text: { type: "string", description: "The text to speak." },
|
|
9319
|
+
voice: { type: "string", description: "Voice id: alloy, echo, fable, onyx, nova, shimmer. Default alloy." },
|
|
9320
|
+
format: { type: "string", description: "Audio format: mp3 (default), opus, aac, flac, wav." },
|
|
9321
|
+
model: { type: "string", description: "Optional: force a specific speech model." }
|
|
9322
|
+
},
|
|
9323
|
+
required: ["text"]
|
|
9324
|
+
};
|
|
9325
|
+
async execute(input, options) {
|
|
9326
|
+
const text = String(input["text"] ?? "").trim();
|
|
9327
|
+
if (!text) return 'Provide "text" to speak.';
|
|
9328
|
+
const r = resolve(this.deps, "speech", input["model"] ? String(input["model"]) : void 0);
|
|
9329
|
+
if (!r.ok) return r.error;
|
|
9330
|
+
const fmt = String(input["format"] ?? "mp3");
|
|
9331
|
+
const allowed = ["mp3", "opus", "aac", "flac", "wav"];
|
|
9332
|
+
const format = allowed.includes(fmt) ? fmt : "mp3";
|
|
9333
|
+
const asset = await generateSpeech(
|
|
9334
|
+
r.selected.capability,
|
|
9335
|
+
r.cfg,
|
|
9336
|
+
{ text, voice: input["voice"] ? String(input["voice"]) : void 0, format },
|
|
9337
|
+
options.signal
|
|
9338
|
+
);
|
|
9339
|
+
const location = await this.deps.sink(asset);
|
|
9340
|
+
return `Speech generated and saved to ${location}.
|
|
9341
|
+
Model: ${asset.modelId} \u2014 ${r.selected.reason}`;
|
|
9342
|
+
}
|
|
9343
|
+
};
|
|
9344
|
+
var TranscribeAudioTool = class extends BaseTool {
|
|
9345
|
+
constructor(deps, readFile) {
|
|
9346
|
+
super();
|
|
9347
|
+
this.deps = deps;
|
|
9348
|
+
this.readFile = readFile;
|
|
9349
|
+
}
|
|
9350
|
+
deps;
|
|
9351
|
+
readFile;
|
|
9352
|
+
name = "transcribe_audio";
|
|
9353
|
+
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.";
|
|
9354
|
+
inputSchema = {
|
|
9355
|
+
type: "object",
|
|
9356
|
+
properties: {
|
|
9357
|
+
path: { type: "string", description: "Path to the audio file to transcribe." },
|
|
9358
|
+
language: { type: "string", description: 'Optional ISO-639-1 language hint, e.g. "en", improves accuracy.' },
|
|
9359
|
+
model: { type: "string", description: "Optional: force a specific transcription model." }
|
|
9360
|
+
},
|
|
9361
|
+
required: ["path"]
|
|
9362
|
+
};
|
|
9363
|
+
async execute(input, options) {
|
|
9364
|
+
const filePath = String(input["path"] ?? "").trim();
|
|
9365
|
+
if (!filePath) return 'Provide a "path" to the audio file.';
|
|
9366
|
+
const r = resolve(this.deps, "transcription", input["model"] ? String(input["model"]) : void 0);
|
|
9367
|
+
if (!r.ok) return r.error;
|
|
9368
|
+
let audio;
|
|
9369
|
+
try {
|
|
9370
|
+
audio = await this.readFile(filePath);
|
|
9371
|
+
} catch {
|
|
9372
|
+
return `Could not read the audio file at ${filePath}.`;
|
|
9373
|
+
}
|
|
9374
|
+
const { text, modelId } = await transcribeAudio(
|
|
9375
|
+
r.selected.capability,
|
|
9376
|
+
r.cfg,
|
|
9377
|
+
{
|
|
9378
|
+
audio,
|
|
9379
|
+
filename: filePath.split(/[\\/]/).pop() || "audio",
|
|
9380
|
+
...input["language"] ? { language: String(input["language"]) } : {}
|
|
9381
|
+
},
|
|
9382
|
+
options.signal
|
|
9383
|
+
);
|
|
9384
|
+
return text ? `Transcript (${modelId}):
|
|
9385
|
+
|
|
9386
|
+
${text}` : `${modelId} returned an empty transcript \u2014 the file may be silent or in an unsupported format.`;
|
|
9387
|
+
}
|
|
9388
|
+
};
|
|
9389
|
+
function buildMediaTools(deps, readFile) {
|
|
9390
|
+
const tools = [];
|
|
9391
|
+
const modalities = new Set(deps.registry.availableModalities());
|
|
9392
|
+
if (modalities.has("image")) tools.push(new GenerateImageTool(deps));
|
|
9393
|
+
if (modalities.has("speech")) tools.push(new GenerateSpeechTool(deps));
|
|
9394
|
+
if (modalities.has("transcription") && readFile) tools.push(new TranscribeAudioTool(deps, readFile));
|
|
9395
|
+
return tools;
|
|
9396
|
+
}
|
|
9397
|
+
|
|
9398
|
+
// src/core/run-breaker.ts
|
|
9399
|
+
var DEFAULT_FAILURE_THRESHOLD = 3;
|
|
9400
|
+
var RunBreaker = class {
|
|
9401
|
+
threshold;
|
|
9402
|
+
/** modelId → consecutive systemic failures of the SAME kind. */
|
|
9403
|
+
counts = /* @__PURE__ */ new Map();
|
|
9404
|
+
trip = null;
|
|
9405
|
+
constructor(threshold = DEFAULT_FAILURE_THRESHOLD) {
|
|
9406
|
+
this.threshold = Math.max(1, threshold);
|
|
9407
|
+
}
|
|
9408
|
+
/**
|
|
9409
|
+
* Record a failed model call. Returns the trip if this failure is the one
|
|
9410
|
+
* that opened the breaker, otherwise null.
|
|
9411
|
+
*
|
|
9412
|
+
* A non-systemic failure RESETS the model's counter: it is evidence the model
|
|
9413
|
+
* is reachable and answering, which is exactly what a systemic streak claims
|
|
9414
|
+
* it is not.
|
|
9415
|
+
*/
|
|
9416
|
+
record(err, modelId) {
|
|
9417
|
+
if (this.trip) return null;
|
|
9418
|
+
const id = modelId?.trim() || "unknown-model";
|
|
9419
|
+
const c = classifyProviderError(err);
|
|
9420
|
+
if (!c.systemic) {
|
|
9421
|
+
this.counts.delete(id);
|
|
9422
|
+
return null;
|
|
9423
|
+
}
|
|
9424
|
+
const prev = this.counts.get(id);
|
|
9425
|
+
const n = prev && prev.kind === c.kind ? prev.n + 1 : 1;
|
|
9426
|
+
this.counts.set(id, { kind: c.kind, n, last: c });
|
|
9427
|
+
if (n >= this.threshold) {
|
|
9428
|
+
this.trip = {
|
|
9429
|
+
modelId: id,
|
|
9430
|
+
kind: c.kind,
|
|
9431
|
+
failures: n,
|
|
9432
|
+
message: describeProviderError(c, id)
|
|
9433
|
+
};
|
|
9434
|
+
return this.trip;
|
|
9435
|
+
}
|
|
9436
|
+
return null;
|
|
9437
|
+
}
|
|
9438
|
+
/** True once the run should stop launching work that would fail identically. */
|
|
9439
|
+
isOpen() {
|
|
9440
|
+
return this.trip !== null;
|
|
9441
|
+
}
|
|
9442
|
+
/** Why the breaker opened, or null while it is closed. */
|
|
9443
|
+
reason() {
|
|
9444
|
+
return this.trip;
|
|
9445
|
+
}
|
|
9446
|
+
/**
|
|
9447
|
+
* One line for a worker/section that never ran because the breaker was
|
|
9448
|
+
* already open. Says the work was skipped deliberately — not that it failed
|
|
9449
|
+
* on its own merits, which is what an empty FAILED status would imply.
|
|
9450
|
+
*/
|
|
9451
|
+
skipMessage() {
|
|
9452
|
+
const t = this.trip;
|
|
9453
|
+
if (!t) return "Skipped.";
|
|
9454
|
+
return `Skipped \u2014 the run stopped after ${t.failures} consecutive failures on ${t.modelId}. ${t.message}`;
|
|
9455
|
+
}
|
|
9456
|
+
/** Test/diagnostic view of the per-model streaks. */
|
|
9457
|
+
streaks() {
|
|
9458
|
+
const out = {};
|
|
9459
|
+
for (const [k, v] of this.counts) out[k] = { kind: v.kind, n: v.n };
|
|
9460
|
+
return out;
|
|
9461
|
+
}
|
|
9462
|
+
};
|
|
8683
9463
|
var execAsync = util.promisify(child_process.exec);
|
|
8684
9464
|
var ShellTool = class extends BaseTool {
|
|
8685
9465
|
name = "shell";
|
|
@@ -9375,7 +10155,7 @@ var PDFCreateTool = class extends BaseTool {
|
|
|
9375
10155
|
if (!fs23__default.default.existsSync(dir)) {
|
|
9376
10156
|
fs23__default.default.mkdirSync(dir, { recursive: true });
|
|
9377
10157
|
}
|
|
9378
|
-
return new Promise((
|
|
10158
|
+
return new Promise((resolve2, reject) => {
|
|
9379
10159
|
try {
|
|
9380
10160
|
const doc = new PDFDocument__default.default({ margin: 50 });
|
|
9381
10161
|
const stream = fs23__default.default.createWriteStream(filePath);
|
|
@@ -9393,7 +10173,7 @@ var PDFCreateTool = class extends BaseTool {
|
|
|
9393
10173
|
});
|
|
9394
10174
|
doc.end();
|
|
9395
10175
|
stream.on("finish", () => {
|
|
9396
|
-
|
|
10176
|
+
resolve2(`Successfully created PDF at ${filePath} (${content.length} characters)`);
|
|
9397
10177
|
});
|
|
9398
10178
|
stream.on("error", (err) => {
|
|
9399
10179
|
reject(new Error(`Failed to write PDF: ${err.message}`));
|
|
@@ -9465,7 +10245,7 @@ var CodeInterpreterTool = class extends BaseTool {
|
|
|
9465
10245
|
const filePath = path24__default.default.join(tmpDir, fileName);
|
|
9466
10246
|
fs23__default.default.writeFileSync(filePath, code, "utf-8");
|
|
9467
10247
|
const execArgs = [filePath, ...args];
|
|
9468
|
-
return new Promise((
|
|
10248
|
+
return new Promise((resolve2) => {
|
|
9469
10249
|
const startMs = Date.now();
|
|
9470
10250
|
child_process.execFile(cmdPrefix, execArgs, { cwd: this.workspaceRoot, timeout: 3e4 }, (error, stdout, stderr) => {
|
|
9471
10251
|
const duration = Date.now() - startMs;
|
|
@@ -9479,17 +10259,17 @@ var CodeInterpreterTool = class extends BaseTool {
|
|
|
9479
10259
|
if (error) {
|
|
9480
10260
|
const timedOut = error.killed && duration >= 3e4;
|
|
9481
10261
|
if (timedOut) {
|
|
9482
|
-
|
|
10262
|
+
resolve2(`Execution timed out after 30s. Consider breaking the task into smaller pieces.
|
|
9483
10263
|
Partial stdout: ${stdout}
|
|
9484
10264
|
Stderr: ${stderr}`);
|
|
9485
10265
|
} else {
|
|
9486
|
-
|
|
10266
|
+
resolve2(`Execution failed (${duration}ms):
|
|
9487
10267
|
Error: ${error.message}
|
|
9488
10268
|
Stderr: ${stderr}
|
|
9489
10269
|
Stdout: ${stdout}`);
|
|
9490
10270
|
}
|
|
9491
10271
|
} else {
|
|
9492
|
-
|
|
10272
|
+
resolve2(`Execution successful (${duration}ms):
|
|
9493
10273
|
Stdout: ${stdout}
|
|
9494
10274
|
Stderr: ${stderr}`);
|
|
9495
10275
|
}
|
|
@@ -10143,7 +10923,7 @@ var ToolRegistry = class extends EventEmitter__default.default {
|
|
|
10143
10923
|
*/
|
|
10144
10924
|
waitForTool(toolName, timeoutMs = 6e4) {
|
|
10145
10925
|
if (this.tools.has(toolName)) return Promise.resolve();
|
|
10146
|
-
return new Promise((
|
|
10926
|
+
return new Promise((resolve2, reject) => {
|
|
10147
10927
|
const timer = setTimeout(() => {
|
|
10148
10928
|
this.off("tool:added", handler);
|
|
10149
10929
|
reject(new Error(`Timeout waiting for tool: ${toolName}`));
|
|
@@ -10152,7 +10932,7 @@ var ToolRegistry = class extends EventEmitter__default.default {
|
|
|
10152
10932
|
if (name === toolName) {
|
|
10153
10933
|
clearTimeout(timer);
|
|
10154
10934
|
this.off("tool:added", handler);
|
|
10155
|
-
|
|
10935
|
+
resolve2();
|
|
10156
10936
|
}
|
|
10157
10937
|
};
|
|
10158
10938
|
this.on("tool:added", handler);
|
|
@@ -10827,20 +11607,20 @@ var PermissionEscalator = class extends EventEmitter__default.default {
|
|
|
10827
11607
|
resolver(decision);
|
|
10828
11608
|
}
|
|
10829
11609
|
waitForUserDecision(req) {
|
|
10830
|
-
return new Promise((
|
|
11610
|
+
return new Promise((resolve2) => {
|
|
10831
11611
|
let timer;
|
|
10832
11612
|
const wrappedResolver = (decision) => {
|
|
10833
11613
|
if (timer) clearTimeout(timer);
|
|
10834
11614
|
if (decision.always) {
|
|
10835
11615
|
this.taskWideCache.set(req.toolName, decision.approved);
|
|
10836
11616
|
}
|
|
10837
|
-
|
|
11617
|
+
resolve2(decision);
|
|
10838
11618
|
};
|
|
10839
11619
|
this.pendingUserDecisions.set(req.id, wrappedResolver);
|
|
10840
11620
|
if (this.approvalTimeoutMs > 0 && Number.isFinite(this.approvalTimeoutMs)) {
|
|
10841
11621
|
timer = setTimeout(() => {
|
|
10842
11622
|
if (this.pendingUserDecisions.delete(req.id)) {
|
|
10843
|
-
|
|
11623
|
+
resolve2({
|
|
10844
11624
|
requestId: req.id,
|
|
10845
11625
|
approved: false,
|
|
10846
11626
|
decidedBy: "USER",
|
|
@@ -11007,6 +11787,14 @@ var BudgetConfigSchema = zod.z.object({
|
|
|
11007
11787
|
maxTokensPerRun: zod.z.number().int().positive().default(2e5),
|
|
11008
11788
|
/** Optional hard per-task cost ceiling (USD). Unset = only the token cap applies. */
|
|
11009
11789
|
maxCostPerRunUsd: zod.z.number().positive().optional(),
|
|
11790
|
+
/**
|
|
11791
|
+
* Consecutive systemic failures against ONE model before the run stops
|
|
11792
|
+
* launching work that would fail the same way (dead key, wrong model id,
|
|
11793
|
+
* exhausted quota). Lower reacts sooner and wastes less; higher absorbs a
|
|
11794
|
+
* longer transient before giving up. Only failures that will certainly repeat
|
|
11795
|
+
* are counted — see core/run-breaker.ts.
|
|
11796
|
+
*/
|
|
11797
|
+
failureThreshold: zod.z.number().int().min(1).max(20).default(3),
|
|
11010
11798
|
warnAtPct: zod.z.number().default(80)
|
|
11011
11799
|
});
|
|
11012
11800
|
var WorkspaceConfigSchema = zod.z.object({
|
|
@@ -11294,6 +12082,23 @@ var noopTelemetry = {
|
|
|
11294
12082
|
}
|
|
11295
12083
|
};
|
|
11296
12084
|
|
|
12085
|
+
// src/core/router/feedback-prior.ts
|
|
12086
|
+
var MAX_FEEDBACK_ADJUSTMENT = 0.05;
|
|
12087
|
+
var PRIOR_STRENGTH = 10;
|
|
12088
|
+
function feedbackAdjustment(counts) {
|
|
12089
|
+
const good = Math.max(0, Math.floor(counts.good));
|
|
12090
|
+
const bad = Math.max(0, Math.floor(counts.bad));
|
|
12091
|
+
const n = good + bad;
|
|
12092
|
+
if (n === 0) return 0;
|
|
12093
|
+
const half = PRIOR_STRENGTH / 2;
|
|
12094
|
+
const shrunk = (good + half) / (n + PRIOR_STRENGTH);
|
|
12095
|
+
return (shrunk - 0.5) * 2 * MAX_FEEDBACK_ADJUSTMENT;
|
|
12096
|
+
}
|
|
12097
|
+
function applyFeedback(score01, counts) {
|
|
12098
|
+
const adjusted = score01 + feedbackAdjustment(counts);
|
|
12099
|
+
return Math.max(0, Math.min(1, adjusted));
|
|
12100
|
+
}
|
|
12101
|
+
|
|
11297
12102
|
// src/core/router/task-analyzer.ts
|
|
11298
12103
|
var CODE_SIGNALS = [
|
|
11299
12104
|
/\b(?:function|class|interface|async|await|import|export|const|let|var|def|return|if|else|for|while|try|catch)\b/,
|
|
@@ -11368,6 +12173,8 @@ var TASK_TYPE_TAGS = {
|
|
|
11368
12173
|
};
|
|
11369
12174
|
var TaskAnalyzer = class {
|
|
11370
12175
|
tracker;
|
|
12176
|
+
/** Per-model thumbs counts, when the host collects them. */
|
|
12177
|
+
feedback;
|
|
11371
12178
|
bias;
|
|
11372
12179
|
lastProfile = null;
|
|
11373
12180
|
lastSelectedModels = /* @__PURE__ */ new Map();
|
|
@@ -11375,6 +12182,14 @@ var TaskAnalyzer = class {
|
|
|
11375
12182
|
this.tracker = tracker;
|
|
11376
12183
|
this.bias = bias;
|
|
11377
12184
|
}
|
|
12185
|
+
/**
|
|
12186
|
+
* Supply per-model rating counts. Optional by design: a host that collects no
|
|
12187
|
+
* feedback (the CLI, a fresh install) scores exactly as it did before, since
|
|
12188
|
+
* feedbackAdjustment returns 0 for an empty record.
|
|
12189
|
+
*/
|
|
12190
|
+
setFeedbackSource(source) {
|
|
12191
|
+
this.feedback = source;
|
|
12192
|
+
}
|
|
11378
12193
|
setTracker(tracker) {
|
|
11379
12194
|
this.tracker = tracker;
|
|
11380
12195
|
}
|
|
@@ -11458,7 +12273,10 @@ var TaskAnalyzer = class {
|
|
|
11458
12273
|
const perf = this.tracker?.performanceScore(model.id, profile.type) ?? 0.5;
|
|
11459
12274
|
const costEff = this.costEfficiency(model, profile.complexity);
|
|
11460
12275
|
const match = this.taskMatchScore(model, profile);
|
|
11461
|
-
const
|
|
12276
|
+
const rated = this.feedback?.(model.id);
|
|
12277
|
+
const publicScore = benchmarkScore01(model, profile.type);
|
|
12278
|
+
const adjusted = rated ? applyFeedback(publicScore, rated) : publicScore;
|
|
12279
|
+
const benchmark = 0.3 + 0.7 * adjusted;
|
|
11462
12280
|
switch (this.bias) {
|
|
11463
12281
|
case "quality":
|
|
11464
12282
|
return perf * match * benchmark ** 2 * (0.85 + 0.15 * costEff);
|
|
@@ -11860,10 +12678,10 @@ var DynamicTool = class extends BaseTool {
|
|
|
11860
12678
|
`;
|
|
11861
12679
|
const script = await isolate.compileScript(bootstrap);
|
|
11862
12680
|
const runPromise = script.run(context, { timeout: timeoutMs, promise: true }).then((v) => String(v ?? ""));
|
|
11863
|
-
const timeoutPromise = new Promise((
|
|
12681
|
+
const timeoutPromise = new Promise((resolve2) => {
|
|
11864
12682
|
const t = setTimeout(() => {
|
|
11865
12683
|
dispose();
|
|
11866
|
-
|
|
12684
|
+
resolve2(`Dynamic tool "${this.name}" timed out after ${timeoutMs}ms and was terminated.`);
|
|
11867
12685
|
}, timeoutMs + 500);
|
|
11868
12686
|
t.unref?.();
|
|
11869
12687
|
});
|
|
@@ -11881,7 +12699,7 @@ var DynamicTool = class extends BaseTool {
|
|
|
11881
12699
|
/** Spawn the worker, service its callTool/fetch bridge, enforce the kill timeout. */
|
|
11882
12700
|
runInWorker(input, callTool) {
|
|
11883
12701
|
const timeoutMs = Math.max(200, Number(process.env["CASCADE_DYNAMIC_TOOL_TIMEOUT_MS"]) || DYNAMIC_TOOL_TIMEOUT_MS);
|
|
11884
|
-
return new Promise((
|
|
12702
|
+
return new Promise((resolve2) => {
|
|
11885
12703
|
let settled = false;
|
|
11886
12704
|
const worker = new worker_threads.Worker(HARNESS_SRC, {
|
|
11887
12705
|
eval: true,
|
|
@@ -11893,7 +12711,7 @@ var DynamicTool = class extends BaseTool {
|
|
|
11893
12711
|
settled = true;
|
|
11894
12712
|
clearTimeout(timer);
|
|
11895
12713
|
void worker.terminate();
|
|
11896
|
-
|
|
12714
|
+
resolve2(value);
|
|
11897
12715
|
};
|
|
11898
12716
|
const timer = setTimeout(
|
|
11899
12717
|
() => finish(`Dynamic tool "${this.name}" timed out after ${timeoutMs}ms and was terminated.`),
|
|
@@ -13439,6 +14257,7 @@ ${prompt}`;
|
|
|
13439
14257
|
var Cascade = class _Cascade extends EventEmitter__default.default {
|
|
13440
14258
|
router;
|
|
13441
14259
|
toolRegistry;
|
|
14260
|
+
multimodal;
|
|
13442
14261
|
mcpClient;
|
|
13443
14262
|
codeIndex;
|
|
13444
14263
|
config;
|
|
@@ -13477,6 +14296,10 @@ var Cascade = class _Cascade extends EventEmitter__default.default {
|
|
|
13477
14296
|
}
|
|
13478
14297
|
});
|
|
13479
14298
|
this.toolRegistry = new ToolRegistry(this.config.tools, workspacePath);
|
|
14299
|
+
this.multimodal = new MultimodalRegistry(
|
|
14300
|
+
(this.config.providers ?? []).map((p) => p.type)
|
|
14301
|
+
);
|
|
14302
|
+
this.registerMediaTools(workspacePath);
|
|
13480
14303
|
this.telemetry = config.telemetry?.enabled ? new Telemetry(config.telemetry, config.telemetry.distinctId ?? "anonymous") : noopTelemetry;
|
|
13481
14304
|
this.worldStateDB = new WorldStateDB(this.workspacePath, this.config.workspace?.debugWorldState ?? false);
|
|
13482
14305
|
this.router.setWorldStateDB(this.worldStateDB);
|
|
@@ -13530,14 +14353,14 @@ var Cascade = class _Cascade extends EventEmitter__default.default {
|
|
|
13530
14353
|
if (this.listenerCount("mcp:approval-required") === 0) {
|
|
13531
14354
|
return false;
|
|
13532
14355
|
}
|
|
13533
|
-
return await new Promise((
|
|
13534
|
-
this.pendingMcpApprovals.set(server.name,
|
|
14356
|
+
return await new Promise((resolve2) => {
|
|
14357
|
+
this.pendingMcpApprovals.set(server.name, resolve2);
|
|
13535
14358
|
const timeout = setTimeout(() => {
|
|
13536
|
-
if (this.pendingMcpApprovals.delete(server.name))
|
|
14359
|
+
if (this.pendingMcpApprovals.delete(server.name)) resolve2(false);
|
|
13537
14360
|
}, 3e4);
|
|
13538
14361
|
const wrap = (approved) => {
|
|
13539
14362
|
clearTimeout(timeout);
|
|
13540
|
-
|
|
14363
|
+
resolve2(approved);
|
|
13541
14364
|
};
|
|
13542
14365
|
this.pendingMcpApprovals.set(server.name, wrap);
|
|
13543
14366
|
this.emit("mcp:approval-required", { server });
|
|
@@ -13611,17 +14434,17 @@ var Cascade = class _Cascade extends EventEmitter__default.default {
|
|
|
13611
14434
|
}
|
|
13612
14435
|
const t2Count = plan.sections.length;
|
|
13613
14436
|
const t3Count = plan.sections.reduce((sum, s) => sum + (s.t3Subtasks?.length ?? 0), 0);
|
|
13614
|
-
return await new Promise((
|
|
14437
|
+
return await new Promise((resolve2) => {
|
|
13615
14438
|
const timeout = setTimeout(() => {
|
|
13616
14439
|
if (this.pendingPlanApproval) {
|
|
13617
14440
|
this.pendingPlanApproval = void 0;
|
|
13618
|
-
|
|
14441
|
+
resolve2({ approved: true });
|
|
13619
14442
|
}
|
|
13620
14443
|
}, 12e4);
|
|
13621
14444
|
this.pendingPlanApproval = (decision) => {
|
|
13622
14445
|
clearTimeout(timeout);
|
|
13623
14446
|
this.pendingPlanApproval = void 0;
|
|
13624
|
-
|
|
14447
|
+
resolve2(decision);
|
|
13625
14448
|
};
|
|
13626
14449
|
this.emit("plan:approval-required", {
|
|
13627
14450
|
taskId,
|
|
@@ -13654,17 +14477,17 @@ var Cascade = class _Cascade extends EventEmitter__default.default {
|
|
|
13654
14477
|
}
|
|
13655
14478
|
async requestContextApproval(info) {
|
|
13656
14479
|
if (this.listenerCount("context:approval-required") === 0) return true;
|
|
13657
|
-
return await new Promise((
|
|
14480
|
+
return await new Promise((resolve2) => {
|
|
13658
14481
|
const timeout = setTimeout(() => {
|
|
13659
14482
|
if (this.pendingContextApproval) {
|
|
13660
14483
|
this.pendingContextApproval = void 0;
|
|
13661
|
-
|
|
14484
|
+
resolve2(true);
|
|
13662
14485
|
}
|
|
13663
14486
|
}, 12e4);
|
|
13664
14487
|
this.pendingContextApproval = (approved) => {
|
|
13665
14488
|
clearTimeout(timeout);
|
|
13666
14489
|
this.pendingContextApproval = void 0;
|
|
13667
|
-
|
|
14490
|
+
resolve2(approved);
|
|
13668
14491
|
};
|
|
13669
14492
|
this.emit("context:approval-required", info);
|
|
13670
14493
|
});
|
|
@@ -13744,6 +14567,55 @@ ${input}` }],
|
|
|
13744
14567
|
if (this.store) t1.setStore(this.store);
|
|
13745
14568
|
return t1.previewPlan(prompt);
|
|
13746
14569
|
}
|
|
14570
|
+
/**
|
|
14571
|
+
* Register image / speech / transcription tools for the modalities this
|
|
14572
|
+
* config can actually serve.
|
|
14573
|
+
*
|
|
14574
|
+
* The sink writes bytes into the workspace, because that is what a desktop or
|
|
14575
|
+
* CLI run wants and it keeps a generated asset indistinguishable from any
|
|
14576
|
+
* other file the run produced. A host with different storage (the cloud
|
|
14577
|
+
* server stores a file row) constructs the tools itself via buildMediaTools
|
|
14578
|
+
* with its own sink.
|
|
14579
|
+
*/
|
|
14580
|
+
registerMediaTools(workspacePath) {
|
|
14581
|
+
const allowlist = this.config.tools?.enabledTools;
|
|
14582
|
+
const permitted = (name) => !allowlist || allowlist.includes(name);
|
|
14583
|
+
const tools = buildMediaTools(
|
|
14584
|
+
{
|
|
14585
|
+
registry: this.multimodal,
|
|
14586
|
+
lookupProvider: (provider) => (this.config.providers ?? []).find((p) => p.type === provider),
|
|
14587
|
+
sink: async (asset) => {
|
|
14588
|
+
const fsp = await import('fs/promises');
|
|
14589
|
+
const nodePath = await import('path');
|
|
14590
|
+
const dir = nodePath.join(workspacePath, "generated");
|
|
14591
|
+
await fsp.mkdir(dir, { recursive: true });
|
|
14592
|
+
const dest = nodePath.join(dir, asset.filename);
|
|
14593
|
+
await fsp.writeFile(dest, asset.data);
|
|
14594
|
+
return nodePath.relative(workspacePath, dest);
|
|
14595
|
+
}
|
|
14596
|
+
},
|
|
14597
|
+
async (p) => {
|
|
14598
|
+
const fsp = await import('fs/promises');
|
|
14599
|
+
return fsp.readFile(p);
|
|
14600
|
+
}
|
|
14601
|
+
);
|
|
14602
|
+
for (const tool of tools) {
|
|
14603
|
+
if (!permitted(tool.name)) continue;
|
|
14604
|
+
tool.setWorkspaceRoot(workspacePath);
|
|
14605
|
+
this.toolRegistry.register(tool);
|
|
14606
|
+
}
|
|
14607
|
+
}
|
|
14608
|
+
/**
|
|
14609
|
+
* Supply per-model thumbs-up/down counts so Auto routing can weigh how models
|
|
14610
|
+
* have actually performed for this user, on top of public benchmarks.
|
|
14611
|
+
*
|
|
14612
|
+
* Optional everywhere: with no source, or an empty record, scoring is
|
|
14613
|
+
* byte-identical to before. The adjustment is capped and sample-size shrunk —
|
|
14614
|
+
* see core/router/feedback-prior.ts.
|
|
14615
|
+
*/
|
|
14616
|
+
setFeedbackSource(source) {
|
|
14617
|
+
this.router.setFeedbackSource(source);
|
|
14618
|
+
}
|
|
13747
14619
|
/** True when a task stopped at the budget cap and can be resumed via /continue. */
|
|
13748
14620
|
hasResumableRun() {
|
|
13749
14621
|
return this.lastInterruptedRun != null;
|
|
@@ -14233,6 +15105,7 @@ ${prompt}` : prompt;
|
|
|
14233
15105
|
if (toolCreator) toolCreator.setPermissionEscalator(escalator);
|
|
14234
15106
|
const rootPrompt = buildContextualPrompt(options.prompt, options.conversationHistory);
|
|
14235
15107
|
let finalOutput = "";
|
|
15108
|
+
const runBreaker = new RunBreaker(this.config.budget?.failureThreshold);
|
|
14236
15109
|
let t2Results = [];
|
|
14237
15110
|
let runError = null;
|
|
14238
15111
|
let identityPrompt = "";
|
|
@@ -14301,6 +15174,7 @@ ${prompt}` : prompt;
|
|
|
14301
15174
|
this.emit("tier:status", { tierId: "t3-root", status: "COMPLETED", role: "T3" });
|
|
14302
15175
|
} else if (complexity === "Moderate") {
|
|
14303
15176
|
const t2 = new T2Manager(this.router, this.toolRegistry, "root");
|
|
15177
|
+
t2.setRunBreaker(runBreaker);
|
|
14304
15178
|
t2.setPresenter(true);
|
|
14305
15179
|
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.");
|
|
14306
15180
|
if (identityPrompt) {
|
|
@@ -14361,6 +15235,7 @@ _(incomplete: ${reasons.join("; ")})_` : partial;
|
|
|
14361
15235
|
}
|
|
14362
15236
|
} else {
|
|
14363
15237
|
const t1 = new T1Administrator(this.router, this.toolRegistry, this.config);
|
|
15238
|
+
t1.setRunBreaker(runBreaker);
|
|
14364
15239
|
t1.setPresenter(true);
|
|
14365
15240
|
t1.setHierarchyContext("You are the top-level Administrator. You are responsible for the overall plan and supervising multiple T2 Managers.");
|
|
14366
15241
|
if (identityPrompt) {
|
|
@@ -14385,6 +15260,20 @@ _(incomplete: ${reasons.join("; ")})_` : partial;
|
|
|
14385
15260
|
finalOutput = result.output;
|
|
14386
15261
|
t2Results = result.t2Results;
|
|
14387
15262
|
}
|
|
15263
|
+
const trip = runBreaker.reason();
|
|
15264
|
+
if (trip) {
|
|
15265
|
+
finalOutput = [
|
|
15266
|
+
`\u26A0 **Run stopped early \u2014 ${trip.modelId} was failing every request.**`,
|
|
15267
|
+
"",
|
|
15268
|
+
trip.message,
|
|
15269
|
+
"",
|
|
15270
|
+
`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.`,
|
|
15271
|
+
finalOutput ? `
|
|
15272
|
+
---
|
|
15273
|
+
|
|
15274
|
+
${finalOutput}` : ""
|
|
15275
|
+
].join("\n").trimEnd();
|
|
15276
|
+
}
|
|
14388
15277
|
} catch (err) {
|
|
14389
15278
|
if (err instanceof CascadeCancelledError || err instanceof Error && err.name === "AbortError" || options.signal?.aborted) {
|
|
14390
15279
|
this.emit("run:cancelled", {
|
|
@@ -16389,12 +17278,12 @@ var DashboardServer = class {
|
|
|
16389
17278
|
console.warn("\u26A0 Dashboard auth is DISABLED while bound to a non-loopback interface \u2014 this allows unauthenticated remote task execution.");
|
|
16390
17279
|
}
|
|
16391
17280
|
}
|
|
16392
|
-
await new Promise((
|
|
17281
|
+
await new Promise((resolve2, reject) => {
|
|
16393
17282
|
const onError = (err) => reject(err);
|
|
16394
17283
|
this.httpServer.once("error", onError);
|
|
16395
17284
|
this.httpServer.listen(this.port, this.host, () => {
|
|
16396
17285
|
this.httpServer.off("error", onError);
|
|
16397
|
-
|
|
17286
|
+
resolve2();
|
|
16398
17287
|
});
|
|
16399
17288
|
});
|
|
16400
17289
|
try {
|
|
@@ -16418,8 +17307,8 @@ var DashboardServer = class {
|
|
|
16418
17307
|
} catch {
|
|
16419
17308
|
}
|
|
16420
17309
|
this.globalStore = null;
|
|
16421
|
-
await new Promise((
|
|
16422
|
-
this.httpServer.close((err) => err ? reject(err) :
|
|
17310
|
+
await new Promise((resolve2, reject) => {
|
|
17311
|
+
this.httpServer.close((err) => err ? reject(err) : resolve2());
|
|
16423
17312
|
});
|
|
16424
17313
|
}
|
|
16425
17314
|
getSocket() {
|
|
@@ -16616,8 +17505,8 @@ var DashboardServer = class {
|
|
|
16616
17505
|
* timeout denies it.
|
|
16617
17506
|
*/
|
|
16618
17507
|
makeApprovalCallback(sessionId) {
|
|
16619
|
-
return (request) => new Promise((
|
|
16620
|
-
this.pendingApprovals.set(request.id, { resolve, sessionId });
|
|
17508
|
+
return (request) => new Promise((resolve2) => {
|
|
17509
|
+
this.pendingApprovals.set(request.id, { resolve: resolve2, sessionId });
|
|
16621
17510
|
});
|
|
16622
17511
|
}
|
|
16623
17512
|
/**
|
|
@@ -17929,8 +18818,8 @@ var CloudClient = class _CloudClient {
|
|
|
17929
18818
|
}
|
|
17930
18819
|
};
|
|
17931
18820
|
function sleep(ms, signal) {
|
|
17932
|
-
return new Promise((
|
|
17933
|
-
const t = setTimeout(
|
|
18821
|
+
return new Promise((resolve2, reject) => {
|
|
18822
|
+
const t = setTimeout(resolve2, ms);
|
|
17934
18823
|
signal?.addEventListener("abort", () => {
|
|
17935
18824
|
clearTimeout(t);
|
|
17936
18825
|
reject(new Error("aborted"));
|