@introspection-sdk/introspection-node 0.4.2 → 0.4.3

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 (151) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/.turbo/turbo-check-types.log +4 -0
  3. package/.turbo/turbo-lint.log +5 -0
  4. package/dist/client.d.ts +42 -132
  5. package/dist/client.d.ts.map +1 -1
  6. package/dist/client.js +67 -366
  7. package/dist/client.js.map +1 -0
  8. package/dist/converters/claude.d.ts +4 -0
  9. package/dist/converters/claude.d.ts.map +1 -1
  10. package/dist/converters/claude.js +7 -0
  11. package/dist/converters/claude.js.map +1 -0
  12. package/dist/converters/gemini.d.ts +90 -0
  13. package/dist/converters/gemini.d.ts.map +1 -0
  14. package/dist/converters/gemini.js +171 -0
  15. package/dist/converters/gemini.js.map +1 -0
  16. package/dist/converters/index.d.ts +1 -1
  17. package/dist/converters/index.d.ts.map +1 -1
  18. package/dist/converters/index.js +2 -1
  19. package/dist/converters/index.js.map +1 -0
  20. package/dist/converters/openai.js +1 -0
  21. package/dist/converters/openai.js.map +1 -0
  22. package/dist/converters/openclaw.d.ts +16 -0
  23. package/dist/converters/openclaw.d.ts.map +1 -0
  24. package/dist/converters/openclaw.js +49 -0
  25. package/dist/converters/openinference.js +1 -0
  26. package/dist/converters/openinference.js.map +1 -0
  27. package/dist/converters/vercel.d.ts.map +1 -1
  28. package/dist/converters/vercel.js +31 -3
  29. package/dist/converters/vercel.js.map +1 -0
  30. package/dist/http.d.ts +36 -0
  31. package/dist/http.d.ts.map +1 -0
  32. package/dist/http.js +145 -0
  33. package/dist/http.js.map +1 -0
  34. package/dist/index.d.ts +24 -27
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +22 -32
  37. package/dist/index.js.map +1 -0
  38. package/dist/otel/anthropic.d.ts +62 -0
  39. package/dist/otel/anthropic.d.ts.map +1 -0
  40. package/dist/otel/anthropic.js +425 -0
  41. package/dist/otel/anthropic.js.map +1 -0
  42. package/dist/otel/claude-hooks.d.ts +346 -0
  43. package/dist/otel/claude-hooks.d.ts.map +1 -0
  44. package/dist/otel/claude-hooks.js +645 -0
  45. package/dist/otel/claude-hooks.js.map +1 -0
  46. package/dist/otel/claude-wrapper.d.ts +93 -0
  47. package/dist/otel/claude-wrapper.d.ts.map +1 -0
  48. package/dist/otel/claude-wrapper.js +155 -0
  49. package/dist/otel/claude-wrapper.js.map +1 -0
  50. package/dist/otel/gemini.d.ts +48 -0
  51. package/dist/otel/gemini.d.ts.map +1 -0
  52. package/dist/otel/gemini.js +297 -0
  53. package/dist/otel/gemini.js.map +1 -0
  54. package/dist/otel/index.d.ts +27 -0
  55. package/dist/otel/index.d.ts.map +1 -0
  56. package/dist/otel/index.js +33 -0
  57. package/dist/otel/index.js.map +1 -0
  58. package/dist/otel/langchain-handler.d.ts +112 -0
  59. package/dist/otel/langchain-handler.d.ts.map +1 -0
  60. package/dist/otel/langchain-handler.js +555 -0
  61. package/dist/otel/langchain-handler.js.map +1 -0
  62. package/dist/otel/logs.d.ts +82 -0
  63. package/dist/otel/logs.d.ts.map +1 -0
  64. package/dist/otel/logs.js +271 -0
  65. package/dist/otel/logs.js.map +1 -0
  66. package/dist/otel/mastra-exporter.d.ts +87 -0
  67. package/dist/otel/mastra-exporter.d.ts.map +1 -0
  68. package/dist/otel/mastra-exporter.js +525 -0
  69. package/dist/otel/mastra-exporter.js.map +1 -0
  70. package/dist/otel/pi.d.ts +59 -0
  71. package/dist/otel/pi.d.ts.map +1 -0
  72. package/dist/otel/pi.js +67 -0
  73. package/dist/otel/pi.js.map +1 -0
  74. package/dist/otel/setup.d.ts +23 -0
  75. package/dist/otel/setup.d.ts.map +1 -0
  76. package/dist/otel/setup.js +127 -0
  77. package/dist/otel/setup.js.map +1 -0
  78. package/dist/otel/span-processor.d.ts +62 -0
  79. package/dist/otel/span-processor.d.ts.map +1 -0
  80. package/dist/otel/span-processor.js +319 -0
  81. package/dist/otel/span-processor.js.map +1 -0
  82. package/dist/otel/tracing-processor.d.ts +139 -0
  83. package/dist/otel/tracing-processor.d.ts.map +1 -0
  84. package/dist/otel/tracing-processor.js +418 -0
  85. package/dist/otel/tracing-processor.js.map +1 -0
  86. package/dist/resources/experiments.d.ts +31 -0
  87. package/dist/resources/experiments.d.ts.map +1 -0
  88. package/dist/resources/experiments.js +133 -0
  89. package/dist/resources/experiments.js.map +1 -0
  90. package/dist/resources/recipes.d.ts +22 -0
  91. package/dist/resources/recipes.d.ts.map +1 -0
  92. package/dist/resources/recipes.js +61 -0
  93. package/dist/resources/recipes.js.map +1 -0
  94. package/dist/resources/runtimes.d.ts +76 -0
  95. package/dist/resources/runtimes.d.ts.map +1 -0
  96. package/dist/resources/runtimes.js +179 -0
  97. package/dist/resources/runtimes.js.map +1 -0
  98. package/dist/runner-resources/files.d.ts +35 -0
  99. package/dist/runner-resources/files.d.ts.map +1 -0
  100. package/dist/runner-resources/files.js +131 -0
  101. package/dist/runner-resources/files.js.map +1 -0
  102. package/dist/runner-resources/tasks.d.ts +39 -0
  103. package/dist/runner-resources/tasks.d.ts.map +1 -0
  104. package/dist/runner-resources/tasks.js +129 -0
  105. package/dist/runner-resources/tasks.js.map +1 -0
  106. package/dist/runner.d.ts +80 -0
  107. package/dist/runner.d.ts.map +1 -0
  108. package/dist/runner.js +159 -0
  109. package/dist/runner.js.map +1 -0
  110. package/dist/streaming.d.ts +8 -0
  111. package/dist/streaming.d.ts.map +1 -0
  112. package/dist/streaming.js +67 -0
  113. package/dist/streaming.js.map +1 -0
  114. package/dist/types/genai.d.ts +165 -0
  115. package/dist/types/genai.d.ts.map +1 -0
  116. package/dist/types/genai.js +82 -0
  117. package/dist/types.js +1 -0
  118. package/dist/types.js.map +1 -0
  119. package/dist/utils.js +1 -0
  120. package/dist/utils.js.map +1 -0
  121. package/dist/version.js +1 -0
  122. package/dist/version.js.map +1 -0
  123. package/package.json +81 -24
  124. package/src/client.ts +76 -473
  125. package/src/converters/claude.ts +10 -0
  126. package/src/converters/gemini.ts +243 -0
  127. package/src/converters/index.ts +13 -7
  128. package/src/converters/vercel.ts +41 -3
  129. package/src/http.ts +163 -0
  130. package/src/index.ts +110 -75
  131. package/src/{anthropic.ts → otel/anthropic.ts} +95 -8
  132. package/src/{claude-hooks.ts → otel/claude-hooks.ts} +119 -20
  133. package/src/{claude-wrapper.ts → otel/claude-wrapper.ts} +1 -1
  134. package/src/otel/gemini.ts +397 -0
  135. package/src/otel/index.ts +80 -0
  136. package/src/{langchain-handler.ts → otel/langchain-handler.ts} +53 -6
  137. package/src/otel/logs.ts +394 -0
  138. package/src/{mastra-exporter.ts → otel/mastra-exporter.ts} +83 -22
  139. package/src/otel/pi.ts +88 -0
  140. package/src/otel/setup.ts +174 -0
  141. package/src/{span-processor.ts → otel/span-processor.ts} +11 -6
  142. package/src/{tracing-processor.ts → otel/tracing-processor.ts} +94 -19
  143. package/src/resources/experiments.ts +170 -0
  144. package/src/resources/recipes.ts +73 -0
  145. package/src/resources/runtimes.ts +231 -0
  146. package/src/runner-resources/files.ts +163 -0
  147. package/src/runner-resources/tasks.ts +156 -0
  148. package/src/runner.ts +194 -0
  149. package/src/streaming.ts +57 -0
  150. package/src/ai-sdk-integration.ts +0 -691
  151. package/src/converters/ai-sdk.ts +0 -323
