/** * otelObservability — OpenTelemetry distributed-tracing adapter. * * Ships every agentfootprint event as OpenTelemetry spans + span events * via a consumer-supplied OTel API, following the OpenTelemetry **GenAI * semantic conventions** (`gen_ai.*` attribute namespace) plus * agentfootprint-specific explainability attributes (`agentfootprint.*`). * Same hierarchical mapping as the X-Ray adapter, but the destination is * whichever OTel-compat backend the consumer's SDK exports to: * * - **Honeycomb** (OTLP/HTTP) * - **Grafana Cloud / Tempo / Mimir** (OTLP) * - **AWS Distro for OTel** → AWS X-Ray (alternative to xrayObservability) * - **Datadog APM** (OTLP endpoint) * - **Splunk Observability Cloud** (OTLP) * - **New Relic** (OTLP endpoint) * - **Lightstep / ServiceNow Cloud Observability** (OTLP) * - any custom OTel collector / processor pipeline * * Subpath: `agentfootprint/observability-providers` * Peer dep: `@opentelemetry/api` (OPTIONAL — installed only when * this adapter is used. The consumer ALSO installs the * OTel SDK + exporter of their choice — that's the BYO * contract that makes this adapter backend-agnostic.). * * **Why BYO SDK:** OTel's SDK is heavyweight and exporter-specific * (each backend has its own exporter package). Forcing a particular * exporter would defeat the "OTel is portable" guarantee. Consumers * configure the SDK + exporter once at app startup; we just speak * the typed OTel API. * * ## Event → span/attribute mapping * * agent.turn_start ↦ start root span (one trace per turn) — * `gen_ai.operation.name: 'invoke_agent'` * agent.turn_end ↦ end root span (+ turn-total `gen_ai.usage.*`) * agent.iteration_start ↦ start child span under root * agent.iteration_end ↦ end iteration span * stream.llm_start ↦ start child span (inference) — `gen_ai.*` * request attrs (`chat` operation) * stream.llm_end ↦ end llm span (+ `gen_ai.usage.*`, * `gen_ai.response.*`) * stream.tool_start ↦ start child span — `execute_tool` operation, * `gen_ai.tool.name` / `gen_ai.tool.call.id` * stream.tool_end ↦ end tool span (ERROR status + `error.type` * if errored). Correlated by toolCallId so * PARALLEL tool calls close the right span. * cost.tick ↦ setAttribute on topmost active span * error.fatal ↦ ERROR status on root + defensive unwind * context.evaluated ↦ N span events `agentfootprint.skill.routing` * — SYNTHESIZED name (one per routing entry), * not a registry-verbatim forward; all other * span events use the registry name verbatim * * ## Decisions = SPAN EVENTS, not attributes (design decision) * * Explainability signals (route decisions, skill routing, validation * rejections, permission checks, credential lifecycle) are emitted as * **span events** on the currently-active span rather than attributes: * * 1. MULTIPLICITY — an iteration span can carry several decisions * (route + N skill routings + M permission checks). Attributes are * last-write-wins and would clobber; span events accumulate. * 2. ORDERING — span events carry their own timestamps, preserving the * decision sequence inside one span. Compliance review (EU AI Act * Art. 12 record-keeping) needs the order decisions were made. * 3. ROUND-TRIP — OTLP backends (and agentThinkingUI's `fromOTLP` * ingestion) surface span events as first-class timeline entries. * * When the consumer-injected tracer's spans don't implement `addEvent` * (minimal test doubles), the adapter falls back to flattened * `${eventName}.${key}` attributes — degraded (last-write-wins) but * never silently dropped. * * ## PII discipline * * Mirrors the #9 validation contract: attribute values NEVER echo * runtime VALUES that can carry PII — * - tool args → top-level key NAMES only (`agentfootprint.tool.args.keys`) * - tool results → `typeof` only (`agentfootprint.tool.result.type`) * - validation issues → path / expected / got TYPES (bounded upstream) * - decide() evidence → rule labels, operators, thresholds (developer * constants) and the engine's redaction-aware value SUMMARIES * - userPrompt / llm content / thinking → never emitted * - error.fatal → stage + scope only (error MESSAGES can echo values) * - credential events carry no secrets by construction (registry contract) * * @example Basic — Honeycomb via OTLP * ```ts * import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node'; * import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http'; * import { BatchSpanProcessor } from '@opentelemetry/sdk-trace-base'; * import { otelObservability } from 'agentfootprint/observability-providers'; * * // Set up OTel ONCE at app startup. * const provider = new NodeTracerProvider(); * provider.addSpanProcessor(new BatchSpanProcessor(new OTLPTraceExporter({ * url: 'https://api.honeycomb.io/v1/traces', * headers: { 'x-honeycomb-team': process.env.HONEYCOMB_KEY }, * }))); * provider.register(); * * const otel = otelObservability({ * serviceName: 'my-agent', * // genAiSpanNames: true, // opt-in spec span names ('chat gpt-4', …) * }); * agent.enable.observability({ strategy: otel }); * // Optional — operator-level decide()/select() evidence as span events: * // Agent.create({...}).recorder(otel.decisionEvidenceRecorder()) * ``` * * @example Test injection * ```ts * otelObservability({ * serviceName: 'test', * tracer: mockTracer, // anything matching the OTel Tracer interface * }); * ``` */ import type { FlowDecisionEvent, FlowSelectedEvent } from 'footprintjs'; import type { ObservabilityStrategy } from '../../strategies/types.js'; export interface OtelObservabilityOptions { /** Service name on every emitted span. Surfaces in your OTel * backend's service map. Required. */ readonly serviceName: string; /** OTel Tracer to use. Defaults to * `trace.getTracer('agentfootprint', AGENTFOOTPRINT_VERSION)` * (where `trace` is the lazy-imported `@opentelemetry/api`). */ readonly tracer?: OtelTracerLike; /** 0..1 — sample rate for turn-level spans. Default `1.0`. * Sampling decisions are normally an OTel SDK concern (via * `Sampler`); this option is a per-strategy override for cases * where the consumer wants agentfootprint to drop spans BEFORE * they reach the SDK (e.g., aggressive cost control). */ readonly sampleRate?: number; /** * Opt-in OTel GenAI semconv SPAN NAMES (default `false`): * * root → `invoke_agent {serviceName}` (was `{serviceName}`) * llm → `chat {model}` (was `llm`) * tool → `execute_tool {toolName}` (was `tool:{toolName}`) * * Off by default because existing consumers' dashboards / alerts key * on the legacy span names — renames would break them. All `gen_ai.*` * ATTRIBUTES are emitted regardless of this flag (purely additive), * so semconv-aware backends can already group by * `gen_ai.operation.name` with the flag off. */ readonly genAiSpanNames?: boolean; /** * Explainability span events (default `true`): route decisions, skill * routing provenance, validation rejections, permission decisions, * credential lifecycle. Set `false` to emit only the span tree + * `gen_ai.*` attributes (e.g., aggressive per-byte vendor billing). */ readonly explainability?: boolean; } /** Attribute value union we emit. Matches OTel's `AttributeValue` * subset: primitives + homogeneous string arrays * (`gen_ai.response.finish_reasons`, issue lists, …). */ export type OtelAttributeValue = string | number | boolean | readonly string[]; /** Subset of `@opentelemetry/api`'s `Tracer` we depend on. */ export interface OtelTracerLike { startSpan(name: string, options?: OtelSpanOptions, context?: unknown): OtelSpanLike; } /** Subset of `@opentelemetry/api`'s `SpanOptions`. */ export interface OtelSpanOptions { attributes?: Record; startTime?: number; kind?: number; } /** Subset of `@opentelemetry/api`'s `Span` we depend on. */ export interface OtelSpanLike { setAttribute(key: string, value: OtelAttributeValue): unknown; setStatus(status: { code: number; message?: string; }): unknown; end(endTime?: number): void; spanContext(): { traceId: string; spanId: string; traceFlags: number; }; /** OTel `Span.addEvent` — optional in the duck-typed surface so * minimal test doubles still satisfy the interface. Explainability * signals degrade to flattened attributes when absent. */ addEvent?(name: string, attributes?: Record): unknown; } /** * footprintjs CombinedRecorder (FlowRecorder channel) that forwards * decide()/select() operator-level evidence into the paired * otelObservability strategy as span events. Attach via * `Agent.create({...}).recorder(...)` or * `executor.attachCombinedRecorder(...)`. */ export interface OtelDecisionEvidenceRecorder { readonly id: string; onDecision(event: FlowDecisionEvent): void; onSelected(event: FlowSelectedEvent): void; } /** Return type of {@link otelObservability} — the base * ObservabilityStrategy plus the decide()/select() evidence bridge. */ export interface OtelObservabilityStrategy extends ObservabilityStrategy { /** * Build the decide()/select() evidence bridge for this strategy. * * Operator-level decision evidence (which rule fired, the * `key op threshold → actual` conditions) travels on footprintjs's * FlowRecorder channel (`onDecision` / `onSelected`) — it never * reaches the typed event dispatcher, so the strategy alone can't * see it. This recorder is the bridge (same pattern as the #5 * causal-evidence bridge in `memory/causal/evidenceRecorder.ts`). * * Decisions WITHOUT structured evidence are skipped — they already * arrive via the `agent.route_decided` / `composition.route_decided` * typed events, so forwarding them here would double-report. * * @remarks PII: attaching this recorder EXPORTS bounded actual scope * values to your OTel collector — each condition renders as * `key op threshold → actualSummary (bool)`, where `actualSummary` is * the engine's redaction-aware ≤80-char value summary (e.g. * `creditScore gt 700 → 750 (true)`). Keys covered by a footprintjs * `RedactionPolicy` render `[REDACTED]`; everything else leaves the * process. For compliance record-keeping that disclosure is usually * the point — but treat the collector as PII-bearing, or redact the * relevant keys upstream, before attaching. * * @remarks Attach ONCE per executor. Every instance carries the * well-known id `'otel-decision-evidence'`, so re-attaching is * idempotent-by-ID (the replacement prevents double-reported span * events); instances from the same strategy share its turn state by * design. */ decisionEvidenceRecorder(): OtelDecisionEvidenceRecorder; } export declare function otelObservability(opts: OtelObservabilityOptions): OtelObservabilityStrategy; //# sourceMappingURL=otel.d.ts.map