@librechat/agents 3.3.10 → 3.3.12

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.
Files changed (211) hide show
  1. package/dist/cjs/graphs/Graph.cjs +248 -27
  2. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  3. package/dist/cjs/langfuseToolOutputTracing.cjs +228 -16
  4. package/dist/cjs/langfuseToolOutputTracing.cjs.map +1 -1
  5. package/dist/cjs/llm/bedrock/index.cjs +13 -2
  6. package/dist/cjs/llm/bedrock/index.cjs.map +1 -1
  7. package/dist/cjs/llm/init.cjs +1 -1
  8. package/dist/cjs/llm/invoke.cjs +160 -43
  9. package/dist/cjs/llm/invoke.cjs.map +1 -1
  10. package/dist/cjs/llm/openai/index.cjs +190 -13
  11. package/dist/cjs/llm/openai/index.cjs.map +1 -1
  12. package/dist/cjs/llm/streamLimits.cjs +723 -0
  13. package/dist/cjs/llm/streamLimits.cjs.map +1 -0
  14. package/dist/cjs/main.cjs +12 -3
  15. package/dist/cjs/messages/core.cjs +592 -27
  16. package/dist/cjs/messages/core.cjs.map +1 -1
  17. package/dist/cjs/run.cjs +7 -3
  18. package/dist/cjs/run.cjs.map +1 -1
  19. package/dist/cjs/session/AgentSession.cjs +4 -1
  20. package/dist/cjs/session/AgentSession.cjs.map +1 -1
  21. package/dist/cjs/stream.cjs +105 -14
  22. package/dist/cjs/stream.cjs.map +1 -1
  23. package/dist/cjs/summarization/node.cjs +157 -37
  24. package/dist/cjs/summarization/node.cjs.map +1 -1
  25. package/dist/cjs/tools/BashExecutor.cjs +3 -2
  26. package/dist/cjs/tools/BashExecutor.cjs.map +1 -1
  27. package/dist/cjs/tools/CodeExecutor.cjs +4 -3
  28. package/dist/cjs/tools/CodeExecutor.cjs.map +1 -1
  29. package/dist/cjs/tools/ProgrammaticToolCalling.cjs +5 -3
  30. package/dist/cjs/tools/ProgrammaticToolCalling.cjs.map +1 -1
  31. package/dist/cjs/tools/ToolNode.cjs +77 -4
  32. package/dist/cjs/tools/ToolNode.cjs.map +1 -1
  33. package/dist/cjs/tools/ToolSearch.cjs +3 -2
  34. package/dist/cjs/tools/ToolSearch.cjs.map +1 -1
  35. package/dist/cjs/tools/search/crw-scraper.cjs +7 -1
  36. package/dist/cjs/tools/search/crw-scraper.cjs.map +1 -1
  37. package/dist/cjs/tools/search/crw-search.cjs +3 -1
  38. package/dist/cjs/tools/search/crw-search.cjs.map +1 -1
  39. package/dist/cjs/tools/search/firecrawl.cjs +7 -1
  40. package/dist/cjs/tools/search/firecrawl.cjs.map +1 -1
  41. package/dist/cjs/tools/search/keenable-scraper.cjs +7 -1
  42. package/dist/cjs/tools/search/keenable-scraper.cjs.map +1 -1
  43. package/dist/cjs/tools/search/keenable-search.cjs +3 -1
  44. package/dist/cjs/tools/search/keenable-search.cjs.map +1 -1
  45. package/dist/cjs/tools/search/rerankers.cjs +26 -8
  46. package/dist/cjs/tools/search/rerankers.cjs.map +1 -1
  47. package/dist/cjs/tools/search/search.cjs +30 -10
  48. package/dist/cjs/tools/search/search.cjs.map +1 -1
  49. package/dist/cjs/tools/search/serper-scraper.cjs +7 -1
  50. package/dist/cjs/tools/search/serper-scraper.cjs.map +1 -1
  51. package/dist/cjs/tools/search/tavily-scraper.cjs +7 -1
  52. package/dist/cjs/tools/search/tavily-scraper.cjs.map +1 -1
  53. package/dist/cjs/tools/search/tavily-search.cjs +3 -1
  54. package/dist/cjs/tools/search/tavily-search.cjs.map +1 -1
  55. package/dist/cjs/tools/search/tool.cjs +17 -3
  56. package/dist/cjs/tools/search/tool.cjs.map +1 -1
  57. package/dist/cjs/tools/subagent/SubagentExecutor.cjs +54 -3
  58. package/dist/cjs/tools/subagent/SubagentExecutor.cjs.map +1 -1
  59. package/dist/cjs/utils/index.cjs +2 -1
  60. package/dist/cjs/utils/misc.cjs +12 -0
  61. package/dist/cjs/utils/misc.cjs.map +1 -1
  62. package/dist/cjs/utils/proxy.cjs +63 -0
  63. package/dist/cjs/utils/proxy.cjs.map +1 -0
  64. package/dist/esm/graphs/Graph.mjs +247 -26
  65. package/dist/esm/graphs/Graph.mjs.map +1 -1
  66. package/dist/esm/langfuseToolOutputTracing.mjs +228 -16
  67. package/dist/esm/langfuseToolOutputTracing.mjs.map +1 -1
  68. package/dist/esm/llm/bedrock/index.mjs +13 -2
  69. package/dist/esm/llm/bedrock/index.mjs.map +1 -1
  70. package/dist/esm/llm/init.mjs +1 -1
  71. package/dist/esm/llm/invoke.mjs +160 -43
  72. package/dist/esm/llm/invoke.mjs.map +1 -1
  73. package/dist/esm/llm/openai/index.mjs +192 -15
  74. package/dist/esm/llm/openai/index.mjs.map +1 -1
  75. package/dist/esm/llm/streamLimits.mjs +704 -0
  76. package/dist/esm/llm/streamLimits.mjs.map +1 -0
  77. package/dist/esm/main.mjs +8 -6
  78. package/dist/esm/messages/core.mjs +592 -28
  79. package/dist/esm/messages/core.mjs.map +1 -1
  80. package/dist/esm/run.mjs +7 -3
  81. package/dist/esm/run.mjs.map +1 -1
  82. package/dist/esm/session/AgentSession.mjs +4 -1
  83. package/dist/esm/session/AgentSession.mjs.map +1 -1
  84. package/dist/esm/stream.mjs +105 -14
  85. package/dist/esm/stream.mjs.map +1 -1
  86. package/dist/esm/summarization/node.mjs +157 -37
  87. package/dist/esm/summarization/node.mjs.map +1 -1
  88. package/dist/esm/tools/BashExecutor.mjs +3 -2
  89. package/dist/esm/tools/BashExecutor.mjs.map +1 -1
  90. package/dist/esm/tools/CodeExecutor.mjs +4 -3
  91. package/dist/esm/tools/CodeExecutor.mjs.map +1 -1
  92. package/dist/esm/tools/ProgrammaticToolCalling.mjs +5 -3
  93. package/dist/esm/tools/ProgrammaticToolCalling.mjs.map +1 -1
  94. package/dist/esm/tools/ToolNode.mjs +77 -4
  95. package/dist/esm/tools/ToolNode.mjs.map +1 -1
  96. package/dist/esm/tools/ToolSearch.mjs +3 -2
  97. package/dist/esm/tools/ToolSearch.mjs.map +1 -1
  98. package/dist/esm/tools/search/crw-scraper.mjs +7 -1
  99. package/dist/esm/tools/search/crw-scraper.mjs.map +1 -1
  100. package/dist/esm/tools/search/crw-search.mjs +3 -1
  101. package/dist/esm/tools/search/crw-search.mjs.map +1 -1
  102. package/dist/esm/tools/search/firecrawl.mjs +7 -1
  103. package/dist/esm/tools/search/firecrawl.mjs.map +1 -1
  104. package/dist/esm/tools/search/keenable-scraper.mjs +7 -1
  105. package/dist/esm/tools/search/keenable-scraper.mjs.map +1 -1
  106. package/dist/esm/tools/search/keenable-search.mjs +3 -1
  107. package/dist/esm/tools/search/keenable-search.mjs.map +1 -1
  108. package/dist/esm/tools/search/rerankers.mjs +26 -8
  109. package/dist/esm/tools/search/rerankers.mjs.map +1 -1
  110. package/dist/esm/tools/search/search.mjs +30 -10
  111. package/dist/esm/tools/search/search.mjs.map +1 -1
  112. package/dist/esm/tools/search/serper-scraper.mjs +7 -1
  113. package/dist/esm/tools/search/serper-scraper.mjs.map +1 -1
  114. package/dist/esm/tools/search/tavily-scraper.mjs +7 -1
  115. package/dist/esm/tools/search/tavily-scraper.mjs.map +1 -1
  116. package/dist/esm/tools/search/tavily-search.mjs +3 -1
  117. package/dist/esm/tools/search/tavily-search.mjs.map +1 -1
  118. package/dist/esm/tools/search/tool.mjs +17 -3
  119. package/dist/esm/tools/search/tool.mjs.map +1 -1
  120. package/dist/esm/tools/subagent/SubagentExecutor.mjs +54 -3
  121. package/dist/esm/tools/subagent/SubagentExecutor.mjs.map +1 -1
  122. package/dist/esm/utils/index.mjs +2 -1
  123. package/dist/esm/utils/misc.mjs +12 -1
  124. package/dist/esm/utils/misc.mjs.map +1 -1
  125. package/dist/esm/utils/proxy.mjs +62 -0
  126. package/dist/esm/utils/proxy.mjs.map +1 -0
  127. package/dist/types/graphs/Graph.d.ts +53 -1
  128. package/dist/types/index.d.ts +2 -0
  129. package/dist/types/langfuseToolOutputTracing.d.ts +1 -0
  130. package/dist/types/llm/invoke.d.ts +26 -5
  131. package/dist/types/llm/openai/index.d.ts +3 -0
  132. package/dist/types/llm/streamLimits.d.ts +314 -0
  133. package/dist/types/messages/core.d.ts +11 -6
  134. package/dist/types/run.d.ts +1 -0
  135. package/dist/types/summarization/node.d.ts +27 -2
  136. package/dist/types/tools/BashExecutor.d.ts +2 -2
  137. package/dist/types/tools/CodeExecutor.d.ts +3 -3
  138. package/dist/types/tools/ToolNode.d.ts +11 -1
  139. package/dist/types/tools/search/crw-scraper.d.ts +2 -0
  140. package/dist/types/tools/search/firecrawl.d.ts +2 -0
  141. package/dist/types/tools/search/keenable-scraper.d.ts +2 -0
  142. package/dist/types/tools/search/rerankers.d.ts +9 -5
  143. package/dist/types/tools/search/serper-scraper.d.ts +2 -0
  144. package/dist/types/tools/search/tavily-scraper.d.ts +2 -0
  145. package/dist/types/tools/search/types.d.ts +28 -33
  146. package/dist/types/tools/subagent/SubagentExecutor.d.ts +44 -0
  147. package/dist/types/types/graph.d.ts +7 -1
  148. package/dist/types/types/run.d.ts +46 -1
  149. package/dist/types/types/tools.d.ts +21 -0
  150. package/dist/types/utils/index.d.ts +1 -0
  151. package/dist/types/utils/misc.d.ts +7 -0
  152. package/dist/types/utils/proxy.d.ts +31 -0
  153. package/package.json +2 -1
  154. package/src/__tests__/stream.eagerArgsDivergence.test.ts +158 -0
  155. package/src/__tests__/stream.eagerEventExecution.test.ts +1 -0
  156. package/src/__tests__/stream.streamLimits.test.ts +1982 -0
  157. package/src/graphs/Graph.ts +302 -27
  158. package/src/graphs/__tests__/Graph.breakerLifecycle.test.ts +234 -0
  159. package/src/index.ts +11 -0
  160. package/src/langfuseToolOutputTracing.ts +410 -14
  161. package/src/llm/bedrock/index.ts +22 -2
  162. package/src/llm/custom-chat-models.smoke.test.ts +747 -0
  163. package/src/llm/invoke.streamLimits.test.ts +142 -0
  164. package/src/llm/invoke.test.ts +187 -1
  165. package/src/llm/invoke.ts +231 -43
  166. package/src/llm/openai/cacheWriteTokens.test.ts +112 -0
  167. package/src/llm/openai/index.ts +348 -31
  168. package/src/llm/openai/llm.spec.ts +107 -6
  169. package/src/llm/streamLimits.test.ts +450 -0
  170. package/src/llm/streamLimits.ts +1158 -0
  171. package/src/messages/core.ts +1290 -42
  172. package/src/messages/formatAgentMessages.test.ts +2623 -0
  173. package/src/run.ts +4 -0
  174. package/src/session/AgentSession.ts +5 -0
  175. package/src/specs/langfuse-tool-output-tracing.test.ts +887 -0
  176. package/src/specs/preemptSeal.test.ts +374 -5
  177. package/src/stream.ts +141 -6
  178. package/src/summarization/__tests__/node.test.ts +269 -0
  179. package/src/summarization/chunkHandler.test.ts +196 -0
  180. package/src/summarization/node.ts +203 -6
  181. package/src/tools/BashExecutor.ts +4 -3
  182. package/src/tools/CodeExecutor.ts +5 -4
  183. package/src/tools/ProgrammaticToolCalling.ts +7 -5
  184. package/src/tools/ToolNode.ts +109 -6
  185. package/src/tools/ToolSearch.ts +4 -3
  186. package/src/tools/__tests__/BashExecutor.test.ts +2 -2
  187. package/src/tools/__tests__/ProgrammaticToolCalling.test.ts +2 -4
  188. package/src/tools/__tests__/SubagentExecutor.test.ts +126 -0
  189. package/src/tools/__tests__/ToolNode.breakerSignal.test.ts +389 -0
  190. package/src/tools/__tests__/ToolNode.streamLimits.test.ts +69 -0
  191. package/src/tools/search/crw-scraper.ts +6 -0
  192. package/src/tools/search/crw-search.ts +6 -1
  193. package/src/tools/search/firecrawl.ts +6 -0
  194. package/src/tools/search/http-agent.test.ts +133 -0
  195. package/src/tools/search/keenable-scraper.ts +6 -0
  196. package/src/tools/search/keenable-search.ts +6 -1
  197. package/src/tools/search/rerankers.ts +36 -10
  198. package/src/tools/search/search.ts +29 -11
  199. package/src/tools/search/serper-scraper.ts +6 -0
  200. package/src/tools/search/tavily-scraper.ts +6 -0
  201. package/src/tools/search/tavily-search.ts +2 -0
  202. package/src/tools/search/tool.ts +16 -0
  203. package/src/tools/search/types.ts +31 -33
  204. package/src/tools/subagent/SubagentExecutor.ts +96 -3
  205. package/src/types/graph.ts +7 -0
  206. package/src/types/run.ts +49 -1
  207. package/src/types/tools.ts +21 -0
  208. package/src/utils/index.ts +1 -0
  209. package/src/utils/misc.ts +19 -0
  210. package/src/utils/proxy.test.ts +176 -0
  211. package/src/utils/proxy.ts +93 -0
