@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.
- package/dist/auth/browser.d.ts +1 -1
- package/dist/auth/index.d.ts +1 -1
- package/dist/events/index.d.ts +1 -1
- package/dist/{index-CtebBodu.d.ts → index-BOD4IhTr.d.ts} +2 -2
- package/dist/{index-CtebBodu.d.ts.map → index-BOD4IhTr.d.ts.map} +1 -1
- package/dist/{index-DiQX9bjq.d.ts → index-BRyMBSkE.d.ts} +1 -1
- package/dist/{index-DiQX9bjq.d.ts.map → index-BRyMBSkE.d.ts.map} +1 -1
- package/dist/{index-DlPznmKA.d.ts → index-C7zlWVGc.d.ts} +1 -1
- package/dist/{index-DlPznmKA.d.ts.map → index-C7zlWVGc.d.ts.map} +1 -1
- package/dist/index-CBGkvDtv.d.ts +303 -0
- package/dist/index-CBGkvDtv.d.ts.map +1 -0
- package/dist/{index-z2wPdzFb.d.ts → index-Dz1ZLn3K.d.ts} +2 -2
- package/dist/{index-z2wPdzFb.d.ts.map → index-Dz1ZLn3K.d.ts.map} +1 -1
- package/dist/{index-CFA8kgHJ.d.ts → index-VcOZI9jN.d.ts} +1 -1
- package/dist/{index-CFA8kgHJ.d.ts.map → index-VcOZI9jN.d.ts.map} +1 -1
- package/dist/index.d.ts +8 -85
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -106
- package/dist/index.js.map +1 -1
- package/dist/resilience/index.d.ts +1 -1
- package/dist/telemetry/index.d.ts +2 -0
- package/dist/telemetry/index.js +2 -0
- package/dist/telemetry-D2XGH5UL.js +699 -0
- package/dist/telemetry-D2XGH5UL.js.map +1 -0
- package/dist/transport/index.d.ts +1 -1
- package/dist/transport/index.js +1 -1
- package/dist/{transport-mKLoQep5.js → transport-XLxpQmUW.js} +1 -1
- package/dist/{transport-mKLoQep5.js.map → transport-XLxpQmUW.js.map} +1 -1
- package/dist/types/index.d.ts +1 -1
- package/dist/types/index.js +1 -1
- package/dist/{types-D5VLaol0.js → types-Ci7hcTnp.js} +1 -1
- package/dist/{types-D5VLaol0.js.map → types-Ci7hcTnp.js.map} +1 -1
- package/dist/{types-DiDSGM8K.d.ts → types-D32txapu.d.ts} +2 -2
- package/dist/{types-DiDSGM8K.d.ts.map → types-D32txapu.d.ts.map} +1 -1
- 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
|