cascade-ai 0.48.1 → 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/index.d.cts CHANGED
@@ -17,6 +17,21 @@ interface ModelInfo {
17
17
  maxOutputTokens: number;
18
18
  supportsStreaming: boolean;
19
19
  isLocal: boolean;
20
+ /**
21
+ * True when Cascade has NO price for this model — the cost fields above are
22
+ * 0 only because there is nothing to put in them.
23
+ *
24
+ * This exists because 0 is ambiguous: a local Ollama model genuinely costs
25
+ * nothing, while a newly-released cloud preview model that isn't in the
26
+ * pricing dataset costs real money we can't quote. Treating the second like
27
+ * the first made unknown-priced models report $0.00 spend, put them outside
28
+ * the reach of the per-run cost cap, and gave them the best possible score in
29
+ * the value-based rankers. Anything that reads the cost fields must consult
30
+ * this flag first (see `isPricingUnknown` in core/router/pricing.ts).
31
+ *
32
+ * Never set on local models — their $0 is real.
33
+ */
34
+ pricingUnknown?: boolean;
20
35
  minSizeB?: number;
21
36
  /** Tool-use capability. False for Ollama; true for all cloud providers. */
22
37
  supportsToolUse?: boolean;
@@ -46,6 +61,25 @@ interface ProviderConfig {
46
61
  authToken?: string;
47
62
  /** Where an adopted credential came from, e.g. "Claude Code". Informational. */
48
63
  credentialSource?: string;
64
+ /**
65
+ * Local-vs-hosted toggle. `true` declares "this endpoint runs on hardware I
66
+ * already pay for, so inference genuinely costs $0"; `false` declares it is a
67
+ * paid hosted endpoint, so an unpriced model there reports "cost not tracked"
68
+ * rather than free.
69
+ *
70
+ * Only meaningful for `ollama` and `openai-compatible`, which can each point
71
+ * at either. Left unset, it defaults sensibly: ollama ⇒ local, and
72
+ * openai-compatible ⇒ local when `baseUrl` is loopback/LAN (llama.cpp, LM
73
+ * Studio, vLLM on your own box) and hosted otherwise (Together, Groq, …).
74
+ * See `isLocalEndpoint` in core/router/pricing.ts.
75
+ */
76
+ local?: boolean;
77
+ /**
78
+ * Deployment region for providers whose price varies by region (Azure lists
79
+ * different rates for `global`, `us` and `eu` deployments of the same model).
80
+ * Matched against the pricing dataset's per-region entries.
81
+ */
82
+ region?: string;
49
83
  }
50
84
  interface StreamChunk {
51
85
  text: string;
@@ -57,6 +91,13 @@ interface TokenUsage {
57
91
  outputTokens: number;
58
92
  totalTokens: number;
59
93
  estimatedCostUsd: number;
94
+ /**
95
+ * True when `estimatedCostUsd` is 0 because the model's price is UNKNOWN, not
96
+ * because the call was free. Cost readouts must say "not tracked" for these
97
+ * rather than "$0.00", and budget code must know the spend it just recorded
98
+ * is an undercount.
99
+ */
100
+ costUnknown?: boolean;
60
101
  }
61
102
  interface GenerateOptions {
62
103
  messages: ConversationMessage[];
@@ -1095,6 +1136,25 @@ interface DelegationSavings {
1095
1136
 
1096
1137
  type BenchmarkProfile = Partial<Record<Exclude<TaskType, 'mixed'>, number>>;
1097
1138
 
1139
+ interface PriceDisagreement {
1140
+ modelId: string;
1141
+ provider: string;
1142
+ /** Per-1k rates the bundled dataset asserts. */
1143
+ dataset: {
1144
+ input: number;
1145
+ output: number;
1146
+ };
1147
+ /** Per-1k rates the live source asserts. */
1148
+ live: {
1149
+ input: number;
1150
+ output: number;
1151
+ };
1152
+ /** live/dataset ratio on the blended rate; >1 means live is more expensive. */
1153
+ ratio: number;
1154
+ datasetAsOf: string;
1155
+ datasetSource: string;
1156
+ }
1157
+
1098
1158
  type DataSource = 'live' | 'cache' | 'bundled';
1099
1159
  interface PriceEntry {
1100
1160
  input: number;
@@ -1126,6 +1186,8 @@ declare class LiveDataProvider {
1126
1186
  private snapshot;
1127
1187
  private prices;
1128
1188
  private capabilities;
1189
+ /** Dataset-vs-live price mismatches, deduped by `provider:modelId`. */
1190
+ private disagreements;
1129
1191
  private source;
1130
1192
  private fetchedAt;
1131
1193
  private loaded;
@@ -1156,8 +1218,25 @@ declare class LiveDataProvider {
1156
1218
  /**
1157
1219
  * Returns a price-corrected copy of each model when live pricing is known,
1158
1220
  * leaving the original untouched (so the shared catalog is never mutated).
1221
+ *
1222
+ * Reconciliation policy (see reconcilePrice in pricing.ts): the bundled
1223
+ * dataset is the authoritative baseline, and a live quote overrides it
1224
+ * because it is fresher by construction — but the disagreement is RECORDED,
1225
+ * not swallowed. A dataset/live mismatch is the best signal available that
1226
+ * the committed prices have gone stale, so it must be visible rather than
1227
+ * silently resolved in either direction. A live quote of $0 for a non-local
1228
+ * model is rejected outright: that is the very failure mode this change
1229
+ * exists to prevent, and a marketplace's free-tier alias must never zero out
1230
+ * the price of the paid model Cascade is actually calling.
1159
1231
  */
1160
1232
  applyLivePricing(models: ModelInfo[]): ModelInfo[];
1233
+ private recordDisagreement;
1234
+ /**
1235
+ * Dataset-vs-live price mismatches seen this session, newest-model-first
1236
+ * insertion order. Surfaced by `cascade models --verbose`; a non-empty list
1237
+ * means src/core/router/pricing-data.json is probably due a refresh.
1238
+ */
1239
+ getPriceDisagreements(): PriceDisagreement[];
1161
1240
  /** Current capability facts for a model id, or null when unknown. */
1162
1241
  getCapability(modelId: string): CapabilityEntry | null;
1163
1242
  /**
@@ -1345,6 +1424,14 @@ interface RouterStats {
1345
1424
  outputTokensByTier: Record<string, number>;
1346
1425
  /** Accumulated cost (USD) broken down by feature tag (e.g. T2 section names). */
1347
1426
  costByFeature: Record<string, number>;
1427
+ /**
1428
+ * Calls made against models Cascade has no price for. Their spend is NOT in
1429
+ * `totalCostUsd` — it can't be — so a non-zero count here means the totals
1430
+ * above (and any budget check derived from them) are an undercount.
1431
+ */
1432
+ untrackedCostCalls: number;
1433
+ /** Ids of the models responsible, for "cost not tracked for: …" readouts. */
1434
+ untrackedCostModels: string[];
1348
1435
  }
1349
1436
  declare class CascadeRouter extends EventEmitter {
1350
1437
  private selector;
@@ -1771,6 +1858,12 @@ declare abstract class BaseTier extends EventEmitter {
1771
1858
  getStatus(): TierStatus;
1772
1859
  protected setStatus(status: TierStatus, output?: string): void;
1773
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;
1774
1867
  protected setServingModel(model: string | undefined): void;
1775
1868
  protected setLabel(label: string): void;
1776
1869
  setSystemPromptOverride(prompt: string): void;
@@ -1786,6 +1879,61 @@ declare abstract class BaseTier extends EventEmitter {
1786
1879
  protected throwIfCancelled(): void;
1787
1880
  }
1788
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
+
1789
1937
  type T2Evaluator = (req: PermissionRequest) => Promise<PermissionDecision | null>;
1790
1938
  type T1Evaluator = (req: PermissionRequest) => Promise<PermissionDecision | null>;
1791
1939
  /**
@@ -1933,6 +2081,7 @@ declare class T1Administrator extends BaseTier {
1933
2081
  private t2Managers;
1934
2082
  private escalations;
1935
2083
  private store?;
2084
+ private runBreaker?;
1936
2085
  private t2PeerBus;
1937
2086
  private permissionEscalator?;
1938
2087
  private toolCreator?;
@@ -1942,6 +2091,8 @@ declare class T1Administrator extends BaseTier {
1942
2091
  private peerMessageSessionId;
1943
2092
  private planApprovalCallback?;
1944
2093
  constructor(router: CascadeRouter, toolRegistry: ToolRegistry, config: CascadeConfig);
2094
+ /** Share the run-wide circuit breaker with every section manager. */
2095
+ setRunBreaker(breaker: RunBreaker): void;
1945
2096
  setStore(store: MemoryStore): void;
1946
2097
  /**
1947
2098
  * Inject the shared PermissionEscalator for this task run.
@@ -2317,6 +2468,7 @@ declare class T2Manager extends BaseTier {
2317
2468
  private escalations;
2318
2469
  private peerSyncBuffer;
2319
2470
  private store?;
2471
+ private runBreaker?;
2320
2472
  private t3PeerBus;
2321
2473
  private t2PeerBus?;
2322
2474
  private permissionEscalator?;
@@ -2328,6 +2480,13 @@ declare class T2Manager extends BaseTier {
2328
2480
  setPeerBus(bus: PeerBus): void;
2329
2481
  setPeerMessageCallback(cb: (event: PeerMessageEvent) => void, sessionId: string): void;
2330
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;
2331
2490
  setStore(store: MemoryStore): void;
2332
2491
  /**
2333
2492
  * Inject the shared PermissionEscalator for this task run.
package/dist/index.d.ts CHANGED
@@ -17,6 +17,21 @@ interface ModelInfo {
17
17
  maxOutputTokens: number;
18
18
  supportsStreaming: boolean;
19
19
  isLocal: boolean;
20
+ /**
21
+ * True when Cascade has NO price for this model — the cost fields above are
22
+ * 0 only because there is nothing to put in them.
23
+ *
24
+ * This exists because 0 is ambiguous: a local Ollama model genuinely costs
25
+ * nothing, while a newly-released cloud preview model that isn't in the
26
+ * pricing dataset costs real money we can't quote. Treating the second like
27
+ * the first made unknown-priced models report $0.00 spend, put them outside
28
+ * the reach of the per-run cost cap, and gave them the best possible score in
29
+ * the value-based rankers. Anything that reads the cost fields must consult
30
+ * this flag first (see `isPricingUnknown` in core/router/pricing.ts).
31
+ *
32
+ * Never set on local models — their $0 is real.
33
+ */
34
+ pricingUnknown?: boolean;
20
35
  minSizeB?: number;
21
36
  /** Tool-use capability. False for Ollama; true for all cloud providers. */
22
37
  supportsToolUse?: boolean;
@@ -46,6 +61,25 @@ interface ProviderConfig {
46
61
  authToken?: string;
47
62
  /** Where an adopted credential came from, e.g. "Claude Code". Informational. */
48
63
  credentialSource?: string;
64
+ /**
65
+ * Local-vs-hosted toggle. `true` declares "this endpoint runs on hardware I
66
+ * already pay for, so inference genuinely costs $0"; `false` declares it is a
67
+ * paid hosted endpoint, so an unpriced model there reports "cost not tracked"
68
+ * rather than free.
69
+ *
70
+ * Only meaningful for `ollama` and `openai-compatible`, which can each point
71
+ * at either. Left unset, it defaults sensibly: ollama ⇒ local, and
72
+ * openai-compatible ⇒ local when `baseUrl` is loopback/LAN (llama.cpp, LM
73
+ * Studio, vLLM on your own box) and hosted otherwise (Together, Groq, …).
74
+ * See `isLocalEndpoint` in core/router/pricing.ts.
75
+ */
76
+ local?: boolean;
77
+ /**
78
+ * Deployment region for providers whose price varies by region (Azure lists
79
+ * different rates for `global`, `us` and `eu` deployments of the same model).
80
+ * Matched against the pricing dataset's per-region entries.
81
+ */
82
+ region?: string;
49
83
  }
50
84
  interface StreamChunk {
51
85
  text: string;
@@ -57,6 +91,13 @@ interface TokenUsage {
57
91
  outputTokens: number;
58
92
  totalTokens: number;
59
93
  estimatedCostUsd: number;
94
+ /**
95
+ * True when `estimatedCostUsd` is 0 because the model's price is UNKNOWN, not
96
+ * because the call was free. Cost readouts must say "not tracked" for these
97
+ * rather than "$0.00", and budget code must know the spend it just recorded
98
+ * is an undercount.
99
+ */
100
+ costUnknown?: boolean;
60
101
  }
61
102
  interface GenerateOptions {
62
103
  messages: ConversationMessage[];
@@ -1095,6 +1136,25 @@ interface DelegationSavings {
1095
1136
 
1096
1137
  type BenchmarkProfile = Partial<Record<Exclude<TaskType, 'mixed'>, number>>;
1097
1138
 
1139
+ interface PriceDisagreement {
1140
+ modelId: string;
1141
+ provider: string;
1142
+ /** Per-1k rates the bundled dataset asserts. */
1143
+ dataset: {
1144
+ input: number;
1145
+ output: number;
1146
+ };
1147
+ /** Per-1k rates the live source asserts. */
1148
+ live: {
1149
+ input: number;
1150
+ output: number;
1151
+ };
1152
+ /** live/dataset ratio on the blended rate; >1 means live is more expensive. */
1153
+ ratio: number;
1154
+ datasetAsOf: string;
1155
+ datasetSource: string;
1156
+ }
1157
+
1098
1158
  type DataSource = 'live' | 'cache' | 'bundled';
1099
1159
  interface PriceEntry {
1100
1160
  input: number;
@@ -1126,6 +1186,8 @@ declare class LiveDataProvider {
1126
1186
  private snapshot;
1127
1187
  private prices;
1128
1188
  private capabilities;
1189
+ /** Dataset-vs-live price mismatches, deduped by `provider:modelId`. */
1190
+ private disagreements;
1129
1191
  private source;
1130
1192
  private fetchedAt;
1131
1193
  private loaded;
@@ -1156,8 +1218,25 @@ declare class LiveDataProvider {
1156
1218
  /**
1157
1219
  * Returns a price-corrected copy of each model when live pricing is known,
1158
1220
  * leaving the original untouched (so the shared catalog is never mutated).
1221
+ *
1222
+ * Reconciliation policy (see reconcilePrice in pricing.ts): the bundled
1223
+ * dataset is the authoritative baseline, and a live quote overrides it
1224
+ * because it is fresher by construction — but the disagreement is RECORDED,
1225
+ * not swallowed. A dataset/live mismatch is the best signal available that
1226
+ * the committed prices have gone stale, so it must be visible rather than
1227
+ * silently resolved in either direction. A live quote of $0 for a non-local
1228
+ * model is rejected outright: that is the very failure mode this change
1229
+ * exists to prevent, and a marketplace's free-tier alias must never zero out
1230
+ * the price of the paid model Cascade is actually calling.
1159
1231
  */
1160
1232
  applyLivePricing(models: ModelInfo[]): ModelInfo[];
1233
+ private recordDisagreement;
1234
+ /**
1235
+ * Dataset-vs-live price mismatches seen this session, newest-model-first
1236
+ * insertion order. Surfaced by `cascade models --verbose`; a non-empty list
1237
+ * means src/core/router/pricing-data.json is probably due a refresh.
1238
+ */
1239
+ getPriceDisagreements(): PriceDisagreement[];
1161
1240
  /** Current capability facts for a model id, or null when unknown. */
1162
1241
  getCapability(modelId: string): CapabilityEntry | null;
1163
1242
  /**
@@ -1345,6 +1424,14 @@ interface RouterStats {
1345
1424
  outputTokensByTier: Record<string, number>;
1346
1425
  /** Accumulated cost (USD) broken down by feature tag (e.g. T2 section names). */
1347
1426
  costByFeature: Record<string, number>;
1427
+ /**
1428
+ * Calls made against models Cascade has no price for. Their spend is NOT in
1429
+ * `totalCostUsd` — it can't be — so a non-zero count here means the totals
1430
+ * above (and any budget check derived from them) are an undercount.
1431
+ */
1432
+ untrackedCostCalls: number;
1433
+ /** Ids of the models responsible, for "cost not tracked for: …" readouts. */
1434
+ untrackedCostModels: string[];
1348
1435
  }
1349
1436
  declare class CascadeRouter extends EventEmitter {
1350
1437
  private selector;
@@ -1771,6 +1858,12 @@ declare abstract class BaseTier extends EventEmitter {
1771
1858
  getStatus(): TierStatus;
1772
1859
  protected setStatus(status: TierStatus, output?: string): void;
1773
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;
1774
1867
  protected setServingModel(model: string | undefined): void;
1775
1868
  protected setLabel(label: string): void;
1776
1869
  setSystemPromptOverride(prompt: string): void;
@@ -1786,6 +1879,61 @@ declare abstract class BaseTier extends EventEmitter {
1786
1879
  protected throwIfCancelled(): void;
1787
1880
  }
1788
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
+
1789
1937
  type T2Evaluator = (req: PermissionRequest) => Promise<PermissionDecision | null>;
1790
1938
  type T1Evaluator = (req: PermissionRequest) => Promise<PermissionDecision | null>;
1791
1939
  /**
@@ -1933,6 +2081,7 @@ declare class T1Administrator extends BaseTier {
1933
2081
  private t2Managers;
1934
2082
  private escalations;
1935
2083
  private store?;
2084
+ private runBreaker?;
1936
2085
  private t2PeerBus;
1937
2086
  private permissionEscalator?;
1938
2087
  private toolCreator?;
@@ -1942,6 +2091,8 @@ declare class T1Administrator extends BaseTier {
1942
2091
  private peerMessageSessionId;
1943
2092
  private planApprovalCallback?;
1944
2093
  constructor(router: CascadeRouter, toolRegistry: ToolRegistry, config: CascadeConfig);
2094
+ /** Share the run-wide circuit breaker with every section manager. */
2095
+ setRunBreaker(breaker: RunBreaker): void;
1945
2096
  setStore(store: MemoryStore): void;
1946
2097
  /**
1947
2098
  * Inject the shared PermissionEscalator for this task run.
@@ -2317,6 +2468,7 @@ declare class T2Manager extends BaseTier {
2317
2468
  private escalations;
2318
2469
  private peerSyncBuffer;
2319
2470
  private store?;
2471
+ private runBreaker?;
2320
2472
  private t3PeerBus;
2321
2473
  private t2PeerBus?;
2322
2474
  private permissionEscalator?;
@@ -2328,6 +2480,13 @@ declare class T2Manager extends BaseTier {
2328
2480
  setPeerBus(bus: PeerBus): void;
2329
2481
  setPeerMessageCallback(cb: (event: PeerMessageEvent) => void, sessionId: string): void;
2330
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;
2331
2490
  setStore(store: MemoryStore): void;
2332
2491
  /**
2333
2492
  * Inject the shared PermissionEscalator for this task run.