@tangle-network/agent-core 0.2.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/dist/auth/browser.d.ts +1 -1
  2. package/dist/auth/index.d.ts +1 -1
  3. package/dist/events/index.d.ts +1 -1
  4. package/dist/{index-CtebBodu.d.ts → index-BOD4IhTr.d.ts} +2 -2
  5. package/dist/{index-CtebBodu.d.ts.map → index-BOD4IhTr.d.ts.map} +1 -1
  6. package/dist/{index-DiQX9bjq.d.ts → index-BRyMBSkE.d.ts} +1 -1
  7. package/dist/{index-DiQX9bjq.d.ts.map → index-BRyMBSkE.d.ts.map} +1 -1
  8. package/dist/{index-DlPznmKA.d.ts → index-C7zlWVGc.d.ts} +1 -1
  9. package/dist/{index-DlPznmKA.d.ts.map → index-C7zlWVGc.d.ts.map} +1 -1
  10. package/dist/index-CBGkvDtv.d.ts +303 -0
  11. package/dist/index-CBGkvDtv.d.ts.map +1 -0
  12. package/dist/{index-z2wPdzFb.d.ts → index-Dz1ZLn3K.d.ts} +2 -2
  13. package/dist/{index-z2wPdzFb.d.ts.map → index-Dz1ZLn3K.d.ts.map} +1 -1
  14. package/dist/{index-CFA8kgHJ.d.ts → index-VcOZI9jN.d.ts} +1 -1
  15. package/dist/{index-CFA8kgHJ.d.ts.map → index-VcOZI9jN.d.ts.map} +1 -1
  16. package/dist/index.d.ts +8 -85
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js +4 -106
  19. package/dist/index.js.map +1 -1
  20. package/dist/resilience/index.d.ts +1 -1
  21. package/dist/telemetry/index.d.ts +2 -0
  22. package/dist/telemetry/index.js +2 -0
  23. package/dist/telemetry-D2XGH5UL.js +699 -0
  24. package/dist/telemetry-D2XGH5UL.js.map +1 -0
  25. package/dist/transport/index.d.ts +1 -1
  26. package/dist/transport/index.js +1 -1
  27. package/dist/{transport-mKLoQep5.js → transport-XLxpQmUW.js} +1 -1
  28. package/dist/{transport-mKLoQep5.js.map → transport-XLxpQmUW.js.map} +1 -1
  29. package/dist/types/index.d.ts +1 -1
  30. package/dist/types/index.js +1 -1
  31. package/dist/{types-D5VLaol0.js → types-Ci7hcTnp.js} +1 -1
  32. package/dist/{types-D5VLaol0.js.map → types-Ci7hcTnp.js.map} +1 -1
  33. package/dist/{types-DiDSGM8K.d.ts → types-D32txapu.d.ts} +2 -2
  34. package/dist/{types-DiDSGM8K.d.ts.map → types-D32txapu.d.ts.map} +1 -1
  35. package/package.json +7 -2
