agentfootprint
Version:
The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.
567 lines • 32.8 kB
TypeScript
/**
* 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 {};
//# sourceMappingURL=otel.d.ts.map