langsmith 0.7.3 → 0.7.5

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 CHANGED
@@ -4103,6 +4103,23 @@ class Client {
4103
4103
  }
4104
4104
  return json.commits[0].commit_hash;
4105
4105
  }
4106
+ async _createCommitTags(promptOwnerAndName, commitId, tags) {
4107
+ const tagList = typeof tags === "string" ? [tags] : tags;
4108
+ await Promise.all(tagList.map(async (tag) => this.caller.call(async () => {
4109
+ const res = await this._fetch(`${this.apiUrl}/repos/${promptOwnerAndName}/tags`, {
4110
+ method: "POST",
4111
+ headers: {
4112
+ ...this._mergedHeaders,
4113
+ "Content-Type": "application/json",
4114
+ },
4115
+ signal: AbortSignal.timeout(this.timeout_ms),
4116
+ ...this.fetchOptions,
4117
+ body: JSON.stringify({ tag_name: tag, commit_id: commitId }),
4118
+ });
4119
+ await (0, error_js_1.raiseForStatus)(res, "create commit tag");
4120
+ return res;
4121
+ })));
4122
+ }
4106
4123
  async _likeOrUnlikePrompt(promptIdentifier, like) {
4107
4124
  const [owner, promptName, _] = (0, prompts_js_1.parseHubIdentifier)(promptIdentifier);
4108
4125
  const body = JSON.stringify({ like: like });
@@ -4369,6 +4386,8 @@ class Client {
4369
4386
  * @param object - The prompt object/manifest to commit (e.g., ChatPromptTemplate, messages array, etc.)
4370
4387
  * @param options - Optional configuration for the commit
4371
4388
  * @param options.parentCommitHash - The parent commit hash. Defaults to "latest" (the most recent commit).
4389
+ * @param options.tags - A tag or list of tags to apply to the commit.
4390
+ * @param options.description - A description for the commit.
4372
4391
  * @returns A Promise that resolves to the URL of the newly created commit
4373
4392
  * @throws {Error} If the prompt does not exist
4374
4393
  * @example
@@ -4384,9 +4403,9 @@ class Client {
4384
4403
  * const commitUrl = await client.createCommit("my-prompt", template);
4385
4404
  * console.log(`Commit created: ${commitUrl}`);
4386
4405
  *
4387
- * // Create a commit based on a specific parent commit
4406
+ * // Create a commit with tags
4388
4407
  * const commitUrl2 = await client.createCommit("my-prompt", template, {
4389
- * parentCommitHash: "abc123def456"
4408
+ * tags: ["production", "v1"]
4390
4409
  * });
4391
4410
  * ```
4392
4411
  */
@@ -4421,7 +4440,11 @@ class Client {
4421
4440
  return res;
4422
4441
  });
4423
4442
  const result = await response.json();
4424
- return this._getPromptUrl(`${owner}/${promptName}${result.commit_hash ? `:${result.commit_hash}` : ""}`);
4443
+ const commit = result.commit ?? result;
4444
+ if (options?.tags) {
4445
+ await this._createCommitTags(`${owner}/${promptName}`, commit.id, options.tags);
4446
+ }
4447
+ return this._getPromptUrl(`${owner}/${promptName}${commit.commit_hash ? `:${commit.commit_hash}` : ""}`);
4425
4448
  }
4426
4449
  /**
4427
4450
  * Update examples with attachments using multipart form data.
@@ -4756,7 +4779,8 @@ class Client {
4756
4779
  async pushPrompt(promptIdentifier, options) {
4757
4780
  // Create or update prompt metadata
4758
4781
  if (await this.promptExists(promptIdentifier)) {
4759
- if (options && Object.keys(options).some((key) => key !== "object")) {
4782
+ if (options &&
4783
+ ["description", "readme", "tags", "isPublic"].some((key) => options[key] !== undefined)) {
4760
4784
  await this.updatePrompt(promptIdentifier, {
4761
4785
  description: options?.description,
4762
4786
  readme: options?.readme,
@@ -4779,6 +4803,7 @@ class Client {
4779
4803
  // Create a commit with the new manifest
4780
4804
  const url = await this.createCommit(promptIdentifier, options?.object, {
4781
4805
  parentCommitHash: options?.parentCommitHash,
4806
+ tags: options?.commitTags,
4782
4807
  description: options?.commitDescription,
4783
4808
  });
4784
4809
  return url;
@@ -4927,7 +4952,9 @@ class Client {
4927
4952
  });
4928
4953
  const data = (await response.json());
4929
4954
  const commitHash = data.commit.commit_hash;
4930
- return `${this.getHostUrl()}/context/${name}/${commitHash.slice(0, 8)}`;
4955
+ const settings = await this._getSettings();
4956
+ const query = new URLSearchParams({ organizationId: settings.id });
4957
+ return `${this.getHostUrl()}/context/${name}/${commitHash.slice(0, 8)}?${query.toString()}`;
4931
4958
  }
4932
4959
  async _deleteDirectory(identifier) {
4933
4960
  const [owner, name] = (0, prompts_js_1.parseHubIdentifier)(identifier);
package/dist/client.d.ts CHANGED
@@ -1129,6 +1129,7 @@ export declare class Client implements LangSmithTracingClientInterface {
1129
1129
  protected _currentTenantIsOwner(owner: string): Promise<boolean>;
1130
1130
  protected _ownerConflictError(action: string, owner: string): Promise<Error>;
1131
1131
  protected _getLatestCommitHash(promptOwnerAndName: string): Promise<string | undefined>;
1132
+ protected _createCommitTags(promptOwnerAndName: string, commitId: string, tags: string | string[]): Promise<void>;
1132
1133
  protected _likeOrUnlikePrompt(promptIdentifier: string, like: boolean): Promise<LikePromptResponse>;
1133
1134
  protected _getPromptUrl(promptIdentifier: string): Promise<string>;
1134
1135
  /**
@@ -1287,6 +1288,8 @@ export declare class Client implements LangSmithTracingClientInterface {
1287
1288
  * @param object - The prompt object/manifest to commit (e.g., ChatPromptTemplate, messages array, etc.)
1288
1289
  * @param options - Optional configuration for the commit
1289
1290
  * @param options.parentCommitHash - The parent commit hash. Defaults to "latest" (the most recent commit).
1291
+ * @param options.tags - A tag or list of tags to apply to the commit.
1292
+ * @param options.description - A description for the commit.
1290
1293
  * @returns A Promise that resolves to the URL of the newly created commit
1291
1294
  * @throws {Error} If the prompt does not exist
1292
1295
  * @example
@@ -1302,14 +1305,15 @@ export declare class Client implements LangSmithTracingClientInterface {
1302
1305
  * const commitUrl = await client.createCommit("my-prompt", template);
1303
1306
  * console.log(`Commit created: ${commitUrl}`);
1304
1307
  *
1305
- * // Create a commit based on a specific parent commit
1308
+ * // Create a commit with tags
1306
1309
  * const commitUrl2 = await client.createCommit("my-prompt", template, {
1307
- * parentCommitHash: "abc123def456"
1310
+ * tags: ["production", "v1"]
1308
1311
  * });
1309
1312
  * ```
1310
1313
  */
1311
1314
  createCommit(promptIdentifier: string, object: any, options?: {
1312
1315
  parentCommitHash?: string;
1316
+ tags?: string | string[];
1313
1317
  description?: string;
1314
1318
  }): Promise<string>;
1315
1319
  /**
@@ -1413,6 +1417,7 @@ export declare class Client implements LangSmithTracingClientInterface {
1413
1417
  description?: string;
1414
1418
  readme?: string;
1415
1419
  tags?: string[];
1420
+ commitTags?: string | string[];
1416
1421
  commitDescription?: string;
1417
1422
  }): Promise<string>;
1418
1423
  /**
package/dist/client.js CHANGED
@@ -4065,6 +4065,23 @@ export class Client {
4065
4065
  }
4066
4066
  return json.commits[0].commit_hash;
4067
4067
  }
4068
+ async _createCommitTags(promptOwnerAndName, commitId, tags) {
4069
+ const tagList = typeof tags === "string" ? [tags] : tags;
4070
+ await Promise.all(tagList.map(async (tag) => this.caller.call(async () => {
4071
+ const res = await this._fetch(`${this.apiUrl}/repos/${promptOwnerAndName}/tags`, {
4072
+ method: "POST",
4073
+ headers: {
4074
+ ...this._mergedHeaders,
4075
+ "Content-Type": "application/json",
4076
+ },
4077
+ signal: AbortSignal.timeout(this.timeout_ms),
4078
+ ...this.fetchOptions,
4079
+ body: JSON.stringify({ tag_name: tag, commit_id: commitId }),
4080
+ });
4081
+ await raiseForStatus(res, "create commit tag");
4082
+ return res;
4083
+ })));
4084
+ }
4068
4085
  async _likeOrUnlikePrompt(promptIdentifier, like) {
4069
4086
  const [owner, promptName, _] = parseHubIdentifier(promptIdentifier);
4070
4087
  const body = JSON.stringify({ like: like });
@@ -4331,6 +4348,8 @@ export class Client {
4331
4348
  * @param object - The prompt object/manifest to commit (e.g., ChatPromptTemplate, messages array, etc.)
4332
4349
  * @param options - Optional configuration for the commit
4333
4350
  * @param options.parentCommitHash - The parent commit hash. Defaults to "latest" (the most recent commit).
4351
+ * @param options.tags - A tag or list of tags to apply to the commit.
4352
+ * @param options.description - A description for the commit.
4334
4353
  * @returns A Promise that resolves to the URL of the newly created commit
4335
4354
  * @throws {Error} If the prompt does not exist
4336
4355
  * @example
@@ -4346,9 +4365,9 @@ export class Client {
4346
4365
  * const commitUrl = await client.createCommit("my-prompt", template);
4347
4366
  * console.log(`Commit created: ${commitUrl}`);
4348
4367
  *
4349
- * // Create a commit based on a specific parent commit
4368
+ * // Create a commit with tags
4350
4369
  * const commitUrl2 = await client.createCommit("my-prompt", template, {
4351
- * parentCommitHash: "abc123def456"
4370
+ * tags: ["production", "v1"]
4352
4371
  * });
4353
4372
  * ```
4354
4373
  */
@@ -4383,7 +4402,11 @@ export class Client {
4383
4402
  return res;
4384
4403
  });
4385
4404
  const result = await response.json();
4386
- return this._getPromptUrl(`${owner}/${promptName}${result.commit_hash ? `:${result.commit_hash}` : ""}`);
4405
+ const commit = result.commit ?? result;
4406
+ if (options?.tags) {
4407
+ await this._createCommitTags(`${owner}/${promptName}`, commit.id, options.tags);
4408
+ }
4409
+ return this._getPromptUrl(`${owner}/${promptName}${commit.commit_hash ? `:${commit.commit_hash}` : ""}`);
4387
4410
  }
4388
4411
  /**
4389
4412
  * Update examples with attachments using multipart form data.
@@ -4718,7 +4741,8 @@ export class Client {
4718
4741
  async pushPrompt(promptIdentifier, options) {
4719
4742
  // Create or update prompt metadata
4720
4743
  if (await this.promptExists(promptIdentifier)) {
4721
- if (options && Object.keys(options).some((key) => key !== "object")) {
4744
+ if (options &&
4745
+ ["description", "readme", "tags", "isPublic"].some((key) => options[key] !== undefined)) {
4722
4746
  await this.updatePrompt(promptIdentifier, {
4723
4747
  description: options?.description,
4724
4748
  readme: options?.readme,
@@ -4741,6 +4765,7 @@ export class Client {
4741
4765
  // Create a commit with the new manifest
4742
4766
  const url = await this.createCommit(promptIdentifier, options?.object, {
4743
4767
  parentCommitHash: options?.parentCommitHash,
4768
+ tags: options?.commitTags,
4744
4769
  description: options?.commitDescription,
4745
4770
  });
4746
4771
  return url;
@@ -4889,7 +4914,9 @@ export class Client {
4889
4914
  });
4890
4915
  const data = (await response.json());
4891
4916
  const commitHash = data.commit.commit_hash;
4892
- return `${this.getHostUrl()}/context/${name}/${commitHash.slice(0, 8)}`;
4917
+ const settings = await this._getSettings();
4918
+ const query = new URLSearchParams({ organizationId: settings.id });
4919
+ return `${this.getHostUrl()}/context/${name}/${commitHash.slice(0, 8)}?${query.toString()}`;
4893
4920
  }
4894
4921
  async _deleteDirectory(identifier) {
4895
4922
  const [owner, name] = parseHubIdentifier(identifier);
@@ -224,5 +224,5 @@ async function evaluateComparative(experiments, options) {
224
224
  });
225
225
  const results = await Promise.all(promises);
226
226
  await client.awaitPendingTraceBatches();
227
- return { experimentName, results };
227
+ return { experimentName, results, url: viewUrl, comparativeExperiment };
228
228
  }
@@ -1,5 +1,5 @@
1
1
  import { Client } from "../index.js";
2
- import { ComparisonEvaluationResult as ComparisonEvaluationResultRow, Example, Run } from "../schemas.js";
2
+ import { ComparativeExperiment, ComparisonEvaluationResult as ComparisonEvaluationResultRow, Example, Run } from "../schemas.js";
3
3
  import { evaluate } from "./index.js";
4
4
  type ExperimentResults = Awaited<ReturnType<typeof evaluate>>;
5
5
  /** @deprecated Use ComparativeEvaluatorNew instead: (args: { runs, example, inputs, outputs, referenceOutputs }) => ... */
@@ -54,8 +54,14 @@ export interface EvaluateComparativeOptions {
54
54
  maxConcurrency?: number;
55
55
  }
56
56
  export interface ComparisonEvaluationResults {
57
+ /** The name of the comparative experiment. */
57
58
  experimentName: string;
59
+ /** The per-example comparison results. */
58
60
  results: ComparisonEvaluationResultRow[];
61
+ /** URL of the pairwise comparison view in the LangSmith UI, if available. */
62
+ url: string | null;
63
+ /** The comparative experiment, exposing its id, dataset, and metadata. */
64
+ comparativeExperiment: ComparativeExperiment;
59
65
  }
60
66
  /** @deprecated Use `evaluate` and pass two experiments as targets. */
61
67
  export declare function evaluateComparative(experiments: Array<string> | Array<Promise<ExperimentResults> | ExperimentResults>, options: EvaluateComparativeOptions): Promise<ComparisonEvaluationResults>;
@@ -218,5 +218,5 @@ export async function evaluateComparative(experiments, options) {
218
218
  });
