@introspection-sdk/introspection-node 0.4.1 → 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 (166) 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/ai-sdk-integration.d.ts.map +1 -1
  5. package/dist/ai-sdk-integration.js +2 -2
  6. package/dist/claude-hooks.d.ts.map +1 -1
  7. package/dist/claude-hooks.js +3 -3
  8. package/dist/client.d.ts +42 -132
  9. package/dist/client.d.ts.map +1 -1
  10. package/dist/client.js +66 -365
  11. package/dist/client.js.map +1 -0
  12. package/dist/converters/claude.d.ts +5 -1
  13. package/dist/converters/claude.d.ts.map +1 -1
  14. package/dist/converters/claude.js +7 -0
  15. package/dist/converters/claude.js.map +1 -0
  16. package/dist/converters/gemini.d.ts +90 -0
  17. package/dist/converters/gemini.d.ts.map +1 -0
  18. package/dist/converters/gemini.js +171 -0
  19. package/dist/converters/gemini.js.map +1 -0
  20. package/dist/converters/index.d.ts +1 -1
  21. package/dist/converters/index.d.ts.map +1 -1
  22. package/dist/converters/index.js +2 -1
  23. package/dist/converters/index.js.map +1 -0
  24. package/dist/converters/openai.js +1 -0
  25. package/dist/converters/openai.js.map +1 -0
  26. package/dist/converters/openclaw.d.ts +16 -0
  27. package/dist/converters/openclaw.d.ts.map +1 -0
  28. package/dist/converters/openclaw.js +49 -0
  29. package/dist/converters/openinference.js +1 -0
  30. package/dist/converters/openinference.js.map +1 -0
  31. package/dist/converters/vercel.d.ts.map +1 -1
  32. package/dist/converters/vercel.js +31 -3
  33. package/dist/converters/vercel.js.map +1 -0
  34. package/dist/http.d.ts +36 -0
  35. package/dist/http.d.ts.map +1 -0
  36. package/dist/http.js +145 -0
  37. package/dist/http.js.map +1 -0
  38. package/dist/index.d.ts +24 -27
  39. package/dist/index.d.ts.map +1 -1
  40. package/dist/index.js +22 -32
  41. package/dist/index.js.map +1 -0
  42. package/dist/langchain-handler.d.ts.map +1 -1
  43. package/dist/langchain-handler.js +2 -2
  44. package/dist/mastra-exporter.d.ts.map +1 -1
  45. package/dist/mastra-exporter.js +2 -2
  46. package/dist/otel/anthropic.d.ts +62 -0
  47. package/dist/otel/anthropic.d.ts.map +1 -0
  48. package/dist/otel/anthropic.js +425 -0
  49. package/dist/otel/anthropic.js.map +1 -0
  50. package/dist/otel/claude-hooks.d.ts +346 -0
  51. package/dist/otel/claude-hooks.d.ts.map +1 -0
  52. package/dist/otel/claude-hooks.js +645 -0
  53. package/dist/otel/claude-hooks.js.map +1 -0
  54. package/dist/otel/claude-wrapper.d.ts +93 -0
  55. package/dist/otel/claude-wrapper.d.ts.map +1 -0
  56. package/dist/otel/claude-wrapper.js +155 -0
  57. package/dist/otel/claude-wrapper.js.map +1 -0
  58. package/dist/otel/gemini.d.ts +48 -0
  59. package/dist/otel/gemini.d.ts.map +1 -0
  60. package/dist/otel/gemini.js +297 -0
  61. package/dist/otel/gemini.js.map +1 -0
  62. package/dist/otel/index.d.ts +27 -0
  63. package/dist/otel/index.d.ts.map +1 -0
  64. package/dist/otel/index.js +33 -0
  65. package/dist/otel/index.js.map +1 -0
  66. package/dist/otel/langchain-handler.d.ts +112 -0
  67. package/dist/otel/langchain-handler.d.ts.map +1 -0
  68. package/dist/otel/langchain-handler.js +555 -0
  69. package/dist/otel/langchain-handler.js.map +1 -0
  70. package/dist/otel/logs.d.ts +82 -0
  71. package/dist/otel/logs.d.ts.map +1 -0
  72. package/dist/otel/logs.js +271 -0
  73. package/dist/otel/logs.js.map +1 -0
  74. package/dist/otel/mastra-exporter.d.ts +87 -0
  75. package/dist/otel/mastra-exporter.d.ts.map +1 -0
  76. package/dist/otel/mastra-exporter.js +525 -0
  77. package/dist/otel/mastra-exporter.js.map +1 -0
  78. package/dist/otel/pi.d.ts +59 -0
  79. package/dist/otel/pi.d.ts.map +1 -0
  80. package/dist/otel/pi.js +67 -0
  81. package/dist/otel/pi.js.map +1 -0
  82. package/dist/otel/setup.d.ts +23 -0
  83. package/dist/otel/setup.d.ts.map +1 -0
  84. package/dist/otel/setup.js +127 -0
  85. package/dist/otel/setup.js.map +1 -0
  86. package/dist/otel/span-processor.d.ts +62 -0
  87. package/dist/otel/span-processor.d.ts.map +1 -0
  88. package/dist/otel/span-processor.js +319 -0
  89. package/dist/otel/span-processor.js.map +1 -0
  90. package/dist/otel/tracing-processor.d.ts +139 -0
  91. package/dist/otel/tracing-processor.d.ts.map +1 -0
  92. package/dist/otel/tracing-processor.js +418 -0
  93. package/dist/otel/tracing-processor.js.map +1 -0
  94. package/dist/resources/experiments.d.ts +31 -0
  95. package/dist/resources/experiments.d.ts.map +1 -0
  96. package/dist/resources/experiments.js +133 -0
  97. package/dist/resources/experiments.js.map +1 -0
  98. package/dist/resources/recipes.d.ts +22 -0
  99. package/dist/resources/recipes.d.ts.map +1 -0
  100. package/dist/resources/recipes.js +61 -0
  101. package/dist/resources/recipes.js.map +1 -0
  102. package/dist/resources/runtimes.d.ts +76 -0
  103. package/dist/resources/runtimes.d.ts.map +1 -0
  104. package/dist/resources/runtimes.js +179 -0
  105. package/dist/resources/runtimes.js.map +1 -0
  106. package/dist/runner-resources/files.d.ts +35 -0
  107. package/dist/runner-resources/files.d.ts.map +1 -0
  108. package/dist/runner-resources/files.js +131 -0
  109. package/dist/runner-resources/files.js.map +1 -0
  110. package/dist/runner-resources/tasks.d.ts +39 -0
  111. package/dist/runner-resources/tasks.d.ts.map +1 -0
  112. package/dist/runner-resources/tasks.js +129 -0
  113. package/dist/runner-resources/tasks.js.map +1 -0
  114. package/dist/runner.d.ts +80 -0
  115. package/dist/runner.d.ts.map +1 -0
  116. package/dist/runner.js +159 -0
  117. package/dist/runner.js.map +1 -0
  118. package/dist/span-processor.d.ts.map +1 -1
  119. package/dist/span-processor.js +3 -3
  120. package/dist/streaming.d.ts +8 -0
  121. package/dist/streaming.d.ts.map +1 -0
  122. package/dist/streaming.js +67 -0
  123. package/dist/streaming.js.map +1 -0
  124. package/dist/tracing-processor.d.ts.map +1 -1
  125. package/dist/tracing-processor.js +3 -3
  126. package/dist/types/genai.d.ts +165 -0
  127. package/dist/types/genai.d.ts.map +1 -0
  128. package/dist/types/genai.js +82 -0
  129. package/dist/types.js +1 -0
  130. package/dist/types.js.map +1 -0
  131. package/dist/utils.d.ts +1 -0
  132. package/dist/utils.d.ts.map +1 -1
  133. package/dist/utils.js +14 -0
  134. package/dist/utils.js.map +1 -0
  135. package/dist/version.js +1 -0
  136. package/dist/version.js.map +1 -0
  137. package/package.json +83 -25
  138. package/src/client.ts +76 -471
  139. package/src/converters/claude.ts +11 -1
  140. package/src/converters/gemini.ts +243 -0
  141. package/src/converters/index.ts +13 -7
  142. package/src/converters/vercel.ts +41 -3
  143. package/src/http.ts +163 -0
  144. package/src/index.ts +110 -75
  145. package/src/{anthropic.ts → otel/anthropic.ts} +95 -8
  146. package/src/{claude-hooks.ts → otel/claude-hooks.ts} +125 -24
  147. package/src/{claude-wrapper.ts → otel/claude-wrapper.ts} +1 -1
  148. package/src/otel/gemini.ts +397 -0
  149. package/src/otel/index.ts +80 -0
  150. package/src/{langchain-handler.ts → otel/langchain-handler.ts} +56 -7
  151. package/src/otel/logs.ts +394 -0
  152. package/src/{mastra-exporter.ts → otel/mastra-exporter.ts} +86 -23
  153. package/src/otel/pi.ts +88 -0
  154. package/src/otel/setup.ts +174 -0
  155. package/src/{span-processor.ts → otel/span-processor.ts} +17 -10
  156. package/src/{tracing-processor.ts → otel/tracing-processor.ts} +100 -23
  157. package/src/resources/experiments.ts +170 -0
  158. package/src/resources/recipes.ts +73 -0
  159. package/src/resources/runtimes.ts +231 -0
  160. package/src/runner-resources/files.ts +163 -0
  161. package/src/runner-resources/tasks.ts +156 -0
  162. package/src/runner.ts +194 -0
  163. package/src/streaming.ts +57 -0
  164. package/src/utils.ts +15 -0
  165. package/src/ai-sdk-integration.ts +0 -689
  166. 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 } 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
