@introspection-sdk/introspection-node 0.4.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 (89) hide show
  1. package/.turbo/turbo-build.log +4 -0
  2. package/.turbo/turbo-check-types.log +5 -0
  3. package/.turbo/turbo-lint.log +5 -0
  4. package/dist/ai-sdk-integration.d.ts +123 -0
  5. package/dist/ai-sdk-integration.d.ts.map +1 -0
  6. package/dist/ai-sdk-integration.js +475 -0
  7. package/dist/anthropic.d.ts +54 -0
  8. package/dist/anthropic.d.ts.map +1 -0
  9. package/dist/anthropic.js +375 -0
  10. package/dist/claude-hooks.d.ts +328 -0
  11. package/dist/claude-hooks.d.ts.map +1 -0
  12. package/dist/claude-hooks.js +571 -0
  13. package/dist/claude-wrapper.d.ts +93 -0
  14. package/dist/claude-wrapper.d.ts.map +1 -0
  15. package/dist/claude-wrapper.js +154 -0
  16. package/dist/client.d.ts +157 -0
  17. package/dist/client.d.ts.map +1 -0
  18. package/dist/client.js +394 -0
  19. package/dist/converters/ai-sdk.d.ts +65 -0
  20. package/dist/converters/ai-sdk.d.ts.map +1 -0
  21. package/dist/converters/ai-sdk.js +238 -0
  22. package/dist/converters/claude.d.ts +146 -0
  23. package/dist/converters/claude.d.ts.map +1 -0
  24. package/dist/converters/claude.js +202 -0
  25. package/dist/converters/index.d.ts +9 -0
  26. package/dist/converters/index.d.ts.map +1 -0
  27. package/dist/converters/index.js +8 -0
  28. package/dist/converters/openai.d.ts +51 -0
  29. package/dist/converters/openai.d.ts.map +1 -0
  30. package/dist/converters/openai.js +268 -0
  31. package/dist/converters/openclaw.d.ts +16 -0
  32. package/dist/converters/openclaw.d.ts.map +1 -0
  33. package/dist/converters/openclaw.js +49 -0
  34. package/dist/converters/openinference.d.ts +107 -0
  35. package/dist/converters/openinference.d.ts.map +1 -0
  36. package/dist/converters/openinference.js +552 -0
  37. package/dist/converters/vercel.d.ts +21 -0
  38. package/dist/converters/vercel.d.ts.map +1 -0
  39. package/dist/converters/vercel.js +208 -0
  40. package/dist/index.d.ts +38 -0
  41. package/dist/index.d.ts.map +1 -0
  42. package/dist/index.js +39 -0
  43. package/dist/langchain-handler.d.ts +101 -0
  44. package/dist/langchain-handler.d.ts.map +1 -0
  45. package/dist/langchain-handler.js +489 -0
  46. package/dist/mastra-exporter.d.ts +85 -0
  47. package/dist/mastra-exporter.d.ts.map +1 -0
  48. package/dist/mastra-exporter.js +471 -0
  49. package/dist/span-processor.d.ts +62 -0
  50. package/dist/span-processor.d.ts.map +1 -0
  51. package/dist/span-processor.js +310 -0
  52. package/dist/tracing-processor.d.ts +136 -0
  53. package/dist/tracing-processor.d.ts.map +1 -0
  54. package/dist/tracing-processor.js +368 -0
  55. package/dist/types/genai.d.ts +165 -0
  56. package/dist/types/genai.d.ts.map +1 -0
  57. package/dist/types/genai.js +82 -0
  58. package/dist/types.d.ts +7 -0
  59. package/dist/types.d.ts.map +1 -0
  60. package/dist/types.js +1 -0
  61. package/dist/utils.d.ts +19 -0
  62. package/dist/utils.d.ts.map +1 -0
  63. package/dist/utils.js +57 -0
  64. package/dist/version.d.ts +3 -0
  65. package/dist/version.d.ts.map +1 -0
  66. package/dist/version.js +2 -0
  67. package/eslint.config.mjs +4 -0
  68. package/package.json +78 -0
  69. package/src/ai-sdk-integration.ts +689 -0
  70. package/src/anthropic.ts +465 -0
  71. package/src/claude-hooks.ts +1010 -0
  72. package/src/claude-wrapper.ts +240 -0
  73. package/src/client.ts +513 -0
  74. package/src/converters/ai-sdk.ts +298 -0
  75. package/src/converters/claude.ts +334 -0
  76. package/src/converters/index.ts +44 -0
  77. package/src/converters/openai.ts +302 -0
  78. package/src/converters/openinference.ts +673 -0
  79. package/src/converters/vercel.ts +264 -0
  80. package/src/index.ts +121 -0
  81. package/src/langchain-handler.ts +722 -0
  82. package/src/mastra-exporter.ts +650 -0
  83. package/src/span-processor.ts +418 -0
  84. package/src/tracing-processor.ts +548 -0
  85. package/src/types/genai.ts +264 -0
  86. package/src/types.ts +13 -0
  87. package/src/utils.ts +69 -0
  88. package/src/version.ts +2 -0
  89. package/tsconfig.json +8 -0
