langsmith 0.10.4 → 0.10.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 +10 -0
- package/dist/client.d.ts +1 -0
- package/dist/client.js +10 -0
- package/dist/evaluation/_runner.cjs +47 -5
- package/dist/evaluation/_runner.d.ts +12 -0
- package/dist/evaluation/_runner.js +47 -5
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- 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/helpers.cjs +129 -0
- package/dist/sandbox/helpers.d.ts +29 -0
- package/dist/sandbox/helpers.js +123 -0
- package/dist/sandbox/index.cjs +7 -1
- package/dist/sandbox/index.d.ts +5 -1
- package/dist/sandbox/index.js +2 -0
- 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/ws_execute.cjs +31 -1
- package/dist/sandbox/ws_execute.d.ts +9 -0
- package/dist/sandbox/ws_execute.js +31 -1
- package/dist/utils/error.cjs +17 -22
- package/dist/utils/error.js +17 -22
- package/package.json +1 -1
package/dist/sandbox/types.d.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Field names use snake_case to match API response format.
|
|
5
5
|
*/
|
|
6
|
+
import type { AccessDelegation } from "./access_delegation.js";
|
|
6
7
|
/**
|
|
7
8
|
* Result of executing a command in a sandbox.
|
|
8
9
|
*/
|
|
@@ -20,6 +21,164 @@ export interface ResourceStatus {
|
|
|
20
21
|
/** Human-readable details when failed. */
|
|
21
22
|
status_message?: string;
|
|
22
23
|
}
|
|
24
|
+
/**
|
|
25
|
+
* The user, working directory and environment commands run with.
|
|
26
|
+
*
|
|
27
|
+
* Mirrors `docker run -u / -w / -e`: `user` and `work_dir` replace the layer
|
|
28
|
+
* below, `env_vars` merge into it key by key. It applies at three points, each
|
|
29
|
+
* layered over the one before -- the snapshot, the sandbox, and a single
|
|
30
|
+
* command.
|
|
31
|
+
*/
|
|
32
|
+
export interface RunConfig {
|
|
33
|
+
/**
|
|
34
|
+
* Account to run as: `name`, `uid`, `name:group` or `uid:gid`. Defaults to
|
|
35
|
+
* the Docker image's `USER`.
|
|
36
|
+
*/
|
|
37
|
+
user?: string;
|
|
38
|
+
/**
|
|
39
|
+
* Absolute working directory. Defaults to the image's `WORKDIR`. A relative
|
|
40
|
+
* path is rejected by the server.
|
|
41
|
+
*/
|
|
42
|
+
work_dir?: string;
|
|
43
|
+
/** Environment variables, merged over the layer below. */
|
|
44
|
+
env_vars?: Record<string, string>;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* One filesystem entry returned by `sandbox.glob()`.
|
|
48
|
+
*/
|
|
49
|
+
export interface FileInfo {
|
|
50
|
+
/** Absolute path of the entry. */
|
|
51
|
+
path: string;
|
|
52
|
+
/** True for a directory. */
|
|
53
|
+
is_dir: boolean;
|
|
54
|
+
/** Size in bytes. */
|
|
55
|
+
size_bytes: number;
|
|
56
|
+
/** RFC 3339 modification timestamp. */
|
|
57
|
+
modified_at?: string;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Entries matching a glob pattern.
|
|
61
|
+
*/
|
|
62
|
+
export interface GlobResult {
|
|
63
|
+
matches: FileInfo[];
|
|
64
|
+
/**
|
|
65
|
+
* True when the server hit its result cap or deadline, so the search is a
|
|
66
|
+
* partial answer worth refining.
|
|
67
|
+
*/
|
|
68
|
+
truncated: boolean;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* One matching line found by `sandbox.grep()`.
|
|
72
|
+
*/
|
|
73
|
+
export interface GrepMatch {
|
|
74
|
+
/** Absolute path of the file the match was found in. */
|
|
75
|
+
path: string;
|
|
76
|
+
/** 1-based line number. */
|
|
77
|
+
line: number;
|
|
78
|
+
/** The matching line's text. */
|
|
79
|
+
text: string;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Lines matching a literal search.
|
|
83
|
+
*/
|
|
84
|
+
export interface GrepResult {
|
|
85
|
+
matches: GrepMatch[];
|
|
86
|
+
/** True when the server hit its result cap or deadline. */
|
|
87
|
+
truncated: boolean;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Options for the read-only filesystem search operations.
|
|
91
|
+
*/
|
|
92
|
+
export interface GlobOptions {
|
|
93
|
+
/** Maximum matches to return. */
|
|
94
|
+
limit?: number;
|
|
95
|
+
/** Request timeout in seconds. */
|
|
96
|
+
timeout?: number;
|
|
97
|
+
headers?: Record<string, string>;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Options for `sandbox.grep()`.
|
|
101
|
+
*/
|
|
102
|
+
export interface GrepOptions extends GlobOptions {
|
|
103
|
+
/**
|
|
104
|
+
* Restrict which files are searched. A bare `*.py` matches by basename at
|
|
105
|
+
* any depth; a pattern containing `/` or `**` matches the path relative to
|
|
106
|
+
* the search root.
|
|
107
|
+
*/
|
|
108
|
+
glob?: string;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* What a `HEAD` on a sandbox file reports, without transferring it.
|
|
112
|
+
*/
|
|
113
|
+
export interface FileStat {
|
|
114
|
+
/** The file's size. */
|
|
115
|
+
size_bytes: number;
|
|
116
|
+
/**
|
|
117
|
+
* Strong validator for the current contents. Opaque -- compare it, never
|
|
118
|
+
* parse it. Pass it back as `ifRange` to resume a download safely, or as
|
|
119
|
+
* `ifNoneMatch` to poll for a change.
|
|
120
|
+
*/
|
|
121
|
+
etag?: string;
|
|
122
|
+
/** HTTP-date of the last modification, second-resolution. */
|
|
123
|
+
last_modified?: string;
|
|
124
|
+
/** The server's content type for the file. */
|
|
125
|
+
content_type?: string;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Bytes returned by a ranged read, and where they sit in the file.
|
|
129
|
+
*/
|
|
130
|
+
export interface FileChunk {
|
|
131
|
+
/** The bytes returned. Empty when `unchanged` is true. */
|
|
132
|
+
content: Uint8Array;
|
|
133
|
+
/**
|
|
134
|
+
* Validator for the version these bytes came from. Pass it as `ifRange` on
|
|
135
|
+
* the next chunk so a rewrite restarts the read instead of splicing two
|
|
136
|
+
* versions together.
|
|
137
|
+
*/
|
|
138
|
+
etag?: string;
|
|
139
|
+
/** The file's full size, or undefined when the server did not report it. */
|
|
140
|
+
total_bytes?: number;
|
|
141
|
+
/** Offset of the first byte returned. */
|
|
142
|
+
start: number;
|
|
143
|
+
/**
|
|
144
|
+
* True when the server answered 206 with only part of the file. A false
|
|
145
|
+
* here after a ranged request means the file changed and the server sent it
|
|
146
|
+
* whole -- restart from zero.
|
|
147
|
+
*/
|
|
148
|
+
partial: boolean;
|
|
149
|
+
/**
|
|
150
|
+
* True when the caller passed `ifNoneMatch` and the file still matches it.
|
|
151
|
+
* No bytes are returned.
|
|
152
|
+
*/
|
|
153
|
+
unchanged: boolean;
|
|
154
|
+
/** HTTP-date of the last modification. */
|
|
155
|
+
last_modified?: string;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Options for a ranged read. Provide `start` (with optional `end`), or
|
|
159
|
+
* `suffixBytes`.
|
|
160
|
+
*/
|
|
161
|
+
export interface ReadRangeOptions {
|
|
162
|
+
/** First byte to return. With `end`, an inclusive range. */
|
|
163
|
+
start?: number;
|
|
164
|
+
/** Last byte to return, inclusive. Clamped to EOF rather than rejected. */
|
|
165
|
+
end?: number;
|
|
166
|
+
/** Return the last N bytes. Mutually exclusive with `start`/`end`. */
|
|
167
|
+
suffixBytes?: number;
|
|
168
|
+
/**
|
|
169
|
+
* An `ETag` from an earlier chunk. The range is honored only while the file
|
|
170
|
+
* still matches it.
|
|
171
|
+
*/
|
|
172
|
+
ifRange?: string;
|
|
173
|
+
/**
|
|
174
|
+
* An `ETag` the caller already holds. An unchanged file answers with
|
|
175
|
+
* `unchanged: true` and no bytes.
|
|
176
|
+
*/
|
|
177
|
+
ifNoneMatch?: string;
|
|
178
|
+
/** Request timeout in seconds. */
|
|
179
|
+
timeout?: number;
|
|
180
|
+
headers?: Record<string, string>;
|
|
181
|
+
}
|
|
23
182
|
/**
|
|
24
183
|
* Represents a sandbox snapshot.
|
|
25
184
|
*
|
|
@@ -41,6 +200,13 @@ export interface Snapshot {
|
|
|
41
200
|
registry_id?: string;
|
|
42
201
|
created_at?: string;
|
|
43
202
|
updated_at?: string;
|
|
203
|
+
/**
|
|
204
|
+
* User, working directory and environment sandboxes built from this
|
|
205
|
+
* snapshot boot with, resolved from the Docker image at build time. Absent
|
|
206
|
+
* on snapshots built before the server recorded it, which boot as root with
|
|
207
|
+
* no image environment.
|
|
208
|
+
*/
|
|
209
|
+
run_config?: RunConfig;
|
|
44
210
|
}
|
|
45
211
|
/**
|
|
46
212
|
* Data representing a sandbox instance from the API.
|
|
@@ -79,6 +245,16 @@ export interface SandboxData {
|
|
|
79
245
|
mem_bytes?: number;
|
|
80
246
|
/** Root filesystem capacity in bytes. */
|
|
81
247
|
fs_capacity_bytes?: number;
|
|
248
|
+
/**
|
|
249
|
+
* User, working directory and environment the sandbox's commands run with.
|
|
250
|
+
* Absent on sandboxes created before the server recorded it.
|
|
251
|
+
*/
|
|
252
|
+
run_config?: RunConfig;
|
|
253
|
+
/**
|
|
254
|
+
* LangSmith access granted to code inside the sandbox. Absent when it has
|
|
255
|
+
* none.
|
|
256
|
+
*/
|
|
257
|
+
access_delegation?: AccessDelegation;
|
|
82
258
|
}
|
|
83
259
|
/**
|
|
84
260
|
* Configuration options for the SandboxClient.
|
|
@@ -170,6 +346,10 @@ export interface WsRunOptions {
|
|
|
170
346
|
ttlSeconds?: number;
|
|
171
347
|
/** Whether to allocate a PTY. */
|
|
172
348
|
pty?: boolean;
|
|
349
|
+
/** Per-command user, working directory and environment. */
|
|
350
|
+
runConfig?: RunConfig;
|
|
351
|
+
/** Half-close the spawned process's stdin so it reads EOF. */
|
|
352
|
+
closeStdin?: boolean;
|
|
173
353
|
/**
|
|
174
354
|
* Additional headers attached to the WebSocket upgrade request. Merged on
|
|
175
355
|
* top of any default headers the SandboxClient was constructed with.
|
|
@@ -186,12 +366,30 @@ export interface RunOptions {
|
|
|
186
366
|
timeout?: number;
|
|
187
367
|
/**
|
|
188
368
|
* Environment variables to set for the command.
|
|
369
|
+
*
|
|
370
|
+
* @deprecated Use `runConfig.env_vars`. The two cannot be combined.
|
|
189
371
|
*/
|
|
190
372
|
env?: Record<string, string>;
|
|
191
373
|
/**
|
|
192
374
|
* Working directory for command execution.
|
|
375
|
+
*
|
|
376
|
+
* @deprecated Use `runConfig.work_dir`. The two cannot be combined.
|
|
193
377
|
*/
|
|
194
378
|
cwd?: string;
|
|
379
|
+
/**
|
|
380
|
+
* User, working directory and environment for this one command, layered
|
|
381
|
+
* over the sandbox's own.
|
|
382
|
+
*/
|
|
383
|
+
runConfig?: RunConfig;
|
|
384
|
+
/**
|
|
385
|
+
* Half-close the command's stdin so a command that reads it sees EOF
|
|
386
|
+
* rather than blocking until the timeout. Defaults to true for a non-PTY
|
|
387
|
+
* command, which means `sendInput()` on the returned handle throws unless
|
|
388
|
+
* this is set to false. Ignored under `pty: true`, where input and output
|
|
389
|
+
* share one terminal file descriptor -- send an EOT byte (0x04) as input
|
|
390
|
+
* instead.
|
|
391
|
+
*/
|
|
392
|
+
closeInput?: boolean;
|
|
195
393
|
/**
|
|
196
394
|
* Shell to use for command execution. Defaults to "/bin/bash".
|
|
197
395
|
*/
|
|
@@ -262,8 +460,20 @@ export interface SandboxProxySecret {
|
|
|
262
460
|
/** Workspace secret reference or opaque secret value. */
|
|
263
461
|
value: string;
|
|
264
462
|
}
|
|
463
|
+
/** Static credentials used by the sandbox proxy signer. */
|
|
464
|
+
interface AwsStaticAuthConfig {
|
|
465
|
+
access_key_id: SandboxProxySecret;
|
|
466
|
+
secret_access_key: SandboxProxySecret;
|
|
467
|
+
role_arn?: "";
|
|
468
|
+
}
|
|
469
|
+
/** IAM role assumed and renewed by LangSmith, outside the sandbox. */
|
|
470
|
+
interface AwsRoleAuthConfig {
|
|
471
|
+
role_arn: string;
|
|
472
|
+
access_key_id?: never;
|
|
473
|
+
secret_access_key?: never;
|
|
474
|
+
}
|
|
265
475
|
/** AWS auth rule for sandbox proxy SigV4 signing. */
|
|
266
|
-
export interface SandboxAwsAuthRule {
|
|
476
|
+
export interface SandboxAwsAuthRule<Auth extends AwsStaticAuthConfig | AwsRoleAuthConfig = AwsStaticAuthConfig | AwsRoleAuthConfig> {
|
|
267
477
|
/** Rule name. */
|
|
268
478
|
name: string;
|
|
269
479
|
/** AWS auth rules are matched by the sandbox proxy's AWS endpoint matcher. */
|
|
@@ -277,11 +487,8 @@ export interface SandboxAwsAuthRule {
|
|
|
277
487
|
* the wire.
|
|
278
488
|
*/
|
|
279
489
|
env_vars?: Record<string, string>;
|
|
280
|
-
/**
|
|
281
|
-
aws:
|
|
282
|
-
access_key_id: SandboxProxySecret;
|
|
283
|
-
secret_access_key: SandboxProxySecret;
|
|
284
|
-
};
|
|
490
|
+
/** Exclusive static credentials or IAM-role descriptor. */
|
|
491
|
+
aws: Auth;
|
|
285
492
|
}
|
|
286
493
|
/** GCP auth rule for sandbox proxy OAuth bearer injection. */
|
|
287
494
|
export interface SandboxGcpAuthRule {
|
|
@@ -314,7 +521,7 @@ export type SandboxProxyRule = SandboxAwsAuthRule | SandboxGcpAuthRule | Record<
|
|
|
314
521
|
export interface SandboxProxyConfig {
|
|
315
522
|
/** Header-injection rules keyed by host pattern. */
|
|
316
523
|
rules?: SandboxProxyRule[];
|
|
317
|
-
/**
|
|
524
|
+
/** @deprecated Ignored by the server. The sandbox runtime has no proxy bypass list. */
|
|
318
525
|
no_proxy?: string[];
|
|
319
526
|
/** Allow/deny list enforced at the proxy sidecar. */
|
|
320
527
|
access_control?: SandboxAccessControl;
|
|
@@ -427,6 +634,7 @@ export type SandboxMount = S3MountSpec | GCSMountSpec | GitMountSpec | ContextHu
|
|
|
427
634
|
export interface SandboxAwsMountAuthConfig {
|
|
428
635
|
access_key_id: SandboxProxySecret;
|
|
429
636
|
secret_access_key: SandboxProxySecret;
|
|
637
|
+
role_arn?: "";
|
|
430
638
|
}
|
|
431
639
|
/** GCP credentials used by the backend to authenticate GCS mounts. */
|
|
432
640
|
export interface SandboxGcpMountAuthConfig {
|
|
@@ -434,7 +642,8 @@ export interface SandboxGcpMountAuthConfig {
|
|
|
434
642
|
}
|
|
435
643
|
/** Provider auth blocks for sandbox mounts. */
|
|
436
644
|
export interface SandboxMountAuthConfig {
|
|
437
|
-
|
|
645
|
+
/** IAM roles here are restricted to this sandbox's S3 mount scopes. */
|
|
646
|
+
aws?: SandboxAwsMountAuthConfig | AwsRoleAuthConfig;
|
|
438
647
|
gcp?: SandboxGcpMountAuthConfig;
|
|
439
648
|
}
|
|
440
649
|
/** Provider auth helper output accepted by mountConfig. */
|
|
@@ -507,6 +716,21 @@ export interface CreateSandboxOptions {
|
|
|
507
716
|
* auth.
|
|
508
717
|
*/
|
|
509
718
|
proxyConfig?: SandboxProxyConfig;
|
|
719
|
+
/**
|
|
720
|
+
* User, working directory and environment the sandbox boots with,
|
|
721
|
+
* overriding the snapshot's: `user` and `work_dir` replace, `env_vars`
|
|
722
|
+
* merge. The sandbox's own env vars remain a layer above this one.
|
|
723
|
+
*/
|
|
724
|
+
runConfig?: RunConfig;
|
|
725
|
+
/**
|
|
726
|
+
* Grant letting code inside the sandbox call the LangSmith API as you, with
|
|
727
|
+
* no API key of its own. `{ mode: "INHERIT" }` grants everything you can do;
|
|
728
|
+
* `{ mode: "EXPLICIT", permissions: [...] }` grants only the permissions
|
|
729
|
+
* listed, each of which you must already hold. The grant belongs to the
|
|
730
|
+
* sandbox, so anyone who can exec into it can make calls under it. Omit for
|
|
731
|
+
* no access.
|
|
732
|
+
*/
|
|
733
|
+
accessDelegation?: AccessDelegation;
|
|
510
734
|
}
|
|
511
735
|
/**
|
|
512
736
|
* Options for creating a snapshot from a Docker image.
|
|
@@ -514,6 +738,13 @@ export interface CreateSandboxOptions {
|
|
|
514
738
|
export interface CreateSnapshotOptions {
|
|
515
739
|
/** Private registry ID. */
|
|
516
740
|
registryId?: string;
|
|
741
|
+
/**
|
|
742
|
+
* Override the Docker image's `USER`, `WORKDIR` and `ENV` for sandboxes
|
|
743
|
+
* built from this snapshot: `user` and `work_dir` replace the image's,
|
|
744
|
+
* `env_vars` merge over it key by key. Omitting it adopts the image's own
|
|
745
|
+
* configuration. Pass `{ user: "0" }` to keep running as root.
|
|
746
|
+
*/
|
|
747
|
+
runConfig?: RunConfig;
|
|
517
748
|
/** Timeout in seconds when waiting for ready. Default: 60. */
|
|
518
749
|
timeout?: number;
|
|
519
750
|
/** AbortSignal for cancellation. */
|
|
@@ -554,6 +785,11 @@ export interface CaptureSnapshotOptions {
|
|
|
554
785
|
dockerImage?: string;
|
|
555
786
|
/** Filesystem capacity in bytes for Docker image export. */
|
|
556
787
|
fsCapacityBytes?: number;
|
|
788
|
+
/**
|
|
789
|
+
* Override applied over the configuration the captured sandbox was running
|
|
790
|
+
* with: `user` and `work_dir` replace, `env_vars` merge.
|
|
791
|
+
*/
|
|
792
|
+
runConfig?: RunConfig;
|
|
557
793
|
/** Timeout in seconds when waiting for ready. Default: 60. */
|
|
558
794
|
timeout?: number;
|
|
559
795
|
/** AbortSignal for cancellation. */
|
|
@@ -669,6 +905,13 @@ export interface UpdateSandboxOptions {
|
|
|
669
905
|
* `ready`; start a stopped one first.
|
|
670
906
|
*/
|
|
671
907
|
proxyConfig?: SandboxProxyConfig;
|
|
908
|
+
/**
|
|
909
|
+
* Merge into the sandbox's stored run configuration: `user` and `work_dir`
|
|
910
|
+
* replace, `env_vars` merge. Takes effect for subsequent commands; commands
|
|
911
|
+
* already running are unaffected, and a stopped sandbox picks it up on its
|
|
912
|
+
* next start. Omitting it leaves the stored value untouched.
|
|
913
|
+
*/
|
|
914
|
+
runConfig?: RunConfig;
|
|
672
915
|
}
|
|
673
916
|
/**
|
|
674
917
|
* Options for waiting for a sandbox to become ready.
|
|
@@ -136,10 +136,20 @@ class WSStreamControl {
|
|
|
136
136
|
writable: true,
|
|
137
137
|
value: false
|
|
138
138
|
});
|
|
139
|
+
Object.defineProperty(this, "_closeStdinPending", {
|
|
140
|
+
enumerable: true,
|
|
141
|
+
configurable: true,
|
|
142
|
+
writable: true,
|
|
143
|
+
value: false
|
|
144
|
+
});
|
|
139
145
|
}
|
|
140
146
|
/** Bind to the active WebSocket. Called inside the generator. */
|
|
141
147
|
_bind(ws) {
|
|
142
148
|
this._ws = ws;
|
|
149
|
+
if (this._closeStdinPending) {
|
|
150
|
+
this._closeStdinPending = false;
|
|
151
|
+
this.sendCloseStdin();
|
|
152
|
+
}
|
|
143
153
|
}
|
|
144
154
|
/** Mark as closed. Called when the generator exits. */
|
|
145
155
|
_unbind() {
|
|
@@ -163,6 +173,22 @@ class WSStreamControl {
|
|
|
163
173
|
this._ws.send(JSON.stringify({ type: "input", data }));
|
|
164
174
|
}
|
|
165
175
|
}
|
|
176
|
+
/**
|
|
177
|
+
* Half-close the command's stdin so it reads EOF.
|
|
178
|
+
*
|
|
179
|
+
* A reconnected stream binds its socket only once iteration starts, so a
|
|
180
|
+
* close arriving before that is queued and sent on bind — dropping it would
|
|
181
|
+
* leave the command waiting for an EOF that never comes.
|
|
182
|
+
*/
|
|
183
|
+
sendCloseStdin() {
|
|
184
|
+
if (this._closed)
|
|
185
|
+
return;
|
|
186
|
+
if (this._ws && this._ws.readyState === 1) {
|
|
187
|
+
this._ws.send(JSON.stringify({ type: "close_stdin" }));
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
this._closeStdinPending = true;
|
|
191
|
+
}
|
|
166
192
|
}
|
|
167
193
|
exports.WSStreamControl = WSStreamControl;
|
|
168
194
|
// =============================================================================
|
|
@@ -330,7 +356,7 @@ async function* readWsMessages(ws) {
|
|
|
330
356
|
* data arrives in addition to yielding the messages.
|
|
331
357
|
*/
|
|
332
358
|
async function runWsStream(dataplaneUrl, apiKey, command, options = {}) {
|
|
333
|
-
const { timeout = 60, env, cwd, shell = "/bin/bash", onStdout, onStderr, commandId, idleTimeout = 300, killOnDisconnect = false, ttlSeconds = 600, pty, headers: extraHeaders, openTimeout = exports.WS_OPEN_TIMEOUT, } = options;
|
|
359
|
+
const { timeout = 60, env, cwd, shell = "/bin/bash", onStdout, onStderr, commandId, idleTimeout = 300, killOnDisconnect = false, ttlSeconds = 600, pty, runConfig, closeStdin, headers: extraHeaders, openTimeout = exports.WS_OPEN_TIMEOUT, } = options;
|
|
334
360
|
const wsUrl = buildWsUrl(dataplaneUrl);
|
|
335
361
|
const headers = buildAuthHeaders(apiKey, extraHeaders);
|
|
336
362
|
const control = new WSStreamControl();
|
|
@@ -353,6 +379,10 @@ async function runWsStream(dataplaneUrl, apiKey, command, options = {}) {
|
|
|
353
379
|
payload.env = env;
|
|
354
380
|
if (cwd)
|
|
355
381
|
payload.cwd = cwd;
|
|
382
|
+
if (runConfig)
|
|
383
|
+
payload.run_config = runConfig;
|
|
384
|
+
if (closeStdin)
|
|
385
|
+
payload.close_stdin = true;
|
|
356
386
|
if (commandId)
|
|
357
387
|
payload.command_id = commandId;
|
|
358
388
|
if (pty)
|
|
@@ -35,6 +35,7 @@ export declare class WSStreamControl {
|
|
|
35
35
|
private _ws;
|
|
36
36
|
private _closed;
|
|
37
37
|
private _killed;
|
|
38
|
+
private _closeStdinPending;
|
|
38
39
|
/** Bind to the active WebSocket. Called inside the generator. */
|
|
39
40
|
_bind(ws: WsWebSocket): void;
|
|
40
41
|
/** Mark as closed. Called when the generator exits. */
|
|
@@ -45,6 +46,14 @@ export declare class WSStreamControl {
|
|
|
45
46
|
sendKill(): void;
|
|
46
47
|
/** Send stdin data to the running command. */
|
|
47
48
|
sendInput(data: string): void;
|
|
49
|
+
/**
|
|
50
|
+
* Half-close the command's stdin so it reads EOF.
|
|
51
|
+
*
|
|
52
|
+
* A reconnected stream binds its socket only once iteration starts, so a
|
|
53
|
+
* close arriving before that is queued and sent on bind — dropping it would
|
|
54
|
+
* leave the command waiting for an EOF that never comes.
|
|
55
|
+
*/
|
|
56
|
+
sendCloseStdin(): void;
|
|
48
57
|
}
|
|
49
58
|
/**
|
|
50
59
|
* Raise the appropriate exception from a server error message.
|
|
@@ -124,10 +124,20 @@ export class WSStreamControl {
|
|
|
124
124
|
writable: true,
|
|
125
125
|
value: false
|
|
126
126
|
});
|
|
127
|
+
Object.defineProperty(this, "_closeStdinPending", {
|
|
128
|
+
enumerable: true,
|
|
129
|
+
configurable: true,
|
|
130
|
+
writable: true,
|
|
131
|
+
value: false
|
|
132
|
+
});
|
|
127
133
|
}
|
|
128
134
|
/** Bind to the active WebSocket. Called inside the generator. */
|
|
129
135
|
_bind(ws) {
|
|
130
136
|
this._ws = ws;
|
|
137
|
+
if (this._closeStdinPending) {
|
|
138
|
+
this._closeStdinPending = false;
|
|
139
|
+
this.sendCloseStdin();
|
|
140
|
+
}
|
|
131
141
|
}
|
|
132
142
|
/** Mark as closed. Called when the generator exits. */
|
|
133
143
|
_unbind() {
|
|
@@ -151,6 +161,22 @@ export class WSStreamControl {
|
|
|
151
161
|
this._ws.send(JSON.stringify({ type: "input", data }));
|
|
152
162
|
}
|
|
153
163
|
}
|
|
164
|
+
/**
|
|
165
|
+
* Half-close the command's stdin so it reads EOF.
|
|
166
|
+
*
|
|
167
|
+
* A reconnected stream binds its socket only once iteration starts, so a
|
|
168
|
+
* close arriving before that is queued and sent on bind — dropping it would
|
|
169
|
+
* leave the command waiting for an EOF that never comes.
|
|
170
|
+
*/
|
|
171
|
+
sendCloseStdin() {
|
|
172
|
+
if (this._closed)
|
|
173
|
+
return;
|
|
174
|
+
if (this._ws && this._ws.readyState === 1) {
|
|
175
|
+
this._ws.send(JSON.stringify({ type: "close_stdin" }));
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
this._closeStdinPending = true;
|
|
179
|
+
}
|
|
154
180
|
}
|
|
155
181
|
// =============================================================================
|
|
156
182
|
// Error Handling
|
|
@@ -317,7 +343,7 @@ async function* readWsMessages(ws) {
|
|
|
317
343
|
* data arrives in addition to yielding the messages.
|
|
318
344
|
*/
|
|
319
345
|
export async function runWsStream(dataplaneUrl, apiKey, command, options = {}) {
|
|
320
|
-
const { timeout = 60, env, cwd, shell = "/bin/bash", onStdout, onStderr, commandId, idleTimeout = 300, killOnDisconnect = false, ttlSeconds = 600, pty, headers: extraHeaders, openTimeout = WS_OPEN_TIMEOUT, } = options;
|
|
346
|
+
const { timeout = 60, env, cwd, shell = "/bin/bash", onStdout, onStderr, commandId, idleTimeout = 300, killOnDisconnect = false, ttlSeconds = 600, pty, runConfig, closeStdin, headers: extraHeaders, openTimeout = WS_OPEN_TIMEOUT, } = options;
|
|
321
347
|
const wsUrl = buildWsUrl(dataplaneUrl);
|
|
322
348
|
const headers = buildAuthHeaders(apiKey, extraHeaders);
|
|
323
349
|
const control = new WSStreamControl();
|
|
@@ -340,6 +366,10 @@ export async function runWsStream(dataplaneUrl, apiKey, command, options = {}) {
|
|
|
340
366
|
payload.env = env;
|
|
341
367
|
if (cwd)
|
|
342
368
|
payload.cwd = cwd;
|
|
369
|
+
if (runConfig)
|
|
370
|
+
payload.run_config = runConfig;
|
|
371
|
+
if (closeStdin)
|
|
372
|
+
payload.close_stdin = true;
|
|
343
373
|
if (commandId)
|
|
344
374
|
payload.command_id = commandId;
|
|
345
375
|
if (pty)
|
package/dist/utils/error.cjs
CHANGED
|
@@ -140,32 +140,27 @@ async function raiseForStatus(response, context, consumeOnSuccess) {
|
|
|
140
140
|
}
|
|
141
141
|
return;
|
|
142
142
|
}
|
|
143
|
+
try {
|
|
144
|
+
errorBody = await response.text();
|
|
145
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
146
|
+
}
|
|
147
|
+
catch (_e) {
|
|
148
|
+
errorBody = "";
|
|
149
|
+
}
|
|
143
150
|
if (response.status === 403) {
|
|
151
|
+
let errorData;
|
|
144
152
|
try {
|
|
145
|
-
|
|
146
|
-
const errorCode = errorData?.error;
|
|
147
|
-
if (errorCode === "org_scoped_key_requires_workspace") {
|
|
148
|
-
errorBody =
|
|
149
|
-
"This API key is org-scoped and requires workspace specification. " +
|
|
150
|
-
"Please provide 'workspaceId' parameter, " +
|
|
151
|
-
"or set LANGSMITH_WORKSPACE_ID environment variable.";
|
|
152
|
-
}
|
|
153
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
154
|
-
}
|
|
155
|
-
catch (_e) {
|
|
156
|
-
const errorWithStatus = new Error(`${response.status} ${response.statusText}`);
|
|
157
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
158
|
-
errorWithStatus.status = response?.status;
|
|
159
|
-
throw errorWithStatus;
|
|
153
|
+
errorData = JSON.parse(errorBody);
|
|
160
154
|
}
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
errorBody = await response.text();
|
|
165
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
155
|
+
catch {
|
|
156
|
+
// Non-JSON body (e.g. a proxy in front of a self-hosted instance):
|
|
157
|
+
// fall through and surface the raw body like every other status.
|
|
166
158
|
}
|
|
167
|
-
|
|
168
|
-
errorBody =
|
|
159
|
+
if (errorData?.error === "org_scoped_key_requires_workspace") {
|
|
160
|
+
errorBody =
|
|
161
|
+
"This API key is org-scoped and requires workspace specification. " +
|
|
162
|
+
"Please provide 'workspaceId' parameter, " +
|
|
163
|
+
"or set LANGSMITH_WORKSPACE_ID environment variable.";
|
|
169
164
|
}
|
|
170
165
|
}
|
|
171
166
|
const fullMessage = `Failed to ${context}. Received status [${response.status}]: ${response.statusText}. Message: ${errorBody}`;
|
package/dist/utils/error.js
CHANGED
|
@@ -129,32 +129,27 @@ export async function raiseForStatus(response, context, consumeOnSuccess) {
|
|
|
129
129
|
}
|
|
130
130
|
return;
|
|
131
131
|
}
|
|
132
|
+
try {
|
|
133
|
+
errorBody = await response.text();
|
|
134
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
135
|
+
}
|
|
136
|
+
catch (_e) {
|
|
137
|
+
errorBody = "";
|
|
138
|
+
}
|
|
132
139
|
if (response.status === 403) {
|
|
140
|
+
let errorData;
|
|
133
141
|
try {
|
|
134
|
-
|
|
135
|
-
const errorCode = errorData?.error;
|
|
136
|
-
if (errorCode === "org_scoped_key_requires_workspace") {
|
|
137
|
-
errorBody =
|
|
138
|
-
"This API key is org-scoped and requires workspace specification. " +
|
|
139
|
-
"Please provide 'workspaceId' parameter, " +
|
|
140
|
-
"or set LANGSMITH_WORKSPACE_ID environment variable.";
|
|
141
|
-
}
|
|
142
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
143
|
-
}
|
|
144
|
-
catch (_e) {
|
|
145
|
-
const errorWithStatus = new Error(`${response.status} ${response.statusText}`);
|
|
146
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
147
|
-
errorWithStatus.status = response?.status;
|
|
148
|
-
throw errorWithStatus;
|
|
142
|
+
errorData = JSON.parse(errorBody);
|
|
149
143
|
}
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
errorBody = await response.text();
|
|
154
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
144
|
+
catch {
|
|
145
|
+
// Non-JSON body (e.g. a proxy in front of a self-hosted instance):
|
|
146
|
+
// fall through and surface the raw body like every other status.
|
|
155
147
|
}
|
|
156
|
-
|
|
157
|
-
errorBody =
|
|
148
|
+
if (errorData?.error === "org_scoped_key_requires_workspace") {
|
|
149
|
+
errorBody =
|
|
150
|
+
"This API key is org-scoped and requires workspace specification. " +
|
|
151
|
+
"Please provide 'workspaceId' parameter, " +
|
|
152
|
+
"or set LANGSMITH_WORKSPACE_ID environment variable.";
|
|
158
153
|
}
|
|
159
154
|
}
|
|
160
155
|
const fullMessage = `Failed to ${context}. Received status [${response.status}]: ${response.statusText}. Message: ${errorBody}`;
|