@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
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|