@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
@@ -6,6 +6,7 @@ import {
6
6
  } from '@langchain/core/messages';
7
7
  import type { UsageMetadata, BaseMessage } from '@langchain/core/messages';
8
8
  import type { RunnableConfig } from '@langchain/core/runnables';
9
+ import type { StreamLimitState } from '@/llm/streamLimits';
9
10
  import type { AgentContext } from '@/agents/AgentContext';
10
11
  import type { HookRegistry } from '@/hooks';
11
12
  import type { OnChunk } from '@/llm/invoke';
@@ -33,6 +34,11 @@ import {
33
34
  Providers,
34
35
  } from '@/common';
35
36
  import { safeDispatchCustomEvent, emitAgentLog } from '@/utils/events';
37
+ import {
38
+ enforceStreamLimitsForWireChunk,
39
+ StreamLimitExceededError,
40
+ STREAM_LIMIT_EPOCH_KEY,
41
+ } from '@/llm/streamLimits';
36
42
  import { attemptInvoke, tryFallbackProviders } from '@/llm/invoke';
37
43
  import { calculateMaxToolResultChars } from '@/utils/truncation';
38
44
  import { createRemoveAllMessage } from '@/messages/reducer';
@@ -611,6 +617,8 @@ async function executeSummarizationWithFallback(params: {
611
617
  stepId: string;
612
618
  usePromptCache: boolean;
613
619
  log: LogFn;
620
+ /** Carries the run's stream limits so the event cap covers summary streams. */
621
+ graph?: StreamLimitState & { getBreakerSignal?: () => AbortSignal };
614
622
  }): Promise<{
615
623
  text: string;
616
624
  usage?: Partial<UsageMetadata>;
@@ -629,6 +637,7 @@ async function executeSummarizationWithFallback(params: {
629
637
  stepId,
630
638
  usePromptCache,
631
639
  log,
640
+ graph,
632
641
  } = params;
633
642
 
634
643
  const priorSummaryText = agentContext.getSummaryText()?.trim() ?? '';
@@ -659,6 +668,7 @@ async function executeSummarizationWithFallback(params: {
659
668
  stepId,
660
669
  provider: clientConfig.provider as Providers,
661
670
  reasoningKey: agentContext.reasoningKey,
671
+ graph,
662
672
  usePromptCache,
663
673
  promptCacheTtl:
664
674
  (clientConfig.provider as Providers) === Providers.ANTHROPIC ||
@@ -686,6 +696,34 @@ async function executeSummarizationWithFallback(params: {
686
696
  messagesToRefineCount: messages.length,
687
697
  });
688
698
 
699
+ /**
700
+ * A tripped stream circuit breaker is a safety abort, not a
701
+ * summarization failure to recover from: retrying on fallback providers
702
+ * would spend more provider work after the limit fired, and degrading to
703
+ * the metadata stub would let the run continue (and periodic compaction
704
+ * commit the stub) despite the `StreamLimits` contract that a breach
705
+ * rejects the run. Rethrown so the summarize node fails the run with
706
+ * the actionable limit error, consistent with agent turns and subagents.
707
+ */
708
+ if (primaryError instanceof StreamLimitExceededError) {
709
+ throw primaryError;
710
+ }
711
+ /** A parallel branch's trip aborts the composed summarization signal,
712
+ * and a provider can surface that as a generic abort error; entering
713
+ * fallback recovery or degrading to the metadata stub would continue
714
+ * work after the run-wide safety abort. Checked on BOTH channels: the
715
+ * graph's own breaker, and the composed config signal that carries a
716
+ * parent run's trip into subagent child graphs. */
717
+ {
718
+ const trippedReason = findStreamLimitAbortReason(
719
+ graph?.getBreakerSignal?.(),
720
+ summarizeConfig?.signal
721
+ );
722
+ if (trippedReason != null) {
723
+ throw trippedReason;
724
+ }
725
+ }
726
+
689
727
  const rawFallbacks = (
690
728
  clientConfig.clientOptions as unknown as t.LLMConfig | undefined
691
729
  )?.fallbacks;
@@ -697,6 +735,7 @@ async function executeSummarizationWithFallback(params: {
697
735
  config: traceConfig(summarizeConfig, 'cache_hit_compaction'),
698
736
  provider: clientConfig.provider as Providers,
699
737
  reasoningKey: agentContext.reasoningKey,
738
+ graph,
700
739
  });
701
740
  const fbResult = await tryFallbackProviders({
702
741
  fallbacks,
@@ -711,9 +750,13 @@ async function executeSummarizationWithFallback(params: {
711
750
  )
712
751
  ),
713
752
  ],
714
- config: traceConfig(summarizeConfig, 'cache_hit_compaction'),
753
+ config: withBreakerSignal(
754
+ traceConfig(summarizeConfig, 'cache_hit_compaction'),
755
+ graph
756
+ ),
715
757
  primaryError,
716
758
  onChunk,
759
+ streamLimitState: graph,
717
760
  });
718
761
  const fbMsg = fbResult?.messages?.[0];
719
762
  if (fbMsg) {
@@ -722,6 +765,9 @@ async function executeSummarizationWithFallback(params: {
722
765
  );
723
766
  }
724
767
  } catch (fbErr) {
768
+ if (fbErr instanceof StreamLimitExceededError) {
769
+ throw fbErr;
770
+ }
725
771
  const fbDescribed = describeFallbackError(fbErr, fallbacks);
726
772
  log('warn', `Fallback providers also failed ${fbDescribed.suffix}`, {
727
773
  ...fbDescribed.data,
@@ -858,13 +904,64 @@ interface CreateSummarizeNodeParams {
858
904
  result: t.StepCompleted,
859
905
  config?: RunnableConfig
860
906
  ) => Promise<void>;
861
- };
907
+ /** The run's shared breaker signal, composed into every summarization
908
+ * model attempt so a sibling branch tripping a stream limit also
909
+ * cancels in-flight summaries. */
910
+ getBreakerSignal?: () => AbortSignal;
911
+ /** The run's shared breaker controller. Preferred over the bare signal
912
+ * accessor: the node captures it at entry so a breach detected by its
913
+ * own chunk handler trips the run that STARTED the summarization. */
914
+ getBreakerController?: () => AbortController;
915
+ /** The controller's epoch, captured at entry and stamped into summary
916
+ * attempt metadata so the wire consumer epoch-gates old-run summary
917
+ * chunks like model-attempt chunks. */
918
+ getBreakerEpoch?: () => number;
919
+ } & StreamLimitState;
862
920
  generateStepId: (stepKey: string) => [string, number];
863
921
  }
864
922
 
923
+ /** Composes the run's breaker signal (when the adapter provides one) into a
924
+ * summarization attempt's config, so breaches in sibling branches cancel
925
+ * summaries too. */
926
+ function withBreakerSignal<T extends { signal?: AbortSignal }>(
927
+ config: T | undefined,
928
+ graph?: { getBreakerSignal?: () => AbortSignal }
929
+ ): T | undefined {
930
+ const breakerSignal = graph?.getBreakerSignal?.();
931
+ if (breakerSignal == null) {
932
+ return config;
933
+ }
934
+ if (config == null) {
935
+ return { signal: breakerSignal } as T;
936
+ }
937
+ const existing = config.signal;
938
+ if (existing == null || existing === breakerSignal) {
939
+ return { ...config, signal: breakerSignal };
940
+ }
941
+ return { ...config, signal: AbortSignal.any([existing, breakerSignal]) };
942
+ }
943
+
944
+ /** First stream-limit reason found on any of the given signals. Checked
945
+ * alongside the graph's own breaker because a subagent child graph receives
946
+ * a ROOT sibling's trip through its constructor/invocation signal — the
947
+ * child's private breaker never fires for it. */
948
+ function findStreamLimitAbortReason(
949
+ ...signals: Array<AbortSignal | undefined>
950
+ ): StreamLimitExceededError | undefined {
951
+ for (const signal of signals) {
952
+ if (
953
+ signal?.aborted === true &&
954
+ signal.reason instanceof StreamLimitExceededError
955
+ ) {
956
+ return signal.reason;
957
+ }
958
+ }
959
+ return undefined;
960
+ }
961
+
865
962
  export function createSummarizeNode({
866
963
  agentContext,
867
- graph,
964
+ graph: adapterGraph,
868
965
  generateStepId,
869
966
  }: CreateSummarizeNodeParams) {
870
967
  return async (
@@ -874,10 +971,45 @@ export function createSummarizeNode({
874
971
  },
875
972
  config?: RunnableConfig
876
973
  ): Promise<{ summarizationRequest: undefined; messages?: BaseMessage[] }> => {
974
+ /** The breaker signal is captured at node ENTRY, before dispatchRunStep,
975
+ * ON_SUMMARIZE_START, or PreCompact awaits: a sibling's trip plus a
976
+ * prompt next run can reset the controller during one of those awaits,
977
+ * and resolving the live accessor later would bind this summarization
978
+ * to the fresh controller. `Object.create` freezes the signal while
979
+ * DELEGATING every other adapter member — spreading would snapshot the
980
+ * charge-credit accessor pair and detach it from graph resets. */
981
+ const entryBreakerController = adapterGraph.getBreakerController?.();
982
+ const entryBreakerSignal =
983
+ entryBreakerController?.signal ?? adapterGraph.getBreakerSignal?.();
984
+ const entryBreakerEpoch = adapterGraph.getBreakerEpoch?.();
985
+ const graph =
986
+ entryBreakerSignal == null
987
+ ? adapterGraph
988
+ : (Object.create(adapterGraph, {
989
+ getBreakerSignal: { value: (): AbortSignal => entryBreakerSignal },
990
+ ...(entryBreakerController != null && {
991
+ getBreakerController: {
992
+ value: (): AbortController => entryBreakerController,
993
+ },
994
+ }),
995
+ }) as typeof adapterGraph);
877
996
  const request = state.summarizationRequest;
878
997
  if (request == null) {
879
998
  return { summarizationRequest: undefined };
880
999
  }
1000
+ /** Already-tripped-at-entry: a parallel sibling's breach failed the run
1001
+ * before this node was scheduled — via this graph's own breaker or, in
1002
+ * a subagent child graph, the parent trip on the invocation signal.
1003
+ * Rethrow before dispatching steps, hooks, or the model call — an
1004
+ * adapter that doesn't synchronously reject an aborted signal would
1005
+ * otherwise spend summarization provider work on a failed run. */
1006
+ const entryTripReason = findStreamLimitAbortReason(
1007
+ entryBreakerSignal,
1008
+ config?.signal
1009
+ );
1010
+ if (entryTripReason != null) {
1011
+ throw entryTripReason;
1012
+ }
881
1013
 
882
1014
  /**
883
1015
  * Overflow recovery routes through this node purely to get back to the
@@ -1111,10 +1243,31 @@ export function createSummarizeNode({
1111
1243
  ...(clientConfig.modelName != null && clientConfig.modelName !== ''
1112
1244
  ? { [Constants.INVOKED_MODEL]: clientConfig.modelName }
1113
1245
  : {}),
1246
+ /** Entry-captured breaker epoch: the wire consumer epoch-gates
1247
+ * old-run summary chunks exactly like model-attempt chunks —
1248
+ * without the stamp, a straggling summary from a failed run
1249
+ * reads as current and could abort the next run's controller. */
1250
+ ...(entryBreakerEpoch != null
1251
+ ? { [STREAM_LIMIT_EPOCH_KEY]: entryBreakerEpoch }
1252
+ : {}),
1114
1253
  },
1115
1254
  }
1116
1255
  : undefined;
1117
1256
 
1257
+ /** Rechecked after the pre-call awaits (dispatchRunStep,
1258
+ * ON_SUMMARIZE_START, PreCompact): a sibling can trip the breaker while
1259
+ * this node is paused in one of them, and a provider that doesn't
1260
+ * synchronously reject an aborted signal would still start the
1261
+ * summarization call. Mirrors the model node's pre-invoke recheck. */
1262
+ {
1263
+ const preCallTrip = findStreamLimitAbortReason(
1264
+ entryBreakerSignal,
1265
+ config?.signal
1266
+ );
1267
+ if (preCallTrip != null) {
1268
+ throw preCallTrip;
1269
+ }
1270
+ }
1118
1271
  const {
1119
1272
  text: rawText,
1120
1273
  usage: summaryUsage,
@@ -1127,6 +1280,7 @@ export function createSummarizeNode({
1127
1280
  stepId,
1128
1281
  usePromptCache: isSelfSummarizeModel && hasPromptCache,
1129
1282
  log,
1283
+ graph,
1130
1284
  });
1131
1285
 
1132
1286
  /**
@@ -1331,21 +1485,58 @@ function buildSummarizationInstruction(
1331
1485
  }
1332
1486
 
1333
1487
  /** Creates an `onChunk` callback that dispatches `ON_SUMMARIZE_DELTA` events for streaming. */
1334
- function createSummarizationChunkHandler({
1488
+ export function createSummarizationChunkHandler({
1335
1489
  stepId,
1336
1490
  config,
1337
1491
  provider,
1338
1492
  reasoningKey = 'reasoning_content',
1493
+ graph,
1339
1494
  }: {
1340
1495
  stepId?: string;
1341
1496
  config?: RunnableConfig;
1342
1497
  provider?: Providers;
1343
1498
  reasoningKey?: 'reasoning_content' | 'reasoning';
1499
+ graph?: StreamLimitState & { getBreakerController?: () => AbortController };
1344
1500
  }): OnChunk | undefined {
1345
1501
  if (stepId == null || stepId === '' || !config) {
1346
1502
  return undefined;
1347
1503
  }
1348
- return (chunk) => {
1504
+ const limitMetadata = config.metadata as Record<string, unknown> | undefined;
1505
+ return (chunk, attemptMetadata) => {
1506
+ /**
1507
+ * Charged as `producer` so it PAIRS with the run's registered stream
1508
+ * handler: inside a live run the provider callbacks route each chunk
1509
+ * through the graph's `on_chat_model_stream` consumer too, and two
1510
+ * same-side consumer claims would charge the chunk twice — halving an
1511
+ * opt-in event cap. Standalone callers (tests, custom hosts, runs with
1512
+ * no registered handler) still get single-sided enforcement from this
1513
+ * one claim. Summarization deliberately passes NO `context` to
1514
+ * `attemptInvoke`, whose onChunk branch would otherwise add a second
1515
+ * producer claim. A breach trips the run's shared breaker (the
1516
+ * ENTRY-captured controller when the adapter provides one) before
1517
+ * rethrowing: this producer claim wins the race, so the wire consumer's
1518
+ * breaker-aborting catch never fires for the same chunk — without the
1519
+ * trip here, sibling model calls, tools, and subagents would keep
1520
+ * consuming quota while the summary branch rejects.
1521
+ * `executeSummarizationWithFallback` then rethrows past fallbacks and
1522
+ * the metadata stub, failing the run like any other breach.
1523
+ */
1524
+ if (graph != null) {
1525
+ try {
1526
+ enforceStreamLimitsForWireChunk({
1527
+ graph,
1528
+ metadata: attemptMetadata ?? limitMetadata,
1529
+ chunk: chunk as Parameters<
1530
+ typeof enforceStreamLimitsForWireChunk
1531
+ >[0]['chunk'],
1532
+ });
1533
+ } catch (error) {
1534
+ if (error instanceof StreamLimitExceededError) {
1535
+ graph.getBreakerController?.().abort(error);
1536
+ }
1537
+ throw error;
1538
+ }
1539
+ }
1349
1540
  const chunkAny = chunk as Parameters<typeof getChunkContent>[0]['chunk'];
1350
1541
  const raw = getChunkContent({ chunk: chunkAny, provider, reasoningKey });
1351
1542
  if (raw == null || (typeof raw === 'string' && !raw)) {
@@ -1404,6 +1595,7 @@ async function summarizeWithCacheHit({
1404
1595
  stepId,
1405
1596
  provider,
1406
1597
  reasoningKey,
1598
+ graph,
1407
1599
  usePromptCache,
1408
1600
  promptCacheTtl,
1409
1601
  log,
@@ -1417,6 +1609,7 @@ async function summarizeWithCacheHit({
1417
1609
  stepId?: string;
1418
1610
  provider: Providers;
1419
1611
  reasoningKey?: 'reasoning_content' | 'reasoning';
1612
+ graph?: StreamLimitState & { getBreakerSignal?: () => AbortSignal };
1420
1613
  usePromptCache?: boolean;
1421
1614
  promptCacheTtl?: PromptCacheTtl;
1422
1615
  log?: LogFn;
@@ -1443,9 +1636,13 @@ async function summarizeWithCacheHit({
1443
1636
  config: traceConfig(config, 'cache_hit_compaction'),
1444
1637
  provider,
1445
1638
  reasoningKey,
1639
+ graph,
1446
1640
  }),
1641
+ /** Accounting lease only (summarization passes no `context` so its
1642
+ * producer claim never stacks with the onChunk handler's). */
1643
+ streamLimitState: graph,
1447
1644
  },
1448
- traceConfig(config, 'cache_hit_compaction')
1645
+ withBreakerSignal(traceConfig(config, 'cache_hit_compaction'), graph)
1449
1646
  );
1450
1647
 
1451
1648
  const responseMsg = result.messages?.[0];
@@ -1,6 +1,5 @@
1
1
  import { config } from 'dotenv';
2
2
  import fetch, { RequestInit } from 'node-fetch';
3
- import { HttpsProxyAgent } from 'https-proxy-agent';
4
3
  import { tool, DynamicStructuredTool } from '@langchain/core/tools';
5
4
  import type * as t from '@/types';
6
5
  import {
@@ -16,6 +15,7 @@ import {
16
15
  normalizeCodeApiRequestError,
17
16
  resolveCodeApiAuthHeaders,
18
17
  } from './CodeExecutor';
18
+ import { resolveFetchProxyAgent } from '@/utils/proxy';
19
19
  import { INTENT_PROPERTY } from '@/tools/intentArg';
20
20
  import { Constants } from '@/common';
21
21
 
@@ -253,8 +253,9 @@ function createBashExecutionTool(
253
253
  body: JSON.stringify(postData),
254
254
  };
255
255
 
256
- if (process.env.PROXY != null && process.env.PROXY !== '') {
257
- fetchOptions.agent = new HttpsProxyAgent(process.env.PROXY);
256
+ const proxyAgent = resolveFetchProxyAgent(EXEC_ENDPOINT);
257
+ if (proxyAgent) {
258
+ fetchOptions.agent = proxyAgent;
258
259
  }
259
260
  const response = await fetch(EXEC_ENDPOINT, fetchOptions);
260
261
  if (!response.ok) {
@@ -1,10 +1,10 @@
1
1
  import { config } from 'dotenv';
2
2
  import fetch, { RequestInit } from 'node-fetch';
3
- import { HttpsProxyAgent } from 'https-proxy-agent';
4
3
  import { getEnvironmentVariable } from '@langchain/core/utils/env';
5
4
  import { tool, DynamicStructuredTool } from '@langchain/core/tools';
6
5
  import type * as t from '@/types';
7
6
  import { appendCodeSessionFileSummary } from '@/tools/CodeSessionFileSummary';
7
+ import { resolveFetchProxyAgent } from '@/utils/proxy';
8
8
  import { INTENT_PROPERTY } from '@/tools/intentArg';
9
9
  import { EnvVar, Constants } from '@/common';
10
10
 
@@ -23,7 +23,7 @@ export const emptyOutputMessage =
23
23
  'stdout: Empty. Ensure you\'re writing output explicitly.\n';
24
24
 
25
25
  export const CODE_ARTIFACT_PATH_GUIDANCE =
26
- 'Persist handoff artifacts in `/mnt/data` with standard extensions (.json/.txt/.csv/.tsv/.log/.parquet/.png/.jpg/.pdf/.xlsx); failed executions do not register new files; `/tmp` and odd extensions are same-call scratch only, not later-call storage.';
26
+ 'Anything a later call needs (data, helper scripts/modules, partial results) MUST be written under `/mnt/data` in the same call that produces it; `/tmp` never survives the call. `/mnt/data` keeps files with recognized extensions, covering common source, text, data, document, image, and archive formats (.py/.sh/.sql/.md/.json/.csv/.parquet/.png/.pdf/.zip and similar); extensionless or unusual extensions are not kept. Failed executions register nothing; fix the error and rerun before relying on new files.';
27
27
 
28
28
  export const BASH_SHELL_GUIDANCE =
29
29
  'Bash: multi-line files use heredoc/printf; run Python via python3 -c/heredoc, not bare Python.';
@@ -426,8 +426,9 @@ function createCodeExecutionTool(
426
426
  body: JSON.stringify(postData),
427
427
  };
428
428
 
429
- if (process.env.PROXY != null && process.env.PROXY !== '') {
430
- fetchOptions.agent = new HttpsProxyAgent(process.env.PROXY);
429
+ const proxyAgent = resolveFetchProxyAgent(EXEC_ENDPOINT);
430
+ if (proxyAgent) {
431
+ fetchOptions.agent = proxyAgent;
431
432
  }
432
433
  const response = await fetch(EXEC_ENDPOINT, fetchOptions);
433
434
  if (!response.ok) {
@@ -1,7 +1,6 @@
1
1
  // src/tools/ProgrammaticToolCalling.ts
2
2
  import { config } from 'dotenv';
3
3
  import fetch, { RequestInit } from 'node-fetch';
4
- import { HttpsProxyAgent } from 'https-proxy-agent';
5
4
  import { tool, DynamicStructuredTool } from '@langchain/core/tools';
6
5
  import type { ToolCall } from '@langchain/core/messages/tool';
7
6
  import type { ProgrammaticToolCallingJsonSchema } from './ptcTimeout';
@@ -24,6 +23,7 @@ import {
24
23
  createCodeApiRunTimeoutSchema,
25
24
  resolveCodeApiRunTimeoutMs,
26
25
  } from './ptcTimeout';
26
+ import { resolveFetchProxyAgent } from '@/utils/proxy';
27
27
  import { INTENT_PROPERTY } from '@/tools/intentArg';
28
28
  import { Constants } from '@/common';
29
29
 
@@ -438,8 +438,9 @@ export async function fetchSessionFiles(
438
438
  },
439
439
  };
440
440
 
441
- if (proxy != null && proxy !== '') {
442
- fetchOptions.agent = new HttpsProxyAgent(proxy);
441
+ const proxyAgent = resolveFetchProxyAgent(filesEndpoint, proxy);
442
+ if (proxyAgent) {
443
+ fetchOptions.agent = proxyAgent;
443
444
  }
444
445
 
445
446
  const response = await fetch(filesEndpoint, fetchOptions);
@@ -491,8 +492,9 @@ export async function makeRequest(
491
492
  body: JSON.stringify(body),
492
493
  };
493
494
 
494
- if (proxy != null && proxy !== '') {
495
- fetchOptions.agent = new HttpsProxyAgent(proxy);
495
+ const proxyAgent = resolveFetchProxyAgent(endpoint, proxy);
496
+ if (proxyAgent) {
497
+ fetchOptions.agent = proxyAgent;
496
498
  }
497
499
 
498
500
  const response = await fetch(endpoint, fetchOptions);
@@ -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);