@lynxops/sdk 1.0.2
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 +470 -0
- package/dist/index.cjs +1371 -0
- package/dist/index.d.cts +601 -0
- package/dist/index.d.ts +601 -0
- package/dist/index.js +1339 -0
- package/package.json +56 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1339 @@
|
|
|
1
|
+
// src/core/tracer.ts
|
|
2
|
+
import { AsyncLocalStorage } from "async_hooks";
|
|
3
|
+
|
|
4
|
+
// src/utils/payload.ts
|
|
5
|
+
import { createHash } from "crypto";
|
|
6
|
+
|
|
7
|
+
// src/config/patterns.json
|
|
8
|
+
var patterns_default = {
|
|
9
|
+
riskPatterns: [
|
|
10
|
+
{ pattern: "ignore", flags: "i" },
|
|
11
|
+
{ pattern: "transfer", flags: "i" },
|
|
12
|
+
{ pattern: "payment", flags: "i" },
|
|
13
|
+
{ pattern: "delete", flags: "i" },
|
|
14
|
+
{ pattern: "system prompt", flags: "i" },
|
|
15
|
+
{ pattern: "bypass", flags: "i" },
|
|
16
|
+
{ pattern: "shutdown", flags: "i" },
|
|
17
|
+
{ pattern: "sudo", flags: "i" },
|
|
18
|
+
{ pattern: "admin", flags: "i" },
|
|
19
|
+
{ pattern: "leak", flags: "i" }
|
|
20
|
+
],
|
|
21
|
+
piiRules: [
|
|
22
|
+
{
|
|
23
|
+
name: "EMAIL",
|
|
24
|
+
pattern: "[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}",
|
|
25
|
+
flags: "g",
|
|
26
|
+
replacement: "[MASKED_EMAIL]"
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
name: "SSN",
|
|
30
|
+
pattern: "\\d{6}-[1-9]\\d{6}",
|
|
31
|
+
flags: "g",
|
|
32
|
+
replacement: "[MASKED_SSN]"
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
name: "CREDIT_CARD",
|
|
36
|
+
pattern: "\\b(?:\\d[ -]*?){13,16}\\b",
|
|
37
|
+
flags: "g",
|
|
38
|
+
replacement: "[MASKED_CARD]"
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
name: "PHONE",
|
|
42
|
+
pattern: "(?:01[016789]-\\d{3,4}-\\d{4})|(?:\\+82-\\d{1,2}-\\d{3,4}-\\d{4})",
|
|
43
|
+
flags: "g",
|
|
44
|
+
replacement: "[MASKED_PHONE]"
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
name: "JWT",
|
|
48
|
+
pattern: "ey[hH]bGciOi[A-Za-z0-9-_=]+\\.[A-Za-z0-9-_=]+\\.?[A-Za-z0-9-_.+/=]*",
|
|
49
|
+
flags: "g",
|
|
50
|
+
replacement: "[MASKED_JWT]"
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
name: "API_KEY",
|
|
54
|
+
pattern: "(?:sk-[a-zA-Z0-9]{32,})|(?:Bearer\\s+[a-zA-Z0-9-_.]+)",
|
|
55
|
+
flags: "gi",
|
|
56
|
+
replacement: "[MASKED_KEY]"
|
|
57
|
+
}
|
|
58
|
+
]
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
// src/utils/payload.ts
|
|
62
|
+
var RISK_PATTERNS = patterns_default.riskPatterns.map(
|
|
63
|
+
(item) => new RegExp(item.pattern, item.flags)
|
|
64
|
+
);
|
|
65
|
+
function getHash(data) {
|
|
66
|
+
const str = typeof data === "string" ? data : JSON.stringify(data);
|
|
67
|
+
return createHash("sha256").update(str || "").digest("hex").slice(0, 16);
|
|
68
|
+
}
|
|
69
|
+
function extractRiskFlags(text) {
|
|
70
|
+
const flags = [];
|
|
71
|
+
for (const pattern of RISK_PATTERNS) {
|
|
72
|
+
if (pattern.test(text)) {
|
|
73
|
+
const cleanPattern = pattern.source.replace(/\\/g, "");
|
|
74
|
+
flags.push(cleanPattern);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return flags;
|
|
78
|
+
}
|
|
79
|
+
function headTailTruncate(text, maxLen = 1e3) {
|
|
80
|
+
if (text.length <= maxLen) return text;
|
|
81
|
+
const reserve = 80;
|
|
82
|
+
const half = Math.floor((maxLen - reserve) / 2);
|
|
83
|
+
if (half <= 0) return "... [TRUNCATED] ...";
|
|
84
|
+
const head = text.substring(0, half);
|
|
85
|
+
const tail = text.substring(text.length - half);
|
|
86
|
+
const truncatedCount = text.length - (head.length + tail.length);
|
|
87
|
+
return `${head}... [TRUNCATED ${truncatedCount} CHARS] ...${tail}`;
|
|
88
|
+
}
|
|
89
|
+
var PII_RULES = patterns_default.piiRules.map((rule) => ({
|
|
90
|
+
name: rule.name,
|
|
91
|
+
regex: new RegExp(rule.pattern, rule.flags),
|
|
92
|
+
replacement: rule.replacement
|
|
93
|
+
}));
|
|
94
|
+
var SENSITIVE_KEY_PATTERN = /(?:api[-_]?key|authorization|auth[-_]?token|bearer|client[-_]?secret|cookie|credential|jwt|password|private[-_]?key|refresh[-_]?token|secret|session[-_]?token|token)/i;
|
|
95
|
+
function maskPIIString(text) {
|
|
96
|
+
let masked = text;
|
|
97
|
+
for (const rule of PII_RULES) {
|
|
98
|
+
masked = masked.replace(rule.regex, rule.replacement);
|
|
99
|
+
}
|
|
100
|
+
return masked;
|
|
101
|
+
}
|
|
102
|
+
function recursiveMaskPII(obj) {
|
|
103
|
+
if (obj === null || obj === void 0) return obj;
|
|
104
|
+
if (typeof obj === "string") {
|
|
105
|
+
return maskPIIString(obj);
|
|
106
|
+
}
|
|
107
|
+
if (Array.isArray(obj)) {
|
|
108
|
+
return obj.map((item) => recursiveMaskPII(item));
|
|
109
|
+
}
|
|
110
|
+
if (typeof obj === "object") {
|
|
111
|
+
const res = {};
|
|
112
|
+
for (const key of Object.keys(obj)) {
|
|
113
|
+
if (SENSITIVE_KEY_PATTERN.test(key)) {
|
|
114
|
+
res[key] = "[MASKED_SECRET]";
|
|
115
|
+
} else {
|
|
116
|
+
res[key] = recursiveMaskPII(obj[key]);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return res;
|
|
120
|
+
}
|
|
121
|
+
return obj;
|
|
122
|
+
}
|
|
123
|
+
function processPayload(payload, config) {
|
|
124
|
+
if (!payload || typeof payload !== "object") return payload;
|
|
125
|
+
let copy;
|
|
126
|
+
try {
|
|
127
|
+
copy = JSON.parse(JSON.stringify(payload));
|
|
128
|
+
} catch {
|
|
129
|
+
return { error: "[Unserializable Payload]" };
|
|
130
|
+
}
|
|
131
|
+
copy = recursiveMaskPII(copy);
|
|
132
|
+
if (config.captureInput === false && "input" in copy) {
|
|
133
|
+
delete copy.input;
|
|
134
|
+
}
|
|
135
|
+
if (config.captureOutput === false && "output" in copy) {
|
|
136
|
+
delete copy.output;
|
|
137
|
+
}
|
|
138
|
+
const mode = config.captureMode || "smart";
|
|
139
|
+
let textToAnalyze = "";
|
|
140
|
+
if (copy.input) {
|
|
141
|
+
textToAnalyze += typeof copy.input === "string" ? copy.input : JSON.stringify(copy.input);
|
|
142
|
+
}
|
|
143
|
+
if (copy.output) {
|
|
144
|
+
textToAnalyze += typeof copy.output === "string" ? copy.output : JSON.stringify(copy.output);
|
|
145
|
+
}
|
|
146
|
+
const riskFlags = textToAnalyze ? extractRiskFlags(textToAnalyze) : [];
|
|
147
|
+
if (riskFlags.length > 0) {
|
|
148
|
+
copy.riskFlags = riskFlags;
|
|
149
|
+
}
|
|
150
|
+
if (mode === "metadata-only") {
|
|
151
|
+
if ("input" in copy) {
|
|
152
|
+
const inputStr = typeof copy.input === "string" ? copy.input : JSON.stringify(copy.input);
|
|
153
|
+
copy.promptHash = getHash(copy.input);
|
|
154
|
+
copy.promptLength = inputStr.length;
|
|
155
|
+
delete copy.input;
|
|
156
|
+
}
|
|
157
|
+
if ("output" in copy) {
|
|
158
|
+
const outputStr = typeof copy.output === "string" ? copy.output : JSON.stringify(copy.output);
|
|
159
|
+
copy.responseHash = getHash(copy.output);
|
|
160
|
+
copy.responseLength = outputStr.length;
|
|
161
|
+
delete copy.output;
|
|
162
|
+
}
|
|
163
|
+
return copy;
|
|
164
|
+
}
|
|
165
|
+
if (mode === "smart") {
|
|
166
|
+
const maxLen = config.maxPayloadLength ?? 1e3;
|
|
167
|
+
const walkAndSmartTruncate = (obj) => {
|
|
168
|
+
if (!obj || typeof obj !== "object") return obj;
|
|
169
|
+
if (Array.isArray(obj)) {
|
|
170
|
+
return obj.map((item) => walkAndSmartTruncate(item));
|
|
171
|
+
}
|
|
172
|
+
const res = {};
|
|
173
|
+
for (const key of Object.keys(obj)) {
|
|
174
|
+
const priorityKeys = [
|
|
175
|
+
"spanid",
|
|
176
|
+
"parentspanid",
|
|
177
|
+
"latency",
|
|
178
|
+
"usage",
|
|
179
|
+
"cost",
|
|
180
|
+
"error",
|
|
181
|
+
"path",
|
|
182
|
+
"riskflags"
|
|
183
|
+
];
|
|
184
|
+
if (priorityKeys.includes(key.toLowerCase())) {
|
|
185
|
+
res[key] = obj[key];
|
|
186
|
+
} else if (typeof obj[key] === "object") {
|
|
187
|
+
res[key] = walkAndSmartTruncate(obj[key]);
|
|
188
|
+
} else if (typeof obj[key] === "string") {
|
|
189
|
+
res[key] = headTailTruncate(obj[key], maxLen);
|
|
190
|
+
} else {
|
|
191
|
+
res[key] = obj[key];
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return res;
|
|
195
|
+
};
|
|
196
|
+
return walkAndSmartTruncate(copy);
|
|
197
|
+
}
|
|
198
|
+
return copy;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// src/utils/id.ts
|
|
202
|
+
import { randomBytes, randomUUID } from "crypto";
|
|
203
|
+
function generateTraceId() {
|
|
204
|
+
try {
|
|
205
|
+
return randomBytes(16).toString("hex");
|
|
206
|
+
} catch {
|
|
207
|
+
return randomUUID().replace(/-/g, "").slice(0, 32);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
function generateSpanId() {
|
|
211
|
+
try {
|
|
212
|
+
return randomBytes(8).toString("hex");
|
|
213
|
+
} catch {
|
|
214
|
+
return randomUUID().replace(/-/g, "").slice(0, 16);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
function generateEventId() {
|
|
218
|
+
try {
|
|
219
|
+
return randomBytes(16).toString("hex");
|
|
220
|
+
} catch {
|
|
221
|
+
return randomUUID().replace(/-/g, "").slice(0, 32);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// src/utils/usage.ts
|
|
226
|
+
function extractTokenUsage(result) {
|
|
227
|
+
if (!result || typeof result !== "object") return void 0;
|
|
228
|
+
const usage = result.usage ?? result.llmOutput?.tokenUsage ?? result.generationInfo?.tokenUsage ?? result.response_metadata?.tokenUsage ?? result.response_metadata?.usage;
|
|
229
|
+
if (usage) {
|
|
230
|
+
const promptTokens = usage.prompt_tokens ?? usage.input_tokens ?? usage.promptTokens ?? usage.inputTokens ?? 0;
|
|
231
|
+
const completionTokens = usage.completion_tokens ?? usage.output_tokens ?? usage.completionTokens ?? usage.outputTokens ?? 0;
|
|
232
|
+
return {
|
|
233
|
+
promptTokens,
|
|
234
|
+
completionTokens,
|
|
235
|
+
totalTokens: promptTokens + completionTokens
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
const usageMetadata = result.usageMetadata;
|
|
239
|
+
if (usageMetadata) {
|
|
240
|
+
const promptTokens = usageMetadata.promptTokenCount ?? 0;
|
|
241
|
+
const completionTokens = usageMetadata.candidatesTokenCount ?? usageMetadata.completionTokenCount ?? 0;
|
|
242
|
+
return {
|
|
243
|
+
promptTokens,
|
|
244
|
+
completionTokens,
|
|
245
|
+
totalTokens: promptTokens + completionTokens
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
return void 0;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// src/instrumentation/index.ts
|
|
252
|
+
function stableHash(data) {
|
|
253
|
+
const str = typeof data === "string" ? data : JSON.stringify(data ?? "");
|
|
254
|
+
let hash = 0;
|
|
255
|
+
for (let i = 0; i < str.length; i++) {
|
|
256
|
+
hash = hash * 31 + str.charCodeAt(i) >>> 0;
|
|
257
|
+
}
|
|
258
|
+
return hash.toString(16).padStart(8, "0");
|
|
259
|
+
}
|
|
260
|
+
function getProvider(path, model) {
|
|
261
|
+
const pathStr = path.join(".");
|
|
262
|
+
if (pathStr.includes("messages.create")) return "anthropic";
|
|
263
|
+
if (pathStr.includes("generateContent")) return "google";
|
|
264
|
+
if (pathStr.includes("generateText") || pathStr.includes("streamText")) return "vercel-ai";
|
|
265
|
+
if (pathStr.includes("ollama")) return "ollama";
|
|
266
|
+
if (pathStr.includes("cohere")) return "cohere";
|
|
267
|
+
if (model?.toLowerCase().includes("claude")) return "anthropic";
|
|
268
|
+
if (model?.toLowerCase().includes("gemini")) return "google";
|
|
269
|
+
return "openai";
|
|
270
|
+
}
|
|
271
|
+
function getMessages(input) {
|
|
272
|
+
if (Array.isArray(input?.messages)) return input.messages;
|
|
273
|
+
if (Array.isArray(input?.contents)) return input.contents;
|
|
274
|
+
if (Array.isArray(input?.prompt)) return input.prompt;
|
|
275
|
+
return void 0;
|
|
276
|
+
}
|
|
277
|
+
function getTextLength(data) {
|
|
278
|
+
if (data === void 0 || data === null) return 0;
|
|
279
|
+
if (typeof data === "string") return data.length;
|
|
280
|
+
try {
|
|
281
|
+
return JSON.stringify(data).length;
|
|
282
|
+
} catch {
|
|
283
|
+
return 0;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
function extractLlmMetadata(input, path) {
|
|
287
|
+
const messages = getMessages(input);
|
|
288
|
+
const systemMessage = messages?.find((item) => item?.role === "system");
|
|
289
|
+
const userMessage = [...messages ?? []].reverse().find((item) => item?.role === "user");
|
|
290
|
+
const model = input?.model ?? input?.modelName;
|
|
291
|
+
const maxTokens = input?.max_tokens ?? input?.maxTokens ?? input?.maxOutputTokens;
|
|
292
|
+
return {
|
|
293
|
+
provider: getProvider(path, model),
|
|
294
|
+
model,
|
|
295
|
+
temperature: input?.temperature,
|
|
296
|
+
topP: input?.top_p ?? input?.topP,
|
|
297
|
+
maxTokens,
|
|
298
|
+
seed: input?.seed,
|
|
299
|
+
promptVersion: input?.promptVersion,
|
|
300
|
+
systemPromptHash: systemMessage ? stableHash(systemMessage) : void 0,
|
|
301
|
+
userPromptHash: userMessage ? stableHash(userMessage) : void 0,
|
|
302
|
+
messageCount: messages?.length,
|
|
303
|
+
contextLength: getTextLength(input)
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
function summarizeResult(result) {
|
|
307
|
+
if (result === void 0 || result === null) return void 0;
|
|
308
|
+
if (typeof result === "string") return result.slice(0, 240);
|
|
309
|
+
if (typeof result === "number" || typeof result === "boolean") return String(result);
|
|
310
|
+
try {
|
|
311
|
+
return JSON.stringify(result).slice(0, 240);
|
|
312
|
+
} catch {
|
|
313
|
+
return "[Unserializable Result]";
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
function instrumentLLM(tracer, instance, optionsOrLabel = "llm.inference") {
|
|
317
|
+
const options = typeof optionsOrLabel === "string" ? { modelLabel: optionsOrLabel } : optionsOrLabel;
|
|
318
|
+
const modelLabel = options.modelLabel ?? "llm.inference";
|
|
319
|
+
const customRule = options.customRule;
|
|
320
|
+
const isTargetLLMMethod = (path) => {
|
|
321
|
+
if (customRule?.isTargetMethod) {
|
|
322
|
+
return customRule.isTargetMethod(path);
|
|
323
|
+
}
|
|
324
|
+
const pathStr = path.join(".");
|
|
325
|
+
return pathStr === "chat.completions.create" || // OpenAI
|
|
326
|
+
pathStr === "messages.create" || // Anthropic (Claude)
|
|
327
|
+
pathStr === "models.generateContent" || // Google Gen AI 신규
|
|
328
|
+
pathStr === "generateContent" || // Google Generative AI 기존
|
|
329
|
+
pathStr === "generateText" || // Vercel AI SDK
|
|
330
|
+
pathStr === "streamText" || // Vercel AI SDK
|
|
331
|
+
pathStr === "generateObject" || // Vercel AI SDK
|
|
332
|
+
pathStr === "streamObject" || // Vercel AI SDK
|
|
333
|
+
pathStr === "ollama.chat" || // Ollama Chat
|
|
334
|
+
pathStr === "ollama.generate" || // Ollama Generate
|
|
335
|
+
pathStr === "cohere.chat" || // Cohere Chat
|
|
336
|
+
pathStr === "cohere.generate" || // Cohere Generate
|
|
337
|
+
pathStr === "chat" || // Ollama/Cohere direct
|
|
338
|
+
pathStr === "invoke" || // LangChain
|
|
339
|
+
pathStr === "predict";
|
|
340
|
+
};
|
|
341
|
+
const createRecursiveProxy = (target, path = []) => {
|
|
342
|
+
if (target == null || typeof target !== "object" && typeof target !== "function") {
|
|
343
|
+
return target;
|
|
344
|
+
}
|
|
345
|
+
return new Proxy(target, {
|
|
346
|
+
get(obj, prop) {
|
|
347
|
+
if (typeof prop === "symbol") {
|
|
348
|
+
return Reflect.get(obj, prop);
|
|
349
|
+
}
|
|
350
|
+
const val = obj[prop];
|
|
351
|
+
const newPath = [...path, prop];
|
|
352
|
+
if (typeof val === "function" && isTargetLLMMethod(newPath)) {
|
|
353
|
+
return async function(...args) {
|
|
354
|
+
const context = LynxTracer.getStore();
|
|
355
|
+
const spanId = generateSpanId();
|
|
356
|
+
const parentSpanId = context?.spanId;
|
|
357
|
+
const startTime = Date.now();
|
|
358
|
+
const rawInput = customRule?.extractInput ? customRule.extractInput(args) : args[0];
|
|
359
|
+
const llmMetadata = extractLlmMetadata(rawInput, newPath);
|
|
360
|
+
if (context) {
|
|
361
|
+
tracer.captureInternal("LLM_CALL", modelLabel, {
|
|
362
|
+
input: rawInput,
|
|
363
|
+
path: newPath.join("."),
|
|
364
|
+
phase: "start",
|
|
365
|
+
spanId,
|
|
366
|
+
parentSpanId,
|
|
367
|
+
...llmMetadata
|
|
368
|
+
}, context);
|
|
369
|
+
}
|
|
370
|
+
try {
|
|
371
|
+
const result = Reflect.apply(val, obj, args);
|
|
372
|
+
const resolvedResult = result instanceof Promise ? await result : result;
|
|
373
|
+
const latency = Date.now() - startTime;
|
|
374
|
+
const rawOutput = customRule?.extractOutput ? customRule.extractOutput(resolvedResult) : resolvedResult;
|
|
375
|
+
const usage = customRule?.extractUsage ? customRule.extractUsage(resolvedResult) : extractTokenUsage(resolvedResult);
|
|
376
|
+
if (context) {
|
|
377
|
+
tracer.captureInternal("LLM_CALL", modelLabel, {
|
|
378
|
+
output: rawOutput,
|
|
379
|
+
phase: "end",
|
|
380
|
+
spanId,
|
|
381
|
+
parentSpanId,
|
|
382
|
+
latency,
|
|
383
|
+
usage,
|
|
384
|
+
...llmMetadata
|
|
385
|
+
}, context);
|
|
386
|
+
}
|
|
387
|
+
return resolvedResult;
|
|
388
|
+
} catch (err) {
|
|
389
|
+
const error = err;
|
|
390
|
+
const latency = Date.now() - startTime;
|
|
391
|
+
if (context) {
|
|
392
|
+
tracer.captureInternal("ERROR", modelLabel, {
|
|
393
|
+
error: error.message,
|
|
394
|
+
phase: "error",
|
|
395
|
+
spanId,
|
|
396
|
+
parentSpanId,
|
|
397
|
+
latency
|
|
398
|
+
}, context);
|
|
399
|
+
}
|
|
400
|
+
throw err;
|
|
401
|
+
}
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
if (val && (typeof val === "object" || typeof val === "function")) {
|
|
405
|
+
return createRecursiveProxy(val, newPath);
|
|
406
|
+
}
|
|
407
|
+
return val;
|
|
408
|
+
}
|
|
409
|
+
});
|
|
410
|
+
};
|
|
411
|
+
return createRecursiveProxy(instance);
|
|
412
|
+
}
|
|
413
|
+
function instrumentTool(tracer, toolName, fn, metadata = {}) {
|
|
414
|
+
return (async (...args) => {
|
|
415
|
+
const context = LynxTracer.getStore();
|
|
416
|
+
const spanId = generateSpanId();
|
|
417
|
+
const parentSpanId = context?.spanId;
|
|
418
|
+
const startTime = Date.now();
|
|
419
|
+
if (context) {
|
|
420
|
+
tracer.captureInternal("TOOL_CALL", toolName, {
|
|
421
|
+
toolName,
|
|
422
|
+
toolVersion: metadata.toolVersion,
|
|
423
|
+
sideEffect: metadata.sideEffect,
|
|
424
|
+
riskLevel: metadata.riskLevel,
|
|
425
|
+
externalTarget: metadata.externalTarget,
|
|
426
|
+
idempotencyKey: metadata.idempotencyKey,
|
|
427
|
+
args: args[0],
|
|
428
|
+
argsHash: stableHash(args[0]),
|
|
429
|
+
input: args[0],
|
|
430
|
+
phase: "start",
|
|
431
|
+
spanId,
|
|
432
|
+
parentSpanId
|
|
433
|
+
}, context);
|
|
434
|
+
}
|
|
435
|
+
try {
|
|
436
|
+
const result = fn(...args);
|
|
437
|
+
const resolvedResult = result instanceof Promise ? await result : result;
|
|
438
|
+
const latency = Date.now() - startTime;
|
|
439
|
+
if (context) {
|
|
440
|
+
tracer.captureInternal("TOOL_RESULT", toolName, {
|
|
441
|
+
toolName,
|
|
442
|
+
toolVersion: metadata.toolVersion,
|
|
443
|
+
result: resolvedResult,
|
|
444
|
+
resultSummary: summarizeResult(resolvedResult),
|
|
445
|
+
output: resolvedResult,
|
|
446
|
+
phase: "end",
|
|
447
|
+
spanId,
|
|
448
|
+
parentSpanId,
|
|
449
|
+
latency
|
|
450
|
+
}, context);
|
|
451
|
+
}
|
|
452
|
+
return resolvedResult;
|
|
453
|
+
} catch (err) {
|
|
454
|
+
const error = err;
|
|
455
|
+
const latency = Date.now() - startTime;
|
|
456
|
+
if (context) {
|
|
457
|
+
tracer.captureInternal("ERROR", toolName, {
|
|
458
|
+
error: error.message,
|
|
459
|
+
phase: "error",
|
|
460
|
+
spanId,
|
|
461
|
+
parentSpanId,
|
|
462
|
+
latency
|
|
463
|
+
}, context);
|
|
464
|
+
}
|
|
465
|
+
throw err;
|
|
466
|
+
}
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// src/core/tracer.ts
|
|
471
|
+
var SDK_VERSION = "1.0.0";
|
|
472
|
+
var DEFAULT_ENDPOINT = "https://api.lynxops.co";
|
|
473
|
+
var DEFAULT_MAX_QUEUE_SIZE = 1e3;
|
|
474
|
+
var DEFAULT_BATCH_SIZE = 50;
|
|
475
|
+
var LynxPolicyError = class extends Error {
|
|
476
|
+
constructor(message, action, policyId, severity, reason, metadata) {
|
|
477
|
+
super(message);
|
|
478
|
+
this.action = action;
|
|
479
|
+
this.policyId = policyId;
|
|
480
|
+
this.severity = severity;
|
|
481
|
+
this.reason = reason;
|
|
482
|
+
this.metadata = metadata;
|
|
483
|
+
this.name = "LynxPolicyError";
|
|
484
|
+
}
|
|
485
|
+
action;
|
|
486
|
+
policyId;
|
|
487
|
+
severity;
|
|
488
|
+
reason;
|
|
489
|
+
metadata;
|
|
490
|
+
};
|
|
491
|
+
function stableHash2(data) {
|
|
492
|
+
const str = typeof data === "string" ? data : JSON.stringify(data ?? "");
|
|
493
|
+
let hash = 0;
|
|
494
|
+
for (let i = 0; i < str.length; i++) {
|
|
495
|
+
hash = hash * 31 + str.charCodeAt(i) >>> 0;
|
|
496
|
+
}
|
|
497
|
+
return hash.toString(16).padStart(8, "0");
|
|
498
|
+
}
|
|
499
|
+
var LynxTracer = class _LynxTracer {
|
|
500
|
+
static storage = new AsyncLocalStorage();
|
|
501
|
+
static instances = /* @__PURE__ */ new Set();
|
|
502
|
+
static beforeExitHookRegistered = false;
|
|
503
|
+
config;
|
|
504
|
+
pendingPromises = /* @__PURE__ */ new Set();
|
|
505
|
+
eventQueue = [];
|
|
506
|
+
retryQueue = [];
|
|
507
|
+
lastFailureTime = 0;
|
|
508
|
+
backoffDelayMs = 1e3;
|
|
509
|
+
flushTimer = null;
|
|
510
|
+
isShuttingDown = false;
|
|
511
|
+
isFlushInProgress = false;
|
|
512
|
+
consecutiveFlushFailures = 0;
|
|
513
|
+
circuitOpenedAt = 0;
|
|
514
|
+
droppedEventCount = 0;
|
|
515
|
+
lastDeliveryAt;
|
|
516
|
+
lastError;
|
|
517
|
+
/**
|
|
518
|
+
* Creates a tracer with the provided Lynx telemetry configuration.
|
|
519
|
+
*
|
|
520
|
+
* The constructor starts a background flush timer. The timer is unref'ed in
|
|
521
|
+
* Node.js so it will not keep the process alive by itself. Call `shutdown()`
|
|
522
|
+
* in tests, short-lived scripts, or server shutdown hooks to flush remaining
|
|
523
|
+
* telemetry and clear the timer.
|
|
524
|
+
*
|
|
525
|
+
* @param config Runtime configuration for telemetry capture and delivery.
|
|
526
|
+
*/
|
|
527
|
+
constructor(config) {
|
|
528
|
+
this.config = {
|
|
529
|
+
...config,
|
|
530
|
+
endpoint: config.endpoint ?? DEFAULT_ENDPOINT
|
|
531
|
+
};
|
|
532
|
+
_LynxTracer.instances.add(this);
|
|
533
|
+
const interval = this.config.delivery?.flushIntervalMs ?? 3e3;
|
|
534
|
+
this.flushTimer = setInterval(() => {
|
|
535
|
+
void this.flushInternal(false);
|
|
536
|
+
}, interval);
|
|
537
|
+
if (this.flushTimer && typeof this.flushTimer.unref === "function") {
|
|
538
|
+
this.flushTimer.unref();
|
|
539
|
+
}
|
|
540
|
+
if (typeof process !== "undefined" && !_LynxTracer.beforeExitHookRegistered) {
|
|
541
|
+
_LynxTracer.beforeExitHookRegistered = true;
|
|
542
|
+
process.once("beforeExit", () => {
|
|
543
|
+
void _LynxTracer.shutdownAll();
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
static async shutdownAll() {
|
|
548
|
+
await Promise.allSettled(
|
|
549
|
+
Array.from(_LynxTracer.instances).map((instance) => instance.shutdown())
|
|
550
|
+
);
|
|
551
|
+
}
|
|
552
|
+
/**
|
|
553
|
+
* Returns the current Lynx async execution context, if one is active.
|
|
554
|
+
*
|
|
555
|
+
* This is mainly useful for advanced instrumentation helpers that need to
|
|
556
|
+
* attach their own telemetry to the currently running `run()` context.
|
|
557
|
+
*
|
|
558
|
+
* @returns The current context, or `undefined` when called outside `run()`.
|
|
559
|
+
*/
|
|
560
|
+
static getStore() {
|
|
561
|
+
return _LynxTracer.storage.getStore();
|
|
562
|
+
}
|
|
563
|
+
getFlushOnRunEnd() {
|
|
564
|
+
return this.config.delivery?.flushOnRunEnd ?? false;
|
|
565
|
+
}
|
|
566
|
+
getRequestTimeoutMs() {
|
|
567
|
+
return this.config.delivery?.timeoutMs ?? 1e3;
|
|
568
|
+
}
|
|
569
|
+
isBackgroundOnly() {
|
|
570
|
+
return (this.config.delivery?.mode ?? "BACKGROUND") === "BACKGROUND";
|
|
571
|
+
}
|
|
572
|
+
getMaxQueueSize() {
|
|
573
|
+
return this.config.delivery?.maxQueueSize ?? DEFAULT_MAX_QUEUE_SIZE;
|
|
574
|
+
}
|
|
575
|
+
getOverflowStrategy() {
|
|
576
|
+
return this.config.delivery?.overflowStrategy ?? "DROP_OLDEST";
|
|
577
|
+
}
|
|
578
|
+
enqueueEvent(event) {
|
|
579
|
+
const maxQueueSize = this.getMaxQueueSize();
|
|
580
|
+
if (maxQueueSize <= 0) {
|
|
581
|
+
this.droppedEventCount += 1;
|
|
582
|
+
return false;
|
|
583
|
+
}
|
|
584
|
+
const totalQueued = this.eventQueue.length + this.retryQueue.length;
|
|
585
|
+
if (totalQueued >= maxQueueSize) {
|
|
586
|
+
this.droppedEventCount += 1;
|
|
587
|
+
if (this.getOverflowStrategy() === "DROP_NEWEST") {
|
|
588
|
+
return false;
|
|
589
|
+
}
|
|
590
|
+
if (this.retryQueue.length > 0) {
|
|
591
|
+
this.retryQueue.shift();
|
|
592
|
+
} else {
|
|
593
|
+
this.eventQueue.shift();
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
this.eventQueue.push(event);
|
|
597
|
+
return true;
|
|
598
|
+
}
|
|
599
|
+
enqueueRetryEvent(event) {
|
|
600
|
+
const maxQueueSize = this.getMaxQueueSize();
|
|
601
|
+
if (maxQueueSize <= 0) {
|
|
602
|
+
this.droppedEventCount += 1;
|
|
603
|
+
return false;
|
|
604
|
+
}
|
|
605
|
+
const totalQueued = this.eventQueue.length + this.retryQueue.length;
|
|
606
|
+
if (totalQueued >= maxQueueSize) {
|
|
607
|
+
this.droppedEventCount += 1;
|
|
608
|
+
if (this.getOverflowStrategy() === "DROP_NEWEST") {
|
|
609
|
+
return false;
|
|
610
|
+
}
|
|
611
|
+
if (this.eventQueue.length > 0) {
|
|
612
|
+
this.eventQueue.shift();
|
|
613
|
+
} else {
|
|
614
|
+
this.retryQueue.shift();
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
this.retryQueue.push(event);
|
|
618
|
+
return true;
|
|
619
|
+
}
|
|
620
|
+
/**
|
|
621
|
+
* Flushes queued telemetry events to the Lynx ingestion endpoint.
|
|
622
|
+
*
|
|
623
|
+
* Events are sent in a single batch request. If delivery fails, the batch is
|
|
624
|
+
* moved into an in-memory retry queue and future flushes are delayed with
|
|
625
|
+
* exponential backoff. Normal applications usually do not need to call this
|
|
626
|
+
* manually because the tracer flushes on an interval.
|
|
627
|
+
*
|
|
628
|
+
* @returns A promise that resolves after the current flush attempt completes.
|
|
629
|
+
*/
|
|
630
|
+
async flush() {
|
|
631
|
+
await this.flushInternal(true);
|
|
632
|
+
}
|
|
633
|
+
async flushInternal(waitForDelivery) {
|
|
634
|
+
if (this.eventQueue.length === 0 && this.retryQueue.length === 0) {
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
637
|
+
if (this.isFlushInProgress) {
|
|
638
|
+
return;
|
|
639
|
+
}
|
|
640
|
+
if (this.isCircuitOpen()) {
|
|
641
|
+
return;
|
|
642
|
+
}
|
|
643
|
+
if (this.lastFailureTime > 0 && Date.now() - this.lastFailureTime < this.backoffDelayMs) {
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
646
|
+
const eventsToFlush = [...this.retryQueue, ...this.eventQueue];
|
|
647
|
+
this.retryQueue.length = 0;
|
|
648
|
+
this.eventQueue.length = 0;
|
|
649
|
+
if (eventsToFlush.length === 0) {
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
652
|
+
this.isFlushInProgress = true;
|
|
653
|
+
const url = `${this.config.endpoint}/openapi/v1/events/batch`;
|
|
654
|
+
const headers = {
|
|
655
|
+
"Content-Type": "application/json"
|
|
656
|
+
};
|
|
657
|
+
if (this.config.apiKey) {
|
|
658
|
+
headers["x-api-key"] = this.config.apiKey;
|
|
659
|
+
headers["Authorization"] = `Bearer ${this.config.apiKey}`;
|
|
660
|
+
}
|
|
661
|
+
const promise = fetch(url, {
|
|
662
|
+
method: "POST",
|
|
663
|
+
headers,
|
|
664
|
+
body: JSON.stringify(eventsToFlush),
|
|
665
|
+
signal: AbortSignal.timeout(this.getRequestTimeoutMs())
|
|
666
|
+
}).then(async (res) => {
|
|
667
|
+
if (!res.ok) {
|
|
668
|
+
const text = await res.text();
|
|
669
|
+
console.error(
|
|
670
|
+
`[LynxTracer] telemetry batch flush failed with status ${res.status}: ${text}`
|
|
671
|
+
);
|
|
672
|
+
this.handleFailedEvents(eventsToFlush, "http_error");
|
|
673
|
+
} else {
|
|
674
|
+
const result = await res.json().catch(() => ({ success: true }));
|
|
675
|
+
if (result && result.success === false) {
|
|
676
|
+
console.error(
|
|
677
|
+
"[LynxTracer] telemetry batch flush returned failed details:",
|
|
678
|
+
result
|
|
679
|
+
);
|
|
680
|
+
this.handleFailedEvents(eventsToFlush, "batch_failed");
|
|
681
|
+
} else {
|
|
682
|
+
this.handleSuccessfulFlush();
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
}).catch((err) => {
|
|
686
|
+
console.error("LynxTracer telemetry batch flush failed:", err);
|
|
687
|
+
this.handleFailedEvents(eventsToFlush, "network_error");
|
|
688
|
+
}).finally(() => {
|
|
689
|
+
this.pendingPromises.delete(promise);
|
|
690
|
+
this.isFlushInProgress = false;
|
|
691
|
+
});
|
|
692
|
+
this.pendingPromises.add(promise);
|
|
693
|
+
if (waitForDelivery || this.isShuttingDown) {
|
|
694
|
+
await promise;
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
/**
|
|
698
|
+
* Returns a lightweight snapshot of SDK delivery health.
|
|
699
|
+
*
|
|
700
|
+
* Use this for readiness diagnostics, operational dashboards, or tests. The
|
|
701
|
+
* status reflects local SDK state only; it does not perform a network request.
|
|
702
|
+
*
|
|
703
|
+
* @returns Current queue, circuit breaker, and delivery state.
|
|
704
|
+
*/
|
|
705
|
+
getStatus() {
|
|
706
|
+
return {
|
|
707
|
+
queueSize: this.eventQueue.length + this.retryQueue.length,
|
|
708
|
+
droppedEvents: this.droppedEventCount,
|
|
709
|
+
circuitState: this.getCircuitState(),
|
|
710
|
+
lastDeliveryAt: this.lastDeliveryAt,
|
|
711
|
+
lastError: this.lastError,
|
|
712
|
+
pendingTransmissions: this.pendingPromises.size
|
|
713
|
+
};
|
|
714
|
+
}
|
|
715
|
+
/**
|
|
716
|
+
* Flushes queued telemetry and releases SDK timers.
|
|
717
|
+
*
|
|
718
|
+
* Use this for graceful process shutdown, tests, CLI scripts, and serverless
|
|
719
|
+
* handlers where the runtime may terminate before the background interval
|
|
720
|
+
* fires. Calling `shutdown()` more than once is safe.
|
|
721
|
+
*
|
|
722
|
+
* @returns A promise that resolves after pending telemetry transmissions settle.
|
|
723
|
+
*/
|
|
724
|
+
async shutdown(options = {}) {
|
|
725
|
+
if (this.isShuttingDown) return;
|
|
726
|
+
this.isShuttingDown = true;
|
|
727
|
+
if (this.flushTimer) {
|
|
728
|
+
clearInterval(this.flushTimer);
|
|
729
|
+
this.flushTimer = null;
|
|
730
|
+
}
|
|
731
|
+
const shutdownWork = async () => {
|
|
732
|
+
await this.flushInternal(true);
|
|
733
|
+
if (this.pendingPromises.size > 0) {
|
|
734
|
+
await Promise.allSettled(Array.from(this.pendingPromises));
|
|
735
|
+
}
|
|
736
|
+
};
|
|
737
|
+
if (options.timeoutMs !== void 0) {
|
|
738
|
+
await Promise.race([
|
|
739
|
+
shutdownWork(),
|
|
740
|
+
new Promise((resolve) => setTimeout(resolve, options.timeoutMs))
|
|
741
|
+
]);
|
|
742
|
+
} else {
|
|
743
|
+
await shutdownWork();
|
|
744
|
+
}
|
|
745
|
+
_LynxTracer.instances.delete(this);
|
|
746
|
+
}
|
|
747
|
+
isCircuitBreakerEnabled() {
|
|
748
|
+
return this.config.circuitBreaker?.enabled ?? true;
|
|
749
|
+
}
|
|
750
|
+
isCircuitOpen() {
|
|
751
|
+
if (!this.isCircuitBreakerEnabled() || this.circuitOpenedAt === 0) {
|
|
752
|
+
return false;
|
|
753
|
+
}
|
|
754
|
+
return Date.now() - this.circuitOpenedAt < (this.config.circuitBreaker?.cooldownMs ?? 3e4);
|
|
755
|
+
}
|
|
756
|
+
getCircuitState() {
|
|
757
|
+
if (!this.isCircuitBreakerEnabled()) {
|
|
758
|
+
return "DISABLED";
|
|
759
|
+
}
|
|
760
|
+
if (this.circuitOpenedAt === 0) {
|
|
761
|
+
return "CLOSED";
|
|
762
|
+
}
|
|
763
|
+
return this.isCircuitOpen() ? "OPEN" : "HALF_OPEN";
|
|
764
|
+
}
|
|
765
|
+
handleSuccessfulFlush() {
|
|
766
|
+
this.lastFailureTime = 0;
|
|
767
|
+
this.backoffDelayMs = 1e3;
|
|
768
|
+
this.consecutiveFlushFailures = 0;
|
|
769
|
+
this.circuitOpenedAt = 0;
|
|
770
|
+
this.lastDeliveryAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
771
|
+
this.lastError = void 0;
|
|
772
|
+
}
|
|
773
|
+
handleFailedEvents(events, reason) {
|
|
774
|
+
this.lastFailureTime = Date.now();
|
|
775
|
+
this.consecutiveFlushFailures += 1;
|
|
776
|
+
if (this.isCircuitBreakerEnabled() && this.consecutiveFlushFailures >= (this.config.circuitBreaker?.failureThreshold ?? 3)) {
|
|
777
|
+
this.circuitOpenedAt = Date.now();
|
|
778
|
+
console.warn(
|
|
779
|
+
`[LynxTracer] Telemetry circuit breaker opened after ${this.consecutiveFlushFailures} consecutive failures. Reason: ${reason}.`
|
|
780
|
+
);
|
|
781
|
+
}
|
|
782
|
+
this.lastError = reason;
|
|
783
|
+
for (const event of events) {
|
|
784
|
+
this.enqueueRetryEvent(event);
|
|
785
|
+
}
|
|
786
|
+
this.backoffDelayMs = Math.min(this.backoffDelayMs * 2, 6e4);
|
|
787
|
+
console.warn(
|
|
788
|
+
`[LynxTracer] Offline retry buffer saved ${events.length} events. Total size: ${this.retryQueue.length}. Backing off for ${this.backoffDelayMs}ms.`
|
|
789
|
+
);
|
|
790
|
+
}
|
|
791
|
+
/**
|
|
792
|
+
* Runs code inside a Lynx trace context.
|
|
793
|
+
*
|
|
794
|
+
* Every semantic event, instrumented LLM call, and instrumented tool call made
|
|
795
|
+
* inside `executionBlock` will be associated with the same run/session. Nested
|
|
796
|
+
* `run()` calls automatically inherit the parent run id and create a child
|
|
797
|
+
* span relationship.
|
|
798
|
+
*
|
|
799
|
+
* @typeParam T The value returned by the wrapped execution block.
|
|
800
|
+
* @param optionsOrAgentName Agent name or detailed run options.
|
|
801
|
+
* @param executionBlock Function to execute while the Lynx context is active.
|
|
802
|
+
* @returns The original return value of `executionBlock`.
|
|
803
|
+
*
|
|
804
|
+
* @example
|
|
805
|
+
* ```ts
|
|
806
|
+
* await lynx.run({ agentName: "SupportAgent", sessionId: "s-123" }, async () => {
|
|
807
|
+
* lynx.userInput("I need a refund");
|
|
808
|
+
* return await agent.handle();
|
|
809
|
+
* });
|
|
810
|
+
* ```
|
|
811
|
+
*/
|
|
812
|
+
async run(optionsOrAgentName, executionBlock) {
|
|
813
|
+
const options = typeof optionsOrAgentName === "string" ? { agentName: optionsOrAgentName } : optionsOrAgentName;
|
|
814
|
+
const agentName = options.agentName;
|
|
815
|
+
const parentContext = _LynxTracer.storage.getStore();
|
|
816
|
+
const runId = options.runId ?? (parentContext ? parentContext.runId : generateTraceId());
|
|
817
|
+
const parentSpanId = parentContext ? parentContext.spanId : void 0;
|
|
818
|
+
const spanId = generateSpanId();
|
|
819
|
+
const workspaceId = options.workspaceId ?? parentContext?.workspaceId ?? this.config.workspaceId ?? process.env.LYNX_WORKSPACE_ID;
|
|
820
|
+
const agentId = options.agentId ?? parentContext?.agentId ?? this.config.agentId ?? process.env.LYNX_AGENT_ID ?? agentName;
|
|
821
|
+
const sessionId = options.sessionId ?? parentContext?.sessionId ?? process.env.LYNX_SESSION_ID ?? `session_${runId}`;
|
|
822
|
+
const sampled = parentContext ? parentContext.sampled ?? true : this.config.sampleRate !== void 0 ? Math.random() < this.config.sampleRate : true;
|
|
823
|
+
const currentContext = {
|
|
824
|
+
runId,
|
|
825
|
+
agentName,
|
|
826
|
+
spanId,
|
|
827
|
+
parentSpanId,
|
|
828
|
+
sampled,
|
|
829
|
+
workspaceId,
|
|
830
|
+
agentId,
|
|
831
|
+
sessionId,
|
|
832
|
+
eventCounts: parentContext?.eventCounts ?? {},
|
|
833
|
+
attributes: { ...parentContext?.attributes }
|
|
834
|
+
};
|
|
835
|
+
this.captureInternal(
|
|
836
|
+
"CONTEXT_ALERT",
|
|
837
|
+
`run.start:${agentName}`,
|
|
838
|
+
{
|
|
839
|
+
message: `Agent execution thread started`,
|
|
840
|
+
phase: "start",
|
|
841
|
+
spanId,
|
|
842
|
+
parentSpanId
|
|
843
|
+
},
|
|
844
|
+
currentContext
|
|
845
|
+
);
|
|
846
|
+
const startTime = Date.now();
|
|
847
|
+
return _LynxTracer.storage.run(currentContext, async () => {
|
|
848
|
+
try {
|
|
849
|
+
const result = await executionBlock();
|
|
850
|
+
const latency = Date.now() - startTime;
|
|
851
|
+
this.captureInternal(
|
|
852
|
+
"CONTEXT_ALERT",
|
|
853
|
+
`run.end:${agentName}`,
|
|
854
|
+
{
|
|
855
|
+
message: `Agent execution thread succeeded`,
|
|
856
|
+
phase: "end",
|
|
857
|
+
spanId,
|
|
858
|
+
parentSpanId,
|
|
859
|
+
latency
|
|
860
|
+
},
|
|
861
|
+
currentContext
|
|
862
|
+
);
|
|
863
|
+
return result;
|
|
864
|
+
} catch (err) {
|
|
865
|
+
const latency = Date.now() - startTime;
|
|
866
|
+
this.captureInternal(
|
|
867
|
+
"ERROR",
|
|
868
|
+
`run.error:${agentName}`,
|
|
869
|
+
{
|
|
870
|
+
error: err.message,
|
|
871
|
+
phase: "error",
|
|
872
|
+
spanId,
|
|
873
|
+
parentSpanId,
|
|
874
|
+
latency
|
|
875
|
+
},
|
|
876
|
+
currentContext
|
|
877
|
+
);
|
|
878
|
+
throw err;
|
|
879
|
+
} finally {
|
|
880
|
+
if (this.getFlushOnRunEnd()) {
|
|
881
|
+
await this.flushInternal(!this.isBackgroundOnly());
|
|
882
|
+
}
|
|
883
|
+
if (!this.isBackgroundOnly() && this.pendingPromises.size > 0) {
|
|
884
|
+
await Promise.all(Array.from(this.pendingPromises));
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
});
|
|
888
|
+
}
|
|
889
|
+
/**
|
|
890
|
+
* Captures a custom context event for the active run.
|
|
891
|
+
*
|
|
892
|
+
* Prefer semantic helpers such as `userInput()`, `decision()`, `context()`,
|
|
893
|
+
* `memory()`, and `outcome()` when the event has a clear meaning. Use `log()`
|
|
894
|
+
* for compatibility or for ad-hoc diagnostic payloads.
|
|
895
|
+
*
|
|
896
|
+
* @param label Human-readable event label.
|
|
897
|
+
* @param payload JSON-serializable diagnostic payload.
|
|
898
|
+
*/
|
|
899
|
+
log(label, payload) {
|
|
900
|
+
const context = _LynxTracer.storage.getStore();
|
|
901
|
+
if (context) {
|
|
902
|
+
this.captureInternal("CONTEXT_ALERT", label, payload, context);
|
|
903
|
+
} else {
|
|
904
|
+
console.warn(
|
|
905
|
+
"[LynxTracer] Log was called outside a running context. Event ignored."
|
|
906
|
+
);
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
/**
|
|
910
|
+
* Captures the user input that started or influenced the current agent run.
|
|
911
|
+
*
|
|
912
|
+
* This event gives root-cause analysis a clear starting point and separates
|
|
913
|
+
* user intent from prompts, tool results, and internal agent state.
|
|
914
|
+
*
|
|
915
|
+
* @param input Raw or structured user input.
|
|
916
|
+
* @param metadata Optional metadata such as `userId`, `channel`, or `locale`.
|
|
917
|
+
*/
|
|
918
|
+
userInput(input, metadata = {}) {
|
|
919
|
+
this.capture("USER_INPUT", "user.input", { input, ...metadata });
|
|
920
|
+
}
|
|
921
|
+
/**
|
|
922
|
+
* Captures an agent decision and the reason behind it.
|
|
923
|
+
*
|
|
924
|
+
* Use this when the agent chooses a workflow, tool, policy branch, or final
|
|
925
|
+
* action. The `options` object can include candidates, confidence scores, or
|
|
926
|
+
* any domain-specific reasoning metadata.
|
|
927
|
+
*
|
|
928
|
+
* @param reason Short explanation of the selected action.
|
|
929
|
+
* @param options Optional structured metadata about the decision.
|
|
930
|
+
*/
|
|
931
|
+
decision(reasonOrDecision, options = {}) {
|
|
932
|
+
if (typeof reasonOrDecision === "string") {
|
|
933
|
+
this.capture("AGENT_DECISION", "agent.decision", {
|
|
934
|
+
reason: reasonOrDecision,
|
|
935
|
+
...options
|
|
936
|
+
});
|
|
937
|
+
return;
|
|
938
|
+
}
|
|
939
|
+
this.capture("AGENT_DECISION", `agent.decision:${reasonOrDecision.name}`, {
|
|
940
|
+
...reasonOrDecision,
|
|
941
|
+
...reasonOrDecision.metadata
|
|
942
|
+
});
|
|
943
|
+
}
|
|
944
|
+
/**
|
|
945
|
+
* Captures retrieved or constructed context used by the agent.
|
|
946
|
+
*
|
|
947
|
+
* Use this for RAG results, conversation summaries, selected documents,
|
|
948
|
+
* request-scoped state, or any context that may have influenced the next LLM
|
|
949
|
+
* or tool call.
|
|
950
|
+
*
|
|
951
|
+
* @param data Context data or a summary of the context.
|
|
952
|
+
* @param metadata Optional metadata such as source, query, score, or label.
|
|
953
|
+
*/
|
|
954
|
+
context(labelOrData, dataOrMetadata = {}, metadata = {}) {
|
|
955
|
+
if (typeof labelOrData === "string") {
|
|
956
|
+
this.capture("CONTEXT_RETRIEVAL", labelOrData, {
|
|
957
|
+
data: dataOrMetadata,
|
|
958
|
+
...metadata
|
|
959
|
+
});
|
|
960
|
+
return;
|
|
961
|
+
}
|
|
962
|
+
this.capture(
|
|
963
|
+
"CONTEXT_RETRIEVAL",
|
|
964
|
+
dataOrMetadata.label ?? "context.retrieval",
|
|
965
|
+
{
|
|
966
|
+
data: labelOrData,
|
|
967
|
+
...dataOrMetadata
|
|
968
|
+
}
|
|
969
|
+
);
|
|
970
|
+
}
|
|
971
|
+
/**
|
|
972
|
+
* Adds attributes to the current run context.
|
|
973
|
+
*
|
|
974
|
+
* Attributes are attached to subsequent events captured in the same async
|
|
975
|
+
* context. Use this for stable request or business identifiers such as
|
|
976
|
+
* `orderId`, `tenantId`, or `workflowId`.
|
|
977
|
+
*
|
|
978
|
+
* @param attributes Key-value attributes to merge into the active context.
|
|
979
|
+
*/
|
|
980
|
+
setAttributes(attributes) {
|
|
981
|
+
const context = _LynxTracer.storage.getStore();
|
|
982
|
+
if (!context) {
|
|
983
|
+
console.warn("[LynxTracer] setAttributes called outside a run context.");
|
|
984
|
+
return;
|
|
985
|
+
}
|
|
986
|
+
context.attributes = {
|
|
987
|
+
...context.attributes,
|
|
988
|
+
...attributes
|
|
989
|
+
};
|
|
990
|
+
}
|
|
991
|
+
/**
|
|
992
|
+
* Captures access to short-term or long-term agent memory.
|
|
993
|
+
*
|
|
994
|
+
* This helps identify stale memory, missing memory, or memory values that
|
|
995
|
+
* influenced an incorrect action.
|
|
996
|
+
*
|
|
997
|
+
* @param operation Memory operation name, such as `read`, `write`, or `search`.
|
|
998
|
+
* @param data Memory key/value, query result, or a safe summary.
|
|
999
|
+
* @param metadata Optional metadata such as hit/miss, store name, or freshness.
|
|
1000
|
+
*/
|
|
1001
|
+
memory(operation, data, metadata = {}) {
|
|
1002
|
+
this.capture("MEMORY_ACCESS", `memory.${operation}`, {
|
|
1003
|
+
operation,
|
|
1004
|
+
data,
|
|
1005
|
+
...metadata
|
|
1006
|
+
});
|
|
1007
|
+
}
|
|
1008
|
+
/**
|
|
1009
|
+
* Captures the final technical and business outcome of the current session.
|
|
1010
|
+
*
|
|
1011
|
+
* Use this to distinguish "the code ran successfully" from "the business task
|
|
1012
|
+
* succeeded." That distinction is central for detecting AI failures that do
|
|
1013
|
+
* not throw runtime exceptions.
|
|
1014
|
+
*
|
|
1015
|
+
* @param options Outcome status, business status, reason, impact, and metadata.
|
|
1016
|
+
*/
|
|
1017
|
+
outcome(options) {
|
|
1018
|
+
this.capture("SESSION_OUTCOME", "session.outcome", options);
|
|
1019
|
+
}
|
|
1020
|
+
/**
|
|
1021
|
+
* Captures a human-readable annotation for the active run.
|
|
1022
|
+
*
|
|
1023
|
+
* `annotate()` is intended for breadcrumbs that help operators understand the
|
|
1024
|
+
* trace but do not fit one of the stricter semantic event helpers.
|
|
1025
|
+
*
|
|
1026
|
+
* @param label Annotation label.
|
|
1027
|
+
* @param payload JSON-serializable annotation payload.
|
|
1028
|
+
*/
|
|
1029
|
+
annotate(label, payload) {
|
|
1030
|
+
this.capture("CONTEXT_ALERT", label, { annotation: true, ...payload });
|
|
1031
|
+
}
|
|
1032
|
+
/**
|
|
1033
|
+
* Captures a semantic event for the active Lynx run context.
|
|
1034
|
+
*/
|
|
1035
|
+
capture(eventType, label, payload) {
|
|
1036
|
+
const context = _LynxTracer.storage.getStore();
|
|
1037
|
+
if (!context) {
|
|
1038
|
+
console.warn(
|
|
1039
|
+
`[LynxTracer] Warning: event ${eventType} was captured outside a LynxTracer context!`
|
|
1040
|
+
);
|
|
1041
|
+
return;
|
|
1042
|
+
}
|
|
1043
|
+
this.captureInternal(eventType, label, payload, context);
|
|
1044
|
+
}
|
|
1045
|
+
/**
|
|
1046
|
+
* Captures an event using an explicit Lynx context.
|
|
1047
|
+
*
|
|
1048
|
+
* This method is public for low-level instrumentation modules, but application
|
|
1049
|
+
* code should usually call the semantic helpers or `instrumentLLM()` /
|
|
1050
|
+
* `instrumentTool()` instead. Payloads are processed for PII masking,
|
|
1051
|
+
* capture-mode filtering, runtime metadata, and loop detection before they are
|
|
1052
|
+
* queued for delivery.
|
|
1053
|
+
*
|
|
1054
|
+
* @param eventType Lynx event type to record.
|
|
1055
|
+
* @param label Human-readable event label.
|
|
1056
|
+
* @param payload JSON-serializable event payload.
|
|
1057
|
+
* @param context Explicit Lynx trace context.
|
|
1058
|
+
*/
|
|
1059
|
+
captureInternal(eventType, label, payload, context) {
|
|
1060
|
+
const isError = eventType === "ERROR" || payload && (payload.error || "error" in payload);
|
|
1061
|
+
if (context.sampled === false && !isError) {
|
|
1062
|
+
return;
|
|
1063
|
+
}
|
|
1064
|
+
const loopPayload = this.detectLoop(eventType, label, payload, context);
|
|
1065
|
+
const processedPayload = processPayload(
|
|
1066
|
+
{
|
|
1067
|
+
...context.attributes,
|
|
1068
|
+
...payload,
|
|
1069
|
+
...loopPayload,
|
|
1070
|
+
sdkVersion: SDK_VERSION,
|
|
1071
|
+
droppedEventCount: this.droppedEventCount || void 0,
|
|
1072
|
+
appVersion: payload?.appVersion ?? this.config.appVersion,
|
|
1073
|
+
deploymentId: payload?.deploymentId ?? this.config.deploymentId,
|
|
1074
|
+
environment: payload?.environment ?? this.config.environment,
|
|
1075
|
+
policyVersion: payload?.policyVersion ?? this.config.policyVersion
|
|
1076
|
+
},
|
|
1077
|
+
this.config
|
|
1078
|
+
);
|
|
1079
|
+
const eventPayload = {
|
|
1080
|
+
...processedPayload,
|
|
1081
|
+
spanId: processedPayload.spanId || context.spanId,
|
|
1082
|
+
parentSpanId: processedPayload.parentSpanId || context.parentSpanId
|
|
1083
|
+
};
|
|
1084
|
+
const eventDto = {
|
|
1085
|
+
eventId: generateEventId(),
|
|
1086
|
+
clientId: this.config.clientId,
|
|
1087
|
+
runId: context.runId,
|
|
1088
|
+
agentName: context.agentName,
|
|
1089
|
+
eventType,
|
|
1090
|
+
label,
|
|
1091
|
+
payload: eventPayload,
|
|
1092
|
+
timestamp: Date.now(),
|
|
1093
|
+
workspaceId: context.workspaceId,
|
|
1094
|
+
agentId: context.agentId,
|
|
1095
|
+
sessionId: context.sessionId,
|
|
1096
|
+
schemaVersion: "1"
|
|
1097
|
+
};
|
|
1098
|
+
const queued = this.enqueueEvent(eventDto);
|
|
1099
|
+
if (!queued) {
|
|
1100
|
+
return;
|
|
1101
|
+
}
|
|
1102
|
+
if (loopPayload.loopDetected && eventType !== "LOOP_DETECTED") {
|
|
1103
|
+
this.captureInternal(
|
|
1104
|
+
"LOOP_DETECTED",
|
|
1105
|
+
`loop:${label}`,
|
|
1106
|
+
{
|
|
1107
|
+
repeatedLabel: label,
|
|
1108
|
+
loopCount: loopPayload.loopCount,
|
|
1109
|
+
argsHash: loopPayload.argsHash
|
|
1110
|
+
},
|
|
1111
|
+
context
|
|
1112
|
+
);
|
|
1113
|
+
}
|
|
1114
|
+
const maxBatchSize = this.config.delivery?.batchSize ?? DEFAULT_BATCH_SIZE;
|
|
1115
|
+
if (!this.isBackgroundOnly() && this.eventQueue.length >= maxBatchSize) {
|
|
1116
|
+
void this.flushInternal(false);
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
/**
|
|
1120
|
+
* Wraps an LLM client with automatic Lynx telemetry.
|
|
1121
|
+
*
|
|
1122
|
+
* The returned proxy preserves the original client shape while intercepting
|
|
1123
|
+
* supported generation methods such as OpenAI `chat.completions.create`,
|
|
1124
|
+
* Anthropic `messages.create`, Google `generateContent`, Vercel AI SDK
|
|
1125
|
+
* `generateText`, LangChain `invoke`, and similar methods. Captured telemetry
|
|
1126
|
+
* includes input/output, provider, model, generation config, latency, token
|
|
1127
|
+
* usage, span ids, and errors.
|
|
1128
|
+
*
|
|
1129
|
+
* @typeParam T LLM client object type.
|
|
1130
|
+
* @param instance LLM client instance to wrap.
|
|
1131
|
+
* @param optionsOrLabel Event label or custom extraction/interception rules.
|
|
1132
|
+
* @returns A proxy with the same public interface as `instance`.
|
|
1133
|
+
*/
|
|
1134
|
+
instrumentLLM(instance, optionsOrLabel = "llm.inference") {
|
|
1135
|
+
return instrumentLLM(this, instance, optionsOrLabel);
|
|
1136
|
+
}
|
|
1137
|
+
/**
|
|
1138
|
+
* Wraps a tool function with automatic Lynx telemetry.
|
|
1139
|
+
*
|
|
1140
|
+
* The wrapped function emits `TOOL_CALL` before execution and `TOOL_RESULT`
|
|
1141
|
+
* after success. Errors are captured as `ERROR` and then rethrown. Use
|
|
1142
|
+
* `metadata` to describe side effects, risk level, external targets, or tool
|
|
1143
|
+
* version so debugging and governance views can reason about the call.
|
|
1144
|
+
*
|
|
1145
|
+
* @typeParam T Tool function type.
|
|
1146
|
+
* @param toolName Stable tool name shown in traces and analytics.
|
|
1147
|
+
* @param fn Tool function to execute.
|
|
1148
|
+
* @param metadata Optional tool metadata for governance and RCA.
|
|
1149
|
+
* @returns A wrapped function with the same call signature as `fn`.
|
|
1150
|
+
*/
|
|
1151
|
+
instrumentTool(toolName, fn, metadata = {}) {
|
|
1152
|
+
return instrumentTool(this, toolName, fn, metadata);
|
|
1153
|
+
}
|
|
1154
|
+
getDefaultFailureMode(riskLevel) {
|
|
1155
|
+
if (riskLevel === "HIGH" || riskLevel === "CRITICAL") {
|
|
1156
|
+
return "FAIL_CLOSED";
|
|
1157
|
+
}
|
|
1158
|
+
if (riskLevel === "MEDIUM") {
|
|
1159
|
+
return "REQUIRE_APPROVAL";
|
|
1160
|
+
}
|
|
1161
|
+
return "FAIL_OPEN";
|
|
1162
|
+
}
|
|
1163
|
+
normalizePolicyDecision(decision) {
|
|
1164
|
+
const action = decision.action ?? (decision.allow === false ? "BLOCK" : "ALLOW");
|
|
1165
|
+
const allow = action === "ALLOW" || action === "WARN";
|
|
1166
|
+
return {
|
|
1167
|
+
...decision,
|
|
1168
|
+
action,
|
|
1169
|
+
allow
|
|
1170
|
+
};
|
|
1171
|
+
}
|
|
1172
|
+
decisionFromPolicyError(err, options) {
|
|
1173
|
+
const failureMode = options.failureMode ?? this.getDefaultFailureMode(options.riskLevel);
|
|
1174
|
+
const reason = err instanceof Error ? err.message : "Policy evaluation failed";
|
|
1175
|
+
const action = failureMode === "FAIL_OPEN" ? "ALLOW" : failureMode === "REQUIRE_APPROVAL" ? "REQUIRE_APPROVAL" : "BLOCK";
|
|
1176
|
+
return {
|
|
1177
|
+
action,
|
|
1178
|
+
allow: action === "ALLOW",
|
|
1179
|
+
reason,
|
|
1180
|
+
severity: options.riskLevel,
|
|
1181
|
+
metadata: {
|
|
1182
|
+
policyError: true,
|
|
1183
|
+
failureMode
|
|
1184
|
+
}
|
|
1185
|
+
};
|
|
1186
|
+
}
|
|
1187
|
+
/**
|
|
1188
|
+
* Wraps a tool with a local policy check before execution.
|
|
1189
|
+
*
|
|
1190
|
+
* `guardTool()` emits a `POLICY_EVALUATION` event before the tool runs. When
|
|
1191
|
+
* `beforeCall` returns `{ allow: false }`, Lynx also emits
|
|
1192
|
+
* `POLICY_VIOLATION` and `GUARDRAIL_ACTIVATED`, then throws before executing
|
|
1193
|
+
* the original tool. This provides the SDK-side hook needed for "observe first,
|
|
1194
|
+
* then prevent recurrence" workflows.
|
|
1195
|
+
*
|
|
1196
|
+
* @typeParam T Tool function type.
|
|
1197
|
+
* @param toolName Stable tool name shown in traces and policy events.
|
|
1198
|
+
* @param fn Tool function to guard.
|
|
1199
|
+
* @param options Tool metadata and an optional `beforeCall` policy callback.
|
|
1200
|
+
* @returns A guarded function with the same call signature as `fn`.
|
|
1201
|
+
*/
|
|
1202
|
+
guardTool(toolName, fn, options = {}) {
|
|
1203
|
+
const guarded = (async (...args) => {
|
|
1204
|
+
const input = args[0];
|
|
1205
|
+
let decision;
|
|
1206
|
+
try {
|
|
1207
|
+
decision = this.normalizePolicyDecision(
|
|
1208
|
+
options.beforeCall ? await options.beforeCall({
|
|
1209
|
+
toolName,
|
|
1210
|
+
input,
|
|
1211
|
+
args,
|
|
1212
|
+
metadata: options
|
|
1213
|
+
}) : { action: "ALLOW" }
|
|
1214
|
+
);
|
|
1215
|
+
} catch (err) {
|
|
1216
|
+
decision = this.decisionFromPolicyError(err, options);
|
|
1217
|
+
}
|
|
1218
|
+
this.capture("POLICY_EVALUATION", `policy:${toolName}`, {
|
|
1219
|
+
toolName,
|
|
1220
|
+
input,
|
|
1221
|
+
args,
|
|
1222
|
+
argsHash: stableHash2(input),
|
|
1223
|
+
action: decision.action,
|
|
1224
|
+
allow: decision.allow,
|
|
1225
|
+
policyId: decision.policyId,
|
|
1226
|
+
policyVersion: decision.policyVersion ?? options.policyVersion ?? this.config.policyVersion,
|
|
1227
|
+
reason: decision.reason,
|
|
1228
|
+
severity: decision.severity,
|
|
1229
|
+
metadata: decision.metadata
|
|
1230
|
+
});
|
|
1231
|
+
if (!decision.allow) {
|
|
1232
|
+
this.capture("POLICY_VIOLATION", `policy.violation:${toolName}`, {
|
|
1233
|
+
toolName,
|
|
1234
|
+
input,
|
|
1235
|
+
args,
|
|
1236
|
+
argsHash: stableHash2(input),
|
|
1237
|
+
action: decision.action,
|
|
1238
|
+
policyId: decision.policyId,
|
|
1239
|
+
policyVersion: decision.policyVersion ?? options.policyVersion ?? this.config.policyVersion,
|
|
1240
|
+
reason: decision.reason,
|
|
1241
|
+
severity: decision.severity ?? options.riskLevel
|
|
1242
|
+
});
|
|
1243
|
+
this.capture("GUARDRAIL_ACTIVATED", `guardrail.blocked:${toolName}`, {
|
|
1244
|
+
toolName,
|
|
1245
|
+
action: decision.action,
|
|
1246
|
+
reason: decision.reason,
|
|
1247
|
+
riskLevel: decision.severity ?? options.riskLevel
|
|
1248
|
+
});
|
|
1249
|
+
throw new LynxPolicyError(
|
|
1250
|
+
decision.reason || `Lynx guard blocked tool call: ${toolName}`,
|
|
1251
|
+
decision.action === "REQUIRE_APPROVAL" ? "REQUIRE_APPROVAL" : "BLOCK",
|
|
1252
|
+
decision.policyId,
|
|
1253
|
+
decision.severity ?? options.riskLevel,
|
|
1254
|
+
decision.reason,
|
|
1255
|
+
decision.metadata
|
|
1256
|
+
);
|
|
1257
|
+
}
|
|
1258
|
+
const instrumented = this.instrumentTool(toolName, fn, options);
|
|
1259
|
+
return instrumented(...args);
|
|
1260
|
+
});
|
|
1261
|
+
return guarded;
|
|
1262
|
+
}
|
|
1263
|
+
detectLoop(eventType, label, payload, context) {
|
|
1264
|
+
if (eventType !== "TOOL_CALL" && eventType !== "CALL_TOOLS" && eventType !== "LLM_CALL") {
|
|
1265
|
+
return {};
|
|
1266
|
+
}
|
|
1267
|
+
const phase = payload?.phase;
|
|
1268
|
+
if (phase && phase !== "start") {
|
|
1269
|
+
return {};
|
|
1270
|
+
}
|
|
1271
|
+
const argsHash = stableHash2(
|
|
1272
|
+
payload?.args ?? payload?.input ?? payload?.model ?? label
|
|
1273
|
+
);
|
|
1274
|
+
const key = `${eventType}:${label}:${argsHash}`;
|
|
1275
|
+
context.eventCounts ??= {};
|
|
1276
|
+
const loopCount = (context.eventCounts[key] ?? 0) + 1;
|
|
1277
|
+
context.eventCounts[key] = loopCount;
|
|
1278
|
+
if (loopCount < 5) {
|
|
1279
|
+
return { argsHash };
|
|
1280
|
+
}
|
|
1281
|
+
return {
|
|
1282
|
+
argsHash,
|
|
1283
|
+
loopDetected: true,
|
|
1284
|
+
loopCount,
|
|
1285
|
+
repeatedLabel: label
|
|
1286
|
+
};
|
|
1287
|
+
}
|
|
1288
|
+
};
|
|
1289
|
+
|
|
1290
|
+
// src/core/register.ts
|
|
1291
|
+
var parseBool = (val) => {
|
|
1292
|
+
if (val === void 0) return void 0;
|
|
1293
|
+
return val.toLowerCase() === "true";
|
|
1294
|
+
};
|
|
1295
|
+
var parseNum = (val) => {
|
|
1296
|
+
if (val === void 0) return void 0;
|
|
1297
|
+
const num = parseFloat(val);
|
|
1298
|
+
return isNaN(num) ? void 0 : num;
|
|
1299
|
+
};
|
|
1300
|
+
var lynx = new LynxTracer({
|
|
1301
|
+
clientId: process.env.LYNX_CLIENT_ID || "local_dev_env",
|
|
1302
|
+
endpoint: process.env.LYNX_ENDPOINT || DEFAULT_ENDPOINT,
|
|
1303
|
+
sampleRate: parseNum(process.env.LYNX_SAMPLE_RATE),
|
|
1304
|
+
captureInput: parseBool(process.env.LYNX_CAPTURE_INPUT),
|
|
1305
|
+
captureOutput: parseBool(process.env.LYNX_CAPTURE_OUTPUT),
|
|
1306
|
+
maxPayloadLength: parseNum(process.env.LYNX_MAX_PAYLOAD_LENGTH),
|
|
1307
|
+
captureMode: process.env.LYNX_CAPTURE_MODE || "smart",
|
|
1308
|
+
workspaceId: process.env.LYNX_WORKSPACE_ID,
|
|
1309
|
+
agentId: process.env.LYNX_AGENT_ID,
|
|
1310
|
+
apiKey: process.env.LYNX_API_KEY,
|
|
1311
|
+
appVersion: process.env.LYNX_APP_VERSION,
|
|
1312
|
+
deploymentId: process.env.LYNX_DEPLOYMENT_ID,
|
|
1313
|
+
environment: process.env.LYNX_ENVIRONMENT || process.env.NODE_ENV,
|
|
1314
|
+
policyVersion: process.env.LYNX_POLICY_VERSION,
|
|
1315
|
+
delivery: {
|
|
1316
|
+
mode: process.env.LYNX_DELIVERY_MODE,
|
|
1317
|
+
timeoutMs: parseNum(process.env.LYNX_DELIVERY_TIMEOUT_MS),
|
|
1318
|
+
flushOnRunEnd: parseBool(process.env.LYNX_DELIVERY_FLUSH_ON_RUN_END),
|
|
1319
|
+
flushIntervalMs: parseNum(process.env.LYNX_DELIVERY_FLUSH_INTERVAL_MS),
|
|
1320
|
+
batchSize: parseNum(process.env.LYNX_DELIVERY_BATCH_SIZE),
|
|
1321
|
+
maxQueueSize: parseNum(process.env.LYNX_DELIVERY_MAX_QUEUE_SIZE),
|
|
1322
|
+
overflowStrategy: process.env.LYNX_DELIVERY_OVERFLOW_STRATEGY
|
|
1323
|
+
},
|
|
1324
|
+
circuitBreaker: {
|
|
1325
|
+
enabled: parseBool(process.env.LYNX_CIRCUIT_BREAKER_ENABLED),
|
|
1326
|
+
failureThreshold: parseNum(
|
|
1327
|
+
process.env.LYNX_CIRCUIT_BREAKER_FAILURE_THRESHOLD
|
|
1328
|
+
),
|
|
1329
|
+
cooldownMs: parseNum(process.env.LYNX_CIRCUIT_BREAKER_COOLDOWN_MS)
|
|
1330
|
+
}
|
|
1331
|
+
});
|
|
1332
|
+
export {
|
|
1333
|
+
DEFAULT_ENDPOINT,
|
|
1334
|
+
LynxPolicyError,
|
|
1335
|
+
LynxTracer,
|
|
1336
|
+
instrumentLLM,
|
|
1337
|
+
instrumentTool,
|
|
1338
|
+
lynx
|
|
1339
|
+
};
|