/** * Telemetry Provider — BYOT (Bring Your Own Telemetry) abstraction. * * Defines a minimal interface over distributed tracing providers such as * OpenTelemetry, Datadog APM, Honeycomb, etc. Network-AI core never imports * a concrete telemetry SDK — only this interface — preserving the zero- * dependency BYOC design. * * ## Wiring into adapter lifecycle hooks * * ```typescript * import { createOtelHooks, CapturingTelemetryProvider } from 'network-ai'; * import { AdapterHookManager } from 'network-ai'; * * const provider = new CapturingTelemetryProvider(); // or your own impl * const hookManager = new AdapterHookManager(); * createOtelHooks(provider).forEach(h => hookManager.register(h)); * ``` * * ## Implementing for OpenTelemetry * * ```typescript * import { trace, SpanStatusCode } from '@opentelemetry/api'; * import type { ITelemetryProvider, SpanAttributes } from 'network-ai'; * * class OtelProvider implements ITelemetryProvider { * private tracer = trace.getTracer('network-ai'); * private spans = new Map(); * * startSpan(name: string, attrs: SpanAttributes = {}): string { * const span = this.tracer.startSpan(name, { attributes: attrs as Attributes }); * const id = `span_${Date.now()}`; * this.spans.set(id, span); * return id; * } * endSpan(id: string, status: 'ok' | 'error'): void { * const span = this.spans.get(id); * if (!span) return; * span.setStatus({ code: status === 'ok' ? SpanStatusCode.OK : SpanStatusCode.ERROR }); * span.end(); * this.spans.delete(id); * } * recordEvent(id: string, name: string, attrs: SpanAttributes = {}): void { * this.spans.get(id)?.addEvent(name, attrs as Attributes); * } * } * ``` */ import type { ExecutionHook } from './adapter-hooks'; /** * Flat attribute bag for span and event annotations. * Values must be serialisable primitives for cross-backend compatibility. */ export interface SpanAttributes { [key: string]: string | number | boolean | undefined; } /** * A span captured by `CapturingTelemetryProvider` — use in tests to assert * on emitted traces. */ export interface CapturedSpan { spanId: string; name: string; attributes: SpanAttributes; startedAt: number; endedAt?: number; status?: 'ok' | 'error'; events: Array<{ name: string; attributes: SpanAttributes; ts: number; }>; } /** * Minimal telemetry abstraction. Implement this interface to plug any * tracing backend into Network-AI without adding runtime dependencies. * * **Contract:** all methods are synchronous or fire-and-forget. * Implementations **must not throw** — catch and handle internally. */ export interface ITelemetryProvider { /** * Start a new span. * @param name Human-readable operation name (e.g. `'adapter.execute'`). * @param attributes Initial span attributes. * @returns Opaque spanId — pass to `endSpan` / `recordEvent`. */ startSpan(name: string, attributes?: SpanAttributes): string; /** * End a span with a final status. * @param spanId Value returned by `startSpan`. * @param status `'ok'` for success, `'error'` for failure. * @param attributes Additional attributes to attach at close time. */ endSpan(spanId: string, status: 'ok' | 'error', attributes?: SpanAttributes): void; /** * Record a point-in-time event within an active span. * @param spanId Value returned by `startSpan`. * @param name Event name (e.g. `'blackboard.commit'`). * @param attributes Event annotations. */ recordEvent(spanId: string, name: string, attributes?: SpanAttributes): void; } /** * No-op implementation. Used as the default when no telemetry provider is * supplied so the instrumentation path compiles to a handful of dead calls * that the JIT eliminates. */ export declare class NullTelemetryProvider implements ITelemetryProvider { /** @inheritdoc */ startSpan(_name: string, _attributes?: SpanAttributes): string; /** @inheritdoc */ endSpan(_spanId: string, _status: 'ok' | 'error', _attributes?: SpanAttributes): void; /** @inheritdoc */ recordEvent(_spanId: string, _name: string, _attributes?: SpanAttributes): void; } /** * In-memory provider that stores every span and event for test assertions. * * @example * ```typescript * const provider = new CapturingTelemetryProvider(); * createOtelHooks(provider).forEach(h => hookManager.register(h)); * * await registry.executeAgent('agent:foo', payload, ctx); * * const span = provider.spans.find(s => s.name === 'adapter.execute'); * expect(span?.status).toBe('ok'); * ``` */ export declare class CapturingTelemetryProvider implements ITelemetryProvider { /** All spans created since construction or last `clear()`. */ readonly spans: CapturedSpan[]; private counter; /** @inheritdoc */ startSpan(name: string, attributes?: SpanAttributes): string; /** @inheritdoc */ endSpan(spanId: string, status: 'ok' | 'error', attributes?: SpanAttributes): void; /** @inheritdoc */ recordEvent(spanId: string, name: string, attributes?: SpanAttributes): void; /** Clear all captured data and reset span counter. */ clear(): void; } /** * Create a set of `ExecutionHook` objects that emit traces to `provider`. * * Register the returned hooks with an `AdapterHookManager`: * ```typescript * createOtelHooks(provider).forEach(h => hookManager.register(h)); * ``` * * Three hooks are created — one per `HookPhase`: * - `otel:beforeExecute` — calls `provider.startSpan('adapter.execute', {...})` * - `otel:afterExecute` — calls `provider.endSpan(spanId, 'ok')` * - `otel:onError` — calls `provider.endSpan(spanId, 'error')` * * Each hook has `priority: 100` so it runs before most user-defined hooks. * The spanId is stored in `ctx.metadata._otelSpanId` for downstream hooks * that wish to add their own `recordEvent` calls. * * **Permission check semantics:** `beforeExecute` fires once when execution * begins — not per streaming chunk — matching the documented "once at start" * semantics of `StreamingBaseAdapter`. */ export declare function createOtelHooks(provider: ITelemetryProvider): ExecutionHook[]; /** Span/event name emitted for a classifier refusal. */ export declare const REFUSAL_EVENT = "model.refusal"; /** Span/event name emitted when a fallback model serves a turn. */ export declare const FALLBACK_SERVED_EVENT = "model.fallback_served"; /** Argument to {@link RefusalTelemetry.recordRefusal}. */ export interface RefusalEventInfo { model: string; category: string | null; agentId?: string; } /** Argument to {@link RefusalTelemetry.recordFallbackServed}. */ export interface FallbackServedInfo { requestedModel: string; servedModel: string; agentId?: string; } /** Point-in-time counters from {@link RefusalTelemetry.snapshot}. */ export interface RefusalSnapshot { refusals: number; fallbackServed: number; /** Refusals never served by a fallback — the gap to alert on. */ unservedRefusals: number; byCategory: Record; byModel: Record; } /** * Refusal/fallback observability. * * A classifier refusal is a successful **HTTP 200**, so monitoring built on * error rates or 5xx responses never sees it. `RefusalTelemetry` records each * refusal and each fallback-served response as discrete **non-error** signals, * keeps counters, and exposes the gap between them (`unservedRefusalCount`) so * you can alert when refusals are not being served by a fallback. * * It satisfies the `RefusalTelemetrySink` contract consumed by * {@link ../lib/model-gateway!GovernedModelGateway}. Pass an * {@link ITelemetryProvider} to also emit spans to your tracing backend. * * @example * ```typescript * const refusals = new RefusalTelemetry(new CapturingTelemetryProvider()); * const gateway = new GovernedModelGateway({ caller, primaryModel, fallbackModels, telemetry: refusals }); * // ...later * if (refusals.unservedRefusalCount > 0) alert('refusals are reaching users'); * ``` */ export declare class RefusalTelemetry { private readonly provider; private _refusals; private _fallbackServed; private readonly _byCategory; private readonly _byModel; constructor(provider?: ITelemetryProvider); /** Record a classifier refusal (counted as a signal, never as an error). */ recordRefusal(info: RefusalEventInfo): void; /** Record that a fallback model served a turn the primary declined. */ recordFallbackServed(info: FallbackServedInfo): void; /** Total refusals observed. */ get refusalCount(): number; /** Total fallback-served responses observed. */ get fallbackServedCount(): number; /** Refusals that were never served by a fallback (the gap to alert on). */ get unservedRefusalCount(): number; /** Refusal counts keyed by classifier category. */ byCategory(): Record; /** A full counter snapshot. */ snapshot(): RefusalSnapshot; /** Reset all counters. */ reset(): void; /** Emit a discrete non-error span for a refusal/fallback signal. @internal */ private emit; } //# sourceMappingURL=telemetry-provider.d.ts.map