lynkr 9.7.2 → 9.9.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 +29 -19
- package/bin/cli.js +11 -0
- package/bin/lynkr-init.js +14 -1
- package/bin/lynkr-usage.js +78 -0
- package/bin/wrap.js +60 -35
- package/config/difficulty-anchors.json +22 -0
- package/package.json +24 -3
- package/scripts/audit-log-reader.js +399 -0
- package/scripts/calibrate-thresholds.js +38 -157
- package/scripts/compact-dictionary.js +204 -0
- package/scripts/test-deduplication.js +448 -0
- package/scripts/ws7-anchor-replay.js +108 -0
- package/skills/lynkr/SKILL.md +195 -0
- package/src/agents/context-manager.js +18 -2
- package/src/agents/definitions/loader.js +90 -0
- package/src/agents/executor.js +24 -2
- package/src/agents/index.js +9 -1
- package/src/agents/parallel-coordinator.js +2 -2
- package/src/agents/reflector.js +11 -1
- package/src/api/middleware/loop-guard.js +87 -0
- package/src/api/middleware/request-logging.js +5 -64
- package/src/api/middleware/session.js +0 -0
- package/src/api/openai-router.js +120 -101
- package/src/api/providers-handler.js +27 -2
- package/src/api/router.js +450 -125
- package/src/budget/index.js +2 -19
- package/src/cache/semantic.js +9 -0
- package/src/clients/databricks.js +459 -146
- package/src/clients/gpt-utils.js +11 -105
- package/src/clients/openai-format.js +10 -3
- package/src/clients/openrouter-utils.js +49 -24
- package/src/clients/prompt-cache-injection.js +1 -0
- package/src/clients/provider-capabilities.js +1 -1
- package/src/clients/responses-format.js +34 -3
- package/src/clients/routing.js +15 -0
- package/src/config/index.js +36 -2
- package/src/context/gcf.js +275 -0
- package/src/context/tool-result-compressor.js +51 -9
- package/src/dashboard/api.js +1 -0
- package/src/logger/index.js +14 -1
- package/src/memory/search.js +12 -40
- package/src/memory/tools.js +3 -24
- package/src/orchestrator/bypass.js +4 -2
- package/src/orchestrator/index.js +144 -88
- package/src/routing/affinity-store.js +194 -0
- package/src/routing/agentic-detector.js +36 -6
- package/src/routing/bandit.js +25 -6
- package/src/routing/calibration.js +212 -0
- package/src/routing/client-profiles.js +292 -0
- package/src/routing/complexity-analyzer.js +48 -11
- package/src/routing/deescalator.js +148 -0
- package/src/routing/degradation.js +109 -0
- package/src/routing/feedback.js +157 -0
- package/src/routing/index.js +897 -87
- package/src/routing/intent-score.js +339 -0
- package/src/routing/interaction.js +3 -0
- package/src/routing/knn-router.js +70 -21
- package/src/routing/model-registry.js +28 -7
- package/src/routing/model-tiers.js +25 -2
- package/src/routing/reward-pipeline.js +68 -2
- package/src/routing/risk-analyzer.js +30 -1
- package/src/routing/risk-classifier.js +6 -2
- package/src/routing/session-affinity.js +162 -34
- package/src/routing/telemetry.js +298 -10
- package/src/routing/verifier.js +267 -0
- package/src/server.js +66 -21
- package/src/sessions/cleanup.js +17 -0
- package/src/tools/index.js +1 -15
- package/src/tools/smart-selection.js +10 -0
- package/src/tools/web-client.js +3 -3
- package/.eslintrc.cjs +0 -12
- package/benchmark-configs/litellm_config.yaml +0 -86
- package/benchmark-configs/lynkr.env +0 -48
- package/benchmark-configs/portkey-config.json +0 -60
- package/benchmark-configs/portkey-docker.sh +0 -23
- package/benchmark-tier-routing.js +0 -449
- package/funding.json +0 -110
- package/src/api/middleware/validation.js +0 -261
- package/src/routing/drift-monitor.js +0 -113
- package/src/workers/helpers.js +0 -185
|
@@ -1,261 +0,0 @@
|
|
|
1
|
-
const { BadRequestError } = require("./error-handling");
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Input validation middleware
|
|
5
|
-
*
|
|
6
|
-
* Features:
|
|
7
|
-
* - JSON schema-like validation
|
|
8
|
-
* - Type checking
|
|
9
|
-
* - Range validation
|
|
10
|
-
* - Sanitization
|
|
11
|
-
* - Performance-optimized (no external dependencies)
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Validate request body against schema
|
|
16
|
-
*/
|
|
17
|
-
function validateBody(schema) {
|
|
18
|
-
return (req, res, next) => {
|
|
19
|
-
try {
|
|
20
|
-
const errors = validateObject(req.body, schema, "body");
|
|
21
|
-
|
|
22
|
-
if (errors.length > 0) {
|
|
23
|
-
throw new BadRequestError("Validation failed", { errors });
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
next();
|
|
27
|
-
} catch (err) {
|
|
28
|
-
next(err);
|
|
29
|
-
}
|
|
30
|
-
};
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Validate query parameters against schema
|
|
35
|
-
*/
|
|
36
|
-
function validateQuery(schema) {
|
|
37
|
-
return (req, res, next) => {
|
|
38
|
-
try {
|
|
39
|
-
const errors = validateObject(req.query, schema, "query");
|
|
40
|
-
|
|
41
|
-
if (errors.length > 0) {
|
|
42
|
-
throw new BadRequestError("Validation failed", { errors });
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
next();
|
|
46
|
-
} catch (err) {
|
|
47
|
-
next(err);
|
|
48
|
-
}
|
|
49
|
-
};
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Validate object against schema
|
|
54
|
-
*/
|
|
55
|
-
function validateObject(obj, schema, path = "") {
|
|
56
|
-
const errors = [];
|
|
57
|
-
|
|
58
|
-
// Check required fields
|
|
59
|
-
if (schema.required && Array.isArray(schema.required)) {
|
|
60
|
-
for (const field of schema.required) {
|
|
61
|
-
if (obj[field] === undefined || obj[field] === null) {
|
|
62
|
-
errors.push({
|
|
63
|
-
field: `${path}.${field}`,
|
|
64
|
-
message: `Field is required`,
|
|
65
|
-
code: "required",
|
|
66
|
-
});
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// Check properties
|
|
72
|
-
if (schema.properties) {
|
|
73
|
-
for (const [field, fieldSchema] of Object.entries(schema.properties)) {
|
|
74
|
-
const value = obj[field];
|
|
75
|
-
|
|
76
|
-
// Skip if optional and not present
|
|
77
|
-
if (value === undefined || value === null) {
|
|
78
|
-
if (!schema.required || !schema.required.includes(field)) {
|
|
79
|
-
continue;
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
// Validate field
|
|
84
|
-
const fieldErrors = validateField(value, fieldSchema, `${path}.${field}`);
|
|
85
|
-
errors.push(...fieldErrors);
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
return errors;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* Validate individual field
|
|
94
|
-
*/
|
|
95
|
-
function validateField(value, schema, path) {
|
|
96
|
-
const errors = [];
|
|
97
|
-
|
|
98
|
-
// Type validation
|
|
99
|
-
if (schema.type) {
|
|
100
|
-
const actualType = Array.isArray(value) ? "array" : typeof value;
|
|
101
|
-
|
|
102
|
-
if (actualType !== schema.type) {
|
|
103
|
-
errors.push({
|
|
104
|
-
field: path,
|
|
105
|
-
message: `Expected type ${schema.type}, got ${actualType}`,
|
|
106
|
-
code: "invalid_type",
|
|
107
|
-
});
|
|
108
|
-
return errors; // Stop further validation if type is wrong
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
// String validations
|
|
113
|
-
if (schema.type === "string") {
|
|
114
|
-
if (schema.minLength && value.length < schema.minLength) {
|
|
115
|
-
errors.push({
|
|
116
|
-
field: path,
|
|
117
|
-
message: `String length must be at least ${schema.minLength}`,
|
|
118
|
-
code: "min_length",
|
|
119
|
-
});
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
if (schema.maxLength && value.length > schema.maxLength) {
|
|
123
|
-
errors.push({
|
|
124
|
-
field: path,
|
|
125
|
-
message: `String length must be at most ${schema.maxLength}`,
|
|
126
|
-
code: "max_length",
|
|
127
|
-
});
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
if (schema.pattern) {
|
|
131
|
-
const regex = new RegExp(schema.pattern);
|
|
132
|
-
if (!regex.test(value)) {
|
|
133
|
-
errors.push({
|
|
134
|
-
field: path,
|
|
135
|
-
message: `String does not match pattern ${schema.pattern}`,
|
|
136
|
-
code: "pattern_mismatch",
|
|
137
|
-
});
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
if (schema.enum && !schema.enum.includes(value)) {
|
|
142
|
-
errors.push({
|
|
143
|
-
field: path,
|
|
144
|
-
message: `Value must be one of: ${schema.enum.join(", ")}`,
|
|
145
|
-
code: "invalid_enum",
|
|
146
|
-
});
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
// Number validations
|
|
151
|
-
if (schema.type === "number") {
|
|
152
|
-
if (schema.minimum !== undefined && value < schema.minimum) {
|
|
153
|
-
errors.push({
|
|
154
|
-
field: path,
|
|
155
|
-
message: `Value must be at least ${schema.minimum}`,
|
|
156
|
-
code: "minimum",
|
|
157
|
-
});
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
if (schema.maximum !== undefined && value > schema.maximum) {
|
|
161
|
-
errors.push({
|
|
162
|
-
field: path,
|
|
163
|
-
message: `Value must be at most ${schema.maximum}`,
|
|
164
|
-
code: "maximum",
|
|
165
|
-
});
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
// Array validations
|
|
170
|
-
if (schema.type === "array") {
|
|
171
|
-
if (schema.minItems && value.length < schema.minItems) {
|
|
172
|
-
errors.push({
|
|
173
|
-
field: path,
|
|
174
|
-
message: `Array must have at least ${schema.minItems} items`,
|
|
175
|
-
code: "min_items",
|
|
176
|
-
});
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
if (schema.maxItems && value.length > schema.maxItems) {
|
|
180
|
-
errors.push({
|
|
181
|
-
field: path,
|
|
182
|
-
message: `Array must have at most ${schema.maxItems} items`,
|
|
183
|
-
code: "max_items",
|
|
184
|
-
});
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
// Validate array items
|
|
188
|
-
if (schema.items) {
|
|
189
|
-
value.forEach((item, index) => {
|
|
190
|
-
const itemErrors = validateField(item, schema.items, `${path}[${index}]`);
|
|
191
|
-
errors.push(...itemErrors);
|
|
192
|
-
});
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
// Object validations
|
|
197
|
-
if (schema.type === "object" && schema.properties) {
|
|
198
|
-
const objectErrors = validateObject(value, schema, path);
|
|
199
|
-
errors.push(...objectErrors);
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
return errors;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
/**
|
|
206
|
-
* Common validation schemas
|
|
207
|
-
*/
|
|
208
|
-
const commonSchemas = {
|
|
209
|
-
// Messages endpoint
|
|
210
|
-
messagesRequest: {
|
|
211
|
-
type: "object",
|
|
212
|
-
required: ["model", "messages"],
|
|
213
|
-
properties: {
|
|
214
|
-
model: {
|
|
215
|
-
type: "string",
|
|
216
|
-
minLength: 1,
|
|
217
|
-
maxLength: 200,
|
|
218
|
-
},
|
|
219
|
-
messages: {
|
|
220
|
-
type: "array",
|
|
221
|
-
minItems: 1,
|
|
222
|
-
maxItems: 1000,
|
|
223
|
-
items: {
|
|
224
|
-
type: "object",
|
|
225
|
-
required: ["role", "content"],
|
|
226
|
-
properties: {
|
|
227
|
-
role: {
|
|
228
|
-
type: "string",
|
|
229
|
-
enum: ["user", "assistant", "system"],
|
|
230
|
-
},
|
|
231
|
-
content: {
|
|
232
|
-
type: "string",
|
|
233
|
-
minLength: 1,
|
|
234
|
-
},
|
|
235
|
-
},
|
|
236
|
-
},
|
|
237
|
-
},
|
|
238
|
-
max_tokens: {
|
|
239
|
-
type: "number",
|
|
240
|
-
minimum: 1,
|
|
241
|
-
maximum: 100000,
|
|
242
|
-
},
|
|
243
|
-
temperature: {
|
|
244
|
-
type: "number",
|
|
245
|
-
minimum: 0,
|
|
246
|
-
maximum: 2,
|
|
247
|
-
},
|
|
248
|
-
stream: {
|
|
249
|
-
type: "boolean",
|
|
250
|
-
},
|
|
251
|
-
},
|
|
252
|
-
},
|
|
253
|
-
};
|
|
254
|
-
|
|
255
|
-
module.exports = {
|
|
256
|
-
validateBody,
|
|
257
|
-
validateQuery,
|
|
258
|
-
validateObject,
|
|
259
|
-
validateField,
|
|
260
|
-
commonSchemas,
|
|
261
|
-
};
|
|
@@ -1,113 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Drift monitor (Phase 4.3).
|
|
3
|
-
*
|
|
4
|
-
* Tracks two kinds of drift:
|
|
5
|
-
* - Input drift: distribution of query embeddings week-over-week via PSI
|
|
6
|
-
* (Population Stability Index) over coarse bucket assignments.
|
|
7
|
-
* - Output drift: refusal rate, average response length, latency
|
|
8
|
-
* distribution per model.
|
|
9
|
-
*
|
|
10
|
-
* Computes a PSI per metric; alerts when PSI > 0.2 (warning) or > 0.3
|
|
11
|
-
* (full retrain recommended). Writes alerts to data/drift-alerts.json.
|
|
12
|
-
*
|
|
13
|
-
* Auto-retrain is gated on LYNKR_AUTO_RETRAIN=true and not implemented here —
|
|
14
|
-
* the consumer (a cron job or the dashboard) decides what to do.
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
const fs = require('fs');
|
|
18
|
-
const path = require('path');
|
|
19
|
-
const logger = require('../logger');
|
|
20
|
-
|
|
21
|
-
const ALERTS_PATH = path.join(__dirname, '../../data/drift-alerts.json');
|
|
22
|
-
const WARN_THRESHOLD = 0.2;
|
|
23
|
-
const RETRAIN_THRESHOLD = 0.3;
|
|
24
|
-
|
|
25
|
-
function _bucketize(values, bucketCount = 10, min, max) {
|
|
26
|
-
if (values.length === 0) return new Array(bucketCount).fill(0);
|
|
27
|
-
const lo = min ?? Math.min(...values);
|
|
28
|
-
const hi = max ?? Math.max(...values);
|
|
29
|
-
const range = Math.max(1e-9, hi - lo);
|
|
30
|
-
const counts = new Array(bucketCount).fill(0);
|
|
31
|
-
for (const v of values) {
|
|
32
|
-
const idx = Math.max(0, Math.min(bucketCount - 1, Math.floor(((v - lo) / range) * bucketCount)));
|
|
33
|
-
counts[idx]++;
|
|
34
|
-
}
|
|
35
|
-
return counts;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Population Stability Index between two distributions.
|
|
40
|
-
* PSI = Σ (p_new - p_old) · ln(p_new / p_old)
|
|
41
|
-
*/
|
|
42
|
-
function psi(oldCounts, newCounts) {
|
|
43
|
-
const oldTotal = oldCounts.reduce((s, c) => s + c, 0);
|
|
44
|
-
const newTotal = newCounts.reduce((s, c) => s + c, 0);
|
|
45
|
-
if (oldTotal === 0 || newTotal === 0) return 0;
|
|
46
|
-
let sum = 0;
|
|
47
|
-
for (let i = 0; i < oldCounts.length; i++) {
|
|
48
|
-
const p = (oldCounts[i] + 0.5) / (oldTotal + oldCounts.length * 0.5);
|
|
49
|
-
const q = (newCounts[i] + 0.5) / (newTotal + newCounts.length * 0.5);
|
|
50
|
-
sum += (q - p) * Math.log(q / p);
|
|
51
|
-
}
|
|
52
|
-
return sum;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function _writeAlert(alert) {
|
|
56
|
-
try {
|
|
57
|
-
fs.mkdirSync(path.dirname(ALERTS_PATH), { recursive: true });
|
|
58
|
-
let existing = [];
|
|
59
|
-
if (fs.existsSync(ALERTS_PATH)) {
|
|
60
|
-
try { existing = JSON.parse(fs.readFileSync(ALERTS_PATH, 'utf8')); } catch {}
|
|
61
|
-
}
|
|
62
|
-
const out = Array.isArray(existing) ? existing : [];
|
|
63
|
-
out.push({ ...alert, timestamp: Date.now() });
|
|
64
|
-
fs.writeFileSync(ALERTS_PATH, JSON.stringify(out.slice(-200), null, 2));
|
|
65
|
-
} catch (err) {
|
|
66
|
-
logger.warn({ err: err.message }, '[DriftMonitor] Alert write failed');
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* Detect drift between two value series.
|
|
72
|
-
* @param {string} metric - name for logging
|
|
73
|
-
* @param {number[]} oldValues - reference window (e.g. last week)
|
|
74
|
-
* @param {number[]} newValues - current window (e.g. last 24h)
|
|
75
|
-
* @returns {{ psi, level, metric }}
|
|
76
|
-
*/
|
|
77
|
-
function detect(metric, oldValues, newValues) {
|
|
78
|
-
if (oldValues.length < 50 || newValues.length < 20) {
|
|
79
|
-
return { psi: 0, level: 'insufficient_data', metric };
|
|
80
|
-
}
|
|
81
|
-
const combinedMin = Math.min(...oldValues, ...newValues);
|
|
82
|
-
const combinedMax = Math.max(...oldValues, ...newValues);
|
|
83
|
-
const oldB = _bucketize(oldValues, 10, combinedMin, combinedMax);
|
|
84
|
-
const newB = _bucketize(newValues, 10, combinedMin, combinedMax);
|
|
85
|
-
const p = psi(oldB, newB);
|
|
86
|
-
let level = 'ok';
|
|
87
|
-
if (p >= RETRAIN_THRESHOLD) level = 'retrain';
|
|
88
|
-
else if (p >= WARN_THRESHOLD) level = 'warn';
|
|
89
|
-
|
|
90
|
-
if (level !== 'ok') {
|
|
91
|
-
_writeAlert({ metric, psi: p, level, oldSize: oldValues.length, newSize: newValues.length });
|
|
92
|
-
}
|
|
93
|
-
return { psi: p, level, metric };
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* Detect refusal-rate drift by counting the share of responses containing
|
|
98
|
-
* refusal markers in two windows.
|
|
99
|
-
*/
|
|
100
|
-
function refusalRateShift(oldResponses, newResponses) {
|
|
101
|
-
const markers = [/i can't help/i, /i won't/i, /against (?:my )?guidelines/i, /i cannot/i];
|
|
102
|
-
const _rate = (arr) => arr.filter(t => markers.some(m => m.test(t))).length / Math.max(1, arr.length);
|
|
103
|
-
return { old: _rate(oldResponses), new: _rate(newResponses) };
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
module.exports = {
|
|
107
|
-
psi,
|
|
108
|
-
detect,
|
|
109
|
-
refusalRateShift,
|
|
110
|
-
_bucketize,
|
|
111
|
-
WARN_THRESHOLD,
|
|
112
|
-
RETRAIN_THRESHOLD,
|
|
113
|
-
};
|
package/src/workers/helpers.js
DELETED
|
@@ -1,185 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Worker Pool Helper Functions
|
|
3
|
-
*
|
|
4
|
-
* Provides easy-to-use async wrappers for offloading CPU-intensive operations
|
|
5
|
-
* to worker threads with automatic fallback to sync operations.
|
|
6
|
-
*
|
|
7
|
-
* @module workers/helpers
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
const { getWorkerPool, isWorkerPoolReady } = require('./pool');
|
|
11
|
-
const config = require('../config');
|
|
12
|
-
|
|
13
|
-
// Threshold for offloading (bytes)
|
|
14
|
-
const OFFLOAD_THRESHOLD = config.workerPool?.offloadThresholdBytes || 10000;
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Deep clone an object, using worker thread for large objects
|
|
18
|
-
* @param {*} obj - Object to clone
|
|
19
|
-
* @returns {Promise<*>} - Cloned object
|
|
20
|
-
*/
|
|
21
|
-
async function asyncClone(obj) {
|
|
22
|
-
// Estimate size quickly
|
|
23
|
-
let estimatedSize;
|
|
24
|
-
try {
|
|
25
|
-
estimatedSize = JSON.stringify(obj).length;
|
|
26
|
-
} catch {
|
|
27
|
-
// If stringify fails, fall back to sync clone
|
|
28
|
-
return syncClone(obj);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
// Use worker for large objects, sync for small ones
|
|
32
|
-
if (config.workerPool?.enabled !== false && isWorkerPoolReady() && estimatedSize >= OFFLOAD_THRESHOLD) {
|
|
33
|
-
try {
|
|
34
|
-
const pool = getWorkerPool();
|
|
35
|
-
return await pool.clone(obj);
|
|
36
|
-
} catch (err) {
|
|
37
|
-
// Fall back to sync on error
|
|
38
|
-
return syncClone(obj);
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
return syncClone(obj);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Synchronous clone (inline, for small objects)
|
|
47
|
-
*/
|
|
48
|
-
function syncClone(obj) {
|
|
49
|
-
if (typeof structuredClone === 'function') {
|
|
50
|
-
try {
|
|
51
|
-
return structuredClone(obj);
|
|
52
|
-
} catch {
|
|
53
|
-
// structuredClone can fail on some objects
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
return JSON.parse(JSON.stringify(obj));
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Parse JSON, using worker thread for large strings
|
|
61
|
-
* @param {string} jsonString - JSON string to parse
|
|
62
|
-
* @returns {Promise<*>} - Parsed object
|
|
63
|
-
*/
|
|
64
|
-
async function asyncParse(jsonString) {
|
|
65
|
-
if (typeof jsonString !== 'string') {
|
|
66
|
-
throw new TypeError('Expected a JSON string');
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
if (config.workerPool?.enabled !== false && isWorkerPoolReady() && jsonString.length >= OFFLOAD_THRESHOLD) {
|
|
70
|
-
try {
|
|
71
|
-
const pool = getWorkerPool();
|
|
72
|
-
return await pool.parse(jsonString);
|
|
73
|
-
} catch (err) {
|
|
74
|
-
// Fall back to sync on error
|
|
75
|
-
return JSON.parse(jsonString);
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
return JSON.parse(jsonString);
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* Stringify object, using worker thread for large objects
|
|
84
|
-
* @param {*} obj - Object to stringify
|
|
85
|
-
* @returns {Promise<string>} - JSON string
|
|
86
|
-
*/
|
|
87
|
-
async function asyncStringify(obj) {
|
|
88
|
-
// Quick check for size
|
|
89
|
-
const quick = JSON.stringify(obj);
|
|
90
|
-
|
|
91
|
-
if (config.workerPool?.enabled !== false && isWorkerPoolReady() && quick.length >= OFFLOAD_THRESHOLD) {
|
|
92
|
-
try {
|
|
93
|
-
const pool = getWorkerPool();
|
|
94
|
-
return await pool.stringify(obj);
|
|
95
|
-
} catch {
|
|
96
|
-
// Already have the result from quick check
|
|
97
|
-
return quick;
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
return quick;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/**
|
|
105
|
-
* Transform messages (compression, truncation), using worker thread
|
|
106
|
-
* @param {Array} messages - Messages to transform
|
|
107
|
-
* @param {Object} options - Transform options
|
|
108
|
-
* @returns {Promise<Object>} - Transformed messages with stats
|
|
109
|
-
*/
|
|
110
|
-
async function asyncTransform(messages, options = {}) {
|
|
111
|
-
if (!Array.isArray(messages) || messages.length === 0) {
|
|
112
|
-
return { messages, transformed: false, stats: {} };
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
if (config.workerPool?.enabled !== false && isWorkerPoolReady()) {
|
|
116
|
-
try {
|
|
117
|
-
const pool = getWorkerPool();
|
|
118
|
-
return await pool.transform(messages, options);
|
|
119
|
-
} catch (err) {
|
|
120
|
-
// Return untransformed on error
|
|
121
|
-
return { messages, transformed: false, stats: {}, error: err.message };
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// Sync fallback - basic truncation
|
|
126
|
-
return syncTransform(messages, options);
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
/**
|
|
130
|
-
* Synchronous transform fallback
|
|
131
|
-
*/
|
|
132
|
-
function syncTransform(messages, options = {}) {
|
|
133
|
-
const { maxAssistantLength = 5000, maxToolResultLength = 3000 } = options;
|
|
134
|
-
|
|
135
|
-
const transformed = messages.map(msg => {
|
|
136
|
-
if (!msg) return msg;
|
|
137
|
-
|
|
138
|
-
// Truncate long assistant messages
|
|
139
|
-
if (msg.role === 'assistant' && typeof msg.content === 'string' && msg.content.length > maxAssistantLength) {
|
|
140
|
-
return {
|
|
141
|
-
...msg,
|
|
142
|
-
content: msg.content.substring(0, maxAssistantLength) + '\n[...truncated...]',
|
|
143
|
-
};
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
// Truncate tool results
|
|
147
|
-
if (msg.role === 'tool' && typeof msg.content === 'string' && msg.content.length > maxToolResultLength) {
|
|
148
|
-
return {
|
|
149
|
-
...msg,
|
|
150
|
-
content: msg.content.substring(0, maxToolResultLength) + '\n[...truncated...]',
|
|
151
|
-
};
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
return msg;
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
return { messages: transformed, transformed: true, stats: {} };
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
/**
|
|
161
|
-
* Get worker pool statistics
|
|
162
|
-
* @returns {Object|null} - Pool stats or null if not available
|
|
163
|
-
*/
|
|
164
|
-
function getPoolStats() {
|
|
165
|
-
if (!isWorkerPoolReady()) {
|
|
166
|
-
return null;
|
|
167
|
-
}
|
|
168
|
-
try {
|
|
169
|
-
const pool = getWorkerPool();
|
|
170
|
-
return pool.getStats();
|
|
171
|
-
} catch {
|
|
172
|
-
return null;
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
module.exports = {
|
|
177
|
-
asyncClone,
|
|
178
|
-
asyncParse,
|
|
179
|
-
asyncStringify,
|
|
180
|
-
asyncTransform,
|
|
181
|
-
syncClone,
|
|
182
|
-
syncTransform,
|
|
183
|
-
getPoolStats,
|
|
184
|
-
OFFLOAD_THRESHOLD,
|
|
185
|
-
};
|