inngest 4.5.1 → 4.6.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.
Files changed (70) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/api/schema.d.cts +2 -2
  3. package/api/schema.d.cts.map +1 -1
  4. package/api/schema.d.ts +2 -2
  5. package/api/schema.d.ts.map +1 -1
  6. package/components/Inngest.cjs +3 -0
  7. package/components/Inngest.cjs.map +1 -1
  8. package/components/Inngest.d.cts.map +1 -1
  9. package/components/Inngest.d.ts.map +1 -1
  10. package/components/Inngest.js +4 -1
  11. package/components/Inngest.js.map +1 -1
  12. package/components/InngestFunction.cjs +3 -0
  13. package/components/InngestFunction.cjs.map +1 -1
  14. package/components/InngestFunction.d.cts +2 -0
  15. package/components/InngestFunction.d.cts.map +1 -1
  16. package/components/InngestFunction.d.ts +2 -0
  17. package/components/InngestFunction.d.ts.map +1 -1
  18. package/components/InngestFunction.js +3 -0
  19. package/components/InngestFunction.js.map +1 -1
  20. package/components/InngestGroupTools.cjs +2 -2
  21. package/components/InngestGroupTools.cjs.map +1 -1
  22. package/components/InngestGroupTools.js +2 -2
  23. package/components/InngestGroupTools.js.map +1 -1
  24. package/components/InngestMetadata.cjs.map +1 -1
  25. package/components/InngestMetadata.d.cts +1 -1
  26. package/components/InngestMetadata.d.cts.map +1 -1
  27. package/components/InngestMetadata.d.ts +1 -1
  28. package/components/InngestMetadata.d.ts.map +1 -1
  29. package/components/InngestMetadata.js.map +1 -1
  30. package/components/execution/engine.cjs +15 -0
  31. package/components/execution/engine.cjs.map +1 -1
  32. package/components/execution/engine.d.cts +8 -0
  33. package/components/execution/engine.d.cts.map +1 -1
  34. package/components/execution/engine.d.ts +8 -0
  35. package/components/execution/engine.d.ts.map +1 -1
  36. package/components/execution/engine.js +15 -0
  37. package/components/execution/engine.js.map +1 -1
  38. package/components/execution/otel/aiExtractor.cjs +114 -0
  39. package/components/execution/otel/aiExtractor.cjs.map +1 -0
  40. package/components/execution/otel/aiExtractor.d.cts +16 -0
  41. package/components/execution/otel/aiExtractor.d.cts.map +1 -0
  42. package/components/execution/otel/aiExtractor.d.ts +19 -0
  43. package/components/execution/otel/aiExtractor.d.ts.map +1 -0
  44. package/components/execution/otel/aiExtractor.js +111 -0
  45. package/components/execution/otel/aiExtractor.js.map +1 -0
  46. package/components/execution/otel/metadataProcessor.cjs +195 -0
  47. package/components/execution/otel/metadataProcessor.cjs.map +1 -0
  48. package/components/execution/otel/metadataProcessor.js +193 -0
  49. package/components/execution/otel/metadataProcessor.js.map +1 -0
  50. package/components/execution/otel/middleware.cjs +1 -1
  51. package/components/execution/otel/middleware.d.cts +5 -5
  52. package/components/execution/otel/middleware.d.ts +5 -5
  53. package/components/execution/otel/middleware.js +1 -1
  54. package/components/execution/otel/processor.cjs +1 -1
  55. package/components/execution/otel/processor.js +1 -1
  56. package/components/execution/otel/util.cjs.map +1 -1
  57. package/components/execution/otel/util.js.map +1 -1
  58. package/package.json +1 -1
  59. package/types.cjs.map +1 -1
  60. package/types.d.cts +17 -15
  61. package/types.d.cts.map +1 -1
  62. package/types.d.ts +17 -15
  63. package/types.d.ts.map +1 -1
  64. package/types.js.map +1 -1
  65. package/version.cjs +1 -1
  66. package/version.cjs.map +1 -1
  67. package/version.d.cts +1 -1
  68. package/version.d.ts +1 -1
  69. package/version.js +1 -1
  70. package/version.js.map +1 -1