@@ -0,0 +1,650 @@
1
+ /**
2
+ * Mastra-compatible Exporter for Introspection.
3
+ *
4
+ * Extends Mastra's BaseExporter to receive TracingEvents (Mastra's native
5
+ * span format) and convert them to gen_ai.* OTel spans for the Introspection
6
+ * backend.
7
+ *
8
+ * **Important:** This module requires `@mastra/observability` and `@mastra/core`
9
+ * at runtime. Import via the subpath: `@introspection-sdk/introspection-node/mastra`
10
+ *
11
+ * @example
12
+ * ```ts
13
+ * import { Mastra } from "@mastra/core/mastra";
14
+ * import { IntrospectionMastraExporter } from "@introspection-sdk/introspection-node/mastra";
15
+ *
16
+ * const mastra = new Mastra({
17
+ * agents: { myAgent },
18
+ * observability: {
19
+ * configs: {
20
+ * otel: {
21
+ * serviceName: "my-mastra-app",
22
+ * exporters: [new IntrospectionMastraExporter()],
23
+ * },
24
+ * },
25
+ * },
26
+ * });
27
+ * ```
28
+ */
29
+
30
+ import { BaseExporter } from "@mastra/observability";
31
+ import type {
32
+ TracingEvent,
33
+ InitExporterOptions,
34
+ AnyExportedSpan,
35
+ } from "@mastra/core/observability";
36
+
37
+ import {
38
+ context as otelContext,
39
+ trace as otelTrace,
40
+ type Span as OtelSpan,
41
+ } from "@opentelemetry/api";
42
+ import {
43
+ BasicTracerProvider,
44
+ BatchSpanProcessor,
45
+ SimpleSpanProcessor,
46
+ type SpanExporter,
47
+ type IdGenerator,
48
+ } from "@opentelemetry/sdk-trace-base";
49
+ import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-proto";
50
+ import { resourceFromAttributes } from "@opentelemetry/resources";
51
+ import { ATTR_SERVICE_NAME } from "@opentelemetry/semantic-conventions";
52
+ import { randomUUID } from "crypto";
53
+
54
+ import { logger } from "./utils.js";
55
+ import { VERSION } from "./version.js";
56
+ import type {
57
+ InputMessage,
58
+ OutputMessage,
59
+ MessagePart,
60
+ } from "./types/genai.js";
61
+
62
+ // ---------------------------------------------------------------------------
63
+ // Configuration
64
+ // ---------------------------------------------------------------------------
65
+
66
+ /** Advanced options for testing and customization. */
67
+ export interface MastraExporterAdvancedOptions {
68
+ /** Custom span exporter for testing. */
69
+ spanExporter?: SpanExporter;
70
+ /** Custom ID generator for testing. */
71
+ idGenerator?: IdGenerator;
72
+ /** Use SimpleSpanProcessor instead of BatchSpanProcessor. */
73
+ useSimpleSpanProcessor?: boolean;
74
+ }
75
+
76
+ /** Configuration for {@link IntrospectionMastraExporter}. */
77
+ export interface IntrospectionMastraExporterOptions {
78
+ /** Authentication token (env: INTROSPECTION_TOKEN). */
79
+ token?: string;
80
+ /** Base URL for the API (env: INTROSPECTION_BASE_URL). */
81
+ baseUrl?: string;
82
+ /** Additional headers to include in requests. */
83
+ additionalHeaders?: Record<string, string>;
84
+ /** Enable debug logging. */
85
+ debug?: boolean;
86
+ /** Advanced options for testing. */
87
+ advanced?: MastraExporterAdvancedOptions;
88
+ }
89
+
90
+ // ---------------------------------------------------------------------------
91
+ // Exporter
92
+ // ---------------------------------------------------------------------------
93
+
94
+ export class IntrospectionMastraExporter extends BaseExporter {
95
+ name = "introspection";
96
+
97
+ private _options: IntrospectionMastraExporterOptions;
98
+ private _tracerProvider?: BasicTracerProvider;
99
+ private _tracer?: ReturnType<BasicTracerProvider["getTracer"]>;
100
+ private _conversationIds: Map<string, string> = new Map();
101
+ // Track root OTel spans per Mastra traceId so children share the same trace
102
+ private _rootSpans: Map<string, OtelSpan> = new Map();
103
+
104
+ constructor(options: IntrospectionMastraExporterOptions = {}) {
105
+ super({ logLevel: options.debug ? "debug" : "info" });
106
+ this._options = options;
107
+
108
+ const token = options.token || process.env.INTROSPECTION_TOKEN;
109
+ if (!token && !options.advanced?.spanExporter) {
110
+ this.setDisabled("INTROSPECTION_TOKEN is required");
111
+ }
112
+ }
113
+
114
+ /**
115
+ * Called by Mastra after the instance is fully configured.
116
+ * We defer TracerProvider creation to here so we can use the service name.
117
+ */
118
+ init(options: InitExporterOptions): void {
119
+ if (this.isDisabled) return;
120
+
121
+ const config = options.config as Record<string, unknown> | undefined;
122
+ const serviceName = (config?.["serviceName"] as string) || "mastra-app";
123
+
124
+ this._initProvider(serviceName);
125
+ }
126
+
127
+ private _initProvider(serviceName: string): void {
128
+ if (this._tracerProvider) return; // Already initialized
129
+
130
+ const opts = this._options;
131
+ const advanced = opts.advanced;
132
+
133
+ const resource = resourceFromAttributes({
134
+ [ATTR_SERVICE_NAME]: serviceName,
135
+ });
136
+
137
+ if (advanced?.spanExporter) {
138
+ const sp = advanced.useSimpleSpanProcessor
139
+ ? new SimpleSpanProcessor(advanced.spanExporter)
140
+ : new BatchSpanProcessor(advanced.spanExporter, {
141
+ scheduledDelayMillis: 100,
142
+ });
143
+ this._tracerProvider = new BasicTracerProvider({
144
+ resource,
145
+ idGenerator: advanced.idGenerator,
146
+ spanProcessors: [sp],
147
+ });
148
+ logger.info("IntrospectionMastraExporter initialized in test mode");
149
+ } else {
150
+ const token = opts.token || process.env.INTROSPECTION_TOKEN;
151
+ if (!token) return;
152
+
153
+ const baseUrl =
154
+ opts.baseUrl ||
155
+ process.env.INTROSPECTION_BASE_URL ||
156
+ "https://otel.introspection.dev";
157
+ const endpoint = baseUrl.endsWith("/v1/traces")
158
+ ? baseUrl
159
+ : `${baseUrl.replace(/\/$/, "")}/v1/traces`;
160
+
161
+ const headers: Record<string, string> = {
162
+ "User-Agent": `introspection-sdk/${VERSION}`,
163
+ Authorization: `Bearer ${token}`,
164
+ ...opts.additionalHeaders,
165
+ };
166
+
167
+ const spanExporter = new OTLPTraceExporter({ url: endpoint, headers });
168
+ const effectiveBatchSize =
169
+ token.startsWith("intro_dev") || token.startsWith("intro_staging")
170
+ ? 1
171
+ : undefined;
172
+ const useSimple =
173
+ advanced?.useSimpleSpanProcessor || effectiveBatchSize === 1;
174
+
175
+ const sp = useSimple
176
+ ? new SimpleSpanProcessor(spanExporter)
177
+ : new BatchSpanProcessor(spanExporter, {
178
+ scheduledDelayMillis: 1000,
179
+ ...(effectiveBatchSize
180
+ ? { maxExportBatchSize: effectiveBatchSize }
181
+ : {}),
182
+ });
183
+
184
+ this._tracerProvider = new BasicTracerProvider({
185
+ resource,
186
+ idGenerator: advanced?.idGenerator,
187
+ spanProcessors: [sp],
188
+ });
189
+
190
+ logger.info(
191
+ `IntrospectionMastraExporter initialized: endpoint=${endpoint}, serviceName=${serviceName}`,
192
+ );
193
+ }
194
+
195
+ this._tracer = this._tracerProvider.getTracer(
196
+ "introspection-mastra",
197
+ VERSION,
198
+ );
199
+ }
200
+
201
+ protected async _exportTracingEvent(event: TracingEvent): Promise<void> {
202
+ if (this.isDisabled) return;
203
+ if (event.type !== "span_ended") return;
204
+
205
+ // Lazy init if init() wasn't called
206
+ if (!this._tracerProvider) {
207
+ this._initProvider("mastra-app");
208
+ }
209
+ if (!this._tracer) return;
210
+
211
+ this._exportSpan(event.exportedSpan);
212
+ }
213
+
214
+ async flush(): Promise<void> {
215
+ await this._tracerProvider?.forceFlush();
216
+ }
217
+
218
+ async shutdown(): Promise<void> {
219
+ // End any open root spans
220
+ for (const span of this._rootSpans.values()) span.end();
221
+ this._rootSpans.clear();
222
+ await this._tracerProvider?.shutdown();
223
+ }
224
+
225
+ // -------------------------------------------------------------------------
226
+ // Span routing
227
+ // -------------------------------------------------------------------------
228
+
229
+ private _exportSpan(span: AnyExportedSpan): void {
230
+ switch (span.type as string) {
231
+ case "agent_run":
232
+ this._exportAgentRun(span);
233
+ break;
234
+ case "model_step":
235
+ this._exportModelStep(span);
236
+ break;
237
+ case "tool_call":
238
+ case "mcp_tool_call":
239
+ this._exportToolCall(span);
240
+ break;
241
+ // model_generation — skipped; model_step has per-step tool calls
242
+ default:
243
+ break;
244
+ }
245
+ }
246
+
247
+ // -------------------------------------------------------------------------
248
+ // Span creation with shared trace context
249
+ // -------------------------------------------------------------------------
250
+
251
+ /** Ensure a root OTel span exists for this Mastra trace.
252
+ * Events arrive in end-order (children first, agent_run last),
253
+ * so we create a synthetic root on first encounter. */
254
+ private _ensureRoot(span: AnyExportedSpan): void {
255
+ if (this._rootSpans.has(span.traceId)) return;
256
+ const rootSpan = this._tracer!.startSpan("trace", {
257
+ startTime: span.startTime,
258
+ });
259
+ this._rootSpans.set(span.traceId, rootSpan);
260
+ }
261
+
262
+ private _createSpan(span: AnyExportedSpan, name?: string): OtelSpan {
263
+ this._ensureRoot(span);
264
+ const parentCtx = otelTrace.setSpan(
265
+ otelContext.active(),
266
+ this._rootSpans.get(span.traceId)!,
267
+ );
268
+ return this._tracer!.startSpan(
269
+ name || span.name,
270
+ { startTime: span.startTime },
271
+ parentCtx,
272
+ );
273
+ }
274
+
275
+ // -------------------------------------------------------------------------
276
+ // Span converters
277
+ // -------------------------------------------------------------------------
278
+
279
+ private _exportAgentRun(span: AnyExportedSpan): void {
280
+ const attrs = span.attributes as Record<string, unknown> | undefined;
281
+ const conversationId = this._getConversationId(span);
282
+
283
+ // agent_run arrives last — set attrs on the synthetic root and end it
284
+ this._ensureRoot(span);
285
+ const rootSpan = this._rootSpans.get(span.traceId)!;
286
+ rootSpan.setAttribute("gen_ai.conversation.id", conversationId);
287
+ rootSpan.setAttribute(
288
+ "gen_ai.agent.name",
289
+ span.entityName || span.name || "agent",
290
+ );
291
+
292
+ if (attrs?.["instructions"] && typeof attrs["instructions"] === "string") {
293
+ rootSpan.setAttribute(
294
+ "gen_ai.system_instructions",
295
+ JSON.stringify([{ type: "text", content: attrs["instructions"] }]),
296
+ );
297
+ }
298
+
299
+ // Metadata passthrough
300
+ if (span.metadata) {
301
+ for (const [key, value] of Object.entries(span.metadata)) {
302
+ if (value != null) {
303
+ rootSpan.setAttribute(
304
+ `ai.telemetry.metadata.${key}`,
305
+ typeof value === "string" ? value : JSON.stringify(value),
306
+ );
307
+ }
308
+ }
309
+ }
310
+
311
+ rootSpan.end(span.endTime || new Date());
312
+ this._rootSpans.delete(span.traceId);
313
+ }
314
+
315
+ private _exportModelStep(span: AnyExportedSpan): void {
316
+ const attrs = span.attributes as Record<string, unknown> | undefined;
317
+ const conversationId = this._getConversationId(span);
318
+
319
+ // Model info lives in metadata.modelMetadata (set by Mastra)
320
+ const modelMeta = span.metadata?.["modelMetadata"] as
321
+ | Record<string, unknown>
322
+ | undefined;
323
+ const model = (modelMeta?.["modelId"] as string) || "";
324
+ const provider = (modelMeta?.["modelProvider"] as string) || "";
325
+
326
+ // Response metadata from metadata.body (the API response)
327
+ const respBody = span.metadata?.["body"] as
328
+ | Record<string, unknown>
329
+ | undefined;
330
+ const responseModel = (respBody?.["model"] as string) || "";
331
+ const responseId = (respBody?.["id"] as string) || "";
332
+
333
+ const spanName = model ? `chat ${model}` : span.name;
334
+ const otelSpan = this._createSpan(span, spanName);
335
+
336
+ otelSpan.setAttribute("gen_ai.operation.name", "chat");
337
+ otelSpan.setAttribute("gen_ai.conversation.id", conversationId);
338
+ otelSpan.setAttribute("openinference.span.kind", "LLM");
339
+
340
+ if (model) otelSpan.setAttribute("gen_ai.request.model", model);
341
+ if (provider) otelSpan.setAttribute("gen_ai.system", provider);
342
+ if (responseModel)
343
+ otelSpan.setAttribute("gen_ai.response.model", responseModel);
344
+ if (responseId) otelSpan.setAttribute("gen_ai.response.id", responseId);
345
+
346
+ // Input messages — model_step has input.body.input (raw API messages)
347
+ 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)) {
352
+ const inputMessages = this._convertInput(apiMessages);
353
+ if (inputMessages.length > 0) {
354
+ otelSpan.setAttribute(
355
+ "gen_ai.input.messages",
356
+ JSON.stringify(inputMessages),
357
+ );
358
+ }
359
+
360
+ // System instructions
361
+ const systemMsgs = (apiMessages as Record<string, unknown>[]).filter(
362
+ (m) => m.role === "system",
363
+ );
364
+ if (systemMsgs.length > 0) {
365
+ otelSpan.setAttribute(
366
+ "gen_ai.system_instructions",
367
+ JSON.stringify(
368
+ systemMsgs.map((m) => ({
369
+ type: "text",
370
+ content:
371
+ typeof m.content === "string"
372
+ ? m.content
373
+ : JSON.stringify(m.content),
374
+ })),
375
+ ),
376
+ );
377
+ }
378
+ }
379
+
380
+ // Tool definitions from input.body.tools
381
+ const tools = body?.tools;
382
+ if (Array.isArray(tools) && tools.length > 0) {
383
+ otelSpan.setAttribute(
384
+ "gen_ai.tool.definitions",
385
+ JSON.stringify(
386
+ (tools as Record<string, unknown>[]).map((t) => ({
387
+ type: (t.type as string) || "function",
388
+ name: (t.name as string) || "",
389
+ description: (t.description as string) || "",
390
+ parameters: t.parameters,
391
+ })),
392
+ ),
393
+ );
394
+ }
395
+ }
396
+
397
+ // Output messages — model_step output has {text, toolCalls} per step
398
+ if (span.output != null) {
399
+ const outputMessages = this._convertOutput(
400
+ span.output,
401
+ attrs?.["finishReason"] as string | undefined,
402
+ );
403
+ if (outputMessages.length > 0) {
404
+ otelSpan.setAttribute(
405
+ "gen_ai.output.messages",
406
+ JSON.stringify(outputMessages),
407
+ );
408
+ }
409
+ }
410
+
411
+ // Token usage (per step)
412
+ const usage = attrs?.["usage"] as Record<string, unknown> | undefined;
413
+ if (usage) {
414
+ if (typeof usage["inputTokens"] === "number")
415
+ otelSpan.setAttribute(
416
+ "gen_ai.usage.input_tokens",
417
+ usage["inputTokens"],
418
+ );
419
+ if (typeof usage["outputTokens"] === "number")
420
+ otelSpan.setAttribute(
421
+ "gen_ai.usage.output_tokens",
422
+ usage["outputTokens"],
423
+ );
424
+ const inputDetails = usage["inputDetails"] as
425
+ | Record<string, unknown>
426
+ | undefined;
427
+ if (typeof inputDetails?.["cacheRead"] === "number")
428
+ otelSpan.setAttribute(
429
+ "gen_ai.usage.cache_read.input_tokens",
430
+ inputDetails["cacheRead"],
431
+ );
432
+ }
433
+
434
+ // Finish reason
435
+ const finishReason = attrs?.["finishReason"] as string | undefined;
436
+ if (finishReason)
437
+ otelSpan.setAttribute("gen_ai.response.finish_reasons", [finishReason]);
438
+
439
+ otelSpan.end(span.endTime || new Date());
440
+ }
441
+
442
+ private _exportToolCall(span: AnyExportedSpan): void {
443
+ const conversationId = this._getConversationId(span);
444
+ const toolName = span.entityName || span.name || "tool";
445
+
446
+ const otelSpan = this._createSpan(span, toolName);
447
+ otelSpan.setAttribute("gen_ai.tool.name", toolName);
448
+ otelSpan.setAttribute("gen_ai.conversation.id", conversationId);
449
+ otelSpan.setAttribute("openinference.span.kind", "TOOL");
450
+
451
+ if (span.input != null) {
452
+ otelSpan.setAttribute(
453
+ "gen_ai.tool.input",
454
+ typeof span.input === "string"
455
+ ? span.input
456
+ : JSON.stringify(span.input),
457
+ );
458
+ }
459
+ if (span.output != null) {
460
+ otelSpan.setAttribute(
461
+ "gen_ai.tool.output",
462
+ typeof span.output === "string"
463
+ ? span.output
464
+ : JSON.stringify(span.output),
465
+ );
466
+ }
467
+
468
+ otelSpan.end(span.endTime || new Date());
469
+ }
470
+
471
+ // -------------------------------------------------------------------------
472
+ // Helpers
473
+ // -------------------------------------------------------------------------
474
+
475
+ private _getConversationId(span: AnyExportedSpan): string {
476
+ const metaConvId = span.metadata?.["gen_ai.conversation.id"] as
477
+ | string
478
+ | undefined;
479
+ if (metaConvId) return metaConvId;
480
+
481
+ const attrs = span.attributes as Record<string, unknown> | undefined;
482
+ const attrConvId = attrs?.["conversationId"] as string | undefined;
483
+ if (attrConvId) return attrConvId;
484
+
485
+ const traceId = span.traceId;
486
+ if (!this._conversationIds.has(traceId)) {
487
+ this._conversationIds.set(
488
+ traceId,
489
+ `intro_conv_${randomUUID().replace(/-/g, "")}`,
490
+ );
491
+ }
492
+ return this._conversationIds.get(traceId)!;
493
+ }
494
+
495
+ private _convertInput(input: unknown): InputMessage[] {
496
+ if (!input) return [];
497
+ if (Array.isArray(input)) {
498
+ const result: InputMessage[] = [];
499
+ for (const m of input) {
500
+ const msg = m as Record<string, unknown>;
501
+ if (!msg) continue;
502
+
503
+ // Skip system messages (handled separately)
504
+ if (msg.role === "system") continue;
505
+
506
+ // Standard role-based messages
507
+ if (msg.role) {
508
+ result.push({
509
+ role: msg.role as InputMessage["role"],
510
+ parts: this._extractParts(msg.content),
511
+ });
512
+ continue;
513
+ }
514
+
515
+ // OpenAI Responses API: function_call_output (no role, has type)
516
+ if (msg.type === "function_call_output") {
517
+ result.push({
518
+ role: "tool",
519
+ parts: [
520
+ {
521
+ type: "tool_call_response",
522
+ id: (msg.call_id || "") as string,
523
+ response:
524
+ typeof msg.output === "string"
525
+ ? msg.output
526
+ : JSON.stringify(msg.output),
527
+ },
528
+ ],
529
+ });
530
+ continue;
531
+ }
532
+
533
+ // item_reference — skip (OpenAI internal reference)
534
+ if (msg.type === "item_reference") continue;
535
+ }
536
+ return result;
537
+ }
538
+ if (typeof input === "string") {
539
+ return [{ role: "user", parts: [{ type: "text", content: input }] }];
540
+ }
541
+ return [];
542
+ }
543
+
544
+ private _convertOutput(
545
+ output: unknown,
546
+ finishReason?: string,
547
+ ): OutputMessage[] {
548
+ if (!output) return [];
549
+ const parts: MessagePart[] = [];
550
+
551
+ if (typeof output === "string") {
552
+ parts.push({ type: "text", content: output });
553
+ } else if (typeof output === "object" && output !== null) {
554
+ const out = output as Record<string, unknown>;
555
+
556
+ // Reasoning
557
+ if (Array.isArray(out.reasoning)) {
558
+ for (const r of out.reasoning as Record<string, unknown>[]) {
559
+ const text = (r.text || r.content || "") as string;
560
+ if (text) parts.push({ type: "thinking", content: text });
561
+ }
562
+ }
563
+
564
+ // Text
565
+ if (typeof out.text === "string" && out.text) {
566
+ parts.push({ type: "text", content: out.text });
567
+ }
568
+
569
+ // Tool calls
570
+ if (Array.isArray(out.toolCalls)) {
571
+ for (const tc of out.toolCalls as Record<string, unknown>[]) {
572
+ parts.push({
573
+ type: "tool_call",
574
+ name: (tc.toolName || tc.name || "") as string,
575
+ id: (tc.toolCallId || tc.id || "") as string,
576
+ arguments:
577
+ typeof tc.args === "string"
578
+ ? tc.args
579
+ : JSON.stringify(tc.args || tc.input),
580
+ });
581
+ }
582
+ }
583
+ }
584
+
585
+ if (parts.length === 0) return [];
586
+ return [{ role: "assistant", parts, finish_reason: finishReason }];
587
+ }
588
+
589
+ private _extractParts(content: unknown): MessagePart[] {
590
+ if (typeof content === "string") {
591
+ return [{ type: "text", content }];
592
+ }
593
+ if (Array.isArray(content)) {
594
+ return content
595
+ .map((part: unknown) => {
596
+ const p = part as Record<string, unknown>;
597
+ // Standard text
598
+ if (p.type === "text")
599
+ return {
600
+ type: "text" as const,
601
+ content: (p.text || p.content || "") as string,
602
+ };
603
+ // OpenAI Responses API input_text format
604
+ if (p.type === "input_text")
605
+ return {
606
+ type: "text" as const,
607
+ content: (p.text || "") as string,
608
+ };
609
+ // Tool call
610
+ if (
611
+ p.type === "tool-call" ||
612
+ p.type === "tool_call" ||
613
+ p.type === "function_call"
614
+ )
615
+ return {
616
+ type: "tool_call" as const,
617
+ name: (p.toolName || p.name || "") as string,
618
+ id: (p.toolCallId || p.id || p.call_id || "") as string,
619
+ arguments:
620
+ typeof p.args === "string"
621
+ ? p.args
622
+ : typeof p.arguments === "string"
623
+ ? p.arguments
624
+ : JSON.stringify(p.args || p.arguments || p.input),
625
+ };
626
+ // Tool result
627
+ if (
628
+ p.type === "tool-result" ||
629
+ p.type === "tool_call_response" ||
630
+ p.type === "function_call_output"
631
+ )
632
+ return {
633
+ type: "tool_call_response" as const,
634
+ id: (p.toolCallId || p.id || p.call_id || "") as string,
635
+ response:
636
+ typeof p.result === "string"
637
+ ? p.result
638
+ : typeof p.output === "string"
639
+ ? p.output
640
+ : JSON.stringify(p.result || p.output),
641
+ };
642
+ // item_reference (OpenAI Responses API) — skip
643
+ if (p.type === "item_reference") return null;
644
+ return { type: "text" as const, content: JSON.stringify(p) };
645
+ })
646
+ .filter((p) => p !== null) as MessagePart[];
647
+ }
648
+ return [];
649
+ }
650
+ }