dd-trace 6.16.0 → 6.17.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 (334) hide show
  1. package/ci/diagnose.js +0 -23
  2. package/ci/test-optimization-validation/approval-artifacts.js +0 -3
  3. package/ci/test-optimization-validation/approval.js +0 -6
  4. package/ci/test-optimization-validation/artifact-id.js +0 -1
  5. package/ci/test-optimization-validation/bounded-json.js +0 -1
  6. package/ci/test-optimization-validation/cli.js +0 -12
  7. package/ci/test-optimization-validation/command-output-policy.js +0 -1
  8. package/ci/test-optimization-validation/command-runner.js +0 -3
  9. package/ci/test-optimization-validation/environment.js +0 -4
  10. package/ci/test-optimization-validation/executable-approval.js +0 -1
  11. package/ci/test-optimization-validation/executable.js +0 -5
  12. package/ci/test-optimization-validation/framework-adapters/cucumber.js +0 -6
  13. package/ci/test-optimization-validation/framework-adapters/cypress.js +0 -3
  14. package/ci/test-optimization-validation/framework-adapters/playwright.js +0 -6
  15. package/ci/test-optimization-validation/framework-adapters/vitest.js +0 -4
  16. package/ci/test-optimization-validation/generated-test-contract.js +0 -5
  17. package/ci/test-optimization-validation/generated-verifier.js +0 -1
  18. package/ci/test-optimization-validation/literal-environment.js +0 -1
  19. package/ci/test-optimization-validation/manifest-scaffold.js +0 -17
  20. package/ci/test-optimization-validation/manifest-schema.js +0 -21
  21. package/ci/test-optimization-validation/offline-fixtures.js +0 -4
  22. package/ci/test-optimization-validation/plan-writer.js +0 -11
  23. package/ci/test-optimization-validation/preflight-runner.js +0 -1
  24. package/ci/test-optimization-validation/redaction.js +0 -4
  25. package/ci/test-optimization-validation/report-writer.js +0 -16
  26. package/ci/test-optimization-validation/runner-command.js +0 -3
  27. package/ci/test-optimization-validation/runner-contract.js +0 -4
  28. package/ci/test-optimization-validation/safe-files.js +0 -1
  29. package/ci/test-optimization-validation/scenarios/basic-reporting.js +0 -1
  30. package/ci/test-optimization-validation/scenarios/ci-wiring.js +0 -1
  31. package/ci/test-optimization-validation/source-text.js +0 -3
  32. package/ci/vitest-no-worker-init-setup.mjs +41 -16
  33. package/index.d.ts +165 -0
  34. package/package.json +17 -16
  35. package/packages/datadog-esbuild/src/utils.js +0 -2
  36. package/packages/datadog-instrumentations/src/aerospike.js +28 -6
  37. package/packages/datadog-instrumentations/src/aws-durable-execution-sdk-js.js +0 -2
  38. package/packages/datadog-instrumentations/src/aws-sdk.js +0 -2
  39. package/packages/datadog-instrumentations/src/claude-agent-sdk.js +0 -2
  40. package/packages/datadog-instrumentations/src/console.js +97 -0
  41. package/packages/datadog-instrumentations/src/cucumber.js +72 -12
  42. package/packages/datadog-instrumentations/src/cypress-config.js +0 -23
  43. package/packages/datadog-instrumentations/src/helpers/channel.js +0 -2
  44. package/packages/datadog-instrumentations/src/helpers/graphql-jit-runtime.js +0 -5
  45. package/packages/datadog-instrumentations/src/helpers/hooks.js +8 -0
  46. package/packages/datadog-instrumentations/src/helpers/instrumentation-utils.js +0 -3
  47. package/packages/datadog-instrumentations/src/helpers/pool-acquire.js +0 -6
  48. package/packages/datadog-instrumentations/src/helpers/register.js +4 -0
  49. package/packages/datadog-instrumentations/src/helpers/rewriter/index.js +72 -24
  50. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentation-registry.js +39 -0
  51. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/index.js +3 -17
  52. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/playwright.js +12 -0
  53. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/postgres.js +36 -0
  54. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/supabase.js +128 -0
  55. package/packages/datadog-instrumentations/src/helpers/rewriter/targets.json +16 -0
  56. package/packages/datadog-instrumentations/src/helpers/rewriter/transforms/postgres.js +388 -0
  57. package/packages/datadog-instrumentations/src/helpers/rewriter/transforms.js +0 -5
  58. package/packages/datadog-instrumentations/src/helpers/router-helper.js +0 -1
  59. package/packages/datadog-instrumentations/src/jest.js +141 -51
  60. package/packages/datadog-instrumentations/src/knex.js +0 -1
  61. package/packages/datadog-instrumentations/src/mariadb-bundle.js +0 -24
  62. package/packages/datadog-instrumentations/src/mariadb.js +0 -2
  63. package/packages/datadog-instrumentations/src/mocha/common.js +0 -1
  64. package/packages/datadog-instrumentations/src/mocha/main.js +22 -25
  65. package/packages/datadog-instrumentations/src/mocha/utils.js +119 -49
  66. package/packages/datadog-instrumentations/src/mocha/webdriverio-protocol.js +0 -6
  67. package/packages/datadog-instrumentations/src/mocha/worker.js +14 -17
  68. package/packages/datadog-instrumentations/src/mysql2.js +0 -2
  69. package/packages/datadog-instrumentations/src/openai-realtime/audio-accumulator.js +241 -0
  70. package/packages/datadog-instrumentations/src/openai-realtime/events.js +76 -0
  71. package/packages/datadog-instrumentations/src/openai-realtime/index.js +301 -0
  72. package/packages/datadog-instrumentations/src/openai-realtime/session.js +998 -0
  73. package/packages/datadog-instrumentations/src/openai-realtime/turns.js +211 -0
  74. package/packages/datadog-instrumentations/src/openai.js +51 -12
  75. package/packages/datadog-instrumentations/src/oracledb.js +9 -3
  76. package/packages/datadog-instrumentations/src/playwright-reporter.js +3 -16
  77. package/packages/datadog-instrumentations/src/playwright.js +238 -55
  78. package/packages/datadog-instrumentations/src/postgres.js +7 -0
  79. package/packages/datadog-instrumentations/src/router.js +0 -2
  80. package/packages/datadog-instrumentations/src/rum-browser-scripts.js +0 -2
  81. package/packages/datadog-instrumentations/src/sequelize.js +0 -1
  82. package/packages/datadog-instrumentations/src/supabase.js +15 -0
  83. package/packages/datadog-instrumentations/src/vitest-main-no-worker-init.js +18 -18
  84. package/packages/datadog-instrumentations/src/vitest-main.js +37 -17
  85. package/packages/datadog-instrumentations/src/vitest-util.js +7 -1
  86. package/packages/datadog-instrumentations/src/vitest-worker.js +49 -13
  87. package/packages/datadog-instrumentations/src/webdriverio.js +23 -42
  88. package/packages/datadog-instrumentations/src/ws.js +0 -1
  89. package/packages/datadog-plugin-ai/src/utils.js +0 -2
  90. package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/trace-checkpoint.js +0 -1
  91. package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/util.js +0 -2
  92. package/packages/datadog-plugin-aws-sdk/src/base.js +0 -1
  93. package/packages/datadog-plugin-aws-sdk/src/services/bedrockruntime/utils.js +0 -1
  94. package/packages/datadog-plugin-aws-sdk/src/services/eventbridge.js +0 -1
  95. package/packages/datadog-plugin-aws-sdk/src/util.js +0 -1
  96. package/packages/datadog-plugin-azure-durable-functions/src/index.js +0 -1
  97. package/packages/datadog-plugin-cucumber/src/index.js +10 -0
  98. package/packages/datadog-plugin-cypress/src/cypress-plugin.js +171 -75
  99. package/packages/datadog-plugin-cypress/src/finalization.js +0 -3
  100. package/packages/datadog-plugin-cypress/src/source-map-utils.js +0 -4
  101. package/packages/datadog-plugin-cypress/src/support.js +52 -7
  102. package/packages/datadog-plugin-graphql/src/resolve-error.js +1 -2
  103. package/packages/datadog-plugin-graphql/src/utils.js +0 -5
  104. package/packages/datadog-plugin-jest/src/index.js +13 -0
  105. package/packages/datadog-plugin-kafkajs/src/producer.js +0 -1
  106. package/packages/datadog-plugin-mocha/src/index.js +39 -11
  107. package/packages/datadog-plugin-mongodb-core/src/query.js +0 -1
  108. package/packages/datadog-plugin-openai/src/index.js +16 -1
  109. package/packages/datadog-plugin-openai/src/realtime.js +144 -0
  110. package/packages/datadog-plugin-openai-agents/src/integration.js +0 -4
  111. package/packages/datadog-plugin-openai-agents/src/util.js +0 -1
  112. package/packages/datadog-plugin-playwright/src/index.js +76 -9
  113. package/packages/datadog-plugin-postgres/src/index.js +167 -0
  114. package/packages/datadog-plugin-prisma/src/datadog-tracing-helper.js +0 -1
  115. package/packages/datadog-plugin-redis/src/index.js +15 -1
  116. package/packages/datadog-plugin-supabase/src/error.js +18 -0
  117. package/packages/datadog-plugin-supabase/src/index.js +21 -0
  118. package/packages/datadog-plugin-supabase/src/supabase-auth-js-gotrueclient-getuser.js +69 -0
  119. package/packages/datadog-plugin-supabase/src/supabase-functions-js-functionsclient-invoke.js +70 -0
  120. package/packages/datadog-plugin-supabase/src/supabase-postgrest-js-postgrestbuilder-then.js +122 -0
  121. package/packages/datadog-plugin-supabase/src/supabase-realtime-js-realtimechannel-send.js +66 -0
  122. package/packages/datadog-plugin-supabase/src/supabase-storage-js-handle-request.js +93 -0
  123. package/packages/datadog-plugin-supabase/src/url.js +17 -0
  124. package/packages/datadog-plugin-vitest/src/index.js +10 -0
  125. package/packages/datadog-plugin-ws/src/util.js +0 -4
  126. package/packages/datadog-turbopack/index.js +0 -6
  127. package/packages/datadog-turbopack/src/loader.js +0 -16
  128. package/packages/datadog-webpack/src/loader.js +0 -1
  129. package/packages/dd-trace/src/aiguard/client.js +2 -3
  130. package/packages/dd-trace/src/aiguard/evaluation.js +6 -12
  131. package/packages/dd-trace/src/aiguard/index.js +2 -2
  132. package/packages/dd-trace/src/aiguard/integrations/index.js +1 -1
  133. package/packages/dd-trace/src/aiguard/integrations/openai.js +81 -7
  134. package/packages/dd-trace/src/aiguard/messages/anthropic.js +0 -2
  135. package/packages/dd-trace/src/aiguard/messages/openai.js +64 -1
  136. package/packages/dd-trace/src/aiguard/messages/utils.js +0 -1
  137. package/packages/dd-trace/src/aiguard/redaction.js +0 -1
  138. package/packages/dd-trace/src/aiguard/sdk.js +1 -1
  139. package/packages/dd-trace/src/appsec/activation.js +1 -1
  140. package/packages/dd-trace/src/appsec/api_security/normalized-route.js +0 -5
  141. package/packages/dd-trace/src/appsec/api_security/sampler.js +0 -4
  142. package/packages/dd-trace/src/appsec/blocking/index.js +3 -3
  143. package/packages/dd-trace/src/appsec/downstream_requests.js +1 -4
  144. package/packages/dd-trace/src/appsec/iast/index.js +1 -1
  145. package/packages/dd-trace/src/appsec/iast/overhead-controller.js +5 -5
  146. package/packages/dd-trace/src/appsec/iast/taint-tracking/index.js +1 -1
  147. package/packages/dd-trace/src/appsec/iast/taint-tracking/plugin.js +1 -1
  148. package/packages/dd-trace/src/appsec/iast/taint-tracking/rewriter.js +3 -3
  149. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-analyzers/sql-sensitive-analyzer.js +8 -10
  150. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-handler.js +2 -2
  151. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/utils.js +4 -1
  152. package/packages/dd-trace/src/appsec/iast/vulnerability-reporter.js +7 -7
  153. package/packages/dd-trace/src/appsec/index.js +2 -2
  154. package/packages/dd-trace/src/appsec/lambda.js +71 -2
  155. package/packages/dd-trace/src/appsec/rasp/utils.js +5 -1
  156. package/packages/dd-trace/src/appsec/remote_config.js +8 -6
  157. package/packages/dd-trace/src/appsec/reporter.js +10 -6
  158. package/packages/dd-trace/src/appsec/rule_manager.js +3 -3
  159. package/packages/dd-trace/src/appsec/sdk/track_event.js +0 -2
  160. package/packages/dd-trace/src/appsec/waf/index.js +2 -2
  161. package/packages/dd-trace/src/appsec/waf/waf_manager.js +8 -5
  162. package/packages/dd-trace/src/carrier.js +0 -5
  163. package/packages/dd-trace/src/ci-visibility/dynamic-atr-retries.js +104 -0
  164. package/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/worker/index.js +0 -2
  165. package/packages/dd-trace/src/ci-visibility/efd-retry-policy.js +34 -3
  166. package/packages/dd-trace/src/ci-visibility/exporters/agentless/coverage-writer.js +0 -1
  167. package/packages/dd-trace/src/ci-visibility/exporters/agentless/di-logs-writer.js +0 -1
  168. package/packages/dd-trace/src/ci-visibility/exporters/agentless/writer.js +0 -1
  169. package/packages/dd-trace/src/ci-visibility/exporters/ci-validation/index.js +0 -4
  170. package/packages/dd-trace/src/ci-visibility/exporters/ci-validation/msgpack-to-json.js +0 -1
  171. package/packages/dd-trace/src/ci-visibility/exporters/ci-validation/sink.js +0 -2
  172. package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +37 -15
  173. package/packages/dd-trace/src/ci-visibility/exporters/git/git_metadata.js +0 -3
  174. package/packages/dd-trace/src/ci-visibility/exporters/request.js +0 -11
  175. package/packages/dd-trace/src/ci-visibility/exporters/settings-cache-key.js +0 -1
  176. package/packages/dd-trace/src/ci-visibility/intelligent-test-runner/get-skippable-suites.js +0 -1
  177. package/packages/dd-trace/src/ci-visibility/requests/fs-cache.js +0 -6
  178. package/packages/dd-trace/src/ci-visibility/requests/get-library-configuration.js +2 -2
  179. package/packages/dd-trace/src/ci-visibility/requests/rate-limit.js +0 -1
  180. package/packages/dd-trace/src/ci-visibility/requests/upload-test-screenshot.js +0 -5
  181. package/packages/dd-trace/src/ci-visibility/requests/video-request.js +0 -1
  182. package/packages/dd-trace/src/ci-visibility/telemetry.js +13 -0
  183. package/packages/dd-trace/src/ci-visibility/test-optimization-cache.js +0 -1
  184. package/packages/dd-trace/src/ci-visibility/test-screenshot.js +0 -2
  185. package/packages/dd-trace/src/ci-visibility/test-video.js +0 -1
  186. package/packages/dd-trace/src/config/defaults.js +3 -2
  187. package/packages/dd-trace/src/config/generated-config-types.d.ts +73 -67
  188. package/packages/dd-trace/src/config/helper.js +0 -1
  189. package/packages/dd-trace/src/config/index.js +24 -24
  190. package/packages/dd-trace/src/config/parsers.js +34 -6
  191. package/packages/dd-trace/src/config/remote_config.js +25 -109
  192. package/packages/dd-trace/src/config/supported-configurations.json +139 -10
  193. package/packages/dd-trace/src/datastreams/encoding.js +0 -1
  194. package/packages/dd-trace/src/datastreams/pathway.js +0 -1
  195. package/packages/dd-trace/src/debugger/constants.js +10 -0
  196. package/packages/dd-trace/src/debugger/devtools_client/breakpoints.js +62 -10
  197. package/packages/dd-trace/src/debugger/devtools_client/config.js +2 -0
  198. package/packages/dd-trace/src/debugger/devtools_client/guardrail-metrics.js +15 -0
  199. package/packages/dd-trace/src/debugger/devtools_client/index.js +103 -27
  200. package/packages/dd-trace/src/debugger/devtools_client/json-buffer.js +50 -7
  201. package/packages/dd-trace/src/debugger/devtools_client/log.js +3 -1
  202. package/packages/dd-trace/src/debugger/devtools_client/probe_sampler.js +21 -8
  203. package/packages/dd-trace/src/debugger/devtools_client/send.js +40 -14
  204. package/packages/dd-trace/src/debugger/devtools_client/snapshot/index.js +53 -13
  205. package/packages/dd-trace/src/debugger/devtools_client/snapshot/processor.js +118 -71
  206. package/packages/dd-trace/src/debugger/devtools_client/snapshot-pruner.js +0 -3
  207. package/packages/dd-trace/src/debugger/devtools_client/status.js +20 -6
  208. package/packages/dd-trace/src/debugger/guardrail-metrics.js +167 -0
  209. package/packages/dd-trace/src/debugger/index.js +50 -2
  210. package/packages/dd-trace/src/debugger/inspect-segment.js +0 -3
  211. package/packages/dd-trace/src/debugger/probe_sampler.js +133 -9
  212. package/packages/dd-trace/src/debugger/probe_sampler_constants.js +31 -1
  213. package/packages/dd-trace/src/dogstatsd.js +0 -23
  214. package/packages/dd-trace/src/encode/0.4-cross-payload.js +0 -3
  215. package/packages/dd-trace/src/encode/0.4.js +0 -7
  216. package/packages/dd-trace/src/encode/0.5.js +0 -2
  217. package/packages/dd-trace/src/encode/tags-processors.js +0 -1
  218. package/packages/dd-trace/src/evp_proxy/discovery.js +0 -1
  219. package/packages/dd-trace/src/evp_proxy/path.js +0 -2
  220. package/packages/dd-trace/src/exporters/agent/writer.js +0 -1
  221. package/packages/dd-trace/src/exporters/agentless/index.js +0 -1
  222. package/packages/dd-trace/src/exporters/agentless/intake.js +0 -1
  223. package/packages/dd-trace/src/exporters/agentless/writer.js +0 -3
  224. package/packages/dd-trace/src/exporters/common/final-flush-request-tracker.js +0 -6
  225. package/packages/dd-trace/src/exporters/common/request.js +1 -0
  226. package/packages/dd-trace/src/exporters/common/url.js +0 -2
  227. package/packages/dd-trace/src/exporters/common/writer.js +0 -2
  228. package/packages/dd-trace/src/id.js +0 -11
  229. package/packages/dd-trace/src/llmobs/audio-codec.js +192 -0
  230. package/packages/dd-trace/src/llmobs/audio-utils.js +121 -0
  231. package/packages/dd-trace/src/llmobs/constants/audio.js +35 -0
  232. package/packages/dd-trace/src/llmobs/constants/prompts.js +14 -0
  233. package/packages/dd-trace/src/llmobs/constants/tags.js +17 -0
  234. package/packages/dd-trace/src/llmobs/eval-metric.js +0 -6
  235. package/packages/dd-trace/src/llmobs/experiments/dataset.js +0 -4
  236. package/packages/dd-trace/src/llmobs/experiments/experiment.js +0 -3
  237. package/packages/dd-trace/src/llmobs/experiments/index.js +7 -6
  238. package/packages/dd-trace/src/llmobs/experiments/util.js +1 -11
  239. package/packages/dd-trace/src/llmobs/index.js +1 -3
  240. package/packages/dd-trace/src/llmobs/noop.js +10 -0
  241. package/packages/dd-trace/src/llmobs/plugins/ai/util.js +0 -4
  242. package/packages/dd-trace/src/llmobs/plugins/anthropic/util.js +0 -1
  243. package/packages/dd-trace/src/llmobs/plugins/genai/util.js +0 -4
  244. package/packages/dd-trace/src/llmobs/plugins/modelcontextprotocol-sdk/utils.js +0 -2
  245. package/packages/dd-trace/src/llmobs/plugins/openai/constants.js +0 -2
  246. package/packages/dd-trace/src/llmobs/plugins/openai/index.js +4 -6
  247. package/packages/dd-trace/src/llmobs/plugins/openai/realtime.js +285 -0
  248. package/packages/dd-trace/src/llmobs/plugins/openai/utils.js +16 -3
  249. package/packages/dd-trace/src/llmobs/prompts/cache.js +272 -0
  250. package/packages/dd-trace/src/llmobs/prompts/manager.js +639 -0
  251. package/packages/dd-trace/src/llmobs/prompts/noop.js +29 -0
  252. package/packages/dd-trace/src/llmobs/prompts/prompt.js +92 -0
  253. package/packages/dd-trace/src/llmobs/sdk.js +21 -6
  254. package/packages/dd-trace/src/llmobs/span_processor.js +81 -2
  255. package/packages/dd-trace/src/llmobs/tagger.js +30 -7
  256. package/packages/dd-trace/src/llmobs/telemetry.js +36 -4
  257. package/packages/dd-trace/src/llmobs/util.js +0 -34
  258. package/packages/dd-trace/src/llmobs/writers/base.js +0 -1
  259. package/packages/dd-trace/src/llmobs/writers/util.js +1 -2
  260. package/packages/dd-trace/src/log/channels.js +3 -0
  261. package/packages/dd-trace/src/log/index.js +23 -5
  262. package/packages/dd-trace/src/log-submission/log-submission-plugin.js +74 -6
  263. package/packages/dd-trace/src/msgpack/chunk.js +0 -1
  264. package/packages/dd-trace/src/openfeature/agentless_configuration_source.js +0 -12
  265. package/packages/dd-trace/src/openfeature/encoding.js +0 -2
  266. package/packages/dd-trace/src/openfeature/eval-metrics-hook.js +0 -1
  267. package/packages/dd-trace/src/openfeature/index.js +0 -4
  268. package/packages/dd-trace/src/openfeature/span-enrichment-hook.js +0 -1
  269. package/packages/dd-trace/src/openfeature/span-enrichment.js +0 -4
  270. package/packages/dd-trace/src/openfeature/writers/base.js +0 -6
  271. package/packages/dd-trace/src/openfeature/writers/exposures.js +0 -5
  272. package/packages/dd-trace/src/openfeature/writers/util.js +0 -4
  273. package/packages/dd-trace/src/opentelemetry/logs/logger.js +0 -1
  274. package/packages/dd-trace/src/opentelemetry/logs/logger_provider.js +0 -1
  275. package/packages/dd-trace/src/opentelemetry/logs/otlp_http_log_exporter.js +0 -1
  276. package/packages/dd-trace/src/opentelemetry/logs/otlp_transformer.js +0 -2
  277. package/packages/dd-trace/src/opentelemetry/metrics/meter_provider.js +0 -1
  278. package/packages/dd-trace/src/opentelemetry/metrics/otlp_span_stats_transformer.js +0 -1
  279. package/packages/dd-trace/src/opentelemetry/metrics/otlp_transformer.js +0 -1
  280. package/packages/dd-trace/src/opentelemetry/metrics/periodic_metric_reader.js +1 -14
  281. package/packages/dd-trace/src/opentelemetry/metrics/time.js +0 -1
  282. package/packages/dd-trace/src/opentelemetry/otlp/otlp_http_exporter_base.js +30 -13
  283. package/packages/dd-trace/src/opentelemetry/otlp/otlp_transformer_base.js +0 -1
  284. package/packages/dd-trace/src/opentelemetry/span-helpers.js +0 -1
  285. package/packages/dd-trace/src/opentelemetry/trace/otlp_http_trace_exporter.js +0 -1
  286. package/packages/dd-trace/src/opentelemetry/trace/otlp_transformer.js +0 -3
  287. package/packages/dd-trace/src/opentracing/propagation/text_map.js +0 -6
  288. package/packages/dd-trace/src/opentracing/span_context.js +0 -1
  289. package/packages/dd-trace/src/otel-thread-ctx.js +38 -11
  290. package/packages/dd-trace/src/payload-tagging/tagging.js +15 -3
  291. package/packages/dd-trace/src/plugins/ci_plugin.js +0 -1
  292. package/packages/dd-trace/src/plugins/database.js +2 -6
  293. package/packages/dd-trace/src/plugins/index.js +7 -0
  294. package/packages/dd-trace/src/plugins/plugin.js +0 -4
  295. package/packages/dd-trace/src/plugins/tracing.js +1 -1
  296. package/packages/dd-trace/src/plugins/util/git.js +1 -3
  297. package/packages/dd-trace/src/plugins/util/http-otel-semantics.js +0 -1
  298. package/packages/dd-trace/src/plugins/util/llm.js +0 -2
  299. package/packages/dd-trace/src/plugins/util/status-validator.js +0 -1
  300. package/packages/dd-trace/src/plugins/util/test.js +21 -20
  301. package/packages/dd-trace/src/plugins/util/url.js +0 -5
  302. package/packages/dd-trace/src/priority_sampler.js +0 -6
  303. package/packages/dd-trace/src/process-tags/index.js +0 -1
  304. package/packages/dd-trace/src/profiler.js +132 -35
  305. package/packages/dd-trace/src/profiling/exporters/event_serializer.js +0 -1
  306. package/packages/dd-trace/src/profiling/profiler.js +101 -41
  307. package/packages/dd-trace/src/profiling/ssi-heuristics.js +29 -15
  308. package/packages/dd-trace/src/propagation-hash/index.js +0 -1
  309. package/packages/dd-trace/src/proxy.js +21 -45
  310. package/packages/dd-trace/src/random_sampler.js +0 -4
  311. package/packages/dd-trace/src/rate_limiter.js +0 -4
  312. package/packages/dd-trace/src/remote_config/capabilities.js +1 -9
  313. package/packages/dd-trace/src/remote_config/index.js +0 -1
  314. package/packages/dd-trace/src/runtime_metrics/otlp_runtime_metrics.js +0 -3
  315. package/packages/dd-trace/src/sampler.js +0 -4
  316. package/packages/dd-trace/src/sampling_rule.js +0 -7
  317. package/packages/dd-trace/src/serverless/vercel.js +0 -1
  318. package/packages/dd-trace/src/serverless.js +0 -1
  319. package/packages/dd-trace/src/service-naming/index.js +0 -1
  320. package/packages/dd-trace/src/service-naming/schemas/util.js +0 -1
  321. package/packages/dd-trace/src/service-naming/schemas/v0/storage.js +7 -0
  322. package/packages/dd-trace/src/service-naming/schemas/v1/storage.js +5 -0
  323. package/packages/dd-trace/src/span_processor.js +2 -2
  324. package/packages/dd-trace/src/span_sampler.js +0 -1
  325. package/packages/dd-trace/src/standalone/product.js +2 -2
  326. package/packages/dd-trace/src/standalone/tracesource.js +0 -1
  327. package/packages/dd-trace/src/startup-log.js +1 -1
  328. package/packages/dd-trace/src/telemetry/endpoints.js +0 -8
  329. package/packages/dd-trace/src/telemetry/logs/index.js +16 -5
  330. package/packages/dd-trace/src/telemetry/logs/log-collector.js +16 -6
  331. package/packages/dd-trace/src/telemetry/metrics.js +0 -2
  332. package/packages/dd-trace/src/telemetry/telemetry.js +4 -1
  333. package/packages/dd-trace/src/util.js +13 -1
  334. package/vendor/dist/@datadog/openfeature-node-server/index.js +1 -1
