langsmith 0.5.19 → 0.6.0

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 (161) hide show
  1. package/dist/client.cjs +460 -22
  2. package/dist/client.d.ts +165 -1
  3. package/dist/client.js +464 -26
  4. package/dist/evaluation/_runner.cjs +4 -7
  5. package/dist/evaluation/_runner.js +2 -5
  6. package/dist/evaluation/evaluate_comparative.cjs +10 -10
  7. package/dist/evaluation/evaluate_comparative.js +1 -1
  8. package/dist/evaluation/evaluator.cjs +2 -2
  9. package/dist/evaluation/evaluator.js +1 -1
  10. package/dist/experimental/anthropic/context.cjs +419 -49
  11. package/dist/experimental/anthropic/context.js +420 -50
  12. package/dist/experimental/anthropic/index.cjs +78 -10
  13. package/dist/experimental/anthropic/index.js +80 -12
  14. package/dist/experimental/anthropic/messages.cjs +53 -0
  15. package/dist/experimental/anthropic/messages.d.ts +6 -0
  16. package/dist/experimental/anthropic/messages.js +52 -0
  17. package/dist/experimental/anthropic/transcripts.cjs +144 -0
  18. package/dist/experimental/anthropic/transcripts.d.ts +22 -0
  19. package/dist/experimental/anthropic/transcripts.js +141 -0
  20. package/dist/experimental/anthropic/types.d.ts +1 -0
  21. package/dist/experimental/anthropic/usage.cjs +19 -20
  22. package/dist/experimental/anthropic/usage.d.ts +2 -1
  23. package/dist/experimental/anthropic/usage.js +18 -20
  24. package/dist/experimental/opencode/index.cjs +36 -0
  25. package/dist/experimental/opencode/index.d.ts +3 -0
  26. package/dist/experimental/opencode/index.js +32 -0
  27. package/dist/experimental/opencode/tracer.cjs +389 -0
  28. package/dist/experimental/opencode/tracer.d.ts +30 -0
  29. package/dist/experimental/opencode/tracer.js +385 -0
  30. package/dist/experimental/otel/setup.cjs +1 -1
  31. package/dist/experimental/otel/setup.js +1 -1
  32. package/dist/experimental/sandbox/client.cjs +308 -409
  33. package/dist/experimental/sandbox/client.d.ts +136 -157
  34. package/dist/experimental/sandbox/client.js +310 -411
  35. package/dist/experimental/sandbox/errors.cjs +1 -2
  36. package/dist/experimental/sandbox/errors.d.ts +1 -2
  37. package/dist/experimental/sandbox/errors.js +1 -2
  38. package/dist/experimental/sandbox/helpers.cjs +8 -98
  39. package/dist/experimental/sandbox/helpers.d.ts +0 -29
  40. package/dist/experimental/sandbox/helpers.js +9 -95
  41. package/dist/experimental/sandbox/index.cjs +6 -1
  42. package/dist/experimental/sandbox/index.d.ts +7 -2
  43. package/dist/experimental/sandbox/index.js +6 -1
  44. package/dist/experimental/sandbox/sandbox.cjs +71 -13
  45. package/dist/experimental/sandbox/sandbox.d.ts +39 -9
  46. package/dist/experimental/sandbox/sandbox.js +71 -13
  47. package/dist/experimental/sandbox/types.d.ts +147 -137
  48. package/dist/experimental/vercel/index.cjs +1 -1
  49. package/dist/experimental/vercel/index.js +1 -1
  50. package/dist/experimental/vercel/middleware.cjs +2 -1
  51. package/dist/experimental/vercel/middleware.js +2 -1
  52. package/dist/index.cjs +1 -1
  53. package/dist/index.d.ts +1 -1
  54. package/dist/index.js +1 -1
  55. package/dist/run_trees.cjs +8 -7
  56. package/dist/run_trees.d.ts +7 -0
  57. package/dist/run_trees.js +7 -6
  58. package/dist/schemas.d.ts +58 -0
  59. package/dist/singletons/otel.cjs +3 -2
  60. package/dist/singletons/otel.js +4 -3
  61. package/dist/singletons/traceable.cjs +1 -3
  62. package/dist/singletons/traceable.js +1 -3
  63. package/dist/traceable.cjs +2 -5
  64. package/dist/traceable.js +2 -5
  65. package/dist/utils/_git.cjs +2 -2
  66. package/dist/utils/_git.js +2 -2
  67. package/dist/utils/_uuid.cjs +2 -2
  68. package/dist/utils/_uuid.js +1 -1
  69. package/dist/utils/env.cjs +35 -2
  70. package/dist/utils/env.d.ts +9 -0
  71. package/dist/utils/env.js +34 -2
  72. package/dist/utils/error.cjs +9 -2
  73. package/dist/utils/error.d.ts +1 -0
  74. package/dist/utils/error.js +8 -2
  75. package/dist/utils/fast-safe-stringify/index.cjs +203 -0
  76. package/dist/utils/fast-safe-stringify/index.d.ts +46 -0
  77. package/dist/utils/fast-safe-stringify/index.js +202 -0
  78. package/dist/utils/jestlike/index.cjs +5 -5
  79. package/dist/utils/jestlike/index.js +1 -1
  80. package/dist/utils/jestlike/reporter.cjs +1 -1
  81. package/dist/utils/jestlike/reporter.js +1 -1
  82. package/dist/utils/jestlike/vendor/chain.cjs +2 -3
  83. package/dist/utils/jestlike/vendor/chain.js +2 -3
  84. package/dist/utils/jestlike/vendor/evaluatedBy.cjs +3 -3
  85. package/dist/utils/jestlike/vendor/evaluatedBy.js +1 -1
  86. package/dist/utils/prompts.cjs +7 -2
  87. package/dist/utils/prompts.d.ts +6 -1
  88. package/dist/utils/prompts.js +6 -1
  89. package/dist/utils/serialize_worker.cjs +388 -0
  90. package/dist/utils/serialize_worker.d.ts +66 -0
  91. package/dist/utils/serialize_worker.js +382 -0
  92. package/dist/utils/uuid/src/index.cjs +24 -0
  93. package/dist/utils/uuid/src/index.d.ts +10 -0
  94. package/dist/utils/uuid/src/index.js +9 -0
  95. package/dist/utils/uuid/src/max.cjs +3 -0
  96. package/dist/utils/uuid/src/max.d.ts +2 -0
  97. package/dist/utils/uuid/src/max.js +1 -0
  98. package/dist/utils/uuid/src/nil.cjs +3 -0
  99. package/dist/utils/uuid/src/nil.d.ts +2 -0
  100. package/dist/utils/uuid/src/nil.js +1 -0
  101. package/dist/utils/uuid/src/parse.cjs +23 -0
  102. package/dist/utils/uuid/src/parse.d.ts +3 -0
  103. package/dist/utils/uuid/src/parse.js +18 -0
  104. package/dist/utils/uuid/src/regex.cjs +3 -0
  105. package/dist/utils/uuid/src/regex.d.ts +2 -0
  106. package/dist/utils/uuid/src/regex.js +1 -0
  107. package/dist/utils/uuid/src/rng.cjs +10 -0
  108. package/dist/utils/uuid/src/rng.d.ts +1 -0
  109. package/dist/utils/uuid/src/rng.js +7 -0
  110. package/dist/utils/uuid/src/sha1.cjs +75 -0
  111. package/dist/utils/uuid/src/sha1.d.ts +2 -0
  112. package/dist/utils/uuid/src/sha1.js +73 -0
  113. package/dist/utils/uuid/src/stringify.cjs +55 -0
  114. package/dist/utils/uuid/src/stringify.d.ts +3 -0
  115. package/dist/utils/uuid/src/stringify.js +49 -0
  116. package/dist/utils/uuid/src/types.cjs +2 -0
  117. package/dist/utils/uuid/src/types.d.ts +22 -0
  118. package/dist/utils/uuid/src/types.js +1 -0
  119. package/dist/utils/uuid/src/v35.cjs +52 -0
  120. package/dist/utils/uuid/src/v35.d.ts +7 -0
  121. package/dist/utils/uuid/src/v35.js +44 -0
  122. package/dist/utils/uuid/src/v4.cjs +40 -0
  123. package/dist/utils/uuid/src/v4.d.ts +4 -0
  124. package/dist/utils/uuid/src/v4.js +35 -0
  125. package/dist/utils/uuid/src/v5.cjs +50 -0
  126. package/dist/utils/uuid/src/v5.d.ts +9 -0
  127. package/dist/utils/uuid/src/v5.js +9 -0
  128. package/dist/utils/uuid/src/v7.cjs +88 -0
  129. package/dist/utils/uuid/src/v7.d.ts +9 -0
  130. package/dist/utils/uuid/src/v7.js +82 -0
  131. package/dist/utils/uuid/src/validate.cjs +10 -0
  132. package/dist/utils/uuid/src/validate.d.ts +2 -0
  133. package/dist/utils/uuid/src/validate.js +5 -0
  134. package/dist/utils/uuid/src/version.cjs +13 -0
  135. package/dist/utils/uuid/src/version.d.ts +2 -0
  136. package/dist/utils/uuid/src/version.js +8 -0
  137. package/dist/utils/worker_threads.browser.cjs +16 -0
  138. package/dist/utils/worker_threads.browser.d.ts +14 -0
  139. package/dist/utils/worker_threads.browser.js +13 -0
  140. package/dist/utils/worker_threads.cjs +16 -0
  141. package/dist/utils/worker_threads.d.ts +13 -0
  142. package/dist/utils/worker_threads.js +13 -0
  143. package/dist/uuid.cjs +2 -2
  144. package/dist/uuid.js +1 -1
  145. package/dist/vitest/utils/esm.mjs +4 -4
  146. package/dist/wrappers/gemini.cjs +1 -1
  147. package/dist/wrappers/gemini.js +1 -1
  148. package/dist/wrappers/openai.cjs +2 -6
  149. package/dist/wrappers/openai.js +2 -6
  150. package/dist/wrappers/openai_agents.cjs +849 -0
  151. package/dist/wrappers/openai_agents.d.ts +92 -0
  152. package/dist/wrappers/openai_agents.js +845 -0
  153. package/experimental/opencode.cjs +1 -0
  154. package/experimental/opencode.d.cts +1 -0
  155. package/experimental/opencode.d.ts +1 -0
  156. package/experimental/opencode.js +1 -0
  157. package/package.json +47 -25
  158. package/wrappers/openai_agents.cjs +1 -0
  159. package/wrappers/openai_agents.d.cts +1 -0
  160. package/wrappers/openai_agents.d.ts +1 -0
  161. package/wrappers/openai_agents.js +1 -0
