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.
Files changed (43) hide show
  1. package/dist/client.cjs +10 -0
  2. package/dist/client.d.ts +1 -0
  3. package/dist/client.js +10 -0
  4. package/dist/evaluation/_runner.cjs +47 -5
  5. package/dist/evaluation/_runner.d.ts +12 -0
  6. package/dist/evaluation/_runner.js +47 -5
  7. package/dist/index.cjs +1 -1
  8. package/dist/index.d.ts +1 -1
  9. package/dist/index.js +1 -1
  10. package/dist/sandbox/access_delegation.cjs +24 -0
  11. package/dist/sandbox/access_delegation.d.ts +15 -0
  12. package/dist/sandbox/access_delegation.js +21 -0
  13. package/dist/sandbox/client.cjs +70 -5
  14. package/dist/sandbox/client.d.ts +18 -0
  15. package/dist/sandbox/client.js +70 -5
  16. package/dist/sandbox/command_handle.cjs +34 -0
  17. package/dist/sandbox/command_handle.d.ts +10 -0
  18. package/dist/sandbox/command_handle.js +34 -0
  19. package/dist/sandbox/helpers.cjs +129 -0
  20. package/dist/sandbox/helpers.d.ts +29 -0
  21. package/dist/sandbox/helpers.js +123 -0
  22. package/dist/sandbox/index.cjs +7 -1
  23. package/dist/sandbox/index.d.ts +5 -1
  24. package/dist/sandbox/index.js +2 -0
  25. package/dist/sandbox/mounts.cjs +21 -3
  26. package/dist/sandbox/mounts.d.ts +7 -1
  27. package/dist/sandbox/mounts.js +21 -3
  28. package/dist/sandbox/proxy_config.cjs +41 -10
  29. package/dist/sandbox/proxy_config.d.ts +33 -6
  30. package/dist/sandbox/proxy_config.js +40 -10
  31. package/dist/sandbox/sandbox.cjs +160 -4
  32. package/dist/sandbox/sandbox.d.ts +71 -1
  33. package/dist/sandbox/sandbox.js +161 -5
  34. package/dist/sandbox/service_url.cjs +125 -0
  35. package/dist/sandbox/service_url.d.ts +50 -0
  36. package/dist/sandbox/service_url.js +120 -0
  37. package/dist/sandbox/types.d.ts +251 -8
  38. package/dist/sandbox/ws_execute.cjs +31 -1
  39. package/dist/sandbox/ws_execute.d.ts +9 -0
  40. package/dist/sandbox/ws_execute.js +31 -1
  41. package/dist/utils/error.cjs +17 -22
  42. package/dist/utils/error.js +17 -22
  43. package/package.json +1 -1
@@ -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
- /** AWS credentials used by the proxy signer. */
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
- /** Hosts that bypass the proxy entirely. */
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
- aws?: SandboxAwsMountAuthConfig;
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)
@@ -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
- const errorData = await response.json();
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
- if (errorBody === undefined) {
163
- try {
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
- catch (_e) {
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}`;
@@ -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
- const errorData = await response.json();
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
- if (errorBody === undefined) {
152
- try {
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
- catch (_e) {
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}`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "langsmith",
3
- "version": "0.10.4",
3
+ "version": "0.10.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": [