//#region src/telemetry/content-attributes.d.ts /** * Content-key alias vocabulary — the single source of truth for "where does a * trace producer put the human-readable CONTENT of a turn": the task, the * prompt, the model completion, and a tool call's name / args / result. * * This is the content sibling of {@link ./genai-attributes.ts} (model / token / * cost vocabulary) and follows the identical pattern: writers emit a producer's * own keys; the ingest READER accepts a wider candidate list per normalized * field because every coding agent and SDK spells the same field differently * (OTel GenAI, OpenInference, our own `tangle.*`, Claude Code's `claude_code.*`, * Codex's `codex.*`, OpenCode's `message.*` / `tool.*`). Co-locating the per-field * candidate lists and the resolver here is the drift guard: a producer's emitted * key and the reader's candidate list can never silently disagree, and adding an * agent is a DATA change (one alias row) rather than a code change in every * reader. * * Both consumers — the Intelligence ingest (span + log-record content recovery, * onboarding content check) and the standalone OpenCode exporter (which WRITES * under these keys) — import this layer, so "the producer shipped content" and * "the product can read it" mean the same thing by construction. * * Coding agents frequently carry this content on OTLP LOG events rather than * spans; {@link logRecordContentBag} is the one place that decides how a log row * becomes a bag the resolver can read. * * Fail-loud: a key whose value isn't a usable non-empty string (or, for * `messages`, a parseable array) is NOT recorded — {@link extractContent} * returns only the fields it actually found, never a fabricated default. An * empty bag returns an empty object, never sentinels. */ /** The normalized content a single attribute bag can carry. Every field is * optional: a bag carries only the slices the emitter put on it. */ interface NormalizedContent { task?: string; prompt?: string; completion?: string; toolName?: string; toolArgs?: string; toolResult?: string; messages?: unknown[]; } /** The normalized field a content key maps to. `messages` is parsed as an * array (or a JSON string that decodes to one); every other field is a * non-empty string. */ type ContentField = keyof NormalizedContent; /** Prompt / user message — the writer key OpenCode's user-turn export uses. */ declare const CONTENT_PROMPT_KEY = "gen_ai.prompt"; /** Model completion — the writer key OpenCode's assistant-text export uses. */ declare const CONTENT_COMPLETION_KEY = "message.part.text"; /** Invoked tool's name. */ declare const CONTENT_TOOL_NAME_KEY = "tool.name"; /** Tool call arguments. */ declare const CONTENT_TOOL_ARGS_KEY = "tool.input"; /** Tool call output / result. */ declare const CONTENT_TOOL_RESULT_KEY = "tool.output"; /** * Every content key the alias layer knows about. Callers use this to detect * "is there any content on this bag at all" without re-deriving the set. * Frozen so a caller can't mutate the shared table. Excludes the span-only * prompt spellings, which are intent-recovery candidates rather than * content-presence signals. */ declare const CONTENT_KEYS: readonly string[]; /** Map of every known content key → its normalized field, exposed read-only * for callers that want to inspect or extend the mapping (e.g. enrichment * joins that need to know which field a matched key feeds). */ declare const CONTENT_KEY_FIELD: Readonly>; /** A non-empty trimmed string, or null. Absent/blank/non-string values are an * honest miss, never "". */ declare function asContentField(value: unknown): string | null; /** Flatten a tool-args / tool-result value to a string. Strings pass through; * objects/arrays are JSON-stringified so a structured tool payload is still * readable content. Null/empty → null (never "" or "null"). */ declare function asContentString(value: unknown): string | null; /** * SQL `LIKE` patterns (over `jsonb_object_keys`) that detect indexed / array / * tool-call content on a flattened bag — the DB-side complement to * `normalizeContentAttributes` for a reader (e.g. the onboarding "content seen" * check) that must decide "does any content-bearing key exist" in SQL without * materializing rows. Kept beside the reconstruction so the in-process reader and * the SQL predicate can never drift on which shapes count as content. */ declare const INDEXED_CONTENT_KEY_LIKE_PATTERNS: readonly string[]; /** * Reconstruct a flattened attribute bag's indexed / array / tool-call content * keys into the canonical coarse aliases the reader understands. Pure and * NON-DESTRUCTIVE: an already-present coarse key wins, and the SAME reference is * returned when nothing needs reconstructing. Every content read path runs this * first, so a new provider's flattening is learned in ONE place. */ declare function normalizeContentAttributes(bag: Record | null | undefined): Record; /** * Extract normalized content from a flattened attribute bag (span attributes * OR a log record's merged attribute bag). Pure: same input → same output, no * I/O, no env reads. Returns only the fields actually present; an empty/absent * bag returns `{}`. */ declare function extractContent(attributes: Record | undefined | null): NormalizedContent; /** True when the bag carries ANY known content key with a usable value. A cheap * presence check that SHORT-CIRCUITS on the first usable key — it does not * build the NormalizedContent object or JSON-parse `messages` bodies, so a hot * scan path (onboarding) pays per-row only until the first hit. */ declare function hasContent(attributes: Record | undefined | null): boolean; /** * Build the content-detection bag for a single OTLP log record. A log carries * its content either as flattened attributes (keyed by a content key) OR as the * record `body` for a prompt event whose text has no attribute key (a * `*.user_prompt` event whose prompt IS the body). This is the ONE place that * decides "how a log row becomes a bag `extractContent` can read", so the * intent-audit recovery, the trace-analyst log enrichment, and the onboarding * content check can never drift on it. * * The body is surfaced under the standard `prompt` alias ONLY for `*.user_prompt` * events (a generic log body is not content); an attribute-carried `prompt` * already present is never overwritten. */ declare function logRecordContentBag(attributes: Record | null | undefined, body: string | null | undefined): Record; /** * Deterministic coding-agent label for an attribute bag, by the namespace of * the content/event keys it carries, then the standard `gen_ai.system` / * `service.name` identity. Returns a stable lowercase id * (`claude-code` | `codex` | `opencode` | the gen_ai.system name) or * `"unknown"` when nothing reliably identifies the emitter — never a fabricated * default that pretends to know which agent produced the row. * * `service.name` is matched against a KNOWN-AGENT ALLOWLIST (exact), not a * substring guess, so an arbitrary customer service named e.g. `claude-helper` * stays `unknown` rather than being mislabelled `claude-code` and driving a * wrong onboarding hint. */ declare function classifyAgent(bag: Record | null | undefined): string; /** A declared-intent recovery from a bag: the recovered text plus the source * it came from — either the exact attribute key that matched, or `"messages"` * when it came from the first user message of a parsed messages array. Lets a * caller record traceable evidence (which key carried the intent) while still * resolving through the ONE shared vocabulary. */ interface DeclaredIntentMatch { text: string; /** The matched attribute key, or `"messages"` for a messages-array source. */ source: string; } /** * Recover the declared intent from a flattened attribute bag through the shared * task/prompt/messages vocabulary, reporting WHICH key matched. The span path * and the log-record path both call this, so they resolve intent through one * key source and a key added to the shared layer is read by both. Walks the * `task` field's keys, then the `prompt` field's keys (which include the * span-only `user_request` / `input` / `llm.input` spellings folded into the * priority list), then the first user message of a messages body. Returns null * when nothing usable is present — never a fabricated intent. */ declare function resolveDeclaredIntent(bag0: Record | null | undefined): DeclaredIntentMatch | null; /** * Best-effort declared-task text from normalized content, in the order a * declared intent is most likely to live: an explicit task, then the prompt, * then the first user message in a messages array. Deliberately does NOT * consult `completion` — an assistant completion is the agent's OUTPUT, not the * declared task; treating it as the task would fabricate intent. Returns null * when no genuine declared-task source is present — the caller skips, never * fabricates. */ declare function declaredTaskText(content: NormalizedContent): string | null; //#endregion //#region src/telemetry/genai-attributes.d.ts /** * OpenTelemetry GenAI semantic-convention span attributes — the single * vocabulary every trace producer in the stack emits and the intelligence * ingest reads, so model / token / cost attribution is identical regardless of * which producer lowered the span (workflow emitter, SDK trace sink, …). * * Writers emit the PRIMARY keys (`GEN_AI_*`). The ingest reader accepts the * wider candidate lists (`GEN_AI_*_KEYS`) because foreign SDKs spell the same * field differently (Langfuse, raw OTLP GenAI, our own legacy `model`). * Co-locating writer keys and reader candidates here is the drift guard: a * writer's key and the reader's first candidate can never silently disagree. * * Spec: OpenTelemetry GenAI semantic conventions (`gen_ai.*`). */ /** Model that served a request — the primary key writers emit. */ declare const GEN_AI_REQUEST_MODEL = "gen_ai.request.model"; /** Model named on a response — a reader fallback; writers prefer request.model. */ declare const GEN_AI_RESPONSE_MODEL = "gen_ai.response.model"; /** Prompt / input token count. */ declare const GEN_AI_USAGE_INPUT_TOKENS = "gen_ai.usage.input_tokens"; /** Completion / output token count. */ declare const GEN_AI_USAGE_OUTPUT_TOKENS = "gen_ai.usage.output_tokens"; /** Prompt tokens served from the provider's prompt cache. Emitted separately * from {@link GEN_AI_USAGE_INPUT_TOKENS} because the two bill at rates ~50x * apart, so an aggregate that cannot tell them apart cannot compute cost. */ declare const GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS = "gen_ai.usage.cache_read_input_tokens"; /** Prompt tokens written INTO the provider's prompt cache. */ declare const GEN_AI_USAGE_CACHE_CREATION_INPUT_TOKENS = "gen_ai.usage.cache_creation_input_tokens"; /** Operation name (e.g. `"invoke_agent"`). */ declare const GEN_AI_OPERATION_NAME = "gen_ai.operation.name"; /** Conversation / session id grouping a multi-turn agent run. */ declare const GEN_AI_CONVERSATION_ID = "gen_ai.conversation.id"; /** * Reader candidate keys for the model, highest priority first. The first entry * is the writers' primary key; the rest accept foreign-SDK spellings. */ declare const GEN_AI_MODEL_KEYS: readonly string[]; /** Reader candidate keys for input tokens, highest priority first. These name * SPAN ATTRIBUTES on a lowered span — for the raw producer `tokenUsage` OBJECT * field names (a distinct layer) see `TOKEN_USAGE_INPUT_KEYS` in * `token-usage.ts`; a new producer shape may need an entry in both. */ declare const GEN_AI_INPUT_TOKEN_KEYS: readonly string[]; /** Reader candidate keys for output tokens, highest priority first. */ declare const GEN_AI_OUTPUT_TOKEN_KEYS: readonly string[]; /** A model/token usage record to lower into the GenAI attribute bag. */ interface GenAiUsage { /** Model slug; omitted from the attribute bag when absent or empty. */ model?: string; /** Prompt tokens; omitted unless a finite, non-negative number. */ inputTokens?: number; /** Completion tokens; omitted unless a finite, non-negative number. */ outputTokens?: number; /** Prompt tokens served from cache; omitted unless finite and non-negative. */ cacheReadTokens?: number; /** Prompt tokens written into cache; omitted unless finite and non-negative. */ cacheWriteTokens?: number; } /** * Lower a usage record to the GenAI semantic-convention attribute bag, OMITTING * any unknown field. A synthesized zero token count or empty model would * corrupt every downstream token/cost aggregate, so absent stays absent — the * ingest then reads "cost not computed", never "free". This is the writer-side * mirror of {@link GEN_AI_MODEL_KEYS} / the `*_TOKEN_KEYS` reader candidates. */ declare function genAiUsageAttributes(usage: GenAiUsage): Record; //#endregion //#region src/telemetry/spine-attributes.d.ts /** * Spine run/subject/tree + trigger span-attribute vocabulary — the single set of * `tangle.*` keys that name a run, its parent, its kind, its trigger, and the * durable subject it worked on. Co-located here so the WRITER (the platform * workflow-trace-emitter) and the READERS (intelligence-api's run-attrs derivers * + reconstruct) key off one source and can never silently drift. * * Writers emit the PRIMARY keys (`*_KEY`). The ingest reader accepts the wider * CANDIDATE lists (`*_ATTRS`) because the emit sites disagree on spelling: the * workflow emitter writes camel `tangle.runId`, the agent setup prompt documents * dotted `tangle.run.id`. Each candidate list's first entry is a writer primary * key, so the drift guard (`spine-attributes.test.ts`) can assert writer/reader * agreement. * * The DERIVATION LOGIC (priority resolution, parent!=own-run guard, run-kind * inference, subject `'default'` fallback, `source:id` formatting) lives in * intelligence-api's `run-attrs.ts`; only the key constants and candidate lists * live here. The foreign subject candidates (`symphony.issue.identifier`, * `git.repository`, `service.name`) are members of {@link SUBJECT_KEY_ATTRS}. */ /** Primary run-id key writers stamp; first entry of {@link RUN_KEY_ATTRS}. */ declare const TANGLE_RUN_ID_KEY = "tangle.run.id"; /** Camel run-id key the workflow emitter actually writes on every span. */ declare const TANGLE_RUN_ID_CAMEL_KEY = "tangle.runId"; /** Primary parent-run-id key; first entry of {@link PARENT_RUN_KEY_ATTRS}. */ declare const TANGLE_RUN_PARENT_ID_KEY = "tangle.run.parent_id"; /** Primary run-kind key; the sole entry of {@link RUN_KIND_ATTRS}. */ declare const TANGLE_RUN_KIND_KEY = "tangle.run.kind"; /** Primary subject key; first entry of {@link SUBJECT_KEY_ATTRS}. */ declare const TANGLE_SUBJECT_KEY = "tangle.subject.key"; /** Primary trigger-source key; first entry of {@link TRIGGER_SOURCE_ATTRS}. */ declare const TANGLE_TRIGGER_SOURCE_KEY = "tangle.workflow.trigger.source"; /** Trigger-kind key the workflow emitter writes on the root span. */ declare const TANGLE_TRIGGER_KIND_KEY = "tangle.trigger.kind"; /** Primary trigger-id key; first entry of {@link TRIGGER_ID_ATTRS}. */ declare const TANGLE_TRIGGER_ID_KEY = "tangle.workflow.trigger.id"; /** Workflow-id key the workflow emitter writes on the root span; a * {@link WORKFLOW_MARKER_ATTRS} member. */ declare const TANGLE_WORKFLOW_ID_KEY = "tangle.workflowId"; /** Run-id reader candidates, highest priority first. */ declare const RUN_KEY_ATTRS: readonly string[]; /** * Parent-run-id reader candidates, highest priority first. After the explicit * parent keys come the workflow-run ids, which name a parent only when this span * is a distinct child run (the deriver's own-run guard enforces that). */ declare const PARENT_RUN_KEY_ATTRS: readonly string[]; /** Trigger-source reader candidates, highest priority first. */ declare const TRIGGER_SOURCE_ATTRS: readonly string[]; /** Trigger-id reader candidates, highest priority first. */ declare const TRIGGER_ID_ATTRS: readonly string[]; /** * Subject-key reader candidates, highest priority first: declared subject key / * project, else the agent's own work-item identity, else repo, else service. The * deriver appends a `'default'` fallback so no run is orphaned. */ declare const SUBJECT_KEY_ATTRS: readonly string[]; /** Explicit run-kind reader candidates (exact `workflow`/`consultant`/`session` * wins in the deriver). */ declare const RUN_KIND_ATTRS: readonly string[]; /** Workflow-presence markers — any one present implies a `workflow` run kind. */ declare const WORKFLOW_MARKER_ATTRS: readonly string[]; //#endregion //#region src/telemetry/token-usage.d.ts /** * Candidate field names for a producer-supplied `tokenUsage` bag on a * `message.updated` trace event. Agents and SDK layers disagree on the * spelling: `StreamTokenUsage` emits `inputTokens`/`outputTokens`, the eval and * workflow agent.run paths emit `input`/`output`, and persisted / foreign * shapes use snake_case or prompt/completion naming. A reader that wants the * token counts must try all of them, highest-priority first, or it silently * drops usage for the producers it does not name. * * Shared so every `tokenUsage` reader (the trace sink's root-span aggregation, * the signal extractor) keys off the identical set and cannot drift. * * Distinct layer from `genai-attributes.ts`: those keys (`GEN_AI_*_TOKEN_KEYS`) * name OTel SPAN ATTRIBUTES on an already-lowered span; these name the fields of * the raw producer `tokenUsage` OBJECT before lowering. A new producer that * spells token usage differently may need an entry in BOTH places. */ declare const TOKEN_USAGE_INPUT_KEYS: readonly string[]; declare const TOKEN_USAGE_OUTPUT_KEYS: readonly string[]; declare const TOKEN_USAGE_COST_KEYS: readonly string[]; /** * Prompt-cache counters, split by whether the provider ALREADY counted them * inside its input/prompt token field. The split is not cosmetic: the two * families disagree, and a reader that ignores the disagreement reports a * number that is wrong in one direction or the other on every call. * * INCLUSIVE (OpenAI-compatible): `prompt_tokens` is the WHOLE prompt and * `prompt_tokens_details.cached_tokens` names the cached share of it. Measured * on router.tangle.tools against `glm-5.2` (provider `zai`): * `prompt_tokens=8656, cached_tokens=8576, completion=42, total=8698` — * `8656 + 42 === 8698`, so the cached tokens are inside `prompt_tokens`. * * EXCLUSIVE (Anthropic-native): `input_tokens` is only the UNCACHED tail and * `cache_read_input_tokens` sits beside it. Measured on the same router, * same request shape: `prompt_tokens=39, cache_read_input_tokens=9924, * completion=10, total=49` — `39 + 10 === 49`, so the 9,924 cached tokens are * NOT in `prompt_tokens`. * * Both shapes reach a caller through the same OpenAI-compatible endpoint, so * the convention cannot be inferred from the transport — only from which key * carried the count. That is why these are two lists and not one. */ declare const TOKEN_USAGE_CACHE_READ_INCLUSIVE_KEYS: readonly string[]; declare const TOKEN_USAGE_CACHE_READ_EXCLUSIVE_KEYS: readonly string[]; declare const TOKEN_USAGE_CACHE_WRITE_INCLUSIVE_KEYS: readonly string[]; declare const TOKEN_USAGE_CACHE_WRITE_EXCLUSIVE_KEYS: readonly string[]; /** Nested bags that spell cache counters with the SAME vocabulary as the flat * usage object — the OpenAI-compatible `*_details` shape, whose * `cached_tokens` is inclusive. Searched after the flat keys, in this order. */ declare const TOKEN_USAGE_DETAIL_KEYS: readonly string[]; /** * Dedicated prompt-cache bags, which spell their counters with BARE names * (`read` / `write`) that only mean "cache" because of the bag they sit in. * `cache` is opencode's `step_finish.tokens.cache`; `prompt_cache` is the * Tangle router's provider-normalized bag. * * Counters found here are treated as EXCLUSIVE of the reported input total. * Measured for opencode on 2026-08-03, `glm-5.2` via the router: * `{total: 17007, input: 16940, output: 3, reasoning: 0, cache: {read: 64, write: 0}}` * — `16940 + 64 + 0 + 3 === 17007`, so `input` is the uncached tail. The * router's `prompt_cache` only ever reaches a caller ALONGSIDE the * provider-native key, which wins over this fallback, so this rule decides * only the opencode-shaped case it was measured on. */ declare const TOKEN_USAGE_CACHE_BAG_KEYS: readonly string[]; declare const TOKEN_USAGE_CACHE_BAG_READ_KEYS: readonly string[]; declare const TOKEN_USAGE_CACHE_BAG_WRITE_KEYS: readonly string[]; /** * Normalized token counts for one call. * * The three prompt-side fields are DISJOINT and additive by construction: * `inputTokens + cacheReadTokens + cacheWriteTokens` is the size of the whole * prompt the model saw, and each term is billed at its own rate. `inputTokens` * is the freshly-billed tail ONLY — it is not the context size, and reading it * as one understates a warm agent loop's context by the cached share, which on * a stable-prefix loop is the overwhelming majority of it. */ interface TokenUsageCounts { /** Prompt tokens billed at the full input rate: the uncached tail. */ inputTokens: number; outputTokens: number; /** Prompt tokens served from the provider's prompt cache, billed at the * (much cheaper) cache-read rate. Absent when the producer reported no * cache information at all — which is not the same as a measured zero. */ cacheReadTokens?: number; /** Prompt tokens written INTO the provider's cache on this call. */ cacheWriteTokens?: number; } declare function tokenCount(value: unknown): number | undefined; declare function firstTokenCount(source: Record | undefined, keys: readonly string[]): number | undefined; declare function firstUsageCostUsd(source: Record | undefined, keys?: readonly string[]): number | undefined; declare function tokenUsageSource(data: Record): Record; declare function readTokenUsage(data: Record): TokenUsageCounts | undefined; declare function readTokenCostUsd(data: Record): number | undefined; declare function addTokenUsage(current: TokenUsageCounts | undefined, next: TokenUsageCounts): TokenUsageCounts; /** The size of the prompt the model actually saw: billed tail + cache read + * cache write. This — never `inputTokens` — is the number to read as "context * size", and the gap between them is the whole point of the cache. */ declare function promptTokens(usage: TokenUsageCounts): number; /** Share of the prompt served from cache, in `[0, 1]`. `undefined` when the * producer reported no cache information, so an unmetered path cannot be read * as a 0% hit rate. */ declare function cacheHitRate(usage: TokenUsageCounts): number | undefined; //#endregion export { CONTENT_KEY_FIELD as $, TANGLE_SUBJECT_KEY as A, GEN_AI_MODEL_KEYS as B, RUN_KEY_ATTRS as C, TANGLE_RUN_ID_KEY as D, TANGLE_RUN_ID_CAMEL_KEY as E, TRIGGER_ID_ATTRS as F, GEN_AI_USAGE_CACHE_CREATION_INPUT_TOKENS as G, GEN_AI_OUTPUT_TOKEN_KEYS as H, TRIGGER_SOURCE_ATTRS as I, GEN_AI_USAGE_OUTPUT_TOKENS as J, GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS as K, WORKFLOW_MARKER_ATTRS as L, TANGLE_TRIGGER_KIND_KEY as M, TANGLE_TRIGGER_SOURCE_KEY as N, TANGLE_RUN_KIND_KEY as O, TANGLE_WORKFLOW_ID_KEY as P, CONTENT_KEYS as Q, GEN_AI_CONVERSATION_ID as R, PARENT_RUN_KEY_ATTRS as S, SUBJECT_KEY_ATTRS as T, GEN_AI_REQUEST_MODEL as U, GEN_AI_OPERATION_NAME as V, GEN_AI_RESPONSE_MODEL as W, genAiUsageAttributes as X, GenAiUsage as Y, CONTENT_COMPLETION_KEY as Z, promptTokens as _, TOKEN_USAGE_CACHE_READ_INCLUSIVE_KEYS as a, DeclaredIntentMatch as at, tokenCount as b, TOKEN_USAGE_COST_KEYS as c, asContentField as ct, TOKEN_USAGE_OUTPUT_KEYS as d, declaredTaskText as dt, CONTENT_PROMPT_KEY as et, TokenUsageCounts as f, extractContent as ft, firstUsageCostUsd as g, resolveDeclaredIntent as gt, firstTokenCount as h, normalizeContentAttributes as ht, TOKEN_USAGE_CACHE_READ_EXCLUSIVE_KEYS as i, ContentField as it, TANGLE_TRIGGER_ID_KEY as j, TANGLE_RUN_PARENT_ID_KEY as k, TOKEN_USAGE_DETAIL_KEYS as l, asContentString as lt, cacheHitRate as m, logRecordContentBag as mt, TOKEN_USAGE_CACHE_BAG_READ_KEYS as n, CONTENT_TOOL_NAME_KEY as nt, TOKEN_USAGE_CACHE_WRITE_EXCLUSIVE_KEYS as o, INDEXED_CONTENT_KEY_LIKE_PATTERNS as ot, addTokenUsage as p, hasContent as pt, GEN_AI_USAGE_INPUT_TOKENS as q, TOKEN_USAGE_CACHE_BAG_WRITE_KEYS as r, CONTENT_TOOL_RESULT_KEY as rt, TOKEN_USAGE_CACHE_WRITE_INCLUSIVE_KEYS as s, NormalizedContent as st, TOKEN_USAGE_CACHE_BAG_KEYS as t, CONTENT_TOOL_ARGS_KEY as tt, TOKEN_USAGE_INPUT_KEYS as u, classifyAgent as ut, readTokenCostUsd as v, RUN_KIND_ATTRS as w, tokenUsageSource as x, readTokenUsage as y, GEN_AI_INPUT_TOKEN_KEYS as z }; //# sourceMappingURL=index-BvjA5i93.d.ts.map