@librechat/agents 3.3.11 → 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 (192) hide show
  1. package/dist/cjs/graphs/Graph.cjs +246 -25
  2. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  3. package/dist/cjs/llm/bedrock/index.cjs +13 -2
  4. package/dist/cjs/llm/bedrock/index.cjs.map +1 -1
  5. package/dist/cjs/llm/invoke.cjs +146 -36
  6. package/dist/cjs/llm/invoke.cjs.map +1 -1
  7. package/dist/cjs/llm/openai/index.cjs +2 -2
  8. package/dist/cjs/llm/openai/index.cjs.map +1 -1
  9. package/dist/cjs/llm/streamLimits.cjs +723 -0
  10. package/dist/cjs/llm/streamLimits.cjs.map +1 -0
  11. package/dist/cjs/main.cjs +8 -0
  12. package/dist/cjs/run.cjs +6 -2
  13. package/dist/cjs/run.cjs.map +1 -1
  14. package/dist/cjs/session/AgentSession.cjs +4 -1
  15. package/dist/cjs/session/AgentSession.cjs.map +1 -1
  16. package/dist/cjs/stream.cjs +103 -12
  17. package/dist/cjs/stream.cjs.map +1 -1
  18. package/dist/cjs/summarization/node.cjs +157 -37
  19. package/dist/cjs/summarization/node.cjs.map +1 -1
  20. package/dist/cjs/tools/BashExecutor.cjs +3 -2
  21. package/dist/cjs/tools/BashExecutor.cjs.map +1 -1
  22. package/dist/cjs/tools/CodeExecutor.cjs +4 -3
  23. package/dist/cjs/tools/CodeExecutor.cjs.map +1 -1
  24. package/dist/cjs/tools/ProgrammaticToolCalling.cjs +5 -3
  25. package/dist/cjs/tools/ProgrammaticToolCalling.cjs.map +1 -1
  26. package/dist/cjs/tools/ToolNode.cjs +76 -3
  27. package/dist/cjs/tools/ToolNode.cjs.map +1 -1
  28. package/dist/cjs/tools/ToolSearch.cjs +3 -2
  29. package/dist/cjs/tools/ToolSearch.cjs.map +1 -1
  30. package/dist/cjs/tools/search/crw-scraper.cjs +7 -1
  31. package/dist/cjs/tools/search/crw-scraper.cjs.map +1 -1
  32. package/dist/cjs/tools/search/crw-search.cjs +3 -1
  33. package/dist/cjs/tools/search/crw-search.cjs.map +1 -1
  34. package/dist/cjs/tools/search/firecrawl.cjs +7 -1
  35. package/dist/cjs/tools/search/firecrawl.cjs.map +1 -1
  36. package/dist/cjs/tools/search/keenable-scraper.cjs +7 -1
  37. package/dist/cjs/tools/search/keenable-scraper.cjs.map +1 -1
  38. package/dist/cjs/tools/search/keenable-search.cjs +3 -1
  39. package/dist/cjs/tools/search/keenable-search.cjs.map +1 -1
  40. package/dist/cjs/tools/search/rerankers.cjs +26 -8
  41. package/dist/cjs/tools/search/rerankers.cjs.map +1 -1
  42. package/dist/cjs/tools/search/search.cjs +30 -10
  43. package/dist/cjs/tools/search/search.cjs.map +1 -1
  44. package/dist/cjs/tools/search/serper-scraper.cjs +7 -1
  45. package/dist/cjs/tools/search/serper-scraper.cjs.map +1 -1
  46. package/dist/cjs/tools/search/tavily-scraper.cjs +7 -1
  47. package/dist/cjs/tools/search/tavily-scraper.cjs.map +1 -1
  48. package/dist/cjs/tools/search/tavily-search.cjs +3 -1
  49. package/dist/cjs/tools/search/tavily-search.cjs.map +1 -1
  50. package/dist/cjs/tools/search/tool.cjs +16 -2
  51. package/dist/cjs/tools/search/tool.cjs.map +1 -1
  52. package/dist/cjs/tools/subagent/SubagentExecutor.cjs +54 -3
  53. package/dist/cjs/tools/subagent/SubagentExecutor.cjs.map +1 -1
  54. package/dist/cjs/utils/index.cjs +1 -0
  55. package/dist/cjs/utils/misc.cjs +12 -0
  56. package/dist/cjs/utils/misc.cjs.map +1 -1
  57. package/dist/cjs/utils/proxy.cjs +63 -0
  58. package/dist/cjs/utils/proxy.cjs.map +1 -0
  59. package/dist/esm/graphs/Graph.mjs +245 -24
  60. package/dist/esm/graphs/Graph.mjs.map +1 -1
  61. package/dist/esm/llm/bedrock/index.mjs +13 -2
  62. package/dist/esm/llm/bedrock/index.mjs.map +1 -1
  63. package/dist/esm/llm/invoke.mjs +146 -36
  64. package/dist/esm/llm/invoke.mjs.map +1 -1
  65. package/dist/esm/llm/openai/index.mjs +2 -2
  66. package/dist/esm/llm/openai/index.mjs.map +1 -1
  67. package/dist/esm/llm/streamLimits.mjs +704 -0
  68. package/dist/esm/llm/streamLimits.mjs.map +1 -0
  69. package/dist/esm/main.mjs +4 -2
  70. package/dist/esm/run.mjs +6 -2
  71. package/dist/esm/run.mjs.map +1 -1
  72. package/dist/esm/session/AgentSession.mjs +4 -1
  73. package/dist/esm/session/AgentSession.mjs.map +1 -1
  74. package/dist/esm/stream.mjs +103 -12
  75. package/dist/esm/stream.mjs.map +1 -1
  76. package/dist/esm/summarization/node.mjs +157 -37
  77. package/dist/esm/summarization/node.mjs.map +1 -1
  78. package/dist/esm/tools/BashExecutor.mjs +3 -2
  79. package/dist/esm/tools/BashExecutor.mjs.map +1 -1
  80. package/dist/esm/tools/CodeExecutor.mjs +4 -3
  81. package/dist/esm/tools/CodeExecutor.mjs.map +1 -1
  82. package/dist/esm/tools/ProgrammaticToolCalling.mjs +5 -3
  83. package/dist/esm/tools/ProgrammaticToolCalling.mjs.map +1 -1
  84. package/dist/esm/tools/ToolNode.mjs +76 -3
  85. package/dist/esm/tools/ToolNode.mjs.map +1 -1
  86. package/dist/esm/tools/ToolSearch.mjs +3 -2
  87. package/dist/esm/tools/ToolSearch.mjs.map +1 -1
  88. package/dist/esm/tools/search/crw-scraper.mjs +7 -1
  89. package/dist/esm/tools/search/crw-scraper.mjs.map +1 -1
  90. package/dist/esm/tools/search/crw-search.mjs +3 -1
  91. package/dist/esm/tools/search/crw-search.mjs.map +1 -1
  92. package/dist/esm/tools/search/firecrawl.mjs +7 -1
  93. package/dist/esm/tools/search/firecrawl.mjs.map +1 -1
  94. package/dist/esm/tools/search/keenable-scraper.mjs +7 -1
  95. package/dist/esm/tools/search/keenable-scraper.mjs.map +1 -1
  96. package/dist/esm/tools/search/keenable-search.mjs +3 -1
  97. package/dist/esm/tools/search/keenable-search.mjs.map +1 -1
  98. package/dist/esm/tools/search/rerankers.mjs +26 -8
  99. package/dist/esm/tools/search/rerankers.mjs.map +1 -1
  100. package/dist/esm/tools/search/search.mjs +30 -10
  101. package/dist/esm/tools/search/search.mjs.map +1 -1
  102. package/dist/esm/tools/search/serper-scraper.mjs +7 -1
  103. package/dist/esm/tools/search/serper-scraper.mjs.map +1 -1
  104. package/dist/esm/tools/search/tavily-scraper.mjs +7 -1
  105. package/dist/esm/tools/search/tavily-scraper.mjs.map +1 -1
  106. package/dist/esm/tools/search/tavily-search.mjs +3 -1
  107. package/dist/esm/tools/search/tavily-search.mjs.map +1 -1
  108. package/dist/esm/tools/search/tool.mjs +16 -2
  109. package/dist/esm/tools/search/tool.mjs.map +1 -1
  110. package/dist/esm/tools/subagent/SubagentExecutor.mjs +54 -3
  111. package/dist/esm/tools/subagent/SubagentExecutor.mjs.map +1 -1
  112. package/dist/esm/utils/index.mjs +1 -0
  113. package/dist/esm/utils/misc.mjs +12 -1
  114. package/dist/esm/utils/misc.mjs.map +1 -1
  115. package/dist/esm/utils/proxy.mjs +62 -0
  116. package/dist/esm/utils/proxy.mjs.map +1 -0
  117. package/dist/types/graphs/Graph.d.ts +53 -1
  118. package/dist/types/index.d.ts +2 -0
  119. package/dist/types/llm/invoke.d.ts +25 -4
  120. package/dist/types/llm/openai/index.d.ts +3 -0
  121. package/dist/types/llm/streamLimits.d.ts +314 -0
  122. package/dist/types/run.d.ts +1 -0
  123. package/dist/types/summarization/node.d.ts +27 -2
  124. package/dist/types/tools/BashExecutor.d.ts +2 -2
  125. package/dist/types/tools/CodeExecutor.d.ts +3 -3
  126. package/dist/types/tools/ToolNode.d.ts +11 -1
  127. package/dist/types/tools/search/crw-scraper.d.ts +2 -0
  128. package/dist/types/tools/search/firecrawl.d.ts +2 -0
  129. package/dist/types/tools/search/keenable-scraper.d.ts +2 -0
  130. package/dist/types/tools/search/rerankers.d.ts +9 -5
  131. package/dist/types/tools/search/serper-scraper.d.ts +2 -0
  132. package/dist/types/tools/search/tavily-scraper.d.ts +2 -0
  133. package/dist/types/tools/search/types.d.ts +28 -33
  134. package/dist/types/tools/subagent/SubagentExecutor.d.ts +44 -0
  135. package/dist/types/types/graph.d.ts +7 -1
  136. package/dist/types/types/run.d.ts +46 -1
  137. package/dist/types/types/tools.d.ts +21 -0
  138. package/dist/types/utils/index.d.ts +1 -0
  139. package/dist/types/utils/misc.d.ts +7 -0
  140. package/dist/types/utils/proxy.d.ts +31 -0
  141. package/package.json +2 -1
  142. package/src/__tests__/stream.eagerArgsDivergence.test.ts +158 -0
  143. package/src/__tests__/stream.eagerEventExecution.test.ts +1 -0
  144. package/src/__tests__/stream.streamLimits.test.ts +1982 -0
  145. package/src/graphs/Graph.ts +302 -27
  146. package/src/graphs/__tests__/Graph.breakerLifecycle.test.ts +234 -0
  147. package/src/index.ts +11 -0
  148. package/src/llm/bedrock/index.ts +22 -2
  149. package/src/llm/invoke.streamLimits.test.ts +142 -0
  150. package/src/llm/invoke.test.ts +89 -1
  151. package/src/llm/invoke.ts +197 -20
  152. package/src/llm/openai/cacheWriteTokens.test.ts +112 -0
  153. package/src/llm/openai/index.ts +14 -6
  154. package/src/llm/streamLimits.test.ts +450 -0
  155. package/src/llm/streamLimits.ts +1158 -0
  156. package/src/run.ts +4 -0
  157. package/src/session/AgentSession.ts +5 -0
  158. package/src/stream.ts +141 -6
  159. package/src/summarization/__tests__/node.test.ts +269 -0
  160. package/src/summarization/chunkHandler.test.ts +196 -0
  161. package/src/summarization/node.ts +203 -6
  162. package/src/tools/BashExecutor.ts +4 -3
  163. package/src/tools/CodeExecutor.ts +5 -4
  164. package/src/tools/ProgrammaticToolCalling.ts +7 -5
  165. package/src/tools/ToolNode.ts +109 -6
  166. package/src/tools/ToolSearch.ts +4 -3
  167. package/src/tools/__tests__/BashExecutor.test.ts +2 -2
  168. package/src/tools/__tests__/ProgrammaticToolCalling.test.ts +2 -4
  169. package/src/tools/__tests__/SubagentExecutor.test.ts +126 -0
  170. package/src/tools/__tests__/ToolNode.breakerSignal.test.ts +389 -0
  171. package/src/tools/__tests__/ToolNode.streamLimits.test.ts +69 -0
  172. package/src/tools/search/crw-scraper.ts +6 -0
  173. package/src/tools/search/crw-search.ts +6 -1
  174. package/src/tools/search/firecrawl.ts +6 -0
  175. package/src/tools/search/http-agent.test.ts +133 -0
  176. package/src/tools/search/keenable-scraper.ts +6 -0
  177. package/src/tools/search/keenable-search.ts +6 -1
  178. package/src/tools/search/rerankers.ts +36 -10
  179. package/src/tools/search/search.ts +29 -11
  180. package/src/tools/search/serper-scraper.ts +6 -0
  181. package/src/tools/search/tavily-scraper.ts +6 -0
  182. package/src/tools/search/tavily-search.ts +2 -0
  183. package/src/tools/search/tool.ts +16 -0
  184. package/src/tools/search/types.ts +31 -33
  185. package/src/tools/subagent/SubagentExecutor.ts +96 -3
  186. package/src/types/graph.ts +7 -0
  187. package/src/types/run.ts +49 -1
  188. package/src/types/tools.ts +21 -0
  189. package/src/utils/index.ts +1 -0
  190. package/src/utils/misc.ts +19 -0
  191. package/src/utils/proxy.test.ts +176 -0
  192. package/src/utils/proxy.ts +93 -0
