dd-trace 5.117.0 → 5.118.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 (83) hide show
  1. package/ci/diagnose.js +162 -21
  2. package/ci/init.js +0 -1
  3. package/ci/runbook.md +187 -180
  4. package/ci/test-optimization-validation/approval-artifacts.js +3 -1
  5. package/ci/test-optimization-validation/approval.js +52 -15
  6. package/ci/test-optimization-validation/ci-command-candidate.js +26 -32
  7. package/ci/test-optimization-validation/ci-discovery.js +1 -1
  8. package/ci/test-optimization-validation/ci-package-scripts.js +141 -0
  9. package/ci/test-optimization-validation/ci-remediation.js +112 -39
  10. package/ci/test-optimization-validation/cli.js +516 -681
  11. package/ci/test-optimization-validation/command-blocker.js +188 -33
  12. package/ci/test-optimization-validation/command-output-policy.js +3 -26
  13. package/ci/test-optimization-validation/command-runner.js +189 -245
  14. package/ci/test-optimization-validation/environment.js +90 -0
  15. package/ci/test-optimization-validation/executable.js +159 -388
  16. package/ci/test-optimization-validation/framework-adapters/cucumber.js +119 -0
  17. package/ci/test-optimization-validation/framework-adapters/cypress.js +107 -0
  18. package/ci/test-optimization-validation/framework-adapters/playwright.js +181 -0
  19. package/ci/test-optimization-validation/generated-files.js +75 -13
  20. package/ci/test-optimization-validation/generated-test-contract.js +283 -0
  21. package/ci/test-optimization-validation/generated-verifier.js +49 -16
  22. package/ci/test-optimization-validation/literal-environment.js +57 -0
  23. package/ci/test-optimization-validation/manifest-loader.js +2 -22
  24. package/ci/test-optimization-validation/manifest-scaffold.js +816 -515
  25. package/ci/test-optimization-validation/manifest-schema.js +494 -702
  26. package/ci/test-optimization-validation/offline-fixtures.js +4 -1
  27. package/ci/test-optimization-validation/plan-writer.js +243 -984
  28. package/ci/test-optimization-validation/preflight-runner.js +49 -55
  29. package/ci/test-optimization-validation/redaction.js +2 -1
  30. package/ci/test-optimization-validation/report-writer.js +391 -1357
  31. package/ci/test-optimization-validation/result-semantics.js +86 -0
  32. package/ci/test-optimization-validation/runner-command.js +249 -0
  33. package/ci/test-optimization-validation/runner-contract.js +664 -0
  34. package/ci/test-optimization-validation/scenarios/auto-test-retries.js +29 -3
  35. package/ci/test-optimization-validation/scenarios/basic-reporting.js +240 -543
  36. package/ci/test-optimization-validation/scenarios/ci-wiring.js +450 -670
  37. package/ci/test-optimization-validation/scenarios/early-flake-detection.js +4 -3
  38. package/ci/test-optimization-validation/scenarios/helpers.js +67 -9
  39. package/ci/test-optimization-validation/scenarios/test-management.js +4 -3
  40. package/ci/test-optimization-validation/source-text.js +87 -0
  41. package/ci/test-optimization-validation/test-output.js +12 -22
  42. package/ext/tags.d.ts +1 -0
  43. package/index.d.ts +7 -0
  44. package/package.json +2 -2
  45. package/packages/datadog-instrumentations/src/helpers/hooks.js +2 -0
  46. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/index.js +1 -0
  47. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/openai-agents.js +31 -0
  48. package/packages/datadog-instrumentations/src/openai-agents.js +159 -0
  49. package/packages/datadog-plugin-openai-agents/src/index.js +74 -0
  50. package/packages/datadog-plugin-openai-agents/src/integration.js +503 -0
  51. package/packages/datadog-plugin-openai-agents/src/processor.js +108 -0
  52. package/packages/datadog-plugin-openai-agents/src/util.js +60 -0
  53. package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +48 -49
  54. package/packages/dd-trace/src/ci-visibility/requests/get-library-configuration.js +161 -33
  55. package/packages/dd-trace/src/ci-visibility/test-optimization-http-cache-schema.js +6 -4
  56. package/packages/dd-trace/src/config/generated-config-types.d.ts +2 -0
  57. package/packages/dd-trace/src/config/supported-configurations.json +7 -0
  58. package/packages/dd-trace/src/encode/span-stats.js +7 -1
  59. package/packages/dd-trace/src/exporter.js +16 -9
  60. package/packages/dd-trace/src/llmobs/index.js +9 -1
  61. package/packages/dd-trace/src/llmobs/plugins/ai/util.js +104 -22
  62. package/packages/dd-trace/src/llmobs/plugins/openai/index.js +5 -9
  63. package/packages/dd-trace/src/llmobs/plugins/openai/utils.js +20 -0
  64. package/packages/dd-trace/src/llmobs/plugins/openai-agents/utils.js +321 -0
  65. package/packages/dd-trace/src/llmobs/sdk.js +2 -1
  66. package/packages/dd-trace/src/llmobs/span_processor.js +7 -2
  67. package/packages/dd-trace/src/llmobs/tagger.js +23 -3
  68. package/packages/dd-trace/src/llmobs/util.js +56 -3
  69. package/packages/dd-trace/src/openfeature/agentless_configuration_source.js +3 -6
  70. package/packages/dd-trace/src/openfeature/configuration_source.js +5 -8
  71. package/packages/dd-trace/src/opentelemetry/metrics/otlp_span_stats_transformer.js +5 -1
  72. package/packages/dd-trace/src/plugins/ci_plugin.js +9 -3
  73. package/packages/dd-trace/src/plugins/index.js +1 -0
  74. package/packages/dd-trace/src/plugins/outbound.js +4 -1
  75. package/packages/dd-trace/src/span_processor.js +1 -1
  76. package/packages/dd-trace/src/span_stats.js +22 -4
  77. package/ci/test-optimization-validation/command-suitability.js +0 -471
  78. package/ci/test-optimization-validation/init-probe-preload.js +0 -163
  79. package/ci/test-optimization-validation/init-probe.js +0 -246
  80. package/ci/test-optimization-validation/late-initialization.js +0 -63
  81. package/ci/test-optimization-validation/local-command.js +0 -163
  82. package/ci/test-optimization-validation/setup-runner.js +0 -97
  83. package/ci/test-optimization-validation-manifest.schema.json +0 -1
