@struct-ai/sdk 0.3.0 → 0.4.2

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 (58) hide show
  1. package/README.md +101 -16
  2. package/dist/commonjs/context.d.ts +45 -0
  3. package/dist/commonjs/context.js +78 -1
  4. package/dist/commonjs/core.js +184 -29
  5. package/dist/commonjs/events.d.ts +17 -6
  6. package/dist/commonjs/events.js +82 -59
  7. package/dist/commonjs/genai-content.d.ts +52 -0
  8. package/dist/commonjs/genai-content.js +143 -0
  9. package/dist/commonjs/instrument.d.ts +47 -0
  10. package/dist/commonjs/instrument.js +158 -0
  11. package/dist/commonjs/integrations/anthropic-content.js +18 -6
  12. package/dist/commonjs/integrations/anthropic.d.ts +8 -1
  13. package/dist/commonjs/integrations/anthropic.js +515 -104
  14. package/dist/commonjs/integrations/index.js +8 -0
  15. package/dist/commonjs/integrations/langchain-callback.d.ts +182 -27
  16. package/dist/commonjs/integrations/langchain-callback.js +754 -87
  17. package/dist/commonjs/integrations/langchain-content.js +1 -1
  18. package/dist/commonjs/integrations/langchain.d.ts +3 -0
  19. package/dist/commonjs/integrations/langchain.js +353 -7
  20. package/dist/commonjs/integrations/openai-content.d.ts +34 -0
  21. package/dist/commonjs/integrations/openai-content.js +375 -0
  22. package/dist/commonjs/integrations/openai.d.ts +39 -0
  23. package/dist/commonjs/integrations/openai.js +305 -0
  24. package/dist/commonjs/semconv.d.ts +12 -0
  25. package/dist/commonjs/semconv.js +13 -1
  26. package/dist/commonjs/truncation.d.ts +29 -0
  27. package/dist/commonjs/truncation.js +184 -10
  28. package/dist/commonjs/version.d.ts +2 -0
  29. package/dist/commonjs/version.js +6 -0
  30. package/dist/esm/context.d.ts +45 -0
  31. package/dist/esm/context.js +74 -1
  32. package/dist/esm/core.js +185 -30
  33. package/dist/esm/events.d.ts +17 -6
  34. package/dist/esm/events.js +82 -61
  35. package/dist/esm/genai-content.d.ts +52 -0
  36. package/dist/esm/genai-content.js +137 -0
  37. package/dist/esm/instrument.d.ts +47 -0
  38. package/dist/esm/instrument.js +155 -0
  39. package/dist/esm/integrations/anthropic-content.js +19 -7
  40. package/dist/esm/integrations/anthropic.d.ts +8 -1
  41. package/dist/esm/integrations/anthropic.js +514 -107
  42. package/dist/esm/integrations/index.js +8 -0
  43. package/dist/esm/integrations/langchain-callback.d.ts +182 -27
  44. package/dist/esm/integrations/langchain-callback.js +756 -89
  45. package/dist/esm/integrations/langchain-content.js +1 -1
  46. package/dist/esm/integrations/langchain.d.ts +3 -0
  47. package/dist/esm/integrations/langchain.js +352 -7
  48. package/dist/esm/integrations/openai-content.d.ts +34 -0
  49. package/dist/esm/integrations/openai-content.js +360 -0
  50. package/dist/esm/integrations/openai.d.ts +39 -0
  51. package/dist/esm/integrations/openai.js +296 -0
  52. package/dist/esm/semconv.d.ts +12 -0
  53. package/dist/esm/semconv.js +12 -0
  54. package/dist/esm/truncation.d.ts +29 -0
  55. package/dist/esm/truncation.js +182 -10
  56. package/dist/esm/version.d.ts +2 -0
  57. package/dist/esm/version.js +3 -0
  58. package/package.json +11 -3
package/README.md CHANGED
@@ -1,8 +1,9 @@
1
1
  # @struct-ai/sdk
