/**
 * 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/observe`
 * 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.
 *   a bracket carrying        ↦  NO span: the call never executed (9.113.0).
 *   `notDispatched`              Its tool_start adds ONE span event
 *                                `agentfootprint.tool.not_dispatched` to the
 *                                active span — SYNTHESIZED name — with
 *                                `gen_ai.tool.name`, `gen_ai.tool.call.id` and
 *                                the paused call's id and name; its tool_end
 *                                closes nothing. KNOWN LIMIT: only on a leg
 *                                this adapter traces, and a trace opens on
 *                                `agent.turn_start` only. A resumed leg emits
 *                                none and carries a new `meta.runId`, so none
 *                                of it is traced today — and a settled bracket
 *                                only ever rides a resumed leg, so no run
 *                                records this span event yet
 *   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; apart from
 *                                it, `agentfootprint.tool.not_dispatched`
 *                                above and `agentfootprint.integrity.check`
 *                                below, span events use the registry name
 *                                verbatim
 *
 * ## What an evaluation tool reads
 *
 *   meta.sessionId            ↦  `gen_ai.conversation.id` on the agent, chat
 *                                and execute_tool spans of a session-bound run
 *                                — the SHA-256 DIGEST of the session id by
 *                                default (`conversationId: 'raw'` for the id
 *                                itself, safe only behind a verifying door);
 *                                never the runId in its place
 *   eval.score                ↦  `gen_ai.evaluation.result` — the spec's
 *                                evaluation event, carried as a SPAN event
 *                                (the spec's reference emitter logs it; this
 *                                adapter speaks the Tracer API only). On the
 *                                span it evaluates while the turn is open; on
 *                                a short `agentfootprint.evaluation` span
 *                                under the turn once it closed — and PARENTED
 *                                only when the score's own meta places it in
 *                                that run's session. Otherwise (every
 *                                `agent.emit` after the run) it rides an
 *                                `agentfootprint.evaluation` span of its own,
 *                                unparented, naming the ref. `explanation`
 *                                only under `captureContent`; `evidence` never
 *   tools.absent /            ↦  span event on the call's own span: a COUNT
 *   tools.coverage_declared      per list (checked / not_checked /
 *                                cannot_cover), the suggested tool's name only
 *                                when this strategy has seen that tool run —
 *                                never a sentence the tool author wrote
 *   agent.evidence_checked    ↦  span event: verdict + unsupported COUNT; the
 *                                values only under `captureContent`
 *   findings.standing         ↦  span event per row (ids, enums, counts; an
 *                                unknown id as a flag only), at most
 *                                {@link MAX_STANDING_EVENTS_PER_TURN} per turn
 *                                then one `agentfootprint.findings.standing_overflow`
 *                                (SYNTHESIZED); a count per standing on the
 *                                agent span when the turn closes
 *   integrity.disposition     ↦  filed AFTER turn_end. By default exported
 *                                only when a row is actionable (findings, or
 *                                wiring rot): a short
 *                                `agentfootprint.integrity.disposition` span
 *                                under the turn, one
 *                                `agentfootprint.integrity.check` span event
 *                                (SYNTHESIZED) per actionable row.
 *                                `checkAccounting: 'all'` for every row
 *   (captureToolContent only) ↦  `gen_ai.tool.call.arguments` /
 *                                `gen_ai.tool.call.result` on the tool span of
 *                                a call that ran and returned — the proposal
 *                                snapshot with every rule-changed key
 *                                withheld, and what the model READ; objects
 *                                only; omitted over `maxContentChars`; never a
 *                                code runner's program
 *
 * The structure events above ride the `explainability` switch; the
 * evaluation event does not (it is a `gen_ai.*` signal, like the attributes).
 * A span started after its parent ended (the disposition, a late score) is
 * valid OTel; most trace views draw it outside the parent's bar.
 *
 * ## 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: by default attribute values NEVER echo
 * runtime VALUES that can carry PII —
 *   - tool args  → top-level key NAMES only (`agentfootprint.tool.args.keys`)
 *     (by default; see `captureToolContent`)
 *   - tool results → `typeof` only (`agentfootprint.tool.result.type`)
 *     (by default; see `captureToolContent`)
 *   - 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 (by default; see
 *     `captureContent`)
 *   - error.fatal → stage + scope only (error MESSAGES can echo values)
 *   - credential events carry no secrets by construction (registry contract)
 *   - the session id → its SHA-256 digest (`conversationId`)
 *   - absence / coverage events → list COUNTS, and the suggested tool's NAME
 *     only when this strategy has seen that tool run; never
 *     `looked_for`, a `what` / `why`, `tryInstead` or an unregistered
 *     `tryInsteadTool.tool` — any field a tool author writes can interpolate
 *     an argument (coverage README, "What that does not close"). The EVENT
 *     never carries them, content on or off; under `captureToolContent` an
 *     `absent()` call's result (the envelope itself) does, as its
 *     `gen_ai.tool.call.result`, beside the arguments any interpolation came
 *     from
 *   - evidence verdict → the unsupported COUNT; findings → ids, enums,
 *     counts, never the model's lines (an id the model wrote that names no
 *     result is a flag, not the id); disposition → check names + counts
 *   - eval.score → metric name, score, label (a vocabulary word by contract),
 *     target words from the payload's closed vocabularies; `evidence` never
 *   - EVERY string attribute outside the content switches is capped at 256
 *     characters and every list at 20 items plus an overflow marker, in one
 *     funnel (`capAttrs`) —
 *     whoever chose the value, a caller-sized session id or a model-written
 *     id cannot choose the size of an attribute. A capped value is cut with
 *     `…` (never mid surrogate pair): a cut id is no longer the id. Span
 *     NAMES carry a tool or model name at most 64 characters
 *     (`spanNamePart`), since a tool name is the model's text when it names
 *     no registered tool
 *   - the session digest hashes the id's UTF-8 bytes, where every lone
 *     surrogate becomes U+FFFD: two ids differing only in a lone surrogate
 *     share a digest. Library-minted ids are UUIDs; documented, not handled
 *
 * Two content switches, each off by default: `captureContent` (the turn's
 * prompt and answer, the evidence gate's unsupported values, an evaluation's
 * explanation) and `captureToolContent` (tool arguments and results). The
 * adapter redacts nothing of its own: it exports what the events carry. A
 * call's arguments are the proposal `tool_start` carried, SERIALIZED THEN
 * (the engine hands the same object to the tool, which may write into it),
 * with every key `tool_end` names in `changedArgKeys` — a rule rewrote it,
 * or the tool's own `redact` policy hides it — withheld as `'REDACTED'`; the
 * result is what the model read (`modelResult`, after the `onToolResult`
 * scrub and the removal of the record-only coverage fields `short` / `kind`).
 * Never a raw value a rule replaced, never a value a rule added.
 *
 * @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/observe';
 *
 * // 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({...}).watch(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 { AgentfootprintEvent } from '../../events/registry.js';
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(scopeName)` — where
     *  `trace` is the lazy-imported `@opentelemetry/api` and `scopeName` is the
     *  option below. */
    readonly tracer?: OtelTracerLike;
    /**
     * The INSTRUMENTATION SCOPE name every span is recorded under — what a
     * backend shows as `scope.name`. Default `'agentfootprint'`.
     *
     * ── Why this is a knob and not a constant ────────────────────────────────
     * Some telemetry consumers ROUTE on this name rather than read it. AWS
     * Bedrock AgentCore Evaluations is the case that forced the option: it will
     * score any agent's traces, from anywhere, but only classifies spans whose
     * scope name begins `opentelemetry.instrumentation.` or
     * `openinference.instrumentation.` — everything else is skipped in silence,
     * which is indistinguishable from "your agent was fine".
     *
     * The default does NOT change, because a rename moves every existing
     * dashboard's spans out from under it. Consumers who need a routable name
     * pass one; `agentCoreEvaluationSpans()` in the AgentCore adapter is that
     * one consumer's spelling, kept in that vendor's own file.
     *
     * Ignored when `tracer` is supplied — a tracer you built already has a name.
     */
    readonly scopeName?: string;
    /**
     * Put the turn's PROMPT and ANSWER TEXT on the agent span (default `false`):
     * `gen_ai.task.input` from the user's prompt and `gen_ai.task.output` from
     * the final answer. Two more values that a person or a model WROTE ride
     * under the same switch when their events occur: the evidence gate's
     * unsupported values (`agentfootprint.agent.evidence_checked`) and an
     * evaluation's `explanation` (`gen_ai.evaluation.result`).
     *
     * It does NOT export tool arguments or results — that is
     * {@link OtelObservabilityOptions.captureToolContent}, a switch of its own:
     * what the two parties SAID and what the run's tools carried are different
     * disclosures (`hosting/sessionWire.ts` · `TranscriptMessage` draws the
     * same line).
     *
     * ── The two attribute names are pre-existing, and not the spec's ─────────
     * `gen_ai.task.input` / `gen_ai.task.output` are the names AgentCore
     * Evaluations reads; the current GenAI registry does not define them. They
     * predate the rule that this adapter uses spec names only where the spec
     * has the thing, are kept for the consumers that read them, and are not
     * extended (follow-up: the spec's `gen_ai.input.messages` /
     * `gen_ai.output.messages` shape, CHANGELOG).
     *
     * ── Off by default, deliberately ─────────────────────────────────────────
     * This adapter's standing rule is that content does not ride spans — the
     * snapshot / audit-log channel carries it under the consumer's own
     * redaction policy. Turning this on EXPORTS raw prompt and answer text to
     * whatever backend the tracer points at, where it is typically retained and
     * searchable, and no redaction of ours stands between the two.
     *
     * ── Why it exists ────────────────────────────────────────────────────────
     * Trace-scoring services read the answer they are grading off the span.
     * AgentCore Evaluations reads exactly these two attributes (after an event
     * body it only emits in its own runtime), so a scorer sees an empty turn
     * without them. Enable it when a consumer's whole purpose is to read the
     * content, and not to make dashboards prettier.
     *
     * ── What it is NOT ───────────────────────────────────────────────────────
     * Per-inference message arrays (`gen_ai.input.messages` /
     * `gen_ai.output.messages`) are NOT emitted: this adapter's `llm_start` /
     * `llm_end` events carry model, usage and stop reason, never the messages,
     * so there is nothing truthful to put there. Turn-level input and output,
     * the gate's unsupported values and an evaluation's explanation are what we
     * have and all this claims.
     */
    readonly captureContent?: boolean;
    /**
     * Put each executed tool call's ARGUMENTS and RESULT on its `execute_tool`
     * span (default `false`) — the GenAI spec's Opt-In
     * `gen_ai.tool.call.arguments` / `gen_ai.tool.call.result`.
     *
     * ── What is exported: the MODEL's view, never more ───────────────────────
     * Only for a call that RAN and RETURNED — a call that failed, or that a
     * rule refused, exports no content (the spec defines the result "if
     * execution was successful").
     * Arguments: the proposal `tool_start` carried, serialized AT `tool_start`
     * (the tool receives that same object and may write into it — a request's
     * auth header must never ride out as its "arguments"). Every key the
     * dispatch path names in `changedArgKeys` — an `onToolCall` link set,
     * rewrote or removed it, or the tool's own redaction hides it
     * (`flowchartAsTool({ redact })` / `runbookAsTool({ redact })`) — reads
     * `'REDACTED'`: the value the tool ran with is withheld, since a link can
     * add a value (a server-side key) the model never saw, and a scrubbed
     * original must not leave either. A `wants` tool shows the refs it named,
     * never the artifact data they resolved to. A CODE RUNNER's arguments are
     * the generated program and are withheld
     * (`agentfootprint.tool.args.withheld`); the library's law is that the
     * program never reaches an exporter (`events/payloads.ts` ·
     * `ToolsCodeRunPayload`). Recognized by the `tools.code_run` event, which
     * only `codeRunnerTool` files (under the name the model called, so a
     * re-exposed runner is covered): any OTHER code-executing tool — an MCP
     * code interpreter, a hand-written `eval` tool — is not recognized, and
     * its arguments export like any tool's.
     * Result: what the model read — after every `onToolResult` rewrite (a
     * scrub the model saw is the scrub the trace gets; a result a link denied
     * exports the refusal the model read), the placement ticket, the
     * `maxToolResultChars` cut. These come off the batch path's events, so a
     * resumed leg — which this adapter does not trace — is not covered.
     *
     * ── The spec's object schema ─────────────────────────────────────────────
     * Both attributes are objects in the spec; a span carries them as JSON
     * strings (OTel JS span attributes hold no objects). A value that is, or is
     * text that parses to, a plain object rides the spec attribute. Anything
     * else — plain text, a number, an array, the string `"123"` — rides this
     * adapter's own `agentfootprint.tool.args.content` /
     * `agentfootprint.tool.result.content`, verbatim for text and JSON for the
     * rest, so a string is never exported as a number.
     *
     * ── Size: omitted, never cut ─────────────────────────────────────────────
     * A value over {@link OtelObservabilityOptions.maxContentChars} is not
     * exported at all; `agentfootprint.tool.args.omitted_chars` /
     * `agentfootprint.tool.result.omitted_chars` states its size. A cut JSON
     * value reads as complete data, and one oversized attribute can fail the
     * export batch it rides in — taking other runs' spans with it.
     *
     * ── What no switch can scrub ─────────────────────────────────────────────
     * A tool whose result echoes a credential (a custom kind with an
     * enumerable secret, or `cred.toHeaders()` returned as data) exports it:
     * the built-in kinds hide their secret from `JSON.stringify`
     * (`identity/kinds.ts` · `hideSecrets`), a hand-built object does not.
     * Turning this on EXPORTS the run's tool traffic — raw records, whatever a
     * tool returns — to the tracer's backend.
     */
    readonly captureToolContent?: boolean;
    /**
     * The largest CONTENT value exported as one attribute, in characters
     * (default `65536`): a tool call's arguments or result, an evaluation's
     * explanation. Over it the value is OMITTED and its size stated — never
     * truncated. Applies to `gen_ai.task.input` / `gen_ai.task.output` too
     * (`agentfootprint.task.<input|output>.omitted_chars`): a hosted caller
     * chooses the prompt's size, and one oversized attribute can fail the
     * export batch it rides in.
     */
    readonly maxContentChars?: number;
    /**
     * How a session-bound run's `gen_ai.conversation.id` is written (default
     * `'digest'`).
     *
     *   - `'digest'` — the lowercase hex SHA-256 of the session id's UTF-8
     *     bytes: 64 characters, whatever the caller sent. It groups a backend's
     *     session view exactly as the raw id would, and an operator finds a
     *     known session's traces by hashing its id. The library's standing rule
     *     for a key it must be able to JOIN on but not disclose ("keyHash, never
     *     the key", `events/payloads.ts` · `ToolSessionPayloadBase`).
     *   - `'raw'` — the session id as issued. Safe ONLY behind a door that
     *     verifies who may open a conversation (`standingAgent({ identity })`):
     *     at a door with no verifier the session id IS the handle to the
     *     conversation — its history, its pending question, its artifacts — so
     *     exporting it hands that handle to everyone who can read the traces,
     *     for as long as they are retained.
     *
     * A digest is a join key, not a secret: a GUESSABLE session id stays
     * guessable by hashing guesses. The library's own ids are random UUIDs.
     */
    readonly conversationId?: 'digest' | 'raw';
    /**
     * Which checker-accounting rows a turn exports (default `'actionable'`).
     * Every agent run files its per-check accounting (`integrity.disposition`)
     * after the turn closes; a plain healthy run files nine rows that say
     * nothing a reader acts on.
     *
     *   - `'actionable'` — a short `agentfootprint.integrity.disposition` span
     *     under the turn ONLY when a row carries something to act on: a check
     *     that filed findings, or a registered check that filed nothing at all
     *     while the run did work (wiring rot — the rule is
     *     `integrity/disposition/ledger.ts` · `filedNothing`, the one
     *     `assertAlive` throws on). Only those rows ride as events. A healthy
     *     run exports no span, byte-identical to before this mapping existed.
     *   - `'all'` — the span on every traced run, one event per registered
     *     check.
     */
    readonly checkAccounting?: 'actionable' | 'all';
    /** 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 — and the library's own structure: declared
     * absence / coverage, the evidence verdict, findings standings (with their
     * per-standing counts on the agent span) and the checker-accounting span
     * (`agentfootprint.integrity.disposition`). Set `false` to emit only the
     * span tree + `gen_ai.*` attributes and events (e.g., aggressive per-byte
     * vendor billing) — `gen_ai.evaluation.result` still rides, like the
     * attributes.
     */
    readonly explainability?: boolean;
    /**
     * Where errors go (8.11.0).
     *
     * This adapter writes to a tracer you own, so most failures surface in your
     * OTel pipeline rather than here — but a throwing tracer, a malformed span
     * or a dispatch-layer error still has to land somewhere. Without this they
     * reach the default sink (a rate-limited `console.error`), because telemetry
     * that fails invisibly is indistinguishable from telemetry that works.
     *
     * Equivalent to assigning the strategy's `_onError` property after
     * construction, but visible at the call site.
     */
    readonly onError?: (error: Error, event?: AgentfootprintEvent) => void;
    /**
     * @internal Pre-resolved `@opentelemetry/api` module, for tests that need
     * the path where this adapter builds its OWN tracer — the only path on
     * which {@link OtelObservabilityOptions.scopeName} is observable. Skips the
     * lazy require, exactly as `_client` does on the SDK-backed adapters.
     */
    readonly _otelApi?: OtelApiModule;
}
/** 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<string, OtelAttributeValue>;
    startTime?: number;
    kind?: number;
    /** Start a new trace, ignoring any span active in the context — OTel's own
     *  `SpanOptions.root`. */
    root?: boolean;
}
/** 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<string, OtelAttributeValue>): unknown;
}
interface OtelApiModule {
    readonly trace?: {
        getTracer(name: string, version?: string): OtelTracerLike;
        setSpan(context: unknown, span: OtelSpanLike): unknown;
    };
    readonly context?: {
        active(): unknown;
        with<T>(ctx: unknown, fn: () => T): T;
    };
    readonly SpanStatusCode?: {
        OK: number;
        ERROR: number;
        UNSET: number;
    };
}
/**
 * footprintjs CombinedRecorder (FlowRecorder channel) that forwards
 * decide()/select() operator-level evidence into the paired
 * otelObservability strategy as span events. Attach via
 * `Agent.create({...}).watch(...)` 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;
export {};