@@ -0,0 +1,699 @@
1
+ //#region src/telemetry/content-attributes.ts
2
+ /** Prompt / user message — the writer key OpenCode's user-turn export uses. */
3
+ const CONTENT_PROMPT_KEY = "gen_ai.prompt";
4
+ /** Model completion — the writer key OpenCode's assistant-text export uses. */
5
+ const CONTENT_COMPLETION_KEY = "message.part.text";
6
+ /** Invoked tool's name. */
7
+ const CONTENT_TOOL_NAME_KEY = "tool.name";
8
+ /** Tool call arguments. */
9
+ const CONTENT_TOOL_ARGS_KEY = "tool.input";
10
+ /** Tool call output / result. */
11
+ const CONTENT_TOOL_RESULT_KEY = "tool.output";
12
+ /**
13
+ * The single alias table: content key → normalized field. Ordered by emitter
14
+ * for auditability; resolution order within a field is "first matching key in
15
+ * priority order wins" (see {@link FIELD_KEY_PRIORITY}), NOT table order.
16
+ *
17
+ * Coverage:
18
+ * - Standard OTel GenAI / OpenInference / our own `tangle.*` conventions.
19
+ * - Claude Code: the `claude_code.*` event attributes.
20
+ * - Codex: the `codex.*` event attributes (inner prompt key `prompt`,
21
+ * verified from codex-rs/otel; looser shim spellings folded in as
22
+ * low-priority fallbacks).
23
+ * - OpenCode: the `message.*` / `tool.*` event attributes.
24
+ *
25
+ * Adding an agent = add its rows here. No reader changes.
26
+ */
27
+ const CONTENT_KEY_ALIASES = {
28
+ "gen_ai.prompt": "prompt",
29
+ "gen_ai.request.prompt": "prompt",
30
+ "gen_ai.completion": "completion",
31
+ "gen_ai.response.completion": "completion",
32
+ "input.value": "prompt",
33
+ "output.value": "completion",
34
+ "llm.input_messages": "messages",
35
+ "llm.output_messages": "messages",
36
+ "user.message": "prompt",
37
+ prompt: "prompt",
38
+ "tangle.task": "task",
39
+ "tangle.declared_intent": "task",
40
+ "tangle.tool.args": "toolArgs",
41
+ "tangle.tool.result": "toolResult",
42
+ "tangle.tool.name": "toolName",
43
+ "tool.name": "toolName",
44
+ "tool.args": "toolArgs",
45
+ "claude_code.user_prompt.prompt": "prompt",
46
+ "claude_code.tool_result.tool_input": "toolArgs",
47
+ "claude_code.tool_result.tool_parameters": "toolArgs",
48
+ "claude_code.tool_result.tool_output": "toolResult",
49
+ "claude_code.tool.tool.output": "toolResult",
50
+ "claude_code.tool.output": "toolResult",
51
+ "claude_code.api_request_body.body": "messages",
52
+ "claude_code.api_response_body.body": "messages",
53
+ "codex.user_prompt.prompt": "prompt",
54
+ "codex.user_prompt.text": "prompt",
55
+ "codex.user_prompt.message": "prompt",
56
+ "codex.user_prompt.input": "prompt",
57
+ "codex.tool_result.output": "toolResult",
58
+ "codex.tool_result.result": "toolResult",
59
+ "message.part.text": "completion",
60
+ "message.text": "completion",
61
+ "tool.input": "toolArgs",
62
+ "tool.output": "toolResult"
63
+ };
64
+ /**
65
+ * Reader candidate keys per normalized field, highest-confidence first.
66
+ * When several aliased keys for the same field are present on one bag, the
67
+ * first key in this list that is present wins — so `tangle.task` beats a bare
68
+ * `prompt` for `task`, and the richest source (a full `messages` body) is
69
+ * preferred where it exists. A key not listed here for its field still
70
+ * resolves (it is appended after the explicit order), so adding an alias row
71
+ * without touching this list is safe — it just lands at lowest priority.
72
+ *
73
+ * The `user_request` / `task` / `input` span-only prompt spellings the
74
+ * intent-audit span path historically read are folded in here so the span and
75
+ * log recovery paths resolve intent through ONE vocabulary.
76
+ */
77
+ const FIELD_KEY_PRIORITY = {
78
+ task: [
79
+ "tangle.task",
80
+ "tangle.declared_intent",
81
+ "task"
82
+ ],
83
+ prompt: [
84
+ "gen_ai.prompt",
85
+ "gen_ai.request.prompt",
86
+ "claude_code.user_prompt.prompt",
87
+ "codex.user_prompt.prompt",
88
+ "codex.user_prompt.text",
89
+ "codex.user_prompt.message",
90
+ "codex.user_prompt.input",
91
+ "input.value",
92
+ "user.message",
93
+ "user_request",
94
+ "llm.input",
95
+ "input",
96
+ "prompt"
97
+ ],
98
+ completion: [
99
+ "gen_ai.completion",
100
+ "gen_ai.response.completion",
101
+ "output.value",
102
+ "message.part.text",
103
+ "message.text"
104
+ ],
105
+ toolName: ["tangle.tool.name", "tool.name"],
106
+ toolArgs: [
107
+ "tangle.tool.args",
108
+ "claude_code.tool_result.tool_input",
109
+ "claude_code.tool_result.tool_parameters",
110
+ "tool.input",
111
+ "tool.args"
112
+ ],
113
+ toolResult: [
114
+ "tangle.tool.result",
115
+ "claude_code.tool_result.tool_output",
116
+ "claude_code.tool.tool.output",
117
+ "claude_code.tool.output",
118
+ "codex.tool_result.output",
119
+ "codex.tool_result.result",
120
+ "tool.output"
121
+ ],
122
+ messages: [
123
+ "claude_code.api_request_body.body",
124
+ "claude_code.api_response_body.body",
125
+ "llm.input_messages",
126
+ "llm.output_messages"
127
+ ]
128
+ };
129
+ /**
130
+ * Every content key the alias layer knows about. Callers use this to detect
131
+ * "is there any content on this bag at all" without re-deriving the set.
132
+ * Frozen so a caller can't mutate the shared table. Excludes the span-only
133
+ * prompt spellings, which are intent-recovery candidates rather than
134
+ * content-presence signals.
135
+ */
136
+ const CONTENT_KEYS = Object.freeze(Object.keys(CONTENT_KEY_ALIASES));
137
+ /** Map of every known content key → its normalized field, exposed read-only
138
+ * for callers that want to inspect or extend the mapping (e.g. enrichment
139
+ * joins that need to know which field a matched key feeds). */
140
+ const CONTENT_KEY_FIELD = Object.freeze({ ...CONTENT_KEY_ALIASES });
141
+ /** A non-empty trimmed string, or null. Absent/blank/non-string values are an
142
+ * honest miss, never "". */
143
+ function asContentField(value) {
144
+ if (typeof value === "string") {
145
+ const t = value.trim();
146
+ return t.length > 0 ? t : null;
147
+ }
148
+ return null;
149
+ }
150
+ /** Flatten a tool-args / tool-result value to a string. Strings pass through;
151
+ * objects/arrays are JSON-stringified so a structured tool payload is still
152
+ * readable content. Null/empty → null (never "" or "null"). */
153
+ function asContentString(value) {
154
+ if (value === null || value === void 0) return null;
155
+ const s = asContentField(value);
156
+ if (s !== null) return s;
157
+ if (typeof value === "object") try {
158
+ const json = JSON.stringify(value);
159
+ return json && json !== "{}" && json !== "[]" ? json : null;
160
+ } catch {
161
+ return null;
162
+ }
163
+ if (typeof value === "number" || typeof value === "boolean") return String(value);
164
+ return null;
165
+ }
166
+ /**
167
+ * Coerce a `messages`-aliased value into an array. Accepts an already-decoded
168
+ * array, a JSON string that parses to an array, or a JSON object (a
169
+ * Claude/Codex api_request_body whose top level is `{ messages: [...] }` or a
170
+ * single message object). Returns null when nothing array-shaped is
171
+ * recoverable — never a fabricated `[]`.
172
+ */
173
+ function asMessages(value) {
174
+ let v = value;
175
+ if (typeof v === "string") {
176
+ const t = v.trim();
177
+ if (t.length === 0) return null;
178
+ try {
179
+ v = JSON.parse(t);
180
+ } catch {
181
+ return null;
182
+ }
183
+ }
184
+ if (Array.isArray(v)) return v.length > 0 ? v : null;
185
+ if (v && typeof v === "object") {
186
+ const inner = v.messages;
187
+ if (Array.isArray(inner)) return inner.length > 0 ? inner : null;
188
+ if (messageHasText(v)) return [v];
189
+ return null;
190
+ }
191
+ return null;
192
+ }
193
+ /**
194
+ * Recover the human-readable text from a single message-shaped object. The one
195
+ * reader every message-text caller goes through, in priority order:
196
+ * 1. `content` as a plain string;
197
+ * 2. `content` as a LIST OF BLOCKS — the Anthropic / Claude Code message shape
198
+ * (`content: [{ type: "text", text }, { type: "tool_use", … }]`): the text
199
+ * of every text-bearing block is concatenated (newline-joined), tool/other
200
+ * blocks contribute nothing. Without this branch a Claude Code message
201
+ * whose content is an array is silently invisible to content recovery;
202
+ * 3. a top-level `text` string;
203
+ * 4. a nested `content.text` string (object-wrapped content).
204
+ * Returns the first non-empty result, or null when no usable text exists — a
205
+ * bare/arbitrary object yields null and is not treated as a usable message.
206
+ */
207
+ function readMessageText(msg) {
208
+ const direct = asContentField(msg.content);
209
+ if (direct !== null) return direct;
210
+ const blocks = msg.content;
211
+ if (Array.isArray(blocks)) {
212
+ const parts = [];
213
+ for (const block of blocks) {
214
+ if (!block || typeof block !== "object") continue;
215
+ const text = asContentField(block.text);
216
+ if (text !== null) parts.push(text);
217
+ }
218
+ if (parts.length > 0) return parts.join("\n");
219
+ }
220
+ const top = asContentField(msg.text);
221
+ if (top !== null) return top;
222
+ return asContentField(msg.content?.text);
223
+ }
224
+ /** True when a single message-shaped object carries recoverable text. A
225
+ * bare/arbitrary object has none, so it is not treated as a usable message. */
226
+ function messageHasText(msg) {
227
+ return readMessageText(msg) !== null;
228
+ }
229
+ /**
230
+ * Precomputed resolution order per field: the explicit priority list, then any
231
+ * remaining aliased keys for that field appended at lowest priority. Built once
232
+ * at module load (frozen) so the content-presence hot path (onboarding scans
233
+ * every row) never re-runs an `Object.keys().filter()` over the alias table per
234
+ * row — it reads a fixed array instead.
235
+ */
236
+ const FIELD_KEY_ORDER = Object.freeze(Object.fromEntries(Object.keys(FIELD_KEY_PRIORITY).map((field) => {
237
+ const priority = FIELD_KEY_PRIORITY[field];
238
+ const extras = Object.keys(CONTENT_KEY_ALIASES).filter((k) => CONTENT_KEY_ALIASES[k] === field && !priority.includes(k));
239
+ return [field, Object.freeze([...priority, ...extras])];
240
+ })));
241
+ /** The resolution order for one field: explicit priority list, then any
242
+ * remaining aliased keys for that field appended at lowest priority. */
243
+ function fieldKeyOrder(field) {
244
+ return FIELD_KEY_ORDER[field];
245
+ }
246
+ /** Resolve one normalized field from a bag: walk its priority list, then any
247
+ * remaining aliased keys for that field, returning the first usable value. */
248
+ function resolveField(bag, field) {
249
+ for (const key of fieldKeyOrder(field)) {
250
+ if (!(key in bag)) continue;
251
+ const raw = bag[key];
252
+ if (field === "messages") {
253
+ const msgs = asMessages(raw);
254
+ if (msgs) return msgs;
255
+ continue;
256
+ }
257
+ if (field === "toolArgs" || field === "toolResult") {
258
+ const s = asContentString(raw);
259
+ if (s) return s;
260
+ continue;
261
+ }
262
+ const s = asContentField(raw);
263
+ if (s) return s;
264
+ }
265
+ }
266
+ const ALL_FIELDS = [
267
+ "task",
268
+ "prompt",
269
+ "completion",
270
+ "toolName",
271
+ "toolArgs",
272
+ "toolResult",
273
+ "messages"
274
+ ];
275
+ /**
276
+ * Extract normalized content from a flattened attribute bag (span attributes
277
+ * OR a log record's merged attribute bag). Pure: same input → same output, no
278
+ * I/O, no env reads. Returns only the fields actually present; an empty/absent
279
+ * bag returns `{}`.
280
+ */
281
+ function extractContent(attributes) {
282
+ const out = {};
283
+ if (!attributes) return out;
284
+ for (const field of ALL_FIELDS) {
285
+ const v = resolveField(attributes, field);
286
+ if (v !== void 0) out[field] = v;
287
+ }
288
+ return out;
289
+ }
290
+ /**
291
+ * The keys whose presence (with a usable value) counts as "this bag carries
292
+ * content". The alias-table keys only — the span-only prompt spellings are
293
+ * deliberately excluded so a metadata row that happens to carry a bare `input`
294
+ * is not mislabelled content-bearing.
295
+ */
296
+ const PRESENCE_KEYS = Object.keys(CONTENT_KEY_ALIASES);
297
+ /** True when the bag carries ANY known content key with a usable value. A cheap
298
+ * presence check that SHORT-CIRCUITS on the first usable key — it does not
299
+ * build the NormalizedContent object or JSON-parse `messages` bodies, so a hot
300
+ * scan path (onboarding) pays per-row only until the first hit. */
301
+ function hasContent(attributes) {
302
+ if (!attributes) return false;
303
+ for (const key of PRESENCE_KEYS) {
304
+ if (!(key in attributes)) continue;
305
+ const field = CONTENT_KEY_ALIASES[key];
306
+ const raw = attributes[key];
307
+ if (field === "messages") {
308
+ if (asMessages(raw)) return true;
309
+ continue;
310
+ }
311
+ if (field === "toolArgs" || field === "toolResult") {
312
+ if (asContentString(raw)) return true;
313
+ continue;
314
+ }
315
+ if (asContentField(raw)) return true;
316
+ }
317
+ return false;
318
+ }
319
+ /**
320
+ * Build the content-detection bag for a single OTLP log record. A log carries
321
+ * its content either as flattened attributes (keyed by a content key) OR as the
322
+ * record `body` for a prompt event whose text has no attribute key (a
323
+ * `*.user_prompt` event whose prompt IS the body). This is the ONE place that
324
+ * decides "how a log row becomes a bag `extractContent` can read", so the
325
+ * intent-audit recovery, the trace-analyst log enrichment, and the onboarding
326
+ * content check can never drift on it.
327
+ *
328
+ * The body is surfaced under the standard `prompt` alias ONLY for `*.user_prompt`
329
+ * events (a generic log body is not content); an attribute-carried `prompt`
330
+ * already present is never overwritten.
331
+ */
332
+ function logRecordContentBag(attributes, body) {
333
+ const bag = { ...attributes ?? {} };
334
+ if (typeof body === "string" && body.trim().length > 0) {
335
+ const eventName = asContentField(bag["event.name"]);
336
+ if (eventName && /(?:^|\.)user_prompt$/.test(eventName) && !("prompt" in bag)) bag.prompt = body;
337
+ }
338
+ return bag;
339
+ }
340
+ /** Known coding-agent service names, mapped to their canonical agent id. An
341
+ * EXACT match only — a substring guess ('claude-helper-bot') would mislabel a
342
+ * customer service into a coding-agent bucket and drive a wrong "you missed
343
+ * THIS gate" onboarding hint. The namespaced-key / gen_ai.system paths above
344
+ * this remain the reliable identity; this allowlist is the last resort for a
345
+ * content-less metadata row carrying only service.name. */
346
+ const KNOWN_AGENT_SERVICE_NAMES = {
347
+ "claude-code": "claude-code",
348
+ "claude code": "claude-code",
349
+ claudecode: "claude-code",
350
+ codex: "codex",
351
+ "codex-cli": "codex",
352
+ opencode: "opencode"
353
+ };
354
+ /**
355
+ * Deterministic coding-agent label for an attribute bag, by the namespace of
356
+ * the content/event keys it carries, then the standard `gen_ai.system` /
357
+ * `service.name` identity. Returns a stable lowercase id
358
+ * (`claude-code` | `codex` | `opencode` | the gen_ai.system name) or
359
+ * `"unknown"` when nothing reliably identifies the emitter — never a fabricated
360
+ * default that pretends to know which agent produced the row.
361
+ *
362
+ * `service.name` is matched against a KNOWN-AGENT ALLOWLIST (exact), not a
363
+ * substring guess, so an arbitrary customer service named e.g. `claude-helper`
364
+ * stays `unknown` rather than being mislabelled `claude-code` and driving a
365
+ * wrong onboarding hint.
366
+ */
367
+ function classifyAgent(bag) {
368
+ if (!bag) return "unknown";
369
+ const keys = Object.keys(bag);
370
+ if (keys.some((k) => k.startsWith("claude_code."))) return "claude-code";
371
+ if (keys.some((k) => k.startsWith("codex."))) return "codex";
372
+ const eventName = asContentField(bag["event.name"]);
373
+ if (eventName?.startsWith("claude_code.")) return "claude-code";
374
+ if (eventName?.startsWith("codex.")) return "codex";
375
+ const system = asContentField(bag["gen_ai.system"]);
376
+ if (system) return system.toLowerCase();
377
+ if (keys.some((k) => k.startsWith("message."))) return "opencode";
378
+ const service = asContentField(bag["service.name"]);
379
+ if (service) {
380
+ const mapped = KNOWN_AGENT_SERVICE_NAMES[service.toLowerCase()];
381
+ if (mapped) return mapped;
382
+ }
383
+ return "unknown";
384
+ }
385
+ /**
386
+ * Recover the declared intent from a flattened attribute bag through the shared
387
+ * task/prompt/messages vocabulary, reporting WHICH key matched. The span path
388
+ * and the log-record path both call this, so they resolve intent through one
389
+ * key source and a key added to the shared layer is read by both. Walks the
390
+ * `task` field's keys, then the `prompt` field's keys (which include the
391
+ * span-only `user_request` / `input` / `llm.input` spellings folded into the
392
+ * priority list), then the first user message of a messages body. Returns null
393
+ * when nothing usable is present — never a fabricated intent.
394
+ */
395
+ function resolveDeclaredIntent(bag) {
396
+ if (!bag) return null;
397
+ for (const field of ["task", "prompt"]) for (const key of fieldKeyOrder(field)) {
398
+ if (!(key in bag)) continue;
399
+ const text = asContentField(bag[key]);
400
+ if (text) return {
401
+ text,
402
+ source: key
403
+ };
404
+ }
405
+ for (const key of fieldKeyOrder("messages")) {
406
+ if (!(key in bag)) continue;
407
+ const msgs = asMessages(bag[key]);
408
+ if (!msgs) continue;
409
+ const text = declaredTaskText({ messages: msgs });
410
+ if (text) return {
411
+ text,
412
+ source: "messages"
413
+ };
414
+ }
415
+ return null;
416
+ }
417
+ /**
418
+ * Best-effort declared-task text from normalized content, in the order a
419
+ * declared intent is most likely to live: an explicit task, then the prompt,
420
+ * then the first user message in a messages array. Deliberately does NOT
421
+ * consult `completion` — an assistant completion is the agent's OUTPUT, not the
422
+ * declared task; treating it as the task would fabricate intent. Returns null
423
+ * when no genuine declared-task source is present — the caller skips, never
424
+ * fabricates.
425
+ */
426
+ function declaredTaskText(content) {
427
+ if (content.task) return content.task;
428
+ if (content.prompt) return content.prompt;
429
+ if (content.messages) for (const m of content.messages) {
430
+ if (!m || typeof m !== "object") continue;
431
+ const msg = m;
432
+ const role = asContentField(msg.role);
433
+ if (role && role !== "user") continue;
434
+ const text = readMessageText(msg);
435
+ if (text) return text;
436
+ }
437
+ return null;
438
+ }
439
+ //#endregion
440
+ //#region src/telemetry/genai-attributes.ts
441
+ /**
442
+ * OpenTelemetry GenAI semantic-convention span attributes — the single
443
+ * vocabulary every trace producer in the stack emits and the intelligence
444
+ * ingest reads, so model / token / cost attribution is identical regardless of
445
+ * which producer lowered the span (workflow emitter, SDK trace sink, …).
446
+ *
447
+ * Writers emit the PRIMARY keys (`GEN_AI_*`). The ingest reader accepts the
448
+ * wider candidate lists (`GEN_AI_*_KEYS`) because foreign SDKs spell the same
449
+ * field differently (Langfuse, raw OTLP GenAI, our own legacy `model`).
450
+ * Co-locating writer keys and reader candidates here is the drift guard: a
451
+ * writer's key and the reader's first candidate can never silently disagree.
452
+ *
453
+ * Spec: OpenTelemetry GenAI semantic conventions (`gen_ai.*`).
454
+ */
455
+ /** Model that served a request — the primary key writers emit. */
456
+ const GEN_AI_REQUEST_MODEL = "gen_ai.request.model";
457
+ /** Model named on a response — a reader fallback; writers prefer request.model. */
458
+ const GEN_AI_RESPONSE_MODEL = "gen_ai.response.model";
459
+ /** Prompt / input token count. */
460
+ const GEN_AI_USAGE_INPUT_TOKENS = "gen_ai.usage.input_tokens";
461
+ /** Completion / output token count. */
462
+ const GEN_AI_USAGE_OUTPUT_TOKENS = "gen_ai.usage.output_tokens";
463
+ /** Operation name (e.g. `"invoke_agent"`). */
464
+ const GEN_AI_OPERATION_NAME = "gen_ai.operation.name";
465
+ /** Conversation / session id grouping a multi-turn agent run. */
466
+ const GEN_AI_CONVERSATION_ID = "gen_ai.conversation.id";
467
+ /**
468
+ * Reader candidate keys for the model, highest priority first. The first entry
469
+ * is the writers' primary key; the rest accept foreign-SDK spellings.
470
+ */
471
+ const GEN_AI_MODEL_KEYS = Object.freeze([
472
+ GEN_AI_REQUEST_MODEL,
473
+ GEN_AI_RESPONSE_MODEL,
474
+ "llm.model_name",
475
+ "model"
476
+ ]);
477
+ /** Reader candidate keys for input tokens, highest priority first. These name
478
+ * SPAN ATTRIBUTES on a lowered span — for the raw producer `tokenUsage` OBJECT
479
+ * field names (a distinct layer) see `TOKEN_USAGE_INPUT_KEYS` in
480
+ * `token-usage.ts`; a new producer shape may need an entry in both. */
481
+ const GEN_AI_INPUT_TOKEN_KEYS = Object.freeze([
482
+ GEN_AI_USAGE_INPUT_TOKENS,
483
+ "gen_ai.usage.prompt_tokens",
484
+ "llm.token_count.prompt"
485
+ ]);
486
+ /** Reader candidate keys for output tokens, highest priority first. */
487
+ const GEN_AI_OUTPUT_TOKEN_KEYS = Object.freeze([
488
+ GEN_AI_USAGE_OUTPUT_TOKENS,
489
+ "gen_ai.usage.completion_tokens",
490
+ "llm.token_count.completion"
491
+ ]);
492
+ function isNonNegativeFinite(n) {
493
+ return typeof n === "number" && Number.isFinite(n) && n >= 0;
494
+ }
495
+ /**
496
+ * Lower a usage record to the GenAI semantic-convention attribute bag, OMITTING
497
+ * any unknown field. A synthesized zero token count or empty model would
498
+ * corrupt every downstream token/cost aggregate, so absent stays absent — the
499
+ * ingest then reads "cost not computed", never "free". This is the writer-side
500
+ * mirror of {@link GEN_AI_MODEL_KEYS} / the `*_TOKEN_KEYS` reader candidates.
501
+ */
502
+ function genAiUsageAttributes(usage) {
503
+ const attrs = {};
504
+ if (typeof usage.model === "string" && usage.model.length > 0) attrs[GEN_AI_REQUEST_MODEL] = usage.model;
505
+ if (isNonNegativeFinite(usage.inputTokens)) attrs[GEN_AI_USAGE_INPUT_TOKENS] = usage.inputTokens;
506
+ if (isNonNegativeFinite(usage.outputTokens)) attrs[GEN_AI_USAGE_OUTPUT_TOKENS] = usage.outputTokens;
507
+ return attrs;
508
+ }
509
+ //#endregion
510
+ //#region src/telemetry/spine-attributes.ts
511
+ /**
512
+ * Spine run/subject/tree + trigger span-attribute vocabulary — the single set of
513
+ * `tangle.*` keys that name a run, its parent, its kind, its trigger, and the
514
+ * durable subject it worked on. Co-located here so the WRITER (the platform
515
+ * workflow-trace-emitter) and the READERS (intelligence-api's run-attrs derivers
516
+ * + reconstruct) key off one source and can never silently drift.
517
+ *
518
+ * Writers emit the PRIMARY keys (`*_KEY`). The ingest reader accepts the wider
519
+ * CANDIDATE lists (`*_ATTRS`) because the emit sites disagree on spelling: the
520
+ * workflow emitter writes camel `tangle.runId`, the agent setup prompt documents
521
+ * dotted `tangle.run.id`. Each candidate list's first entry is a writer primary
522
+ * key, so the drift guard (`spine-attributes.test.ts`) can assert writer/reader
523
+ * agreement.
524
+ *
525
+ * The DERIVATION LOGIC (priority resolution, parent!=own-run guard, run-kind
526
+ * inference, subject `'default'` fallback, `source:id` formatting) lives in
527
+ * intelligence-api's `run-attrs.ts`; only the key constants and candidate lists
528
+ * live here. The foreign subject candidates (`symphony.issue.identifier`,
529
+ * `git.repository`, `service.name`) are members of {@link SUBJECT_KEY_ATTRS}.
530
+ */
531
+ /** Primary run-id key writers stamp; first entry of {@link RUN_KEY_ATTRS}. */
532
+ const TANGLE_RUN_ID_KEY = "tangle.run.id";
533
+ /** Camel run-id key the workflow emitter actually writes on every span. */
534
+ const TANGLE_RUN_ID_CAMEL_KEY = "tangle.runId";
535
+ /** Primary parent-run-id key; first entry of {@link PARENT_RUN_KEY_ATTRS}. */
536
+ const TANGLE_RUN_PARENT_ID_KEY = "tangle.run.parent_id";
537
+ /** Primary run-kind key; the sole entry of {@link RUN_KIND_ATTRS}. */
538
+ const TANGLE_RUN_KIND_KEY = "tangle.run.kind";
539
+ /** Primary subject key; first entry of {@link SUBJECT_KEY_ATTRS}. */
540
+ const TANGLE_SUBJECT_KEY = "tangle.subject.key";
541
+ /** Primary trigger-source key; first entry of {@link TRIGGER_SOURCE_ATTRS}. */
542
+ const TANGLE_TRIGGER_SOURCE_KEY = "tangle.workflow.trigger.source";
543
+ /** Trigger-kind key the workflow emitter writes on the root span. */
544
+ const TANGLE_TRIGGER_KIND_KEY = "tangle.trigger.kind";
545
+ /** Primary trigger-id key; first entry of {@link TRIGGER_ID_ATTRS}. */
546
+ const TANGLE_TRIGGER_ID_KEY = "tangle.workflow.trigger.id";
547
+ /** Workflow-id key the workflow emitter writes on the root span; a
548
+ * {@link WORKFLOW_MARKER_ATTRS} member. */
549
+ const TANGLE_WORKFLOW_ID_KEY = "tangle.workflowId";
550
+ /** Run-id reader candidates, highest priority first. */
551
+ const RUN_KEY_ATTRS = Object.freeze([TANGLE_RUN_ID_KEY, TANGLE_RUN_ID_CAMEL_KEY]);
552
+ /**
553
+ * Parent-run-id reader candidates, highest priority first. After the explicit
554
+ * parent keys come the workflow-run ids, which name a parent only when this span
555
+ * is a distinct child run (the deriver's own-run guard enforces that).
556
+ */
557
+ const PARENT_RUN_KEY_ATTRS = Object.freeze([
558
+ TANGLE_RUN_PARENT_ID_KEY,
559
+ "tangle.run.parentId",
560
+ "tangle.workflow.parent.run.id",
561
+ "tangle.workflow.run.id",
562
+ "tangle.workflow.runId"
563
+ ]);
564
+ /** Trigger-source reader candidates, highest priority first. */
565
+ const TRIGGER_SOURCE_ATTRS = Object.freeze([
566
+ TANGLE_TRIGGER_SOURCE_KEY,
567
+ "tangle.trigger.source",
568
+ TANGLE_TRIGGER_KIND_KEY
569
+ ]);
570
+ /** Trigger-id reader candidates, highest priority first. */
571
+ const TRIGGER_ID_ATTRS = Object.freeze([TANGLE_TRIGGER_ID_KEY, "tangle.trigger.id"]);
572
+ /**
573
+ * Subject-key reader candidates, highest priority first: declared subject key /
574
+ * project, else the agent's own work-item identity, else repo, else service. The
575
+ * deriver appends a `'default'` fallback so no run is orphaned.
576
+ */
577
+ const SUBJECT_KEY_ATTRS = Object.freeze([
578
+ TANGLE_SUBJECT_KEY,
579
+ "tangle.project.name",
580
+ "symphony.issue.identifier",
581
+ "git.repository",
582
+ "service.name"
583
+ ]);
584
+ /** Explicit run-kind reader candidates (exact `workflow`/`consultant`/`session`
585
+ * wins in the deriver). */
586
+ const RUN_KIND_ATTRS = Object.freeze([TANGLE_RUN_KIND_KEY]);
587
+ /** Workflow-presence markers — any one present implies a `workflow` run kind. */
588
+ const WORKFLOW_MARKER_ATTRS = Object.freeze([
589
+ "tangle.workflow.id",
590
+ TANGLE_WORKFLOW_ID_KEY,
591
+ "tangle.workflow.name",
592
+ "tangle.workflow.run.id",
593
+ "tangle.workflow.runId"
594
+ ]);
595
+ //#endregion
596
+ //#region src/telemetry/token-usage.ts
597
+ /**
598
+ * Candidate field names for a producer-supplied `tokenUsage` bag on a
599
+ * `message.updated` trace event. Agents and SDK layers disagree on the
600
+ * spelling: `StreamTokenUsage` emits `inputTokens`/`outputTokens`, the eval and
601
+ * workflow agent.run paths emit `input`/`output`, and persisted / foreign
602
+ * shapes use snake_case or prompt/completion naming. A reader that wants the
603
+ * token counts must try all of them, highest-priority first, or it silently
604
+ * drops usage for the producers it does not name.
605
+ *
606
+ * Shared so every `tokenUsage` reader (the trace sink's root-span aggregation,
607
+ * the signal extractor) keys off the identical set and cannot drift.
608
+ *
609
+ * Distinct layer from `genai-attributes.ts`: those keys (`GEN_AI_*_TOKEN_KEYS`)
610
+ * name OTel SPAN ATTRIBUTES on an already-lowered span; these name the fields of
611
+ * the raw producer `tokenUsage` OBJECT before lowering. A new producer that
612
+ * spells token usage differently may need an entry in BOTH places.
613
+ */
614
+ const TOKEN_USAGE_INPUT_KEYS = Object.freeze([
615
+ "inputTokens",
616
+ "input",
617
+ "input_tokens",
618
+ "promptTokens",
619
+ "prompt_tokens"
620
+ ]);
621
+ const TOKEN_USAGE_OUTPUT_KEYS = Object.freeze([
622
+ "outputTokens",
623
+ "output",
624
+ "output_tokens",
625
+ "completionTokens",
626
+ "completion_tokens"
627
+ ]);
628
+ const TOKEN_USAGE_COST_KEYS = Object.freeze([
629
+ "totalCostUsd",
630
+ "costUsd",
631
+ "total_cost_usd",
632
+ "cost_usd",
633
+ "cost"
634
+ ]);
635
+ function tokenCount(value) {
636
+ if (typeof value === "boolean") return void 0;
637
+ let n;
638
+ if (typeof value === "number") n = value;
639
+ else if (typeof value === "string") {
640
+ const trimmed = value.trim();
641
+ n = trimmed === "" ? NaN : Number(trimmed);
642
+ } else n = NaN;
643
+ if (!Number.isFinite(n) || n < 0) return void 0;
644
+ return Math.trunc(n);
645
+ }
646
+ function firstTokenCount(source, keys) {
647
+ if (!source) return void 0;
648
+ for (const key of keys) {
649
+ const count = tokenCount(source[key]);
650
+ if (count !== void 0) return count;
651
+ }
652
+ }
653
+ function finiteNonNegativeNumber(value) {
654
+ if (typeof value === "boolean") return void 0;
655
+ let n;
656
+ if (typeof value === "number") n = value;
657
+ else if (typeof value === "string") {
658
+ const trimmed = value.trim();
659
+ n = trimmed === "" ? NaN : Number(trimmed);
660
+ } else n = NaN;
661
+ return Number.isFinite(n) && n >= 0 ? n : void 0;
662
+ }
663
+ function firstUsageCostUsd(source, keys = TOKEN_USAGE_COST_KEYS) {
664
+ if (!source) return void 0;
665
+ for (const key of keys) {
666
+ const cost = finiteNonNegativeNumber(source[key]);
667
+ if (cost !== void 0) return cost;
668
+ }
669
+ }
670
+ function tokenUsageSource(data) {
671
+ if (data.tokenUsage && typeof data.tokenUsage === "object") return data.tokenUsage;
672
+ if (data.usage && typeof data.usage === "object") return data.usage;
673
+ return data;
674
+ }
675
+ function readTokenUsage(data) {
676
+ const source = tokenUsageSource(data);
677
+ const inputTokens = firstTokenCount(source, TOKEN_USAGE_INPUT_KEYS);
678
+ const outputTokens = firstTokenCount(source, TOKEN_USAGE_OUTPUT_KEYS);
679
+ if (inputTokens === void 0 && outputTokens === void 0) return;
680
+ return {
681
+ inputTokens: inputTokens ?? 0,
682
+ outputTokens: outputTokens ?? 0
683
+ };
684
+ }
685
+ function readTokenCostUsd(data) {
686
+ const direct = firstUsageCostUsd(data);
687
+ if (direct !== void 0) return direct;
688
+ return firstUsageCostUsd(tokenUsageSource(data));
689
+ }
690
+ function addTokenUsage(current, next) {
691
+ return current ? {
692
+ inputTokens: current.inputTokens + next.inputTokens,
693
+ outputTokens: current.outputTokens + next.outputTokens
694
+ } : next;
695
+ }
696
+ //#endregion
697
+ export { GEN_AI_OPERATION_NAME as A, CONTENT_PROMPT_KEY as B, TANGLE_WORKFLOW_ID_KEY as C, GEN_AI_CONVERSATION_ID as D, WORKFLOW_MARKER_ATTRS as E, GEN_AI_USAGE_OUTPUT_TOKENS as F, asContentString as G, CONTENT_TOOL_NAME_KEY as H, genAiUsageAttributes as I, extractContent as J, classifyAgent as K, CONTENT_COMPLETION_KEY as L, GEN_AI_REQUEST_MODEL as M, GEN_AI_RESPONSE_MODEL as N, GEN_AI_INPUT_TOKEN_KEYS as O, GEN_AI_USAGE_INPUT_TOKENS as P, CONTENT_KEYS as R, TANGLE_TRIGGER_SOURCE_KEY as S, TRIGGER_SOURCE_ATTRS as T, CONTENT_TOOL_RESULT_KEY as U, CONTENT_TOOL_ARGS_KEY as V, asContentField as W, logRecordContentBag as X, hasContent as Y, resolveDeclaredIntent as Z, TANGLE_RUN_KIND_KEY as _, firstTokenCount as a, TANGLE_TRIGGER_ID_KEY as b, readTokenUsage as c, PARENT_RUN_KEY_ATTRS as d, RUN_KEY_ATTRS as f, TANGLE_RUN_ID_KEY as g, TANGLE_RUN_ID_CAMEL_KEY as h, addTokenUsage as i, GEN_AI_OUTPUT_TOKEN_KEYS as j, GEN_AI_MODEL_KEYS as k, tokenCount as l, SUBJECT_KEY_ATTRS as m, TOKEN_USAGE_INPUT_KEYS as n, firstUsageCostUsd as o, RUN_KIND_ATTRS as p, declaredTaskText as q, TOKEN_USAGE_OUTPUT_KEYS as r, readTokenCostUsd as s, TOKEN_USAGE_COST_KEYS as t, tokenUsageSource as u, TANGLE_RUN_PARENT_ID_KEY as v, TRIGGER_ID_ATTRS as w, TANGLE_TRIGGER_KIND_KEY as x, TANGLE_SUBJECT_KEY as y, CONTENT_KEY_FIELD as z };
698
+
699
+ //# sourceMappingURL=telemetry-D2XGH5UL.js.map