import type { LlmGenerationEvent, LlmRunSpan, LlmStepSpan, LlmToolSpan, LlmToolSpanContext, LogRecord, OperationalCounterSample, OperationalGaugeSample } from '@cat-factory/kernel'; import type { PlatformObservability } from '@cat-factory/contracts'; /** Default OTLP resource `service.name`; overridable via `OTEL_SERVICE_NAME`. */ export declare const DEFAULT_SERVICE_NAME = "cat-factory"; /** The instrumentation scope name stamped on every emitted span/metric. */ export declare const SCOPE_NAME = "@cat-factory/observability-otel"; /** * Attribute keys, following the OpenTelemetry GenAI semantic conventions where they exist * (`gen_ai.*`) plus a small `cat_factory.*` namespace for our own dimensions. Centralised * so the two transports can never disagree on a key. What of the convention we cover, what * we deliberately omit, and where we extend it is documented per key in the README's "GenAI * semantic-convention coverage" table — that table and this object are edited together. */ export declare const ATTR: { readonly system: 'gen_ai.system'; readonly operationName: 'gen_ai.operation.name'; readonly requestModel: 'gen_ai.request.model'; readonly inputTokens: 'gen_ai.usage.input_tokens'; readonly cacheReadTokens: 'gen_ai.usage.cache_read_input_tokens'; readonly cacheWriteTokens: 'gen_ai.usage.cache_creation_input_tokens'; readonly outputTokens: 'gen_ai.usage.output_tokens'; readonly finishReasons: 'gen_ai.response.finish_reasons'; readonly tokenType: 'gen_ai.token.type'; readonly agentName: 'gen_ai.agent.name'; readonly toolName: 'gen_ai.tool.name'; readonly toolCallJobId: 'cat_factory.tool_call.job_id'; readonly toolCallSeq: 'cat_factory.tool_call.seq'; readonly toolArgsDropped: 'cat_factory.tool_call.arguments_dropped_chars'; readonly toolResultDropped: 'cat_factory.tool_call.result_dropped_chars'; readonly workspaceId: 'cat_factory.workspace_id'; readonly agentKind: 'cat_factory.agent_kind'; readonly executionId: 'cat_factory.execution_id'; readonly pipeline: 'cat_factory.pipeline'; readonly stepCount: 'cat_factory.step_count'; readonly attemptCount: 'cat_factory.attempt_count'; readonly serviceName: 'service.name'; }; /** * The `gen_ai.operation.name` values we emit. The convention's registry is open, but every * value here is one of its own: `chat` for a model call, `execute_tool` for a tool * invocation, `invoke_agent` for an agent's whole turn (our step span). A run's ROOT span * carries none of them on purpose — a pipeline run is not a GenAI operation, so claiming one * would put non-model work on an operator's GenAI dashboards. */ export declare const OPERATION: { readonly chat: 'chat'; readonly executeTool: 'execute_tool'; readonly invokeAgent: 'invoke_agent'; }; /** Metric names + units (OTel GenAI client metrics). */ export declare const METRIC: { readonly tokenUsage: 'gen_ai.client.token.usage'; readonly duration: 'gen_ai.client.operation.duration'; }; export declare const TOKEN_UNIT = "{token}"; export declare const DURATION_UNIT = "s"; /** * A neutral attribute value both transports understand (string / number / boolean / string * list). Booleans arrive only from LOG fields, which are free-form where a span's attributes * are ours to choose; they are carried as a native OTLP `boolValue` rather than stringified, * because a backend filters `ok = false` and `ok = "false"` differently. */ export type AttributeValue = string | number | boolean | string[]; export type AttributeMap = Record; interface MappedEvent { name: string; /** Epoch ms. */ timeMs: number; attributes: AttributeMap; } /** * Which OTel span kind a mapped span carries, named neutrally so the two transports each * translate it into their own enum instead of the caller passing a proto number to one and an * SDK enum to the other (which is a way for them to silently disagree). */ export type MappedSpanKind = 'client' | 'internal'; /** A transport-neutral span, ready to encode as OTLP JSON or feed the SDK tracer. */ export interface MappedSpan { /** 32-hex trace id (a run's spans share one; standalone calls get a random one). */ traceId: string; /** 16-hex span id. Random for a leaf; DERIVED for a parent (see {@link deriveRunSpanId}). */ spanId: string; /** * 16-hex id of this span's parent, or undefined for a root. A leaf names its parent by * DERIVING the id rather than by having been told it, which is what lets a stateless * per-call emission take part in a hierarchy assembled by the backend. */ parentSpanId?: string; name: string; kind: MappedSpanKind; /** Epoch ms. */ startTimeMs: number; /** Epoch ms. */ endTimeMs: number; /** false ⇒ the span carries ERROR status + {@link statusMessage}. */ ok: boolean; statusMessage?: string; attributes: AttributeMap; events: MappedEvent[]; } /** One token-usage counter data point (one per {@link ATTR.tokenType}). */ interface MappedTokenUsage { value: number; attributes: AttributeMap; } /** The metrics derived from one generation. */ export interface MappedMetrics { tokenUsage: MappedTokenUsage[]; /** Request duration in seconds (histogram value). */ durationSeconds: number; durationAttributes: AttributeMap; /** Epoch ms bounds for the (delta) data points. */ startTimeMs: number; endTimeMs: number; } export declare function randomTraceId(): string; export declare function randomSpanId(): string; /** The DERIVED span id of a run's root span. */ export declare function deriveRunSpanId(executionId: string): string; /** * The DERIVED span id of one `(run, agent kind)` step span — the parent every generation and * tool span of that kind hangs under. Keyed on the agent kind because that is all a generation * event carries; see `LlmStepSpan` for why that grain is the right one rather than a compromise. */ export declare function deriveStepSpanId(executionId: string, agentKind: string): string; /** Epoch ms → OTLP unix-nano string (string arithmetic avoids float precision loss). */ export declare function toUnixNano(ms: number): string; /** Map one completed LLM call to a neutral span. */ export declare function mapGeneration(event: LlmGenerationEvent): MappedSpan; /** Map one completed LLM call to its token-usage + duration metrics. */ export declare function mapGenerationMetrics(event: LlmGenerationEvent): MappedMetrics; /** Metric names for the deployment-level platform observability gauges. */ export declare const PLATFORM_METRIC: { /** Windowed run count, split by run status (done/failed/running/…). */ readonly runs: 'cat_factory.platform.runs'; /** Windowed `done / (done + failed)` success ratio (0..1). */ readonly runSuccessRate: 'cat_factory.platform.run_success_rate'; /** Windowed failed-run count, split by failure kind. */ readonly runFailures: 'cat_factory.platform.run_failures'; /** Current live/parked run count (a snapshot, not windowed), split by lifecycle state. */ readonly liveRuns: 'cat_factory.platform.live_runs'; /** Windowed wall-clock run duration (seconds), split by statistic (avg/min/max/pNN). */ readonly runDuration: 'cat_factory.platform.run_duration'; /** * Windowed count of SETTLED polling gates, split by gate kind and how it settled * (`passed` / `exhausted` / `clean`, the last being a pass that spun up no helper at all, * which is a separate series precisely because it is the one the precheck-first design is * supposed to maximise). */ readonly gates: 'cat_factory.platform.gates'; /** * Windowed count of helper-agent dispatches those gates spent (the CI-fixer attempt count), * split by gate kind and by whether the helper's own job succeeded or failed. */ readonly gateAttempts: 'cat_factory.platform.gate_attempts'; }; /** * Attribute keys for the platform metrics. `account_id` is the tenant scope (bounded — the * billing entity, far fewer than workspaces, so safe on a metric time series, unlike the * workspace id excluded from the per-call metrics); `window` labels the trailing aggregation * window. The remaining keys are the bounded split dimensions of each gauge. */ export declare const PLATFORM_ATTR: { readonly accountId: 'cat_factory.account_id'; readonly window: 'cat_factory.window'; readonly runStatus: 'cat_factory.run_status'; readonly runState: 'cat_factory.run_state'; readonly failureKind: 'cat_factory.failure_kind'; readonly durationStat: 'cat_factory.duration_stat'; /** The gate step's agent kind (`ci` / `conflicts` / …): a registry key, so bounded. */ readonly gateKind: 'cat_factory.gate_kind'; /** How a settled gate ended, or how a helper dispatch did. A closed vocabulary. */ readonly gateOutcome: 'cat_factory.gate_outcome'; }; /** One gauge data point: its dimensions, value, and whether to encode as int or double. */ interface MappedGaugePoint { attributes: AttributeMap; value: number; /** true ⇒ encode as an integer (counts); false ⇒ a double (ratios/durations). */ isInt: boolean; } /** A gauge metric ready to encode as OTLP or feed the SDK meter. */ export interface MappedGauge { name: string; unit: string; points: MappedGaugePoint[]; } /** * Map a {@link PlatformObservability} projection to the OpenTelemetry gauge metrics. All are * point-in-time gauges (the OTel backend builds trends from the series over time), stamped * with the projection's `generatedAt`. Every point carries the `account_id`; the windowed * gauges additionally carry the `window` label. Null/absent aggregates (e.g. a success rate * or percentiles with no terminal runs) are omitted rather than emitted as a misleading zero. */ export declare function mapPlatformMetrics(snapshot: PlatformObservability, dims: { accountId: string; }): MappedGauge[]; /** Map one container tool call to a neutral span under its agent kind's step span. */ export declare function mapToolSpan(context: LlmToolSpanContext, span: LlmToolSpan): MappedSpan; /** * Map a settled run to its ROOT span: the ancestor of every step span, and transitively of * every generation and tool span the run emitted. * * It carries no `gen_ai.operation.name`. A pipeline run is orchestration, not a model * operation, and stamping one would file the wait on a human decision as GenAI activity. * * The name is the bare `run`, with the pipeline riding as `cat_factory.pipeline` rather than * interpolated into it. A span name is the one field a backend treats as a low-cardinality * CLASS: it keys the RED metrics a span-metrics connector derives, and the trace-side * counterpart of the rule that a metric dimension must be BOUNDED. Every other name here is a * closed vocabulary (an operation, a model, an agent kind, a tool), but a pipeline is * workspace-authored free text, so interpolating it would let a tenant mint an unbounded * number of series on an operator's backend by renaming pipelines. */ export declare function mapRunSpan(run: LlmRunSpan): MappedSpan; /** * Map one `(run, agent kind)` slice to the step span its generations and tool calls hang under. * * A HELPER kind (a gate's `ci-fixer`, a Tester's fixer, a `fork-proposer`) names its hosting * kind as parent instead of the run, so an escalation reads as what it is rather than as a * pipeline step of its own. */ export declare function mapStepSpan(step: LlmStepSpan): MappedSpan; /** One counter metric ready to encode as an OTLP delta sum. */ export interface MappedCounter { name: string; unit: string; points: MappedGaugePoint[]; } /** * Map drained counter deltas onto OTLP delta sums, one metric per counter with a data point * per dimension set. Samples for the same counter are grouped, because OTLP wants one metric * carrying many points rather than the same metric repeated. * * Deliberately NOT account-scoped: an eviction or a cache miss belongs to the deployment, not * to a tenant, and stamping a synthetic account on it would invent an attribution the event * does not have. */ export declare function mapOperationalCounters(samples: OperationalCounterSample[]): MappedCounter[]; /** Map probed gauge readings onto OTLP gauges, one metric per gauge. */ export declare function mapOperationalGauges(samples: OperationalGaugeSample[]): MappedGauge[]; /** A transport-neutral OTLP log record. */ export interface MappedLogRecord { /** Epoch ms the line was emitted at. */ timeMs: number; severityNumber: number; severityText: string; /** The line's fixed message; everything variable is an attribute. */ body: string; attributes: AttributeMap; /** * The trace this line belongs to, when it named a run. Set through the SAME `deriveTraceId` * the run's spans go through (never a second copy of the derivation), so a backend joins * logs to the trace structurally rather than by matching an attribute; absent for a line * with no run (most boot, sweep and request lines). */ traceId?: string; /** * The span this line belongs BESIDE, present only alongside {@link traceId} and only when the * trace was ADOPTED from an inbound `traceparent`: it is the caller's own span id. * * A run-derived trace never sets it: that trace's spans are emitted by this package, and * pointing a line at one of them would mean picking which (the run root? the step? the * generation that happened to be open?), a question the line has no way to answer. Attaching * to the trace and letting the backend place it is the honest shape there. An adopted trace * is the opposite case: the caller's span is exactly and unambiguously the one the request * ran under. */ spanId?: string; /** * W3C trace flags for {@link traceId}, present only alongside it. Always SAMPLED, because * this pipeline has no sampler: a line that reached the exporter is a line the deployment * chose to export. Spans carry no `flags` and need none (a span's presence in the batch * states its own sampling); a log record is the side that has to state it, since a backend * decides from these flags whether the trace it points at is one it should have. */ traceFlags?: number; } /** * Map one emitted line to a neutral OTLP log record. * * Fields are exported under the names they already carry on the local writer (`workspaceId`, * `executionId`, `jobId`, `scope`, …) rather than being renamed into the `cat_factory.*` * namespace the spans use. An operator greps stdout and queries the collector with the same * key, which is worth more than symmetry with a signal whose attribute names this package * chose in the first place. The one thing the two signals DO share is the trace id, and it is * shared structurally: a line carrying an `executionId` is stamped with the run's derived * trace id, so a run's logs and its spans meet in the backend without either naming the other. * * That sharing is a CALL to `deriveTraceId`, not a second copy of what it does. The two signals * agreeing is the whole feature and nothing else enforces it: a re-derivation here would let a * change to the span side pass every test in this package while silently unjoining every log * line from the run it belongs to. */ export declare function mapLogRecord(record: LogRecord): MappedLogRecord; export {}; //# sourceMappingURL=mapping.d.ts.map