lynkr 9.9.1 → 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.
- package/README.md +50 -9
- package/bin/cli.js +2 -0
- package/bin/lynkr-init.js +4 -12
- package/bin/lynkr-reset.js +71 -0
- package/config/model-tiers.json +199 -52
- package/package.json +2 -2
- package/scripts/validate-difficulty-classifier.js +27 -6
- package/src/api/providers-handler.js +0 -1
- package/src/api/router.js +275 -160
- package/src/clients/databricks.js +95 -6
- package/src/config/index.js +3 -43
- package/src/orchestrator/index.js +117 -1235
- package/src/orchestrator/passthrough-stream.js +382 -0
- package/src/orchestrator/sse-transformer.js +408 -0
- package/src/routing/affinity-store.js +17 -3
- package/src/routing/difficulty-classifier.js +52 -10
- package/src/routing/index.js +35 -3
- package/src/routing/intent-score.js +20 -2
- package/src/routing/session-affinity.js +8 -1
- package/src/routing/side-channel-detector.js +103 -0
- package/src/server.js +0 -46
- package/src/agents/context-manager.js +0 -236
- package/src/agents/decomposition/dispatcher.js +0 -185
- package/src/agents/decomposition/gate.js +0 -136
- package/src/agents/decomposition/index.js +0 -183
- package/src/agents/decomposition/model-call.js +0 -75
- package/src/agents/decomposition/planner.js +0 -223
- package/src/agents/decomposition/synthesizer.js +0 -89
- package/src/agents/decomposition/telemetry.js +0 -55
- package/src/agents/definitions/loader.js +0 -653
- package/src/agents/executor.js +0 -457
- package/src/agents/index.js +0 -165
- package/src/agents/parallel-coordinator.js +0 -68
- package/src/agents/reflector.js +0 -331
- package/src/agents/skillbook.js +0 -331
- package/src/agents/store.js +0 -259
- package/src/edits/index.js +0 -171
- package/src/indexer/babel-parser.js +0 -213
- package/src/indexer/index.js +0 -1629
- package/src/indexer/navigation/index.js +0 -32
- package/src/indexer/navigation/providers/treeSitter.js +0 -36
- package/src/indexer/parser.js +0 -443
- package/src/tasks/store.js +0 -349
- package/src/tests/coverage.js +0 -173
- package/src/tests/index.js +0 -171
- package/src/tests/store.js +0 -213
- package/src/tools/agent-task.js +0 -145
- package/src/tools/code-mode.js +0 -304
- package/src/tools/decompose.js +0 -91
- package/src/tools/edits.js +0 -94
- package/src/tools/execution.js +0 -171
- package/src/tools/git.js +0 -1346
- package/src/tools/index.js +0 -306
- package/src/tools/indexer.js +0 -360
- package/src/tools/lazy-loader.js +0 -366
- package/src/tools/mcp-remote.js +0 -88
- package/src/tools/mcp.js +0 -116
- package/src/tools/process.js +0 -167
- package/src/tools/smart-selection.js +0 -180
- package/src/tools/stubs.js +0 -55
- package/src/tools/tasks.js +0 -260
- package/src/tools/tests.js +0 -132
- package/src/tools/tinyfish.js +0 -358
- package/src/tools/truncate.js +0 -106
- package/src/tools/web-client.js +0 -71
- package/src/tools/web.js +0 -415
- package/src/tools/workspace.js +0 -204
package/src/tools/tests.js
DELETED
|
@@ -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
|
-
};
|
package/src/tools/tinyfish.js
DELETED
|
@@ -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
|
-
};
|
package/src/tools/truncate.js
DELETED
|
@@ -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
|
-
};
|
package/src/tools/web-client.js
DELETED
|
@@ -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
|
-
};
|