@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
@@ -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",
@@ -1,4 +1,5 @@
1
1
  import type { StructSDK } from "../core.js";
2
+ import { getStore } from "../context.js";
2
3
  import { StructCallbackHandler } from "./langchain-callback.js";
3
4
  /**
4
5
  * Export the handler so callers can register it explicitly
@@ -8,4 +9,6 @@ import { StructCallbackHandler } from "./langchain-callback.js";
8
9
  export declare function getLangchainHandler(): StructCallbackHandler | undefined;
9
10
  export declare function patch(sdk: StructSDK): Promise<void>;
10
11
  export declare function unpatch(): Promise<void>;
12
+ /** @internal */
13
+ export declare function _wrapIteratorWithSuppressionForTest(streamLike: unknown, suppressedSnapshot: ReturnType<typeof getStore>): void;
11
14
  //# sourceMappingURL=langchain.d.ts.map
@@ -3,6 +3,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.getLangchainHandler = getLangchainHandler;
4
4
  exports.patch = patch;
5
5
  exports.unpatch = unpatch;
6
+ exports._wrapIteratorWithSuppressionForTest = _wrapIteratorWithSuppressionForTest;
7
+ const context_js_1 = require("../context.js");
6
8
  const langchain_callback_js_1 = require("./langchain-callback.js");
7
9
  /**
8
10
  * LangChain.js integration — registers a BaseCallbackHandler subclass with
@@ -15,6 +17,7 @@ const langchain_callback_js_1 = require("./langchain-callback.js");
15
17
  * async microtask shuffling (which otherwise drops OTel's active context).
16
18
  */
17
19
  const STRUCT_WRAPPED = Symbol.for("struct.wrapped");
20
+ const SUPPRESS_ORIGINAL = Symbol.for("struct.suppress.original");
18
21
  const state = {};
