dd-trace 6.0.0 → 6.2.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 (98) hide show
  1. package/LICENSE-3rdparty.csv +1 -2
  2. package/index.d.ts +28 -1
  3. package/initialize.mjs +4 -2
  4. package/package.json +34 -30
  5. package/packages/datadog-instrumentations/src/ai.js +45 -0
  6. package/packages/datadog-instrumentations/src/apollo-server.js +5 -13
  7. package/packages/datadog-instrumentations/src/child_process.js +3 -3
  8. package/packages/datadog-instrumentations/src/claude-agent-sdk.js +587 -0
  9. package/packages/datadog-instrumentations/src/cucumber.js +102 -43
  10. package/packages/datadog-instrumentations/src/cypress-config.js +11 -3
  11. package/packages/datadog-instrumentations/src/fastify.js +27 -8
  12. package/packages/datadog-instrumentations/src/helpers/hooks.js +3 -0
  13. package/packages/datadog-instrumentations/src/helpers/instrument.js +7 -0
  14. package/packages/datadog-instrumentations/src/helpers/rewriter/index.js +5 -1
  15. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/ai.js +6 -6
  16. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/claude-agent-sdk.js +17 -0
  17. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/index.js +2 -0
  18. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/mercurius.js +31 -0
  19. package/packages/datadog-instrumentations/src/helpers/rewriter/transforms.js +19 -5
  20. package/packages/datadog-instrumentations/src/jest.js +713 -63
  21. package/packages/datadog-instrumentations/src/mercurius.js +11 -0
  22. package/packages/datadog-instrumentations/src/mocha/main.js +24 -3
  23. package/packages/datadog-instrumentations/src/mocha/utils.js +128 -22
  24. package/packages/datadog-instrumentations/src/mocha/worker.js +13 -0
  25. package/packages/datadog-instrumentations/src/mongodb.js +74 -0
  26. package/packages/datadog-instrumentations/src/mongoose.js +4 -5
  27. package/packages/datadog-instrumentations/src/otel-sdk-trace.js +15 -0
  28. package/packages/datadog-instrumentations/src/playwright.js +13 -9
  29. package/packages/datadog-instrumentations/src/vitest-main-no-worker-init.js +4 -0
  30. package/packages/datadog-instrumentations/src/vitest-main.js +26 -1
  31. package/packages/datadog-instrumentations/src/vitest-util.js +2 -0
  32. package/packages/datadog-instrumentations/src/vitest-worker.js +103 -34
  33. package/packages/datadog-plugin-ai/src/index.js +1 -1
  34. package/packages/datadog-plugin-ai/src/tracing.js +66 -3
  35. package/packages/datadog-plugin-ai/src/utils.js +17 -4
  36. package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/context.js +1 -0
  37. package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/handler.js +1 -0
  38. package/packages/datadog-plugin-child_process/src/index.js +13 -2
  39. package/packages/datadog-plugin-claude-agent-sdk/src/index.js +31 -0
  40. package/packages/datadog-plugin-claude-agent-sdk/src/tracing.js +107 -0
  41. package/packages/datadog-plugin-claude-agent-sdk/src/util.js +15 -0
  42. package/packages/datadog-plugin-cucumber/src/index.js +14 -23
  43. package/packages/datadog-plugin-cypress/src/cypress-plugin.js +351 -11
  44. package/packages/datadog-plugin-cypress/src/index.js +47 -2
  45. package/packages/datadog-plugin-cypress/src/plugin.js +1 -0
  46. package/packages/datadog-plugin-graphql/src/execute.js +25 -25
  47. package/packages/datadog-plugin-graphql/src/index.js +23 -1
  48. package/packages/datadog-plugin-graphql/src/request.js +104 -0
  49. package/packages/datadog-plugin-graphql/src/utils.js +186 -2
  50. package/packages/datadog-plugin-graphql/src/validate.js +25 -2
  51. package/packages/datadog-plugin-jest/src/index.js +30 -14
  52. package/packages/datadog-plugin-mocha/src/index.js +39 -14
  53. package/packages/datadog-plugin-mongodb-core/src/bulk-write.js +43 -0
  54. package/packages/datadog-plugin-mongodb-core/src/index.js +8 -443
  55. package/packages/datadog-plugin-mongodb-core/src/query.js +452 -0
  56. package/packages/datadog-plugin-playwright/src/index.js +3 -2
  57. package/packages/datadog-plugin-vitest/src/index.js +43 -9
  58. package/packages/dd-trace/src/aiguard/index.js +9 -14
  59. package/packages/dd-trace/src/aiguard/integrations/index.js +34 -0
  60. package/packages/dd-trace/src/aiguard/integrations/openai.js +47 -33
  61. package/packages/dd-trace/src/aiguard/integrations/vercel-ai.js +42 -28
  62. package/packages/dd-trace/src/appsec/graphql.js +9 -6
  63. package/packages/dd-trace/src/appsec/iast/analyzers/nosql-injection-mongodb-analyzer.js +3 -10
  64. package/packages/dd-trace/src/appsec/lambda.js +8 -8
  65. package/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/index.js +26 -1
  66. package/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/worker/index.js +115 -24
  67. package/packages/dd-trace/src/ci-visibility/exporters/agent-proxy/index.js +3 -0
  68. package/packages/dd-trace/src/ci-visibility/exporters/agentless/index.js +3 -0
  69. package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +75 -7
  70. package/packages/dd-trace/src/ci-visibility/requests/upload-test-screenshot.js +156 -0
  71. package/packages/dd-trace/src/config/generated-config-types.d.ts +10 -0
  72. package/packages/dd-trace/src/config/supported-configurations.json +37 -0
  73. package/packages/dd-trace/src/crashtracking/crashtracker.js +2 -2
  74. package/packages/dd-trace/src/datastreams/writer.js +14 -2
  75. package/packages/dd-trace/src/encode/0.4.js +23 -2
  76. package/packages/dd-trace/src/encode/0.5.js +12 -1
  77. package/packages/dd-trace/src/encode/agentless-ci-visibility.js +11 -1
  78. package/packages/dd-trace/src/encode/coverage-ci-visibility.js +20 -2
  79. package/packages/dd-trace/src/exporters/common/writer.js +17 -1
  80. package/packages/dd-trace/src/guardrails/index.js +3 -1
  81. package/packages/dd-trace/src/guardrails/telemetry.js +40 -3
  82. package/packages/dd-trace/src/llmobs/plugins/ai/ddTelemetry.js +403 -0
  83. package/packages/dd-trace/src/llmobs/plugins/ai/index.js +8 -395
  84. package/packages/dd-trace/src/llmobs/plugins/ai/util.js +27 -0
  85. package/packages/dd-trace/src/llmobs/plugins/ai/vercelTelemetry.js +363 -0
  86. package/packages/dd-trace/src/llmobs/plugins/claude-agent-sdk/index.js +270 -0
  87. package/packages/dd-trace/src/llmobs/plugins/claude-agent-sdk/utils.js +10 -0
  88. package/packages/dd-trace/src/msgpack/chunk.js +33 -1
  89. package/packages/dd-trace/src/msgpack/index.js +6 -1
  90. package/packages/dd-trace/src/opentelemetry/span.js +5 -0
  91. package/packages/dd-trace/src/plugin_manager.js +16 -0
  92. package/packages/dd-trace/src/plugins/ci_plugin.js +147 -7
  93. package/packages/dd-trace/src/plugins/index.js +5 -0
  94. package/packages/dd-trace/src/plugins/util/test.js +4 -0
  95. package/packages/dd-trace/src/profiling/exporters/agent.js +28 -3
  96. package/packages/dd-trace/src/service-naming/schemas/v0/graphql.js +6 -0
  97. package/packages/dd-trace/src/service-naming/schemas/v1/graphql.js +8 -0
  98. package/vendor/dist/@apm-js-collab/code-transformer/index.js +5 -6