@@ -17,6 +17,12 @@ import type {
17
17
  MessageContent,
18
18
  } from '@langchain/core/messages';
19
19
  import type { ToolCall } from '@langchain/core/messages/tool';
20
+ import type {
21
+ ResolvedStreamLimits,
22
+ RunBreakerScope,
23
+ StreamedToolCallArgTally,
24
+ StreamDeltaEventTally,
25
+ } from '@/llm/streamLimits';
20
26
  import type { OverflowRecoveryPlan } from '@/llm/contextOverflowRecovery';
21
27
  import type { FallbackErrorContext } from '@/llm/invoke';
22
28
  import type { HookRegistry } from '@/hooks';
@@ -69,6 +75,7 @@ import {
69
75
  isGoogleLike,
70
76
  apportionTokenCounts,
71
77
  calculateMaxToolResultChars,
78
+ composeAbortSignals,
72
79
  joinKeys,
73
80
  sleep,
74
81
  } from '@/utils';
@@ -115,6 +122,13 @@ import {
115
122
  findCallback,
116
123
  type CallbackEntry,
117
124
  } from '@/utils/callbacks';
125
+ import {
126
+ resolveStreamLimits,
127
+ StreamLimitExceededError,
128
+ sweepStaleStreamLimitEntries,
129
+ STREAM_LIMIT_EPOCH_KEY,
130
+ RUN_BREAKER_SCOPE_CONFIG_KEY,
131
+ } from '@/llm/streamLimits';
118
132
  import { partitionAndMarkOpenRouterToolCache } from '@/llm/openrouter/toolCache';