@@ -0,0 +1,394 @@
1
+ /**
2
+ * IntrospectionLogs — OTel logs exporter for Introspection.
3
+ *
4
+ * Owns its own `LoggerProvider` and OTLP log exporter. Provides `track`,
5
+ * `feedback`, `identify`, and async-context helpers (`withBaggage`,
6
+ * `withAgent`, `withConversation`, etc.) that propagate identity and
7
+ * gen_ai context through OpenTelemetry baggage.
8
+ *
9
+ * Fully independent of {@link IntrospectionClient}. Construct it directly
10
+ * with the OTel peer dependencies installed.
11
+ *
12
+ * @example
13
+ * ```typescript
14
+ * import { IntrospectionLogs } from "@introspection-sdk/introspection-node/otel";
15
+ *
16
+ * const logs = new IntrospectionLogs({
17
+ * token: process.env.INTROSPECTION_TOKEN,
18
+ * serviceName: "my-service",
19
+ * });
20
+ *
21
+ * await logs.withAgent("support-bot", "agent_1", () =>
22
+ * logs.withConversation("conv_123", undefined, () => {
23
+ * logs.feedback("thumbs_up", { comments: "Great answer" });
24
+ * }),
25
+ * );
26
+ *
27
+ * await logs.shutdown();
28
+ * ```
29
+ */
30
+
31
+ import { SeverityNumber, type LogAttributes } from "@opentelemetry/api-logs";
32
+ import {
33
+ LoggerProvider,
34
+ BatchLogRecordProcessor,
35
+ } from "@opentelemetry/sdk-logs";
36
+ import { OTLPLogExporter } from "@opentelemetry/exporter-logs-otlp-proto";
37
+ import {
38
+ resourceFromAttributes,
39
+ defaultResource,
40
+ } from "@opentelemetry/resources";
41
+ import { ATTR_SERVICE_NAME } from "@opentelemetry/semantic-conventions";
42
+ import { propagation, context } from "@opentelemetry/api";
43
+
44
+ import { logger as sdkLogger, withOtlpHttpsProxy } from "../utils.js";
45
+ import { VERSION } from "../version.js";
46
+ import {
47
+ generateEventId,
48
+ type FeedbackOptions,
49
+ type UserTraits,
50
+ type GenAiContext,
51
+ type IdentityContext,
52
+ } from "../types.js";
53
+
54
+ /**
55
+ * Configuration for {@link IntrospectionLogs}.
56
+ */
57
+ export interface IntrospectionLogsOptions {
58
+ /** Authentication token (env: INTROSPECTION_TOKEN). */
59
+ token?: string;
60
+ /** Service name for telemetry (env: INTROSPECTION_SERVICE_NAME, default: "introspection-client"). */
61
+ serviceName?: string;
62
+ /**
63
+ * Base URL for the OTLP collector (env: INTROSPECTION_BASE_OTEL_URL,
64
+ * default: "https://otel.introspection.dev").
65
+ */
66
+ baseOtelUrl?: string;
67
+ /** Additional HTTP headers to include in exporter requests. */
68
+ additionalHeaders?: Record<string, string>;
69
+ /** Flush interval in milliseconds (default: 5000). */
70
+ flushInterval?: number;
71
+ /** Maximum queue size before auto-flush (default: 100). */
72
+ maxBatchSize?: number;
73
+ }
74
+
75
+ export class IntrospectionLogs {
76
+ private loggerProvider: LoggerProvider;
77
+ private otelLogger: ReturnType<LoggerProvider["getLogger"]>;
78
+ private userId: string | undefined;
79
+ private anonymousId: string | undefined;
80
+ private traits: Record<string, unknown> = {};
81
+
82
+ constructor(options: IntrospectionLogsOptions = {}) {
83
+ const token = options.token || process.env.INTROSPECTION_TOKEN || "";
84
+ const serviceName =
85
+ options.serviceName ||
86
+ process.env.INTROSPECTION_SERVICE_NAME ||
87
+ "introspection-client";
88
+ const baseOtelUrl =
89
+ options.baseOtelUrl ||
90
+ process.env.INTROSPECTION_BASE_OTEL_URL ||
91
+ "https://otel.introspection.dev";
92
+
93
+ if (!token) {
94
+ sdkLogger.warn(
95
+ "IntrospectionLogs: No token provided. Events will not be sent.",
96
+ );
97
+ }
98
+
99
+ // Construct endpoint URL for logs.
100
+ const endpoint = baseOtelUrl.endsWith("/v1/logs")
101
+ ? baseOtelUrl
102
+ : `${baseOtelUrl.replace(/\/$/, "")}/v1/logs`;
103
+
104
+ const headers: Record<string, string> = {
105
+ Authorization: `Bearer ${token}`,
106
+ ...(options.additionalHeaders || {}),
107
+ };
108
+
109
+ const exporter = new OTLPLogExporter(
110
+ withOtlpHttpsProxy({
111
+ url: endpoint,
112
+ headers,
113
+ }),
114
+ );
115
+
116
+ const processor = new BatchLogRecordProcessor(exporter, {
117
+ maxQueueSize: options.maxBatchSize ?? 100,
118
+ scheduledDelayMillis: options.flushInterval ?? 5000,
119
+ });
120
+
121
+ const baseResource = defaultResource();
122
+ const introspectionResource = resourceFromAttributes({
123
+ [ATTR_SERVICE_NAME]: serviceName,
124
+ });
125
+ const resource = baseResource.merge(introspectionResource);
126
+
127
+ this.loggerProvider = new LoggerProvider({
128
+ resource,
129
+ processors: [processor],
130
+ });
131
+
132
+ this.otelLogger = this.loggerProvider.getLogger(
133
+ "@introspection-sdk/introspection-node",
134
+ VERSION,
135
+ );
136
+
137
+ sdkLogger.info(
138
+ `IntrospectionLogs initialized: service=${serviceName}, otlp=${endpoint}`,
139
+ );
140
+ }
141
+
142
+ /** Get gen_ai context from baggage */
143
+ private getGenAiFromContext(): GenAiContext {
144
+ const baggage = propagation.getBaggage(context.active());
145
+ return {
146
+ conversationId: baggage?.getEntry("gen_ai.conversation.id")?.value,
147
+ previousResponseId: baggage?.getEntry(
148
+ "gen_ai.request.previous_response_id",
149
+ )?.value,
150
+ agentName: baggage?.getEntry("gen_ai.agent.name")?.value,
151
+ agentId: baggage?.getEntry("gen_ai.agent.id")?.value,
152
+ };
153
+ }
154
+
155
+ /** Get identity from baggage or instance state */
156
+ private getIdentityFromContext(): IdentityContext {
157
+ const baggage = propagation.getBaggage(context.active());
158
+ return {
159
+ userId: baggage?.getEntry("identity.user_id")?.value || this.userId,
160
+ anonymousId:
161
+ baggage?.getEntry("identity.anonymous_id")?.value || this.anonymousId,
162
+ };
163
+ }
164
+
165
+ private getTimestamp(): [number, number] {
166
+ const hrTimeNs = process.hrtime.bigint();
167
+ const epochNs = BigInt(Date.now()) * BigInt(1_000_000);
168
+ const offsetNs = hrTimeNs - process.hrtime.bigint() + epochNs;
169
+ const seconds = Number(offsetNs / BigInt(1_000_000_000));
170
+ const nanos = Number(offsetNs % BigInt(1_000_000_000));
171
+ return [seconds, nanos];
172
+ }
173
+
174
+ private buildAttributes(
175
+ eventName: string,
176
+ options: {
177
+ properties?: Record<string, unknown>;
178
+ traits?: Record<string, unknown>;
179
+ conversationId?: string;
180
+ previousResponseId?: string;
181
+ eventId?: string;
182
+ } = {},
183
+ ): LogAttributes {
184
+ const { properties, traits, conversationId, previousResponseId, eventId } =
185
+ options;
186
+
187
+ const attributes: LogAttributes = {
188
+ "event.name": eventName,
189
+ "event.id": eventId || generateEventId(),
190
+ };
191
+
192
+ const identity = this.getIdentityFromContext();
193
+ if (identity.userId) attributes["identity.user.id"] = identity.userId;
194
+ if (identity.anonymousId)
195
+ attributes["identity.anonymous.id"] = identity.anonymousId;
196
+
197
+ const genAi = this.getGenAiFromContext();
198
+ const finalConversationId = conversationId || genAi.conversationId;
199
+ const finalPreviousResponseId =
200
+ previousResponseId || genAi.previousResponseId;
201
+
202
+ if (finalConversationId)
203
+ attributes["gen_ai.conversation.id"] = finalConversationId;
204
+ if (finalPreviousResponseId)
205
+ attributes["gen_ai.request.previous_response_id"] =
206
+ finalPreviousResponseId;
207
+ if (genAi.agentName) attributes["gen_ai.agent.name"] = genAi.agentName;
208
+ if (genAi.agentId) attributes["gen_ai.agent.id"] = genAi.agentId;
209
+
210
+ if (properties) {
211
+ for (const [key, value] of Object.entries(properties)) {
212
+ if (value != null) {
213
+ const attrKey = `properties.${key}`;
214
+ if (typeof value === "object") {
215
+ attributes[attrKey] = JSON.stringify(value);
216
+ } else if (
217
+ typeof value === "string" ||
218
+ typeof value === "number" ||
219
+ typeof value === "boolean"
220
+ ) {
221
+ attributes[attrKey] = value;
222
+ }
223
+ }
224
+ }
225
+ }
226
+
227
+ if (traits) {
228
+ for (const [key, value] of Object.entries(traits)) {
229
+ if (value != null) {
230
+ const attrKey = `context.traits.${key}`;
231
+ if (typeof value === "object") {
232
+ attributes[attrKey] = JSON.stringify(value);
233
+ } else if (
234
+ typeof value === "string" ||
235
+ typeof value === "number" ||
236
+ typeof value === "boolean"
237
+ ) {
238
+ attributes[attrKey] = value;
239
+ }
240
+ }
241
+ }
242
+ }
243
+
244
+ return attributes;
245
+ }
246
+
247
+ track(
248
+ eventName: string,
249
+ properties?: Record<string, unknown>,
250
+ options?: { eventId?: string },
251
+ ): void {
252
+ const attributes = this.buildAttributes(eventName, {
253
+ properties,
254
+ eventId: options?.eventId,
255
+ });
256
+ this.otelLogger.emit({
257
+ timestamp: this.getTimestamp(),
258
+ context: context.active(),
259
+ severityNumber: SeverityNumber.INFO,
260
+ severityText: "INFO",
261
+ attributes,
262
+ });
263
+ sdkLogger.debug(`Tracked: ${eventName}`);
264
+ }
265
+
266
+ feedback(name: string, options: FeedbackOptions = {}): void {
267
+ const { comments, conversationId, previousResponseId, eventId, ...extra } =
268
+ options;
269
+ const properties: Record<string, unknown> = { name, ...extra };
270
+ if (comments) properties.comments = comments;
271
+ const attributes = this.buildAttributes("introspection.feedback", {
272
+ properties,
273
+ conversationId: conversationId as string | undefined,
274
+ previousResponseId: previousResponseId as string | undefined,
275
+ eventId: eventId as string | undefined,
276
+ });
277
+ this.otelLogger.emit({
278
+ timestamp: this.getTimestamp(),
279
+ context: context.active(),
280
+ severityNumber: SeverityNumber.INFO,
281
+ severityText: "INFO",
282
+ attributes,
283
+ });
284
+ sdkLogger.debug(`Feedback: ${name}`);
285
+ }
286
+
287
+ identify(
288
+ userId: string,
289
+ traits?: UserTraits,
290
+ anonymousId?: string,
291
+ eventId?: string,
292
+ ): void {
293
+ this.userId = userId;
294
+ if (anonymousId) this.anonymousId = anonymousId;
295
+ if (traits) this.traits = { ...this.traits, ...traits };
296
+ const attributes = this.buildAttributes("identify", { traits, eventId });
297
+ this.otelLogger.emit({
298
+ timestamp: this.getTimestamp(),
299
+ context: context.active(),
300
+ severityNumber: SeverityNumber.INFO,
301
+ severityText: "INFO",
302
+ attributes,
303
+ });
304
+ sdkLogger.debug(`Identified: ${userId}`);
305
+ }
306
+
307
+ createBaggageContext(values: Record<string, string>) {
308
+ const ctx = context.active();
309
+ let bag = propagation.getBaggage(ctx) || propagation.createBaggage();
310
+ for (const [key, value] of Object.entries(values)) {
311
+ bag = bag.setEntry(key, { value });
312
+ }
313
+ return propagation.setBaggage(ctx, bag);
314
+ }
315
+
316
+ async withBaggage<T>(
317
+ values: Record<string, string>,
318
+ callback: () => T | Promise<T>,
319
+ ): Promise<T> {
320
+ const ctx = this.createBaggageContext(values);
321
+ return await context.with(ctx, callback);
322
+ }
323
+
324
+ async withAgent<T>(
325
+ agentName: string,
326
+ agentId: string | undefined,
327
+ callback: () => T | Promise<T>,
328
+ ): Promise<T> {
329
+ const values: Record<string, string> = { "gen_ai.agent.name": agentName };
330
+ if (agentId) values["gen_ai.agent.id"] = agentId;
331
+ return await context.with(this.createBaggageContext(values), callback);
332
+ }
333
+
334
+ async withConversation<T>(
335
+ conversationId: string | undefined,
336
+ previousResponseId: string | undefined,
337
+ callback: () => T | Promise<T>,
338
+ ): Promise<T> {
339
+ const values: Record<string, string> = {};
340
+ if (conversationId) values["gen_ai.conversation.id"] = conversationId;
341
+ if (previousResponseId)
342
+ values["gen_ai.request.previous_response_id"] = previousResponseId;
343
+ return await context.with(this.createBaggageContext(values), callback);
344
+ }
345
+
346
+ setUserId(userId: string): void {
347
+ this.userId = userId;
348
+ }
349
+
350
+ setAnonymousId(anonymousId: string): void {
351
+ this.anonymousId = anonymousId;
352
+ }
353
+
354
+ async withUserId<T>(
355
+ userId: string,
356
+ callback: () => T | Promise<T>,
357
+ ): Promise<T> {
358
+ return await context.with(
359
+ this.createBaggageContext({ "identity.user_id": userId }),
360
+ callback,
361
+ );
362
+ }
363
+
364
+ async withAnonymousId<T>(
365
+ anonymousId: string,
366
+ callback: () => T | Promise<T>,
367
+ ): Promise<T> {
368
+ return await context.with(
369
+ this.createBaggageContext({ "identity.anonymous_id": anonymousId }),
370
+ callback,
371
+ );
372
+ }
373
+
374
+ getAnonymousId(): string | undefined {
375
+ return this.anonymousId;
376
+ }
377
+
378
+ reset(): void {
379
+ this.userId = undefined;
380
+ this.anonymousId = undefined;
381
+ this.traits = {};
382
+ sdkLogger.debug("IntrospectionLogs state reset");
383
+ }
384
+
385
+ async flush(): Promise<void> {
386
+ await this.loggerProvider.forceFlush();
387
+ sdkLogger.debug("Flushed pending log records");
388
+ }
389
+
390
+ async shutdown(): Promise<void> {
391
+ await this.loggerProvider.shutdown();
392
+ sdkLogger.debug("IntrospectionLogs shutdown complete");
393
+ }
394
+ }
@@ -51,8 +51,8 @@ import { resourceFromAttributes } from "@opentelemetry/resources";
51
51
  import { ATTR_SERVICE_NAME } from "@opentelemetry/semantic-conventions";