@@ -0,0 +1,114 @@
1
+
2
+ //#region src/components/execution/otel/aiExtractor.ts
3
+ /**
4
+ * A `convention` identifies a namespace of AI span attributes and orders which
5
+ * should win when multiple namespaces are present on the same span. Lower
6
+ * numbers override higher numbers.
7
+ */
8
+ var Convention = /* @__PURE__ */ function(Convention$1) {
9
+ Convention$1[Convention$1["Semconv"] = 1] = "Semconv";
10
+ Convention$1[Convention$1["OpenInference"] = 2] = "OpenInference";
11
+ Convention$1[Convention$1["Vercel"] = 3] = "Vercel";
12
+ return Convention$1;
13
+ }(Convention || {});
14
+ /**
15
+ * Maps a source attribute key to the canonical {@link AIMetadata} field it
16
+ * populates and the convention it belongs to.
17
+ */
18
+ const keyFieldMap = {
19
+ "gen_ai.request.model": {
20
+ field: "model",
21
+ convention: Convention.Semconv
22
+ },
23
+ "gen_ai.usage.input_tokens": {
24
+ field: "inputTokens",
25
+ convention: Convention.Semconv
26
+ },
27
+ "llm.model_name": {
28
+ field: "model",
29
+ convention: Convention.OpenInference
30
+ },
31
+ "llm.token_count.prompt": {
32
+ field: "inputTokens",
33
+ convention: Convention.OpenInference
34
+ },
35
+ "ai.model.id": {
36
+ field: "model",
37
+ convention: Convention.Vercel
38
+ },
39
+ "ai.usage.inputTokens": {
40
+ field: "inputTokens",
41
+ convention: Convention.Vercel
42
+ },
43
+ "ai.usage.tokens": {
44
+ field: "inputTokens",
45
+ convention: Convention.Vercel
46
+ }
47
+ };
48
+ /**
49
+ * Extracts AI model metadata from a span's attributes.
50
+ *
51
+ * Attributes are matched across multiple instrumentation conventions
52
+ * (OpenTelemetry semconv, OpenInference, Vercel AI SDK). When more than one
53
+ * convention supplies the same field, the highest-precedence value wins.
54
+ *
55
+ * @param attributes - The span attributes, as exposed by
56
+ * `ReadableSpan.attributes`.
57
+ */
58
+ const extractAIMetadataFromAttributes = (attributes) => {
59
+ const candidates = {};
60
+ for (const [key, value] of Object.entries(attributes)) {
61
+ if (value === void 0) continue;
62
+ const mapping = keyFieldMap[key];
63
+ if (!mapping) continue;
64
+ const existing = candidates[mapping.field];
65
+ if (!existing || mapping.convention < existing.convention) candidates[mapping.field] = {
66
+ value,
67
+ convention: mapping.convention
68
+ };
69
+ }
70
+ const metadata = {};
71
+ const model = candidates.model?.value;
72
+ if (typeof model === "string" && model !== "") metadata.model = model;
73
+ const inputTokens = candidates.inputTokens?.value;
74
+ if (inputTokens !== void 0) {
75
+ const n = Number(inputTokens);
76
+ if (!Number.isNaN(n)) metadata.inputTokens = n;
77
+ }
78
+ return metadata;
79
+ };
80
+ /**
81
+ * Aggregates two {@link AIMetadata} values into one.
82
+ *
83
+ * Input token counts are summed, while `a`'s model takes precedence over `b`'s.
84
+ * Each field is only present in the result when at least one input supplies it.
85
+ *
86
+ * @param a - The primary metadata; its `model` wins when both are present.
87
+ * @param b - The secondary metadata.
88
+ */
89
+ const aggregate = (a, b) => {
90
+ const metadata = {};
91
+ const model = a.model ?? b.model;
92
+ if (model !== void 0) metadata.model = model;
93
+ if (a.inputTokens !== void 0 || b.inputTokens !== void 0) metadata.inputTokens = (a.inputTokens ?? 0) + (b.inputTokens ?? 0);
94
+ return metadata;
95
+ };
96
+ /**
97
+ * Maps an {@link AIMetadata} value onto the server's `inngest.ai` metadata
98
+ * schema (snake_case keys). Only the fields we extract are emitted; absent
99
+ * fields are omitted rather than zero-valued. Returns `undefined` when there
100
+ * is nothing to emit, so callers can skip stamping metadata entirely.
101
+ */
102
+ const toInngestAIMetadataValues = (metadata) => {
103
+ const values = {};
104
+ if (metadata.model !== void 0) values.model = metadata.model;
105
+ if (metadata.inputTokens !== void 0) values.input_tokens = metadata.inputTokens;
106
+ if (Object.keys(values).length === 0) return;
107
+ return values;
108
+ };
109
+
110
+ //#endregion
111
+ exports.aggregate = aggregate;
112
+ exports.extractAIMetadataFromAttributes = extractAIMetadataFromAttributes;
113
+ exports.toInngestAIMetadataValues = toInngestAIMetadataValues;
114
+ //# sourceMappingURL=aiExtractor.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"aiExtractor.cjs","names":["keyFieldMap: Record<string, Mapping>","candidates: Partial<Record<Field, Candidate>>","metadata: AIMetadata","values: Record<string, unknown>"],"sources":["../../../../src/components/execution/otel/aiExtractor.ts"],"sourcesContent":["import type { Attributes, AttributeValue } from \"@opentelemetry/api\";\n\n/**\n * AI metadata extracted from a span's attributes.\n *\n * A field is only present when a matching attribute was found on the span;\n * absent fields are omitted rather than zero-valued.\n */\nexport interface AIMetadata {\n /** The requested model, e.g. `gpt-4.1-nano`. */\n model?: string;\n /** The number of input (prompt) tokens consumed by the request. */\n inputTokens?: number;\n}\n\n/**\n * A `convention` identifies a namespace of AI span attributes and orders which\n * should win when multiple namespaces are present on the same span. Lower\n * numbers override higher numbers.\n */\nenum Convention {\n Semconv = 1,\n OpenInference = 2,\n Vercel = 3,\n}\n\ntype Field = keyof AIMetadata;\n\ninterface Mapping {\n field: Field;\n convention: Convention;\n}\n\n/**\n * Maps a source attribute key to the canonical {@link AIMetadata} field it\n * populates and the convention it belongs to.\n */\nconst keyFieldMap: Record<string, Mapping> = {\n // OpenTelemetry Semantic Conventions\n \"gen_ai.request.model\": { field: \"model\", convention: Convention.Semconv },\n \"gen_ai.usage.input_tokens\": {\n field: \"inputTokens\",\n convention: Convention.Semconv,\n },\n\n // OpenInference\n \"llm.model_name\": { field: \"model\", convention: Convention.OpenInference },\n \"llm.token_count.prompt\": {\n field: \"inputTokens\",\n convention: Convention.OpenInference,\n },\n\n // Vercel AI SDK (native `ai.*` telemetry)\n \"ai.model.id\": { field: \"model\", convention: Convention.Vercel },\n \"ai.usage.inputTokens\": {\n field: \"inputTokens\",\n convention: Convention.Vercel,\n },\n // Embeddings spans emit only a single `ai.usage.tokens` count (no\n // input/output split); map it to inputTokens to match the semconv embeddings\n // case.\n \"ai.usage.tokens\": { field: \"inputTokens\", convention: Convention.Vercel },\n};\n\ninterface Candidate {\n value: AttributeValue;\n convention: Convention;\n}\n\n/**\n * Extracts AI model metadata from a span's attributes.\n *\n * Attributes are matched across multiple instrumentation conventions\n * (OpenTelemetry semconv, OpenInference, Vercel AI SDK). When more than one\n * convention supplies the same field, the highest-precedence value wins.\n *\n * @param attributes - The span attributes, as exposed by\n * `ReadableSpan.attributes`.\n */\nexport const extractAIMetadataFromAttributes = (\n attributes: Attributes,\n): AIMetadata => {\n // Track the highest-precedence (lowest convention) candidate seen per field.\n const candidates: Partial<Record<Field, Candidate>> = {};\n\n for (const [key, value] of Object.entries(attributes)) {\n if (value === undefined) {\n continue;\n }\n\n const mapping = keyFieldMap[key];\n if (!mapping) {\n continue;\n }\n\n const existing = candidates[mapping.field];\n if (!existing || mapping.convention < existing.convention) {\n candidates[mapping.field] = { value, convention: mapping.convention };\n }\n }\n\n const metadata: AIMetadata = {};\n\n const model = candidates.model?.value;\n if (typeof model === \"string\" && model !== \"\") {\n metadata.model = model;\n }\n\n const inputTokens = candidates.inputTokens?.value;\n if (inputTokens !== undefined) {\n // Token counts arrive as numbers from the SDK, but OTLP/JSON encodes int64\n // as either a number or a quoted string, so coerce defensively.\n const n = Number(inputTokens);\n if (!Number.isNaN(n)) {\n metadata.inputTokens = n;\n }\n }\n\n return metadata;\n};\n\n/**\n * Aggregates two {@link AIMetadata} values into one.\n *\n * Input token counts are summed, while `a`'s model takes precedence over `b`'s.\n * Each field is only present in the result when at least one input supplies it.\n *\n * @param a - The primary metadata; its `model` wins when both are present.\n * @param b - The secondary metadata.\n */\nexport const aggregate = (a: AIMetadata, b: AIMetadata): AIMetadata => {\n const metadata: AIMetadata = {};\n\n const model = a.model ?? b.model;\n if (model !== undefined) {\n metadata.model = model;\n }\n\n if (a.inputTokens !== undefined || b.inputTokens !== undefined) {\n metadata.inputTokens = (a.inputTokens ?? 0) + (b.inputTokens ?? 0);\n }\n\n return metadata;\n};\n\n/**\n * Maps an {@link AIMetadata} value onto the server's `inngest.ai` metadata\n * schema (snake_case keys). Only the fields we extract are emitted; absent\n * fields are omitted rather than zero-valued. Returns `undefined` when there\n * is nothing to emit, so callers can skip stamping metadata entirely.\n */\nexport const toInngestAIMetadataValues = (\n metadata: AIMetadata,\n): Record<string, unknown> | undefined => {\n const values: Record<string, unknown> = {};\n\n if (metadata.model !== undefined) {\n values.model = metadata.model;\n }\n\n if (metadata.inputTokens !== undefined) {\n values.input_tokens = metadata.inputTokens;\n }\n\n if (Object.keys(values).length === 0) {\n return undefined;\n }\n\n return values;\n};\n"],"mappings":";;;;;;;AAoBA,IAAK,oDAAL;AACE;AACA;AACA;;EAHG;;;;;AAiBL,MAAMA,cAAuC;CAE3C,wBAAwB;EAAE,OAAO;EAAS,YAAY,WAAW;EAAS;CAC1E,6BAA6B;EAC3B,OAAO;EACP,YAAY,WAAW;EACxB;CAGD,kBAAkB;EAAE,OAAO;EAAS,YAAY,WAAW;EAAe;CAC1E,0BAA0B;EACxB,OAAO;EACP,YAAY,WAAW;EACxB;CAGD,eAAe;EAAE,OAAO;EAAS,YAAY,WAAW;EAAQ;CAChE,wBAAwB;EACtB,OAAO;EACP,YAAY,WAAW;EACxB;CAID,mBAAmB;EAAE,OAAO;EAAe,YAAY,WAAW;EAAQ;CAC3E;;;;;;;;;;;AAiBD,MAAa,mCACX,eACe;CAEf,MAAMC,aAAgD,EAAE;AAExD,MAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,WAAW,EAAE;AACrD,MAAI,UAAU,OACZ;EAGF,MAAM,UAAU,YAAY;AAC5B,MAAI,CAAC,QACH;EAGF,MAAM,WAAW,WAAW,QAAQ;AACpC,MAAI,CAAC,YAAY,QAAQ,aAAa,SAAS,WAC7C,YAAW,QAAQ,SAAS;GAAE;GAAO,YAAY,QAAQ;GAAY;;CAIzE,MAAMC,WAAuB,EAAE;CAE/B,MAAM,QAAQ,WAAW,OAAO;AAChC,KAAI,OAAO,UAAU,YAAY,UAAU,GACzC,UAAS,QAAQ;CAGnB,MAAM,cAAc,WAAW,aAAa;AAC5C,KAAI,gBAAgB,QAAW;EAG7B,MAAM,IAAI,OAAO,YAAY;AAC7B,MAAI,CAAC,OAAO,MAAM,EAAE,CAClB,UAAS,cAAc;;AAI3B,QAAO;;;;;;;;;;;AAYT,MAAa,aAAa,GAAe,MAA8B;CACrE,MAAMA,WAAuB,EAAE;CAE/B,MAAM,QAAQ,EAAE,SAAS,EAAE;AAC3B,KAAI,UAAU,OACZ,UAAS,QAAQ;AAGnB,KAAI,EAAE,gBAAgB,UAAa,EAAE,gBAAgB,OACnD,UAAS,eAAe,EAAE,eAAe,MAAM,EAAE,eAAe;AAGlE,QAAO;;;;;;;;AAST,MAAa,6BACX,aACwC;CACxC,MAAMC,SAAkC,EAAE;AAE1C,KAAI,SAAS,UAAU,OACrB,QAAO,QAAQ,SAAS;AAG1B,KAAI,SAAS,gBAAgB,OAC3B,QAAO,eAAe,SAAS;AAGjC,KAAI,OAAO,KAAK,OAAO,CAAC,WAAW,EACjC;AAGF,QAAO"}
@@ -0,0 +1,16 @@
1
+ //#region src/components/execution/otel/aiExtractor.d.ts
2
+ /**
3
+ * AI metadata extracted from a span's attributes.
4
+ *
5
+ * A field is only present when a matching attribute was found on the span;
6
+ * absent fields are omitted rather than zero-valued.
7
+ */
8
+ interface AIMetadata {
9
+ /** The requested model, e.g. `gpt-4.1-nano`. */
10
+ model?: string;
11
+ /** The number of input (prompt) tokens consumed by the request. */
12
+ inputTokens?: number;
13
+ }
14
+ //#endregion
15
+ export { AIMetadata };
16
+ //# sourceMappingURL=aiExtractor.d.cts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"aiExtractor.d.cts","names":[],"sources":["../../../../src/components/execution/otel/aiExtractor.ts"],"sourcesContent":[],"mappings":";AAQA;;;;;;UAAiB,UAAA"}
@@ -0,0 +1,19 @@
1
+ import "@opentelemetry/api";
2
+
3
+ //#region src/components/execution/otel/aiExtractor.d.ts
4
+
5
+ /**
6
+ * AI metadata extracted from a span's attributes.
7
+ *
8
+ * A field is only present when a matching attribute was found on the span;
9
+ * absent fields are omitted rather than zero-valued.
10
+ */
11
+ interface AIMetadata {
12
+ /** The requested model, e.g. `gpt-4.1-nano`. */
13
+ model?: string;
14
+ /** The number of input (prompt) tokens consumed by the request. */
15
+ inputTokens?: number;
16
+ }
17
+ //#endregion
18
+ export { AIMetadata };
19
+ //# sourceMappingURL=aiExtractor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"aiExtractor.d.ts","names":[],"sources":["../../../../src/components/execution/otel/aiExtractor.ts"],"sourcesContent":[],"mappings":";;;;;;AAQA;;;;UAAiB,UAAA"}
@@ -0,0 +1,111 @@
1
+ //#region src/components/execution/otel/aiExtractor.ts
2
+ /**
3
+ * A `convention` identifies a namespace of AI span attributes and orders which
4
+ * should win when multiple namespaces are present on the same span. Lower
5
+ * numbers override higher numbers.
6
+ */
7
+ var Convention = /* @__PURE__ */ function(Convention$1) {
8
+ Convention$1[Convention$1["Semconv"] = 1] = "Semconv";
9
+ Convention$1[Convention$1["OpenInference"] = 2] = "OpenInference";
10
+ Convention$1[Convention$1["Vercel"] = 3] = "Vercel";
11
+ return Convention$1;
12
+ }(Convention || {});
13
+ /**
14
+ * Maps a source attribute key to the canonical {@link AIMetadata} field it
15
+ * populates and the convention it belongs to.
16
+ */
17
+ const keyFieldMap = {
18
+ "gen_ai.request.model": {
19
+ field: "model",
20
+ convention: Convention.Semconv
21
+ },
22
+ "gen_ai.usage.input_tokens": {
23
+ field: "inputTokens",
24
+ convention: Convention.Semconv
25
+ },
26
+ "llm.model_name": {
27
+ field: "model",
28
+ convention: Convention.OpenInference
29
+ },
30
+ "llm.token_count.prompt": {
31
+ field: "inputTokens",
32
+ convention: Convention.OpenInference
33
+ },
34
+ "ai.model.id": {
35
+ field: "model",
36
+ convention: Convention.Vercel
37
+ },
38
+ "ai.usage.inputTokens": {
39
+ field: "inputTokens",
40
+ convention: Convention.Vercel
41
+ },
42
+ "ai.usage.tokens": {
43
+ field: "inputTokens",
44
+ convention: Convention.Vercel
45
+ }
46
+ };
47
+ /**
48
+ * Extracts AI model metadata from a span's attributes.
49
+ *
50
+ * Attributes are matched across multiple instrumentation conventions
51
+ * (OpenTelemetry semconv, OpenInference, Vercel AI SDK). When more than one
52
+ * convention supplies the same field, the highest-precedence value wins.
53
+ *
54
+ * @param attributes - The span attributes, as exposed by
55
+ * `ReadableSpan.attributes`.
56
+ */
57
+ const extractAIMetadataFromAttributes = (attributes) => {
58
+ const candidates = {};
59
+ for (const [key, value] of Object.entries(attributes)) {
60
+ if (value === void 0) continue;
61
+ const mapping = keyFieldMap[key];
62
+ if (!mapping) continue;
63
+ const existing = candidates[mapping.field];
64
+ if (!existing || mapping.convention < existing.convention) candidates[mapping.field] = {
65
+ value,
66
+ convention: mapping.convention
67
+ };
68
+ }
69
+ const metadata = {};
70
+ const model = candidates.model?.value;
71
+ if (typeof model === "string" && model !== "") metadata.model = model;
72
+ const inputTokens = candidates.inputTokens?.value;
73
+ if (inputTokens !== void 0) {
74
+ const n = Number(inputTokens);
75
+ if (!Number.isNaN(n)) metadata.inputTokens = n;
76
+ }
77
+ return metadata;
78
+ };
79
+ /**
80
+ * Aggregates two {@link AIMetadata} values into one.
81
+ *
82
+ * Input token counts are summed, while `a`'s model takes precedence over `b`'s.
83
+ * Each field is only present in the result when at least one input supplies it.
84
+ *
85
+ * @param a - The primary metadata; its `model` wins when both are present.
86
+ * @param b - The secondary metadata.
87
+ */
88
+ const aggregate = (a, b) => {
89
+ const metadata = {};
90
+ const model = a.model ?? b.model;
91
+ if (model !== void 0) metadata.model = model;
92
+ if (a.inputTokens !== void 0 || b.inputTokens !== void 0) metadata.inputTokens = (a.inputTokens ?? 0) + (b.inputTokens ?? 0);
93
+ return metadata;
94
+ };
95
+ /**
96
+ * Maps an {@link AIMetadata} value onto the server's `inngest.ai` metadata
97
+ * schema (snake_case keys). Only the fields we extract are emitted; absent
98
+ * fields are omitted rather than zero-valued. Returns `undefined` when there
99
+ * is nothing to emit, so callers can skip stamping metadata entirely.
100
+ */
101
+ const toInngestAIMetadataValues = (metadata) => {
102
+ const values = {};
103
+ if (metadata.model !== void 0) values.model = metadata.model;
104
+ if (metadata.inputTokens !== void 0) values.input_tokens = metadata.inputTokens;
105
+ if (Object.keys(values).length === 0) return;
106
+ return values;
107
+ };
108
+
109
+ //#endregion
110
+ export { aggregate, extractAIMetadataFromAttributes, toInngestAIMetadataValues };
111
+ //# sourceMappingURL=aiExtractor.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"aiExtractor.js","names":["keyFieldMap: Record<string, Mapping>","candidates: Partial<Record<Field, Candidate>>","metadata: AIMetadata","values: Record<string, unknown>"],"sources":["../../../../src/components/execution/otel/aiExtractor.ts"],"sourcesContent":["import type { Attributes, AttributeValue } from \"@opentelemetry/api\";\n\n/**\n * AI metadata extracted from a span's attributes.\n *\n * A field is only present when a matching attribute was found on the span;\n * absent fields are omitted rather than zero-valued.\n */\nexport interface AIMetadata {\n /** The requested model, e.g. `gpt-4.1-nano`. */\n model?: string;\n /** The number of input (prompt) tokens consumed by the request. */\n inputTokens?: number;\n}\n\n/**\n * A `convention` identifies a namespace of AI span attributes and orders which\n * should win when multiple namespaces are present on the same span. Lower\n * numbers override higher numbers.\n */\nenum Convention {\n Semconv = 1,\n OpenInference = 2,\n Vercel = 3,\n}\n\ntype Field = keyof AIMetadata;\n\ninterface Mapping {\n field: Field;\n convention: Convention;\n}\n\n/**\n * Maps a source attribute key to the canonical {@link AIMetadata} field it\n * populates and the convention it belongs to.\n */\nconst keyFieldMap: Record<string, Mapping> = {\n // OpenTelemetry Semantic Conventions\n \"gen_ai.request.model\": { field: \"model\", convention: Convention.Semconv },\n \"gen_ai.usage.input_tokens\": {\n field: \"inputTokens\",\n convention: Convention.Semconv,\n },\n\n // OpenInference\n \"llm.model_name\": { field: \"model\", convention: Convention.OpenInference },\n \"llm.token_count.prompt\": {\n field: \"inputTokens\",\n convention: Convention.OpenInference,\n },\n\n // Vercel AI SDK (native `ai.*` telemetry)\n \"ai.model.id\": { field: \"model\", convention: Convention.Vercel },\n \"ai.usage.inputTokens\": {\n field: \"inputTokens\",\n convention: Convention.Vercel,\n },\n // Embeddings spans emit only a single `ai.usage.tokens` count (no\n // input/output split); map it to inputTokens to match the semconv embeddings\n // case.\n \"ai.usage.tokens\": { field: \"inputTokens\", convention: Convention.Vercel },\n};\n\ninterface Candidate {\n value: AttributeValue;\n convention: Convention;\n}\n\n/**\n * Extracts AI model metadata from a span's attributes.\n *\n * Attributes are matched across multiple instrumentation conventions\n * (OpenTelemetry semconv, OpenInference, Vercel AI SDK). When more than one\n * convention supplies the same field, the highest-precedence value wins.\n *\n * @param attributes - The span attributes, as exposed by\n * `ReadableSpan.attributes`.\n */\nexport const extractAIMetadataFromAttributes = (\n attributes: Attributes,\n): AIMetadata => {\n // Track the highest-precedence (lowest convention) candidate seen per field.\n const candidates: Partial<Record<Field, Candidate>> = {};\n\n for (const [key, value] of Object.entries(attributes)) {\n if (value === undefined) {\n continue;\n }\n\n const mapping = keyFieldMap[key];\n if (!mapping) {\n continue;\n }\n\n const existing = candidates[mapping.field];\n if (!existing || mapping.convention < existing.convention) {\n candidates[mapping.field] = { value, convention: mapping.convention };\n }\n }\n\n const metadata: AIMetadata = {};\n\n const model = candidates.model?.value;\n if (typeof model === \"string\" && model !== \"\") {\n metadata.model = model;\n }\n\n const inputTokens = candidates.inputTokens?.value;\n if (inputTokens !== undefined) {\n // Token counts arrive as numbers from the SDK, but OTLP/JSON encodes int64\n // as either a number or a quoted string, so coerce defensively.\n const n = Number(inputTokens);\n if (!Number.isNaN(n)) {\n metadata.inputTokens = n;\n }\n }\n\n return metadata;\n};\n\n/**\n * Aggregates two {@link AIMetadata} values into one.\n *\n * Input token counts are summed, while `a`'s model takes precedence over `b`'s.\n * Each field is only present in the result when at least one input supplies it.\n *\n * @param a - The primary metadata; its `model` wins when both are present.\n * @param b - The secondary metadata.\n */\nexport const aggregate = (a: AIMetadata, b: AIMetadata): AIMetadata => {\n const metadata: AIMetadata = {};\n\n const model = a.model ?? b.model;\n if (model !== undefined) {\n metadata.model = model;\n }\n\n if (a.inputTokens !== undefined || b.inputTokens !== undefined) {\n metadata.inputTokens = (a.inputTokens ?? 0) + (b.inputTokens ?? 0);\n }\n\n return metadata;\n};\n\n/**\n * Maps an {@link AIMetadata} value onto the server's `inngest.ai` metadata\n * schema (snake_case keys). Only the fields we extract are emitted; absent\n * fields are omitted rather than zero-valued. Returns `undefined` when there\n * is nothing to emit, so callers can skip stamping metadata entirely.\n */\nexport const toInngestAIMetadataValues = (\n metadata: AIMetadata,\n): Record<string, unknown> | undefined => {\n const values: Record<string, unknown> = {};\n\n if (metadata.model !== undefined) {\n values.model = metadata.model;\n }\n\n if (metadata.inputTokens !== undefined) {\n values.input_tokens = metadata.inputTokens;\n }\n\n if (Object.keys(values).length === 0) {\n return undefined;\n }\n\n return values;\n};\n"],"mappings":";;;;;;AAoBA,IAAK,oDAAL;AACE;AACA;AACA;;EAHG;;;;;AAiBL,MAAMA,cAAuC;CAE3C,wBAAwB;EAAE,OAAO;EAAS,YAAY,WAAW;EAAS;CAC1E,6BAA6B;EAC3B,OAAO;EACP,YAAY,WAAW;EACxB;CAGD,kBAAkB;EAAE,OAAO;EAAS,YAAY,WAAW;EAAe;CAC1E,0BAA0B;EACxB,OAAO;EACP,YAAY,WAAW;EACxB;CAGD,eAAe;EAAE,OAAO;EAAS,YAAY,WAAW;EAAQ;CAChE,wBAAwB;EACtB,OAAO;EACP,YAAY,WAAW;EACxB;CAID,mBAAmB;EAAE,OAAO;EAAe,YAAY,WAAW;EAAQ;CAC3E;;;;;;;;;;;AAiBD,MAAa,mCACX,eACe;CAEf,MAAMC,aAAgD,EAAE;AAExD,MAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,WAAW,EAAE;AACrD,MAAI,UAAU,OACZ;EAGF,MAAM,UAAU,YAAY;AAC5B,MAAI,CAAC,QACH;EAGF,MAAM,WAAW,WAAW,QAAQ;AACpC,MAAI,CAAC,YAAY,QAAQ,aAAa,SAAS,WAC7C,YAAW,QAAQ,SAAS;GAAE;GAAO,YAAY,QAAQ;GAAY;;CAIzE,MAAMC,WAAuB,EAAE;CAE/B,MAAM,QAAQ,WAAW,OAAO;AAChC,KAAI,OAAO,UAAU,YAAY,UAAU,GACzC,UAAS,QAAQ;CAGnB,MAAM,cAAc,WAAW,aAAa;AAC5C,KAAI,gBAAgB,QAAW;EAG7B,MAAM,IAAI,OAAO,YAAY;AAC7B,MAAI,CAAC,OAAO,MAAM,EAAE,CAClB,UAAS,cAAc;;AAI3B,QAAO;;;;;;;;;;;AAYT,MAAa,aAAa,GAAe,MAA8B;CACrE,MAAMA,WAAuB,EAAE;CAE/B,MAAM,QAAQ,EAAE,SAAS,EAAE;AAC3B,KAAI,UAAU,OACZ,UAAS,QAAQ;AAGnB,KAAI,EAAE,gBAAgB,UAAa,EAAE,gBAAgB,OACnD,UAAS,eAAe,EAAE,eAAe,MAAM,EAAE,eAAe;AAGlE,QAAO;;;;;;;;AAST,MAAa,6BACX,aACwC;CACxC,MAAMC,SAAkC,EAAE;AAE1C,KAAI,SAAS,UAAU,OACrB,QAAO,QAAQ,SAAS;AAG1B,KAAI,SAAS,gBAAgB,OAC3B,QAAO,eAAe,SAAS;AAGjC,KAAI,OAAO,KAAK,OAAO,CAAC,WAAW,EACjC;AAGF,QAAO"}
@@ -0,0 +1,195 @@
1
+ const require_rolldown_runtime = require('../../../_virtual/rolldown_runtime.cjs');
2
+ const require_types = require('../../../helpers/types.cjs');
3
+ const require_aiExtractor = require('./aiExtractor.cjs');
4
+ const require_consts = require('./consts.cjs');
5
+ let debug = require("debug");
6
+ debug = require_rolldown_runtime.__toESM(debug);
7
+ let __opentelemetry_api = require("@opentelemetry/api");
8
+
9
+ //#region src/components/execution/otel/metadataProcessor.ts
10
+ const processorDevDebug = (0, debug.default)(`${require_consts.debugPrefix}:InngestMetadataSpanProcessor`);
11
+ /**
12
+ * Resolve the currently-registered global OTel tracer provider, unwrapping
13
+ * the `ProxyTracerProvider` wrapper that `trace.getTracerProvider()` returns.
14
+ * Returns `undefined` when no provider is registered.
15
+ */
16
+ const getGlobalProvider = () => {
17
+ const globalProvider = __opentelemetry_api.trace.getTracerProvider();
18
+ if (!globalProvider) return;
19
+ return ("getDelegate" in globalProvider && typeof globalProvider.getDelegate === "function" ? globalProvider.getDelegate() : globalProvider) ?? void 0;
20
+ };
21
+ /**
22
+ * Attempts to add the given span processor to the given OTel provider.
23
+ * Returns `true` if the processor was attached, `false` if the provider could
24
+ * not be extended.
25
+ *
26
+ * It handles both OTel SDK v1 (`addSpanProcessor()`) and v2 (internal
27
+ * `_spanProcessors` array).
28
+ *
29
+ * This intentionally duplicates the attach logic inside `extendProvider`
30
+ * (`util.ts`) to avoid imports with instrumentation side effects.
31
+ */
32
+ const attachToProvider = (provider, processor) => {
33
+ if ("addSpanProcessor" in provider && typeof provider.addSpanProcessor === "function") {
34
+ provider.addSpanProcessor(processor);
35
+ return true;
36
+ }
37
+ const spanProcessors = getInternalSpanProcessors(provider);
38
+ if (spanProcessors) {
39
+ spanProcessors.push(processor);
40
+ return true;
41
+ }
42
+ return false;
43
+ };
44
+ /**
45
+ * Extract the internal span processors array from a BasicTracerProvider.
46
+ * Returns the mutable array if accessible, undefined otherwise.
47
+ *
48
+ * BasicTracerProvider._activeSpanProcessor is a MultiSpanProcessor,
49
+ * which holds a _spanProcessors: SpanProcessor[] array.
50
+ * Both are TypeScript `private` (not ES #private), so accessible at runtime.
51
+ *
52
+ * Wrapped in try/catch because this accesses internal OTel fields that may
53
+ * change — must never crash the host app.
54
+ */
55
+ function getInternalSpanProcessors(provider) {
56
+ if (!require_types.isRecord(provider)) return;
57
+ try {
58
+ const active = provider._activeSpanProcessor;
59
+ if (!require_types.isRecord(active)) return void 0;
60
+ const arr = active._spanProcessors;
61
+ return Array.isArray(arr) ? arr : void 0;
62
+ } catch {
63
+ return;
64
+ }
65
+ }
66
+ /**
67
+ * Builds the `#spanSinks` key for a span. Span IDs are only guaranteed unique
68
+ * within a single trace, so spans are keyed by trace ID + span ID to avoid
69
+ * cross-trace collisions.
70
+ */
71
+ const spanSinkKey = (traceId, spanId) => `${traceId}:${spanId}`;
72
+ /**
73
+ * A read-only OTel span processor that is independent of the Extended Traces
74
+ * processor (`InngestSpanProcessor`).
75
+ *
76
+ * It tracks which spans belong to an Inngest step (seeded by
77
+ * {@link declareStartingSpan} and passed from parent to child in `onStart`).
78
+ *
79
+ * When a tracked span ends, it extracts {@link AIMetadata} from the span's
80
+ * attributes and pushes it to the span's {@link AIMetadataSink}.
81
+ */
82
+ var InngestMetadataSpanProcessor = class {
83
+ /**
84
+ * A map of tracked spans to their sink.
85
+ *
86
+ * We use traceId:spanID as the key, which uniquely identifies each span. See
87
+ * {@link spanSinkKey}
88
+ *
89
+ * The engine seeds the map during {@link declareStartingSpan} with the root
90
+ * span and its sink.
91
+ *
92
+ * During onStart, the processor looks up the span's parent's sink and then
93
+ * records the span as also using that sink. If the parent is not found, then
94
+ * the span is not descended from a root step span, and therefore does not
95
+ * need to have a sink.
96
+ *
97
+ * All spans with the same root span that started the step will share the
98
+ * same sink.
99
+ */
100
+ #spanSinks = /* @__PURE__ */ new Map();
101
+ /**
102
+ * A registry used to clean up items from `#spanSinks` when spans fall out of
103
+ * reference without ending. Avoids leaking entries (and the engine sink
104
+ * closures they reference) for spans that are never ended and are GC'd.
105
+ */
106
+ #spanCleanup = new FinalizationRegistry((key) => {
107
+ if (key) this.#spanSinks.delete(key);
108
+ });
109
+ /**
110
+ * Latches once this processor has been attached to a global OTel provider, so
111
+ * {@link attach} can never push it into a provider's processor list twice
112
+ * (which would double-process every span and double-count tokens).
113
+ */
114
+ #attached = false;
115
+ /**
116
+ * Idempotently attach this processor to the global OTel provider that already
117
+ * exists, so it begins receiving span lifecycle events.
118
+ */
119
+ attach() {
120
+ if (this.#attached) return;
121
+ const provider = getGlobalProvider();
122
+ if (!provider) return;
123
+ if (attachToProvider(provider, this)) {
124
+ this.#attached = true;
125
+ processorDevDebug("attached to global OTel provider");
126
+ }
127
+ }
128
+ /**
129
+ * Declare the step's root span. Seeds tracking so that the root and all of
130
+ * its descendants share the same AIMetadata sink.
131
+ */
132
+ declareStartingSpan({ span, traceparent, onAIMetadata }) {
133
+ if (!this.#attached) return;
134
+ if (!traceparent) return processorDevDebug("no traceparent found for span", span.spanContext().spanId, "so skipping it");
135
+ this.trackSpan(span, onAIMetadata);
136
+ }
137
+ /**
138
+ * Mark a span as tracked, recording its step's sink and registering it for
139
+ * cleanup.
140
+ *
141
+ * Read-only: unlike the Extended Traces processor, no attributes
142
+ * are stamped on the span.
143
+ */
144
+ trackSpan(span, sink) {
145
+ const { traceId, spanId } = span.spanContext();
146
+ const key = spanSinkKey(traceId, spanId);
147
+ this.#spanCleanup.register(span, key, span);
148
+ this.#spanSinks.set(key, sink);
149
+ }
150
+ /**
151
+ * Clean up references to a span that has ended (or been GC'd).
152
+ */
153
+ cleanupSpan(span) {
154
+ const { traceId, spanId } = span.spanContext();
155
+ this.#spanCleanup.unregister(span);
156
+ this.#spanSinks.delete(spanSinkKey(traceId, spanId));
157
+ }
158
+ /**
159
+ * Track children of spans we already care about, so the whole subtree under a
160
+ * declared root is captured.
161
+ */
162
+ onStart(span, parentContext) {
163
+ const parentSpanId = __opentelemetry_api.trace.getSpanContext(parentContext)?.spanId;
164
+ if (!parentSpanId) return;
165
+ const sink = this.#spanSinks.get(spanSinkKey(span.spanContext().traceId, parentSpanId));
166
+ if (!sink) return;
167
+ this.trackSpan(span, sink);
168
+ }
169
+ /**
170
+ * On end, extract any AI metadata from the span's attributes and push it to
171
+ * its sink, then clean up the span's tracking entry.
172
+ */
173
+ onEnd(span) {
174
+ const { traceId, spanId } = span.spanContext();
175
+ try {
176
+ const sink = this.#spanSinks.get(spanSinkKey(traceId, spanId));
177
+ if (!sink) return;
178
+ const aiMetadata = require_aiExtractor.extractAIMetadataFromAttributes(span.attributes);
179
+ if (Object.keys(aiMetadata).length === 0) return;
180
+ sink(aiMetadata);
181
+ } finally {
182
+ this.cleanupSpan(span);
183
+ }
184
+ }
185
+ async forceFlush() {}
186
+ async shutdown() {}
187
+ };
188
+ /**
189
+ * The process-wide metadata span processor instance.
190
+ */
191
+ const metadataSpanProcessor = new InngestMetadataSpanProcessor();
192
+
193
+ //#endregion
194
+ exports.metadataSpanProcessor = metadataSpanProcessor;
195
+ //# sourceMappingURL=metadataProcessor.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"metadataProcessor.cjs","names":["debugPrefix","trace","isRecord","#spanSinks","#attached","#spanCleanup","extractAIMetadataFromAttributes"],"sources":["../../../../src/components/execution/otel/metadataProcessor.ts"],"sourcesContent":["import { type Context, type Span, trace } from \"@opentelemetry/api\";\nimport type {\n ReadableSpan,\n SpanProcessor,\n} from \"@opentelemetry/sdk-trace-base\";\nimport Debug from \"debug\";\nimport { isRecord } from \"../../../helpers/types.ts\";\nimport {\n type AIMetadata,\n extractAIMetadataFromAttributes,\n} from \"./aiExtractor.ts\";\nimport { debugPrefix } from \"./consts.ts\";\n\nconst processorDevDebug = Debug(`${debugPrefix}:InngestMetadataSpanProcessor`);\n\n/**\n * Receives {@link AIMetadata} extracted from a span the moment it ends.\n * Supplied by the engine; the engine owns all aggregation and\n * step-attribution of the pushed values.\n */\nexport type AIMetadataSink = (metadata: AIMetadata) => void;\n\n/**\n * Resolve the currently-registered global OTel tracer provider, unwrapping\n * the `ProxyTracerProvider` wrapper that `trace.getTracerProvider()` returns.\n * Returns `undefined` when no provider is registered.\n */\nconst getGlobalProvider = (): object | undefined => {\n const globalProvider = trace.getTracerProvider();\n if (!globalProvider) {\n return undefined;\n }\n\n const existingProvider =\n \"getDelegate\" in globalProvider &&\n typeof globalProvider.getDelegate === \"function\"\n ? globalProvider.getDelegate()\n : globalProvider;\n\n return existingProvider ?? undefined;\n};\n\n/**\n * Attempts to add the given span processor to the given OTel provider.\n * Returns `true` if the processor was attached, `false` if the provider could\n * not be extended.\n *\n * It handles both OTel SDK v1 (`addSpanProcessor()`) and v2 (internal\n * `_spanProcessors` array).\n *\n * This intentionally duplicates the attach logic inside `extendProvider`\n * (`util.ts`) to avoid imports with instrumentation side effects.\n */\nconst attachToProvider = (\n provider: object,\n processor: SpanProcessor,\n): boolean => {\n // OTel SDK v1 exposes addSpanProcessor() on BasicTracerProvider.\n if (\n \"addSpanProcessor\" in provider &&\n typeof (provider as { addSpanProcessor?: unknown }).addSpanProcessor ===\n \"function\"\n ) {\n (\n provider as unknown as {\n addSpanProcessor: (p: SpanProcessor) => void;\n }\n ).addSpanProcessor(processor);\n return true;\n }\n\n // OTel SDK v2 removed addSpanProcessor() — span processors are constructor-only.\n // No public API exists to add processors post-construction (OTel issue #5299),\n // so push into the internal _spanProcessors array.\n // These fields are TypeScript `private` (not #private), so accessible at runtime.\n const spanProcessors = getInternalSpanProcessors(provider);\n if (spanProcessors) {\n spanProcessors.push(processor);\n return true;\n }\n\n return false;\n};\n\n/**\n * Extract the internal span processors array from a BasicTracerProvider.\n * Returns the mutable array if accessible, undefined otherwise.\n *\n * BasicTracerProvider._activeSpanProcessor is a MultiSpanProcessor,\n * which holds a _spanProcessors: SpanProcessor[] array.\n * Both are TypeScript `private` (not ES #private), so accessible at runtime.\n *\n * Wrapped in try/catch because this accesses internal OTel fields that may\n * change — must never crash the host app.\n */\nfunction getInternalSpanProcessors(provider: unknown): unknown[] | undefined {\n if (!isRecord(provider)) {\n return undefined;\n }\n\n try {\n const active = provider._activeSpanProcessor;\n if (!isRecord(active)) return undefined;\n\n const arr = active._spanProcessors;\n return Array.isArray(arr) ? arr : undefined;\n } catch {\n return undefined;\n }\n}\n\n/**\n * Builds the `#spanSinks` key for a span. Span IDs are only guaranteed unique\n * within a single trace, so spans are keyed by trace ID + span ID to avoid\n * cross-trace collisions.\n */\nconst spanSinkKey = (traceId: string, spanId: string): string =>\n `${traceId}:${spanId}`;\n\n/**\n * A read-only OTel span processor that is independent of the Extended Traces\n * processor (`InngestSpanProcessor`).\n *\n * It tracks which spans belong to an Inngest step (seeded by\n * {@link declareStartingSpan} and passed from parent to child in `onStart`).\n *\n * When a tracked span ends, it extracts {@link AIMetadata} from the span's\n * attributes and pushes it to the span's {@link AIMetadataSink}.\n */\nexport class InngestMetadataSpanProcessor implements SpanProcessor {\n /**\n * A map of tracked spans to their sink.\n *\n * We use traceId:spanID as the key, which uniquely identifies each span. See\n * {@link spanSinkKey}\n *\n * The engine seeds the map during {@link declareStartingSpan} with the root\n * span and its sink.\n *\n * During onStart, the processor looks up the span's parent's sink and then\n * records the span as also using that sink. If the parent is not found, then\n * the span is not descended from a root step span, and therefore does not\n * need to have a sink.\n *\n * All spans with the same root span that started the step will share the\n * same sink.\n */\n #spanSinks = new Map<string, AIMetadataSink>();\n\n /**\n * A registry used to clean up items from `#spanSinks` when spans fall out of\n * reference without ending. Avoids leaking entries (and the engine sink\n * closures they reference) for spans that are never ended and are GC'd.\n */\n #spanCleanup = new FinalizationRegistry<string>((key) => {\n if (key) {\n this.#spanSinks.delete(key);\n }\n });\n\n /**\n * Latches once this processor has been attached to a global OTel provider, so\n * {@link attach} can never push it into a provider's processor list twice\n * (which would double-process every span and double-count tokens).\n */\n #attached = false;\n\n /**\n * Idempotently attach this processor to the global OTel provider that already\n * exists, so it begins receiving span lifecycle events.\n */\n attach(): void {\n if (this.#attached) {\n return;\n }\n\n const provider = getGlobalProvider();\n if (!provider) {\n return;\n }\n\n if (attachToProvider(provider, this)) {\n this.#attached = true;\n processorDevDebug(\"attached to global OTel provider\");\n }\n }\n\n /**\n * Declare the step's root span. Seeds tracking so that the root and all of\n * its descendants share the same AIMetadata sink.\n */\n public declareStartingSpan({\n span,\n traceparent,\n onAIMetadata,\n }: {\n span: Span;\n traceparent: string | undefined;\n onAIMetadata: AIMetadataSink;\n }): void {\n // If this processor is not attached to a provider, we don't need to\n // declare starting spans.\n if (!this.#attached) {\n return;\n }\n\n // If we don't have a traceparent, then this isn't a step the Executor is\n // tracking, so we don't track it either.\n if (!traceparent) {\n return processorDevDebug(\n \"no traceparent found for span\",\n span.spanContext().spanId,\n \"so skipping it\",\n );\n }\n\n this.trackSpan(span, onAIMetadata);\n }\n\n /**\n * Mark a span as tracked, recording its step's sink and registering it for\n * cleanup.\n *\n * Read-only: unlike the Extended Traces processor, no attributes\n * are stamped on the span.\n */\n private trackSpan(span: Span, sink: AIMetadataSink): void {\n const { traceId, spanId } = span.spanContext();\n const key = spanSinkKey(traceId, spanId);\n\n this.#spanCleanup.register(span, key, span);\n this.#spanSinks.set(key, sink);\n }\n\n /**\n * Clean up references to a span that has ended (or been GC'd).\n */\n private cleanupSpan(span: ReadableSpan): void {\n const { traceId, spanId } = span.spanContext();\n this.#spanCleanup.unregister(span);\n this.#spanSinks.delete(spanSinkKey(traceId, spanId));\n }\n\n /**\n * Track children of spans we already care about, so the whole subtree under a\n * declared root is captured.\n */\n onStart(span: Span, parentContext: Context): void {\n const parentSpanId = trace.getSpanContext(parentContext)?.spanId;\n\n if (!parentSpanId) {\n return;\n }\n\n // A child span always shares its parent's trace ID, so the parent's key\n // can be built from the child's own span context.\n const sink = this.#spanSinks.get(\n spanSinkKey(span.spanContext().traceId, parentSpanId),\n );\n if (!sink) {\n return;\n }\n\n this.trackSpan(span, sink);\n }\n\n /**\n * On end, extract any AI metadata from the span's attributes and push it to\n * its sink, then clean up the span's tracking entry.\n */\n onEnd(span: ReadableSpan): void {\n const { traceId, spanId } = span.spanContext();\n\n try {\n const sink = this.#spanSinks.get(spanSinkKey(traceId, spanId));\n if (!sink) {\n return;\n }\n\n const aiMetadata = extractAIMetadataFromAttributes(span.attributes);\n if (Object.keys(aiMetadata).length === 0) {\n return;\n }\n\n sink(aiMetadata);\n } finally {\n this.cleanupSpan(span);\n }\n }\n\n // Nothing to flush or shut down: this processor is read-only and has no\n // exporter.\n async forceFlush(): Promise<void> {}\n\n async shutdown(): Promise<void> {}\n}\n\n/**\n * The process-wide metadata span processor instance.\n */\nexport const metadataSpanProcessor = new InngestMetadataSpanProcessor();\n"],"mappings":";;;;;;;;;AAaA,MAAM,uCAA0B,GAAGA,2BAAY,+BAA+B;;;;;;AAc9E,MAAM,0BAA8C;CAClD,MAAM,iBAAiBC,0BAAM,mBAAmB;AAChD,KAAI,CAAC,eACH;AASF,SALE,iBAAiB,kBACjB,OAAO,eAAe,gBAAgB,aAClC,eAAe,aAAa,GAC5B,mBAEqB;;;;;;;;;;;;;AAc7B,MAAM,oBACJ,UACA,cACY;AAEZ,KACE,sBAAsB,YACtB,OAAQ,SAA4C,qBAClD,YACF;AACA,EACE,SAGA,iBAAiB,UAAU;AAC7B,SAAO;;CAOT,MAAM,iBAAiB,0BAA0B,SAAS;AAC1D,KAAI,gBAAgB;AAClB,iBAAe,KAAK,UAAU;AAC9B,SAAO;;AAGT,QAAO;;;;;;;;;;;;;AAcT,SAAS,0BAA0B,UAA0C;AAC3E,KAAI,CAACC,uBAAS,SAAS,CACrB;AAGF,KAAI;EACF,MAAM,SAAS,SAAS;AACxB,MAAI,CAACA,uBAAS,OAAO,CAAE,QAAO;EAE9B,MAAM,MAAM,OAAO;AACnB,SAAO,MAAM,QAAQ,IAAI,GAAG,MAAM;SAC5B;AACN;;;;;;;;AASJ,MAAM,eAAe,SAAiB,WACpC,GAAG,QAAQ,GAAG;;;;;;;;;;;AAYhB,IAAa,+BAAb,MAAmE;;;;;;;;;;;;;;;;;;CAkBjE,6BAAa,IAAI,KAA6B;;;;;;CAO9C,eAAe,IAAI,sBAA8B,QAAQ;AACvD,MAAI,IACF,OAAKC,UAAW,OAAO,IAAI;GAE7B;;;;;;CAOF,YAAY;;;;;CAMZ,SAAe;AACb,MAAI,MAAKC,SACP;EAGF,MAAM,WAAW,mBAAmB;AACpC,MAAI,CAAC,SACH;AAGF,MAAI,iBAAiB,UAAU,KAAK,EAAE;AACpC,SAAKA,WAAY;AACjB,qBAAkB,mCAAmC;;;;;;;CAQzD,AAAO,oBAAoB,EACzB,MACA,aACA,gBAKO;AAGP,MAAI,CAAC,MAAKA,SACR;AAKF,MAAI,CAAC,YACH,QAAO,kBACL,iCACA,KAAK,aAAa,CAAC,QACnB,iBACD;AAGH,OAAK,UAAU,MAAM,aAAa;;;;;;;;;CAUpC,AAAQ,UAAU,MAAY,MAA4B;EACxD,MAAM,EAAE,SAAS,WAAW,KAAK,aAAa;EAC9C,MAAM,MAAM,YAAY,SAAS,OAAO;AAExC,QAAKC,YAAa,SAAS,MAAM,KAAK,KAAK;AAC3C,QAAKF,UAAW,IAAI,KAAK,KAAK;;;;;CAMhC,AAAQ,YAAY,MAA0B;EAC5C,MAAM,EAAE,SAAS,WAAW,KAAK,aAAa;AAC9C,QAAKE,YAAa,WAAW,KAAK;AAClC,QAAKF,UAAW,OAAO,YAAY,SAAS,OAAO,CAAC;;;;;;CAOtD,QAAQ,MAAY,eAA8B;EAChD,MAAM,eAAeF,0BAAM,eAAe,cAAc,EAAE;AAE1D,MAAI,CAAC,aACH;EAKF,MAAM,OAAO,MAAKE,UAAW,IAC3B,YAAY,KAAK,aAAa,CAAC,SAAS,aAAa,CACtD;AACD,MAAI,CAAC,KACH;AAGF,OAAK,UAAU,MAAM,KAAK;;;;;;CAO5B,MAAM,MAA0B;EAC9B,MAAM,EAAE,SAAS,WAAW,KAAK,aAAa;AAE9C,MAAI;GACF,MAAM,OAAO,MAAKA,UAAW,IAAI,YAAY,SAAS,OAAO,CAAC;AAC9D,OAAI,CAAC,KACH;GAGF,MAAM,aAAaG,oDAAgC,KAAK,WAAW;AACnE,OAAI,OAAO,KAAK,WAAW,CAAC,WAAW,EACrC;AAGF,QAAK,WAAW;YACR;AACR,QAAK,YAAY,KAAK;;;CAM1B,MAAM,aAA4B;CAElC,MAAM,WAA0B;;;;;AAMlC,MAAa,wBAAwB,IAAI,8BAA8B"}