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