@agenttrace-io/sdk 0.1.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/dist/benchmark.d.ts +79 -0
  2. package/dist/benchmark.d.ts.map +1 -0
  3. package/dist/benchmark.js +324 -0
  4. package/dist/benchmark.js.map +1 -0
  5. package/dist/index.d.ts +358 -0
  6. package/dist/index.d.ts.map +1 -0
  7. package/dist/index.js +1169 -0
  8. package/dist/index.js.map +1 -0
  9. package/dist/migrations/001-initial.d.ts +5 -0
  10. package/dist/migrations/001-initial.d.ts.map +1 -0
  11. package/dist/migrations/001-initial.js +86 -0
  12. package/dist/migrations/001-initial.js.map +1 -0
  13. package/dist/migrations/002-scores.d.ts +5 -0
  14. package/dist/migrations/002-scores.d.ts.map +1 -0
  15. package/dist/migrations/002-scores.js +17 -0
  16. package/dist/migrations/002-scores.js.map +1 -0
  17. package/dist/migrations/003-alerts.d.ts +5 -0
  18. package/dist/migrations/003-alerts.d.ts.map +1 -0
  19. package/dist/migrations/003-alerts.js +27 -0
  20. package/dist/migrations/003-alerts.js.map +1 -0
  21. package/dist/migrations/004-trace-context.d.ts +5 -0
  22. package/dist/migrations/004-trace-context.d.ts.map +1 -0
  23. package/dist/migrations/004-trace-context.js +16 -0
  24. package/dist/migrations/004-trace-context.js.map +1 -0
  25. package/dist/migrations/005-agent-usage.d.ts +5 -0
  26. package/dist/migrations/005-agent-usage.d.ts.map +1 -0
  27. package/dist/migrations/005-agent-usage.js +27 -0
  28. package/dist/migrations/005-agent-usage.js.map +1 -0
  29. package/dist/migrations/005-webhooks.d.ts +5 -0
  30. package/dist/migrations/005-webhooks.d.ts.map +1 -0
  31. package/dist/migrations/005-webhooks.js +33 -0
  32. package/dist/migrations/005-webhooks.js.map +1 -0
  33. package/dist/migrations/006-api-keys.d.ts +5 -0
  34. package/dist/migrations/006-api-keys.d.ts.map +1 -0
  35. package/dist/migrations/006-api-keys.js +27 -0
  36. package/dist/migrations/006-api-keys.js.map +1 -0
  37. package/dist/migrations.d.ts +29 -0
  38. package/dist/migrations.d.ts.map +1 -0
  39. package/dist/migrations.js +107 -0
  40. package/dist/migrations.js.map +1 -0
  41. package/dist/rate-limiter.d.ts +34 -0
  42. package/dist/rate-limiter.d.ts.map +1 -0
  43. package/dist/rate-limiter.js +74 -0
  44. package/dist/rate-limiter.js.map +1 -0
  45. package/dist/self-track.d.ts +42 -0
  46. package/dist/self-track.d.ts.map +1 -0
  47. package/dist/self-track.js +288 -0
  48. package/dist/self-track.js.map +1 -0
  49. package/dist/storage.d.ts +149 -0
  50. package/dist/storage.d.ts.map +1 -0
  51. package/dist/storage.js +1479 -0
  52. package/dist/storage.js.map +1 -0
  53. package/dist/types.d.ts +323 -0
  54. package/dist/types.d.ts.map +1 -0
  55. package/dist/types.js +19 -0
  56. package/dist/types.js.map +1 -0
  57. package/package.json +53 -0
