@runtypelabs/flue-otel 0.1.0

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/index.cjs ADDED
@@ -0,0 +1,939 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ ADAPTER_NAME: () => ADAPTER_NAME,
24
+ ADAPTER_VERSION: () => ADAPTER_VERSION,
25
+ GEN_AI: () => GEN_AI,
26
+ RUNTYPE: () => RUNTYPE,
27
+ RUNTYPE_FLUE_INSTRUMENTATION_KEY: () => RUNTYPE_FLUE_INSTRUMENTATION_KEY,
28
+ RUNTYPE_SCHEMA_VERSION: () => RUNTYPE_SCHEMA_VERSION,
29
+ RUNTYPE_STOP_REASONS: () => RUNTYPE_STOP_REASONS,
30
+ RUNTYPE_TOOL_TYPES: () => RUNTYPE_TOOL_TYPES,
31
+ createRuntypeFlueInstrumentation: () => createRuntypeFlueInstrumentation,
32
+ runtypeFlueResourceAttributes: () => runtypeFlueResourceAttributes
33
+ });
34
+ module.exports = __toCommonJS(index_exports);
35
+ var import_api2 = require("@opentelemetry/api");
36
+
37
+ // src/compat.ts
38
+ function resolveToolType(toolName, detail) {
39
+ if (isTaskDelegationTool(toolName, detail)) return "subagent";
40
+ if (detail.toolType === "datastore") return "data_connection";
41
+ return void 0;
42
+ }
43
+ function isTaskDelegationTool(toolName, detail) {
44
+ return detail.origin === "framework" && toolName === "task";
45
+ }
46
+ function isCallerShellCall(toolName, detail) {
47
+ return detail.origin === "caller" && toolName === "bash";
48
+ }
49
+ function mapFinishReason(finishReason) {
50
+ if (!finishReason) return void 0;
51
+ const normalized = finishReason.toLowerCase().replace(/[\s-]+/g, "_");
52
+ if (END_TURN_REASONS.has(normalized)) return "end_turn";
53
+ if (LENGTH_REASONS.has(normalized)) return "length";
54
+ if (CONTENT_FILTER_REASONS.has(normalized)) return "content_filter";
55
+ if (ERROR_REASONS.has(normalized)) return "error";
56
+ return "unknown";
57
+ }
58
+ var END_TURN_REASONS = /* @__PURE__ */ new Set([
59
+ "stop",
60
+ "end_turn",
61
+ "stop_sequence",
62
+ "complete",
63
+ "completed"
64
+ ]);
65
+ var LENGTH_REASONS = /* @__PURE__ */ new Set(["length", "max_tokens", "maxtokens"]);
66
+ var CONTENT_FILTER_REASONS = /* @__PURE__ */ new Set([
67
+ "content_filter",
68
+ "contentfilter",
69
+ "refusal",
70
+ "safety"
71
+ ]);
72
+ var ERROR_REASONS = /* @__PURE__ */ new Set(["error", "aborted", "abort", "cancelled"]);
73
+ function mapSettlementOutcome(outcome) {
74
+ if (outcome === "failed" || outcome === "aborted") return "error";
75
+ return void 0;
76
+ }
77
+
78
+ // package.json
79
+ var version = "0.1.0";
80
+
81
+ // src/semconv.ts
82
+ var GEN_AI = {
83
+ operationName: "gen_ai.operation.name",
84
+ providerName: "gen_ai.provider.name",
85
+ agentName: "gen_ai.agent.name",
86
+ conversationId: "gen_ai.conversation.id",
87
+ requestModel: "gen_ai.request.model",
88
+ responseModel: "gen_ai.response.model",
89
+ responseId: "gen_ai.response.id",
90
+ requestStream: "gen_ai.request.stream",
91
+ reasoningLevel: "gen_ai.request.reasoning.level",
92
+ maxTokens: "gen_ai.request.max_tokens",
93
+ temperature: "gen_ai.request.temperature",
94
+ finishReasons: "gen_ai.response.finish_reasons",
95
+ usageInputTokens: "gen_ai.usage.input_tokens",
96
+ usageOutputTokens: "gen_ai.usage.output_tokens",
97
+ usageCacheReadTokens: "gen_ai.usage.cache_read.input_tokens",
98
+ usageCacheCreationTokens: "gen_ai.usage.cache_creation.input_tokens",
99
+ toolName: "gen_ai.tool.name",
100
+ toolCallId: "gen_ai.tool.call.id",
101
+ toolType: "gen_ai.tool.type",
102
+ conversationCompacted: "gen_ai.conversation.compacted",
103
+ errorType: "error.type",
104
+ serverAddress: "server.address",
105
+ serverPort: "server.port"
106
+ };
107
+ var GEN_AI_OPERATION_INVOKE_AGENT = "invoke_agent";
108
+ var GEN_AI_OPERATION_CHAT = "chat";
109
+ var GEN_AI_OPERATION_EXECUTE_TOOL = "execute_tool";
110
+ var RUNTYPE = {
111
+ agentId: "runtype.agent.id",
112
+ schemaVersion: "runtype.schema.version",
113
+ adapterName: "runtype.adapter.name",
114
+ adapterVersion: "runtype.adapter.version",
115
+ executionId: "runtype.execution.id",
116
+ iteration: "runtype.iteration",
117
+ stopReason: "runtype.stop_reason",
118
+ toolsReported: "runtype.tools.reported",
119
+ toolType: "runtype.tool.type",
120
+ turnId: "runtype.turn.id",
121
+ turnIndex: "runtype.turn.index"
122
+ };
123
+ var FLUE = {
124
+ instanceId: "flue.instance.id",
125
+ submissionId: "flue.submission.id",
126
+ agentName: "flue.agent.name",
127
+ harnessName: "flue.harness.name",
128
+ sessionName: "flue.session.name",
129
+ parentSessionName: "flue.parent_session.name",
130
+ operationId: "flue.operation.id",
131
+ operationKind: "flue.operation.kind",
132
+ turnId: "flue.turn.id",
133
+ turnPurpose: "flue.turn.purpose",
134
+ taskId: "flue.task.id",
135
+ eventIndex: "flue.event.index",
136
+ toolOrigin: "flue.tool.origin",
137
+ compactionReason: "flue.compaction.reason",
138
+ usageTotalTokens: "flue.usage.total_tokens"
139
+ };
140
+ var RUNTYPE_TOOL_TYPES = [
141
+ "flow",
142
+ "mcp",
143
+ "builtin",
144
+ "custom",
145
+ "external",
146
+ "advisor",
147
+ "subagent",
148
+ "local",
149
+ "data_connection",
150
+ "search"
151
+ ];
152
+ var RUNTYPE_STOP_REASONS = [
153
+ "end_turn",
154
+ "max_tool_calls",
155
+ "length",
156
+ "content_filter",
157
+ "error",
158
+ "unknown"
159
+ ];
160
+ var RUNTYPE_SCHEMA_VERSION = "1";
161
+ var ADAPTER_NAME = "@runtypelabs/flue-otel";
162
+ var ADAPTER_VERSION = version;
163
+
164
+ // src/projection.ts
165
+ function runtypeFlueResourceAttributes(resource) {
166
+ return {
167
+ [RUNTYPE.schemaVersion]: RUNTYPE_SCHEMA_VERSION,
168
+ [RUNTYPE.adapterName]: ADAPTER_NAME,
169
+ [RUNTYPE.adapterVersion]: ADAPTER_VERSION,
170
+ ...resource?.agentId ? { [RUNTYPE.agentId]: resource.agentId } : {}
171
+ };
172
+ }
173
+ function createFlueProjection(options = {}) {
174
+ const open = /* @__PURE__ */ new Map();
175
+ const envelopes = /* @__PURE__ */ new Map();
176
+ const envelopeByChain = /* @__PURE__ */ new Map();
177
+ const envelopeBySubmission = /* @__PURE__ */ new Map();
178
+ const envelopeByOperationId = /* @__PURE__ */ new Map();
179
+ const envelopeByTaskScope = /* @__PURE__ */ new Map();
180
+ const turnPositions = /* @__PURE__ */ new Map();
181
+ const taskTurnCounts = /* @__PURE__ */ new Map();
182
+ function observe(observation) {
183
+ switch (observation.type) {
184
+ case "operation_start":
185
+ return onOperationStart(observation);
186
+ case "operation":
187
+ return onOperationEnd(observation);
188
+ case "task_start":
189
+ return onTaskStart(observation);
190
+ case "task":
191
+ return onTaskEnd(observation);
192
+ case "compaction_start":
193
+ return onCompactionStart(observation);
194
+ case "compaction":
195
+ return onCompactionEnd(observation);
196
+ case "turn_request":
197
+ return onTurnRequest(observation);
198
+ case "turn":
199
+ return onTurnEnd(observation);
200
+ case "tool_start":
201
+ return onToolStart(observation);
202
+ case "tool":
203
+ return onToolEnd(observation);
204
+ case "submission_settled":
205
+ return onSubmissionSettled(observation);
206
+ default:
207
+ return [];
208
+ }
209
+ }
210
+ function onOperationStart(event) {
211
+ const kind = readString(event, "operationKind");
212
+ if (kind !== "prompt" && kind !== "skill") return [];
213
+ const ref = operationKey(event);
214
+ if (open.has(ref)) return [];
215
+ if (event.taskId && open.has(taskKey(event))) return [];
216
+ const parentRef = resolveParentRef(event);
217
+ const chain = chainKey(event);
218
+ const isEnvelope = !envelopeByChain.has(chain);
219
+ if (isEnvelope) {
220
+ envelopeByChain.set(chain, ref);
221
+ if (event.submissionId) envelopeBySubmission.set(event.submissionId, ref);
222
+ if (event.operationId) envelopeByOperationId.set(event.operationId, ref);
223
+ envelopes.set(ref, freshEnvelope());
224
+ }
225
+ open.set(ref, {
226
+ ref,
227
+ ...parentRef ? { parentRef } : {},
228
+ chainKey: chain,
229
+ isTask: false,
230
+ ...isEnvelope ? {
231
+ scope: {
232
+ instanceId: event.instanceId,
233
+ harness: event.harness,
234
+ conversationId: event.conversationId,
235
+ session: event.session
236
+ }
237
+ } : {}
238
+ });
239
+ const agentId = event.agentName ? options.agents?.[event.agentName] : void 0;
240
+ return [
241
+ {
242
+ kind: "open",
243
+ ref,
244
+ name: isEnvelope ? event.agentName ? `invoke_agent ${event.agentName}` : "invoke_agent" : `flue.operation ${kind}`,
245
+ spanKind: "internal",
246
+ ...parentRef ? { parentRef } : {},
247
+ ...event.timestamp ? { startTime: event.timestamp } : {},
248
+ attributes: {
249
+ ...identifiers(event),
250
+ [FLUE.operationKind]: kind,
251
+ ...isEnvelope ? { [GEN_AI.operationName]: GEN_AI_OPERATION_INVOKE_AGENT } : {},
252
+ ...isEnvelope && event.agentName ? { [GEN_AI.agentName]: event.agentName } : {},
253
+ ...isEnvelope && event.conversationId ? { [GEN_AI.conversationId]: event.conversationId } : {},
254
+ // The execution id LIFTS the tier and is claim-checked at ingest like
255
+ // any inbound id; it does not key the row, which stays derived from
256
+ // the trace id.
257
+ ...isEnvelope && event.submissionId ? { [RUNTYPE.executionId]: event.submissionId } : {},
258
+ ...isEnvelope && agentId ? { [RUNTYPE.agentId]: agentId } : {}
259
+ }
260
+ }
261
+ ];
262
+ }
263
+ function onOperationEnd(event) {
264
+ const ref = operationKey(event);
265
+ const record = open.get(ref);
266
+ if (!record) return [];
267
+ const intents = sweepDescendants(ref, event.timestamp);
268
+ const envelope = envelopes.get(ref);
269
+ const isError = readBoolean(event, "isError") === true;
270
+ if (envelope) {
271
+ intents.push({ kind: "update", ref, attributes: envelopeAttributes(envelope, isError) });
272
+ envelopes.delete(ref);
273
+ if (envelopeByChain.get(record.chainKey) === ref) envelopeByChain.delete(record.chainKey);
274
+ for (const [submissionId, submissionRef] of envelopeBySubmission) {
275
+ if (submissionRef === ref) envelopeBySubmission.delete(submissionId);
276
+ }
277
+ for (const [operationId, operationRef] of envelopeByOperationId) {
278
+ if (operationRef === ref) envelopeByOperationId.delete(operationId);
279
+ }
280
+ for (const [taskScope, taskEnvelopeRef] of envelopeByTaskScope) {
281
+ if (taskEnvelopeRef === ref) envelopeByTaskScope.delete(taskScope);
282
+ }
283
+ for (const [key, position] of turnPositions) {
284
+ if (position.envelopeRef === ref) turnPositions.delete(key);
285
+ }
286
+ }
287
+ intents.push(closeIntent(ref, event, isError, readErrorInfo(event)));
288
+ open.delete(ref);
289
+ return intents;
290
+ }
291
+ function onTaskStart(event) {
292
+ const ref = taskKey(event);
293
+ if (open.has(ref)) return [];
294
+ const envelopeRef = event.operationId ? envelopeByOperationId.get(event.operationId) : void 0;
295
+ if (envelopeRef) envelopeByTaskScope.set(ref, envelopeRef);
296
+ const callerScope = {
297
+ ...envelopeRef ? open.get(envelopeRef)?.scope ?? {} : {},
298
+ operationId: event.operationId,
299
+ turnId: event.turnId,
300
+ toolCallId: event.toolCallId
301
+ };
302
+ const parentRef = resolveParentRef(callerScope) ?? envelopeRef;
303
+ open.set(ref, {
304
+ ref,
305
+ ...parentRef ? { parentRef } : {},
306
+ chainKey: chainKey(event),
307
+ isTask: true
308
+ });
309
+ taskTurnCounts.set(ref, 0);
310
+ const agent = readString(event, "agent");
311
+ return [
312
+ {
313
+ kind: "open",
314
+ ref,
315
+ // Deliberately NOT `invoke_agent`: see this module's header. A second
316
+ // agent-invocation span in the trace is a coin flip over which one
317
+ // becomes the run's envelope.
318
+ name: agent ? `flue.task ${agent}` : "flue.task",
319
+ spanKind: "internal",
320
+ ...parentRef ? { parentRef } : {},
321
+ ...event.timestamp ? { startTime: event.timestamp } : {},
322
+ attributes: {
323
+ ...identifiers(event),
324
+ ...agent ? { [GEN_AI.agentName]: agent } : {}
325
+ }
326
+ }
327
+ ];
328
+ }
329
+ function onTaskEnd(event) {
330
+ const ref = taskKey(event);
331
+ if (!open.has(ref)) return [];
332
+ const intents = sweepDescendants(ref, event.timestamp);
333
+ intents.push(
334
+ closeIntent(ref, event, readBoolean(event, "isError") === true, readErrorInfo(event))
335
+ );
336
+ open.delete(ref);
337
+ taskTurnCounts.delete(ref);
338
+ return intents;
339
+ }
340
+ function onCompactionStart(event) {
341
+ const ref = compactionKey(event);
342
+ if (open.has(ref)) return [];
343
+ const parentRef = resolveParentRef(event);
344
+ open.set(ref, { ref, ...parentRef ? { parentRef } : {}, chainKey: chainKey(event), isTask: false });
345
+ const reason = readString(event, "reason");
346
+ return [
347
+ {
348
+ kind: "open",
349
+ ref,
350
+ name: "flue.compaction",
351
+ spanKind: "internal",
352
+ // `session.compact()` is callable by the host outside any turn, and a
353
+ // `compact` operation is not intercepted, so this can be parentless.
354
+ requiresParent: true,
355
+ ...parentRef ? { parentRef } : {},
356
+ ...event.timestamp ? { startTime: event.timestamp } : {},
357
+ attributes: {
358
+ ...identifiers(event),
359
+ ...reason ? { [FLUE.compactionReason]: reason } : {}
360
+ }
361
+ }
362
+ ];
363
+ }
364
+ function onCompactionEnd(event) {
365
+ const ref = compactionKey(event);
366
+ if (!open.has(ref)) return [];
367
+ const intents = sweepDescendants(ref, event.timestamp);
368
+ intents.push(
369
+ closeIntent(ref, event, readBoolean(event, "isError") === true, readErrorInfo(event))
370
+ );
371
+ open.delete(ref);
372
+ return intents;
373
+ }
374
+ function onTurnRequest(event) {
375
+ const ref = turnKey(event);
376
+ if (open.has(ref)) return [];
377
+ const request = readRecord(event, "request");
378
+ if (!request) return [];
379
+ const purpose = readString(event, "purpose") ?? "agent";
380
+ const compactionRef = compactionKey(event);
381
+ const parentRef = purpose !== "agent" && open.has(compactionRef) ? compactionRef : resolveParentRef(event);
382
+ open.set(ref, { ref, ...parentRef ? { parentRef } : {}, chainKey: chainKey(event), isTask: false });
383
+ const envelopeRef = resolveEnvelopeRef(event);
384
+ const envelope = envelopeRef ? envelopes.get(envelopeRef) : void 0;
385
+ let position;
386
+ if (purpose === "agent") {
387
+ if (!event.taskId && envelope && envelopeRef) {
388
+ position = {
389
+ turnId: event.turnId ?? "",
390
+ index: envelope.turnCount,
391
+ atEnvelopeScope: true,
392
+ envelopeRef
393
+ };
394
+ envelope.turnCount += 1;
395
+ envelope.maxIteration = position.index;
396
+ } else if (event.taskId) {
397
+ const taskRef = taskKey(event);
398
+ const index = taskTurnCounts.get(taskRef) ?? 0;
399
+ taskTurnCounts.set(taskRef, index + 1);
400
+ position = {
401
+ turnId: event.turnId ?? "",
402
+ index,
403
+ atEnvelopeScope: false,
404
+ ...envelopeRef ? { envelopeRef } : {}
405
+ };
406
+ }
407
+ if (position) turnPositions.set(ref, position);
408
+ }
409
+ if (envelope) {
410
+ if (!envelope.modelPinned && purpose === "agent") envelope.requestModel = request.requestedModel;
411
+ if (Array.isArray(request.input?.tools)) envelope.toolsReported = true;
412
+ }
413
+ return [
414
+ {
415
+ kind: "open",
416
+ ref,
417
+ name: request.requestedModel ? `${GEN_AI_OPERATION_CHAT} ${request.requestedModel}` : GEN_AI_OPERATION_CHAT,
418
+ spanKind: "client",
419
+ ...parentRef ? { parentRef } : {},
420
+ ...event.timestamp ? { startTime: event.timestamp } : {},
421
+ attributes: {
422
+ ...identifiers(event),
423
+ [GEN_AI.operationName]: GEN_AI_OPERATION_CHAT,
424
+ [FLUE.turnPurpose]: purpose,
425
+ ...request.providerName ? { [GEN_AI.providerName]: request.providerName } : {},
426
+ ...request.requestedModel ? { [GEN_AI.requestModel]: request.requestedModel } : {},
427
+ ...event.agentName ? { [GEN_AI.agentName]: event.agentName } : {},
428
+ ...event.conversationId ? { [GEN_AI.conversationId]: event.conversationId } : {},
429
+ ...request.reasoningLevel ? { [GEN_AI.reasoningLevel]: request.reasoningLevel } : {},
430
+ ...typeof request.maxTokens === "number" ? { [GEN_AI.maxTokens]: request.maxTokens } : {},
431
+ ...typeof request.temperature === "number" ? { [GEN_AI.temperature]: request.temperature } : {},
432
+ ...request.serverAddress ? { [GEN_AI.serverAddress]: request.serverAddress } : {},
433
+ ...typeof request.serverPort === "number" ? { [GEN_AI.serverPort]: request.serverPort } : {},
434
+ ...request.contextCompacted ? { [GEN_AI.conversationCompacted]: true } : {},
435
+ ...positionAttributes(position)
436
+ }
437
+ }
438
+ ];
439
+ }
440
+ function onTurnEnd(event) {
441
+ const ref = turnKey(event);
442
+ if (!open.has(ref)) return [];
443
+ const response = readRecord(event, "response") ?? {};
444
+ const purpose = readString(event, "purpose") ?? "agent";
445
+ const isError = readBoolean(event, "isError") === true;
446
+ const envelopeRef = resolveEnvelopeRef(event);
447
+ const envelope = envelopeRef ? envelopes.get(envelopeRef) : void 0;
448
+ if (envelope) {
449
+ if (!envelope.modelPinned && purpose === "agent") {
450
+ envelope.responseModel = response.responseModel;
451
+ envelope.modelPinned = true;
452
+ }
453
+ foldUsage(envelope, response.usage);
454
+ if (purpose === "agent" && response.finishReason) {
455
+ envelope.lastFinishReason = response.finishReason;
456
+ }
457
+ }
458
+ const attributes = {
459
+ ...response.responseModel ? { [GEN_AI.responseModel]: response.responseModel } : {},
460
+ ...response.responseId ? { [GEN_AI.responseId]: response.responseId } : {},
461
+ ...response.finishReason ? { [GEN_AI.finishReasons]: [response.finishReason] } : {},
462
+ ...usageAttributes(response.usage)
463
+ };
464
+ const intents = [];
465
+ if (Object.keys(attributes).length > 0) intents.push({ kind: "update", ref, attributes });
466
+ intents.push(closeIntent(ref, event, isError, response.error ?? readErrorInfo(event)));
467
+ open.delete(ref);
468
+ return intents;
469
+ }
470
+ function onToolStart(event) {
471
+ const toolName = readString(event, "toolName");
472
+ const toolCallId = readString(event, "toolCallId");
473
+ if (!toolName || !toolCallId) return [];
474
+ const ref = toolKey(event);
475
+ if (open.has(ref)) return [];
476
+ const parentRef = resolveParentRef(event);
477
+ open.set(ref, { ref, ...parentRef ? { parentRef } : {}, chainKey: chainKey(event), isTask: false });
478
+ const position = turnPositions.get(turnKey(event));
479
+ if (isCallerShellCall(toolName, event)) {
480
+ return [
481
+ {
482
+ kind: "open",
483
+ ref,
484
+ name: "flue.operation shell",
485
+ spanKind: "internal",
486
+ // `session.shell()` is callable by the host outside any turn, and a
487
+ // `shell` operation is not intercepted, so this can be parentless.
488
+ requiresParent: true,
489
+ ...parentRef ? { parentRef } : {},
490
+ ...event.timestamp ? { startTime: event.timestamp } : {},
491
+ attributes: {
492
+ ...identifiers(event),
493
+ ...event.origin ? { [FLUE.toolOrigin]: event.origin } : {},
494
+ ...positionAttributes(position)
495
+ }
496
+ }
497
+ ];
498
+ }
499
+ const isDelegation = isTaskDelegationTool(toolName, event);
500
+ const toolType = resolveToolType(toolName, event);
501
+ return [
502
+ {
503
+ kind: "open",
504
+ ref,
505
+ name: `${GEN_AI_OPERATION_EXECUTE_TOOL} ${toolName}`,
506
+ spanKind: "internal",
507
+ ...parentRef ? { parentRef } : {},
508
+ ...event.timestamp ? { startTime: event.timestamp } : {},
509
+ attributes: {
510
+ ...identifiers(event),
511
+ [GEN_AI.operationName]: GEN_AI_OPERATION_EXECUTE_TOOL,
512
+ [GEN_AI.toolName]: toolName,
513
+ [GEN_AI.toolCallId]: toolCallId,
514
+ // The semconv `gen_ai.tool.type` domain is the transport-level one
515
+ // (`function` / `extension` / `datastore`); a sub-agent delegation is
516
+ // not a plain function call, so it is left unclaimed there while
517
+ // `runtype.tool.type` says what it actually is.
518
+ ...isDelegation ? {} : { [GEN_AI.toolType]: "function" },
519
+ ...toolType ? { [RUNTYPE.toolType]: toolType } : {},
520
+ ...event.origin ? { [FLUE.toolOrigin]: event.origin } : {},
521
+ ...event.agentName ? { [GEN_AI.agentName]: event.agentName } : {},
522
+ ...event.conversationId ? { [GEN_AI.conversationId]: event.conversationId } : {},
523
+ ...positionAttributes(position)
524
+ }
525
+ }
526
+ ];
527
+ }
528
+ function onToolEnd(event) {
529
+ const ref = toolKey(event);
530
+ if (!open.has(ref)) return [];
531
+ const intents = sweepDescendants(ref, event.timestamp);
532
+ intents.push(
533
+ closeIntent(ref, event, readBoolean(event, "isError") === true, readErrorInfo(event))
534
+ );
535
+ open.delete(ref);
536
+ return intents;
537
+ }
538
+ function onSubmissionSettled(event) {
539
+ const outcome = readString(event, "outcome");
540
+ if (!outcome) return [];
541
+ const stopReason = mapSettlementOutcome(outcome);
542
+ if (!stopReason) return [];
543
+ const submissionId = readString(event, "submissionId");
544
+ if (!submissionId) return [];
545
+ const ref = envelopeBySubmission.get(submissionId);
546
+ const envelope = ref ? envelopes.get(ref) : void 0;
547
+ if (envelope) envelope.settlementStopReason = stopReason;
548
+ return [];
549
+ }
550
+ function envelopeAttributes(envelope, isError) {
551
+ const stopReason = isError ? "error" : envelope.settlementStopReason ?? mapFinishReason(envelope.lastFinishReason);
552
+ return {
553
+ ...envelope.requestModel ? { [GEN_AI.requestModel]: envelope.requestModel } : {},
554
+ ...envelope.responseModel ? { [GEN_AI.responseModel]: envelope.responseModel } : {},
555
+ ...envelope.sawUsage ? {
556
+ [GEN_AI.usageInputTokens]: envelope.inputTokens,
557
+ [GEN_AI.usageOutputTokens]: envelope.outputTokens,
558
+ [GEN_AI.usageCacheReadTokens]: envelope.cacheReadTokens,
559
+ [GEN_AI.usageCacheCreationTokens]: envelope.cacheCreationTokens
560
+ } : {},
561
+ ...stopReason ? { [RUNTYPE.stopReason]: stopReason } : {},
562
+ ...envelope.toolsReported ? { [RUNTYPE.toolsReported]: true } : {},
563
+ // The highest loop position reached. Ingest records `max + 1` as the run's
564
+ // iteration count, so the envelope alone reports it correctly even when
565
+ // the batch carrying the model turns flushed earlier.
566
+ ...envelope.maxIteration !== null ? { [RUNTYPE.iteration]: envelope.maxIteration } : {}
567
+ };
568
+ }
569
+ function resolveEnvelopeRef(event) {
570
+ if (event.taskId) {
571
+ const viaTask = envelopeByTaskScope.get(taskKey(event));
572
+ if (viaTask) return viaTask;
573
+ }
574
+ return envelopeByChain.get(chainKey(event));
575
+ }
576
+ function resolveParentRef(scope) {
577
+ if (scope.taskId) {
578
+ const ref = taskKey(scope);
579
+ if (open.has(ref)) return ref;
580
+ }
581
+ if (scope.toolCallId) {
582
+ const ref = toolKey(scope);
583
+ if (open.has(ref)) return ref;
584
+ }
585
+ if (scope.operationId) {
586
+ const ref = operationKey(scope);
587
+ if (open.has(ref)) return ref;
588
+ }
589
+ return void 0;
590
+ }
591
+ function sweepDescendants(parentRef, endTime) {
592
+ const doomed = [];
593
+ const frontier = [parentRef];
594
+ while (frontier.length > 0) {
595
+ const current = frontier.pop();
596
+ for (const record of open.values()) {
597
+ if (record.parentRef !== current) continue;
598
+ doomed.push(record.ref);
599
+ frontier.push(record.ref);
600
+ }
601
+ }
602
+ const intents = [];
603
+ for (const ref of doomed.reverse()) {
604
+ const record = open.get(ref);
605
+ if (!record) continue;
606
+ intents.push({
607
+ kind: "close",
608
+ ref,
609
+ ...endTime ? { endTime } : {},
610
+ error: { type: "interrupted" }
611
+ });
612
+ open.delete(ref);
613
+ if (record.isTask) taskTurnCounts.delete(ref);
614
+ }
615
+ return intents;
616
+ }
617
+ function drain(endTime) {
618
+ const intents = [];
619
+ for (const ref of [...open.keys()].reverse()) {
620
+ const envelope = envelopes.get(ref);
621
+ if (envelope) {
622
+ intents.push({ kind: "update", ref, attributes: envelopeAttributes(envelope, true) });
623
+ }
624
+ intents.push({
625
+ kind: "close",
626
+ ref,
627
+ ...endTime ? { endTime } : {},
628
+ error: { type: "interrupted" }
629
+ });
630
+ }
631
+ open.clear();
632
+ envelopes.clear();
633
+ envelopeByChain.clear();
634
+ envelopeBySubmission.clear();
635
+ envelopeByOperationId.clear();
636
+ envelopeByTaskScope.clear();
637
+ turnPositions.clear();
638
+ taskTurnCounts.clear();
639
+ return intents;
640
+ }
641
+ return { observe, drain, hasOpenSpan: (ref) => open.has(ref) };
642
+ }
643
+ function freshEnvelope() {
644
+ return {
645
+ turnCount: 0,
646
+ maxIteration: null,
647
+ modelPinned: false,
648
+ inputTokens: 0,
649
+ outputTokens: 0,
650
+ cacheReadTokens: 0,
651
+ cacheCreationTokens: 0,
652
+ sawUsage: false,
653
+ toolsReported: false
654
+ };
655
+ }
656
+ function foldUsage(envelope, usage) {
657
+ if (!usage) return;
658
+ const input = positiveNumber(usage.input);
659
+ const output = positiveNumber(usage.output);
660
+ const cacheRead = positiveNumber(usage.cacheRead);
661
+ const cacheWrite = positiveNumber(usage.cacheWrite);
662
+ if (input === 0 && output === 0 && cacheRead === 0 && cacheWrite === 0) return;
663
+ envelope.sawUsage = true;
664
+ envelope.inputTokens += input + cacheRead + cacheWrite;
665
+ envelope.outputTokens += output;
666
+ envelope.cacheReadTokens += cacheRead;
667
+ envelope.cacheCreationTokens += cacheWrite;
668
+ }
669
+ function usageAttributes(usage) {
670
+ if (!usage) return {};
671
+ const cacheRead = positiveNumber(usage.cacheRead);
672
+ const cacheWrite = positiveNumber(usage.cacheWrite);
673
+ return {
674
+ [GEN_AI.usageInputTokens]: positiveNumber(usage.input) + cacheRead + cacheWrite,
675
+ [GEN_AI.usageOutputTokens]: positiveNumber(usage.output),
676
+ [GEN_AI.usageCacheReadTokens]: cacheRead,
677
+ [GEN_AI.usageCacheCreationTokens]: cacheWrite,
678
+ ...typeof usage.totalTokens === "number" ? { [FLUE.usageTotalTokens]: usage.totalTokens } : {}
679
+ };
680
+ }
681
+ function positionAttributes(position) {
682
+ if (!position) return {};
683
+ return {
684
+ ...position.turnId ? { [RUNTYPE.turnId]: position.turnId } : {},
685
+ [RUNTYPE.turnIndex]: position.index,
686
+ // Only the root agent's loop has an ITERATION. A delegated sub-agent's
687
+ // turns would otherwise inflate the run's iteration count, which ingest
688
+ // computes as the highest position across every span in the trace.
689
+ ...position.atEnvelopeScope ? { [RUNTYPE.iteration]: position.index } : {}
690
+ };
691
+ }
692
+ function identifiers(event) {
693
+ return {
694
+ ...event.instanceId ? { [FLUE.instanceId]: event.instanceId } : {},
695
+ ...event.submissionId ? { [FLUE.submissionId]: event.submissionId } : {},
696
+ ...event.agentName ? { [FLUE.agentName]: event.agentName } : {},
697
+ ...event.harness ? { [FLUE.harnessName]: event.harness } : {},
698
+ ...event.session ? { [FLUE.sessionName]: event.session } : {},
699
+ ...event.parentSession ? { [FLUE.parentSessionName]: event.parentSession } : {},
700
+ ...event.operationId ? { [FLUE.operationId]: event.operationId } : {},
701
+ ...event.turnId ? { [FLUE.turnId]: event.turnId } : {},
702
+ ...event.taskId ? { [FLUE.taskId]: event.taskId } : {},
703
+ ...typeof event.eventIndex === "number" ? { [FLUE.eventIndex]: event.eventIndex } : {}
704
+ };
705
+ }
706
+ function closeIntent(ref, event, isError, errorInfo) {
707
+ return {
708
+ kind: "close",
709
+ ref,
710
+ ...event.timestamp ? { endTime: event.timestamp } : {},
711
+ ...isError ? {
712
+ error: {
713
+ type: errorInfo?.type ?? "_OTHER",
714
+ // The class NAME only. `errorInfo.message` and `.stack` are content
715
+ // and this release emits none: a provider message routinely quotes
716
+ // the prompt back, and a stack ships filesystem layout.
717
+ ...errorInfo?.name ? { exceptionType: errorInfo.name } : {}
718
+ }
719
+ } : {}
720
+ };
721
+ }
722
+ function readErrorInfo(event) {
723
+ const detail = event.errorInfo;
724
+ return detail !== null && typeof detail === "object" ? detail : void 0;
725
+ }
726
+ function readString(event, key) {
727
+ const value = event[key];
728
+ return typeof value === "string" && value.length > 0 ? value : void 0;
729
+ }
730
+ function readBoolean(event, key) {
731
+ const value = event[key];
732
+ return typeof value === "boolean" ? value : void 0;
733
+ }
734
+ function readRecord(event, key) {
735
+ const value = event[key];
736
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
737
+ }
738
+ function positiveNumber(value) {
739
+ return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : 0;
740
+ }
741
+ function chainKey(value) {
742
+ return identityKey("chain", [value.instanceId, value.harness, value.conversationId, value.session]);
743
+ }
744
+ function operationKey(value) {
745
+ return identityKey("operation", [
746
+ value.instanceId,
747
+ value.harness,
748
+ value.conversationId,
749
+ value.session,
750
+ value.taskId,
751
+ value.operationId
752
+ ]);
753
+ }
754
+ function taskKey(value) {
755
+ return identityKey("task", [
756
+ value.instanceId,
757
+ value.harness,
758
+ value.conversationId,
759
+ value.session,
760
+ value.taskId
761
+ ]);
762
+ }
763
+ function compactionKey(value) {
764
+ return identityKey("compaction", [
765
+ value.instanceId,
766
+ value.harness,
767
+ value.conversationId,
768
+ value.session,
769
+ value.taskId,
770
+ value.operationId
771
+ ]);
772
+ }
773
+ function turnKey(value) {
774
+ return identityKey("turn", [
775
+ value.instanceId,
776
+ value.harness,
777
+ value.conversationId,
778
+ value.session,
779
+ value.taskId,
780
+ value.operationId,
781
+ value.turnId
782
+ ]);
783
+ }
784
+ function toolKey(value) {
785
+ return identityKey("tool", [
786
+ value.instanceId,
787
+ value.harness,
788
+ value.conversationId,
789
+ value.session,
790
+ value.taskId,
791
+ value.operationId,
792
+ value.turnId,
793
+ value.toolCallId
794
+ ]);
795
+ }
796
+ function identityKey(kind, fields) {
797
+ return JSON.stringify([kind, ...fields.map((value) => value ?? null)]);
798
+ }
799
+
800
+ // src/spans.ts
801
+ var import_api = require("@opentelemetry/api");
802
+ function createSpanDriver(tracer) {
803
+ const live = /* @__PURE__ */ new Map();
804
+ function apply(intents) {
805
+ for (const intent of intents) {
806
+ switch (intent.kind) {
807
+ case "open": {
808
+ if (live.has(intent.ref)) break;
809
+ const parent = intent.parentRef ? live.get(intent.parentRef) : void 0;
810
+ const active = import_api.context.active();
811
+ const parentContext = parent ? import_api.trace.setSpan(active, parent) : import_api.trace.getSpanContext(active) ? active : void 0;
812
+ if (parentContext === void 0 && intent.requiresParent) break;
813
+ const span = tracer.startSpan(
814
+ intent.name,
815
+ {
816
+ kind: intent.spanKind === "client" ? import_api.SpanKind.CLIENT : import_api.SpanKind.INTERNAL,
817
+ root: parentContext === void 0,
818
+ ...intent.startTime ? { startTime: toDate(intent.startTime) } : {},
819
+ attributes: intent.attributes
820
+ },
821
+ parentContext
822
+ );
823
+ live.set(intent.ref, span);
824
+ break;
825
+ }
826
+ case "update": {
827
+ live.get(intent.ref)?.setAttributes(intent.attributes);
828
+ break;
829
+ }
830
+ case "close": {
831
+ const span = live.get(intent.ref);
832
+ if (!span) break;
833
+ if (intent.error) {
834
+ span.setAttribute(GEN_AI.errorType, intent.error.type);
835
+ span.setStatus({ code: import_api.SpanStatusCode.ERROR });
836
+ if (intent.error.exceptionType) {
837
+ span.recordException({ name: intent.error.exceptionType });
838
+ }
839
+ }
840
+ span.end(intent.endTime ? toDate(intent.endTime) : void 0);
841
+ live.delete(intent.ref);
842
+ break;
843
+ }
844
+ }
845
+ }
846
+ }
847
+ return { apply, spanFor: (ref) => live.get(ref) };
848
+ }
849
+ function contextForOperation(span, carrier) {
850
+ if (span) return import_api.trace.setSpan(import_api.context.active(), span);
851
+ if (carrier) {
852
+ return import_api.propagation.extract(import_api.context.active(), carrier, {
853
+ keys: () => carrier.tracestate ? ["traceparent", "tracestate"] : ["traceparent"],
854
+ get: (value, key) => value[key]
855
+ });
856
+ }
857
+ return void 0;
858
+ }
859
+ function toDate(value) {
860
+ const date = new Date(value);
861
+ return Number.isNaN(date.getTime()) ? void 0 : date;
862
+ }
863
+
864
+ // src/index.ts
865
+ var RUNTYPE_FLUE_INSTRUMENTATION_KEY = /* @__PURE__ */ Symbol.for(
866
+ "@runtypelabs/flue-otel"
867
+ );
868
+ function createRuntypeFlueInstrumentation(options = {}) {
869
+ const tracer = options.tracer ?? import_api2.trace.getTracer(ADAPTER_NAME);
870
+ const projection = createFlueProjection(options);
871
+ const driver = createSpanDriver(tracer);
872
+ let disposed = false;
873
+ const observe = (observation) => {
874
+ if (disposed) return;
875
+ try {
876
+ driver.apply(projection.observe(observation));
877
+ } catch {
878
+ }
879
+ };
880
+ const interceptor = (operation, executionContext, next) => {
881
+ if (disposed) return next();
882
+ const span = spanForOperation(operation, executionContext);
883
+ const active = contextForOperation(span, executionContext.traceCarrier);
884
+ if (!active) return next();
885
+ return import_api2.context.with(active, next);
886
+ };
887
+ function spanForOperation(operation, executionContext) {
888
+ switch (operation.type) {
889
+ case "agent":
890
+ return driver.spanFor(
891
+ operationKey({ ...executionContext, operationId: readIdField(operation, "operationId") })
892
+ );
893
+ case "model":
894
+ return driver.spanFor(
895
+ turnKey({ ...executionContext, turnId: readIdField(operation, "turnId") })
896
+ );
897
+ case "tool":
898
+ return driver.spanFor(
899
+ toolKey({ ...executionContext, toolCallId: readIdField(operation, "toolCallId") })
900
+ );
901
+ case "task":
902
+ return driver.spanFor(
903
+ taskKey({ ...executionContext, taskId: readIdField(operation, "taskId") })
904
+ );
905
+ default:
906
+ return void 0;
907
+ }
908
+ }
909
+ return {
910
+ key: RUNTYPE_FLUE_INSTRUMENTATION_KEY,
911
+ observe,
912
+ interceptor,
913
+ dispose() {
914
+ if (disposed) return;
915
+ disposed = true;
916
+ try {
917
+ driver.apply(projection.drain((/* @__PURE__ */ new Date()).toISOString()));
918
+ } catch {
919
+ }
920
+ }
921
+ };
922
+ }
923
+ function readIdField(operation, key) {
924
+ const value = operation[key];
925
+ return typeof value === "string" && value.length > 0 ? value : void 0;
926
+ }
927
+ // Annotate the CommonJS export names for ESM import in node:
928
+ 0 && (module.exports = {
929
+ ADAPTER_NAME,
930
+ ADAPTER_VERSION,
931
+ GEN_AI,
932
+ RUNTYPE,
933
+ RUNTYPE_FLUE_INSTRUMENTATION_KEY,
934
+ RUNTYPE_SCHEMA_VERSION,
935
+ RUNTYPE_STOP_REASONS,
936
+ RUNTYPE_TOOL_TYPES,
937
+ createRuntypeFlueInstrumentation,
938
+ runtypeFlueResourceAttributes
939
+ });