cascade-ai 0.49.0 → 0.50.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 +239 -3
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +239 -3
- package/dist/cli.js.map +1 -1
- package/dist/desktop-core.cjs +239 -3
- package/dist/index.cjs +239 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +72 -0
- package/dist/index.d.ts +72 -0
- package/dist/index.js +239 -3
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.cts
CHANGED
|
@@ -1858,6 +1858,12 @@ declare abstract class BaseTier extends EventEmitter {
|
|
|
1858
1858
|
getStatus(): TierStatus;
|
|
1859
1859
|
protected setStatus(status: TierStatus, output?: string): void;
|
|
1860
1860
|
/** Record the model serving this tier; future status events carry it. */
|
|
1861
|
+
/**
|
|
1862
|
+
* The model actually serving this tier ("provider:id"), once one has been
|
|
1863
|
+
* chosen. The run breaker reads it to attribute a failure to a model rather
|
|
1864
|
+
* than to the subtask that happened to hit it.
|
|
1865
|
+
*/
|
|
1866
|
+
getServingModel(): string | undefined;
|
|
1861
1867
|
protected setServingModel(model: string | undefined): void;
|
|
1862
1868
|
protected setLabel(label: string): void;
|
|
1863
1869
|
setSystemPromptOverride(prompt: string): void;
|
|
@@ -1873,6 +1879,61 @@ declare abstract class BaseTier extends EventEmitter {
|
|
|
1873
1879
|
protected throwIfCancelled(): void;
|
|
1874
1880
|
}
|
|
1875
1881
|
|
|
1882
|
+
/**
|
|
1883
|
+
* What kind of failure this is, and — crucially — whether retrying a DIFFERENT
|
|
1884
|
+
* subtask against the same model could plausibly succeed.
|
|
1885
|
+
*/
|
|
1886
|
+
type ProviderErrorKind = 'rate_limit' | 'quota_exhausted' | 'auth' | 'model_unavailable' | 'context_length' | 'content_filter' | 'network' | 'unknown';
|
|
1887
|
+
interface ClassifiedError {
|
|
1888
|
+
kind: ProviderErrorKind;
|
|
1889
|
+
/** True when every other worker on this model is expected to fail the same way. */
|
|
1890
|
+
systemic: boolean;
|
|
1891
|
+
/** HTTP status, when the provider gave us one. */
|
|
1892
|
+
status?: number;
|
|
1893
|
+
/** The provider's own message, trimmed — never invented or paraphrased. */
|
|
1894
|
+
raw: string;
|
|
1895
|
+
}
|
|
1896
|
+
|
|
1897
|
+
interface BreakerTrip {
|
|
1898
|
+
modelId: string;
|
|
1899
|
+
kind: ClassifiedError['kind'];
|
|
1900
|
+
/** How many systemic failures on this model were seen before tripping. */
|
|
1901
|
+
failures: number;
|
|
1902
|
+
/** User-facing explanation, including the provider's own words. */
|
|
1903
|
+
message: string;
|
|
1904
|
+
}
|
|
1905
|
+
declare class RunBreaker {
|
|
1906
|
+
private readonly threshold;
|
|
1907
|
+
/** modelId → consecutive systemic failures of the SAME kind. */
|
|
1908
|
+
private counts;
|
|
1909
|
+
private trip;
|
|
1910
|
+
constructor(threshold?: number);
|
|
1911
|
+
/**
|
|
1912
|
+
* Record a failed model call. Returns the trip if this failure is the one
|
|
1913
|
+
* that opened the breaker, otherwise null.
|
|
1914
|
+
*
|
|
1915
|
+
* A non-systemic failure RESETS the model's counter: it is evidence the model
|
|
1916
|
+
* is reachable and answering, which is exactly what a systemic streak claims
|
|
1917
|
+
* it is not.
|
|
1918
|
+
*/
|
|
1919
|
+
record(err: unknown, modelId: string | undefined): BreakerTrip | null;
|
|
1920
|
+
/** True once the run should stop launching work that would fail identically. */
|
|
1921
|
+
isOpen(): boolean;
|
|
1922
|
+
/** Why the breaker opened, or null while it is closed. */
|
|
1923
|
+
reason(): BreakerTrip | null;
|
|
1924
|
+
/**
|
|
1925
|
+
* One line for a worker/section that never ran because the breaker was
|
|
1926
|
+
* already open. Says the work was skipped deliberately — not that it failed
|
|
1927
|
+
* on its own merits, which is what an empty FAILED status would imply.
|
|
1928
|
+
*/
|
|
1929
|
+
skipMessage(): string;
|
|
1930
|
+
/** Test/diagnostic view of the per-model streaks. */
|
|
1931
|
+
streaks(): Record<string, {
|
|
1932
|
+
kind: string;
|
|
1933
|
+
n: number;
|
|
1934
|
+
}>;
|
|
1935
|
+
}
|
|
1936
|
+
|
|
1876
1937
|
type T2Evaluator = (req: PermissionRequest) => Promise<PermissionDecision | null>;
|
|
1877
1938
|
type T1Evaluator = (req: PermissionRequest) => Promise<PermissionDecision | null>;
|
|
1878
1939
|
/**
|
|
@@ -2020,6 +2081,7 @@ declare class T1Administrator extends BaseTier {
|
|
|
2020
2081
|
private t2Managers;
|
|
2021
2082
|
private escalations;
|
|
2022
2083
|
private store?;
|
|
2084
|
+
private runBreaker?;
|
|
2023
2085
|
private t2PeerBus;
|
|
2024
2086
|
private permissionEscalator?;
|
|
2025
2087
|
private toolCreator?;
|
|
@@ -2029,6 +2091,8 @@ declare class T1Administrator extends BaseTier {
|
|
|
2029
2091
|
private peerMessageSessionId;
|
|
2030
2092
|
private planApprovalCallback?;
|
|
2031
2093
|
constructor(router: CascadeRouter, toolRegistry: ToolRegistry, config: CascadeConfig);
|
|
2094
|
+
/** Share the run-wide circuit breaker with every section manager. */
|
|
2095
|
+
setRunBreaker(breaker: RunBreaker): void;
|
|
2032
2096
|
setStore(store: MemoryStore): void;
|
|
2033
2097
|
/**
|
|
2034
2098
|
* Inject the shared PermissionEscalator for this task run.
|
|
@@ -2404,6 +2468,7 @@ declare class T2Manager extends BaseTier {
|
|
|
2404
2468
|
private escalations;
|
|
2405
2469
|
private peerSyncBuffer;
|
|
2406
2470
|
private store?;
|
|
2471
|
+
private runBreaker?;
|
|
2407
2472
|
private t3PeerBus;
|
|
2408
2473
|
private t2PeerBus?;
|
|
2409
2474
|
private permissionEscalator?;
|
|
@@ -2415,6 +2480,13 @@ declare class T2Manager extends BaseTier {
|
|
|
2415
2480
|
setPeerBus(bus: PeerBus): void;
|
|
2416
2481
|
setPeerMessageCallback(cb: (event: PeerMessageEvent) => void, sessionId: string): void;
|
|
2417
2482
|
constructor(router: CascadeRouter, toolRegistry: ToolRegistry, parentId: string);
|
|
2483
|
+
/**
|
|
2484
|
+
* Share the run-wide circuit breaker. Every T2 in a run gets the SAME
|
|
2485
|
+
* instance: three sections each failing once against a dead model is the
|
|
2486
|
+
* same fact as one section failing three times, and a per-section breaker
|
|
2487
|
+
* would never see it.
|
|
2488
|
+
*/
|
|
2489
|
+
setRunBreaker(breaker: RunBreaker): void;
|
|
2418
2490
|
setStore(store: MemoryStore): void;
|
|
2419
2491
|
/**
|
|
2420
2492
|
* Inject the shared PermissionEscalator for this task run.
|
package/dist/index.d.ts
CHANGED
|
@@ -1858,6 +1858,12 @@ declare abstract class BaseTier extends EventEmitter {
|
|
|
1858
1858
|
getStatus(): TierStatus;
|
|
1859
1859
|
protected setStatus(status: TierStatus, output?: string): void;
|
|
1860
1860
|
/** Record the model serving this tier; future status events carry it. */
|
|
1861
|
+
/**
|
|
1862
|
+
* The model actually serving this tier ("provider:id"), once one has been
|
|
1863
|
+
* chosen. The run breaker reads it to attribute a failure to a model rather
|
|
1864
|
+
* than to the subtask that happened to hit it.
|
|
1865
|
+
*/
|
|
1866
|
+
getServingModel(): string | undefined;
|
|
1861
1867
|
protected setServingModel(model: string | undefined): void;
|
|
1862
1868
|
protected setLabel(label: string): void;
|
|
1863
1869
|
setSystemPromptOverride(prompt: string): void;
|
|
@@ -1873,6 +1879,61 @@ declare abstract class BaseTier extends EventEmitter {
|
|
|
1873
1879
|
protected throwIfCancelled(): void;
|
|
1874
1880
|
}
|
|
1875
1881
|
|
|
1882
|
+
/**
|
|
1883
|
+
* What kind of failure this is, and — crucially — whether retrying a DIFFERENT
|
|
1884
|
+
* subtask against the same model could plausibly succeed.
|
|
1885
|
+
*/
|
|
1886
|
+
type ProviderErrorKind = 'rate_limit' | 'quota_exhausted' | 'auth' | 'model_unavailable' | 'context_length' | 'content_filter' | 'network' | 'unknown';
|
|
1887
|
+
interface ClassifiedError {
|
|
1888
|
+
kind: ProviderErrorKind;
|
|
1889
|
+
/** True when every other worker on this model is expected to fail the same way. */
|
|
1890
|
+
systemic: boolean;
|
|
1891
|
+
/** HTTP status, when the provider gave us one. */
|
|
1892
|
+
status?: number;
|
|
1893
|
+
/** The provider's own message, trimmed — never invented or paraphrased. */
|
|
1894
|
+
raw: string;
|
|
1895
|
+
}
|
|
1896
|
+
|
|
1897
|
+
interface BreakerTrip {
|
|
1898
|
+
modelId: string;
|
|
1899
|
+
kind: ClassifiedError['kind'];
|
|
1900
|
+
/** How many systemic failures on this model were seen before tripping. */
|
|
1901
|
+
failures: number;
|
|
1902
|
+
/** User-facing explanation, including the provider's own words. */
|
|
1903
|
+
message: string;
|
|
1904
|
+
}
|
|
1905
|
+
declare class RunBreaker {
|
|
1906
|
+
private readonly threshold;
|
|
1907
|
+
/** modelId → consecutive systemic failures of the SAME kind. */
|
|
1908
|
+
private counts;
|
|
1909
|
+
private trip;
|
|
1910
|
+
constructor(threshold?: number);
|
|
1911
|
+
/**
|
|
1912
|
+
* Record a failed model call. Returns the trip if this failure is the one
|
|
1913
|
+
* that opened the breaker, otherwise null.
|
|
1914
|
+
*
|
|
1915
|
+
* A non-systemic failure RESETS the model's counter: it is evidence the model
|
|
1916
|
+
* is reachable and answering, which is exactly what a systemic streak claims
|
|
1917
|
+
* it is not.
|
|
1918
|
+
*/
|
|
1919
|
+
record(err: unknown, modelId: string | undefined): BreakerTrip | null;
|
|
1920
|
+
/** True once the run should stop launching work that would fail identically. */
|
|
1921
|
+
isOpen(): boolean;
|
|
1922
|
+
/** Why the breaker opened, or null while it is closed. */
|
|
1923
|
+
reason(): BreakerTrip | null;
|
|
1924
|
+
/**
|
|
1925
|
+
* One line for a worker/section that never ran because the breaker was
|
|
1926
|
+
* already open. Says the work was skipped deliberately — not that it failed
|
|
1927
|
+
* on its own merits, which is what an empty FAILED status would imply.
|
|
1928
|
+
*/
|
|
1929
|
+
skipMessage(): string;
|
|
1930
|
+
/** Test/diagnostic view of the per-model streaks. */
|
|
1931
|
+
streaks(): Record<string, {
|
|
1932
|
+
kind: string;
|
|
1933
|
+
n: number;
|
|
1934
|
+
}>;
|
|
1935
|
+
}
|
|
1936
|
+
|
|
1876
1937
|
type T2Evaluator = (req: PermissionRequest) => Promise<PermissionDecision | null>;
|
|
1877
1938
|
type T1Evaluator = (req: PermissionRequest) => Promise<PermissionDecision | null>;
|
|
1878
1939
|
/**
|
|
@@ -2020,6 +2081,7 @@ declare class T1Administrator extends BaseTier {
|
|
|
2020
2081
|
private t2Managers;
|
|
2021
2082
|
private escalations;
|
|
2022
2083
|
private store?;
|
|
2084
|
+
private runBreaker?;
|
|
2023
2085
|
private t2PeerBus;
|
|
2024
2086
|
private permissionEscalator?;
|
|
2025
2087
|
private toolCreator?;
|
|
@@ -2029,6 +2091,8 @@ declare class T1Administrator extends BaseTier {
|
|
|
2029
2091
|
private peerMessageSessionId;
|
|
2030
2092
|
private planApprovalCallback?;
|
|
2031
2093
|
constructor(router: CascadeRouter, toolRegistry: ToolRegistry, config: CascadeConfig);
|
|
2094
|
+
/** Share the run-wide circuit breaker with every section manager. */
|
|
2095
|
+
setRunBreaker(breaker: RunBreaker): void;
|
|
2032
2096
|
setStore(store: MemoryStore): void;
|
|
2033
2097
|
/**
|
|
2034
2098
|
* Inject the shared PermissionEscalator for this task run.
|
|
@@ -2404,6 +2468,7 @@ declare class T2Manager extends BaseTier {
|
|
|
2404
2468
|
private escalations;
|
|
2405
2469
|
private peerSyncBuffer;
|
|
2406
2470
|
private store?;
|
|
2471
|
+
private runBreaker?;
|
|
2407
2472
|
private t3PeerBus;
|
|
2408
2473
|
private t2PeerBus?;
|
|
2409
2474
|
private permissionEscalator?;
|
|
@@ -2415,6 +2480,13 @@ declare class T2Manager extends BaseTier {
|
|
|
2415
2480
|
setPeerBus(bus: PeerBus): void;
|
|
2416
2481
|
setPeerMessageCallback(cb: (event: PeerMessageEvent) => void, sessionId: string): void;
|
|
2417
2482
|
constructor(router: CascadeRouter, toolRegistry: ToolRegistry, parentId: string);
|
|
2483
|
+
/**
|
|
2484
|
+
* Share the run-wide circuit breaker. Every T2 in a run gets the SAME
|
|
2485
|
+
* instance: three sections each failing once against a dead model is the
|
|
2486
|
+
* same fact as one section failing three times, and a per-section breaker
|
|
2487
|
+
* would never see it.
|
|
2488
|
+
*/
|
|
2489
|
+
setRunBreaker(breaker: RunBreaker): void;
|
|
2418
2490
|
setStore(store: MemoryStore): void;
|
|
2419
2491
|
/**
|
|
2420
2492
|
* Inject the shared PermissionEscalator for this task run.
|
package/dist/index.js
CHANGED
|
@@ -1845,7 +1845,7 @@ function reconcilePrice(model, live) {
|
|
|
1845
1845
|
}
|
|
1846
1846
|
|
|
1847
1847
|
// src/constants.ts
|
|
1848
|
-
var CASCADE_VERSION = "0.
|
|
1848
|
+
var CASCADE_VERSION = "0.50.0";
|
|
1849
1849
|
var CASCADE_CONFIG_DIR = ".cascade";
|
|
1850
1850
|
var CASCADE_MD_FILE = "CASCADE.md";
|
|
1851
1851
|
var CASCADE_IGNORE_FILE = ".cascadeignore";
|
|
@@ -5461,6 +5461,14 @@ var BaseTier = class extends EventEmitter {
|
|
|
5461
5461
|
this.emit("tier:status", event);
|
|
5462
5462
|
}
|
|
5463
5463
|
/** Record the model serving this tier; future status events carry it. */
|
|
5464
|
+
/**
|
|
5465
|
+
* The model actually serving this tier ("provider:id"), once one has been
|
|
5466
|
+
* chosen. The run breaker reads it to attribute a failure to a model rather
|
|
5467
|
+
* than to the subtask that happened to hit it.
|
|
5468
|
+
*/
|
|
5469
|
+
getServingModel() {
|
|
5470
|
+
return this.servingModel;
|
|
5471
|
+
}
|
|
5464
5472
|
setServingModel(model) {
|
|
5465
5473
|
this.servingModel = model || void 0;
|
|
5466
5474
|
}
|
|
@@ -7196,6 +7204,7 @@ var T2Manager = class extends BaseTier {
|
|
|
7196
7204
|
escalations = [];
|
|
7197
7205
|
peerSyncBuffer = [];
|
|
7198
7206
|
store;
|
|
7207
|
+
runBreaker;
|
|
7199
7208
|
t3PeerBus = new PeerBus();
|
|
7200
7209
|
// ← T3↔T3 bus (local to this T2)
|
|
7201
7210
|
t2PeerBus;
|
|
@@ -7226,6 +7235,15 @@ var T2Manager = class extends BaseTier {
|
|
|
7226
7235
|
this.router = router;
|
|
7227
7236
|
this.toolRegistry = toolRegistry;
|
|
7228
7237
|
}
|
|
7238
|
+
/**
|
|
7239
|
+
* Share the run-wide circuit breaker. Every T2 in a run gets the SAME
|
|
7240
|
+
* instance: three sections each failing once against a dead model is the
|
|
7241
|
+
* same fact as one section failing three times, and a per-section breaker
|
|
7242
|
+
* would never see it.
|
|
7243
|
+
*/
|
|
7244
|
+
setRunBreaker(breaker) {
|
|
7245
|
+
this.runBreaker = breaker;
|
|
7246
|
+
}
|
|
7229
7247
|
setStore(store) {
|
|
7230
7248
|
this.store = store;
|
|
7231
7249
|
}
|
|
@@ -7349,7 +7367,8 @@ Guidance (must be followed): ${decision.note}`
|
|
|
7349
7367
|
const overallStatus = this.determineStatus(t3Results);
|
|
7350
7368
|
const isOk = overallStatus === "COMPLETED" || overallStatus === "PARTIAL";
|
|
7351
7369
|
this.setStatus(isOk ? "COMPLETED" : "FAILED", summary);
|
|
7352
|
-
|
|
7370
|
+
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";
|
|
7371
|
+
this.sendStatusUpdate({ progressPct: 100, currentAction: sectionAction, status: "IN_PROGRESS", output: summary });
|
|
7353
7372
|
const result = {
|
|
7354
7373
|
sectionId: assignment.sectionId,
|
|
7355
7374
|
sectionTitle: assignment.sectionTitle,
|
|
@@ -7519,6 +7538,24 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
|
7519
7538
|
const reinforceCfg = this.router.getReinforcementsConfig?.() ?? { enabled: false, maxPerSection: 4 };
|
|
7520
7539
|
let reinforcementsAdded = 0;
|
|
7521
7540
|
while (remaining.size > 0) {
|
|
7541
|
+
if (this.runBreaker?.isOpen()) {
|
|
7542
|
+
const why = this.runBreaker.skipMessage();
|
|
7543
|
+
this.log(`Run breaker open \u2014 skipping ${remaining.size} remaining subtask(s)`);
|
|
7544
|
+
for (const id of remaining) {
|
|
7545
|
+
this.t3PeerBus.publish(this.id, id, why, "FAILED");
|
|
7546
|
+
resultMap.set(id, {
|
|
7547
|
+
subtaskId: id,
|
|
7548
|
+
status: "FAILED",
|
|
7549
|
+
output: why,
|
|
7550
|
+
testResults: { checksRun: [], passed: [], failed: [] },
|
|
7551
|
+
issues: [why],
|
|
7552
|
+
peerSyncsUsed: [],
|
|
7553
|
+
correctionAttempts: 0
|
|
7554
|
+
});
|
|
7555
|
+
}
|
|
7556
|
+
remaining.clear();
|
|
7557
|
+
break;
|
|
7558
|
+
}
|
|
7522
7559
|
const runnableIds = [...remaining].filter((id) => (inDegree.get(id) ?? 0) === 0);
|
|
7523
7560
|
if (runnableIds.length === 0) {
|
|
7524
7561
|
const fallbackId = [...remaining].sort(
|
|
@@ -7621,8 +7658,32 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
|
7621
7658
|
remaining.delete(id);
|
|
7622
7659
|
const r = waveResults[i];
|
|
7623
7660
|
if (r.status === "rejected") {
|
|
7624
|
-
this.log(`T3 worker ${id} failed: ${r.reason instanceof Error ? r.reason.message : String(r.reason)} \u2014 retrying once`);
|
|
7625
7661
|
const assignment = sanitizedAssignments.find((a) => a.subtaskId === id);
|
|
7662
|
+
const failedModel = workerMap.get(id)?.getServingModel();
|
|
7663
|
+
const tripped = this.runBreaker?.record(r.reason, failedModel);
|
|
7664
|
+
if (tripped) {
|
|
7665
|
+
this.log(`Run breaker opened: ${tripped.failures} consecutive ${tripped.kind} failures on ${tripped.modelId} \u2014 stopping`);
|
|
7666
|
+
this.emit("run:breaker", tripped);
|
|
7667
|
+
}
|
|
7668
|
+
if (this.runBreaker?.isOpen()) {
|
|
7669
|
+
const why = this.runBreaker.skipMessage();
|
|
7670
|
+
this.log(`T3 worker ${id} failed and the run breaker is open \u2014 not retrying`);
|
|
7671
|
+
this.t3PeerBus.publish(this.id, id, why, "FAILED");
|
|
7672
|
+
resultMap.set(id, {
|
|
7673
|
+
subtaskId: id,
|
|
7674
|
+
status: "FAILED",
|
|
7675
|
+
output: why,
|
|
7676
|
+
testResults: { checksRun: [], passed: [], failed: [] },
|
|
7677
|
+
issues: [why],
|
|
7678
|
+
peerSyncsUsed: [],
|
|
7679
|
+
correctionAttempts: 0
|
|
7680
|
+
});
|
|
7681
|
+
for (const dependent of adj.get(id) ?? []) {
|
|
7682
|
+
inDegree.set(dependent, Math.max(0, (inDegree.get(dependent) ?? 0) - 1));
|
|
7683
|
+
}
|
|
7684
|
+
continue;
|
|
7685
|
+
}
|
|
7686
|
+
this.log(`T3 worker ${id} failed: ${r.reason instanceof Error ? r.reason.message : String(r.reason)} \u2014 retrying once`);
|
|
7626
7687
|
try {
|
|
7627
7688
|
const retried = await this.retryT3(assignment, taskId);
|
|
7628
7689
|
resultMap.set(id, retried);
|
|
@@ -7954,6 +8015,7 @@ var T1Administrator = class extends BaseTier {
|
|
|
7954
8015
|
t2Managers = /* @__PURE__ */ new Map();
|
|
7955
8016
|
escalations = [];
|
|
7956
8017
|
store;
|
|
8018
|
+
runBreaker;
|
|
7957
8019
|
t2PeerBus = new PeerBus();
|
|
7958
8020
|
permissionEscalator;
|
|
7959
8021
|
toolCreator;
|
|
@@ -7968,6 +8030,10 @@ var T1Administrator = class extends BaseTier {
|
|
|
7968
8030
|
this.toolRegistry = toolRegistry;
|
|
7969
8031
|
this.config = config;
|
|
7970
8032
|
}
|
|
8033
|
+
/** Share the run-wide circuit breaker with every section manager. */
|
|
8034
|
+
setRunBreaker(breaker) {
|
|
8035
|
+
this.runBreaker = breaker;
|
|
8036
|
+
}
|
|
7971
8037
|
setStore(store) {
|
|
7972
8038
|
this.store = store;
|
|
7973
8039
|
}
|
|
@@ -8330,6 +8396,7 @@ SPEC RULES \u2014 each subtask is a self-contained spec slice (workers execute f
|
|
|
8330
8396
|
};
|
|
8331
8397
|
const managers = sections.map((section) => {
|
|
8332
8398
|
const manager = new T2Manager(this.router, this.toolRegistry, this.id);
|
|
8399
|
+
if (this.runBreaker) manager.setRunBreaker(this.runBreaker);
|
|
8333
8400
|
manager.setHierarchyContext(`You are a T2 Manager for the section "${section.sectionTitle}". You are part of a COMPLEX task overseen by T1 Administrator.`);
|
|
8334
8401
|
if (this.store) {
|
|
8335
8402
|
manager.setStore(this.store);
|
|
@@ -8615,6 +8682,158 @@ Reply with exactly one word: YES, NO, or UNSURE.
|
|
|
8615
8682
|
}
|
|
8616
8683
|
};
|
|
8617
8684
|
|
|
8685
|
+
// src/core/router/provider-errors.ts
|
|
8686
|
+
var SYSTEMIC = /* @__PURE__ */ new Set([
|
|
8687
|
+
"rate_limit",
|
|
8688
|
+
"quota_exhausted",
|
|
8689
|
+
"auth",
|
|
8690
|
+
"model_unavailable"
|
|
8691
|
+
]);
|
|
8692
|
+
function isSystemicKind(kind) {
|
|
8693
|
+
return SYSTEMIC.has(kind);
|
|
8694
|
+
}
|
|
8695
|
+
function statusOf(err) {
|
|
8696
|
+
if (!err || typeof err !== "object") return void 0;
|
|
8697
|
+
const e = err;
|
|
8698
|
+
for (const key of ["status", "statusCode", "code"]) {
|
|
8699
|
+
const v = e[key];
|
|
8700
|
+
if (typeof v === "number" && v >= 100 && v < 600) return v;
|
|
8701
|
+
}
|
|
8702
|
+
const resp = e["response"];
|
|
8703
|
+
if (resp && typeof resp["status"] === "number") return resp["status"];
|
|
8704
|
+
const inner = e["error"];
|
|
8705
|
+
if (inner && typeof inner["code"] === "number") return inner["code"];
|
|
8706
|
+
return void 0;
|
|
8707
|
+
}
|
|
8708
|
+
function messageOf(err) {
|
|
8709
|
+
if (err instanceof Error) return err.message;
|
|
8710
|
+
if (typeof err === "string") return err;
|
|
8711
|
+
if (err && typeof err === "object") {
|
|
8712
|
+
const e = err;
|
|
8713
|
+
if (typeof e["message"] === "string") return e["message"];
|
|
8714
|
+
const inner = e["error"];
|
|
8715
|
+
if (inner && typeof inner["message"] === "string") return inner["message"];
|
|
8716
|
+
try {
|
|
8717
|
+
return JSON.stringify(err);
|
|
8718
|
+
} catch {
|
|
8719
|
+
}
|
|
8720
|
+
}
|
|
8721
|
+
return String(err);
|
|
8722
|
+
}
|
|
8723
|
+
function classifyProviderError(err) {
|
|
8724
|
+
const raw = messageOf(err).trim();
|
|
8725
|
+
const status = statusOf(err);
|
|
8726
|
+
const m = raw.toLowerCase();
|
|
8727
|
+
const kind = (() => {
|
|
8728
|
+
if (status === 429) {
|
|
8729
|
+
return /quota|billing|credit|exceeded your current quota|insufficient/.test(m) ? "quota_exhausted" : "rate_limit";
|
|
8730
|
+
}
|
|
8731
|
+
if (status === 401 || status === 403) return "auth";
|
|
8732
|
+
if (status === 404) return "model_unavailable";
|
|
8733
|
+
if (/rate limit|too many requests|resource_exhausted/.test(m)) {
|
|
8734
|
+
return /quota|billing|credit|insufficient/.test(m) ? "quota_exhausted" : "rate_limit";
|
|
8735
|
+
}
|
|
8736
|
+
if (/quota|billing|insufficient_quota|insufficient credit|payment required/.test(m)) return "quota_exhausted";
|
|
8737
|
+
if (/api key|unauthorized|authentication|permission denied|invalid[_ ]api/.test(m)) return "auth";
|
|
8738
|
+
if (/model .*(not found|does not exist|unavailable)|no such model|unknown model|not supported for/.test(m)) {
|
|
8739
|
+
return "model_unavailable";
|
|
8740
|
+
}
|
|
8741
|
+
if (/context length|too many tokens|maximum context|prompt is too long|input too long/.test(m)) return "context_length";
|
|
8742
|
+
if (/safety|content filter|blocked|recitation|prohibited/.test(m)) return "content_filter";
|
|
8743
|
+
if (/econnreset|etimedout|enotfound|socket hang up|network|fetch failed|aborted/.test(m)) return "network";
|
|
8744
|
+
return "unknown";
|
|
8745
|
+
})();
|
|
8746
|
+
return { kind, systemic: isSystemicKind(kind), status, raw };
|
|
8747
|
+
}
|
|
8748
|
+
function describeProviderError(c, modelId) {
|
|
8749
|
+
const on = ` on ${modelId}` ;
|
|
8750
|
+
const detail = c.raw ? ` Provider said: ${c.raw}` : "";
|
|
8751
|
+
switch (c.kind) {
|
|
8752
|
+
case "rate_limit":
|
|
8753
|
+
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}`;
|
|
8754
|
+
case "quota_exhausted":
|
|
8755
|
+
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}`;
|
|
8756
|
+
case "auth":
|
|
8757
|
+
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}`;
|
|
8758
|
+
case "model_unavailable":
|
|
8759
|
+
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}`;
|
|
8760
|
+
case "context_length":
|
|
8761
|
+
return `The prompt was too long${on}. Reduce the attached context, or use a model with a larger context window for this tier.${detail}`;
|
|
8762
|
+
case "content_filter":
|
|
8763
|
+
return `The provider's safety filter refused this request${on}.${detail}`;
|
|
8764
|
+
case "network":
|
|
8765
|
+
return `Network error reaching the provider${on}. This is usually transient.${detail}`;
|
|
8766
|
+
default:
|
|
8767
|
+
return `The model call failed${on}.${detail}`;
|
|
8768
|
+
}
|
|
8769
|
+
}
|
|
8770
|
+
|
|
8771
|
+
// src/core/run-breaker.ts
|
|
8772
|
+
var DEFAULT_FAILURE_THRESHOLD = 3;
|
|
8773
|
+
var RunBreaker = class {
|
|
8774
|
+
threshold;
|
|
8775
|
+
/** modelId → consecutive systemic failures of the SAME kind. */
|
|
8776
|
+
counts = /* @__PURE__ */ new Map();
|
|
8777
|
+
trip = null;
|
|
8778
|
+
constructor(threshold = DEFAULT_FAILURE_THRESHOLD) {
|
|
8779
|
+
this.threshold = Math.max(1, threshold);
|
|
8780
|
+
}
|
|
8781
|
+
/**
|
|
8782
|
+
* Record a failed model call. Returns the trip if this failure is the one
|
|
8783
|
+
* that opened the breaker, otherwise null.
|
|
8784
|
+
*
|
|
8785
|
+
* A non-systemic failure RESETS the model's counter: it is evidence the model
|
|
8786
|
+
* is reachable and answering, which is exactly what a systemic streak claims
|
|
8787
|
+
* it is not.
|
|
8788
|
+
*/
|
|
8789
|
+
record(err, modelId) {
|
|
8790
|
+
if (this.trip) return null;
|
|
8791
|
+
const id = modelId?.trim() || "unknown-model";
|
|
8792
|
+
const c = classifyProviderError(err);
|
|
8793
|
+
if (!c.systemic) {
|
|
8794
|
+
this.counts.delete(id);
|
|
8795
|
+
return null;
|
|
8796
|
+
}
|
|
8797
|
+
const prev = this.counts.get(id);
|
|
8798
|
+
const n = prev && prev.kind === c.kind ? prev.n + 1 : 1;
|
|
8799
|
+
this.counts.set(id, { kind: c.kind, n, last: c });
|
|
8800
|
+
if (n >= this.threshold) {
|
|
8801
|
+
this.trip = {
|
|
8802
|
+
modelId: id,
|
|
8803
|
+
kind: c.kind,
|
|
8804
|
+
failures: n,
|
|
8805
|
+
message: describeProviderError(c, id)
|
|
8806
|
+
};
|
|
8807
|
+
return this.trip;
|
|
8808
|
+
}
|
|
8809
|
+
return null;
|
|
8810
|
+
}
|
|
8811
|
+
/** True once the run should stop launching work that would fail identically. */
|
|
8812
|
+
isOpen() {
|
|
8813
|
+
return this.trip !== null;
|
|
8814
|
+
}
|
|
8815
|
+
/** Why the breaker opened, or null while it is closed. */
|
|
8816
|
+
reason() {
|
|
8817
|
+
return this.trip;
|
|
8818
|
+
}
|
|
8819
|
+
/**
|
|
8820
|
+
* One line for a worker/section that never ran because the breaker was
|
|
8821
|
+
* already open. Says the work was skipped deliberately — not that it failed
|
|
8822
|
+
* on its own merits, which is what an empty FAILED status would imply.
|
|
8823
|
+
*/
|
|
8824
|
+
skipMessage() {
|
|
8825
|
+
const t = this.trip;
|
|
8826
|
+
if (!t) return "Skipped.";
|
|
8827
|
+
return `Skipped \u2014 the run stopped after ${t.failures} consecutive failures on ${t.modelId}. ${t.message}`;
|
|
8828
|
+
}
|
|
8829
|
+
/** Test/diagnostic view of the per-model streaks. */
|
|
8830
|
+
streaks() {
|
|
8831
|
+
const out = {};
|
|
8832
|
+
for (const [k, v] of this.counts) out[k] = { kind: v.kind, n: v.n };
|
|
8833
|
+
return out;
|
|
8834
|
+
}
|
|
8835
|
+
};
|
|
8836
|
+
|
|
8618
8837
|
// src/tools/base.ts
|
|
8619
8838
|
var BaseTool = class {
|
|
8620
8839
|
workspaceRoot = process.cwd();
|
|
@@ -14187,6 +14406,7 @@ ${prompt}` : prompt;
|
|
|
14187
14406
|
if (toolCreator) toolCreator.setPermissionEscalator(escalator);
|
|
14188
14407
|
const rootPrompt = buildContextualPrompt(options.prompt, options.conversationHistory);
|
|
14189
14408
|
let finalOutput = "";
|
|
14409
|
+
const runBreaker = new RunBreaker();
|
|
14190
14410
|
let t2Results = [];
|
|
14191
14411
|
let runError = null;
|
|
14192
14412
|
let identityPrompt = "";
|
|
@@ -14255,6 +14475,7 @@ ${prompt}` : prompt;
|
|
|
14255
14475
|
this.emit("tier:status", { tierId: "t3-root", status: "COMPLETED", role: "T3" });
|
|
14256
14476
|
} else if (complexity === "Moderate") {
|
|
14257
14477
|
const t2 = new T2Manager(this.router, this.toolRegistry, "root");
|
|
14478
|
+
t2.setRunBreaker(runBreaker);
|
|
14258
14479
|
t2.setPresenter(true);
|
|
14259
14480
|
t2.setHierarchyContext("You are the ROOT Manager for this task. There is no T1 Administrator involved in this run. You are responsible for decomposing the task and managing T3 workers directly.");
|
|
14260
14481
|
if (identityPrompt) {
|
|
@@ -14315,6 +14536,7 @@ _(incomplete: ${reasons.join("; ")})_` : partial;
|
|
|
14315
14536
|
}
|
|
14316
14537
|
} else {
|
|
14317
14538
|
const t1 = new T1Administrator(this.router, this.toolRegistry, this.config);
|
|
14539
|
+
t1.setRunBreaker(runBreaker);
|
|
14318
14540
|
t1.setPresenter(true);
|
|
14319
14541
|
t1.setHierarchyContext("You are the top-level Administrator. You are responsible for the overall plan and supervising multiple T2 Managers.");
|
|
14320
14542
|
if (identityPrompt) {
|
|
@@ -14339,6 +14561,20 @@ _(incomplete: ${reasons.join("; ")})_` : partial;
|
|
|
14339
14561
|
finalOutput = result.output;
|
|
14340
14562
|
t2Results = result.t2Results;
|
|
14341
14563
|
}
|
|
14564
|
+
const trip = runBreaker.reason();
|
|
14565
|
+
if (trip) {
|
|
14566
|
+
finalOutput = [
|
|
14567
|
+
`\u26A0 **Run stopped early \u2014 ${trip.modelId} was failing every request.**`,
|
|
14568
|
+
"",
|
|
14569
|
+
trip.message,
|
|
14570
|
+
"",
|
|
14571
|
+
`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.`,
|
|
14572
|
+
finalOutput ? `
|
|
14573
|
+
---
|
|
14574
|
+
|
|
14575
|
+
${finalOutput}` : ""
|
|
14576
|
+
].join("\n").trimEnd();
|
|
14577
|
+
}
|
|
14342
14578
|
} catch (err) {
|
|
14343
14579
|
if (err instanceof CascadeCancelledError || err instanceof Error && err.name === "AbortError" || options.signal?.aborted) {
|
|
14344
14580
|
this.emit("run:cancelled", {
|