@strands-agents/sdk 0.3.0 → 0.5.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 (302) hide show
  1. package/README.md +43 -0
  2. package/dist/src/__fixtures__/agent-helpers.d.ts +10 -1
  3. package/dist/src/__fixtures__/agent-helpers.d.ts.map +1 -1
  4. package/dist/src/__fixtures__/agent-helpers.js +13 -2
  5. package/dist/src/__fixtures__/agent-helpers.js.map +1 -1
  6. package/dist/src/__fixtures__/mock-hook-provider.d.ts +3 -7
  7. package/dist/src/__fixtures__/mock-hook-provider.d.ts.map +1 -1
  8. package/dist/src/__fixtures__/mock-hook-provider.js +3 -9
  9. package/dist/src/__fixtures__/mock-hook-provider.js.map +1 -1
  10. package/dist/src/__fixtures__/mock-message-model.d.ts +8 -2
  11. package/dist/src/__fixtures__/mock-message-model.d.ts.map +1 -1
  12. package/dist/src/__fixtures__/mock-message-model.js +1 -0
  13. package/dist/src/__fixtures__/mock-message-model.js.map +1 -1
  14. package/dist/src/__fixtures__/mock-span.d.ts +78 -0
  15. package/dist/src/__fixtures__/mock-span.d.ts.map +1 -0
  16. package/dist/src/__fixtures__/mock-span.js +93 -0
  17. package/dist/src/__fixtures__/mock-span.js.map +1 -0
  18. package/dist/src/__fixtures__/mock-storage-provider.d.ts +37 -0
  19. package/dist/src/__fixtures__/mock-storage-provider.d.ts.map +1 -0
  20. package/dist/src/__fixtures__/mock-storage-provider.js +105 -0
  21. package/dist/src/__fixtures__/mock-storage-provider.js.map +1 -0
  22. package/dist/src/__fixtures__/slim-types.d.ts +50 -0
  23. package/dist/src/__fixtures__/slim-types.d.ts.map +1 -0
  24. package/dist/src/__fixtures__/slim-types.js +6 -0
  25. package/dist/src/__fixtures__/slim-types.js.map +1 -0
  26. package/dist/src/__fixtures__/tool-helpers.d.ts +10 -5
  27. package/dist/src/__fixtures__/tool-helpers.d.ts.map +1 -1
  28. package/dist/src/__fixtures__/tool-helpers.js +5 -5
  29. package/dist/src/__fixtures__/tool-helpers.js.map +1 -1
  30. package/dist/src/__tests__/app-state.test.d.ts +2 -0
  31. package/dist/src/__tests__/app-state.test.d.ts.map +1 -0
  32. package/dist/src/{agent/__tests__/state.test.js → __tests__/app-state.test.js} +62 -43
  33. package/dist/src/__tests__/app-state.test.js.map +1 -0
  34. package/dist/src/__tests__/mcp.test.js +96 -15
  35. package/dist/src/__tests__/mcp.test.js.map +1 -1
  36. package/dist/src/agent/__tests__/agent.hook.test.js +18 -18
  37. package/dist/src/agent/__tests__/agent.hook.test.js.map +1 -1
  38. package/dist/src/agent/__tests__/agent.test.js +234 -8
  39. package/dist/src/agent/__tests__/agent.test.js.map +1 -1
  40. package/dist/src/agent/__tests__/agent.tracer.test.d.ts +2 -0
  41. package/dist/src/agent/__tests__/agent.tracer.test.d.ts.map +1 -0
  42. package/dist/src/agent/__tests__/agent.tracer.test.js +470 -0
  43. package/dist/src/agent/__tests__/agent.tracer.test.js.map +1 -0
  44. package/dist/src/agent/__tests__/printer.test.js +5 -9
  45. package/dist/src/agent/__tests__/printer.test.js.map +1 -1
  46. package/dist/src/agent/__tests__/snapshot.test.d.ts +2 -0
  47. package/dist/src/agent/__tests__/snapshot.test.d.ts.map +1 -0
  48. package/dist/src/agent/__tests__/snapshot.test.js +249 -0
  49. package/dist/src/agent/__tests__/snapshot.test.js.map +1 -0
  50. package/dist/src/agent/agent.d.ts +78 -10
  51. package/dist/src/agent/agent.d.ts.map +1 -1
  52. package/dist/src/agent/agent.js +252 -55
  53. package/dist/src/agent/agent.js.map +1 -1
  54. package/dist/src/agent/printer.d.ts +4 -0
  55. package/dist/src/agent/printer.d.ts.map +1 -1
  56. package/dist/src/agent/printer.js +18 -6
  57. package/dist/src/agent/printer.js.map +1 -1
  58. package/dist/src/agent/snapshot.d.ts +132 -0
  59. package/dist/src/agent/snapshot.d.ts.map +1 -0
  60. package/dist/src/agent/snapshot.js +151 -0
  61. package/dist/src/agent/snapshot.js.map +1 -0
  62. package/dist/src/{agent/state.d.ts → app-state.d.ts} +19 -6
  63. package/dist/src/app-state.d.ts.map +1 -0
  64. package/dist/src/{agent/state.js → app-state.js} +27 -6
  65. package/dist/src/app-state.js.map +1 -0
  66. package/dist/src/conversation-manager/sliding-window-conversation-manager.d.ts +1 -1
  67. package/dist/src/conversation-manager/sliding-window-conversation-manager.js +1 -1
  68. package/dist/src/errors.d.ts +15 -0
  69. package/dist/src/errors.d.ts.map +1 -1
  70. package/dist/src/errors.js +18 -0
  71. package/dist/src/errors.js.map +1 -1
  72. package/dist/src/hooks/__tests__/events.test.js +102 -21
  73. package/dist/src/hooks/__tests__/events.test.js.map +1 -1
  74. package/dist/src/hooks/events.d.ts +156 -22
  75. package/dist/src/hooks/events.d.ts.map +1 -1
  76. package/dist/src/hooks/events.js +158 -18
  77. package/dist/src/hooks/events.js.map +1 -1
  78. package/dist/src/hooks/index.d.ts +12 -4
  79. package/dist/src/hooks/index.d.ts.map +1 -1
  80. package/dist/src/hooks/index.js +11 -3
  81. package/dist/src/hooks/index.js.map +1 -1
  82. package/dist/src/hooks/registry.d.ts +5 -5
  83. package/dist/src/hooks/registry.d.ts.map +1 -1
  84. package/dist/src/hooks/registry.js.map +1 -1
  85. package/dist/src/hooks/types.d.ts +5 -5
  86. package/dist/src/hooks/types.d.ts.map +1 -1
  87. package/dist/src/index.d.ts +6 -3
  88. package/dist/src/index.d.ts.map +1 -1
  89. package/dist/src/index.js +8 -3
  90. package/dist/src/index.js.map +1 -1
  91. package/dist/src/mcp.d.ts +3 -0
  92. package/dist/src/mcp.d.ts.map +1 -1
  93. package/dist/src/mcp.js +38 -1
  94. package/dist/src/mcp.js.map +1 -1
  95. package/dist/src/models/__tests__/anthropic.test.js +31 -42
  96. package/dist/src/models/__tests__/anthropic.test.js.map +1 -1
  97. package/dist/src/models/__tests__/bedrock.test.js +70 -107
  98. package/dist/src/models/__tests__/bedrock.test.js.map +1 -1
  99. package/dist/src/models/__tests__/gemini.test.js +18 -18
  100. package/dist/src/models/__tests__/gemini.test.js.map +1 -1
  101. package/dist/src/models/__tests__/model.test.js +21 -13
  102. package/dist/src/models/__tests__/model.test.js.map +1 -1
  103. package/dist/src/models/__tests__/openai.test.js +73 -83
  104. package/dist/src/models/__tests__/openai.test.js.map +1 -1
  105. package/dist/src/models/model.d.ts +4 -0
  106. package/dist/src/models/model.d.ts.map +1 -1
  107. package/dist/src/models/model.js +6 -0
  108. package/dist/src/models/model.js.map +1 -1
  109. package/dist/src/models/streaming.d.ts +9 -1
  110. package/dist/src/models/streaming.d.ts.map +1 -1
  111. package/dist/src/models/streaming.js +17 -0
  112. package/dist/src/models/streaming.js.map +1 -1
  113. package/dist/src/multiagent/__tests__/events.test.d.ts +2 -0
  114. package/dist/src/multiagent/__tests__/events.test.d.ts.map +1 -0
  115. package/dist/src/multiagent/__tests__/events.test.js +189 -0
  116. package/dist/src/multiagent/__tests__/events.test.js.map +1 -0
  117. package/dist/src/multiagent/__tests__/nodes.test.d.ts +2 -0
  118. package/dist/src/multiagent/__tests__/nodes.test.d.ts.map +1 -0
  119. package/dist/src/multiagent/__tests__/nodes.test.js +194 -0
  120. package/dist/src/multiagent/__tests__/nodes.test.js.map +1 -0
  121. package/dist/src/multiagent/__tests__/queue.test.d.ts +2 -0
  122. package/dist/src/multiagent/__tests__/queue.test.d.ts.map +1 -0
  123. package/dist/src/multiagent/__tests__/queue.test.js +96 -0
  124. package/dist/src/multiagent/__tests__/queue.test.js.map +1 -0
  125. package/dist/src/multiagent/base.d.ts +25 -0
  126. package/dist/src/multiagent/base.d.ts.map +1 -0
  127. package/dist/src/multiagent/base.js +2 -0
  128. package/dist/src/multiagent/base.js.map +1 -0
  129. package/dist/src/multiagent/edge.d.ts +29 -0
  130. package/dist/src/multiagent/edge.d.ts.map +1 -0
  131. package/dist/src/multiagent/edge.js +15 -0
  132. package/dist/src/multiagent/edge.js.map +1 -0
  133. package/dist/src/multiagent/events.d.ts +135 -0
  134. package/dist/src/multiagent/events.d.ts.map +1 -0
  135. package/dist/src/multiagent/events.js +140 -0
  136. package/dist/src/multiagent/events.js.map +1 -0
  137. package/dist/src/multiagent/index.d.ts +13 -0
  138. package/dist/src/multiagent/index.d.ts.map +1 -0
  139. package/dist/src/multiagent/index.js +8 -0
  140. package/dist/src/multiagent/index.js.map +1 -0
  141. package/dist/src/multiagent/nodes.d.ts +123 -0
  142. package/dist/src/multiagent/nodes.d.ts.map +1 -0
  143. package/dist/src/multiagent/nodes.js +148 -0
  144. package/dist/src/multiagent/nodes.js.map +1 -0
  145. package/dist/src/multiagent/queue.d.ts +67 -0
  146. package/dist/src/multiagent/queue.d.ts.map +1 -0
  147. package/dist/src/multiagent/queue.js +59 -0
  148. package/dist/src/multiagent/queue.js.map +1 -0
  149. package/dist/src/multiagent/state.d.ts +122 -0
  150. package/dist/src/multiagent/state.d.ts.map +1 -0
  151. package/dist/src/multiagent/state.js +132 -0
  152. package/dist/src/multiagent/state.js.map +1 -0
  153. package/dist/src/registry/tool-registry.d.ts +2 -1
  154. package/dist/src/registry/tool-registry.d.ts.map +1 -1
  155. package/dist/src/registry/tool-registry.js +4 -2
  156. package/dist/src/registry/tool-registry.js.map +1 -1
  157. package/dist/src/session/__tests__/file-storage.test.node.d.ts +2 -0
  158. package/dist/src/session/__tests__/file-storage.test.node.d.ts.map +1 -0
  159. package/dist/src/session/__tests__/file-storage.test.node.js +218 -0
  160. package/dist/src/session/__tests__/file-storage.test.node.js.map +1 -0
  161. package/dist/src/session/__tests__/s3-storage.test.node.d.ts +2 -0
  162. package/dist/src/session/__tests__/s3-storage.test.node.d.ts.map +1 -0
  163. package/dist/src/session/__tests__/s3-storage.test.node.js +375 -0
  164. package/dist/src/session/__tests__/s3-storage.test.node.js.map +1 -0
  165. package/dist/src/session/__tests__/validation.test.d.ts +2 -0
  166. package/dist/src/session/__tests__/validation.test.d.ts.map +1 -0
  167. package/dist/src/session/__tests__/validation.test.js +20 -0
  168. package/dist/src/session/__tests__/validation.test.js.map +1 -0
  169. package/dist/src/session/file-storage.d.ts +79 -0
  170. package/dist/src/session/file-storage.d.ts.map +1 -0
  171. package/dist/src/session/file-storage.js +144 -0
  172. package/dist/src/session/file-storage.js.map +1 -0
  173. package/dist/src/session/index.d.ts +19 -0
  174. package/dist/src/session/index.d.ts.map +1 -0
  175. package/dist/src/session/index.js +18 -0
  176. package/dist/src/session/index.js.map +1 -0
  177. package/dist/src/session/s3-storage.d.ts +93 -0
  178. package/dist/src/session/s3-storage.d.ts.map +1 -0
  179. package/dist/src/session/s3-storage.js +150 -0
  180. package/dist/src/session/s3-storage.js.map +1 -0
  181. package/dist/src/session/storage.d.ts +91 -0
  182. package/dist/src/session/storage.d.ts.map +1 -0
  183. package/dist/src/session/storage.js +2 -0
  184. package/dist/src/session/storage.js.map +1 -0
  185. package/dist/src/session/types.d.ts +49 -0
  186. package/dist/src/session/types.d.ts.map +1 -0
  187. package/dist/src/session/types.js +2 -0
  188. package/dist/src/session/types.js.map +1 -0
  189. package/dist/src/session/validation.d.ts +10 -0
  190. package/dist/src/session/validation.d.ts.map +1 -0
  191. package/dist/src/session/validation.js +16 -0
  192. package/dist/src/session/validation.js.map +1 -0
  193. package/dist/src/structured-output/__tests__/context.test.d.ts +2 -0
  194. package/dist/src/structured-output/__tests__/context.test.d.ts.map +1 -0
  195. package/dist/src/structured-output/__tests__/context.test.js +201 -0
  196. package/dist/src/structured-output/__tests__/context.test.js.map +1 -0
  197. package/dist/src/structured-output/__tests__/exceptions.test.d.ts +2 -0
  198. package/dist/src/structured-output/__tests__/exceptions.test.d.ts.map +1 -0
  199. package/dist/src/structured-output/__tests__/exceptions.test.js +103 -0
  200. package/dist/src/structured-output/__tests__/exceptions.test.js.map +1 -0
  201. package/dist/src/structured-output/__tests__/tool.test.d.ts +2 -0
  202. package/dist/src/structured-output/__tests__/tool.test.d.ts.map +1 -0
  203. package/dist/src/structured-output/__tests__/tool.test.js +256 -0
  204. package/dist/src/structured-output/__tests__/tool.test.js.map +1 -0
  205. package/dist/src/structured-output/__tests__/utils.test.d.ts +2 -0
  206. package/dist/src/structured-output/__tests__/utils.test.d.ts.map +1 -0
  207. package/dist/src/structured-output/__tests__/utils.test.js +183 -0
  208. package/dist/src/structured-output/__tests__/utils.test.js.map +1 -0
  209. package/dist/src/structured-output/context.d.ts +91 -0
  210. package/dist/src/structured-output/context.d.ts.map +1 -0
  211. package/dist/src/structured-output/context.js +112 -0
  212. package/dist/src/structured-output/context.js.map +1 -0
  213. package/dist/src/structured-output/exceptions.d.ts +18 -0
  214. package/dist/src/structured-output/exceptions.d.ts.map +1 -0
  215. package/dist/src/structured-output/exceptions.js +28 -0
  216. package/dist/src/structured-output/exceptions.js.map +1 -0
  217. package/dist/src/structured-output/tool.d.ts +33 -0
  218. package/dist/src/structured-output/tool.d.ts.map +1 -0
  219. package/dist/src/structured-output/tool.js +73 -0
  220. package/dist/src/structured-output/tool.js.map +1 -0
  221. package/dist/src/structured-output/utils.d.ts +23 -0
  222. package/dist/src/structured-output/utils.d.ts.map +1 -0
  223. package/dist/src/structured-output/utils.js +104 -0
  224. package/dist/src/structured-output/utils.js.map +1 -0
  225. package/dist/src/telemetry/__tests__/config.test.node.d.ts +2 -0
  226. package/dist/src/telemetry/__tests__/config.test.node.d.ts.map +1 -0
  227. package/dist/src/telemetry/__tests__/config.test.node.js +129 -0
  228. package/dist/src/telemetry/__tests__/config.test.node.js.map +1 -0
  229. package/dist/src/telemetry/__tests__/json.test.d.ts +2 -0
  230. package/dist/src/telemetry/__tests__/json.test.d.ts.map +1 -0
  231. package/dist/src/telemetry/__tests__/json.test.js +89 -0
  232. package/dist/src/telemetry/__tests__/json.test.js.map +1 -0
  233. package/dist/src/telemetry/__tests__/tracer.test.node.d.ts +2 -0
  234. package/dist/src/telemetry/__tests__/tracer.test.node.d.ts.map +1 -0
  235. package/dist/src/telemetry/__tests__/tracer.test.node.js +611 -0
  236. package/dist/src/telemetry/__tests__/tracer.test.node.js.map +1 -0
  237. package/dist/src/telemetry/config.d.ts +61 -0
  238. package/dist/src/telemetry/config.d.ts.map +1 -0
  239. package/dist/src/telemetry/config.js +101 -0
  240. package/dist/src/telemetry/config.js.map +1 -0
  241. package/dist/src/telemetry/index.d.ts +34 -0
  242. package/dist/src/telemetry/index.d.ts.map +1 -0
  243. package/dist/src/telemetry/index.js +33 -0
  244. package/dist/src/telemetry/index.js.map +1 -0
  245. package/dist/src/telemetry/json.d.ts +11 -0
  246. package/dist/src/telemetry/json.d.ts.map +1 -0
  247. package/dist/src/telemetry/json.js +25 -0
  248. package/dist/src/telemetry/json.js.map +1 -0
  249. package/dist/src/telemetry/tracer.d.ts +219 -0
  250. package/dist/src/telemetry/tracer.d.ts.map +1 -0
  251. package/dist/src/telemetry/tracer.js +610 -0
  252. package/dist/src/telemetry/tracer.js.map +1 -0
  253. package/dist/src/telemetry/types.d.ts +101 -0
  254. package/dist/src/telemetry/types.d.ts.map +1 -0
  255. package/dist/src/telemetry/types.js +5 -0
  256. package/dist/src/telemetry/types.js.map +1 -0
  257. package/dist/src/tools/tool.d.ts +1 -1
  258. package/dist/src/tools/tool.js +1 -1
  259. package/dist/src/tools/zod-tool.d.ts.map +1 -1
  260. package/dist/src/tools/zod-tool.js +2 -5
  261. package/dist/src/tools/zod-tool.js.map +1 -1
  262. package/dist/src/tsconfig.tsbuildinfo +1 -1
  263. package/dist/src/types/__tests__/media.test.js +216 -1
  264. package/dist/src/types/__tests__/media.test.js.map +1 -1
  265. package/dist/src/types/__tests__/messages.test.js +193 -4
  266. package/dist/src/types/__tests__/messages.test.js.map +1 -1
  267. package/dist/src/types/agent.d.ts +16 -10
  268. package/dist/src/types/agent.d.ts.map +1 -1
  269. package/dist/src/types/agent.js +8 -1
  270. package/dist/src/types/agent.js.map +1 -1
  271. package/dist/src/types/json.d.ts +61 -0
  272. package/dist/src/types/json.d.ts.map +1 -1
  273. package/dist/src/types/json.js +24 -0
  274. package/dist/src/types/json.js.map +1 -1
  275. package/dist/src/types/media.d.ts +84 -4
  276. package/dist/src/types/media.d.ts.map +1 -1
  277. package/dist/src/types/media.js +194 -0
  278. package/dist/src/types/media.js.map +1 -1
  279. package/dist/src/types/messages.d.ts +152 -13
  280. package/dist/src/types/messages.d.ts.map +1 -1
  281. package/dist/src/types/messages.js +235 -8
  282. package/dist/src/types/messages.js.map +1 -1
  283. package/dist/src/types/serializable.d.ts +31 -0
  284. package/dist/src/types/serializable.d.ts.map +1 -0
  285. package/dist/src/types/serializable.js +19 -0
  286. package/dist/src/types/serializable.js.map +1 -0
  287. package/dist/src/utils/zod.d.ts +11 -0
  288. package/dist/src/utils/zod.d.ts.map +1 -0
  289. package/dist/src/utils/zod.js +14 -0
  290. package/dist/src/utils/zod.js.map +1 -0
  291. package/dist/src/vended-tools/bash/__tests__/bash.test.node.js +2 -2
  292. package/dist/src/vended-tools/bash/__tests__/bash.test.node.js.map +1 -1
  293. package/dist/src/vended-tools/file_editor/__tests__/file-editor.test.node.js +4 -4
  294. package/dist/src/vended-tools/file_editor/__tests__/file-editor.test.node.js.map +1 -1
  295. package/dist/src/vended-tools/notebook/__tests__/notebook.test.js +2 -2
  296. package/dist/src/vended-tools/notebook/__tests__/notebook.test.js.map +1 -1
  297. package/package.json +17 -3
  298. package/dist/src/agent/__tests__/state.test.d.ts +0 -2
  299. package/dist/src/agent/__tests__/state.test.d.ts.map +0 -1
  300. package/dist/src/agent/__tests__/state.test.js.map +0 -1
  301. package/dist/src/agent/state.d.ts.map +0 -1
  302. package/dist/src/agent/state.js.map +0 -1
