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/cli.cjs
CHANGED
|
@@ -1793,7 +1793,7 @@ var CASCADE_VERSION, CASCADE_CONFIG_FILE, CASCADE_DB_FILE, CASCADE_DASHBOARD_SEC
|
|
|
1793
1793
|
var init_constants = __esm({
|
|
1794
1794
|
"src/constants.ts"() {
|
|
1795
1795
|
init_pricing();
|
|
1796
|
-
CASCADE_VERSION = "0.
|
|
1796
|
+
CASCADE_VERSION = "0.51.0";
|
|
1797
1797
|
CASCADE_CONFIG_FILE = ".cascade/config.json";
|
|
1798
1798
|
CASCADE_DB_FILE = ".cascade/memory.db";
|
|
1799
1799
|
CASCADE_DASHBOARD_SECRET_FILE = ".cascade/dashboard-secret";
|
|
@@ -2311,49 +2311,40 @@ function isChatModel(id, methods) {
|
|
|
2311
2311
|
}
|
|
2312
2312
|
return !NON_CHAT.test(id);
|
|
2313
2313
|
}
|
|
2314
|
-
var
|
|
2314
|
+
var MODALITY_PATTERNS, NON_CHAT;
|
|
2315
2315
|
var init_model_filter = __esm({
|
|
2316
2316
|
"src/providers/model-filter.ts"() {
|
|
2317
|
-
|
|
2318
|
-
"embed",
|
|
2317
|
+
MODALITY_PATTERNS = [
|
|
2318
|
+
["embedding", ["embed"]],
|
|
2319
2319
|
// text-embedding-3, nomic-embed, mxbai-embed, snowflake-arctic-embed
|
|
2320
|
-
"(?:^|[-_/])
|
|
2321
|
-
"text-to-speech",
|
|
2322
|
-
"
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
"
|
|
2334
|
-
"
|
|
2335
|
-
"
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
"veo",
|
|
2346
|
-
"sora",
|
|
2347
|
-
"(?:^|[-_/])video(?:[-_/]|$)",
|
|
2348
|
-
// video
|
|
2349
|
-
"(?:^|[-_/])aqa(?:[-_/]|$)",
|
|
2350
|
-
// attributed QA
|
|
2351
|
-
"(?:^|[-_/])live(?:[-_/]|$)",
|
|
2352
|
-
// live-audio variants
|
|
2353
|
-
"(?:^|[-_/])(?:davinci|babbage)(?:[-_/]|$)"
|
|
2354
|
-
// legacy completion-only base models
|
|
2320
|
+
["transcription", ["whisper", "transcrib", "(?:^|[-_/])stt(?:[-_/]|$)"]],
|
|
2321
|
+
["speech", ["(?:^|[-_/])tts(?:[-_/]|$)", "text-to-speech", "speech"]],
|
|
2322
|
+
["video", ["veo", "sora", "(?:^|[-_/])video(?:[-_/]|$)"]],
|
|
2323
|
+
["image", [
|
|
2324
|
+
"dall[-]?e",
|
|
2325
|
+
"imagen",
|
|
2326
|
+
"image-generation",
|
|
2327
|
+
"gpt-image",
|
|
2328
|
+
"(?:^|[-_/])image(?:[-_/]|$)",
|
|
2329
|
+
"stable-diffusion",
|
|
2330
|
+
"sdxl",
|
|
2331
|
+
"flux"
|
|
2332
|
+
]],
|
|
2333
|
+
["moderation", ["moderation", "(?:^|[-_/])guard(?:[-_/]|$)"]],
|
|
2334
|
+
["rerank", ["rerank"]],
|
|
2335
|
+
["realtime", [
|
|
2336
|
+
"(?:^|[-_/])audio(?:[-_/]|$)",
|
|
2337
|
+
"native-audio",
|
|
2338
|
+
"realtime",
|
|
2339
|
+
"(?:^|[-_/])live(?:[-_/]|$)",
|
|
2340
|
+
// live-audio variants
|
|
2341
|
+
"(?:^|[-_/])aqa(?:[-_/]|$)"
|
|
2342
|
+
// attributed QA
|
|
2343
|
+
]],
|
|
2344
|
+
["legacy-completion", ["(?:^|[-_/])(?:davinci|babbage)(?:[-_/]|$)"]]
|
|
2355
2345
|
];
|
|
2356
|
-
|
|
2346
|
+
MODALITY_PATTERNS.map(([m, pats]) => [m, new RegExp(pats.join("|"), "i")]);
|
|
2347
|
+
NON_CHAT = new RegExp(MODALITY_PATTERNS.flatMap(([, p]) => p).join("|"), "i");
|
|
2357
2348
|
}
|
|
2358
2349
|
});
|
|
2359
2350
|
|
|
@@ -2921,6 +2912,118 @@ var init_azure = __esm({
|
|
|
2921
2912
|
}
|
|
2922
2913
|
});
|
|
2923
2914
|
|
|
2915
|
+
// src/providers/gemini-schema.ts
|
|
2916
|
+
function isPlainObject(v) {
|
|
2917
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
2918
|
+
}
|
|
2919
|
+
function normalizeType(value) {
|
|
2920
|
+
if (typeof value === "string") {
|
|
2921
|
+
return ALLOWED_TYPES.has(value) ? { type: value } : {};
|
|
2922
|
+
}
|
|
2923
|
+
if (Array.isArray(value)) {
|
|
2924
|
+
const types = value.filter((t) => typeof t === "string");
|
|
2925
|
+
const nullable = types.includes("null");
|
|
2926
|
+
const concrete = types.find((t) => t !== "null" && ALLOWED_TYPES.has(t));
|
|
2927
|
+
return {
|
|
2928
|
+
...concrete ? { type: concrete } : {},
|
|
2929
|
+
...nullable ? { nullable: true } : {}
|
|
2930
|
+
};
|
|
2931
|
+
}
|
|
2932
|
+
return {};
|
|
2933
|
+
}
|
|
2934
|
+
function normalizeEnum(value) {
|
|
2935
|
+
if (!Array.isArray(value) || value.length === 0) return void 0;
|
|
2936
|
+
return value.map((v) => typeof v === "string" ? v : JSON.stringify(v));
|
|
2937
|
+
}
|
|
2938
|
+
function sanitizeGeminiSchema(schema) {
|
|
2939
|
+
if (!isPlainObject(schema)) return void 0;
|
|
2940
|
+
const out = {};
|
|
2941
|
+
for (const [key, value] of Object.entries(schema)) {
|
|
2942
|
+
if (!ALLOWED_KEYS.has(key)) continue;
|
|
2943
|
+
switch (key) {
|
|
2944
|
+
case "type": {
|
|
2945
|
+
Object.assign(out, normalizeType(value));
|
|
2946
|
+
break;
|
|
2947
|
+
}
|
|
2948
|
+
case "enum": {
|
|
2949
|
+
const e = normalizeEnum(value);
|
|
2950
|
+
if (e) out["enum"] = e;
|
|
2951
|
+
break;
|
|
2952
|
+
}
|
|
2953
|
+
case "properties": {
|
|
2954
|
+
if (!isPlainObject(value)) break;
|
|
2955
|
+
const props = {};
|
|
2956
|
+
for (const [propName, propSchema] of Object.entries(value)) {
|
|
2957
|
+
const cleaned = sanitizeGeminiSchema(propSchema);
|
|
2958
|
+
props[propName] = cleaned && Object.keys(cleaned).length ? cleaned : { type: "string" };
|
|
2959
|
+
}
|
|
2960
|
+
out["properties"] = props;
|
|
2961
|
+
break;
|
|
2962
|
+
}
|
|
2963
|
+
case "items": {
|
|
2964
|
+
const cleaned = sanitizeGeminiSchema(value);
|
|
2965
|
+
if (cleaned) out["items"] = cleaned;
|
|
2966
|
+
break;
|
|
2967
|
+
}
|
|
2968
|
+
case "anyOf": {
|
|
2969
|
+
if (!Array.isArray(value)) break;
|
|
2970
|
+
const variants = value.map((v) => sanitizeGeminiSchema(v)).filter((v) => !!v && Object.keys(v).length > 0);
|
|
2971
|
+
if (variants.length) out["anyOf"] = variants;
|
|
2972
|
+
break;
|
|
2973
|
+
}
|
|
2974
|
+
case "required": {
|
|
2975
|
+
if (Array.isArray(value)) {
|
|
2976
|
+
out["required"] = value.filter((r) => typeof r === "string");
|
|
2977
|
+
}
|
|
2978
|
+
break;
|
|
2979
|
+
}
|
|
2980
|
+
default:
|
|
2981
|
+
out[key] = value;
|
|
2982
|
+
}
|
|
2983
|
+
}
|
|
2984
|
+
if (!out["type"] && out["properties"]) out["type"] = "object";
|
|
2985
|
+
if (!out["type"] && out["items"]) out["type"] = "array";
|
|
2986
|
+
return out;
|
|
2987
|
+
}
|
|
2988
|
+
function toGeminiParameters(inputSchema) {
|
|
2989
|
+
const cleaned = sanitizeGeminiSchema(inputSchema);
|
|
2990
|
+
if (!cleaned) return void 0;
|
|
2991
|
+
const props = cleaned["properties"];
|
|
2992
|
+
if (isPlainObject(props) && Object.keys(props).length === 0) return void 0;
|
|
2993
|
+
if (Object.keys(cleaned).length === 0) return void 0;
|
|
2994
|
+
return cleaned;
|
|
2995
|
+
}
|
|
2996
|
+
var ALLOWED_KEYS, ALLOWED_TYPES;
|
|
2997
|
+
var init_gemini_schema = __esm({
|
|
2998
|
+
"src/providers/gemini-schema.ts"() {
|
|
2999
|
+
ALLOWED_KEYS = /* @__PURE__ */ new Set([
|
|
3000
|
+
"type",
|
|
3001
|
+
"format",
|
|
3002
|
+
"title",
|
|
3003
|
+
"description",
|
|
3004
|
+
"nullable",
|
|
3005
|
+
"enum",
|
|
3006
|
+
"items",
|
|
3007
|
+
"properties",
|
|
3008
|
+
"required",
|
|
3009
|
+
"propertyOrdering",
|
|
3010
|
+
"minItems",
|
|
3011
|
+
"maxItems",
|
|
3012
|
+
"minProperties",
|
|
3013
|
+
"maxProperties",
|
|
3014
|
+
"minimum",
|
|
3015
|
+
"maximum",
|
|
3016
|
+
"minLength",
|
|
3017
|
+
"maxLength",
|
|
3018
|
+
"pattern",
|
|
3019
|
+
"example",
|
|
3020
|
+
"default",
|
|
3021
|
+
"anyOf"
|
|
3022
|
+
]);
|
|
3023
|
+
ALLOWED_TYPES = /* @__PURE__ */ new Set(["string", "number", "integer", "boolean", "array", "object"]);
|
|
3024
|
+
}
|
|
3025
|
+
});
|
|
3026
|
+
|
|
2924
3027
|
// src/providers/gemini.ts
|
|
2925
3028
|
var gemini_exports = {};
|
|
2926
3029
|
__export(gemini_exports, {
|
|
@@ -2933,6 +3036,7 @@ var init_gemini = __esm({
|
|
|
2933
3036
|
init_base();
|
|
2934
3037
|
init_pricing();
|
|
2935
3038
|
init_model_filter();
|
|
3039
|
+
init_gemini_schema();
|
|
2936
3040
|
GeminiProvider = class extends BaseProvider {
|
|
2937
3041
|
client;
|
|
2938
3042
|
constructor(config, model) {
|
|
@@ -3153,7 +3257,12 @@ var init_gemini = __esm({
|
|
|
3153
3257
|
return {
|
|
3154
3258
|
name: tool.name,
|
|
3155
3259
|
description: tool.description,
|
|
3156
|
-
parameters
|
|
3260
|
+
// Convert, never cast. Gemini's parameters field is an OpenAPI subset
|
|
3261
|
+
// that 400s on unknown keys, and MCP servers ship schemas carrying their
|
|
3262
|
+
// own extensions (GitHub's adds `x-mcp-header` to every header-bound
|
|
3263
|
+
// property). Casting sent those through verbatim and every request failed
|
|
3264
|
+
// before the model saw it. See providers/gemini-schema.ts.
|
|
3265
|
+
parameters: toGeminiParameters(tool.inputSchema)
|
|
3157
3266
|
};
|
|
3158
3267
|
}
|
|
3159
3268
|
};
|
|
@@ -3178,7 +3287,7 @@ async function nodeHttpFetch(input, init = {}, redirectCount = 0) {
|
|
|
3178
3287
|
headers["accept-encoding"] = "gzip, deflate, br";
|
|
3179
3288
|
}
|
|
3180
3289
|
const body = init.body == null ? void 0 : typeof init.body === "string" ? init.body : Buffer.from(init.body);
|
|
3181
|
-
return new Promise((
|
|
3290
|
+
return new Promise((resolve2, reject) => {
|
|
3182
3291
|
const req = lib.request(
|
|
3183
3292
|
{
|
|
3184
3293
|
hostname: u.hostname,
|
|
@@ -3195,7 +3304,7 @@ async function nodeHttpFetch(input, init = {}, redirectCount = 0) {
|
|
|
3195
3304
|
const nextUrl = new URL(location, u).href;
|
|
3196
3305
|
const downgrade = status === 303 || (status === 301 || status === 302) && method !== "GET" && method !== "HEAD";
|
|
3197
3306
|
const nextInit = downgrade ? { ...init, method: "GET", body: void 0 } : init;
|
|
3198
|
-
|
|
3307
|
+
resolve2(nodeHttpFetch(nextUrl, nextInit, redirectCount + 1));
|
|
3199
3308
|
return;
|
|
3200
3309
|
}
|
|
3201
3310
|
const encoding = (res.headers["content-encoding"] ?? "").toLowerCase();
|
|
@@ -3210,7 +3319,7 @@ async function nodeHttpFetch(input, init = {}, redirectCount = 0) {
|
|
|
3210
3319
|
if (Array.isArray(v)) respHeaders.set(k, v.join(", "));
|
|
3211
3320
|
else if (typeof v === "string") respHeaders.set(k, v);
|
|
3212
3321
|
}
|
|
3213
|
-
|
|
3322
|
+
resolve2(new Response(stream$1, {
|
|
3214
3323
|
status,
|
|
3215
3324
|
statusText: res.statusMessage ?? "",
|
|
3216
3325
|
headers: respHeaders
|
|
@@ -5009,6 +5118,14 @@ var BudgetConfigSchema = zod.z.object({
|
|
|
5009
5118
|
maxTokensPerRun: zod.z.number().int().positive().default(2e5),
|
|
5010
5119
|
/** Optional hard per-task cost ceiling (USD). Unset = only the token cap applies. */
|
|
5011
5120
|
maxCostPerRunUsd: zod.z.number().positive().optional(),
|
|
5121
|
+
/**
|
|
5122
|
+
* Consecutive systemic failures against ONE model before the run stops
|
|
5123
|
+
* launching work that would fail the same way (dead key, wrong model id,
|
|
5124
|
+
* exhausted quota). Lower reacts sooner and wastes less; higher absorbs a
|
|
5125
|
+
* longer transient before giving up. Only failures that will certainly repeat
|
|
5126
|
+
* are counted — see core/run-breaker.ts.
|
|
5127
|
+
*/
|
|
5128
|
+
failureThreshold: zod.z.number().int().min(1).max(20).default(3),
|
|
5012
5129
|
warnAtPct: zod.z.number().default(80)
|
|
5013
5130
|
});
|
|
5014
5131
|
var WorkspaceConfigSchema = zod.z.object({
|
|
@@ -6061,7 +6178,7 @@ var TpmLimiter = class {
|
|
|
6061
6178
|
}
|
|
6062
6179
|
const deficit = want - bucket.available;
|
|
6063
6180
|
const waitMs = Math.max(50, Math.ceil(deficit / bucket.tokensPerMinute * 6e4));
|
|
6064
|
-
await new Promise((
|
|
6181
|
+
await new Promise((resolve2) => setTimeout(resolve2, waitMs));
|
|
6065
6182
|
}
|
|
6066
6183
|
}
|
|
6067
6184
|
/**
|
|
@@ -6124,14 +6241,14 @@ var LocalRequestQueue = class {
|
|
|
6124
6241
|
this.active++;
|
|
6125
6242
|
return this.makeRelease();
|
|
6126
6243
|
}
|
|
6127
|
-
return new Promise((
|
|
6244
|
+
return new Promise((resolve2, reject) => {
|
|
6128
6245
|
let settled = false;
|
|
6129
6246
|
let timer;
|
|
6130
6247
|
const resolver = (release) => {
|
|
6131
6248
|
if (settled) return;
|
|
6132
6249
|
settled = true;
|
|
6133
6250
|
if (timer !== void 0) clearTimeout(timer);
|
|
6134
|
-
|
|
6251
|
+
resolve2(release);
|
|
6135
6252
|
};
|
|
6136
6253
|
if (timeoutMs !== void 0 && timeoutMs > 0) {
|
|
6137
6254
|
timer = setTimeout(() => {
|
|
@@ -6758,6 +6875,7 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
|
|
|
6758
6875
|
tpmLimiter;
|
|
6759
6876
|
localQueue;
|
|
6760
6877
|
taskAnalyzer;
|
|
6878
|
+
pendingFeedbackSource;
|
|
6761
6879
|
worldStateDB;
|
|
6762
6880
|
privacyPaths;
|
|
6763
6881
|
guidanceQueue;
|
|
@@ -7212,8 +7330,18 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
|
|
|
7212
7330
|
return windows.length ? Math.min(...windows) : void 0;
|
|
7213
7331
|
}
|
|
7214
7332
|
/** Wire the Cascade Auto task analyzer used for per-subtask model routing. */
|
|
7333
|
+
/**
|
|
7334
|
+
* Feed per-model thumbs counts into Auto routing. Forwarded to the analyzer
|
|
7335
|
+
* because that is where the public benchmark score is combined; the prior
|
|
7336
|
+
* adjusts that number rather than sitting anywhere else in the pipeline.
|
|
7337
|
+
*/
|
|
7338
|
+
setFeedbackSource(source) {
|
|
7339
|
+
this.taskAnalyzer?.setFeedbackSource(source);
|
|
7340
|
+
this.pendingFeedbackSource = source;
|
|
7341
|
+
}
|
|
7215
7342
|
setTaskAnalyzer(analyzer) {
|
|
7216
7343
|
this.taskAnalyzer = analyzer;
|
|
7344
|
+
if (this.pendingFeedbackSource) analyzer.setFeedbackSource(this.pendingFeedbackSource);
|
|
7217
7345
|
}
|
|
7218
7346
|
setWorldStateDB(db) {
|
|
7219
7347
|
this.worldStateDB = db;
|
|
@@ -7668,6 +7796,14 @@ var BaseTier = class extends EventEmitter__default.default {
|
|
|
7668
7796
|
this.emit("tier:status", event);
|
|
7669
7797
|
}
|
|
7670
7798
|
/** Record the model serving this tier; future status events carry it. */
|
|
7799
|
+
/**
|
|
7800
|
+
* The model actually serving this tier ("provider:id"), once one has been
|
|
7801
|
+
* chosen. The run breaker reads it to attribute a failure to a model rather
|
|
7802
|
+
* than to the subtask that happened to hit it.
|
|
7803
|
+
*/
|
|
7804
|
+
getServingModel() {
|
|
7805
|
+
return this.servingModel;
|
|
7806
|
+
}
|
|
7671
7807
|
setServingModel(model) {
|
|
7672
7808
|
this.servingModel = model || void 0;
|
|
7673
7809
|
}
|
|
@@ -8300,8 +8436,11 @@ Now execute your subtask using this context where relevant.`
|
|
|
8300
8436
|
const result = await this.runAgentLoop(systemPrompt, this.tools);
|
|
8301
8437
|
output = result.output;
|
|
8302
8438
|
toolCalls = result.toolCalls;
|
|
8303
|
-
this.
|
|
8304
|
-
|
|
8439
|
+
const mustProduceArtifact = this.requiresArtifact();
|
|
8440
|
+
if (mustProduceArtifact) {
|
|
8441
|
+
this.sendStatusUpdate({ progressPct: 65, currentAction: "Verifying required artifacts", status: "IN_PROGRESS" });
|
|
8442
|
+
}
|
|
8443
|
+
const artifactCheck = mustProduceArtifact ? await this.verifyArtifacts(assignment) : { ok: true, issues: [] };
|
|
8305
8444
|
if (!artifactCheck.ok) {
|
|
8306
8445
|
correctionAttempts = 1;
|
|
8307
8446
|
issues.push(...artifactCheck.issues);
|
|
@@ -8581,11 +8720,11 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : "") + textToolSuffix,
|
|
|
8581
8720
|
if (!wasApproved) return `Tool ${tc.name} was denied by user.`;
|
|
8582
8721
|
} else {
|
|
8583
8722
|
const LEGACY_APPROVAL_TIMEOUT_MS = 6e5;
|
|
8584
|
-
const legacyDecision = await new Promise((
|
|
8723
|
+
const legacyDecision = await new Promise((resolve2) => {
|
|
8585
8724
|
const eventName = `tool:approval-response:${this.id}-${tc.id}`;
|
|
8586
8725
|
const timer = setTimeout(() => {
|
|
8587
8726
|
this.removeAllListeners(eventName);
|
|
8588
|
-
|
|
8727
|
+
resolve2({ approved: false });
|
|
8589
8728
|
}, LEGACY_APPROVAL_TIMEOUT_MS);
|
|
8590
8729
|
timer.unref?.();
|
|
8591
8730
|
this.emit("tool:approval-request", {
|
|
@@ -8598,7 +8737,7 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : "") + textToolSuffix,
|
|
|
8598
8737
|
});
|
|
8599
8738
|
this.once(eventName, (d) => {
|
|
8600
8739
|
clearTimeout(timer);
|
|
8601
|
-
|
|
8740
|
+
resolve2(d);
|
|
8602
8741
|
});
|
|
8603
8742
|
});
|
|
8604
8743
|
if (legacyDecision.always) this.sessionApprovals.set(tc.name, legacyDecision.approved);
|
|
@@ -8618,6 +8757,9 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : "") + textToolSuffix,
|
|
|
8618
8757
|
tierId: this.id,
|
|
8619
8758
|
sessionId: this.taskId,
|
|
8620
8759
|
requireApproval: false,
|
|
8760
|
+
// Media generation can run for a minute; without this a cancelled run
|
|
8761
|
+
// still pays for an image nobody will see.
|
|
8762
|
+
...this.signal ? { signal: this.signal } : {},
|
|
8621
8763
|
saveSnapshot: async (path37, content) => {
|
|
8622
8764
|
this.store?.addFileSnapshot(this.taskId, path37, content);
|
|
8623
8765
|
},
|
|
@@ -8665,7 +8807,8 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : "") + textToolSuffix,
|
|
|
8665
8807
|
const result = await this.toolRegistry.execute(altTool, tc.input, {
|
|
8666
8808
|
tierId: this.id,
|
|
8667
8809
|
sessionId: this.taskId,
|
|
8668
|
-
requireApproval: false
|
|
8810
|
+
requireApproval: false,
|
|
8811
|
+
...this.signal ? { signal: this.signal } : {}
|
|
8669
8812
|
});
|
|
8670
8813
|
const str2 = typeof result === "string" ? result : JSON.stringify(result);
|
|
8671
8814
|
if (!str2.startsWith("Tool error:") && !str2.startsWith("Error:")) {
|
|
@@ -8687,7 +8830,8 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : "") + textToolSuffix,
|
|
|
8687
8830
|
const result = await this.toolRegistry.execute(newToolName, tc.input, {
|
|
8688
8831
|
tierId: this.id,
|
|
8689
8832
|
sessionId: this.taskId,
|
|
8690
|
-
requireApproval: false
|
|
8833
|
+
requireApproval: false,
|
|
8834
|
+
...this.signal ? { signal: this.signal } : {}
|
|
8691
8835
|
});
|
|
8692
8836
|
const str2 = typeof result === "string" ? result : JSON.stringify(result);
|
|
8693
8837
|
if (!str2.startsWith("Tool error:")) return `[Synthesized ${newToolName}]: ${str2}`;
|
|
@@ -9105,7 +9249,7 @@ var PeerBus = class extends EventEmitter__default.default {
|
|
|
9105
9249
|
this.outputs.set(subtaskId, entry);
|
|
9106
9250
|
this.emit("output:ready", entry);
|
|
9107
9251
|
const waiting = this.waiters.get(subtaskId) ?? [];
|
|
9108
|
-
for (const
|
|
9252
|
+
for (const resolve2 of waiting) resolve2(entry);
|
|
9109
9253
|
this.waiters.delete(subtaskId);
|
|
9110
9254
|
}
|
|
9111
9255
|
/**
|
|
@@ -9136,10 +9280,10 @@ var PeerBus = class extends EventEmitter__default.default {
|
|
|
9136
9280
|
waitFor(subtaskId, timeoutMs = 12e4) {
|
|
9137
9281
|
const existing = this.outputs.get(subtaskId);
|
|
9138
9282
|
if (existing && !this.retryPending.has(subtaskId)) return Promise.resolve(existing);
|
|
9139
|
-
return new Promise((
|
|
9283
|
+
return new Promise((resolve2, reject) => {
|
|
9140
9284
|
const resolver = (output) => {
|
|
9141
9285
|
clearTimeout(timer);
|
|
9142
|
-
|
|
9286
|
+
resolve2(output);
|
|
9143
9287
|
};
|
|
9144
9288
|
const timer = setTimeout(() => {
|
|
9145
9289
|
const waiting = this.waiters.get(subtaskId);
|
|
@@ -9193,10 +9337,10 @@ var PeerBus = class extends EventEmitter__default.default {
|
|
|
9193
9337
|
*/
|
|
9194
9338
|
collect(timeoutMs) {
|
|
9195
9339
|
const collected = [...this.broadcastLog];
|
|
9196
|
-
return new Promise((
|
|
9340
|
+
return new Promise((resolve2) => {
|
|
9197
9341
|
const timer = setTimeout(() => {
|
|
9198
9342
|
off();
|
|
9199
|
-
|
|
9343
|
+
resolve2(collected);
|
|
9200
9344
|
}, timeoutMs);
|
|
9201
9345
|
const handler = (msg) => {
|
|
9202
9346
|
collected.push({ fromId: msg.fromId, payload: msg.payload, timestamp: msg.timestamp });
|
|
@@ -9210,7 +9354,7 @@ var PeerBus = class extends EventEmitter__default.default {
|
|
|
9210
9354
|
const broadcasters = new Set(collected.map((m) => m.fromId));
|
|
9211
9355
|
if (broadcasters.size >= this.members.size) {
|
|
9212
9356
|
off();
|
|
9213
|
-
|
|
9357
|
+
resolve2(collected);
|
|
9214
9358
|
}
|
|
9215
9359
|
};
|
|
9216
9360
|
this.on("broadcast", checkComplete);
|
|
@@ -9228,7 +9372,7 @@ var PeerBus = class extends EventEmitter__default.default {
|
|
|
9228
9372
|
return;
|
|
9229
9373
|
}
|
|
9230
9374
|
this.emitCoordination(tierId, `\u23F3 waiting for ${filePath} (held by ${existing.holderId})`);
|
|
9231
|
-
return new Promise((
|
|
9375
|
+
return new Promise((resolve2, reject) => {
|
|
9232
9376
|
const timer = setTimeout(() => {
|
|
9233
9377
|
reject(new Error(`File lock timeout for ${filePath} (held by ${existing.holderId})`));
|
|
9234
9378
|
}, timeoutMs);
|
|
@@ -9236,7 +9380,7 @@ var PeerBus = class extends EventEmitter__default.default {
|
|
|
9236
9380
|
clearTimeout(timer);
|
|
9237
9381
|
this.fileLocks.set(filePath, { holderId: tierId, lockedAt: (/* @__PURE__ */ new Date()).toISOString(), waiters: [] });
|
|
9238
9382
|
this.emitCoordination(tierId, `\u{1F512} locked ${filePath}`);
|
|
9239
|
-
|
|
9383
|
+
resolve2();
|
|
9240
9384
|
});
|
|
9241
9385
|
});
|
|
9242
9386
|
}
|
|
@@ -9261,11 +9405,11 @@ var PeerBus = class extends EventEmitter__default.default {
|
|
|
9261
9405
|
*/
|
|
9262
9406
|
waitForFileRelease(filePath, timeoutMs = 3e4) {
|
|
9263
9407
|
if (!this.fileLocks.has(filePath)) return Promise.resolve();
|
|
9264
|
-
return new Promise((
|
|
9408
|
+
return new Promise((resolve2, reject) => {
|
|
9265
9409
|
const timer = setTimeout(() => reject(new Error(`Timeout waiting for file release: ${filePath}`)), timeoutMs);
|
|
9266
9410
|
this.once(`file:released:${filePath}`, () => {
|
|
9267
9411
|
clearTimeout(timer);
|
|
9268
|
-
|
|
9412
|
+
resolve2();
|
|
9269
9413
|
});
|
|
9270
9414
|
});
|
|
9271
9415
|
}
|
|
@@ -9331,13 +9475,13 @@ var PeerBus = class extends EventEmitter__default.default {
|
|
|
9331
9475
|
this.emit(`barrier:${barrierName}`);
|
|
9332
9476
|
return;
|
|
9333
9477
|
}
|
|
9334
|
-
return new Promise((
|
|
9478
|
+
return new Promise((resolve2, reject) => {
|
|
9335
9479
|
const timer = setTimeout(() => {
|
|
9336
9480
|
reject(new Error(`Barrier timeout: ${barrierName} (${bar.arrived.size}/${bar.total} arrived)`));
|
|
9337
9481
|
}, 12e4);
|
|
9338
9482
|
this.once(`barrier:${barrierName}`, () => {
|
|
9339
9483
|
clearTimeout(timer);
|
|
9340
|
-
|
|
9484
|
+
resolve2();
|
|
9341
9485
|
});
|
|
9342
9486
|
});
|
|
9343
9487
|
}
|
|
@@ -9404,6 +9548,7 @@ var T2Manager = class extends BaseTier {
|
|
|
9404
9548
|
escalations = [];
|
|
9405
9549
|
peerSyncBuffer = [];
|
|
9406
9550
|
store;
|
|
9551
|
+
runBreaker;
|
|
9407
9552
|
t3PeerBus = new PeerBus();
|
|
9408
9553
|
// ← T3↔T3 bus (local to this T2)
|
|
9409
9554
|
t2PeerBus;
|
|
@@ -9434,6 +9579,15 @@ var T2Manager = class extends BaseTier {
|
|
|
9434
9579
|
this.router = router;
|
|
9435
9580
|
this.toolRegistry = toolRegistry;
|
|
9436
9581
|
}
|
|
9582
|
+
/**
|
|
9583
|
+
* Share the run-wide circuit breaker. Every T2 in a run gets the SAME
|
|
9584
|
+
* instance: three sections each failing once against a dead model is the
|
|
9585
|
+
* same fact as one section failing three times, and a per-section breaker
|
|
9586
|
+
* would never see it.
|
|
9587
|
+
*/
|
|
9588
|
+
setRunBreaker(breaker) {
|
|
9589
|
+
this.runBreaker = breaker;
|
|
9590
|
+
}
|
|
9437
9591
|
setStore(store) {
|
|
9438
9592
|
this.store = store;
|
|
9439
9593
|
}
|
|
@@ -9557,7 +9711,8 @@ Guidance (must be followed): ${decision.note}`
|
|
|
9557
9711
|
const overallStatus = this.determineStatus(t3Results);
|
|
9558
9712
|
const isOk = overallStatus === "COMPLETED" || overallStatus === "PARTIAL";
|
|
9559
9713
|
this.setStatus(isOk ? "COMPLETED" : "FAILED", summary);
|
|
9560
|
-
|
|
9714
|
+
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";
|
|
9715
|
+
this.sendStatusUpdate({ progressPct: 100, currentAction: sectionAction, status: "IN_PROGRESS", output: summary });
|
|
9561
9716
|
const result = {
|
|
9562
9717
|
sectionId: assignment.sectionId,
|
|
9563
9718
|
sectionTitle: assignment.sectionTitle,
|
|
@@ -9727,6 +9882,24 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
|
9727
9882
|
const reinforceCfg = this.router.getReinforcementsConfig?.() ?? { enabled: false, maxPerSection: 4 };
|
|
9728
9883
|
let reinforcementsAdded = 0;
|
|
9729
9884
|
while (remaining.size > 0) {
|
|
9885
|
+
if (this.runBreaker?.isOpen()) {
|
|
9886
|
+
const why = this.runBreaker.skipMessage();
|
|
9887
|
+
this.log(`Run breaker open \u2014 skipping ${remaining.size} remaining subtask(s)`);
|
|
9888
|
+
for (const id of remaining) {
|
|
9889
|
+
this.t3PeerBus.publish(this.id, id, why, "FAILED");
|
|
9890
|
+
resultMap.set(id, {
|
|
9891
|
+
subtaskId: id,
|
|
9892
|
+
status: "FAILED",
|
|
9893
|
+
output: why,
|
|
9894
|
+
testResults: { checksRun: [], passed: [], failed: [] },
|
|
9895
|
+
issues: [why],
|
|
9896
|
+
peerSyncsUsed: [],
|
|
9897
|
+
correctionAttempts: 0
|
|
9898
|
+
});
|
|
9899
|
+
}
|
|
9900
|
+
remaining.clear();
|
|
9901
|
+
break;
|
|
9902
|
+
}
|
|
9730
9903
|
const runnableIds = [...remaining].filter((id) => (inDegree.get(id) ?? 0) === 0);
|
|
9731
9904
|
if (runnableIds.length === 0) {
|
|
9732
9905
|
const fallbackId = [...remaining].sort(
|
|
@@ -9829,8 +10002,32 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
|
9829
10002
|
remaining.delete(id);
|
|
9830
10003
|
const r = waveResults[i];
|
|
9831
10004
|
if (r.status === "rejected") {
|
|
9832
|
-
this.log(`T3 worker ${id} failed: ${r.reason instanceof Error ? r.reason.message : String(r.reason)} \u2014 retrying once`);
|
|
9833
10005
|
const assignment = sanitizedAssignments.find((a) => a.subtaskId === id);
|
|
10006
|
+
const failedModel = workerMap.get(id)?.getServingModel();
|
|
10007
|
+
const tripped = this.runBreaker?.record(r.reason, failedModel);
|
|
10008
|
+
if (tripped) {
|
|
10009
|
+
this.log(`Run breaker opened: ${tripped.failures} consecutive ${tripped.kind} failures on ${tripped.modelId} \u2014 stopping`);
|
|
10010
|
+
this.emit("run:breaker", tripped);
|
|
10011
|
+
}
|
|
10012
|
+
if (this.runBreaker?.isOpen()) {
|
|
10013
|
+
const why = this.runBreaker.skipMessage();
|
|
10014
|
+
this.log(`T3 worker ${id} failed and the run breaker is open \u2014 not retrying`);
|
|
10015
|
+
this.t3PeerBus.publish(this.id, id, why, "FAILED");
|
|
10016
|
+
resultMap.set(id, {
|
|
10017
|
+
subtaskId: id,
|
|
10018
|
+
status: "FAILED",
|
|
10019
|
+
output: why,
|
|
10020
|
+
testResults: { checksRun: [], passed: [], failed: [] },
|
|
10021
|
+
issues: [why],
|
|
10022
|
+
peerSyncsUsed: [],
|
|
10023
|
+
correctionAttempts: 0
|
|
10024
|
+
});
|
|
10025
|
+
for (const dependent of adj.get(id) ?? []) {
|
|
10026
|
+
inDegree.set(dependent, Math.max(0, (inDegree.get(dependent) ?? 0) - 1));
|
|
10027
|
+
}
|
|
10028
|
+
continue;
|
|
10029
|
+
}
|
|
10030
|
+
this.log(`T3 worker ${id} failed: ${r.reason instanceof Error ? r.reason.message : String(r.reason)} \u2014 retrying once`);
|
|
9834
10031
|
try {
|
|
9835
10032
|
const retried = await this.retryT3(assignment, taskId);
|
|
9836
10033
|
resultMap.set(id, retried);
|
|
@@ -10165,6 +10362,7 @@ var T1Administrator = class extends BaseTier {
|
|
|
10165
10362
|
t2Managers = /* @__PURE__ */ new Map();
|
|
10166
10363
|
escalations = [];
|
|
10167
10364
|
store;
|
|
10365
|
+
runBreaker;
|
|
10168
10366
|
t2PeerBus = new PeerBus();
|
|
10169
10367
|
permissionEscalator;
|
|
10170
10368
|
toolCreator;
|
|
@@ -10179,6 +10377,10 @@ var T1Administrator = class extends BaseTier {
|
|
|
10179
10377
|
this.toolRegistry = toolRegistry;
|
|
10180
10378
|
this.config = config;
|
|
10181
10379
|
}
|
|
10380
|
+
/** Share the run-wide circuit breaker with every section manager. */
|
|
10381
|
+
setRunBreaker(breaker) {
|
|
10382
|
+
this.runBreaker = breaker;
|
|
10383
|
+
}
|
|
10182
10384
|
setStore(store) {
|
|
10183
10385
|
this.store = store;
|
|
10184
10386
|
}
|
|
@@ -10541,6 +10743,7 @@ SPEC RULES \u2014 each subtask is a self-contained spec slice (workers execute f
|
|
|
10541
10743
|
};
|
|
10542
10744
|
const managers = sections.map((section) => {
|
|
10543
10745
|
const manager = new T2Manager(this.router, this.toolRegistry, this.id);
|
|
10746
|
+
if (this.runBreaker) manager.setRunBreaker(this.runBreaker);
|
|
10544
10747
|
manager.setHierarchyContext(`You are a T2 Manager for the section "${section.sectionTitle}". You are part of a COMPLEX task overseen by T1 Administrator.`);
|
|
10545
10748
|
if (this.store) {
|
|
10546
10749
|
manager.setStore(this.store);
|
|
@@ -10829,8 +11032,156 @@ Reply with exactly one word: YES, NO, or UNSURE.
|
|
|
10829
11032
|
// src/core/cascade.ts
|
|
10830
11033
|
init_cost();
|
|
10831
11034
|
|
|
10832
|
-
// src/
|
|
10833
|
-
|
|
11035
|
+
// src/core/multimodal/registry.ts
|
|
11036
|
+
init_pricing();
|
|
11037
|
+
var CAPABILITIES = [
|
|
11038
|
+
// ── Image ──
|
|
11039
|
+
{
|
|
11040
|
+
modality: "image",
|
|
11041
|
+
provider: "openai",
|
|
11042
|
+
modelId: "dall-e-3",
|
|
11043
|
+
api: "openai-images"
|
|
11044
|
+
},
|
|
11045
|
+
{
|
|
11046
|
+
modality: "image",
|
|
11047
|
+
provider: "gemini",
|
|
11048
|
+
modelId: "gemini-3-pro-image",
|
|
11049
|
+
api: "gemini-predict"
|
|
11050
|
+
},
|
|
11051
|
+
// ── Speech (text → audio) ──
|
|
11052
|
+
{
|
|
11053
|
+
modality: "speech",
|
|
11054
|
+
provider: "openai",
|
|
11055
|
+
modelId: "tts-1",
|
|
11056
|
+
api: "openai-speech"
|
|
11057
|
+
},
|
|
11058
|
+
{
|
|
11059
|
+
// The only quality claim in this table that needs no leaderboard: OpenAI
|
|
11060
|
+
// ships these as the same voice at two fidelities and charges double for
|
|
11061
|
+
// this one. That is the vendor's own ordering, not an inference.
|
|
11062
|
+
modality: "speech",
|
|
11063
|
+
provider: "openai",
|
|
11064
|
+
modelId: "tts-1-hd",
|
|
11065
|
+
api: "openai-speech",
|
|
11066
|
+
quality: 70,
|
|
11067
|
+
qualityBasis: "OpenAI's own hd variant of tts-1, priced 2x"
|
|
11068
|
+
},
|
|
11069
|
+
// ── Transcription (audio → text) ──
|
|
11070
|
+
{
|
|
11071
|
+
modality: "transcription",
|
|
11072
|
+
provider: "openai",
|
|
11073
|
+
modelId: "whisper-1",
|
|
11074
|
+
api: "openai-transcriptions"
|
|
11075
|
+
},
|
|
11076
|
+
// ── Video ──
|
|
11077
|
+
{
|
|
11078
|
+
// Listed so `describe()` can tell the truth about what exists, and so the
|
|
11079
|
+
// planner is told video is out of reach rather than left to invent a tool.
|
|
11080
|
+
// Veo is a long-running operation: submit, poll, then fetch bytes from a
|
|
11081
|
+
// signed URL. That is a different execution shape from every other entry
|
|
11082
|
+
// here and is not wired up — claiming otherwise would produce a tool that
|
|
11083
|
+
// fails at call time instead of an honest "not yet".
|
|
11084
|
+
modality: "video",
|
|
11085
|
+
provider: "gemini",
|
|
11086
|
+
modelId: "veo-3.1-generate-001",
|
|
11087
|
+
api: "gemini-predict",
|
|
11088
|
+
unsupported: "Veo runs as a long-running operation (submit \u2192 poll \u2192 fetch). Not wired up yet."
|
|
11089
|
+
}
|
|
11090
|
+
];
|
|
11091
|
+
function capabilityCost(cap) {
|
|
11092
|
+
const match = findPricing(cap.modelId, cap.provider, {
|
|
11093
|
+
fallbackProviders: cap.provider === "gemini" ? ["vertex"] : []
|
|
11094
|
+
});
|
|
11095
|
+
const rate = match?.entry.rates?.[0];
|
|
11096
|
+
if (!rate) return null;
|
|
11097
|
+
const unitLabel = {
|
|
11098
|
+
per_image: "per image",
|
|
11099
|
+
per_second: "per second of video",
|
|
11100
|
+
per_character: "per character",
|
|
11101
|
+
per_audio_minute: "per audio minute"
|
|
11102
|
+
};
|
|
11103
|
+
const unit = unitLabel[rate.unit] ?? rate.unit;
|
|
11104
|
+
const amount = rate.amount;
|
|
11105
|
+
const shown = amount < 1e-3 ? amount.toExponential(2) : amount.toFixed(4);
|
|
11106
|
+
return { label: `$${shown} ${unit}`, amount, unit };
|
|
11107
|
+
}
|
|
11108
|
+
var MultimodalRegistry = class {
|
|
11109
|
+
/** Provider types the user has actually configured a key for. */
|
|
11110
|
+
available;
|
|
11111
|
+
constructor(availableProviders) {
|
|
11112
|
+
this.available = new Set(availableProviders);
|
|
11113
|
+
}
|
|
11114
|
+
/** Every capability whose provider is configured AND which Cascade can call. */
|
|
11115
|
+
usable(modality) {
|
|
11116
|
+
return CAPABILITIES.filter(
|
|
11117
|
+
(c) => !c.unsupported && this.available.has(c.provider) && (!modality || c.modality === modality)
|
|
11118
|
+
);
|
|
11119
|
+
}
|
|
11120
|
+
/** Modalities this account can actually generate right now. */
|
|
11121
|
+
availableModalities() {
|
|
11122
|
+
return [...new Set(this.usable().map((c) => c.modality))];
|
|
11123
|
+
}
|
|
11124
|
+
/**
|
|
11125
|
+
* Pick a model for a modality.
|
|
11126
|
+
*
|
|
11127
|
+
* Ranked models beat unranked ones, then higher quality, then lower unit
|
|
11128
|
+
* price. An explicit `preferModel` wins outright when it is usable — the user
|
|
11129
|
+
* naming a model is a stronger signal than any prior.
|
|
11130
|
+
*/
|
|
11131
|
+
select(modality, opts = {}) {
|
|
11132
|
+
const pool = this.usable(modality);
|
|
11133
|
+
if (!pool.length) return null;
|
|
11134
|
+
if (opts.preferModel) {
|
|
11135
|
+
const wanted = pool.find((c) => c.modelId === opts.preferModel);
|
|
11136
|
+
if (wanted) {
|
|
11137
|
+
return { capability: wanted, cost: capabilityCost(wanted), reason: `Requested explicitly: ${wanted.modelId}.` };
|
|
11138
|
+
}
|
|
11139
|
+
}
|
|
11140
|
+
const scored = pool.map((c) => ({ c, cost: capabilityCost(c) })).sort((a, b) => {
|
|
11141
|
+
const qa = a.c.quality ?? -1;
|
|
11142
|
+
const qb = b.c.quality ?? -1;
|
|
11143
|
+
if (qa !== qb) return qb - qa;
|
|
11144
|
+
const pa = a.cost?.amount ?? Number.POSITIVE_INFINITY;
|
|
11145
|
+
const pb = b.cost?.amount ?? Number.POSITIVE_INFINITY;
|
|
11146
|
+
return pa - pb;
|
|
11147
|
+
});
|
|
11148
|
+
const best = scored[0];
|
|
11149
|
+
const ranked = best.c.quality !== void 0;
|
|
11150
|
+
const reason = ranked ? `${best.c.modelId} (${best.c.provider}) \u2014 ranked highest for ${modality}: ${best.c.qualityBasis}.` + (best.cost ? ` ${best.cost.label}.` : "") : `${best.c.modelId} (${best.c.provider}) \u2014 cheapest usable ${modality} model` + (best.cost ? ` at ${best.cost.label}` : "") + ". No published quality ranking is applied for this modality, so this is a cost choice, not a quality one.";
|
|
11151
|
+
return { capability: best.c, cost: best.cost, reason };
|
|
11152
|
+
}
|
|
11153
|
+
/**
|
|
11154
|
+
* A plain-language account of what this account can and cannot generate —
|
|
11155
|
+
* including the things it cannot, and why. The planner sees this, so being
|
|
11156
|
+
* candid here is what stops it inventing a `generate_music` call.
|
|
11157
|
+
*/
|
|
11158
|
+
describe() {
|
|
11159
|
+
const lines = [];
|
|
11160
|
+
const usable = this.usable();
|
|
11161
|
+
if (usable.length) {
|
|
11162
|
+
lines.push("Generation models available on your configured providers:");
|
|
11163
|
+
for (const modality of ["image", "speech", "transcription", "video"]) {
|
|
11164
|
+
const forModality = usable.filter((c) => c.modality === modality);
|
|
11165
|
+
if (!forModality.length) continue;
|
|
11166
|
+
const shown = forModality.map((c) => {
|
|
11167
|
+
const cost = capabilityCost(c);
|
|
11168
|
+
return `${c.modelId}${cost ? ` (${cost.label})` : ""}`;
|
|
11169
|
+
});
|
|
11170
|
+
lines.push(` ${modality}: ${shown.join(", ")}`);
|
|
11171
|
+
}
|
|
11172
|
+
} else {
|
|
11173
|
+
lines.push("No generation models are available \u2014 none of your configured providers offers one.");
|
|
11174
|
+
}
|
|
11175
|
+
const blocked = CAPABILITIES.filter((c) => c.unsupported && this.available.has(c.provider));
|
|
11176
|
+
for (const c of blocked) {
|
|
11177
|
+
lines.push(` ${c.modality}: ${c.modelId} exists on your account but is not callable. ${c.unsupported}`);
|
|
11178
|
+
}
|
|
11179
|
+
lines.push(
|
|
11180
|
+
" music: no supported provider exposes a music-generation API, so Cascade cannot generate music."
|
|
11181
|
+
);
|
|
11182
|
+
return lines.join("\n");
|
|
11183
|
+
}
|
|
11184
|
+
};
|
|
10834
11185
|
|
|
10835
11186
|
// src/tools/base.ts
|
|
10836
11187
|
var BaseTool = class {
|
|
@@ -10850,7 +11201,449 @@ var BaseTool = class {
|
|
|
10850
11201
|
}
|
|
10851
11202
|
};
|
|
10852
11203
|
|
|
10853
|
-
// src/
|
|
11204
|
+
// src/core/router/provider-errors.ts
|
|
11205
|
+
var SYSTEMIC = /* @__PURE__ */ new Set([
|
|
11206
|
+
"rate_limit",
|
|
11207
|
+
"quota_exhausted",
|
|
11208
|
+
"auth",
|
|
11209
|
+
"model_unavailable"
|
|
11210
|
+
]);
|
|
11211
|
+
function isSystemicKind(kind) {
|
|
11212
|
+
return SYSTEMIC.has(kind);
|
|
11213
|
+
}
|
|
11214
|
+
function statusOf(err) {
|
|
11215
|
+
if (!err || typeof err !== "object") return void 0;
|
|
11216
|
+
const e = err;
|
|
11217
|
+
for (const key of ["status", "statusCode", "code"]) {
|
|
11218
|
+
const v = e[key];
|
|
11219
|
+
if (typeof v === "number" && v >= 100 && v < 600) return v;
|
|
11220
|
+
}
|
|
11221
|
+
const resp = e["response"];
|
|
11222
|
+
if (resp && typeof resp["status"] === "number") return resp["status"];
|
|
11223
|
+
const inner = e["error"];
|
|
11224
|
+
if (inner && typeof inner["code"] === "number") return inner["code"];
|
|
11225
|
+
return void 0;
|
|
11226
|
+
}
|
|
11227
|
+
function messageOf(err) {
|
|
11228
|
+
if (err instanceof Error) return err.message;
|
|
11229
|
+
if (typeof err === "string") return err;
|
|
11230
|
+
if (err && typeof err === "object") {
|
|
11231
|
+
const e = err;
|
|
11232
|
+
if (typeof e["message"] === "string") return e["message"];
|
|
11233
|
+
const inner = e["error"];
|
|
11234
|
+
if (inner && typeof inner["message"] === "string") return inner["message"];
|
|
11235
|
+
try {
|
|
11236
|
+
return JSON.stringify(err);
|
|
11237
|
+
} catch {
|
|
11238
|
+
}
|
|
11239
|
+
}
|
|
11240
|
+
return String(err);
|
|
11241
|
+
}
|
|
11242
|
+
function classifyProviderError(err) {
|
|
11243
|
+
const raw = messageOf(err).trim();
|
|
11244
|
+
const status = statusOf(err);
|
|
11245
|
+
const m = raw.toLowerCase();
|
|
11246
|
+
const kind = (() => {
|
|
11247
|
+
if (status === 429) {
|
|
11248
|
+
return /quota|billing|credit|exceeded your current quota|insufficient/.test(m) ? "quota_exhausted" : "rate_limit";
|
|
11249
|
+
}
|
|
11250
|
+
if (status === 401 || status === 403) return "auth";
|
|
11251
|
+
if (status === 404) return "model_unavailable";
|
|
11252
|
+
if (/rate limit|too many requests|resource_exhausted/.test(m)) {
|
|
11253
|
+
return /quota|billing|credit|insufficient/.test(m) ? "quota_exhausted" : "rate_limit";
|
|
11254
|
+
}
|
|
11255
|
+
if (/quota|billing|insufficient_quota|insufficient credit|payment required/.test(m)) return "quota_exhausted";
|
|
11256
|
+
if (/api key|unauthorized|authentication|permission denied|invalid[_ ]api/.test(m)) return "auth";
|
|
11257
|
+
if (/model .*(not found|does not exist|unavailable)|no such model|unknown model|not supported for/.test(m)) {
|
|
11258
|
+
return "model_unavailable";
|
|
11259
|
+
}
|
|
11260
|
+
if (/context length|too many tokens|maximum context|prompt is too long|input too long/.test(m)) return "context_length";
|
|
11261
|
+
if (/safety|content filter|blocked|recitation|prohibited/.test(m)) return "content_filter";
|
|
11262
|
+
if (/econnreset|etimedout|enotfound|socket hang up|network|fetch failed|aborted/.test(m)) return "network";
|
|
11263
|
+
return "unknown";
|
|
11264
|
+
})();
|
|
11265
|
+
return { kind, systemic: isSystemicKind(kind), status, raw };
|
|
11266
|
+
}
|
|
11267
|
+
function describeProviderError(c, modelId) {
|
|
11268
|
+
const on = modelId ? ` on ${modelId}` : "";
|
|
11269
|
+
const detail = c.raw ? ` Provider said: ${c.raw}` : "";
|
|
11270
|
+
switch (c.kind) {
|
|
11271
|
+
case "rate_limit":
|
|
11272
|
+
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}`;
|
|
11273
|
+
case "quota_exhausted":
|
|
11274
|
+
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}`;
|
|
11275
|
+
case "auth":
|
|
11276
|
+
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}`;
|
|
11277
|
+
case "model_unavailable":
|
|
11278
|
+
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}`;
|
|
11279
|
+
case "context_length":
|
|
11280
|
+
return `The prompt was too long${on}. Reduce the attached context, or use a model with a larger context window for this tier.${detail}`;
|
|
11281
|
+
case "content_filter":
|
|
11282
|
+
return `The provider's safety filter refused this request${on}.${detail}`;
|
|
11283
|
+
case "network":
|
|
11284
|
+
return `Network error reaching the provider${on}. This is usually transient.${detail}`;
|
|
11285
|
+
default:
|
|
11286
|
+
return `The model call failed${on}.${detail}`;
|
|
11287
|
+
}
|
|
11288
|
+
}
|
|
11289
|
+
|
|
11290
|
+
// src/core/multimodal/generate.ts
|
|
11291
|
+
var DEFAULT_TIMEOUT_MS = 12e4;
|
|
11292
|
+
async function callProvider(fn, modelId) {
|
|
11293
|
+
try {
|
|
11294
|
+
return await fn();
|
|
11295
|
+
} catch (err) {
|
|
11296
|
+
const classified = classifyProviderError(err);
|
|
11297
|
+
throw new Error(describeProviderError(classified, modelId));
|
|
11298
|
+
}
|
|
11299
|
+
}
|
|
11300
|
+
async function postJson(url, body, headers, signal) {
|
|
11301
|
+
const ac = new AbortController();
|
|
11302
|
+
const timer = setTimeout(() => ac.abort(), DEFAULT_TIMEOUT_MS);
|
|
11303
|
+
const onAbort = () => ac.abort();
|
|
11304
|
+
signal?.addEventListener("abort", onAbort);
|
|
11305
|
+
try {
|
|
11306
|
+
const res = await fetch(url, {
|
|
11307
|
+
method: "POST",
|
|
11308
|
+
headers: { "Content-Type": "application/json", ...headers },
|
|
11309
|
+
body: JSON.stringify(body),
|
|
11310
|
+
signal: ac.signal
|
|
11311
|
+
});
|
|
11312
|
+
if (!res.ok) {
|
|
11313
|
+
const text = await res.text().catch(() => "");
|
|
11314
|
+
throw Object.assign(new Error(text || res.statusText), { status: res.status });
|
|
11315
|
+
}
|
|
11316
|
+
return res;
|
|
11317
|
+
} finally {
|
|
11318
|
+
clearTimeout(timer);
|
|
11319
|
+
signal?.removeEventListener("abort", onAbort);
|
|
11320
|
+
}
|
|
11321
|
+
}
|
|
11322
|
+
function baseUrlFor(cfg, fallback) {
|
|
11323
|
+
return (cfg.baseUrl ?? fallback).replace(/\/+$/, "");
|
|
11324
|
+
}
|
|
11325
|
+
function stamp(ext) {
|
|
11326
|
+
return `cascade-${Date.now()}.${ext}`;
|
|
11327
|
+
}
|
|
11328
|
+
async function generateImage(cap, cfg, req, signal) {
|
|
11329
|
+
return callProvider(async () => {
|
|
11330
|
+
if (cap.api === "openai-images") {
|
|
11331
|
+
const url = `${baseUrlFor(cfg, "https://api.openai.com/v1")}/images/generations`;
|
|
11332
|
+
const res = await postJson(url, {
|
|
11333
|
+
model: cap.modelId,
|
|
11334
|
+
prompt: req.prompt,
|
|
11335
|
+
size: req.size ?? "1024x1024",
|
|
11336
|
+
n: 1,
|
|
11337
|
+
// b64 rather than a URL: see the file header — provider URLs expire.
|
|
11338
|
+
response_format: "b64_json"
|
|
11339
|
+
}, { Authorization: `Bearer ${cfg.apiKey ?? ""}` }, signal);
|
|
11340
|
+
const body = await res.json();
|
|
11341
|
+
const first = body.data?.[0];
|
|
11342
|
+
if (!first?.b64_json) throw new Error("Image response contained no image data.");
|
|
11343
|
+
return {
|
|
11344
|
+
data: Buffer.from(first.b64_json, "base64"),
|
|
11345
|
+
mimeType: "image/png",
|
|
11346
|
+
filename: stamp("png"),
|
|
11347
|
+
modelId: cap.modelId,
|
|
11348
|
+
provider: cap.provider,
|
|
11349
|
+
...first.revised_prompt ? { revisedPrompt: first.revised_prompt } : {}
|
|
11350
|
+
};
|
|
11351
|
+
}
|
|
11352
|
+
if (cap.api === "gemini-predict") {
|
|
11353
|
+
const base = baseUrlFor(cfg, "https://generativelanguage.googleapis.com/v1beta");
|
|
11354
|
+
const url = `${base}/models/${encodeURIComponent(cap.modelId)}:predict`;
|
|
11355
|
+
const res = await postJson(url, {
|
|
11356
|
+
instances: [{ prompt: req.prompt }],
|
|
11357
|
+
parameters: { sampleCount: 1 }
|
|
11358
|
+
}, { "x-goog-api-key": cfg.apiKey ?? "" }, signal);
|
|
11359
|
+
const body = await res.json();
|
|
11360
|
+
const first = body.predictions?.[0];
|
|
11361
|
+
if (!first?.bytesBase64Encoded) throw new Error("Image response contained no image data.");
|
|
11362
|
+
const mimeType = first.mimeType ?? "image/png";
|
|
11363
|
+
return {
|
|
11364
|
+
data: Buffer.from(first.bytesBase64Encoded, "base64"),
|
|
11365
|
+
mimeType,
|
|
11366
|
+
filename: stamp(mimeType.includes("jpeg") ? "jpg" : "png"),
|
|
11367
|
+
modelId: cap.modelId,
|
|
11368
|
+
provider: cap.provider
|
|
11369
|
+
};
|
|
11370
|
+
}
|
|
11371
|
+
throw new Error(`${cap.modelId} is not an image generator.`);
|
|
11372
|
+
}, cap.modelId);
|
|
11373
|
+
}
|
|
11374
|
+
async function generateSpeech(cap, cfg, req, signal) {
|
|
11375
|
+
return callProvider(async () => {
|
|
11376
|
+
if (cap.api !== "openai-speech") throw new Error(`${cap.modelId} is not a speech model.`);
|
|
11377
|
+
const format = req.format ?? "mp3";
|
|
11378
|
+
const url = `${baseUrlFor(cfg, "https://api.openai.com/v1")}/audio/speech`;
|
|
11379
|
+
const res = await postJson(url, {
|
|
11380
|
+
model: cap.modelId,
|
|
11381
|
+
input: req.text,
|
|
11382
|
+
voice: req.voice ?? "alloy",
|
|
11383
|
+
response_format: format
|
|
11384
|
+
}, { Authorization: `Bearer ${cfg.apiKey ?? ""}` }, signal);
|
|
11385
|
+
const mime = {
|
|
11386
|
+
mp3: "audio/mpeg",
|
|
11387
|
+
opus: "audio/opus",
|
|
11388
|
+
aac: "audio/aac",
|
|
11389
|
+
flac: "audio/flac",
|
|
11390
|
+
wav: "audio/wav"
|
|
11391
|
+
};
|
|
11392
|
+
return {
|
|
11393
|
+
data: Buffer.from(await res.arrayBuffer()),
|
|
11394
|
+
mimeType: mime[format] ?? "application/octet-stream",
|
|
11395
|
+
filename: stamp(format),
|
|
11396
|
+
modelId: cap.modelId,
|
|
11397
|
+
provider: cap.provider
|
|
11398
|
+
};
|
|
11399
|
+
}, cap.modelId);
|
|
11400
|
+
}
|
|
11401
|
+
async function transcribeAudio(cap, cfg, req, signal) {
|
|
11402
|
+
return callProvider(async () => {
|
|
11403
|
+
if (cap.api !== "openai-transcriptions") throw new Error(`${cap.modelId} is not a transcription model.`);
|
|
11404
|
+
const url = `${baseUrlFor(cfg, "https://api.openai.com/v1")}/audio/transcriptions`;
|
|
11405
|
+
const form = new FormData();
|
|
11406
|
+
form.append("model", cap.modelId);
|
|
11407
|
+
if (req.language) form.append("language", req.language);
|
|
11408
|
+
form.append("file", new Blob([new Uint8Array(req.audio)]), req.filename);
|
|
11409
|
+
const ac = new AbortController();
|
|
11410
|
+
const timer = setTimeout(() => ac.abort(), DEFAULT_TIMEOUT_MS);
|
|
11411
|
+
const onAbort = () => ac.abort();
|
|
11412
|
+
signal?.addEventListener("abort", onAbort);
|
|
11413
|
+
try {
|
|
11414
|
+
const res = await fetch(url, {
|
|
11415
|
+
method: "POST",
|
|
11416
|
+
headers: { Authorization: `Bearer ${cfg.apiKey ?? ""}` },
|
|
11417
|
+
body: form,
|
|
11418
|
+
signal: ac.signal
|
|
11419
|
+
});
|
|
11420
|
+
if (!res.ok) {
|
|
11421
|
+
const text = await res.text().catch(() => "");
|
|
11422
|
+
throw Object.assign(new Error(text || res.statusText), { status: res.status });
|
|
11423
|
+
}
|
|
11424
|
+
const body = await res.json();
|
|
11425
|
+
return { text: body.text ?? "", modelId: cap.modelId };
|
|
11426
|
+
} finally {
|
|
11427
|
+
clearTimeout(timer);
|
|
11428
|
+
signal?.removeEventListener("abort", onAbort);
|
|
11429
|
+
}
|
|
11430
|
+
}, cap.modelId);
|
|
11431
|
+
}
|
|
11432
|
+
|
|
11433
|
+
// src/tools/generate-media.ts
|
|
11434
|
+
function resolve(deps, modality, preferModel) {
|
|
11435
|
+
const selected = deps.registry.select(modality, preferModel ? { preferModel } : {});
|
|
11436
|
+
if (!selected) {
|
|
11437
|
+
return { ok: false, error: `No ${modality} model is available on your configured providers.` };
|
|
11438
|
+
}
|
|
11439
|
+
const cfg = deps.lookupProvider(selected.capability.provider);
|
|
11440
|
+
if (!cfg) {
|
|
11441
|
+
return {
|
|
11442
|
+
ok: false,
|
|
11443
|
+
error: `${selected.capability.modelId} needs the ${selected.capability.provider} provider, which is not configured.`
|
|
11444
|
+
};
|
|
11445
|
+
}
|
|
11446
|
+
return { ok: true, selected, cfg };
|
|
11447
|
+
}
|
|
11448
|
+
var GenerateImageTool = class extends BaseTool {
|
|
11449
|
+
constructor(deps) {
|
|
11450
|
+
super();
|
|
11451
|
+
this.deps = deps;
|
|
11452
|
+
}
|
|
11453
|
+
deps;
|
|
11454
|
+
name = "generate_image";
|
|
11455
|
+
description = "Generate an image from a text description using the best available image model. Returns where the image was saved. Use when the user asks for a picture, illustration, diagram render, logo or any visual asset.";
|
|
11456
|
+
inputSchema = {
|
|
11457
|
+
type: "object",
|
|
11458
|
+
properties: {
|
|
11459
|
+
prompt: { type: "string", description: "What the image should show. Be specific about subject, style and composition." },
|
|
11460
|
+
size: { type: "string", description: 'Pixel size, e.g. "1024x1024" (default), "1792x1024", "1024x1792".' },
|
|
11461
|
+
model: { type: "string", description: "Optional: force a specific image model instead of the automatic choice." }
|
|
11462
|
+
},
|
|
11463
|
+
required: ["prompt"]
|
|
11464
|
+
};
|
|
11465
|
+
async execute(input, options) {
|
|
11466
|
+
const prompt = String(input["prompt"] ?? "").trim();
|
|
11467
|
+
if (!prompt) return 'Provide a "prompt" describing the image.';
|
|
11468
|
+
const r = resolve(this.deps, "image", input["model"] ? String(input["model"]) : void 0);
|
|
11469
|
+
if (!r.ok) return r.error;
|
|
11470
|
+
const size = input["size"] ? String(input["size"]) : void 0;
|
|
11471
|
+
const asset = await generateImage(
|
|
11472
|
+
r.selected.capability,
|
|
11473
|
+
r.cfg,
|
|
11474
|
+
{ prompt, ...size ? { size } : {} },
|
|
11475
|
+
options.signal
|
|
11476
|
+
);
|
|
11477
|
+
const location = await this.deps.sink(asset);
|
|
11478
|
+
return [
|
|
11479
|
+
`Image generated and saved to ${location}.`,
|
|
11480
|
+
`Model: ${asset.modelId} \u2014 ${r.selected.reason}`,
|
|
11481
|
+
// DALL·E rewrites the prompt before drawing; the model needs to know what
|
|
11482
|
+
// was actually rendered, not what it asked for, or its description of the
|
|
11483
|
+
// result will not match the image.
|
|
11484
|
+
asset.revisedPrompt ? `The provider rewrote the prompt to: ${asset.revisedPrompt}` : ""
|
|
11485
|
+
].filter(Boolean).join("\n");
|
|
11486
|
+
}
|
|
11487
|
+
};
|
|
11488
|
+
var GenerateSpeechTool = class extends BaseTool {
|
|
11489
|
+
constructor(deps) {
|
|
11490
|
+
super();
|
|
11491
|
+
this.deps = deps;
|
|
11492
|
+
}
|
|
11493
|
+
deps;
|
|
11494
|
+
name = "generate_speech";
|
|
11495
|
+
description = "Turn text into spoken audio using the best available text-to-speech model. Returns where the audio was saved. Use when the user asks for narration, a voiceover or an audio version of some text.";
|
|
11496
|
+
inputSchema = {
|
|
11497
|
+
type: "object",
|
|
11498
|
+
properties: {
|
|
11499
|
+
text: { type: "string", description: "The text to speak." },
|
|
11500
|
+
voice: { type: "string", description: "Voice id: alloy, echo, fable, onyx, nova, shimmer. Default alloy." },
|
|
11501
|
+
format: { type: "string", description: "Audio format: mp3 (default), opus, aac, flac, wav." },
|
|
11502
|
+
model: { type: "string", description: "Optional: force a specific speech model." }
|
|
11503
|
+
},
|
|
11504
|
+
required: ["text"]
|
|
11505
|
+
};
|
|
11506
|
+
async execute(input, options) {
|
|
11507
|
+
const text = String(input["text"] ?? "").trim();
|
|
11508
|
+
if (!text) return 'Provide "text" to speak.';
|
|
11509
|
+
const r = resolve(this.deps, "speech", input["model"] ? String(input["model"]) : void 0);
|
|
11510
|
+
if (!r.ok) return r.error;
|
|
11511
|
+
const fmt = String(input["format"] ?? "mp3");
|
|
11512
|
+
const allowed = ["mp3", "opus", "aac", "flac", "wav"];
|
|
11513
|
+
const format = allowed.includes(fmt) ? fmt : "mp3";
|
|
11514
|
+
const asset = await generateSpeech(
|
|
11515
|
+
r.selected.capability,
|
|
11516
|
+
r.cfg,
|
|
11517
|
+
{ text, voice: input["voice"] ? String(input["voice"]) : void 0, format },
|
|
11518
|
+
options.signal
|
|
11519
|
+
);
|
|
11520
|
+
const location = await this.deps.sink(asset);
|
|
11521
|
+
return `Speech generated and saved to ${location}.
|
|
11522
|
+
Model: ${asset.modelId} \u2014 ${r.selected.reason}`;
|
|
11523
|
+
}
|
|
11524
|
+
};
|
|
11525
|
+
var TranscribeAudioTool = class extends BaseTool {
|
|
11526
|
+
constructor(deps, readFile) {
|
|
11527
|
+
super();
|
|
11528
|
+
this.deps = deps;
|
|
11529
|
+
this.readFile = readFile;
|
|
11530
|
+
}
|
|
11531
|
+
deps;
|
|
11532
|
+
readFile;
|
|
11533
|
+
name = "transcribe_audio";
|
|
11534
|
+
description = "Transcribe a local audio file to text using the best available speech-to-text model. Use when the user supplies audio and wants its contents in writing.";
|
|
11535
|
+
inputSchema = {
|
|
11536
|
+
type: "object",
|
|
11537
|
+
properties: {
|
|
11538
|
+
path: { type: "string", description: "Path to the audio file to transcribe." },
|
|
11539
|
+
language: { type: "string", description: 'Optional ISO-639-1 language hint, e.g. "en", improves accuracy.' },
|
|
11540
|
+
model: { type: "string", description: "Optional: force a specific transcription model." }
|
|
11541
|
+
},
|
|
11542
|
+
required: ["path"]
|
|
11543
|
+
};
|
|
11544
|
+
async execute(input, options) {
|
|
11545
|
+
const filePath = String(input["path"] ?? "").trim();
|
|
11546
|
+
if (!filePath) return 'Provide a "path" to the audio file.';
|
|
11547
|
+
const r = resolve(this.deps, "transcription", input["model"] ? String(input["model"]) : void 0);
|
|
11548
|
+
if (!r.ok) return r.error;
|
|
11549
|
+
let audio;
|
|
11550
|
+
try {
|
|
11551
|
+
audio = await this.readFile(filePath);
|
|
11552
|
+
} catch {
|
|
11553
|
+
return `Could not read the audio file at ${filePath}.`;
|
|
11554
|
+
}
|
|
11555
|
+
const { text, modelId } = await transcribeAudio(
|
|
11556
|
+
r.selected.capability,
|
|
11557
|
+
r.cfg,
|
|
11558
|
+
{
|
|
11559
|
+
audio,
|
|
11560
|
+
filename: filePath.split(/[\\/]/).pop() || "audio",
|
|
11561
|
+
...input["language"] ? { language: String(input["language"]) } : {}
|
|
11562
|
+
},
|
|
11563
|
+
options.signal
|
|
11564
|
+
);
|
|
11565
|
+
return text ? `Transcript (${modelId}):
|
|
11566
|
+
|
|
11567
|
+
${text}` : `${modelId} returned an empty transcript \u2014 the file may be silent or in an unsupported format.`;
|
|
11568
|
+
}
|
|
11569
|
+
};
|
|
11570
|
+
function buildMediaTools(deps, readFile) {
|
|
11571
|
+
const tools = [];
|
|
11572
|
+
const modalities = new Set(deps.registry.availableModalities());
|
|
11573
|
+
if (modalities.has("image")) tools.push(new GenerateImageTool(deps));
|
|
11574
|
+
if (modalities.has("speech")) tools.push(new GenerateSpeechTool(deps));
|
|
11575
|
+
if (modalities.has("transcription") && readFile) tools.push(new TranscribeAudioTool(deps, readFile));
|
|
11576
|
+
return tools;
|
|
11577
|
+
}
|
|
11578
|
+
|
|
11579
|
+
// src/core/run-breaker.ts
|
|
11580
|
+
var DEFAULT_FAILURE_THRESHOLD = 3;
|
|
11581
|
+
var RunBreaker = class {
|
|
11582
|
+
threshold;
|
|
11583
|
+
/** modelId → consecutive systemic failures of the SAME kind. */
|
|
11584
|
+
counts = /* @__PURE__ */ new Map();
|
|
11585
|
+
trip = null;
|
|
11586
|
+
constructor(threshold = DEFAULT_FAILURE_THRESHOLD) {
|
|
11587
|
+
this.threshold = Math.max(1, threshold);
|
|
11588
|
+
}
|
|
11589
|
+
/**
|
|
11590
|
+
* Record a failed model call. Returns the trip if this failure is the one
|
|
11591
|
+
* that opened the breaker, otherwise null.
|
|
11592
|
+
*
|
|
11593
|
+
* A non-systemic failure RESETS the model's counter: it is evidence the model
|
|
11594
|
+
* is reachable and answering, which is exactly what a systemic streak claims
|
|
11595
|
+
* it is not.
|
|
11596
|
+
*/
|
|
11597
|
+
record(err, modelId) {
|
|
11598
|
+
if (this.trip) return null;
|
|
11599
|
+
const id = modelId?.trim() || "unknown-model";
|
|
11600
|
+
const c = classifyProviderError(err);
|
|
11601
|
+
if (!c.systemic) {
|
|
11602
|
+
this.counts.delete(id);
|
|
11603
|
+
return null;
|
|
11604
|
+
}
|
|
11605
|
+
const prev = this.counts.get(id);
|
|
11606
|
+
const n = prev && prev.kind === c.kind ? prev.n + 1 : 1;
|
|
11607
|
+
this.counts.set(id, { kind: c.kind, n, last: c });
|
|
11608
|
+
if (n >= this.threshold) {
|
|
11609
|
+
this.trip = {
|
|
11610
|
+
modelId: id,
|
|
11611
|
+
kind: c.kind,
|
|
11612
|
+
failures: n,
|
|
11613
|
+
message: describeProviderError(c, id)
|
|
11614
|
+
};
|
|
11615
|
+
return this.trip;
|
|
11616
|
+
}
|
|
11617
|
+
return null;
|
|
11618
|
+
}
|
|
11619
|
+
/** True once the run should stop launching work that would fail identically. */
|
|
11620
|
+
isOpen() {
|
|
11621
|
+
return this.trip !== null;
|
|
11622
|
+
}
|
|
11623
|
+
/** Why the breaker opened, or null while it is closed. */
|
|
11624
|
+
reason() {
|
|
11625
|
+
return this.trip;
|
|
11626
|
+
}
|
|
11627
|
+
/**
|
|
11628
|
+
* One line for a worker/section that never ran because the breaker was
|
|
11629
|
+
* already open. Says the work was skipped deliberately — not that it failed
|
|
11630
|
+
* on its own merits, which is what an empty FAILED status would imply.
|
|
11631
|
+
*/
|
|
11632
|
+
skipMessage() {
|
|
11633
|
+
const t = this.trip;
|
|
11634
|
+
if (!t) return "Skipped.";
|
|
11635
|
+
return `Skipped \u2014 the run stopped after ${t.failures} consecutive failures on ${t.modelId}. ${t.message}`;
|
|
11636
|
+
}
|
|
11637
|
+
/** Test/diagnostic view of the per-model streaks. */
|
|
11638
|
+
streaks() {
|
|
11639
|
+
const out = {};
|
|
11640
|
+
for (const [k, v] of this.counts) out[k] = { kind: v.kind, n: v.n };
|
|
11641
|
+
return out;
|
|
11642
|
+
}
|
|
11643
|
+
};
|
|
11644
|
+
|
|
11645
|
+
// src/tools/registry.ts
|
|
11646
|
+
init_constants();
|
|
10854
11647
|
var execAsync = util.promisify(child_process.exec);
|
|
10855
11648
|
var ShellTool = class extends BaseTool {
|
|
10856
11649
|
name = "shell";
|
|
@@ -11546,7 +12339,7 @@ var PDFCreateTool = class extends BaseTool {
|
|
|
11546
12339
|
if (!fs29__default.default.existsSync(dir)) {
|
|
11547
12340
|
fs29__default.default.mkdirSync(dir, { recursive: true });
|
|
11548
12341
|
}
|
|
11549
|
-
return new Promise((
|
|
12342
|
+
return new Promise((resolve2, reject) => {
|
|
11550
12343
|
try {
|
|
11551
12344
|
const doc = new PDFDocument__default.default({ margin: 50 });
|
|
11552
12345
|
const stream = fs29__default.default.createWriteStream(filePath);
|
|
@@ -11564,7 +12357,7 @@ var PDFCreateTool = class extends BaseTool {
|
|
|
11564
12357
|
});
|
|
11565
12358
|
doc.end();
|
|
11566
12359
|
stream.on("finish", () => {
|
|
11567
|
-
|
|
12360
|
+
resolve2(`Successfully created PDF at ${filePath} (${content.length} characters)`);
|
|
11568
12361
|
});
|
|
11569
12362
|
stream.on("error", (err) => {
|
|
11570
12363
|
reject(new Error(`Failed to write PDF: ${err.message}`));
|
|
@@ -11636,7 +12429,7 @@ var CodeInterpreterTool = class extends BaseTool {
|
|
|
11636
12429
|
const filePath = path32__default.default.join(tmpDir, fileName);
|
|
11637
12430
|
fs29__default.default.writeFileSync(filePath, code, "utf-8");
|
|
11638
12431
|
const execArgs = [filePath, ...args];
|
|
11639
|
-
return new Promise((
|
|
12432
|
+
return new Promise((resolve2) => {
|
|
11640
12433
|
const startMs = Date.now();
|
|
11641
12434
|
child_process.execFile(cmdPrefix, execArgs, { cwd: this.workspaceRoot, timeout: 3e4 }, (error, stdout, stderr) => {
|
|
11642
12435
|
const duration = Date.now() - startMs;
|
|
@@ -11650,17 +12443,17 @@ var CodeInterpreterTool = class extends BaseTool {
|
|
|
11650
12443
|
if (error) {
|
|
11651
12444
|
const timedOut = error.killed && duration >= 3e4;
|
|
11652
12445
|
if (timedOut) {
|
|
11653
|
-
|
|
12446
|
+
resolve2(`Execution timed out after 30s. Consider breaking the task into smaller pieces.
|
|
11654
12447
|
Partial stdout: ${stdout}
|
|
11655
12448
|
Stderr: ${stderr}`);
|
|
11656
12449
|
} else {
|
|
11657
|
-
|
|
12450
|
+
resolve2(`Execution failed (${duration}ms):
|
|
11658
12451
|
Error: ${error.message}
|
|
11659
12452
|
Stderr: ${stderr}
|
|
11660
12453
|
Stdout: ${stdout}`);
|
|
11661
12454
|
}
|
|
11662
12455
|
} else {
|
|
11663
|
-
|
|
12456
|
+
resolve2(`Execution successful (${duration}ms):
|
|
11664
12457
|
Stdout: ${stdout}
|
|
11665
12458
|
Stderr: ${stderr}`);
|
|
11666
12459
|
}
|
|
@@ -12314,7 +13107,7 @@ var ToolRegistry = class extends EventEmitter__default.default {
|
|
|
12314
13107
|
*/
|
|
12315
13108
|
waitForTool(toolName, timeoutMs = 6e4) {
|
|
12316
13109
|
if (this.tools.has(toolName)) return Promise.resolve();
|
|
12317
|
-
return new Promise((
|
|
13110
|
+
return new Promise((resolve2, reject) => {
|
|
12318
13111
|
const timer = setTimeout(() => {
|
|
12319
13112
|
this.off("tool:added", handler);
|
|
12320
13113
|
reject(new Error(`Timeout waiting for tool: ${toolName}`));
|
|
@@ -12323,7 +13116,7 @@ var ToolRegistry = class extends EventEmitter__default.default {
|
|
|
12323
13116
|
if (name === toolName) {
|
|
12324
13117
|
clearTimeout(timer);
|
|
12325
13118
|
this.off("tool:added", handler);
|
|
12326
|
-
|
|
13119
|
+
resolve2();
|
|
12327
13120
|
}
|
|
12328
13121
|
};
|
|
12329
13122
|
this.on("tool:added", handler);
|
|
@@ -12976,20 +13769,20 @@ var PermissionEscalator = class extends EventEmitter__default.default {
|
|
|
12976
13769
|
resolver(decision);
|
|
12977
13770
|
}
|
|
12978
13771
|
waitForUserDecision(req) {
|
|
12979
|
-
return new Promise((
|
|
13772
|
+
return new Promise((resolve2) => {
|
|
12980
13773
|
let timer;
|
|
12981
13774
|
const wrappedResolver = (decision) => {
|
|
12982
13775
|
if (timer) clearTimeout(timer);
|
|
12983
13776
|
if (decision.always) {
|
|
12984
13777
|
this.taskWideCache.set(req.toolName, decision.approved);
|
|
12985
13778
|
}
|
|
12986
|
-
|
|
13779
|
+
resolve2(decision);
|
|
12987
13780
|
};
|
|
12988
13781
|
this.pendingUserDecisions.set(req.id, wrappedResolver);
|
|
12989
13782
|
if (this.approvalTimeoutMs > 0 && Number.isFinite(this.approvalTimeoutMs)) {
|
|
12990
13783
|
timer = setTimeout(() => {
|
|
12991
13784
|
if (this.pendingUserDecisions.delete(req.id)) {
|
|
12992
|
-
|
|
13785
|
+
resolve2({
|
|
12993
13786
|
requestId: req.id,
|
|
12994
13787
|
approved: false,
|
|
12995
13788
|
decidedBy: "USER",
|
|
@@ -13057,6 +13850,23 @@ var noopTelemetry = {
|
|
|
13057
13850
|
}
|
|
13058
13851
|
};
|
|
13059
13852
|
|
|
13853
|
+
// src/core/router/feedback-prior.ts
|
|
13854
|
+
var MAX_FEEDBACK_ADJUSTMENT = 0.05;
|
|
13855
|
+
var PRIOR_STRENGTH = 10;
|
|
13856
|
+
function feedbackAdjustment(counts) {
|
|
13857
|
+
const good = Math.max(0, Math.floor(counts.good));
|
|
13858
|
+
const bad = Math.max(0, Math.floor(counts.bad));
|
|
13859
|
+
const n = good + bad;
|
|
13860
|
+
if (n === 0) return 0;
|
|
13861
|
+
const half = PRIOR_STRENGTH / 2;
|
|
13862
|
+
const shrunk = (good + half) / (n + PRIOR_STRENGTH);
|
|
13863
|
+
return (shrunk - 0.5) * 2 * MAX_FEEDBACK_ADJUSTMENT;
|
|
13864
|
+
}
|
|
13865
|
+
function applyFeedback(score01, counts) {
|
|
13866
|
+
const adjusted = score01 + feedbackAdjustment(counts);
|
|
13867
|
+
return Math.max(0, Math.min(1, adjusted));
|
|
13868
|
+
}
|
|
13869
|
+
|
|
13060
13870
|
// src/core/router/task-analyzer.ts
|
|
13061
13871
|
init_pricing();
|
|
13062
13872
|
var CODE_SIGNALS = [
|
|
@@ -13132,6 +13942,8 @@ var TASK_TYPE_TAGS = {
|
|
|
13132
13942
|
};
|
|
13133
13943
|
var TaskAnalyzer = class {
|
|
13134
13944
|
tracker;
|
|
13945
|
+
/** Per-model thumbs counts, when the host collects them. */
|
|
13946
|
+
feedback;
|
|
13135
13947
|
bias;
|
|
13136
13948
|
lastProfile = null;
|
|
13137
13949
|
lastSelectedModels = /* @__PURE__ */ new Map();
|
|
@@ -13139,6 +13951,14 @@ var TaskAnalyzer = class {
|
|
|
13139
13951
|
this.tracker = tracker;
|
|
13140
13952
|
this.bias = bias;
|
|
13141
13953
|
}
|
|
13954
|
+
/**
|
|
13955
|
+
* Supply per-model rating counts. Optional by design: a host that collects no
|
|
13956
|
+
* feedback (the CLI, a fresh install) scores exactly as it did before, since
|
|
13957
|
+
* feedbackAdjustment returns 0 for an empty record.
|
|
13958
|
+
*/
|
|
13959
|
+
setFeedbackSource(source) {
|
|
13960
|
+
this.feedback = source;
|
|
13961
|
+
}
|
|
13142
13962
|
setTracker(tracker) {
|
|
13143
13963
|
this.tracker = tracker;
|
|
13144
13964
|
}
|
|
@@ -13222,7 +14042,10 @@ var TaskAnalyzer = class {
|
|
|
13222
14042
|
const perf = this.tracker?.performanceScore(model.id, profile.type) ?? 0.5;
|
|
13223
14043
|
const costEff = this.costEfficiency(model, profile.complexity);
|
|
13224
14044
|
const match = this.taskMatchScore(model, profile);
|
|
13225
|
-
const
|
|
14045
|
+
const rated = this.feedback?.(model.id);
|
|
14046
|
+
const publicScore = benchmarkScore01(model, profile.type);
|
|
14047
|
+
const adjusted = rated ? applyFeedback(publicScore, rated) : publicScore;
|
|
14048
|
+
const benchmark = 0.3 + 0.7 * adjusted;
|
|
13226
14049
|
switch (this.bias) {
|
|
13227
14050
|
case "quality":
|
|
13228
14051
|
return perf * match * benchmark ** 2 * (0.85 + 0.15 * costEff);
|
|
@@ -13627,10 +14450,10 @@ var DynamicTool = class extends BaseTool {
|
|
|
13627
14450
|
`;
|
|
13628
14451
|
const script = await isolate.compileScript(bootstrap);
|
|
13629
14452
|
const runPromise = script.run(context, { timeout: timeoutMs, promise: true }).then((v) => String(v ?? ""));
|
|
13630
|
-
const timeoutPromise = new Promise((
|
|
14453
|
+
const timeoutPromise = new Promise((resolve2) => {
|
|
13631
14454
|
const t = setTimeout(() => {
|
|
13632
14455
|
dispose();
|
|
13633
|
-
|
|
14456
|
+
resolve2(`Dynamic tool "${this.name}" timed out after ${timeoutMs}ms and was terminated.`);
|
|
13634
14457
|
}, timeoutMs + 500);
|
|
13635
14458
|
t.unref?.();
|
|
13636
14459
|
});
|
|
@@ -13648,7 +14471,7 @@ var DynamicTool = class extends BaseTool {
|
|
|
13648
14471
|
/** Spawn the worker, service its callTool/fetch bridge, enforce the kill timeout. */
|
|
13649
14472
|
runInWorker(input, callTool) {
|
|
13650
14473
|
const timeoutMs = Math.max(200, Number(process.env["CASCADE_DYNAMIC_TOOL_TIMEOUT_MS"]) || DYNAMIC_TOOL_TIMEOUT_MS);
|
|
13651
|
-
return new Promise((
|
|
14474
|
+
return new Promise((resolve2) => {
|
|
13652
14475
|
let settled = false;
|
|
13653
14476
|
const worker = new worker_threads.Worker(HARNESS_SRC, {
|
|
13654
14477
|
eval: true,
|
|
@@ -13660,7 +14483,7 @@ var DynamicTool = class extends BaseTool {
|
|
|
13660
14483
|
settled = true;
|
|
13661
14484
|
clearTimeout(timer);
|
|
13662
14485
|
void worker.terminate();
|
|
13663
|
-
|
|
14486
|
+
resolve2(value);
|
|
13664
14487
|
};
|
|
13665
14488
|
const timer = setTimeout(
|
|
13666
14489
|
() => finish(`Dynamic tool "${this.name}" timed out after ${timeoutMs}ms and was terminated.`),
|
|
@@ -15167,6 +15990,7 @@ ${prompt}`;
|
|
|
15167
15990
|
var Cascade = class _Cascade extends EventEmitter__default.default {
|
|
15168
15991
|
router;
|
|
15169
15992
|
toolRegistry;
|
|
15993
|
+
multimodal;
|
|
15170
15994
|
mcpClient;
|
|
15171
15995
|
codeIndex;
|
|
15172
15996
|
config;
|
|
@@ -15205,6 +16029,10 @@ var Cascade = class _Cascade extends EventEmitter__default.default {
|
|
|
15205
16029
|
}
|
|
15206
16030
|
});
|
|
15207
16031
|
this.toolRegistry = new ToolRegistry(this.config.tools, workspacePath);
|
|
16032
|
+
this.multimodal = new MultimodalRegistry(
|
|
16033
|
+
(this.config.providers ?? []).map((p) => p.type)
|
|
16034
|
+
);
|
|
16035
|
+
this.registerMediaTools(workspacePath);
|
|
15208
16036
|
this.telemetry = config.telemetry?.enabled ? new Telemetry(config.telemetry, config.telemetry.distinctId ?? "anonymous") : noopTelemetry;
|
|
15209
16037
|
this.worldStateDB = new WorldStateDB(this.workspacePath, this.config.workspace?.debugWorldState ?? false);
|
|
15210
16038
|
this.router.setWorldStateDB(this.worldStateDB);
|
|
@@ -15258,14 +16086,14 @@ var Cascade = class _Cascade extends EventEmitter__default.default {
|
|
|
15258
16086
|
if (this.listenerCount("mcp:approval-required") === 0) {
|
|
15259
16087
|
return false;
|
|
15260
16088
|
}
|
|
15261
|
-
return await new Promise((
|
|
15262
|
-
this.pendingMcpApprovals.set(server.name,
|
|
16089
|
+
return await new Promise((resolve2) => {
|
|
16090
|
+
this.pendingMcpApprovals.set(server.name, resolve2);
|
|
15263
16091
|
const timeout = setTimeout(() => {
|
|
15264
|
-
if (this.pendingMcpApprovals.delete(server.name))
|
|
16092
|
+
if (this.pendingMcpApprovals.delete(server.name)) resolve2(false);
|
|
15265
16093
|
}, 3e4);
|
|
15266
16094
|
const wrap = (approved) => {
|
|
15267
16095
|
clearTimeout(timeout);
|
|
15268
|
-
|
|
16096
|
+
resolve2(approved);
|
|
15269
16097
|
};
|
|
15270
16098
|
this.pendingMcpApprovals.set(server.name, wrap);
|
|
15271
16099
|
this.emit("mcp:approval-required", { server });
|
|
@@ -15339,17 +16167,17 @@ var Cascade = class _Cascade extends EventEmitter__default.default {
|
|
|
15339
16167
|
}
|
|
15340
16168
|
const t2Count = plan.sections.length;
|
|
15341
16169
|
const t3Count = plan.sections.reduce((sum, s) => sum + (s.t3Subtasks?.length ?? 0), 0);
|
|
15342
|
-
return await new Promise((
|
|
16170
|
+
return await new Promise((resolve2) => {
|
|
15343
16171
|
const timeout = setTimeout(() => {
|
|
15344
16172
|
if (this.pendingPlanApproval) {
|
|
15345
16173
|
this.pendingPlanApproval = void 0;
|
|
15346
|
-
|
|
16174
|
+
resolve2({ approved: true });
|
|
15347
16175
|
}
|
|
15348
16176
|
}, 12e4);
|
|
15349
16177
|
this.pendingPlanApproval = (decision) => {
|
|
15350
16178
|
clearTimeout(timeout);
|
|
15351
16179
|
this.pendingPlanApproval = void 0;
|
|
15352
|
-
|
|
16180
|
+
resolve2(decision);
|
|
15353
16181
|
};
|
|
15354
16182
|
this.emit("plan:approval-required", {
|
|
15355
16183
|
taskId,
|
|
@@ -15382,17 +16210,17 @@ var Cascade = class _Cascade extends EventEmitter__default.default {
|
|
|
15382
16210
|
}
|
|
15383
16211
|
async requestContextApproval(info) {
|
|
15384
16212
|
if (this.listenerCount("context:approval-required") === 0) return true;
|
|
15385
|
-
return await new Promise((
|
|
16213
|
+
return await new Promise((resolve2) => {
|
|
15386
16214
|
const timeout = setTimeout(() => {
|
|
15387
16215
|
if (this.pendingContextApproval) {
|
|
15388
16216
|
this.pendingContextApproval = void 0;
|
|
15389
|
-
|
|
16217
|
+
resolve2(true);
|
|
15390
16218
|
}
|
|
15391
16219
|
}, 12e4);
|
|
15392
16220
|
this.pendingContextApproval = (approved) => {
|
|
15393
16221
|
clearTimeout(timeout);
|
|
15394
16222
|
this.pendingContextApproval = void 0;
|
|
15395
|
-
|
|
16223
|
+
resolve2(approved);
|
|
15396
16224
|
};
|
|
15397
16225
|
this.emit("context:approval-required", info);
|
|
15398
16226
|
});
|
|
@@ -15472,6 +16300,55 @@ ${input}` }],
|
|
|
15472
16300
|
if (this.store) t1.setStore(this.store);
|
|
15473
16301
|
return t1.previewPlan(prompt);
|
|
15474
16302
|
}
|
|
16303
|
+
/**
|
|
16304
|
+
* Register image / speech / transcription tools for the modalities this
|
|
16305
|
+
* config can actually serve.
|
|
16306
|
+
*
|
|
16307
|
+
* The sink writes bytes into the workspace, because that is what a desktop or
|
|
16308
|
+
* CLI run wants and it keeps a generated asset indistinguishable from any
|
|
16309
|
+
* other file the run produced. A host with different storage (the cloud
|
|
16310
|
+
* server stores a file row) constructs the tools itself via buildMediaTools
|
|
16311
|
+
* with its own sink.
|
|
16312
|
+
*/
|
|
16313
|
+
registerMediaTools(workspacePath) {
|
|
16314
|
+
const allowlist = this.config.tools?.enabledTools;
|
|
16315
|
+
const permitted = (name) => !allowlist || allowlist.includes(name);
|
|
16316
|
+
const tools = buildMediaTools(
|
|
16317
|
+
{
|
|
16318
|
+
registry: this.multimodal,
|
|
16319
|
+
lookupProvider: (provider) => (this.config.providers ?? []).find((p) => p.type === provider),
|
|
16320
|
+
sink: async (asset) => {
|
|
16321
|
+
const fsp = await import('fs/promises');
|
|
16322
|
+
const nodePath = await import('path');
|
|
16323
|
+
const dir = nodePath.join(workspacePath, "generated");
|
|
16324
|
+
await fsp.mkdir(dir, { recursive: true });
|
|
16325
|
+
const dest = nodePath.join(dir, asset.filename);
|
|
16326
|
+
await fsp.writeFile(dest, asset.data);
|
|
16327
|
+
return nodePath.relative(workspacePath, dest);
|
|
16328
|
+
}
|
|
16329
|
+
},
|
|
16330
|
+
async (p) => {
|
|
16331
|
+
const fsp = await import('fs/promises');
|
|
16332
|
+
return fsp.readFile(p);
|
|
16333
|
+
}
|
|
16334
|
+
);
|
|
16335
|
+
for (const tool of tools) {
|
|
16336
|
+
if (!permitted(tool.name)) continue;
|
|
16337
|
+
tool.setWorkspaceRoot(workspacePath);
|
|
16338
|
+
this.toolRegistry.register(tool);
|
|
16339
|
+
}
|
|
16340
|
+
}
|
|
16341
|
+
/**
|
|
16342
|
+
* Supply per-model thumbs-up/down counts so Auto routing can weigh how models
|
|
16343
|
+
* have actually performed for this user, on top of public benchmarks.
|
|
16344
|
+
*
|
|
16345
|
+
* Optional everywhere: with no source, or an empty record, scoring is
|
|
16346
|
+
* byte-identical to before. The adjustment is capped and sample-size shrunk —
|
|
16347
|
+
* see core/router/feedback-prior.ts.
|
|
16348
|
+
*/
|
|
16349
|
+
setFeedbackSource(source) {
|
|
16350
|
+
this.router.setFeedbackSource(source);
|
|
16351
|
+
}
|
|
15475
16352
|
/** True when a task stopped at the budget cap and can be resumed via /continue. */
|
|
15476
16353
|
hasResumableRun() {
|
|
15477
16354
|
return this.lastInterruptedRun != null;
|
|
@@ -15961,6 +16838,7 @@ ${prompt}` : prompt;
|
|
|
15961
16838
|
if (toolCreator) toolCreator.setPermissionEscalator(escalator);
|
|
15962
16839
|
const rootPrompt = buildContextualPrompt(options.prompt, options.conversationHistory);
|
|
15963
16840
|
let finalOutput = "";
|
|
16841
|
+
const runBreaker = new RunBreaker(this.config.budget?.failureThreshold);
|
|
15964
16842
|
let t2Results = [];
|
|
15965
16843
|
let runError = null;
|
|
15966
16844
|
let identityPrompt = "";
|
|
@@ -16029,6 +16907,7 @@ ${prompt}` : prompt;
|
|
|
16029
16907
|
this.emit("tier:status", { tierId: "t3-root", status: "COMPLETED", role: "T3" });
|
|
16030
16908
|
} else if (complexity === "Moderate") {
|
|
16031
16909
|
const t2 = new T2Manager(this.router, this.toolRegistry, "root");
|
|
16910
|
+
t2.setRunBreaker(runBreaker);
|
|
16032
16911
|
t2.setPresenter(true);
|
|
16033
16912
|
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.");
|
|
16034
16913
|
if (identityPrompt) {
|
|
@@ -16089,6 +16968,7 @@ _(incomplete: ${reasons.join("; ")})_` : partial;
|
|
|
16089
16968
|
}
|
|
16090
16969
|
} else {
|
|
16091
16970
|
const t1 = new T1Administrator(this.router, this.toolRegistry, this.config);
|
|
16971
|
+
t1.setRunBreaker(runBreaker);
|
|
16092
16972
|
t1.setPresenter(true);
|
|
16093
16973
|
t1.setHierarchyContext("You are the top-level Administrator. You are responsible for the overall plan and supervising multiple T2 Managers.");
|
|
16094
16974
|
if (identityPrompt) {
|
|
@@ -16113,6 +16993,20 @@ _(incomplete: ${reasons.join("; ")})_` : partial;
|
|
|
16113
16993
|
finalOutput = result.output;
|
|
16114
16994
|
t2Results = result.t2Results;
|
|
16115
16995
|
}
|
|
16996
|
+
const trip = runBreaker.reason();
|
|
16997
|
+
if (trip) {
|
|
16998
|
+
finalOutput = [
|
|
16999
|
+
`\u26A0 **Run stopped early \u2014 ${trip.modelId} was failing every request.**`,
|
|
17000
|
+
"",
|
|
17001
|
+
trip.message,
|
|
17002
|
+
"",
|
|
17003
|
+
`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.`,
|
|
17004
|
+
finalOutput ? `
|
|
17005
|
+
---
|
|
17006
|
+
|
|
17007
|
+
${finalOutput}` : ""
|
|
17008
|
+
].join("\n").trimEnd();
|
|
17009
|
+
}
|
|
16116
17010
|
} catch (err) {
|
|
16117
17011
|
if (err instanceof CascadeCancelledError || err instanceof Error && err.name === "AbortError" || options.signal?.aborted) {
|
|
16118
17012
|
this.emit("run:cancelled", {
|
|
@@ -16647,8 +17541,8 @@ var CloudClient = class _CloudClient {
|
|
|
16647
17541
|
}
|
|
16648
17542
|
};
|
|
16649
17543
|
function sleep(ms, signal) {
|
|
16650
|
-
return new Promise((
|
|
16651
|
-
const t = setTimeout(
|
|
17544
|
+
return new Promise((resolve2, reject) => {
|
|
17545
|
+
const t = setTimeout(resolve2, ms);
|
|
16652
17546
|
signal?.addEventListener("abort", () => {
|
|
16653
17547
|
clearTimeout(t);
|
|
16654
17548
|
reject(new Error("aborted"));
|
|
@@ -18440,8 +19334,8 @@ function Repl({ config, workspacePath, themeName, initialPrompt, identityName, a
|
|
|
18440
19334
|
onExit: () => exit(),
|
|
18441
19335
|
onThemeChange: (name) => setTheme(getTheme(name)),
|
|
18442
19336
|
onExport: async (fmt) => {
|
|
18443
|
-
const
|
|
18444
|
-
const exportPath = path32__default.default.join(workspacePath, `cascade-export-${
|
|
19337
|
+
const stamp2 = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
19338
|
+
const exportPath = path32__default.default.join(workspacePath, `cascade-export-${stamp2}.${fmt === "json" ? "json" : "md"}`);
|
|
18445
19339
|
if (fmt === "json") {
|
|
18446
19340
|
await fs10__default.default.writeFile(exportPath, JSON.stringify({ sessionId: sessionIdRef.current, messages: state.messages }, null, 2), "utf-8");
|
|
18447
19341
|
} else {
|
|
@@ -18873,8 +19767,8 @@ ${lastUser.content}`;
|
|
|
18873
19767
|
conversationHistory: toConversationHistory(state.messages),
|
|
18874
19768
|
approvalCallback: async (req) => {
|
|
18875
19769
|
dispatch({ type: "SET_APPROVAL", request: req });
|
|
18876
|
-
return new Promise((
|
|
18877
|
-
approvalResolverRef.current =
|
|
19770
|
+
return new Promise((resolve2) => {
|
|
19771
|
+
approvalResolverRef.current = resolve2;
|
|
18878
19772
|
});
|
|
18879
19773
|
}
|
|
18880
19774
|
});
|
|
@@ -20140,13 +21034,13 @@ function SetupWizard({ workspacePath, onComplete }) {
|
|
|
20140
21034
|
}
|
|
20141
21035
|
async function runSetupWizard(workspacePath) {
|
|
20142
21036
|
printTelemetryBanner();
|
|
20143
|
-
return new Promise((
|
|
21037
|
+
return new Promise((resolve2) => {
|
|
20144
21038
|
const { unmount } = ink.render(
|
|
20145
21039
|
React2__default.default.createElement(SetupWizard, {
|
|
20146
21040
|
workspacePath,
|
|
20147
21041
|
onComplete: (config) => {
|
|
20148
21042
|
unmount();
|
|
20149
|
-
|
|
21043
|
+
resolve2(config);
|
|
20150
21044
|
}
|
|
20151
21045
|
}),
|
|
20152
21046
|
{ exitOnCtrlC: true }
|
|
@@ -20614,7 +21508,7 @@ function notSignedIn() {
|
|
|
20614
21508
|
process.exitCode = 1;
|
|
20615
21509
|
}
|
|
20616
21510
|
function promptHidden(question) {
|
|
20617
|
-
return new Promise((
|
|
21511
|
+
return new Promise((resolve2) => {
|
|
20618
21512
|
const rl = readline__default.default.createInterface({ input: process.stdin, output: process.stdout, terminal: true });
|
|
20619
21513
|
let masked = false;
|
|
20620
21514
|
rl._writeToOutput = (s) => {
|
|
@@ -20623,7 +21517,7 @@ function promptHidden(question) {
|
|
|
20623
21517
|
rl.question(question, (answer) => {
|
|
20624
21518
|
rl.close();
|
|
20625
21519
|
process.stdout.write("\n");
|
|
20626
|
-
|
|
21520
|
+
resolve2(answer.trim());
|
|
20627
21521
|
});
|
|
20628
21522
|
masked = true;
|
|
20629
21523
|
});
|
|
@@ -21550,12 +22444,12 @@ var DashboardServer = class {
|
|
|
21550
22444
|
console.warn("\u26A0 Dashboard auth is DISABLED while bound to a non-loopback interface \u2014 this allows unauthenticated remote task execution.");
|
|
21551
22445
|
}
|
|
21552
22446
|
}
|
|
21553
|
-
await new Promise((
|
|
22447
|
+
await new Promise((resolve2, reject) => {
|
|
21554
22448
|
const onError = (err) => reject(err);
|
|
21555
22449
|
this.httpServer.once("error", onError);
|
|
21556
22450
|
this.httpServer.listen(this.port, this.host, () => {
|
|
21557
22451
|
this.httpServer.off("error", onError);
|
|
21558
|
-
|
|
22452
|
+
resolve2();
|
|
21559
22453
|
});
|
|
21560
22454
|
});
|
|
21561
22455
|
try {
|
|
@@ -21579,8 +22473,8 @@ var DashboardServer = class {
|
|
|
21579
22473
|
} catch {
|
|
21580
22474
|
}
|
|
21581
22475
|
this.globalStore = null;
|
|
21582
|
-
await new Promise((
|
|
21583
|
-
this.httpServer.close((err) => err ? reject(err) :
|
|
22476
|
+
await new Promise((resolve2, reject) => {
|
|
22477
|
+
this.httpServer.close((err) => err ? reject(err) : resolve2());
|
|
21584
22478
|
});
|
|
21585
22479
|
}
|
|
21586
22480
|
getSocket() {
|
|
@@ -21777,8 +22671,8 @@ var DashboardServer = class {
|
|
|
21777
22671
|
* timeout denies it.
|
|
21778
22672
|
*/
|
|
21779
22673
|
makeApprovalCallback(sessionId) {
|
|
21780
|
-
return (request) => new Promise((
|
|
21781
|
-
this.pendingApprovals.set(request.id, { resolve, sessionId });
|
|
22674
|
+
return (request) => new Promise((resolve2) => {
|
|
22675
|
+
this.pendingApprovals.set(request.id, { resolve: resolve2, sessionId });
|
|
21782
22676
|
});
|
|
21783
22677
|
}
|
|
21784
22678
|
/**
|