@langchain/core 0.3.79 → 0.3.80

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.
@@ -1,4 +1,39 @@
1
1
  "use strict";
2
+ /**
3
+ * Load LangChain objects from JSON strings or objects.
4
+ *
5
+ * ## How it works
6
+ *
7
+ * Each `Serializable` LangChain object has a unique identifier (its "class path"),
8
+ * which is a list of strings representing the module path and class name. For example:
9
+ *
10
+ * - `AIMessage` -> `["langchain_core", "messages", "ai", "AIMessage"]`
11
+ * - `ChatPromptTemplate` -> `["langchain_core", "prompts", "chat", "ChatPromptTemplate"]`
12
+ *
13
+ * When deserializing, the class path is validated against supported namespaces.
14
+ *
15
+ * ## Security model
16
+ *
17
+ * The `secretsFromEnv` parameter controls whether secrets can be loaded from environment
18
+ * variables:
19
+ *
20
+ * - `false` (default): Secrets must be provided in `secretsMap`. If a secret is not
21
+ * found, `null` is returned instead of loading from environment variables.
22
+ * - `true`: If a secret is not found in `secretsMap`, it will be loaded from
23
+ * environment variables. Use this only in trusted environments.
24
+ *
25
+ * ### Injection protection (escape-based)
26
+ *
27
+ * During serialization, plain objects that contain an `'lc'` key are escaped by wrapping
28
+ * them: `{"__lc_escaped__": {...}}`. During deserialization, escaped objects are unwrapped
29
+ * and returned as plain objects, NOT instantiated as LC objects.
30
+ *
31
+ * This is an allowlist approach: only objects explicitly produced by
32
+ * `Serializable.toJSON()` (which are NOT escaped) are treated as LC objects;
33
+ * everything else is user data.
34
+ *
35
+ * @module
36
+ */
2
37
  var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
38
  if (k2 === undefined) k2 = k;
4
39
  var desc = Object.getOwnPropertyDescriptor(m, k);
@@ -39,6 +74,12 @@ const import_constants_js_1 = require("./import_constants.cjs");
39
74
  const coreImportMap = __importStar(require("./import_map.cjs"));
40
75
  const map_keys_js_1 = require("./map_keys.cjs");
41
76
  const env_js_1 = require("../utils/env.cjs");
77
+ const validation_js_1 = require("./validation.cjs");
78
+ /**
79
+ * Default maximum recursion depth for deserialization.
80
+ * This provides protection against DoS attacks via deeply nested structures.
81
+ */
82
+ const DEFAULT_MAX_DEPTH = 50;
42
83
  function combineAliasesAndInvert(constructor) {
43
84
  const aliases = {};
44
85
  for (
@@ -51,53 +92,74 @@ function combineAliasesAndInvert(constructor) {
51
92
  return acc;
52
93
  }, {});
53
94
  }