2
2
 
3
3
  Struct agent observability SDK for TypeScript/Node.js. Auto-instruments AI agent
4
- frameworks — the Anthropic SDK and LangChain.js — and emits OpenTelemetry
5
- traces + logs to [struct.ai](https://struct.ai) with zero config.
4
+ frameworks and LLM SDKs — the Anthropic SDK, the OpenAI SDK (Responses API),
5
+ and LangChain.js — and emits OpenTelemetry traces + logs to
6
+ [struct.ai](https://struct.ai) with zero config.
6
7
 
7
8
  This is the TypeScript port of [`struct-sdk`](https://pypi.org/project/struct-sdk/)
8
9
  (Python). Span names, attribute keys, and log event shapes are identical across
@@ -13,7 +14,7 @@ the two SDKs so the server processes both uniformly.
13
14
  ```bash
14
15
  npm install @struct-ai/sdk
15
16
  # optional — the SDK auto-instruments these if present
16
- npm install @anthropic-ai/sdk @langchain/core @langchain/langgraph
17
+ npm install @anthropic-ai/sdk openai @langchain/core @langchain/langgraph
17
18
  ```
18
19
 
19
20
  Requires Node 18+.
@@ -55,9 +56,10 @@ await struct.agent({ name: "checkout" }, async () => {
55
56
 
56
57
  | Library | Hook | Span type | Notes |
57
58
  |---|---|---|---|
58
- | `@anthropic-ai/sdk` | `Messages.prototype.create`, `.stream` | `chat {model}` | Cache-token accounting, streaming with tool-use reconstruction |
59
+ | `@anthropic-ai/sdk` | `Messages.prototype.create`, `.stream` | `chat {model}` | Cache-token accounting, streaming with tool-use reconstruction. Defers to a LangChain `BaseChatModel` call already in progress (e.g. `ChatAnthropic`) so the two integrations don't double-emit — see ownership order below |
59
60
  | `@anthropic-ai/bedrock-sdk`, `@anthropic-ai/vertex-sdk` | `Messages.prototype.*` | `chat {model}` | Best-effort, if installed |
60
- | `@langchain/core` `BaseChatModel` | `.invoke`, `.stream` | `chat {model}` | Skipped when a provider-direct instrumentor is active (e.g. ChatAnthropic + Anthropic patch → single span) |
61
+ | `openai` | `Responses.prototype.create` | `chat {model}` | **Responses API, non-streaming calls only** — `responses.create({stream: true})` and `responses.stream()` pass through untouched (no span), and `chat.completions` is not instrumented. Cache-read/cache-write/reasoning token accounting; `function_call` → `struct.tool()` id auto-linkage. Requires an openai release with the Responses API (v4 releases without it are a graceful no-op). Defers to a LangChain call in progress, same as Anthropic |
62
+ | `@langchain/core` `BaseChatModel` | `.invoke`, `.stream` | `chat {model}` | Always owns the `chat` span for `ChatAnthropic`/etc. calls, even when a provider-direct instrumentor (e.g. the Anthropic patch) is also active — the framework layer outranks the provider layer (single span; see [AGENTS.md](./AGENTS.md) §5) |
61
63
  | `@langchain/core` `StructuredTool` | `.invoke` | `execute_tool {name}` | Extracts `tool_call_id` from LangChain ToolCall input or pending queue |
62
64
  | `@langchain/core` `BaseRetriever` | `.invoke` | `retrieval {name}` | |
63
65
  | `@langchain/langgraph` `Pregel` | `.invoke`, `.stream` | `invoke_agent {name}` | Covers `createReactAgent` and custom graphs. Reads conversation id from any of: `configurable.thread_id` (LangGraph canonical), or `metadata.{thread_id, session_id, conversation_id}` (LangSmith conventions). For multi-turn HTTP-style threading, wrap your entry point in [`struct.agent({ sessionId: convId }, ...)`](#recommended-pattern-wrap-langchain-entry-points-in-structagent) — the struct-native replacement for LangSmith's `tracing_context(parent=run_tree)`. |
@@ -166,6 +168,37 @@ await struct.agent({ name: "checkout" }, async () => {
166
168
  `@anthropic-ai/sdk`, `@anthropic-ai/bedrock-sdk`, and `@anthropic-ai/vertex-sdk`
167
169
  are all auto-instrumented for chat spans.
168
170
 
171
+ #### OpenAI SDK (raw, Responses API)
172
+
173
+ Same rule as raw Anthropic — chat spans emit automatically; wrap your agent
174
+ loop and tools yourself:
175
+
176
+ ```ts
177
+ import { struct } from "@struct-ai/sdk";
178
+ struct.init({ ingestKey: "pk-...", serviceName: "support-agent" });
179
+
180
+ import OpenAI from "openai";
181
+ const client = new OpenAI();
182
+
183
+ await struct.agent({ name: "support" }, async () => {
184
+ const resp = await client.responses.create({
185
+ model: "gpt-5.5",
186
+ input: "triage this ticket",
187
+ });
188
+
189
+ // tool_call_id is auto-filled from the preceding response's function_call.
190
+ await struct.tool({ name: "lookup_order" }, async () => {
191
+ return await lookupOrder(resp);
192
+ });
193
+ });
194
+ ```
195
+
196
+ **Scope:** the OpenAI integration covers **non-streaming `responses.create()`**
197
+ only. Streaming calls (`stream: true` / `responses.stream()`) run unmodified and
198
+ emit no span, and `chat.completions` is not instrumented. Azure OpenAI clients
199
+ (`AzureOpenAI`) share the same `Responses` class and are covered by the same
200
+ patch.
201
+
169
202
  #### LangChain `BaseChatModel` (no agent/graph)
170
203
 
171
204
  If you call `ChatAnthropic.invoke(...)` (or any other `BaseChatModel`)
@@ -189,8 +222,8 @@ await struct.agent({ name: "my-agent" }, async () => {
189
222
  ```
190
223
 
191
224
  When you do use `ChatAnthropic` *and* have `@anthropic-ai/sdk` installed,
192
- the chat span comes from the Anthropic patch (single span); the LangChain
193
- layer suppresses its duplicate.
225
+ the chat span comes from the LangChain layer (single span); the Anthropic
226
+ patch detects the framework already owns the call and defers.
194
227
 
195
228
  ## Content capture
196
229
 
@@ -206,13 +239,13 @@ struct.init({
206
239
  ```
207
240
 
208
241
  - **`EventOnly`** (default): per-message content lands on OTel log records
209
- (`gen_ai.{user,assistant,system,tool}.message`, `gen_ai.choice`). Spans carry
210
- metadata only.
242
+ (`gen_ai.{user,assistant,system,tool}.message`, `gen_ai.choice`).
211
243
  - **`SpanOnly`**: content on span attributes (`gen_ai.input.messages`,
212
244
  `gen_ai.output.messages`).
213
245
  - **`SpanAndEvent`**: both.
214
- - **`None`**: no content captured. Token counts, tool call IDs, finish reasons,
215
- and other metadata still flow.
246
+ - **`None`**: no content captured, anywhere — no log events, no span content
247
+ attributes. Token counts, tool call IDs, finish reasons, and other metadata
248
+ still flow.
216
249
 
217
250
  Set `captureContent: false` for the legacy bool API (equivalent to
218
251
  `ContentCaptureMode.None`).
@@ -252,7 +285,7 @@ the troubleshooting section below for the workaround.
252
285
  Emits attributes per the OTel GenAI semantic conventions:
253
286
 
254
287
  - `gen_ai.operation.name` — `chat`, `execute_tool`, `invoke_agent`, `retrieval`
255
- - `gen_ai.provider.name` — `anthropic`, `openai`, `langchain`, `struct`, …
288
+ - `gen_ai.provider.name` — the GenAI provider on inference spans (`anthropic`, `openai`, …); platform-routed calls report the platform value (`aws.bedrock`, `gcp.vertex_ai`, `azure.ai.openai`) when detectable from the client. `invoke_agent` spans inherit the provider from their first child inference call (omitted when no model is reached); `execute_tool`/`retrieval` spans carry no provider.
256
289
  - `gen_ai.request.{model, max_tokens, temperature, top_p, top_k, stop_sequences}`
257
290
  - `gen_ai.response.{model, id, finish_reasons}`
258
291
  - `gen_ai.usage.{input_tokens, output_tokens, cache_read.input_tokens, cache_creation.input_tokens}`
@@ -264,6 +297,48 @@ Note: `gen_ai.usage.input_tokens` for Anthropic is the TRUE total — we add bac
264
297
  `cache_read_input_tokens + cache_creation_input_tokens` (which Anthropic's raw
265
298
  response excludes). Matches the Python SDK.
266
299
 
300
+ ## Development
301
+
302
+ - `pnpm test` — unit + e2e tests (mocked, no network, no API keys). Excludes
303
+ `test/live/**`.
304
+ - `pnpm typecheck` — `tsc --noEmit`.
305
+ - `pnpm build` — `tshy`, producing the dual ESM/CJS `dist/`.
306
+ - `pnpm test:live` — the live real-model suite: real provider SDKs
307
+ (`@anthropic-ai/sdk`, `openai`) and frameworks (`@langchain/*`) against the
308
+ real Anthropic / OpenAI APIs, verifying the emitted spans + log events
309
+ **in-memory** (no ingester needed). Each gated block skips cleanly (exit 0)
310
+ when its API key is absent — safe to leave in normal CI.
311
+
312
+ To run it, point `STRUCT_LIVE_ENV_FILE` at a file with the keys you want to
313
+ exercise (`ANTHROPIC_API_KEY=...` and/or `OPENAI_API_KEY=...`; dotenv-style
314
+ `KEY=value` lines, quotes optional). The path is never committed — it's
315
+ supplied per-invocation:
316
+
317
+ ```bash
318
+ STRUCT_LIVE_ENV_FILE=/path/to/your/.env pnpm test:live
319
+ ```
320
+
321
+ Only blocks whose key is present run, so a file with both keys runs both the
322
+ Anthropic and OpenAI suites (a handful of small real calls each — cheap model
323
+ aliases, small token budgets).
324
+
325
+ **Full-pipeline e2e** (`test/live/openai-ingest-e2e.live.test.ts`)
326
+ additionally ships the emitted telemetry to a *real* ingest endpoint and
327
+ flushes it, so you can confirm a change travels the whole path
328
+ (SDK → OTLP export → your Struct instance). It's gated on `STRUCT_INGEST_URL`
329
+ + `STRUCT_INGEST_KEY` (on top of `OPENAI_API_KEY`), so it stays skipped unless
330
+ you deliberately supply real ingest credentials. It still self-asserts the
331
+ span shape in-memory; confirming the span actually *landed* means querying
332
+ your Struct instance's telemetry — see [AGENTS.md](./AGENTS.md).
333
+ - `pnpm test:parity` — the cross-language conformance harness: drives the
334
+ same canonical agent topology through both this SDK and `struct-sdk-python`
335
+ and diffs their normalized span shapes, failing (non-zero exit) on any
336
+ divergence. No network calls or API keys required.
337
+
338
+ See [AGENTS.md](./AGENTS.md) for the full set of governance rules (fault
339
+ isolation, OTel citizenship, semconv conformance, the release-version-bump
340
+ gate) that apply to any change in this package.
341
+
267
342
  ## Troubleshooting
268
343
 
269
344
  - **Spans missing after instrumenting:** Import `@struct-ai/sdk` (or `struct.init()`)
@@ -273,10 +348,20 @@ response excludes). Matches the Python SDK.
273
348
  - **No logs appearing:** `LogRecord`s only emit when `sdk.emitEvents` is true
274
349
  (`EventOnly` or `SpanAndEvent` capture mode, which is the default). If you set
275
350
  `captureContent: false` you disable them.
276
- - **Duplicate chat spans:** The LangChain integration suppresses its own chat
277
- span when a provider-direct patch is active (e.g. ChatAnthropic calls through
278
- to `@anthropic-ai/sdk`, which emits its own `chat` span). If you see doubles,
279
- confirm both integrations are auto-instrumenting (check `struct.initialized`).
351
+ - **Duplicate chat spans:** Ownership of the `chat` span is fixed (manual >
352
+ framework > provider — see [AGENTS.md](./AGENTS.md) §5): when you call
353
+ `ChatAnthropic.invoke()`/`.stream()`, the LangChain integration always owns
354
+ the `chat` span (`handleChatModelStart` in `langchain-callback.ts`). It runs
355
+ the underlying provider call inside an internal scope
356
+ (`suppressGenAi: true`) that the direct `@anthropic-ai/sdk` patch checks via
357
+ `isGenAiSuppressed()` — when set, the provider patch defers and emits
358
+ nothing, so only the LangChain-owned span is produced. If you see doubles,
359
+ the two integrations are likely observing different `@anthropic-ai/sdk`
360
+ module instances (common with pnpm hoisting a nested copy under
361
+ `@langchain/anthropic`), so the provider patch never sees the suppression
362
+ scope set by the framework layer; confirm both integrations are
363
+ auto-instrumenting the SAME module instance (check `struct.initialized` and
364
+ your lockfile's dedupe of `@anthropic-ai/sdk`).
280
365
  - **Subagent in a different trace / missing from parent's "Subagents" list:**
281
366
  If you invoke a nested agent (`subagent.invoke(...)`) from inside a tool body,
282
367
  define the outer tool with `tool(func, { name, description, schema })` from
@@ -4,12 +4,38 @@ export interface StructContext {
4
4
  conversationId?: string;
5
5
  agentSpan?: Span;
6
6
  pendingToolCalls?: Record<string, string[]>;
7
+ /**
8
+ * When true, a framework-layer integration (e.g. LangChain's
9
+ * BaseChatModel.generate/.stream) already owns the chat span for the call
10
+ * in progress. Provider-SDK patches (anthropic.ts) check this and skip
11
+ * emitting their own chat span to avoid a duplicate.
12
+ */
13
+ suppressGenAi?: boolean;
14
+ /**
15
+ * Set by `struct.agent()` while its body is running. Signals ownership:
16
+ * when the LangChain callback handler sees a TOP-LEVEL chain start (no
17
+ * parentRunId) while this is set, the manual `struct.agent()` call already
18
+ * owns the `invoke_agent` span for this run — the handler must not emit a
19
+ * duplicate ("twin") span, only register the run so descendants parent
20
+ * under the manual span. Parity: python `_manual_agent_active` contextvar
21
+ * (core.py:64-68).
22
+ */
23
+ manualAgentSpan?: Span;
7
24
  }
8
25
  export declare function getStore(): StructContext | undefined;
9
26
  export declare function getSessionId(): string | undefined;
10
27
  export declare function getConversationId(): string | undefined;
11
28
  export declare function getAgentSpan(): Span | undefined;
29
+ /**
30
+ * The manually-created `struct.agent()` span for the currently-running
31
+ * scope, if any. Read by the LangChain callback handler to detect
32
+ * "manual struct.agent() wraps a top-level LangChain chain" and suppress
33
+ * the twin `invoke_agent` span it would otherwise emit.
34
+ */
35
+ export declare function getManualAgentSpan(): Span | undefined;
12
36
  export declare function getPendingToolCalls(): Record<string, string[]> | undefined;
37
+ /** True when a framework layer (e.g. LangChain) already owns the chat span. */
38
+ export declare function isGenAiSuppressed(): boolean;
13
39
  export declare function runWithContext<T>(patch: Partial<StructContext>, fn: () => T): T;
14
40
  export declare function ensurePendingToolCallsSlot(): Record<string, string[]>;
15
41
  /**
@@ -27,4 +53,23 @@ export declare function snapshotStore(): StructContext | undefined;
27
53
  export declare function runWithStore<T>(store: StructContext | undefined, fn: () => T): T;
28
54
  /** Test-only: run `fn` inside a completely fresh context (no parent store). */
29
55
  export declare function runInFreshContext<T>(fn: () => T): T;
56
+ /**
57
+ * Write-once `gen_ai.provider.name` on an invoke_agent span.
58
+ *
59
+ * CONTRACT: `agentSpan` is always an SDK-OWNED span — created by our own
60
+ * tracer in `struct.agent()` or the LangChain handler and delivered via the
61
+ * ALS store / run map, which nothing else writes. Never a host object. So
62
+ * the industry-standard owned-object pattern applies (state lives ON the
63
+ * object — Sentry/dd-trace private span fields, OTel JS symbol markers): a
64
+ * private Symbol sentinel set after a successful write. No registries or
65
+ * lifecycle bookkeeping — those are for FOREIGN objects.
66
+ *
67
+ * Semantics: "a real child provider" — racing children with different
68
+ * providers may pick either; the sentinel is set only after a successful
69
+ * write so a transient failure can be retried. Parity: python
70
+ * `stamp_provider_once`.
71
+ */
72
+ export declare function stampProviderOnce(agentSpan: Span | undefined, provider: string | undefined): void;
73
+ /** Stamp the ambient agent span with the child call's provider (write-once). */
74
+ export declare function propagateProviderToParent(provider: string | undefined): void;
30
75
  //# sourceMappingURL=context.d.ts.map
@@ -4,7 +4,9 @@ exports.getStore = getStore;
4
4
  exports.getSessionId = getSessionId;
5
5
  exports.getConversationId = getConversationId;
6
6
  exports.getAgentSpan = getAgentSpan;
7
+ exports.getManualAgentSpan = getManualAgentSpan;
7
8
  exports.getPendingToolCalls = getPendingToolCalls;
9
+ exports.isGenAiSuppressed = isGenAiSuppressed;
8
10
  exports.runWithContext = runWithContext;
9
11
  exports.ensurePendingToolCallsSlot = ensurePendingToolCallsSlot;
10
12
  exports.popPendingToolCallId = popPendingToolCallId;
@@ -12,6 +14,8 @@ exports.pushPendingToolCalls = pushPendingToolCalls;
12
14
  exports.snapshotStore = snapshotStore;
13
15
  exports.runWithStore = runWithStore;
14
16
  exports.runInFreshContext = runInFreshContext;
17
+ exports.stampProviderOnce = stampProviderOnce;
18
+ exports.propagateProviderToParent = propagateProviderToParent;
15
19
  const node_async_hooks_1 = require("node:async_hooks");
16
20
  const als = new node_async_hooks_1.AsyncLocalStorage();
17
21
  function getStore() {
@@ -26,9 +30,22 @@ function getConversationId() {
26
30
  function getAgentSpan() {
27
31
  return als.getStore()?.agentSpan;
28
32
  }
33
+ /**
34
+ * The manually-created `struct.agent()` span for the currently-running
35
+ * scope, if any. Read by the LangChain callback handler to detect
36
+ * "manual struct.agent() wraps a top-level LangChain chain" and suppress
37
+ * the twin `invoke_agent` span it would otherwise emit.
38
+ */
39
+ function getManualAgentSpan() {
40
+ return als.getStore()?.manualAgentSpan;
41
+ }
29
42
  function getPendingToolCalls() {
30
43
  return als.getStore()?.pendingToolCalls;
31
44
  }
45
+ /** True when a framework layer (e.g. LangChain) already owns the chat span. */
46
+ function isGenAiSuppressed() {
47
+ return als.getStore()?.suppressGenAi === true;
48
+ }
32
49
  function runWithContext(patch, fn) {
33
50
  const current = als.getStore() ?? {};
34
51
  const next = { ...current, ...patch };
@@ -80,7 +97,11 @@ function pushPendingToolCalls(pairs) {
80
97
  /** Snapshot the store for wrapping async iterators. */
81
98
  function snapshotStore() {
82
99
  const store = als.getStore();
83
- return store ? { ...store } : undefined;
100
+ if (!store)
101
+ return undefined;
102
+ if (!store.pendingToolCalls)
103
+ store.pendingToolCalls = {}; // materialize BEFORE copying so the queue object is shared by reference
104
+ return { ...store };
84
105
  }
85
106
  function runWithStore(store, fn) {
86
107
  if (!store)
@@ -91,4 +112,60 @@ function runWithStore(store, fn) {
91
112
  function runInFreshContext(fn) {
92
113
  return als.run({}, fn);
93
114
  }
115
+ /**
116
+ * Write-once `gen_ai.provider.name` on an invoke_agent span.
117
+ *
118
+ * The GenAI spec puts `gen_ai.provider.name` on invoke_agent spans, but the
119
+ * layer that CREATES those spans (`struct.agent()`, the LangChain handler) is
120
+ * provider-agnostic and can't know the value yet — only the child inference
121
+ * call can. The first chat call within the agent scope stamps it (best
122
+ * knowledge; first one wins); an agent that never reaches a model omits the
123
+ * attribute rather than carrying a framework name. Parity: python
124
+ * `_genai_content.stamp_provider_once`.
125
+ */
126
+ const PROVIDER_STAMPED = Symbol("struct.providerStamped");
127
+ /**
128
+ * Write-once `gen_ai.provider.name` on an invoke_agent span.
129
+ *
130
+ * CONTRACT: `agentSpan` is always an SDK-OWNED span — created by our own
131
+ * tracer in `struct.agent()` or the LangChain handler and delivered via the
132
+ * ALS store / run map, which nothing else writes. Never a host object. So
133
+ * the industry-standard owned-object pattern applies (state lives ON the
134
+ * object — Sentry/dd-trace private span fields, OTel JS symbol markers): a
135
+ * private Symbol sentinel set after a successful write. No registries or
136
+ * lifecycle bookkeeping — those are for FOREIGN objects.
137
+ *
138
+ * Semantics: "a real child provider" — racing children with different
139
+ * providers may pick either; the sentinel is set only after a successful
140
+ * write so a transient failure can be retried. Parity: python
141
+ * `stamp_provider_once`.
142
+ */
143
+ function stampProviderOnce(agentSpan, provider) {
144
+ try {
145
+ if (!agentSpan || !provider)
146
+ return;
147
+ const marked = agentSpan;
148
+ if (marked[PROVIDER_STAMPED])
149
+ return;
150
+ agentSpan.setAttribute("gen_ai.provider.name", provider);
151
+ try {
152
+ marked[PROVIDER_STAMPED] = true;
153
+ }
154
+ catch {
155
+ /* frozen span — worst case a later child re-stamps a real provider */
156
+ }
157
+ }
158
+ catch {
159
+ /* never fail the application for telemetry */
160
+ }
161
+ }
162
+ /** Stamp the ambient agent span with the child call's provider (write-once). */
163
+ function propagateProviderToParent(provider) {
164
+ try {
165
+ stampProviderOnce(getAgentSpan(), provider);
166
+ }
167
+ catch {
168
+ /* never fail the application for telemetry */
169
+ }
170
+ }
94
171
  //# sourceMappingURL=context.js.map