@@ -0,0 +1,285 @@
1
+ 'use strict'
2
+
3
+ const {
4
+ g711ToPcm16,
5
+ g711Variant,
6
+ isPcm16AudioMime,
7
+ pcm16ToWav,
8
+ } = require('../../audio-codec')
9
+ const { fitsInlineAudioBudget, formatAudioPartWithGuard } = require('../../audio-utils')
10
+ const {
11
+ AUDIO_FALLBACK,
12
+ G711_SAMPLE_RATE,
13
+ LLMOBS_AUDIO_INLINE_MAX_BYTES,
14
+ PCM16_BYTES_PER_SAMPLE,
15
+ WAV_HEADER_BYTES,
16
+ } = require('../../constants/audio')
17
+ const { storage: llmobsStorage } = require('../../storage')
18
+ const { safeJsonParse } = require('../../util')
19
+ const LLMObsPlugin = require('../base')
20
+ const { getModelProviderAndClient } = require('./utils')
21
+
22
+ /**
23
+ * @typedef {import('../../audio-utils').AudioPart} AudioPart
24
+ * @typedef {{
25
+ * role: string,
26
+ * content: string,
27
+ * audioParts?: AudioPart[],
28
+ * toolCalls?: object[],
29
+ * toolResults?: object[],
30
+ * }} Message
31
+ */
32
+
33
+ /**
34
+ * Turn a turn side's raw audio into a playable audio part.
35
+ *
36
+ * Realtime audio is raw PCM16 (24 kHz mono) by default, which the UI can't render, so it is wrapped
37
+ * in a WAV container — lossless, just a header. G.711 telephony audio, used by phone-call
38
+ * integrations, is decoded to PCM16 and likewise WAV-wrapped at its fixed 8 kHz rate. Anything else
39
+ * is passed to the size guard as-is, which keeps it only if it is renderable and within budget.
40
+ *
41
+ * Both conversions expand what they are given — G.711 doubles it, and either way a WAV header is
42
+ * prepended — so the budget is checked against the size the conversion *would* produce before
43
+ * running it. Checking afterwards, as the guard alone does, means decoding and copying megabytes
44
+ * only to discard them: the retention cap admits roughly twice what a G.711 turn can ever inline.
45
+ *
46
+ * @param {Buffer} audio
47
+ * @param {string} mimeType
48
+ * @param {number} sampleRate
49
+ * @param {number} maxBytes - Encoded bytes still available to this turn. See `setLLMObsTags`.
50
+ * @returns {AudioPart | undefined}
51
+ */
52
+ function buildAudioPart (audio, mimeType, sampleRate, maxBytes) {
53
+ if (!audio.length) return
54
+
55
+ if (isPcm16AudioMime(mimeType)) {
56
+ if (!fitsInlineAudioBudget(audio.length + WAV_HEADER_BYTES, maxBytes)) return
57
+ return formatAudioPartWithGuard(pcm16ToWav(audio, sampleRate), 'audio/wav', maxBytes)
58
+ }
59
+
60
+ const variant = g711Variant(mimeType)
61
+ if (variant !== undefined) {
62
+ if (!fitsInlineAudioBudget(audio.length * PCM16_BYTES_PER_SAMPLE + WAV_HEADER_BYTES, maxBytes)) return
63
+ return formatAudioPartWithGuard(
64
+ pcm16ToWav(g711ToPcm16(audio, variant), G711_SAMPLE_RATE), 'audio/wav', maxBytes
65
+ )
66
+ }
67
+
68
+ return formatAudioPartWithGuard(audio, mimeType, maxBytes)
69
+ }
70
+
71
+ /**
72
+ * Encoded size an already-built message contributes to the span event, which for audio is the
73
+ * base64 content itself.
74
+ *
75
+ * @param {Message | undefined} message
76
+ */
77
+ function audioBytesOf (message) {
78
+ return message?.audioParts?.[0]?.content?.length ?? 0
79
+ }
80
+
81
+ /**
82
+ * @param {object[]} toolCalls
83
+ * @returns {object[]}
84
+ */
85
+ function parseToolCallArguments (toolCalls) {
86
+ return toolCalls.map(toolCall => ({ ...toolCall, arguments: safeJsonParse(toolCall.arguments ?? '', {}) }))
87
+ }
88
+
89
+ /**
90
+ * Build the message for one side of a turn.
91
+ *
92
+ * @param {string} role
93
+ * @param {import('../../../../../datadog-instrumentations/src/openai-realtime/session').TurnSide} side
94
+ * @param {number} maxAudioBytes - Encoded audio bytes still available to this turn.
95
+ * @returns {Message | undefined}
96
+ */
97
+ function buildMessage (role, side, maxAudioBytes) {
98
+ const audioPart = buildAudioPart(side.audio, side.mimeType, side.sampleRate, maxAudioBytes)
99
+
100
+ let content = side.transcript || side.text
101
+ if (!content && audioPart === undefined && side.audioPresent) {
102
+ // Audio was captured but couldn't be turned into a playable part — an unsupported format, or
103
+ // over the size budget — and there is no transcript, so surface a marker rather than let the
104
+ // turn look silently empty.
105
+ content = AUDIO_FALLBACK
106
+ }
107
+
108
+ const toolCalls = side.toolCalls?.length ? parseToolCallArguments(side.toolCalls) : undefined
109
+ const toolResults = side.toolResults.length ? side.toolResults : undefined
110
+
111
+ if (!content && audioPart === undefined && toolCalls === undefined && toolResults === undefined) return
112
+
113
+ /** @type {Message} */
114
+ const message = { role, content: content || '' }
115
+ if (audioPart !== undefined) message.audioParts = [audioPart]
116
+ if (toolCalls !== undefined) message.toolCalls = toolCalls
117
+ if (toolResults !== undefined) message.toolResults = toolResults
118
+
119
+ return message
120
+ }
121
+
122
+ /**
123
+ * @param {{ input_tokens?: number, output_tokens?: number, total_tokens?: number } | undefined} usage
124
+ * @returns {Record<string, number> | undefined}
125
+ */
126
+ function usageMetrics (usage) {
127
+ if (!usage) return
128
+
129
+ const { input_tokens: inputTokens, output_tokens: outputTokens } = usage
130
+ let { total_tokens: totalTokens } = usage
131
+
132
+ /** @type {Record<string, number>} */
133
+ const metrics = {}
134
+ if (inputTokens != null) metrics.input_tokens = inputTokens
135
+ if (outputTokens != null) metrics.output_tokens = outputTokens
136
+ // Mirror the chat and responses fallback.
137
+ if (totalTokens == null && inputTokens != null && outputTokens != null) {
138
+ totalTokens = inputTokens + outputTokens
139
+ }
140
+ if (totalTokens != null) metrics.total_tokens = totalTokens
141
+
142
+ return metrics.total_tokens === undefined && metrics.input_tokens === undefined ? undefined : metrics
143
+ }
144
+
145
+ /**
146
+ * LLM Observability spans for one OpenAI Realtime turn.
147
+ *
148
+ * The turn is modelled as a small tree — a `workflow` root with a `user speech` window, the `llm`
149
+ * generation span, and an `agent speech` window — so span duration is meaningful (the llm span
150
+ * measures model work, not the human's speaking time) and time-to-first-agent-audio falls out of the
151
+ * span boundaries. Consumers identify the phase spans by span kind and name, so those strings are a
152
+ * contract: renaming one is a breaking change for the UI and the backend metric alike.
153
+ */
154
+ class RealtimeLLMObsPlugin extends LLMObsPlugin {
155
+ static integration = 'openai'
156
+ static system = 'openai'
157
+
158
+ /**
159
+ * The instrumentation replays the turn with `traceSync`, which never publishes `asyncEnd`, so tag
160
+ * on `end` instead — before the sibling tracing plugin's `end` finishes the span, since the LLM
161
+ * Observability plugins are registered first.
162
+ *
163
+ * @param {object} ctx
164
+ */
165
+ end (ctx) {
166
+ super.end(ctx)
167
+ super.asyncEnd(ctx)
168
+ }
169
+ }
170
+
171
+ /** The whole perceived turn. Carries the transcripts so the waterfall row reads as a conversation. */
172
+ class RealtimeTurnLLMObsPlugin extends RealtimeLLMObsPlugin {
173
+ static id = 'openai_realtime_turn_llmobs'
174
+ static prefix = 'tracing:apm:openai:realtime:turn'
175
+
176
+ constructor (...args) {
177
+ super(...args)
178
+
179
+ this.addSub('dd-trace:openai:realtime:capture-context', turn => {
180
+ if (!this._tracerConfig.llmobs.DD_LLMOBS_ENABLED) return
181
+
182
+ // Composed on top of the tracing plugin's, so the replayed tree restores both the APM and the
183
+ // LLM Observability context the caller had active when the turn began.
184
+ const store = llmobsStorage.getStore()
185
+ const previous = turn.runInContext ?? (fn => fn())
186
+ turn.runInContext = fn => previous(() => llmobsStorage.run(store, fn))
187
+ })
188
+ }
189
+
190
+ getLLMObsSpanRegisterOptions (ctx) {
191
+ return {
192
+ kind: 'workflow',
193
+ // Provider-agnostic `audio turn` marker, so a consumer can gate on "is this a voice turn?".
194
+ name: 'realtime audio turn',
195
+ sessionId: ctx.turn.sessionId,
196
+ }
197
+ }
198
+
199
+ setLLMObsTags (ctx) {
200
+ const span = ctx.currentStore?.span
201
+ if (!span) return
202
+
203
+ const { input, output } = ctx.turn
204
+ this._tagger.tagTextIO(span, input.transcript || input.text, output.transcript || output.text)
205
+ }
206
+ }
207
+
208
+ /** The model's generation work. The turn's audio, tool calls and token usage ride here. */
209
+ class RealtimeResponseLLMObsPlugin extends RealtimeLLMObsPlugin {
210
+ static id = 'openai_realtime_response_llmobs'
211
+ static prefix = 'tracing:apm:openai:realtime:response'
212
+
213
+ constructor (...args) {
214
+ super(...args)
215
+
216
+ // This plugin is the only consumer of a turn's audio bytes, and the instrumentation keeps them
217
+ // only while something is subscribed here — a disabled plugin unsubscribes, so an APM-only
218
+ // process never buffers audio it cannot use. Nothing is ever published; the subscription is the
219
+ // signal. Keep it if `buildMessage` stops reading `side.audio`, and the buffering stops with it.
220
+ this.addSub('dd-trace:openai:realtime:audio', () => {})
221
+ }
222
+
223
+ getLLMObsSpanRegisterOptions (ctx) {
224
+ const { turn } = ctx
225
+ const { modelProvider, client } = getModelProviderAndClient(turn.basePath)
226
+
227
+ return {
228
+ kind: 'llm',
229
+ name: `${client}.createRealtimeResponse`,
230
+ modelName: turn.model || 'unknown_model',
231
+ modelProvider,
232
+ sessionId: turn.sessionId,
233
+ }
234
+ }
235
+
236
+ setLLMObsTags (ctx) {
237
+ const span = ctx.currentStore?.span
238
+ if (!span) return
239
+
240
+ const { turn } = ctx
241
+
242
+ // One budget for the whole event, spent across both sides rather than offered to each.
243
+ // A turn normally carries audio on both sides, so a per-message budget lets two individually
244
+ // accepted clips add up to twice the limit — and `writers/spans.js` responds by truncating the
245
+ // event's *entire* input and output, losing the transcripts too. Input is served first and the
246
+ // output falls back to its transcript when nothing is left, which is the same graceful
247
+ // degradation an oversize single clip already gets.
248
+ const inputMessage = buildMessage('user', turn.input, LLMOBS_AUDIO_INLINE_MAX_BYTES)
249
+ const remainingAudioBytes = LLMOBS_AUDIO_INLINE_MAX_BYTES - audioBytesOf(inputMessage)
250
+ const outputMessage = buildMessage('assistant', turn.output, remainingAudioBytes)
251
+
252
+ this._tagger.tagLLMIO(span, inputMessage ? [inputMessage] : [], outputMessage ? [outputMessage] : [])
253
+ this._tagger.tagMetadata(span, turn.metadata)
254
+
255
+ const metrics = usageMetrics(turn.usage)
256
+ if (metrics !== undefined) this._tagger.tagMetrics(span, metrics)
257
+ }
258
+ }
259
+
260
+ /** A speaking window — the human's or the agent's. These are timing regions; the audio rides on the llm span. */
261
+ class RealtimeSpeechLLMObsPlugin extends RealtimeLLMObsPlugin {
262
+ static id = 'openai_realtime_speech_llmobs'
263
+ static prefix = 'tracing:apm:openai:realtime:speech'
264
+
265
+ getLLMObsSpanRegisterOptions (ctx) {
266
+ return {
267
+ kind: 'workflow',
268
+ name: ctx.phase.llmobsName,
269
+ sessionId: ctx.turn.sessionId,
270
+ }
271
+ }
272
+
273
+ setLLMObsTags (ctx) {
274
+ const span = ctx.currentStore?.span
275
+ if (!span) return
276
+
277
+ this._tagger.tagTextIO(span, undefined, ctx.transcript)
278
+ }
279
+ }
280
+
281
+ module.exports = [
282
+ RealtimeTurnLLMObsPlugin,
283
+ RealtimeResponseLLMObsPlugin,
284
+ RealtimeSpeechLLMObsPlugin,
285
+ ]
@@ -1,13 +1,13 @@
1
1
  'use strict'