119
133
  import { ToolNode as CustomToolNode, toolsCondition } from '@/tools/ToolNode';
120
134
  import { shouldTraceToolNodeForLangfuse } from '@/langfuseToolOutputTracing';
@@ -154,25 +168,6 @@ const EMPTY_PREEMPT_BOUNDARY: PreemptBoundaryResult = {
154
168
  preventContinuation: false,
155
169
  };
156
170
 
157
- /**
158
- * One signal that fires when either input fires. `AbortSignal.any` is skipped
159
- * when the inputs collapse to a single signal — the composite is a fresh
160
- * object per call, and the common cases (one channel, or the host reusing the
161
- * same controller for both) don't need one.
162
- */
163
- function composeAbortSignals(
164
- a: AbortSignal | undefined,
165
- b: AbortSignal | undefined
166
- ): AbortSignal | undefined {
167
- if (a == null || a === b) {
168
- return b;
169
- }
170
- if (b == null) {
171
- return a;
172
- }
173
- return AbortSignal.any([a, b]);
174
- }
175
-
176
171
  /** Minimum relative variance before calibrated toolSchemaTokens overrides current value. */
177
172
  const CALIBRATION_VARIANCE_THRESHOLD = 0.15;
178
173
 
@@ -1051,6 +1046,80 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
1051
1046
  subagentScope: boolean;