@@ -0,0 +1,363 @@
1
+ 'use strict'
2
+
3
+ const { parseModelProvider } = require('../../../../../datadog-plugin-ai/src/utils')
4
+ const BaseLLMObsPlugin = require('../base')
5
+ const {
6
+ getLlmObsSpanName,
7
+ getGenerationMetadataFromEvent,
8
+ getJsonStringValue,
9
+ getToolCallResultContent,
10
+ } = require('./util')
11
+
12
+ // TODO: add in embedMany once it has tracingChannel support
13
+ const SPAN_NAME_TO_KIND_MAPPING = {
14
+ // embeddings
15
+ embed: 'embedding',
16
+ // text generation
17
+ generateText: 'workflow',
18
+ streamText: 'workflow',
19
+ // llm operations
20
+ languageModelCall: 'llm',
21
+ // steps
22
+ step: 'step', // TODO: support step spans for manual instrumentation as well
23
+ // tools
24
+ executeTool: 'tool',
25
+ }
26
+
27
+ function nameFromOperation (operation, event) {
28
+ if (operation === 'executeTool') {
29
+ return event.toolCall?.toolName
30
+ }
31
+
32
+ return operation
33
+ }
34
+
35
+ function formatLanguageModelInputMessages (instructions, messages) {
36
+ if (!Array.isArray(messages)) return
37
+ const inputMessages = []
38
+
39
+ if (instructions) {
40
+ const systemPrompt = typeof instructions === 'string'
41
+ ? instructions
42
+ : Array.isArray(instructions)
43
+ ? instructions.map(instruction => instruction.content).join('')
44
+ : instructions.content
45
+
46
+ inputMessages.push({ role: 'system', content: systemPrompt })
47
+ }
48
+
49
+ for (const message of messages) {
50
+ const { role, content } = message
51
+
52
+ if (role === 'system') {
53
+ inputMessages.push({ role, content })
54
+ } else if (role === 'user') {
55
+ const userMessageContent =
56
+ typeof content === 'string'
57
+ ? content
58
+ : content
59
+ .filter(part => part.type === 'text')
60
+ .map(part => part.text)
61
+ .join('')
62
+
63
+ inputMessages.push({ role, content: userMessageContent })
64
+ } else if (role === 'assistant') {
65
+ if (typeof content === 'string') {
66
+ inputMessages.push({ role, content })
67
+ } else {
68
+ // re-use existing output message formatting
69
+ inputMessages.push(...formatLanguageModelOutputMessages(content))
70
+ }
71
+ } else if (role === 'tool') {
72
+ for (const part of content) {
73
+ if (part.type === 'tool-result') { // TODO: support tool approvals
74
+ const safeResult = getToolCallResultContent(part)
75
+
76
+ inputMessages.push({
77
+ role,
78
+ content: safeResult,
79
+ toolId: part.toolCallId,
80
+ })
81
+ }
82
+ }
83
+ }
84
+ }
85
+
86
+ return inputMessages
87
+ }
88
+
89
+ function formatLanguageModelOutputMessages (content) {
90
+ if (!Array.isArray(content)) return
91
+
92
+ const outputMessages = []
93
+ const toolCalls = []
94
+
95
+ let textContent = ''
96
+ let reasoningContent = ''
97
+
98
+ for (const part of content) {
99
+ const { type } = part
100
+
101
+ if (type === 'text') {
102
+ textContent += part.text
103
+ } else if (type === 'reasoning') {
104
+ reasoningContent += part.text
105
+ } else if (type === 'tool-call') {
106
+ const toolCallArguments = typeof part.input === 'string'
107
+ ? getJsonStringValue(part.input, {})
108
+ : part.input
109
+
110
+ toolCalls.push({
111
+ arguments: toolCallArguments,
112
+ name: part.toolName,
113
+ type: 'function',
114
+ toolId: part.toolCallId,
115
+ })
116
+ }
117
+ }
118
+
119
+ if (reasoningContent) {
120
+ outputMessages.push({ role: 'reasoning', content: reasoningContent })
121
+ }
122
+
123
+ const finalTextMessage = { role: 'assistant' }
124
+
125
+ if (textContent) {
126
+ finalTextMessage.content = textContent
127
+ }
128
+
129
+ if (toolCalls.length) {
130
+ finalTextMessage.toolCalls = toolCalls
131
+ }
132
+
133
+ outputMessages.push(finalTextMessage)
134
+
135
+ return outputMessages
136
+ }
137
+
138
+ class VercelAiTelemetryPlugin extends BaseLLMObsPlugin {
139
+ static id = 'ai_llmobs_vercel_telemetry'
140
+ static integration = 'ai'
141
+ static prefix = 'tracing:ai:telemetry'
142
+
143
+ /** @type {Map<string, Array<Record<string, unknown>>>} */
144
+ #lastOutputContentByCallId = new Map()
145
+
146
+ constructor () {
147
+ super(...arguments)
148
+
149
+ this.addSub('dd-trace:vercel-ai:chunk', ({ ctx, chunk, done }) => {
150
+ ctx.chunks ??= []
151
+ const chunks = ctx.chunks
152
+ if (chunk) chunks.push(chunk)
153
+
154
+ ctx.streamConsumed = done
155
+
156
+ if (!done) return
157
+
158
+ ctx.result ??= {}
159
+ const contentById = {}
160
+
161
+ for (const chunk of chunks) {
162
+ if (chunk.type === 'finish') {
163
+ ctx.result.usage = chunk.usage
164
+ } else if (chunk.type === 'reasoning-start') {
165
+ contentById[chunk.id] = { type: 'reasoning', text: '' }
166
+ } else if (chunk.type === 'text-start') {
167
+ contentById[chunk.id] = { type: 'text', text: '' }
168
+ } else if (chunk.type === 'reasoning-delta' || chunk.type === 'text-delta') {
169
+ if (!contentById[chunk.id]) {
170
+ // special-case handling when the start event did not come through as a chunk
171
+ contentById[chunk.id] = {
172
+ type: chunk.type === 'reasoning-delta' ? 'reasoning' : 'text',
173
+ text: '',
174
+ }
175
+ }
176
+ contentById[chunk.id].text += chunk.delta
177
+ } else if (chunk.type === 'tool-call') {
178
+ contentById[chunk.toolCallId] = chunk
179
+ }
180
+ }
181
+
182
+ const content = Object.values(contentById)
183
+ ctx.result.content = content
184
+
185
+ this.#lastOutputContentByCallId.set(ctx.event.callId, content)
186
+ })
187
+ }
188
+
189
+ /**
190
+ * @override
191
+ */
192
+ asyncEnd (ctx) {
193
+ // check if isStreamed and stream resolved
194
+ // this event will fire multiple times for the same channel
195
+ if (ctx.isStream && ctx.result?.stream && !ctx.streamConsumed) return
196
+
197
+ super.asyncEnd(ctx)
198
+ }
199
+
200
+ /**
201
+ * @override
202
+ */
203
+ getLLMObsSpanRegisterOptions (ctx) {
204
+ const span = ctx.currentStore?.span
205
+ if (!span) return
206
+
207
+ const { type: operation, event } = ctx
208
+ const kind = SPAN_NAME_TO_KIND_MAPPING[operation]
209
+ if (!kind) return
210
+
211
+ const normalizedName = nameFromOperation(operation, event) || operation
212
+
213
+ const options = {
214
+ kind, name: getLlmObsSpanName(normalizedName, event.functionId),
215
+ }
216
+
217
+ if (kind === 'llm' || kind === 'embedding') {
218
+ const modelName = event.modelId
219
+
220
+ options.modelName = modelName
221
+ options.modelProvider = parseModelProvider(event.provider, modelName)
222
+ }
223
+
224
+ return options
225
+ }
226
+
227
+ /**
228
+ * @override
229
+ */
230
+ setLLMObsTags (ctx) {
231
+ const span = ctx.currentStore?.span
232
+ if (!span) return
233
+
234
+ const { type: operation } = ctx
235
+ const kind = SPAN_NAME_TO_KIND_MAPPING[operation]
236
+ if (!kind) return
237
+
238
+ switch (operation) {
239
+ case 'embed':
240
+ this.setEmbeddingTags(span, ctx)
241
+ break
242
+ case 'generateText':
243
+ case 'streamText':
244
+ this.setTextGenerationTags(span, ctx)
245
+ break
246
+ case 'languageModelCall':
247
+ this.setLanguageModelCallTags(span, ctx)
248
+ break
249
+ case 'step':
250
+ this.setStepTags(span, ctx)
251
+ break
252
+ case 'executeTool':
253
+ this.setToolTags(span, ctx)
254
+ break
255
+ default:
256
+ break
257
+ }
258
+ }
259
+
260
+ setEmbeddingTags (span, ctx) {
261
+ const { event, result } = ctx
262
+
263
+ const input = event.value
264
+ const embedding = result?.embedding
265
+ const embeddingTextResult = embedding ? `[1 embedding(s) returned with size ${embedding.length}]` : ''
266
+
267
+ this._tagger.tagEmbeddingIO(span, input, embeddingTextResult)
268
+
269
+ this._tagger.tagMetrics(span, {
270
+ inputTokens: result?.usage?.tokens,
271
+ })
272
+ }
273
+
274
+ setTextGenerationTags (span, ctx) {
275
+ const { event, result } = ctx
276
+
277
+ const lastUserPrompt = event.messages.findLast(message => message.role === 'user')?.content
278
+ const input = Array.isArray(lastUserPrompt) ? lastUserPrompt.map(part => part.text ?? '').join('') : lastUserPrompt
279
+
280
+ const output =
281
+ ctx.isStream
282
+ ? this.#lastOutputContentByCallId.get(event.callId)?.find(part => part.type === 'text')?.text
283
+ : result?._output
284
+
285
+ this._tagger.tagTextIO(span, input, output)
286
+
287
+ const metadata = getGenerationMetadataFromEvent(event)
288
+ this._tagger.tagMetadata(span, metadata)
289
+
290
+ if (ctx.isStream) {
291
+ this.#lastOutputContentByCallId.delete(event.callId)
292
+ }
293
+ }
294
+
295
+ setStepTags (span, ctx) {
296
+ const { result, event } = ctx
297
+
298
+ const content = ctx.isStream ? this.#lastOutputContentByCallId.get(event.callId) : result?.content
299
+
300
+ // capture reasoning if applicable
301
+ const reasoning = content?.find(part => part.type === 'reasoning')?.text
302
+ if (reasoning) {
303
+ this._tagger.tagTextIO(span, reasoning)
304
+ }
305
+ }
306
+
307
+ setLanguageModelCallTags (span, ctx) {
308
+ const { event, result } = ctx
309
+
310
+ // input messages
311
+ const { instructions, messages } = event
312
+ const inputMessages = formatLanguageModelInputMessages(instructions, messages)
313
+
314
+ // output messages
315
+ const outputMessages =
316
+ result ? formatLanguageModelOutputMessages(result.content) : [{ role: 'assistant', content: '' }]
317
+
318
+ this._tagger.tagLLMIO(span, inputMessages, outputMessages)
319
+
320
+ // tool definitions
321
+ const { tools } = event
322
+ if (Array.isArray(tools)) {
323
+ this._tagger.tagToolDefinitions(
324
+ span,
325
+ tools.map(({ inputSchema, ...rest }) => ({
326
+ ...rest,
327
+ schema: {
328
+ properties: inputSchema?.properties ?? {},
329
+ required: inputSchema?.required ?? [],
330
+ type: inputSchema?.type ?? '',
331
+ },
332
+ }))
333
+ )
334
+ }
335
+
336
+ // metadata
337
+ const metadata = getGenerationMetadataFromEvent(event)
338
+ this._tagger.tagMetadata(span, metadata)
339
+
340
+ if (!result) return
341
+
342
+ // metrics
343
+ const { usage } = result
344
+ this._tagger.tagMetrics(span, {
345
+ inputTokens: usage?.inputTokens?.total,
346
+ cacheWriteTokens: usage?.inputTokens?.cacheWrite ?? 0,
347
+ cacheReadTokens: usage?.inputTokens?.cacheRead ?? 0,
348
+ outputTokens: usage?.outputTokens?.total,
349
+ reasoningOutputTokens: usage?.outputTokens?.reasoning ?? 0,
350
+ })
351
+ }
352
+
353
+ setToolTags (span, ctx) {
354
+ const { event, result } = ctx
355
+
356
+ const { toolCall } = event
357
+ if (!toolCall) return
358
+
359
+ this._tagger.tagTextIO(span, toolCall.input, result?.output?.output)
360
+ }
361
+ }
362
+
363
+ module.exports = VercelAiTelemetryPlugin
@@ -0,0 +1,270 @@
1
+ 'use strict'
2
+
3
+ const LLMObsPlugin = require('../base')
4
+ const { storage: llmobsStorage } = require('../../storage')
5
+ const { NAME, SESSION_ID } = require('../../constants/tags')
6
+ const { splitModel } = require('../../../../../datadog-plugin-claude-agent-sdk/src/util')
7
+
8
+ const subagentToolIds = new Set()
9
+
10
+ function normalizeToolOutputString (raw) {
11
+ const footerIndex = raw.search(/\s*agentId: [^\s]+ \(use SendMessage\b/)
12
+ if (footerIndex === -1) return raw
13
+
14
+ return raw.slice(0, footerIndex).trimEnd()
15
+ }
16
+
17
+ function getToolOutputText (raw) {
18
+ if (raw == null) return
19
+
20
+ if (Array.isArray(raw)) {
21
+ const output = []
22
+ for (const block of raw) {
23
+ const text = getToolOutputText(block)
24
+ if (text) output.push(text)
25
+ }
26
+ return output.join('\n') || undefined
27
+ }
28
+
29
+ if (raw.type === 'tool_result') return getToolOutputText(raw.content)
30
+ if (raw.type === 'text') return normalizeToolOutputString(raw.text)
31
+ if (raw.type === 'tool_reference') return raw.tool_name
32
+ if (raw.content !== undefined) return getToolOutputText(raw.content)
33
+
34
+ if (typeof raw === 'string') return normalizeToolOutputString(raw)
35
+
36
+ return JSON.stringify(raw)
37
+ }
38
+
39
+ function buildOutputMessages (chunks, llmStartIdx, llmEndIdx) {
40
+ let thinking = ''
41
+ let text = ''
42
+ const toolCalls = []
43
+
44
+ for (let i = llmStartIdx; i < llmEndIdx; i++) {
45
+ const c = chunks[i]
46
+ if (c.type !== 'assistant') continue
47
+ const block = c.message?.content?.[0]
48
+ if (block?.type === 'thinking') thinking += block.thinking ?? ''
49
+ else if (block?.type === 'text') text += block.text ?? ''
50
+ else if (block?.type === 'tool_use') {
51
+ toolCalls.push({ name: block.name, arguments: block.input ?? {}, toolId: block.id, type: block.type })
52
+ }
53
+ }
54
+
55
+ const messages = []
56
+ if (thinking) messages.push({ role: 'thinking', content: thinking })
57
+ const msg = { role: 'assistant', content: text }
58
+ if (toolCalls.length) msg.toolCalls = toolCalls
59
+ messages.push(msg)
60
+ return messages
61
+ }
62
+
63
+ class QueryLLMObsPlugin extends LLMObsPlugin {
64
+ static integration = 'claude-agent-sdk'
65
+ static id = 'llmobs_claude_agent_sdk_query'
66
+ static prefix = 'tracing:orchestrion:@anthropic-ai/claude-agent-sdk:query'
67
+
68
+ getLLMObsSpanRegisterOptions (ctx) {
69
+ return { kind: 'agent' }
70
+ }
71
+
72
+ start (ctx) {
73
+ super.start(ctx)
74
+ if (!this._tracerConfig.llmobs.DD_LLMOBS_ENABLED) return
75
+ const store = llmobsStorage.getStore()
76
+ const prev = ctx.runInContext ?? (fn => fn())
77
+ ctx.runInContext = fn => prev(() => llmobsStorage.run(store, fn))
78
+ }
79
+
80
+ asyncEnd (ctx) {
81
+ if (!ctx.streamResolved) return
82
+ super.asyncEnd(ctx)
83
+ }
84
+
85
+ setLLMObsTags (ctx) {
86
+ const span = ctx.currentStore?.span
87
+ if (!span) return
88
+
89
+ // post-populate session_id
90
+ if (ctx.session_id) this._tagger._setTag(span, SESSION_ID, ctx.session_id)
91
+ this._tagger.tagTextIO(span, ctx.arguments?.[0]?.prompt, ctx.output)
92
+
93
+ // metadata
94
+ const { cwd, permissionMode } = ctx
95
+ const metadata = {}
96
+
97
+ if (cwd) metadata.cwd = cwd
98
+ if (permissionMode) metadata.permissionMode = permissionMode
99
+
100
+ this._tagger.tagMetadata(span, metadata)
101
+ }
102
+ }
103
+
104
+ class StepLlmObsPlugin extends LLMObsPlugin {
105
+ static integration = 'claude-agent-sdk'
106
+ static id = 'claude_agent_sdk_step_llmobs'
107
+ static system = 'claude-agent-sdk'
108
+ static prefix = 'tracing:apm:claude-agent-sdk:step'
109
+
110
+ getLLMObsSpanRegisterOptions (ctx) {
111
+ if (ctx.parentToolUseId) subagentToolIds.add(ctx.parentToolUseId)
112
+ return { kind: 'step', name: `step-${ctx.stepIndex}`, sessionId: ctx.sessionId }
113
+ }
114
+
115
+ end (ctx) {
116
+ super.end(ctx)
117
+ super.asyncEnd(ctx)
118
+ }
119
+
120
+ setLLMObsTags (ctx) {
121
+ const span = ctx.currentStore?.span
122
+ if (!span) return
123
+
124
+ const { chunks, llmStartIdx, llmEndIdx, toolOutputs } = ctx
125
+ if (!chunks) return
126
+
127
+ const outputMessages = buildOutputMessages(chunks, llmStartIdx, llmEndIdx)
128
+ const thinking = outputMessages.find(m => m.role === 'thinking')?.content ?? ''
129
+
130
+ const output = toolOutputs?.length
131
+ ? getToolOutputText(toolOutputs)
132
+ : outputMessages.find(m => m.role === 'assistant')?.content ?? ''
133
+
134
+ this._tagger.tagTextIO(span, thinking, output)
135
+ }
136
+ }
137
+
138
+ class LlmLlmObsPlugin extends LLMObsPlugin {
139
+ static integration = 'claude-agent-sdk'
140
+ static id = 'claude_agent_sdk_llm_llmobs'
141
+ static system = 'claude-agent-sdk'
142
+ static prefix = 'tracing:apm:claude-agent-sdk:llm'
143
+
144
+ getLLMObsSpanRegisterOptions (ctx) {
145
+ const { modelName, modelProvider } = splitModel(ctx.model)
146
+ return { kind: 'llm', name: ctx.model, modelName, modelProvider, sessionId: ctx.sessionId }
147
+ }
148
+
149
+ end (ctx) {
150
+ super.end(ctx)
151
+ super.asyncEnd(ctx)
152
+ }
153
+
154
+ setLLMObsTags (ctx) {
155
+ const span = ctx.currentStore?.span
156
+ if (!span) return
157
+
158
+ const { chunks, llmStartIdx, llmEndIdx, parentToolUseId, initialPrompt, usage } = ctx
159
+
160
+ if (chunks) {
161
+ const inputMessages = this.#buildInputMessages(chunks, llmStartIdx, parentToolUseId, initialPrompt)
162
+ const outputMessages = buildOutputMessages(chunks, llmStartIdx, llmEndIdx)
163
+ this._tagger.tagLLMIO(span, inputMessages, outputMessages)
164
+ }
165
+
166
+ if (usage) {
167
+ const cacheWriteTokens = usage.cache_creation_input_tokens ?? 0
168
+ const cacheReadTokens = usage.cache_read_input_tokens ?? 0
169
+ const inputTokens = (usage.input_tokens ?? 0) + cacheWriteTokens + cacheReadTokens
170
+ const outputTokens = usage.output_tokens ?? 0
171
+ this._tagger.tagMetrics(span, {
172
+ input_tokens: inputTokens,
173
+ output_tokens: outputTokens,
174
+ cache_read_input_tokens: cacheReadTokens,
175
+ cache_write_input_tokens: cacheWriteTokens,
176
+ total_tokens: inputTokens + outputTokens,
177
+ })
178
+ }
179
+ }
180
+
181
+ #buildInputMessages (chunks, llmStartIdx, parentToolUseId, initialPrompt) {
182
+ const messages = []
183
+ if (initialPrompt) messages.push({ role: 'user', content: initialPrompt })
184
+ const seenIds = new Set()
185
+
186
+ for (let i = 0; i < llmStartIdx; i++) {
187
+ const c = chunks[i]
188
+ if (c.parent_tool_use_id !== parentToolUseId) continue
189
+
190
+ if (c.type === 'assistant') {
191
+ const msgId = c.message?.id
192
+ if (!msgId || seenIds.has(msgId)) continue
193
+ seenIds.add(msgId)
194
+
195
+ let thinking = ''
196
+ let text = ''
197
+ const toolCalls = []
198
+ for (let j = i; j < llmStartIdx; j++) {
199
+ const cc = chunks[j]
200
+ if (cc.type !== 'assistant' || cc.message?.id !== msgId) break
201
+ const block = cc.message?.content?.[0]
202
+ if (block?.type === 'thinking') thinking += block.thinking ?? ''
203
+ else if (block?.type === 'text') text += block.text ?? ''
204
+ else if (block?.type === 'tool_use') {
205
+ toolCalls.push({ name: block.name, arguments: block.input ?? {}, toolId: block.id, type: block.type })
206
+ }
207
+ }
208
+ if (thinking) messages.push({ role: 'thinking', content: thinking })
209
+ const msg = { role: 'assistant', content: text }
210
+ if (toolCalls.length) msg.toolCalls = toolCalls
211
+ messages.push(msg)
212
+ } else if (c.type === 'user') {
213
+ const content = c.message?.content
214
+ if (!content) continue
215
+ for (const block of content) {
216
+ if (block.type === 'text') {
217
+ messages.push({ role: 'user', content: block.text ?? '' })
218
+ } else if (block.type === 'tool_result') {
219
+ const text = getToolOutputText(block.content) ?? ''
220
+ messages.push({ role: 'tool', content: text })
221
+ }
222
+ }
223
+ }
224
+ }
225
+
226
+ return messages
227
+ }
228
+ }
229
+
230
+ class ToolLlmObsPlugin extends LLMObsPlugin {
231
+ static integration = 'claude-agent-sdk'
232
+ static id = 'claude_agent_sdk_tool_llmobs'
233
+ static system = 'claude-agent-sdk'
234
+ static prefix = 'tracing:apm:claude-agent-sdk:tool'
235
+
236
+ getLLMObsSpanRegisterOptions (ctx) {
237
+ return { kind: 'tool', name: ctx.name, sessionId: ctx.sessionId }
238
+ }
239
+
240
+ end (ctx) {
241
+ super.end(ctx)
242
+ super.asyncEnd(ctx)
243
+ }
244
+
245
+ setLLMObsTags (ctx) {
246
+ const span = ctx.currentStore?.span
247
+ if (!span) return
248
+
249
+ if (subagentToolIds.has(ctx.id)) {
250
+ subagentToolIds.delete(ctx.id)
251
+ const description = ctx.input?.description
252
+ this._tagger.changeKind(span, 'agent')
253
+ if (description) this._tagger._setTag(span, NAME, `${ctx.name} (${description})`)
254
+ const output = getToolOutputText(ctx.output)
255
+ this._tagger.tagTextIO(span, ctx.input?.prompt, output)
256
+ return
257
+ }
258
+
259
+ const input = ctx.input ? JSON.stringify(ctx.input) : undefined
260
+ const output = getToolOutputText(ctx.output)
261
+ this._tagger.tagTextIO(span, input, output)
262
+ }
263
+ }
264
+
265
+ module.exports = [
266
+ QueryLLMObsPlugin,
267
+ StepLlmObsPlugin,
268
+ ToolLlmObsPlugin,
269
+ LlmLlmObsPlugin,
270
+ ]
@@ -0,0 +1,10 @@
1
+ 'use strict'
2
+
3
+ function splitModel (model) {
4
+ if (!model) return { modelName: undefined, modelProvider: 'anthropic' }
5
+ const idx = model.indexOf('/')
6
+ if (idx === -1) return { modelName: model, modelProvider: 'anthropic' }
7
+ return { modelName: model.slice(idx + 1), modelProvider: model.slice(0, idx) }
8
+ }
9
+
10
+ module.exports = { splitModel }
@@ -10,6 +10,33 @@ const SHRINK_AFTER_FLUSHES = 32
10
10
  // shape: after a halving step the post-shrink fill is the prior peak doubled,
