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