@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.
- package/README.md +101 -16
- package/dist/commonjs/context.d.ts +45 -0
- package/dist/commonjs/context.js +78 -1
- package/dist/commonjs/core.js +184 -29
- package/dist/commonjs/events.d.ts +17 -6
- package/dist/commonjs/events.js +82 -59
- package/dist/commonjs/genai-content.d.ts +52 -0
- package/dist/commonjs/genai-content.js +143 -0
- package/dist/commonjs/instrument.d.ts +47 -0
- package/dist/commonjs/instrument.js +158 -0
- package/dist/commonjs/integrations/anthropic-content.js +18 -6
- package/dist/commonjs/integrations/anthropic.d.ts +8 -1
- package/dist/commonjs/integrations/anthropic.js +515 -104
- package/dist/commonjs/integrations/index.js +8 -0
- package/dist/commonjs/integrations/langchain-callback.d.ts +182 -27
- package/dist/commonjs/integrations/langchain-callback.js +754 -87
- package/dist/commonjs/integrations/langchain-content.js +1 -1
- package/dist/commonjs/integrations/langchain.d.ts +3 -0
- package/dist/commonjs/integrations/langchain.js +353 -7
- package/dist/commonjs/integrations/openai-content.d.ts +34 -0
- package/dist/commonjs/integrations/openai-content.js +375 -0
- package/dist/commonjs/integrations/openai.d.ts +39 -0
- package/dist/commonjs/integrations/openai.js +305 -0
- package/dist/commonjs/semconv.d.ts +12 -0
- package/dist/commonjs/semconv.js +13 -1
- package/dist/commonjs/truncation.d.ts +29 -0
- package/dist/commonjs/truncation.js +184 -10
- package/dist/commonjs/version.d.ts +2 -0
- package/dist/commonjs/version.js +6 -0
- package/dist/esm/context.d.ts +45 -0
- package/dist/esm/context.js +74 -1
- package/dist/esm/core.js +185 -30
- package/dist/esm/events.d.ts +17 -6
- package/dist/esm/events.js +82 -61
- package/dist/esm/genai-content.d.ts +52 -0
- package/dist/esm/genai-content.js +137 -0
- package/dist/esm/instrument.d.ts +47 -0
- package/dist/esm/instrument.js +155 -0
- package/dist/esm/integrations/anthropic-content.js +19 -7
- package/dist/esm/integrations/anthropic.d.ts +8 -1
- package/dist/esm/integrations/anthropic.js +514 -107
- package/dist/esm/integrations/index.js +8 -0
- package/dist/esm/integrations/langchain-callback.d.ts +182 -27
- package/dist/esm/integrations/langchain-callback.js +756 -89
- package/dist/esm/integrations/langchain-content.js +1 -1
- package/dist/esm/integrations/langchain.d.ts +3 -0
- package/dist/esm/integrations/langchain.js +352 -7
- package/dist/esm/integrations/openai-content.d.ts +34 -0
- package/dist/esm/integrations/openai-content.js +360 -0
- package/dist/esm/integrations/openai.d.ts +39 -0
- package/dist/esm/integrations/openai.js +296 -0
- package/dist/esm/semconv.d.ts +12 -0
- package/dist/esm/semconv.js +12 -0
- package/dist/esm/truncation.d.ts +29 -0
- package/dist/esm/truncation.js +182 -10
- package/dist/esm/version.d.ts +2 -0
- package/dist/esm/version.js +3 -0
- 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
|
|
5
|
-
traces + logs to
|
|
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
|
-
|
|
|
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
|
|
193
|
-
|
|
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`).
|
|
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
|
|
215
|
-
and other metadata
|
|
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`, `
|
|
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:**
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
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
|
package/dist/commonjs/context.js
CHANGED
|
@@ -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
|
-
|
|
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
|