@struct-ai/sdk 0.3.17 → 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 (54) hide show
  1. package/README.md +62 -17
  2. package/dist/commonjs/context.d.ts +19 -0
  3. package/dist/commonjs/context.js +58 -0
  4. package/dist/commonjs/core.js +104 -8
  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 +6 -1
  13. package/dist/commonjs/integrations/anthropic.js +113 -125
  14. package/dist/commonjs/integrations/index.js +8 -0
  15. package/dist/commonjs/integrations/langchain-callback.d.ts +3 -0
  16. package/dist/commonjs/integrations/langchain-callback.js +84 -6
  17. package/dist/commonjs/integrations/langchain-content.js +1 -1
  18. package/dist/commonjs/integrations/openai-content.d.ts +34 -0
  19. package/dist/commonjs/integrations/openai-content.js +375 -0
  20. package/dist/commonjs/integrations/openai.d.ts +39 -0
  21. package/dist/commonjs/integrations/openai.js +305 -0
  22. package/dist/commonjs/semconv.d.ts +1 -0
  23. package/dist/commonjs/semconv.js +1 -0
  24. package/dist/commonjs/truncation.d.ts +29 -0
  25. package/dist/commonjs/truncation.js +184 -10
  26. package/dist/commonjs/version.d.ts +1 -1
  27. package/dist/commonjs/version.js +1 -1
  28. package/dist/esm/context.d.ts +19 -0
  29. package/dist/esm/context.js +56 -0
  30. package/dist/esm/core.js +104 -8
  31. package/dist/esm/events.d.ts +17 -6
  32. package/dist/esm/events.js +82 -61
  33. package/dist/esm/genai-content.d.ts +52 -0
  34. package/dist/esm/genai-content.js +137 -0
  35. package/dist/esm/instrument.d.ts +47 -0
  36. package/dist/esm/instrument.js +155 -0
  37. package/dist/esm/integrations/anthropic-content.js +19 -7
  38. package/dist/esm/integrations/anthropic.d.ts +6 -1
  39. package/dist/esm/integrations/anthropic.js +112 -126
  40. package/dist/esm/integrations/index.js +8 -0
  41. package/dist/esm/integrations/langchain-callback.d.ts +3 -0
  42. package/dist/esm/integrations/langchain-callback.js +85 -7
  43. package/dist/esm/integrations/langchain-content.js +1 -1
  44. package/dist/esm/integrations/openai-content.d.ts +34 -0
  45. package/dist/esm/integrations/openai-content.js +360 -0
  46. package/dist/esm/integrations/openai.d.ts +39 -0
  47. package/dist/esm/integrations/openai.js +296 -0
  48. package/dist/esm/semconv.d.ts +1 -0
  49. package/dist/esm/semconv.js +1 -0
  50. package/dist/esm/truncation.d.ts +29 -0
  51. package/dist/esm/truncation.js +182 -10
  52. package/dist/esm/version.d.ts +1 -1
  53. package/dist/esm/version.js +1 -1
  54. package/package.json +8 -2
@@ -1,5 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports._setChatRequestAttrsForTest = exports._detectProviderForTest = void 0;
3
4
  exports.patch = patch;
4
5
  exports.unpatch = unpatch;
5
6
  exports.wrapCreate = wrapCreate;
@@ -16,9 +17,11 @@ function childParentContext() {
16
17
  : api_1.context.active();
17
18
  }
18
19
  const core_js_1 = require("../core.js");
20
+ const genai_content_js_1 = require("../genai-content.js");
19
21
  const events_js_1 = require("../events.js");
22
+ const genai_content_js_2 = require("../genai-content.js");
23
+ const instrument_js_1 = require("../instrument.js");
20
24
  const semconv_js_1 = require("../semconv.js");
21
- const truncation_js_1 = require("../truncation.js");
22
25
  const anthropic_content_js_1 = require("./anthropic-content.js");
23
26
  const STRUCT_WRAPPED = Symbol.for("struct.wrapped");
24
27
  const STRUCT_ORIGINAL = Symbol.for("struct.original");
@@ -106,113 +109,96 @@ function restoreMethod(proto, methodName) {
106
109
  proto[methodName] = current[STRUCT_ORIGINAL];
107
110
  }
108
111
  }