@@ -10,6 +10,27 @@ const async_caller_js_1 = require("../../utils/async_caller.cjs");
10
10
  const sandbox_js_1 = require("./sandbox.cjs");
11
11
  const errors_js_1 = require("./errors.cjs");
12
12
  const helpers_js_1 = require("./helpers.cjs");
13
+ /**
14
+ * Sleep that can be interrupted by an AbortSignal.
15
+ * Resolves after `ms` milliseconds or rejects immediately if the signal fires.
16
+ */
17
+ function sleepWithSignal(ms, signal) {
18
+ if (!signal) {
19
+ return new Promise((resolve) => setTimeout(resolve, ms));
20
+ }
21
+ signal.throwIfAborted();
22
+ return new Promise((resolve, reject) => {
23
+ const timer = setTimeout(() => {
24
+ signal.removeEventListener("abort", onAbort);
25
+ resolve();
26
+ }, ms);
27
+ function onAbort() {
28
+ clearTimeout(timer);
29
+ reject(signal.reason);
30
+ }
31
+ signal.addEventListener("abort", onAbort, { once: true });
32
+ });
33
+ }
13
34
  /**
14
35
  * Get the default sandbox API endpoint from environment.
15
36
  *
@@ -29,7 +50,7 @@ function getDefaultApiKey() {
29
50
  /**
30
51
  * Client for interacting with the Sandbox Server API.
31
52
  *
32
- * This client provides a simple interface for managing sandboxes and templates.
53
+ * This client provides a simple interface for managing sandboxes and snapshots.
33
54
  *
34
55
  * @example
35
56
  * ```typescript
@@ -44,8 +65,13 @@ function getDefaultApiKey() {
44
65
  * apiKey: "your-api-key",
45
66
  * });
46
67
  *
47
- * // Create a sandbox and run commands
48
- * const sandbox = await client.createSandbox("python-sandbox");
68
+ * // Build a snapshot, then create a sandbox from it
69
+ * const snapshot = await client.createSnapshot(
70
+ * "python",
71
+ * "python:3.12-slim",
72
+ * 1_073_741_824 // 1 GiB
73
+ * );
74
+ * const sandbox = await client.createSandbox(snapshot.id);
49
75
  * try {
50
76
  * const result = await sandbox.run("python --version");
51
77
  * console.log(result.stdout);
@@ -115,422 +141,60 @@ class SandboxClient {
115
141
  getApiKey() {
116
142
  return this._apiKey;
117
143
  }
118
- // =========================================================================
119
- // Volume Operations
120
- // =========================================================================
121
- /**
122
- * Create a new persistent volume.
123
- *
124
- * Creates a persistent storage volume that can be referenced in templates.
125
- *
126
- * @param name - Volume name.
127
- * @param options - Creation options including size and optional timeout.
128
- * @returns Created Volume.
129
- * @throws SandboxCreationError if volume provisioning fails.
130
- * @throws ResourceTimeoutError if volume doesn't become ready within timeout.
131
- */
132
- async createVolume(name, options) {
133
- const { size, timeout = 60 } = options;
134
- const url = `${this._baseUrl}/volumes`;
135
- const payload = {
136
- name,
137
- size,
138
- wait_for_ready: true,
139
- timeout,
140
- };
141
- const response = await this._fetch(url, {
142
- method: "POST",
143
- headers: { "Content-Type": "application/json" },
144
- body: JSON.stringify(payload),
145
- signal: AbortSignal.timeout((timeout + 30) * 1000),
146
- });
147
- if (!response.ok) {
148
- await (0, helpers_js_1.handleVolumeCreationError)(response);
149
- }
150
- return (await response.json());
151
- }
152
- /**
153
- * Get a volume by name.
154
- *
155
- * @param name - Volume name.
156
- * @returns Volume.
157
- * @throws LangSmithResourceNotFoundError if volume not found.
158
- */
159
- async getVolume(name) {
160
- const url = `${this._baseUrl}/volumes/${encodeURIComponent(name)}`;
161
- const response = await this._fetch(url);
162
- if (!response.ok) {
163
- if (response.status === 404) {
164
- throw new errors_js_1.LangSmithResourceNotFoundError(`Volume '${name}' not found`, "volume");
165
- }
166
- await (0, helpers_js_1.handleClientHttpError)(response);
167
- }
168
- return (await response.json());
169
- }
170
- /**
171
- * List all volumes.
172
- *
173
- * @returns List of Volumes.
174
- */
175
- async listVolumes() {
176
- const url = `${this._baseUrl}/volumes`;
177
- const response = await this._fetch(url);
178
- if (!response.ok) {
179
- if (response.status === 404) {
180
- throw new errors_js_1.LangSmithSandboxAPIError(`API endpoint not found: ${url}. Check that apiEndpoint is correct.`);
181
- }
182
- await (0, helpers_js_1.handleClientHttpError)(response);
183
- }
184
- const data = await response.json();
185
- return (data.volumes ?? []);
186
- }
187
- /**
188
- * Delete a volume.
189
- *
190
- * @param name - Volume name.
191
- * @throws LangSmithResourceNotFoundError if volume not found.
192
- * @throws ResourceInUseError if volume is referenced by templates.
193
- */
194
- async deleteVolume(name) {
195
- const url = `${this._baseUrl}/volumes/${encodeURIComponent(name)}`;
196
- const response = await this._fetch(url, { method: "DELETE" });
197
- if (!response.ok) {
198
- if (response.status === 404) {
199
- throw new errors_js_1.LangSmithResourceNotFoundError(`Volume '${name}' not found`, "volume");
200
- }
201
- if (response.status === 409) {
202
- await (0, helpers_js_1.handleResourceInUseError)(response, "volume");
203
- }
204
- await (0, helpers_js_1.handleClientHttpError)(response);
205
- }
206
- }
207
- /**
208
- * Update a volume's name and/or size.
209
- *
210
- * You can update the display name, size, or both in a single request.
211
- * Only storage size increases are allowed (storage backend limitation).
212
- *
213
- * @param name - Current volume name.
214
- * @param options - Update options.
215
- * @returns Updated Volume.
216
- * @throws LangSmithResourceNotFoundError if volume not found.
217
- * @throws ValidationError if storage decrease attempted.
218
- * @throws LangSmithResourceNameConflictError if newName is already in use.
219
- */
220
- async updateVolume(name, options) {
221
- const { newName, size } = options;
222
- if (newName === undefined && size === undefined) {
223
- // Nothing to update, just return the current volume
224
- return this.getVolume(name);
225
- }
226
- const url = `${this._baseUrl}/volumes/${encodeURIComponent(name)}`;
227
- const payload = {};
228
- if (newName !== undefined) {
229
- payload.name = newName;
230
- }
231
- if (size !== undefined) {
232
- payload.size = size;
233
- }
234
- const response = await this._fetch(url, {
235
- method: "PATCH",
236
- headers: { "Content-Type": "application/json" },
237
- body: JSON.stringify(payload),
238
- });
239
- if (!response.ok) {
240
- if (response.status === 404) {
241
- throw new errors_js_1.LangSmithResourceNotFoundError(`Volume '${name}' not found`, "volume");
242
- }
243
- if (response.status === 409) {
244
- await (0, helpers_js_1.handleConflictError)(response, "volume");
245
- }
246
- await (0, helpers_js_1.handleClientHttpError)(response);
247
- }
248
- return (await response.json());
249
- }
250
- // =========================================================================
251
- // Template Operations
252
- // =========================================================================
253
- /**
254
- * Create a new SandboxTemplate.
255
- *
256
- * Only the container image, resource limits, and volume mounts can be
257
- * configured. All other container details are handled by the server.
258
- *
259
- * @param name - Template name.
260
- * @param options - Creation options including image and resource limits.
261
- * @returns Created SandboxTemplate.
262
- */
263
- async createTemplate(name, options) {
264
- const { image, cpu = "500m", memory = "512Mi", storage, volumeMounts, } = options;
265
- const url = `${this._baseUrl}/templates`;
266
- const payload = {
267
- name,
268
- image,
269
- resources: {
270
- cpu,
271
- memory,
272
- },
273
- };
274
- if (storage) {
275
- payload.resources.storage = storage;
276
- }
277
- if (volumeMounts && volumeMounts.length > 0) {
278
- payload.volume_mounts = volumeMounts.map((vm) => ({
279
- volume_name: vm.volume_name,
280
- mount_path: vm.mount_path,
281
- }));
282
- }
283
- const response = await this._fetch(url, {
284
- method: "POST",
285
- headers: { "Content-Type": "application/json" },
286
- body: JSON.stringify(payload),
287
- });
288
- if (!response.ok) {
289
- await (0, helpers_js_1.handleClientHttpError)(response);
290
- }
291
- return (await response.json());
292
- }
293
- /**
294
- * Get a SandboxTemplate by name.
295
- *
296
- * @param name - Template name.
297
- * @returns SandboxTemplate.
298
- * @throws LangSmithResourceNotFoundError if template not found.
299
- */
300
- async getTemplate(name) {
301
- const url = `${this._baseUrl}/templates/${encodeURIComponent(name)}`;
302
- const response = await this._fetch(url);
303
- if (!response.ok) {
304
- if (response.status === 404) {
305
- throw new errors_js_1.LangSmithResourceNotFoundError(`Template '${name}' not found`, "template");
306
- }
307
- await (0, helpers_js_1.handleClientHttpError)(response);
308
- }
309
- return (await response.json());
310
- }
311
- /**
312
- * List all SandboxTemplates.
313
- *
314
- * @returns List of SandboxTemplates.
315
- */
316
- async listTemplates() {
317
- const url = `${this._baseUrl}/templates`;
318
- const response = await this._fetch(url);
319
- if (!response.ok) {
320
- if (response.status === 404) {
321
- throw new errors_js_1.LangSmithSandboxAPIError(`API endpoint not found: ${url}. Check that apiEndpoint is correct.`);
322
- }
323
- await (0, helpers_js_1.handleClientHttpError)(response);
324
- }
325
- const data = await response.json();
326
- return (data.templates ?? []);
327
- }
328
- /**
329
- * Update a template.
330
- *
331
- * @param name - Current template name.
332
- * @param options - Update options (e.g., newName).
333
- * @returns Updated SandboxTemplate.
334
- * @throws LangSmithResourceNotFoundError if template not found.
335
- * @throws LangSmithResourceNameConflictError if newName is already in use.
336
- */
337
- async updateTemplate(name, options) {
338
- const { newName } = options;
339
- if (newName === undefined) {
340
- // Nothing to update, just return the current template
341
- return this.getTemplate(name);
342
- }
343
- const url = `${this._baseUrl}/templates/${encodeURIComponent(name)}`;
344
- const payload = { name: newName };
345
- const response = await this._fetch(url, {
346
- method: "PATCH",
347
- headers: { "Content-Type": "application/json" },
348
- body: JSON.stringify(payload),
349
- });
350
- if (!response.ok) {
351
- if (response.status === 404) {
352
- throw new errors_js_1.LangSmithResourceNotFoundError(`Template '${name}' not found`, "template");
353
- }
354
- if (response.status === 409) {
355
- await (0, helpers_js_1.handleConflictError)(response, "template");
356
- }
357
- await (0, helpers_js_1.handleClientHttpError)(response);
358
- }
359
- return (await response.json());
360
- }
361
- /**
362
- * Delete a SandboxTemplate.
363
- *
364
- * @param name - Template name.
365
- * @throws LangSmithResourceNotFoundError if template not found.
366
- * @throws ResourceInUseError if template is referenced by sandboxes or pools.
367
- */
368
- async deleteTemplate(name) {
369
- const url = `${this._baseUrl}/templates/${encodeURIComponent(name)}`;
370
- const response = await this._fetch(url, { method: "DELETE" });
371
- if (!response.ok) {
372
- if (response.status === 404) {
373
- throw new errors_js_1.LangSmithResourceNotFoundError(`Template '${name}' not found`, "template");
374
- }
375
- if (response.status === 409) {
376
- await (0, helpers_js_1.handleResourceInUseError)(response, "template");
377
- }
378
- await (0, helpers_js_1.handleClientHttpError)(response);
379
- }
380
- }
381
- // =========================================================================
382
- // Pool Operations
383
- // =========================================================================
384
144
  /**
385
- * Create a new Sandbox Pool.
386
- *
387
- * Pools pre-provision sandboxes from a template for faster startup.
388
- *
389
- * @param name - Pool name (lowercase letters, numbers, hyphens; max 63 chars).
390
- * @param options - Creation options including templateName, replicas, and optional timeout.
391
- * @returns Created Pool.
392
- * @throws LangSmithResourceNotFoundError if template not found.
393
- * @throws ValidationError if template has volumes attached.
394
- * @throws ResourceAlreadyExistsError if pool with this name already exists.
395
- * @throws ResourceTimeoutError if pool doesn't reach ready state within timeout.
396
- * @throws QuotaExceededError if organization quota is exceeded.
145
+ * JSON POST helper. Sends JSON body, checks response status,
146
+ * and returns the Response for further processing.
147
+ * Throws on non-ok responses via handleClientHttpError.
148
+ * Callers can add specific status checks (e.g. 404) before calling this.
149
+ * @internal
397
150
  */
