@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.
- package/dist/cjs/graphs/Graph.cjs +246 -25
- package/dist/cjs/graphs/Graph.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/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 +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/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/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
|
@@ -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];
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import axios from 'axios';
|
|
2
|
+
import { Agent as HttpAgent } from 'http';
|
|
3
|
+
import { Agent as HttpsAgent } from 'https';
|
|
4
|
+
import { createFirecrawlScraper } from './firecrawl';
|
|
5
|
+
import { createReranker } from './rerankers';
|
|
6
|
+
import { createCrwAPI } from './crw-search';
|
|
7
|
+
import { createSearchAPI } from './search';
|
|
8
|
+
|
|
9
|
+
jest.mock('axios');
|
|
10
|
+
const mockedAxios = axios as jest.Mocked<typeof axios>;
|
|
11
|
+
|
|
12
|
+
const httpAgent = new HttpAgent();
|
|
13
|
+
const httpsAgent = new HttpsAgent();
|
|
14
|
+
|
|
15
|
+
describe('injected http(s) agent', () => {
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
jest.clearAllMocks();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('passes both agents to the firecrawl scraper request config', async () => {
|
|
21
|
+
mockedAxios.post.mockResolvedValueOnce({
|
|
22
|
+
data: { success: true, data: { markdown: 'x' } },
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const scraper = createFirecrawlScraper({
|
|
26
|
+
apiKey: 'test-key',
|
|
27
|
+
httpAgent,
|
|
28
|
+
httpsAgent,
|
|
29
|
+
});
|
|
30
|
+
await scraper.scrapeUrl('https://example.com');
|
|
31
|
+
|
|
32
|
+
expect(mockedAxios.post).toHaveBeenCalledWith(
|
|
33
|
+
expect.any(String),
|
|
34
|
+
expect.anything(),
|
|
35
|
+
expect.objectContaining({ httpAgent, httpsAgent })
|
|
36
|
+
);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('omits agents from the firecrawl request config when none are provided', async () => {
|
|
40
|
+
mockedAxios.post.mockResolvedValueOnce({
|
|
41
|
+
data: { success: true, data: { markdown: 'x' } },
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const scraper = createFirecrawlScraper({ apiKey: 'test-key' });
|
|
45
|
+
await scraper.scrapeUrl('https://example.com');
|
|
46
|
+
|
|
47
|
+
const requestConfig = mockedAxios.post.mock.calls[0][2];
|
|
48
|
+
expect(requestConfig?.httpAgent).toBeUndefined();
|
|
49
|
+
expect(requestConfig?.httpsAgent).toBeUndefined();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('passes both agents to the Jina reranker request config', async () => {
|
|
53
|
+
mockedAxios.post.mockResolvedValueOnce({ data: { results: [] } });
|
|
54
|
+
|
|
55
|
+
const reranker = createReranker({
|
|
56
|
+
rerankerType: 'jina',
|
|
57
|
+
jinaApiKey: 'test-key',
|
|
58
|
+
httpAgent,
|
|
59
|
+
httpsAgent,
|
|
60
|
+
});
|
|
61
|
+
await reranker?.rerank('query', ['a', 'b'], 2);
|
|
62
|
+
|
|
63
|
+
expect(mockedAxios.post).toHaveBeenCalledWith(
|
|
64
|
+
expect.any(String),
|
|
65
|
+
expect.anything(),
|
|
66
|
+
expect.objectContaining({ httpAgent, httpsAgent })
|
|
67
|
+
);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('passes agents supplied through the crw search options bag', async () => {
|
|
71
|
+
mockedAxios.post.mockResolvedValueOnce({
|
|
72
|
+
data: { success: true, data: { web: [] } },
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const api = createCrwAPI('test-key', undefined, { httpAgent, httpsAgent });
|
|
76
|
+
await api.getSources({ query: 'query' });
|
|
77
|
+
|
|
78
|
+
expect(mockedAxios.post).toHaveBeenCalledWith(
|
|
79
|
+
expect.any(String),
|
|
80
|
+
expect.anything(),
|
|
81
|
+
expect.objectContaining({ httpAgent, httpsAgent })
|
|
82
|
+
);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('threads agents from the tool config into the serper search request', async () => {
|
|
86
|
+
mockedAxios.post.mockResolvedValueOnce({ data: { organic: [] } });
|
|
87
|
+
|
|
88
|
+
const api = createSearchAPI({
|
|
89
|
+
searchProvider: 'serper',
|
|
90
|
+
serperApiKey: 'test-key',
|
|
91
|
+
httpAgent,
|
|
92
|
+
httpsAgent,
|
|
93
|
+
});
|
|
94
|
+
await api.getSources({ query: 'query' });
|
|
95
|
+
|
|
96
|
+
expect(mockedAxios.post).toHaveBeenCalledWith(
|
|
97
|
+
expect.any(String),
|
|
98
|
+
expect.anything(),
|
|
99
|
+
expect.objectContaining({ httpAgent, httpsAgent })
|
|
100
|
+
);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('threads agents from the tool config into the searxng search request', async () => {
|
|
104
|
+
mockedAxios.get.mockResolvedValueOnce({ data: { results: [] } });
|
|
105
|
+
|
|
106
|
+
const api = createSearchAPI({
|
|
107
|
+
searchProvider: 'searxng',
|
|
108
|
+
searxngInstanceUrl: 'https://searxng.example.com',
|
|
109
|
+
httpAgent,
|
|
110
|
+
httpsAgent,
|
|
111
|
+
});
|
|
112
|
+
await api.getSources({ query: 'query' });
|
|
113
|
+
|
|
114
|
+
expect(mockedAxios.get).toHaveBeenCalledWith(
|
|
115
|
+
expect.any(String),
|
|
116
|
+
expect.objectContaining({ httpAgent, httpsAgent })
|
|
117
|
+
);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('omits agents from the serper search request when none are provided', async () => {
|
|
121
|
+
mockedAxios.post.mockResolvedValueOnce({ data: { organic: [] } });
|
|
122
|
+
|
|
123
|
+
const api = createSearchAPI({
|
|
124
|
+
searchProvider: 'serper',
|
|
125
|
+
serperApiKey: 'test-key',
|
|
126
|
+
});
|
|
127
|
+
await api.getSources({ query: 'query' });
|
|
128
|
+
|
|
129
|
+
const requestConfig = mockedAxios.post.mock.calls[0][2];
|
|
130
|
+
expect(requestConfig?.httpAgent).toBeUndefined();
|
|
131
|
+
expect(requestConfig?.httpsAgent).toBeUndefined();
|
|
132
|
+
});
|
|
133
|
+
});
|
|
@@ -15,6 +15,8 @@ export class KeenableScraper implements t.BaseScraper {
|
|
|
15
15
|
private timeout: number;
|
|
16
16
|
private attributionTitle: string;
|
|
17
17
|
private logger: t.Logger;
|
|
18
|
+
private httpAgent?: t.HttpAgent;
|
|
19
|
+
private httpsAgent?: t.HttpsAgent;
|
|
18
20
|
|
|
19
21
|
constructor(config: t.KeenableScraperConfig = {}) {
|
|
20
22
|
const resolvedKey = config.apiKey ?? process.env.KEENABLE_API_KEY;
|
|
@@ -28,6 +30,8 @@ export class KeenableScraper implements t.BaseScraper {
|
|
|
28
30
|
: KEENABLE_FETCH_PUBLIC_URL);
|
|
29
31
|
this.timeout = config.timeout ?? DEFAULT_KEENABLE_SCRAPE_TIMEOUT;
|
|
30
32
|
this.attributionTitle = config.attributionTitle ?? 'LibreChat';
|
|
33
|
+
this.httpAgent = config.httpAgent;
|
|
34
|
+
this.httpsAgent = config.httpsAgent;
|
|
31
35
|
this.logger = config.logger || createDefaultLogger();
|
|
32
36
|
}
|
|
33
37
|
|
|
@@ -57,6 +61,8 @@ export class KeenableScraper implements t.BaseScraper {
|
|
|
57
61
|
params: { url },
|
|
58
62
|
headers: this.buildHeaders(),
|
|
59
63
|
timeout: options.timeout ?? this.timeout,
|
|
64
|
+
httpAgent: this.httpAgent,
|
|
65
|
+
httpsAgent: this.httpsAgent,
|
|
60
66
|
});
|
|
61
67
|
|
|
62
68
|
const data = response.data;
|
|
@@ -65,7 +65,12 @@ export const createKeenableAPI = (
|
|
|
65
65
|
const response = await axios.post<t.KeenableSearchResponse>(
|
|
66
66
|
resolvedUrl,
|
|
67
67
|
payload,
|
|
68
|
-
{
|
|
68
|
+
{
|
|
69
|
+
headers,
|
|
70
|
+
timeout,
|
|
71
|
+
httpAgent: options?.httpAgent,
|
|
72
|
+
httpsAgent: options?.httpsAgent,
|
|
73
|
+
}
|
|
69
74
|
);
|
|
70
75
|
|
|
71
76
|
const maxResults = Math.min(
|