19
22
  /**
20
23
  * Export the handler so callers can register it explicitly
@@ -46,8 +49,26 @@ async function patch(sdk) {
46
49
  state.cfg.originalConfigure = originalConfigure;
47
50
  const wrappedConfigure = function (...args) {
48
51
  const active = state.handler;
49
- if (active)
50
- args[0] = injectHandler(args[0], active);
52
+ if (active) {
53
+ // Self-audit round 5 (FIX D): resolveConfigureHandlerArgs invokes
54
+ // FALLIBLE methods on a CUSTOMER-supplied callback manager
55
+ // (mgr.addHandler / mgr.removeHandler, reading mgr.handlers) —
56
+ // operations vanilla LangChain's own configure() never makes on
57
+ // these exact caller-supplied objects. A throwing/exotic manager
58
+ // (broken subclass, hostile proxy) must not turn an otherwise
59
+ // successful invoke/stream/batch call into a thrown exception.
60
+ // Degrade: fall through to the ORIGINAL configure with the
61
+ // customer's UNMODIFIED args (args[0]/[1] are only reassigned AFTER
62
+ // resolveConfigureHandlerArgs returns, so a throw here leaves them
63
+ // untouched) — maybe no struct handler gets injected for this call,
64
+ // but the customer's call never breaks.
65
+ try {
66
+ [args[0], args[1]] = resolveConfigureHandlerArgs(args[0], args[1], active);
67
+ }
68
+ catch {
69
+ return originalConfigure.apply(cm, args);
70
+ }
71
+ }
51
72
  return originalConfigure.apply(cm, args);
52
73
  };
53
74
  wrappedConfigure[STRUCT_WRAPPED] = true;
@@ -58,13 +79,234 @@ async function patch(sdk) {
58
79
  state.cfg.originalConfigureSync = originalSync;
59
80
  const wrappedSync = function (...args) {
60
81
  const active = state.handler;
61
- if (active)
62
- args[0] = injectHandler(args[0], active);
82
+ if (active) {
83
+ // Same hazard as wrappedConfigure above — see its comment.
84
+ try {
85
+ [args[0], args[1]] = resolveConfigureHandlerArgs(args[0], args[1], active);
86
+ }
87
+ catch {
88
+ return originalSync.apply(cm, args);
89
+ }
90
+ }
63
91
  return originalSync.apply(cm, args);
64
92
  };
65
93
  wrappedSync[STRUCT_WRAPPED] = true;
66
94
  cm._configureSync = wrappedSync;
67
95
  }
96
+ await patchChatModelSuppression();
97
+ }
98
+ /**
99
+ * Chat-suppression handshake: wrap the base-class methods that every
100
+ * BaseChatModel subclass routes through (subclasses override `_generate` /
101
+ * `_streamResponseChunks`, never `generate` / `stream` directly) in an ALS
102
+ * scope with `suppressGenAi: true`. The provider-SDK patch (anthropic.ts)
103
+ * checks `isGenAiSuppressed()` inside that scope and skips emitting its own
104
+ * chat span, since the LangChain callback handler already owns it.
105
+ *
106
+ * `generate` lives directly on `BaseChatModel.prototype`. `stream` does NOT
107
+ * — LangChain's `BaseChatModel` inherits it, unmodified, from
108
+ * `Runnable.prototype` (in `@langchain/core/runnables`); subclasses hook
109
+ * streaming via `_streamResponseChunks`, which `Runnable.stream()` reaches
110
+ * through `_streamIterator` — verified against the installed @langchain/core
111
+ * (0.3.80) via `Object.getPrototypeOf` walk + `hasOwnProperty` checks.
112
+ *
113
+ * Critically, `Runnable` is also the base class of chains, tools, AND
114
+ * compiled LangGraph graphs (`CompiledStateGraph` / `Pregel`), and
115
+ * `Pregel.invoke()` delegates internally to `this.stream()`. Wrapping
116
+ * `Runnable.prototype.stream` directly would therefore suppress GenAI spans
117
+ * for the ENTIRE graph run, not just one LLM call — a direct
118
+ * `@anthropic-ai/sdk` call made from inside a tool/node body during
119
+ * `graph.stream()`/`graph.invoke()` would incorrectly see
120
+ * `isGenAiSuppressed() === true` and drop its chat span.
121
+ *
122
+ * So `stream` is SHADOWED on `BaseChatModel.prototype` instead of patched on
123
+ * `Runnable.prototype`: we read the inherited `Runnable.prototype.stream`
124
+ * (property lookup walks the prototype chain) and assign the wrapped
125
+ * version as an OWN property of `BaseChatModel.prototype` (assignment never
126
+ * walks the chain — it always creates/overwrites an own property). Chat
127
+ * model subclasses (`ChatAnthropic`, `ChatOpenAI`, etc.) resolve `stream` to
128
+ * this new own property and get the suppression scope; plain Runnables,
129
+ * tools, and compiled graphs never had an own `stream` and keep resolving to
130
+ * the untouched `Runnable.prototype.stream`.
131
+ *
132
+ * Execution-scoped via AsyncLocalStorage (JS has no imperative
133
+ * context.attach/detach), so this is parallel-safe: concurrent
134
+ * `model.generate()` calls each get their own ALS frame.
135
+ */
136
+ async function patchChatModelSuppression() {
137
+ const targets = [];
138
+ let chatProto;
139
+ try {
140
+ const chatModelsMod = (await import("@langchain/core/language_models/chat_models"));
141
+ chatProto = chatModelsMod.BaseChatModel?.prototype;
142
+ if (chatProto) {
143
+ const hadOwnProperty = Object.prototype.hasOwnProperty.call(chatProto, "generate");
144
+ wrapSuppressedGenerate(chatProto, "generate");
145
+ targets.push({
146
+ proto: chatProto,
147
+ method: "generate",
148
+ createdOwnProperty: !hadOwnProperty,
149
+ });
150
+ }
151
+ }
152
+ catch {
153
+ /* @langchain/core/language_models/chat_models not installed — skip */
154
+ }
155
+ try {
156
+ if (chatProto) {
157
+ const createdOwnProperty = wrapSuppressedStreamShadow(chatProto, "stream");
158
+ if (createdOwnProperty !== undefined) {
159
+ targets.push({ proto: chatProto, method: "stream", createdOwnProperty });
160
+ }
161
+ }
162
+ }
163
+ catch {
164
+ /* stream is read off the already-resolved chatProto above (no separate
165
+ import here) — guards defensively against an unexpected shape change
166
+ in the installed @langchain/core, not a missing package. */
167
+ }
168
+ // Merge with any targets from a prior patch() call so unpatch() can still
169
+ // restore everything (mirrors the CallbackManager prev-state preservation
170
+ // above — a second patch() call must not strand the original refs).
171
+ const prevTargets = state.suppressionTargets ?? [];
172
+ const merged = [...prevTargets];
173
+ for (const t of targets) {
174
+ if (!merged.some((m) => m.proto === t.proto && m.method === t.method)) {
175
+ merged.push(t);
176
+ }
177
+ }
178
+ state.suppressionTargets = merged;
179
+ }
180
+ /** Wrap `proto[method]` (BaseChatModel.generate) in an ALS-suppressed scope. */
181
+ function wrapSuppressedGenerate(proto, method) {
182
+ const original = proto[method];
183
+ if (typeof original !== "function" || original[STRUCT_WRAPPED])
184
+ return;
185
+ const wrapped = function (...args) {
186
+ return (0, context_js_1.runWithContext)({ suppressGenAi: true }, () => original.apply(this, args));
187
+ };
188
+ wrapped[STRUCT_WRAPPED] = true;
189
+ wrapped[SUPPRESS_ORIGINAL] = original;
190
+ proto[method] = wrapped;
191
+ }
192
+ /**
193
+ * Shadow `proto[method]` (BaseChatModel.prototype.stream) with an
194
+ * ALS-suppressed wrapper around the INHERITED implementation
195
+ * (Runnable.prototype.stream), without ever touching Runnable.prototype
196
+ * itself.
197
+ *
198
+ * Reading `proto[method]` resolves up the prototype chain to
199
+ * `Runnable.prototype.stream` (BaseChatModel has no own `stream`).
200
+ * Assigning `proto[method] = wrapped` then creates a NEW OWN property on
201
+ * `BaseChatModel.prototype` that shadows the inherited one — subclasses of
202
+ * BaseChatModel resolve to this own property; plain Runnables (chains,
203
+ * tools, compiled LangGraph graphs), which don't inherit from
204
+ * BaseChatModel, are completely unaffected.
205
+ *
206
+ * `stream()` is async but returns an `IterableReadableStream` whose
207
+ * underlying provider call happens lazily on first pull — the `await
208
+ * original.apply(...)` below only awaits the initial buffering (see
209
+ * `AsyncGeneratorWithSetup` in @langchain/core), not the full generation. So
210
+ * the suppression scope must also wrap the returned stream's
211
+ * `Symbol.asyncIterator` and re-enter a suppressed store snapshot on every
212
+ * `next()/return()/throw()` — same pattern as the iterator wrap in
213
+ * anthropic.ts's `wrapStream` (lines ~414-437).
214
+ *
215
+ * Returns whether this call created a new own property (true), found one
216
+ * already shadowed by a prior patch (false — idempotent no-op), or could
217
+ * not proceed because there's no inherited method to wrap (undefined).
218
+ */
219
+ function wrapSuppressedStreamShadow(proto, method) {
220
+ const hadOwnProperty = Object.prototype.hasOwnProperty.call(proto, method);
221
+ const original = proto[method];
222
+ if (typeof original !== "function")
223
+ return undefined;
224
+ if (original[STRUCT_WRAPPED])
225
+ return false;
226
+ const wrapped = function (...args) {
227
+ return (0, context_js_1.runWithContext)({ suppressGenAi: true }, async () => {
228
+ const result = await original.apply(this, args);
229
+ const suppressedSnapshot = { ...((0, context_js_1.getStore)() ?? {}), suppressGenAi: true };
230
+ wrapIteratorWithSuppression(result, suppressedSnapshot);
231
+ return result;
232
+ });
233
+ };
234
+ wrapped[STRUCT_WRAPPED] = true;
235
+ wrapped[SUPPRESS_ORIGINAL] = original;
236
+ proto[method] = wrapped;
237
+ // If BaseChatModel.prototype already had its own `stream` (unexpected on
238
+ // current @langchain/core, but defensive against future versions), this
239
+ // wrap only reassigned an existing own property — restoring on unpatch()
240
+ // must reassign the original back, not delete.
241
+ return !hadOwnProperty;
242
+ }
243
+ function wrapIteratorWithSuppression(streamLike, suppressedSnapshot) {
244
+ const it = streamLike;
245
+ try {
246
+ // Read + bind are INSIDE the guard: a hostile/proxy Symbol.asyncIterator
247
+ // getter throws on the property READ (not just the assignment), and this
248
+ // runs synchronously right after `await original.stream()` resolves — an
249
+ // escaped throw would reject the customer's successful call.
250
+ if (!it || typeof it[Symbol.asyncIterator] !== "function")
251
+ return;
252
+ const originalIter = it[Symbol.asyncIterator].bind(it);
253
+ it[Symbol.asyncIterator] = () => {
254
+ const inner = originalIter();
255
+ const wrappedIterator = {
256
+ next() {
257
+ return (0, context_js_1.runWithStore)(suppressedSnapshot, () => inner.next());
258
+ },
259
+ return(value) {
260
+ return (0, context_js_1.runWithStore)(suppressedSnapshot, () => inner.return ? inner.return(value) : Promise.resolve({ value, done: true }));
261
+ },
262
+ throw(err) {
263
+ return (0, context_js_1.runWithStore)(suppressedSnapshot, () => inner.throw ? inner.throw(err) : Promise.reject(err));
264
+ },
265
+ };
266
+ return wrappedIterator;
267
+ };
268
+ }
269
+ catch {
270
+ // Same hazard class as anthropic.ts's instrumentRawStream /
271
+ // instrumentSyncIterable (Task-10 review fix): the LangChain-created
272
+ // stream returned by `Runnable.prototype.stream` (a host-owned
273
+ // IterableReadableStream-like object) may be frozen/sealed, or otherwise
274
+ // have a non-writable/non-configurable Symbol.asyncIterator — assigning
275
+ // to it throws a TypeError in strict mode. This call happens
276
+ // synchronously inside `wrapSuppressedStreamShadow`'s async body, right
277
+ // after `await original.apply(...)` resolves — an uncaught throw here
278
+ // would turn the customer's SUCCESSFUL `.stream()` call into a rejected
279
+ // promise purely because OUR suppression instrumentation couldn't
280
+ // attach. Degrade gracefully instead: leave the stream's own iterator
281
+ // untouched (the failed assignment never took effect) and let the
282
+ // caller return the original object unchanged. Consequence: a raw
283
+ // provider-SDK call made while consuming this stream won't see
284
+ // `suppressGenAi` and may emit a duplicate chat span — acceptable;
285
+ // breaking the host is not.
286
+ }
287
+ }
288
+ function unpatchChatModelSuppression() {
289
+ const targets = state.suppressionTargets;
290
+ if (!targets)
291
+ return;
292
+ for (const { proto, method, createdOwnProperty } of targets) {
293
+ const current = proto[method];
294
+ if (!current?.[STRUCT_WRAPPED])
295
+ continue;
296
+ if (createdOwnProperty) {
297
+ // The wrap created this as a new own property (e.g. shadowing
298
+ // BaseChatModel.prototype.stream over the inherited
299
+ // Runnable.prototype.stream) — delete it to restore inheritance
300
+ // rather than reassigning the original function onto the prototype,
301
+ // which would leave a stray own property and change prototype
302
+ // semantics (hasOwnProperty, for..in, etc.) even after "restore".
303
+ delete proto[method];
304
+ }
305
+ else if (current[SUPPRESS_ORIGINAL]) {
306
+ proto[method] = current[SUPPRESS_ORIGINAL];
307
+ }
308
+ }
309
+ state.suppressionTargets = undefined;
68
310
  }