@@ -79,9 +79,26 @@ import {
79
79
  } from '@/tools/local';
80
80
  import { stripCodeSessionFileSummary } from '@/tools/CodeSessionFileSummary';
81
81
  import { Constants, GraphEvents, CODE_EXECUTION_TOOLS } from '@/common';
82
+ import type { RunBreakerScope } from '@/llm/streamLimits';
83
+ import {
84
+ StreamLimitExceededError,
85
+ RUN_BREAKER_SCOPE_CONFIG_KEY,
86
+ } from '@/llm/streamLimits';
87
+
88
+ /** Host-facing batch requests must not carry the batch's breaker scope —
89
+ * hosts spread `configurable` into their own run configs. */
90
+ function stripRunBreakerScope(
91
+ configurable: Record<string, unknown> | undefined
92
+ ): Record<string, unknown> | undefined {
93
+ if (configurable == null || !(RUN_BREAKER_SCOPE_CONFIG_KEY in configurable)) {
94
+ return configurable;
95
+ }
96
+ const { [RUN_BREAKER_SCOPE_CONFIG_KEY]: _scope, ...rest } = configurable;
97
+ return rest;
98
+ }
82
99
  import { convertInjectedMessages } from '@/messages/injected';
83
100
  import { safeDispatchCustomEvent } from '@/utils/events';
