dd-trace 6.9.0 → 6.11.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 (175) hide show
  1. package/README.md +0 -17
  2. package/ci/vitest-no-worker-init-setup.mjs +112 -10
  3. package/index.d.ts +150 -3
  4. package/initialize.mjs +28 -17
  5. package/openfeature.js +2 -2
  6. package/package.json +14 -10
  7. package/packages/datadog-esbuild/index.js +0 -34
  8. package/packages/datadog-instrumentations/src/anthropic.js +210 -15
  9. package/packages/datadog-instrumentations/src/aws-sdk.js +9 -1
  10. package/packages/datadog-instrumentations/src/browser-bunyan.js +11 -0
  11. package/packages/datadog-instrumentations/src/bunyan.js +3 -16
  12. package/packages/datadog-instrumentations/src/confluentinc-kafka-javascript.js +111 -6
  13. package/packages/datadog-instrumentations/src/cucumber.js +26 -11
  14. package/packages/datadog-instrumentations/src/cypress-config.js +182 -16
  15. package/packages/datadog-instrumentations/src/cypress-legacy-finalizers.js +30 -0
  16. package/packages/datadog-instrumentations/src/cypress.js +2 -0
  17. package/packages/datadog-instrumentations/src/fastify.js +2 -0
  18. package/packages/datadog-instrumentations/src/helpers/bunyan.js +22 -0
  19. package/packages/datadog-instrumentations/src/helpers/hooks.js +2 -1
  20. package/packages/datadog-instrumentations/src/helpers/pool-acquire.js +634 -0
  21. package/packages/datadog-instrumentations/src/helpers/register.js +10 -8
  22. package/packages/datadog-instrumentations/src/helpers/rewriter/index.js +2 -1
  23. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/webdriverio.js +81 -0
  24. package/packages/datadog-instrumentations/src/helpers/rewriter/targets.json +1 -0
  25. package/packages/datadog-instrumentations/src/helpers/rewriter/transforms.js +83 -1
  26. package/packages/datadog-instrumentations/src/jest.js +26 -18
  27. package/packages/datadog-instrumentations/src/mariadb.js +278 -21
  28. package/packages/datadog-instrumentations/src/mocha/main.js +540 -56
  29. package/packages/datadog-instrumentations/src/mocha/utils.js +132 -25
  30. package/packages/datadog-instrumentations/src/mysql.js +45 -15
  31. package/packages/datadog-instrumentations/src/mysql2.js +119 -18
  32. package/packages/datadog-instrumentations/src/path-to-regexp.js +67 -1
  33. package/packages/datadog-instrumentations/src/pg.js +116 -92
  34. package/packages/datadog-instrumentations/src/playwright-reporter.js +99 -0
  35. package/packages/datadog-instrumentations/src/playwright.js +149 -2
  36. package/packages/datadog-instrumentations/src/router.js +2 -0
  37. package/packages/datadog-instrumentations/src/vitest-main-no-worker-init.js +43 -4
  38. package/packages/datadog-instrumentations/src/vitest-main.js +180 -20
  39. package/packages/datadog-instrumentations/src/vitest-util.js +64 -5
  40. package/packages/datadog-instrumentations/src/vitest-worker.js +150 -25
  41. package/packages/datadog-instrumentations/src/webdriverio.js +31 -17
  42. package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/trace-checkpoint.js +12 -6
  43. package/packages/datadog-plugin-aws-sdk/src/base.js +1 -0
  44. package/packages/datadog-plugin-aws-sdk/src/services/eventbridge.js +163 -44
  45. package/packages/datadog-plugin-aws-sdk/src/services/kinesis.js +4 -13
  46. package/packages/datadog-plugin-aws-sdk/src/services/sqs.js +1 -5
  47. package/packages/datadog-plugin-azure-functions/src/index.js +1 -1
  48. package/packages/datadog-plugin-browser-bunyan/src/index.js +9 -0
  49. package/packages/datadog-plugin-bunyan/src/index.js +5 -8
  50. package/packages/datadog-plugin-cucumber/src/index.js +11 -0
  51. package/packages/datadog-plugin-cypress/src/after-run.js +5 -1
  52. package/packages/datadog-plugin-cypress/src/after-spec.js +5 -1
  53. package/packages/datadog-plugin-cypress/src/cypress-plugin.js +82 -30
  54. package/packages/datadog-plugin-cypress/src/finalization.js +76 -0
  55. package/packages/datadog-plugin-cypress/src/index.js +12 -6
  56. package/packages/datadog-plugin-cypress/src/plugin.js +16 -3
  57. package/packages/datadog-plugin-dd-trace-api/src/index.js +4 -0
  58. package/packages/datadog-plugin-electron/src/index.js +2 -0
  59. package/packages/datadog-plugin-fs/src/index.js +1 -1
  60. package/packages/datadog-plugin-google-cloud-pubsub/src/producer.js +31 -17
  61. package/packages/datadog-plugin-jest/src/index.js +11 -4
  62. package/packages/datadog-plugin-kafkajs/src/batch-consumer.js +2 -3
  63. package/packages/datadog-plugin-kafkajs/src/consumer.js +2 -3
  64. package/packages/datadog-plugin-kafkajs/src/producer.js +3 -2
  65. package/packages/datadog-plugin-kafkajs/src/utils.js +65 -7
  66. package/packages/datadog-plugin-mocha/src/index.js +606 -40
  67. package/packages/datadog-plugin-mysql/src/index.js +37 -0
  68. package/packages/datadog-plugin-nats/src/index.js +1 -1
  69. package/packages/datadog-plugin-next/src/index.js +1 -19
  70. package/packages/datadog-plugin-openai/src/tracing.js +41 -30
  71. package/packages/datadog-plugin-openai-agents/src/integration.js +279 -69
  72. package/packages/datadog-plugin-openai-agents/src/processor.js +16 -4
  73. package/packages/datadog-plugin-pg/src/index.js +1 -0
  74. package/packages/datadog-plugin-playwright/src/index.js +42 -13
  75. package/packages/datadog-plugin-vitest/src/index.js +14 -20
  76. package/packages/datadog-plugin-ws/src/util.js +2 -1
  77. package/packages/datadog-webpack/index.js +0 -11
  78. package/packages/dd-trace/index.js +0 -1
  79. package/packages/dd-trace/src/agent/info.js +6 -5
  80. package/packages/dd-trace/src/aiguard/client.js +100 -1
  81. package/packages/dd-trace/src/aiguard/errors.js +41 -0
  82. package/packages/dd-trace/src/aiguard/evaluation.js +357 -0
  83. package/packages/dd-trace/src/aiguard/integrations/anthropic.js +70 -0
  84. package/packages/dd-trace/src/aiguard/integrations/index.js +4 -0
  85. package/packages/dd-trace/src/aiguard/messages/anthropic.js +423 -0
  86. package/packages/dd-trace/src/aiguard/messages/openai.js +1 -24
  87. package/packages/dd-trace/src/aiguard/messages/utils.js +28 -0
  88. package/packages/dd-trace/src/aiguard/messages/vercel-ai.js +1 -11
  89. package/packages/dd-trace/src/aiguard/sdk.js +22 -278
  90. package/packages/dd-trace/src/appsec/api_security/normalized-route.js +453 -0
  91. package/packages/dd-trace/src/appsec/handlers/http-request.js +13 -0
  92. package/packages/dd-trace/src/appsec/iast/taint-tracking/rewriter.js +9 -12
  93. package/packages/dd-trace/src/carrier.js +356 -0
  94. package/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/worker/index.js +1 -0
  95. package/packages/dd-trace/src/ci-visibility/exporters/agent-proxy/index.js +27 -3
  96. package/packages/dd-trace/src/ci-visibility/exporters/agentless/coverage-writer.js +22 -6
  97. package/packages/dd-trace/src/ci-visibility/exporters/agentless/di-logs-writer.js +21 -5
  98. package/packages/dd-trace/src/ci-visibility/exporters/agentless/request-tracker.js +164 -0
  99. package/packages/dd-trace/src/ci-visibility/exporters/agentless/writer.js +22 -6
  100. package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +365 -32
  101. package/packages/dd-trace/src/ci-visibility/exporters/request.js +201 -0
  102. package/packages/dd-trace/src/ci-visibility/exporters/test-worker/index.js +5 -3
  103. package/packages/dd-trace/src/ci-visibility/exporters/test-worker/writer.js +9 -5
  104. package/packages/dd-trace/src/ci-visibility/intelligent-test-runner/get-skippable-suites.js +2 -1
  105. package/packages/dd-trace/src/ci-visibility/requests/rate-limit.js +39 -0
  106. package/packages/dd-trace/src/ci-visibility/requests/request.js +3 -6
  107. package/packages/dd-trace/src/ci-visibility/requests/upload-test-screenshot.js +6 -2
  108. package/packages/dd-trace/src/config/generated-config-types.d.ts +7 -0
  109. package/packages/dd-trace/src/config/index.js +13 -2
  110. package/packages/dd-trace/src/config/major-overrides.js +7 -0
  111. package/packages/dd-trace/src/config/supported-configurations.json +24 -0
  112. package/packages/dd-trace/src/datastreams/context.js +6 -1
  113. package/packages/dd-trace/src/datastreams/index.js +2 -0
  114. package/packages/dd-trace/src/datastreams/manager.js +14 -3
  115. package/packages/dd-trace/src/datastreams/pathway.js +23 -22
  116. package/packages/dd-trace/src/datastreams/processor.js +16 -13
  117. package/packages/dd-trace/src/datastreams/size.js +5 -0
  118. package/packages/dd-trace/src/debugger/devtools_client/index.js +3 -2
  119. package/packages/dd-trace/src/exporters/agent/writer.js +49 -9
  120. package/packages/dd-trace/src/exporters/common/buffering-exporter.js +6 -4
  121. package/packages/dd-trace/src/exporters/common/writer.js +11 -6
  122. package/packages/dd-trace/src/llmobs/constants/tags.js +5 -0
  123. package/packages/dd-trace/src/llmobs/experiments/client.js +163 -5
  124. package/packages/dd-trace/src/llmobs/experiments/dataset.js +241 -87
  125. package/packages/dd-trace/src/llmobs/experiments/experiment.js +280 -20
  126. package/packages/dd-trace/src/llmobs/experiments/index.js +58 -59
  127. package/packages/dd-trace/src/llmobs/experiments/noop.js +71 -3
  128. package/packages/dd-trace/src/llmobs/experiments/result.js +1 -0
  129. package/packages/dd-trace/src/llmobs/experiments/util.js +41 -5
  130. package/packages/dd-trace/src/llmobs/index.js +34 -2
  131. package/packages/dd-trace/src/llmobs/plugins/ai/util.js +12 -3
  132. package/packages/dd-trace/src/llmobs/plugins/base.js +4 -2
  133. package/packages/dd-trace/src/llmobs/span_processor.js +16 -0
  134. package/packages/dd-trace/src/llmobs/tagger.js +96 -0
  135. package/packages/dd-trace/src/llmobs/util.js +78 -0
  136. package/packages/dd-trace/src/noop/proxy.js +3 -4
  137. package/packages/dd-trace/src/openfeature/flagging_provider.js +18 -2
  138. package/packages/dd-trace/src/openfeature/remote_config.js +1 -1
  139. package/packages/dd-trace/src/opentelemetry/context_manager.js +7 -7
  140. package/packages/dd-trace/src/opentelemetry/logs/batch_log_processor.js +2 -1
  141. package/packages/dd-trace/src/opentelemetry/logs/logger.js +67 -11
  142. package/packages/dd-trace/src/opentelemetry/logs/otlp_transformer.js +14 -3
  143. package/packages/dd-trace/src/opentelemetry/metrics/index.js +25 -12
  144. package/packages/dd-trace/src/opentelemetry/metrics/otlp_span_stats_exporter.js +2 -5
  145. package/packages/dd-trace/src/opentelemetry/metrics/otlp_span_stats_transformer.js +96 -43
  146. package/packages/dd-trace/src/opentracing/propagation/text_map.js +126 -120
  147. package/packages/dd-trace/src/opentracing/propagation/text_map_dsm.js +4 -8
  148. package/packages/dd-trace/src/opentracing/tracer.js +5 -2
  149. package/packages/dd-trace/src/otel-thread-ctx.js +387 -0
  150. package/packages/dd-trace/src/plugin_manager.js +5 -3
  151. package/packages/dd-trace/src/plugins/ci_plugin.js +6 -1
  152. package/packages/dd-trace/src/plugins/index.js +1 -0
  153. package/packages/{datadog-plugin-jest/src/util.js → dd-trace/src/plugins/util/jest.js} +5 -65
  154. package/packages/dd-trace/src/plugins/util/test.js +93 -14
  155. package/packages/dd-trace/src/plugins/util/web.js +56 -2
  156. package/packages/dd-trace/src/profiling/profiler.js +1 -1
  157. package/packages/dd-trace/src/profiling/profilers/space.js +1 -3
  158. package/packages/dd-trace/src/profiling/profilers/wall.js +37 -90
  159. package/packages/dd-trace/src/profiling/webspan-utils.js +45 -0
  160. package/packages/dd-trace/src/proxy.js +38 -29
  161. package/packages/dd-trace/src/ritm.js +9 -2
  162. package/packages/dd-trace/src/serverless.js +37 -0
  163. package/packages/dd-trace/src/span_format.js +1 -0
  164. package/packages/dd-trace/src/span_stats.js +30 -3
  165. package/packages/dd-trace/src/storage-channels.js +86 -0
  166. package/packages/dd-trace/src/tracer.js +9 -2
  167. package/packages/dd-trace/src/tracer_metadata.js +14 -1
  168. package/packages/dd-trace/src/web-tags-cache.js +132 -0
  169. package/vendor/dist/@datadog/openfeature-node-server/LICENSE +201 -0
  170. package/vendor/dist/@datadog/openfeature-node-server/index.js +1 -0
  171. package/packages/datadog-instrumentations/src/helpers/optional-peer-bundler.js +0 -65
  172. package/packages/datadog-webpack/src/optional-peer-loader.js +0 -17
  173. package/packages/dd-trace/src/feature-registry.js +0 -29
  174. package/packages/dd-trace/src/openfeature/register.js +0 -35
  175. package/packages/dd-trace/src/openfeature/require-provider.js +0 -18