package/dist/index.js ADDED
@@ -0,0 +1,1169 @@
1
+ /**
2
+ * AgentTrace -- Core SDK
3
+ * Drop-in tracing for any AI agent
4
+ */
5
+ import { randomUUID, createHash, createHmac } from 'node:crypto';
6
+ import { EventEmitter } from 'node:events';
7
+ import { TraceStorage } from './storage.js';
8
+ import { TokenBucketRateLimiter } from './rate-limiter.js';
9
+ export const VERSION = '0.1.0';
10
+ export const PACKAGE_NAME = '@agenttrace-io/sdk';
11
+ // Value import — TraceContext is a class (value), not just a type.
12
+ // Must be a separate import from the type-only block above so isolatedModules
13
+ // doesn't erase it at compile time.
14
+ import { TraceContext } from './types.js';
15
+ export { TraceContext } from './types.js';
16
+ export { TraceStorage } from './storage.js';
17
+ export { SelfTracker } from './self-track.js';
18
+ export { TokenBucketRateLimiter } from './rate-limiter.js';
19
+ // Default cost calculator (approximate 2026 pricing)
20
+ // Rates are in USD per 1000 tokens. Extended with additional models for v0.2.0.
21
+ const modelRates = {
22
+ 'gpt-4o': { prompt: 0.0025, completion: 0.01 },
23
+ 'gpt-4o-mini': { prompt: 0.00015, completion: 0.0006 },
24
+ 'claude-sonnet-4': { prompt: 0.003, completion: 0.015 },
25
+ 'claude-haiku-4': { prompt: 0.00025, completion: 0.00125 },
26
+ 'gemini-2.0-flash': { prompt: 0.0001, completion: 0.0004 },
27
+ 'llama-3.1-70b': { prompt: 0.0009, completion: 0.0009 },
28
+ // Added models (approximate current pricing researched 2026)
29
+ 'claude-opus-4': { prompt: 0.005, completion: 0.025 },
30
+ 'claude-sonnet-4.5': { prompt: 0.003, completion: 0.015 },
31
+ 'claude-haiku-4.5': { prompt: 0.001, completion: 0.005 },
32
+ 'gpt-4.1': { prompt: 0.002, completion: 0.008 },
33
+ 'gpt-4.1-mini': { prompt: 0.0004, completion: 0.0016 },
34
+ 'gpt-4.1-nano': { prompt: 0.0001, completion: 0.0004 },
35
+ 'gemini-2.5-pro': { prompt: 0.00125, completion: 0.01 },
36
+ 'gemini-2.5-flash': { prompt: 0.0003, completion: 0.0025 },
37
+ 'llama-4-scout': { prompt: 0.00008, completion: 0.0003 },
38
+ 'llama-4-maverick': { prompt: 0.00015, completion: 0.0006 },
39
+ };
40
+ function defaultCostCalculator(tokens, model) {
41
+ const rate = modelRates[model || ''] || { prompt: 0.001, completion: 0.002 };
42
+ return (tokens.promptTokens * rate.prompt + tokens.completionTokens * rate.completion) / 1000;
43
+ }
44
+ /**
45
+ * Register or override pricing rates for a model in the default cost calculator (used at runtime).
46
+ * Rates are USD per 1,000 tokens (matching the calculator convention).
47
+ * Example: registerModelRate('my-model', 0.001, 0.002) => $1/M prompt, $2/M completion
48
+ */
49
+ export function registerModelRate(model, promptRatePerK, completionRatePerK) {
50
+ modelRates[model] = { prompt: promptRatePerK, completion: completionRatePerK };
51
+ }
52
+ // ---- OpenTelemetry (OTLP JSON) export helpers (no external deps) ----
53
+ function toOtelId(id, length) {
54
+ const cleaned = id.replace(/-/g, '');
55
+ if (/^[0-9a-fA-F]{32}$/.test(cleaned)) {
56
+ return cleaned.toLowerCase().slice(0, length);
57
+ }
58
+ // Fallback for non-UUID ids (e.g. test data like 'trace-1'); deterministic
59
+ return createHash('sha256').update(id).digest('hex').slice(0, length);
60
+ }
61
+ function toUnixNano(timestampMs) {
62
+ return String(BigInt(Math.floor(timestampMs)) * 1000000n);
63
+ }
64
+ function toOtelAttrValue(v) {
65
+ if (v === null || v === undefined)
66
+ return undefined;
67
+ const t = typeof v;
68
+ if (t === 'string')
69
+ return { stringValue: v };
70
+ if (t === 'number') {
71
+ return Number.isInteger(v) ? { intValue: v } : { doubleValue: v };
72
+ }
73
+ if (t === 'boolean')
74
+ return { boolValue: v };
75
+ return { stringValue: JSON.stringify(v) };
76
+ }
77
+ function stringifyForAttr(v, maxLen = 2048) {
78
+ const s = typeof v === 'string' ? v : JSON.stringify(v);
79
+ if (s.length <= maxLen)
80
+ return s;
81
+ return s.slice(0, maxLen - 3) + '...';
82
+ }
83
+ function buildOtelAttributes(trace) {
84
+ const attrs = [];
85
+ attrs.push({ key: 'agenttrace.status', value: { stringValue: trace.status } });
86
+ attrs.push({ key: 'agenttrace.latency_ms', value: { intValue: trace.latencyMs } });
87
+ attrs.push({ key: 'agenttrace.cost_usd', value: { doubleValue: trace.costUsd } });
88
+ attrs.push({ key: 'agenttrace.run_id', value: { stringValue: trace.runId } });
89
+ const tok = trace.tokens || { promptTokens: 0, completionTokens: 0, totalTokens: 0 };
90
+ attrs.push({ key: 'agenttrace.tokens.prompt', value: { intValue: tok.promptTokens } });
91
+ attrs.push({ key: 'agenttrace.tokens.completion', value: { intValue: tok.completionTokens } });
92
+ attrs.push({ key: 'agenttrace.tokens.total', value: { intValue: tok.totalTokens } });
93
+ if (tok.model) {
94
+ attrs.push({ key: 'agenttrace.model', value: { stringValue: tok.model } });
95
+ }
96
+ if (tok.provider) {
97
+ attrs.push({ key: 'agenttrace.provider', value: { stringValue: tok.provider } });
98
+ }
99
+ if (trace.error) {
100
+ attrs.push({ key: 'agenttrace.error', value: { stringValue: trace.error } });
101
+ }
102
+ // metadata
103
+ const meta = trace.metadata || {};
104
+ for (const [k, v] of Object.entries(meta)) {
105
+ const otelVal = toOtelAttrValue(v);
106
+ if (otelVal) {
107
+ attrs.push({ key: `agenttrace.metadata.${k}`, value: otelVal });
108
+ }
109
+ }
110
+ // input/output (stringified, truncated)
111
+ if (trace.input != null) {
112
+ attrs.push({ key: 'agenttrace.input', value: { stringValue: stringifyForAttr(trace.input) } });
113
+ }
114
+ if (trace.output != null) {
115
+ attrs.push({
116
+ key: 'agenttrace.output',
117
+ value: { stringValue: stringifyForAttr(trace.output) },
118
+ });
119
+ }
120
+ return attrs;
121
+ }
122
+ function buildResourceAttributes() {
123
+ return [
124
+ { key: 'service.name', value: { stringValue: 'agenttrace' } },
125
+ { key: 'telemetry.sdk.name', value: { stringValue: 'agenttrace' } },
126
+ { key: 'telemetry.sdk.version', value: { stringValue: VERSION } },
127
+ ];
128
+ }
129
+ function traceToOtelSpan(trace) {
130
+ const traceId = toOtelId(trace.id, 32);
131
+ const spanId = toOtelId(trace.id, 16);
132
+ const endMs = trace.createdAt || Date.now();
133
+ const startMs = Math.max(0, endMs - (trace.latencyMs || 0));
134
+ const startTimeUnixNano = toUnixNano(startMs);
135
+ const endTimeUnixNano = toUnixNano(endMs);
136
+ const attributes = buildOtelAttributes(trace);
137
+ const isSuccess = trace.status === 'success';
138
+ const status = isSuccess
139
+ ? { code: 1 } // STATUS_CODE_OK
140
+ : { code: 2, message: trace.error || trace.status }; // STATUS_CODE_ERROR
141
+ return {
142
+ traceId,
143
+ spanId,
144
+ name: trace.name,
145
+ kind: 1, // SPAN_KIND_INTERNAL
146
+ startTimeUnixNano,
147
+ endTimeUnixNano,
148
+ attributes,
149
+ status,
150
+ };
151
+ }
152
+ export class AgentTrace {
153
+ storage;
154
+ config;
155
+ currentRunId = null;
156
+ registeredAlerts = [];
157
+ usageEmitter = new EventEmitter();
158
+ webhookEmitter = new EventEmitter();
159
+ _cleanupInterval;
160
+ rateLimiter = null;
161
+ activeTraceContext = null;
162
+ constructor(config = {}) {
163
+ const dbPath = config.dbPath || './agenttrace.db';
164
+ const tenantId = config.tenantId ?? '';
165
+ this.storage = new TraceStorage(dbPath, tenantId);
166
+ const persisted = this.storage.getRetentionPolicy();
167
+ this.config = {
168
+ dbPath,
169
+ maxTraces: config.maxTraces ?? 10000,
170
+ autoCleanup: config.autoCleanup !== false,
171
+ costCalculator: config.costCalculator || defaultCostCalculator,
172
+ hallucinationDetector: config.hallucinationDetector || (() => false),
173
+ silent: !!config.silent,
174
+ retentionDays: config.retentionDays !== undefined ? config.retentionDays : persisted.retentionDays,
175
+ cleanupIntervalHours: config.cleanupIntervalHours !== undefined
176
+ ? config.cleanupIntervalHours
177
+ : persisted.cleanupIntervalHours,
178
+ tenantId,
179
+ maxTracesPerSecond: config.maxTracesPerSecond ?? 0,
180
+ maxTracesPerMinute: config.maxTracesPerMinute ?? 0,
181
+ burstAllowance: config.burstAllowance ?? 10,
182
+ };
183
+ // Initialize rate limiter if any rate limit is configured
184
+ if (this.config.maxTracesPerSecond > 0 || this.config.maxTracesPerMinute > 0) {
185
+ this.rateLimiter = new TokenBucketRateLimiter({
186
+ maxTracesPerSecond: this.config.maxTracesPerSecond,
187
+ maxTracesPerMinute: this.config.maxTracesPerMinute,
188
+ burstAllowance: this.config.burstAllowance,
189
+ });
190
+ }
191
+ this.setupRetentionCleanup();
192
+ }
193
+ setupRetentionCleanup() {
194
+ if (this._cleanupInterval) {
195
+ clearInterval(this._cleanupInterval);
196
+ this._cleanupInterval = undefined;
197
+ }
198
+ if (this.config.retentionDays > 0) {
199
+ const hours = this.config.cleanupIntervalHours > 0 ? this.config.cleanupIntervalHours : 24;
200
+ const intervalMs = hours * 60 * 60 * 1000;
201
+ this._cleanupInterval = setInterval(() => {
202
+ const before = Date.now() - this.config.retentionDays * 24 * 60 * 60 * 1000;
203
+ try {
204
+ this.storage.cleanupOldTraces(before);
205
+ this.storage.cleanupOldRuns(before);
206
+ this.storage.cleanupOldAgentUsage(before);
207
+ }
208
+ catch (_) {
209
+ /* scheduled cleanup must never crash host process */
210
+ }
211
+ }, intervalMs);
212
+ if (this._cleanupInterval &&
213
+ typeof this._cleanupInterval.unref === 'function') {
214
+ this._cleanupInterval.unref();
215
+ }
216
+ }
217
+ }
218
+ /**
219
+ * Start a new agent run
220
+ */
221
+ startRun(name, metadata = {}) {
222
+ const runId = randomUUID();
223
+ this.storage.createRun({
224
+ id: runId,
225
+ name,
226
+ startedAt: Date.now(),
227
+ metadata,
228
+ tenantId: this.config.tenantId || undefined,
229
+ });
230
+ this.currentRunId = runId;
231
+ return runId;
232
+ }
233
+ /**
234
+ * Complete the current run
235
+ */
236
+ completeRun(status = 'success') {
237
+ if (this.currentRunId) {
238
+ this.storage.completeRun(this.currentRunId, status);
239
+ const runId = this.currentRunId;
240
+ this.currentRunId = null;
241
+ // Fire-and-forget webhook delivery for run complete/error
242
+ if (status === 'success') {
243
+ this.triggerWebhook('run.complete', { runId }).catch(() => { });
244
+ try {
245
+ this.webhookEmitter.emit('webhook', 'run.complete', { runId });
246
+ }
247
+ catch (_) {
248
+ /* handler errors must not break */
249
+ }
250
+ }
251
+ else {
252
+ this.triggerWebhook('run.error', { runId }).catch(() => { });
253
+ try {
254
+ this.webhookEmitter.emit('webhook', 'run.error', { runId });
255
+ }
256
+ catch (_) {
257
+ /* handler errors must not break */
258
+ }
259
+ }
260
+ }
261
+ }
262
+ /**
263
+ * Trace an async function call
264
+ */
265
+ async trace(name, fn, options = {}) {
266
+ const traceId = options.context && typeof options.context.traceId === 'string'
267
+ ? options.context.traceId
268
+ : randomUUID();
269
+ const parentId = options.context && typeof options.context.traceId === 'string'
270
+ ? options.context.parentSpanId
271
+ : (options.parentId ?? undefined);
272
+ const startTime = Date.now();
273
+ let result;
274
+ let error;
275
+ let status = 'success';
276
+ // Rate limit check
277
+ if (this.rateLimiter && !this.rateLimiter.tryConsume()) {
278
+ // Rate limited — execute the function but don't record the trace
279
+ return fn();
280
+ }
281
+ const prevContext = this.activeTraceContext;
282
+ this.activeTraceContext = { traceId, toolCalls: [] };
283
+ try {
284
+ result = await fn();
285
+ }
286
+ catch (e) {
287
+ error = e instanceof Error ? e.message : String(e);
288
+ status = 'error';
289
+ throw e;
290
+ }
291
+ finally {
292
+ const latencyMs = Date.now() - startTime;
293
+ const tokens = options.tokens || {
294
+ promptTokens: 0,
295
+ completionTokens: 0,
296
+ totalTokens: 0,
297
+ model: options.model,
298
+ provider: options.provider,
299
+ };
300
+ const costUsd = this.config.costCalculator(tokens, options.model);
301
+ const baseMeta = options.metadata || {};
302
+ const ctxMeta = options.context ? options.context.metadata || {} : {};
303
+ const mergedMeta = { ...ctxMeta, ...baseMeta };
304
+ // Drain collected tool calls from active context (populated by recordToolCall during fn)
305
+ const toolCalls = this.activeTraceContext ? this.activeTraceContext.toolCalls : [];
306
+ // Restore previous context (supports nesting; top-level restores to null)
307
+ this.activeTraceContext = prevContext;
308
+ const trace = {
309
+ id: traceId,
310
+ runId: this.currentRunId || randomUUID(),
311
+ name,
312
+ status,
313
+ input: options.input ?? null,
314
+ output: result ?? null,
315
+ tokens,
316
+ toolCalls,
317
+ latencyMs,
318
+ costUsd,
319
+ error,
320
+ metadata: mergedMeta,
321
+ parentId,
322
+ tenantId: this.config.tenantId || undefined,
323
+ };
324
+ this.storage.createTrace(trace);
325
+ if (this.config.autoCleanup) {
326
+ this.storage.cleanup(this.config.maxTraces);
327
+ }
328
+ // Auto-check alerts after each trace (awaited so history/delivery visible to caller immediately)
329
+ try {
330
+ await this.checkAlerts();
331
+ }
332
+ catch (_) {
333
+ /* alerts must never cause trace() to fail */
334
+ }
335
+ // Fire-and-forget webhook delivery for trace complete/error
336
+ if (status === 'success') {
337
+ this.triggerWebhook('trace.complete', {
338
+ traceId,
339
+ runId: trace.runId,
340
+ name,
341
+ latencyMs,
342
+ costUsd,
343
+ }).catch(() => { });
344
+ try {
345
+ this.webhookEmitter.emit('webhook', 'trace.complete', {
346
+ traceId,
347
+ runId: trace.runId,
348
+ name,
349
+ latencyMs,
350
+ costUsd,
351
+ });
352
+ }
353
+ catch (_) {
354
+ /* handler errors must not break */
355
+ }
356
+ }
357
+ else {
358
+ this.triggerWebhook('trace.error', { traceId, runId: trace.runId, name, error }).catch(() => { });
359
+ try {
360
+ this.webhookEmitter.emit('webhook', 'trace.error', {
361
+ traceId,
362
+ runId: trace.runId,
363
+ name,
364
+ error,
365
+ });
366
+ }
367
+ catch (_) {
368
+ /* handler errors must not break */
369
+ }
370
+ }
371
+ }
372
+ return result;
373
+ }
374
+ /**
375
+ * Record a tool call within the current trace
376
+ */
377
+ recordToolCall(call) {
378
+ const id = randomUUID();
379
+ const timestamp = Date.now();
380
+ const fullCall = { ...call, id, timestamp };
381
+ if (this.activeTraceContext) {
382
+ this.activeTraceContext.toolCalls.push(fullCall);
383
+ return id;
384
+ }
385
+ // Outside active trace: still return id (for potential manual correlation) but warn
386
+ if (!this.config.silent) {
387
+ console.warn(`[AgentTrace] recordToolCall("${call.name}") called outside an active trace() — tool call will not be stored. Call recordToolCall from within a trace() callback.`);
388
+ }
389
+ return id;
390
+ }
391
+ /**
392
+ * Get traces with filtering
393
+ */
394
+ getTraces(filter = {}) {
395
+ return this.storage.getTraces(filter);
396
+ }
397
+ /**
398
+ * Get a specific trace
399
+ */
400
+ getTrace(id) {
401
+ return this.storage.getTrace(id);
402
+ }
403
+ /**
404
+ * Get recent runs (most recent first)
405
+ */
406
+ getRuns(limit = 100) {
407
+ return this.storage.getRuns(limit);
408
+ }
409
+ /**
410
+ * Get a specific run
411
+ */
412
+ getRun(id) {
413
+ return this.storage.getRun(id);
414
+ }
415
+ /**
416
+ * Get summary statistics
417
+ */
418
+ getStats() {
419
+ return this.storage.getStats(this.config.tenantId || undefined);
420
+ }
421
+ /**
422
+ * Get the number of traces dropped due to rate limiting.
423
+ * Returns 0 if rate limiting is not configured.
424
+ */
425
+ getDroppedTraces() {
426
+ return this.rateLimiter?.getDroppedTraces() ?? 0;
427
+ }
428
+ /**
429
+ * Get cost breakdown (by model, by day, total). Supports optional run filter.
430
+ */
431
+ getCostBreakdown(filter = {}) {
432
+ return this.storage.getCostBreakdown(filter.runId, this.config.tenantId || undefined);
433
+ }
434
+ // ---- Agent usage tracking (for self-observability by agents) ----
435
+ /**
436
+ * Record a usage/action event from an agent (for agent self-tracking of its own operations/costs).
437
+ * Agents can call this to log high-level actions beyond LLM traces.
438
+ */
439
+ recordAgentUsage(record) {
440
+ const full = {
441
+ id: record.id || randomUUID(),
442
+ createdAt: record.createdAt || Date.now(),
443
+ agentName: record.agentName,
444
+ agentType: record.agentType,
445
+ sessionId: record.sessionId,
446
+ action: record.action,
447
+ target: record.target,
448
+ tokensUsed: record.tokensUsed ?? 0,
449
+ costUsd: record.costUsd ?? 0,
450
+ durationMs: record.durationMs ?? 0,
451
+ status: record.status || 'success',
452
+ metadata: record.metadata || {},
453
+ tenantId: this.config.tenantId || undefined,
454
+ };
455
+ this.storage.recordAgentUsage(full);
456
+ this.usageEmitter.emit('usage', full);
457
+ }
458
+ /**
459
+ * Query recorded agent usage records.
460
+ */
461
+ getAgentUsage(filter = {}) {
462
+ return this.storage.getAgentUsage(filter, this.config.tenantId || undefined);
463
+ }
464
+ /**
465
+ * Get aggregated usage statistics across agent actions.
466
+ */
467
+ getUsageStats(agentName, fromDate, toDate) {
468
+ return this.storage.getUsageStats(agentName, fromDate, toDate, this.config.tenantId || undefined);
469
+ }
470
+ /**
471
+ * Get list of agents with their last active time (all time, sorted recent first).
472
+ */
473
+ getActiveAgents() {
474
+ return this.storage.getActiveAgents();
475
+ }
476
+ /**
477
+ * Get 'who' summary (active agents overview, supports activeOnly for last 30min).
478
+ */
479
+ getAgentWho(filter = {}) {
480
+ return this.storage.getAgentWho(filter);
481
+ }
482
+ /**
483
+ * Get agent sessions summary.
484
+ */
485
+ getAgentSessions(filter = {}) {
486
+ return this.storage.getAgentSessions(filter);
487
+ }
488
+ // ---- API key management ----
489
+ /**
490
+ * Create a new API key for dashboard API authentication.
491
+ * Returns the full secret key (display once) + metadata record.
492
+ * The secret is never stored; only its SHA-256 hash is persisted.
493
+ */
494
+ createApiKey(name) {
495
+ if (!name || typeof name !== 'string' || name.trim().length === 0) {
496
+ throw new Error('API key name is required');
497
+ }
498
+ const fullKey = `at_${randomUUID().replace(/-/g, '')}`;
499
+ const meta = this.storage.createApiKey(name.trim(), ['read', 'write'], fullKey);
500
+ return { ...meta, key: fullKey };
501
+ }
502
+ /**
503
+ * List API keys (masked/previewed, no secrets).
504
+ */
505
+ listApiKeys() {
506
+ return this.storage.getApiKeys();
507
+ }
508
+ /**
509
+ * Revoke an API key by its id. Returns true if deleted.
510
+ */
511
+ revokeApiKey(id) {
512
+ if (!id)
513
+ return;
514
+ this.storage.revokeApiKey(id);
515
+ }
516
+ /**
517
+ * Validate a raw API key string (e.g. from header). Returns matching metadata or null.
518
+ * Side effect: updates lastUsedAt on success.
519
+ */
520
+ validateApiKey(key) {
521
+ return this.storage.validateApiKey(key);
522
+ }
523
+ /**
524
+ * Subscribe to new agent usage records (for live dashboards / SSE).
525
+ */
526
+ onUsage(listener) {
527
+ this.usageEmitter.on('usage', listener);
528
+ }
529
+ /**
530
+ * Unsubscribe from agent usage events.
531
+ */
532
+ offUsage(listener) {
533
+ this.usageEmitter.off('usage', listener);
534
+ }
535
+ // ---- Multi-tenant Project Management ----
536
+ /**
537
+ * Create a new project for multi-tenant isolation.
538
+ * Returns the project with its API key (shown only once).
539
+ */
540
+ createProject(name) {
541
+ if (!name || typeof name !== 'string' || name.trim().length === 0) {
542
+ throw new Error('Project name is required');
543
+ }
544
+ return this.storage.createProject(name.trim());
545
+ }
546
+ /**
547
+ * Look up a project by its API key.
548
+ */
549
+ getProject(apiKey) {
550
+ const p = this.storage.getProject(apiKey);
551
+ if (!p)
552
+ return null;
553
+ // Don't expose the apiKey in the return — caller already has it
554
+ return { id: p.id, name: p.name, createdAt: p.createdAt };
555
+ }
556
+ /**
557
+ * Delete a project by ID. Returns true if deleted.
558
+ */
559
+ deleteProject(id) {
560
+ return this.storage.deleteProject(id);
561
+ }
562
+ // ---- Webhook Management ----
563
+ /**
564
+ * Register a new webhook. Returns the webhook ID.
565
+ */
566
+ addWebhook(url, events, secret) {
567
+ return this.storage.registerWebhook(url, events, secret);
568
+ }
569
+ /**
570
+ * List all configured webhooks.
571
+ */
572
+ getWebhooks() {
573
+ return this.storage.getWebhooks();
574
+ }
575
+ /**
576
+ * Remove a webhook by ID.
577
+ */
578
+ removeWebhook(id) {
579
+ this.storage.deleteWebhook(id);
580
+ }
581
+ /**
582
+ * Register a new webhook (alias for addWebhook). Returns the webhook ID.
583
+ */
584
+ registerWebhook(url, events, secret) {
585
+ return this.addWebhook(url, events, secret);
586
+ }
587
+ /**
588
+ * Delete a webhook by ID (alias for removeWebhook).
589
+ */
590
+ deleteWebhook(id) {
591
+ this.removeWebhook(id);
592
+ }
593
+ /**
594
+ * Trigger webhooks for a given event. Finds all enabled webhooks registered for
595
+ * the event, builds the payload, signs it if a secret is configured, and POSTs
596
+ * to each URL. Returns delivery results.
597
+ */
598
+ async triggerWebhook(event, payload) {
599
+ const webhooks = this.storage.getEnabledWebhooksForEvent(event);
600
+ const results = [];
601
+ const timestamp = Date.now();
602
+ for (const wh of webhooks) {
603
+ const fullPayload = { event, timestamp, ...payload };
604
+ const bodyStr = JSON.stringify(fullPayload);
605
+ const headers = {
606
+ 'Content-Type': 'application/json',
607
+ 'User-Agent': `AgentTrace/${VERSION}`,
608
+ };
609
+ if (wh.secret) {
610
+ const sig = createHash('sha256')
611
+ .update(wh.secret + '.' + bodyStr)
612
+ .digest('hex');
613
+ headers['X-AgentTrace-Signature'] = `sha256=${sig}`;
614
+ }
615
+ let deliveryStatus = 'failure';
616
+ let httpStatus;
617
+ let errorMsg;
618
+ try {
619
+ const resp = await fetch(wh.url, {
620
+ method: 'POST',
621
+ headers,
622
+ body: bodyStr,
623
+ });
624
+ httpStatus = resp.ok ? resp.status : resp.status;
625
+ deliveryStatus = resp.ok ? 'success' : 'failure';
626
+ if (resp.ok) {
627
+ this.storage.resetWebhookFailures(wh.id);
628
+ }
629
+ else {
630
+ this.storage.incrementWebhookFailures(wh.id);
631
+ errorMsg = `webhook responded ${resp.status}`;
632
+ }
633
+ }
634
+ catch (e) {
635
+ this.storage.incrementWebhookFailures(wh.id);
636
+ errorMsg = e instanceof Error ? e.message : String(e);
637
+ }
638
+ const delivery = {
639
+ id: randomUUID(),
640
+ webhookId: wh.id,
641
+ event,
642
+ payload: bodyStr,
643
+ status: deliveryStatus,
644
+ httpStatus,
645
+ error: errorMsg,
646
+ createdAt: timestamp,
647
+ };
648
+ results.push(delivery);
649
+ }
650
+ return results;
651
+ }
652
+ /**
653
+ * Test a webhook by ID: fires a test event payload to the webhook URL.
654
+ * Returns delivery result.
655
+ */
656
+ async testWebhook(id) {
657
+ const webhooks = this.getWebhooks();
658
+ const wh = webhooks.find((w) => w.id === id);
659
+ if (!wh) {
660
+ throw new Error(`Webhook '${id}' not found. List webhooks with: agenttrace-io webhook list`);
661
+ }
662
+ if (!wh.enabled) {
663
+ throw new Error(`Webhook '${id}' is disabled.`);
664
+ }
665
+ const payload = {
666
+ event: 'webhook.test',
667
+ timestamp: Date.now(),
668
+ message: 'AgentTrace webhook test',
669
+ };
670
+ try {
671
+ const resp = await fetch(wh.url, {
672
+ method: 'POST',
673
+ headers: {
674
+ 'Content-Type': 'application/json',
675
+ 'User-Agent': `AgentTrace/${VERSION}`,
676
+ },
677
+ body: JSON.stringify(payload),
678
+ });
679
+ if (resp.ok) {
680
+ this.storage.resetWebhookFailures(id);
681
+ }
682
+ else {
683
+ this.storage.incrementWebhookFailures(id);
684
+ }
685
+ return { ok: resp.ok, status: resp.status };
686
+ }
687
+ catch (e) {
688
+ this.storage.incrementWebhookFailures(id);
689
+ return { ok: false, error: e instanceof Error ? e.message : String(e) };
690
+ }
691
+ }
692
+ /**
693
+ * Register a webhook event handler callback.
694
+ * The handler is called for every emitted webhook event (both explicit emissions
695
+ * and automatic events from trace()/completeRun()).
696
+ * Returns a function that unsubscribes the handler when called.
697
+ */
698
+ onWebhook(handler) {
699
+ this.webhookEmitter.on('webhook', handler);
700
+ return () => {
701
+ this.webhookEmitter.off('webhook', handler);
702
+ };
703
+ }
704
+ /**
705
+ * Explicitly emit a webhook event.
706
+ * Fires all registered HTTP webhooks (via triggerWebhook) and invokes all
707
+ * onWebhook handler callbacks. Does not auto-emit from trace()/completeRun() --
708
+ * those methods already call triggerWebhook directly.
709
+ * Use this for custom events (e.g. 'cost.threshold', 'agent.inactive').
710
+ * Returns the HTTP delivery results (same as triggerWebhook).
711
+ */
712
+ async emitWebhookEvent(event, payload) {
713
+ // Fire HTTP webhooks
714
+ const results = await this.triggerWebhook(event, payload);
715
+ // Notify in-process handlers (fire-and-forget)
716
+ try {
717
+ this.webhookEmitter.emit('webhook', event, payload);
718
+ }
719
+ catch (_) {
720
+ /* handler errors must not break emission */
721
+ }
722
+ return results;
723
+ }
724
+ /**
725
+ * Trigger all webhooks registered for the given event.
726
+ * Alias for triggerWebhook — provided for API clarity when the intent is
727
+ * to fan-out to all matching webhooks rather than targeting a specific one.
728
+ * Also invokes any onWebhook handler callbacks after HTTP delivery.
729
+ */
730
+ async triggerAllWebhooks(event, payload) {
731
+ const results = await this.triggerWebhook(event, payload);
732
+ // Notify in-process handlers (fire-and-forget)
733
+ try {
734
+ this.webhookEmitter.emit('webhook', event, payload);
735
+ }
736
+ catch (_) {
737
+ /* handler errors must not break emission */
738
+ }
739
+ return results;
740
+ }
741
+ // ---- Multi-agent tracing (v0.2) ----
742
+ /**
743
+ * Create a TraceContext for a child operation linked to the provided parent context.
744
+ * The child context has a freshly generated traceId (use as the child's trace id)
745
+ * and parentSpanId pointing to the parent's traceId (span).
746
+ * Pass the returned context via options.context when calling trace() (on any AgentTrace instance).
747
+ */
748
+ createChild(context) {
749
+ if (!context || typeof context.traceId !== 'string' || context.traceId.length === 0) {
750
+ throw new Error('createChild requires a valid TraceContext with traceId');
751
+ }
752
+ const childTraceId = randomUUID();
753
+ return new TraceContext(childTraceId, context.traceId, { ...context.metadata });
754
+ }
755
+ /**
756
+ * Manually link a set of trace IDs as related (for cross-agent collaboration without strict parent/child).
757
+ * Uses an internal links table; getTraceTree will surface linked traces as children in the tree.
758
+ */
759
+ linkTraces(traceIds) {
760
+ if (!Array.isArray(traceIds) || traceIds.length < 2) {
761
+ return;
762
+ }
763
+ this.storage.linkTraces(traceIds);
764
+ }
765
+ /**
766
+ * Get the full tree (parent -> children, including manually linked) for the given traceId.
767
+ * The tree is rooted at the ultimate ancestor (following parentId links).
768
+ */
769
+ getTraceTree(traceId) {
770
+ return this.storage.getTraceTree(traceId);
771
+ }
772
+ // ---- Alerting (v0.2) ----
773
+ /**
774
+ * Register an alert condition. Persists config (without function) and enables auto-checks.
775
+ */
776
+ registerAlert(alert) {
777
+ if (!alert || !alert.name || typeof alert.condition !== 'function') {
778
+ throw new Error('Invalid alert: must have name and condition function');
779
+ }
780
+ // dedupe by name (last wins)
781
+ this.registeredAlerts = this.registeredAlerts.filter((a) => a.name !== alert.name);
782
+ const copy = {
783
+ name: alert.name,
784
+ condition: alert.condition,
785
+ webhook: alert.webhook,
786
+ email: alert.email,
787
+ cooldown: alert.cooldown ?? 0,
788
+ lastTriggered: alert.lastTriggered,
789
+ };
790
+ this.registeredAlerts.push(copy);
791
+ // persist without the function
792
+ const { condition: _cond, ...serializable } = copy;
793
+ this.storage.saveAlert(copy.name, serializable);
794
+ }
795
+ /**
796
+ * Check all registered alerts against current stats.
797
+ * Fires (and records history) for those whose condition is true and cooldown elapsed.
798
+ * Returns the AlertHistory entries that were triggered this check.
799
+ */
800
+ async checkAlerts() {
801
+ const results = [];
802
+ if (this.registeredAlerts.length === 0)
803
+ return results;
804
+ const stats = this.getStats();
805
+ const now = Date.now();
806
+ for (const alert of this.registeredAlerts) {
807
+ const cooldownMs = Math.max(0, alert.cooldown || 0) * 1000;
808
+ const last = alert.lastTriggered || 0;
809
+ if (cooldownMs > 0 && now - last < cooldownMs) {
810
+ continue;
811
+ }
812
+ const met = (() => {
813
+ try {
814
+ return !!alert.condition(stats);
815
+ }
816
+ catch (condErr) {
817
+ if (!this.config.silent) {
818
+ console.error(`[AgentTrace] Alert condition error for ${alert.name}:`, condErr);
819
+ }
820
+ return false;
821
+ }
822
+ })();
823
+ if (met) {
824
+ const hist = await this.deliverAlert(alert, stats, now);
825
+ results.push(hist);
826
+ alert.lastTriggered = now;
827
+ const { condition: _c, ...toStore } = alert;
828
+ this.storage.saveAlert(alert.name, toStore);
829
+ }
830
+ }
831
+ return results;
832
+ }
833
+ async deliverAlert(alert, stats, now) {
834
+ const numericStats = {
835
+ totalRuns: stats.totalRuns || 0,
836
+ totalTraces: stats.totalTraces || 0,
837
+ successRate: stats.successRate || 0,
838
+ avgLatencyMs: stats.avgLatencyMs || 0,
839
+ totalCostUsd: stats.totalCostUsd || 0,
840
+ totalTokens: stats.totalTokens || 0,
841
+ avgTokensPerTrace: stats.avgTokensPerTrace || 0,
842
+ };
843
+ let delivered;
844
+ let errMsg;
845
+ const payload = { alertName: alert.name, stats, timestamp: now };
846
+ if (alert.webhook) {
847
+ try {
848
+ const url = new URL(alert.webhook);
849
+ const host = url.hostname;
850
+ const bareHost = host.replace(/^\[|\]$/g, '');
851
+ const isLoopback = host === 'localhost' ||
852
+ bareHost === 'localhost' ||
853
+ bareHost === '127.0.0.1' ||
854
+ host.startsWith('127.') ||
855
+ bareHost === '::1';
856
+ if (url.protocol !== 'https:' && !isLoopback) {
857
+ throw new Error('webhook must use HTTPS');
858
+ }
859
+ let is172Private = false;
860
+ if (bareHost.startsWith('172.')) {
861
+ const second = parseInt(bareHost.split('.')[1] || '0', 10);
862
+ is172Private = second >= 16 && second <= 31;
863
+ }
864
+ const isPrivate = bareHost.startsWith('10.') ||
865
+ bareHost.startsWith('192.168.') ||
866
+ bareHost.startsWith('169.254.') ||
867
+ is172Private ||
868
+ bareHost.startsWith('fc00:') ||
869
+ bareHost.startsWith('fe80:');
870
+ if (isPrivate) {
871
+ throw new Error('webhook URL resolves to private IP');
872
+ }
873
+ const webhooks = this.storage.getWebhooks ? this.storage.getWebhooks() : [];
874
+ const wh = webhooks.find((w) => w.url === alert.webhook);
875
+ const secret = wh?.secret;
876
+ const headers = {
877
+ 'Content-Type': 'application/json',
878
+ 'User-Agent': `AgentTrace/${VERSION}`,
879
+ };
880
+ if (secret) {
881
+ const sig = createHmac('sha256', secret).update(JSON.stringify(payload)).digest('hex');
882
+ headers['X-AgentTrace-Signature'] = `sha256=${sig}`;
883
+ }
884
+ const resp = await fetch(alert.webhook, {
885
+ method: 'POST',
886
+ headers,
887
+ body: JSON.stringify(payload),
888
+ signal: AbortSignal.timeout(10000),
889
+ });
890
+ delivered = resp.ok;
891
+ if (!resp.ok) {
892
+ errMsg = `webhook responded ${resp.status}`;
893
+ }
894
+ }
895
+ catch (e) {
896
+ delivered = false;
897
+ errMsg = e?.message || String(e);
898
+ }
899
+ }
900
+ else if (alert.email) {
901
+ delivered = false;
902
+ errMsg = 'email delivery not supported in this version';
903
+ }
904
+ else {
905
+ delivered = false;
906
+ errMsg = 'no delivery channel configured';
907
+ }
908
+ const history = {
909
+ id: randomUUID(),
910
+ alertName: alert.name,
911
+ triggeredAt: now,
912
+ stats: numericStats,
913
+ delivered,
914
+ ...(errMsg ? { error: errMsg } : {}),
915
+ };
916
+ this.storage.insertAlertHistory(history);
917
+ if (!this.config.silent) {
918
+ const status = delivered ? 'delivered' : `delivery failed${errMsg ? ': ' + errMsg : ''}`;
919
+ console.log(`[AgentTrace] Alert '${alert.name}' triggered. ${status}`);
920
+ }
921
+ return history;
922
+ }
923
+ /**
924
+ * Get currently registered (in-memory) alerts. Falls back to persisted configs (with no-op condition) for CLI.
925
+ */
926
+ getAlerts() {
927
+ const map = new Map();
928
+ // load persisted (dummy condition)
929
+ for (const s of this.storage.getStoredAlerts()) {
930
+ const cfg = s.config || {};
931
+ map.set(s.name, {
932
+ name: s.name,
933
+ condition: () => false,
934
+ webhook: cfg.webhook,
935
+ email: cfg.email,
936
+ cooldown: typeof cfg.cooldown === 'number' ? cfg.cooldown : 0,
937
+ lastTriggered: cfg.lastTriggered,
938
+ });
939
+ }
940
+ // overlay runtime registered (have real conditions)
941
+ for (const r of this.registeredAlerts) {
942
+ map.set(r.name, { ...r });
943
+ }
944
+ return Array.from(map.values());
945
+ }
946
+ /**
947
+ * Get alert firing history from storage.
948
+ */
949
+ getAlertHistory() {
950
+ return this.storage.getAlertHistory();
951
+ }
952
+ /**
953
+ * Return health report: status, version (sdk), uptime, dbPath, traceCount, dbSize + integrity check.
954
+ * Integrity verifies required tables exist and detects orphaned child records.
955
+ */
956
+ getHealth() {
957
+ const h = this.storage.getHealthInfo();
958
+ return {
959
+ status: 'ok',
960
+ version: VERSION,
961
+ uptime: process.uptime(),
962
+ dbPath: h.dbPath,
963
+ traceCount: h.traceCount,
964
+ dbSize: h.dbSize,
965
+ integrity: h.integrity,
966
+ };
967
+ }
968
+ // ---- Retention / data lifecycle ----
969
+ /**
970
+ * Delete traces with created_at < before (timestamp ms). Also cleans dependent scores/links.
971
+ * Returns number of traces deleted.
972
+ */
973
+ cleanupOldTraces(before) {
974
+ return this.storage.cleanupOldTraces(before);
975
+ }
976
+ /**
977
+ * Delete runs with started_at < before (timestamp ms). Cascades to their traces.
978
+ * Returns number of runs deleted.
979
+ */
980
+ cleanupOldRuns(before) {
981
+ return this.storage.cleanupOldRuns(before);
982
+ }
983
+ /**
984
+ * Delete agent_usage records with created_at < before (timestamp ms).
985
+ * Returns number deleted.
986
+ */
987
+ cleanupOldAgentUsage(before) {
988
+ return this.storage.cleanupOldAgentUsage(before);
989
+ }
990
+ /**
991
+ * Return basic storage stats for the backing DB.
992
+ */
993
+ getStorageStats() {
994
+ return this.storage.getStorageStats();
995
+ }
996
+ /**
997
+ * Get the active retention policy (from this instance config, which may come from persisted defaults).
998
+ */
999
+ getRetentionPolicy() {
1000
+ return {
1001
+ retentionDays: this.config.retentionDays,
1002
+ cleanupIntervalHours: this.config.cleanupIntervalHours,
1003
+ };
1004
+ }
1005
+ /**
1006
+ * Set retention policy for this DB (persists it) and update live config + reschedule timer if needed.
1007
+ */
1008
+ setRetentionPolicy(retentionDays, cleanupIntervalHours) {
1009
+ this.storage.setRetentionPolicy(retentionDays, cleanupIntervalHours);
1010
+ this.config.retentionDays = Math.max(0, Math.floor(Number(retentionDays) || 0));
1011
+ if (cleanupIntervalHours !== undefined) {
1012
+ this.config.cleanupIntervalHours = Math.max(1, Math.floor(Number(cleanupIntervalHours) || 24));
1013
+ }
1014
+ this.setupRetentionCleanup();
1015
+ }
1016
+ /**
1017
+ * Export traces to JSON, CSV, or OpenTelemetry (OTLP JSON)
1018
+ */
1019
+ export(format = 'json', filter = {}) {
1020
+ const traces = this.storage.getTraces(filter);
1021
+ if (format === 'json') {
1022
+ return JSON.stringify(traces, null, 2);
1023
+ }
1024
+ if (format === 'otel') {
1025
+ const resourceAttributes = buildResourceAttributes();
1026
+ const spans = traces.map((t) => traceToOtelSpan(t));
1027
+ const otlp = {
1028
+ resourceSpans: [
1029
+ {
1030
+ resource: { attributes: resourceAttributes },
1031
+ scopeSpans: [
1032
+ {
1033
+ scope: { name: 'agenttrace' },
1034
+ spans,
1035
+ },
1036
+ ],
1037
+ },
1038
+ ],
1039
+ };
1040
+ return JSON.stringify(otlp, null, 2);
1041
+ }
1042
+ // CSV
1043
+ const headers = [
1044
+ 'id',
1045
+ 'runId',
1046
+ 'name',
1047
+ 'status',
1048
+ 'latencyMs',
1049
+ 'costUsd',
1050
+ 'totalTokens',
1051
+ 'createdAt',
1052
+ ];
1053
+ const rows = traces.map((t) => [
1054
+ t.id,
1055
+ t.runId,
1056
+ t.name,
1057
+ t.status,
1058
+ t.latencyMs,
1059
+ t.costUsd,
1060
+ t.tokens.totalTokens,
1061
+ t.createdAt,
1062
+ ]);
1063
+ return [headers.join(','), ...rows.map((r) => r.join(','))].join('\n');
1064
+ }
1065
+ /**
1066
+ * Close the database connection
1067
+ */
1068
+ close() {
1069
+ if (this._cleanupInterval) {
1070
+ clearInterval(this._cleanupInterval);
1071
+ this._cleanupInterval = undefined;
1072
+ }
1073
+ this.storage.close();
1074
+ }
1075
+ /**
1076
+ * Evaluate traces using the provided scorers.
1077
+ * If traceIds provided, scores only those; if runId, scores traces in that run; otherwise all traces.
1078
+ */
1079
+ async evaluate(options) {
1080
+ const { scorers, runId, traceIds, concurrency } = options;
1081
+ if (!scorers || scorers.length === 0) {
1082
+ return [];
1083
+ }
1084
+ const traces = traceIds && traceIds.length > 0
1085
+ ? traceIds.map((id) => this.getTrace(id)).filter((t) => t != null)
1086
+ : runId
1087
+ ? this.getTraces({ runId })
1088
+ : this.getTraces();
1089
+ return this.scoreLoop(traces, scorers, concurrency);
1090
+ }
1091
+ /**
1092
+ * Score a single trace by id.
1093
+ */
1094
+ async evaluateTrace(traceId, scorers) {
1095
+ const trace = this.getTrace(traceId);
1096
+ if (!trace) {
1097
+ return { traceId, scores: {}, errors: {} };
1098
+ }
1099
+ return this.scoreTrace(trace, scorers);
1100
+ }
1101
+ /**
1102
+ * Internal helper to iterate traces with limited concurrency, run scorers, store scores.
1103
+ */
1104
+ async scoreLoop(traces, scorers, concurrency) {
1105
+ if (traces.length === 0)
1106
+ return [];
1107
+ const limit = Math.max(1, concurrency ?? 5);
1108
+ const results = [];
1109
+ for (let i = 0; i < traces.length; i += limit) {
1110
+ const chunk = traces.slice(i, i + limit);
1111
+ const chunkResults = await Promise.all(chunk.map((t) => this.scoreTrace(t, scorers)));
1112
+ results.push(...chunkResults);
1113
+ }
1114
+ return results;
1115
+ }
1116
+ /**
1117
+ * Internal: run all scorers on one trace (in parallel), catch errors, store successful scores.
1118
+ */
1119
+ async scoreTrace(trace, scorers) {
1120
+ const traceId = trace.id;
1121
+ const scores = {};
1122
+ const errors = {};
1123
+ await Promise.all(scorers.map(async (scorer) => {
1124
+ try {
1125
+ const val = await Promise.resolve(scorer.fn(trace));
1126
+ if (typeof val === 'number' && Number.isFinite(val)) {
1127
+ scores[scorer.name] = val;
1128
+ const id = randomUUID();
1129
+ this.storage.createScore(id, traceId, scorer.name, val);
1130
+ }
1131
+ else {
1132
+ errors[scorer.name] = `Invalid score returned: ${val}`;
1133
+ }
1134
+ }
1135
+ catch (e) {
1136
+ errors[scorer.name] = e instanceof Error ? e.message : String(e);
1137
+ }
1138
+ }));
1139
+ return { traceId, scores, errors };
1140
+ }
1141
+ }
1142
+ // Singleton for convenience
1143
+ let instance = null;
1144
+ export function init(config) {
1145
+ instance = new AgentTrace(config);
1146
+ return instance;
1147
+ }
1148
+ export function getAgentTrace() {
1149
+ if (!instance) {
1150
+ instance = new AgentTrace();
1151
+ }
1152
+ return instance;
1153
+ }
1154
+ /**
1155
+ * Helper to create a Scorer from name + function.
1156
+ */
1157
+ export function score(name, fn) {
1158
+ return { name, fn };
1159
+ }
1160
+ /**
1161
+ * Helper to create an AlertCondition (omits lastTriggered which is internal).
1162
+ */
1163
+ export function alert(config) {
1164
+ return {
1165
+ ...config,
1166
+ lastTriggered: undefined,
1167
+ };
1168
+ }
1169
+ //# sourceMappingURL=index.js.map