langsmith 0.7.2 → 0.7.4

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
@@ -4013,6 +4013,35 @@ class Client {
4013
4013
  const run = await response.json();
4014
4014
  return _normalizeRunTimestamps(run);
4015
4015
  }
4016
+ /**
4017
+ * List the runs in an annotation queue.
4018
+ * @param queueId - The ID of the annotation queue
4019
+ * @param options - The options for listing runs in the annotation queue
4020
+ * @param options.status - Filter runs by review status. If omitted, returns
4021
+ * runs across all review states.
4022
+ * @param options.limit - The maximum number of runs to return
4023
+ * @returns An iterator of RunWithAnnotationQueueInfo objects
4024
+ */
4025
+ async *listRunsFromAnnotationQueue(queueId, options = {}) {
4026
+ const { status, limit: userLimit } = options;
4027
+ const params = new URLSearchParams();
4028
+ const limit = userLimit !== undefined && Number.isFinite(userLimit)
4029
+ ? Math.min(userLimit, 100)
4030
+ : 100;
4031
+ if (status)
4032
+ params.append("status", status);
4033
+ params.append("limit", limit.toString());
4034
+ let count = 0;
4035
+ const path = `/annotation-queues/${(0, _uuid_js_1.assertUuid)(queueId, "queueId")}/runs`;
4036
+ for await (const runs of this._getPaginated(path, params)) {
4037
+ for (const run of runs) {
4038
+ yield _normalizeRunTimestamps(run);
4039
+ count++;
4040
+ if (count >= limit)
4041
+ return;
4042
+ }
4043
+ }
4044
+ }
4016
4045
  /**
4017
4046
  * Delete a run from an an annotation queue.
4018
4047
  * @param queueId - The ID of the annotation queue to delete the run from
@@ -4898,12 +4927,9 @@ class Client {
4898
4927
  });
4899
4928
  const data = (await response.json());
4900
4929
  const commitHash = data.commit.commit_hash;
4901
- let ownerForUrl = owner;
4902
- if (owner === "-") {
4903
- const settings = await this._getSettings();
4904
- ownerForUrl = settings.tenant_handle || owner;
4905
- }
4906
- return `${this.getHostUrl()}/hub/${ownerForUrl}/${name}:${commitHash.slice(0, 8)}`;
4930
+ const settings = await this._getSettings();
4931
+ const query = new URLSearchParams({ organizationId: settings.id });
4932
+ return `${this.getHostUrl()}/context/${name}/${commitHash.slice(0, 8)}?${query.toString()}`;
4907
4933
  }
4908
4934
  async _deleteDirectory(identifier) {
4909
4935
  const [owner, name] = (0, prompts_js_1.parseHubIdentifier)(identifier);
package/dist/client.d.ts CHANGED
@@ -1100,6 +1100,19 @@ export declare class Client implements LangSmithTracingClientInterface {
1100
1100
  * @throws {Error} If the run is not found at the given index or for other API-related errors
1101
1101
  */
1102
1102
  getRunFromAnnotationQueue(queueId: string, index: number): Promise<RunWithAnnotationQueueInfo>;