84
- import { RunnableCallable } from '@/utils';
101
+ import { RunnableCallable, composeAbortSignals } from '@/utils';
85
102
  import { executeHooks } from '@/hooks';
86
103
 
87
104
  /**
@@ -636,6 +653,10 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
636
653
  private toolOutputRegistry?: ToolOutputReferenceRegistry;
637
654
  /** Run-scoped selection for swapping remote code tools to local executors. */
638
655
  private toolExecution?: t.ToolExecutionConfig;
656
+ /** Owning graph's run-scoped breaker signal, composed into each batch's config. */
657
+ private getBreakerSignal?: () => AbortSignal | undefined;
658
+ /** Owning graph's immutable run scope; captured once per batch. */
659
+ private getRunScope?: () => RunBreakerScope;
639
660
  /**
640
661
  * Monotonic counter used to mint a unique scope id for anonymous
641
662
  * batches (ones invoked without a `run_id` in
@@ -677,6 +698,8 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
677
698
  toolOutputRegistry,
678
699
  toolExecution,
679
700
  fileCheckpointer,
701
+ getBreakerSignal,
702
+ getRunScope,
680
703
  }: t.ToolNodeConstructorParams) {
681
704
  super({
682
705
  name: name ?? TOOL_NODE_RUN_NAME,
@@ -720,6 +743,8 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
720
743
  this.hookRegistry = hookRegistry;
721
744
  this.humanInTheLoop = humanInTheLoop;
722
745
  this.toolExecution = toolExecution;
746
+ this.getBreakerSignal = getBreakerSignal;
747
+ this.getRunScope = getRunScope;
723
748
  // Caller-provided checkpointer wins. Graphs use this to share a
724
749
  // single per-Run instance across every ToolNode they compile so
725
750
  // `Run.rewindFiles()` reaches the same snapshot store regardless
@@ -1212,6 +1237,10 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
1212
1237
  (config.configurable?.writer as ToolRuntime<T>['writer']) ??
1213
1238
  null,
1214
1239
  };
1240
+ /** A sibling can trip the breaker while the PreToolUse hooks above
1241
+ * are awaited; a tool that never inspects its runtime signal would
1242
+ * still run. Recheck at the last moment before execution. */
1243
+ this.throwIfBreakerTripped(config);
1215
1244
  const output = await tool.invoke(invokeParams, runtime);