11
11
  // still under 50 %.
12
12
  const SHRINK_USAGE_RATIO = 4
13
+ // Hard cap on chunk growth. The agent's trace intake rejects payloads over
14
+ // 50 MiB, so anything past that is dead on arrival anyway. A pathological
15
+ // single trace (unsanitized meta tag the size of a media file, multi-MB
16
+ // stack trace) used to grow the buffer without limit until either the
17
+ // allocation failed or the process tripped its memory ceiling. `reserve`
18
+ // now refuses the growth with a tagged `RangeError`; `AgentEncoder` catches
19
+ // it, resets the in-flight payload, and logs.
20
+ const MAX_SIZE = 50 * 1024 * 1024 // 50 MiB
21
+
22
+ /**
23
+ * Thrown when a chunk — or an assembled payload stitched from several chunks —
24
+ * would cross `MAX_SIZE`. Shared so the `reserve` cap and the per-encoder
25
+ * assembled-size guards (`0.5`, agentless CI Visibility) raise the same `code`,
26
+ * which every writer's `flush` recognises to drop the payload instead of
27
+ * crashing the host.
28
+ */
29
+ class OverflowError extends RangeError {
30
+ code = 'ERR_MSGPACK_CHUNK_OVERFLOW'
31
+
32
+ /**
33
+ * @param {number} needed Requested total byte size.
34
+ */
35
+ constructor (needed) {
36
+ super(`MsgpackChunk capped at ${MAX_SIZE} bytes; requested ${needed}`)
37
+ this.name = 'OverflowError'
38
+ }
39
+ }
13
40
 