2
2
 
3
+ const { audioMimeTypeFromFormat, formatAudioPart } = require('../../audio-utils')
4
+ const { AUDIO_FALLBACK } = require('../../constants/audio')
3
5
  const { UNKNOWN_MODEL_PROVIDER } = require('../../constants/tags')
4
- const { audioMimeTypeFromFormat, formatAudioPart } = require('../../util')
5
6
  const {
6
7
  INPUT_TYPE_IMAGE,
7
8
  INPUT_TYPE_FILE,
8
9
  IMAGE_FALLBACK,
9
10
  FILE_FALLBACK,
10
- AUDIO_FALLBACK,
11
11
  AUDIO_MIME_TYPES,
12
12
  } = require('./constants')
13
13
 
@@ -177,7 +177,6 @@ function extractContentParts (parts) {
177
177
  * client baseURL convention.
178
178
  *
179
179
  * @param {string} baseUrl
180
- * @returns {string}
181
180
  */
182
181
  function getOpenAIModelProvider (baseUrl = '') {
183
182
  if (baseUrl.includes('azure')) return 'azure_openai'
@@ -186,11 +185,25 @@ function getOpenAIModelProvider (baseUrl = '') {
186
185
  return UNKNOWN_MODEL_PROVIDER
187
186
  }
188
187
 
188
+ /**
189
+ * Pair an OpenAI-compatible base URL's model provider with the client label used in span names.
190
+ *
191
+ * @param {string} [baseUrl]
192
+ * @returns {{ modelProvider: string, client: string }}
193
+ */
194
+ function getModelProviderAndClient (baseUrl = '') {
195
+ const modelProvider = getOpenAIModelProvider(baseUrl)
196
+ if (modelProvider === 'azure_openai') return { modelProvider, client: 'AzureOpenAI' }
197
+ if (modelProvider === 'deepseek') return { modelProvider, client: 'DeepSeek' }
198
+ return { modelProvider, client: 'OpenAI' }
199
+ }
200
+
189
201
  module.exports = {
190
202
  extractChatTemplateFromInstructions,
191
203
  normalizePromptVariables,
192
204
  extractTextFromContentItem,
193
205
  extractContentParts,
194
206
  hasMultimodalInputs,
207
+ getModelProviderAndClient,
195
208
  getOpenAIModelProvider,
196
209
  }
@@ -0,0 +1,272 @@
1
+ 'use strict'
2
+
3
+ const { createHash, randomUUID } = require('node:crypto')
4
+ const fs = require('node:fs')
5
+ const os = require('node:os')
6
+ const path = require('node:path')
7
+
8
+ const { LRUCache } = require('../../../../../vendor/dist/lru-cache')
9
+
10
+ const log = require('../../log')
11
+ const ManagedPrompt = require('./prompt')
12
+
13
+ const CACHE_SUBPATH = path.join('datadog', 'llmobs', 'prompts')
14
+ const MAX_HOT_ENTRIES = 1024
15
+ const PROMPT_SOURCES = new Set(['registry', 'cache', 'fallback', 'ff', 'resolve'])
16
+
17
+ /** @param {string} key */
18
+ function promptIdFromKey (key) {
19
+ return key.slice(0, key.lastIndexOf(':'))
20
+ }
21
+
22
+ /**
23
+ * @param {string} promptId
24
+ * @param {Array<unknown>} selector
25
+ */
26
+ function cacheKey (promptId, selector) {
27
+ const hash = createHash('sha1').update(JSON.stringify(selector)).digest('hex').slice(0, 16)
28
+ return `${promptId}:${hash}`
29
+ }
30
+
31
+ function defaultCacheDir () {
32
+ try {
33
+ const home = os.homedir()
34
+ if (home) return path.join(home, '.cache', CACHE_SUBPATH)
35
+ } catch {}
36
+ return path.join(os.tmpdir(), CACHE_SUBPATH)
37
+ }
38
+
39
+ class HotCache {
40
+ /**
41
+ * Create the bounded in-memory prompt cache.
42
+ * @param {object} options
43
+ * @param {number} options.ttlMs
44
+ * @param {(key: string, stale: ManagedPrompt | undefined, options: {context: object}) =>
45
+ * Promise<ManagedPrompt | undefined>} [options.fetchMethod]
46
+ */
47
+ constructor ({ ttlMs, fetchMethod }) {
48
+ this.enabled = ttlMs > 0
49
+ this.cache = new LRUCache({
50
+ max: MAX_HOT_ENTRIES,
51
+ ttl: this.enabled && Number.isFinite(ttlMs) ? ttlMs : undefined,
52
+ allowStale: true,
53
+ noDeleteOnStaleGet: true,
54
+ fetchMethod,
55
+ })
56
+ }
57
+
58
+ /**
59
+ * Read a prompt and its freshness.
60
+ * @param {string} key
61
+ */
62
+ get (key) {
63
+ if (!this.enabled) return
64
+ const status = {}
65
+ const prompt = this.cache.get(key, { status })
66
+ if (!prompt) return
67
+ return { prompt, stale: status.get === 'stale' }
68
+ }
69
+
70
+ /**
71
+ * Store a prompt.
72
+ * @param {string} key
73
+ * @param {ManagedPrompt} prompt
74
+ * @param {number} [ageMs]
75
+ */
76
+ set (key, prompt, ageMs = 0) {
77
+ if (this.enabled) this.cache.set(key, prompt, { start: performance.now() - ageMs })
78
+ }
79
+
80
+ /**
81
+ * Refresh a stale prompt in the background.
82
+ * @param {string} key
83
+ * @param {object} context
84
+ */
85
+ refresh (key, context) {
86
+ if (!this.enabled) return
87
+ void this.cache.fetch(key, {
88
+ allowStaleOnFetchRejection: true,
89
+ context,
90
+ forceRefresh: true,
91
+ noDeleteOnFetchRejection: true,
92
+ }).catch(() => {})
93
+ }
94
+
95
+ /**
96
+ * Delete one selector.
97
+ * @param {string} key
98
+ */
99
+ delete (key) {
100
+ if (this.enabled) this.cache.delete(key)
101
+ }
102
+
103
+ /**
104
+ * Clear all hot prompt entries.
105
+ */
106
+ clear () {
107
+ if (this.enabled) this.cache.clear()
108
+ }
109
+
110
+ /**
111
+ * Evict every selector for one exact prompt ID.
112
+ * @param {string} promptId
113
+ */
114
+ evictPrompt (promptId) {
115
+ if (!this.enabled) return
116
+ for (const [key] of this.cache.dump()) {
117
+ if (promptIdFromKey(key) === promptId) this.cache.delete(key)
118
+ }
119
+ }
120
+ }
121
+
122
+ class WarmCache {
123
+ /**
124
+ * Create the optional filesystem prompt cache.
125
+ * @param {object} options
126
+ * @param {string | undefined} options.cacheDir
127
+ * @param {boolean} [options.enabled]
128
+ * @param {number} options.ttlMs
129
+ */
130
+ constructor ({ cacheDir, enabled = true, ttlMs }) {
131
+ this.enabled = enabled && ttlMs > 0
132
+ this.ttlMs = ttlMs
133
+ this.cacheDir = cacheDir || defaultCacheDir()
134
+ if (this.enabled) this.#ensureDir(this.cacheDir)
135
+ }
136
+
137
+ /**
138
+ * Create a secure cache directory.
139
+ * @param {string} directory
140
+ */
141
+ #ensureDir (directory) {
142
+ try {
143
+ fs.mkdirSync(directory, { recursive: true, mode: 0o700 })
144
+ } catch (error) {
145
+ log.warn('Failed to create prompt cache directory: %s', error.message)
146
+ this.enabled = false
147
+ }
148
+ }
149
+
150
+ /**
151
+ * Resolve the collision-safe directory for a prompt ID.
152
+ * @param {string} promptId
153
+ */
154
+ #promptDir (promptId) {
155
+ return path.join(this.cacheDir, Buffer.from(promptId).toString('base64url') || '_')
156
+ }
157
+
158
+ /**
159
+ * Resolve the cache file for a selector key.
160
+ * @param {string} key
161
+ */
162
+ #path (key) {
163
+ const index = key.lastIndexOf(':')
164
+ return path.join(this.#promptDir(key.slice(0, index)), `${key.slice(index + 1)}.json`)
165
+ }
166
+
167
+ /**
168
+ * Restore and validate a prompt from the warm cache.
169
+ * @param {object} data
170
+ */
171
+ #deserialize (data) {
172
+ const validTemplate = typeof data?.template === 'string' || (
173
+ Array.isArray(data?.template) && data.template.every(message => {
174
+ return message && typeof message.role === 'string' && typeof message.content === 'string'
175
+ })
176
+ )
177
+ if (
178
+ typeof data?.id !== 'string' ||
179
+ typeof data.version !== 'string' ||
180
+ !PROMPT_SOURCES.has(data.source) ||
181
+ !validTemplate ||
182
+ (data.promptUuid !== undefined && typeof data.promptUuid !== 'string') ||
183
+ (data.promptVersionUuid !== undefined && typeof data.promptVersionUuid !== 'string')
184
+ ) {
185
+ throw new TypeError('Invalid managed prompt cache entry')
186
+ }
187
+ return new ManagedPrompt(data)
188
+ }
189
+
190
+ /**
191
+ * Read a prompt and its freshness.
192
+ * @param {string} key
193
+ */
194
+ get (key) {
195
+ if (!this.enabled) return
196
+ try {
197
+ const data = JSON.parse(fs.readFileSync(this.#path(key), 'utf8'))
198
+ if (!Number.isFinite(data.timestamp)) throw new TypeError('Invalid prompt cache timestamp')
199
+ const prompt = this.#deserialize(data.prompt)
200
+ const ageMs = Math.max(0, Date.now() - data.timestamp)
201
+ return { prompt, stale: ageMs > this.ttlMs, ageMs }
202
+ } catch (error) {
203
+ log.debug('Failed to read prompt from cache: %s', error.message)
204
+ }
205
+ }
206
+
207
+ /**
208
+ * Store a prompt with restrictive permissions.
209
+ * @param {string} key
210
+ * @param {ManagedPrompt} prompt
211
+ */
212
+ set (key, prompt) {
213
+ if (!this.enabled) return
214
+ const file = this.#path(key)
215
+ const temporary = `${file}.tmp.${randomUUID()}`
216
+ try {
217
+ this.#ensureDir(path.dirname(file))
218
+ if (!this.enabled) return
219
+ // Write separately, then rename so other processes see either the old file or the complete new one.
220
+ fs.writeFileSync(temporary, JSON.stringify({ prompt, timestamp: Date.now() }), {
221
+ encoding: 'utf8',
222
+ mode: 0o600,
223
+ })
224
+ fs.renameSync(temporary, file)
225
+ } catch (error) {
226
+ try { fs.rmSync(temporary, { force: true }) } catch {}
227
+ log.debug('Failed to write prompt to cache: %s', error.message)
228
+ }
229
+ }
230
+
231
+ /**
232
+ * Delete one selector.
233
+ * @param {string} key
234
+ */
235
+ delete (key) {
236
+ if (!this.enabled) return
237
+ try {
238
+ fs.rmSync(this.#path(key), { force: true })
239
+ } catch (error) {
240
+ log.debug('Failed to delete prompt from cache: %s', error.message)
241
+ }
242
+ }
243
+
244
+ /**
245
+ * Evict every selector for one exact prompt ID.
246
+ * @param {string} promptId
247
+ */
248
+ evictPrompt (promptId) {
249
+ if (!this.enabled) return
250
+ try {
251
+ fs.rmSync(this.#promptDir(promptId), { recursive: true, force: true })
252
+ } catch (error) {
253
+ log.debug('Failed to evict prompt from cache: %s', error.message)
254
+ }
255
+ }
256
+
257
+ /**
258
+ * Clear all warm prompt entries.
259
+ */
260
+ clear () {
261
+ if (!this.enabled) return
262
+ try {
263
+ for (const entry of fs.readdirSync(this.cacheDir)) {
264
+ fs.rmSync(path.join(this.cacheDir, entry), { recursive: true, force: true })
265
+ }
266
+ } catch (error) {
267
+ log.debug('Failed to clear prompt cache: %s', error.message)
268
+ }
269
+ }
270
+ }
271
+
272
+ module.exports = { HotCache, WarmCache, cacheKey, promptIdFromKey }