lynkr 9.9.0 → 9.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/README.md +92 -23
  2. package/bin/cli.js +13 -7
  3. package/bin/lynkr-init.js +34 -12
  4. package/bin/lynkr-reset.js +71 -0
  5. package/config/model-tiers.json +199 -52
  6. package/package.json +3 -3
  7. package/scripts/build-eval-set.js +256 -0
  8. package/scripts/mine-difficulty-anchors.js +288 -0
  9. package/scripts/validate-difficulty-classifier.js +144 -0
  10. package/scripts/validate-intent-anchors.js +186 -0
  11. package/src/api/providers-handler.js +0 -1
  12. package/src/api/router.js +292 -160
  13. package/src/clients/databricks.js +95 -6
  14. package/src/config/index.js +3 -43
  15. package/src/context/tool-result-compressor.js +883 -40
  16. package/src/orchestrator/index.js +117 -1235
  17. package/src/orchestrator/passthrough-stream.js +382 -0
  18. package/src/orchestrator/sse-transformer.js +408 -0
  19. package/src/routing/affinity-store.js +17 -3
  20. package/src/routing/classifier-setup.js +207 -0
  21. package/src/routing/complexity-analyzer.js +40 -4
  22. package/src/routing/difficulty-classifier.js +261 -0
  23. package/src/routing/index.js +70 -7
  24. package/src/routing/intent-score.js +132 -12
  25. package/src/routing/session-affinity.js +8 -1
  26. package/src/routing/side-channel-detector.js +103 -0
  27. package/src/server.js +20 -46
  28. package/src/agents/context-manager.js +0 -236
  29. package/src/agents/decomposition/dispatcher.js +0 -185
  30. package/src/agents/decomposition/gate.js +0 -136
  31. package/src/agents/decomposition/index.js +0 -183
  32. package/src/agents/decomposition/model-call.js +0 -75
  33. package/src/agents/decomposition/planner.js +0 -223
  34. package/src/agents/decomposition/synthesizer.js +0 -89
  35. package/src/agents/decomposition/telemetry.js +0 -55
  36. package/src/agents/definitions/loader.js +0 -653
  37. package/src/agents/executor.js +0 -457
  38. package/src/agents/index.js +0 -165
  39. package/src/agents/parallel-coordinator.js +0 -68
  40. package/src/agents/reflector.js +0 -331
  41. package/src/agents/skillbook.js +0 -331
  42. package/src/agents/store.js +0 -259
  43. package/src/edits/index.js +0 -171
  44. package/src/indexer/babel-parser.js +0 -213
  45. package/src/indexer/index.js +0 -1629
  46. package/src/indexer/navigation/index.js +0 -32
  47. package/src/indexer/navigation/providers/treeSitter.js +0 -36
  48. package/src/indexer/parser.js +0 -443
  49. package/src/tasks/store.js +0 -349
  50. package/src/tests/coverage.js +0 -173
  51. package/src/tests/index.js +0 -171
  52. package/src/tests/store.js +0 -213
  53. package/src/tools/agent-task.js +0 -145
  54. package/src/tools/code-mode.js +0 -304
  55. package/src/tools/decompose.js +0 -91
  56. package/src/tools/edits.js +0 -94
  57. package/src/tools/execution.js +0 -171
  58. package/src/tools/git.js +0 -1346
  59. package/src/tools/index.js +0 -306
  60. package/src/tools/indexer.js +0 -360
  61. package/src/tools/lazy-loader.js +0 -366
  62. package/src/tools/mcp-remote.js +0 -88
  63. package/src/tools/mcp.js +0 -116
  64. package/src/tools/process.js +0 -167
  65. package/src/tools/smart-selection.js +0 -180
  66. package/src/tools/stubs.js +0 -55
  67. package/src/tools/tasks.js +0 -260
  68. package/src/tools/tests.js +0 -132
  69. package/src/tools/tinyfish.js +0 -358
  70. package/src/tools/truncate.js +0 -106
  71. package/src/tools/web-client.js +0 -71
  72. package/src/tools/web.js +0 -415
  73. package/src/tools/workspace.js +0 -204
