langsmith 0.10.4 → 0.10.6
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/address.cjs +119 -0
- package/dist/address.d.ts +13 -0
- package/dist/address.js +107 -0
- package/dist/client.cjs +68 -22
- package/dist/client.d.ts +13 -0
- package/dist/client.js +68 -22
- package/dist/evaluation/_runner.cjs +47 -5
- package/dist/evaluation/_runner.d.ts +12 -0
- package/dist/evaluation/_runner.js +47 -5
- package/dist/experimental/otel/constants.cjs +5 -1
- package/dist/experimental/otel/constants.d.ts +3 -0
- package/dist/experimental/otel/constants.js +3 -0
- package/dist/experimental/otel/translator.cjs +14 -0
- package/dist/experimental/otel/translator.js +14 -0
- package/dist/index.cjs +4 -2
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/run_trees.cjs +154 -24
- package/dist/run_trees.d.ts +13 -1
- package/dist/run_trees.js +154 -24
- package/dist/sandbox/access_delegation.cjs +24 -0
- package/dist/sandbox/access_delegation.d.ts +15 -0
- package/dist/sandbox/access_delegation.js +21 -0
- package/dist/sandbox/client.cjs +70 -5
- package/dist/sandbox/client.d.ts +18 -0
- package/dist/sandbox/client.js +70 -5
- package/dist/sandbox/command_handle.cjs +34 -0
- package/dist/sandbox/command_handle.d.ts +10 -0
- package/dist/sandbox/command_handle.js +34 -0
- package/dist/sandbox/errors.cjs +12 -1
- package/dist/sandbox/errors.d.ts +7 -0
- package/dist/sandbox/errors.js +10 -0
- package/dist/sandbox/helpers.cjs +129 -0
- package/dist/sandbox/helpers.d.ts +29 -0
- package/dist/sandbox/helpers.js +123 -0
- package/dist/sandbox/index.cjs +12 -1
- package/dist/sandbox/index.d.ts +8 -2
- package/dist/sandbox/index.js +4 -1
- package/dist/sandbox/mounts.cjs +21 -3
- package/dist/sandbox/mounts.d.ts +7 -1
- package/dist/sandbox/mounts.js +21 -3
- package/dist/sandbox/proxy_config.cjs +41 -10
- package/dist/sandbox/proxy_config.d.ts +33 -6
- package/dist/sandbox/proxy_config.js +40 -10
- package/dist/sandbox/sandbox.cjs +160 -4
- package/dist/sandbox/sandbox.d.ts +71 -1
- package/dist/sandbox/sandbox.js +161 -5
- package/dist/sandbox/service_url.cjs +125 -0
- package/dist/sandbox/service_url.d.ts +50 -0
- package/dist/sandbox/service_url.js +120 -0
- package/dist/sandbox/types.d.ts +251 -8
- package/dist/sandbox/verify.cjs +300 -0
- package/dist/sandbox/verify.d.ts +122 -0
- package/dist/sandbox/verify.js +296 -0
- package/dist/sandbox/ws_execute.cjs +31 -1
- package/dist/sandbox/ws_execute.d.ts +9 -0
- package/dist/sandbox/ws_execute.js +31 -1
- package/dist/schemas.d.ts +5 -0
- package/dist/traceable.cjs +7 -1
- package/dist/traceable.js +7 -1
- package/dist/utils/agent_addressing.cjs +92 -0
- package/dist/utils/agent_addressing.d.ts +30 -0
- package/dist/utils/agent_addressing.js +83 -0
- package/dist/utils/env.cjs +2 -0
- package/dist/utils/env.js +2 -0
- package/dist/utils/error.cjs +17 -22
- package/dist/utils/error.js +17 -22
- package/package.json +5 -5
package/dist/run_trees.js
CHANGED
|
@@ -4,11 +4,12 @@ import { isConflictingEndpointsError, ConflictingEndpointsError, } from "./utils
|
|
|
4
4
|
import { _LC_CONTEXT_VARIABLES_KEY, _REPLICA_TRACE_ROOTS_KEY, } from "./singletons/constants.js";
|
|
5
5
|
import { getContextVar, setContextVar } from "./utils/context_vars.js";
|
|
6
6
|
import { getEnvironmentVariable, getRuntimeEnvironment, } from "./utils/env.js";
|
|
7
|
-
import { getDefaultProjectName } from "./utils/project.js";
|
|
8
7
|
import { getLangSmithEnvironmentVariable } from "./utils/env.js";
|
|
9
8
|
import { warnOnce } from "./utils/warn.js";
|
|
10
9
|
import { uuid7FromTime, nonCryptographicUuid7Deterministic, } from "./utils/_uuid.js";
|
|
11
10
|
import { v5 as uuidv5 } from "./utils/uuid/src/index.js";
|
|
11
|
+
import { checkAddress, logUntraced, rejectConflicting, resolveFromEnv, } from "./utils/agent_addressing.js";
|
|
12
|
+
import { EnvAddressError, fromWire, sameAddress, seed as addressSeed, toWire, wireKeys, } from "./address.js";
|
|
12
13
|
const TIMESTAMP_LENGTH = 36;
|
|
13
14
|
// DNS namespace for UUID v5 (same as Python's uuid.NAMESPACE_DNS)
|
|
14
15
|
const UUID_NAMESPACE_DNS = "6ba7b810-9dad-11d1-80b4-00c04fd430c8";
|
|
@@ -43,7 +44,31 @@ const HEADER_SAFE_REPLICA_FIELDS = new Set([
|
|
|
43
44
|
"primary",
|
|
44
45
|
"updates",
|
|
45
46
|
"reroot",
|
|
47
|
+
...wireKeys(),
|
|
46
48
|
]);
|
|
49
|
+
// URL-encoded JSON of the address's wire fields.
|
|
50
|
+
const LANGSMITH_ADDRESS = "langsmith-address";
|
|
51
|
+
/** Pops the address wire keys off untrusted `values`; never throws. */
|
|
52
|
+
function takeHeaderAddress(values) {
|
|
53
|
+
const wire = {};
|
|
54
|
+
for (const key of wireKeys()) {
|
|
55
|
+
if (key in values) {
|
|
56
|
+
wire[key] = values[key];
|
|
57
|
+
delete values[key];
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
try {
|
|
61
|
+
return {
|
|
62
|
+
named: Object.keys(wire).length > 0,
|
|
63
|
+
address: fromWire(wire),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
// Values are untrusted, so not logged.
|
|
68
|
+
console.warn("Ignoring an invalid address in a `baggage` header.");
|
|
69
|
+
return { named: true };
|
|
70
|
+
}
|
|
71
|
+
}
|
|
47
72
|
function filterReplicaForHeaders(replica) {
|
|
48
73
|
const filtered = {};
|
|
49
74
|
for (const key of Object.keys(replica)) {
|
|
@@ -60,7 +85,7 @@ function filterReplicaForHeaders(replica) {
|
|
|
60
85
|
* Baggage header information
|
|
61
86
|
*/
|
|
62
87
|
class Baggage {
|
|
63
|
-
constructor(metadata, tags, project_name, replicas) {
|
|
88
|
+
constructor(metadata, tags, project_name, replicas, address) {
|
|
64
89
|
Object.defineProperty(this, "metadata", {
|
|
65
90
|
enumerable: true,
|
|
66
91
|
configurable: true,
|
|
@@ -85,10 +110,17 @@ class Baggage {
|
|
|
85
110
|
writable: true,
|
|
86
111
|
value: void 0
|
|
87
112
|
});
|
|
113
|
+
Object.defineProperty(this, "address", {
|
|
114
|
+
enumerable: true,
|
|
115
|
+
configurable: true,
|
|
116
|
+
writable: true,
|
|
117
|
+
value: void 0
|
|
118
|
+
});
|
|
88
119
|
this.metadata = metadata;
|
|
89
120
|
this.tags = tags;
|
|
90
121
|
this.project_name = project_name;
|
|
91
122
|
this.replicas = replicas;
|
|
123
|
+
this.address = address;
|
|
92
124
|
}
|
|
93
125
|
static fromHeader(value) {
|
|
94
126
|
const items = value.split(",");
|
|
@@ -96,6 +128,7 @@ class Baggage {
|
|
|
96
128
|
let tags = [];
|
|
97
129
|
let project_name;
|
|
98
130
|
let replicas;
|
|
131
|
+
let address;
|
|
99
132
|
for (const item of items) {
|
|
100
133
|
const [key, uriValue] = item.split("=");
|
|
101
134
|
const value = decodeURIComponent(uriValue);
|
|
@@ -108,17 +141,36 @@ class Baggage {
|
|
|
108
141
|
else if (key === "langsmith-project") {
|
|
109
142
|
project_name = value;
|
|
110
143
|
}
|
|
144
|
+
else if (key === LANGSMITH_ADDRESS) {
|
|
145
|
+
let parsed;
|
|
146
|
+
try {
|
|
147
|
+
parsed = JSON.parse(value);
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
parsed = undefined;
|
|
151
|
+
}
|
|
152
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
153
|
+
address = takeHeaderAddress({ ...parsed }).address;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
111
156
|
else if (key === "langsmith-replicas") {
|
|
112
157
|
const parsed = JSON.parse(value);
|
|
113
|
-
replicas = parsed.
|
|
158
|
+
replicas = parsed.flatMap((replica) => {
|
|
114
159
|
if (Array.isArray(replica)) {
|
|
115
|
-
return replica;
|
|
160
|
+
return [replica];
|
|
116
161
|
}
|
|
117
|
-
|
|
162
|
+
const filtered = filterReplicaForHeaders(replica);
|
|
163
|
+
const { named, address } = takeHeaderAddress(filtered);
|
|
164
|
+
// A project wins over an address; a replica with an invalid
|
|
165
|
+
// address and no project is dropped.
|
|
166
|
+
if (filtered.projectName || !named) {
|
|
167
|
+
return [filtered];
|
|
168
|
+
}
|
|
169
|
+
return address ? [{ ...filtered, address }] : [];
|
|
118
170
|
});
|
|
119
171
|
}
|
|
120
172
|
}
|
|
121
|
-
return new Baggage(metadata, tags, project_name, replicas);
|
|
173
|
+
return new Baggage(metadata, tags, project_name, replicas, address);
|
|
122
174
|
}
|
|
123
175
|
toHeader() {
|
|
124
176
|
const items = [];
|
|
@@ -131,6 +183,9 @@ class Baggage {
|
|
|
131
183
|
if (this.project_name) {
|
|
132
184
|
items.push(`langsmith-project=${encodeURIComponent(this.project_name)}`);
|
|
133
185
|
}
|
|
186
|
+
if (this.address) {
|
|
187
|
+
items.push(`${LANGSMITH_ADDRESS}=${encodeURIComponent(JSON.stringify(toWire(this.address)))}`);
|
|
188
|
+
}
|
|
134
189
|
return items.join(",");
|
|
135
190
|
}
|
|
136
191
|
}
|
|
@@ -158,12 +213,20 @@ export class RunTree {
|
|
|
158
213
|
writable: true,
|
|
159
214
|
value: void 0
|
|
160
215
|
});
|
|
216
|
+
/** Unset for a run sent to an `address`. */
|
|
161
217
|
Object.defineProperty(this, "project_name", {
|
|
162
218
|
enumerable: true,
|
|
163
219
|
configurable: true,
|
|
164
220
|
writable: true,
|
|
165
221
|
value: void 0
|
|
166
222
|
});
|
|
223
|
+
/** (beta) Set instead of `project_name` for an addressed run. */
|
|
224
|
+
Object.defineProperty(this, "address", {
|
|
225
|
+
enumerable: true,
|
|
226
|
+
configurable: true,
|
|
227
|
+
writable: true,
|
|
228
|
+
value: void 0
|
|
229
|
+
});
|
|
167
230
|
Object.defineProperty(this, "parent_run", {
|
|
168
231
|
enumerable: true,
|
|
169
232
|
configurable: true,
|
|
@@ -338,6 +401,7 @@ export class RunTree {
|
|
|
338
401
|
delete config.id;
|
|
339
402
|
}
|
|
340
403
|
Object.assign(this, { ...defaultConfig, ...config, client });
|
|
404
|
+
this._resolveAddressing(config);
|
|
341
405
|
this.execution_order ??= 1;
|
|
342
406
|
this.child_execution_order ??= 1;
|
|
343
407
|
// Generate serialized start time for ID generation
|
|
@@ -368,6 +432,31 @@ export class RunTree {
|
|
|
368
432
|
}
|
|
369
433
|
}
|
|
370
434
|
}
|
|
435
|
+
/** Falls back to the env; a bad env leaves the run untraced. */
|
|
436
|
+
_resolveAddressing(config) {
|
|
437
|
+
const address = checkAddress(config.address);
|
|
438
|
+
const project = config.project_name || undefined;
|
|
439
|
+
rejectConflicting(project, address);
|
|
440
|
+
if (project || address) {
|
|
441
|
+
this.project_name = project;
|
|
442
|
+
this.address = address;
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
try {
|
|
446
|
+
[this.project_name, this.address] = resolveFromEnv();
|
|
447
|
+
}
|
|
448
|
+
catch (e) {
|
|
449
|
+
if (!(e instanceof EnvAddressError)) {
|
|
450
|
+
throw e;
|
|
451
|
+
}
|
|
452
|
+
if (isEnvTracingEnabled(this.tracingEnabled)) {
|
|
453
|
+
logUntraced(e);
|
|
454
|
+
}
|
|
455
|
+
this.project_name = undefined;
|
|
456
|
+
this.address = undefined;
|
|
457
|
+
this.tracingEnabled = false;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
371
460
|
set metadata(metadata) {
|
|
372
461
|
this.extra = {
|
|
373
462
|
...this.extra,
|
|
@@ -384,7 +473,6 @@ export class RunTree {
|
|
|
384
473
|
const start_time = Date.now();
|
|
385
474
|
return {
|
|
386
475
|
run_type: "chain",
|
|
387
|
-
project_name: getDefaultProjectName(),
|
|
388
476
|
child_runs: [],
|
|
389
477
|
api_url: getEnvironmentVariable("LANGCHAIN_ENDPOINT") ?? "http://localhost:1984",
|
|
390
478
|
api_key: getEnvironmentVariable("LANGCHAIN_API_KEY"),
|
|
@@ -414,6 +502,7 @@ export class RunTree {
|
|
|
414
502
|
...config,
|
|
415
503
|
parent_run: this,
|
|
416
504
|
project_name: this.project_name,
|
|
505
|
+
address: this.address,
|
|
417
506
|
replicas: childReplicas,
|
|
418
507
|
client: this.client,
|
|
419
508
|
tracingEnabled: this.tracingEnabled,
|
|
@@ -512,6 +601,7 @@ export class RunTree {
|
|
|
512
601
|
inputs: run.inputs,
|
|
513
602
|
outputs: run.outputs,
|
|
514
603
|
session_name: run.project_name,
|
|
604
|
+
address: run.address,
|
|
515
605
|
child_runs: child_runs,
|
|
516
606
|
parent_run_id: parent_run_id,
|
|
517
607
|
trace_id: run.trace_id,
|
|
@@ -568,16 +658,33 @@ export class RunTree {
|
|
|
568
658
|
child._setReplicaTraceRoot(replicaKey, traceRootId);
|
|
569
659
|
}
|
|
570
660
|
}
|
|
661
|
+
/** A replica naming neither inherits the run tree's destination. */
|
|
662
|
+
_replicaAddressing(replica) {
|
|
663
|
+
const address = checkAddress(replica.address);
|
|
664
|
+
if (replica.projectName != null) {
|
|
665
|
+
rejectConflicting(replica.projectName, address);
|
|
666
|
+
return [replica.projectName, undefined];
|
|
667
|
+
}
|
|
668
|
+
if (address) {
|
|
669
|
+
return [undefined, address];
|
|
670
|
+
}
|
|
671
|
+
return [this.project_name, this.address];
|
|
672
|
+
}
|
|
571
673
|
_remapForProject(params) {
|
|
572
|
-
const { projectName, primary, runtimeEnv, excludeChildRuns = true, reroot = false, distributedParentId, apiUrl, apiKey, workspaceId, } = params;
|
|
674
|
+
const { projectName, address, primary, runtimeEnv, excludeChildRuns = true, reroot = false, distributedParentId, apiUrl, apiKey, workspaceId, } = params;
|
|
573
675
|
const baseRun = this._convertToCreate(this, runtimeEnv, excludeChildRuns);
|
|
574
|
-
// Preserve legacy behavior when `primary` is omitted.
|
|
575
|
-
if (primary === undefined &&
|
|
676
|
+
// Preserve legacy behavior when `primary` is omitted, unless another replica is primary.
|
|
677
|
+
if (primary === undefined &&
|
|
678
|
+
!this.replicas?.some((r) => r.primary === true) &&
|
|
679
|
+
projectName === this.project_name &&
|
|
680
|
+
sameAddress(address, this.address)) {
|
|
576
681
|
return {
|
|
577
682
|
...baseRun,
|
|
578
683
|
session_name: projectName,
|
|
684
|
+
address,
|
|
579
685
|
};
|
|
580
686
|
}
|
|
687
|
+
const seed = projectName ?? (address ? addressSeed(address) : undefined) ?? "agent//";
|
|
581
688
|
// Apply reroot logic before ID remapping
|
|
582
689
|
if (reroot) {
|
|
583
690
|
if (distributedParentId) {
|
|
@@ -601,7 +708,7 @@ export class RunTree {
|
|
|
601
708
|
// Store this run's original ID in context vars so descendants know the new trace root
|
|
602
709
|
// We store the original ID (before remapping) so it can be found in dotted_order
|
|
603
710
|
const replicaKey = getReplicaKey({
|
|
604
|
-
projectName,
|
|
711
|
+
projectName: seed,
|
|
605
712
|
apiUrl,
|
|
606
713
|
apiKey,
|
|
607
714
|
workspaceId,
|
|
@@ -614,7 +721,7 @@ export class RunTree {
|
|
|
614
721
|
if (!reroot) {
|
|
615
722
|
const replicaTraceRoots = getContextVar(this, _REPLICA_TRACE_ROOTS_KEY) ?? {};
|
|
616
723
|
const replicaKey = getReplicaKey({
|
|
617
|
-
projectName,
|
|
724
|
+
projectName: seed,
|
|
618
725
|
apiUrl,
|
|
619
726
|
apiKey,
|
|
620
727
|
workspaceId,
|
|
@@ -649,17 +756,18 @@ export class RunTree {
|
|
|
649
756
|
return {
|
|
650
757
|
...baseRun,
|
|
651
758
|
session_name: projectName,
|
|
759
|
+
address,
|
|
652
760
|
};
|
|
653
761
|
}
|
|
654
762
|
// Remap IDs for the replica using nonCryptographicUuid7Deterministic
|
|
655
763
|
// This ensures consistency across runs in the same replica while
|
|
656
764
|
// preserving UUID7 properties (time-ordering, monotonicity)
|
|
657
765
|
const oldId = baseRun.id;
|
|
658
|
-
const newId = nonCryptographicUuid7Deterministic(oldId,
|
|
766
|
+
const newId = nonCryptographicUuid7Deterministic(oldId, seed);
|
|
659
767
|
// Remap trace_id
|
|
660
768
|
let newTraceId;
|
|
661
769
|
if (baseRun.trace_id) {
|
|
662
|
-
newTraceId = nonCryptographicUuid7Deterministic(baseRun.trace_id,
|
|
770
|
+
newTraceId = nonCryptographicUuid7Deterministic(baseRun.trace_id, seed);
|
|
663
771
|
}
|
|
664
772
|
else {
|
|
665
773
|
newTraceId = newId;
|
|
@@ -667,7 +775,7 @@ export class RunTree {
|
|
|
667
775
|
// Remap parent_run_id
|
|
668
776
|
let newParentId;
|
|
669
777
|
if (baseRun.parent_run_id) {
|
|
670
|
-
newParentId = nonCryptographicUuid7Deterministic(baseRun.parent_run_id,
|
|
778
|
+
newParentId = nonCryptographicUuid7Deterministic(baseRun.parent_run_id, seed);
|
|
671
779
|
}
|
|
672
780
|
// Remap dotted_order segments
|
|
673
781
|
let newDottedOrder;
|
|
@@ -676,7 +784,7 @@ export class RunTree {
|
|
|
676
784
|
const remappedSegs = segs.map((seg) => {
|
|
677
785
|
// Extract the UUID from the segment (last TIMESTAMP_LENGTH characters)
|
|
678
786
|
const segId = seg.slice(-TIMESTAMP_LENGTH);
|
|
679
|
-
const remappedId = nonCryptographicUuid7Deterministic(segId,
|
|
787
|
+
const remappedId = nonCryptographicUuid7Deterministic(segId, seed);
|
|
680
788
|
// Replace the UUID part while keeping the timestamp prefix
|
|
681
789
|
return seg.slice(0, -TIMESTAMP_LENGTH) + remappedId;
|
|
682
790
|
});
|
|
@@ -689,6 +797,7 @@ export class RunTree {
|
|
|
689
797
|
parent_run_id: newParentId,
|
|
690
798
|
dotted_order: newDottedOrder,
|
|
691
799
|
session_name: projectName,
|
|
800
|
+
address,
|
|
692
801
|
};
|
|
693
802
|
}
|
|
694
803
|
async postRun(excludeChildRuns = true) {
|
|
@@ -699,9 +808,12 @@ export class RunTree {
|
|
|
699
808
|
try {
|
|
700
809
|
const runtimeEnv = getRuntimeEnvironment();
|
|
701
810
|
if (this.replicas && this.replicas.length > 0) {
|
|
702
|
-
for (const
|
|
811
|
+
for (const replica of this.replicas) {
|
|
812
|
+
const { primary, apiKey, apiUrl, workspaceId, reroot, client: replicaClient, } = replica;
|
|
813
|
+
const [projectName, address] = this._replicaAddressing(replica);
|
|
703
814
|
const runCreate = this._remapForProject({
|
|
704
|
-
projectName
|
|
815
|
+
projectName,
|
|
816
|
+
address,
|
|
705
817
|
primary,
|
|
706
818
|
runtimeEnv,
|
|
707
819
|
excludeChildRuns: true,
|
|
@@ -750,9 +862,12 @@ export class RunTree {
|
|
|
750
862
|
async patchRun(options) {
|
|
751
863
|
const excludeInputs = options?.excludeInputs ?? getExcludeInputsOnPatch();
|
|
752
864
|
if (this.replicas && this.replicas.length > 0) {
|
|
753
|
-
for (const
|
|
865
|
+
for (const replica of this.replicas) {
|
|
866
|
+
const { primary, apiKey, apiUrl, workspaceId, updates, reroot, client: replicaClient, } = replica;
|
|
867
|
+
const [projectName, address] = this._replicaAddressing(replica);
|
|
754
868
|
const runData = this._remapForProject({
|
|
755
|
-
projectName
|
|
869
|
+
projectName,
|
|
870
|
+
address,
|
|
756
871
|
primary,
|
|
757
872
|
runtimeEnv: undefined,
|
|
758
873
|
excludeChildRuns: true,
|
|
@@ -771,6 +886,7 @@ export class RunTree {
|
|
|
771
886
|
error: runData.error,
|
|
772
887
|
parent_run_id: runData.parent_run_id,
|
|
773
888
|
session_name: runData.session_name,
|
|
889
|
+
address: runData.address,
|
|
774
890
|
reference_example_id: runData.reference_example_id,
|
|
775
891
|
end_time: runData.end_time,
|
|
776
892
|
dotted_order: runData.dotted_order,
|
|
@@ -813,6 +929,7 @@ export class RunTree {
|
|
|
813
929
|
tags: this.tags,
|
|
814
930
|
attachments: this.attachments,
|
|
815
931
|
session_name: this.project_name,
|
|
932
|
+
address: this.address,
|
|
816
933
|
};
|
|
817
934
|
// Important that inputs is not a key in the run update
|
|
818
935
|
// if excluded because it will overwrite the run create if the
|
|
@@ -873,7 +990,7 @@ export class RunTree {
|
|
|
873
990
|
...props,
|
|
874
991
|
client,
|
|
875
992
|
tracingEnabled,
|
|
876
|
-
project_name: projectName,
|
|
993
|
+
project_name: props.address ? undefined : projectName,
|
|
877
994
|
});
|
|
878
995
|
}
|
|
879
996
|
const parentRunTree = new RunTree({
|
|
@@ -928,7 +1045,19 @@ export class RunTree {
|
|
|
928
1045
|
const baggage = Baggage.fromHeader(rawHeaders["baggage"]);
|
|
929
1046
|
config.metadata = baggage.metadata;
|
|
930
1047
|
config.tags = baggage.tags;
|
|
931
|
-
|
|
1048
|
+
if (baggage.project_name && baggage.address) {
|
|
1049
|
+
console.warn("Ignoring a `baggage` header that names both a project and an address.");
|
|
1050
|
+
return undefined;
|
|
1051
|
+
}
|
|
1052
|
+
// The header's destination outranks the caller's.
|
|
1053
|
+
if (baggage.project_name) {
|
|
1054
|
+
config.project_name = baggage.project_name;
|
|
1055
|
+
delete config.address;
|
|
1056
|
+
}
|
|
1057
|
+
else if (baggage.address) {
|
|
1058
|
+
config.address = baggage.address;
|
|
1059
|
+
delete config.project_name;
|
|
1060
|
+
}
|
|
932
1061
|
config.replicas = baggage.replicas;
|
|
933
1062
|
}
|
|
934
1063
|
const runTree = new RunTree(config);
|
|
@@ -939,7 +1068,7 @@ export class RunTree {
|
|
|
939
1068
|
toHeaders(headers) {
|
|
940
1069
|
const result = {
|
|
941
1070
|
"langsmith-trace": this.dotted_order,
|
|
942
|
-
baggage: new Baggage(this.extra?.metadata, this.tags, this.project_name, this.replicas).toHeader(),
|
|
1071
|
+
baggage: new Baggage(this.extra?.metadata, this.tags, this.project_name, this.replicas, this.address).toHeader(),
|
|
943
1072
|
};
|
|
944
1073
|
if (headers) {
|
|
945
1074
|
for (const [key, value] of Object.entries(result)) {
|
|
@@ -1075,7 +1204,8 @@ function _ensureWriteReplicas(replicas) {
|
|
|
1075
1204
|
updates: replica[1],
|
|
1076
1205
|
};
|
|
1077
1206
|
}
|
|
1078
|
-
|
|
1207
|
+
const address = checkAddress(replica.address);
|
|
1208
|
+
return address ? { ...replica, address } : replica;
|
|
1079
1209
|
})
|
|
1080
1210
|
: _getWriteReplicasFromEnv();
|
|
1081
1211
|
if (ensured.filter((replica) => replica.primary === true).length > 1) {
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.validateAccessDelegation = validateAccessDelegation;
|
|
4
|
+
const errors_js_1 = require("./errors.cjs");
|
|
5
|
+
/** Rejects grants the server would refuse, without the round trip. */
|
|
6
|
+
function validateAccessDelegation(value) {
|
|
7
|
+
if (value.mode !== "INHERIT" && value.mode !== "EXPLICIT") {
|
|
8
|
+
throw new errors_js_1.LangSmithValidationError('accessDelegation.mode must be "INHERIT" or "EXPLICIT"', "accessDelegation");
|
|
9
|
+
}
|
|
10
|
+
const { permissions } = value;
|
|
11
|
+
if (permissions !== undefined && !Array.isArray(permissions)) {
|
|
12
|
+
throw new errors_js_1.LangSmithValidationError("accessDelegation.permissions must be an array of strings", "accessDelegation");
|
|
13
|
+
}
|
|
14
|
+
if (value.mode === "INHERIT") {
|
|
15
|
+
if (permissions !== undefined && permissions.length > 0) {
|
|
16
|
+
throw new errors_js_1.LangSmithValidationError('accessDelegation.permissions is not allowed with mode "INHERIT"', "accessDelegation");
|
|
17
|
+
}
|
|
18
|
+
return { mode: "INHERIT" };
|
|
19
|
+
}
|
|
20
|
+
if (permissions === undefined || permissions.length === 0) {
|
|
21
|
+
throw new errors_js_1.LangSmithValidationError('accessDelegation.permissions is required with mode "EXPLICIT"', "accessDelegation");
|
|
22
|
+
}
|
|
23
|
+
return { mode: "EXPLICIT", permissions: [...permissions] };
|
|
24
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** How a delegation grant derives its permission set. */
|
|
2
|
+
export type AccessDelegationMode = "INHERIT" | "EXPLICIT";
|
|
3
|
+
/**
|
|
4
|
+
* Grant letting code inside a sandbox call the LangSmith API as the creator.
|
|
5
|
+
*
|
|
6
|
+
* `INHERIT` tracks everything the creator can do; `EXPLICIT` is capped to
|
|
7
|
+
* `permissions`. Permissions are a ceiling re-checked on every request rather
|
|
8
|
+
* than a snapshot, so access the creator loses is lost here too.
|
|
9
|
+
*/
|
|
10
|
+
export interface AccessDelegation {
|
|
11
|
+
mode: AccessDelegationMode;
|
|
12
|
+
permissions?: string[];
|
|
13
|
+
}
|
|
14
|
+
/** Rejects grants the server would refuse, without the round trip. */
|
|
15
|
+
export declare function validateAccessDelegation(value: AccessDelegation): AccessDelegation;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { LangSmithValidationError } from "./errors.js";
|
|
2
|
+
/** Rejects grants the server would refuse, without the round trip. */
|
|
3
|
+
export function validateAccessDelegation(value) {
|
|
4
|
+
if (value.mode !== "INHERIT" && value.mode !== "EXPLICIT") {
|
|
5
|
+
throw new LangSmithValidationError('accessDelegation.mode must be "INHERIT" or "EXPLICIT"', "accessDelegation");
|
|
6
|
+
}
|
|
7
|
+
const { permissions } = value;
|
|
8
|
+
if (permissions !== undefined && !Array.isArray(permissions)) {
|
|
9
|
+
throw new LangSmithValidationError("accessDelegation.permissions must be an array of strings", "accessDelegation");
|
|
10
|
+
}
|
|
11
|
+
if (value.mode === "INHERIT") {
|
|
12
|
+
if (permissions !== undefined && permissions.length > 0) {
|
|
13
|
+
throw new LangSmithValidationError('accessDelegation.permissions is not allowed with mode "INHERIT"', "accessDelegation");
|
|
14
|
+
}
|
|
15
|
+
return { mode: "INHERIT" };
|
|
16
|
+
}
|
|
17
|
+
if (permissions === undefined || permissions.length === 0) {
|
|
18
|
+
throw new LangSmithValidationError('accessDelegation.permissions is required with mode "EXPLICIT"', "accessDelegation");
|
|
19
|
+
}
|
|
20
|
+
return { mode: "EXPLICIT", permissions: [...permissions] };
|
|
21
|
+
}
|
package/dist/sandbox/client.cjs
CHANGED
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
*/
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.SandboxClient = void 0;
|
|
7
|
+
const access_delegation_js_1 = require("./access_delegation.cjs");
|
|
8
|
+
const service_url_js_1 = require("./service_url.cjs");
|
|
7
9
|
const env_js_1 = require("../utils/env.cjs");
|
|
8
10
|
const fetch_js_1 = require("../singletons/fetch.cjs");
|
|
9
11
|
const async_caller_js_1 = require("../utils/async_caller.cjs");
|
|
@@ -407,6 +409,53 @@ class SandboxClient {
|
|
|
407
409
|
const suffix = segments.length ? `/${segments.join("/")}` : "";
|
|
408
410
|
return `${this._baseUrl}/boxes/${encodeURIComponent(name)}${suffix}`;
|
|
409
411
|
}
|
|
412
|
+
/**
|
|
413
|
+
* Generate an authenticated URL for an HTTP service running in a sandbox.
|
|
414
|
+
*
|
|
415
|
+
* Without `access`, mints a short-lived token and returns a {@link ServiceUrl}
|
|
416
|
+
* that refreshes it as it nears expiry. With `access` set to `"restricted"`
|
|
417
|
+
* or `"workspace"` the URL is gated by LangSmith login instead and a
|
|
418
|
+
* {@link ServiceLoginUrl} is returned: no token, no expiry, browser only.
|
|
419
|
+
*
|
|
420
|
+
* A login grant is durable, so token mode is refused with 409 while one is
|
|
421
|
+
* in place, and `expiresInSeconds` does not apply to the login modes.
|
|
422
|
+
*/
|
|
423
|
+
async serviceUrl(name, options) {
|
|
424
|
+
const { port, expiresInSeconds, access, signal } = options;
|
|
425
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
426
|
+
throw new errors_js_1.LangSmithValidationError(`port must be between 1 and 65535 (got ${port})`, "port");
|
|
427
|
+
}
|
|
428
|
+
if (access !== undefined &&
|
|
429
|
+
access !== "restricted" &&
|
|
430
|
+
access !== "workspace") {
|
|
431
|
+
throw new errors_js_1.LangSmithValidationError(`access must be "restricted" or "workspace" (got ${String(access)})`, "access");
|
|
432
|
+
}
|
|
433
|
+
const loginMode = access !== undefined;
|
|
434
|
+
if (expiresInSeconds !== undefined && loginMode) {
|
|
435
|
+
throw new errors_js_1.LangSmithValidationError(`expiresInSeconds does not apply to access "${access}": a LangSmith ` +
|
|
436
|
+
"login URL carries no token and does not expire", "expiresInSeconds");
|
|
437
|
+
}
|
|
438
|
+
const url = this._boxUrl(name, "service-url");
|
|
439
|
+
const payload = { port };
|
|
440
|
+
if (!loginMode && expiresInSeconds !== undefined) {
|
|
441
|
+
payload.expires_in_seconds = expiresInSeconds;
|
|
442
|
+
}
|
|
443
|
+
if (access !== undefined) {
|
|
444
|
+
payload.access = access;
|
|
445
|
+
}
|
|
446
|
+
const response = await this._postJson(url, payload, { signal });
|
|
447
|
+
const data = await response.json();
|
|
448
|
+
if (loginMode) {
|
|
449
|
+
return new service_url_js_1.ServiceLoginUrl(data);
|
|
450
|
+
}
|
|
451
|
+
// Refresh with the token parameters only. Carrying options.signal over
|
|
452
|
+
// would tie every later refresh to the first request's lifetime, so a
|
|
453
|
+
// caller-supplied timeout signal would leave the URL unable to refresh.
|
|
454
|
+
return new service_url_js_1.ServiceUrl(data, () => this.serviceUrl(name, {
|
|
455
|
+
port,
|
|
456
|
+
expiresInSeconds,
|
|
457
|
+
}));
|
|
458
|
+
}
|
|
410
459
|
async _postJson(url, body, options) {
|
|
411
460
|
const response = await this._fetch(url, {
|
|
412
461
|
method: "POST",
|
|
@@ -424,7 +473,7 @@ class SandboxClient {
|
|
|
424
473
|
const resolvedOptions = typeof snapshotIdOrOptions === "object" && snapshotIdOrOptions !== null
|
|
425
474
|
? snapshotIdOrOptions
|
|
426
475
|
: options;
|
|
427
|
-
const { snapshotName, name, timeout = 30, waitForReady = true, idleTtlSeconds, deleteAfterStopSeconds, vCpus, memBytes, fsCapacityBytes, mountConfig, proxyConfig, } = resolvedOptions;
|
|
476
|
+
const { snapshotName, name, timeout = 30, waitForReady = true, idleTtlSeconds, deleteAfterStopSeconds, vCpus, memBytes, fsCapacityBytes, mountConfig, proxyConfig, runConfig, accessDelegation, } = resolvedOptions;
|
|
428
477
|
if (snapshotId && snapshotName) {
|
|
429
478
|
throw new errors_js_1.LangSmithValidationError("At most one of snapshotId or options.snapshotName may be set", "snapshotId");
|
|
430
479
|
}
|
|
@@ -468,6 +517,12 @@ class SandboxClient {
|
|
|
468
517
|
if (proxyConfig !== undefined) {
|
|
469
518
|
payload.proxy_config = proxyConfig;
|
|
470
519
|
}
|
|
520
|
+
if (runConfig !== undefined) {
|
|
521
|
+
payload.run_config = runConfig;
|
|
522
|
+
}
|
|
523
|
+
if (accessDelegation !== undefined) {
|
|
524
|
+
payload.access_delegation = (0, access_delegation_js_1.validateAccessDelegation)(accessDelegation);
|
|
525
|
+
}
|
|
471
526
|
const httpTimeout = waitForReady ? (timeout + 30) * 1000 : 30 * 1000;
|
|
472
527
|
const response = await this._fetch(url, {
|
|
473
528
|
method: "POST",
|
|
@@ -523,13 +578,14 @@ class SandboxClient {
|
|
|
523
578
|
const options = typeof newNameOrOptions === "string"
|
|
524
579
|
? { newName: newNameOrOptions }
|
|
525
580
|
: newNameOrOptions;
|
|
526
|
-
const { newName, idleTtlSeconds, deleteAfterStopSeconds, proxyConfig } = options;
|
|
581
|
+
const { newName, idleTtlSeconds, deleteAfterStopSeconds, proxyConfig, runConfig, } = options;
|
|
527
582
|
(0, helpers_js_1.validateTtl)(idleTtlSeconds, "idleTtlSeconds");
|
|
528
583
|
(0, helpers_js_1.validateTtl)(deleteAfterStopSeconds, "deleteAfterStopSeconds");
|
|
529
584
|
if (newName === undefined &&
|
|
530
585
|
idleTtlSeconds === undefined &&
|
|
531
586
|
deleteAfterStopSeconds === undefined &&
|
|
532
|
-
proxyConfig === undefined
|
|
587
|
+
proxyConfig === undefined &&
|
|
588
|
+
runConfig === undefined) {
|
|
533
589
|
return this.getSandbox(name);
|
|
534
590
|
}
|
|
535
591
|
const url = this._boxUrl(name);
|
|
@@ -546,6 +602,9 @@ class SandboxClient {
|
|
|
546
602
|
if (proxyConfig !== undefined) {
|
|
547
603
|
payload.proxy_config = proxyConfig;
|
|
548
604
|
}
|
|
605
|
+
if (runConfig !== undefined) {
|
|
606
|
+
payload.run_config = runConfig;
|
|
607
|
+
}
|
|
549
608
|
const response = await this._fetch(url, {
|
|
550
609
|
method: "PATCH",
|
|
551
610
|
headers: { "Content-Type": "application/json" },
|
|
@@ -730,7 +789,7 @@ class SandboxClient {
|
|
|
730
789
|
* @returns Snapshot in "ready" status.
|
|
731
790
|
*/
|
|
732
791
|
async createSnapshot(name, dockerImage, fsCapacityBytes, options = {}) {
|
|
733
|
-
const { registryId, timeout = 60, signal } = options;
|
|
792
|
+
const { registryId, runConfig, timeout = 60, signal } = options;
|
|
734
793
|
const url = `${this._baseUrl}/snapshots`;
|
|
735
794
|
const payload = {
|
|
736
795
|
name,
|
|
@@ -740,6 +799,9 @@ class SandboxClient {
|
|
|
740
799
|
if (registryId !== undefined) {
|
|
741
800
|
payload.registry_id = registryId;
|
|
742
801
|
}
|
|
802
|
+
if (runConfig !== undefined) {
|
|
803
|
+
payload.run_config = runConfig;
|
|
804
|
+
}
|
|
743
805
|
const response = await this._postJson(url, payload, { signal });
|
|
744
806
|
const snapshot = (await response.json());
|
|
745
807
|
return this.waitForSnapshot(snapshot.id, { timeout, signal });
|
|
@@ -832,7 +894,7 @@ class SandboxClient {
|
|
|
832
894
|
* @returns Snapshot in "ready" status.
|
|
833
895
|
*/
|
|
834
896
|
async captureSnapshot(sandboxName, name, options = {}) {
|
|
835
|
-
const { dockerImage, fsCapacityBytes, timeout = 60, signal } = options;
|
|
897
|
+
const { dockerImage, fsCapacityBytes, runConfig, timeout = 60, signal, } = options;
|
|
836
898
|
const url = this._boxUrl(sandboxName, "snapshot");
|
|
837
899
|
const payload = { name };
|
|
838
900
|
if (dockerImage !== undefined) {
|
|
@@ -841,6 +903,9 @@ class SandboxClient {
|
|
|
841
903
|
if (fsCapacityBytes !== undefined) {
|
|
842
904
|
payload.fs_capacity_bytes = fsCapacityBytes;
|
|
843
905
|
}
|
|
906
|
+
if (runConfig !== undefined) {
|
|
907
|
+
payload.run_config = runConfig;
|
|
908
|
+
}
|
|
844
909
|
const response = await this._postJson(url, payload, { signal });
|
|
845
910
|
const snapshot = (await response.json());
|
|
846
911
|
return this.waitForSnapshot(snapshot.id, { timeout, signal });
|
package/dist/sandbox/client.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Main SandboxClient class for interacting with the sandbox server API.
|
|
3
3
|
*/
|
|
4
|
+
import { ServiceLoginUrl, ServiceUrl, type ServiceAccess } from "./service_url.js";
|
|
4
5
|
import { Registries } from "../_openapi_client/resources/sandboxes/registries.js";
|
|
5
6
|
import type { CaptureSnapshotOptions, CreateDockerfileSnapshotOptions, CreateSandboxOptions, CreateSnapshotOptions, DownloadURL, GenerateDownloadURLOptions, ListSnapshotsOptions, ResourceStatus, SandboxClientConfig, Snapshot, StartSandboxOptions, UpdateSandboxOptions, WaitForSandboxOptions, WaitForSnapshotOptions } from "./types.js";
|
|
6
7
|
import { Sandbox } from "./sandbox.js";
|
|
@@ -64,6 +65,23 @@ export declare class SandboxClient {
|
|
|
64
65
|
* ```
|
|
65
66
|
*/
|
|
66
67
|
get registries(): Registries;
|
|
68
|
+
/**
|
|
69
|
+
* Generate an authenticated URL for an HTTP service running in a sandbox.
|
|
70
|
+
*
|
|
71
|
+
* Without `access`, mints a short-lived token and returns a {@link ServiceUrl}
|
|
72
|
+
* that refreshes it as it nears expiry. With `access` set to `"restricted"`
|
|
73
|
+
* or `"workspace"` the URL is gated by LangSmith login instead and a
|
|
74
|
+
* {@link ServiceLoginUrl} is returned: no token, no expiry, browser only.
|
|
75
|
+
*
|
|
76
|
+
* A login grant is durable, so token mode is refused with 409 while one is
|
|
77
|
+
* in place, and `expiresInSeconds` does not apply to the login modes.
|
|
78
|
+
*/
|
|
79
|
+
serviceUrl(name: string, options: {
|
|
80
|
+
port: number;
|
|
81
|
+
expiresInSeconds?: number;
|
|
82
|
+
access?: ServiceAccess;
|
|
83
|
+
signal?: AbortSignal;
|
|
84
|
+
}): Promise<ServiceUrl | ServiceLoginUrl>;
|
|
67
85
|
private _postJson;
|
|
68
86
|
/**
|
|
69
87
|
* Create a new Sandbox.
|