@@ -164,7 +164,9 @@ export class IntrospectionMastraExporter extends BaseExporter {
164
164
  ...opts.additionalHeaders,
165
165
  };
166
166
 
167
- const spanExporter = new OTLPTraceExporter({ url: endpoint, headers });
167
+ const spanExporter = new OTLPTraceExporter(
168
+ withOtlpHttpsProxy({ url: endpoint, headers }),
169
+ );
168
170
  const effectiveBatchSize =
169
171
  token.startsWith("intro_dev") || token.startsWith("intro_staging")
170
172
  ? 1
@@ -289,7 +291,26 @@ export class IntrospectionMastraExporter extends BaseExporter {
289
291
  span.entityName || span.name || "agent",
290
292
  );
291
293
 
292
- 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
+ ) {
293
314
  rootSpan.setAttribute(
294
315
  "gen_ai.system_instructions",
295
316
  JSON.stringify([{ type: "text", content: attrs["instructions"] }]),
@@ -343,12 +364,30 @@ export class IntrospectionMastraExporter extends BaseExporter {
343
364
  otelSpan.setAttribute("gen_ai.response.model", responseModel);
344
365
  if (responseId) otelSpan.setAttribute("gen_ai.response.id", responseId);
345
366
 
346
- // 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.
347
370
  if (span.input != null) {
348
- const rawInput = span.input as Record<string, unknown>;
349
- const body = rawInput?.body as Record<string, unknown> | undefined;
350
- const apiMessages = body?.input ?? body?.messages;
351
- 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) {
352
391
  const inputMessages = this._convertInput(apiMessages);
353
392
  if (inputMessages.length > 0) {
354
393
  otelSpan.setAttribute(
@@ -357,28 +396,22 @@ export class IntrospectionMastraExporter extends BaseExporter {
357
396
  );
358
397
  }
359
398
 
360
- // System instructions
361
- const systemMsgs = (apiMessages as Record<string, unknown>[]).filter(
362
- (m) => m.role === "system",
363
- );
399
+ // System instructions — combine all system messages into one attribute
400
+ const systemMsgs = apiMessages.filter((m) => m.role === "system");
364
401
  if (systemMsgs.length > 0) {
365
402
  otelSpan.setAttribute(
366
403
  "gen_ai.system_instructions",
367
404
  JSON.stringify(
368
405
  systemMsgs.map((m) => ({
369
406
  type: "text",
370
- content:
371
- typeof m.content === "string"
372
- ? m.content
373
- : JSON.stringify(m.content),
407
+ content: this._extractTextContent(m.content),
374
408
  })),
375
409
  ),
376
410
  );
377
411
  }
378
412
  }
379
413
 
380
- // Tool definitions from input.body.tools
381
- const tools = body?.tools;
414
+ // Tool definitions (only present on path (b))
382
415
  if (Array.isArray(tools) && tools.length > 0) {
383
416
  otelSpan.setAttribute(
384
417
  "gen_ai.tool.definitions",
@@ -429,6 +462,11 @@ export class IntrospectionMastraExporter extends BaseExporter {
429
462
  "gen_ai.usage.cache_read.input_tokens",
430
463
  inputDetails["cacheRead"],
431
464
  );
465
+ if (typeof inputDetails?.["cacheWrite"] === "number")
466
+ otelSpan.setAttribute(
467
+ "gen_ai.usage.cache_creation.input_tokens",
468
+ inputDetails["cacheWrite"],
469
+ );
432
470
  }
433
471
 
434
472
  // Finish reason
@@ -503,11 +541,17 @@ export class IntrospectionMastraExporter extends BaseExporter {
503
541
  // Skip system messages (handled separately)
504
542
  if (msg.role === "system") continue;
505
543
 
506
- // Standard role-based messages
544
+ // Standard role-based messages (AIv4 uses msg.content; AIv5 may use msg.parts)
507
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
+ : [];
508
552
  result.push({
509
553
  role: msg.role as InputMessage["role"],
510
- parts: this._extractParts(msg.content),
554
+ parts,
511
555
  });
512
556
  continue;
513
557
  }
@@ -586,6 +630,25 @@ export class IntrospectionMastraExporter extends BaseExporter {
586
630
  return [{ role: "assistant", parts, finish_reason: finishReason }];
587
631
  }
588
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
+
589
652
  private _extractParts(content: unknown): MessagePart[] {
590
653
  if (typeof content === "string") {
591
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
+ }