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