@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.
- package/dist/cjs/graphs/Graph.cjs +248 -27
- package/dist/cjs/graphs/Graph.cjs.map +1 -1
- package/dist/cjs/langfuseToolOutputTracing.cjs +228 -16
- package/dist/cjs/langfuseToolOutputTracing.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/init.cjs +1 -1
- package/dist/cjs/llm/invoke.cjs +160 -43
- package/dist/cjs/llm/invoke.cjs.map +1 -1
- package/dist/cjs/llm/openai/index.cjs +190 -13
- 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 +12 -3
- package/dist/cjs/messages/core.cjs +592 -27
- package/dist/cjs/messages/core.cjs.map +1 -1
- package/dist/cjs/run.cjs +7 -3
- 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 +105 -14
- 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 +77 -4
- 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 +17 -3
- 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 +2 -1
- 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 +247 -26
- package/dist/esm/graphs/Graph.mjs.map +1 -1
- package/dist/esm/langfuseToolOutputTracing.mjs +228 -16
- package/dist/esm/langfuseToolOutputTracing.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/init.mjs +1 -1
- package/dist/esm/llm/invoke.mjs +160 -43
- package/dist/esm/llm/invoke.mjs.map +1 -1
- package/dist/esm/llm/openai/index.mjs +192 -15
- 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 +8 -6
- package/dist/esm/messages/core.mjs +592 -28
- package/dist/esm/messages/core.mjs.map +1 -1
- package/dist/esm/run.mjs +7 -3
- 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 +105 -14
- 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 +77 -4
- 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 +17 -3
- 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 +2 -1
- 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/langfuseToolOutputTracing.d.ts +1 -0
- package/dist/types/llm/invoke.d.ts +26 -5
- package/dist/types/llm/openai/index.d.ts +3 -0
- package/dist/types/llm/streamLimits.d.ts +314 -0
- package/dist/types/messages/core.d.ts +11 -6
- 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 +7 -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/langfuseToolOutputTracing.ts +410 -14
- package/src/llm/bedrock/index.ts +22 -2
- package/src/llm/custom-chat-models.smoke.test.ts +747 -0
- package/src/llm/invoke.streamLimits.test.ts +142 -0
- package/src/llm/invoke.test.ts +187 -1
- package/src/llm/invoke.ts +231 -43
- package/src/llm/openai/cacheWriteTokens.test.ts +112 -0
- package/src/llm/openai/index.ts +348 -31
- package/src/llm/openai/llm.spec.ts +107 -6
- package/src/llm/streamLimits.test.ts +450 -0
- package/src/llm/streamLimits.ts +1158 -0
- package/src/messages/core.ts +1290 -42
- package/src/messages/formatAgentMessages.test.ts +2623 -0
- package/src/run.ts +4 -0
- package/src/session/AgentSession.ts +5 -0
- package/src/specs/langfuse-tool-output-tracing.test.ts +887 -0
- package/src/specs/preemptSeal.test.ts +374 -5
- 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 +7 -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
|
@@ -24,9 +24,9 @@ describe('buildBashExecutionToolDescription', () => {
|
|
|
24
24
|
expect(BashExecutionToolDescription).toContain('heredoc/printf');
|
|
25
25
|
expect(BashExecutionToolDescription).toContain('not bare Python');
|
|
26
26
|
expect(BashExecutionToolDescription).toContain(
|
|
27
|
-
'
|
|
27
|
+
'Failed executions register nothing'
|
|
28
28
|
);
|
|
29
|
-
expect(BashExecutionToolDescription).toContain('
|
|
29
|
+
expect(BashExecutionToolDescription).toContain('`/tmp` never survives the call');
|
|
30
30
|
});
|
|
31
31
|
|
|
32
32
|
it('appends the tool-output references guide when enabled', () => {
|
|
@@ -51,10 +51,8 @@ describe('ProgrammaticToolCalling', () => {
|
|
|
51
51
|
expect(description).toContain('raw=$(tool');
|
|
52
52
|
expect(description).toContain('direct tool > file may be empty');
|
|
53
53
|
expect(description).toContain('/mnt/data/sf.json');
|
|
54
|
-
expect(description).toContain(
|
|
55
|
-
|
|
56
|
-
);
|
|
57
|
-
expect(description).toContain('not later-call storage');
|
|
54
|
+
expect(description).toContain('Failed executions register nothing');
|
|
55
|
+
expect(description).toContain('`/tmp` never survives the call');
|
|
58
56
|
});
|
|
59
57
|
});
|
|
60
58
|
|
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
} from '../subagent';
|
|
21
21
|
import { sanitizeForwardedSubagentUpdateData } from '../subagent/SubagentExecutor';
|
|
22
22
|
import { Constants, Providers, GraphEvents, StepTypes } from '@/common';
|
|
23
|
+
import { StreamLimitExceededError } from '@/llm/streamLimits';
|
|
23
24
|
import { AgentContext } from '@/agents/AgentContext';
|
|
24
25
|
import { HookRegistry } from '@/hooks/HookRegistry';
|
|
25
26
|
import { HandlerRegistry } from '@/events';
|
|
@@ -451,6 +452,131 @@ describe('SubagentExecutor', () => {
|
|
|
451
452
|
expect(result.messages).toEqual([]);
|
|
452
453
|
});
|
|
453
454
|
|
|
455
|
+
it('rethrows a child stream-limit trip instead of converting it to a tool result', async () => {
|
|
456
|
+
const executor = createExecutor({
|
|
457
|
+
createChildGraph: (): StandardGraph =>
|
|
458
|
+
({
|
|
459
|
+
createWorkflow: (): { invoke: jest.Mock } => ({
|
|
460
|
+
invoke: jest.fn().mockRejectedValue(
|
|
461
|
+
new StreamLimitExceededError({
|
|
462
|
+
kind: 'tool_call_args',
|
|
463
|
+
limit: 10,
|
|
464
|
+
observed: 11,
|
|
465
|
+
toolName: 'db_query',
|
|
466
|
+
})
|
|
467
|
+
),
|
|
468
|
+
}),
|
|
469
|
+
clearHeavyState: jest.fn(),
|
|
470
|
+
}) as unknown as StandardGraph,
|
|
471
|
+
});
|
|
472
|
+
await expect(
|
|
473
|
+
executor.execute({ description: 'Do something', subagentType: 'researcher' })
|
|
474
|
+
).rejects.toBeInstanceOf(StreamLimitExceededError);
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
it('binds the child to the batch-captured controller, not the live scope', async () => {
|
|
478
|
+
const batchBreaker = new AbortController();
|
|
479
|
+
const liveBreaker = new AbortController();
|
|
480
|
+
const executor = createExecutor({
|
|
481
|
+
breakerScope: { controller: () => liveBreaker },
|
|
482
|
+
createChildGraph: (): StandardGraph =>
|
|
483
|
+
({
|
|
484
|
+
createWorkflow: (): { invoke: jest.Mock } => ({
|
|
485
|
+
invoke: jest.fn().mockRejectedValue(
|
|
486
|
+
new StreamLimitExceededError({
|
|
487
|
+
kind: 'tool_call_args',
|
|
488
|
+
limit: 10,
|
|
489
|
+
observed: 11,
|
|
490
|
+
toolName: 'db_query',
|
|
491
|
+
})
|
|
492
|
+
),
|
|
493
|
+
}),
|
|
494
|
+
clearHeavyState: jest.fn(),
|
|
495
|
+
}) as unknown as StandardGraph,
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
/** `breaker` is the controller the parent TOOL BATCH captured before
|
|
499
|
+
* PreToolUse hooks; a reset during those hooks replaced the live
|
|
500
|
+
* scope, and the child must not revive on — or trip — the new run's
|
|
501
|
+
* controller. */
|
|
502
|
+
await expect(
|
|
503
|
+
executor.execute({
|
|
504
|
+
description: 'Do something',
|
|
505
|
+
subagentType: 'researcher',
|
|
506
|
+
breaker: batchBreaker,
|
|
507
|
+
})
|
|
508
|
+
).rejects.toBeInstanceOf(StreamLimitExceededError);
|
|
509
|
+
expect(batchBreaker.signal.aborted).toBe(true);
|
|
510
|
+
expect(liveBreaker.signal.aborted).toBe(false);
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
it('trips the breaker controller captured at execution start, not the current one', async () => {
|
|
514
|
+
const oldRun = new AbortController();
|
|
515
|
+
const newRun = new AbortController();
|
|
516
|
+
let current = oldRun;
|
|
517
|
+
let releaseChild: (() => void) | undefined;
|
|
518
|
+
const executor = createExecutor({
|
|
519
|
+
breakerScope: { controller: () => current },
|
|
520
|
+
createChildGraph: (): StandardGraph =>
|
|
521
|
+
({
|
|
522
|
+
createWorkflow: (): { invoke: jest.Mock } => ({
|
|
523
|
+
invoke: jest.fn().mockImplementation(
|
|
524
|
+
() =>
|
|
525
|
+
new Promise((_resolve, reject) => {
|
|
526
|
+
releaseChild = (): void =>
|
|
527
|
+
reject(
|
|
528
|
+
new StreamLimitExceededError({
|
|
529
|
+
kind: 'tool_call_args',
|
|
530
|
+
limit: 10,
|
|
531
|
+
observed: 11,
|
|
532
|
+
toolName: 'db_query',
|
|
533
|
+
})
|
|
534
|
+
);
|
|
535
|
+
})
|
|
536
|
+
),
|
|
537
|
+
}),
|
|
538
|
+
clearHeavyState: jest.fn(),
|
|
539
|
+
}) as unknown as StandardGraph,
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
const pending = executor.execute({
|
|
543
|
+
description: 'Do something',
|
|
544
|
+
subagentType: 'researcher',
|
|
545
|
+
});
|
|
546
|
+
while (releaseChild == null) {
|
|
547
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
548
|
+
}
|
|
549
|
+
/** The failed run's reset installs a fresh controller while the child
|
|
550
|
+
* is still settling; the straggler's trip must land on the controller
|
|
551
|
+
* captured when its execution started. */
|
|
552
|
+
current = newRun;
|
|
553
|
+
releaseChild();
|
|
554
|
+
await expect(pending).rejects.toBeInstanceOf(StreamLimitExceededError);
|
|
555
|
+
expect(oldRun.signal.aborted).toBe(true);
|
|
556
|
+
expect(newRun.signal.aborted).toBe(false);
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
it('threads run-level streamLimits into every child graph input', async () => {
|
|
560
|
+
const childGraphInputs: StandardGraphInput[] = [];
|
|
561
|
+
const noopFactory = makeNoopGraphFactory();
|
|
562
|
+
const executor = createExecutor({
|
|
563
|
+
streamLimits: { maxToolCallArgBytes: 1234, maxDeltaEventsPerTurn: 9 },
|
|
564
|
+
createChildGraph: (input: StandardGraphInput): StandardGraph => {
|
|
565
|
+
childGraphInputs.push(input);
|
|
566
|
+
return noopFactory();
|
|
567
|
+
},
|
|
568
|
+
});
|
|
569
|
+
await executor.execute({
|
|
570
|
+
description: 'Do something',
|
|
571
|
+
subagentType: 'researcher',
|
|
572
|
+
});
|
|
573
|
+
expect(childGraphInputs).toHaveLength(1);
|
|
574
|
+
expect(childGraphInputs[0].streamLimits).toEqual({
|
|
575
|
+
maxToolCallArgBytes: 1234,
|
|
576
|
+
maxDeltaEventsPerTurn: 9,
|
|
577
|
+
});
|
|
578
|
+
});
|
|
579
|
+
|
|
454
580
|
it('executes child graph and returns filtered content', async () => {
|
|
455
581
|
const { factory, clearHeavyState } = makeStubGraphFactory({
|
|
456
582
|
messages: [
|
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { tool } from '@langchain/core/tools';
|
|
3
|
+
import { AIMessage, ToolMessage } from '@langchain/core/messages';
|
|
4
|
+
import { describe, it, expect, jest, afterEach } from '@jest/globals';
|
|
5
|
+
import type { StructuredToolInterface } from '@langchain/core/tools';
|
|
6
|
+
import type { RunnableConfig } from '@langchain/core/runnables';
|
|
7
|
+
import type * as t from '@/types';
|
|
8
|
+
import * as events from '@/utils/events';
|
|
9
|
+
import {
|
|
10
|
+
StreamLimitExceededError,
|
|
11
|
+
RUN_BREAKER_SCOPE_CONFIG_KEY,
|
|
12
|
+
} from '@/llm/streamLimits';
|
|
13
|
+
import { GraphEvents } from '@/common';
|
|
14
|
+
import { HookRegistry } from '@/hooks';
|
|
15
|
+
import { ToolNode } from '../ToolNode';
|
|
16
|
+
|
|
17
|
+
function createSignalCaptureTool(name: string): {
|
|
18
|
+
tool: StructuredToolInterface;
|
|
19
|
+
observed: () => AbortSignal | undefined;
|
|
20
|
+
} {
|
|
21
|
+
let signal: AbortSignal | undefined;
|
|
22
|
+
const captureTool = tool(
|
|
23
|
+
async (_input, config) => {
|
|
24
|
+
signal = (config as RunnableConfig | undefined)?.signal;
|
|
25
|
+
return 'captured';
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
name,
|
|
29
|
+
description: 'captures the abort signal its runtime receives',
|
|
30
|
+
schema: z.object({}),
|
|
31
|
+
}
|
|
32
|
+
) as unknown as StructuredToolInterface;
|
|
33
|
+
return { tool: captureTool, observed: () => signal };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function createToolCallMessage(id: string, name: string): AIMessage {
|
|
37
|
+
return new AIMessage({ content: '', tool_calls: [{ id, name, args: {} }] });
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** A minimal tool whose invoke ignores the abort signal entirely — unlike
|
|
41
|
+
* langchain `tool()` Runnables, which race the signal and reject with its
|
|
42
|
+
* reason the moment a trip fires mid-execution. */
|
|
43
|
+
function createSignalBlindTool(
|
|
44
|
+
name: string,
|
|
45
|
+
fn: () => Promise<string>
|
|
46
|
+
): StructuredToolInterface {
|
|
47
|
+
return {
|
|
48
|
+
name,
|
|
49
|
+
description: `signal-blind ${name}`,
|
|
50
|
+
invoke: fn,
|
|
51
|
+
} as unknown as StructuredToolInterface;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function installToolExecuteResponder(): {
|
|
55
|
+
toolExecuteCalls: t.ToolExecuteBatchRequest[];
|
|
56
|
+
} {
|
|
57
|
+
const toolExecuteCalls: t.ToolExecuteBatchRequest[] = [];
|
|
58
|
+
jest
|
|
59
|
+
.spyOn(events, 'safeDispatchCustomEvent')
|
|
60
|
+
.mockImplementation(async (event, data): Promise<void> => {
|
|
61
|
+
if (event !== GraphEvents.ON_TOOL_EXECUTE) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
const batch = data as t.ToolExecuteBatchRequest;
|
|
65
|
+
toolExecuteCalls.push(batch);
|
|
66
|
+
batch.resolve(
|
|
67
|
+
batch.toolCalls.map((call) => ({
|
|
68
|
+
toolCallId: call.id,
|
|
69
|
+
status: 'success' as const,
|
|
70
|
+
content: `ok ${call.name}`,
|
|
71
|
+
}))
|
|
72
|
+
);
|
|
73
|
+
});
|
|
74
|
+
return { toolExecuteCalls };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
describe('ToolNode breaker signal composition', () => {
|
|
78
|
+
afterEach(() => {
|
|
79
|
+
jest.restoreAllMocks();
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('exposes the run breaker to direct tool runtimes', async () => {
|
|
83
|
+
const breaker = new AbortController();
|
|
84
|
+
const { tool: capture, observed } = createSignalCaptureTool('capture');
|
|
85
|
+
const node = new ToolNode({
|
|
86
|
+
tools: [capture],
|
|
87
|
+
getBreakerSignal: () => breaker.signal,
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
const result = (await node.invoke({
|
|
91
|
+
messages: [createToolCallMessage('call_1', 'capture')],
|
|
92
|
+
})) as { messages: ToolMessage[] };
|
|
93
|
+
|
|
94
|
+
expect(result.messages[0].content).toBe('captured');
|
|
95
|
+
const signal = observed();
|
|
96
|
+
expect(signal).toBeDefined();
|
|
97
|
+
expect(signal?.aborted).toBe(false);
|
|
98
|
+
|
|
99
|
+
breaker.abort(new Error('stream limit breach'));
|
|
100
|
+
expect(signal?.aborted).toBe(true);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('composes the breaker with the caller signal instead of replacing it', async () => {
|
|
104
|
+
const breaker = new AbortController();
|
|
105
|
+
const caller = new AbortController();
|
|
106
|
+
const { tool: capture, observed } = createSignalCaptureTool('capture');
|
|
107
|
+
const node = new ToolNode({
|
|
108
|
+
tools: [capture],
|
|
109
|
+
getBreakerSignal: () => breaker.signal,
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
await node.invoke(
|
|
113
|
+
{ messages: [createToolCallMessage('call_1', 'capture')] },
|
|
114
|
+
{ signal: caller.signal }
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
const signal = observed();
|
|
118
|
+
expect(signal).toBeDefined();
|
|
119
|
+
expect(signal?.aborted).toBe(false);
|
|
120
|
+
|
|
121
|
+
caller.abort(new Error('caller cancelled'));
|
|
122
|
+
expect(signal?.aborted).toBe(true);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('leaves the caller signal untouched when no breaker accessor is set', async () => {
|
|
126
|
+
const caller = new AbortController();
|
|
127
|
+
const { tool: capture, observed } = createSignalCaptureTool('capture');
|
|
128
|
+
const node = new ToolNode({ tools: [capture] });
|
|
129
|
+
|
|
130
|
+
await node.invoke(
|
|
131
|
+
{ messages: [createToolCallMessage('call_1', 'capture')] },
|
|
132
|
+
{ signal: caller.signal }
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
expect(observed()).toBe(caller.signal);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('rejects the batch at entry when the breaker has already tripped', async () => {
|
|
139
|
+
const breaker = new AbortController();
|
|
140
|
+
const trip = new StreamLimitExceededError({
|
|
141
|
+
kind: 'tool_call_args',
|
|
142
|
+
limit: 10,
|
|
143
|
+
observed: 11,
|
|
144
|
+
toolName: 'db_query',
|
|
145
|
+
});
|
|
146
|
+
breaker.abort(trip);
|
|
147
|
+
let toolRan = false;
|
|
148
|
+
const sideEffect = tool(
|
|
149
|
+
async () => {
|
|
150
|
+
toolRan = true;
|
|
151
|
+
return 'ran';
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
name: 'side_effect',
|
|
155
|
+
description: 'must never run on a failed run',
|
|
156
|
+
schema: z.object({}),
|
|
157
|
+
}
|
|
158
|
+
) as unknown as StructuredToolInterface;
|
|
159
|
+
const node = new ToolNode({
|
|
160
|
+
tools: [sideEffect],
|
|
161
|
+
getBreakerSignal: () => breaker.signal,
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
await expect(
|
|
165
|
+
node.invoke({
|
|
166
|
+
messages: [createToolCallMessage('call_1', 'side_effect')],
|
|
167
|
+
})
|
|
168
|
+
).rejects.toBe(trip);
|
|
169
|
+
expect(toolRan).toBe(false);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it('stops regular siblings when an interrupting tool trips the breaker', async () => {
|
|
173
|
+
const breaker = new AbortController();
|
|
174
|
+
const trip = new StreamLimitExceededError({
|
|
175
|
+
kind: 'tool_call_args',
|
|
176
|
+
limit: 10,
|
|
177
|
+
observed: 11,
|
|
178
|
+
toolName: 'db_query',
|
|
179
|
+
});
|
|
180
|
+
let sideEffectRan = false;
|
|
181
|
+
/** Plain tool objects, NOT langchain `tool()`: Runnable invokes race
|
|
182
|
+
* the composed signal and reject with its reason the instant the trip
|
|
183
|
+
* fires, which would mask the stage boundary this test targets. The
|
|
184
|
+
* exposure is exactly tools that ignore cancellation. */
|
|
185
|
+
const tripper = createSignalBlindTool('ask_question', async () => {
|
|
186
|
+
breaker.abort(trip);
|
|
187
|
+
return 'completed normally across the trip';
|
|
188
|
+
});
|
|
189
|
+
const sideEffect = createSignalBlindTool('send_email', async () => {
|
|
190
|
+
sideEffectRan = true;
|
|
191
|
+
return 'sent';
|
|
192
|
+
});
|
|
193
|
+
const node = new ToolNode({
|
|
194
|
+
tools: [tripper, sideEffect],
|
|
195
|
+
interruptingToolNames: new Set(['ask_question']),
|
|
196
|
+
getBreakerSignal: () => breaker.signal,
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
await expect(
|
|
200
|
+
node.invoke({
|
|
201
|
+
messages: [
|
|
202
|
+
new AIMessage({
|
|
203
|
+
content: '',
|
|
204
|
+
tool_calls: [
|
|
205
|
+
{ id: 'call_1', name: 'ask_question', args: {} },
|
|
206
|
+
{ id: 'call_2', name: 'send_email', args: {} },
|
|
207
|
+
],
|
|
208
|
+
}),
|
|
209
|
+
],
|
|
210
|
+
})
|
|
211
|
+
).rejects.toBe(trip);
|
|
212
|
+
expect(sideEffectRan).toBe(false);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it('stops event dispatch when a direct tool trips the breaker mid-batch', async () => {
|
|
216
|
+
const breaker = new AbortController();
|
|
217
|
+
const trip = new StreamLimitExceededError({
|
|
218
|
+
kind: 'tool_call_args',
|
|
219
|
+
limit: 10,
|
|
220
|
+
observed: 11,
|
|
221
|
+
toolName: 'db_query',
|
|
222
|
+
});
|
|
223
|
+
const { toolExecuteCalls } = installToolExecuteResponder();
|
|
224
|
+
const tripper = createSignalBlindTool('direct_tripper', async () => {
|
|
225
|
+
breaker.abort(trip);
|
|
226
|
+
return 'completed normally across the trip';
|
|
227
|
+
});
|
|
228
|
+
const node = new ToolNode({
|
|
229
|
+
tools: [tripper],
|
|
230
|
+
eventDrivenMode: true,
|
|
231
|
+
directToolNames: new Set(['direct_tripper']),
|
|
232
|
+
toolCallStepIds: new Map([
|
|
233
|
+
['call_1', 'step_1'],
|
|
234
|
+
['call_2', 'step_2'],
|
|
235
|
+
]),
|
|
236
|
+
getBreakerSignal: () => breaker.signal,
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
await expect(
|
|
240
|
+
node.invoke({
|
|
241
|
+
messages: [
|
|
242
|
+
new AIMessage({
|
|
243
|
+
content: '',
|
|
244
|
+
tool_calls: [
|
|
245
|
+
{ id: 'call_1', name: 'direct_tripper', args: {} },
|
|
246
|
+
{ id: 'call_2', name: 'remote_tool', args: {} },
|
|
247
|
+
],
|
|
248
|
+
}),
|
|
249
|
+
],
|
|
250
|
+
})
|
|
251
|
+
).rejects.toBe(trip);
|
|
252
|
+
expect(toolExecuteCalls).toHaveLength(0);
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it('stops a direct tool when the breaker trips during its PreToolUse hook', async () => {
|
|
256
|
+
const breaker = new AbortController();
|
|
257
|
+
const trip = new StreamLimitExceededError({
|
|
258
|
+
kind: 'tool_call_args',
|
|
259
|
+
limit: 10,
|
|
260
|
+
observed: 11,
|
|
261
|
+
toolName: 'db_query',
|
|
262
|
+
});
|
|
263
|
+
let toolRan = false;
|
|
264
|
+
const sideEffect = createSignalBlindTool('send_email', async () => {
|
|
265
|
+
toolRan = true;
|
|
266
|
+
return 'sent';
|
|
267
|
+
});
|
|
268
|
+
const registry = new HookRegistry();
|
|
269
|
+
registry.register('PreToolUse', {
|
|
270
|
+
hooks: [
|
|
271
|
+
async () => {
|
|
272
|
+
breaker.abort(trip);
|
|
273
|
+
return { decision: 'allow' };
|
|
274
|
+
},
|
|
275
|
+
],
|
|
276
|
+
});
|
|
277
|
+
const node = new ToolNode({
|
|
278
|
+
tools: [sideEffect],
|
|
279
|
+
hookRegistry: registry,
|
|
280
|
+
getBreakerSignal: () => breaker.signal,
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
await expect(
|
|
284
|
+
node.invoke({
|
|
285
|
+
messages: [createToolCallMessage('call_1', 'send_email')],
|
|
286
|
+
})
|
|
287
|
+
).rejects.toBe(trip);
|
|
288
|
+
expect(toolRan).toBe(false);
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
it('stops the host batch when the breaker trips during approval hooks', async () => {
|
|
292
|
+
const breaker = new AbortController();
|
|
293
|
+
const trip = new StreamLimitExceededError({
|
|
294
|
+
kind: 'tool_call_args',
|
|
295
|
+
limit: 10,
|
|
296
|
+
observed: 11,
|
|
297
|
+
toolName: 'db_query',
|
|
298
|
+
});
|
|
299
|
+
const { toolExecuteCalls } = installToolExecuteResponder();
|
|
300
|
+
const registry = new HookRegistry();
|
|
301
|
+
registry.register('PreToolUse', {
|
|
302
|
+
hooks: [
|
|
303
|
+
async () => {
|
|
304
|
+
breaker.abort(trip);
|
|
305
|
+
return { decision: 'allow' };
|
|
306
|
+
},
|
|
307
|
+
],
|
|
308
|
+
});
|
|
309
|
+
const node = new ToolNode({
|
|
310
|
+
tools: [],
|
|
311
|
+
eventDrivenMode: true,
|
|
312
|
+
hookRegistry: registry,
|
|
313
|
+
toolCallStepIds: new Map([['call_1', 'step_1']]),
|
|
314
|
+
getBreakerSignal: () => breaker.signal,
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
await expect(
|
|
318
|
+
node.invoke({
|
|
319
|
+
messages: [createToolCallMessage('call_1', 'remote_tool')],
|
|
320
|
+
})
|
|
321
|
+
).rejects.toBe(trip);
|
|
322
|
+
expect(toolExecuteCalls).toHaveLength(0);
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
it('stamps the batch-entry run scope into tool configs and strips it from host batches', async () => {
|
|
326
|
+
const scope = Object.freeze({ epoch: 3, controller: new AbortController() });
|
|
327
|
+
let seenScope: unknown;
|
|
328
|
+
const capture = {
|
|
329
|
+
name: 'capture_scope',
|
|
330
|
+
description: 'captures the batch scope from its config',
|
|
331
|
+
invoke: async (
|
|
332
|
+
_params: unknown,
|
|
333
|
+
config?: { configurable?: Record<string, unknown> }
|
|
334
|
+
): Promise<string> => {
|
|
335
|
+
seenScope = config?.configurable?.[RUN_BREAKER_SCOPE_CONFIG_KEY];
|
|
336
|
+
return 'ok';
|
|
337
|
+
},
|
|
338
|
+
} as unknown as StructuredToolInterface;
|
|
339
|
+
const node = new ToolNode({
|
|
340
|
+
tools: [capture],
|
|
341
|
+
getRunScope: () => scope,
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
await node.invoke({
|
|
345
|
+
messages: [createToolCallMessage('call_1', 'capture_scope')],
|
|
346
|
+
});
|
|
347
|
+
expect(seenScope).toBe(scope);
|
|
348
|
+
|
|
349
|
+
/** Host batch requests spread `configurable` into their own run
|
|
350
|
+
* configs — the scope must not leak there. */
|
|
351
|
+
const { toolExecuteCalls } = installToolExecuteResponder();
|
|
352
|
+
const eventNode = new ToolNode({
|
|
353
|
+
tools: [],
|
|
354
|
+
eventDrivenMode: true,
|
|
355
|
+
toolCallStepIds: new Map([['call_1', 'step_1']]),
|
|
356
|
+
getRunScope: () => scope,
|
|
357
|
+
});
|
|
358
|
+
await eventNode.invoke({
|
|
359
|
+
messages: [createToolCallMessage('call_1', 'remote_tool')],
|
|
360
|
+
});
|
|
361
|
+
expect(toolExecuteCalls).toHaveLength(1);
|
|
362
|
+
expect(
|
|
363
|
+
toolExecuteCalls[0].configurable?.[RUN_BREAKER_SCOPE_CONFIG_KEY]
|
|
364
|
+
).toBeUndefined();
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
it('sends a breaker-composed signal on ON_TOOL_EXECUTE batch requests', async () => {
|
|
368
|
+
const breaker = new AbortController();
|
|
369
|
+
const { toolExecuteCalls } = installToolExecuteResponder();
|
|
370
|
+
const node = new ToolNode({
|
|
371
|
+
tools: [],
|
|
372
|
+
eventDrivenMode: true,
|
|
373
|
+
toolCallStepIds: new Map([['call_1', 'step_1']]),
|
|
374
|
+
getBreakerSignal: () => breaker.signal,
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
await node.invoke({
|
|
378
|
+
messages: [createToolCallMessage('call_1', 'remote_tool')],
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
expect(toolExecuteCalls).toHaveLength(1);
|
|
382
|
+
const { signal } = toolExecuteCalls[0];
|
|
383
|
+
expect(signal).toBeDefined();
|
|
384
|
+
expect(signal?.aborted).toBe(false);
|
|
385
|
+
|
|
386
|
+
breaker.abort(new Error('stream limit breach'));
|
|
387
|
+
expect(signal?.aborted).toBe(true);
|
|
388
|
+
});
|
|
389
|
+
});
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `StreamLimitExceededError` must pass through ToolNode's tool-error
|
|
3
|
+
* conversion like a `GraphInterrupt`: a child run (subagent) that trips a
|
|
4
|
+
* stream circuit breaker is a safety abort, and converting it into an error
|
|
5
|
+
* ToolMessage would let the parent keep generating after the limit fired.
|
|
6
|
+
*/
|
|
7
|
+
import { z } from 'zod';
|
|
8
|
+
import { tool } from '@langchain/core/tools';
|
|
9
|
+
import { AIMessage } from '@langchain/core/messages';
|
|
10
|
+
import { describe, it, expect } from '@jest/globals';
|
|
11
|
+
import type { StructuredToolInterface } from '@langchain/core/tools';
|
|
12
|
+
import { StreamLimitExceededError } from '@/llm/streamLimits';
|
|
13
|
+
import { ToolNode } from '@/tools/ToolNode';
|
|
14
|
+
|
|
15
|
+
const stateWith = (name: string): { messages: AIMessage[] } => ({
|
|
16
|
+
messages: [
|
|
17
|
+
new AIMessage({
|
|
18
|
+
content: '',
|
|
19
|
+
tool_calls: [{ id: 'call_1', name, args: { command: 'x' } }],
|
|
20
|
+
}),
|
|
21
|
+
],
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
describe('ToolNode stream-limit passthrough', () => {
|
|
25
|
+
it('rethrows StreamLimitExceededError instead of converting it to a ToolMessage', async () => {
|
|
26
|
+
const limitBoom = tool(
|
|
27
|
+
async () => {
|
|
28
|
+
throw new StreamLimitExceededError({
|
|
29
|
+
kind: 'tool_call_args',
|
|
30
|
+
limit: 10,
|
|
31
|
+
observed: 11,
|
|
32
|
+
toolName: 'db_query',
|
|
33
|
+
});
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
name: 'limitBoom',
|
|
37
|
+
description: 'tool that trips a stream limit',
|
|
38
|
+
schema: z.object({ command: z.string() }),
|
|
39
|
+
}
|
|
40
|
+
) as unknown as StructuredToolInterface;
|
|
41
|
+
|
|
42
|
+
const node = new ToolNode({ tools: [limitBoom] });
|
|
43
|
+
await expect(
|
|
44
|
+
node.invoke(stateWith('limitBoom'), {
|
|
45
|
+
configurable: { run_id: 'limit-run' },
|
|
46
|
+
})
|
|
47
|
+
).rejects.toBeInstanceOf(StreamLimitExceededError);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('still converts ordinary tool errors to error ToolMessages', async () => {
|
|
51
|
+
const plainBoom = tool(
|
|
52
|
+
async () => {
|
|
53
|
+
throw new Error('ordinary failure');
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
name: 'plainBoom',
|
|
57
|
+
description: 'tool that throws a normal error',
|
|
58
|
+
schema: z.object({ command: z.string() }),
|
|
59
|
+
}
|
|
60
|
+
) as unknown as StructuredToolInterface;
|
|
61
|
+
|
|
62
|
+
const node = new ToolNode({ tools: [plainBoom] });
|
|
63
|
+
const result = (await node.invoke(stateWith('plainBoom'), {
|
|
64
|
+
configurable: { run_id: 'plain-run' },
|
|
65
|
+
})) as { messages: Array<{ content: unknown; status?: string }> };
|
|
66
|
+
expect(result.messages).toHaveLength(1);
|
|
67
|
+
expect(String(result.messages[0].content)).toContain('ordinary failure');
|
|
68
|
+
});
|
|
69
|
+
});
|
|
@@ -27,6 +27,8 @@ export class CrwScraper implements t.BaseScraper {
|
|
|
27
27
|
private xpath?: string;
|
|
28
28
|
private proxy?: string;
|
|
29
29
|
private stealth?: boolean;
|
|
30
|
+
private httpAgent?: t.HttpAgent;
|
|
31
|
+
private httpsAgent?: t.HttpsAgent;
|
|
30
32
|
|
|
31
33
|
constructor(config: t.CrwScraperConfig = {}) {
|
|
32
34
|
this.apiKey = config.apiKey ?? process.env.CRW_API_KEY ?? '';
|
|
@@ -49,6 +51,8 @@ export class CrwScraper implements t.BaseScraper {
|
|
|
49
51
|
this.xpath = config.xpath;
|
|
50
52
|
this.proxy = config.proxy;
|
|
51
53
|
this.stealth = config.stealth;
|
|
54
|
+
this.httpAgent = config.httpAgent;
|
|
55
|
+
this.httpsAgent = config.httpsAgent;
|
|
52
56
|
|
|
53
57
|
// Self-host fastCRW may run without auth, so a missing key is only a
|
|
54
58
|
// warning — unlike Firecrawl/Tavily, scrapeUrl does NOT early-return on it.
|
|
@@ -96,6 +100,8 @@ export class CrwScraper implements t.BaseScraper {
|
|
|
96
100
|
{
|
|
97
101
|
headers,
|
|
98
102
|
timeout: payloadTimeout + CRW_TIMEOUT_BUFFER,
|
|
103
|
+
httpAgent: this.httpAgent,
|
|
104
|
+
httpsAgent: this.httpsAgent,
|
|
99
105
|
}
|
|
100
106
|
);
|
|
101
107
|
|
|
@@ -91,7 +91,12 @@ export const createCrwAPI = (
|
|
|
91
91
|
const response = await axios.post<t.CrwSearchResponse>(
|
|
92
92
|
config.apiUrl,
|
|
93
93
|
payload,
|
|
94
|
-
{
|
|
94
|
+
{
|
|
95
|
+
headers,
|
|
96
|
+
timeout: config.timeout,
|
|
97
|
+
httpAgent: options?.httpAgent,
|
|
98
|
+
httpsAgent: options?.httpsAgent,
|
|
99
|
+
}
|
|
95
100
|
);
|
|
96
101
|
|
|
97
102
|
const body = response.data;
|
|
@@ -29,6 +29,8 @@ export class FirecrawlScraper implements t.BaseScraper {
|
|
|
29
29
|
private location?: { country?: string; languages?: string[] };
|
|
30
30
|
private onlyMainContent?: boolean;
|
|
31
31
|
private changeTrackingOptions?: object;
|
|
32
|
+
private httpAgent?: t.HttpAgent;
|
|
33
|
+
private httpsAgent?: t.HttpsAgent;
|
|
32
34
|
|
|
33
35
|
constructor(config: t.FirecrawlScraperConfig = {}) {
|
|
34
36
|
this.apiKey = config.apiKey ?? process.env.FIRECRAWL_API_KEY ?? '';
|
|
@@ -61,6 +63,8 @@ export class FirecrawlScraper implements t.BaseScraper {
|
|
|
61
63
|
this.location = config.location;
|
|
62
64
|
this.onlyMainContent = config.onlyMainContent;
|
|
63
65
|
this.changeTrackingOptions = config.changeTrackingOptions;
|
|
66
|
+
this.httpAgent = config.httpAgent;
|
|
67
|
+
this.httpsAgent = config.httpsAgent;
|
|
64
68
|
|
|
65
69
|
if (!this.apiKey) {
|
|
66
70
|
this.logger.warn('FIRECRAWL_API_KEY is not set. Scraping will not work.');
|
|
@@ -121,6 +125,8 @@ export class FirecrawlScraper implements t.BaseScraper {
|
|
|
121
125
|
Authorization: `Bearer ${this.apiKey}`,
|
|
122
126
|
},
|
|
123
127
|
timeout: this.timeout,
|
|
128
|
+
httpAgent: this.httpAgent,
|
|
129
|
+
httpsAgent: this.httpsAgent,
|
|
124
130
|
});
|
|
125
131
|
|
|
126
132
|
return [url, response.data];
|