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.js
CHANGED
|
@@ -1740,7 +1740,7 @@ var CASCADE_VERSION, CASCADE_CONFIG_FILE, CASCADE_DB_FILE, CASCADE_DASHBOARD_SEC
|
|
|
1740
1740
|
var init_constants = __esm({
|
|
1741
1741
|
"src/constants.ts"() {
|
|
1742
1742
|
init_pricing();
|
|
1743
|
-
CASCADE_VERSION = "0.
|
|
1743
|
+
CASCADE_VERSION = "0.51.0";
|
|
1744
1744
|
CASCADE_CONFIG_FILE = ".cascade/config.json";
|
|
1745
1745
|
CASCADE_DB_FILE = ".cascade/memory.db";
|
|
1746
1746
|
CASCADE_DASHBOARD_SECRET_FILE = ".cascade/dashboard-secret";
|
|
@@ -2258,49 +2258,40 @@ function isChatModel(id, methods) {
|
|
|
2258
2258
|
}
|
|
2259
2259
|
return !NON_CHAT.test(id);
|
|
2260
2260
|
}
|
|
2261
|
-
var
|
|
2261
|
+
var MODALITY_PATTERNS, NON_CHAT;
|
|
2262
2262
|
var init_model_filter = __esm({
|
|
2263
2263
|
"src/providers/model-filter.ts"() {
|
|
2264
|
-
|
|
2265
|
-
"embed",
|
|
2264
|
+
MODALITY_PATTERNS = [
|
|
2265
|
+
["embedding", ["embed"]],
|
|
2266
2266
|
// text-embedding-3, nomic-embed, mxbai-embed, snowflake-arctic-embed
|
|
2267
|
-
"(?:^|[-_/])
|
|
2268
|
-
"text-to-speech",
|
|
2269
|
-
"
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
"
|
|
2281
|
-
"
|
|
2282
|
-
"
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
"veo",
|
|
2293
|
-
"sora",
|
|
2294
|
-
"(?:^|[-_/])video(?:[-_/]|$)",
|
|
2295
|
-
// video
|
|
2296
|
-
"(?:^|[-_/])aqa(?:[-_/]|$)",
|
|
2297
|
-
// attributed QA
|
|
2298
|
-
"(?:^|[-_/])live(?:[-_/]|$)",
|
|
2299
|
-
// live-audio variants
|
|
2300
|
-
"(?:^|[-_/])(?:davinci|babbage)(?:[-_/]|$)"
|
|
2301
|
-
// legacy completion-only base models
|
|
2267
|
+
["transcription", ["whisper", "transcrib", "(?:^|[-_/])stt(?:[-_/]|$)"]],
|
|
2268
|
+
["speech", ["(?:^|[-_/])tts(?:[-_/]|$)", "text-to-speech", "speech"]],
|
|
2269
|
+
["video", ["veo", "sora", "(?:^|[-_/])video(?:[-_/]|$)"]],
|
|
2270
|
+
["image", [
|
|
2271
|
+
"dall[-]?e",
|
|
2272
|
+
"imagen",
|
|
2273
|
+
"image-generation",
|
|
2274
|
+
"gpt-image",
|
|
2275
|
+
"(?:^|[-_/])image(?:[-_/]|$)",
|
|
2276
|
+
"stable-diffusion",
|
|
2277
|
+
"sdxl",
|
|
2278
|
+
"flux"
|
|
2279
|
+
]],
|
|
2280
|
+
["moderation", ["moderation", "(?:^|[-_/])guard(?:[-_/]|$)"]],
|
|
2281
|
+
["rerank", ["rerank"]],
|
|
2282
|
+
["realtime", [
|
|
2283
|
+
"(?:^|[-_/])audio(?:[-_/]|$)",
|
|
2284
|
+
"native-audio",
|
|
2285
|
+
"realtime",
|
|
2286
|
+
"(?:^|[-_/])live(?:[-_/]|$)",
|
|
2287
|
+
// live-audio variants
|
|
2288
|
+
"(?:^|[-_/])aqa(?:[-_/]|$)"
|
|
2289
|
+
// attributed QA
|
|
2290
|
+
]],
|
|
2291
|
+
["legacy-completion", ["(?:^|[-_/])(?:davinci|babbage)(?:[-_/]|$)"]]
|
|
2302
2292
|
];
|
|
2303
|
-
|
|
2293
|
+
MODALITY_PATTERNS.map(([m, pats]) => [m, new RegExp(pats.join("|"), "i")]);
|
|
2294
|
+
NON_CHAT = new RegExp(MODALITY_PATTERNS.flatMap(([, p]) => p).join("|"), "i");
|
|
2304
2295
|
}
|
|
2305
2296
|
});
|
|
2306
2297
|
|
|
@@ -2868,6 +2859,118 @@ var init_azure = __esm({
|
|
|
2868
2859
|
}
|
|
2869
2860
|
});
|
|
2870
2861
|
|
|
2862
|
+
// src/providers/gemini-schema.ts
|
|
2863
|
+
function isPlainObject(v) {
|
|
2864
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
2865
|
+
}
|
|
2866
|
+
function normalizeType(value) {
|
|
2867
|
+
if (typeof value === "string") {
|
|
2868
|
+
return ALLOWED_TYPES.has(value) ? { type: value } : {};
|
|
2869
|
+
}
|
|
2870
|
+
if (Array.isArray(value)) {
|
|
2871
|
+
const types = value.filter((t) => typeof t === "string");
|
|
2872
|
+
const nullable = types.includes("null");
|
|
2873
|
+
const concrete = types.find((t) => t !== "null" && ALLOWED_TYPES.has(t));
|
|
2874
|
+
return {
|
|
2875
|
+
...concrete ? { type: concrete } : {},
|
|
2876
|
+
...nullable ? { nullable: true } : {}
|
|
2877
|
+
};
|
|
2878
|
+
}
|
|
2879
|
+
return {};
|
|
2880
|
+
}
|
|
2881
|
+
function normalizeEnum(value) {
|
|
2882
|
+
if (!Array.isArray(value) || value.length === 0) return void 0;
|
|
2883
|
+
return value.map((v) => typeof v === "string" ? v : JSON.stringify(v));
|
|
2884
|
+
}
|
|
2885
|
+
function sanitizeGeminiSchema(schema) {
|
|
2886
|
+
if (!isPlainObject(schema)) return void 0;
|
|
2887
|
+
const out = {};
|
|
2888
|
+
for (const [key, value] of Object.entries(schema)) {
|
|
2889
|
+
if (!ALLOWED_KEYS.has(key)) continue;
|
|
2890
|
+
switch (key) {
|
|
2891
|
+
case "type": {
|
|
2892
|
+
Object.assign(out, normalizeType(value));
|
|
2893
|
+
break;
|
|
2894
|
+
}
|
|
2895
|
+
case "enum": {
|
|
2896
|
+
const e = normalizeEnum(value);
|
|
2897
|
+
if (e) out["enum"] = e;
|
|
2898
|
+
break;
|
|
2899
|
+
}
|
|
2900
|
+
case "properties": {
|
|
2901
|
+
if (!isPlainObject(value)) break;
|
|
2902
|
+
const props = {};
|
|
2903
|
+
for (const [propName, propSchema] of Object.entries(value)) {
|
|
2904
|
+
const cleaned = sanitizeGeminiSchema(propSchema);
|
|
2905
|
+
props[propName] = cleaned && Object.keys(cleaned).length ? cleaned : { type: "string" };
|
|
2906
|
+
}
|
|
2907
|
+
out["properties"] = props;
|
|
2908
|
+
break;
|
|
2909
|
+
}
|
|
2910
|
+
case "items": {
|
|
2911
|
+
const cleaned = sanitizeGeminiSchema(value);
|
|
2912
|
+
if (cleaned) out["items"] = cleaned;
|
|
2913
|
+
break;
|
|
2914
|
+
}
|
|
2915
|
+
case "anyOf": {
|
|
2916
|
+
if (!Array.isArray(value)) break;
|
|
2917
|
+
const variants = value.map((v) => sanitizeGeminiSchema(v)).filter((v) => !!v && Object.keys(v).length > 0);
|
|
2918
|
+
if (variants.length) out["anyOf"] = variants;
|
|
2919
|
+
break;
|
|
2920
|
+
}
|
|
2921
|
+
case "required": {
|
|
2922
|
+
if (Array.isArray(value)) {
|
|
2923
|
+
out["required"] = value.filter((r) => typeof r === "string");
|
|
2924
|
+
}
|
|
2925
|
+
break;
|
|
2926
|
+
}
|
|
2927
|
+
default:
|
|
2928
|
+
out[key] = value;
|
|
2929
|
+
}
|
|
2930
|
+
}
|
|
2931
|
+
if (!out["type"] && out["properties"]) out["type"] = "object";
|
|
2932
|
+
if (!out["type"] && out["items"]) out["type"] = "array";
|
|
2933
|
+
return out;
|
|
2934
|
+
}
|
|
2935
|
+
function toGeminiParameters(inputSchema) {
|
|
2936
|
+
const cleaned = sanitizeGeminiSchema(inputSchema);
|
|
2937
|
+
if (!cleaned) return void 0;
|
|
2938
|
+
const props = cleaned["properties"];
|
|
2939
|
+
if (isPlainObject(props) && Object.keys(props).length === 0) return void 0;
|
|
2940
|
+
if (Object.keys(cleaned).length === 0) return void 0;
|
|
2941
|
+
return cleaned;
|
|
2942
|
+
}
|
|
2943
|
+
var ALLOWED_KEYS, ALLOWED_TYPES;
|
|
2944
|
+
var init_gemini_schema = __esm({
|
|
2945
|
+
"src/providers/gemini-schema.ts"() {
|
|
2946
|
+
ALLOWED_KEYS = /* @__PURE__ */ new Set([
|
|
2947
|
+
"type",
|
|
2948
|
+
"format",
|
|
2949
|
+
"title",
|
|
2950
|
+
"description",
|
|
2951
|
+
"nullable",
|
|
2952
|
+
"enum",
|
|
2953
|
+
"items",
|
|
2954
|
+
"properties",
|
|
2955
|
+
"required",
|
|
2956
|
+
"propertyOrdering",
|
|
2957
|
+
"minItems",
|
|
2958
|
+
"maxItems",
|
|
2959
|
+
"minProperties",
|
|
2960
|
+
"maxProperties",
|
|
2961
|
+
"minimum",
|
|
2962
|
+
"maximum",
|
|
2963
|
+
"minLength",
|
|
2964
|
+
"maxLength",
|
|
2965
|
+
"pattern",
|
|
2966
|
+
"example",
|
|
2967
|
+
"default",
|
|
2968
|
+
"anyOf"
|
|
2969
|
+
]);
|
|
2970
|
+
ALLOWED_TYPES = /* @__PURE__ */ new Set(["string", "number", "integer", "boolean", "array", "object"]);
|
|
2971
|
+
}
|
|
2972
|
+
});
|
|
2973
|
+
|
|
2871
2974
|
// src/providers/gemini.ts
|
|
2872
2975
|
var gemini_exports = {};
|
|
2873
2976
|
__export(gemini_exports, {
|
|
@@ -2880,6 +2983,7 @@ var init_gemini = __esm({
|
|
|
2880
2983
|
init_base();
|
|
2881
2984
|
init_pricing();
|
|
2882
2985
|
init_model_filter();
|
|
2986
|
+
init_gemini_schema();
|
|
2883
2987
|
GeminiProvider = class extends BaseProvider {
|
|
2884
2988
|
client;
|
|
2885
2989
|
constructor(config, model) {
|
|
@@ -3100,7 +3204,12 @@ var init_gemini = __esm({
|
|
|
3100
3204
|
return {
|
|
3101
3205
|
name: tool.name,
|
|
3102
3206
|
description: tool.description,
|
|
3103
|
-
parameters
|
|
3207
|
+
// Convert, never cast. Gemini's parameters field is an OpenAPI subset
|
|
3208
|
+
// that 400s on unknown keys, and MCP servers ship schemas carrying their
|
|
3209
|
+
// own extensions (GitHub's adds `x-mcp-header` to every header-bound
|
|
3210
|
+
// property). Casting sent those through verbatim and every request failed
|
|
3211
|
+
// before the model saw it. See providers/gemini-schema.ts.
|
|
3212
|
+
parameters: toGeminiParameters(tool.inputSchema)
|
|
3104
3213
|
};
|
|
3105
3214
|
}
|
|
3106
3215
|
};
|
|
@@ -3125,7 +3234,7 @@ async function nodeHttpFetch(input, init = {}, redirectCount = 0) {
|
|
|
3125
3234
|
headers["accept-encoding"] = "gzip, deflate, br";
|
|
3126
3235
|
}
|
|
3127
3236
|
const body = init.body == null ? void 0 : typeof init.body === "string" ? init.body : Buffer.from(init.body);
|
|
3128
|
-
return new Promise((
|
|
3237
|
+
return new Promise((resolve2, reject) => {
|
|
3129
3238
|
const req = lib.request(
|
|
3130
3239
|
{
|
|
3131
3240
|
hostname: u.hostname,
|
|
@@ -3142,7 +3251,7 @@ async function nodeHttpFetch(input, init = {}, redirectCount = 0) {
|
|
|
3142
3251
|
const nextUrl = new URL(location, u).href;
|
|
3143
3252
|
const downgrade = status === 303 || (status === 301 || status === 302) && method !== "GET" && method !== "HEAD";
|
|
3144
3253
|
const nextInit = downgrade ? { ...init, method: "GET", body: void 0 } : init;
|
|
3145
|
-
|
|
3254
|
+
resolve2(nodeHttpFetch(nextUrl, nextInit, redirectCount + 1));
|
|
3146
3255
|
return;
|
|
3147
3256
|
}
|
|
3148
3257
|
const encoding = (res.headers["content-encoding"] ?? "").toLowerCase();
|
|
@@ -3157,7 +3266,7 @@ async function nodeHttpFetch(input, init = {}, redirectCount = 0) {
|
|
|
3157
3266
|
if (Array.isArray(v)) respHeaders.set(k, v.join(", "));
|
|
3158
3267
|
else if (typeof v === "string") respHeaders.set(k, v);
|
|
3159
3268
|
}
|
|
3160
|
-
|
|
3269
|
+
resolve2(new Response(stream, {
|
|
3161
3270
|
status,
|
|
3162
3271
|
statusText: res.statusMessage ?? "",
|
|
3163
3272
|
headers: respHeaders
|
|
@@ -4956,6 +5065,14 @@ var BudgetConfigSchema = z.object({
|
|
|
4956
5065
|
maxTokensPerRun: z.number().int().positive().default(2e5),
|
|
4957
5066
|
/** Optional hard per-task cost ceiling (USD). Unset = only the token cap applies. */
|
|
4958
5067
|
maxCostPerRunUsd: z.number().positive().optional(),
|
|
5068
|
+
/**
|
|
5069
|
+
* Consecutive systemic failures against ONE model before the run stops
|
|
5070
|
+
* launching work that would fail the same way (dead key, wrong model id,
|
|
5071
|
+
* exhausted quota). Lower reacts sooner and wastes less; higher absorbs a
|
|
5072
|
+
* longer transient before giving up. Only failures that will certainly repeat
|
|
5073
|
+
* are counted — see core/run-breaker.ts.
|
|
5074
|
+
*/
|
|
5075
|
+
failureThreshold: z.number().int().min(1).max(20).default(3),
|
|
4959
5076
|
warnAtPct: z.number().default(80)
|
|
4960
5077
|
});
|
|
4961
5078
|
var WorkspaceConfigSchema = z.object({
|
|
@@ -6008,7 +6125,7 @@ var TpmLimiter = class {
|
|
|
6008
6125
|
}
|
|
6009
6126
|
const deficit = want - bucket.available;
|
|
6010
6127
|
const waitMs = Math.max(50, Math.ceil(deficit / bucket.tokensPerMinute * 6e4));
|
|
6011
|
-
await new Promise((
|
|
6128
|
+
await new Promise((resolve2) => setTimeout(resolve2, waitMs));
|
|
6012
6129
|
}
|
|
6013
6130
|
}
|
|
6014
6131
|
/**
|
|
@@ -6071,14 +6188,14 @@ var LocalRequestQueue = class {
|
|
|
6071
6188
|
this.active++;
|
|
6072
6189
|
return this.makeRelease();
|
|
6073
6190
|
}
|
|
6074
|
-
return new Promise((
|
|
6191
|
+
return new Promise((resolve2, reject) => {
|
|
6075
6192
|
let settled = false;
|
|
6076
6193
|
let timer;
|
|
6077
6194
|
const resolver = (release) => {
|
|
6078
6195
|
if (settled) return;
|
|
6079
6196
|
settled = true;
|
|
6080
6197
|
if (timer !== void 0) clearTimeout(timer);
|
|
6081
|
-
|
|
6198
|
+
resolve2(release);
|
|
6082
6199
|
};
|
|
6083
6200
|
if (timeoutMs !== void 0 && timeoutMs > 0) {
|
|
6084
6201
|
timer = setTimeout(() => {
|
|
@@ -6705,6 +6822,7 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter {
|
|
|
6705
6822
|
tpmLimiter;
|
|
6706
6823
|
localQueue;
|
|
6707
6824
|
taskAnalyzer;
|
|
6825
|
+
pendingFeedbackSource;
|
|
6708
6826
|
worldStateDB;
|
|
6709
6827
|
privacyPaths;
|
|
6710
6828
|
guidanceQueue;
|
|
@@ -7159,8 +7277,18 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter {
|
|
|
7159
7277
|
return windows.length ? Math.min(...windows) : void 0;
|
|
7160
7278
|
}
|
|
7161
7279
|
/** Wire the Cascade Auto task analyzer used for per-subtask model routing. */
|
|
7280
|
+
/**
|
|
7281
|
+
* Feed per-model thumbs counts into Auto routing. Forwarded to the analyzer
|
|
7282
|
+
* because that is where the public benchmark score is combined; the prior
|
|
7283
|
+
* adjusts that number rather than sitting anywhere else in the pipeline.
|
|
7284
|
+
*/
|
|
7285
|
+
setFeedbackSource(source) {
|
|
7286
|
+
this.taskAnalyzer?.setFeedbackSource(source);
|
|
7287
|
+
this.pendingFeedbackSource = source;
|
|
7288
|
+
}
|
|
7162
7289
|
setTaskAnalyzer(analyzer) {
|
|
7163
7290
|
this.taskAnalyzer = analyzer;
|
|
7291
|
+
if (this.pendingFeedbackSource) analyzer.setFeedbackSource(this.pendingFeedbackSource);
|
|
7164
7292
|
}
|
|
7165
7293
|
setWorldStateDB(db) {
|
|
7166
7294
|
this.worldStateDB = db;
|
|
@@ -7615,6 +7743,14 @@ var BaseTier = class extends EventEmitter {
|
|
|
7615
7743
|
this.emit("tier:status", event);
|
|
7616
7744
|
}
|
|
7617
7745
|
/** Record the model serving this tier; future status events carry it. */
|
|
7746
|
+
/**
|
|
7747
|
+
* The model actually serving this tier ("provider:id"), once one has been
|
|
7748
|
+
* chosen. The run breaker reads it to attribute a failure to a model rather
|
|
7749
|
+
* than to the subtask that happened to hit it.
|
|
7750
|
+
*/
|
|
7751
|
+
getServingModel() {
|
|
7752
|
+
return this.servingModel;
|
|
7753
|
+
}
|
|
7618
7754
|
setServingModel(model) {
|
|
7619
7755
|
this.servingModel = model || void 0;
|
|
7620
7756
|
}
|
|
@@ -8247,8 +8383,11 @@ Now execute your subtask using this context where relevant.`
|
|
|
8247
8383
|
const result = await this.runAgentLoop(systemPrompt, this.tools);
|
|
8248
8384
|
output = result.output;
|
|
8249
8385
|
toolCalls = result.toolCalls;
|
|
8250
|
-
this.
|
|
8251
|
-
|
|
8386
|
+
const mustProduceArtifact = this.requiresArtifact();
|
|
8387
|
+
if (mustProduceArtifact) {
|
|
8388
|
+
this.sendStatusUpdate({ progressPct: 65, currentAction: "Verifying required artifacts", status: "IN_PROGRESS" });
|
|
8389
|
+
}
|
|
8390
|
+
const artifactCheck = mustProduceArtifact ? await this.verifyArtifacts(assignment) : { ok: true, issues: [] };
|
|
8252
8391
|
if (!artifactCheck.ok) {
|
|
8253
8392
|
correctionAttempts = 1;
|
|
8254
8393
|
issues.push(...artifactCheck.issues);
|
|
@@ -8528,11 +8667,11 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : "") + textToolSuffix,
|
|
|
8528
8667
|
if (!wasApproved) return `Tool ${tc.name} was denied by user.`;
|
|
8529
8668
|
} else {
|
|
8530
8669
|
const LEGACY_APPROVAL_TIMEOUT_MS = 6e5;
|
|
8531
|
-
const legacyDecision = await new Promise((
|
|
8670
|
+
const legacyDecision = await new Promise((resolve2) => {
|
|
8532
8671
|
const eventName = `tool:approval-response:${this.id}-${tc.id}`;
|
|
8533
8672
|
const timer = setTimeout(() => {
|
|
8534
8673
|
this.removeAllListeners(eventName);
|
|
8535
|
-
|
|
8674
|
+
resolve2({ approved: false });
|
|
8536
8675
|
}, LEGACY_APPROVAL_TIMEOUT_MS);
|
|
8537
8676
|
timer.unref?.();
|
|
8538
8677
|
this.emit("tool:approval-request", {
|
|
@@ -8545,7 +8684,7 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : "") + textToolSuffix,
|
|
|
8545
8684
|
});
|
|
8546
8685
|
this.once(eventName, (d) => {
|
|
8547
8686
|
clearTimeout(timer);
|
|
8548
|
-
|
|
8687
|
+
resolve2(d);
|
|
8549
8688
|
});
|
|
8550
8689
|
});
|
|
8551
8690
|
if (legacyDecision.always) this.sessionApprovals.set(tc.name, legacyDecision.approved);
|
|
@@ -8565,6 +8704,9 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : "") + textToolSuffix,
|
|
|
8565
8704
|
tierId: this.id,
|
|
8566
8705
|
sessionId: this.taskId,
|
|
8567
8706
|
requireApproval: false,
|
|
8707
|
+
// Media generation can run for a minute; without this a cancelled run
|
|
8708
|
+
// still pays for an image nobody will see.
|
|
8709
|
+
...this.signal ? { signal: this.signal } : {},
|
|
8568
8710
|
saveSnapshot: async (path37, content) => {
|
|
8569
8711
|
this.store?.addFileSnapshot(this.taskId, path37, content);
|
|
8570
8712
|
},
|
|
@@ -8612,7 +8754,8 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : "") + textToolSuffix,
|
|
|
8612
8754
|
const result = await this.toolRegistry.execute(altTool, tc.input, {
|
|
8613
8755
|
tierId: this.id,
|
|
8614
8756
|
sessionId: this.taskId,
|
|
8615
|
-
requireApproval: false
|
|
8757
|
+
requireApproval: false,
|
|
8758
|
+
...this.signal ? { signal: this.signal } : {}
|
|
8616
8759
|
});
|
|
8617
8760
|
const str2 = typeof result === "string" ? result : JSON.stringify(result);
|
|
8618
8761
|
if (!str2.startsWith("Tool error:") && !str2.startsWith("Error:")) {
|
|
@@ -8634,7 +8777,8 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : "") + textToolSuffix,
|
|
|
8634
8777
|
const result = await this.toolRegistry.execute(newToolName, tc.input, {
|
|
8635
8778
|
tierId: this.id,
|
|
8636
8779
|
sessionId: this.taskId,
|
|
8637
|
-
requireApproval: false
|
|
8780
|
+
requireApproval: false,
|
|
8781
|
+
...this.signal ? { signal: this.signal } : {}
|
|
8638
8782
|
});
|
|
8639
8783
|
const str2 = typeof result === "string" ? result : JSON.stringify(result);
|
|
8640
8784
|
if (!str2.startsWith("Tool error:")) return `[Synthesized ${newToolName}]: ${str2}`;
|
|
@@ -9052,7 +9196,7 @@ var PeerBus = class extends EventEmitter {
|
|
|
9052
9196
|
this.outputs.set(subtaskId, entry);
|
|
9053
9197
|
this.emit("output:ready", entry);
|
|
9054
9198
|
const waiting = this.waiters.get(subtaskId) ?? [];
|
|
9055
|
-
for (const
|
|
9199
|
+
for (const resolve2 of waiting) resolve2(entry);
|
|
9056
9200
|
this.waiters.delete(subtaskId);
|
|
9057
9201
|
}
|
|
9058
9202
|
/**
|
|
@@ -9083,10 +9227,10 @@ var PeerBus = class extends EventEmitter {
|
|
|
9083
9227
|
waitFor(subtaskId, timeoutMs = 12e4) {
|
|
9084
9228
|
const existing = this.outputs.get(subtaskId);
|
|
9085
9229
|
if (existing && !this.retryPending.has(subtaskId)) return Promise.resolve(existing);
|
|
9086
|
-
return new Promise((
|
|
9230
|
+
return new Promise((resolve2, reject) => {
|
|
9087
9231
|
const resolver = (output) => {
|
|
9088
9232
|
clearTimeout(timer);
|
|
9089
|
-
|
|
9233
|
+
resolve2(output);
|
|
9090
9234
|
};
|
|
9091
9235
|
const timer = setTimeout(() => {
|
|
9092
9236
|
const waiting = this.waiters.get(subtaskId);
|
|
@@ -9140,10 +9284,10 @@ var PeerBus = class extends EventEmitter {
|
|
|
9140
9284
|
*/
|
|
9141
9285
|
collect(timeoutMs) {
|
|
9142
9286
|
const collected = [...this.broadcastLog];
|
|
9143
|
-
return new Promise((
|
|
9287
|
+
return new Promise((resolve2) => {
|
|
9144
9288
|
const timer = setTimeout(() => {
|
|
9145
9289
|
off();
|
|
9146
|
-
|
|
9290
|
+
resolve2(collected);
|
|
9147
9291
|
}, timeoutMs);
|
|
9148
9292
|
const handler = (msg) => {
|
|
9149
9293
|
collected.push({ fromId: msg.fromId, payload: msg.payload, timestamp: msg.timestamp });
|
|
@@ -9157,7 +9301,7 @@ var PeerBus = class extends EventEmitter {
|
|
|
9157
9301
|
const broadcasters = new Set(collected.map((m) => m.fromId));
|
|
9158
9302
|
if (broadcasters.size >= this.members.size) {
|
|
9159
9303
|
off();
|
|
9160
|
-
|
|
9304
|
+
resolve2(collected);
|
|
9161
9305
|
}
|
|
9162
9306
|
};
|
|
9163
9307
|
this.on("broadcast", checkComplete);
|
|
@@ -9175,7 +9319,7 @@ var PeerBus = class extends EventEmitter {
|
|
|
9175
9319
|
return;
|
|
9176
9320
|
}
|
|
9177
9321
|
this.emitCoordination(tierId, `\u23F3 waiting for ${filePath} (held by ${existing.holderId})`);
|
|
9178
|
-
return new Promise((
|
|
9322
|
+
return new Promise((resolve2, reject) => {
|
|
9179
9323
|
const timer = setTimeout(() => {
|
|
9180
9324
|
reject(new Error(`File lock timeout for ${filePath} (held by ${existing.holderId})`));
|
|
9181
9325
|
}, timeoutMs);
|
|
@@ -9183,7 +9327,7 @@ var PeerBus = class extends EventEmitter {
|
|
|
9183
9327
|
clearTimeout(timer);
|
|
9184
9328
|
this.fileLocks.set(filePath, { holderId: tierId, lockedAt: (/* @__PURE__ */ new Date()).toISOString(), waiters: [] });
|
|
9185
9329
|
this.emitCoordination(tierId, `\u{1F512} locked ${filePath}`);
|
|
9186
|
-
|
|
9330
|
+
resolve2();
|
|
9187
9331
|
});
|
|
9188
9332
|
});
|
|
9189
9333
|
}
|
|
@@ -9208,11 +9352,11 @@ var PeerBus = class extends EventEmitter {
|
|
|
9208
9352
|
*/
|
|
9209
9353
|
waitForFileRelease(filePath, timeoutMs = 3e4) {
|
|
9210
9354
|
if (!this.fileLocks.has(filePath)) return Promise.resolve();
|
|
9211
|
-
return new Promise((
|
|
9355
|
+
return new Promise((resolve2, reject) => {
|
|
9212
9356
|
const timer = setTimeout(() => reject(new Error(`Timeout waiting for file release: ${filePath}`)), timeoutMs);
|
|
9213
9357
|
this.once(`file:released:${filePath}`, () => {
|
|
9214
9358
|
clearTimeout(timer);
|
|
9215
|
-
|
|
9359
|
+
resolve2();
|
|
9216
9360
|
});
|
|
9217
9361
|
});
|
|
9218
9362
|
}
|
|
@@ -9278,13 +9422,13 @@ var PeerBus = class extends EventEmitter {
|
|
|
9278
9422
|
this.emit(`barrier:${barrierName}`);
|
|
9279
9423
|
return;
|
|
9280
9424
|
}
|
|
9281
|
-
return new Promise((
|
|
9425
|
+
return new Promise((resolve2, reject) => {
|
|
9282
9426
|
const timer = setTimeout(() => {
|
|
9283
9427
|
reject(new Error(`Barrier timeout: ${barrierName} (${bar.arrived.size}/${bar.total} arrived)`));
|
|
9284
9428
|
}, 12e4);
|
|
9285
9429
|
this.once(`barrier:${barrierName}`, () => {
|
|
9286
9430
|
clearTimeout(timer);
|
|
9287
|
-
|
|
9431
|
+
resolve2();
|
|
9288
9432
|
});
|
|
9289
9433
|
});
|
|
9290
9434
|
}
|
|
@@ -9351,6 +9495,7 @@ var T2Manager = class extends BaseTier {
|
|
|
9351
9495
|
escalations = [];
|
|
9352
9496
|
peerSyncBuffer = [];
|
|
9353
9497
|
store;
|
|
9498
|
+
runBreaker;
|
|
9354
9499
|
t3PeerBus = new PeerBus();
|
|
9355
9500
|
// ← T3↔T3 bus (local to this T2)
|
|
9356
9501
|
t2PeerBus;
|
|
@@ -9381,6 +9526,15 @@ var T2Manager = class extends BaseTier {
|
|
|
9381
9526
|
this.router = router;
|
|
9382
9527
|
this.toolRegistry = toolRegistry;
|
|
9383
9528
|
}
|
|
9529
|
+
/**
|
|
9530
|
+
* Share the run-wide circuit breaker. Every T2 in a run gets the SAME
|
|
9531
|
+
* instance: three sections each failing once against a dead model is the
|
|
9532
|
+
* same fact as one section failing three times, and a per-section breaker
|
|
9533
|
+
* would never see it.
|
|
9534
|
+
*/
|
|
9535
|
+
setRunBreaker(breaker) {
|
|
9536
|
+
this.runBreaker = breaker;
|
|
9537
|
+
}
|
|
9384
9538
|
setStore(store) {
|
|
9385
9539
|
this.store = store;
|
|
9386
9540
|
}
|
|
@@ -9504,7 +9658,8 @@ Guidance (must be followed): ${decision.note}`
|
|
|
9504
9658
|
const overallStatus = this.determineStatus(t3Results);
|
|
9505
9659
|
const isOk = overallStatus === "COMPLETED" || overallStatus === "PARTIAL";
|
|
9506
9660
|
this.setStatus(isOk ? "COMPLETED" : "FAILED", summary);
|
|
9507
|
-
|
|
9661
|
+
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";
|
|
9662
|
+
this.sendStatusUpdate({ progressPct: 100, currentAction: sectionAction, status: "IN_PROGRESS", output: summary });
|
|
9508
9663
|
const result = {
|
|
9509
9664
|
sectionId: assignment.sectionId,
|
|
9510
9665
|
sectionTitle: assignment.sectionTitle,
|
|
@@ -9674,6 +9829,24 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
|
9674
9829
|
const reinforceCfg = this.router.getReinforcementsConfig?.() ?? { enabled: false, maxPerSection: 4 };
|
|
9675
9830
|
let reinforcementsAdded = 0;
|
|
9676
9831
|
while (remaining.size > 0) {
|
|
9832
|
+
if (this.runBreaker?.isOpen()) {
|
|
9833
|
+
const why = this.runBreaker.skipMessage();
|
|
9834
|
+
this.log(`Run breaker open \u2014 skipping ${remaining.size} remaining subtask(s)`);
|
|
9835
|
+
for (const id of remaining) {
|
|
9836
|
+
this.t3PeerBus.publish(this.id, id, why, "FAILED");
|
|
9837
|
+
resultMap.set(id, {
|
|
9838
|
+
subtaskId: id,
|
|
9839
|
+
status: "FAILED",
|
|
9840
|
+
output: why,
|
|
9841
|
+
testResults: { checksRun: [], passed: [], failed: [] },
|
|
9842
|
+
issues: [why],
|
|
9843
|
+
peerSyncsUsed: [],
|
|
9844
|
+
correctionAttempts: 0
|
|
9845
|
+
});
|
|
9846
|
+
}
|
|
9847
|
+
remaining.clear();
|
|
9848
|
+
break;
|
|
9849
|
+
}
|
|
9677
9850
|
const runnableIds = [...remaining].filter((id) => (inDegree.get(id) ?? 0) === 0);
|
|
9678
9851
|
if (runnableIds.length === 0) {
|
|
9679
9852
|
const fallbackId = [...remaining].sort(
|
|
@@ -9776,8 +9949,32 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
|
9776
9949
|
remaining.delete(id);
|
|
9777
9950
|
const r = waveResults[i];
|
|
9778
9951
|
if (r.status === "rejected") {
|
|
9779
|
-
this.log(`T3 worker ${id} failed: ${r.reason instanceof Error ? r.reason.message : String(r.reason)} \u2014 retrying once`);
|
|
9780
9952
|
const assignment = sanitizedAssignments.find((a) => a.subtaskId === id);
|
|
9953
|
+
const failedModel = workerMap.get(id)?.getServingModel();
|
|
9954
|
+
const tripped = this.runBreaker?.record(r.reason, failedModel);
|
|
9955
|
+
if (tripped) {
|
|
9956
|
+
this.log(`Run breaker opened: ${tripped.failures} consecutive ${tripped.kind} failures on ${tripped.modelId} \u2014 stopping`);
|
|
9957
|
+
this.emit("run:breaker", tripped);
|
|
9958
|
+
}
|
|
9959
|
+
if (this.runBreaker?.isOpen()) {
|
|
9960
|
+
const why = this.runBreaker.skipMessage();
|
|
9961
|
+
this.log(`T3 worker ${id} failed and the run breaker is open \u2014 not retrying`);
|
|
9962
|
+
this.t3PeerBus.publish(this.id, id, why, "FAILED");
|
|
9963
|
+
resultMap.set(id, {
|
|
9964
|
+
subtaskId: id,
|
|
9965
|
+
status: "FAILED",
|
|
9966
|
+
output: why,
|
|
9967
|
+
testResults: { checksRun: [], passed: [], failed: [] },
|
|
9968
|
+
issues: [why],
|
|
9969
|
+
peerSyncsUsed: [],
|
|
9970
|
+
correctionAttempts: 0
|
|
9971
|
+
});
|
|
9972
|
+
for (const dependent of adj.get(id) ?? []) {
|
|
9973
|
+
inDegree.set(dependent, Math.max(0, (inDegree.get(dependent) ?? 0) - 1));
|
|
9974
|
+
}
|
|
9975
|
+
continue;
|
|
9976
|
+
}
|
|
9977
|
+
this.log(`T3 worker ${id} failed: ${r.reason instanceof Error ? r.reason.message : String(r.reason)} \u2014 retrying once`);
|
|
9781
9978
|
try {
|
|
9782
9979
|
const retried = await this.retryT3(assignment, taskId);
|
|
9783
9980
|
resultMap.set(id, retried);
|
|
@@ -10112,6 +10309,7 @@ var T1Administrator = class extends BaseTier {
|
|
|
10112
10309
|
t2Managers = /* @__PURE__ */ new Map();
|
|
10113
10310
|
escalations = [];
|
|
10114
10311
|
store;
|
|
10312
|
+
runBreaker;
|
|
10115
10313
|
t2PeerBus = new PeerBus();
|
|
10116
10314
|
permissionEscalator;
|
|
10117
10315
|
toolCreator;
|
|
@@ -10126,6 +10324,10 @@ var T1Administrator = class extends BaseTier {
|
|
|
10126
10324
|
this.toolRegistry = toolRegistry;
|
|
10127
10325
|
this.config = config;
|
|
10128
10326
|
}
|
|
10327
|
+
/** Share the run-wide circuit breaker with every section manager. */
|
|
10328
|
+
setRunBreaker(breaker) {
|
|
10329
|
+
this.runBreaker = breaker;
|
|
10330
|
+
}
|
|
10129
10331
|
setStore(store) {
|
|
10130
10332
|
this.store = store;
|
|
10131
10333
|
}
|
|
@@ -10488,6 +10690,7 @@ SPEC RULES \u2014 each subtask is a self-contained spec slice (workers execute f
|
|
|
10488
10690
|
};
|
|
10489
10691
|
const managers = sections.map((section) => {
|
|
10490
10692
|
const manager = new T2Manager(this.router, this.toolRegistry, this.id);
|
|
10693
|
+
if (this.runBreaker) manager.setRunBreaker(this.runBreaker);
|
|
10491
10694
|
manager.setHierarchyContext(`You are a T2 Manager for the section "${section.sectionTitle}". You are part of a COMPLEX task overseen by T1 Administrator.`);
|
|
10492
10695
|
if (this.store) {
|
|
10493
10696
|
manager.setStore(this.store);
|
|
@@ -10776,8 +10979,156 @@ Reply with exactly one word: YES, NO, or UNSURE.
|
|
|
10776
10979
|
// src/core/cascade.ts
|
|
10777
10980
|
init_cost();
|
|
10778
10981
|
|
|
10779
|
-
// src/
|
|
10780
|
-
|
|
10982
|
+
// src/core/multimodal/registry.ts
|
|
10983
|
+
init_pricing();
|
|
10984
|
+
var CAPABILITIES = [
|
|
10985
|
+
// ── Image ──
|
|
10986
|
+
{
|
|
10987
|
+
modality: "image",
|
|
10988
|
+
provider: "openai",
|
|
10989
|
+
modelId: "dall-e-3",
|
|
10990
|
+
api: "openai-images"
|
|
10991
|
+
},
|
|
10992
|
+
{
|
|
10993
|
+
modality: "image",
|
|
10994
|
+
provider: "gemini",
|
|
10995
|
+
modelId: "gemini-3-pro-image",
|
|
10996
|
+
api: "gemini-predict"
|
|
10997
|
+
},
|
|
10998
|
+
// ── Speech (text → audio) ──
|
|
10999
|
+
{
|
|
11000
|
+
modality: "speech",
|
|
11001
|
+
provider: "openai",
|
|
11002
|
+
modelId: "tts-1",
|
|
11003
|
+
api: "openai-speech"
|
|
11004
|
+
},
|
|
11005
|
+
{
|
|
11006
|
+
// The only quality claim in this table that needs no leaderboard: OpenAI
|
|
11007
|
+
// ships these as the same voice at two fidelities and charges double for
|
|
11008
|
+
// this one. That is the vendor's own ordering, not an inference.
|
|
11009
|
+
modality: "speech",
|
|
11010
|
+
provider: "openai",
|
|
11011
|
+
modelId: "tts-1-hd",
|
|
11012
|
+
api: "openai-speech",
|
|
11013
|
+
quality: 70,
|
|
11014
|
+
qualityBasis: "OpenAI's own hd variant of tts-1, priced 2x"
|
|
11015
|
+
},
|
|
11016
|
+
// ── Transcription (audio → text) ──
|
|
11017
|
+
{
|
|
11018
|
+
modality: "transcription",
|
|
11019
|
+
provider: "openai",
|
|
11020
|
+
modelId: "whisper-1",
|
|
11021
|
+
api: "openai-transcriptions"
|
|
11022
|
+
},
|
|
11023
|
+
// ── Video ──
|
|
11024
|
+
{
|
|
11025
|
+
// Listed so `describe()` can tell the truth about what exists, and so the
|
|
11026
|
+
// planner is told video is out of reach rather than left to invent a tool.
|
|
11027
|
+
// Veo is a long-running operation: submit, poll, then fetch bytes from a
|
|
11028
|
+
// signed URL. That is a different execution shape from every other entry
|
|
11029
|
+
// here and is not wired up — claiming otherwise would produce a tool that
|
|
11030
|
+
// fails at call time instead of an honest "not yet".
|
|
11031
|
+
modality: "video",
|
|
11032
|
+
provider: "gemini",
|
|
11033
|
+
modelId: "veo-3.1-generate-001",
|
|
11034
|
+
api: "gemini-predict",
|
|
11035
|
+
unsupported: "Veo runs as a long-running operation (submit \u2192 poll \u2192 fetch). Not wired up yet."
|
|
11036
|
+
}
|
|
11037
|
+
];
|
|
11038
|
+
function capabilityCost(cap) {
|
|
11039
|
+
const match = findPricing(cap.modelId, cap.provider, {
|
|
11040
|
+
fallbackProviders: cap.provider === "gemini" ? ["vertex"] : []
|
|
11041
|
+
});
|
|
11042
|
+
const rate = match?.entry.rates?.[0];
|
|
11043
|
+
if (!rate) return null;
|
|
11044
|
+
const unitLabel = {
|
|
11045
|
+
per_image: "per image",
|
|
11046
|
+
per_second: "per second of video",
|
|
11047
|
+
per_character: "per character",
|
|
11048
|
+
per_audio_minute: "per audio minute"
|
|
11049
|
+
};
|
|
11050
|
+
const unit = unitLabel[rate.unit] ?? rate.unit;
|
|
11051
|
+
const amount = rate.amount;
|
|
11052
|
+
const shown = amount < 1e-3 ? amount.toExponential(2) : amount.toFixed(4);
|
|
11053
|
+
return { label: `$${shown} ${unit}`, amount, unit };
|
|
11054
|
+
}
|
|
11055
|
+
var MultimodalRegistry = class {
|
|
11056
|
+
/** Provider types the user has actually configured a key for. */
|
|
11057
|
+
available;
|
|
11058
|
+
constructor(availableProviders) {
|
|
11059
|
+
this.available = new Set(availableProviders);
|
|
11060
|
+
}
|
|
11061
|
+
/** Every capability whose provider is configured AND which Cascade can call. */
|
|
11062
|
+
usable(modality) {
|
|
11063
|
+
return CAPABILITIES.filter(
|
|
11064
|
+
(c) => !c.unsupported && this.available.has(c.provider) && (!modality || c.modality === modality)
|
|
11065
|
+
);
|
|
11066
|
+
}
|
|
11067
|
+
/** Modalities this account can actually generate right now. */
|
|
11068
|
+
availableModalities() {
|
|
11069
|
+
return [...new Set(this.usable().map((c) => c.modality))];
|
|
11070
|
+
}
|
|
11071
|
+
/**
|
|
11072
|
+
* Pick a model for a modality.
|
|
11073
|
+
*
|
|
11074
|
+
* Ranked models beat unranked ones, then higher quality, then lower unit
|
|
11075
|
+
* price. An explicit `preferModel` wins outright when it is usable — the user
|
|
11076
|
+
* naming a model is a stronger signal than any prior.
|
|
11077
|
+
*/
|
|
11078
|
+
select(modality, opts = {}) {
|
|
11079
|
+
const pool = this.usable(modality);
|
|
11080
|
+
if (!pool.length) return null;
|
|
11081
|
+
if (opts.preferModel) {
|
|
11082
|
+
const wanted = pool.find((c) => c.modelId === opts.preferModel);
|
|
11083
|
+
if (wanted) {
|
|
11084
|
+
return { capability: wanted, cost: capabilityCost(wanted), reason: `Requested explicitly: ${wanted.modelId}.` };
|
|
11085
|
+
}
|
|
11086
|
+
}
|
|
11087
|
+
const scored = pool.map((c) => ({ c, cost: capabilityCost(c) })).sort((a, b) => {
|
|
11088
|
+
const qa = a.c.quality ?? -1;
|
|
11089
|
+
const qb = b.c.quality ?? -1;
|
|
11090
|
+
if (qa !== qb) return qb - qa;
|
|
11091
|
+
const pa = a.cost?.amount ?? Number.POSITIVE_INFINITY;
|
|
11092
|
+
const pb = b.cost?.amount ?? Number.POSITIVE_INFINITY;
|
|
11093
|
+
return pa - pb;
|
|
11094
|
+
});
|
|
11095
|
+
const best = scored[0];
|
|
11096
|
+
const ranked = best.c.quality !== void 0;
|
|
11097
|
+
const reason = ranked ? `${best.c.modelId} (${best.c.provider}) \u2014 ranked highest for ${modality}: ${best.c.qualityBasis}.` + (best.cost ? ` ${best.cost.label}.` : "") : `${best.c.modelId} (${best.c.provider}) \u2014 cheapest usable ${modality} model` + (best.cost ? ` at ${best.cost.label}` : "") + ". No published quality ranking is applied for this modality, so this is a cost choice, not a quality one.";
|
|
11098
|
+
return { capability: best.c, cost: best.cost, reason };
|
|
11099
|
+
}
|
|
11100
|
+
/**
|
|
11101
|
+
* A plain-language account of what this account can and cannot generate —
|
|
11102
|
+
* including the things it cannot, and why. The planner sees this, so being
|
|
11103
|
+
* candid here is what stops it inventing a `generate_music` call.
|
|
11104
|
+
*/
|
|
11105
|
+
describe() {
|
|
11106
|
+
const lines = [];
|
|
11107
|
+
const usable = this.usable();
|
|
11108
|
+
if (usable.length) {
|
|
11109
|
+
lines.push("Generation models available on your configured providers:");
|
|
11110
|
+
for (const modality of ["image", "speech", "transcription", "video"]) {
|
|
11111
|
+
const forModality = usable.filter((c) => c.modality === modality);
|
|
11112
|
+
if (!forModality.length) continue;
|
|
11113
|
+
const shown = forModality.map((c) => {
|
|
11114
|
+
const cost = capabilityCost(c);
|
|
11115
|
+
return `${c.modelId}${cost ? ` (${cost.label})` : ""}`;
|
|
11116
|
+
});
|
|
11117
|
+
lines.push(` ${modality}: ${shown.join(", ")}`);
|
|
11118
|
+
}
|
|
11119
|
+
} else {
|
|
11120
|
+
lines.push("No generation models are available \u2014 none of your configured providers offers one.");
|
|
11121
|
+
}
|
|
11122
|
+
const blocked = CAPABILITIES.filter((c) => c.unsupported && this.available.has(c.provider));
|
|
11123
|
+
for (const c of blocked) {
|
|
11124
|
+
lines.push(` ${c.modality}: ${c.modelId} exists on your account but is not callable. ${c.unsupported}`);
|
|
11125
|
+
}
|
|
11126
|
+
lines.push(
|
|
11127
|
+
" music: no supported provider exposes a music-generation API, so Cascade cannot generate music."
|
|
11128
|
+
);
|
|
11129
|
+
return lines.join("\n");
|
|
11130
|
+
}
|
|
11131
|
+
};
|
|
10781
11132
|
|
|
10782
11133
|
// src/tools/base.ts
|
|
10783
11134
|
var BaseTool = class {
|
|
@@ -10797,7 +11148,449 @@ var BaseTool = class {
|
|
|
10797
11148
|
}
|
|
10798
11149
|
};
|
|
10799
11150
|
|
|
10800
|
-
// src/
|
|
11151
|
+
// src/core/router/provider-errors.ts
|
|
11152
|
+
var SYSTEMIC = /* @__PURE__ */ new Set([
|
|
11153
|
+
"rate_limit",
|
|
11154
|
+
"quota_exhausted",
|
|
11155
|
+
"auth",
|
|
11156
|
+
"model_unavailable"
|
|
11157
|
+
]);
|
|
11158
|
+
function isSystemicKind(kind) {
|
|
11159
|
+
return SYSTEMIC.has(kind);
|
|
11160
|
+
}
|
|
11161
|
+
function statusOf(err) {
|
|
11162
|
+
if (!err || typeof err !== "object") return void 0;
|
|
11163
|
+
const e = err;
|
|
11164
|
+
for (const key of ["status", "statusCode", "code"]) {
|
|
11165
|
+
const v = e[key];
|
|
11166
|
+
if (typeof v === "number" && v >= 100 && v < 600) return v;
|
|
11167
|
+
}
|
|
11168
|
+
const resp = e["response"];
|
|
11169
|
+
if (resp && typeof resp["status"] === "number") return resp["status"];
|
|
11170
|
+
const inner = e["error"];
|
|
11171
|
+
if (inner && typeof inner["code"] === "number") return inner["code"];
|
|
11172
|
+
return void 0;
|
|
11173
|
+
}
|
|
11174
|
+
function messageOf(err) {
|
|
11175
|
+
if (err instanceof Error) return err.message;
|
|
11176
|
+
if (typeof err === "string") return err;
|
|
11177
|
+
if (err && typeof err === "object") {
|
|
11178
|
+
const e = err;
|
|
11179
|
+
if (typeof e["message"] === "string") return e["message"];
|
|
11180
|
+
const inner = e["error"];
|
|
11181
|
+
if (inner && typeof inner["message"] === "string") return inner["message"];
|
|
11182
|
+
try {
|
|
11183
|
+
return JSON.stringify(err);
|
|
11184
|
+
} catch {
|
|
11185
|
+
}
|
|
11186
|
+
}
|
|
11187
|
+
return String(err);
|
|
11188
|
+
}
|
|
11189
|
+
function classifyProviderError(err) {
|
|
11190
|
+
const raw = messageOf(err).trim();
|
|
11191
|
+
const status = statusOf(err);
|
|
11192
|
+
const m = raw.toLowerCase();
|
|
11193
|
+
const kind = (() => {
|
|
11194
|
+
if (status === 429) {
|
|
11195
|
+
return /quota|billing|credit|exceeded your current quota|insufficient/.test(m) ? "quota_exhausted" : "rate_limit";
|
|
11196
|
+
}
|
|
11197
|
+
if (status === 401 || status === 403) return "auth";
|
|
11198
|
+
if (status === 404) return "model_unavailable";
|
|
11199
|
+
if (/rate limit|too many requests|resource_exhausted/.test(m)) {
|
|
11200
|
+
return /quota|billing|credit|insufficient/.test(m) ? "quota_exhausted" : "rate_limit";
|
|
11201
|
+
}
|
|
11202
|
+
if (/quota|billing|insufficient_quota|insufficient credit|payment required/.test(m)) return "quota_exhausted";
|
|
11203
|
+
if (/api key|unauthorized|authentication|permission denied|invalid[_ ]api/.test(m)) return "auth";
|
|
11204
|
+
if (/model .*(not found|does not exist|unavailable)|no such model|unknown model|not supported for/.test(m)) {
|
|
11205
|
+
return "model_unavailable";
|
|
11206
|
+
}
|
|
11207
|
+
if (/context length|too many tokens|maximum context|prompt is too long|input too long/.test(m)) return "context_length";
|
|
11208
|
+
if (/safety|content filter|blocked|recitation|prohibited/.test(m)) return "content_filter";
|
|
11209
|
+
if (/econnreset|etimedout|enotfound|socket hang up|network|fetch failed|aborted/.test(m)) return "network";
|
|
11210
|
+
return "unknown";
|
|
11211
|
+
})();
|
|
11212
|
+
return { kind, systemic: isSystemicKind(kind), status, raw };
|
|
11213
|
+
}
|
|
11214
|
+
function describeProviderError(c, modelId) {
|
|
11215
|
+
const on = modelId ? ` on ${modelId}` : "";
|
|
11216
|
+
const detail = c.raw ? ` Provider said: ${c.raw}` : "";
|
|
11217
|
+
switch (c.kind) {
|
|
11218
|
+
case "rate_limit":
|
|
11219
|
+
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}`;
|
|
11220
|
+
case "quota_exhausted":
|
|
11221
|
+
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}`;
|
|
11222
|
+
case "auth":
|
|
11223
|
+
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}`;
|
|
11224
|
+
case "model_unavailable":
|
|
11225
|
+
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}`;
|
|
11226
|
+
case "context_length":
|
|
11227
|
+
return `The prompt was too long${on}. Reduce the attached context, or use a model with a larger context window for this tier.${detail}`;
|
|
11228
|
+
case "content_filter":
|
|
11229
|
+
return `The provider's safety filter refused this request${on}.${detail}`;
|
|
11230
|
+
case "network":
|
|
11231
|
+
return `Network error reaching the provider${on}. This is usually transient.${detail}`;
|
|
11232
|
+
default:
|
|
11233
|
+
return `The model call failed${on}.${detail}`;
|
|
11234
|
+
}
|
|
11235
|
+
}
|
|
11236
|
+
|
|
11237
|
+
// src/core/multimodal/generate.ts
|
|
11238
|
+
var DEFAULT_TIMEOUT_MS = 12e4;
|
|
11239
|
+
async function callProvider(fn, modelId) {
|
|
11240
|
+
try {
|
|
11241
|
+
return await fn();
|
|
11242
|
+
} catch (err) {
|
|
11243
|
+
const classified = classifyProviderError(err);
|
|
11244
|
+
throw new Error(describeProviderError(classified, modelId));
|
|
11245
|
+
}
|
|
11246
|
+
}
|
|
11247
|
+
async function postJson(url, body, headers, signal) {
|
|
11248
|
+
const ac = new AbortController();
|
|
11249
|
+
const timer = setTimeout(() => ac.abort(), DEFAULT_TIMEOUT_MS);
|
|
11250
|
+
const onAbort = () => ac.abort();
|
|
11251
|
+
signal?.addEventListener("abort", onAbort);
|
|
11252
|
+
try {
|
|
11253
|
+
const res = await fetch(url, {
|
|
11254
|
+
method: "POST",
|
|
11255
|
+
headers: { "Content-Type": "application/json", ...headers },
|
|
11256
|
+
body: JSON.stringify(body),
|
|
11257
|
+
signal: ac.signal
|
|
11258
|
+
});
|
|
11259
|
+
if (!res.ok) {
|
|
11260
|
+
const text = await res.text().catch(() => "");
|
|
11261
|
+
throw Object.assign(new Error(text || res.statusText), { status: res.status });
|
|
11262
|
+
}
|
|
11263
|
+
return res;
|
|
11264
|
+
} finally {
|
|
11265
|
+
clearTimeout(timer);
|
|
11266
|
+
signal?.removeEventListener("abort", onAbort);
|
|
11267
|
+
}
|
|
11268
|
+
}
|
|
11269
|
+
function baseUrlFor(cfg, fallback) {
|
|
11270
|
+
return (cfg.baseUrl ?? fallback).replace(/\/+$/, "");
|
|
11271
|
+
}
|
|
11272
|
+
function stamp(ext) {
|
|
11273
|
+
return `cascade-${Date.now()}.${ext}`;
|
|
11274
|
+
}
|
|
11275
|
+
async function generateImage(cap, cfg, req, signal) {
|
|
11276
|
+
return callProvider(async () => {
|
|
11277
|
+
if (cap.api === "openai-images") {
|
|
11278
|
+
const url = `${baseUrlFor(cfg, "https://api.openai.com/v1")}/images/generations`;
|
|
11279
|
+
const res = await postJson(url, {
|
|
11280
|
+
model: cap.modelId,
|
|
11281
|
+
prompt: req.prompt,
|
|
11282
|
+
size: req.size ?? "1024x1024",
|
|
11283
|
+
n: 1,
|
|
11284
|
+
// b64 rather than a URL: see the file header — provider URLs expire.
|
|
11285
|
+
response_format: "b64_json"
|
|
11286
|
+
}, { Authorization: `Bearer ${cfg.apiKey ?? ""}` }, signal);
|
|
11287
|
+
const body = await res.json();
|
|
11288
|
+
const first = body.data?.[0];
|
|
11289
|
+
if (!first?.b64_json) throw new Error("Image response contained no image data.");
|
|
11290
|
+
return {
|
|
11291
|
+
data: Buffer.from(first.b64_json, "base64"),
|
|
11292
|
+
mimeType: "image/png",
|
|
11293
|
+
filename: stamp("png"),
|
|
11294
|
+
modelId: cap.modelId,
|
|
11295
|
+
provider: cap.provider,
|
|
11296
|
+
...first.revised_prompt ? { revisedPrompt: first.revised_prompt } : {}
|
|
11297
|
+
};
|
|
11298
|
+
}
|
|
11299
|
+
if (cap.api === "gemini-predict") {
|
|
11300
|
+
const base = baseUrlFor(cfg, "https://generativelanguage.googleapis.com/v1beta");
|
|
11301
|
+
const url = `${base}/models/${encodeURIComponent(cap.modelId)}:predict`;
|
|
11302
|
+
const res = await postJson(url, {
|
|
11303
|
+
instances: [{ prompt: req.prompt }],
|
|
11304
|
+
parameters: { sampleCount: 1 }
|
|
11305
|
+
}, { "x-goog-api-key": cfg.apiKey ?? "" }, signal);
|
|
11306
|
+
const body = await res.json();
|
|
11307
|
+
const first = body.predictions?.[0];
|
|
11308
|
+
if (!first?.bytesBase64Encoded) throw new Error("Image response contained no image data.");
|
|
11309
|
+
const mimeType = first.mimeType ?? "image/png";
|
|
11310
|
+
return {
|
|
11311
|
+
data: Buffer.from(first.bytesBase64Encoded, "base64"),
|
|
11312
|
+
mimeType,
|
|
11313
|
+
filename: stamp(mimeType.includes("jpeg") ? "jpg" : "png"),
|
|
11314
|
+
modelId: cap.modelId,
|
|
11315
|
+
provider: cap.provider
|
|
11316
|
+
};
|
|
11317
|
+
}
|
|
11318
|
+
throw new Error(`${cap.modelId} is not an image generator.`);
|
|
11319
|
+
}, cap.modelId);
|
|
11320
|
+
}
|
|
11321
|
+
async function generateSpeech(cap, cfg, req, signal) {
|
|
11322
|
+
return callProvider(async () => {
|
|
11323
|
+
if (cap.api !== "openai-speech") throw new Error(`${cap.modelId} is not a speech model.`);
|
|
11324
|
+
const format = req.format ?? "mp3";
|
|
11325
|
+
const url = `${baseUrlFor(cfg, "https://api.openai.com/v1")}/audio/speech`;
|
|
11326
|
+
const res = await postJson(url, {
|
|
11327
|
+
model: cap.modelId,
|
|
11328
|
+
input: req.text,
|
|
11329
|
+
voice: req.voice ?? "alloy",
|
|
11330
|
+
response_format: format
|
|
11331
|
+
}, { Authorization: `Bearer ${cfg.apiKey ?? ""}` }, signal);
|
|
11332
|
+
const mime = {
|
|
11333
|
+
mp3: "audio/mpeg",
|
|
11334
|
+
opus: "audio/opus",
|
|
11335
|
+
aac: "audio/aac",
|
|
11336
|
+
flac: "audio/flac",
|
|
11337
|
+
wav: "audio/wav"
|
|
11338
|
+
};
|
|
11339
|
+
return {
|
|
11340
|
+
data: Buffer.from(await res.arrayBuffer()),
|
|
11341
|
+
mimeType: mime[format] ?? "application/octet-stream",
|
|
11342
|
+
filename: stamp(format),
|
|
11343
|
+
modelId: cap.modelId,
|
|
11344
|
+
provider: cap.provider
|
|
11345
|
+
};
|
|
11346
|
+
}, cap.modelId);
|
|
11347
|
+
}
|
|
11348
|
+
async function transcribeAudio(cap, cfg, req, signal) {
|
|
11349
|
+
return callProvider(async () => {
|
|
11350
|
+
if (cap.api !== "openai-transcriptions") throw new Error(`${cap.modelId} is not a transcription model.`);
|
|
11351
|
+
const url = `${baseUrlFor(cfg, "https://api.openai.com/v1")}/audio/transcriptions`;
|
|
11352
|
+
const form = new FormData();
|
|
11353
|
+
form.append("model", cap.modelId);
|
|
11354
|
+
if (req.language) form.append("language", req.language);
|
|
11355
|
+
form.append("file", new Blob([new Uint8Array(req.audio)]), req.filename);
|
|
11356
|
+
const ac = new AbortController();
|
|
11357
|
+
const timer = setTimeout(() => ac.abort(), DEFAULT_TIMEOUT_MS);
|
|
11358
|
+
const onAbort = () => ac.abort();
|
|
11359
|
+
signal?.addEventListener("abort", onAbort);
|
|
11360
|
+
try {
|
|
11361
|
+
const res = await fetch(url, {
|
|
11362
|
+
method: "POST",
|
|
11363
|
+
headers: { Authorization: `Bearer ${cfg.apiKey ?? ""}` },
|
|
11364
|
+
body: form,
|
|
11365
|
+
signal: ac.signal
|
|
11366
|
+
});
|
|
11367
|
+
if (!res.ok) {
|
|
11368
|
+
const text = await res.text().catch(() => "");
|
|
11369
|
+
throw Object.assign(new Error(text || res.statusText), { status: res.status });
|
|
11370
|
+
}
|
|
11371
|
+
const body = await res.json();
|
|
11372
|
+
return { text: body.text ?? "", modelId: cap.modelId };
|
|
11373
|
+
} finally {
|
|
11374
|
+
clearTimeout(timer);
|
|
11375
|
+
signal?.removeEventListener("abort", onAbort);
|
|
11376
|
+
}
|
|
11377
|
+
}, cap.modelId);
|
|
11378
|
+
}
|
|
11379
|
+
|
|
11380
|
+
// src/tools/generate-media.ts
|
|
11381
|
+
function resolve(deps, modality, preferModel) {
|
|
11382
|
+
const selected = deps.registry.select(modality, preferModel ? { preferModel } : {});
|
|
11383
|
+
if (!selected) {
|
|
11384
|
+
return { ok: false, error: `No ${modality} model is available on your configured providers.` };
|
|
11385
|
+
}
|
|
11386
|
+
const cfg = deps.lookupProvider(selected.capability.provider);
|
|
11387
|
+
if (!cfg) {
|
|
11388
|
+
return {
|
|
11389
|
+
ok: false,
|
|
11390
|
+
error: `${selected.capability.modelId} needs the ${selected.capability.provider} provider, which is not configured.`
|
|
11391
|
+
};
|
|
11392
|
+
}
|
|
11393
|
+
return { ok: true, selected, cfg };
|
|
11394
|
+
}
|
|
11395
|
+
var GenerateImageTool = class extends BaseTool {
|
|
11396
|
+
constructor(deps) {
|
|
11397
|
+
super();
|
|
11398
|
+
this.deps = deps;
|
|
11399
|
+
}
|
|
11400
|
+
deps;
|
|
11401
|
+
name = "generate_image";
|
|
11402
|
+
description = "Generate an image from a text description using the best available image model. Returns where the image was saved. Use when the user asks for a picture, illustration, diagram render, logo or any visual asset.";
|
|
11403
|
+
inputSchema = {
|
|
11404
|
+
type: "object",
|
|
11405
|
+
properties: {
|
|
11406
|
+
prompt: { type: "string", description: "What the image should show. Be specific about subject, style and composition." },
|
|
11407
|
+
size: { type: "string", description: 'Pixel size, e.g. "1024x1024" (default), "1792x1024", "1024x1792".' },
|
|
11408
|
+
model: { type: "string", description: "Optional: force a specific image model instead of the automatic choice." }
|
|
11409
|
+
},
|
|
11410
|
+
required: ["prompt"]
|
|
11411
|
+
};
|
|
11412
|
+
async execute(input, options) {
|
|
11413
|
+
const prompt = String(input["prompt"] ?? "").trim();
|
|
11414
|
+
if (!prompt) return 'Provide a "prompt" describing the image.';
|
|
11415
|
+
const r = resolve(this.deps, "image", input["model"] ? String(input["model"]) : void 0);
|
|
11416
|
+
if (!r.ok) return r.error;
|
|
11417
|
+
const size = input["size"] ? String(input["size"]) : void 0;
|
|
11418
|
+
const asset = await generateImage(
|
|
11419
|
+
r.selected.capability,
|
|
11420
|
+
r.cfg,
|
|
11421
|
+
{ prompt, ...size ? { size } : {} },
|
|
11422
|
+
options.signal
|
|
11423
|
+
);
|
|
11424
|
+
const location = await this.deps.sink(asset);
|
|
11425
|
+
return [
|
|
11426
|
+
`Image generated and saved to ${location}.`,
|
|
11427
|
+
`Model: ${asset.modelId} \u2014 ${r.selected.reason}`,
|
|
11428
|
+
// DALL·E rewrites the prompt before drawing; the model needs to know what
|
|
11429
|
+
// was actually rendered, not what it asked for, or its description of the
|
|
11430
|
+
// result will not match the image.
|
|
11431
|
+
asset.revisedPrompt ? `The provider rewrote the prompt to: ${asset.revisedPrompt}` : ""
|
|
11432
|
+
].filter(Boolean).join("\n");
|
|
11433
|
+
}
|
|
11434
|
+
};
|
|
11435
|
+
var GenerateSpeechTool = class extends BaseTool {
|
|
11436
|
+
constructor(deps) {
|
|
11437
|
+
super();
|
|
11438
|
+
this.deps = deps;
|
|
11439
|
+
}
|
|
11440
|
+
deps;
|
|
11441
|
+
name = "generate_speech";
|
|
11442
|
+
description = "Turn text into spoken audio using the best available text-to-speech model. Returns where the audio was saved. Use when the user asks for narration, a voiceover or an audio version of some text.";
|
|
11443
|
+
inputSchema = {
|
|
11444
|
+
type: "object",
|
|
11445
|
+
properties: {
|
|
11446
|
+
text: { type: "string", description: "The text to speak." },
|
|
11447
|
+
voice: { type: "string", description: "Voice id: alloy, echo, fable, onyx, nova, shimmer. Default alloy." },
|
|
11448
|
+
format: { type: "string", description: "Audio format: mp3 (default), opus, aac, flac, wav." },
|
|
11449
|
+
model: { type: "string", description: "Optional: force a specific speech model." }
|
|
11450
|
+
},
|
|
11451
|
+
required: ["text"]
|
|
11452
|
+
};
|
|
11453
|
+
async execute(input, options) {
|
|
11454
|
+
const text = String(input["text"] ?? "").trim();
|
|
11455
|
+
if (!text) return 'Provide "text" to speak.';
|
|
11456
|
+
const r = resolve(this.deps, "speech", input["model"] ? String(input["model"]) : void 0);
|
|
11457
|
+
if (!r.ok) return r.error;
|
|
11458
|
+
const fmt = String(input["format"] ?? "mp3");
|
|
11459
|
+
const allowed = ["mp3", "opus", "aac", "flac", "wav"];
|
|
11460
|
+
const format = allowed.includes(fmt) ? fmt : "mp3";
|
|
11461
|
+
const asset = await generateSpeech(
|
|
11462
|
+
r.selected.capability,
|
|
11463
|
+
r.cfg,
|
|
11464
|
+
{ text, voice: input["voice"] ? String(input["voice"]) : void 0, format },
|
|
11465
|
+
options.signal
|
|
11466
|
+
);
|
|
11467
|
+
const location = await this.deps.sink(asset);
|
|
11468
|
+
return `Speech generated and saved to ${location}.
|
|
11469
|
+
Model: ${asset.modelId} \u2014 ${r.selected.reason}`;
|
|
11470
|
+
}
|
|
11471
|
+
};
|
|
11472
|
+
var TranscribeAudioTool = class extends BaseTool {
|
|
11473
|
+
constructor(deps, readFile) {
|
|
11474
|
+
super();
|
|
11475
|
+
this.deps = deps;
|
|
11476
|
+
this.readFile = readFile;
|
|
11477
|
+
}
|
|
11478
|
+
deps;
|
|
11479
|
+
readFile;
|
|
11480
|
+
name = "transcribe_audio";
|
|
11481
|
+
description = "Transcribe a local audio file to text using the best available speech-to-text model. Use when the user supplies audio and wants its contents in writing.";
|
|
11482
|
+
inputSchema = {
|
|
11483
|
+
type: "object",
|
|
11484
|
+
properties: {
|
|
11485
|
+
path: { type: "string", description: "Path to the audio file to transcribe." },
|
|
11486
|
+
language: { type: "string", description: 'Optional ISO-639-1 language hint, e.g. "en", improves accuracy.' },
|
|
11487
|
+
model: { type: "string", description: "Optional: force a specific transcription model." }
|
|
11488
|
+
},
|
|
11489
|
+
required: ["path"]
|
|
11490
|
+
};
|
|
11491
|
+
async execute(input, options) {
|
|
11492
|
+
const filePath = String(input["path"] ?? "").trim();
|
|
11493
|
+
if (!filePath) return 'Provide a "path" to the audio file.';
|
|
11494
|
+
const r = resolve(this.deps, "transcription", input["model"] ? String(input["model"]) : void 0);
|
|
11495
|
+
if (!r.ok) return r.error;
|
|
11496
|
+
let audio;
|
|
11497
|
+
try {
|
|
11498
|
+
audio = await this.readFile(filePath);
|
|
11499
|
+
} catch {
|
|
11500
|
+
return `Could not read the audio file at ${filePath}.`;
|
|
11501
|
+
}
|
|
11502
|
+
const { text, modelId } = await transcribeAudio(
|
|
11503
|
+
r.selected.capability,
|
|
11504
|
+
r.cfg,
|
|
11505
|
+
{
|
|
11506
|
+
audio,
|
|
11507
|
+
filename: filePath.split(/[\\/]/).pop() || "audio",
|
|
11508
|
+
...input["language"] ? { language: String(input["language"]) } : {}
|
|
11509
|
+
},
|
|
11510
|
+
options.signal
|
|
11511
|
+
);
|
|
11512
|
+
return text ? `Transcript (${modelId}):
|
|
11513
|
+
|
|
11514
|
+
${text}` : `${modelId} returned an empty transcript \u2014 the file may be silent or in an unsupported format.`;
|
|
11515
|
+
}
|
|
11516
|
+
};
|
|
11517
|
+
function buildMediaTools(deps, readFile) {
|
|
11518
|
+
const tools = [];
|
|
11519
|
+
const modalities = new Set(deps.registry.availableModalities());
|
|
11520
|
+
if (modalities.has("image")) tools.push(new GenerateImageTool(deps));
|
|
11521
|
+
if (modalities.has("speech")) tools.push(new GenerateSpeechTool(deps));
|
|
11522
|
+
if (modalities.has("transcription") && readFile) tools.push(new TranscribeAudioTool(deps, readFile));
|
|
11523
|
+
return tools;
|
|
11524
|
+
}
|
|
11525
|
+
|
|
11526
|
+
// src/core/run-breaker.ts
|
|
11527
|
+
var DEFAULT_FAILURE_THRESHOLD = 3;
|
|
11528
|
+
var RunBreaker = class {
|
|
11529
|
+
threshold;
|
|
11530
|
+
/** modelId → consecutive systemic failures of the SAME kind. */
|
|
11531
|
+
counts = /* @__PURE__ */ new Map();
|
|
11532
|
+
trip = null;
|
|
11533
|
+
constructor(threshold = DEFAULT_FAILURE_THRESHOLD) {
|
|
11534
|
+
this.threshold = Math.max(1, threshold);
|
|
11535
|
+
}
|
|
11536
|
+
/**
|
|
11537
|
+
* Record a failed model call. Returns the trip if this failure is the one
|
|
11538
|
+
* that opened the breaker, otherwise null.
|
|
11539
|
+
*
|
|
11540
|
+
* A non-systemic failure RESETS the model's counter: it is evidence the model
|
|
11541
|
+
* is reachable and answering, which is exactly what a systemic streak claims
|
|
11542
|
+
* it is not.
|
|
11543
|
+
*/
|
|
11544
|
+
record(err, modelId) {
|
|
11545
|
+
if (this.trip) return null;
|
|
11546
|
+
const id = modelId?.trim() || "unknown-model";
|
|
11547
|
+
const c = classifyProviderError(err);
|
|
11548
|
+
if (!c.systemic) {
|
|
11549
|
+
this.counts.delete(id);
|
|
11550
|
+
return null;
|
|
11551
|
+
}
|
|
11552
|
+
const prev = this.counts.get(id);
|
|
11553
|
+
const n = prev && prev.kind === c.kind ? prev.n + 1 : 1;
|
|
11554
|
+
this.counts.set(id, { kind: c.kind, n, last: c });
|
|
11555
|
+
if (n >= this.threshold) {
|
|
11556
|
+
this.trip = {
|
|
11557
|
+
modelId: id,
|
|
11558
|
+
kind: c.kind,
|
|
11559
|
+
failures: n,
|
|
11560
|
+
message: describeProviderError(c, id)
|
|
11561
|
+
};
|
|
11562
|
+
return this.trip;
|
|
11563
|
+
}
|
|
11564
|
+
return null;
|
|
11565
|
+
}
|
|
11566
|
+
/** True once the run should stop launching work that would fail identically. */
|
|
11567
|
+
isOpen() {
|
|
11568
|
+
return this.trip !== null;
|
|
11569
|
+
}
|
|
11570
|
+
/** Why the breaker opened, or null while it is closed. */
|
|
11571
|
+
reason() {
|
|
11572
|
+
return this.trip;
|
|
11573
|
+
}
|
|
11574
|
+
/**
|
|
11575
|
+
* One line for a worker/section that never ran because the breaker was
|
|
11576
|
+
* already open. Says the work was skipped deliberately — not that it failed
|
|
11577
|
+
* on its own merits, which is what an empty FAILED status would imply.
|
|
11578
|
+
*/
|
|
11579
|
+
skipMessage() {
|
|
11580
|
+
const t = this.trip;
|
|
11581
|
+
if (!t) return "Skipped.";
|
|
11582
|
+
return `Skipped \u2014 the run stopped after ${t.failures} consecutive failures on ${t.modelId}. ${t.message}`;
|
|
11583
|
+
}
|
|
11584
|
+
/** Test/diagnostic view of the per-model streaks. */
|
|
11585
|
+
streaks() {
|
|
11586
|
+
const out = {};
|
|
11587
|
+
for (const [k, v] of this.counts) out[k] = { kind: v.kind, n: v.n };
|
|
11588
|
+
return out;
|
|
11589
|
+
}
|
|
11590
|
+
};
|
|
11591
|
+
|
|
11592
|
+
// src/tools/registry.ts
|
|
11593
|
+
init_constants();
|
|
10801
11594
|
var execAsync = promisify(exec);
|
|
10802
11595
|
var ShellTool = class extends BaseTool {
|
|
10803
11596
|
name = "shell";
|
|
@@ -11493,7 +12286,7 @@ var PDFCreateTool = class extends BaseTool {
|
|
|
11493
12286
|
if (!fs29.existsSync(dir)) {
|
|
11494
12287
|
fs29.mkdirSync(dir, { recursive: true });
|
|
11495
12288
|
}
|
|
11496
|
-
return new Promise((
|
|
12289
|
+
return new Promise((resolve2, reject) => {
|
|
11497
12290
|
try {
|
|
11498
12291
|
const doc = new PDFDocument({ margin: 50 });
|
|
11499
12292
|
const stream = fs29.createWriteStream(filePath);
|
|
@@ -11511,7 +12304,7 @@ var PDFCreateTool = class extends BaseTool {
|
|
|
11511
12304
|
});
|
|
11512
12305
|
doc.end();
|
|
11513
12306
|
stream.on("finish", () => {
|
|
11514
|
-
|
|
12307
|
+
resolve2(`Successfully created PDF at ${filePath} (${content.length} characters)`);
|
|
11515
12308
|
});
|
|
11516
12309
|
stream.on("error", (err) => {
|
|
11517
12310
|
reject(new Error(`Failed to write PDF: ${err.message}`));
|
|
@@ -11583,7 +12376,7 @@ var CodeInterpreterTool = class extends BaseTool {
|
|
|
11583
12376
|
const filePath = path32.join(tmpDir, fileName);
|
|
11584
12377
|
fs29.writeFileSync(filePath, code, "utf-8");
|
|
11585
12378
|
const execArgs = [filePath, ...args];
|
|
11586
|
-
return new Promise((
|
|
12379
|
+
return new Promise((resolve2) => {
|
|
11587
12380
|
const startMs = Date.now();
|
|
11588
12381
|
execFile(cmdPrefix, execArgs, { cwd: this.workspaceRoot, timeout: 3e4 }, (error, stdout, stderr) => {
|
|
11589
12382
|
const duration = Date.now() - startMs;
|
|
@@ -11597,17 +12390,17 @@ var CodeInterpreterTool = class extends BaseTool {
|
|
|
11597
12390
|
if (error) {
|
|
11598
12391
|
const timedOut = error.killed && duration >= 3e4;
|
|
11599
12392
|
if (timedOut) {
|
|
11600
|
-
|
|
12393
|
+
resolve2(`Execution timed out after 30s. Consider breaking the task into smaller pieces.
|
|
11601
12394
|
Partial stdout: ${stdout}
|
|
11602
12395
|
Stderr: ${stderr}`);
|
|
11603
12396
|
} else {
|
|
11604
|
-
|
|
12397
|
+
resolve2(`Execution failed (${duration}ms):
|
|
11605
12398
|
Error: ${error.message}
|
|
11606
12399
|
Stderr: ${stderr}
|
|
11607
12400
|
Stdout: ${stdout}`);
|
|
11608
12401
|
}
|
|
11609
12402
|
} else {
|
|
11610
|
-
|
|
12403
|
+
resolve2(`Execution successful (${duration}ms):
|
|
11611
12404
|
Stdout: ${stdout}
|
|
11612
12405
|
Stderr: ${stderr}`);
|
|
11613
12406
|
}
|
|
@@ -12261,7 +13054,7 @@ var ToolRegistry = class extends EventEmitter {
|
|
|
12261
13054
|
*/
|
|
12262
13055
|
waitForTool(toolName, timeoutMs = 6e4) {
|
|
12263
13056
|
if (this.tools.has(toolName)) return Promise.resolve();
|
|
12264
|
-
return new Promise((
|
|
13057
|
+
return new Promise((resolve2, reject) => {
|
|
12265
13058
|
const timer = setTimeout(() => {
|
|
12266
13059
|
this.off("tool:added", handler);
|
|
12267
13060
|
reject(new Error(`Timeout waiting for tool: ${toolName}`));
|
|
@@ -12270,7 +13063,7 @@ var ToolRegistry = class extends EventEmitter {
|
|
|
12270
13063
|
if (name === toolName) {
|
|
12271
13064
|
clearTimeout(timer);
|
|
12272
13065
|
this.off("tool:added", handler);
|
|
12273
|
-
|
|
13066
|
+
resolve2();
|
|
12274
13067
|
}
|
|
12275
13068
|
};
|
|
12276
13069
|
this.on("tool:added", handler);
|
|
@@ -12923,20 +13716,20 @@ var PermissionEscalator = class extends EventEmitter {
|
|
|
12923
13716
|
resolver(decision);
|
|
12924
13717
|
}
|
|
12925
13718
|
waitForUserDecision(req) {
|
|
12926
|
-
return new Promise((
|
|
13719
|
+
return new Promise((resolve2) => {
|
|
12927
13720
|
let timer;
|
|
12928
13721
|
const wrappedResolver = (decision) => {
|
|
12929
13722
|
if (timer) clearTimeout(timer);
|
|
12930
13723
|
if (decision.always) {
|
|
12931
13724
|
this.taskWideCache.set(req.toolName, decision.approved);
|
|
12932
13725
|
}
|
|
12933
|
-
|
|
13726
|
+
resolve2(decision);
|
|
12934
13727
|
};
|
|
12935
13728
|
this.pendingUserDecisions.set(req.id, wrappedResolver);
|
|
12936
13729
|
if (this.approvalTimeoutMs > 0 && Number.isFinite(this.approvalTimeoutMs)) {
|
|
12937
13730
|
timer = setTimeout(() => {
|
|
12938
13731
|
if (this.pendingUserDecisions.delete(req.id)) {
|
|
12939
|
-
|
|
13732
|
+
resolve2({
|
|
12940
13733
|
requestId: req.id,
|
|
12941
13734
|
approved: false,
|
|
12942
13735
|
decidedBy: "USER",
|
|
@@ -13004,6 +13797,23 @@ var noopTelemetry = {
|
|
|
13004
13797
|
}
|
|
13005
13798
|
};
|
|
13006
13799
|
|
|
13800
|
+
// src/core/router/feedback-prior.ts
|
|
13801
|
+
var MAX_FEEDBACK_ADJUSTMENT = 0.05;
|
|
13802
|
+
var PRIOR_STRENGTH = 10;
|
|
13803
|
+
function feedbackAdjustment(counts) {
|
|
13804
|
+
const good = Math.max(0, Math.floor(counts.good));
|
|
13805
|
+
const bad = Math.max(0, Math.floor(counts.bad));
|
|
13806
|
+
const n = good + bad;
|
|
13807
|
+
if (n === 0) return 0;
|
|
13808
|
+
const half = PRIOR_STRENGTH / 2;
|
|
13809
|
+
const shrunk = (good + half) / (n + PRIOR_STRENGTH);
|
|
13810
|
+
return (shrunk - 0.5) * 2 * MAX_FEEDBACK_ADJUSTMENT;
|
|
13811
|
+
}
|
|
13812
|
+
function applyFeedback(score01, counts) {
|
|
13813
|
+
const adjusted = score01 + feedbackAdjustment(counts);
|
|
13814
|
+
return Math.max(0, Math.min(1, adjusted));
|
|
13815
|
+
}
|
|
13816
|
+
|
|
13007
13817
|
// src/core/router/task-analyzer.ts
|
|
13008
13818
|
init_pricing();
|
|
13009
13819
|
var CODE_SIGNALS = [
|
|
@@ -13079,6 +13889,8 @@ var TASK_TYPE_TAGS = {
|
|
|
13079
13889
|
};
|
|
13080
13890
|
var TaskAnalyzer = class {
|
|
13081
13891
|
tracker;
|
|
13892
|
+
/** Per-model thumbs counts, when the host collects them. */
|
|
13893
|
+
feedback;
|
|
13082
13894
|
bias;
|
|
13083
13895
|
lastProfile = null;
|
|
13084
13896
|
lastSelectedModels = /* @__PURE__ */ new Map();
|
|
@@ -13086,6 +13898,14 @@ var TaskAnalyzer = class {
|
|
|
13086
13898
|
this.tracker = tracker;
|
|
13087
13899
|
this.bias = bias;
|
|
13088
13900
|
}
|
|
13901
|
+
/**
|
|
13902
|
+
* Supply per-model rating counts. Optional by design: a host that collects no
|
|
13903
|
+
* feedback (the CLI, a fresh install) scores exactly as it did before, since
|
|
13904
|
+
* feedbackAdjustment returns 0 for an empty record.
|
|
13905
|
+
*/
|
|
13906
|
+
setFeedbackSource(source) {
|
|
13907
|
+
this.feedback = source;
|
|
13908
|
+
}
|
|
13089
13909
|
setTracker(tracker) {
|
|
13090
13910
|
this.tracker = tracker;
|
|
13091
13911
|
}
|
|
@@ -13169,7 +13989,10 @@ var TaskAnalyzer = class {
|
|
|
13169
13989
|
const perf = this.tracker?.performanceScore(model.id, profile.type) ?? 0.5;
|
|
13170
13990
|
const costEff = this.costEfficiency(model, profile.complexity);
|
|
13171
13991
|
const match = this.taskMatchScore(model, profile);
|
|
13172
|
-
const
|
|
13992
|
+
const rated = this.feedback?.(model.id);
|
|
13993
|
+
const publicScore = benchmarkScore01(model, profile.type);
|
|
13994
|
+
const adjusted = rated ? applyFeedback(publicScore, rated) : publicScore;
|
|
13995
|
+
const benchmark = 0.3 + 0.7 * adjusted;
|
|
13173
13996
|
switch (this.bias) {
|
|
13174
13997
|
case "quality":
|
|
13175
13998
|
return perf * match * benchmark ** 2 * (0.85 + 0.15 * costEff);
|
|
@@ -13574,10 +14397,10 @@ var DynamicTool = class extends BaseTool {
|
|
|
13574
14397
|
`;
|
|
13575
14398
|
const script = await isolate.compileScript(bootstrap);
|
|
13576
14399
|
const runPromise = script.run(context, { timeout: timeoutMs, promise: true }).then((v) => String(v ?? ""));
|
|
13577
|
-
const timeoutPromise = new Promise((
|
|
14400
|
+
const timeoutPromise = new Promise((resolve2) => {
|
|
13578
14401
|
const t = setTimeout(() => {
|
|
13579
14402
|
dispose();
|
|
13580
|
-
|
|
14403
|
+
resolve2(`Dynamic tool "${this.name}" timed out after ${timeoutMs}ms and was terminated.`);
|
|
13581
14404
|
}, timeoutMs + 500);
|
|
13582
14405
|
t.unref?.();
|
|
13583
14406
|
});
|
|
@@ -13595,7 +14418,7 @@ var DynamicTool = class extends BaseTool {
|
|
|
13595
14418
|
/** Spawn the worker, service its callTool/fetch bridge, enforce the kill timeout. */
|
|
13596
14419
|
runInWorker(input, callTool) {
|
|
13597
14420
|
const timeoutMs = Math.max(200, Number(process.env["CASCADE_DYNAMIC_TOOL_TIMEOUT_MS"]) || DYNAMIC_TOOL_TIMEOUT_MS);
|
|
13598
|
-
return new Promise((
|
|
14421
|
+
return new Promise((resolve2) => {
|
|
13599
14422
|
let settled = false;
|
|
13600
14423
|
const worker = new Worker(HARNESS_SRC, {
|
|
13601
14424
|
eval: true,
|
|
@@ -13607,7 +14430,7 @@ var DynamicTool = class extends BaseTool {
|
|
|
13607
14430
|
settled = true;
|
|
13608
14431
|
clearTimeout(timer);
|
|
13609
14432
|
void worker.terminate();
|
|
13610
|
-
|
|
14433
|
+
resolve2(value);
|
|
13611
14434
|
};
|
|
13612
14435
|
const timer = setTimeout(
|
|
13613
14436
|
() => finish(`Dynamic tool "${this.name}" timed out after ${timeoutMs}ms and was terminated.`),
|
|
@@ -15114,6 +15937,7 @@ ${prompt}`;
|
|
|
15114
15937
|
var Cascade = class _Cascade extends EventEmitter {
|
|
15115
15938
|
router;
|
|
15116
15939
|
toolRegistry;
|
|
15940
|
+
multimodal;
|
|
15117
15941
|
mcpClient;
|
|
15118
15942
|
codeIndex;
|
|
15119
15943
|
config;
|
|
@@ -15152,6 +15976,10 @@ var Cascade = class _Cascade extends EventEmitter {
|
|
|
15152
15976
|
}
|
|
15153
15977
|
});
|
|
15154
15978
|
this.toolRegistry = new ToolRegistry(this.config.tools, workspacePath);
|
|
15979
|
+
this.multimodal = new MultimodalRegistry(
|
|
15980
|
+
(this.config.providers ?? []).map((p) => p.type)
|
|
15981
|
+
);
|
|
15982
|
+
this.registerMediaTools(workspacePath);
|
|
15155
15983
|
this.telemetry = config.telemetry?.enabled ? new Telemetry(config.telemetry, config.telemetry.distinctId ?? "anonymous") : noopTelemetry;
|
|
15156
15984
|
this.worldStateDB = new WorldStateDB(this.workspacePath, this.config.workspace?.debugWorldState ?? false);
|
|
15157
15985
|
this.router.setWorldStateDB(this.worldStateDB);
|
|
@@ -15205,14 +16033,14 @@ var Cascade = class _Cascade extends EventEmitter {
|
|
|
15205
16033
|
if (this.listenerCount("mcp:approval-required") === 0) {
|
|
15206
16034
|
return false;
|
|
15207
16035
|
}
|
|
15208
|
-
return await new Promise((
|
|
15209
|
-
this.pendingMcpApprovals.set(server.name,
|
|
16036
|
+
return await new Promise((resolve2) => {
|
|
16037
|
+
this.pendingMcpApprovals.set(server.name, resolve2);
|
|
15210
16038
|
const timeout = setTimeout(() => {
|
|
15211
|
-
if (this.pendingMcpApprovals.delete(server.name))
|
|
16039
|
+
if (this.pendingMcpApprovals.delete(server.name)) resolve2(false);
|
|
15212
16040
|
}, 3e4);
|
|
15213
16041
|
const wrap = (approved) => {
|
|
15214
16042
|
clearTimeout(timeout);
|
|
15215
|
-
|
|
16043
|
+
resolve2(approved);
|
|
15216
16044
|
};
|
|
15217
16045
|
this.pendingMcpApprovals.set(server.name, wrap);
|
|
15218
16046
|
this.emit("mcp:approval-required", { server });
|
|
@@ -15286,17 +16114,17 @@ var Cascade = class _Cascade extends EventEmitter {
|
|
|
15286
16114
|
}
|
|
15287
16115
|
const t2Count = plan.sections.length;
|
|
15288
16116
|
const t3Count = plan.sections.reduce((sum, s) => sum + (s.t3Subtasks?.length ?? 0), 0);
|
|
15289
|
-
return await new Promise((
|
|
16117
|
+
return await new Promise((resolve2) => {
|
|
15290
16118
|
const timeout = setTimeout(() => {
|
|
15291
16119
|
if (this.pendingPlanApproval) {
|
|
15292
16120
|
this.pendingPlanApproval = void 0;
|
|
15293
|
-
|
|
16121
|
+
resolve2({ approved: true });
|
|
15294
16122
|
}
|
|
15295
16123
|
}, 12e4);
|
|
15296
16124
|
this.pendingPlanApproval = (decision) => {
|
|
15297
16125
|
clearTimeout(timeout);
|
|
15298
16126
|
this.pendingPlanApproval = void 0;
|
|
15299
|
-
|
|
16127
|
+
resolve2(decision);
|
|
15300
16128
|
};
|
|
15301
16129
|
this.emit("plan:approval-required", {
|
|
15302
16130
|
taskId,
|
|
@@ -15329,17 +16157,17 @@ var Cascade = class _Cascade extends EventEmitter {
|
|
|
15329
16157
|
}
|
|
15330
16158
|
async requestContextApproval(info) {
|
|
15331
16159
|
if (this.listenerCount("context:approval-required") === 0) return true;
|
|
15332
|
-
return await new Promise((
|
|
16160
|
+
return await new Promise((resolve2) => {
|
|
15333
16161
|
const timeout = setTimeout(() => {
|
|
15334
16162
|
if (this.pendingContextApproval) {
|
|
15335
16163
|
this.pendingContextApproval = void 0;
|
|
15336
|
-
|
|
16164
|
+
resolve2(true);
|
|
15337
16165
|
}
|
|
15338
16166
|
}, 12e4);
|
|
15339
16167
|
this.pendingContextApproval = (approved) => {
|
|
15340
16168
|
clearTimeout(timeout);
|
|
15341
16169
|
this.pendingContextApproval = void 0;
|
|
15342
|
-
|
|
16170
|
+
resolve2(approved);
|
|
15343
16171
|
};
|
|
15344
16172
|
this.emit("context:approval-required", info);
|
|
15345
16173
|
});
|
|
@@ -15419,6 +16247,55 @@ ${input}` }],
|
|
|
15419
16247
|
if (this.store) t1.setStore(this.store);
|
|
15420
16248
|
return t1.previewPlan(prompt);
|
|
15421
16249
|
}
|
|
16250
|
+
/**
|
|
16251
|
+
* Register image / speech / transcription tools for the modalities this
|
|
16252
|
+
* config can actually serve.
|
|
16253
|
+
*
|
|
16254
|
+
* The sink writes bytes into the workspace, because that is what a desktop or
|
|
16255
|
+
* CLI run wants and it keeps a generated asset indistinguishable from any
|
|
16256
|
+
* other file the run produced. A host with different storage (the cloud
|
|
16257
|
+
* server stores a file row) constructs the tools itself via buildMediaTools
|
|
16258
|
+
* with its own sink.
|
|
16259
|
+
*/
|
|
16260
|
+
registerMediaTools(workspacePath) {
|
|
16261
|
+
const allowlist = this.config.tools?.enabledTools;
|
|
16262
|
+
const permitted = (name) => !allowlist || allowlist.includes(name);
|
|
16263
|
+
const tools = buildMediaTools(
|
|
16264
|
+
{
|
|
16265
|
+
registry: this.multimodal,
|
|
16266
|
+
lookupProvider: (provider) => (this.config.providers ?? []).find((p) => p.type === provider),
|
|
16267
|
+
sink: async (asset) => {
|
|
16268
|
+
const fsp = await import('fs/promises');
|
|
16269
|
+
const nodePath = await import('path');
|
|
16270
|
+
const dir = nodePath.join(workspacePath, "generated");
|
|
16271
|
+
await fsp.mkdir(dir, { recursive: true });
|
|
16272
|
+
const dest = nodePath.join(dir, asset.filename);
|
|
16273
|
+
await fsp.writeFile(dest, asset.data);
|
|
16274
|
+
return nodePath.relative(workspacePath, dest);
|
|
16275
|
+
}
|
|
16276
|
+
},
|
|
16277
|
+
async (p) => {
|
|
16278
|
+
const fsp = await import('fs/promises');
|
|
16279
|
+
return fsp.readFile(p);
|
|
16280
|
+
}
|
|
16281
|
+
);
|
|
16282
|
+
for (const tool of tools) {
|
|
16283
|
+
if (!permitted(tool.name)) continue;
|
|
16284
|
+
tool.setWorkspaceRoot(workspacePath);
|
|
16285
|
+
this.toolRegistry.register(tool);
|
|
16286
|
+
}
|
|
16287
|
+
}
|
|
16288
|
+
/**
|
|
16289
|
+
* Supply per-model thumbs-up/down counts so Auto routing can weigh how models
|
|
16290
|
+
* have actually performed for this user, on top of public benchmarks.
|
|
16291
|
+
*
|
|
16292
|
+
* Optional everywhere: with no source, or an empty record, scoring is
|
|
16293
|
+
* byte-identical to before. The adjustment is capped and sample-size shrunk —
|
|
16294
|
+
* see core/router/feedback-prior.ts.
|
|
16295
|
+
*/
|
|
16296
|
+
setFeedbackSource(source) {
|
|
16297
|
+
this.router.setFeedbackSource(source);
|
|
16298
|
+
}
|
|
15422
16299
|
/** True when a task stopped at the budget cap and can be resumed via /continue. */
|
|
15423
16300
|
hasResumableRun() {
|
|
15424
16301
|
return this.lastInterruptedRun != null;
|
|
@@ -15908,6 +16785,7 @@ ${prompt}` : prompt;
|
|
|
15908
16785
|
if (toolCreator) toolCreator.setPermissionEscalator(escalator);
|
|
15909
16786
|
const rootPrompt = buildContextualPrompt(options.prompt, options.conversationHistory);
|
|
15910
16787
|
let finalOutput = "";
|
|
16788
|
+
const runBreaker = new RunBreaker(this.config.budget?.failureThreshold);
|
|
15911
16789
|
let t2Results = [];
|
|
15912
16790
|
let runError = null;
|
|
15913
16791
|
let identityPrompt = "";
|
|
@@ -15976,6 +16854,7 @@ ${prompt}` : prompt;
|
|
|
15976
16854
|
this.emit("tier:status", { tierId: "t3-root", status: "COMPLETED", role: "T3" });
|
|
15977
16855
|
} else if (complexity === "Moderate") {
|
|
15978
16856
|
const t2 = new T2Manager(this.router, this.toolRegistry, "root");
|
|
16857
|
+
t2.setRunBreaker(runBreaker);
|
|
15979
16858
|
t2.setPresenter(true);
|
|
15980
16859
|
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.");
|
|
15981
16860
|
if (identityPrompt) {
|
|
@@ -16036,6 +16915,7 @@ _(incomplete: ${reasons.join("; ")})_` : partial;
|
|
|
16036
16915
|
}
|
|
16037
16916
|
} else {
|
|
16038
16917
|
const t1 = new T1Administrator(this.router, this.toolRegistry, this.config);
|
|
16918
|
+
t1.setRunBreaker(runBreaker);
|
|
16039
16919
|
t1.setPresenter(true);
|
|
16040
16920
|
t1.setHierarchyContext("You are the top-level Administrator. You are responsible for the overall plan and supervising multiple T2 Managers.");
|
|
16041
16921
|
if (identityPrompt) {
|
|
@@ -16060,6 +16940,20 @@ _(incomplete: ${reasons.join("; ")})_` : partial;
|
|
|
16060
16940
|
finalOutput = result.output;
|
|
16061
16941
|
t2Results = result.t2Results;
|
|
16062
16942
|
}
|
|
16943
|
+
const trip = runBreaker.reason();
|
|
16944
|
+
if (trip) {
|
|
16945
|
+
finalOutput = [
|
|
16946
|
+
`\u26A0 **Run stopped early \u2014 ${trip.modelId} was failing every request.**`,
|
|
16947
|
+
"",
|
|
16948
|
+
trip.message,
|
|
16949
|
+
"",
|
|
16950
|
+
`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.`,
|
|
16951
|
+
finalOutput ? `
|
|
16952
|
+
---
|
|
16953
|
+
|
|
16954
|
+
${finalOutput}` : ""
|
|
16955
|
+
].join("\n").trimEnd();
|
|
16956
|
+
}
|
|
16063
16957
|
} catch (err) {
|
|
16064
16958
|
if (err instanceof CascadeCancelledError || err instanceof Error && err.name === "AbortError" || options.signal?.aborted) {
|
|
16065
16959
|
this.emit("run:cancelled", {
|
|
@@ -16594,8 +17488,8 @@ var CloudClient = class _CloudClient {
|
|
|
16594
17488
|
}
|
|
16595
17489
|
};
|
|
16596
17490
|
function sleep(ms, signal) {
|
|
16597
|
-
return new Promise((
|
|
16598
|
-
const t = setTimeout(
|
|
17491
|
+
return new Promise((resolve2, reject) => {
|
|
17492
|
+
const t = setTimeout(resolve2, ms);
|
|
16599
17493
|
signal?.addEventListener("abort", () => {
|
|
16600
17494
|
clearTimeout(t);
|
|
16601
17495
|
reject(new Error("aborted"));
|
|
@@ -18387,8 +19281,8 @@ function Repl({ config, workspacePath, themeName, initialPrompt, identityName, a
|
|
|
18387
19281
|
onExit: () => exit(),
|
|
18388
19282
|
onThemeChange: (name) => setTheme(getTheme(name)),
|
|
18389
19283
|
onExport: async (fmt) => {
|
|
18390
|
-
const
|
|
18391
|
-
const exportPath = path32.join(workspacePath, `cascade-export-${
|
|
19284
|
+
const stamp2 = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
19285
|
+
const exportPath = path32.join(workspacePath, `cascade-export-${stamp2}.${fmt === "json" ? "json" : "md"}`);
|
|
18392
19286
|
if (fmt === "json") {
|
|
18393
19287
|
await fs10.writeFile(exportPath, JSON.stringify({ sessionId: sessionIdRef.current, messages: state.messages }, null, 2), "utf-8");
|
|
18394
19288
|
} else {
|
|
@@ -18820,8 +19714,8 @@ ${lastUser.content}`;
|
|
|
18820
19714
|
conversationHistory: toConversationHistory(state.messages),
|
|
18821
19715
|
approvalCallback: async (req) => {
|
|
18822
19716
|
dispatch({ type: "SET_APPROVAL", request: req });
|
|
18823
|
-
return new Promise((
|
|
18824
|
-
approvalResolverRef.current =
|
|
19717
|
+
return new Promise((resolve2) => {
|
|
19718
|
+
approvalResolverRef.current = resolve2;
|
|
18825
19719
|
});
|
|
18826
19720
|
}
|
|
18827
19721
|
});
|
|
@@ -20087,13 +20981,13 @@ function SetupWizard({ workspacePath, onComplete }) {
|
|
|
20087
20981
|
}
|
|
20088
20982
|
async function runSetupWizard(workspacePath) {
|
|
20089
20983
|
printTelemetryBanner();
|
|
20090
|
-
return new Promise((
|
|
20984
|
+
return new Promise((resolve2) => {
|
|
20091
20985
|
const { unmount } = render(
|
|
20092
20986
|
React2.createElement(SetupWizard, {
|
|
20093
20987
|
workspacePath,
|
|
20094
20988
|
onComplete: (config) => {
|
|
20095
20989
|
unmount();
|
|
20096
|
-
|
|
20990
|
+
resolve2(config);
|
|
20097
20991
|
}
|
|
20098
20992
|
}),
|
|
20099
20993
|
{ exitOnCtrlC: true }
|
|
@@ -20561,7 +21455,7 @@ function notSignedIn() {
|
|
|
20561
21455
|
process.exitCode = 1;
|
|
20562
21456
|
}
|
|
20563
21457
|
function promptHidden(question) {
|
|
20564
|
-
return new Promise((
|
|
21458
|
+
return new Promise((resolve2) => {
|
|
20565
21459
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout, terminal: true });
|
|
20566
21460
|
let masked = false;
|
|
20567
21461
|
rl._writeToOutput = (s) => {
|
|
@@ -20570,7 +21464,7 @@ function promptHidden(question) {
|
|
|
20570
21464
|
rl.question(question, (answer) => {
|
|
20571
21465
|
rl.close();
|
|
20572
21466
|
process.stdout.write("\n");
|
|
20573
|
-
|
|
21467
|
+
resolve2(answer.trim());
|
|
20574
21468
|
});
|
|
20575
21469
|
masked = true;
|
|
20576
21470
|
});
|
|
@@ -21497,12 +22391,12 @@ var DashboardServer = class {
|
|
|
21497
22391
|
console.warn("\u26A0 Dashboard auth is DISABLED while bound to a non-loopback interface \u2014 this allows unauthenticated remote task execution.");
|
|
21498
22392
|
}
|
|
21499
22393
|
}
|
|
21500
|
-
await new Promise((
|
|
22394
|
+
await new Promise((resolve2, reject) => {
|
|
21501
22395
|
const onError = (err) => reject(err);
|
|
21502
22396
|
this.httpServer.once("error", onError);
|
|
21503
22397
|
this.httpServer.listen(this.port, this.host, () => {
|
|
21504
22398
|
this.httpServer.off("error", onError);
|
|
21505
|
-
|
|
22399
|
+
resolve2();
|
|
21506
22400
|
});
|
|
21507
22401
|
});
|
|
21508
22402
|
try {
|
|
@@ -21526,8 +22420,8 @@ var DashboardServer = class {
|
|
|
21526
22420
|
} catch {
|
|
21527
22421
|
}
|
|
21528
22422
|
this.globalStore = null;
|
|
21529
|
-
await new Promise((
|
|
21530
|
-
this.httpServer.close((err) => err ? reject(err) :
|
|
22423
|
+
await new Promise((resolve2, reject) => {
|
|
22424
|
+
this.httpServer.close((err) => err ? reject(err) : resolve2());
|
|
21531
22425
|
});
|
|
21532
22426
|
}
|
|
21533
22427
|
getSocket() {
|
|
@@ -21724,8 +22618,8 @@ var DashboardServer = class {
|
|
|
21724
22618
|
* timeout denies it.
|
|
21725
22619
|
*/
|
|
21726
22620
|
makeApprovalCallback(sessionId) {
|
|
21727
|
-
return (request) => new Promise((
|
|
21728
|
-
this.pendingApprovals.set(request.id, { resolve, sessionId });
|
|
22621
|
+
return (request) => new Promise((resolve2) => {
|
|
22622
|
+
this.pendingApprovals.set(request.id, { resolve: resolve2, sessionId });
|
|
21729
22623
|
});
|
|
21730
22624
|
}
|
|
21731
22625
|
/**
|