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.
- package/ci/diagnose.js +162 -21
- package/ci/init.js +0 -1
- package/ci/runbook.md +187 -180
- package/ci/test-optimization-validation/approval-artifacts.js +3 -1
- package/ci/test-optimization-validation/approval.js +52 -15
- package/ci/test-optimization-validation/ci-command-candidate.js +26 -32
- package/ci/test-optimization-validation/ci-discovery.js +1 -1
- package/ci/test-optimization-validation/ci-package-scripts.js +141 -0
- package/ci/test-optimization-validation/ci-remediation.js +112 -39
- package/ci/test-optimization-validation/cli.js +516 -681
- package/ci/test-optimization-validation/command-blocker.js +188 -33
- package/ci/test-optimization-validation/command-output-policy.js +3 -26
- package/ci/test-optimization-validation/command-runner.js +189 -245
- package/ci/test-optimization-validation/environment.js +90 -0
- package/ci/test-optimization-validation/executable.js +159 -388
- package/ci/test-optimization-validation/framework-adapters/cucumber.js +119 -0
- package/ci/test-optimization-validation/framework-adapters/cypress.js +107 -0
- package/ci/test-optimization-validation/framework-adapters/playwright.js +181 -0
- package/ci/test-optimization-validation/generated-files.js +75 -13
- package/ci/test-optimization-validation/generated-test-contract.js +283 -0
- package/ci/test-optimization-validation/generated-verifier.js +49 -16
- package/ci/test-optimization-validation/literal-environment.js +57 -0
- package/ci/test-optimization-validation/manifest-loader.js +2 -22
- package/ci/test-optimization-validation/manifest-scaffold.js +816 -515
- package/ci/test-optimization-validation/manifest-schema.js +494 -702
- package/ci/test-optimization-validation/offline-fixtures.js +4 -1
- package/ci/test-optimization-validation/plan-writer.js +243 -984
- package/ci/test-optimization-validation/preflight-runner.js +49 -55
- package/ci/test-optimization-validation/redaction.js +2 -1
- package/ci/test-optimization-validation/report-writer.js +391 -1357
- package/ci/test-optimization-validation/result-semantics.js +86 -0
- package/ci/test-optimization-validation/runner-command.js +249 -0
- package/ci/test-optimization-validation/runner-contract.js +664 -0
- package/ci/test-optimization-validation/scenarios/auto-test-retries.js +29 -3
- package/ci/test-optimization-validation/scenarios/basic-reporting.js +240 -543
- package/ci/test-optimization-validation/scenarios/ci-wiring.js +450 -670
- package/ci/test-optimization-validation/scenarios/early-flake-detection.js +4 -3
- package/ci/test-optimization-validation/scenarios/helpers.js +67 -9
- package/ci/test-optimization-validation/scenarios/test-management.js +4 -3
- package/ci/test-optimization-validation/source-text.js +87 -0
- package/ci/test-optimization-validation/test-output.js +12 -22
- package/ext/tags.d.ts +1 -0
- package/index.d.ts +7 -0
- package/package.json +2 -2
- package/packages/datadog-instrumentations/src/helpers/hooks.js +2 -0
- package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/index.js +1 -0
- package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/openai-agents.js +31 -0
- package/packages/datadog-instrumentations/src/openai-agents.js +159 -0
- package/packages/datadog-plugin-openai-agents/src/index.js +74 -0
- package/packages/datadog-plugin-openai-agents/src/integration.js +503 -0
- package/packages/datadog-plugin-openai-agents/src/processor.js +108 -0
- package/packages/datadog-plugin-openai-agents/src/util.js +60 -0
- package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +48 -49
- package/packages/dd-trace/src/ci-visibility/requests/get-library-configuration.js +161 -33
- package/packages/dd-trace/src/ci-visibility/test-optimization-http-cache-schema.js +6 -4
- package/packages/dd-trace/src/config/generated-config-types.d.ts +2 -0
- package/packages/dd-trace/src/config/supported-configurations.json +7 -0
- package/packages/dd-trace/src/encode/span-stats.js +7 -1
- package/packages/dd-trace/src/exporter.js +16 -9
- package/packages/dd-trace/src/llmobs/index.js +9 -1
- package/packages/dd-trace/src/llmobs/plugins/ai/util.js +104 -22
- package/packages/dd-trace/src/llmobs/plugins/openai/index.js +5 -9
- package/packages/dd-trace/src/llmobs/plugins/openai/utils.js +20 -0
- package/packages/dd-trace/src/llmobs/plugins/openai-agents/utils.js +321 -0
- package/packages/dd-trace/src/llmobs/sdk.js +2 -1
- package/packages/dd-trace/src/llmobs/span_processor.js +7 -2
- package/packages/dd-trace/src/llmobs/tagger.js +23 -3
- package/packages/dd-trace/src/llmobs/util.js +56 -3
- package/packages/dd-trace/src/openfeature/agentless_configuration_source.js +3 -6
- package/packages/dd-trace/src/openfeature/configuration_source.js +5 -8
- package/packages/dd-trace/src/opentelemetry/metrics/otlp_span_stats_transformer.js +5 -1
- package/packages/dd-trace/src/plugins/ci_plugin.js +9 -3
- package/packages/dd-trace/src/plugins/index.js +1 -0
- package/packages/dd-trace/src/plugins/outbound.js +4 -1
- package/packages/dd-trace/src/span_processor.js +1 -1
- package/packages/dd-trace/src/span_stats.js +22 -4
- package/ci/test-optimization-validation/command-suitability.js +0 -471
- package/ci/test-optimization-validation/init-probe-preload.js +0 -163
- package/ci/test-optimization-validation/init-probe.js +0 -246
- package/ci/test-optimization-validation/late-initialization.js +0 -63
- package/ci/test-optimization-validation/local-command.js +0 -163
- package/ci/test-optimization-validation/setup-runner.js +0 -97
- 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
|
+
}
|