@librechat/agents 3.3.11 → 3.3.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/graphs/Graph.cjs +246 -25
- package/dist/cjs/graphs/Graph.cjs.map +1 -1
- package/dist/cjs/instrumentation.cjs +1 -0
- package/dist/cjs/instrumentation.cjs.map +1 -1
- package/dist/cjs/langfuseSpanRegistry.cjs +6 -3
- package/dist/cjs/langfuseSpanRegistry.cjs.map +1 -1
- package/dist/cjs/llm/bedrock/index.cjs +13 -2
- package/dist/cjs/llm/bedrock/index.cjs.map +1 -1
- package/dist/cjs/llm/invoke.cjs +146 -36
- package/dist/cjs/llm/invoke.cjs.map +1 -1
- package/dist/cjs/llm/openai/index.cjs +2 -2
- package/dist/cjs/llm/openai/index.cjs.map +1 -1
- package/dist/cjs/llm/streamLimits.cjs +723 -0
- package/dist/cjs/llm/streamLimits.cjs.map +1 -0
- package/dist/cjs/main.cjs +8 -0
- package/dist/cjs/run.cjs +6 -2
- package/dist/cjs/run.cjs.map +1 -1
- package/dist/cjs/session/AgentSession.cjs +4 -1
- package/dist/cjs/session/AgentSession.cjs.map +1 -1
- package/dist/cjs/stream.cjs +103 -12
- package/dist/cjs/stream.cjs.map +1 -1
- package/dist/cjs/summarization/node.cjs +157 -37
- package/dist/cjs/summarization/node.cjs.map +1 -1
- package/dist/cjs/tools/BashExecutor.cjs +3 -2
- package/dist/cjs/tools/BashExecutor.cjs.map +1 -1
- package/dist/cjs/tools/CodeExecutor.cjs +4 -3
- package/dist/cjs/tools/CodeExecutor.cjs.map +1 -1
- package/dist/cjs/tools/ProgrammaticToolCalling.cjs +5 -3
- package/dist/cjs/tools/ProgrammaticToolCalling.cjs.map +1 -1
- package/dist/cjs/tools/ToolNode.cjs +76 -3
- package/dist/cjs/tools/ToolNode.cjs.map +1 -1
- package/dist/cjs/tools/ToolSearch.cjs +3 -2
- package/dist/cjs/tools/ToolSearch.cjs.map +1 -1
- package/dist/cjs/tools/search/crw-scraper.cjs +7 -1
- package/dist/cjs/tools/search/crw-scraper.cjs.map +1 -1
- package/dist/cjs/tools/search/crw-search.cjs +3 -1
- package/dist/cjs/tools/search/crw-search.cjs.map +1 -1
- package/dist/cjs/tools/search/firecrawl.cjs +7 -1
- package/dist/cjs/tools/search/firecrawl.cjs.map +1 -1
- package/dist/cjs/tools/search/keenable-scraper.cjs +7 -1
- package/dist/cjs/tools/search/keenable-scraper.cjs.map +1 -1
- package/dist/cjs/tools/search/keenable-search.cjs +3 -1
- package/dist/cjs/tools/search/keenable-search.cjs.map +1 -1
- package/dist/cjs/tools/search/rerankers.cjs +26 -8
- package/dist/cjs/tools/search/rerankers.cjs.map +1 -1
- package/dist/cjs/tools/search/search.cjs +30 -10
- package/dist/cjs/tools/search/search.cjs.map +1 -1
- package/dist/cjs/tools/search/serper-scraper.cjs +7 -1
- package/dist/cjs/tools/search/serper-scraper.cjs.map +1 -1
- package/dist/cjs/tools/search/tavily-scraper.cjs +7 -1
- package/dist/cjs/tools/search/tavily-scraper.cjs.map +1 -1
- package/dist/cjs/tools/search/tavily-search.cjs +3 -1
- package/dist/cjs/tools/search/tavily-search.cjs.map +1 -1
- package/dist/cjs/tools/search/tool.cjs +16 -2
- package/dist/cjs/tools/search/tool.cjs.map +1 -1
- package/dist/cjs/tools/subagent/SubagentExecutor.cjs +54 -3
- package/dist/cjs/tools/subagent/SubagentExecutor.cjs.map +1 -1
- package/dist/cjs/utils/index.cjs +1 -0
- package/dist/cjs/utils/misc.cjs +12 -0
- package/dist/cjs/utils/misc.cjs.map +1 -1
- package/dist/cjs/utils/proxy.cjs +63 -0
- package/dist/cjs/utils/proxy.cjs.map +1 -0
- package/dist/esm/graphs/Graph.mjs +245 -24
- package/dist/esm/graphs/Graph.mjs.map +1 -1
- package/dist/esm/instrumentation.mjs +1 -0
- package/dist/esm/instrumentation.mjs.map +1 -1
- package/dist/esm/langfuseSpanRegistry.mjs +6 -3
- package/dist/esm/langfuseSpanRegistry.mjs.map +1 -1
- package/dist/esm/llm/bedrock/index.mjs +13 -2
- package/dist/esm/llm/bedrock/index.mjs.map +1 -1
- package/dist/esm/llm/invoke.mjs +146 -36
- package/dist/esm/llm/invoke.mjs.map +1 -1
- package/dist/esm/llm/openai/index.mjs +2 -2
- package/dist/esm/llm/openai/index.mjs.map +1 -1
- package/dist/esm/llm/streamLimits.mjs +704 -0
- package/dist/esm/llm/streamLimits.mjs.map +1 -0
- package/dist/esm/main.mjs +4 -2
- package/dist/esm/run.mjs +6 -2
- package/dist/esm/run.mjs.map +1 -1
- package/dist/esm/session/AgentSession.mjs +4 -1
- package/dist/esm/session/AgentSession.mjs.map +1 -1
- package/dist/esm/stream.mjs +103 -12
- package/dist/esm/stream.mjs.map +1 -1
- package/dist/esm/summarization/node.mjs +157 -37
- package/dist/esm/summarization/node.mjs.map +1 -1
- package/dist/esm/tools/BashExecutor.mjs +3 -2
- package/dist/esm/tools/BashExecutor.mjs.map +1 -1
- package/dist/esm/tools/CodeExecutor.mjs +4 -3
- package/dist/esm/tools/CodeExecutor.mjs.map +1 -1
- package/dist/esm/tools/ProgrammaticToolCalling.mjs +5 -3
- package/dist/esm/tools/ProgrammaticToolCalling.mjs.map +1 -1
- package/dist/esm/tools/ToolNode.mjs +76 -3
- package/dist/esm/tools/ToolNode.mjs.map +1 -1
- package/dist/esm/tools/ToolSearch.mjs +3 -2
- package/dist/esm/tools/ToolSearch.mjs.map +1 -1
- package/dist/esm/tools/search/crw-scraper.mjs +7 -1
- package/dist/esm/tools/search/crw-scraper.mjs.map +1 -1
- package/dist/esm/tools/search/crw-search.mjs +3 -1
- package/dist/esm/tools/search/crw-search.mjs.map +1 -1
- package/dist/esm/tools/search/firecrawl.mjs +7 -1
- package/dist/esm/tools/search/firecrawl.mjs.map +1 -1
- package/dist/esm/tools/search/keenable-scraper.mjs +7 -1
- package/dist/esm/tools/search/keenable-scraper.mjs.map +1 -1
- package/dist/esm/tools/search/keenable-search.mjs +3 -1
- package/dist/esm/tools/search/keenable-search.mjs.map +1 -1
- package/dist/esm/tools/search/rerankers.mjs +26 -8
- package/dist/esm/tools/search/rerankers.mjs.map +1 -1
- package/dist/esm/tools/search/search.mjs +30 -10
- package/dist/esm/tools/search/search.mjs.map +1 -1
- package/dist/esm/tools/search/serper-scraper.mjs +7 -1
- package/dist/esm/tools/search/serper-scraper.mjs.map +1 -1
- package/dist/esm/tools/search/tavily-scraper.mjs +7 -1
- package/dist/esm/tools/search/tavily-scraper.mjs.map +1 -1
- package/dist/esm/tools/search/tavily-search.mjs +3 -1
- package/dist/esm/tools/search/tavily-search.mjs.map +1 -1
- package/dist/esm/tools/search/tool.mjs +16 -2
- package/dist/esm/tools/search/tool.mjs.map +1 -1
- package/dist/esm/tools/subagent/SubagentExecutor.mjs +54 -3
- package/dist/esm/tools/subagent/SubagentExecutor.mjs.map +1 -1
- package/dist/esm/utils/index.mjs +1 -0
- package/dist/esm/utils/misc.mjs +12 -1
- package/dist/esm/utils/misc.mjs.map +1 -1
- package/dist/esm/utils/proxy.mjs +62 -0
- package/dist/esm/utils/proxy.mjs.map +1 -0
- package/dist/types/graphs/Graph.d.ts +53 -1
- package/dist/types/index.d.ts +2 -0
- package/dist/types/llm/invoke.d.ts +25 -4
- package/dist/types/llm/openai/index.d.ts +3 -0
- package/dist/types/llm/streamLimits.d.ts +314 -0
- package/dist/types/run.d.ts +1 -0
- package/dist/types/summarization/node.d.ts +27 -2
- package/dist/types/tools/BashExecutor.d.ts +2 -2
- package/dist/types/tools/CodeExecutor.d.ts +3 -3
- package/dist/types/tools/ToolNode.d.ts +11 -1
- package/dist/types/tools/search/crw-scraper.d.ts +2 -0
- package/dist/types/tools/search/firecrawl.d.ts +2 -0
- package/dist/types/tools/search/keenable-scraper.d.ts +2 -0
- package/dist/types/tools/search/rerankers.d.ts +9 -5
- package/dist/types/tools/search/serper-scraper.d.ts +2 -0
- package/dist/types/tools/search/tavily-scraper.d.ts +2 -0
- package/dist/types/tools/search/types.d.ts +28 -33
- package/dist/types/tools/subagent/SubagentExecutor.d.ts +44 -0
- package/dist/types/types/graph.d.ts +12 -1
- package/dist/types/types/run.d.ts +46 -1
- package/dist/types/types/tools.d.ts +21 -0
- package/dist/types/utils/index.d.ts +1 -0
- package/dist/types/utils/misc.d.ts +7 -0
- package/dist/types/utils/proxy.d.ts +31 -0
- package/package.json +2 -1
- package/src/__tests__/stream.eagerArgsDivergence.test.ts +158 -0
- package/src/__tests__/stream.eagerEventExecution.test.ts +1 -0
- package/src/__tests__/stream.streamLimits.test.ts +1982 -0
- package/src/graphs/Graph.ts +302 -27
- package/src/graphs/__tests__/Graph.breakerLifecycle.test.ts +234 -0
- package/src/index.ts +11 -0
- package/src/instrumentation.ts +1 -0
- package/src/langfuseSpanRegistry.ts +9 -0
- package/src/llm/bedrock/index.ts +22 -2
- package/src/llm/invoke.streamLimits.test.ts +142 -0
- package/src/llm/invoke.test.ts +89 -1
- package/src/llm/invoke.ts +197 -20
- package/src/llm/openai/cacheWriteTokens.test.ts +112 -0
- package/src/llm/openai/index.ts +14 -6
- package/src/llm/streamLimits.test.ts +450 -0
- package/src/llm/streamLimits.ts +1158 -0
- package/src/run.ts +4 -0
- package/src/session/AgentSession.ts +5 -0
- package/src/specs/langfuse-instrumentation.test.ts +26 -0
- package/src/specs/langfuse-span-registry.test.ts +17 -0
- package/src/specs/summarization.test.ts +5 -2
- package/src/stream.ts +141 -6
- package/src/summarization/__tests__/node.test.ts +269 -0
- package/src/summarization/chunkHandler.test.ts +196 -0
- package/src/summarization/node.ts +203 -6
- package/src/tools/BashExecutor.ts +4 -3
- package/src/tools/CodeExecutor.ts +5 -4
- package/src/tools/ProgrammaticToolCalling.ts +7 -5
- package/src/tools/ToolNode.ts +109 -6
- package/src/tools/ToolSearch.ts +4 -3
- package/src/tools/__tests__/BashExecutor.test.ts +2 -2
- package/src/tools/__tests__/ProgrammaticToolCalling.test.ts +2 -4
- package/src/tools/__tests__/SubagentExecutor.test.ts +126 -0
- package/src/tools/__tests__/ToolNode.breakerSignal.test.ts +389 -0
- package/src/tools/__tests__/ToolNode.streamLimits.test.ts +69 -0
- package/src/tools/search/crw-scraper.ts +6 -0
- package/src/tools/search/crw-search.ts +6 -1
- package/src/tools/search/firecrawl.ts +6 -0
- package/src/tools/search/http-agent.test.ts +133 -0
- package/src/tools/search/keenable-scraper.ts +6 -0
- package/src/tools/search/keenable-search.ts +6 -1
- package/src/tools/search/rerankers.ts +36 -10
- package/src/tools/search/search.ts +29 -11
- package/src/tools/search/serper-scraper.ts +6 -0
- package/src/tools/search/tavily-scraper.ts +6 -0
- package/src/tools/search/tavily-search.ts +2 -0
- package/src/tools/search/tool.ts +16 -0
- package/src/tools/search/types.ts +31 -33
- package/src/tools/subagent/SubagentExecutor.ts +96 -3
- package/src/types/graph.ts +12 -0
- package/src/types/run.ts +49 -1
- package/src/types/tools.ts +21 -0
- package/src/utils/index.ts +1 -0
- package/src/utils/misc.ts +19 -0
- package/src/utils/proxy.test.ts +176 -0
- package/src/utils/proxy.ts +93 -0
|
@@ -28,6 +28,10 @@ import type { AgentContext } from '@/agents/AgentContext';
|
|
|
28
28
|
import type { StandardGraph } from '@/graphs/Graph';
|
|
29
29
|
import type { HandlerRegistry } from '@/events';
|
|
30
30
|
import { Constants, GraphEvents, Callback, StepTypes } from '@/common';
|
|
31
|
+
import {
|
|
32
|
+
StreamLimitExceededError,
|
|
33
|
+
RUN_BREAKER_SCOPE_CONFIG_KEY,
|
|
34
|
+
} from '@/llm/streamLimits';
|
|
31
35
|
import { executeHooks } from '@/hooks';
|
|
32
36
|
|
|
33
37
|
const DEFAULT_MAX_TURNS = 25;
|
|
@@ -157,6 +161,14 @@ export type SubagentExecuteParams = {
|
|
|
157
161
|
description: string;
|
|
158
162
|
subagentType: string;
|
|
159
163
|
threadId?: string;
|
|
164
|
+
/**
|
|
165
|
+
* Breaker controller captured at the parent TOOL BATCH's entry, before
|
|
166
|
+
* PreToolUse hooks. Preferred over the live scope accessor: a graph reset
|
|
167
|
+
* during a hook would otherwise bind this child to the NEW run's
|
|
168
|
+
* controller — reviving it on a fresh signal and letting its trips cancel
|
|
169
|
+
* unrelated work.
|
|
170
|
+
*/
|
|
171
|
+
breaker?: AbortController;
|
|
160
172
|
/**
|
|
161
173
|
* Parent-side `tool_call_id` of the `subagent` tool invocation that
|
|
162
174
|
* triggered this execution. Surfaced on {@link SubagentUpdateEvent} so
|
|
@@ -214,11 +226,31 @@ export type ChildGraphFactory = (input: StandardGraphInput) => StandardGraph;
|
|
|
214
226
|
export type SubagentExecutorOptions = {
|
|
215
227
|
configs: Map<string, ResolvedSubagentConfig>;
|
|
216
228
|
parentSignal?: AbortSignal;
|
|
229
|
+
/** Run-scoped breaker abort shared by every executor of one graph, so a
|
|
230
|
+
* child tripping a stream limit stops subagents running under OTHER
|
|
231
|
+
* parallel agent nodes too. An accessor rather than a captured controller:
|
|
232
|
+
* the graph recreates its controller per run, and each execution must
|
|
233
|
+
* bind to the controller current when it STARTS — signal and trip target
|
|
234
|
+
* together, so a straggler from a failed run can neither revive on nor
|
|
235
|
+
* circuit-break a later run's controller. Absent (tests, minimal hosts),
|
|
236
|
+
* the executor falls back to its own private controller. */
|
|
237
|
+
breakerScope?: {
|
|
238
|
+
controller: () => AbortController;
|
|
239
|
+
};
|
|
217
240
|
hookRegistry?: HookRegistry;
|
|
218
241
|
parentRunId: string;
|
|
219
242
|
parentAgentId?: string;
|
|
220
243
|
langfuse?: StandardGraphInput['langfuse'];
|
|
221
244
|
tokenCounter?: TokenCounter;
|
|
245
|
+
/**
|
|
246
|
+
* Run-level stream circuit breakers, forwarded into every child graph so
|
|
247
|
+
* a host raising, lowering, or disabling the limits governs subagents too.
|
|
248
|
+
* Child model calls run through `attemptInvoke`'s local stream handler
|
|
249
|
+
* (children have no registered dispatcher), which enforces the child
|
|
250
|
+
* graph's own resolved limits; without this the child would silently
|
|
251
|
+
* revert to the defaults.
|
|
252
|
+
*/
|
|
253
|
+
streamLimits?: StandardGraphInput['streamLimits'];
|
|
222
254
|
/** Remaining nesting budget. 0 or negative blocks execution. */
|
|
223
255
|
maxDepth?: number;
|
|
224
256
|
/**
|
|
@@ -254,11 +286,20 @@ export type SubagentExecutorOptions = {
|
|
|
254
286
|
export class SubagentExecutor {
|
|
255
287
|
private readonly configs: Map<string, ResolvedSubagentConfig>;
|
|
256
288
|
private readonly parentSignal?: AbortSignal;
|
|
289
|
+
/** Aborted when a child trips a stream circuit breaker: parallel sibling
|
|
290
|
+
* subagents run concurrently on the parent's signal, and rejecting the
|
|
291
|
+
* batch alone would leave their provider requests streaming after the
|
|
292
|
+
* safety abort. One-way by design — a tripped breaker ends the run, so no
|
|
293
|
+
* later child of this executor should start either. Fallback for hosts
|
|
294
|
+
* that do not supply the graph's shared `breakerScope`. */
|
|
295
|
+
private readonly childRunAbort = new AbortController();
|
|
296
|
+
private readonly breakerScope?: SubagentExecutorOptions['breakerScope'];
|
|
257
297
|
private readonly hookRegistry?: HookRegistry;
|
|
258
298
|
private readonly parentRunId: string;
|
|
259
299
|
private readonly parentAgentId?: string;
|
|
260
300
|
private readonly langfuse?: StandardGraphInput['langfuse'];
|
|
261
301
|
private readonly tokenCounter?: TokenCounter;
|
|
302
|
+
private readonly streamLimits?: StandardGraphInput['streamLimits'];
|
|
262
303
|
private readonly maxDepth: number;
|
|
263
304
|
private readonly createChildGraph: ChildGraphFactory;
|
|
264
305
|
private readonly usageSink?: SubagentUsageSink;
|
|
@@ -269,11 +310,13 @@ export class SubagentExecutor {
|
|
|
269
310
|
constructor(options: SubagentExecutorOptions) {
|
|
270
311
|
this.configs = options.configs;
|
|
271
312
|
this.parentSignal = options.parentSignal;
|
|
313
|
+
this.breakerScope = options.breakerScope;
|
|
272
314
|
this.hookRegistry = options.hookRegistry;
|
|
273
315
|
this.parentRunId = options.parentRunId;
|
|
274
316
|
this.parentAgentId = options.parentAgentId;
|
|
275
317
|
this.langfuse = options.langfuse;
|
|
276
318
|
this.tokenCounter = options.tokenCounter;
|
|
319
|
+
this.streamLimits = options.streamLimits;
|
|
277
320
|
this.maxDepth = options.maxDepth ?? 1;
|
|
278
321
|
this.createChildGraph = options.createChildGraph;
|
|
279
322
|
this.usageSink = options.usageSink;
|
|
@@ -285,6 +328,23 @@ export class SubagentExecutor {
|
|
|
285
328
|
}
|
|
286
329
|
}
|
|
287
330
|
|
|
331
|
+
/** The breaker controller current for this execution — read per spawn
|
|
332
|
+
* because the graph recreates its controller each run. */
|
|
333
|
+
private resolveBreakerController(): AbortController {
|
|
334
|
+
return this.breakerScope?.controller() ?? this.childRunAbort;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/** One signal that fires on the parent's abort or the breaker abort,
|
|
338
|
+
* collapsed to a single signal when possible (mirrors
|
|
339
|
+
* `composeAbortSignals` in Graph.ts). */
|
|
340
|
+
private composeChildSignal(breaker: AbortController): AbortSignal {
|
|
341
|
+
const child = breaker.signal;
|
|
342
|
+
if (this.parentSignal == null || this.parentSignal === child) {
|
|
343
|
+
return child;
|
|
344
|
+
}
|
|
345
|
+
return AbortSignal.any([this.parentSignal, child]);
|
|
346
|
+
}
|
|
347
|
+
|
|
288
348
|
/** Snapshot of the parent's registry at the moment a subagent is dispatched. */
|
|
289
349
|
private getParentHandlerRegistry(): HandlerRegistry | undefined {
|
|
290
350
|
return this.resolveParentHandlerRegistry?.();
|
|
@@ -292,6 +352,15 @@ export class SubagentExecutor {
|
|
|
292
352
|
|
|
293
353
|
async execute(params: SubagentExecuteParams): Promise<SubagentExecuteResult> {
|
|
294
354
|
const { description, subagentType, threadId, parentToolCallId } = params;
|
|
355
|
+
/** Captured ONCE per execution, preferring the controller the parent
|
|
356
|
+
* tool batch captured at ITS entry (before PreToolUse hooks): a failed
|
|
357
|
+
* run's graph reset replaces the live controller, and resolving it here
|
|
358
|
+
* — after the hook awaits — would bind this child to the NEW run's
|
|
359
|
+
* un-aborted controller: reviving old-run work, or worse, tripping the
|
|
360
|
+
* new run's breaker from an old child's stream-limit breach. Signal and
|
|
361
|
+
* trip target both bind to this capture. */
|
|
362
|
+
const childBreaker = params.breaker ?? this.resolveBreakerController();
|
|
363
|
+
const childSignal = this.composeChildSignal(childBreaker);
|
|
295
364
|
const config = this.configs.get(subagentType);
|
|
296
365
|
|
|
297
366
|
if (!config) {
|
|
@@ -369,10 +438,11 @@ export class SubagentExecutor {
|
|
|
369
438
|
const hostUsageSink = this.usageSink;
|
|
370
439
|
const childGraph = this.createChildGraph({
|
|
371
440
|
runId: childRunId,
|
|
372
|
-
signal:
|
|
441
|
+
signal: childSignal,
|
|
373
442
|
agents: [childInputs],
|
|
374
443
|
langfuse: this.langfuse,
|
|
375
444
|
tokenCounter: this.tokenCounter,
|
|
445
|
+
streamLimits: this.streamLimits,
|
|
376
446
|
subagentScope: true,
|
|
377
447
|
/**
|
|
378
448
|
* Forwarded so the child graph's own `SubagentExecutor` (created in
|
|
@@ -501,7 +571,7 @@ export class SubagentExecutor {
|
|
|
501
571
|
{ messages: [new HumanMessage(description)] },
|
|
502
572
|
{
|
|
503
573
|
recursionLimit: maxTurns * RECURSION_MULTIPLIER,
|
|
504
|
-
signal:
|
|
574
|
+
signal: childSignal,
|
|
505
575
|
callbacks,
|
|
506
576
|
runName: `subagent:${subagentType}`,
|
|
507
577
|
configurable: {
|
|
@@ -511,6 +581,16 @@ export class SubagentExecutor {
|
|
|
511
581
|
}
|
|
512
582
|
);
|
|
513
583
|
} catch (error) {
|
|
584
|
+
/** Aborted before any observational work below: parallel siblings — in
|
|
585
|
+
* this executor and, via the graph-scoped breaker, under other
|
|
586
|
+
* parallel agent nodes — stream on the composed child signal, and
|
|
587
|
+
* awaiting forwarding.drain() first would let them consume provider
|
|
588
|
+
* quota for that entire interval. Trips the ENTRY-captured controller:
|
|
589
|
+
* after a reset, a straggler must break its own dead run, not the
|
|
590
|
+
* current one. */
|
|
591
|
+
if (error instanceof StreamLimitExceededError) {
|
|
592
|
+
childBreaker.abort(error);
|
|
593
|
+
}
|
|
514
594
|
const errorMessage = truncateErrorMessage(error);
|
|
515
595
|
if (forwarding) {
|
|
516
596
|
await forwarding.drain();
|
|
@@ -525,6 +605,16 @@ export class SubagentExecutor {
|
|
|
525
605
|
});
|
|
526
606
|
}
|
|
527
607
|
childGraph.clearHeavyState();
|
|
608
|
+
/**
|
|
609
|
+
* A tripped stream circuit breaker is a safety abort, not a recoverable
|
|
610
|
+
* subagent failure: converting it into a tool result would let the
|
|
611
|
+
* parent keep generating (or spawn another child) after the limit
|
|
612
|
+
* fired. Rethrown here and passed through ToolNode's error conversion,
|
|
613
|
+
* so the parent run rejects with the child's limit error.
|
|
614
|
+
*/
|
|
615
|
+
if (error instanceof StreamLimitExceededError) {
|
|
616
|
+
throw error;
|
|
617
|
+
}
|
|
528
618
|
return {
|
|
529
619
|
content: `Subagent error: ${errorMessage}`,
|
|
530
620
|
messages: [],
|
|
@@ -956,7 +1046,10 @@ function sanitizeChildConfigurable(
|
|
|
956
1046
|
function isLangGraphRuntimeConfigKey(key: string): boolean {
|
|
957
1047
|
return (
|
|
958
1048
|
key.startsWith(LANGGRAPH_RUNTIME_CONFIG_PREFIX) ||
|
|
959
|
-
LANGGRAPH_CHECKPOINT_CONFIG_KEYS.has(key)
|
|
1049
|
+
LANGGRAPH_CHECKPOINT_CONFIG_KEYS.has(key) ||
|
|
1050
|
+
/** The parent batch's breaker scope must not leak into the child
|
|
1051
|
+
* workflow's configurable — children own separate controllers. */
|
|
1052
|
+
key === RUN_BREAKER_SCOPE_CONFIG_KEY
|
|
960
1053
|
);
|
|
961
1054
|
}
|
|
962
1055
|
|
package/src/types/graph.ts
CHANGED
|
@@ -32,6 +32,7 @@ import type {
|
|
|
32
32
|
} from '@/types/stream';
|
|
33
33
|
import type {
|
|
34
34
|
TokenCounter,
|
|
35
|
+
StreamLimits,
|
|
35
36
|
StreamPreemption,
|
|
36
37
|
TokenBudgetBreakdown,
|
|
37
38
|
} from '@/types/run';
|
|
@@ -357,6 +358,12 @@ export type StandardGraphInput = {
|
|
|
357
358
|
* this field.
|
|
358
359
|
*/
|
|
359
360
|
preemption?: StreamPreemption;
|
|
361
|
+
/**
|
|
362
|
+
* Stream circuit breakers, forwarded from `RunConfig.streamLimits` and
|
|
363
|
+
* resolved once at graph construction. Enforced by the stream handler on
|
|
364
|
+
* every streamed chunk event. See {@link StreamLimits}.
|
|
365
|
+
*/
|
|
366
|
+
streamLimits?: StreamLimits;
|
|
360
367
|
};
|
|
361
368
|
|
|
362
369
|
export type GraphEdge = {
|
|
@@ -564,6 +571,11 @@ export interface LangfuseConfig {
|
|
|
564
571
|
* `default` environment.
|
|
565
572
|
*/
|
|
566
573
|
environment?: string;
|
|
574
|
+
/**
|
|
575
|
+
* Whether the Langfuse span processor should detect and upload base64 media
|
|
576
|
+
* payloads. Defaults to the Langfuse SDK behavior.
|
|
577
|
+
*/
|
|
578
|
+
mediaUploadEnabled?: boolean;
|
|
567
579
|
metadata?: Record<string, string | number | boolean | null | undefined>;
|
|
568
580
|
/**
|
|
569
581
|
* Internal OTLP span attributes to attach to Langfuse observations before
|
package/src/types/run.ts
CHANGED
|
@@ -28,7 +28,13 @@ export type BaseGraphConfig = {
|
|
|
28
28
|
};
|
|
29
29
|
export type LegacyGraphConfig = BaseGraphConfig & {
|
|
30
30
|
type?: 'standard';
|
|
31
|
-
} & Omit<
|
|
31
|
+
} & Omit<
|
|
32
|
+
g.StandardGraphInput,
|
|
33
|
+
/** `streamLimits` is excluded because legacy graphs receive limits only
|
|
34
|
+
* via the top-level `RunConfig.streamLimits`; accepting the field here
|
|
35
|
+
* would type-check but be silently ignored by `createLegacyGraph`. */
|
|
36
|
+
'provider' | 'clientOptions' | 'agents' | 'streamLimits'
|
|
37
|
+
> &
|
|
32
38
|
Omit<g.AgentInputs, 'provider' | 'clientOptions' | 'agentId'>;
|
|
33
39
|
|
|
34
40
|
/* Supervised graph (opt-in) */
|
|
@@ -174,6 +180,41 @@ export type PreemptStats = {
|
|
|
174
180
|
emptyBoundaries: number;
|
|
175
181
|
};
|
|
176
182
|
|
|
183
|
+
/**
|
|
184
|
+
* Circuit breakers for pathological model streams. A malformed generation
|
|
185
|
+
* can stream a single tool call's arguments for many minutes while they
|
|
186
|
+
* never become executable (observed live: one 149,923-char SQL argument
|
|
187
|
+
* streamed for 26 minutes before the 64k output-token ceiling ended the
|
|
188
|
+
* run). When a limit trips, the run aborts with a `StreamLimitExceededError`
|
|
189
|
+
* and the in-flight provider request is torn down mid-stream.
|
|
190
|
+
*/
|
|
191
|
+
export interface StreamLimits {
|
|
192
|
+
/**
|
|
193
|
+
* Max cumulative UTF-8 bytes a single streamed tool call's arguments may
|
|
194
|
+
* reach before the run is aborted. Defaults to
|
|
195
|
+
* `DEFAULT_MAX_TOOL_CALL_ARG_BYTES` (64 KiB); `0` disables the guard.
|
|
196
|
+
*/
|
|
197
|
+
maxToolCallArgBytes?: number;
|
|
198
|
+
/**
|
|
199
|
+
* Per-tool overrides for `maxToolCallArgBytes`, keyed by the model-facing
|
|
200
|
+
* tool name. A matching entry replaces the global cap for that tool's
|
|
201
|
+
* calls (`0` disables the guard for that tool only). Tools that
|
|
202
|
+
* legitimately stream whole documents as arguments (e.g. file creation)
|
|
203
|
+
* can run with a higher cap without loosening every other tool's guard.
|
|
204
|
+
* Calls whose tool name has not yet arrived on the stream use the global
|
|
205
|
+
* cap; providers send the name in the first chunk, so this only matters
|
|
206
|
+
* for malformed streams.
|
|
207
|
+
*/
|
|
208
|
+
maxToolCallArgBytesByTool?: Record<string, number>;
|
|
209
|
+
/**
|
|
210
|
+
* Max streamed chunk events a single model generation (turn) may emit
|
|
211
|
+
* before the run is aborted. Defense in depth against looping or
|
|
212
|
+
* duplicated provider streams that a byte limit cannot see (e.g. endless
|
|
213
|
+
* empty chunks). Disabled by default; `0` also disables.
|
|
214
|
+
*/
|
|
215
|
+
maxDeltaEventsPerTurn?: number;
|
|
216
|
+
}
|
|
217
|
+
|
|
177
218
|
export type RunConfig = {
|
|
178
219
|
runId: string;
|
|
179
220
|
graphConfig: LegacyGraphConfig | StandardGraphConfig | MultiAgentGraphConfig;
|
|
@@ -213,6 +254,13 @@ export type RunConfig = {
|
|
|
213
254
|
* tool boundary.
|
|
214
255
|
*/
|
|
215
256
|
preemption?: StreamPreemption;
|
|
257
|
+
/**
|
|
258
|
+
* Circuit breakers for pathological model streams (runaway tool-call
|
|
259
|
+
* argument generation, looping delta streams). Omit for defaults: the
|
|
260
|
+
* tool-call argument byte cap is ON by default, the per-turn event cap is
|
|
261
|
+
* opt-in. See {@link StreamLimits}.
|
|
262
|
+
*/
|
|
263
|
+
streamLimits?: StreamLimits;
|
|
216
264
|
returnContent?: boolean;
|
|
217
265
|
tokenCounter?: TokenCounter;
|
|
218
266
|
indexTokenCountMap?: Record<string, number>;
|
package/src/types/tools.ts
CHANGED
|
@@ -3,6 +3,7 @@ import type { StructuredToolInterface } from '@langchain/core/tools';
|
|
|
3
3
|
import type { RunnableToolLike } from '@langchain/core/runnables';
|
|
4
4
|
import type { ToolCall } from '@langchain/core/messages/tool';
|
|
5
5
|
import type { ToolOutputReferenceRegistry } from '@/tools/toolOutputReferences';
|
|
6
|
+
import type { RunBreakerScope } from '@/llm/streamLimits';
|
|
6
7
|
import type { MessageContentComplex, ToolErrorData } from './stream';
|
|
7
8
|
import type { HumanInTheLoopConfig } from './hitl';
|
|
8
9
|
import type { LangfuseConfig } from './graph';
|
|
@@ -259,6 +260,20 @@ export type ToolNodeOptions = {
|
|
|
259
260
|
* `resolveLocalExecutionTools`.
|
|
260
261
|
*/
|
|
261
262
|
fileCheckpointer?: LocalFileCheckpointer;
|
|
263
|
+
/**
|
|
264
|
+
* Returns the owning graph's run-scoped breaker signal. Read once per
|
|
265
|
+
* `run()` invocation and composed into the batch config's `signal`, so
|
|
266
|
+
* every tool execution in the batch aborts when a stream circuit breaker
|
|
267
|
+
* trips elsewhere in the run.
|
|
268
|
+
*/
|
|
269
|
+
getBreakerSignal?: () => AbortSignal | undefined;
|
|
270
|
+
/**
|
|
271
|
+
* Returns the owning graph's immutable run scope. Read once per batch,
|
|
272
|
+
* BEFORE hooks, and threaded to tools that spawn runs (subagents) so a
|
|
273
|
+
* reset during a hook cannot rebind their children to a newer run's
|
|
274
|
+
* controller.
|
|
275
|
+
*/
|
|
276
|
+
getRunScope?: () => RunBreakerScope;
|
|
262
277
|
};
|
|
263
278
|
|
|
264
279
|
export type ToolNodeConstructorParams = ToolRefs & ToolNodeOptions;
|
|
@@ -523,6 +538,12 @@ export type ToolExecuteBatchRequest = {
|
|
|
523
538
|
configurable?: Record<string, unknown>;
|
|
524
539
|
/** Runtime metadata from RunnableConfig (includes thread_id, run_id, provider, etc.) */
|
|
525
540
|
metadata?: Record<string, unknown>;
|
|
541
|
+
/**
|
|
542
|
+
* Aborts when the run is cancelled or its stream circuit breaker trips.
|
|
543
|
+
* Handlers SHOULD forward this to their tool executions so in-flight work
|
|
544
|
+
* stops consuming quota once the run is already failing.
|
|
545
|
+
*/
|
|
546
|
+
signal?: AbortSignal;
|
|
526
547
|
/** Promise resolver - handler calls this with ALL results */
|
|
527
548
|
resolve: (results: ToolExecuteResult[]) => void;
|
|
528
549
|
/** Promise rejector - handler calls this on fatal error */
|
package/src/utils/index.ts
CHANGED
package/src/utils/misc.ts
CHANGED
|
@@ -73,3 +73,22 @@ export function unescapeObject(obj: unknown, key?: string): unknown {
|
|
|
73
73
|
}
|
|
74
74
|
return obj;
|
|
75
75
|
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* One signal that fires when either input fires. `AbortSignal.any` is skipped
|
|
79
|
+
* when the inputs collapse to a single signal — the composite is a fresh
|
|
80
|
+
* object per call, and the common cases (one channel, or the host reusing the
|
|
81
|
+
* same controller for both) don't need one.
|
|
82
|
+
*/
|
|
83
|
+
export function composeAbortSignals(
|
|
84
|
+
a: AbortSignal | undefined,
|
|
85
|
+
b: AbortSignal | undefined
|
|
86
|
+
): AbortSignal | undefined {
|
|
87
|
+
if (a == null || a === b) {
|
|
88
|
+
return b;
|
|
89
|
+
}
|
|
90
|
+
if (b == null) {
|
|
91
|
+
return a;
|
|
92
|
+
}
|
|
93
|
+
return AbortSignal.any([a, b]);
|
|
94
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { HttpsProxyAgent } from 'https-proxy-agent';
|
|
2
|
+
import { SocksProxyAgent } from 'socks-proxy-agent';
|
|
3
|
+
import { resolveFetchProxyAgent, shouldBypassProxy } from './proxy';
|
|
4
|
+
|
|
5
|
+
const CODE_ENDPOINT = 'http://codeapi:3112/v1/exec';
|
|
6
|
+
|
|
7
|
+
describe('shouldBypassProxy', () => {
|
|
8
|
+
it('is false without a NO_PROXY list', () => {
|
|
9
|
+
expect(shouldBypassProxy(CODE_ENDPOINT, undefined)).toBe(false);
|
|
10
|
+
expect(shouldBypassProxy(CODE_ENDPOINT, ' ')).toBe(false);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('matches a bare hostname', () => {
|
|
14
|
+
expect(shouldBypassProxy(CODE_ENDPOINT, 'codeapi')).toBe(true);
|
|
15
|
+
expect(shouldBypassProxy(CODE_ENDPOINT, 'other')).toBe(false);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('accepts comma and whitespace separated lists', () => {
|
|
19
|
+
expect(
|
|
20
|
+
shouldBypassProxy(CODE_ENDPOINT, 'localhost,codeapi,127.0.0.1')
|
|
21
|
+
).toBe(true);
|
|
22
|
+
expect(shouldBypassProxy(CODE_ENDPOINT, 'localhost codeapi')).toBe(true);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('treats a leading dot as host-and-subdomains', () => {
|
|
26
|
+
expect(
|
|
27
|
+
shouldBypassProxy('http://codeapi.internal:3112/v1', '.internal')
|
|
28
|
+
).toBe(true);
|
|
29
|
+
expect(shouldBypassProxy('http://internal:3112/v1', '.internal')).toBe(
|
|
30
|
+
true
|
|
31
|
+
);
|
|
32
|
+
expect(shouldBypassProxy('http://notinternal:3112/v1', '.internal')).toBe(
|
|
33
|
+
false
|
|
34
|
+
);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('honours a port suffix only when it matches', () => {
|
|
38
|
+
expect(shouldBypassProxy(CODE_ENDPOINT, 'codeapi:3112')).toBe(true);
|
|
39
|
+
expect(shouldBypassProxy(CODE_ENDPOINT, 'codeapi:9999')).toBe(false);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('defaults the port from the scheme when the URL omits it', () => {
|
|
43
|
+
expect(
|
|
44
|
+
shouldBypassProxy('https://api.example.com/v1', 'api.example.com:443')
|
|
45
|
+
).toBe(true);
|
|
46
|
+
expect(
|
|
47
|
+
shouldBypassProxy('http://api.example.com/v1', 'api.example.com:80')
|
|
48
|
+
).toBe(true);
|
|
49
|
+
expect(
|
|
50
|
+
shouldBypassProxy('https://api.example.com/v1', 'api.example.com:80')
|
|
51
|
+
).toBe(false);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('supports the wildcard entry', () => {
|
|
55
|
+
expect(shouldBypassProxy('https://anything.example.com', '*')).toBe(true);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('does not claim a bypass for an unparsable target', () => {
|
|
59
|
+
expect(shouldBypassProxy('not a url', '*')).toBe(false);
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
describe('resolveFetchProxyAgent', () => {
|
|
64
|
+
const saved = { ...process.env };
|
|
65
|
+
|
|
66
|
+
afterEach(() => {
|
|
67
|
+
for (const key of ['PROXY', 'NO_PROXY', 'no_proxy']) {
|
|
68
|
+
delete process.env[key];
|
|
69
|
+
}
|
|
70
|
+
Object.assign(process.env, saved);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
function clearProxyEnv(): void {
|
|
74
|
+
for (const key of ['PROXY', 'NO_PROXY', 'no_proxy']) {
|
|
75
|
+
delete process.env[key];
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
it('returns undefined when no proxy is configured', () => {
|
|
80
|
+
clearProxyEnv();
|
|
81
|
+
expect(resolveFetchProxyAgent(CODE_ENDPOINT)).toBeUndefined();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* The defect this module exists for: HttpsProxyAgent speaks HTTP CONNECT, so
|
|
86
|
+
* driving a socks5 proxy with it produces "Proxy connection ended before
|
|
87
|
+
* receiving CONNECT response" rather than a working tunnel.
|
|
88
|
+
*/
|
|
89
|
+
it('uses a SOCKS agent for socks schemes', () => {
|
|
90
|
+
clearProxyEnv();
|
|
91
|
+
process.env.PROXY = 'socks5://127.0.0.1:1080';
|
|
92
|
+
|
|
93
|
+
const agent = resolveFetchProxyAgent('https://api.example.com/v1');
|
|
94
|
+
|
|
95
|
+
expect(agent).toBeInstanceOf(SocksProxyAgent);
|
|
96
|
+
expect(agent).not.toBeInstanceOf(HttpsProxyAgent);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it.each(['socks://host:1080', 'socks4://host:1080', 'socks5h://host:1080'])(
|
|
100
|
+
'recognises %s as SOCKS',
|
|
101
|
+
(proxy) => {
|
|
102
|
+
clearProxyEnv();
|
|
103
|
+
process.env.PROXY = proxy;
|
|
104
|
+
expect(
|
|
105
|
+
resolveFetchProxyAgent('https://api.example.com/v1')
|
|
106
|
+
).toBeInstanceOf(SocksProxyAgent);
|
|
107
|
+
}
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
it('uses an HTTP agent for http schemes', () => {
|
|
111
|
+
clearProxyEnv();
|
|
112
|
+
process.env.PROXY = 'http://proxy.internal:3128';
|
|
113
|
+
|
|
114
|
+
expect(resolveFetchProxyAgent('https://api.example.com/v1')).toBeInstanceOf(
|
|
115
|
+
HttpsProxyAgent
|
|
116
|
+
);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Code execution targets an internal endpoint. Without NO_PROXY support the
|
|
121
|
+
* feature is unusable for anyone with PROXY set, because the internal host is
|
|
122
|
+
* not reachable through an external proxy and there is no way to opt out.
|
|
123
|
+
*/
|
|
124
|
+
it('bypasses the proxy for a NO_PROXY host', () => {
|
|
125
|
+
clearProxyEnv();
|
|
126
|
+
process.env.PROXY = 'socks5://127.0.0.1:1080';
|
|
127
|
+
process.env.NO_PROXY = 'codeapi,localhost';
|
|
128
|
+
|
|
129
|
+
expect(resolveFetchProxyAgent(CODE_ENDPOINT)).toBeUndefined();
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('still proxies hosts outside NO_PROXY', () => {
|
|
133
|
+
clearProxyEnv();
|
|
134
|
+
process.env.PROXY = 'socks5://127.0.0.1:1080';
|
|
135
|
+
process.env.NO_PROXY = 'codeapi';
|
|
136
|
+
|
|
137
|
+
expect(resolveFetchProxyAgent('https://api.example.com/v1')).toBeInstanceOf(
|
|
138
|
+
SocksProxyAgent
|
|
139
|
+
);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('honours the lowercase no_proxy spelling', () => {
|
|
143
|
+
clearProxyEnv();
|
|
144
|
+
process.env.PROXY = 'http://proxy.internal:3128';
|
|
145
|
+
process.env.no_proxy = 'codeapi';
|
|
146
|
+
|
|
147
|
+
expect(resolveFetchProxyAgent(CODE_ENDPOINT)).toBeUndefined();
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('prefers an explicit proxy argument over the environment', () => {
|
|
151
|
+
clearProxyEnv();
|
|
152
|
+
process.env.PROXY = 'http://from-env:3128';
|
|
153
|
+
|
|
154
|
+
expect(
|
|
155
|
+
resolveFetchProxyAgent(
|
|
156
|
+
'https://api.example.com/v1',
|
|
157
|
+
'socks5://explicit:1080'
|
|
158
|
+
)
|
|
159
|
+
).toBeInstanceOf(SocksProxyAgent);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* An empty explicit proxy means "no proxy for this call", not "fall back to
|
|
164
|
+
* the environment" — matching the original `proxy != null && proxy !== ''`
|
|
165
|
+
* guard, where callers resolve `initParams.proxy ?? process.env.PROXY`
|
|
166
|
+
* themselves and an empty result disables proxying.
|
|
167
|
+
*/
|
|
168
|
+
it('treats an empty explicit proxy as no proxy rather than falling back to env', () => {
|
|
169
|
+
clearProxyEnv();
|
|
170
|
+
process.env.PROXY = 'http://from-env:3128';
|
|
171
|
+
|
|
172
|
+
expect(
|
|
173
|
+
resolveFetchProxyAgent('https://api.example.com/v1', '')
|
|
174
|
+
).toBeUndefined();
|
|
175
|
+
});
|
|
176
|
+
});
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { HttpsProxyAgent } from 'https-proxy-agent';
|
|
2
|
+
import { SocksProxyAgent } from 'socks-proxy-agent';
|
|
3
|
+
import type { RequestInit } from 'node-fetch';
|
|
4
|
+
|
|
5
|
+
type FetchAgent = NonNullable<RequestInit['agent']>;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Whether `targetUrl` is excluded from proxying by a NO_PROXY list.
|
|
9
|
+
*
|
|
10
|
+
* Follows the de facto convention: comma or whitespace separated entries, an
|
|
11
|
+
* optional leading dot meaning "this host and its subdomains", an optional
|
|
12
|
+
* `:port` suffix that must match when present, and `*` meaning "bypass
|
|
13
|
+
* everything".
|
|
14
|
+
*/
|
|
15
|
+
export function shouldBypassProxy(
|
|
16
|
+
targetUrl: string,
|
|
17
|
+
noProxy?: string
|
|
18
|
+
): boolean {
|
|
19
|
+
const list = (noProxy ?? '').trim();
|
|
20
|
+
if (list === '') {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
let hostname: string;
|
|
25
|
+
let port: string;
|
|
26
|
+
try {
|
|
27
|
+
const url = new URL(targetUrl);
|
|
28
|
+
hostname = url.hostname.toLowerCase().replace(/^\[|\]$/g, '');
|
|
29
|
+
port = url.port || (url.protocol === 'https:' ? '443' : '80');
|
|
30
|
+
} catch {
|
|
31
|
+
/* An unparsable target tells us nothing, so do not claim a bypass. */
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return list
|
|
36
|
+
.split(/[\s,]+/)
|
|
37
|
+
.filter(Boolean)
|
|
38
|
+
.some((raw) => {
|
|
39
|
+
if (raw === '*') {
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
const match = /^(.*?)(?::(\d+))?$/.exec(raw);
|
|
43
|
+
if (!match) {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
const entryHost = (match[1] ?? '').replace(/^\./, '').toLowerCase();
|
|
47
|
+
const entryPort = match[2];
|
|
48
|
+
if (entryHost === '') {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
if (entryPort != null && entryPort !== port) {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
return hostname === entryHost || hostname.endsWith(`.${entryHost}`);
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Build the proxy agent for a `node-fetch` request, or `undefined` when the
|
|
60
|
+
* request should go direct.
|
|
61
|
+
*
|
|
62
|
+
* Two rules that a bare `new HttpsProxyAgent(process.env.PROXY)` gets wrong:
|
|
63
|
+
*
|
|
64
|
+
* - The agent must match the proxy scheme. `HttpsProxyAgent` speaks HTTP
|
|
65
|
+
* CONNECT; handing it a `socks5://` URL produces a request the proxy never
|
|
66
|
+
* answers in that protocol, surfacing as "Proxy connection ended before
|
|
67
|
+
* receiving CONNECT response".
|
|
68
|
+
* - NO_PROXY has to be honoured. Code execution targets an internal endpoint
|
|
69
|
+
* (`http://codeapi:3112`), and forcing that through an external proxy makes
|
|
70
|
+
* the whole feature unusable for anyone who has PROXY set, with no way to
|
|
71
|
+
* opt out.
|
|
72
|
+
*
|
|
73
|
+
* @param targetUrl Absolute URL the request is going to.
|
|
74
|
+
* @param proxyUrl Explicit proxy; falls back to the PROXY environment variable.
|
|
75
|
+
*/
|
|
76
|
+
export function resolveFetchProxyAgent(
|
|
77
|
+
targetUrl: string,
|
|
78
|
+
proxyUrl?: string
|
|
79
|
+
): FetchAgent | undefined {
|
|
80
|
+
const proxy = (proxyUrl ?? process.env.PROXY ?? '').trim();
|
|
81
|
+
if (proxy === '') {
|
|
82
|
+
return undefined;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const noProxy = process.env.NO_PROXY ?? process.env.no_proxy;
|
|
86
|
+
if (shouldBypassProxy(targetUrl, noProxy)) {
|
|
87
|
+
return undefined;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return /^socks/i.test(proxy)
|
|
91
|
+
? (new SocksProxyAgent(proxy) as unknown as FetchAgent)
|
|
92
|
+
: (new HttpsProxyAgent(proxy) as unknown as FetchAgent);
|
|
93
|
+
}
|