95
+ /**
96
+ * Recursively revive a value, handling escape markers and LC objects.
97
+ *
98
+ * This function handles:
99
+ * 1. Escaped dicts - unwrapped and returned as plain objects
100
+ * 2. LC secret objects - resolved from secretsMap or env
101
+ * 3. LC constructor objects - instantiated
102
+ * 4. Regular objects/arrays - recursed into
103
+ */
54
104
  async function reviver(value) {
55
- const { optionalImportsMap = {}, optionalImportEntrypoints = [], importMap = {}, secretsMap = {}, path = ["$"], } = this;
105
+ const { optionalImportsMap, optionalImportEntrypoints, importMap, secretsMap, secretsFromEnv, path, depth, maxDepth, } = this;
56
106
  const pathStr = path.join(".");
57
- if (typeof value === "object" &&
58
- value !== null &&
59
- !Array.isArray(value) &&
60
- "lc" in value &&
61
- "type" in value &&
62
- "id" in value &&
63
- value.lc === 1 &&
64
- value.type === "secret") {
65
- const serialized = value;
107
+ // Check recursion depth to prevent DoS via deeply nested structures
108
+ if (depth > maxDepth) {
109
+ throw new Error(`Maximum recursion depth (${maxDepth}) exceeded during deserialization. ` +
110
+ `This may indicate a malicious payload or you may need to increase maxDepth.`);
111
+ }
112
+ // If not an object, return as-is
113
+ if (typeof value !== "object" || value == null) {
114
+ return value;
115
+ }
116
+ // Handle arrays - recurse into elements
117
+ if (Array.isArray(value)) {
118
+ return Promise.all(value.map((v, i) => reviver.call({ ...this, path: [...path, `${i}`], depth: depth + 1 }, v)));
119
+ }
120
+ // It's an object - check for escape marker FIRST
121
+ const record = value;
122
+ if ((0, validation_js_1.isEscapedObject)(record)) {
123
+ // This is an escaped user object - unwrap and return as-is (no LC processing)
124
+ return (0, validation_js_1.unescapeValue)(record);
125
+ }
126
+ // Check for LC secret object
127
+ if ("lc" in record &&
128
+ "type" in record &&
129
+ "id" in record &&
130
+ record.lc === 1 &&
131
+ record.type === "secret") {
132
+ const serialized = record;
66
133
  const [key] = serialized.id;
67
134
  if (key in secretsMap) {
68
135
  return secretsMap[key];
69
136
  }
70
- else {
137
+ else if (secretsFromEnv) {
71
138
  const secretValueInEnv = (0, env_js_1.getEnvironmentVariable)(key);
72
139
  if (secretValueInEnv) {
73
140
  return secretValueInEnv;
74
141
  }
75
- else {
76
- throw new Error(`Missing key "${key}" for ${pathStr} in load(secretsMap={})`);
77
- }
78
142
  }
143
+ throw new Error(`Missing secret "${key}" at ${pathStr}`);
79
144
  }
80
- else if (typeof value === "object" &&
81
- value !== null &&
82
- !Array.isArray(value) &&
83
- "lc" in value &&
84
- "type" in value &&
85
- "id" in value &&
86
- value.lc === 1 &&
87
- value.type === "not_implemented") {
88
- const serialized = value;
145
+ // Check for LC not_implemented object
146
+ if ("lc" in record &&
147
+ "type" in record &&
148
+ "id" in record &&
149
+ record.lc === 1 &&
150
+ record.type === "not_implemented") {
151
+ const serialized = record;
89
152
  const str = JSON.stringify(serialized);
90
153
  throw new Error(`Trying to load an object that doesn't implement serialization: ${pathStr} -> ${str}`);
91
154
  }
92
- else if (typeof value === "object" &&
93
- value !== null &&
94
- !Array.isArray(value) &&
95
- "lc" in value &&
96
- "type" in value &&
97
- "id" in value &&
98
- "kwargs" in value &&
99
- value.lc === 1) {
100
- const serialized = value;
155
+ // Check for LC constructor object
156
+ if ("lc" in record &&
157
+ "type" in record &&
158
+ "id" in record &&
159
+ "kwargs" in record &&
160
+ record.lc === 1 &&
161
+ record.type === "constructor") {
162
+ const serialized = record;
101
163
  const str = JSON.stringify(serialized);
102
164
  const [name, ...namespaceReverse] = serialized.id.slice().reverse();
103
165
  const namespace = namespaceReverse.reverse();
@@ -164,35 +226,60 @@ async function reviver(value) {
164
226
  throw new Error(`Invalid identifer: ${pathStr} -> ${str}`);
165
227
  }
166
228
  // Recurse on the arguments, which may be serialized objects themselves
167
- const kwargs = await reviver.call({ ...this, path: [...path, "kwargs"] }, serialized.kwargs);
229
+ const kwargs = await reviver.call({ ...this, path: [...path, "kwargs"], depth: depth + 1 }, serialized.kwargs);
168
230
  // Construct the object
169
- if (serialized.type === "constructor") {
170
- // eslint-disable-next-line new-cap, @typescript-eslint/no-explicit-any
171
- const instance = new builder((0, map_keys_js_1.mapKeys)(kwargs, map_keys_js_1.keyFromJson, combineAliasesAndInvert(builder)));
172
- // Minification in severless/edge runtimes will mange the
173
- // name of classes presented in traces. As the names in import map
174
- // are present as-is even with minification, use these names instead
175
- Object.defineProperty(instance.constructor, "name", { value: name });
176
- return instance;
177
- }
178
- else {
179
- throw new Error(`Invalid type: ${pathStr} -> ${str}`);
180
- }
231
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
232
+ const instance = new builder((0, map_keys_js_1.mapKeys)(kwargs, map_keys_js_1.keyFromJson, combineAliasesAndInvert(builder)));
233
+ // Minification in severless/edge runtimes will mange the
234
+ // name of classes presented in traces. As the names in import map
235
+ // are present as-is even with minification, use these names instead
236
+ Object.defineProperty(instance.constructor, "name", { value: name });
237
+ return instance;
181
238
  }
182
- else if (typeof value === "object" && value !== null) {
183
- if (Array.isArray(value)) {
184
- return Promise.all(value.map((v, i) => reviver.call({ ...this, path: [...path, `${i}`] }, v)));
185
- }
186
- else {
187
- return Object.fromEntries(await Promise.all(Object.entries(value).map(async ([key, value]) => [
188
- key,
189
- await reviver.call({ ...this, path: [...path, key] }, value),
190
- ])));
191
- }
239
+ // Regular object - recurse into values
240
+ const result = {};
241
+ for (const [key, val] of Object.entries(record)) {
242
+ result[key] = await reviver.call({ ...this, path: [...path, key], depth: depth + 1 }, val);
192
243
  }
193
- return value;
244
+ return result;
194
245
  }
195
- async function load(text, mappings) {
246
+ /**
247
+ * Load a LangChain object from a JSON string.
248
+ *
249
+ * @param text - The JSON string to parse and load.
250
+ * @param options - Options for loading.
251
+ * @returns The loaded LangChain object.
252
+ *
253
+ * @example
254
+ * ```typescript
255
+ * import { load } from "@langchain/core/load";
256
+ * import { AIMessage } from "@langchain/core/messages";
257
+ *
258
+ * // Basic usage - secrets must be provided explicitly
259
+ * const msg = await load<AIMessage>(jsonString);
260
+ *
261
+ * // With secrets from a map
262
+ * const msg = await load<AIMessage>(jsonString, {
263
+ * secretsMap: { OPENAI_API_KEY: "sk-..." }
264
+ * });
265
+ *
266
+ * // Allow loading secrets from environment (use with caution)
267
+ * const msg = await load<AIMessage>(jsonString, {
268
+ * secretsFromEnv: true
269
+ * });
270
+ * ```
271
+ */
272
+ async function load(text, options) {
196
273
  const json = JSON.parse(text);
197
- return reviver.call({ ...mappings }, json);
274
+ const context = {
275
+ optionalImportsMap: options?.optionalImportsMap ?? {},
276
+ optionalImportEntrypoints: options?.optionalImportEntrypoints ?? [],
277
+ secretsMap: options?.secretsMap ?? {},
278
+ secretsFromEnv: options?.secretsFromEnv ?? false,
279
+ importMap: options?.importMap ?? {},
280
+ path: ["$"],
281
+ depth: 0,
282
+ maxDepth: options?.maxDepth ?? DEFAULT_MAX_DEPTH,
283
+ };
284
+ return reviver.call(context, json);
198
285
  }
@@ -1,7 +1,151 @@
1
+ /**
2
+ * Load LangChain objects from JSON strings or objects.
3
+ *
4
+ * ## How it works
5
+ *
6
+ * Each `Serializable` LangChain object has a unique identifier (its "class path"),
7
+ * which is a list of strings representing the module path and class name. For example:
8
+ *
9
+ * - `AIMessage` -> `["langchain_core", "messages", "ai", "AIMessage"]`
10
+ * - `ChatPromptTemplate` -> `["langchain_core", "prompts", "chat", "ChatPromptTemplate"]`
11
+ *
12
+ * When deserializing, the class path is validated against supported namespaces.
13
+ *
14
+ * ## Security model
15
+ *
16
+ * The `secretsFromEnv` parameter controls whether secrets can be loaded from environment
17
+ * variables:
18
+ *
19
+ * - `false` (default): Secrets must be provided in `secretsMap`. If a secret is not
20
+ * found, `null` is returned instead of loading from environment variables.
21
+ * - `true`: If a secret is not found in `secretsMap`, it will be loaded from
22
+ * environment variables. Use this only in trusted environments.
23
+ *
24
+ * ### Injection protection (escape-based)
25
+ *
26
+ * During serialization, plain objects that contain an `'lc'` key are escaped by wrapping
27
+ * them: `{"__lc_escaped__": {...}}`. During deserialization, escaped objects are unwrapped
28
+ * and returned as plain objects, NOT instantiated as LC objects.
29
+ *
30
+ * This is an allowlist approach: only objects explicitly produced by
31
+ * `Serializable.toJSON()` (which are NOT escaped) are treated as LC objects;
32
+ * everything else is user data.
33
+ *
34
+ * @module
35
+ */
1
36
  import type { OptionalImportMap, SecretMap } from "./import_type.js";
2
- export declare function load<T>(text: string, mappings?: {
37
+ /**
38
+ * Options for loading serialized LangChain objects.
39
+ *
40
+ * @remarks
41
+ * **Security considerations:**
42
+ *
43
+ * Deserialization can instantiate arbitrary classes from the allowed namespaces.
44
+ * When loading untrusted data, be aware that:
45
+ *
46
+ * 1. **`secretsFromEnv`**: Defaults to `false`. Setting to `true` allows the
47
+ * deserializer to read environment variables, which could leak secrets if
48
+ * the serialized data contains malicious secret references.
49
+ *
50
+ * 2. **`importMap` / `optionalImportsMap`**: These allow extending which classes
51
+ * can be instantiated. Never populate these from user input. Only include
52
+ * modules you explicitly trust.
53
+ *
54
+ * 3. **Class instantiation**: Allowed classes will have their constructors called
55
+ * with the deserialized kwargs. If a class performs side effects in its
56
+ * constructor (network calls, file I/O, etc.), those will execute.
57
+ */
58
+ export interface LoadOptions {
59
+ /**
60
+ * A map of secrets to load. Keys are secret identifiers, values are the secret values.
61
+ *
62
+ * If a secret is not found in this map and `secretsFromEnv` is `false`, an error is
63
+ * thrown. If `secretsFromEnv` is `true`, the secret will be loaded from environment
64
+ * variables (if not found there either, an error is thrown).
65
+ */
3
66
  secretsMap?: SecretMap;
67
+ /**
68
+ * Whether to load secrets from environment variables when not found in `secretsMap`.
69
+ *
70
+ * @default false
71
+ *
72
+ * @remarks
73
+ * **Security warning:** Setting this to `true` allows the deserializer to read
74
+ * environment variables, which could be a security risk if the serialized data
75
+ * is not trusted. Only set this to `true` when deserializing data from trusted
76
+ * sources (e.g., your own database, not user input).
77
+ */
78
+ secretsFromEnv?: boolean;
79
+ /**
80
+ * A map of optional imports. Keys are namespace paths (e.g., "langchain_community/llms"),
81
+ * values are the imported modules.
82
+ *
83
+ * @remarks
84
+ * **Security warning:** This extends which classes can be instantiated during
85
+ * deserialization. Never populate this map with values derived from user input.
86
+ * Only include modules that you explicitly trust and have reviewed.
87
+ *
88
+ * Classes in these modules can be instantiated with attacker-controlled kwargs
89
+ * if the serialized data is untrusted.
90
+ */
4
91
  optionalImportsMap?: OptionalImportMap;
92
+ /**
93
+ * Additional optional import entrypoints to allow beyond the defaults.
94
+ *
95
+ * @remarks
96
+ * **Security warning:** This extends which namespace paths are considered valid
97
+ * for deserialization. Never populate this array with values derived from user
98
+ * input. Each entrypoint you add expands the attack surface for deserialization.
99
+ */
5
100
  optionalImportEntrypoints?: string[];
101
+ /**
102
+ * Additional import map for the "langchain" namespace.
103
+ *
104
+ * @remarks
105
+ * **Security warning:** This extends which classes can be instantiated during
106
+ * deserialization. Never populate this map with values derived from user input.
107
+ * Only include modules that you explicitly trust and have reviewed.
108
+ *
109
+ * Any class exposed through this map can be instantiated with attacker-controlled
110
+ * kwargs if the serialized data is untrusted.
111
+ */
6
112
  importMap?: Record<string, unknown>;
7
- }): Promise<T>;
113
+ /**
114
+ * Maximum recursion depth allowed during deserialization.
115
+ *
116
+ * @default 50
117
+ *
118
+ * @remarks
119
+ * This limit protects against denial-of-service attacks using deeply nested
120
+ * JSON structures that could cause stack overflow. If your legitimate data
121
+ * requires deeper nesting, you can increase this limit.
122
+ */
123
+ maxDepth?: number;
124
+ }
125
+ /**
126
+ * Load a LangChain object from a JSON string.
127
+ *
128
+ * @param text - The JSON string to parse and load.
129
+ * @param options - Options for loading.
130
+ * @returns The loaded LangChain object.
131
+ *
132
+ * @example
133
+ * ```typescript
134
+ * import { load } from "@langchain/core/load";
135
+ * import { AIMessage } from "@langchain/core/messages";
136
+ *
137
+ * // Basic usage - secrets must be provided explicitly
138
+ * const msg = await load<AIMessage>(jsonString);
139
+ *
140
+ * // With secrets from a map
141
+ * const msg = await load<AIMessage>(jsonString, {
142
+ * secretsMap: { OPENAI_API_KEY: "sk-..." }
143
+ * });
144
+ *
145
+ * // Allow loading secrets from environment (use with caution)
146
+ * const msg = await load<AIMessage>(jsonString, {
147
+ * secretsFromEnv: true
148
+ * });
149
+ * ```
150
+ */
151
+ export declare function load<T>(text: string, options?: LoadOptions): Promise<T>;
@@ -1,8 +1,49 @@
1
+ /**
2
+ * Load LangChain objects from JSON strings or objects.
3
+ *
4
+ * ## How it works
5
+ *
6
+ * Each `Serializable` LangChain object has a unique identifier (its "class path"),
7
+ * which is a list of strings representing the module path and class name. For example:
8
+ *
9
+ * - `AIMessage` -> `["langchain_core", "messages", "ai", "AIMessage"]`
10
+ * - `ChatPromptTemplate` -> `["langchain_core", "prompts", "chat", "ChatPromptTemplate"]`
11
+ *
12
+ * When deserializing, the class path is validated against supported namespaces.
13
+ *
14
+ * ## Security model
15
+ *
16
+ * The `secretsFromEnv` parameter controls whether secrets can be loaded from environment
17
+ * variables:
18
+ *
19
+ * - `false` (default): Secrets must be provided in `secretsMap`. If a secret is not
20
+ * found, `null` is returned instead of loading from environment variables.
21
+ * - `true`: If a secret is not found in `secretsMap`, it will be loaded from
22
+ * environment variables. Use this only in trusted environments.
23
+ *
24
+ * ### Injection protection (escape-based)
25
+ *
26
+ * During serialization, plain objects that contain an `'lc'` key are escaped by wrapping
27
+ * them: `{"__lc_escaped__": {...}}`. During deserialization, escaped objects are unwrapped
28
+ * and returned as plain objects, NOT instantiated as LC objects.
29
+ *
30
+ * This is an allowlist approach: only objects explicitly produced by
31
+ * `Serializable.toJSON()` (which are NOT escaped) are treated as LC objects;
32
+ * everything else is user data.
33
+ *
34
+ * @module
35
+ */
1
36
  import { get_lc_unique_name, } from "./serializable.js";
2
37
  import { optionalImportEntrypoints as defaultOptionalImportEntrypoints } from "./import_constants.js";
3
38
  import * as coreImportMap from "./import_map.js";
4
39
  import { keyFromJson, mapKeys } from "./map_keys.js";
5
40
  import { getEnvironmentVariable } from "../utils/env.js";
41
+ import { isEscapedObject, unescapeValue } from "./validation.js";
42
+ /**
43
+ * Default maximum recursion depth for deserialization.
44
+ * This provides protection against DoS attacks via deeply nested structures.
45
+ */
46
+ const DEFAULT_MAX_DEPTH = 50;
6
47
  function combineAliasesAndInvert(constructor) {
7
48
  const aliases = {};
8
49
  for (
@@ -15,53 +56,74 @@ function combineAliasesAndInvert(constructor) {
15
56
  return acc;
16
57
  }, {});
17
58
  }
59
+ /**
60
+ * Recursively revive a value, handling escape markers and LC objects.
61
+ *
62
+ * This function handles:
63
+ * 1. Escaped dicts - unwrapped and returned as plain objects
64
+ * 2. LC secret objects - resolved from secretsMap or env
65
+ * 3. LC constructor objects - instantiated
66
+ * 4. Regular objects/arrays - recursed into
67
+ */
18
68
  async function reviver(value) {
19
- const { optionalImportsMap = {}, optionalImportEntrypoints = [], importMap = {}, secretsMap = {}, path = ["$"], } = this;
69
+ const { optionalImportsMap, optionalImportEntrypoints, importMap, secretsMap, secretsFromEnv, path, depth, maxDepth, } = this;
20
70
  const pathStr = path.join(".");
21
- if (typeof value === "object" &&
22
- value !== null &&
23
- !Array.isArray(value) &&
24
- "lc" in value &&
25
- "type" in value &&
26
- "id" in value &&
27
- value.lc === 1 &&
28
- value.type === "secret") {
29
- const serialized = value;
71
+ // Check recursion depth to prevent DoS via deeply nested structures
72
+ if (depth > maxDepth) {
73
+ throw new Error(`Maximum recursion depth (${maxDepth}) exceeded during deserialization. ` +
74
+ `This may indicate a malicious payload or you may need to increase maxDepth.`);
75
+ }
76
+ // If not an object, return as-is
77
+ if (typeof value !== "object" || value == null) {
78
+ return value;
79
+ }
80
+ // Handle arrays - recurse into elements
81
+ if (Array.isArray(value)) {
82
+ return Promise.all(value.map((v, i) => reviver.call({ ...this, path: [...path, `${i}`], depth: depth + 1 }, v)));
83
+ }
84
+ // It's an object - check for escape marker FIRST
85
+ const record = value;
86
+ if (isEscapedObject(record)) {
87
+ // This is an escaped user object - unwrap and return as-is (no LC processing)
88
+ return unescapeValue(record);
89
+ }
90
+ // Check for LC secret object
91
+ if ("lc" in record &&
92
+ "type" in record &&
93
+ "id" in record &&
94
+ record.lc === 1 &&
95
+ record.type === "secret") {
96
+ const serialized = record;
30
97
  const [key] = serialized.id;
31
98
  if (key in secretsMap) {
32
99
  return secretsMap[key];
33
100
  }
34
- else {
101
+ else if (secretsFromEnv) {
35
102
  const secretValueInEnv = getEnvironmentVariable(key);
36
103
  if (secretValueInEnv) {
37
104
  return secretValueInEnv;
38
105
  }
39
- else {
40
- throw new Error(`Missing key "${key}" for ${pathStr} in load(secretsMap={})`);
41
- }
42
106
  }
107
+ throw new Error(`Missing secret "${key}" at ${pathStr}`);
43
108
  }
44
- else if (typeof value === "object" &&
45
- value !== null &&
46
- !Array.isArray(value) &&
47
- "lc" in value &&
48
- "type" in value &&
49
- "id" in value &&
50
- value.lc === 1 &&
51
- value.type === "not_implemented") {
52
- const serialized = value;
109
+ // Check for LC not_implemented object
110
+ if ("lc" in record &&
111
+ "type" in record &&
112
+ "id" in record &&
113
+ record.lc === 1 &&
114
+ record.type === "not_implemented") {
115
+ const serialized = record;
53
116
  const str = JSON.stringify(serialized);
54
117
  throw new Error(`Trying to load an object that doesn't implement serialization: ${pathStr} -> ${str}`);
55
118
  }
56
- else if (typeof value === "object" &&
57
- value !== null &&
58
- !Array.isArray(value) &&
59
- "lc" in value &&
60
- "type" in value &&
61
- "id" in value &&
62
- "kwargs" in value &&
63
- value.lc === 1) {
64
- const serialized = value;
119
+ // Check for LC constructor object
120
+ if ("lc" in record &&
121
+ "type" in record &&
122
+ "id" in record &&
123
+ "kwargs" in record &&
124
+ record.lc === 1 &&
125
+ record.type === "constructor") {
126
+ const serialized = record;
65
127
  const str = JSON.stringify(serialized);
66
128
  const [name, ...namespaceReverse] = serialized.id.slice().reverse();
67
129
  const namespace = namespaceReverse.reverse();
@@ -128,35 +190,60 @@ async function reviver(value) {
128
190
  throw new Error(`Invalid identifer: ${pathStr} -> ${str}`);
129
191
  }
130
192
  // Recurse on the arguments, which may be serialized objects themselves
131
- const kwargs = await reviver.call({ ...this, path: [...path, "kwargs"] }, serialized.kwargs);
193
+ const kwargs = await reviver.call({ ...this, path: [...path, "kwargs"], depth: depth + 1 }, serialized.kwargs);
132
194
  // Construct the object
133
- if (serialized.type === "constructor") {
134
- // eslint-disable-next-line new-cap, @typescript-eslint/no-explicit-any
135
- const instance = new builder(mapKeys(kwargs, keyFromJson, combineAliasesAndInvert(builder)));
136
- // Minification in severless/edge runtimes will mange the
137
- // name of classes presented in traces. As the names in import map
138
- // are present as-is even with minification, use these names instead
139
- Object.defineProperty(instance.constructor, "name", { value: name });
140
- return instance;
141
- }
142
- else {
143
- throw new Error(`Invalid type: ${pathStr} -> ${str}`);
144
- }
195
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
196
+ const instance = new builder(mapKeys(kwargs, keyFromJson, combineAliasesAndInvert(builder)));
197
+ // Minification in severless/edge runtimes will mange the
198
+ // name of classes presented in traces. As the names in import map
199
+ // are present as-is even with minification, use these names instead
200
+ Object.defineProperty(instance.constructor, "name", { value: name });
201
+ return instance;
145
202
  }
146
- else if (typeof value === "object" && value !== null) {
147
- if (Array.isArray(value)) {
148
- return Promise.all(value.map((v, i) => reviver.call({ ...this, path: [...path, `${i}`] }, v)));
149
- }
150
- else {
151
- return Object.fromEntries(await Promise.all(Object.entries(value).map(async ([key, value]) => [
152
- key,
153
- await reviver.call({ ...this, path: [...path, key] }, value),
154
- ])));
155
- }
203
+ // Regular object - recurse into values
204
+ const result = {};
205
+ for (const [key, val] of Object.entries(record)) {
206
+ result[key] = await reviver.call({ ...this, path: [...path, key], depth: depth + 1 }, val);
156
207
  }
157
- return value;
208
+ return result;
158
209
  }
159
- export async function load(text, mappings) {
210
+ /**
211
+ * Load a LangChain object from a JSON string.
212
+ *
213
+ * @param text - The JSON string to parse and load.
214
+ * @param options - Options for loading.
215
+ * @returns The loaded LangChain object.
216
+ *
217
+ * @example
218
+ * ```typescript
219
+ * import { load } from "@langchain/core/load";
220
+ * import { AIMessage } from "@langchain/core/messages";
221
+ *
222
+ * // Basic usage - secrets must be provided explicitly
223
+ * const msg = await load<AIMessage>(jsonString);
224
+ *
225
+ * // With secrets from a map
226
+ * const msg = await load<AIMessage>(jsonString, {
227
+ * secretsMap: { OPENAI_API_KEY: "sk-..." }
228
+ * });
229
+ *
230
+ * // Allow loading secrets from environment (use with caution)
231
+ * const msg = await load<AIMessage>(jsonString, {
232
+ * secretsFromEnv: true
233
+ * });
234
+ * ```
235
+ */
236
+ export async function load(text, options) {
160
237
  const json = JSON.parse(text);
161
- return reviver.call({ ...mappings }, json);
238
+ const context = {
239
+ optionalImportsMap: options?.optionalImportsMap ?? {},
240
+ optionalImportEntrypoints: options?.optionalImportEntrypoints ?? [],
241
+ secretsMap: options?.secretsMap ?? {},
242
+ secretsFromEnv: options?.secretsFromEnv ?? false,
243
+ importMap: options?.importMap ?? {},
244
+ path: ["$"],
245
+ depth: 0,
246
+ maxDepth: options?.maxDepth ?? DEFAULT_MAX_DEPTH,
247
+ };
248
+ return reviver.call(context, json);
162
249
  }
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.Serializable = void 0;
4
4
  exports.get_lc_unique_name = get_lc_unique_name;
5
5
  const map_keys_js_1 = require("./map_keys.cjs");
6
+ const validation_js_1 = require("./validation.cjs");
6
7
  function shallowCopy(obj) {
7
8
  return Array.isArray(obj) ? [...obj] : { ...obj };
8
9
  }
@@ -174,11 +175,21 @@ class Serializable {
174
175
  write[last] = write[last] || read[last];
175
176
  }
176
177
  });
178
+ const escapedKwargs = {};
179
+ for (const [key, value] of Object.entries(kwargs)) {
180
+ escapedKwargs[key] = (0, validation_js_1.escapeIfNeeded)(value);
181
+ }
182
+ // Now add secret markers - these are added AFTER escaping so they won't be escaped
183
+ const kwargsWithSecrets = Object.keys(secrets).length
184
+ ? replaceSecrets(escapedKwargs, secrets)
185
+ : escapedKwargs;
186
+ // Finally transform keys to JSON format
187
+ const processedKwargs = (0, map_keys_js_1.mapKeys)(kwargsWithSecrets, map_keys_js_1.keyToJson, aliases);
177
188
  return {
178
189
  lc: 1,
179
190
  type: "constructor",
180
191
  id: this.lc_id,
181
- kwargs: (0, map_keys_js_1.mapKeys)(Object.keys(secrets).length ? replaceSecrets(kwargs, secrets) : kwargs, map_keys_js_1.keyToJson, aliases),
192
+ kwargs: processedKwargs,
182
193
  };
183
194
  }
184
195
  toJSONNotImplemented() {
@@ -19,6 +19,20 @@ export type Serialized = SerializedConstructor | SerializedSecret | SerializedNo
19
19
  * Should not be subclassed, subclass lc_name above instead.
20
20
  */
21
21
  export declare function get_lc_unique_name(serializableClass: typeof Serializable): string;
22
+ /**
23
+ * Interface for objects that can be serialized.
24
+ * This is a duck-typed interface to avoid circular imports.
25
+ */
26
+ export interface SerializableLike {
27
+ lc_serializable: boolean;
28
+ lc_secrets?: Record<string, string>;
29
+ toJSON(): {
30
+ lc: number;
31
+ type: string;
32
+ id: string[];
33
+ kwargs?: Record<string, unknown>;
34
+ };
35
+ }
22
36
  export interface SerializableInterface {
23
37
  get lc_id(): string[];
24
38
  }
@@ -1,4 +1,5 @@
1
1
  import { keyToJson, mapKeys } from "./map_keys.js";
2
+ import { escapeIfNeeded } from "./validation.js";
2
3
  function shallowCopy(obj) {
3
4
  return Array.isArray(obj) ? [...obj] : { ...obj };
4
5
  }
@@ -170,11 +171,21 @@ export class Serializable {
170
171
  write[last] = write[last] || read[last];
171
172
  }
172
173
  });
174
+ const escapedKwargs = {};
175
+ for (const [key, value] of Object.entries(kwargs)) {
176
+ escapedKwargs[key] = escapeIfNeeded(value);
177
+ }
178
+ // Now add secret markers - these are added AFTER escaping so they won't be escaped
179
+ const kwargsWithSecrets = Object.keys(secrets).length
180
+ ? replaceSecrets(escapedKwargs, secrets)
181
+ : escapedKwargs;
182
+ // Finally transform keys to JSON format
183
+ const processedKwargs = mapKeys(kwargsWithSecrets, keyToJson, aliases);
173
184
  return {
174
185
  lc: 1,
175
186
  type: "constructor",
176
187
  id: this.lc_id,
177
- kwargs: mapKeys(Object.keys(secrets).length ? replaceSecrets(kwargs, secrets) : kwargs, keyToJson, aliases),
188
+ kwargs: processedKwargs,
178
189
  };
179
190
  }
180
191
  toJSONNotImplemented() {
@@ -0,0 +1,222 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.LC_ESCAPED_KEY = void 0;
4
+ exports.needsEscaping = needsEscaping;
5
+ exports.escapeObject = escapeObject;
6
+ exports.isEscapedObject = isEscapedObject;
7
+ exports.serializeValue = serializeValue;
8
+ exports.serializeLcObject = serializeLcObject;
9
+ exports.escapeIfNeeded = escapeIfNeeded;
10
+ exports.unescapeValue = unescapeValue;
11
+ /**
12
+ * Sentinel key used to mark escaped user objects during serialization.
13
+ *
14
+ * When a plain object contains 'lc' key (which could be confused with LC objects),
15
+ * we wrap it as `{"__lc_escaped__": {...original...}}`.
16
+ */
17
+ exports.LC_ESCAPED_KEY = "__lc_escaped__";
18
+ /**
19
+ * Check if an object needs escaping to prevent confusion with LC objects.
20
+ *
21
+ * An object needs escaping if:
22
+ * 1. It has an `'lc'` key (could be confused with LC serialization format)
23
+ * 2. It has only the escape key (would be mistaken for an escaped object)
24
+ */
25
+ function needsEscaping(obj) {
26
+ return ("lc" in obj || (Object.keys(obj).length === 1 && exports.LC_ESCAPED_KEY in obj));
27
+ }
28
+ /**
29
+ * Wrap an object in the escape marker.
30
+ *
31
+ * @example
32
+ * ```typescript
33
+ * {"key": "value"} // becomes {"__lc_escaped__": {"key": "value"}}
34
+ * ```
35
+ */
36
+ function escapeObject(obj) {
37
+ return { [exports.LC_ESCAPED_KEY]: obj };
38
+ }
39
+ /**
40
+ * Check if an object is an escaped user object.
41
+ *
42
+ * @example
43
+ * ```typescript
44
+ * {"__lc_escaped__": {...}} // is an escaped object
45
+ * ```
46
+ */
47
+ function isEscapedObject(obj) {
48
+ return Object.keys(obj).length === 1 && exports.LC_ESCAPED_KEY in obj;
49
+ }
50
+ /**
51
+ * Check if an object looks like a Serializable instance (duck typing).
52
+ */
53
+ function isSerializableLike(obj) {
54
+ return (obj !== null &&
55
+ typeof obj === "object" &&
56
+ "lc_serializable" in obj &&
57
+ typeof obj.toJSON === "function");
58
+ }
59
+ /**
60
+ * Create a "not_implemented" serialization result for objects that cannot be serialized.
61
+ */
62
+ function createNotImplemented(obj) {
63
+ let id;
64
+ if (obj !== null && typeof obj === "object") {
65
+ if ("lc_id" in obj && Array.isArray(obj.lc_id)) {
66
+ id = obj.lc_id;
67
+ }
68
+ else {
69
+ id = [obj.constructor?.name ?? "Object"];
70
+ }
71
+ }
72
+ else {
73
+ id = [typeof obj];
74
+ }
75
+ return {
76
+ lc: 1,
77
+ type: "not_implemented",
78
+ id,
79
+ };
80
+ }
81
+ /**
82
+ * Serialize a value with escaping of user objects.
83
+ *
84
+ * Called recursively on kwarg values to escape any plain objects that could be
85
+ * confused with LC objects.
86
+ *
87
+ * @param obj - The value to serialize.
88
+ * @returns The serialized value with user objects escaped as needed.
89
+ */
90
+ function serializeValue(obj) {
91
+ if (isSerializableLike(obj)) {
92
+ // This is an LC object - serialize it properly (not escaped)
93
+ return serializeLcObject(obj);
94
+ }
95
+ if (obj !== null && typeof obj === "object" && !Array.isArray(obj)) {
96
+ const record = obj;
97
+ // Check if object needs escaping BEFORE recursing into values.
98
+ // If it needs escaping, wrap it as-is - the contents are user data that
99
+ // will be returned as-is during deserialization (no instantiation).
100
+ // This prevents re-escaping of already-escaped nested content.
101
+ if (needsEscaping(record)) {
102
+ return escapeObject(record);
103
+ }
104
+ // Safe object (no 'lc' key) - recurse into values
105
+ const result = {};
106
+ for (const [key, value] of Object.entries(record)) {
107
+ result[key] = serializeValue(value);
108
+ }
109
+ return result;
110
+ }
111
+ if (Array.isArray(obj)) {
112
+ return obj.map((item) => serializeValue(item));
113
+ }
114
+ if (typeof obj === "string" ||
115
+ typeof obj === "number" ||
116
+ typeof obj === "boolean" ||
117
+ obj === null) {
118
+ return obj;
119
+ }
120
+ // Non-JSON-serializable object (Date, custom objects, etc.)
121
+ return createNotImplemented(obj);
122
+ }
123
+ /**
124
+ * Serialize a `Serializable` object with escaping of user data in kwargs.
125
+ *
126
+ * @param obj - The `Serializable` object to serialize.
127
+ * @returns The serialized object with user data in kwargs escaped as needed.
128
+ *
129
+ * @remarks
130
+ * Kwargs values are processed with `serializeValue` to escape user data (like
131
+ * metadata) that contains `'lc'` keys. Secret fields (from `lc_secrets`) are
132
+ * skipped because `toJSON()` replaces their values with secret markers.
133
+ */
134
+ function serializeLcObject(obj) {
135
+ // Secret fields are handled by toJSON() - it replaces values with secret markers
136
+ const secretFields = new Set(Object.keys(obj.lc_secrets ?? {}));
137
+ const serialized = { ...obj.toJSON() };
138
+ // Process kwargs to escape user data that could be confused with LC objects
139
+ // Skip secret fields - toJSON() already converted them to secret markers
140
+ if (serialized.type === "constructor" && serialized.kwargs) {
141
+ const newKwargs = {};
142
+ for (const [key, value] of Object.entries(serialized.kwargs)) {
143
+ if (secretFields.has(key)) {
144
+ newKwargs[key] = value;
145
+ }
146
+ else {
147
+ newKwargs[key] = serializeValue(value);
148
+ }
149
+ }
150
+ serialized.kwargs = newKwargs;
151
+ }
152
+ return serialized;
153
+ }
154
+ /**
155
+ * Escape a value if it needs escaping (contains `lc` key).
156
+ *
157
+ * This is a simpler version of `serializeValue` that doesn't handle Serializable
158
+ * objects - it's meant to be called on kwargs values that have already been
159
+ * processed by `toJSON()`.
160
+ *
161
+ * @param value - The value to potentially escape.
162
+ * @returns The value with any `lc`-containing objects wrapped in escape markers.
163
+ */
164
+ function escapeIfNeeded(value) {
165
+ if (value !== null && typeof value === "object" && !Array.isArray(value)) {
166
+ // Preserve Serializable objects - they have their own toJSON() that will be
167
+ // called by JSON.stringify. We don't want to convert them to plain objects.
168
+ if (isSerializableLike(value)) {
169
+ return value;
170
+ }
171
+ const record = value;
172
+ // Check if object needs escaping BEFORE recursing into values.
173
+ // If it needs escaping, wrap it as-is - the contents are user data that
174
+ // will be returned as-is during deserialization (no instantiation).
175
+ if (needsEscaping(record)) {
176
+ return escapeObject(record);
177
+ }
178
+ // Safe object (no 'lc' key) - recurse into values
179
+ const result = {};
180
+ for (const [key, val] of Object.entries(record)) {
181
+ result[key] = escapeIfNeeded(val);
182
+ }
183
+ return result;
184
+ }
185
+ if (Array.isArray(value)) {
186
+ return value.map((item) => escapeIfNeeded(item));
187
+ }
188
+ return value;
189
+ }
190
+ /**
191
+ * Unescape a value, processing escape markers in object values and arrays.
192
+ *
193
+ * When an escaped object is encountered (`{"__lc_escaped__": ...}`), it's
194
+ * unwrapped and the contents are returned AS-IS (no further processing).
195
+ * The contents represent user data that should not be modified.
196
+ *
197
+ * For regular objects and arrays, we recurse to find any nested escape markers.
198
+ *
199
+ * @param obj - The value to unescape.
200
+ * @returns The unescaped value.
201
+ */
202
+ function unescapeValue(obj) {
203
+ if (obj !== null && typeof obj === "object" && !Array.isArray(obj)) {
204
+ const record = obj;
205
+ if (isEscapedObject(record)) {
206
+ // Unwrap and return the user data as-is (no further unescaping).
207
+ // The contents are user data that may contain more escape keys,
208
+ // but those are part of the user's actual data.
209
+ return record[exports.LC_ESCAPED_KEY];
210
+ }
211
+ // Regular object - recurse into values to find nested escape markers
212
+ const result = {};
213
+ for (const [key, value] of Object.entries(record)) {
214
+ result[key] = unescapeValue(value);
215
+ }
216
+ return result;
217
+ }
218
+ if (Array.isArray(obj)) {
219
+ return obj.map((item) => unescapeValue(item));
220
+ }
221
+ return obj;
222
+ }
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Sentinel key used to mark escaped user objects during serialization.
3
+ *
4
+ * When a plain object contains 'lc' key (which could be confused with LC objects),
5
+ * we wrap it as `{"__lc_escaped__": {...original...}}`.
6
+ */
7
+ export declare const LC_ESCAPED_KEY = "__lc_escaped__";
8
+ /**
9
+ * Check if an object needs escaping to prevent confusion with LC objects.
10
+ *
11
+ * An object needs escaping if:
12
+ * 1. It has an `'lc'` key (could be confused with LC serialization format)
13
+ * 2. It has only the escape key (would be mistaken for an escaped object)
14
+ */
15
+ export declare function needsEscaping(obj: Record<string, unknown>): boolean;
16
+ /**
17
+ * Wrap an object in the escape marker.
18
+ *
19
+ * @example
20
+ * ```typescript
21
+ * {"key": "value"} // becomes {"__lc_escaped__": {"key": "value"}}
22
+ * ```
23
+ */
24
+ export declare function escapeObject(obj: Record<string, unknown>): Record<string, unknown>;
25
+ /**
26
+ * Check if an object is an escaped user object.
27
+ *
28
+ * @example
29
+ * ```typescript
30
+ * {"__lc_escaped__": {...}} // is an escaped object
31
+ * ```
32
+ */
33
+ export declare function isEscapedObject(obj: Record<string, unknown>): boolean;
34
+ /**
35
+ * Interface for objects that can be serialized.
36
+ * This is a duck-typed interface to avoid circular imports.
37
+ */
38
+ interface SerializableLike {
39
+ lc_serializable: boolean;
40
+ lc_secrets?: Record<string, string>;
41
+ toJSON(): {
42
+ lc: number;
43
+ type: string;
44
+ id: string[];
45
+ kwargs?: Record<string, unknown>;
46
+ };
47
+ }
48
+ /**
49
+ * Serialize a value with escaping of user objects.
50
+ *
51
+ * Called recursively on kwarg values to escape any plain objects that could be
52
+ * confused with LC objects.
53
+ *
54
+ * @param obj - The value to serialize.
55
+ * @returns The serialized value with user objects escaped as needed.
56
+ */
57
+ export declare function serializeValue(obj: unknown): unknown;
58
+ /**
59
+ * Serialize a `Serializable` object with escaping of user data in kwargs.
60
+ *
61
+ * @param obj - The `Serializable` object to serialize.
62
+ * @returns The serialized object with user data in kwargs escaped as needed.
63
+ *
64
+ * @remarks
65
+ * Kwargs values are processed with `serializeValue` to escape user data (like
66
+ * metadata) that contains `'lc'` keys. Secret fields (from `lc_secrets`) are
67
+ * skipped because `toJSON()` replaces their values with secret markers.
68
+ */
69
+ export declare function serializeLcObject(obj: SerializableLike): {
70
+ lc: number;
71
+ type: string;
72
+ id: string[];
73
+ kwargs?: Record<string, unknown>;
74
+ };
75
+ /**
76
+ * Escape a value if it needs escaping (contains `lc` key).
77
+ *
78
+ * This is a simpler version of `serializeValue` that doesn't handle Serializable
79
+ * objects - it's meant to be called on kwargs values that have already been
80
+ * processed by `toJSON()`.
81
+ *
82
+ * @param value - The value to potentially escape.
83
+ * @returns The value with any `lc`-containing objects wrapped in escape markers.
84
+ */
85
+ export declare function escapeIfNeeded(value: unknown): unknown;
86
+ /**
87
+ * Unescape a value, processing escape markers in object values and arrays.
88
+ *
89
+ * When an escaped object is encountered (`{"__lc_escaped__": ...}`), it's
90
+ * unwrapped and the contents are returned AS-IS (no further processing).
91
+ * The contents represent user data that should not be modified.
92
+ *
93
+ * For regular objects and arrays, we recurse to find any nested escape markers.
94
+ *
95
+ * @param obj - The value to unescape.
96
+ * @returns The unescaped value.
97
+ */
98
+ export declare function unescapeValue(obj: unknown): unknown;
99
+ export {};
@@ -0,0 +1,212 @@
1
+ /**
2
+ * Sentinel key used to mark escaped user objects during serialization.
3
+ *
4
+ * When a plain object contains 'lc' key (which could be confused with LC objects),
5
+ * we wrap it as `{"__lc_escaped__": {...original...}}`.
6
+ */
7
+ export const LC_ESCAPED_KEY = "__lc_escaped__";
8
+ /**
9
+ * Check if an object needs escaping to prevent confusion with LC objects.
10
+ *
11
+ * An object needs escaping if:
12
+ * 1. It has an `'lc'` key (could be confused with LC serialization format)
13
+ * 2. It has only the escape key (would be mistaken for an escaped object)
14
+ */
15
+ export function needsEscaping(obj) {
16
+ return ("lc" in obj || (Object.keys(obj).length === 1 && LC_ESCAPED_KEY in obj));
17
+ }
18
+ /**
19
+ * Wrap an object in the escape marker.
20
+ *
21
+ * @example
22
+ * ```typescript
23
+ * {"key": "value"} // becomes {"__lc_escaped__": {"key": "value"}}
24
+ * ```
25
+ */
26
+ export function escapeObject(obj) {
27
+ return { [LC_ESCAPED_KEY]: obj };
28
+ }
29
+ /**
30
+ * Check if an object is an escaped user object.
31
+ *
32
+ * @example
33
+ * ```typescript
34
+ * {"__lc_escaped__": {...}} // is an escaped object
35
+ * ```
36
+ */
37
+ export function isEscapedObject(obj) {
38
+ return Object.keys(obj).length === 1 && LC_ESCAPED_KEY in obj;
39
+ }
40
+ /**
41
+ * Check if an object looks like a Serializable instance (duck typing).
42
+ */
43
+ function isSerializableLike(obj) {
44
+ return (obj !== null &&
45
+ typeof obj === "object" &&
46
+ "lc_serializable" in obj &&
47
+ typeof obj.toJSON === "function");
48
+ }
49
+ /**
50
+ * Create a "not_implemented" serialization result for objects that cannot be serialized.
51
+ */
52
+ function createNotImplemented(obj) {
53
+ let id;
54
+ if (obj !== null && typeof obj === "object") {
55
+ if ("lc_id" in obj && Array.isArray(obj.lc_id)) {
56
+ id = obj.lc_id;
57
+ }
58
+ else {
59
+ id = [obj.constructor?.name ?? "Object"];
60
+ }
61
+ }
62
+ else {
63
+ id = [typeof obj];
64
+ }
65
+ return {
66
+ lc: 1,
67
+ type: "not_implemented",
68
+ id,
69
+ };
70
+ }
71
+ /**
72
+ * Serialize a value with escaping of user objects.
73
+ *
74
+ * Called recursively on kwarg values to escape any plain objects that could be
75
+ * confused with LC objects.
76
+ *
77
+ * @param obj - The value to serialize.
78
+ * @returns The serialized value with user objects escaped as needed.
79
+ */
80
+ export function serializeValue(obj) {
81
+ if (isSerializableLike(obj)) {
82
+ // This is an LC object - serialize it properly (not escaped)
83
+ return serializeLcObject(obj);
84
+ }
85
+ if (obj !== null && typeof obj === "object" && !Array.isArray(obj)) {
86
+ const record = obj;
87
+ // Check if object needs escaping BEFORE recursing into values.
88
+ // If it needs escaping, wrap it as-is - the contents are user data that
89
+ // will be returned as-is during deserialization (no instantiation).
90
+ // This prevents re-escaping of already-escaped nested content.
91
+ if (needsEscaping(record)) {
92
+ return escapeObject(record);
93
+ }
94
+ // Safe object (no 'lc' key) - recurse into values
95
+ const result = {};
96
+ for (const [key, value] of Object.entries(record)) {
97
+ result[key] = serializeValue(value);
98
+ }
99
+ return result;
100
+ }
101
+ if (Array.isArray(obj)) {
102
+ return obj.map((item) => serializeValue(item));
103
+ }
104
+ if (typeof obj === "string" ||
105
+ typeof obj === "number" ||
106
+ typeof obj === "boolean" ||
107
+ obj === null) {
108
+ return obj;
109
+ }
110
+ // Non-JSON-serializable object (Date, custom objects, etc.)
111
+ return createNotImplemented(obj);
112
+ }
113
+ /**
114
+ * Serialize a `Serializable` object with escaping of user data in kwargs.
115
+ *
116
+ * @param obj - The `Serializable` object to serialize.
117
+ * @returns The serialized object with user data in kwargs escaped as needed.
118
+ *
119
+ * @remarks
120
+ * Kwargs values are processed with `serializeValue` to escape user data (like
121
+ * metadata) that contains `'lc'` keys. Secret fields (from `lc_secrets`) are
122
+ * skipped because `toJSON()` replaces their values with secret markers.
123
+ */
124
+ export function serializeLcObject(obj) {
125
+ // Secret fields are handled by toJSON() - it replaces values with secret markers
126
+ const secretFields = new Set(Object.keys(obj.lc_secrets ?? {}));
127
+ const serialized = { ...obj.toJSON() };
128
+ // Process kwargs to escape user data that could be confused with LC objects
129
+ // Skip secret fields - toJSON() already converted them to secret markers
130
+ if (serialized.type === "constructor" && serialized.kwargs) {
131
+ const newKwargs = {};
132
+ for (const [key, value] of Object.entries(serialized.kwargs)) {
133
+ if (secretFields.has(key)) {
134
+ newKwargs[key] = value;
135
+ }
136
+ else {
137
+ newKwargs[key] = serializeValue(value);
138
+ }
139
+ }
140
+ serialized.kwargs = newKwargs;
141
+ }
142
+ return serialized;
143
+ }
144
+ /**
145
+ * Escape a value if it needs escaping (contains `lc` key).
146
+ *
147
+ * This is a simpler version of `serializeValue` that doesn't handle Serializable
148
+ * objects - it's meant to be called on kwargs values that have already been
149
+ * processed by `toJSON()`.
150
+ *
151
+ * @param value - The value to potentially escape.
152
+ * @returns The value with any `lc`-containing objects wrapped in escape markers.
153
+ */
154
+ export function escapeIfNeeded(value) {
155
+ if (value !== null && typeof value === "object" && !Array.isArray(value)) {
156
+ // Preserve Serializable objects - they have their own toJSON() that will be
157
+ // called by JSON.stringify. We don't want to convert them to plain objects.
158
+ if (isSerializableLike(value)) {
159
+ return value;
160
+ }
161
+ const record = value;
162
+ // Check if object needs escaping BEFORE recursing into values.
163
+ // If it needs escaping, wrap it as-is - the contents are user data that
164
+ // will be returned as-is during deserialization (no instantiation).
165
+ if (needsEscaping(record)) {
166
+ return escapeObject(record);
167
+ }
168
+ // Safe object (no 'lc' key) - recurse into values
169
+ const result = {};
170
+ for (const [key, val] of Object.entries(record)) {
171
+ result[key] = escapeIfNeeded(val);
172
+ }
173
+ return result;
174
+ }
175
+ if (Array.isArray(value)) {
176
+ return value.map((item) => escapeIfNeeded(item));
177
+ }
178
+ return value;
179
+ }
180
+ /**
181
+ * Unescape a value, processing escape markers in object values and arrays.
182
+ *
183
+ * When an escaped object is encountered (`{"__lc_escaped__": ...}`), it's
184
+ * unwrapped and the contents are returned AS-IS (no further processing).
185
+ * The contents represent user data that should not be modified.
186
+ *
187
+ * For regular objects and arrays, we recurse to find any nested escape markers.
188
+ *
189
+ * @param obj - The value to unescape.
190
+ * @returns The unescaped value.
191
+ */
192
+ export function unescapeValue(obj) {
193
+ if (obj !== null && typeof obj === "object" && !Array.isArray(obj)) {
194
+ const record = obj;
195
+ if (isEscapedObject(record)) {
196
+ // Unwrap and return the user data as-is (no further unescaping).
197
+ // The contents are user data that may contain more escape keys,
198
+ // but those are part of the user's actual data.
199
+ return record[LC_ESCAPED_KEY];
200
+ }
201
+ // Regular object - recurse into values to find nested escape markers
202
+ const result = {};
203
+ for (const [key, value] of Object.entries(record)) {
204
+ result[key] = unescapeValue(value);
205
+ }
206
+ return result;
207
+ }
208
+ if (Array.isArray(obj)) {
209
+ return obj.map((item) => unescapeValue(item));
210
+ }
211
+ return obj;
212
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@langchain/core",
3
- "version": "0.3.79",
3
+ "version": "0.3.80",
4
4
  "description": "Core LangChain.js abstractions and schemas",
5
5
  "type": "module",
6
6
  "engines": {