@@ -0,0 +1,70 @@
1
+ 'use strict'
2
+
3
+ const { channel } = require('dc-polyfill')
4
+
5
+ const log = require('../../log')
6
+ const { getMessagesInputMessages, getMessagesOutputMessages } = require('../messages/anthropic')
7
+ const { SOURCE_AUTO } = require('../tags')
8
+ const { pushEvaluation } = require('./evaluate')
9
+
10
+ const messagesBeforeChannel = channel('dd-trace:anthropic:messages:before')
11
+ const messagesAfterChannel = channel('dd-trace:anthropic:messages:after')
12
+
13
+ let isEnabled = false
14
+ let aiguard
15
+ let opts
16
+
17
+ /**
18
+ * Subscribes AI Guard to Anthropic lifecycle channels.
19
+ *
20
+ * @param {object} aiguardInstance
21
+ * @param {boolean} block
22
+ */
23
+ function enable (aiguardInstance, block) {
24
+ if (isEnabled) return
25
+
26
+ aiguard = aiguardInstance
27
+ opts = { block, source: SOURCE_AUTO, integration: 'anthropic' }
28
+
29
+ messagesBeforeChannel.subscribe(onMessagesBefore)
30
+ messagesAfterChannel.subscribe(onMessagesAfter)
31
+
32
+ isEnabled = true
33
+ }
34
+
35
+ function disable () {
36
+ if (!isEnabled) return
37
+
38
+ messagesBeforeChannel.unsubscribe(onMessagesBefore)
39
+ messagesAfterChannel.unsubscribe(onMessagesAfter)
40
+
41
+ aiguard = undefined
42
+ opts = undefined
43
+ isEnabled = false
44
+ }
45
+
46
+ function onMessagesBefore (ctx) {
47
+ pushEvaluation(ctx, aiguard, getMessagesInputMessages(ctx.args?.[0]), opts)
48
+ }
49
+
50
+ function onMessagesAfter (ctx) {
51
+ const inputMessages = getMessagesInputMessages(ctx.args?.[0])
52
+ if (!inputMessages?.length) return
53
+
54
+ let body = ctx.body
55
+ if (typeof body === 'string') {
56
+ try {
57
+ body = JSON.parse(body)
58
+ } catch {
59
+ log.error('AIGuard: unable to decode Anthropic response body')
60
+ return
61
+ }
62
+ }
63
+
64
+ const outputMessages = getMessagesOutputMessages(body)
65
+ if (!outputMessages.length) return
66
+
67
+ pushEvaluation(ctx, aiguard, [...inputMessages, ...outputMessages], opts)
68
+ }
69
+
70
+ module.exports = { enable, disable }
@@ -1,5 +1,6 @@
1
1
  'use strict'