1216
1245
  if (isCommand(output)) {
1217
1246
  return output;
@@ -1323,6 +1352,14 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
1323
1352
  if (isGraphInterrupt(e)) {
1324
1353
  throw e;
1325
1354
  }
1355
+ /**
1356
+ * A stream circuit breaker tripped by a child run (subagent) is a
1357
+ * safety abort, not a tool failure to report back to the model. It
1358
+ * passes through like an interrupt so the whole run rejects.
1359
+ */
1360
+ if (e instanceof StreamLimitExceededError) {
1361
+ throw e;
1362
+ }
1326
1363
  if (this.errorHandler) {
1327
1364
  try {
1328
1365
  const dispatched = await this.errorHandler(
@@ -2237,11 +2274,27 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
2237
2274
  * 4. Injected messages from results are collected and returned alongside
2238
2275
  * ToolMessages (appended AFTER to respect provider ordering).
2239
2276
  */
2277
+ /** Rethrows a stream-limit trip carried on the batch config's composed
2278
+ * signal. Rechecked at each later execution/dispatch stage because a tool
2279
+ * that ignores cancellation can complete normally across the trip, and
2280
+ * the next stage would otherwise start fresh side effects on a failed
2281
+ * run. */
2282
+ private throwIfBreakerTripped(config: RunnableConfig): void {
2283
+ const signal = config.signal;
2284
+ if (
2285
+ signal?.aborted === true &&
2286
+ signal.reason instanceof StreamLimitExceededError
2287
+ ) {
2288
+ throw signal.reason;
2289
+ }
2290
+ }
2291
+
2240
2292
  private async dispatchToolEvents(
2241
2293
  toolCalls: ToolCall[],
2242
2294
  config: RunnableConfig,
2243
2295
  batchContext: DispatchBatchContext = {}
2244
2296
  ): Promise<{ toolMessages: ToolMessage[]; injected: BaseMessage[] }> {
2297
+ this.throwIfBreakerTripped(config);
2245
2298
  const {
2246
2299
  batchIndices,
2247
2300
  turn,
@@ -2941,9 +2994,12 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
2941
2994
  dispatchRequests.map((request) => [request.id, request])
2942
2995
  );
2943
2996
  const onResult = (result: t.ToolExecuteResult): void => {
2997
+ /** Runtime-honest widening: hosts may omit the id despite the
2998
+ * field type. */
2999
+ const resultToolCallId = result.toolCallId as string | undefined;
2944
3000
  const request =
2945
- result.toolCallId != null
2946
- ? dispatchRequestById.get(result.toolCallId)
3001
+ resultToolCallId != null
3002
+ ? dispatchRequestById.get(resultToolCallId)
2947
3003
  : undefined;
2948
3004
  if (
2949
3005
  request == null ||
@@ -2966,6 +3022,12 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
2966
3022
  );
2967
3023
  };
2968
3024
 
3025
+ /** The approval hooks above are awaited; a sibling's trip during them
3026
+ * must stop the approved batch before it reaches a host handler that
3027
+ * may never inspect the request signal. */
3028
+ if (dispatchRequests.length > 0) {
3029
+ this.throwIfBreakerTripped(config);
3030
+ }
2969
3031
  const dispatchPromise =
2970
3032
  dispatchRequests.length === 0
2971
3033
  ? Promise.resolve([] as t.ToolExecuteResult[])
@@ -2988,12 +3050,13 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
2988
3050
  // the eager path sends `agentContext.agentId` — this must
2989
3051
  // match it at the top level too.
2990
3052
  agentId: this.executingAgentId,
2991
- configurable: config.configurable as
2992
- | Record<string, unknown>
2993
- | undefined,
3053
+ configurable: stripRunBreakerScope(
3054
+ config.configurable as Record<string, unknown> | undefined
3055
+ ),
2994
3056
  metadata: config.metadata as
2995
3057
  | Record<string, unknown>
2996
3058
  | undefined,
3059
+ signal: config.signal,
2997
3060
  resolve: (results): void => {
2998
3061
  resultSettled = true;
2999
3062
  settledResults = results;
@@ -3678,6 +3741,11 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
3678
3741
  outputs[i] = interruptingOutputs[k];
3679
3742
  });
3680
3743
 
3744
+ /** The breaker can trip while the interrupting group is awaited — e.g.
3745
+ * a tool that ignores cancellation and completes normally. Recheck
3746
+ * before starting the non-idempotent siblings. */
3747
+ this.throwIfBreakerTripped(config);
3748
+
3681
3749
  // No interrupting call suspended — safe to run the remaining siblings.
3682
3750
  const regularOutputs = await Promise.all(
3683
3751
  regularPositions.map((i) => runOne(directCalls[i], i))
@@ -3718,6 +3786,41 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
3718
3786
 
3719
3787
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
3720
3788
  protected async run(input: any, config: RunnableConfig): Promise<T> {
3789
+ /**
3790
+ * Breaker read once at batch entry: every downstream path (direct
3791
+ * `tool.invoke` runtimes and the ON_TOOL_EXECUTE dispatch) inherits this
3792
+ * config, and a graph reset mid-batch must not detach in-flight tools
3793
+ * from an already-tripped signal.
3794
+ */
3795
+ const breakerSignal = this.getBreakerSignal?.();
3796
+ const composedSignal = composeAbortSignals(config.signal, breakerSignal);
3797
+ /** Immutable batch scope, captured with the signal BEFORE hooks: tools
3798
+ * that spawn runs (subagents) bind their child to this controller, so a
3799
+ * reset during a PreToolUse hook cannot hand them the new run's one. */
3800
+ const batchRunScope = this.getRunScope?.();
3801
+ if (composedSignal !== config.signal || batchRunScope != null) {
3802
+ config = {
3803
+ ...config,
3804
+ signal: composedSignal,
3805
+ ...(batchRunScope != null && {
3806
+ configurable: {
3807
+ ...config.configurable,
3808
+ [RUN_BREAKER_SCOPE_CONFIG_KEY]: batchRunScope,
3809
+ },
3810
+ }),
3811
+ };
3812
+ }
3813
+ /** Already-tripped-at-entry: a parallel sibling's breach failed the run
3814
+ * before this batch was scheduled. Rethrow before hooks, direct
3815
+ * `tool.invoke` calls, or ON_TOOL_EXECUTE dispatch — a tool or host
3816
+ * handler that doesn't synchronously inspect an aborted signal would
3817
+ * otherwise perform side effects on a failed run. */
3818
+ if (
3819
+ composedSignal?.aborted === true &&
3820
+ composedSignal.reason instanceof StreamLimitExceededError
3821
+ ) {
3822
+ throw composedSignal.reason;
3823
+ }
3721
3824
  this.toolCallTurns.clear();
3722
3825
  /**
3723
3826
  * Per-batch local map for resolved (post-substitution) args.
@@ -21,7 +21,7 @@ function getBM25Function(): BM25Fn {
21
21
 
22
22
  const BM25 = getBM25Function();
23
23
  import fetch, { RequestInit } from 'node-fetch';
24
- import { HttpsProxyAgent } from 'https-proxy-agent';
24
+ import { resolveFetchProxyAgent } from '@/utils/proxy';
25
25
  import { tool, DynamicStructuredTool } from '@langchain/core/tools';
26
26
  import type * as t from '@/types';
27
27
  import { INTENT_PROPERTY } from '@/tools/intentArg';
@@ -1102,8 +1102,9 @@ ${mcpNote}${toolsListSection}
1102
1102
  body: JSON.stringify(postData),
1103
1103
  };
1104
1104
 
1105
- if (process.env.PROXY != null && process.env.PROXY !== '') {
1106
- fetchOptions.agent = new HttpsProxyAgent(process.env.PROXY);
1105
+ const proxyAgent = resolveFetchProxyAgent(EXEC_ENDPOINT);
1106
+ if (proxyAgent) {
1107
+ fetchOptions.agent = proxyAgent;
1107
1108
  }
1108
1109
 
1109
1110
  const response = await fetch(EXEC_ENDPOINT, fetchOptions);
@@ -24,9 +24,9 @@ describe('buildBashExecutionToolDescription', () => {
24
24
  expect(BashExecutionToolDescription).toContain('heredoc/printf');
25
25
  expect(BashExecutionToolDescription).toContain('not bare Python');
26
26
  expect(BashExecutionToolDescription).toContain(
27
- 'failed executions do not register new files'
27
+ 'Failed executions register nothing'
28
28
  );
29
- expect(BashExecutionToolDescription).toContain('not later-call storage');
29
+ expect(BashExecutionToolDescription).toContain('`/tmp` never survives the call');
30
30
  });
31
31
 
32
32
  it('appends the tool-output references guide when enabled', () => {
@@ -51,10 +51,8 @@ describe('ProgrammaticToolCalling', () => {
51
51
  expect(description).toContain('raw=$(tool');
52
52
  expect(description).toContain('direct tool > file may be empty');
53
53
  expect(description).toContain('/mnt/data/sf.json');
54
- expect(description).toContain(
55
- 'failed executions do not register new files'
56
- );
57
- expect(description).toContain('not later-call storage');
54
+ expect(description).toContain('Failed executions register nothing');
55
+ expect(description).toContain('`/tmp` never survives the call');
58
56
  });
59
57
  });
60
58
 
@@ -20,6 +20,7 @@ import {
20
20
  } from '../subagent';
21
21
  import { sanitizeForwardedSubagentUpdateData } from '../subagent/SubagentExecutor';
22
22
  import { Constants, Providers, GraphEvents, StepTypes } from '@/common';
23
+ import { StreamLimitExceededError } from '@/llm/streamLimits';
23
24
  import { AgentContext } from '@/agents/AgentContext';
24
25
  import { HookRegistry } from '@/hooks/HookRegistry';
25
26
  import { HandlerRegistry } from '@/events';
@@ -451,6 +452,131 @@ describe('SubagentExecutor', () => {
451
452
  expect(result.messages).toEqual([]);
452
453
  });
453
454
 
455
+ it('rethrows a child stream-limit trip instead of converting it to a tool result', async () => {
456
+ const executor = createExecutor({
457
+ createChildGraph: (): StandardGraph =>
458
+ ({
459
+ createWorkflow: (): { invoke: jest.Mock } => ({
460
+ invoke: jest.fn().mockRejectedValue(
461
+ new StreamLimitExceededError({
462
+ kind: 'tool_call_args',
463
+ limit: 10,
464
+ observed: 11,
465
+ toolName: 'db_query',
466
+ })
467
+ ),
468
+ }),
469
+ clearHeavyState: jest.fn(),
470
+ }) as unknown as StandardGraph,
471
+ });
472
+ await expect(
473
+ executor.execute({ description: 'Do something', subagentType: 'researcher' })
474
+ ).rejects.toBeInstanceOf(StreamLimitExceededError);
475
+ });
476
+
477
+ it('binds the child to the batch-captured controller, not the live scope', async () => {
478
+ const batchBreaker = new AbortController();
479
+ const liveBreaker = new AbortController();
480
+ const executor = createExecutor({
481
+ breakerScope: { controller: () => liveBreaker },
482
+ createChildGraph: (): StandardGraph =>
483
+ ({
484
+ createWorkflow: (): { invoke: jest.Mock } => ({
485
+ invoke: jest.fn().mockRejectedValue(
486
+ new StreamLimitExceededError({
487
+ kind: 'tool_call_args',
488
+ limit: 10,
489
+ observed: 11,
490
+ toolName: 'db_query',
491
+ })
492
+ ),
493
+ }),
494
+ clearHeavyState: jest.fn(),
495
+ }) as unknown as StandardGraph,
496
+ });
497
+
498
+ /** `breaker` is the controller the parent TOOL BATCH captured before
499
+ * PreToolUse hooks; a reset during those hooks replaced the live
500
+ * scope, and the child must not revive on — or trip — the new run's
501
+ * controller. */
502
+ await expect(
503
+ executor.execute({
504
+ description: 'Do something',
505
+ subagentType: 'researcher',
506
+ breaker: batchBreaker,
507
+ })
508
+ ).rejects.toBeInstanceOf(StreamLimitExceededError);
509
+ expect(batchBreaker.signal.aborted).toBe(true);
510
+ expect(liveBreaker.signal.aborted).toBe(false);
511
+ });
512
+
513
+ it('trips the breaker controller captured at execution start, not the current one', async () => {
514
+ const oldRun = new AbortController();
515
+ const newRun = new AbortController();
516
+ let current = oldRun;
517
+ let releaseChild: (() => void) | undefined;
518
+ const executor = createExecutor({
519
+ breakerScope: { controller: () => current },
520
+ createChildGraph: (): StandardGraph =>
521
+ ({
522
+ createWorkflow: (): { invoke: jest.Mock } => ({
523
+ invoke: jest.fn().mockImplementation(
524
+ () =>
525
+ new Promise((_resolve, reject) => {
526
+ releaseChild = (): void =>
527
+ reject(
528
+ new StreamLimitExceededError({
529
+ kind: 'tool_call_args',
530
+ limit: 10,
531
+ observed: 11,
532
+ toolName: 'db_query',
533
+ })
534
+ );
535
+ })
536
+ ),
537
+ }),
538
+ clearHeavyState: jest.fn(),
539
+ }) as unknown as StandardGraph,
540
+ });
541
+
542
+ const pending = executor.execute({
543
+ description: 'Do something',
544
+ subagentType: 'researcher',
545
+ });
546
+ while (releaseChild == null) {
547
+ await new Promise((resolve) => setTimeout(resolve, 0));
548
+ }
549
+ /** The failed run's reset installs a fresh controller while the child
550
+ * is still settling; the straggler's trip must land on the controller
551
+ * captured when its execution started. */
552
+ current = newRun;
553
+ releaseChild();
554
+ await expect(pending).rejects.toBeInstanceOf(StreamLimitExceededError);
555
+ expect(oldRun.signal.aborted).toBe(true);
556
+ expect(newRun.signal.aborted).toBe(false);
557
+ });
558
+
559
+ it('threads run-level streamLimits into every child graph input', async () => {
560
+ const childGraphInputs: StandardGraphInput[] = [];
561
+ const noopFactory = makeNoopGraphFactory();
562
+ const executor = createExecutor({
563
+ streamLimits: { maxToolCallArgBytes: 1234, maxDeltaEventsPerTurn: 9 },
564
+ createChildGraph: (input: StandardGraphInput): StandardGraph => {
565
+ childGraphInputs.push(input);
566
+ return noopFactory();
567
+ },
568
+ });
569
+ await executor.execute({
570
+ description: 'Do something',
571
+ subagentType: 'researcher',
572
+ });
573
+ expect(childGraphInputs).toHaveLength(1);
574
+ expect(childGraphInputs[0].streamLimits).toEqual({
575
+ maxToolCallArgBytes: 1234,
576
+ maxDeltaEventsPerTurn: 9,
577
+ });
578
+ });
579
+
454
580
  it('executes child graph and returns filtered content', async () => {
455
581
  const { factory, clearHeavyState } = makeStubGraphFactory({
456
582
  messages: [