1052
1047
  /** See {@link t.StandardGraphInput.preemption}. */
1053
1048
  preemption?: t.StreamPreemption;
1049
+ /**
1050
+ * Stream circuit breakers, resolved once from
1051
+ * {@link t.StandardGraphInput.streamLimits}. The stream handler enforces
1052
+ * these on every streamed chunk event.
1053
+ */
1054
+ streamLimits: ResolvedStreamLimits;
1055
+ /**
1056
+ * Cumulative streamed argument bytes per in-flight tool call, keyed by
1057
+ * generation key + chunk index (see `resolveGenerationKey`). Per-run
1058
+ * accumulation state, cleared by both reset paths.
1059
+ */
1060
+ streamedToolCallArgTallies: Map<string, StreamedToolCallArgTally> =
1061
+ new Map();
1062
+ /** Streamed chunk events per model generation, keyed by generation key. */
1063
+ streamDeltaEventCounts: Map<string, StreamDeltaEventTally> = new Map();
1064
+ /** Per-chunk-object, per-generation charge balances (lazily created; see
1065
+ * `StreamLimitState`). Reinitialized by both reset paths: a model may
1066
+ * retain and re-yield one mutable chunk object, whose nested map would
1067
+ * otherwise grow by one attempt-stamped entry per model call for the
1068
+ * graph's lifetime. */
1069
+ streamLimitChargeCredits?: WeakMap<object, Map<string, number>>;
1070
+ /** Run-scoped breaker abort: composed into every model invocation and
1071
+ * every SubagentExecutor child signal, and tripped when a stream circuit
1072
+ * breaker fires anywhere in the run, so parallel agent nodes' in-flight
1073
+ * provider calls and subagents stop consuming quota while the rejection
1074
+ * propagates. Recreated by both reset paths — the abort is one-way within
1075
+ * a run, and a reused graph must start its next run unaborted. */
1076
+ breakerAbort = new AbortController();
1077
+
1078
+ /** Incremented whenever `breakerAbort` is replaced. Stamped into each
1079
+ * model attempt's metadata ({@link STREAM_LIMIT_EPOCH_KEY}) so the stream
1080
+ * handler's consumer-side trip binds to the run that produced the event
1081
+ * rather than whichever controller is live when a straggling chunk is
1082
+ * finally handled. */
1083
+ breakerEpoch = 0;
1084
+
1085
+ /** Immutable snapshot of the run's breaker identity (epoch + controller),
1086
+ * replaced as ONE object whenever `resetValues` installs a fresh
1087
+ * controller. Sites that pause across awaits capture it at entry and
1088
+ * revalidate by REFERENCE afterwards — a single identity comparison
1089
+ * proves no reset happened while suspended, where separate epoch and
1090
+ * controller reads could interleave with one. */
1091
+ runScope: RunBreakerScope = Object.freeze({
1092
+ epoch: 0,
1093
+ controller: this.breakerAbort,
1094
+ });
1095
+
1096
+ /** Generation keys of model attempts still in flight (see
1097
+ * `StreamLimitState.activeStreamLimitGenerations`). Lazily created by the
1098
+ * attempt lease; spans resets on purpose. */
1099
+ activeStreamLimitGenerations?: Set<string>;
1100
+
1101
+ /** The stream-limit error behind an already-fired breaker, whether this
1102
+ * graph's own controller tripped or a parent run's breaker arrived through
1103
+ * the composed constructor signal (child graphs own separate controllers).
1104
+ * Providers can translate either abort into a generic error, and recovery
1105
+ * paths must not run in that state. */
1106
+ protected resolveTrippedBreakerReason(
1107
+ breakerSignal: AbortSignal = this.breakerAbort.signal
1108
+ ): StreamLimitExceededError | undefined {
1109
+ if (
1110
+ breakerSignal.aborted &&
1111
+ breakerSignal.reason instanceof StreamLimitExceededError
1112
+ ) {
1113
+ return breakerSignal.reason;
1114
+ }
1115
+ if (
1116
+ this.signal?.aborted === true &&
1117
+ this.signal.reason instanceof StreamLimitExceededError
1118
+ ) {
1119
+ return this.signal.reason;
1120
+ }
1121
+ return undefined;
1122
+ }
1054
1123
  /**
1055
1124
  * Seals charged against `preemption.maxSeals`. Per-turn: cleared by both
1056
1125
  * reset paths so a fresh turn gets a fresh budget, while a HITL resume —
@@ -1108,6 +1177,7 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
1108
1177
  subagentUsageSink,
1109
1178
  subagentScope,
1110
1179
  preemption,
1180
+ streamLimits,
1111
1181
  }: t.StandardGraphInput) {
1112
1182
  super();
1113
1183
  this.runId = runId;
@@ -1117,6 +1187,7 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
1117
1187
  this.subagentUsageSink = subagentUsageSink;
1118
1188
  this.subagentScope = subagentScope === true;
1119
1189
  this.preemption = preemption;
1190
+ this.streamLimits = resolveStreamLimits(streamLimits);
1120
1191
 
1121
1192
  if (agents.length === 0) {
1122
1193
  throw new Error('At least one agent configuration is required');
@@ -1162,6 +1233,36 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
1162
1233
  this.clearEagerEventToolUsageCounts();
1163
1234
  this.eagerEventToolCallChunks.clear();
1164
1235
  this.eagerEventToolSuppressions.clear();
1236
+ /** Grace sweep instead of a clear: producer loops of straggling
1237
+ * attempts use these maps directly and sit outside the consumer-only
1238
+ * epoch gate — clearing would hand a cancellation-ignoring provider a
1239
+ * fresh allowance at every run start. Entries from the epoch that is
1240
+ * ending survive exactly one reset so those stragglers stay on their
1241
+ * original budgets; older entries are removed. */
1242
+ sweepStaleStreamLimitEntries(
1243
+ this.streamedToolCallArgTallies,
1244
+ this.breakerEpoch,
1245
+ this.activeStreamLimitGenerations
1246
+ );
1247
+ sweepStaleStreamLimitEntries(
1248
+ this.streamDeltaEventCounts,
1249
+ this.breakerEpoch,
1250
+ this.activeStreamLimitGenerations
1251
+ );
1252
+ this.streamLimitChargeCredits = undefined;
1253
+ /** Run-start is the only safe replacement point for the breaker:
1254
+ * end-of-run cleanup must leave it in place so straggling parallel
1255
+ * children from the failed run cannot start on a fresh signal.
1256
+ * Replaced UNCONDITIONALLY here — a run that failed on an ordinary
1257
+ * error leaves the controller un-aborted, and stragglers still settling
1258
+ * hold their entry-time capture of it; a late stream-limit trip on that
1259
+ * old controller must not cancel the run starting now. */
1260
+ this.breakerAbort = new AbortController();
1261
+ this.breakerEpoch += 1;
1262
+ this.runScope = Object.freeze({
1263
+ epoch: this.breakerEpoch,
1264
+ controller: this.breakerAbort,
1265
+ });
1165
1266
  this.handlerDispatchedStepIds = resetIfNotEmpty(
1166
1267
  this.handlerDispatchedStepIds,
1167
1268
  new Set()
@@ -1219,6 +1320,21 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
1219
1320
  super.clearHeavyState();
1220
1321
  this.messages = [];
1221
1322
  this.overrideModel = undefined;
1323
+ /** Stream-limit accounting (argument tallies, event counts, charge
1324
+ * credits) deliberately SURVIVES cleanup: this runs in `processStream`'s
1325
+ * finally, which an ordinary parallel-branch failure reaches while
1326
+ * sibling attempts are still unwinding on the retained breaker — a
1327
+ * cancellation-ignoring provider's late chunks would otherwise recreate
1328
+ * their budgets from zero and stream another full allowance. The maps
1329
+ * are bounded by in-flight call sizes and `resetValues` clears them at
1330
+ * the next run start, where the epoch bump already drops stamped
1331
+ * straggler events before accounting. */
1332
+ /** Deliberately NOT recreating a tripped breakerAbort here: this runs in
1333
+ * `processStream`'s cleanup, which a rejected parallel batch reaches
1334
+ * while sibling subagents can still be pre-invoke — a fresh controller
1335
+ * would hand them an un-aborted signal and let provider requests start
1336
+ * after the run already failed. `resetValues` recreates it when the next
1337
+ * run begins. */
1222
1338
  /**
1223
1339
  * Turn state only. The reported totals must outlive cleanup — this runs
1224
1340
  * in `processStream`'s `finally`, and the host reads `getPreemptStats()`
@@ -1501,13 +1617,17 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
1501
1617
  /* Misc.*/
1502
1618
 
1503
1619
  getRunMessages(): BaseMessage[] | undefined {
1504
- if (this.messages == null) {
1620
+ /** Runtime-honest widening: a disposed-but-cached graph (HITL
1621
+ * resume/reconnect through a WeakRef cache) can carry null here despite
1622
+ * the field type. */
1623
+ const messages = this.messages as BaseMessage[] | undefined;
1624
+ if (messages == null) {
1505
1625
  return this.cachedRunMessages;
1506
1626
  }
1507
- if (this.messages.length === 0 && this.cachedRunMessages != null) {
1627
+ if (messages.length === 0 && this.cachedRunMessages != null) {
1508
1628
  return this.cachedRunMessages;
1509
1629
  }
1510
- return this.messages.slice(this.startIndex);
1630
+ return messages.slice(this.startIndex);
1511
1631
  }
1512
1632
 
1513
1633
  override getDiscoveredTools(agentId?: string): string[] {
@@ -1559,10 +1679,11 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
1559
1679
  // `messages` can be null/undefined on a graph that has been disposed
1560
1680
  // (clearHeavyState) but is still reachable via a cache (e.g. RedisJobStore's
1561
1681
  // WeakRef) during a HITL resume/reconnect. Guard instead of dereferencing null.
1562
- if (this.messages == null) {
1682
+ const messages = this.messages as BaseMessage[] | undefined;
1683
+ if (messages == null) {
1563
1684
  return undefined;
1564
1685
  }
1565
- return convertMessagesToContent(this.messages.slice(this.startIndex));
1686
+ return convertMessagesToContent(messages.slice(this.startIndex));
1566
1687
  }
1567
1688
 
1568
1689
  getCalibrationRatio(): number {
@@ -1597,13 +1718,14 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
1597
1718
  // `contentData` can be null/undefined on a disposed-but-cached graph during a
1598
1719
  // HITL resume/reconnect; without this guard `[...this.contentData]` throws
1599
1720
  // "this.contentData is not iterable".
1600
- if (this.contentData == null) {
1721
+ const contentData = this.contentData as t.RunStep[] | undefined;
1722
+ if (contentData == null) {
1601
1723
  return [];
1602
1724
  }
1603
1725
  if (agentId == null || agentId === '') {
1604
- return [...this.contentData];
1726
+ return [...contentData];
1605
1727
  }
1606
- return this.contentData.filter((step) => step.agentId === agentId);
1728
+ return contentData.filter((step) => step.agentId === agentId);
1607
1729
  }
1608
1730
 
1609
1731
  /**
@@ -1734,6 +1856,8 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
1734
1856
  maxToolResultChars: agentContext?.maxToolResultChars,
1735
1857
  toolOutputRegistry: this.getOrCreateToolOutputRegistry(),
1736
1858
  fileCheckpointer: this.getOrCreateFileCheckpointer(),
1859
+ getBreakerSignal: (): AbortSignal => this.breakerAbort.signal,
1860
+ getRunScope: (): RunBreakerScope => this.runScope,
1737
1861
  errorHandler: (data, metadata): Promise<boolean> =>
1738
1862
  StandardGraph.handleToolCallErrorStatic(this, data, metadata),
1739
1863
  });
@@ -1801,6 +1925,8 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
1801
1925
  maxToolResultChars: agentContext?.maxToolResultChars,
1802
1926
  toolOutputRegistry: this.getOrCreateToolOutputRegistry(),
1803
1927
  fileCheckpointer: this.getOrCreateFileCheckpointer(),
1928
+ getBreakerSignal: (): AbortSignal => this.breakerAbort.signal,
1929
+ getRunScope: (): RunBreakerScope => this.runScope,
1804
1930
  });
1805
1931
  this.registerCompiledToolNode(node);
1806
1932
  return node;
@@ -1927,6 +2053,25 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
1927
2053
  state: t.AgentSubgraphState,
1928
2054
  config?: RunnableConfig
1929
2055
  ): Promise<Partial<t.AgentSubgraphState>> => {
2056
+ /** Captured at node ENTRY, before any host-facing await (context-usage
2057
+ * dispatch, hooks): a sibling's trip can fail the run and a prompt
2058
+ * next run can reset the controller while this node is paused in one
2059
+ * of those awaits, and a later capture would bind this attempt to the
2060
+ * fresh controller. Trips and reason reads below stay on this
2061
+ * capture. */
2062
+ const attemptBreaker = this.breakerAbort;
2063
+ const attemptBreakerEpoch = this.breakerEpoch;
2064
+ /** Already-tripped-at-entry: a parallel sibling's breach has failed
2065
+ * the run before this node was scheduled. Rethrow before hooks or the
2066
+ * provider call — a custom provider that doesn't synchronously reject
2067
+ * an aborted signal would otherwise start another model request on a
2068
+ * failed run. */
2069
+ const entryTripReason = this.resolveTrippedBreakerReason(
2070
+ attemptBreaker.signal
2071
+ );
2072
+ if (entryTripReason != null) {
2073
+ throw entryTripReason;
2074
+ }
1930
2075
  const agentContext = this.agentContexts.get(agentId);
1931
2076
  if (!agentContext) {
1932
2077
  throw new Error(`Agent context not found for agentId: ${agentId}`);
@@ -3120,6 +3265,10 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
3120
3265
  let langfuseHandler: CallbackEntry | undefined;
3121
3266
  let invokeConfig = {
3122
3267
  ...config,
3268
+ /** The run-scoped breaker composed in, so a stream-limit trip in one
3269
+ * parallel agent node also cancels sibling nodes' in-flight model
3270
+ * calls, not only their subagents. */
3271
+ signal: composeAbortSignals(config.signal, attemptBreaker.signal),
3123
3272
  metadata: {
3124
3273
  ...(config.metadata ?? {}),
3125
3274
  ...traceMetadata,
@@ -3128,6 +3277,7 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
3128
3277
  * length cap, but scope trust (`isForeignScope`) needs the id
3129
3278
  * verbatim regardless of length. */
3130
3279
  agentId,
3280
+ [STREAM_LIMIT_EPOCH_KEY]: attemptBreakerEpoch,
3131
3281
  },
3132
3282
  };
3133
3283
  initializeLangfuseTracing(langfuse);
@@ -3166,6 +3316,18 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
3166
3316
  if (preInvokeContextOverflowError != null) {
3167
3317
  throw preInvokeContextOverflowError;
3168
3318
  }
3319
+ /** Rechecked after the pre-invoke awaits (context-usage dispatch,
3320
+ * hooks): a sibling can trip the breaker while this node is paused
3321
+ * in one of them, and a provider that doesn't synchronously reject
3322
+ * an aborted signal would still start the request. */
3323
+ {
3324
+ const preInvokeTrip = this.resolveTrippedBreakerReason(
3325
+ attemptBreaker.signal
3326
+ );
3327
+ if (preInvokeTrip != null) {
3328
+ throw preInvokeTrip;
3329
+ }
3330
+ }
3169
3331
  result = await withLangfuseRuntimeScope(
3170
3332
  resolveLangfuseRuntimeScope({
3171
3333
  runLangfuse: this.langfuse,
@@ -3189,6 +3351,30 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
3189
3351
  graph: this,
3190
3352
  metadata,
3191
3353
  });
3354
+ /**
3355
+ * A tripped stream circuit breaker is a deliberate abort, not a
3356
+ * provider failure: entering overflow recovery or the fallback chain
3357
+ * would spend more provider work after the safety limit fired, and a
3358
+ * succeeding fallback would resolve a run the public contract says
3359
+ * must reject. Rethrow before any recovery path.
3360
+ */
3361
+ if (primaryError instanceof StreamLimitExceededError) {
3362
+ /** Tripped before rethrowing so parallel agent nodes' in-flight
3363
+ * model calls and subagents stop while the rejection propagates. */
3364
+ attemptBreaker.abort(primaryError);
3365
+ throw primaryError;
3366
+ }
3367
+ /** A sibling that tripped the shared breaker aborts this branch's
3368
+ * composed signal, and some providers surface that as a generic
3369
+ * abort error; entering overflow planning or the fallback chain
3370
+ * would start new provider work after the run-wide breaker fired.
3371
+ * Rethrow the breaker's own stream-limit reason instead. */
3372
+ {
3373
+ const trippedReason = this.resolveTrippedBreakerReason(attemptBreaker.signal);
3374
+ if (trippedReason != null) {
3375
+ throw trippedReason;
3376
+ }
3377
+ }
3192
3378
  /**
3193
3379
  * A context overflow is a deterministic consequence of the payload,
3194
3380
  * not a provider being unavailable — so it is answered by compacting
@@ -3422,6 +3608,20 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
3422
3608
  })
3423
3609
  );
3424
3610
  } catch (fallbackError) {
3611
+ if (fallbackError instanceof StreamLimitExceededError) {
3612
+ /** Same treatment as the primary path: a fallback stream that
3613
+ * trips the breaker must stop parallel agent nodes' model calls
3614
+ * and subagents before the rejection propagates. */
3615
+ attemptBreaker.abort(fallbackError);
3616
+ throw fallbackError;
3617
+ }
3618
+ {
3619
+ /** Same sibling-abort translation guard as the primary catch. */
3620
+ const trippedReason = this.resolveTrippedBreakerReason(attemptBreaker.signal);
3621
+ if (trippedReason != null) {
3622
+ throw trippedReason;
3623
+ }
3624
+ }
3425
3625
  const overflowCandidates =
3426
3626
  getFallbackOverflowCandidates(fallbackError);
3427
3627
  let fallbackRecovery: OverflowRecoveryPlan | null = null;
@@ -3840,6 +4040,9 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
3840
4040
  const executor = new SubagentExecutor({
3841
4041
  configs: new Map(resolvedConfigs.map((c) => [c.type, c])),
3842
4042
  parentSignal: this.signal,
4043
+ breakerScope: {
4044
+ controller: (): AbortController => this.breakerAbort,
4045
+ },
3843
4046
  hookRegistry: this.hookRegistry,
3844
4047
  /** Lazy — Run wires the registry onto the graph AFTER
3845
4048
  * `createWorkflow()` runs, so a direct capture here would be
@@ -3850,6 +4053,7 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
3850
4053
  langfuse: this.langfuse,
3851
4054
  tokenCounter: agentContext.tokenCounter,
3852
4055
  usageSink: this.subagentUsageSink,
4056
+ streamLimits: this.streamLimits,
3853
4057
  maxDepth: effectiveSubagentDepth,
3854
4058
  createChildGraph: (input): StandardGraph => {
3855
4059
  const childGraph = new StandardGraph(input);
@@ -3912,11 +4116,19 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
3912
4116
  const toolCall = (config as { toolCall?: { id?: string } }).toolCall;
3913
4117
  const parentToolCallId =
3914
4118
  typeof toolCall?.id === 'string' ? toolCall.id : undefined;
4119
+ /** The parent tool batch's entry-captured scope (stamped by
4120
+ * ToolNode before PreToolUse hooks) — binds this child to the
4121
+ * run that dispatched it, not to whatever controller a reset
4122
+ * installed while the hooks were awaited. */
4123
+ const batchScope = config.configurable?.[
4124
+ RUN_BREAKER_SCOPE_CONFIG_KEY
4125
+ ] as RunBreakerScope | undefined;
3915
4126
  const result = await executor.execute({
3916
4127
  description,
3917
4128
  subagentType,
3918
4129
  threadId,
3919
4130
  parentToolCallId,
4131
+ breaker: batchScope?.controller,
3920
4132
  /**
3921
4133
  * Forward the parent's `configurable` so host-set fields
3922
4134
  * (`requestBody`, `user`, etc.) propagate into the child
@@ -4002,6 +4214,21 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
4002
4214
  }),
4003
4215
  });
4004
4216
 
4217
+ const readChargeCredits = ():
4218
+ | WeakMap<object, Map<string, number>>
4219
+ | undefined => this.streamLimitChargeCredits;
4220
+ const readBreakerEpoch = (): number => this.breakerEpoch;
4221
+ const readActiveGenerations = (): Set<string> | undefined =>
4222
+ this.activeStreamLimitGenerations;
4223
+ const writeActiveGenerations = (value: Set<string> | undefined): void => {
4224
+ this.activeStreamLimitGenerations = value;
4225
+ };
4226
+ const writeChargeCredits = (
4227
+ value: WeakMap<object, Map<string, number>> | undefined
4228
+ ): void => {
4229
+ this.streamLimitChargeCredits = value;
4230
+ };
4231
+
4005
4232
  const workflow = new StateGraph(StateAnnotation)
4006
4233
  .addNode(agentNode, this.createCallModel(agentId))
4007
4234
  .addNode(
@@ -4025,6 +4252,54 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
4025
4252
  runId: this.runId,
4026
4253
  isMultiAgent: this.isMultiAgentGraph(),
4027
4254
  hookRegistry: this.hookRegistry,
4255
+ /**
4256
+ * Live references (both maps are cleared in place, never
4257
+ * replaced), so summarization streams share the run's event
4258
+ * budget accounting under their own generation key.
4259
+ */
4260
+ streamLimits: this.streamLimits,
4261
+ streamDeltaEventCounts: this.streamDeltaEventCounts,
4262
+ streamedToolCallArgTallies: this.streamedToolCallArgTallies,
4263
+ /** Accessor pair, not a value copy: unlike the two maps above,
4264
+ * the credit map is REPLACED by graph resets rather than cleared
4265
+ * in place, and the guards' lazy `??=` must install onto the
4266
+ * graph — a copy held here would survive resets and grow one
4267
+ * attempt-stamped entry per compaction for a retained reused
4268
+ * chunk object. */
4269
+ get streamLimitChargeCredits() {
4270
+ return readChargeCredits();
4271
+ },
4272
+ set streamLimitChargeCredits(
4273
+ value: WeakMap<object, Map<string, number>> | undefined
4274
+ ) {
4275
+ writeChargeCredits(value);
4276
+ },
4277
+ /** Live epoch, so summary tallies are creation-tagged and the
4278
+ * resetValues grace sweep treats them like model-attempt
4279
+ * entries. */
4280
+ get breakerEpoch(): number {
4281
+ return readBreakerEpoch();
4282
+ },
4283
+ /** Accessor pair like the charge credits: summary attempts
4284
+ * lease their generations on the GRAPH's active set, and the
4285
+ * lazy `??=` in the lease helper must install there. */
4286
+ get activeStreamLimitGenerations(): Set<string> | undefined {
4287
+ return readActiveGenerations();
4288
+ },
4289
+ set activeStreamLimitGenerations(value: Set<string> | undefined) {
4290
+ writeActiveGenerations(value);
4291
+ },
4292
+ /** Read per attempt: a sibling branch tripping the run breaker
4293
+ * must also cancel in-flight summarization model calls. */
4294
+ getBreakerSignal: (): AbortSignal => this.breakerAbort.signal,
4295
+ /** The node captures this at entry so its own chunk handler's
4296
+ * breach trips the run that STARTED the summarization, not a
4297
+ * controller installed by a later reset. */
4298
+ getBreakerController: (): AbortController => this.breakerAbort,
4299
+ /** Captured at node entry and stamped into the summary attempt
4300
+ * metadata, so the wire consumer epoch-gates old-run summary
4301
+ * chunks exactly like model-attempt chunks. */
4302
+ getBreakerEpoch: (): number => this.breakerEpoch,
4028
4303
  dispatchRunStep: async (runStep, nodeConfig) => {
4029
4304
  const resolvedConfig = nodeConfig ?? this.config;
4030
4305
  if (runStep.agentId != null) {