@@ -1,132 +0,0 @@
1
- const { registerTool } = require(".");
2
- const { runWorkspaceTests, listTestRuns, getTestSummary } = require("../tests");
3
-
4
- function formatJson(payload) {
5
- return JSON.stringify(payload, null, 2);
6
- }
7
-
8
- function parseBoolean(value) {
9
- if (typeof value === "boolean") return value;
10
- if (typeof value === "string") {
11
- const normalised = value.trim().toLowerCase();
12
- if (normalised === "true" || normalised === "1" || normalised === "yes") return true;
13
- if (normalised === "false" || normalised === "0" || normalised === "no") return false;
14
- }
15
- return undefined;
16
- }
17
-
18
- function parseNumber(value, fallback) {
19
- if (value === undefined || value === null) return fallback;
20
- const parsed = Number(value);
21
- return Number.isFinite(parsed) ? parsed : fallback;
22
- }
23
-
24
- function parseArgs(args) {
25
- if (!args) return [];
26
- if (Array.isArray(args)) return args.map(String);
27
- if (typeof args === "string" && args.trim().length > 0) {
28
- return args
29
- .split(/\s+/)
30
- .map((item) => item.trim())
31
- .filter(Boolean);
32
- }
33
- return [];
34
- }
35
-
36
- function registerTestRunTool() {
37
- registerTool(
38
- "workspace_test_run",
39
- async ({ args = {} }) => {
40
- const profile = args.profile ?? args.name ?? null;
41
- const collectCoverage = parseBoolean(args.collect_coverage ?? args.collectCoverage);
42
- const sandbox = typeof args.sandbox === "string" ? args.sandbox : undefined;
43
- const sessionId = args.session_id ?? args.sessionId ?? null;
44
-
45
- const result = await runWorkspaceTests({
46
- profileName: profile,
47
- args: parseArgs(args.args ?? args.extra_args),
48
- cwd: typeof args.cwd === "string" ? args.cwd : undefined,
49
- env: typeof args.env === "object" && args.env !== null ? args.env : undefined,
50
- timeoutMs: parseNumber(args.timeout_ms ?? args.timeout, undefined),
51
- sandbox,
52
- sessionId,
53
- collectCoverage: collectCoverage !== undefined ? collectCoverage : true,
54
- });
55
-
56
- return {
57
- ok: true,
58
- status: 200,
59
- content: formatJson({
60
- run: result.run,
61
- coverage: result.coverage,
62
- }),
63
- metadata: {
64
- status: result.run.status,
65
- profile: result.run.profile,
66
- durationMs: result.run.durationMs,
67
- },
68
- };
69
- },
70
- { category: "tests" },
71
- );
72
- }
73
-
74
- function registerTestHistoryTool() {
75
- registerTool(
76
- "workspace_test_history",
77
- async ({ args = {} }) => {
78
- const includeLogs = parseBoolean(args.include_logs ?? args.includeLogs);
79
- const limit = parseNumber(args.limit, 5);
80
- const runs = listTestRuns({
81
- limit,
82
- includeLogs: includeLogs === true,
83
- });
84
- return {
85
- ok: true,
86
- status: 200,
87
- content: formatJson({
88
- runs,
89
- count: runs.length,
90
- }),
91
- metadata: {
92
- count: runs.length,
93
- },
94
- };
95
- },
96
- { category: "tests" },
97
- );
98
- }
99
-
100
- function registerTestSummaryTool() {
101
- registerTool(
102
- "workspace_test_summary",
103
- async ({ args = {} }) => {
104
- const includeRecent = parseBoolean(args.include_recent ?? args.includeRecent);
105
- const recentLimit = parseNumber(args.recent_limit ?? args.recentLimit, 5);
106
- const summary = getTestSummary({
107
- includeRecent,
108
- recentLimit,
109
- });
110
- return {
111
- ok: true,
112
- status: 200,
113
- content: formatJson(summary),
114
- metadata: {
115
- totalRuns: summary.totalRuns,
116
- passRate: summary.passRate,
117
- },
118
- };
119
- },
120
- { category: "tests" },
121
- );
122
- }
123
-
124
- function registerTestTools() {
125
- registerTestRunTool();
126
- registerTestHistoryTool();
127
- registerTestSummaryTool();
128
- }
129
-
130
- module.exports = {
131
- registerTestTools,
132
- };
@@ -1,358 +0,0 @@
1
- const { URL } = require("url");
2
- const { Agent } = require("undici");
3
- const config = require("../config");
4
- const logger = require("../logger");
5
- const { registerTool } = require(".");
6
-
7
- /**
8
- * Dedicated HTTP agent for TinyFish SSE streams.
9
- * The default webAgent in web-client.js has a 30s bodyTimeout which is too
10
- * short for browser-automation tasks that can take up to 120s.
11
- */
12
- const sseAgent = new Agent({
13
- connections: 10,
14
- pipelining: 1,
15
- keepAliveTimeout: 60000,
16
- connectTimeout: 15000,
17
- bodyTimeout: 0, // no body timeout — we manage timeout via AbortController
18
- headersTimeout: 15000,
19
- maxRedirections: 3,
20
- strictContentLength: false,
21
- });
22
-
23
- // ---------------------------------------------------------------------------
24
- // Argument normalisers
25
- // ---------------------------------------------------------------------------
26
-
27
- function normalizeUrl(args) {
28
- const raw = args.url ?? args.uri ?? args.href ?? args.target_url;
29
- if (typeof raw !== "string" || raw.trim().length === 0) {
30
- throw new Error("web_agent requires a non-empty url string.");
31
- }
32
- // Validate URL
33
- try {
34
- new URL(raw.trim());
35
- } catch {
36
- throw new Error(`web_agent received an invalid URL: ${raw}`);
37
- }
38
- return raw.trim();
39
- }
40
-
41
- function normalizeGoal(args) {
42
- const goal = args.goal ?? args.task ?? args.prompt ?? args.instruction;
43
- if (typeof goal !== "string" || goal.trim().length === 0) {
44
- throw new Error("web_agent requires a non-empty goal string.");
45
- }
46
- return goal.trim();
47
- }
48
-
49
- function resolveBrowserProfile(args) {
50
- const profile = args.browser_profile ?? args.browserProfile ?? config.tinyfish.browserProfile;
51
- if (profile === "stealth") return "stealth";
52
- return "lite";
53
- }
54
-
55
- // ---------------------------------------------------------------------------
56
- // SSE stream consumer
57
- // ---------------------------------------------------------------------------
58
-
59
- async function consumeSSEStream(response, timeoutMs) {
60
- const reader = response.body.getReader();
61
- const decoder = new TextDecoder();
62
- let buffer = "";
63
- const startTime = Date.now();
64
-
65
- try {
66
- while (true) {
67
- if (Date.now() - startTime > timeoutMs) {
68
- const err = new Error(`TinyFish SSE stream timed out after ${timeoutMs}ms`);
69
- err.code = "ETIMEDOUT";
70
- err.status = 504;
71
- throw err;
72
- }
73
-
74
- const { done, value } = await reader.read();
75
- if (done) break;
76
-
77
- buffer += decoder.decode(value, { stream: true });
78
-
79
- // SSE events are separated by double newlines
80
- const parts = buffer.split("\n\n");
81
- // Keep the last (possibly incomplete) chunk in the buffer
82
- buffer = parts.pop() || "";
83
-
84
- for (const part of parts) {
85
- // Extract the data: line(s)
86
- const lines = part.split("\n");
87
- let dataStr = "";
88
- for (const line of lines) {
89
- if (line.startsWith("data: ")) {
90
- dataStr += line.slice(6);
91
- } else if (line.startsWith("data:")) {
92
- dataStr += line.slice(5);
93
- }
94
- }
95
-
96
- if (!dataStr) continue;
97
-
98
- let event;
99
- try {
100
- event = JSON.parse(dataStr);
101
- } catch {
102
- // Not valid JSON — skip this SSE frame
103
- logger.debug({ raw: dataStr.slice(0, 200) }, "TinyFish: non-JSON SSE frame, skipping");
104
- continue;
105
- }
106
-
107
- logger.debug(
108
- { type: event.type, status: event.status },
109
- "TinyFish SSE event"
110
- );
111
-
112
- if (event.type === "COMPLETE" || event.type === "complete") {
113
- const status = (event.status ?? "").toUpperCase();
114
- if (status === "COMPLETED" || status === "SUCCESS") {
115
- return event.resultJson ?? event.result ?? event.data ?? event;
116
- }
117
- // Task failed
118
- const errMsg = event.error ?? event.message ?? "TinyFish task failed";
119
- const err = new Error(typeof errMsg === "string" ? errMsg : JSON.stringify(errMsg));
120
- err.code = "TINYFISH_TASK_FAILED";
121
- err.status = 502;
122
- throw err;
123
- }
124
- }
125
- }
126
-
127
- // Stream ended without a COMPLETE event
128
- const err = new Error("TinyFish SSE stream ended without a COMPLETE event");
129
- err.code = "TINYFISH_INCOMPLETE";
130
- err.status = 502;
131
- throw err;
132
- } finally {
133
- reader.releaseLock();
134
- }
135
- }
136
-
137
- // ---------------------------------------------------------------------------
138
- // Core API call
139
- // ---------------------------------------------------------------------------
140
-
141
- async function callTinyFishAPI({ url, goal, browserProfile, proxyConfig, timeoutMs }) {
142
- const endpoint = config.tinyfish.endpoint;
143
- const apiKey = config.tinyfish.apiKey;
144
-
145
- if (!apiKey) {
146
- return {
147
- ok: false,
148
- status: 503,
149
- content: JSON.stringify({
150
- error: "tinyfish_not_configured",
151
- message:
152
- "TinyFish API key is not configured. Set TINYFISH_API_KEY in your .env file. Get a key from https://tinyfish.ai",
153
- }, null, 2),
154
- };
155
- }
156
-
157
- const body = {
158
- url,
159
- goal,
160
- browserProfile,
161
- };
162
-
163
- if (proxyConfig) {
164
- body.proxy = proxyConfig;
165
- }
166
-
167
- const controller = new AbortController();
168
- const timeout = setTimeout(() => controller.abort(), timeoutMs);
169
-
170
- try {
171
- const response = await fetch(endpoint, {
172
- method: "POST",
173
- headers: {
174
- "Content-Type": "application/json",
175
- "X-API-Key": apiKey,
176
- Accept: "text/event-stream",
177
- },
178
- body: JSON.stringify(body),
179
- signal: controller.signal,
180
- dispatcher: sseAgent,
181
- });
182
-
183
- // Handle non-2xx responses before attempting SSE parse
184
- if (!response.ok) {
185
- const text = await response.text().catch(() => "");
186
- const shouldRetry = response.status === 429 || response.status >= 500;
187
-
188
- if (shouldRetry) {
189
- // Retry once with 2s backoff
190
- logger.warn(
191
- { status: response.status, body: text.slice(0, 200) },
192
- "TinyFish API error, retrying once"
193
- );
194
- await new Promise((r) => setTimeout(r, 2000));
195
-
196
- const retryController = new AbortController();
197
- const retryTimeout = setTimeout(() => retryController.abort(), timeoutMs);
198
- try {
199
- const retryResponse = await fetch(endpoint, {
200
- method: "POST",
201
- headers: {
202
- "Content-Type": "application/json",
203
- "X-API-Key": apiKey,
204
- Accept: "text/event-stream",
205
- },
206
- body: JSON.stringify(body),
207
- signal: retryController.signal,
208
- dispatcher: sseAgent,
209
- });
210
-
211
- if (!retryResponse.ok) {
212
- const retryText = await retryResponse.text().catch(() => "");
213
- const err = new Error(
214
- `TinyFish API error (${retryResponse.status}): ${retryResponse.statusText}`
215
- );
216
- err.status = retryResponse.status;
217
- err.body = retryText;
218
- throw err;
219
- }
220
-
221
- const result = await consumeSSEStream(retryResponse, timeoutMs);
222
- return {
223
- ok: true,
224
- status: 200,
225
- result,
226
- };
227
- } finally {
228
- clearTimeout(retryTimeout);
229
- }
230
- }
231
-
232
- const err = new Error(
233
- `TinyFish API error (${response.status}): ${response.statusText}`
234
- );
235
- err.status = response.status;
236
- err.body = text;
237
- throw err;
238
- }
239
-
240
- const result = await consumeSSEStream(response, timeoutMs);
241
- return {
242
- ok: true,
243
- status: 200,
244
- result,
245
- };
246
- } catch (error) {
247
- if (error.name === "AbortError") {
248
- const err = new Error(`TinyFish request timed out after ${timeoutMs}ms`);
249
- err.code = "ETIMEDOUT";
250
- err.status = 504;
251
- throw err;
252
- }
253
- throw error;
254
- } finally {
255
- clearTimeout(timeout);
256
- }
257
- }
258
-
259
- // ---------------------------------------------------------------------------
260
- // Tool registration
261
- // ---------------------------------------------------------------------------
262
-
263
- function registerTinyFishTool() {
264
- registerTool(
265
- "web_agent",
266
- async ({ args = {} }) => {
267
- const url = normalizeUrl(args);
268
- const goal = normalizeGoal(args);
269
- const browserProfile = resolveBrowserProfile(args);
270
- const timeoutMs = config.tinyfish.timeoutMs;
271
-
272
- // Build proxy config if enabled
273
- let proxyConfig = null;
274
- if (config.tinyfish.proxyEnabled) {
275
- proxyConfig = {
276
- enabled: true,
277
- country: config.tinyfish.proxyCountry,
278
- };
279
- }
280
-
281
- try {
282
- const response = await callTinyFishAPI({
283
- url,
284
- goal,
285
- browserProfile,
286
- proxyConfig,
287
- timeoutMs,
288
- });
289
-
290
- // Guard clause: not configured
291
- if (!response.ok && response.status === 503) {
292
- return response;
293
- }
294
-
295
- const resultStr =
296
- typeof response.result === "string"
297
- ? response.result
298
- : JSON.stringify(response.result, null, 2);
299
-
300
- logger.debug(
301
- {
302
- url,
303
- goal: goal.slice(0, 100),
304
- browserProfile,
305
- resultLength: resultStr.length,
306
- },
307
- "TinyFish web_agent completed"
308
- );
309
-
310
- return {
311
- ok: true,
312
- status: 200,
313
- content: resultStr,
314
- metadata: {
315
- url,
316
- goal,
317
- browserProfile,
318
- resultLength: resultStr.length,
319
- },
320
- };
321
- } catch (err) {
322
- logger.error(
323
- { err, url, goal: goal.slice(0, 100) },
324
- "web_agent request failed"
325
- );
326
- return {
327
- ok: false,
328
- status: err.status ?? 500,
329
- content: JSON.stringify(
330
- {
331
- error: err.code ?? "web_agent_failed",
332
- message: err.message,
333
- url,
334
- ...(err.status ? { http_status: err.status } : {}),
335
- },
336
- null,
337
- 2
338
- ),
339
- metadata: {
340
- url,
341
- goal,
342
- error_code: err.code,
343
- ...(err.status ? { http_status: err.status } : {}),
344
- },
345
- };
346
- }
347
- },
348
- { category: "tinyfish" }
349
- );
350
- }
351
-
352
- function registerTinyFishTools() {
353
- registerTinyFishTool();
354
- }
355
-
356
- module.exports = {
357
- registerTinyFishTools,
358
- };
@@ -1,106 +0,0 @@
1
- const logger = require("../logger");
2
- const config = require("../config");
3
-
4
- const TRUNCATION_LIMITS = {
5
- Read: { maxChars: 8000, strategy: 'middle' },
6
- Bash: { maxChars: 30000, strategy: 'tail' },
7
- Grep: { maxChars: 12000, strategy: 'head' },
8
- Glob: { maxChars: 8000, strategy: 'head' },
9
- WebFetch: { maxChars: 16000, strategy: 'head' },
10
- WebSearch: { maxChars: 12000, strategy: 'head' },
11
- WebAgent: { maxChars: 16000, strategy: 'head' },
12
- LSP: { maxChars: 8000, strategy: 'head' },
13
- Edit: { maxChars: 8000, strategy: 'middle' },
14
- Write: { maxChars: 8000, strategy: 'middle' },
15
- Task: { maxChars: 20000, strategy: 'tail' },
16
- AgentTask: { maxChars: 20000, strategy: 'tail' },
17
- };
18
-
19
- /**
20
- * Apply truncation strategy to text
21
- */
22
- function applyTruncationStrategy(text, maxChars, strategy) {
23
- if (text.length <= maxChars) {
24
- return text;
25
- }
26
-
27
- switch (strategy) {
28
- case 'head':
29
- // Keep beginning
30
- return text.slice(0, maxChars);
31
-
32
- case 'tail':
33
- // Keep end
34
- return text.slice(-maxChars);
35
-
36
- case 'middle': {
37
- // Keep start and end, remove middle
38
- const keepSize = Math.floor(maxChars / 2);
39
- const start = text.slice(0, keepSize);
40
- const end = text.slice(-keepSize);
41
- const removed = text.length - (keepSize * 2);
42
- return `${start}\n\n... [${removed} characters truncated for token efficiency] ...\n\n${end}`;
43
- }
44
-
45
- default:
46
- return text.slice(0, maxChars);
47
- }
48
- }
49
-
50
- /**
51
- * Truncate tool output based on tool type
52
- */
53
- function truncateToolOutput(toolName, output) {
54
- // Skip if truncation disabled
55
- if (config.toolTruncation?.enabled === false) {
56
- return output;
57
- }
58
-
59
- if (!output || typeof output !== 'string') {
60
- return output;
61
- }
62
-
63
- const limit = TRUNCATION_LIMITS[toolName];
64
- if (!limit) {
65
- // No truncation for unknown tools
66
- return output;
67
- }
68
-
69
- if (output.length <= limit.maxChars) {
70
- return output;
71
- }
72
-
73
- const truncated = applyTruncationStrategy(output, limit.maxChars, limit.strategy);
74
- const removed = output.length - truncated.length;
75
-
76
- logger.debug({
77
- tool: toolName,
78
- originalLength: output.length,
79
- truncatedLength: truncated.length,
80
- removed,
81
- strategy: limit.strategy
82
- }, 'Truncated tool output for token efficiency');
83
-
84
- return truncated;
85
- }
86
-
87
- /**
88
- * Get truncation limit for a specific tool
89
- */
90
- function getTruncationLimit(toolName) {
91
- return TRUNCATION_LIMITS[toolName] || null;
92
- }
93
-
94
- /**
95
- * Update truncation limit for a tool (useful for testing)
96
- */
97
- function setTruncationLimit(toolName, maxChars, strategy = 'head') {
98
- TRUNCATION_LIMITS[toolName] = { maxChars, strategy };
99
- }
100
-
101
- module.exports = {
102
- truncateToolOutput,
103
- getTruncationLimit,
104
- setTruncationLimit,
105
- TRUNCATION_LIMITS
106
- };
@@ -1,71 +0,0 @@
1
- const { Agent, fetch: undiciFetch, setGlobalDispatcher } = require("undici");
2
- const logger = require("../logger");
3
-
4
- /**
5
- * Create an optimized HTTP agent for web search and fetch operations
6
- * with connection pooling and keep-alive enabled
7
- */
8
- function createWebAgent() {
9
- const agent = new Agent({
10
- // Connection pooling settings
11
- connections: 50, // Max concurrent connections per origin
12
- pipelining: 10, // Max pipelined requests per connection
13
-
14
- // Keep-alive settings
15
- keepAliveTimeout: 60000, // Keep connections alive for 60s
16
- keepAliveMaxTimeout: 600000, // Maximum keep-alive time (10 minutes)
17
-
18
- // Connection timeouts
19
- connectTimeout: 10000, // 10s to establish connection
20
- bodyTimeout: 30000, // 30s to receive response body
21
- headersTimeout: 10000, // 10s to receive headers
22
-
23
- // Connection reuse
24
- maxRedirections: 5,
25
-
26
- // Performance optimizations
27
- strictContentLength: false, // Don't require Content-Length header
28
- });
29
-
30
- logger.info({
31
- connections: 50,
32
- keepAliveTimeout: 60000,
33
- pipelining: 10,
34
- }, "Web HTTP agent initialized with connection pooling");
35
-
36
- return agent;
37
- }
38
-
39
- /**
40
- * Global web agent instance - reused across all web search/fetch calls
41
- */
42
- const webAgent = createWebAgent();
43
-
44
- /**
45
- * Fetch with the optimized agent (uses undici.fetch for dispatcher support)
46
- */
47
- async function fetchWithAgent(url, options = {}) {
48
- return undiciFetch(url, {
49
- ...options,
50
- dispatcher: webAgent,
51
- });
52
- }
53
-
54
- /**
55
- * Get connection pool statistics
56
- */
57
- function getAgentStats() {
58
- // Undici doesn't expose detailed stats easily, but we can log connection info
59
- return {
60
- agent: "undici",
61
- keepAlive: true,
62
- maxConnections: 50,
63
- pipelining: 10,
64
- };
65
- }
66
-
67
- module.exports = {
68
- webAgent,
69
- fetchWithAgent,
70
- getAgentStats,
71
- };