112
+ // Exact client class names per platform (prototype chain covers subclasses)
113
+ // and official endpoint host shapes only — a generic cloud host (API-gateway,
114
+ // storage, arbitrary *.amazonaws.com/*.googleapis.com proxies) is NOT
115
+ // platform routing and must stay on the fallback. Parity: python anthropic.py.
116
+ const CLASS_NAME_RULES = [
117
+ // Mantle flavor extends BaseMantleClient, NOT the plain Bedrock base, so it
118
+ // needs its own names; default endpoint bedrock-mantle.{region}.api.aws.
119
+ [
120
+ new Set([
121
+ "AnthropicBedrock",
122
+ "BaseAnthropicBedrock",
123
+ "AnthropicBedrockMantle",
124
+ "BaseMantleClient",
125
+ ]),
126
+ "aws.bedrock",
127
+ ],
128
+ [new Set(["AnthropicVertex", "BaseAnthropicVertex"]), "gcp.vertex_ai"],
129
+ ];
130
+ function isBedrockHost(host) {
131
+ if ((host.startsWith("bedrock-runtime.") ||
132
+ host.startsWith("bedrock-runtime-fips.")) &&
133
+ (host.endsWith(".amazonaws.com") || host.endsWith(".amazonaws.com.cn"))) {
134
+ return true;
135
+ }
136
+ // Bedrock Mantle default endpoint: bedrock-mantle.{region}.api.aws
137
+ return host.startsWith("bedrock-mantle.") && host.endsWith(".api.aws");
138
+ }
139
+ function isVertexHost(host) {
140
+ return (host === "aiplatform.googleapis.com" ||
141
+ host.endsWith("-aiplatform.googleapis.com"));
142
+ }
143
+ const HOST_RULES = [
144
+ [isBedrockHost, "aws.bedrock"],
145
+ [isVertexHost, "gcp.vertex_ai"],
146
+ ];
147
+ function detectProvider(resource) {
148
+ return (0, genai_content_js_1.detectProviderFromResource)(resource, CLASS_NAME_RULES, HOST_RULES, "anthropic");
149
+ }
150
+ /** @internal */
151
+ exports._detectProviderForTest = detectProvider;
152
+ /** @internal */
153
+ const _setChatRequestAttrsForTest = (span, params, sdk, logger, provider) => setChatRequestAttrs(span, params, sdk, logger, provider);
154
+ exports._setChatRequestAttrsForTest = _setChatRequestAttrsForTest;
109
155
  function wrapCreate(original) {
110
156
  return function wrappedCreate(params, opts) {
111
157
  const patchCtx = activePatchCtx.value;
112
158
  if (!patchCtx) {
113
159
  return original.call(this, params, opts);
114
160
  }
161
+ // CLASS RULE: detection => propagation, immediately and on EVERY path
162
+ // (see openai.ts). Guarded; write-once makes repeats harmless.
163
+ const provider = detectProvider(this);
164
+ (0, context_js_1.propagateProviderToParent)(provider);
115
165
  if ((0, context_js_1.isGenAiSuppressed)()) {
116
166
  // A framework layer owns this chat span — run the call, emit no span.
117
167
  return original.call(this, params, opts);
118
168
  }
169
+ // Preflight reads touch CALLER-owned `params` — a Proxy / lazy request
170
+ // object can have throwing getters; a throw here would prevent the API
171
+ // call. Degrade: exactly one plain, uninstrumented original.call.
172
+ let streaming = false;
173
+ let model = "unknown";
174
+ try {
175
+ streaming = !!(params && params.stream === true);
176
+ model = params?.model ?? "unknown";
177
+ }
178
+ catch {
179
+ return original.call(this, params, opts);
180
+ }
119
181
  // If params.stream is true, Messages.create delegates to the streaming path.
120
182
  // We still need to instrument the returned stream like we do in messages.stream.
121
- if (params && params.stream === true) {
183
+ if (streaming) {
122
184
  return wrapStream(original).call(this, params, opts);
123
185
  }
124
186
  const { tracer, sdk, logger } = patchCtx;
125
- const internalLogger = sdk.getInternalLogger();
126
- const model = params?.model ?? "unknown";
127
- // Span creation itself can fail (custom tracer, broken context). If it
128
- // does, fall through to calling `original` directly so the user's API
129
- // call always runs. Mirrors the Python `_wrap_create` fall-through.
130
- let span;
131
- let ctx;
132
- (0, core_js_1.safe)(() => {
133
- const parentCtx = childParentContext();
134
- span = tracer.startSpan(`chat ${model}`, { kind: api_1.SpanKind.CLIENT }, parentCtx);
135
- ctx = api_1.trace.setSpan(parentCtx, span);
136
- }, "anthropic.create.start_span", internalLogger);
137
- if (!span || !ctx) {
138
- return original.call(this, params, opts);
139
- }
140
- const liveSpan = span;
141
- const liveCtx = ctx;
142
- // Enter the span's context BEFORE emitting request-side log records so
143
- // `logger.emit({ context: otelContext.active() })` picks up the chat
144
- // span as the log's TraceId / SpanId. Without this the UI can't link
145
- // user/system message logs back to the chat span.
146
- return api_1.context.with(liveCtx, () => {
147
- (0, core_js_1.safe)(() => setChatRequestAttrs(liveSpan, params, sdk, logger), "anthropic.create.set_request_attrs", internalLogger);
148
- let rawResult;
149
- try {
150
- rawResult = original.call(this, params, opts);
151
- }
152
- catch (err) {
153
- // Sync throw from the original call (e.g. a validation error thrown
154
- // before any request goes out) would otherwise leak the span — no
155
- // promise is ever created to drive the .then/.catch below.
156
- (0, core_js_1.safe)(() => recordErrorOnSpan(liveSpan, err), "anthropic.create.record_error", internalLogger);
157
- (0, core_js_1.safe)(() => liveSpan.end(), "anthropic.create.span_end_on_error", internalLogger);
158
- throw err;
159
- }
160
- // Observe the SDK's own return value (Anthropic's APIPromise) for
161
- // telemetry, then hand it BACK UNCHANGED — replacing it with a native
162
- // `Promise.resolve(...).then(...)` strips APIPromise methods
163
- // (.withResponse/.asResponse), breaking customer code only when
164
- // instrumentation is enabled. Our observer chain must not create an
165
- // unhandled rejection. (The prior code returned a native Promise here;
166
- // this is adjacent hardening of pre-existing behavior — the identity
167
- // hazard predates the streaming work.)
168
- //
169
- // Both the `.then` PROPERTY READ (`rt.then`) and the `.then(...)` CALL
170
- // that registers our observer run SYNCHRONOUSLY on the success path of
171
- // a customer's `create()` call, and were previously unguarded — a
172
- // hostile thenable (a `.then` accessor that throws on read, or a
173
- // `.then` function that throws synchronously when invoked) would turn
174
- // a SUCCESSFUL Anthropic request into a synchronous exception thrown
175
- // out of `wrappedCreate`. Wrap the read + registration so a broken
176
- // thenable degrades instead of crashing the host: close the span with
177
- // no telemetry and hand back rawResult UNCHANGED — never
178
- // `Promise.resolve(...)`, which would also strip APIPromise
179
- // identity/methods.
180
- try {
181
- const rt = rawResult;
182
- if (rt && typeof rt.then === "function") {
183
- rawResult.then((result) => {
184
- (0, core_js_1.safe)(() => setChatResponseAttrs(liveSpan, sdk, result, logger), "anthropic.create.set_response_attrs", internalLogger);
185
- (0, core_js_1.safe)(() => liveSpan.setStatus({ code: api_1.SpanStatusCode.OK }), "anthropic.create.set_ok_status", internalLogger);
186
- (0, core_js_1.safe)(() => liveSpan.end(), "anthropic.create.span_end", internalLogger);
187
- }, (err) => {
188
- (0, core_js_1.safe)(() => recordErrorOnSpan(liveSpan, err), "anthropic.create.record_error", internalLogger);
189
- (0, core_js_1.safe)(() => liveSpan.end(), "anthropic.create.span_end_on_error", internalLogger);
190
- // Observer branch only — do NOT rethrow; the original APIPromise
191
- // still rejects to the customer independently.
192
- });
193
- return rawResult;
194
- }
195
- }
196
- catch {
197
- // Reading `.then` or invoking it threw (hostile thenable). The
198
- // underlying call already succeeded (rawResult exists) — this is an
199
- // instrumentation-only failure, not an API error. Degrade like the
200
- // other host-crash guards in this file (e.g. wrapStream's dispatch
201
- // region below): close the span with no telemetry, hand back
202
- // rawResult untouched.
203
- (0, core_js_1.safe)(() => liveSpan.setStatus({ code: api_1.SpanStatusCode.OK }), "anthropic.create.set_ok_status", internalLogger);
204
- (0, core_js_1.safe)(() => liveSpan.end(), "anthropic.create.span_end", internalLogger);
205
- return rawResult;
206
- }
207
- // Non-thenable return (a synchronous / mocked result, or any SDK shape
208
- // that isn't a promise): run the response telemetry inline on it, close
209
- // the span, and hand it back untouched — matching the prior
210
- // `Promise.resolve(...).then(...)` behavior, which unified thenable and
211
- // non-thenable returns.
212
- (0, core_js_1.safe)(() => setChatResponseAttrs(liveSpan, sdk, rawResult, logger), "anthropic.create.set_response_attrs", internalLogger);
213
- (0, core_js_1.safe)(() => liveSpan.setStatus({ code: api_1.SpanStatusCode.OK }), "anthropic.create.set_ok_status", internalLogger);
214
- (0, core_js_1.safe)(() => liveSpan.end(), "anthropic.create.span_end", internalLogger);
215
- return rawResult;
187
+ // All host-boundary safety lives in the shared instrumentCall harness: the
188
+ // host call runs exactly once, nothing host-controllable wraps it, and every
189
+ // telemetry side-effect degrades via safe(). Log-record→span linkage is
190
+ // carried by the explicit span passed to the emitters, so no
191
+ // `otelContext.with(...)` is needed around the call.
192
+ return (0, instrument_js_1.instrumentCall)(() => original.call(this, params, opts), {
193
+ tracer,
194
+ spanName: `chat ${model}`,
195
+ spanKind: api_1.SpanKind.CLIENT,
196
+ parentContext: childParentContext,
197
+ sitePrefix: "anthropic.create",
198
+ internalLogger: sdk.getInternalLogger(),
199
+ onStart: (span) => setChatRequestAttrs(span, params, sdk, logger, provider),
200
+ onSuccess: (span, result) => setChatResponseAttrs(span, sdk, result, logger, provider),
201
+ onError: (span, err) => recordErrorOnSpan(span, err),
216
202
  });
217
203
  };
218
204
  }
@@ -222,13 +208,26 @@ function wrapStream(original) {
222
208
  if (!patchCtx) {
223
209
  return original.call(this, params, opts);
224
210
  }
211
+ // CLASS RULE: detection => propagation, immediately and on EVERY path —
212
+ // when suppressed (langchain owns the span), the raw client here is the
213
+ // only layer that can detect the real platform for the agent span.
214
+ const streamProvider = detectProvider(this);
215
+ (0, context_js_1.propagateProviderToParent)(streamProvider);
225
216
  if ((0, context_js_1.isGenAiSuppressed)()) {
226
217
  // A framework layer owns this chat span — run the call, emit no span.
227
218
  return original.call(this, params, opts);
228
219
  }
229
220
  const { tracer, sdk, logger } = patchCtx;
230
221
  const internalLogger = sdk.getInternalLogger();
231
- const model = params?.model ?? "unknown";
222
+ // Preflight read of CALLER-owned params — a throwing `model` getter must
223
+ // degrade to one plain uninstrumented call, never block the stream.
224
+ let model = "unknown";
225
+ try {
226
+ model = params?.model ?? "unknown";
227
+ }
228
+ catch {
229
+ return original.call(this, params, opts);
230
+ }
232
231
  // Span creation itself can fail. If it does, fall through to calling
233
232
  // `original` directly so the user's stream call always runs. Mirrors
234
233
  // the Python `_wrap_stream` fall-through pattern.
@@ -255,7 +254,7 @@ function wrapStream(original) {
255
254
  (0, core_js_1.safe)(() => recordErrorOnSpan(liveSpan, err), "anthropic.stream.record_error", internalLogger);
256
255
  }
257
256
  else if (result) {
258
- (0, core_js_1.safe)(() => api_1.context.with(liveCtx, () => setChatResponseAttrs(liveSpan, sdk, result, logger)), "anthropic.stream.set_response_attrs", internalLogger);
257
+ (0, core_js_1.safe)(() => api_1.context.with(liveCtx, () => setChatResponseAttrs(liveSpan, sdk, result, logger, streamProvider)), "anthropic.stream.set_response_attrs", internalLogger);
259
258
  (0, core_js_1.safe)(() => liveSpan.setStatus({ code: api_1.SpanStatusCode.OK }), "anthropic.stream.set_ok_status", internalLogger);
260
259
  }
261
260
  else {
@@ -294,10 +293,14 @@ function wrapStream(original) {
294
293
  // runs with a clean (unsuppressed) ALS store.
295
294
  let stream;
296
295
  try {
297
- stream = api_1.context.with(liveCtx, () => {
298
- (0, core_js_1.safe)(() => setChatRequestAttrs(liveSpan, params, sdk, logger), "anthropic.stream.set_request_attrs", internalLogger);
299
- return (0, context_js_1.runWithContext)({ suppressGenAi: true }, () => original.call(this, params, opts));
300
- });
296
+ (0, core_js_1.safe)(() => setChatRequestAttrs(liveSpan, params, sdk, logger, streamProvider), "anthropic.stream.set_request_attrs", internalLogger);
297
+ // `suppressGenAi` is ALS-based (`als.run`), NOT host-controllable, so it
298
+ // may wrap the original stream call. We deliberately do NOT wrap it in
299
+ // `otelContext.with(liveCtx, ...)`: a customer's global ContextManager
300
+ // could throw there and block the stream (host-fault). Log-record→span
301
+ // linkage is carried by the explicit span passed inside
302
+ // setChatRequestAttrs, not by ambient context.
303
+ stream = (0, context_js_1.runWithContext)({ suppressGenAi: true }, () => original.call(this, params, opts));
301
304
  }
302
305
  catch (err) {
303
306
  // Sync throw from the original call would otherwise leak the span —
@@ -667,9 +670,9 @@ function wrapStream(original) {
667
670
  }
668
671
  };
669
672
  }
670
- function setChatRequestAttrs(span, params, sdk, logger) {
673
+ function setChatRequestAttrs(span, params, sdk, logger, provider = "anthropic") {
671
674
  span.setAttribute(semconv_js_1.GEN_AI.OPERATION_NAME, "chat");
672
- span.setAttribute(semconv_js_1.GEN_AI.PROVIDER_NAME, "anthropic");
675
+ span.setAttribute(semconv_js_1.GEN_AI.PROVIDER_NAME, provider);
673
676
  const model = params?.model ?? "unknown";
674
677
  span.setAttribute(semconv_js_1.GEN_AI.REQUEST_MODEL, model);
675
678
  const sessionId = (0, context_js_1.getSessionId)();
@@ -693,10 +696,15 @@ function setChatRequestAttrs(span, params, sdk, logger) {
693
696
  }
694
697
  if (Array.isArray(params?.messages)) {
695
698
  span.setAttribute(semconv_js_1.STRUCT.INPUT_MESSAGE_COUNT, params.messages.length);
696
- propagateUserPromptToParent(params.messages);
699
+ // Parent-prompt preview is CONTENT — never emit it in ContentCaptureMode.None.
700
+ // Deliberately kept in EventOnly (the default): shipped behavior in both
701
+ // SDKs and what the waterfall UI reads.
702
+ if (sdk.captureContent) {
703
+ (0, genai_content_js_2.propagateUserPromptToParent)((0, anthropic_content_js_1.lastUserMessageParts)(params.messages));
704
+ }
697
705
  }
698
706
  if (sdk.emitEvents && logger && Array.isArray(params?.messages)) {
699
- (0, events_js_1.emitAnthropicMessageEvents)(logger, params.messages, params.system);
707
+ (0, events_js_1.emitAnthropicMessageEvents)(logger, params.messages, params.system, span, provider);
700
708
  }
701
709
  if (sdk.emitSpanContent) {
702
710
  if (Array.isArray(params?.messages)) {
@@ -710,7 +718,7 @@ function setChatRequestAttrs(span, params, sdk, logger) {
710
718
  }
711
719
  }
712
720
  }
713
- function setChatResponseAttrs(span, sdk, response, logger) {
721
+ function setChatResponseAttrs(span, sdk, response, logger, provider = "anthropic") {
714
722
  const usage = response.usage;
715
723
  if (usage) {
716
724
  const inputTokens = usage.input_tokens ?? 0;
@@ -740,7 +748,7 @@ function setChatResponseAttrs(span, sdk, response, logger) {
740
748
  // Always runs — populates pending queue regardless of content-capture mode.
741
749
  recordPendingToolCalls(response.content);
742
750
  if (sdk.emitEvents && logger) {
743
- (0, events_js_1.emitAnthropicChoiceEvent)(logger, response.content, response.stop_reason);
751
+ (0, events_js_1.emitAnthropicChoiceEvent)(logger, response.content, response.stop_reason, span, provider);
744
752
  }
745
753
  if (sdk.emitSpanContent) {
746
754
  span.setAttribute(semconv_js_1.GEN_AI.OUTPUT_MESSAGES, (0, anthropic_content_js_1.toOutputMessages)(response.content, response.stop_reason));
@@ -756,26 +764,6 @@ function recordPendingToolCalls(contentBlocks) {
756
764
  (0, context_js_1.ensurePendingToolCallsSlot)();
757
765
  (0, context_js_1.pushPendingToolCalls)(pairs);
758
766
  }
759
- function propagateUserPromptToParent(messages) {
760
- try {
761
- const agentSpan = (0, context_js_1.getAgentSpan)();
762
- if (!agentSpan)
763
- return;
764
- // The SDK span type doesn't expose `attributes` — we use a brand check on
765
- // the SDK's ReadableSpan-like shape.
766
- const agentAttrs = agentSpan
767
- .attributes;
768
- if (agentAttrs && agentAttrs[semconv_js_1.GEN_AI.INPUT_MESSAGES])
769
- return;
770
- const parts = (0, anthropic_content_js_1.lastUserMessageParts)(messages);
771
- if (!parts)
772
- return;
773
- agentSpan.setAttribute(semconv_js_1.GEN_AI.INPUT_MESSAGES, (0, truncation_js_1.truncateAndSerialize)([{ role: "user", parts }]));
774
- }
775
- catch {
776
- /* never fail the application for telemetry */
777
- }
778
- }
779
767
  function recordErrorOnSpan(span, err) {
780
768
  const errorType = err instanceof Error ? err.constructor.name : typeof err;
781
769
  const message = err instanceof Error ? err.message : String(err);
@@ -16,6 +16,14 @@ const INTEGRATIONS = [
16
16
  await mod.patch(sdk);
17
17
  },
18
18
  },
19
+ {
20
+ name: "openai",
21
+ probe: () => import("openai"),
22
+ apply: async (sdk) => {
23
+ const mod = await import("./openai.js");
24
+ await mod.patch(sdk);
25
+ },
26
+ },
19
27
  {
20
28
  name: "langchain",
21
29
  probe: () => import("@langchain/core/language_models/chat_models"),
@@ -282,5 +282,8 @@ export declare class StructCallbackHandler {
282
282
  private resolveAgentSessionId;
283
283
  private propagateUserPrompt;
284
284
  }
285
+ declare function detectProviderFromSerialized(llm: Serialized): string;
286
+ /** @internal */
287
+ export declare const _detectProviderFromSerializedForTest: typeof detectProviderFromSerialized;
285
288
  export {};
286
289
  //# sourceMappingURL=langchain-callback.d.ts.map
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.StructCallbackHandler = void 0;
3
+ exports._detectProviderFromSerializedForTest = exports.StructCallbackHandler = void 0;
4
4
  const node_crypto_1 = require("node:crypto");
5
5
  const api_1 = require("@opentelemetry/api");
6
6
  const context_js_1 = require("../context.js");
@@ -330,7 +330,9 @@ class StructCallbackHandler {
330
330
  const startedSpan = span;
331
331
  (0, core_js_1.safe)(() => {
332
332
  startedSpan.setAttribute(semconv_js_1.GEN_AI.OPERATION_NAME, "invoke_agent");
333
- startedSpan.setAttribute(semconv_js_1.GEN_AI.PROVIDER_NAME, "langchain");
333
+ // gen_ai.provider.name is stamped later by the first chat run under
334
+ // this agent (write-once) — "langchain" is a framework, not a
335
+ // provider, and the real one isn't known at chain start.
334
336
  startedSpan.setAttribute(semconv_js_1.GEN_AI.AGENT_NAME, String(agentName));
335
337
  // Do NOT set gen_ai.agent.id from sessionId — that conflates agent
336
338
  // identity (spec: stable agent-definition id) with a per-invocation
@@ -423,6 +425,17 @@ class StructCallbackHandler {
423
425
  const parentCtx = parentSpan
424
426
  ? api_1.trace.setSpan(api_1.context.active(), parentSpan)
425
427
  : api_1.context.active();
428
+ // CLASS RULE: detection => propagation BEFORE any child-span telemetry —
429
+ // a throwing tracer or failing chat-span write must not leave the healthy
430
+ // ancestor agent provider-less. ("langchain" is the unknown-model
431
+ // fallback sentinel, never propagated.) Parity: python
432
+ // on_chat_model_start stamp_ancestor.
433
+ (0, core_js_1.safe)(() => {
434
+ const providerAncestor = this.inheritedAgentSpan(parentRunId);
435
+ if (providerAncestor && provider !== "langchain") {
436
+ (0, context_js_1.stampProviderOnce)(providerAncestor, provider);
437
+ }
438
+ }, "langchain.handleChatModelStart.stamp_provider", this.internalLogger);
426
439
  let span;
427
440
  (0, core_js_1.safe)(() => {
428
441
  span = this.tracer.startSpan(`chat ${model}`, { kind: api_1.SpanKind.CLIENT }, parentCtx);
@@ -574,7 +587,8 @@ class StructCallbackHandler {
574
587
  const queueOwnership = this.resolveQueueOwnership(parentRunId);
575
588
  (0, core_js_1.safe)(() => {
576
589
  startedSpan.setAttribute(semconv_js_1.GEN_AI.OPERATION_NAME, "execute_tool");
577
- startedSpan.setAttribute(semconv_js_1.GEN_AI.PROVIDER_NAME, "langchain");
590
+ // No gen_ai.provider.name: the spec's execute_tool span does not
591
+ // define that attribute.
578
592
  startedSpan.setAttribute(semconv_js_1.GEN_AI.TOOL_NAME, String(toolName));
579
593
  // Tool call id from metadata (LangChain passes it there for ToolCall
580
594
  // inputs) or fallback to the pending queue populated by the LLM's
@@ -658,7 +672,18 @@ class StructCallbackHandler {
658
672
  span.setAttribute(semconv_js_1.GEN_AI.TOOL_CALL_RESULT, (0, truncation_js_1.safeJsonStringify)(output).slice(0, 8192));
659
673
  }
660
674
  }, "langchain.handleToolEnd.set_result", this.internalLogger);
661
- (0, core_js_1.safe)(() => span.setStatus({ code: api_1.SpanStatusCode.OK }), "langchain.handleToolEnd.set_status", this.internalLogger);
675
+ (0, core_js_1.safe)(() => {
676
+ if (toolOutputSignalsError(output)) {
677
+ span.setAttribute(semconv_js_1.ERROR_TYPE, "tool_error");
678
+ span.setStatus({
679
+ code: api_1.SpanStatusCode.ERROR,
680
+ message: "tool returned an error result",
681
+ });
682
+ }
683
+ else {
684
+ span.setStatus({ code: api_1.SpanStatusCode.OK });
685
+ }
686
+ }, "langchain.handleToolEnd.set_status", this.internalLogger);
662
687
  (0, core_js_1.safe)(() => span.end(), "langchain.handleToolEnd.span_end", this.internalLogger);
663
688
  };
664
689
  handleToolError = (err, runId) => {
@@ -691,7 +716,8 @@ class StructCallbackHandler {
691
716
  const sessionId = this.resolveSessionId(parentRunId, metadata);
692
717
  (0, core_js_1.safe)(() => {
693
718
  startedSpan.setAttribute(semconv_js_1.GEN_AI.OPERATION_NAME, "retrieval");
694
- startedSpan.setAttribute(semconv_js_1.GEN_AI.PROVIDER_NAME, "langchain");
719
+ // No gen_ai.provider.name: not an inference span, and "langchain"
720
+ // isn't a provider.
695
721
  startedSpan.setAttribute(semconv_js_1.GEN_AI.DATA_SOURCE_ID, String(name));
696
722
  if (sessionId)
697
723
  startedSpan.setAttribute(semconv_js_1.GEN_AI.CONVERSATION_ID, sessionId);
@@ -910,6 +936,11 @@ class StructCallbackHandler {
910
936
  }
911
937
  propagateUserPrompt(parentSpan, messages) {
912
938
  try {
939
+ // Parent-prompt preview is CONTENT — never emit it in ContentCaptureMode
940
+ // .None (same gate as the anthropic/openai provider paths; kept in
941
+ // EventOnly deliberately: shipped behavior the waterfall UI reads).
942
+ if (!this.sdk.captureContent)
943
+ return;
913
944
  const attrs = parentSpan
914
945
  .attributes;
915
946
  if (attrs && attrs[semconv_js_1.GEN_AI.INPUT_MESSAGES])
@@ -1116,8 +1147,30 @@ function extractClassName(obj) {
1116
1147
  function detectProviderFromSerialized(llm) {
1117
1148
  const cls = extractClassName(llm) ?? "";
1118
1149
  const fake = { constructor: { name: cls }, _llmType: undefined };
1119
- return (0, langchain_content_js_1.detectProvider)(fake);
1150
+ const byClass = (0, langchain_content_js_1.detectProvider)(fake);
1151
+ if (byClass !== "langchain")
1152
+ return byClass;
1153
+ // Module-path fallback (parity with python's _detect_provider_from_serialized):
1154
+ // an unknown/wrapped class under a known provider module (e.g.
1155
+ // langchain_openai) still resolves to the real provider.
1156
+ const ids = llm.id;
1157
+ const modulePath = Array.isArray(ids) && typeof ids[0] === "string" ? ids[0] : "";
1158
+ // SEGMENT-exact matching, scoped to langchain partner packages — substring
1159
+ // classified lookalikes (langchain_notopenai -> openai), and the value gets
1160
+ // write-once stamped onto agent spans, so a false positive is sticky.
1161
+ if (modulePath === "langchain" ||
1162
+ modulePath.startsWith("langchain_") ||
1163
+ modulePath.startsWith("langchain.")) {
1164
+ const segments = modulePath.split(/[._]/).filter(Boolean);
1165
+ for (const [key, provider] of Object.entries(langchain_content_js_1.MODULE_PROVIDER_MAP)) {
1166
+ if (segments.includes(key))
1167
+ return provider;
1168
+ }
1169
+ }
1170
+ return "langchain";
1120
1171
  }
1172
+ /** @internal */
1173
+ exports._detectProviderFromSerializedForTest = detectProviderFromSerialized;
1121
1174
  function extractParam(obj, key) {
1122
1175
  if (!obj || typeof obj !== "object")
1123
1176
  return undefined;
@@ -1344,4 +1397,29 @@ function recordError(span, err) {
1344
1397
  if (err instanceof Error)
1345
1398
  span.recordException(err);
1346
1399
  }
1400
+ /**
1401
+ * Whether a LangChain tool OUTPUT signals in-band failure: a
1402
+ * ToolMessage with status "error", or an MCP-style isError/is_error
1403
+ * boolean-true flag on the top-level object. Mirrors python
1404
+ * langchain.on_tool_end semantics — keep in lockstep.
1405
+ */
1406
+ function toolOutputSignalsError(output) {
1407
+ if (typeof output !== "object" || output === null)
1408
+ return false;
1409
+ // Per-key probe isolation: a hostile getter on one key must not mask a
1410
+ // readable sibling. Mirrors python _tool_output_signals_error.
1411
+ return (probe(output, "status") === "error" ||
1412
+ probe(output, "isError") === true ||
1413
+ probe(output, "is_error") === true);
1414
+ }
1415
+ /** Isolated single-property read of untrusted host data (see core.ts
1416
+ * safeProbe — duplicated here because it is module-private there). */
1417
+ function probe(obj, key) {
1418
+ try {
1419
+ return obj[key];
1420
+ }
1421
+ catch {
1422
+ return undefined;
1423
+ }
1424
+ }
1347
1425
  //# sourceMappingURL=langchain-callback.js.map
@@ -15,7 +15,7 @@ const truncation_js_1 = require("../truncation.js");
15
15
  const semconv_js_1 = require("../semconv.js");
16
16
  exports.PROVIDER_MAP = {
17
17
  ChatOpenAI: "openai",
18
- AzureChatOpenAI: "azure.openai",
18
+ AzureChatOpenAI: "azure.ai.openai",
19
19
  ChatAnthropic: "anthropic",
20
20
  ChatGoogleGenerativeAI: "gcp.generative_ai",
21
21
  ChatVertexAI: "gcp.vertex_ai",
@@ -0,0 +1,34 @@
1
+ import type { Part } from "../genai-content.js";
2
+ /** Map a Responses `message.content` (string or list) to spec parts. */
3
+ export declare function messageContentToParts(content: unknown): Part[];
4
+ /** Map ONE Responses `input` item to `[eventName, role, parts]`, or null. */
5
+ export declare function inputItemToEvent(item: unknown): [string, string, Part[]] | null;
6
+ /** Map ONE `response.output` item to the assistant's choice parts. */
7
+ export declare function outputItemToChoiceParts(item: unknown): Part[];
8
+ /** Normalize the `input` kwarg to a list of items (bare string → one user message). */
9
+ export declare function normalizeInput(input: unknown): unknown[];
10
+ /** Spec parts of the LAST `user` message in `input` (for parent propagation). */
11
+ export declare function lastUserParts(input: unknown): Part[] | undefined;
12
+ /** Every function_call `(name, call_id)` from a response.output (for tool linkage). */
13
+ export declare function iterFunctionCalls(output: unknown): Array<[string, string]>;
14
+ /** Derive a raw finish-reason string from a Responses response. */
15
+ export declare function deriveFinishReason(response: unknown): string | undefined;
16
+ /**
17
+ * True when a Responses response is in a terminal state (generation finished).
18
+ * A `background: true` request can return non-terminal (`"queued"` /
19
+ * `"in_progress"`) with no assistant message yet — we must NOT emit a terminal
20
+ * `gen_ai.choice` / finish reason for those. A response with no status is
21
+ * treated as terminal so we never silently drop telemetry for the common
22
+ * synchronous call (or minimal mocks).
23
+ */
24
+ export declare function isTerminalResponse(response: unknown): boolean;
25
+ /** Map a raw Responses status/derived reason to a spec choice finish reason. */
26
+ export declare function mapChoiceFinishReason(raw: string | undefined): string;
27
+ /** Serialize `input` → spec `gen_ai.input.messages` JSON string. */
28
+ export declare function toInputMessages(input: unknown): string;
29
+ /** Serialize Responses `instructions` → spec `system_instructions` JSON string. */
30
+ export declare function toSystemInstructions(instructions: unknown): string;
31
+ /** Serialize `response.output` → spec `gen_ai.output.messages` JSON string. */
32
+ export declare function toOutputMessages(output: unknown, finishReason: string | undefined): string;
33
+ export declare function safeJsonForTool(obj: unknown): string;
34
+ //# sourceMappingURL=openai-content.d.ts.map