398
- async createPool(name, options) {
399
- const { templateName, replicas, timeout = 30 } = options;
400
- const url = `${this._baseUrl}/pools`;
401
- const payload = {
402
- name,
403
- template_name: templateName,
404
- replicas,
405
- wait_for_ready: true,
406
- timeout,
407
- };
151
+ async _postJson(url, body, options) {
408
152
  const response = await this._fetch(url, {
409
153
  method: "POST",
410
154
  headers: { "Content-Type": "application/json" },
411
- body: JSON.stringify(payload),
412
- signal: AbortSignal.timeout((timeout + 30) * 1000),
413
- });
414
- if (!response.ok) {
415
- await (0, helpers_js_1.handlePoolError)(response);
416
- }
417
- return (await response.json());
418
- }
419
- /**
420
- * Get a Pool by name.
421
- *
422
- * @param name - Pool name.
423
- * @returns Pool.
424
- * @throws LangSmithResourceNotFoundError if pool not found.
425
- */
426
- async getPool(name) {
427
- const url = `${this._baseUrl}/pools/${encodeURIComponent(name)}`;
428
- const response = await this._fetch(url);
429
- if (!response.ok) {
430
- if (response.status === 404) {
431
- throw new errors_js_1.LangSmithResourceNotFoundError(`Pool '${name}' not found`, "pool");
432
- }
433
- await (0, helpers_js_1.handleClientHttpError)(response);
434
- }
435
- return (await response.json());
436
- }
437
- /**
438
- * List all Pools.
439
- *
440
- * @returns List of Pools.
441
- */
442
- async listPools() {
443
- const url = `${this._baseUrl}/pools`;
444
- const response = await this._fetch(url);
445
- if (!response.ok) {
446
- if (response.status === 404) {
447
- throw new errors_js_1.LangSmithSandboxAPIError(`API endpoint not found: ${url}. Check that apiEndpoint is correct.`);
448
- }
449
- await (0, helpers_js_1.handleClientHttpError)(response);
450
- }
451
- const data = await response.json();
452
- return (data.pools ?? []);
453
- }
454
- /**
455
- * Update a Pool's name and/or replica count.
456
- *
457
- * You can update the display name, replica count, or both.
458
- * The template reference cannot be changed after creation.
459
- *
460
- * @param name - Current pool name.
461
- * @param options - Update options.
462
- * @returns Updated Pool.
463
- * @throws LangSmithResourceNotFoundError if pool not found.
464
- * @throws ValidationError if template was deleted.
465
- * @throws LangSmithResourceNameConflictError if newName is already in use.
466
- * @throws QuotaExceededError if quota exceeded when scaling up.
467
- */
468
- async updatePool(name, options) {
469
- const { newName, replicas } = options;
470
- if (newName === undefined && replicas === undefined) {
471
- // Nothing to update, just return the current pool
472
- return this.getPool(name);
473
- }
474
- const url = `${this._baseUrl}/pools/${encodeURIComponent(name)}`;
475
- const payload = {};
476
- if (newName !== undefined) {
477
- payload.name = newName;
478
- }
479
- if (replicas !== undefined) {
480
- payload.replicas = replicas;
481
- }
482
- const response = await this._fetch(url, {
483
- method: "PATCH",
484
- headers: { "Content-Type": "application/json" },
485
- body: JSON.stringify(payload),
155
+ body: JSON.stringify(body),
156
+ signal: options?.signal,
486
157
  });
487
158
  if (!response.ok) {
488
- if (response.status === 404) {
489
- throw new errors_js_1.LangSmithResourceNotFoundError(`Pool '${name}' not found`, "pool");
490
- }
491
- if (response.status === 409) {
492
- await (0, helpers_js_1.handleConflictError)(response, "pool");
493
- }
494
- await (0, helpers_js_1.handlePoolError)(response);
495
- }
496
- return (await response.json());
497
- }
498
- /**
499
- * Delete a Pool.
500
- *
501
- * This will terminate all sandboxes in the pool.
502
- *
503
- * @param name - Pool name.
504
- * @throws LangSmithResourceNotFoundError if pool not found.
505
- */
506
- async deletePool(name) {
507
- const url = `${this._baseUrl}/pools/${encodeURIComponent(name)}`;
508
- const response = await this._fetch(url, { method: "DELETE" });
509
- if (!response.ok) {
510
- if (response.status === 404) {
511
- throw new errors_js_1.LangSmithResourceNotFoundError(`Pool '${name}' not found`, "pool");
512
- }
513
159
  await (0, helpers_js_1.handleClientHttpError)(response);
514
160
  }
161
+ return response;
515
162
  }
516
163
  // =========================================================================
517
164
  // Sandbox Operations
518
165
  // =========================================================================
519
166
  /**
520
- * Create a new Sandbox.
167
+ * Create a new Sandbox from a snapshot.
521
168
  *
522
169
  * Remember to call `sandbox.delete()` when done to clean up resources.
523
170
  *
524
- * @param templateName - Name of the SandboxTemplate to use.
525
- * @param options - Creation options.
171
+ * Exactly one of `snapshotId` (positional) or `options.snapshotName` must
172
+ * be provided. When `snapshotName` is used, the server resolves it to a
173
+ * snapshot owned by the caller's tenant.
174
+ *
175
+ * @param snapshotId - ID of the snapshot to boot from. Create one with
176
+ * `createSnapshot()` or `captureSnapshot()`, or pass an existing snapshot ID.
177
+ * Pass `undefined` when booting by name via `options.snapshotName`.
178
+ * @param options - Creation options. Use `options.snapshotName` to boot
179
+ * by snapshot name instead of ID.
526
180
  * @returns Created Sandbox.
527
181
  * @throws ResourceTimeoutError if timeout waiting for sandbox to be ready.
528
182
  * @throws SandboxCreationError if sandbox creation fails.
529
- * @throws LangSmithValidationError if TTL values are invalid.
183
+ * @throws LangSmithValidationError if TTL values are invalid, or if neither
184
+ * (or both) of `snapshotId` / `options.snapshotName` are provided.
530
185
  *
531
186
  * @example
532
187
  * ```typescript
533
- * const sandbox = await client.createSandbox("python-sandbox");
188
+ * const snapshot = await client.createSnapshot(
189
+ * "python",
190
+ * "python:3.12-slim",
191
+ * 1_073_741_824
192
+ * );
193
+ * const sandbox = await client.createSandbox(snapshot.id);
194
+ * // Or, resolve by snapshot name:
195
+ * const sandbox = await client.createSandbox(undefined, {
196
+ * snapshotName: "python",
197
+ * });
534
198
  * try {
535
199
  * const result = await sandbox.run("echo hello");
536
200
  * console.log(result.stdout);
@@ -539,15 +203,23 @@ class SandboxClient {
539
203
  * }
540
204
  * ```
541
205
  */
542
- async createSandbox(templateName, options = {}) {
543
- const { name, timeout = 30, waitForReady = true, ttlSeconds, idleTtlSeconds, } = options;
206
+ async createSandbox(snapshotId, options = {}) {
207
+ const { snapshotName, name, timeout = 30, waitForReady = true, ttlSeconds, idleTtlSeconds, vCpus, memBytes, fsCapacityBytes, proxyConfig, } = options;
208
+ if (!!snapshotId === !!snapshotName) {
209
+ throw new errors_js_1.LangSmithValidationError("Exactly one of snapshotId or options.snapshotName must be set", "snapshotId");
210
+ }
544
211
  (0, helpers_js_1.validateTtl)(ttlSeconds, "ttlSeconds");
545
212
  (0, helpers_js_1.validateTtl)(idleTtlSeconds, "idleTtlSeconds");
546
213
  const url = `${this._baseUrl}/boxes`;
547
214
  const payload = {
548
- template_name: templateName,
549
215
  wait_for_ready: waitForReady,
550
216
  };
217
+ if (snapshotId) {
218
+ payload.snapshot_id = snapshotId;
219
+ }
220
+ if (snapshotName) {
221
+ payload.snapshot_name = snapshotName;
222
+ }
551
223
  if (waitForReady) {
552
224
  payload.timeout = timeout;
553
225
  }
@@ -560,6 +232,18 @@ class SandboxClient {
560
232
  if (idleTtlSeconds !== undefined) {
561
233
  payload.idle_ttl_seconds = idleTtlSeconds;
562
234
  }
235
+ if (vCpus !== undefined) {
236
+ payload.vcpus = vCpus;
237
+ }
238
+ if (memBytes !== undefined) {
239
+ payload.mem_bytes = memBytes;
240
+ }
241
+ if (fsCapacityBytes !== undefined) {
242
+ payload.fs_capacity_bytes = fsCapacityBytes;
243
+ }
244
+ if (proxyConfig !== undefined) {
245
+ payload.proxy_config = proxyConfig;
246
+ }
563
247
  const httpTimeout = waitForReady ? (timeout + 30) * 1000 : 30 * 1000;
564
248
  const response = await this._fetch(url, {
565
249
  method: "POST",
@@ -582,9 +266,9 @@ class SandboxClient {
582
266
  * @returns Sandbox.
583
267
  * @throws LangSmithResourceNotFoundError if sandbox not found.
584
268
  */
585
- async getSandbox(name) {
269
+ async getSandbox(name, options) {
586
270
  const url = `${this._baseUrl}/boxes/${encodeURIComponent(name)}`;
587
- const response = await this._fetch(url);
271
+ const response = await this._fetch(url, { signal: options?.signal });
588
272
  if (!response.ok) {
589
273
  if (response.status === 404) {
590
274
  throw new errors_js_1.LangSmithResourceNotFoundError(`Sandbox '${name}' not found`, "sandbox");
@@ -679,9 +363,9 @@ class SandboxClient {
679
363
  * @returns ResourceStatus with status and optional status_message.
680
364
  * @throws LangSmithResourceNotFoundError if sandbox not found.
681
365
  */
682
- async getSandboxStatus(name) {
366
+ async getSandboxStatus(name, options) {
683
367
  const url = `${this._baseUrl}/boxes/${encodeURIComponent(name)}/status`;
684
- const response = await this._fetch(url);
368
+ const response = await this._fetch(url, { signal: options?.signal });
685
369
  if (!response.ok) {
686
370
  if (response.status === 404) {
687
371
  throw new errors_js_1.LangSmithResourceNotFoundError(`Sandbox '${name}' not found`, "sandbox");
@@ -705,28 +389,243 @@ class SandboxClient {
705
389
  *
706
390
  * @example
707
391
  * ```typescript
708
- * const sandbox = await client.createSandbox("python-sandbox", { waitForReady: false });
392
+ * const sandbox = await client.createSandbox(snapshot.id, { waitForReady: false });
709
393
  * // ... do other work ...
710
394
  * const readySandbox = await client.waitForSandbox(sandbox.name);
711
395
  * ```
712
396
  */
713
397
  async waitForSandbox(name, options = {}) {
714
- const { timeout = 120, pollInterval = 1.0 } = options;
398
+ const { timeout = 120, pollInterval = 1.0, signal } = options;
715
399
  const deadline = Date.now() + timeout * 1000;
716
400
  let lastStatus = "provisioning";
717
401
  while (Date.now() < deadline) {
718
- const statusResult = await this.getSandboxStatus(name);
402
+ signal?.throwIfAborted();
403
+ const statusResult = await this.getSandboxStatus(name, { signal });
719
404
  lastStatus = statusResult.status;
720
405
  if (statusResult.status === "ready") {
721
- return this.getSandbox(name);
406
+ return this.getSandbox(name, { signal });
722
407
  }
723
408
  if (statusResult.status === "failed") {
724
409
  throw new errors_js_1.LangSmithResourceCreationError(statusResult.status_message ?? `Sandbox '${name}' creation failed`, "sandbox");
725
410
  }
726
- // Wait before polling again
727
- await new Promise((resolve) => setTimeout(resolve, pollInterval * 1000));
411
+ // Wait before polling again, capped to remaining time + jitter
412
+ const remaining = deadline - Date.now();
413
+ const jitter = pollInterval * 200 * (Math.random() - 0.5); // ±10%
414
+ const delay = Math.min(pollInterval * 1000 + jitter, remaining);
415
+ if (delay > 0) {
416
+ await sleepWithSignal(delay, signal);
417
+ }
728
418
  }
729
419
  throw new errors_js_1.LangSmithResourceTimeoutError(`Sandbox '${name}' did not become ready within ${timeout}s`, "sandbox", lastStatus);
730
420
  }
421
+ /**
422
+ * Start a stopped sandbox and wait until ready.
423
+ *
424
+ * @param name - Sandbox name.
425
+ * @param options - Options with timeout.
426
+ * @returns Sandbox in "ready" status.
427
+ */
428
+ async startSandbox(name, options = {}) {
429
+ const { timeout = 120, signal } = options;
430
+ const url = `${this._baseUrl}/boxes/${encodeURIComponent(name)}/start`;
431
+ await this._postJson(url, {}, { signal });
432
+ return this.waitForSandbox(name, { timeout, signal });
433
+ }
434
+ /**
435
+ * Stop a running sandbox (preserves sandbox files for later restart).
436
+ *
437
+ * @param name - Sandbox name.
438
+ */
439
+ async stopSandbox(name) {
440
+ const url = `${this._baseUrl}/boxes/${encodeURIComponent(name)}/stop`;
441
+ await this._postJson(url, {});
442
+ }
443
+ // =========================================================================
444
+ // Snapshot Operations
445
+ // =========================================================================
446
+ /**
447
+ * Build a snapshot from a Docker image.
448
+ *
449
+ * Blocks until the snapshot is ready (polls with 2s interval).
450
+ *
451
+ * @param name - Snapshot name.
452
+ * @param dockerImage - Docker image to build from (e.g., "python:3.12-slim").
453
+ * @param fsCapacityBytes - Filesystem capacity in bytes.
454
+ * @param options - Additional options (registry credentials, timeout).
455
+ * @returns Snapshot in "ready" status.
456
+ */
457
+ async createSnapshot(name, dockerImage, fsCapacityBytes, options = {}) {
458
+ const { registryId, registryUrl, registryUsername, registryPassword, timeout = 60, signal, } = options;
459
+ const url = `${this._baseUrl}/snapshots`;
460
+ const payload = {
461
+ name,
462
+ docker_image: dockerImage,
463
+ fs_capacity_bytes: fsCapacityBytes,
464
+ };
465
+ if (registryId !== undefined) {
466
+ payload.registry_id = registryId;
467
+ }
468
+ if (registryUrl !== undefined) {
469
+ payload.registry_url = registryUrl;
470
+ }
471
+ if (registryUsername !== undefined) {
472
+ payload.registry_username = registryUsername;
473
+ }
474
+ if (registryPassword !== undefined) {
475
+ payload.registry_password = registryPassword;
476
+ }
477
+ const response = await this._postJson(url, payload, { signal });
478
+ const snapshot = (await response.json());
479
+ return this.waitForSnapshot(snapshot.id, { timeout, signal });
480
+ }
481
+ /**
482
+ * Capture a snapshot from a running sandbox.
483
+ *
484
+ * Blocks until the snapshot is ready (polls with 2s interval).
485
+ *
486
+ * @param sandboxName - Name of the sandbox to capture from.
487
+ * @param name - Snapshot name.
488
+ * @param options - Capture options (timeout).
489
+ * @returns Snapshot in "ready" status.
490
+ */
491
+ async captureSnapshot(sandboxName, name, options = {}) {
492
+ const { timeout = 60, signal } = options;
493
+ const url = `${this._baseUrl}/boxes/${encodeURIComponent(sandboxName)}/snapshot`;
494
+ const payload = { name };
495
+ const response = await this._postJson(url, payload, { signal });
496
+ const snapshot = (await response.json());
497
+ return this.waitForSnapshot(snapshot.id, { timeout, signal });
498
+ }
499
+ /**
500
+ * Get a snapshot by ID.
501
+ *
502
+ * @param snapshotId - Snapshot UUID.
503
+ * @returns Snapshot.
504
+ */
505
+ async getSnapshot(snapshotId, options) {
506
+ const url = `${this._baseUrl}/snapshots/${encodeURIComponent(snapshotId)}`;
507
+ const response = await this._fetch(url, { signal: options?.signal });
508
+ if (!response.ok) {
509
+ if (response.status === 404) {
510
+ throw new errors_js_1.LangSmithResourceNotFoundError(`Snapshot '${snapshotId}' not found`, "snapshot");
511
+ }
512
+ await (0, helpers_js_1.handleClientHttpError)(response);
513
+ }
514
+ return (await response.json());
515
+ }
516
+ /**
517
+ * List snapshots, optionally filtered and paginated server-side.
518
+ *
519
+ * The backend always paginates this endpoint. When `limit` is omitted the
520
+ * server applies a default page size (currently 50), so a single call is
521
+ * not guaranteed to return every snapshot visible to the tenant. To iterate
522
+ * through all results, repeat the call with increasing `offset` values (or
523
+ * an explicit `limit`) until fewer than `limit` snapshots come back.
524
+ *
525
+ * @param options - Optional filter/pagination options.
526
+ * - `nameContains`: case-insensitive substring match on snapshot name.
527
+ * - `limit`: page size; must be between 1 and 500 (inclusive). Defaults
528
+ * to 50 server-side when omitted.
529
+ * - `offset`: number of snapshots to skip; must be `>= 0`.
530
+ *
531
+ * Values outside those ranges are rejected by the server.
532
+ * @returns A single page of Snapshots matching the provided filters.
533
+ *
534
+ * @example
535
+ * ```typescript
536
+ * const firstPage = await client.listSnapshots();
537
+ * const page = await client.listSnapshots({
538
+ * nameContains: "python",
539
+ * limit: 100,
540
+ * offset: 0,
541
+ * });
542
+ * ```
543
+ */
544
+ async listSnapshots(options = {}) {
545
+ const { nameContains, limit, offset, signal } = options;
546
+ const params = new URLSearchParams();
547
+ if (nameContains !== undefined) {
548
+ params.set("name_contains", nameContains);
549
+ }
550
+ if (limit !== undefined) {
551
+ params.set("limit", String(limit));
552
+ }
553
+ if (offset !== undefined) {
554
+ params.set("offset", String(offset));
555
+ }
556
+ const query = params.toString();
557
+ const url = query
558
+ ? `${this._baseUrl}/snapshots?${query}`
559
+ : `${this._baseUrl}/snapshots`;
560
+ const response = await this._fetch(url, { signal });
561
+ if (!response.ok) {
562
+ await (0, helpers_js_1.handleClientHttpError)(response);
563
+ }
564
+ const data = await response.json();
565
+ return (data.snapshots ?? []);
566
+ }
567
+ /**
568
+ * Delete a snapshot.
569
+ *
570
+ * @param snapshotId - Snapshot UUID.
571
+ */
572
+ async deleteSnapshot(snapshotId) {
573
+ const url = `${this._baseUrl}/snapshots/${encodeURIComponent(snapshotId)}`;
574
+ const response = await this._fetch(url, { method: "DELETE" });
575
+ if (!response.ok) {
576
+ await (0, helpers_js_1.handleClientHttpError)(response);
577
+ }
578
+ }
579
+ /**
580
+ * Poll until a snapshot reaches "ready" or "failed" status.
581
+ *
582
+ * @param snapshotId - Snapshot UUID.
583
+ * @param options - Polling options (timeout, pollInterval).
584
+ * @returns Snapshot in "ready" status.
585
+ */
586
+ async waitForSnapshot(snapshotId, options = {}) {
587
+ const { timeout = 300, pollInterval = 2.0, signal } = options;
588
+ const deadline = Date.now() + timeout * 1000;
589
+ let lastStatus = "building";
590
+ while (Date.now() < deadline) {
591
+ signal?.throwIfAborted();
592
+ const snapshot = await this.getSnapshot(snapshotId, { signal });
593
+ lastStatus = snapshot.status;
594
+ if (snapshot.status === "ready") {
595
+ return snapshot;
596
+ }
597
+ if (snapshot.status === "failed") {
598
+ throw new errors_js_1.LangSmithResourceCreationError(snapshot.status_message ?? `Snapshot '${snapshotId}' build failed`, "snapshot");
599
+ }
600
+ // Cap sleep to remaining time + jitter
601
+ const remaining = deadline - Date.now();
602
+ const jitter = pollInterval * 200 * (Math.random() - 0.5); // ±10%
603
+ const delay = Math.min(pollInterval * 1000 + jitter, remaining);
604
+ if (delay > 0) {
605
+ await sleepWithSignal(delay, signal);
606
+ }
607
+ }
608
+ throw new errors_js_1.LangSmithResourceTimeoutError(`Snapshot '${snapshotId}' did not become ready within ${timeout}s`, "snapshot", lastStatus);
609
+ }
610
+ /**
611
+ * Returns a string representation of the SandboxClient instance.
612
+ * This method is called when the object is converted to a string
613
+ * or logged, ensuring sensitive information like API keys is not exposed.
614
+ *
615
+ * @returns A string representation of the SandboxClient.
616
+ */
617
+ toString() {
618
+ return `[LangSmithSandboxClient apiEndpoint=${JSON.stringify(this._baseUrl)}]`;
619
+ }
620
+ /**
621
+ * Custom inspect method for Node.js.
622
+ * This method is called when the object is inspected in the Node.js REPL
623
+ * or with console.log, ensuring sensitive information like API keys is not exposed.
624
+ *
625
+ * @returns A string representation of the SandboxClient for inspection.
626
+ */
627
+ [Symbol.for("nodejs.util.inspect.custom")]() {
628
+ return this.toString();
629
+ }
731
630
  }
732
631
  exports.SandboxClient = SandboxClient;