@@ -0,0 +1,503 @@
1
+ 'use strict'
2
+
3
+ const { storage } = require('../../datadog-core')
4
+ const { storage: llmobsStorage } = require('../../dd-trace/src/llmobs/storage')
5
+ const LLMObsTagger = require('../../dd-trace/src/llmobs/tagger')
6
+ const { getOpenAIModelProvider } = require('../../dd-trace/src/llmobs/plugins/openai/utils')
7
+ const {
8
+ extractInputMessages,
9
+ extractOutputMessages,
10
+ extractGenerationOutputMessages,
11
+ extractMetrics,
12
+ extractMetadata,
13
+ } = require('../../dd-trace/src/llmobs/plugins/openai-agents/utils')
14
+ const { AGENTS_ERROR_TYPE, applyError, deriveSpanName } = require('./util')
15
+
16
+ const COMPONENT = 'openai-agents'
17
+ const DEFAULT_MODEL_PROVIDER = 'openai'
18
+ const MODEL_BASE_URL_STORE_KEY = Symbol('openai-agents.model-base-url')
19
+ const legacyStorage = storage('legacy')
20
+
21
+ const KIND_TO_SPAN_KIND = {
22
+ agent: 'internal',
23
+ tool: 'internal',
24
+ task: 'internal',
25
+ llm: 'client',
26
+ }
27
+
28
+ /**
29
+ * @typedef {{
30
+ * spanId: string,
31
+ * traceId: string,
32
+ * currentTopLevelAgentSpanId?: string,
33
+ * currentTopLevelAgentName?: string,
34
+ * inputOaiSpan?: object,
35
+ * inputMessages?: Array<{ role: string, content: string }>,
36
+ * outputOaiSpan?: object,
37
+ * metadata?: Record<string, unknown>,
38
+ * groupId?: string,
39
+ * llmobsParentStore?: object,
40
+ * }} LLMObsTraceInfo
41
+ */
42
+
43
+ /**
44
+ * Owns tracer/tagger refs, maps agents-core span ids → dd-trace spans, and
45
+ * reconstructs workflow-level input/output from the first and last response
46
+ * spans of the top-level agent.
47
+ */
48
+ class OpenAIAgentsIntegration {
49
+ #tracer
50
+ #config
51
+ #enabled = false
52
+ #service
53
+ /**
54
+ * LLMObs is gated independently of APM tracing: when DD_LLMOBS_ENABLED is
55
+ * false we keep emitting APM spans for the agent workflow but skip all
56
+ * LLMObs tagging and the work that feeds it.
57
+ * @type {LLMObsTagger}
58
+ */
59
+ #tagger
60
+ /** @type {Map<string, import('../../dd-trace/src/opentracing/span')>} */
61
+ #oaiToDdSpan = new Map()
62
+ /** @type {Map<string, LLMObsTraceInfo>} */
63
+ #traceInfo = new Map()
64
+
65
+ constructor ({ tracer, config } = {}) {
66
+ this.#tracer = tracer
67
+ this.#config = config ?? { llmobs: {} }
68
+ this.#tagger = new LLMObsTagger(this.#config, true)
69
+ }
70
+
71
+ get enabled () {
72
+ return this.#enabled
73
+ }
74
+
75
+ /**
76
+ * Apply plugin lifecycle configuration.
77
+ *
78
+ * @param {{ enabled?: boolean, service?: string }} [config]
79
+ */
80
+ configure (config) {
81
+ this.#enabled = !!config?.enabled
82
+ this.#service = config?.service
83
+ }
84
+
85
+ /**
86
+ * @param {string} spanId agents-core spanId
87
+ * @returns {import('../../dd-trace/src/opentracing/span') | undefined}
88
+ */
89
+ getDDSpan (spanId) {
90
+ return this.#oaiToDdSpan.get(spanId)
91
+ }
92
+
93
+ /**
94
+ * Ensure a function span is available synchronously at Tool.invoke. Newer
95
+ * agents-core versions dispatch processor callbacks after an async exporter
96
+ * callback, which can otherwise happen after user tool code has started.
97
+ *
98
+ * @param {object} oaiSpan
99
+ * @returns {import('../../dd-trace/src/opentracing/span') | undefined}
100
+ */
101
+ getOrStartToolSpan (oaiSpan) {
102
+ if (!oaiSpan?.spanId) return
103
+ const existingSpan = this.#oaiToDdSpan.get(oaiSpan.spanId)
104
+ if (existingSpan) return existingSpan
105
+ this.startSpan(oaiSpan, 'tool')
106
+ return this.#oaiToDdSpan.get(oaiSpan.spanId)
107
+ }
108
+
109
+ clearState () {
110
+ // Finish any dd-trace spans still in-flight so we don't leak open traces
111
+ // when agents-core's TracingProcessor.shutdown() runs (e.g., process
112
+ // exiting mid-run).
113
+ for (const ddSpan of this.#oaiToDdSpan.values()) {
114
+ ddSpan.finish()
115
+ }
116
+ this.#oaiToDdSpan.clear()
117
+ this.#traceInfo.clear()
118
+ }
119
+
120
+ // ── Trace lifecycle ─────────────────────────────────────────────────────────
121
+
122
+ startTrace (oaiTrace) {
123
+ const traceId = oaiTrace.traceId
124
+ if (!traceId) return
125
+
126
+ const name = oaiTrace.name || 'Agent workflow'
127
+ const parentSpan = legacyStorage.getStore()?.span
128
+ const ddSpan = this.#tracer.startSpan(name, {
129
+ childOf: parentSpan,
130
+ tags: this.#getSpanTags('internal'),
131
+ })
132
+
133
+ const llmobsParentStore = this.#isLLMObsEnabled() ? llmobsStorage.getStore() : undefined
134
+ this.#oaiToDdSpan.set(traceId, ddSpan)
135
+ this.#traceInfo.set(traceId, {
136
+ spanId: ddSpan.context().toSpanId(),
137
+ traceId,
138
+ groupId: oaiTrace.groupId || undefined,
139
+ metadata: oaiTrace.metadata,
140
+ llmobsParentStore,
141
+ })
142
+
143
+ this.#tagger.registerLLMObsSpan(ddSpan, {
144
+ kind: 'workflow',
145
+ name,
146
+ integration: COMPONENT,
147
+ parent: llmobsParentStore?.span,
148
+ sessionId: oaiTrace.groupId || undefined,
149
+ })
150
+ if (LLMObsTagger.tagMap.has(ddSpan)) {
151
+ llmobsStorage.enterWith({ ...llmobsParentStore, span: ddSpan })
152
+ }
153
+ }
154
+
155
+ endTrace (oaiTrace) {
156
+ this.#completeWorkflowSpan(oaiTrace.traceId)
157
+ }
158
+
159
+ /**
160
+ * Finish the workflow dd-trace span and clear its bookkeeping. Used by both
161
+ * agents-core's normal `Trace.end()` path and the orphan-recovery path
162
+ * (when `withTrace` skips its end callback because the body threw). When
163
+ * `rootAgentSpan` is provided, its `error` field is reflected onto the
164
+ * workflow span before finishing.
165
+ *
166
+ * @param {string | undefined} traceId
167
+ * @param {object} [rootAgentSpan] - parentless oai-span that ended in error.
168
+ */
169
+ #completeWorkflowSpan (traceId, rootAgentSpan) {
170
+ if (!traceId) return
171
+ const ddSpan = this.#oaiToDdSpan.get(traceId)
172
+ if (!ddSpan) return
173
+ const info = this.#traceInfo.get(traceId)
174
+
175
+ if (rootAgentSpan?.error) {
176
+ ddSpan.setTag('error', true)
177
+ ddSpan.setTag('error.type', AGENTS_ERROR_TYPE)
178
+ if (rootAgentSpan.error.message) {
179
+ ddSpan.setTag('error.message', rootAgentSpan.error.message)
180
+ }
181
+ }
182
+
183
+ if (this.#isLLMObsEnabled()) this.#setTraceAttributes(ddSpan, traceId)
184
+ ddSpan.finish()
185
+ if (info && LLMObsTagger.tagMap.has(ddSpan)) {
186
+ llmobsStorage.enterWith(info.llmobsParentStore)
187
+ }
188
+ this.#oaiToDdSpan.delete(traceId)
189
+ this.#traceInfo.delete(traceId)
190
+ }
191
+
192
+ // ── Span lifecycle ──────────────────────────────────────────────────────────
193
+
194
+ startSpan (oaiSpan, llmobsKind) {
195
+ const spanId = oaiSpan.spanId
196
+ if (!spanId || this.#oaiToDdSpan.has(spanId)) return
197
+
198
+ const parentSpan = this.#resolveParent(oaiSpan)
199
+ const spanName = deriveSpanName(oaiSpan)
200
+
201
+ const ddSpan = this.#tracer.startSpan(spanName, {
202
+ childOf: parentSpan,
203
+ tags: this.#getSpanTags(KIND_TO_SPAN_KIND[llmobsKind] ?? 'internal'),
204
+ })
205
+
206
+ this.#oaiToDdSpan.set(spanId, ddSpan)
207
+
208
+ if (this.#isLLMObsEnabled()) {
209
+ const llmobsOptions = {
210
+ kind: llmobsKind,
211
+ name: spanName,
212
+ integration: COMPONENT,
213
+ parent: parentSpan,
214
+ }
215
+
216
+ if (oaiSpan.spanData?.type === 'response' || oaiSpan.spanData?.type === 'generation') {
217
+ // Model name only arrives with the response; tagged in
218
+ // `#setResponseAttributes` once known. Model provider is resolved from
219
+ // the agents-openai client's baseURL captured at getResponse time.
220
+ llmobsOptions.modelProvider = this.#getCurrentModelProvider()
221
+ }
222
+
223
+ this.#tagger.registerLLMObsSpan(ddSpan, llmobsOptions)
224
+ this.#updateTraceInfoInput(oaiSpan, spanName)
225
+ }
226
+ }
227
+
228
+ endSpan (oaiSpan) {
229
+ const spanId = oaiSpan.spanId
230
+ const ddSpan = this.#oaiToDdSpan.get(spanId)
231
+ if (!ddSpan) return
232
+
233
+ applyError(ddSpan, oaiSpan)
234
+
235
+ if (oaiSpan.spanData?.type === 'handoff') {
236
+ const spanName = deriveSpanName(oaiSpan)
237
+ ddSpan.setOperationName(spanName)
238
+ if (this.#isLLMObsEnabled()) this.#tagger.setName(ddSpan, spanName)
239
+ }
240
+
241
+ if (this.#isLLMObsEnabled()) {
242
+ const spanData = oaiSpan.spanData
243
+ switch (spanData?.type) {
244
+ case 'response':
245
+ this.#setResponseAttributes(ddSpan, oaiSpan)
246
+ this.#updateTraceInfoOutput(oaiSpan)
247
+ break
248
+ case 'generation':
249
+ this.#setGenerationAttributes(ddSpan, oaiSpan)
250
+ this.#updateTraceInfoOutput(oaiSpan)
251
+ break
252
+ case 'function':
253
+ this.#tagger.tagTextIO(ddSpan, spanData.input ?? '', spanData.output ?? '')
254
+ break
255
+ case 'handoff':
256
+ this.#tagger.tagTextIO(ddSpan, spanData.from_agent ?? '', spanData.to_agent ?? '')
257
+ break
258
+ case 'agent':
259
+ this.#setAgentAttributes(ddSpan, oaiSpan)
260
+ break
261
+ case 'custom':
262
+ if (spanData.data && typeof spanData.data === 'object') {
263
+ this.#tagger.tagMetadata(ddSpan, spanData.data)
264
+ }
265
+ break
266
+ }
267
+ }
268
+
269
+ ddSpan.finish()
270
+ // agents-core's withTrace skips Trace.end() when its callback throws, so an
271
+ // errored parentless span is our last chance to finalize the workflow.
272
+ if (oaiSpan.parentId == null && oaiSpan.error) {
273
+ this.#completeWorkflowSpan(oaiSpan.traceId, oaiSpan)
274
+ }
275
+ this.#oaiToDdSpan.delete(spanId)
276
+ }
277
+
278
+ // ── Per-type attribute setters ──────────────────────────────────────────────
279
+
280
+ #setResponseAttributes (ddSpan, oaiSpan) {
281
+ const response = oaiSpan.spanData?._response
282
+ const input = oaiSpan.spanData?._input
283
+ if (response?.model) {
284
+ this.#tagger.tagModelName(ddSpan, response.model)
285
+ }
286
+
287
+ // Override the LLMObs span name to `{parent_agent_name} (LLM)` only when
288
+ // the response is a direct child of the top-level agent (Python parity:
289
+ // see `_llmobs_set_response_attributes` in dd-trace-py). For bare
290
+ // `withResponseSpan` calls outside a `Runner.run()` flow the default
291
+ // name (`openai_agents.response`) stays.
292
+ const parentAgentName = this.#llmSpanParentAgentName(oaiSpan)
293
+ if (parentAgentName) {
294
+ this.#tagger.setName(ddSpan, `${parentAgentName} (LLM)`)
295
+ }
296
+
297
+ // Always tag LLM I/O so the LLMObs event shape is consistent across
298
+ // happy/error paths. The extract* helpers emit placeholder messages
299
+ // when their source is absent.
300
+ const inputMessages = extractInputMessages(input, response?.instructions)
301
+ this.#tagger.tagLLMIO(ddSpan, inputMessages, extractOutputMessages(response))
302
+
303
+ // Cache messages for the workflow span's trace-level input (Python
304
+ // parity: last message of the first response under the top-level agent).
305
+ // Avoids re-running extractInputMessages in #setTraceAttributes.
306
+ const info = this.#traceInfo.get(oaiSpan.traceId)
307
+ if (info && info.inputOaiSpan === oaiSpan) {
308
+ info.inputMessages = inputMessages
309
+ }
310
+
311
+ if (response) {
312
+ const metrics = extractMetrics(response)
313
+ if (metrics) this.#tagger.tagMetrics(ddSpan, metrics)
314
+
315
+ const metadata = extractMetadata(response)
316
+ if (metadata) this.#tagger.tagMetadata(ddSpan, metadata)
317
+ }
318
+ }
319
+
320
+ /**
321
+ * Tag a Chat Completions generation span from agents-core's public span data.
322
+ *
323
+ * @param {import('../../dd-trace/src/opentracing/span')} ddSpan
324
+ * @param {object} oaiSpan
325
+ */
326
+ #setGenerationAttributes (ddSpan, oaiSpan) {
327
+ const spanData = oaiSpan.spanData
328
+ if (spanData?.model) {
329
+ this.#tagger.tagModelName(ddSpan, spanData.model)
330
+ }
331
+
332
+ const parentAgentName = this.#llmSpanParentAgentName(oaiSpan)
333
+ if (parentAgentName) {
334
+ this.#tagger.setName(ddSpan, `${parentAgentName} (LLM)`)
335
+ }
336
+
337
+ const inputMessages = extractInputMessages(spanData?.input)
338
+ this.#tagger.tagLLMIO(ddSpan, inputMessages, extractGenerationOutputMessages(spanData?.output))
339
+
340
+ const info = this.#traceInfo.get(oaiSpan.traceId)
341
+ if (info && info.inputOaiSpan === oaiSpan) {
342
+ info.inputMessages = inputMessages
343
+ }
344
+
345
+ const metrics = extractMetrics({
346
+ usage: spanData?.usage ?? spanData?.output?.at(-1)?.usage,
347
+ })
348
+ if (metrics) this.#tagger.tagMetrics(ddSpan, metrics)
349
+ if (spanData?.model_config) this.#tagger.tagMetadata(ddSpan, spanData.model_config)
350
+ }
351
+
352
+ /**
353
+ * If this response span's parent is the top-level agent span of the trace,
354
+ * return that agent's dd-trace span name. Used to set the LLMObs span name
355
+ * to `${agentName} (LLM)` (Python parity).
356
+ *
357
+ * @param {object} oaiSpan
358
+ * @returns {string | undefined}
359
+ */
360
+ #llmSpanParentAgentName (oaiSpan) {
361
+ const traceInfo = this.#traceInfo.get(oaiSpan.traceId)
362
+ if (!traceInfo?.currentTopLevelAgentSpanId) return
363
+ if (oaiSpan.parentId !== traceInfo.currentTopLevelAgentSpanId) return
364
+ return traceInfo.currentTopLevelAgentName
365
+ }
366
+
367
+ #setAgentAttributes (ddSpan, oaiSpan) {
368
+ const spanData = oaiSpan.spanData
369
+ let metadata
370
+ if (Array.isArray(spanData?.handoffs) && spanData.handoffs.length > 0) {
371
+ metadata = { handoffs: spanData.handoffs }
372
+ }
373
+ if (Array.isArray(spanData?.tools) && spanData.tools.length > 0) {
374
+ metadata ??= {}
375
+ metadata.tools = spanData.tools
376
+ }
377
+ if (spanData?.output_type) {
378
+ metadata ??= {}
379
+ metadata.output_type = spanData.output_type
380
+ }
381
+ if (metadata) this.#tagger.tagMetadata(ddSpan, metadata)
382
+ }
383
+
384
+ #setTraceAttributes (ddSpan, traceId) {
385
+ const info = this.#traceInfo.get(traceId)
386
+ if (!info) return
387
+
388
+ // Workflow-level input is the last input message of the first response
389
+ // span under the top-level agent; output is `response.output_text` of
390
+ // the last response span. Matches dd-trace-py's
391
+ // `OaiSpanAdapter.llmobs_trace_input` / `response_output_text`. The
392
+ // input messages were cached during #setResponseAttributes.
393
+ const lastInputMessage = info.inputMessages?.at(-1)
394
+ const inputValue = typeof lastInputMessage?.content === 'string' ? lastInputMessage.content : ''
395
+ const outputSpanData = info.outputOaiSpan?.spanData
396
+ let outputValue = outputSpanData?._response?.output_text ?? ''
397
+ if (outputSpanData?.type === 'generation') {
398
+ const outputMessages = extractGenerationOutputMessages(outputSpanData.output)
399
+ outputValue = outputMessages.at(-1)?.content ?? ''
400
+ }
401
+
402
+ this.#tagger.tagTextIO(ddSpan, inputValue, outputValue)
403
+
404
+ if (info.metadata && Object.keys(info.metadata).length > 0) {
405
+ this.#tagger.tagMetadata(ddSpan, info.metadata)
406
+ }
407
+ }
408
+
409
+ // ── Trace-info reconstruction (Python parity) ───────────────────────────────
410
+
411
+ #updateTraceInfoInput (oaiSpan, spanName) {
412
+ const info = this.#traceInfo.get(oaiSpan.traceId)
413
+ if (!info) return
414
+
415
+ const parentId = oaiSpan.parentId
416
+ const type = oaiSpan.spanData?.type
417
+
418
+ // Identify the first top-level agent span under the root trace and
419
+ // stash its display name so `${agentName} (LLM)` doesn't have to read
420
+ // the dd-trace span context's private fields later.
421
+ if (type === 'agent' && parentId == null) {
422
+ info.currentTopLevelAgentSpanId = oaiSpan.spanId
423
+ info.currentTopLevelAgentName = spanName
424
+ }
425
+
426
+ // Capture the first response span whose parent is the top-level agent
427
+ // as the workflow-level input source.
428
+ if (
429
+ (type === 'response' || type === 'generation') &&
430
+ parentId &&
431
+ !info.inputOaiSpan &&
432
+ parentId === info.currentTopLevelAgentSpanId
433
+ ) {
434
+ info.inputOaiSpan = oaiSpan
435
+ }
436
+ }
437
+
438
+ #updateTraceInfoOutput (oaiSpan) {
439
+ const info = this.#traceInfo.get(oaiSpan.traceId)
440
+ if (!info) return
441
+
442
+ if (
443
+ oaiSpan.parentId &&
444
+ info.currentTopLevelAgentSpanId &&
445
+ oaiSpan.parentId === info.currentTopLevelAgentSpanId
446
+ ) {
447
+ info.outputOaiSpan = oaiSpan
448
+ }
449
+ }
450
+
451
+ // ── Helpers ─────────────────────────────────────────────────────────────────
452
+
453
+ #resolveParent (oaiSpan) {
454
+ const parentId = oaiSpan.parentId
455
+ const traceId = oaiSpan.traceId
456
+ if (parentId) {
457
+ const parent = this.#oaiToDdSpan.get(parentId)
458
+ if (parent) return parent
459
+ }
460
+ if (traceId) {
461
+ const root = this.#oaiToDdSpan.get(traceId)
462
+ if (root) return root
463
+ }
464
+ }
465
+
466
+ /**
467
+ * Read the mutable tracer configuration so remote/runtime enablement is
468
+ * reflected without reconstructing the plugin.
469
+ *
470
+ * @returns {boolean}
471
+ */
472
+ #isLLMObsEnabled () {
473
+ return !!this.#config.llmobs?.DD_LLMOBS_ENABLED
474
+ }
475
+
476
+ /**
477
+ * Resolve model provider from the model invocation's async-local context.
478
+ *
479
+ * @returns {string}
480
+ */
481
+ #getCurrentModelProvider () {
482
+ const baseURL = legacyStorage.getStore()?.[MODEL_BASE_URL_STORE_KEY]
483
+ return baseURL ? getOpenAIModelProvider(baseURL) : DEFAULT_MODEL_PROVIDER
484
+ }
485
+
486
+ /**
487
+ * Build common APM tags without overriding the tracer's global service when
488
+ * the integration has no explicit service configuration.
489
+ *
490
+ * @param {string} spanKind
491
+ * @returns {Record<string, string>}
492
+ */
493
+ #getSpanTags (spanKind) {
494
+ const tags = {
495
+ component: COMPONENT,
496
+ 'span.kind': spanKind,
497
+ }
498
+ if (this.#service) tags.service = this.#service
499
+ return tags
500
+ }
501
+ }
502
+
503
+ module.exports = { MODEL_BASE_URL_STORE_KEY, OpenAIAgentsIntegration }
@@ -0,0 +1,108 @@
1
+ 'use strict'
2
+
3
+ const log = require('../../dd-trace/src/log')
4
+
5
+ const SPAN_KIND_BY_TYPE = {
6
+ agent: 'agent',
7
+ function: 'tool',
8
+ handoff: 'tool',
9
+ guardrail: 'task',
10
+ custom: 'task',
11
+ generation: 'llm',
12
+ response: 'llm',
13
+ }
14
+
15
+ // Lifecycle methods are awaited by agents-core. Share one resolved Promise
16
+ // across every callback so we don't allocate per span event.
17
+ const RESOLVED = Promise.resolve()
18
+
19
+ /**
20
+ * dd-trace-js implementation of the agents-core `TracingProcessor` interface.
21
+ * Registered via `addTraceProcessor(new DDOpenAIAgentsProcessor(integration))` inside
22
+ * the `@openai/agents` module load hook. Mirrors Python's LLMObsTraceProcessor.
23
+ *
24
+ * Each agents-core Span / Trace lifecycle event turns into a dd-trace span
25
+ * (APM + LLMObs-annotated) keyed off the agents-core spanId / traceId. Parent
26
+ * hierarchy is resolved through the agents-core parentId chain, which gives us
27
+ * correct multi-agent handoff nesting that ctx-argument capture cannot provide.
28
+ *
29
+ * agents-core awaits the lifecycle methods, so each one returns a settled
30
+ * Promise even though the work is synchronous.
31
+ */
32
+ class DDOpenAIAgentsProcessor {
33
+ /**
34
+ * @param {() => (import('./integration').OpenAIAgentsIntegration | undefined)} getIntegration - Lazy accessor for the
35
+ * current OpenAIAgentsIntegration singleton. Read on each lifecycle event
36
+ * so re-instantiating the plugin doesn't strand the processor against an
37
+ * old integration reference inside agents-core.
38
+ */
39
+ constructor (getIntegration) {
40
+ this._getIntegration = getIntegration
41
+ }
42
+
43
+ onTraceStart (oaiTrace) {
44
+ const integration = this._getIntegration()
45
+ if (!integration?.enabled) return RESOLVED
46
+ try {
47
+ integration.startTrace(oaiTrace)
48
+ } catch (err) {
49
+ log.warn('[openai-agents] onTraceStart failed: %s', err)
50
+ }
51
+ return RESOLVED
52
+ }
53
+
54
+ onTraceEnd (oaiTrace) {
55
+ const integration = this._getIntegration()
56
+ if (!integration?.enabled) return RESOLVED
57
+ try {
58
+ integration.endTrace(oaiTrace)
59
+ } catch (err) {
60
+ log.warn('[openai-agents] onTraceEnd failed: %s', err)
61
+ }
62
+ return RESOLVED
63
+ }
64
+
65
+ onSpanStart (oaiSpan) {
66
+ const integration = this._getIntegration()
67
+ if (!integration?.enabled) return RESOLVED
68
+ if (!oaiSpan?.spanData) return RESOLVED // guard NoopSpan
69
+ const kind = SPAN_KIND_BY_TYPE[oaiSpan.spanData.type]
70
+ if (!kind) return RESOLVED // span types without an LLMObs kind are not traced
71
+ try {
72
+ integration.startSpan(oaiSpan, kind)
73
+ } catch (err) {
74
+ log.warn('[openai-agents] onSpanStart failed: %s', err)
75
+ }
76
+ return RESOLVED
77
+ }
78
+
79
+ onSpanEnd (oaiSpan) {
80
+ const integration = this._getIntegration()
81
+ if (!integration?.enabled) return RESOLVED
82
+ if (!oaiSpan?.spanData) return RESOLVED
83
+ try {
84
+ integration.endSpan(oaiSpan)
85
+ } catch (err) {
86
+ log.warn('[openai-agents] onSpanEnd failed: %s', err)
87
+ }
88
+ return RESOLVED
89
+ }
90
+
91
+ forceFlush () {
92
+ // dd-trace exports on its own schedule; nothing to force here.
93
+ return RESOLVED
94
+ }
95
+
96
+ shutdown () {
97
+ const integration = this._getIntegration()
98
+ if (!integration) return RESOLVED
99
+ try {
100
+ integration.clearState()
101
+ } catch (err) {
102
+ log.warn('[openai-agents] shutdown cleanup failed: %s', err)
103
+ }
104
+ return RESOLVED
105
+ }
106
+ }
107
+
108
+ module.exports = { DDOpenAIAgentsProcessor }
@@ -0,0 +1,60 @@
1
+ 'use strict'
2
+
3
+ // agents-core's `error` is a plain `{ message, data }` object, not a JS Error
4
+ // — there's no constructor to name and no stack. We tag a stable type constant
5
+ // and stringify `data` into the message so the LLMObs error shape stays
6
+ // consistent with other integrations.
7
+ const AGENTS_ERROR_TYPE = 'AgentsCoreError'
8
+
9
+ /**
10
+ * Build the dd-trace span name from an agents-core oai-span. Handoffs collapse
11
+ * the target agent name into snake_case under a `transfer_to_` prefix; other
12
+ * span types use the SDK-provided name, falling back to
13
+ * `openai_agents.<type>` (or `openai_agents.request` when even the type is
14
+ * missing).
15
+ *
16
+ * @param {object} oaiSpan
17
+ * @returns {string}
18
+ */
19
+ function deriveSpanName (oaiSpan) {
20
+ const spanData = oaiSpan.spanData
21
+ if (spanData?.type === 'handoff') {
22
+ const toAgent = spanData.to_agent || ''
23
+ if (toAgent) return `transfer_to_${toAgent.replaceAll(' ', '_').toLowerCase()}`
24
+ }
25
+ if (spanData?.name) return spanData.name
26
+ return spanData?.type ? `openai_agents.${spanData.type}` : 'openai_agents.request'
27
+ }
28
+
29
+ /**
30
+ * Apply agents-core's error shape onto a dd-trace span. No-op when the
31
+ * oai-span has no error attached.
32
+ *
33
+ * @param {object} ddSpan
34
+ * @param {object} oaiSpan
35
+ */
36
+ function applyError (ddSpan, oaiSpan) {
37
+ const err = oaiSpan.error
38
+ if (!err) return
39
+
40
+ ddSpan.setTag('error', true)
41
+
42
+ let errorMessage = err.message || 'Error'
43
+ if (err.data) {
44
+ try {
45
+ errorMessage = JSON.stringify(err.data)
46
+ } catch {
47
+ // circular / non-serializable — fall back to the raw message
48
+ }
49
+ }
50
+
51
+ ddSpan.setTag('error.type', AGENTS_ERROR_TYPE)
52
+ ddSpan.setTag('error.message', errorMessage)
53
+ ddSpan.setTag('error.stack', err.stack || '')
54
+ }
55
+
56
+ module.exports = {
57
+ AGENTS_ERROR_TYPE,
58
+ deriveSpanName,
59
+ applyError,
60
+ }