69
311
  async function unpatch() {
70
312
  if (state.cfg?.mod) {
@@ -77,24 +319,128 @@ async function unpatch() {
77
319
  cm._configureSync = state.cfg.originalConfigureSync;
78
320
  }
79
321
  }
322
+ unpatchChatModelSuppression();
80
323
  state.handler = undefined;
81
324
  state.cfg = undefined;
82
325
  }
326
+ /**
327
+ * Decide the (inheritable, local) pair to hand to the original
328
+ * `configure`/`_configureSync` so our handler fires EXACTLY ONCE per run,
329
+ * no matter how the caller already arranged their `inheritableHandlers` /
330
+ * `localHandlers` (LangChain's `configure(inheritable, local, ...)`
331
+ * parameter order — `args[0]`/`args[1]` at the call sites above).
332
+ *
333
+ * Dedup is by IDENTITY against `handler` (`state.handler`, our own exact
334
+ * instance) — never by `name === "struct"`. A caller-supplied handler that
335
+ * merely happens to be named "struct" (but isn't ours) must survive
336
+ * untouched; matching by name would incorrectly strip or remove it.
337
+ *
338
+ * `existing`/`local` can each be `undefined`, a plain array of handlers, or
339
+ * a `BaseCallbackManager` instance (has `.handlers`/`.addHandler`/
340
+ * `.removeHandler`) — LangChain accepts both shapes for either parameter.
341
+ *
342
+ * Three cases, checked by identity via `containsHandler`:
343
+ * 1. Not present in `local` at all — inject into `inheritable`
344
+ * (idempotent: `injectHandler` no-ops if it's already there, e.g. via
345
+ * legitimate inheritance from a parent run's already-configured
346
+ * inheritable list). This is the common case.
347
+ * 2. Present in `local` only (not yet in `inheritable`) — do NOT inject.
348
+ * The local copy alone will fire once for this run; injecting a
349
+ * second (inheritable) copy would double-fire it, and there's no need
350
+ * to mutate the caller's local value to avoid that — simply not
351
+ * adding it to `inheritable` is enough.
352
+ * 3. Present in BOTH already (e.g. the caller wired the exact same
353
+ * instance into both parameters themselves) — leaving both would
354
+ * double-fire regardless of what we do to `inheritable`, so as a
355
+ * narrow, targeted fallback we remove ONLY our exact instance from
356
+ * `local` (array: pure filter, no mutation of caller state; manager:
357
+ * its own sanctioned `removeHandler` API) and keep the inheritable
358
+ * copy, which is the one that also propagates to descendant runs.
359
+ *
360
+ * EXCEPTION — aliased MANAGER (`inheritable === local`, the exact same
361
+ * `BaseCallbackManager` instance passed as BOTH configure args): that
362
+ * object has only ONE `.handlers` array and ONE `.inheritableHandlers`
363
+ * array. Calling `mgr.removeHandler(handler)` on it removes our handler
364
+ * from BOTH, on the ONE shared object — since `inheritable` and `local`
365
+ * are the same reference, that strips it from the "inheritable" side
366
+ * too, and struct fires ZERO times instead of once. In this case, do
367
+ * NOT call `removeHandler` at all: keep the shared manager instance on
368
+ * the inheritable side (unmodified — it still carries our handler and
369
+ * still propagates to descendant runs) and hand back an empty array for
370
+ * `local` so nothing double-delivers. This exception is manager-only:
371
+ * array aliasing (the same array reference passed as both parameters)
372
+ * has no such hazard — `removeHandlerFromLocal`'s array branch is a
373
+ * pure `.filter` that returns a new array without mutating the shared
374
+ * one `inheritable` still points at, so the general case below already
375
+ * handles it correctly (and preserves any other handlers on that array,
376
+ * which blanket-emptying to `[]` would incorrectly drop).
377
+ */
378
+ function resolveConfigureHandlerArgs(inheritable, local, handler) {
379
+ const localHasIt = containsHandler(local, handler);
380
+ if (!localHasIt) {
381
+ return [injectHandler(inheritable, handler), local];
382
+ }
383
+ if (containsHandler(inheritable, handler)) {
384
+ if (inheritable === local && !Array.isArray(local)) {
385
+ // Aliased manager instance on both sides — see the EXCEPTION note
386
+ // above. Keep it on the inheritable side and supply an empty local
387
+ // value instead of calling removeHandler, which would mutate the one
388
+ // shared object out from under both parameters.
389
+ return [inheritable, []];
390
+ }
391
+ return [inheritable, removeHandlerFromLocal(local, handler)];
392
+ }
393
+ return [inheritable, local];
394
+ }
395
+ /** Identity check: does `v` (array or BaseCallbackManager-shaped) already
396
+ * contain our EXACT `handler` instance? */
397
+ function containsHandler(v, handler) {
398
+ if (Array.isArray(v))
399
+ return v.some((h) => h === handler);
400
+ const mgr = v;
401
+ return !!mgr && Array.isArray(mgr.handlers) && mgr.handlers.some((h) => h === handler);
402
+ }
403
+ /**
404
+ * Remove our exact `handler` instance from a LOCAL value — used only for
405
+ * the narrow "present on both sides already" fallback in
406
+ * `resolveConfigureHandlerArgs`. Array form is a pure filter (returns a new
407
+ * array; the caller's array is never mutated). Manager form calls the
408
+ * manager's own `removeHandler` (BaseCallbackManager's sanctioned API,
409
+ * removes from both `.handlers` and `.inheritableHandlers`) — a targeted,
410
+ * supported mutation, not raw array splicing.
411
+ */
412
+ function removeHandlerFromLocal(v, handler) {
413
+ if (Array.isArray(v))
414
+ return v.filter((h) => h !== handler);
415
+ const mgr = v;
416
+ if (mgr && typeof mgr.removeHandler === "function")
417
+ mgr.removeHandler(handler);
418
+ return v;
419
+ }
83
420
  function injectHandler(existing, handler) {
84
421
  if (existing == null)
85
422
  return [handler];
86
423
  if (Array.isArray(existing)) {
87
- if (existing.some((h) => h?.name === "struct")) {
424
+ if (existing.some((h) => h === handler))
88
425
  return existing;
89
- }
90
426
  return [...existing, handler];
91
427
  }
92
428
  // BaseCallbackManager instance: addHandler on it.
93
429
  const mgr = existing;
94
430
  if (typeof mgr.addHandler === "function" &&
95
- !mgr.handlers?.some((h) => h?.name === "struct")) {
431
+ !mgr.handlers?.some((h) => h === handler)) {
96
432
  mgr.addHandler(handler, true);
97
433
  }
98
434
  return existing;
99
435
  }
436
+ // ---------------------------------------------------------------------------
437
+ // Test-only export — mirrors anthropic.ts's `_wrapStreamForTest` pattern.
438
+ // Lets the mutation-guard test drive `wrapIteratorWithSuppression` directly
439
+ // against a synthetic (possibly frozen) stream-like object without needing
440
+ // to mock `@langchain/core`'s BaseChatModel prototype chain.
441
+ // ---------------------------------------------------------------------------
442
+ /** @internal */
443
+ function _wrapIteratorWithSuppressionForTest(streamLike, suppressedSnapshot) {
444
+ wrapIteratorWithSuppression(streamLike, suppressedSnapshot);
445
+ }
100
446
  //# sourceMappingURL=langchain.js.map
@@ -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