@doclo/core 0.1.5

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.
@@ -0,0 +1,630 @@
1
+ // src/observability/hook-executor.ts
2
+ var DEFAULT_HOOK_TIMEOUT = 5e3;
3
+ async function executeHook(hook, options) {
4
+ const { hookName, config, context, fireAndForget = false } = options;
5
+ if (!hook) {
6
+ return;
7
+ }
8
+ if (config.enabled === false) {
9
+ return;
10
+ }
11
+ if (fireAndForget || config.fireAndForget) {
12
+ executeHookWithErrorHandling(hook, hookName, context, config).catch(() => {
13
+ });
14
+ return;
15
+ }
16
+ const timeout = config.hookTimeout ?? DEFAULT_HOOK_TIMEOUT;
17
+ try {
18
+ await executeHookWithTimeout(hook, context, timeout, hookName, config);
19
+ } catch (error) {
20
+ }
21
+ }
22
+ async function executeHookWithErrorHandling(hook, hookName, context, config) {
23
+ try {
24
+ const result = hook(context);
25
+ if (result && typeof result.then === "function") {
26
+ await result;
27
+ }
28
+ } catch (error) {
29
+ handleHookError(error, hookName, context, config);
30
+ }
31
+ }
32
+ async function executeHookWithTimeout(hook, context, timeoutMs, hookName, config) {
33
+ const timeoutPromise = new Promise((_, reject) => {
34
+ setTimeout(() => {
35
+ reject(new Error(`Hook '${hookName}' exceeded timeout of ${timeoutMs}ms`));
36
+ }, timeoutMs);
37
+ });
38
+ const hookPromise = executeHookWithErrorHandling(hook, hookName, context, config);
39
+ try {
40
+ await Promise.race([hookPromise, timeoutPromise]);
41
+ } catch (error) {
42
+ if (error instanceof Error && error.message.includes("exceeded timeout")) {
43
+ handleHookError(error, hookName, context, config);
44
+ }
45
+ }
46
+ }
47
+ function handleHookError(error, hookName, context, config) {
48
+ const hookError = {
49
+ hookName,
50
+ error,
51
+ context,
52
+ timestamp: Date.now()
53
+ };
54
+ if (config.onHookError) {
55
+ try {
56
+ config.onHookError(hookError);
57
+ } catch (onHookErrorError) {
58
+ console.error("[Observability] onHookError handler failed:", onHookErrorError);
59
+ console.error("[Observability] Original hook error:", hookError);
60
+ }
61
+ } else {
62
+ console.warn(`[Observability] Hook '${hookName}' failed:`, error);
63
+ }
64
+ if (config.failOnHookError) {
65
+ throw error;
66
+ }
67
+ }
68
+ async function executeHooksSerial(hooks) {
69
+ for (const { hook, options } of hooks) {
70
+ await executeHook(hook, options);
71
+ }
72
+ }
73
+ function isObservabilityEnabled(config) {
74
+ if (config.enabled === false) {
75
+ return false;
76
+ }
77
+ if (config.samplingRate === void 0 || config.samplingRate === 1) {
78
+ return true;
79
+ }
80
+ if (config.samplingRate === 0) {
81
+ return false;
82
+ }
83
+ return Math.random() < config.samplingRate;
84
+ }
85
+
86
+ // src/runtime/crypto.ts
87
+ function getRandomBytes(length) {
88
+ const bytes = new Uint8Array(length);
89
+ if (typeof globalThis !== "undefined" && globalThis.crypto?.getRandomValues) {
90
+ globalThis.crypto.getRandomValues(bytes);
91
+ return bytes;
92
+ }
93
+ throw new Error(
94
+ "Web Crypto API not available. This SDK requires:\n- Node.js 18.0.0 or later (has globalThis.crypto)\n- Edge Runtime (Vercel, Cloudflare)\n- Modern browsers"
95
+ );
96
+ }
97
+ function bytesToHex(bytes) {
98
+ return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
99
+ }
100
+ function randomHex(byteLength) {
101
+ const bytes = getRandomBytes(byteLength);
102
+ return bytesToHex(bytes);
103
+ }
104
+ function randomUUID() {
105
+ if (typeof globalThis !== "undefined" && globalThis.crypto?.randomUUID) {
106
+ return globalThis.crypto.randomUUID();
107
+ }
108
+ const bytes = getRandomBytes(16);
109
+ bytes[6] = bytes[6] & 15 | 64;
110
+ bytes[8] = bytes[8] & 63 | 128;
111
+ const hex = bytesToHex(bytes);
112
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`;
113
+ }
114
+
115
+ // src/observability/trace-context.ts
116
+ var TRACE_CONTEXT_VERSION = "00";
117
+ var TRACE_FLAGS_SAMPLED = 1;
118
+ var TRACE_FLAGS_NOT_SAMPLED = 0;
119
+ function generateTraceId() {
120
+ return randomHex(16);
121
+ }
122
+ function generateSpanId() {
123
+ return randomHex(8);
124
+ }
125
+ function generateExecutionId() {
126
+ return randomUUID();
127
+ }
128
+ function createTraceContext(config, sampled) {
129
+ const traceIdGenerator = config.generateTraceId ?? generateTraceId;
130
+ const spanIdGenerator = config.generateSpanId ?? generateSpanId;
131
+ if (config.traceContext) {
132
+ const input = config.traceContext;
133
+ return {
134
+ traceId: input.traceId,
135
+ spanId: spanIdGenerator(),
136
+ parentSpanId: input.parentSpanId,
137
+ traceFlags: input.traceFlags ?? (sampled ? TRACE_FLAGS_SAMPLED : TRACE_FLAGS_NOT_SAMPLED),
138
+ traceState: input.traceState
139
+ };
140
+ }
141
+ return {
142
+ traceId: traceIdGenerator(),
143
+ spanId: spanIdGenerator(),
144
+ traceFlags: sampled ? TRACE_FLAGS_SAMPLED : TRACE_FLAGS_NOT_SAMPLED
145
+ };
146
+ }
147
+ function createChildSpanContext(parent, config) {
148
+ const spanIdGenerator = config?.generateSpanId ?? generateSpanId;
149
+ return {
150
+ traceId: parent.traceId,
151
+ spanId: spanIdGenerator(),
152
+ parentSpanId: parent.spanId,
153
+ traceFlags: parent.traceFlags,
154
+ traceState: parent.traceState
155
+ };
156
+ }
157
+ function formatTraceparent(context) {
158
+ const flags = context.traceFlags.toString(16).padStart(2, "0");
159
+ return `${TRACE_CONTEXT_VERSION}-${context.traceId}-${context.spanId}-${flags}`;
160
+ }
161
+ function parseTraceparent(traceparent) {
162
+ const parts = traceparent.split("-");
163
+ if (parts.length !== 4) {
164
+ return null;
165
+ }
166
+ const [version, traceId, spanId, flags] = parts;
167
+ if (version !== TRACE_CONTEXT_VERSION) {
168
+ return null;
169
+ }
170
+ if (!/^[0-9a-f]{32}$/.test(traceId)) {
171
+ return null;
172
+ }
173
+ if (!/^[0-9a-f]{16}$/.test(spanId)) {
174
+ return null;
175
+ }
176
+ if (!/^[0-9a-f]{2}$/.test(flags)) {
177
+ return null;
178
+ }
179
+ return {
180
+ traceId,
181
+ parentSpanId: spanId,
182
+ traceFlags: parseInt(flags, 16)
183
+ };
184
+ }
185
+ function formatTracestate(context) {
186
+ return context.traceState;
187
+ }
188
+ function isTraceSampled(context) {
189
+ return (context.traceFlags & TRACE_FLAGS_SAMPLED) === TRACE_FLAGS_SAMPLED;
190
+ }
191
+ function isValidTraceId(traceId) {
192
+ if (!/^[0-9a-f]{32}$/.test(traceId)) {
193
+ return false;
194
+ }
195
+ if (traceId === "00000000000000000000000000000000") {
196
+ return false;
197
+ }
198
+ return true;
199
+ }
200
+ function isValidSpanId(spanId) {
201
+ if (!/^[0-9a-f]{16}$/.test(spanId)) {
202
+ return false;
203
+ }
204
+ if (spanId === "0000000000000000") {
205
+ return false;
206
+ }
207
+ return true;
208
+ }
209
+ var TraceContextManager = class {
210
+ traceContext = null;
211
+ config;
212
+ constructor(config) {
213
+ this.config = config;
214
+ }
215
+ /**
216
+ * Initialize trace context for new execution
217
+ */
218
+ initialize(sampled) {
219
+ this.traceContext = createTraceContext(this.config, sampled);
220
+ return this.traceContext;
221
+ }
222
+ /**
223
+ * Get current trace context
224
+ */
225
+ getTraceContext() {
226
+ return this.traceContext;
227
+ }
228
+ /**
229
+ * Create child span context
230
+ */
231
+ createChildSpan() {
232
+ if (!this.traceContext) {
233
+ throw new Error("Trace context not initialized");
234
+ }
235
+ return createChildSpanContext(this.traceContext, this.config);
236
+ }
237
+ /**
238
+ * Get traceparent header value
239
+ */
240
+ getTraceparent() {
241
+ if (!this.traceContext) {
242
+ return null;
243
+ }
244
+ return formatTraceparent(this.traceContext);
245
+ }
246
+ /**
247
+ * Reset trace context (for testing)
248
+ */
249
+ reset() {
250
+ this.traceContext = null;
251
+ }
252
+ };
253
+
254
+ // src/observability/otel-attributes.ts
255
+ var GEN_AI_SYSTEMS = {
256
+ openai: "openai",
257
+ "openai-compatible": "openai",
258
+ anthropic: "anthropic",
259
+ google: "vertex_ai",
260
+ // Google uses Vertex AI
261
+ "google-ai": "vertex_ai",
262
+ cohere: "cohere",
263
+ huggingface: "huggingface",
264
+ openrouter: "openrouter",
265
+ // Custom system
266
+ ollama: "ollama"
267
+ // Custom system
268
+ };
269
+ function mapProviderToSystem(provider) {
270
+ const normalized = provider.toLowerCase();
271
+ return GEN_AI_SYSTEMS[normalized] ?? normalized;
272
+ }
273
+ function buildOtelAttributes(data) {
274
+ const attributes = {};
275
+ if (data.provider) {
276
+ attributes["gen_ai.system"] = mapProviderToSystem(data.provider);
277
+ }
278
+ if (data.stepType) {
279
+ attributes["gen_ai.operation.name"] = data.stepType;
280
+ }
281
+ if (data.model) {
282
+ attributes["gen_ai.request.model"] = data.model;
283
+ }
284
+ if (data.modelUsed) {
285
+ attributes["gen_ai.response.model"] = data.modelUsed;
286
+ }
287
+ if (data.inputTokens !== void 0) {
288
+ attributes["gen_ai.usage.input_tokens"] = data.inputTokens;
289
+ }
290
+ if (data.outputTokens !== void 0) {
291
+ attributes["gen_ai.usage.output_tokens"] = data.outputTokens;
292
+ }
293
+ if (data.finishReason) {
294
+ attributes["gen_ai.response.finish_reason"] = data.finishReason;
295
+ }
296
+ if (data.temperature !== void 0) {
297
+ attributes["gen_ai.request.temperature"] = data.temperature;
298
+ }
299
+ if (data.maxTokens !== void 0) {
300
+ attributes["gen_ai.request.max_tokens"] = data.maxTokens;
301
+ }
302
+ if (data.topP !== void 0) {
303
+ attributes["gen_ai.request.top_p"] = data.topP;
304
+ }
305
+ if (data.topK !== void 0) {
306
+ attributes["gen_ai.request.top_k"] = data.topK;
307
+ }
308
+ return attributes;
309
+ }
310
+ function buildProviderRequestAttributes(data) {
311
+ return buildOtelAttributes({
312
+ provider: data.provider,
313
+ model: data.model,
314
+ temperature: data.config?.temperature,
315
+ maxTokens: data.config?.maxTokens,
316
+ topP: data.config?.topP,
317
+ topK: data.config?.topK
318
+ });
319
+ }
320
+ function buildProviderResponseAttributes(data) {
321
+ return buildOtelAttributes({
322
+ provider: data.provider,
323
+ model: data.model,
324
+ modelUsed: data.modelUsed,
325
+ inputTokens: data.inputTokens,
326
+ outputTokens: data.outputTokens,
327
+ finishReason: data.finishReason
328
+ });
329
+ }
330
+ function buildStepAttributes(data) {
331
+ return buildOtelAttributes({
332
+ stepType: data.stepType,
333
+ provider: data.provider,
334
+ model: data.model,
335
+ modelUsed: data.modelUsed,
336
+ inputTokens: data.inputTokens,
337
+ outputTokens: data.outputTokens,
338
+ finishReason: data.finishReason,
339
+ temperature: data.config?.temperature,
340
+ maxTokens: data.config?.maxTokens,
341
+ topP: data.config?.topP,
342
+ topK: data.config?.topK
343
+ });
344
+ }
345
+ var FINISH_REASON_MAPPING = {
346
+ // OpenAI
347
+ stop: "stop",
348
+ length: "length",
349
+ content_filter: "content_filter",
350
+ tool_calls: "tool_calls",
351
+ function_call: "function_call",
352
+ // Anthropic
353
+ end_turn: "stop",
354
+ max_tokens: "length",
355
+ stop_sequence: "stop",
356
+ // Google
357
+ STOP: "stop",
358
+ MAX_TOKENS: "length",
359
+ SAFETY: "content_filter",
360
+ RECITATION: "content_filter",
361
+ // Generic
362
+ complete: "stop",
363
+ truncated: "length",
364
+ filtered: "content_filter"
365
+ };
366
+ function normalizeFinishReason(reason) {
367
+ if (!reason) {
368
+ return void 0;
369
+ }
370
+ return FINISH_REASON_MAPPING[reason] ?? reason;
371
+ }
372
+ function addStandardSpanAttributes(attributes, data) {
373
+ if (data.spanKind) {
374
+ attributes["span.kind"] = data.spanKind;
375
+ }
376
+ if (data.serviceName) {
377
+ attributes["service.name"] = data.serviceName;
378
+ }
379
+ if (data.serviceVersion) {
380
+ attributes["service.version"] = data.serviceVersion;
381
+ }
382
+ }
383
+ function buildFullOtelContext(data) {
384
+ const attributes = buildOtelAttributes({
385
+ stepType: data.stepType,
386
+ provider: data.provider,
387
+ model: data.model,
388
+ modelUsed: data.modelUsed,
389
+ inputTokens: data.inputTokens,
390
+ outputTokens: data.outputTokens,
391
+ finishReason: data.finishReason,
392
+ temperature: data.config?.temperature,
393
+ maxTokens: data.config?.maxTokens,
394
+ topP: data.config?.topP,
395
+ topK: data.config?.topK
396
+ });
397
+ addStandardSpanAttributes(attributes, {
398
+ spanKind: data.spanKind,
399
+ serviceName: data.serviceName,
400
+ serviceVersion: data.serviceVersion
401
+ });
402
+ return attributes;
403
+ }
404
+
405
+ // src/observability/defaults.ts
406
+ var DEFAULT_OBSERVABILITY_CONFIG = {
407
+ enabled: true,
408
+ samplingRate: 1,
409
+ samplingStrategy: "random",
410
+ asyncHooks: true,
411
+ fireAndForget: false,
412
+ hookTimeout: 5e3,
413
+ failOnHookError: false
414
+ };
415
+ function mergeConfig(userConfig) {
416
+ if (!userConfig) {
417
+ return { ...DEFAULT_OBSERVABILITY_CONFIG };
418
+ }
419
+ return {
420
+ ...DEFAULT_OBSERVABILITY_CONFIG,
421
+ ...userConfig
422
+ };
423
+ }
424
+ function shouldSample(config) {
425
+ if (config.samplingRate === void 0 || config.samplingRate === 1) {
426
+ return true;
427
+ }
428
+ if (config.samplingRate === 0) {
429
+ return false;
430
+ }
431
+ if (config.samplingStrategy === "custom" && config.customSampler) {
432
+ return true;
433
+ }
434
+ return Math.random() < config.samplingRate;
435
+ }
436
+ function validateConfig(config) {
437
+ const warnings = [];
438
+ const errors = [];
439
+ if (config.samplingRate !== void 0) {
440
+ if (config.samplingRate < 0 || config.samplingRate > 1) {
441
+ errors.push("samplingRate must be between 0.0 and 1.0");
442
+ }
443
+ }
444
+ if (config.hookTimeout !== void 0) {
445
+ if (config.hookTimeout <= 0) {
446
+ errors.push("hookTimeout must be positive");
447
+ }
448
+ if (config.hookTimeout > 6e4) {
449
+ warnings.push("hookTimeout > 60s may cause long execution delays");
450
+ }
451
+ }
452
+ if (config.samplingStrategy === "custom" && !config.customSampler) {
453
+ errors.push('customSampler required when samplingStrategy is "custom"');
454
+ }
455
+ if (config.traceContext) {
456
+ const { traceId, parentSpanId } = config.traceContext;
457
+ if (!/^[0-9a-f]{32}$/.test(traceId)) {
458
+ errors.push("traceContext.traceId must be 32 lowercase hex characters");
459
+ }
460
+ if (!/^[0-9a-f]{16}$/.test(parentSpanId)) {
461
+ errors.push("traceContext.parentSpanId must be 16 lowercase hex characters");
462
+ }
463
+ if (traceId === "00000000000000000000000000000000") {
464
+ errors.push("traceContext.traceId cannot be all zeros");
465
+ }
466
+ if (parentSpanId === "0000000000000000") {
467
+ errors.push("traceContext.parentSpanId cannot be all zeros");
468
+ }
469
+ }
470
+ if (config.asyncHooks === false && config.hookTimeout !== void 0) {
471
+ warnings.push("hookTimeout has no effect when asyncHooks is false");
472
+ }
473
+ if (config.fireAndForget === true && config.hookTimeout !== void 0) {
474
+ warnings.push("hookTimeout has no effect when fireAndForget is true");
475
+ }
476
+ if (config.enabled === false && Object.keys(config).length > 1) {
477
+ warnings.push("Observability is disabled, other config options have no effect");
478
+ }
479
+ return {
480
+ valid: errors.length === 0,
481
+ warnings,
482
+ errors
483
+ };
484
+ }
485
+ function getObservabilityVersion(config) {
486
+ return config?.observabilityVersion ?? "1.0.0";
487
+ }
488
+ function isObservabilityDisabled(config) {
489
+ return config.enabled === false || config.samplingRate === 0;
490
+ }
491
+ function hasAnyHooks(config) {
492
+ return !!(config.onFlowStart || config.onFlowEnd || config.onFlowError || config.onStepStart || config.onStepEnd || config.onStepError || config.onConsensusStart || config.onConsensusRunRetry || config.onConsensusRunComplete || config.onConsensusComplete || config.onBatchStart || config.onBatchItemStart || config.onBatchItemEnd || config.onBatchEnd || config.onProviderRequest || config.onProviderResponse || config.onProviderRetry || config.onCircuitBreakerTriggered || config.onLog);
493
+ }
494
+ function createMinimalConfig(overrides) {
495
+ return {
496
+ enabled: true,
497
+ samplingRate: 1,
498
+ asyncHooks: false,
499
+ // Faster for tests
500
+ fireAndForget: false,
501
+ hookTimeout: 1e3,
502
+ // Shorter for tests
503
+ failOnHookError: false,
504
+ ...overrides
505
+ };
506
+ }
507
+
508
+ // src/observability/logger.ts
509
+ var Logger = class {
510
+ options;
511
+ constructor(options = {}) {
512
+ this.options = options;
513
+ }
514
+ /**
515
+ * Log a debug message
516
+ */
517
+ debug(message, data) {
518
+ this.log("debug", message, data);
519
+ }
520
+ /**
521
+ * Log an info message
522
+ */
523
+ info(message, data) {
524
+ this.log("info", message, data);
525
+ }
526
+ /**
527
+ * Log a warning message
528
+ */
529
+ warn(message, data) {
530
+ this.log("warn", message, data);
531
+ }
532
+ /**
533
+ * Log an error message
534
+ */
535
+ error(message, error, data) {
536
+ this.log("error", message, data, error instanceof Error ? error : void 0);
537
+ }
538
+ /**
539
+ * Internal log method that sends to onLog hook
540
+ */
541
+ log(level, message, data, error) {
542
+ const timestamp = Date.now();
543
+ const consoleMethod = level === "error" ? console.error : level === "warn" ? console.warn : console.log;
544
+ if (error) {
545
+ consoleMethod(`[${level.toUpperCase()}] ${message}`, data, error);
546
+ } else if (data !== void 0) {
547
+ consoleMethod(`[${level.toUpperCase()}] ${message}`, data);
548
+ } else {
549
+ consoleMethod(`[${level.toUpperCase()}] ${message}`);
550
+ }
551
+ if (this.options.observability?.onLog && this.options.traceContext) {
552
+ const combinedMetadata = {
553
+ ...this.options.metadata || {}
554
+ };
555
+ if (data !== void 0) {
556
+ combinedMetadata.data = data;
557
+ }
558
+ if (error) {
559
+ combinedMetadata.error = {
560
+ name: error.name,
561
+ message: error.message,
562
+ stack: error.stack
563
+ };
564
+ }
565
+ const logContext = {
566
+ flowId: this.options.flowId ?? "unknown",
567
+ executionId: this.options.executionId ?? "unknown",
568
+ stepId: this.options.stepId,
569
+ timestamp,
570
+ level,
571
+ message,
572
+ metadata: combinedMetadata,
573
+ traceContext: this.options.traceContext
574
+ };
575
+ executeHook(this.options.observability.onLog, {
576
+ hookName: "onLog",
577
+ config: this.options.observability,
578
+ context: logContext,
579
+ fireAndForget: true
580
+ // Special handling for onLog
581
+ }).catch(() => {
582
+ });
583
+ }
584
+ }
585
+ };
586
+ function createLogger(options = {}) {
587
+ return new Logger(options);
588
+ }
589
+
590
+ // src/observability/index.ts
591
+ var OBSERVABILITY_VERSION = "1.0.0";
592
+ export {
593
+ DEFAULT_OBSERVABILITY_CONFIG,
594
+ FINISH_REASON_MAPPING,
595
+ Logger,
596
+ OBSERVABILITY_VERSION,
597
+ TRACE_FLAGS_NOT_SAMPLED,
598
+ TRACE_FLAGS_SAMPLED,
599
+ TraceContextManager,
600
+ addStandardSpanAttributes,
601
+ buildFullOtelContext,
602
+ buildOtelAttributes,
603
+ buildProviderRequestAttributes,
604
+ buildProviderResponseAttributes,
605
+ buildStepAttributes,
606
+ createChildSpanContext,
607
+ createLogger,
608
+ createMinimalConfig,
609
+ createTraceContext,
610
+ executeHook,
611
+ executeHooksSerial,
612
+ formatTraceparent,
613
+ formatTracestate,
614
+ generateExecutionId,
615
+ generateSpanId,
616
+ generateTraceId,
617
+ getObservabilityVersion,
618
+ hasAnyHooks,
619
+ isObservabilityDisabled,
620
+ isObservabilityEnabled,
621
+ isTraceSampled,
622
+ isValidSpanId,
623
+ isValidTraceId,
624
+ mergeConfig,
625
+ normalizeFinishReason,
626
+ parseTraceparent,
627
+ shouldSample,
628
+ validateConfig
629
+ };
630
+ //# sourceMappingURL=index.js.map