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 +32 -5
- package/dist/client.d.ts +7 -2
- package/dist/client.js +32 -5
- package/dist/evaluation/evaluate_comparative.cjs +1 -1
- package/dist/evaluation/evaluate_comparative.d.ts +7 -1
- package/dist/evaluation/evaluate_comparative.js +1 -1
- package/dist/index.cjs +4 -2
- package/dist/index.d.ts +2 -1
- package/dist/index.js +3 -1
- package/dist/sandbox/client.cjs +270 -5
- package/dist/sandbox/client.d.ts +16 -1
- package/dist/sandbox/client.js +270 -5
- package/dist/sandbox/command_handle.cjs +1 -1
- package/dist/sandbox/command_handle.js +1 -1
- package/dist/sandbox/index.d.ts +1 -1
- package/dist/sandbox/types.d.ts +31 -0
- package/package.json +8 -7
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
|
|
4406
|
+
* // Create a commit with tags
|
|
4388
4407
|
* const commitUrl2 = await client.createCommit("my-prompt", template, {
|
|
4389
|
-
*
|
|
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
|
-
|
|
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 &&
|
|
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
|
-
|
|
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
|
|
1308
|
+
* // Create a commit with tags
|
|
1306
1309
|
* const commitUrl2 = await client.createCommit("my-prompt", template, {
|
|
1307
|
-
*
|
|
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
|
|
4368
|
+
* // Create a commit with tags
|
|
4350
4369
|
* const commitUrl2 = await client.createCommit("my-prompt", template, {
|
|
4351
|
-
*
|
|
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
|
-
|
|
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 &&
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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";
|
package/dist/sandbox/client.cjs
CHANGED
|
@@ -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
|
|
22
|
+
const abortSignal = signal;
|
|
23
|
+
abortSignal.throwIfAborted();
|
|
22
24
|
return new Promise((resolve, reject) => {
|
|
23
25
|
const timer = setTimeout(() => {
|
|
24
|
-
|
|
26
|
+
abortSignal.removeEventListener("abort", onAbort);
|
|
25
27
|
resolve();
|
|
26
28
|
}, ms);
|
|
27
29
|
function onAbort() {
|
|
28
30
|
clearTimeout(timer);
|
|
29
|
-
reject(
|
|
31
|
+
reject(abortSignal.reason);
|
|
30
32
|
}
|
|
31
|
-
|
|
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 });
|
package/dist/sandbox/client.d.ts
CHANGED
|
@@ -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
|
*
|
package/dist/sandbox/client.js
CHANGED
|
@@ -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
|
|
19
|
+
const abortSignal = signal;
|
|
20
|
+
abortSignal.throwIfAborted();
|
|
19
21
|
return new Promise((resolve, reject) => {
|
|
20
22
|
const timer = setTimeout(() => {
|
|
21
|
-
|
|
23
|
+
abortSignal.removeEventListener("abort", onAbort);
|
|
22
24
|
resolve();
|
|
23
25
|
}, ms);
|
|
24
26
|
function onAbort() {
|
|
25
27
|
clearTimeout(timer);
|
|
26
|
-
reject(
|
|
28
|
+
reject(abortSignal.reason);
|
|
27
29
|
}
|
|
28
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
package/dist/sandbox/index.d.ts
CHANGED
|
@@ -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";
|
package/dist/sandbox/types.d.ts
CHANGED
|
@@ -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
|
+
"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.
|
|
163
|
-
"@anthropic-ai/sdk": "^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.
|
|
172
|
+
"@openai/agents": "^0.11.5",
|
|
173
173
|
"@opentelemetry/api": "^1.9.0",
|
|
174
|
-
"@opentelemetry/auto-instrumentations-node": "^0.
|
|
174
|
+
"@opentelemetry/auto-instrumentations-node": "^0.76.0",
|
|
175
175
|
"@opentelemetry/context-async-hooks": "^2.6.1",
|
|
176
|
-
"@opentelemetry/sdk-node": "^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": "^
|
|
204
|
+
"typescript": "^6.0.3",
|
|
204
205
|
"vitest": "^3.1.3",
|
|
205
206
|
"ws": "^8.19.0",
|
|
206
207
|
"zod": "^4.3.6"
|