@@ -0,0 +1,611 @@
1
+ import { describe, it, expect, beforeEach, vi } from 'vitest';
2
+ import { SpanStatusCode, trace, context } from '@opentelemetry/api';
3
+ import { Tracer } from '../tracer.js';
4
+ import { Message, TextBlock, ToolResultBlock, ToolUseBlock } from '../../types/messages.js';
5
+ import { MockSpan, eventAttr } from '../../__fixtures__/mock-span.js';
6
+ import { textMessage } from '../../__fixtures__/agent-helpers.js';
7
+ // Partial mock: keep real SpanStatusCode etc., replace context and trace
8
+ vi.mock('@opentelemetry/api', async (importOriginal) => ({
9
+ ...(await importOriginal()),
10
+ context: { active: vi.fn(() => ({})), with: vi.fn((_ctx, fn) => fn()) },
11
+ trace: {
12
+ getTracer: vi.fn(),
13
+ setSpan: vi.fn(),
14
+ },
15
+ }));
16
+ describe('Tracer', () => {
17
+ let mockSpan;
18
+ let mockStartSpan;
19
+ beforeEach(() => {
20
+ mockSpan = new MockSpan();
21
+ mockStartSpan = vi.fn().mockReturnValue(mockSpan);
22
+ vi.mocked(trace.getTracer).mockReturnValue({
23
+ startSpan: mockStartSpan,
24
+ startActiveSpan: vi.fn(),
25
+ });
26
+ // Default to stable conventions; tests needing latest override this
27
+ vi.stubEnv('OTEL_SEMCONV_STABILITY_OPT_IN', '');
28
+ });
29
+ /** Get the [spanName, options] from the first startSpan call. */
30
+ function getStartSpanCall() {
31
+ return mockStartSpan.mock.calls[0];
32
+ }
33
+ describe('constructor', () => {
34
+ it('reads service name from OTEL_SERVICE_NAME env var', () => {
35
+ vi.stubEnv('OTEL_SERVICE_NAME', 'my-custom-service');
36
+ new Tracer();
37
+ expect(trace.getTracer).toHaveBeenCalledWith('my-custom-service');
38
+ });
39
+ it('defaults service name to strands-agents', () => {
40
+ vi.stubEnv('OTEL_SERVICE_NAME', '');
41
+ new Tracer();
42
+ expect(trace.getTracer).toHaveBeenCalledWith('strands-agents');
43
+ });
44
+ });
45
+ describe('startAgentSpan', () => {
46
+ it('creates span with correct name and standard attributes', () => {
47
+ const tracer = new Tracer();
48
+ tracer.startAgentSpan({
49
+ messages: [textMessage('user', 'Hello')],
50
+ agentName: 'test-agent',
51
+ modelId: 'model-123',
52
+ });
53
+ const [spanName, options] = getStartSpanCall();
54
+ expect(spanName).toBe('invoke_agent test-agent');
55
+ expect(options.attributes).toMatchObject({
56
+ 'gen_ai.operation.name': 'invoke_agent',
57
+ 'gen_ai.system': expect.any(String),
58
+ 'gen_ai.agent.name': 'test-agent',
59
+ 'gen_ai.request.model': 'model-123',
60
+ name: 'invoke_agent test-agent',
61
+ });
62
+ });
63
+ it('includes agent id when provided', () => {
64
+ const tracer = new Tracer();
65
+ tracer.startAgentSpan({
66
+ messages: [textMessage('user', 'Hello')],
67
+ agentName: 'test-agent',
68
+ agentId: 'agent-42',
69
+ });
70
+ const [, options] = getStartSpanCall();
71
+ expect(options.attributes['gen_ai.agent.id']).toBe('agent-42');
72
+ });
73
+ it('serializes tool names into gen_ai.agent.tools', () => {
74
+ const tracer = new Tracer();
75
+ tracer.startAgentSpan({
76
+ messages: [textMessage('user', 'Hello')],
77
+ agentName: 'test-agent',
78
+ tools: [{ name: 'calculator' }, { name: 'search' }],
79
+ });
80
+ const [, options] = getStartSpanCall();
81
+ expect(options.attributes['gen_ai.agent.tools']).toBe('["calculator","search"]');
82
+ });
83
+ it('includes tool definitions when gen_ai_tool_definitions opt-in is set', () => {
84
+ vi.stubEnv('OTEL_SEMCONV_STABILITY_OPT_IN', 'gen_ai_tool_definitions');
85
+ const tracer = new Tracer();
86
+ const toolsConfig = { calc: { name: 'calc', description: 'Calculator' } };
87
+ tracer.startAgentSpan({
88
+ messages: [textMessage('user', 'Hello')],
89
+ agentName: 'test-agent',
90
+ toolsConfig,
91
+ });
92
+ const [, options] = getStartSpanCall();
93
+ expect(options.attributes['gen_ai.tool.definitions']).toBe(JSON.stringify(toolsConfig));
94
+ });
95
+ it('serializes system prompt into attribute', () => {
96
+ const tracer = new Tracer();
97
+ tracer.startAgentSpan({
98
+ messages: [textMessage('user', 'Hello')],
99
+ agentName: 'test-agent',
100
+ systemPrompt: 'You are a helpful assistant',
101
+ });
102
+ const [, options] = getStartSpanCall();
103
+ expect(options.attributes['system_prompt']).toBe('"You are a helpful assistant"');
104
+ });
105
+ it('merges constructor-level and call-level trace attributes', () => {
106
+ const tracer = new Tracer({ 'global.attr': 'global-val' });
107
+ tracer.startAgentSpan({
108
+ messages: [textMessage('user', 'Hello')],
109
+ agentName: 'test-agent',
110
+ traceAttributes: { 'custom.session': 'sess-1' },
111
+ });
112
+ const [, options] = getStartSpanCall();
113
+ expect(options.attributes['global.attr']).toBe('global-val');
114
+ expect(options.attributes['custom.session']).toBe('sess-1');
115
+ });
116
+ it('adds separate stable message events per message', () => {
117
+ const tracer = new Tracer();
118
+ tracer.startAgentSpan({
119
+ messages: [textMessage('user', 'Hello'), textMessage('assistant', 'Hi')],
120
+ agentName: 'test-agent',
121
+ });
122
+ expect(mockSpan.getEvents('gen_ai.user.message')).toHaveLength(1);
123
+ expect(mockSpan.getEvents('gen_ai.assistant.message')).toHaveLength(1);
124
+ });
125
+ it('classifies tool result messages as gen_ai.tool.message', () => {
126
+ const tracer = new Tracer();
127
+ const toolResultMsg = new Message({
128
+ role: 'user',
129
+ content: [new ToolResultBlock({ toolUseId: 'tool-1', status: 'success', content: [new TextBlock('done')] })],
130
+ });
131
+ tracer.startAgentSpan({ messages: [toolResultMsg], agentName: 'test-agent' });
132
+ expect(mockSpan.getEvents('gen_ai.tool.message')).toHaveLength(1);
133
+ });
134
+ it('adds single operation details event with latest conventions', () => {
135
+ vi.stubEnv('OTEL_SEMCONV_STABILITY_OPT_IN', 'gen_ai_latest_experimental');
136
+ const tracer = new Tracer();
137
+ tracer.startAgentSpan({
138
+ messages: [textMessage('user', 'Hello'), textMessage('assistant', 'Hi')],
139
+ agentName: 'test-agent',
140
+ });
141
+ const detailEvents = mockSpan.getEvents('gen_ai.client.inference.operation.details');
142
+ expect(detailEvents).toHaveLength(1);
143
+ const inputMessages = JSON.parse(eventAttr(detailEvents[0], 'gen_ai.input.messages'));
144
+ expect(inputMessages).toStrictEqual([
145
+ { role: 'user', parts: [{ type: 'text', content: 'Hello' }] },
146
+ { role: 'assistant', parts: [{ type: 'text', content: 'Hi' }] },
147
+ ]);
148
+ });
149
+ it('uses gen_ai.provider.name with latest conventions', () => {
150
+ vi.stubEnv('OTEL_SEMCONV_STABILITY_OPT_IN', 'gen_ai_latest_experimental');
151
+ const tracer = new Tracer();
152
+ tracer.startAgentSpan({ messages: [textMessage('user', 'Hello')], agentName: 'test-agent' });
153
+ const [, options] = getStartSpanCall();
154
+ expect(options.attributes['gen_ai.provider.name']).toBeDefined();
155
+ expect(options.attributes['gen_ai.system']).toBeUndefined();
156
+ });
157
+ it('uses gen_ai.system with stable conventions', () => {
158
+ const tracer = new Tracer();
159
+ tracer.startAgentSpan({ messages: [textMessage('user', 'Hello')], agentName: 'test-agent' });
160
+ const [, options] = getStartSpanCall();
161
+ expect(options.attributes['gen_ai.system']).toBeDefined();
162
+ expect(options.attributes['gen_ai.provider.name']).toBeUndefined();
163
+ });
164
+ });
165
+ describe('endAgentSpan', () => {
166
+ it('sets OK status and ends span on success', () => {
167
+ const tracer = new Tracer();
168
+ const span = tracer.startAgentSpan({ messages: [textMessage('user', 'Hi')], agentName: 'agent' });
169
+ tracer.endAgentSpan(span);
170
+ expect(mockSpan.calls.setStatus).toContainEqual({ status: { code: SpanStatusCode.OK } });
171
+ expect(mockSpan.calls.end).toHaveLength(1);
172
+ });
173
+ it('sets ERROR status and records exception on error', () => {
174
+ const tracer = new Tracer();
175
+ const span = tracer.startAgentSpan({ messages: [textMessage('user', 'Hi')], agentName: 'agent' });
176
+ const error = new Error('agent failed');
177
+ tracer.endAgentSpan(span, { error });
178
+ expect(mockSpan.calls.setStatus).toContainEqual({
179
+ status: { code: SpanStatusCode.ERROR, message: 'agent failed' },
180
+ });
181
+ expect(mockSpan.calls.recordException).toContainEqual({ exception: error, time: undefined });
182
+ });
183
+ it('sets accumulated usage attributes', () => {
184
+ const tracer = new Tracer();
185
+ const span = tracer.startAgentSpan({ messages: [textMessage('user', 'Hi')], agentName: 'agent' });
186
+ tracer.endAgentSpan(span, {
187
+ accumulatedUsage: { inputTokens: 100, outputTokens: 200, totalTokens: 300 },
188
+ });
189
+ expect(mockSpan.getAttributeValue('gen_ai.usage.input_tokens')).toBe(100);
190
+ expect(mockSpan.getAttributeValue('gen_ai.usage.output_tokens')).toBe(200);
191
+ expect(mockSpan.getAttributeValue('gen_ai.usage.total_tokens')).toBe(300);
192
+ expect(mockSpan.getAttributeValue('gen_ai.usage.prompt_tokens')).toBe(100);
193
+ expect(mockSpan.getAttributeValue('gen_ai.usage.completion_tokens')).toBe(200);
194
+ });
195
+ it('adds response event with stable conventions', () => {
196
+ const tracer = new Tracer();
197
+ const span = tracer.startAgentSpan({ messages: [textMessage('user', 'Hi')], agentName: 'agent' });
198
+ const response = new Message({ role: 'assistant', content: [new TextBlock('Hello back')] });
199
+ tracer.endAgentSpan(span, { response, stopReason: 'end_turn' });
200
+ const choiceEvents = mockSpan.getEvents('gen_ai.choice');
201
+ expect(choiceEvents).toHaveLength(1);
202
+ expect(eventAttr(choiceEvents[0], 'message')).toBe('Hello back');
203
+ expect(eventAttr(choiceEvents[0], 'finish_reason')).toBe('end_turn');
204
+ });
205
+ it('adds response event with latest conventions', () => {
206
+ vi.stubEnv('OTEL_SEMCONV_STABILITY_OPT_IN', 'gen_ai_latest_experimental');
207
+ const tracer = new Tracer();
208
+ const span = tracer.startAgentSpan({ messages: [textMessage('user', 'Hi')], agentName: 'agent' });
209
+ const response = new Message({ role: 'assistant', content: [new TextBlock('Hello back')] });
210
+ tracer.endAgentSpan(span, { response, stopReason: 'end_turn' });
211
+ const detailEvents = mockSpan.getEvents('gen_ai.client.inference.operation.details');
212
+ const outputEvent = detailEvents.find((e) => eventAttr(e, 'gen_ai.output.messages'));
213
+ expect(outputEvent).toBeDefined();
214
+ const parsed = JSON.parse(eventAttr(outputEvent, 'gen_ai.output.messages'));
215
+ expect(parsed).toStrictEqual([
216
+ { role: 'assistant', parts: [{ type: 'text', content: 'Hello back' }], finish_reason: 'end_turn' },
217
+ ]);
218
+ });
219
+ it('handles null span gracefully', () => {
220
+ const tracer = new Tracer();
221
+ expect(() => tracer.endAgentSpan(null)).not.toThrow();
222
+ expect(mockSpan.calls.end).toHaveLength(0);
223
+ });
224
+ });
225
+ describe('startModelInvokeSpan', () => {
226
+ it('creates span with chat operation name and model id', () => {
227
+ const tracer = new Tracer();
228
+ tracer.startModelInvokeSpan({ messages: [textMessage('user', 'Hello')], modelId: 'claude-3' });
229
+ const [spanName, options] = getStartSpanCall();
230
+ expect(spanName).toBe('chat');
231
+ expect(options.attributes).toMatchObject({
232
+ 'gen_ai.operation.name': 'chat',
233
+ 'gen_ai.request.model': 'claude-3',
234
+ });
235
+ });
236
+ it('adds message events to span', () => {
237
+ const tracer = new Tracer();
238
+ tracer.startModelInvokeSpan({ messages: [textMessage('user', 'Hello')] });
239
+ expect(mockSpan.getEvents('gen_ai.user.message')).toHaveLength(1);
240
+ });
241
+ });
242
+ describe('endModelInvokeSpan', () => {
243
+ it('sets usage and metrics attributes', () => {
244
+ const tracer = new Tracer();
245
+ const span = tracer.startModelInvokeSpan({ messages: [textMessage('user', 'Hi')], modelId: 'model-1' });
246
+ tracer.endModelInvokeSpan(span, {
247
+ usage: { inputTokens: 10, outputTokens: 20, totalTokens: 30 },
248
+ metrics: { latencyMs: 500 },
249
+ });
250
+ expect(mockSpan.getAttributeValue('gen_ai.usage.input_tokens')).toBe(10);
251
+ expect(mockSpan.getAttributeValue('gen_ai.usage.output_tokens')).toBe(20);
252
+ expect(mockSpan.getAttributeValue('gen_ai.usage.total_tokens')).toBe(30);
253
+ expect(mockSpan.getAttributeValue('gen_ai.server.request.duration')).toBe(500);
254
+ });
255
+ it('sets cache token attributes when provided', () => {
256
+ const tracer = new Tracer();
257
+ const span = tracer.startModelInvokeSpan({ messages: [textMessage('user', 'Hi')] });
258
+ tracer.endModelInvokeSpan(span, {
259
+ usage: {
260
+ inputTokens: 100,
261
+ outputTokens: 200,
262
+ totalTokens: 300,
263
+ cacheReadInputTokens: 50,
264
+ cacheWriteInputTokens: 25,
265
+ },
266
+ });
267
+ expect(mockSpan.getAttributeValue('gen_ai.usage.cache_read_input_tokens')).toBe(50);
268
+ expect(mockSpan.getAttributeValue('gen_ai.usage.cache_write_input_tokens')).toBe(25);
269
+ });
270
+ it('skips cache token attributes when zero', () => {
271
+ const tracer = new Tracer();
272
+ const span = tracer.startModelInvokeSpan({ messages: [textMessage('user', 'Hi')] });
273
+ tracer.endModelInvokeSpan(span, {
274
+ usage: { inputTokens: 10, outputTokens: 20, totalTokens: 30, cacheReadInputTokens: 0 },
275
+ });
276
+ expect(mockSpan.getAttributeValue('gen_ai.usage.cache_read_input_tokens')).toBeUndefined();
277
+ });
278
+ it('skips latency attribute when zero', () => {
279
+ const tracer = new Tracer();
280
+ const span = tracer.startModelInvokeSpan({ messages: [textMessage('user', 'Hi')] });
281
+ tracer.endModelInvokeSpan(span, {
282
+ usage: { inputTokens: 10, outputTokens: 20, totalTokens: 30 },
283
+ metrics: { latencyMs: 0 },
284
+ });
285
+ expect(mockSpan.getAttributeValue('gen_ai.server.request.duration')).toBeUndefined();
286
+ });
287
+ it('adds output event with stable conventions for mixed content', () => {
288
+ const tracer = new Tracer();
289
+ const span = tracer.startModelInvokeSpan({ messages: [textMessage('user', 'Hi')] });
290
+ const output = new Message({
291
+ role: 'assistant',
292
+ content: [
293
+ new TextBlock('The answer is 42'),
294
+ new ToolUseBlock({ name: 'calc', toolUseId: 'tool-1', input: { expr: '6*7' } }),
295
+ ],
296
+ });
297
+ tracer.endModelInvokeSpan(span, { output, stopReason: 'tool_use' });
298
+ const choiceEvents = mockSpan.getEvents('gen_ai.choice');
299
+ expect(choiceEvents).toHaveLength(1);
300
+ expect(eventAttr(choiceEvents[0], 'finish_reason')).toBe('tool_use');
301
+ const parsed = JSON.parse(eventAttr(choiceEvents[0], 'message'));
302
+ expect(parsed).toStrictEqual([
303
+ { text: 'The answer is 42' },
304
+ { type: 'toolUse', name: 'calc', toolUseId: 'tool-1', input: { expr: '6*7' } },
305
+ ]);
306
+ });
307
+ it('adds output event with latest conventions for mixed content', () => {
308
+ vi.stubEnv('OTEL_SEMCONV_STABILITY_OPT_IN', 'gen_ai_latest_experimental');
309
+ const tracer = new Tracer();
310
+ const span = tracer.startModelInvokeSpan({ messages: [textMessage('user', 'Hi')] });
311
+ const output = new Message({
312
+ role: 'assistant',
313
+ content: [
314
+ new TextBlock('The answer'),
315
+ new ToolUseBlock({ name: 'calc', toolUseId: 'tool-1', input: { x: 1 } }),
316
+ ],
317
+ });
318
+ tracer.endModelInvokeSpan(span, { output, stopReason: 'tool_use' });
319
+ const detailEvents = mockSpan.getEvents('gen_ai.client.inference.operation.details');
320
+ const outputEvent = detailEvents.find((e) => eventAttr(e, 'gen_ai.output.messages'));
321
+ expect(outputEvent).toBeDefined();
322
+ const parsed = JSON.parse(eventAttr(outputEvent, 'gen_ai.output.messages'));
323
+ expect(parsed).toStrictEqual([
324
+ {
325
+ role: 'assistant',
326
+ parts: [
327
+ { type: 'text', content: 'The answer' },
328
+ { type: 'tool_call', name: 'calc', id: 'tool-1', arguments: { x: 1 } },
329
+ ],
330
+ finish_reason: 'tool_use',
331
+ },
332
+ ]);
333
+ });
334
+ it('records error on model invocation failure', () => {
335
+ const tracer = new Tracer();
336
+ const span = tracer.startModelInvokeSpan({ messages: [textMessage('user', 'Hi')] });
337
+ const error = new Error('model timeout');
338
+ tracer.endModelInvokeSpan(span, { error });
339
+ expect(mockSpan.calls.setStatus).toContainEqual({
340
+ status: { code: SpanStatusCode.ERROR, message: 'model timeout' },
341
+ });
342
+ expect(mockSpan.calls.recordException).toContainEqual({ exception: error, time: undefined });
343
+ });
344
+ it('handles null span gracefully', () => {
345
+ const tracer = new Tracer();
346
+ expect(() => tracer.endModelInvokeSpan(null)).not.toThrow();
347
+ });
348
+ });
349
+ describe('startToolCallSpan', () => {
350
+ it('creates span with tool name and call id', () => {
351
+ const tracer = new Tracer();
352
+ tracer.startToolCallSpan({
353
+ tool: { name: 'calculator', toolUseId: 'call-1', input: { expr: '2+2' } },
354
+ });
355
+ const [spanName, options] = getStartSpanCall();
356
+ expect(spanName).toBe('execute_tool calculator');
357
+ expect(options.attributes).toMatchObject({
358
+ 'gen_ai.operation.name': 'execute_tool',
359
+ 'gen_ai.tool.name': 'calculator',
360
+ 'gen_ai.tool.call.id': 'call-1',
361
+ });
362
+ });
363
+ it('adds stable tool message event with serialized input', () => {
364
+ const tracer = new Tracer();
365
+ tracer.startToolCallSpan({
366
+ tool: { name: 'search', toolUseId: 'call-2', input: { query: 'test' } },
367
+ });
368
+ const toolEvents = mockSpan.getEvents('gen_ai.tool.message');
369
+ expect(toolEvents).toHaveLength(1);
370
+ expect(eventAttr(toolEvents[0], 'role')).toBe('tool');
371
+ expect(eventAttr(toolEvents[0], 'content')).toBe('{"query":"test"}');
372
+ expect(eventAttr(toolEvents[0], 'id')).toBe('call-2');
373
+ });
374
+ it('adds latest convention tool input event', () => {
375
+ vi.stubEnv('OTEL_SEMCONV_STABILITY_OPT_IN', 'gen_ai_latest_experimental');
376
+ const tracer = new Tracer();
377
+ tracer.startToolCallSpan({
378
+ tool: { name: 'search', toolUseId: 'call-2', input: { query: 'test' } },
379
+ });
380
+ const detailEvents = mockSpan.getEvents('gen_ai.client.inference.operation.details');
381
+ expect(detailEvents).toHaveLength(1);
382
+ const parsed = JSON.parse(eventAttr(detailEvents[0], 'gen_ai.input.messages'));
383
+ expect(parsed).toStrictEqual([
384
+ {
385
+ role: 'tool',
386
+ parts: [{ type: 'tool_call', name: 'search', id: 'call-2', arguments: { query: 'test' } }],
387
+ },
388
+ ]);
389
+ });
390
+ });
391
+ describe('endToolCallSpan', () => {
392
+ it('sets tool status attribute and adds stable result event', () => {
393
+ const tracer = new Tracer();
394
+ const span = tracer.startToolCallSpan({
395
+ tool: { name: 'calc', toolUseId: 'call-1', input: {} },
396
+ });
397
+ const toolResult = new ToolResultBlock({
398
+ toolUseId: 'call-1',
399
+ status: 'success',
400
+ content: [new TextBlock('42')],
401
+ });
402
+ tracer.endToolCallSpan(span, { toolResult });
403
+ expect(mockSpan.getAttributeValue('gen_ai.tool.status')).toBe('success');
404
+ const choiceEvents = mockSpan.getEvents('gen_ai.choice');
405
+ expect(choiceEvents).toHaveLength(1);
406
+ expect(eventAttr(choiceEvents[0], 'id')).toBe('call-1');
407
+ });
408
+ it('adds latest convention tool result event', () => {
409
+ vi.stubEnv('OTEL_SEMCONV_STABILITY_OPT_IN', 'gen_ai_latest_experimental');
410
+ const tracer = new Tracer();
411
+ const span = tracer.startToolCallSpan({
412
+ tool: { name: 'calc', toolUseId: 'call-1', input: {} },
413
+ });
414
+ const toolResult = new ToolResultBlock({
415
+ toolUseId: 'call-1',
416
+ status: 'success',
417
+ content: [new TextBlock('42')],
418
+ });
419
+ tracer.endToolCallSpan(span, { toolResult });
420
+ const detailEvents = mockSpan.getEvents('gen_ai.client.inference.operation.details');
421
+ const outputEvent = detailEvents.find((e) => eventAttr(e, 'gen_ai.output.messages'));
422
+ expect(outputEvent).toBeDefined();
423
+ const parsed = JSON.parse(eventAttr(outputEvent, 'gen_ai.output.messages'));
424
+ expect(parsed[0].role).toBe('tool');
425
+ expect(parsed[0].parts[0].type).toBe('tool_call_response');
426
+ expect(parsed[0].parts[0].id).toBe('call-1');
427
+ });
428
+ it('records error on tool failure', () => {
429
+ const tracer = new Tracer();
430
+ const span = tracer.startToolCallSpan({
431
+ tool: { name: 'calc', toolUseId: 'call-1', input: {} },
432
+ });
433
+ const error = new Error('tool crashed');
434
+ tracer.endToolCallSpan(span, { error });
435
+ expect(mockSpan.calls.setStatus).toContainEqual({
436
+ status: { code: SpanStatusCode.ERROR, message: 'tool crashed' },
437
+ });
438
+ expect(mockSpan.calls.recordException).toContainEqual({ exception: error, time: undefined });
439
+ });
440
+ it('handles null span gracefully', () => {
441
+ const tracer = new Tracer();
442
+ expect(() => tracer.endToolCallSpan(null)).not.toThrow();
443
+ });
444
+ });
445
+ describe('startAgentLoopSpan', () => {
446
+ it('creates span with cycle id attribute', () => {
447
+ const tracer = new Tracer();
448
+ tracer.startAgentLoopSpan({ cycleId: 'cycle-42', messages: [textMessage('user', 'Hi')] });
449
+ const [spanName, options] = getStartSpanCall();
450
+ expect(spanName).toBe('execute_agent_loop_cycle');
451
+ expect(options.attributes['agent_loop.cycle_id']).toBe('cycle-42');
452
+ });
453
+ it('adds message events to loop span', () => {
454
+ const tracer = new Tracer();
455
+ tracer.startAgentLoopSpan({ cycleId: 'cycle-1', messages: [textMessage('user', 'Hello')] });
456
+ expect(mockSpan.getEvents('gen_ai.user.message')).toHaveLength(1);
457
+ });
458
+ });
459
+ describe('endAgentLoopSpan', () => {
460
+ it('ends span with OK status', () => {
461
+ const tracer = new Tracer();
462
+ const span = tracer.startAgentLoopSpan({ cycleId: 'cycle-1', messages: [textMessage('user', 'Hi')] });
463
+ tracer.endAgentLoopSpan(span);
464
+ expect(mockSpan.calls.setStatus).toContainEqual({ status: { code: SpanStatusCode.OK } });
465
+ expect(mockSpan.calls.end).toHaveLength(1);
466
+ });
467
+ it('records error on loop failure', () => {
468
+ const tracer = new Tracer();
469
+ const span = tracer.startAgentLoopSpan({ cycleId: 'cycle-1', messages: [textMessage('user', 'Hi')] });
470
+ const error = new Error('loop failed');
471
+ tracer.endAgentLoopSpan(span, { error });
472
+ expect(mockSpan.calls.setStatus).toContainEqual({
473
+ status: { code: SpanStatusCode.ERROR, message: 'loop failed' },
474
+ });
475
+ expect(mockSpan.calls.recordException).toContainEqual({ exception: error, time: undefined });
476
+ });
477
+ it('handles null span gracefully', () => {
478
+ const tracer = new Tracer();
479
+ expect(() => tracer.endAgentLoopSpan(null)).not.toThrow();
480
+ });
481
+ });
482
+ describe('withSpanContext', () => {
483
+ it('executes callback directly when span is null', () => {
484
+ const tracer = new Tracer();
485
+ const fn = vi.fn(() => 'result');
486
+ const result = tracer.withSpanContext(null, fn);
487
+ expect(result).toBe('result');
488
+ expect(fn).toHaveBeenCalledOnce();
489
+ expect(context.with).not.toHaveBeenCalled();
490
+ });
491
+ it('executes callback within span context when span is provided', () => {
492
+ const tracer = new Tracer();
493
+ const span = tracer.startAgentSpan({ messages: [textMessage('user', 'Hi')], agentName: 'agent' });
494
+ const mockContext = { spanContext: true };
495
+ vi.mocked(trace.setSpan).mockReturnValue(mockContext);
496
+ tracer.withSpanContext(span, () => 'inside');
497
+ expect(trace.setSpan).toHaveBeenCalledWith({}, span);
498
+ expect(context.with).toHaveBeenCalledWith(mockContext, expect.any(Function));
499
+ });
500
+ it('propagates return value from callback', () => {
501
+ const tracer = new Tracer();
502
+ const span = tracer.startAgentSpan({ messages: [textMessage('user', 'Hi')], agentName: 'agent' });
503
+ const result = tracer.withSpanContext(span, () => 42);
504
+ expect(result).toBe(42);
505
+ });
506
+ });
507
+ describe('message event formatting', () => {
508
+ it('maps tool use blocks to tool_call parts in latest conventions', () => {
509
+ vi.stubEnv('OTEL_SEMCONV_STABILITY_OPT_IN', 'gen_ai_latest_experimental');
510
+ const tracer = new Tracer();
511
+ const messages = [
512
+ new Message({
513
+ role: 'assistant',
514
+ content: [new ToolUseBlock({ name: 'search', toolUseId: 'tu-1', input: { q: 'test' } })],
515
+ }),
516
+ ];
517
+ tracer.startAgentSpan({ messages, agentName: 'agent' });
518
+ const detailEvents = mockSpan.getEvents('gen_ai.client.inference.operation.details');
519
+ const parsed = JSON.parse(eventAttr(detailEvents[0], 'gen_ai.input.messages'));
520
+ expect(parsed[0].parts[0]).toStrictEqual({
521
+ type: 'tool_call',
522
+ name: 'search',
523
+ id: 'tu-1',
524
+ arguments: { q: 'test' },
525
+ });
526
+ });
527
+ it('maps tool result blocks to tool_call_response parts in latest conventions', () => {
528
+ vi.stubEnv('OTEL_SEMCONV_STABILITY_OPT_IN', 'gen_ai_latest_experimental');
529
+ const tracer = new Tracer();
530
+ const messages = [
531
+ new Message({
532
+ role: 'user',
533
+ content: [new ToolResultBlock({ toolUseId: 'tu-1', status: 'success', content: [new TextBlock('result')] })],
534
+ }),
535
+ ];
536
+ tracer.startAgentSpan({ messages, agentName: 'agent' });
537
+ const detailEvents = mockSpan.getEvents('gen_ai.client.inference.operation.details');
538
+ const parsed = JSON.parse(eventAttr(detailEvents[0], 'gen_ai.input.messages'));
539
+ expect(parsed[0].parts[0].type).toBe('tool_call_response');
540
+ expect(parsed[0].parts[0].id).toBe('tu-1');
541
+ });
542
+ it('serializes text block content in stable convention events', () => {
543
+ const tracer = new Tracer();
544
+ tracer.startModelInvokeSpan({ messages: [textMessage('user', 'Hello world')] });
545
+ const userEvents = mockSpan.getEvents('gen_ai.user.message');
546
+ const parsed = JSON.parse(eventAttr(userEvents[0], 'content'));
547
+ expect(parsed[0].text).toBe('Hello world');
548
+ });
549
+ });
550
+ describe('error resilience', () => {
551
+ it.each([
552
+ {
553
+ method: 'startAgentSpan',
554
+ call: (tracer) => tracer.startAgentSpan({ messages: [textMessage('user', 'Hi')], agentName: 'agent' }),
555
+ },
556
+ {
557
+ method: 'startModelInvokeSpan',
558
+ call: (tracer) => tracer.startModelInvokeSpan({ messages: [textMessage('user', 'Hi')] }),
559
+ },
560
+ {
561
+ method: 'startToolCallSpan',
562
+ call: (tracer) => tracer.startToolCallSpan({ tool: { name: 'x', toolUseId: 'y', input: {} } }),
563
+ },
564
+ {
565
+ method: 'startAgentLoopSpan',
566
+ call: (tracer) => tracer.startAgentLoopSpan({ cycleId: 'c', messages: [textMessage('user', 'Hi')] }),
567
+ },
568
+ ])('returns null when $method throws internally', ({ call }) => {
569
+ mockStartSpan.mockImplementation(() => {
570
+ throw new Error('otel failure');
571
+ });
572
+ const tracer = new Tracer();
573
+ expect(call(tracer)).toBeNull();
574
+ });
575
+ it('does not throw when ending null spans with errors', () => {
576
+ const tracer = new Tracer();
577
+ expect(() => {
578
+ tracer.endAgentSpan(null, { error: new Error('test') });
579
+ tracer.endModelInvokeSpan(null, { error: new Error('test') });
580
+ tracer.endToolCallSpan(null, { error: new Error('test') });
581
+ tracer.endAgentLoopSpan(null, { error: new Error('test') });
582
+ }).not.toThrow();
583
+ });
584
+ });
585
+ describe('semantic convention opt-in parsing', () => {
586
+ it('parses multiple comma-separated opt-in values', () => {
587
+ vi.stubEnv('OTEL_SEMCONV_STABILITY_OPT_IN', 'gen_ai_latest_experimental,gen_ai_tool_definitions');
588
+ const tracer = new Tracer();
589
+ const toolsConfig = { calc: { name: 'calc', description: 'Calculator' } };
590
+ tracer.startAgentSpan({ messages: [textMessage('user', 'Hi')], agentName: 'agent', toolsConfig });
591
+ const [, options] = getStartSpanCall();
592
+ expect(options.attributes['gen_ai.provider.name']).toBeDefined();
593
+ expect(options.attributes['gen_ai.tool.definitions']).toBe(JSON.stringify(toolsConfig));
594
+ });
595
+ it('handles whitespace in opt-in values', () => {
596
+ vi.stubEnv('OTEL_SEMCONV_STABILITY_OPT_IN', ' gen_ai_latest_experimental , gen_ai_tool_definitions ');
597
+ const tracer = new Tracer();
598
+ tracer.startAgentSpan({ messages: [textMessage('user', 'Hi')], agentName: 'agent' });
599
+ const [, options] = getStartSpanCall();
600
+ expect(options.attributes['gen_ai.provider.name']).toBeDefined();
601
+ });
602
+ it('defaults to stable conventions when env var is empty', () => {
603
+ const tracer = new Tracer();
604
+ tracer.startAgentSpan({ messages: [textMessage('user', 'Hi')], agentName: 'agent' });
605
+ const [, options] = getStartSpanCall();
606
+ expect(options.attributes['gen_ai.system']).toBeDefined();
607
+ expect(options.attributes['gen_ai.provider.name']).toBeUndefined();
608
+ });
609
+ });
610
+ });
611
+ //# sourceMappingURL=tracer.test.node.js.map