52
52
  import { randomUUID } from "crypto";
53
53
 
54
- import { logger, withOtlpHttpsProxy } from "./utils.js";
55
- import { VERSION } from "./version.js";
54
+ import { logger, withOtlpHttpsProxy } from "../utils.js";
55
+ import { VERSION } from "../version.js";
56
56
  import type {
57
57
  InputMessage,
58
58
  OutputMessage,
@@ -77,7 +77,7 @@ export interface MastraExporterAdvancedOptions {
77
77
  export interface IntrospectionMastraExporterOptions {
78
78
  /** Authentication token (env: INTROSPECTION_TOKEN). */
79
79
  token?: string;
80
- /** Base URL for the API (env: INTROSPECTION_BASE_URL). */
80
+ /** Base URL for the API (env: INTROSPECTION_BASE_OTEL_URL). */
81
81
  baseUrl?: string;
82
82
  /** Additional headers to include in requests. */
83
83
  additionalHeaders?: Record<string, string>;
@@ -152,7 +152,7 @@ export class IntrospectionMastraExporter extends BaseExporter {
152
152
 
153
153
  const baseUrl =
154
154
  opts.baseUrl ||
155
- process.env.INTROSPECTION_BASE_URL ||
155
+ process.env.INTROSPECTION_BASE_OTEL_URL ||
156
156
  "https://otel.introspection.dev";
157
157
  const endpoint = baseUrl.endsWith("/v1/traces")
158
158
  ? baseUrl
@@ -291,7 +291,26 @@ export class IntrospectionMastraExporter extends BaseExporter {
291
291
  span.entityName || span.name || "agent",
292
292
  );
293
293
 
294
- if (attrs?.["instructions"] && typeof attrs["instructions"] === "string") {
294
+ // Prefer system messages from span.input (full prompt sent to LLM) over
295
+ // attrs.instructions (may be just the short placeholder string).
296
+ const inputMsgs = Array.isArray(span.input)
297
+ ? (span.input as Record<string, unknown>[])
298
+ : undefined;
299
+ const systemMsgsFromInput = inputMsgs?.filter((m) => m.role === "system");
300
+ if (systemMsgsFromInput && systemMsgsFromInput.length > 0) {
301
+ rootSpan.setAttribute(
302
+ "gen_ai.system_instructions",
303
+ JSON.stringify(
304
+ systemMsgsFromInput.map((m) => ({
305
+ type: "text",
306
+ content: this._extractTextContent(m.content),
307
+ })),
308
+ ),
309
+ );
310
+ } else if (
311
+ attrs?.["instructions"] &&
312
+ typeof attrs["instructions"] === "string"
313
+ ) {
295
314
  rootSpan.setAttribute(
296
315
  "gen_ai.system_instructions",
297
316
  JSON.stringify([{ type: "text", content: attrs["instructions"] }]),
@@ -345,12 +364,30 @@ export class IntrospectionMastraExporter extends BaseExporter {
345
364
  otelSpan.setAttribute("gen_ai.response.model", responseModel);
346
365
  if (responseId) otelSpan.setAttribute("gen_ai.response.id", responseId);
347
366
 
348
- // Input messages — model_step has input.body.input (raw API messages)
367
+ // Input messages — span.input is either:
368
+ // (a) a flat normalized array { role, content }[] from Mastra's inputMessages path, or
369
+ // (b) an object { body: { input/messages, tools } } from the raw HTTP request path.
349
370
  if (span.input != null) {
350
- const rawInput = span.input as Record<string, unknown>;
351
- const body = rawInput?.body as Record<string, unknown> | undefined;
352
- const apiMessages = body?.input ?? body?.messages;
353
- if (Array.isArray(apiMessages)) {
371
+ let apiMessages: Record<string, unknown>[] | undefined;
372
+ let tools: unknown[] | undefined;
373
+
374
+ if (Array.isArray(span.input)) {
375
+ // Path (a): flat array already normalized by Mastra's normalizeMessages()
376
+ apiMessages = span.input as Record<string, unknown>[];
377
+ } else {
378
+ // Path (b): nested request body
379
+ const rawInput = span.input as Record<string, unknown>;
380
+ const body = rawInput?.body as Record<string, unknown> | undefined;
381
+ const msgs = body?.input ?? body?.messages;
382
+ if (Array.isArray(msgs)) {
383
+ apiMessages = msgs as Record<string, unknown>[];
384
+ }
385
+ if (Array.isArray(body?.tools)) {
386
+ tools = body.tools as unknown[];
387
+ }
388
+ }
389
+
390
+ if (apiMessages && apiMessages.length > 0) {
354
391
  const inputMessages = this._convertInput(apiMessages);
355
392
  if (inputMessages.length > 0) {
356
393
  otelSpan.setAttribute(
@@ -359,28 +396,22 @@ export class IntrospectionMastraExporter extends BaseExporter {
359
396
  );
360
397
  }
361
398
 
362
- // System instructions
363
- const systemMsgs = (apiMessages as Record<string, unknown>[]).filter(
364
- (m) => m.role === "system",
365
- );
399
+ // System instructions — combine all system messages into one attribute
400
+ const systemMsgs = apiMessages.filter((m) => m.role === "system");
366
401
  if (systemMsgs.length > 0) {
367
402
  otelSpan.setAttribute(
368
403
  "gen_ai.system_instructions",
369
404
  JSON.stringify(
370
405
  systemMsgs.map((m) => ({
371
406
  type: "text",
372
- content:
373
- typeof m.content === "string"
374
- ? m.content
375
- : JSON.stringify(m.content),
407
+ content: this._extractTextContent(m.content),
376
408
  })),
377
409
  ),
378
410
  );
379
411
  }
380
412
  }
381
413
 
382
- // Tool definitions from input.body.tools
383
- const tools = body?.tools;
414
+ // Tool definitions (only present on path (b))
384
415
  if (Array.isArray(tools) && tools.length > 0) {
385
416
  otelSpan.setAttribute(
386
417
  "gen_ai.tool.definitions",
@@ -431,6 +462,11 @@ export class IntrospectionMastraExporter extends BaseExporter {
431
462
  "gen_ai.usage.cache_read.input_tokens",
432
463
  inputDetails["cacheRead"],
433
464
  );
465
+ if (typeof inputDetails?.["cacheWrite"] === "number")
466
+ otelSpan.setAttribute(
467
+ "gen_ai.usage.cache_creation.input_tokens",
468
+ inputDetails["cacheWrite"],
469
+ );
434
470
  }
435
471
 
436
472
  // Finish reason
@@ -505,11 +541,17 @@ export class IntrospectionMastraExporter extends BaseExporter {
505
541
  // Skip system messages (handled separately)
506
542
  if (msg.role === "system") continue;
507
543
 
508
- // Standard role-based messages
544
+ // Standard role-based messages (AIv4 uses msg.content; AIv5 may use msg.parts)
509
545
  if (msg.role) {
546
+ const parts =
547
+ msg.content != null
548
+ ? this._extractParts(msg.content)
549
+ : Array.isArray(msg.parts)
550
+ ? this._extractParts(msg.parts)
551
+ : [];
510
552
  result.push({
511
553
  role: msg.role as InputMessage["role"],
512
- parts: this._extractParts(msg.content),
554
+ parts,
513
555
  });
514
556
  continue;
515
557
  }
@@ -588,6 +630,25 @@ export class IntrospectionMastraExporter extends BaseExporter {
588
630
  return [{ role: "assistant", parts, finish_reason: finishReason }];
589
631
  }
590
632
 
633
+ /** Extract plain text from a message content field (string, array of parts, or AIv5 parts). */
634
+ private _extractTextContent(content: unknown): string {
635
+ if (typeof content === "string") return content;
636
+ if (Array.isArray(content)) {
637
+ return content
638
+ .map((p: unknown) => {
639
+ const part = p as Record<string, unknown>;
640
+ return typeof part.text === "string"
641
+ ? part.text
642
+ : typeof part.content === "string"
643
+ ? part.content
644
+ : "";
645
+ })
646
+ .filter(Boolean)
647
+ .join("\n");
648
+ }
649
+ return typeof content === "undefined" ? "" : JSON.stringify(content);
650
+ }
651
+
591
652
  private _extractParts(content: unknown): MessagePart[] {
592
653
  if (typeof content === "string") {
593
654
  return [{ type: "text", content }];
package/src/otel/pi.ts ADDED
@@ -0,0 +1,88 @@
1
+ /**
2
+ * High-level pi Agent SDK integration for Introspection.
3
+ *
4
+ * Wraps {@link @introspection-sdk/introspection-pi} behind a zero-config
5
+ * interface so users don't need to manage OpenTelemetry tracers directly.
6
+ *
7
+ * @example
8
+ * ```ts
9
+ * import { IntrospectionPiInstrumentor, setupTracing } from "@introspection-sdk/introspection-node";
10
+ * import { Agent } from "@mariozechner/pi-agent-core";
11
+ *
12
+ * setupTracing({ serviceName: "my-app" });
13
+ * const instrumentor = new IntrospectionPiInstrumentor();
14
+ *
15
+ * const agent = new Agent({ ... });
16
+ * instrumentor.instrument(agent, {
17
+ * conversationId: "conv-123",
18
+ * agentId: "weather-agent",
19
+ * agentName: "Weather",
20
+ * });
21
+ *
22
+ * await agent.prompt("What's the weather in Tokyo?");
23
+ * instrumentor.stop();
24
+ * ```
25
+ */
26
+
27
+ import { trace } from "@opentelemetry/api";
28
+ import type { Agent } from "@mariozechner/pi-agent-core";
29
+ import {
30
+ instrumentStream,
31
+ instrumentAgent,
32
+ type AgentMeta,
33
+ type AgentInstrumentation,
34
+ } from "@introspection-sdk/introspection-pi";
35
+ import { VERSION } from "../version.js";
36
+
37
+ export type { AgentMeta } from "@introspection-sdk/introspection-pi";
38
+
39
+ export interface IntrospectionPiInstrumentorOptions {
40
+ /** Tracer name used for all spans produced by this instrumentor. */
41
+ tracerName?: string;
42
+ }
43
+
44
+ /**
45
+ * Zero-config pi Agent SDK integration for Introspection.
46
+ *
47
+ * Uses the global OTel tracer provider (registered by {@link setupTracing}).
48
+ * Call {@link instrument} once per {@link Agent} instance, then {@link stop}
49
+ * to unsubscribe all tool instrumentation and finalize open spans.
50
+ */
51
+ export class IntrospectionPiInstrumentor {
52
+ private _tracer: ReturnType<typeof trace.getTracer>;
53
+ private _instrumentations: AgentInstrumentation[] = [];
54
+
55
+ constructor(opts: IntrospectionPiInstrumentorOptions = {}) {
56
+ this._tracer = trace.getTracer(
57
+ opts.tracerName ?? "introspection-pi",
58
+ VERSION,
59
+ );
60
+ }
61
+
62
+ /**
63
+ * Instrument a pi {@link Agent}:
64
+ * - Wraps `agent.streamFn` to emit a `chat ${provider}` span per LLM call.
65
+ * - Subscribes to the agent loop to emit an `execute_tool ${name}` span per
66
+ * tool execution.
67
+ *
68
+ * @param agent - The pi Agent instance to instrument.
69
+ * @param meta - Identity metadata stamped on every span produced by this agent.
70
+ */
71
+ instrument(agent: Agent, meta: AgentMeta): void {
72
+ agent.streamFn = instrumentStream(agent.streamFn, {
73
+ tracer: this._tracer,
74
+ meta,
75
+ });
76
+ this._instrumentations.push(
77
+ instrumentAgent(agent, { tracer: this._tracer, meta }),
78
+ );
79
+ }
80
+
81
+ /**
82
+ * Unsubscribe all active tool instrumentations and finalize any open spans.
83
+ */
84
+ stop(): void {
85
+ for (const inst of this._instrumentations) inst.stop();
86
+ this._instrumentations = [];
87
+ }
88
+ }