1103
+ /**
1104
+ * List the runs in an annotation queue.
1105
+ * @param queueId - The ID of the annotation queue
1106
+ * @param options - The options for listing runs in the annotation queue
1107
+ * @param options.status - Filter runs by review status. If omitted, returns
1108
+ * runs across all review states.
1109
+ * @param options.limit - The maximum number of runs to return
1110
+ * @returns An iterator of RunWithAnnotationQueueInfo objects
1111
+ */
1112
+ listRunsFromAnnotationQueue(queueId: string, options?: {
1113
+ status?: "needs_my_review" | "needs_others_review" | "completed";
1114
+ limit?: number;
1115
+ }): AsyncIterableIterator<RunWithAnnotationQueueInfo>;
1103
1116
  /**
1104
1117
  * Delete a run from an an annotation queue.
1105
1118
  * @param queueId - The ID of the annotation queue to delete the run from
package/dist/client.js CHANGED
@@ -3975,6 +3975,35 @@ export class Client {
3975
3975
  const run = await response.json();
3976
3976
  return _normalizeRunTimestamps(run);
3977
3977
  }
3978
+ /**
3979
+ * List the runs in an annotation queue.
3980
+ * @param queueId - The ID of the annotation queue
3981
+ * @param options - The options for listing runs in the annotation queue
3982
+ * @param options.status - Filter runs by review status. If omitted, returns
3983
+ * runs across all review states.
3984
+ * @param options.limit - The maximum number of runs to return
3985
+ * @returns An iterator of RunWithAnnotationQueueInfo objects
3986
+ */
3987
+ async *listRunsFromAnnotationQueue(queueId, options = {}) {
3988
+ const { status, limit: userLimit } = options;
3989
+ const params = new URLSearchParams();
3990
+ const limit = userLimit !== undefined && Number.isFinite(userLimit)
3991
+ ? Math.min(userLimit, 100)
3992
+ : 100;
3993
+ if (status)
3994
+ params.append("status", status);
3995
+ params.append("limit", limit.toString());
3996
+ let count = 0;
3997
+ const path = `/annotation-queues/${assertUuid(queueId, "queueId")}/runs`;
3998
+ for await (const runs of this._getPaginated(path, params)) {
3999
+ for (const run of runs) {
4000
+ yield _normalizeRunTimestamps(run);
4001
+ count++;
4002
+ if (count >= limit)
4003
+ return;
4004
+ }
4005
+ }
4006
+ }
3978
4007
  /**
3979
4008
  * Delete a run from an an annotation queue.
3980
4009
  * @param queueId - The ID of the annotation queue to delete the run from
@@ -4860,12 +4889,9 @@ export class Client {
4860
4889
  });
4861
4890
  const data = (await response.json());
4862
4891
  const commitHash = data.commit.commit_hash;
4863
- let ownerForUrl = owner;
4864
- if (owner === "-") {
4865
- const settings = await this._getSettings();
4866
- ownerForUrl = settings.tenant_handle || owner;
4867
- }
4868
- return `${this.getHostUrl()}/hub/${ownerForUrl}/${name}:${commitHash.slice(0, 8)}`;
4892
+ const settings = await this._getSettings();
4893
+ const query = new URLSearchParams({ organizationId: settings.id });
4894
+ return `${this.getHostUrl()}/context/${name}/${commitHash.slice(0, 8)}?${query.toString()}`;
4869
4895
  }
4870
4896
  async _deleteDirectory(identifier) {
4871
4897
  const [owner, name] = parseHubIdentifier(identifier);
package/dist/index.cjs CHANGED
@@ -18,4 +18,4 @@ 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.2";
21
+ exports.__version__ = "0.7.4";
package/dist/index.d.ts CHANGED
@@ -5,4 +5,4 @@ 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.2";
8
+ export declare const __version__ = "0.7.4";
package/dist/index.js CHANGED
@@ -5,4 +5,4 @@ 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.2";
8
+ export const __version__ = "0.7.4";
@@ -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/dist/schemas.d.ts CHANGED
@@ -543,7 +543,9 @@ export interface FeedbackConfigSchema {
543
543
  /** Whether a lower score is considered better for this feedback key. */
544
544
  is_lower_score_better?: boolean | null;
545
545
  }
546
- export interface RunWithAnnotationQueueInfo extends BaseRun {
546
+ export interface RunWithAnnotationQueueInfo extends Exclude<BaseRun, "id"> {
547
+ /** An unique identifier for the run. */
548
+ id: string;
547
549
  /** The last time this run was reviewed. */
548
550
  last_reviewed_time?: string;
549
551
  /** The time this run was added to the queue. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "langsmith",
3
- "version": "0.7.2",
3
+ "version": "0.7.4",
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"