@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
@@ -0,0 +1,196 @@
1
+ /**
2
+ * Summarization streams replace `ChatModelStreamHandler` with an `onChunk`
3
+ * callback, so the stream circuit breakers are enforced inside
4
+ * `createSummarizationChunkHandler`: the per-generation event cap, and the
5
+ * argument byte cap for tool calls a tool-bound summarization model may
6
+ * emit. Keys derive from the attempt metadata `attemptInvoke` hands each
7
+ * chunk, so a fallback summary attempt never inherits the failed primary's
8
+ * counts.
9
+ */
10
+ import { AIMessageChunk } from '@langchain/core/messages';
11
+ import { describe, it, expect } from '@jest/globals';
12
+ import type { StreamLimitState } from '@/llm/streamLimits';
13
+ import {
14
+ STREAM_LIMIT_ATTEMPT_KEY,
15
+ StreamLimitExceededError,
16
+ enforceStreamLimitsForWireChunk,
17
+ resolveStreamLimits,
18
+ } from '@/llm/streamLimits';
19
+ import { createSummarizationChunkHandler } from '@/summarization/node';
20
+ import { Providers } from '@/common';
21
+
22
+ const makeHandler = (graph: StreamLimitState) =>
23
+ createSummarizationChunkHandler({
24
+ stepId: 'step_1',
25
+ config: { metadata: { langgraph_node: 'summarize', langgraph_step: 4 } },
26
+ provider: Providers.OPENAI,
27
+ reasoningKey: 'reasoning_content',
28
+ graph,
29
+ });
30
+
31
+ const textChunk = (): AIMessageChunk => new AIMessageChunk({ content: 'part' });
32
+
33
+ describe('createSummarizationChunkHandler stream limits', () => {
34
+ it('enforces the event cap on summary streams', async () => {
35
+ const graph: StreamLimitState = {
36
+ streamLimits: resolveStreamLimits({ maxDeltaEventsPerTurn: 2 }),
37
+ };
38
+ const onChunk = makeHandler(graph);
39
+ expect(onChunk).toBeDefined();
40
+ await onChunk!(textChunk());
41
+ await onChunk!(textChunk());
42
+ await expect(async () => onChunk!(textChunk())).rejects.toThrow(
43
+ StreamLimitExceededError
44
+ );
45
+ });
46
+
47
+ it('keys each summary attempt by the metadata attemptInvoke hands the chunk', async () => {
48
+ const graph: StreamLimitState = {
49
+ streamLimits: resolveStreamLimits({ maxDeltaEventsPerTurn: 2 }),
50
+ };
51
+ const onChunk = makeHandler(graph);
52
+ const primary = {
53
+ langgraph_node: 'summarize',
54
+ langgraph_step: 4,
55
+ [STREAM_LIMIT_ATTEMPT_KEY]: 1,
56
+ };
57
+ const fallback = { ...primary, [STREAM_LIMIT_ATTEMPT_KEY]: 2 };
58
+ await onChunk!(textChunk(), primary);
59
+ await onChunk!(textChunk(), primary);
60
+ await onChunk!(textChunk(), fallback);
61
+ await onChunk!(textChunk(), fallback);
62
+ await expect(async () => onChunk!(textChunk(), fallback)).rejects.toThrow(
63
+ StreamLimitExceededError
64
+ );
65
+ });
66
+
67
+ it('enforces the argument byte cap on tool calls from tool-bound summary models', async () => {
68
+ const graph: StreamLimitState = {
69
+ streamLimits: resolveStreamLimits({ maxToolCallArgBytes: 50 }),
70
+ };
71
+ const onChunk = makeHandler(graph);
72
+ const toolChunk = (args: string): AIMessageChunk =>
73
+ new AIMessageChunk({
74
+ content: '',
75
+ tool_call_chunks: [
76
+ { type: 'tool_call_chunk', name: 'db_query', args, index: 0 },
77
+ ],
78
+ });
79
+ await onChunk!(toolChunk('x'.repeat(40)));
80
+ await expect(async () => onChunk!(toolChunk('x'.repeat(20)))).rejects.toThrow(
81
+ '(tool call: db_query)'
82
+ );
83
+ });
84
+
85
+ it('trips the run breaker when its own enforcement breaches', async () => {
86
+ const breaker = new AbortController();
87
+ const graph: StreamLimitState & {
88
+ getBreakerController?: () => AbortController;
89
+ } = {
90
+ streamLimits: resolveStreamLimits({ maxToolCallArgBytes: 50 }),
91
+ getBreakerController: () => breaker,
92
+ };
93
+ const onChunk = createSummarizationChunkHandler({
94
+ stepId: 'step_1',
95
+ config: { metadata: { langgraph_node: 'summarize', langgraph_step: 4 } },
96
+ provider: Providers.OPENAI,
97
+ reasoningKey: 'reasoning_content',
98
+ graph,
99
+ });
100
+ const toolChunk = (args: string): AIMessageChunk =>
101
+ new AIMessageChunk({
102
+ content: '',
103
+ tool_call_chunks: [
104
+ { type: 'tool_call_chunk', name: 'db_query', args, index: 0 },
105
+ ],
106
+ });
107
+
108
+ /** This producer claim wins the race, so the wire consumer's
109
+ * breaker-aborting catch never fires for the same chunk — the trip has
110
+ * to happen here or sibling branches keep consuming quota. */
111
+ await expect(async () => onChunk!(toolChunk('x'.repeat(60)))).rejects.toThrow(
112
+ StreamLimitExceededError
113
+ );
114
+ expect(breaker.signal.aborted).toBe(true);
115
+ expect(breaker.signal.reason).toMatchObject({ kind: 'tool_call_args' });
116
+ });
117
+
118
+ it('charges a chunk once when the run wire consumer also claims it', async () => {
119
+ const graph: StreamLimitState = {
120
+ streamLimits: resolveStreamLimits({ maxToolCallArgBytes: 100 }),
121
+ };
122
+ const onChunk = makeHandler(graph);
123
+ const metadata = { langgraph_node: 'summarize', langgraph_step: 4 };
124
+ const toolChunk = (args: string): AIMessageChunk =>
125
+ new AIMessageChunk({
126
+ content: '',
127
+ tool_call_chunks: [
128
+ { type: 'tool_call_chunk', name: 'db_query', args, index: 0 },
129
+ ],
130
+ });
131
+
132
+ /** Inside a live run the provider callbacks route the same chunk object
133
+ * through the graph's registered stream consumer as well. Wire consumer
134
+ * first, then the summarization handler: 60 bytes must be tallied once
135
+ * (a second same-side claim would put the tally at 120 and trip). */
136
+ const first = toolChunk('x'.repeat(60));
137
+ enforceStreamLimitsForWireChunk({
138
+ graph,
139
+ metadata,
140
+ chunk: first,
141
+ side: 'consumer',
142
+ });
143
+ await onChunk!(first, metadata);
144
+
145
+ /** Opposite order for the next chunk: handler (producer) before wire
146
+ * consumer. 60 + 39 = 99 stays under the cap only when each chunk was
147
+ * charged exactly once. */
148
+ const second = toolChunk('x'.repeat(39));
149
+ await onChunk!(second, metadata);
150
+ enforceStreamLimitsForWireChunk({
151
+ graph,
152
+ metadata,
153
+ chunk: second,
154
+ side: 'consumer',
155
+ });
156
+
157
+ /** Enforcement stays armed: two more bytes cross the 100-byte cap. */
158
+ await expect(async () => onChunk!(toolChunk('xx'), metadata)).rejects.toThrow(
159
+ StreamLimitExceededError
160
+ );
161
+ });
162
+
163
+ it('counts dual-consumed events once against the event cap', async () => {
164
+ const graph: StreamLimitState = {
165
+ streamLimits: resolveStreamLimits({ maxDeltaEventsPerTurn: 2 }),
166
+ };
167
+ const onChunk = makeHandler(graph);
168
+ const metadata = { langgraph_node: 'summarize', langgraph_step: 4 };
169
+ const passBoth = async (chunk: AIMessageChunk): Promise<void> => {
170
+ enforceStreamLimitsForWireChunk({
171
+ graph,
172
+ metadata,
173
+ chunk,
174
+ side: 'consumer',
175
+ });
176
+ await onChunk!(chunk, metadata);
177
+ };
178
+
179
+ await passBoth(textChunk());
180
+ await passBoth(textChunk());
181
+ await expect(async () => passBoth(textChunk())).rejects.toThrow(
182
+ StreamLimitExceededError
183
+ );
184
+ });
185
+
186
+ it('stays inert without graph state', async () => {
187
+ const onChunk = createSummarizationChunkHandler({
188
+ stepId: 'step_1',
189
+ config: { metadata: {} },
190
+ provider: Providers.OPENAI,
191
+ });
192
+ for (let i = 0; i < 10; i++) {
193
+ await onChunk!(textChunk());
194
+ }
195
+ });
196
+ });
@@ -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);