14
41
  /**
15
42
  * Resizable msgpack write buffer. Owns the byte-layout primitives the encoder
@@ -100,11 +127,14 @@ class MsgpackChunk {
100
127
  const needed = this.length + size
101
128
 
102
129
  if (needed > this.buffer.length) {
130
+ if (needed > MAX_SIZE) {
131
+ throw new OverflowError(needed)
132
+ }
103
133
  let newSize = this.buffer.length
104
134
  // `*= 2` instead of `<<= 1`: `1073741824 << 1` is negative as int32,
105
135
  // and msgpack values can legitimately reach the multi-GiB range.
106
136
  while (newSize < needed) newSize *= 2
107
- this.#resize(newSize)
137
+ this.#resize(Math.min(newSize, MAX_SIZE))
108
138
  }
109
139
 
110
140
  this.length += size
@@ -454,3 +484,5 @@ class MsgpackChunk {
454
484
  }
455
485
 
456
486
  module.exports = MsgpackChunk
487
+ module.exports.MAX_SIZE = MAX_SIZE
488
+ module.exports.OverflowError = OverflowError
@@ -97,4 +97,9 @@ function writeMap (bytes, value) {
97
97
  }
98
98
  }
99
99
 
100
- module.exports = { MsgpackChunk, encode }
100
+ module.exports = {
101
+ MsgpackChunk,
102
+ encode,
103
+ MAX_SIZE: MsgpackChunk.MAX_SIZE,
104
+ OverflowError: MsgpackChunk.OverflowError,
105
+ }