2
2
 
3
+ const anthropic = require('./anthropic')
3
4
  const openai = require('./openai')
4
5
  const vercelAi = require('./vercel-ai')
5
6
 
@@ -14,6 +15,7 @@ let isEnabled = false
14
15
  function enable (aiguard, block) {
15
16
  if (isEnabled) return
16
17
 
18
+ anthropic.enable(aiguard, block)
17
19
  openai.enable(aiguard, block)
18
20
  vercelAi.enable(aiguard, block)
19
21
 
@@ -23,12 +25,14 @@ function enable (aiguard, block) {
23
25
  function disable () {
24
26
  vercelAi.disable()
25
27
  openai.disable()
28
+ anthropic.disable()
26
29
  isEnabled = false
27
30
  }
28
31
 
29
32
  module.exports = {
30
33
  enable,
31
34
  disable,
35
+ anthropic,
32
36
  openai,
33
37
  vercelAi,
34
38
  }
@@ -0,0 +1,423 @@
1
+ 'use strict'
2
+
3
+ const { FILE_FALLBACK, IMAGE_FALLBACK, stringifyOrEmpty } = require('./utils')
4
+ /**
5
+ * Converts an Anthropic image block to an `image_url` content part.
6
+ *
7
+ * @param {object} block
8
+ * @returns {{type: 'image_url', image_url: {url: string}}|undefined}
9
+ */
10
+ function convertAnthropicImageBlock (block) {
11
+ const source = block.source
12
+ if (!source || typeof source !== 'object') return
13
+ if (source.type === 'url' && typeof source.url === 'string') {
14
+ return { type: 'image_url', image_url: { url: source.url } }
15
+ }
16
+ if (source.type === 'base64' && typeof source.data === 'string' && typeof source.media_type === 'string') {
17
+ return { type: 'image_url', image_url: { url: `data:${source.media_type};base64,${source.data}` } }
18
+ }
19
+ }
20
+
21
+ /**
22
+ * Extracts the body content from a document block's source.
23
+ * Inline `text` and `content` sources are normalized to their actual text; URL sources return the
24
+ * URL; base64 / unknown sources return undefined (no readable body).
25
+ *
26
+ * @param {object|undefined} source
27
+ * @returns {string|Array<object>|undefined}
28
+ */
29
+ function extractDocumentSource (source) {
30
+ if (!source) return
31
+ // PlainTextSource stores inline text in `data`, not `text`.
32
+ if (source.type === 'text' && typeof source.data === 'string') return source.data
33
+ if (source.type === 'url' && typeof source.url === 'string') return source.url
34
+ if (source.type === 'content') {
35
+ // ContentBlockSource.content is string | Array<ContentBlockSourceContent>.
36
+ if (typeof source.content === 'string') return source.content
37
+ if (Array.isArray(source.content)) return convertAnthropicBlocksToContent(source.content)
38
+ }
39
+ }
40
+
41
+ /**
42
+ * Combines model-visible metadata strings with an extracted body. When the body has images the
43
+ * metadata becomes leading text parts and an array is returned; otherwise the metadata and a string
44
+ * body are newline-joined. Returns undefined when there is nothing to include.
45
+ *
46
+ * @param {Array<string>} metadata
47
+ * @param {string|Array<object>|undefined} body
48
+ * @returns {string|Array<object>|undefined}
49
+ */
50
+ function combineMetadataWithBody (metadata, body) {
51
+ if (Array.isArray(body)) {
52
+ const parts = metadata.map(text => ({ type: 'text', text }))
53
+ for (const part of body) parts.push(part)
54
+ return parts
55
+ }
56
+
57
+ const lines = [...metadata]
58
+ if (typeof body === 'string') lines.push(body)
59
+ return lines.length ? lines.join('\n') : undefined
60
+ }
61
+
62
+ /**
63
+ * Extracts text from an Anthropic document block. The model-visible `title` and `context` are
64
+ * combined with the source body so prompt-injections placed in document metadata — not just its
65
+ * content — reach AI Guard. base64 / unknown sources fall back to the metadata or [file].
66
+ *
67
+ * @param {object} block
68
+ * @returns {string|Array<object>}
69
+ */
70
+ function convertAnthropicDocumentBlock (block) {
71
+ const metadata = []
72
+ if (typeof block.title === 'string' && block.title) metadata.push(block.title)
73
+ if (typeof block.context === 'string' && block.context) metadata.push(block.context)
74
+
75
+ return combineMetadataWithBody(metadata, extractDocumentSource(block.source)) ?? FILE_FALLBACK
76
+ }
77
+
78
+ /**
79
+ * @param {Array<object>} items
80
+ * @param {string|Array<object>|undefined} content
81
+ */
82
+ function appendContent (items, content) {
83
+ if (Array.isArray(content)) {
84
+ for (const part of content) items.push(part)
85
+ } else if (content !== undefined) {
86
+ items.push({ type: 'text', text: content })
87
+ }
88
+ }
89
+
90
+ /**
91
+ * Walks Anthropic content blocks once and emits normalized content and tool events in source order.
92
+ *
93
+ * `search_result` blocks have their text inside `content: Array<TextBlockParam>`,
94
+ * not a top-level `text` field; they are walked explicitly so RAG-injected text
95
+ * reaches AI Guard for evaluation.
96
+ * `thinking` / `redacted_thinking` are dropped — internal reasoning must not reach
97
+ * AI Guard. Unknown block types fall through to a best-effort `text`-field extraction;
98
+ * purely structural blocks without a `text` field are dropped silently.
99
+ *
100
+ * @param {Array<object>} blocks
101
+ * @returns {Array<object>}
102
+ */
103
+ function walkContentBlocks (blocks) {
104
+ const items = []
105
+ if (!Array.isArray(blocks)) return items
106
+
107
+ for (const block of blocks) {
108
+ if (!block || typeof block !== 'object') continue
109
+ switch (block.type) {
110
+ case 'text':
111
+ if (typeof block.text === 'string') items.push({ type: 'text', text: block.text })
112
+ break
113
+ case 'image': {
114
+ const image = convertAnthropicImageBlock(block)
115
+ if (image) {
116
+ items.push(image)
117
+ } else {
118
+ items.push({ type: 'text', text: IMAGE_FALLBACK })
119
+ }
120
+ break
121
+ }
122
+ case 'document': {
123
+ appendContent(items, convertAnthropicDocumentBlock(block))
124
+ break
125
+ }
126
+ case 'tool_use':
127
+ case 'server_tool_use':
128
+ case 'mcp_tool_use':
129
+ // server_tool_use / mcp_tool_use are the built-in- and MCP-tool counterparts of tool_use.
130
+ items.push({
131
+ kind: 'tool_call',
132
+ value: {
133
+ id: block.id ?? block.name,
134
+ function: {
135
+ name: block.name,
136
+ arguments: stringifyOrEmpty(block.input),
137
+ },
138
+ },
139
+ })
140
+ break
141
+ case 'tool_result':
142
+ items.push({
143
+ kind: 'tool_result',
144
+ value: {
145
+ role: 'tool',
146
+ tool_call_id: block.tool_use_id,
147
+ content: convertAnthropicToolResultContent(block.content),
148
+ },
149
+ })
150
+ break
151
+ case 'search_result': {
152
+ // title and source are required, model-visible strings; include them so a prompt injection
153
+ // placed in the metadata (not only the content) reaches AI Guard for evaluation.
154
+ const metadata = []
155
+ if (typeof block.title === 'string' && block.title) metadata.push(block.title)
156
+ if (typeof block.source === 'string' && block.source) metadata.push(block.source)
157
+ appendContent(items, combineMetadataWithBody(metadata, convertAnthropicBlocksToContent(block.content)))
158
+ break
159
+ }
160
+ case 'web_fetch_tool_result': {
161
+ // Emit as a tool result (like other *_tool_result blocks) so the call -> result -> final-text
162
+ // timeline is preserved and the fetched document isn't merged into the assistant answer.
163
+ const content = block.content
164
+ const resultContent = content?.type === 'web_fetch_result' && content.content
165
+ ? convertAnthropicDocumentBlock(content.content)
166
+ : convertServerToolResultContent(content)
167
+ items.push({
168
+ kind: 'tool_result',
169
+ value: {
170
+ role: 'tool',
171
+ tool_call_id: block.tool_use_id,
172
+ content: resultContent,
173
+ },
174
+ })
175
+ break
176
+ }
177
+ case 'thinking':
178
+ case 'redacted_thinking':
179
+ break
180
+ default:
181
+ if (typeof block.type === 'string' && block.type.endsWith('_tool_result')) {
182
+ items.push({
183
+ kind: 'tool_result',
184
+ value: {
185
+ role: 'tool',
186
+ tool_call_id: block.tool_use_id,
187
+ content: convertServerToolResultContent(block.content),
188
+ },
189
+ })
190
+ } else if (typeof block.text === 'string') {
191
+ items.push({ type: 'text', text: block.text })
192
+ }
193
+ break
194
+ }
195
+ }
196
+ return items
197
+ }
198
+
199
+ /**
200
+ * Reduces walker `parts` to normalized message content: a plain string when
201
+ * only text is present, an array of content parts when images are present,
202
+ * or `undefined` when there is nothing to render.
203
+ *
204
+ * @param {Array<object>} parts
205
+ * @param {boolean} hasImages
206
+ * @returns {string|Array<object>|undefined}
207
+ */
208
+ function partsToContent (parts, hasImages) {
209
+ if (!parts.length) return
210
+ if (hasImages) return parts
211
+ return parts.map(p => p.text).join('\n')
212
+ }
213
+
214
+ /**
215
+ * @param {Array<object>} parts
216
+ * @returns {boolean}
217
+ */
218
+ function hasImageParts (parts) {
219
+ return parts.some(part => part.type === 'image_url')
220
+ }
221
+
222
+ /**
223
+ * Converts Anthropic top-level `system` to a normalized system message.
224
+ *
225
+ * @param {string|Array<object>|undefined} system
226
+ * @returns {{role: 'system', content: string|Array<object>}|undefined}
227
+ */
228
+ function convertAnthropicSystem (system) {
229
+ if (typeof system === 'string') {
230
+ return system.length ? { role: 'system', content: system } : undefined
231
+ }
232
+ const content = convertAnthropicBlocksToContent(system)
233
+ if (content != null) return { role: 'system', content }
234
+ }
235
+
236
+ /**
237
+ * Converts a plain string or array of Anthropic content blocks into normalized message content.
238
+ *
239
+ * @param {string|Array<object>|undefined} blocks
240
+ * @returns {string|Array<object>|undefined}
241
+ */
242
+ function convertAnthropicBlocksToContent (blocks) {
243
+ if (typeof blocks === 'string') return blocks
244
+ const parts = []
245
+ for (const item of walkContentBlocks(blocks)) {
246
+ if (!item.kind) parts.push(item)
247
+ }
248
+ return partsToContent(parts, hasImageParts(parts))
249
+ }
250
+
251
+ /**
252
+ * Converts an Anthropic tool_result block's content into a message content value.
253
+ *
254
+ * @param {string|Array<object>|undefined} content
255
+ * @returns {string|Array<object>}
256
+ */
257
+ function convertAnthropicToolResultContent (content) {
258
+ return convertAnthropicBlocksToContent(content) ?? stringifyOrEmpty(content)
259
+ }
260
+
261
+ /**
262
+ * @param {unknown} content
263
+ * @returns {string}
264
+ */
265
+ function convertServerToolResultContent (content) {
266
+ if (typeof content === 'string') return content || '[tool result]'
267
+ if (!content || typeof content !== 'object') return '[tool result]'
268
+ if (typeof content.error_code === 'string') {
269
+ return content.error_message ? `${content.error_code}: ${content.error_message}` : content.error_code
270
+ }
271
+
272
+ const lines = []
273
+ if (Array.isArray(content)) {
274
+ for (const item of content) {
275
+ if (!item || typeof item !== 'object') continue
276
+ // text blocks (MCP / generic), then web-search title + url.
277
+ if (typeof item.text === 'string') lines.push(item.text)
278
+ if (typeof item.title === 'string') lines.push(item.title)
279
+ if (typeof item.url === 'string') lines.push(item.url)
280
+ }
281
+ } else {
282
+ if (typeof content.stdout === 'string' && content.stdout) lines.push(content.stdout)
283
+ if (typeof content.stderr === 'string' && content.stderr) lines.push(content.stderr)
284
+ if (typeof content.content === 'string' && content.content) lines.push(content.content)
285
+ if (Array.isArray(content.lines)) {
286
+ for (const line of content.lines) {
287
+ if (typeof line === 'string') lines.push(line)
288
+ }
289
+ }
290
+ }
291
+
292
+ return lines.join('\n') || '[tool result]'
293
+ }
294
+
295
+ /**
296
+ * @param {Array<object>} messages
297
+ * @param {string} role
298
+ * @param {Array<object>} parts
299
+ * @param {Array<object>} toolCalls
300
+ */
301
+ function appendPendingMessage (messages, role, parts, toolCalls) {
302
+ const content = partsToContent(parts, hasImageParts(parts))
303
+ if (content == null && !toolCalls.length) return
304
+
305
+ const message = { role }
306
+ if (content != null) message.content = content
307
+ if (toolCalls.length) message.tool_calls = toolCalls
308
+ messages.push(message)
309
+ }
310
+
311
+ /**
312
+ * Converts one turn's worth of content blocks (with no mid_conv_system among them) to normalized
313
+ * messages under a single role.
314
+ *
315
+ * Contiguous tool calls are grouped into one assistant message for parallel calls. Each tool result
316
+ * with pending calls flushes those calls first, preserving sequential server-tool cycles. Standalone
317
+ * user-supplied tool results remain ahead of accompanying user text.
318
+ *
319
+ * @param {string} role
320
+ * @param {Array<object>} blocks
321
+ * @returns {Array<object>}
322
+ */
323
+ function convertContentSegment (role, blocks) {
324
+ const messages = []
325
+ let parts = []
326
+ let toolCalls = []
327
+
328
+ for (const item of walkContentBlocks(blocks)) {
329
+ if (item.kind === 'tool_call') {
330
+ toolCalls.push(item.value)
331
+ } else if (item.kind === 'tool_result') {
332
+ if (toolCalls.length) {
333
+ appendPendingMessage(messages, role, parts, toolCalls)
334
+ parts = []
335
+ toolCalls = []
336
+ }
337
+ messages.push(item.value)
338
+ } else {
339
+ parts.push(item)
340
+ }
341
+ }
342
+
343
+ appendPendingMessage(messages, role, parts, toolCalls)
344
+ return messages
345
+ }
346
+
347
+ /**
348
+ * Converts a single Anthropic message to zero or more normalized messages.
349
+ *
350
+ * `mid_conv_system` blocks carry system-level instructions inserted at a point in the turn; each
351
+ * becomes its own `{ role: 'system' }` message in place, so the content around it keeps its role
352
+ * and chronology instead of the instruction being folded into (and mis-scored as) user/assistant
353
+ * text. See {@link convertContentSegment} for how each surrounding segment is converted.
354
+ *
355
+ * @param {{role: string, content: string|Array<object>}} message
356
+ * @returns {Array<object>}
357
+ */
358
+ function convertAnthropicMessage (message) {
359
+ if (!message || typeof message !== 'object') return []
360
+ const { role, content } = message
361
+
362
+ if (typeof content === 'string') {
363
+ return content.length ? [{ role, content }] : []
364
+ }
365
+ if (!Array.isArray(content)) return []
366
+
367
+ const messages = []
368
+ let segment = []
369
+ for (const block of content) {
370
+ if (block?.type === 'mid_conv_system') {
371
+ for (const converted of convertContentSegment(role, segment)) messages.push(converted)
372
+ segment = []
373
+ const systemContent = convertAnthropicBlocksToContent(block.content)
374
+ if (systemContent != null) messages.push({ role: 'system', content: systemContent })
375
+ } else {
376
+ segment.push(block)
377
+ }
378
+ }
379
+ for (const converted of convertContentSegment(role, segment)) messages.push(converted)
380
+ return messages
381
+ }
382
+
383
+ /**
384
+ * Extracts input messages from an Anthropic `messages.create` call.
385
+ *
386
+ * @param {{system?: string|Array<object>, messages?: Array<object>}|undefined} callArgs
387
+ * @returns {Array<object>|undefined}
388
+ */
389
+ function getMessagesInputMessages (callArgs) {
390
+ const raw = callArgs?.messages
391
+ if (!Array.isArray(raw)) return
392
+
393
+ const result = []
394
+ const system = convertAnthropicSystem(callArgs.system)
395
+ if (system) result.push(system)
396
+
397
+ for (const message of raw) {
398
+ const converted = convertAnthropicMessage(message)
399
+ for (const m of converted) result.push(m)
400
+ }
401
+
402
+ return result.length ? result : undefined
403
+ }
404
+
405
+ /**
406
+ * Extracts output messages from an Anthropic `messages.create` parsed response body.
407
+ *
408
+ * @param {{role?: string, content?: Array<object>}|undefined} body
409
+ * @returns {Array<object>}
410
+ */
411
+ function getMessagesOutputMessages (body) {
412
+ if (!body || typeof body !== 'object') return []
413
+ const role = body.role || 'assistant'
414
+ return convertAnthropicMessage({ role, content: body.content })
415
+ }
416
+
417
+ module.exports = {
418
+ convertAnthropicSystem,
419
+ convertAnthropicBlocksToContent,
420
+ convertAnthropicMessage,
421
+ getMessagesInputMessages,
422
+ getMessagesOutputMessages,
423
+ }
@@ -1,7 +1,6 @@
1
1
  'use strict'
2
2
 
3
- const FILE_FALLBACK = '[file]'
4
- const IMAGE_FALLBACK = '[image]'
3
+ const { FILE_FALLBACK, IMAGE_FALLBACK, stringifyOrEmpty } = require('./utils')
5
4
 
6
5
  const OPENAI_RESPONSE_TOOL_CALL_TYPES = new Set([
7
6
  'apply_patch_call',
@@ -26,28 +25,6 @@ const OPENAI_RESPONSE_TOOL_OUTPUT_TYPES = new Set([
26
25
  'shell_call_output',
27
26
  ])
28
27
 
29
- /**
30
- * Returns the value as a string, JSON-stringifying it when it is not already a string.
31
- * Returns the value unchanged when it is `null` or `undefined`.
32
- *
33
- * @param {unknown} value
34
- * @returns {string|undefined|null}
35
- */
36
- function stringifyIfNeeded (value) {
37
- if (value == null) return value
38
- return typeof value === 'string' ? value : JSON.stringify(value)
39
- }
40
-
41
- /**
42
- * Returns a stringified value, falling back to an empty string for absent values.
43
- *
44
- * @param {unknown} value
45
- * @returns {string}
46
- */
47
- function stringifyOrEmpty (value) {
48
- return stringifyIfNeeded(value) ?? ''
49
- }
50
-
51
28
  /**
52
29
  * Converts OpenAI chat-completions messages to the message format expected by AI Guard.
53
30
  *
@@ -0,0 +1,28 @@
1
+ 'use strict'
2
+
3
+ const FILE_FALLBACK = '[file]'
4
+ const IMAGE_FALLBACK = '[image]'
5
+
6
+ /**
7
+ * @param {unknown} value
8
+ * @returns {string|undefined|null}
9
+ */
10
+ function stringifyIfNeeded (value) {
11
+ if (value == null) return value
12
+ return typeof value === 'string' ? value : JSON.stringify(value)
13
+ }
14
+
15
+ /**
16
+ * @param {unknown} value
17
+ * @returns {string}
18
+ */
19
+ function stringifyOrEmpty (value) {
20
+ return stringifyIfNeeded(value) ?? ''
21
+ }
22
+
23
+ module.exports = {
24
+ FILE_FALLBACK,
25
+ IMAGE_FALLBACK,
26
+ stringifyIfNeeded,
27
+ stringifyOrEmpty,
28
+ }
@@ -1,16 +1,6 @@
1
1
  'use strict'
2
2
 
3
- /**
4
- * Returns the value as a string, JSON-stringifying it when it is not already a string.
5
- * Returns the value unchanged when it is `null` or `undefined`.
6
- *
7
- * @param {unknown} value
8
- * @returns {string|undefined|null}
9
- */
10
- function stringifyIfNeeded (value) {
11
- if (value == null) return value
12
- return typeof value === 'string' ? value : JSON.stringify(value)
13
- }
3
+ const { stringifyIfNeeded } = require('./utils')
14
4
 
15
5
  /**
16
6
  * Converts a LanguageModelV2FilePart with an image mediaType to an AI Guard style image_url content part.