219
219
  const results = await Promise.all(promises);
220
220
  await client.awaitPendingTraceBatches();
221
- return { experimentName, results };
221
+ return { experimentName, results, url: viewUrl, comparativeExperiment };
222
222
  }
package/dist/index.cjs CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.__version__ = exports.promptCacheSingleton = exports.configureGlobalPromptCache = exports.PromptCache = exports.Cache = exports.uuid7FromTime = exports.uuid7 = exports.getDefaultProjectName = exports.overrideFetchImplementation = exports.RunTree = exports.Client = void 0;
3
+ exports.LS_MESSAGE_VIEW_EXCLUDE = exports.__version__ = exports.promptCacheSingleton = exports.configureGlobalPromptCache = exports.PromptCache = exports.Cache = exports.uuid7FromTime = exports.uuid7 = exports.getDefaultProjectName = exports.overrideFetchImplementation = exports.RunTree = exports.Client = void 0;
4
4
  var client_js_1 = require("./client.cjs");
5
5
  Object.defineProperty(exports, "Client", { enumerable: true, get: function () { return client_js_1.Client; } });
6
6
  var run_trees_js_1 = require("./run_trees.cjs");
@@ -18,4 +18,6 @@ Object.defineProperty(exports, "PromptCache", { enumerable: true, get: function
18
18
  Object.defineProperty(exports, "configureGlobalPromptCache", { enumerable: true, get: function () { return index_js_1.configureGlobalPromptCache; } });
19
19
  Object.defineProperty(exports, "promptCacheSingleton", { enumerable: true, get: function () { return index_js_1.promptCacheSingleton; } });
20
20
  // Update using pnpm bump-version
21
- exports.__version__ = "0.7.3";
21
+ exports.__version__ = "0.7.5";
22
+ // Metadata key to hide a traced run from LangSmith's Messages View.
23
+ exports.LS_MESSAGE_VIEW_EXCLUDE = "ls_message_view_exclude";
package/dist/index.d.ts CHANGED
@@ -5,4 +5,5 @@ export { overrideFetchImplementation } from "./singletons/fetch.js";
5
5
  export { getDefaultProjectName } from "./utils/project.js";
6
6
  export { uuid7, uuid7FromTime } from "./uuid.js";
7
7
  export { Cache, PromptCache, type CacheConfig, type CacheMetrics, configureGlobalPromptCache, promptCacheSingleton, } from "./utils/prompt_cache/index.js";
8
- export declare const __version__ = "0.7.3";
8
+ export declare const __version__ = "0.7.5";
9
+ export declare const LS_MESSAGE_VIEW_EXCLUDE: "ls_message_view_exclude";
package/dist/index.js CHANGED
@@ -5,4 +5,6 @@ export { getDefaultProjectName } from "./utils/project.js";
5
5
  export { uuid7, uuid7FromTime } from "./uuid.js";
6
6
  export { Cache, PromptCache, configureGlobalPromptCache, promptCacheSingleton, } from "./utils/prompt_cache/index.js";
7
7
  // Update using pnpm bump-version
8
- export const __version__ = "0.7.3";
8
+ export const __version__ = "0.7.5";
9
+ // Metadata key to hide a traced run from LangSmith's Messages View.
10
+ export const LS_MESSAGE_VIEW_EXCLUDE = "ls_message_view_exclude";
@@ -10,6 +10,7 @@ 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
+ const index_js_1 = require("../utils/uuid/src/index.cjs");
13
14
  /**
14
15
  * Sleep that can be interrupted by an AbortSignal.
15
16
  * Resolves after `ms` milliseconds or rejects immediately if the signal fires.
@@ -18,17 +19,18 @@ function sleepWithSignal(ms, signal) {
18
19
  if (!signal) {
19
20
  return new Promise((resolve) => setTimeout(resolve, ms));
20
21
  }
21
- signal.throwIfAborted();
22
+ const abortSignal = signal;
23
+ abortSignal.throwIfAborted();
22
24
  return new Promise((resolve, reject) => {
23
25
  const timer = setTimeout(() => {
24
- signal.removeEventListener("abort", onAbort);
26
+ abortSignal.removeEventListener("abort", onAbort);
25
27
  resolve();
26
28
  }, ms);
27
29
  function onAbort() {
28
30
  clearTimeout(timer);
29
- reject(signal.reason);
31
+ reject(abortSignal.reason);
30
32
  }
31
- signal.addEventListener("abort", onAbort, { once: true });
33
+ abortSignal.addEventListener("abort", onAbort, { once: true });
32
34
  });
33
35
  }
34
36
  /**
@@ -47,6 +49,192 @@ function getDefaultApiEndpoint() {
47
49
  function getDefaultApiKey() {
48
50
  return (0, env_js_1.getLangSmithEnvironmentVariable)("API_KEY");
49
51
  }
52
+ function shellQuote(value) {
53
+ return `'${value.replace(/'/g, "'\\''")}'`;
54
+ }
55
+ function writeString(header, value, offset, length) {
56
+ header.write(value.slice(0, length), offset, length, "utf8");
57
+ }
58
+ function writeOctal(header, value, offset, length) {
59
+ const octal = value.toString(8).padStart(length - 1, "0");
60
+ header.write(octal.slice(-length + 1) + "\0", offset, length, "ascii");
61
+ }
62
+ function splitTarPath(name) {
63
+ if (Buffer.byteLength(name) <= 100) {
64
+ return { name, prefix: "" };
65
+ }
66
+ const parts = name.split("/");
67
+ for (let i = 1; i < parts.length; i += 1) {
68
+ const prefix = parts.slice(0, i).join("/");
69
+ const basename = parts.slice(i).join("/");
70
+ if (Buffer.byteLength(prefix) <= 155 &&
71
+ Buffer.byteLength(basename) <= 100) {
72
+ return { name: basename, prefix };
73
+ }
74
+ }
75
+ throw new Error(`Docker build context path is too long for tar: ${name}`);
76
+ }
77
+ function makeTarHeader(args) {
78
+ const header = Buffer.alloc(512, 0);
79
+ const split = splitTarPath(args.name);
80
+ writeString(header, split.name, 0, 100);
81
+ writeOctal(header, args.mode, 100, 8);
82
+ writeOctal(header, 0, 108, 8);
83
+ writeOctal(header, 0, 116, 8);
84
+ writeOctal(header, args.size, 124, 12);
85
+ writeOctal(header, Math.floor(args.mtimeMs / 1000), 136, 12);
86
+ header.fill(" ", 148, 156);
87
+ writeString(header, args.type === "directory" ? "5" : args.type === "symlink" ? "2" : "0", 156, 1);
88
+ if (args.linkName) {
89
+ writeString(header, args.linkName, 157, 100);
90
+ }
91
+ writeString(header, "ustar", 257, 6);
92
+ writeString(header, "00", 263, 2);
93
+ if (split.prefix) {
94
+ writeString(header, split.prefix, 345, 155);
95
+ }
96
+ let checksum = 0;
97
+ for (const byte of header) {
98
+ checksum += byte;
99
+ }
100
+ header.write(checksum.toString(8).padStart(6, "0") + "\0 ", 148, 8, "ascii");
101
+ return header;
102
+ }
103
+ async function makeDockerContextTar(contextPath) {
104
+ const fs = await import("node:fs/promises");
105
+ const path = await import("node:path");
106
+ const contextRoot = path.resolve(contextPath);
107
+ const chunks = [];
108
+ async function addEntry(absPath) {
109
+ const rel = path.relative(contextRoot, absPath);
110
+ if (!rel || rel.split(path.sep).includes(".git")) {
111
+ return;
112
+ }
113
+ const tarPath = rel.split(path.sep).join("/");
114
+ const stat = await fs.lstat(absPath);
115
+ if (stat.isDirectory()) {
116
+ chunks.push(makeTarHeader({
117
+ name: tarPath.endsWith("/") ? tarPath : `${tarPath}/`,
118
+ mode: stat.mode & 0o777,
119
+ size: 0,
120
+ type: "directory",
121
+ mtimeMs: stat.mtimeMs,
122
+ }));
123
+ const entries = await fs.readdir(absPath);
124
+ for (const entry of entries.sort()) {
125
+ await addEntry(path.join(absPath, entry));
126
+ }
127
+ return;
128
+ }
129
+ if (stat.isSymbolicLink()) {
130
+ chunks.push(makeTarHeader({
131
+ name: tarPath,
132
+ mode: stat.mode & 0o777,
133
+ size: 0,
134
+ type: "symlink",
135
+ linkName: await fs.readlink(absPath),
136
+ mtimeMs: stat.mtimeMs,
137
+ }));
138
+ return;
139
+ }
140
+ if (!stat.isFile()) {
141
+ return;
142
+ }
143
+ const content = await fs.readFile(absPath);
144
+ chunks.push(makeTarHeader({
145
+ name: tarPath,
146
+ mode: stat.mode & 0o777,
147
+ size: content.byteLength,
148
+ type: "file",
149
+ mtimeMs: stat.mtimeMs,
150
+ }), content);
151
+ const padding = (512 - (content.byteLength % 512)) % 512;
152
+ if (padding) {
153
+ chunks.push(Buffer.alloc(padding, 0));
154
+ }
155
+ }
156
+ const rootEntries = await fs.readdir(contextRoot);
157
+ for (const entry of rootEntries.sort()) {
158
+ await addEntry(path.join(contextRoot, entry));
159
+ }
160
+ chunks.push(Buffer.alloc(1024, 0));
161
+ return new Uint8Array(Buffer.concat(chunks));
162
+ }
163
+ async function resolveDockerfileContext(dockerfile, context) {
164
+ const fs = await import("node:fs/promises");
165
+ const path = await import("node:path");
166
+ const contextPath = path.resolve(context);
167
+ const dockerfilePath = path.resolve(contextPath, dockerfile);
168
+ const contextStat = await fs.stat(contextPath);
169
+ if (!contextStat.isDirectory()) {
170
+ throw new Error(`context must be a directory: ${contextPath}`);
171
+ }
172
+ const dockerfileStat = await fs.stat(dockerfilePath);
173
+ if (!dockerfileStat.isFile()) {
174
+ throw new Error(`dockerfile must be a file: ${dockerfilePath}`);
175
+ }
176
+ const dockerfileRel = path.relative(contextPath, dockerfilePath);
177
+ if (dockerfileRel === "" ||
178
+ dockerfileRel.startsWith("..") ||
179
+ path.isAbsolute(dockerfileRel)) {
180
+ throw new Error("dockerfile must be inside context");
181
+ }
182
+ return {
183
+ contextPath,
184
+ dockerfileRel: dockerfileRel.split(path.sep).join("/"),
185
+ };
186
+ }
187
+ function makeDockerfileBuildCommand(args) {
188
+ const dockerfileRemote = `${args.remoteContext}/${args.dockerfileRel}`;
189
+ const dockerfileDir = dockerfileRemote.split("/").slice(0, -1).join("/");
190
+ const dockerfileName = dockerfileRemote.split("/").at(-1) ?? "Dockerfile";
191
+ const socketPath = `${args.buildkitRun}/buildkitd.sock`;
192
+ const buildctl = [
193
+ "buildctl",
194
+ "--addr",
195
+ `unix://${socketPath}`,
196
+ "build",
197
+ "--progress=plain",
198
+ "--frontend",
199
+ "dockerfile.v0",
200
+ "--local",
201
+ `context=${args.remoteContext}`,
202
+ "--local",
203
+ `dockerfile=${dockerfileDir}`,
204
+ "--opt",
205
+ `filename=${dockerfileName}`,
206
+ "--output",
207
+ `type=docker,name=${args.imageRef}`,
208
+ ];
209
+ if (args.target !== undefined) {
210
+ buildctl.push("--opt", `target=${args.target}`);
211
+ }
212
+ for (const [key, value] of Object.entries(args.buildArgs ?? {}).sort()) {
213
+ buildctl.push("--opt", `build-arg:${key}=${value}`);
214
+ }
215
+ return [
216
+ "set -euo pipefail",
217
+ `mkdir -p ${shellQuote(args.buildkitRoot)} ${shellQuote(args.buildkitRun)}`,
218
+ `buildkitd --addr ${shellQuote(`unix://${socketPath}`)} --root ${shellQuote(args.buildkitRoot)} --oci-worker=true --containerd-worker=false --oci-worker-snapshotter=native --oci-worker-binary buildkit-runc > ${shellQuote(`${args.buildkitRun}/buildkitd.log`)} 2>&1 &`,
219
+ "buildkitd_pid=$!",
220
+ 'cleanup() { kill "$buildkitd_pid" >/dev/null 2>&1 || true; }',
221
+ "trap cleanup EXIT",
222
+ "for i in $(seq 1 300); do",
223
+ ` if buildctl --addr ${shellQuote(`unix://${socketPath}`)} debug workers >/dev/null 2>&1; then break; fi`,
224
+ ` if ! kill -0 "$buildkitd_pid" >/dev/null 2>&1; then cat ${shellQuote(`${args.buildkitRun}/buildkitd.log`)}; exit 1; fi`,
225
+ ` if [ "$i" = 300 ]; then cat ${shellQuote(`${args.buildkitRun}/buildkitd.log`)}; exit 1; fi`,
226
+ " sleep 0.1",
227
+ "done",
228
+ "for i in $(seq 1 300); do",
229
+ " if docker info >/dev/null 2>&1; then break; fi",
230
+ ' if [ "$i" = 300 ]; then docker info; exit 1; fi',
231
+ " sleep 0.1",
232
+ "done",
233
+ `${buildctl.map(shellQuote).join(" ")} | docker load`,
234
+ `rm -rf ${shellQuote(args.buildkitRoot)} || true`,
235
+ "",
236
+ ].join("\n");
237
+ }
50
238
  /**
51
239
  * Client for interacting with the Sandbox Server API.
52
240
  *
@@ -453,6 +641,77 @@ class SandboxClient {
453
641
  const snapshot = (await response.json());
454
642
  return this.waitForSnapshot(snapshot.id, { timeout, signal });
455
643
  }
644
+ /**
645
+ * Build a snapshot from a local Dockerfile context.
646
+ *
647
+ * Creates a temporary builder sandbox, uploads the Docker build context,
648
+ * runs BuildKit inside the sandbox, and captures the built image as a
649
+ * LangSmith snapshot.
650
+ *
651
+ * @param name - Snapshot name.
652
+ * @param dockerfile - Local Dockerfile path, relative to context by default.
653
+ * @param fsCapacityBytes - Filesystem capacity in bytes.
654
+ * @param options - Build context, args, target, build log callback, builder
655
+ * vCPUs/memory, timeout.
656
+ * @returns Snapshot in "ready" status.
657
+ */
658
+ async createSnapshotFromDockerfile(name, dockerfile, fsCapacityBytes, options = {}) {
659
+ const { context = ".", buildArgs, target, onBuildLog, vCpus, memBytes, timeout = 60, } = options;
660
+ const { contextPath, dockerfileRel } = await resolveDockerfileContext(dockerfile, context);
661
+ const builderName = `snapshot-builder-${(0, index_js_1.v4)().replace(/-/g, "").slice(0, 12)}`;
662
+ // Stage the build on the capacity-backed root filesystem, not /tmp.
663
+ // Inside the sandbox /tmp is a RAM-backed tmpfs that fsCapacityBytes does
664
+ // not size, and BuildKit's native snapshotter writes a full copy of every
665
+ // layer under its root, so a /tmp build exhausts guest RAM and fails with
666
+ // "No space left on device".
667
+ const buildRoot = `/var/lib/langsmith-build/${(0, index_js_1.v4)()
668
+ .replace(/-/g, "")
669
+ .slice(0, 12)}`;
670
+ const remoteContext = `${buildRoot}/context`;
671
+ const remoteTar = `${buildRoot}/context.tar`;
672
+ const imageRef = `langsmith-snapshot-build:${(0, index_js_1.v4)().replace(/-/g, "")}`;
673
+ const buildkitRoot = `${buildRoot}/buildkit-root`;
674
+ const buildkitRun = `${buildRoot}/buildkit-run`;
675
+ const builder = await this.createSandbox({
676
+ name: builderName,
677
+ timeout,
678
+ vCpus,
679
+ memBytes,
680
+ fsCapacityBytes,
681
+ });
682
+ try {
683
+ await builder.write(remoteTar, await makeDockerContextTar(contextPath), timeout);
684
+ await builder.run([
685
+ `rm -rf ${shellQuote(remoteContext)}`,
686
+ `mkdir -p ${shellQuote(remoteContext)}`,
687
+ `tar -xf ${shellQuote(remoteTar)} -C ${shellQuote(remoteContext)}`,
688
+ ].join(" && "), { timeout });
689
+ const result = await builder.run(makeDockerfileBuildCommand({
690
+ remoteContext,
691
+ dockerfileRel,
692
+ imageRef,
693
+ buildkitRoot,
694
+ buildkitRun,
695
+ buildArgs,
696
+ target,
697
+ }), {
698
+ timeout,
699
+ onStdout: onBuildLog,
700
+ onStderr: onBuildLog,
701
+ });
702
+ if (result.exit_code !== 0) {
703
+ throw new errors_js_1.LangSmithResourceCreationError("Dockerfile snapshot build failed", "snapshot");
704
+ }
705
+ return await this.captureSnapshot(builder.name, name, {
706
+ dockerImage: imageRef,
707
+ fsCapacityBytes,
708
+ timeout,
709
+ });
710
+ }
711
+ finally {
712
+ await builder.delete();
713
+ }
714
+ }
456
715
  /**
457
716
  * Capture a snapshot from a running sandbox.
458
717
  *
@@ -464,9 +723,15 @@ class SandboxClient {
464
723
  * @returns Snapshot in "ready" status.
465
724
  */
466
725
  async captureSnapshot(sandboxName, name, options = {}) {
467
- const { timeout = 60, signal } = options;
726
+ const { dockerImage, fsCapacityBytes, timeout = 60, signal } = options;
468
727
  const url = `${this._baseUrl}/boxes/${encodeURIComponent(sandboxName)}/snapshot`;
469
728
  const payload = { name };
729
+ if (dockerImage !== undefined) {
730
+ payload.docker_image = dockerImage;
731
+ }
732
+ if (fsCapacityBytes !== undefined) {
733
+ payload.fs_capacity_bytes = fsCapacityBytes;
734
+ }
470
735
  const response = await this._postJson(url, payload, { signal });
471
736
  const snapshot = (await response.json());
472
737
  return this.waitForSnapshot(snapshot.id, { timeout, signal });
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Main SandboxClient class for interacting with the sandbox server API.
3
3
  */
4
- import type { CaptureSnapshotOptions, CreateSandboxOptions, CreateSnapshotOptions, ListSnapshotsOptions, ResourceStatus, SandboxClientConfig, Snapshot, StartSandboxOptions, UpdateSandboxOptions, WaitForSandboxOptions, WaitForSnapshotOptions } from "./types.js";
4
+ import type { CaptureSnapshotOptions, CreateDockerfileSnapshotOptions, CreateSandboxOptions, CreateSnapshotOptions, ListSnapshotsOptions, ResourceStatus, SandboxClientConfig, Snapshot, StartSandboxOptions, UpdateSandboxOptions, WaitForSandboxOptions, WaitForSnapshotOptions } from "./types.js";
5
5
  import { Sandbox } from "./sandbox.js";
6
6
  /**
7
7
  * Client for interacting with the Sandbox Server API.
@@ -173,6 +173,21 @@ export declare class SandboxClient {
173
173
  * @returns Snapshot in "ready" status.
174
174
  */
175
175
  createSnapshot(name: string, dockerImage: string, fsCapacityBytes: number, options?: CreateSnapshotOptions): Promise<Snapshot>;
176
+ /**
177
+ * Build a snapshot from a local Dockerfile context.
178
+ *
179
+ * Creates a temporary builder sandbox, uploads the Docker build context,
180
+ * runs BuildKit inside the sandbox, and captures the built image as a
181
+ * LangSmith snapshot.
182
+ *
183
+ * @param name - Snapshot name.
184
+ * @param dockerfile - Local Dockerfile path, relative to context by default.
185
+ * @param fsCapacityBytes - Filesystem capacity in bytes.
186
+ * @param options - Build context, args, target, build log callback, builder
187
+ * vCPUs/memory, timeout.
188
+ * @returns Snapshot in "ready" status.
189
+ */
190
+ createSnapshotFromDockerfile(name: string, dockerfile: string, fsCapacityBytes: number, options?: CreateDockerfileSnapshotOptions): Promise<Snapshot>;
176
191
  /**
177
192
  * Capture a snapshot from a running sandbox.
178
193
  *
@@ -7,6 +7,7 @@ import { AsyncCaller } from "../utils/async_caller.js";
7
7
  import { Sandbox } from "./sandbox.js";
8
8
  import { LangSmithResourceCreationError, LangSmithResourceNameConflictError, LangSmithResourceNotFoundError, LangSmithResourceTimeoutError, LangSmithSandboxAPIError, LangSmithValidationError, } from "./errors.js";
9
9
  import { handleClientHttpError, handleSandboxCreationError, validateTtl, } from "./helpers.js";
10
+ import { v4 as uuidv4 } from "../utils/uuid/src/index.js";
10
11
  /**
11
12
  * Sleep that can be interrupted by an AbortSignal.
12
13
  * Resolves after `ms` milliseconds or rejects immediately if the signal fires.
@@ -15,17 +16,18 @@ function sleepWithSignal(ms, signal) {
15
16
  if (!signal) {
16
17
  return new Promise((resolve) => setTimeout(resolve, ms));
17
18
  }
18
- signal.throwIfAborted();
19
+ const abortSignal = signal;
20
+ abortSignal.throwIfAborted();
19
21
  return new Promise((resolve, reject) => {
20
22
  const timer = setTimeout(() => {
21
- signal.removeEventListener("abort", onAbort);
23
+ abortSignal.removeEventListener("abort", onAbort);
22
24
  resolve();
23
25
  }, ms);
24
26
  function onAbort() {
25
27
  clearTimeout(timer);
26
- reject(signal.reason);
28
+ reject(abortSignal.reason);
27
29
  }
28
- signal.addEventListener("abort", onAbort, { once: true });
30
+ abortSignal.addEventListener("abort", onAbort, { once: true });
29
31
  });
30
32
  }
31
33
  /**
@@ -44,6 +46,192 @@ function getDefaultApiEndpoint() {
44
46
  function getDefaultApiKey() {
45
47
  return getLangSmithEnvironmentVariable("API_KEY");
46
48
  }
49
+ function shellQuote(value) {
50
+ return `'${value.replace(/'/g, "'\\''")}'`;
51
+ }
52
+ function writeString(header, value, offset, length) {
53
+ header.write(value.slice(0, length), offset, length, "utf8");
54
+ }
55
+ function writeOctal(header, value, offset, length) {
56
+ const octal = value.toString(8).padStart(length - 1, "0");
57
+ header.write(octal.slice(-length + 1) + "\0", offset, length, "ascii");
58
+ }
59
+ function splitTarPath(name) {
60
+ if (Buffer.byteLength(name) <= 100) {
61
+ return { name, prefix: "" };
62
+ }
63
+ const parts = name.split("/");
64
+ for (let i = 1; i < parts.length; i += 1) {
65
+ const prefix = parts.slice(0, i).join("/");
66
+ const basename = parts.slice(i).join("/");
67
+ if (Buffer.byteLength(prefix) <= 155 &&
68
+ Buffer.byteLength(basename) <= 100) {
69
+ return { name: basename, prefix };
70
+ }
71
+ }
72
+ throw new Error(`Docker build context path is too long for tar: ${name}`);
73
+ }
74
+ function makeTarHeader(args) {
75
+ const header = Buffer.alloc(512, 0);
76
+ const split = splitTarPath(args.name);
77
+ writeString(header, split.name, 0, 100);
78
+ writeOctal(header, args.mode, 100, 8);
79
+ writeOctal(header, 0, 108, 8);
80
+ writeOctal(header, 0, 116, 8);
81
+ writeOctal(header, args.size, 124, 12);
82
+ writeOctal(header, Math.floor(args.mtimeMs / 1000), 136, 12);
83
+ header.fill(" ", 148, 156);
84
+ writeString(header, args.type === "directory" ? "5" : args.type === "symlink" ? "2" : "0", 156, 1);
85
+ if (args.linkName) {
86
+ writeString(header, args.linkName, 157, 100);
87
+ }
88
+ writeString(header, "ustar", 257, 6);
89
+ writeString(header, "00", 263, 2);
90
+ if (split.prefix) {
91
+ writeString(header, split.prefix, 345, 155);
92
+ }
93
+ let checksum = 0;
94
+ for (const byte of header) {
95
+ checksum += byte;
96
+ }
97
+ header.write(checksum.toString(8).padStart(6, "0") + "\0 ", 148, 8, "ascii");
98
+ return header;
99
+ }
100
+ async function makeDockerContextTar(contextPath) {
101
+ const fs = await import("node:fs/promises");
102
+ const path = await import("node:path");
103
+ const contextRoot = path.resolve(contextPath);
104
+ const chunks = [];
105
+ async function addEntry(absPath) {
106
+ const rel = path.relative(contextRoot, absPath);
107
+ if (!rel || rel.split(path.sep).includes(".git")) {
108
+ return;
109
+ }
110
+ const tarPath = rel.split(path.sep).join("/");
111
+ const stat = await fs.lstat(absPath);
112
+ if (stat.isDirectory()) {
113
+ chunks.push(makeTarHeader({
114
+ name: tarPath.endsWith("/") ? tarPath : `${tarPath}/`,
115
+ mode: stat.mode & 0o777,
116
+ size: 0,
117
+ type: "directory",
118
+ mtimeMs: stat.mtimeMs,
119
+ }));
120
+ const entries = await fs.readdir(absPath);
121
+ for (const entry of entries.sort()) {
122
+ await addEntry(path.join(absPath, entry));
123
+ }
124
+ return;
125
+ }
126
+ if (stat.isSymbolicLink()) {
127
+ chunks.push(makeTarHeader({
128
+ name: tarPath,
129
+ mode: stat.mode & 0o777,
130
+ size: 0,
131
+ type: "symlink",
132
+ linkName: await fs.readlink(absPath),
133
+ mtimeMs: stat.mtimeMs,
134
+ }));
135
+ return;
136
+ }
137
+ if (!stat.isFile()) {
138
+ return;
139
+ }
140
+ const content = await fs.readFile(absPath);
141
+ chunks.push(makeTarHeader({
142
+ name: tarPath,
143
+ mode: stat.mode & 0o777,
144
+ size: content.byteLength,
145
+ type: "file",
146
+ mtimeMs: stat.mtimeMs,
147
+ }), content);
148
+ const padding = (512 - (content.byteLength % 512)) % 512;
149
+ if (padding) {
150
+ chunks.push(Buffer.alloc(padding, 0));
151
+ }
152
+ }
153
+ const rootEntries = await fs.readdir(contextRoot);
154
+ for (const entry of rootEntries.sort()) {
155
+ await addEntry(path.join(contextRoot, entry));
156
+ }
157
+ chunks.push(Buffer.alloc(1024, 0));
158
+ return new Uint8Array(Buffer.concat(chunks));
159
+ }
160
+ async function resolveDockerfileContext(dockerfile, context) {
161
+ const fs = await import("node:fs/promises");
162
+ const path = await import("node:path");
163
+ const contextPath = path.resolve(context);
164
+ const dockerfilePath = path.resolve(contextPath, dockerfile);
165
+ const contextStat = await fs.stat(contextPath);
166
+ if (!contextStat.isDirectory()) {
167
+ throw new Error(`context must be a directory: ${contextPath}`);
168
+ }
169
+ const dockerfileStat = await fs.stat(dockerfilePath);
170
+ if (!dockerfileStat.isFile()) {
171
+ throw new Error(`dockerfile must be a file: ${dockerfilePath}`);
172
+ }
173
+ const dockerfileRel = path.relative(contextPath, dockerfilePath);
174
+ if (dockerfileRel === "" ||
175
+ dockerfileRel.startsWith("..") ||
176
+ path.isAbsolute(dockerfileRel)) {
177
+ throw new Error("dockerfile must be inside context");
178
+ }
179
+ return {
180
+ contextPath,
181
+ dockerfileRel: dockerfileRel.split(path.sep).join("/"),
182
+ };
183
+ }
184
+ function makeDockerfileBuildCommand(args) {
185
+ const dockerfileRemote = `${args.remoteContext}/${args.dockerfileRel}`;
186
+ const dockerfileDir = dockerfileRemote.split("/").slice(0, -1).join("/");
187
+ const dockerfileName = dockerfileRemote.split("/").at(-1) ?? "Dockerfile";
188
+ const socketPath = `${args.buildkitRun}/buildkitd.sock`;
189
+ const buildctl = [
190
+ "buildctl",
191
+ "--addr",
192
+ `unix://${socketPath}`,
193
+ "build",
194
+ "--progress=plain",
195
+ "--frontend",
196
+ "dockerfile.v0",
197
+ "--local",
198
+ `context=${args.remoteContext}`,
199
+ "--local",
200
+ `dockerfile=${dockerfileDir}`,
201
+ "--opt",
202
+ `filename=${dockerfileName}`,
203
+ "--output",
204
+ `type=docker,name=${args.imageRef}`,
205
+ ];
206
+ if (args.target !== undefined) {
207
+ buildctl.push("--opt", `target=${args.target}`);
208
+ }
209
+ for (const [key, value] of Object.entries(args.buildArgs ?? {}).sort()) {
210
+ buildctl.push("--opt", `build-arg:${key}=${value}`);
211
+ }
212
+ return [
213
+ "set -euo pipefail",
214
+ `mkdir -p ${shellQuote(args.buildkitRoot)} ${shellQuote(args.buildkitRun)}`,
215
+ `buildkitd --addr ${shellQuote(`unix://${socketPath}`)} --root ${shellQuote(args.buildkitRoot)} --oci-worker=true --containerd-worker=false --oci-worker-snapshotter=native --oci-worker-binary buildkit-runc > ${shellQuote(`${args.buildkitRun}/buildkitd.log`)} 2>&1 &`,
216
+ "buildkitd_pid=$!",
217
+ 'cleanup() { kill "$buildkitd_pid" >/dev/null 2>&1 || true; }',
218
+ "trap cleanup EXIT",
219
+ "for i in $(seq 1 300); do",
220
+ ` if buildctl --addr ${shellQuote(`unix://${socketPath}`)} debug workers >/dev/null 2>&1; then break; fi`,
221
+ ` if ! kill -0 "$buildkitd_pid" >/dev/null 2>&1; then cat ${shellQuote(`${args.buildkitRun}/buildkitd.log`)}; exit 1; fi`,
222
+ ` if [ "$i" = 300 ]; then cat ${shellQuote(`${args.buildkitRun}/buildkitd.log`)}; exit 1; fi`,
223
+ " sleep 0.1",
224
+ "done",
225
+ "for i in $(seq 1 300); do",
226
+ " if docker info >/dev/null 2>&1; then break; fi",
227
+ ' if [ "$i" = 300 ]; then docker info; exit 1; fi',
228
+ " sleep 0.1",
229
+ "done",
230
+ `${buildctl.map(shellQuote).join(" ")} | docker load`,
231
+ `rm -rf ${shellQuote(args.buildkitRoot)} || true`,
232
+ "",
233
+ ].join("\n");
234
+ }
47
235
  /**
48
236
  * Client for interacting with the Sandbox Server API.
49
237
  *
@@ -450,6 +638,77 @@ export class SandboxClient {
450
638
  const snapshot = (await response.json());
451
639
  return this.waitForSnapshot(snapshot.id, { timeout, signal });
452
640
  }
641
+ /**
642
+ * Build a snapshot from a local Dockerfile context.
643
+ *
644
+ * Creates a temporary builder sandbox, uploads the Docker build context,
645
+ * runs BuildKit inside the sandbox, and captures the built image as a
646
+ * LangSmith snapshot.
647
+ *
648
+ * @param name - Snapshot name.
649
+ * @param dockerfile - Local Dockerfile path, relative to context by default.
650
+ * @param fsCapacityBytes - Filesystem capacity in bytes.
651
+ * @param options - Build context, args, target, build log callback, builder
652
+ * vCPUs/memory, timeout.
653
+ * @returns Snapshot in "ready" status.
654
+ */
655
+ async createSnapshotFromDockerfile(name, dockerfile, fsCapacityBytes, options = {}) {
656
+ const { context = ".", buildArgs, target, onBuildLog, vCpus, memBytes, timeout = 60, } = options;
657
+ const { contextPath, dockerfileRel } = await resolveDockerfileContext(dockerfile, context);
658
+ const builderName = `snapshot-builder-${uuidv4().replace(/-/g, "").slice(0, 12)}`;
659
+ // Stage the build on the capacity-backed root filesystem, not /tmp.
660
+ // Inside the sandbox /tmp is a RAM-backed tmpfs that fsCapacityBytes does
661
+ // not size, and BuildKit's native snapshotter writes a full copy of every
662
+ // layer under its root, so a /tmp build exhausts guest RAM and fails with
663
+ // "No space left on device".
664
+ const buildRoot = `/var/lib/langsmith-build/${uuidv4()
665
+ .replace(/-/g, "")
666
+ .slice(0, 12)}`;
667
+ const remoteContext = `${buildRoot}/context`;
668
+ const remoteTar = `${buildRoot}/context.tar`;
669
+ const imageRef = `langsmith-snapshot-build:${uuidv4().replace(/-/g, "")}`;
670
+ const buildkitRoot = `${buildRoot}/buildkit-root`;
671
+ const buildkitRun = `${buildRoot}/buildkit-run`;
672
+ const builder = await this.createSandbox({
673
+ name: builderName,
674
+ timeout,
675
+ vCpus,
676
+ memBytes,
677
+ fsCapacityBytes,
678
+ });
679
+ try {
680
+ await builder.write(remoteTar, await makeDockerContextTar(contextPath), timeout);
681
+ await builder.run([
682
+ `rm -rf ${shellQuote(remoteContext)}`,
683
+ `mkdir -p ${shellQuote(remoteContext)}`,
684
+ `tar -xf ${shellQuote(remoteTar)} -C ${shellQuote(remoteContext)}`,
685
+ ].join(" && "), { timeout });
686
+ const result = await builder.run(makeDockerfileBuildCommand({
687
+ remoteContext,
688
+ dockerfileRel,
689
+ imageRef,
690
+ buildkitRoot,
691
+ buildkitRun,
692
+ buildArgs,
693
+ target,
694
+ }), {
695
+ timeout,
696
+ onStdout: onBuildLog,
697
+ onStderr: onBuildLog,
698
+ });
699
+ if (result.exit_code !== 0) {
700
+ throw new LangSmithResourceCreationError("Dockerfile snapshot build failed", "snapshot");
701
+ }
702
+ return await this.captureSnapshot(builder.name, name, {
703
+ dockerImage: imageRef,
704
+ fsCapacityBytes,
705
+ timeout,
706
+ });
707
+ }
708
+ finally {
709
+ await builder.delete();
710
+ }
711
+ }
453
712
  /**
454
713
  * Capture a snapshot from a running sandbox.
455
714
  *
@@ -461,9 +720,15 @@ export class SandboxClient {
461
720
  * @returns Snapshot in "ready" status.
462
721
  */
463
722
  async captureSnapshot(sandboxName, name, options = {}) {
464
- const { timeout = 60, signal } = options;
723
+ const { dockerImage, fsCapacityBytes, timeout = 60, signal } = options;
465
724
  const url = `${this._baseUrl}/boxes/${encodeURIComponent(sandboxName)}/snapshot`;
466
725
  const payload = { name };
726
+ if (dockerImage !== undefined) {
727
+ payload.docker_image = dockerImage;
728
+ }
729
+ if (fsCapacityBytes !== undefined) {
730
+ payload.fs_capacity_bytes = fsCapacityBytes;
731
+ }
467
732
  const response = await this._postJson(url, payload, { signal });
468
733
  const snapshot = (await response.json());
469
734
  return this.waitForSnapshot(snapshot.id, { timeout, signal });
@@ -202,7 +202,7 @@ class CommandHandle {
202
202
  return;
203
203
  }
204
204
  }
205
- this._exhausted = true;
205
+ throw new errors_js_1.LangSmithSandboxConnectionError("Command stream ended without exit message");
206
206
  }
207
207
  /**
208
208
  * Async iterate over output chunks with auto-reconnect on transient errors.
@@ -199,7 +199,7 @@ export class CommandHandle {
199
199
  return;
200
200
  }
201
201
  }
202
- this._exhausted = true;
202
+ throw new LangSmithSandboxConnectionError("Command stream ended without exit message");
203
203
  }
204
204
  /**
205
205
  * Async iterate over output chunks with auto-reconnect on transient errors.
@@ -30,5 +30,5 @@
30
30
  export { SandboxClient } from "./client.js";
31
31
  export { Sandbox } from "./sandbox.js";
32
32
  export { CommandHandle } from "./command_handle.js";
33
- export type { ExecutionResult, OutputChunk, WsMessage, WsRunOptions, ResourceStatus, Snapshot, SandboxData, SandboxClientConfig, RunOptions, CreateSandboxOptions, SandboxAccessControl, SandboxProxyConfig, CreateSnapshotOptions, CaptureSnapshotOptions, ListSnapshotsOptions, WaitForSnapshotOptions, StartSandboxOptions, UpdateSandboxOptions, WaitForSandboxOptions, } from "./types.js";
33
+ export type { ExecutionResult, OutputChunk, WsMessage, WsRunOptions, ResourceStatus, Snapshot, SandboxData, SandboxClientConfig, RunOptions, CreateSandboxOptions, SandboxAccessControl, SandboxProxyConfig, CreateSnapshotOptions, CreateDockerfileSnapshotOptions, CaptureSnapshotOptions, ListSnapshotsOptions, WaitForSnapshotOptions, StartSandboxOptions, UpdateSandboxOptions, WaitForSandboxOptions, } from "./types.js";
34
34
  export { LangSmithSandboxError, LangSmithSandboxAPIError, LangSmithSandboxAuthenticationError, LangSmithSandboxConnectionError, LangSmithSandboxServerReloadError, LangSmithResourceNotFoundError, LangSmithResourceTimeoutError, LangSmithResourceInUseError, LangSmithResourceAlreadyExistsError, LangSmithResourceNameConflictError, LangSmithValidationError, LangSmithQuotaExceededError, LangSmithResourceCreationError, LangSmithSandboxCreationError, LangSmithSandboxNotReadyError, LangSmithSandboxOperationError, LangSmithCommandTimeoutError, LangSmithDataplaneNotConfiguredError, } from "./errors.js";
@@ -333,10 +333,41 @@ export interface CreateSnapshotOptions {
333
333
  /** AbortSignal for cancellation. */
334
334
  signal?: AbortSignal;
335
335
  }
336
+ /**
337
+ * Options for creating a snapshot from a local Dockerfile context.
338
+ */
339
+ export interface CreateDockerfileSnapshotOptions {
340
+ /** Local Docker build context directory. Default: current working directory. */
341
+ context?: string;
342
+ /** Docker build args passed as BuildKit build-arg opts. */
343
+ buildArgs?: Record<string, string>;
344
+ /** Optional Dockerfile target stage. */
345
+ target?: string;
346
+ /** Callback for Docker build stdout/stderr chunks. */
347
+ onBuildLog?: (data: string) => void;
348
+ /**
349
+ * Number of vCPUs for the temporary builder sandbox. The build runs
350
+ * BuildKit plus the native snapshotter's layer copies inside it, which
351
+ * contend for a single core by default, so an extra vCPU can cut a cold
352
+ * build's wall time substantially.
353
+ */
354
+ vCpus?: number;
355
+ /** Memory in bytes for the temporary builder sandbox. */
356
+ memBytes?: number;
357
+ /** Timeout in seconds for builder sandbox operations. Default: 60. */
358
+ timeout?: number;
359
+ }
336
360
  /**
337
361
  * Options for capturing a snapshot from a running sandbox.
338
362
  */
339
363
  export interface CaptureSnapshotOptions {
364
+ /**
365
+ * Docker image tag inside the sandbox to export into the snapshot instead
366
+ * of capturing the live root filesystem.
367
+ */
368
+ dockerImage?: string;
369
+ /** Filesystem capacity in bytes for Docker image export. */
370
+ fsCapacityBytes?: number;
340
371
  /** Timeout in seconds when waiting for ready. Default: 60. */
341
372
  timeout?: number;
342
373
  /** AbortSignal for cancellation. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "langsmith",
3
- "version": "0.7.3",
3
+ "version": "0.7.5",
4
4
  "description": "Client library to connect to the LangSmith Observability and Evaluation Platform.",
5
5
  "packageManager": "pnpm@10.33.0",
6
6
  "files": [
@@ -159,8 +159,8 @@
159
159
  "@ai-sdk/openai": "4.0.0-canary.59",
160
160
  "@ai-sdk/provider": "4.0.0-canary.16",
161
161
  "@ai-sdk/anthropic": "4.0.0-canary.55",
162
- "@anthropic-ai/claude-agent-sdk": "^0.2.83",
163
- "@anthropic-ai/sdk": "^0.95.0",
162
+ "@anthropic-ai/claude-agent-sdk": "^0.3.150",
163
+ "@anthropic-ai/sdk": "^0.98.0",
164
164
  "@babel/preset-env": "^7.22.4",
165
165
  "@faker-js/faker": "^8.4.1",
166
166
  "@google/genai": "^2.0.1",
@@ -169,15 +169,16 @@
169
169
  "@langchain/core": "^0.3.72",
170
170
  "@langchain/langgraph": "^0.3.6",
171
171
  "@langchain/openai": "^0.6.17",
172
- "@openai/agents": "^0.8.3",
172
+ "@openai/agents": "^0.11.5",
173
173
  "@opentelemetry/api": "^1.9.0",
174
- "@opentelemetry/auto-instrumentations-node": "^0.75.0",
174
+ "@opentelemetry/auto-instrumentations-node": "^0.76.0",
175
175
  "@opentelemetry/context-async-hooks": "^2.6.1",
176
- "@opentelemetry/sdk-node": "^0.217.0",
176
+ "@opentelemetry/sdk-node": "^0.218.0",
177
177
  "@opentelemetry/sdk-trace-base": "^2.0.0",
178
178
  "@opentelemetry/sdk-trace-node": "^2.0.0",
179
179
  "@tsconfig/recommended": "^1.0.2",
180
180
  "@types/jest": "^29.5.1",
181
+ "@types/node": "^25.9.1",
181
182
  "@types/node-fetch": "^2.6.12",
182
183
  "@types/semver": "^7.7.1",
183
184
  "@types/ws": "^8.18.1",
@@ -200,7 +201,7 @@
200
201
  "ts-node": "^10.9.1",
201
202
  "typedoc": "^0.28.16",
202
203
  "typedoc-plugin-expand-object-like-types": "^0.1.2",
203
- "typescript": "^5.4.5",
204
+ "typescript": "^6.0.3",
204
205
  "vitest": "^3.1.3",
205
206
  "ws": "^8.19.0",
206
207
  "zod": "^4.3.6"