langsmith 0.10.3 → 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 (46) hide show
  1. package/dist/client.cjs +19 -4
  2. package/dist/client.d.ts +2 -1
  3. package/dist/client.js +19 -4
  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/dist/wrappers/openai_agents.cjs +37 -28
  44. package/dist/wrappers/openai_agents.d.ts +0 -1
  45. package/dist/wrappers/openai_agents.js +37 -28
  46. package/package.json +2 -2
@@ -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 });
@@ -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.
@@ -1,6 +1,8 @@
1
1
  /**
2
2
  * Main SandboxClient class for interacting with the sandbox server API.
3
3
  */
4
+ import { validateAccessDelegation } from "./access_delegation.js";
5
+ import { ServiceLoginUrl, ServiceUrl, } from "./service_url.js";
4
6
  import { getLangSmithEnvironmentVariable } from "../utils/env.js";
5
7
  import { _getFetchImplementation } from "../singletons/fetch.js";
6
8
  import { AsyncCaller } from "../utils/async_caller.js";
@@ -404,6 +406,53 @@ export class SandboxClient {
404
406
  const suffix = segments.length ? `/${segments.join("/")}` : "";
405
407
  return `${this._baseUrl}/boxes/${encodeURIComponent(name)}${suffix}`;
406
408
  }
409
+ /**
410
+ * Generate an authenticated URL for an HTTP service running in a sandbox.
411
+ *
412
+ * Without `access`, mints a short-lived token and returns a {@link ServiceUrl}
413
+ * that refreshes it as it nears expiry. With `access` set to `"restricted"`
414
+ * or `"workspace"` the URL is gated by LangSmith login instead and a
415
+ * {@link ServiceLoginUrl} is returned: no token, no expiry, browser only.
416
+ *
417
+ * A login grant is durable, so token mode is refused with 409 while one is
418
+ * in place, and `expiresInSeconds` does not apply to the login modes.
419
+ */
420
+ async serviceUrl(name, options) {
421
+ const { port, expiresInSeconds, access, signal } = options;
422
+ if (!Number.isInteger(port) || port < 1 || port > 65535) {
423
+ throw new LangSmithValidationError(`port must be between 1 and 65535 (got ${port})`, "port");
424
+ }
425
+ if (access !== undefined &&
426
+ access !== "restricted" &&
427
+ access !== "workspace") {
428
+ throw new LangSmithValidationError(`access must be "restricted" or "workspace" (got ${String(access)})`, "access");
429
+ }
430
+ const loginMode = access !== undefined;
431
+ if (expiresInSeconds !== undefined && loginMode) {
432
+ throw new LangSmithValidationError(`expiresInSeconds does not apply to access "${access}": a LangSmith ` +
433
+ "login URL carries no token and does not expire", "expiresInSeconds");
434
+ }
435
+ const url = this._boxUrl(name, "service-url");
436
+ const payload = { port };
437
+ if (!loginMode && expiresInSeconds !== undefined) {
438
+ payload.expires_in_seconds = expiresInSeconds;
439
+ }
440
+ if (access !== undefined) {
441
+ payload.access = access;
442
+ }
443
+ const response = await this._postJson(url, payload, { signal });
444
+ const data = await response.json();
445
+ if (loginMode) {
446
+ return new ServiceLoginUrl(data);
447
+ }
448
+ // Refresh with the token parameters only. Carrying options.signal over
449
+ // would tie every later refresh to the first request's lifetime, so a
450
+ // caller-supplied timeout signal would leave the URL unable to refresh.
451
+ return new ServiceUrl(data, () => this.serviceUrl(name, {
452
+ port,
453
+ expiresInSeconds,
454
+ }));
455
+ }
407
456
  async _postJson(url, body, options) {
408
457
  const response = await this._fetch(url, {
409
458
  method: "POST",
@@ -421,7 +470,7 @@ export class SandboxClient {
421
470
  const resolvedOptions = typeof snapshotIdOrOptions === "object" && snapshotIdOrOptions !== null
422
471
  ? snapshotIdOrOptions
423
472
  : options;
424
- const { snapshotName, name, timeout = 30, waitForReady = true, idleTtlSeconds, deleteAfterStopSeconds, vCpus, memBytes, fsCapacityBytes, mountConfig, proxyConfig, } = resolvedOptions;
473
+ const { snapshotName, name, timeout = 30, waitForReady = true, idleTtlSeconds, deleteAfterStopSeconds, vCpus, memBytes, fsCapacityBytes, mountConfig, proxyConfig, runConfig, accessDelegation, } = resolvedOptions;
425
474
  if (snapshotId && snapshotName) {
426
475
  throw new LangSmithValidationError("At most one of snapshotId or options.snapshotName may be set", "snapshotId");
427
476
  }
@@ -465,6 +514,12 @@ export class SandboxClient {
465
514
  if (proxyConfig !== undefined) {
466
515
  payload.proxy_config = proxyConfig;
467
516
  }
517
+ if (runConfig !== undefined) {
518
+ payload.run_config = runConfig;
519
+ }
520
+ if (accessDelegation !== undefined) {
521
+ payload.access_delegation = validateAccessDelegation(accessDelegation);
522
+ }
468
523
  const httpTimeout = waitForReady ? (timeout + 30) * 1000 : 30 * 1000;
469
524
  const response = await this._fetch(url, {
470
525
  method: "POST",
@@ -520,13 +575,14 @@ export class SandboxClient {
520
575
  const options = typeof newNameOrOptions === "string"
521
576
  ? { newName: newNameOrOptions }
522
577
  : newNameOrOptions;
523
- const { newName, idleTtlSeconds, deleteAfterStopSeconds, proxyConfig } = options;
578
+ const { newName, idleTtlSeconds, deleteAfterStopSeconds, proxyConfig, runConfig, } = options;
524
579
  validateTtl(idleTtlSeconds, "idleTtlSeconds");
525
580
  validateTtl(deleteAfterStopSeconds, "deleteAfterStopSeconds");
526
581
  if (newName === undefined &&
527
582
  idleTtlSeconds === undefined &&
528
583
  deleteAfterStopSeconds === undefined &&
529
- proxyConfig === undefined) {
584
+ proxyConfig === undefined &&
585
+ runConfig === undefined) {
530
586
  return this.getSandbox(name);
531
587
  }
532
588
  const url = this._boxUrl(name);
@@ -543,6 +599,9 @@ export class SandboxClient {
543
599
  if (proxyConfig !== undefined) {
544
600
  payload.proxy_config = proxyConfig;
545
601
  }
602
+ if (runConfig !== undefined) {
603
+ payload.run_config = runConfig;
604
+ }
546
605
  const response = await this._fetch(url, {
547
606
  method: "PATCH",
548
607
  headers: { "Content-Type": "application/json" },
@@ -727,7 +786,7 @@ export class SandboxClient {
727
786
  * @returns Snapshot in "ready" status.
728
787
  */
729
788
  async createSnapshot(name, dockerImage, fsCapacityBytes, options = {}) {
730
- const { registryId, timeout = 60, signal } = options;
789
+ const { registryId, runConfig, timeout = 60, signal } = options;
731
790
  const url = `${this._baseUrl}/snapshots`;
732
791
  const payload = {
733
792
  name,
@@ -737,6 +796,9 @@ export class SandboxClient {
737
796
  if (registryId !== undefined) {
738
797
  payload.registry_id = registryId;
739
798
  }
799
+ if (runConfig !== undefined) {
800
+ payload.run_config = runConfig;
801
+ }
740
802
  const response = await this._postJson(url, payload, { signal });
741
803
  const snapshot = (await response.json());
742
804
  return this.waitForSnapshot(snapshot.id, { timeout, signal });
@@ -829,7 +891,7 @@ export class SandboxClient {
829
891
  * @returns Snapshot in "ready" status.
830
892
  */
831
893
  async captureSnapshot(sandboxName, name, options = {}) {
832
- const { dockerImage, fsCapacityBytes, timeout = 60, signal } = options;
894
+ const { dockerImage, fsCapacityBytes, runConfig, timeout = 60, signal, } = options;
833
895
  const url = this._boxUrl(sandboxName, "snapshot");
834
896
  const payload = { name };
835
897
  if (dockerImage !== undefined) {
@@ -838,6 +900,9 @@ export class SandboxClient {
838
900
  if (fsCapacityBytes !== undefined) {
839
901
  payload.fs_capacity_bytes = fsCapacityBytes;
840
902
  }
903
+ if (runConfig !== undefined) {
904
+ payload.run_config = runConfig;
905
+ }
841
906
  const response = await this._postJson(url, payload, { signal });
842
907
  const snapshot = (await response.json());
843
908
  return this.waitForSnapshot(snapshot.id, { timeout, signal });
@@ -141,6 +141,18 @@ class CommandHandle {
141
141
  writable: true,
142
142
  value: void 0
143
143
  });
144
+ Object.defineProperty(this, "_stdinClosed", {
145
+ enumerable: true,
146
+ configurable: true,
147
+ writable: true,
148
+ value: void 0
149
+ });
150
+ Object.defineProperty(this, "_pty", {
151
+ enumerable: true,
152
+ configurable: true,
153
+ writable: true,
154
+ value: void 0
155
+ });
144
156
  this._stream = messageStream;
145
157
  this._control = control;
146
158
  this._sandbox = sandbox;
@@ -148,6 +160,8 @@ class CommandHandle {
148
160
  this._lastStderrOffset = options?.stderrOffset ?? 0;
149
161
  this._onStdout = options?.onStdout;
150
162
  this._onStderr = options?.onStderr;
163
+ this._stdinClosed = options?.stdinClosed ?? false;
164
+ this._pty = options?.pty ?? false;
151
165
  // New executions (no commandId): _ensureStarted reads "started".
152
166
  // Reconnections (commandId set): skip since reconnect streams
153
167
  // don't send a "started" message.
@@ -319,10 +333,28 @@ class CommandHandle {
319
333
  * Write data to the command's stdin.
320
334
  */
321
335
  sendInput(data) {
336
+ if (this._stdinClosed) {
337
+ throw new errors_js_1.LangSmithSandboxOperationError("stdin is closed for this command. Non-PTY commands close stdin by " +
338
+ "default so a command that reads it sees EOF instead of hanging; " +
339
+ "pass closeInput: false to run() to stream input into it.", "sendInput");
340
+ }
322
341
  if (this._control) {
323
342
  this._control.sendInput(data);
324
343
  }
325
344
  }
345
+ /**
346
+ * Half-close stdin so the command reads EOF.
347
+ *
348
+ * Idempotent, and a no-op under a PTY, where input and output share one
349
+ * terminal file descriptor and there is no write end to close -- send an
350
+ * EOT byte (0x04) with {@link sendInput} instead.
351
+ */
352
+ closeInput() {
353
+ if (this._pty || this._stdinClosed)
354
+ return;
355
+ this._stdinClosed = true;
356
+ this._control?.sendCloseStdin();
357
+ }
326
358
  /** Last known stdout byte offset (for manual reconnection). */
327
359
  get lastStdoutOffset() {
328
360
  return this._lastStdoutOffset;
@@ -344,6 +376,8 @@ class CommandHandle {
344
376
  return this._sandbox.reconnect(this._commandId, {
345
377
  stdoutOffset: this._lastStdoutOffset,
346
378
  stderrOffset: this._lastStderrOffset,
379
+ stdinClosed: this._stdinClosed,
380
+ pty: this._pty,
347
381
  });
348
382
  }
349
383
  }
@@ -48,6 +48,8 @@ export declare class CommandHandle {
48
48
  private _reconnectAttempts;
49
49
  private _onStdout?;
50
50
  private _onStderr?;
51
+ private _stdinClosed;
52
+ private _pty;
51
53
  /**
52
54
  * Read the 'started' message to populate commandId and pid.
53
55
  *
@@ -87,6 +89,14 @@ export declare class CommandHandle {
87
89
  * Write data to the command's stdin.
88
90
  */
89
91
  sendInput(data: string): void;
92
+ /**
93
+ * Half-close stdin so the command reads EOF.
94
+ *
95
+ * Idempotent, and a no-op under a PTY, where input and output share one
96
+ * terminal file descriptor and there is no write end to close -- send an
97
+ * EOT byte (0x04) with {@link sendInput} instead.
98
+ */
99
+ closeInput(): void;
90
100
  /** Last known stdout byte offset (for manual reconnection). */
91
101
  get lastStdoutOffset(): number;
92
102
  /** Last known stderr byte offset (for manual reconnection). */
@@ -138,6 +138,18 @@ export class CommandHandle {
138
138
  writable: true,
139
139
  value: void 0
140
140
  });
141
+ Object.defineProperty(this, "_stdinClosed", {
142
+ enumerable: true,
143
+ configurable: true,
144
+ writable: true,
145
+ value: void 0
146
+ });
147
+ Object.defineProperty(this, "_pty", {
148
+ enumerable: true,
149
+ configurable: true,
150
+ writable: true,
151
+ value: void 0
152
+ });
141
153
  this._stream = messageStream;
142
154
  this._control = control;
143
155
  this._sandbox = sandbox;
@@ -145,6 +157,8 @@ export class CommandHandle {
145
157
  this._lastStderrOffset = options?.stderrOffset ?? 0;
146
158
  this._onStdout = options?.onStdout;
147
159
  this._onStderr = options?.onStderr;
160
+ this._stdinClosed = options?.stdinClosed ?? false;
161
+ this._pty = options?.pty ?? false;
148
162
  // New executions (no commandId): _ensureStarted reads "started".
149
163
  // Reconnections (commandId set): skip since reconnect streams
150
164
  // don't send a "started" message.
@@ -316,10 +330,28 @@ export class CommandHandle {
316
330
  * Write data to the command's stdin.
317
331
  */
318
332
  sendInput(data) {
333
+ if (this._stdinClosed) {
334
+ throw new LangSmithSandboxOperationError("stdin is closed for this command. Non-PTY commands close stdin by " +
335
+ "default so a command that reads it sees EOF instead of hanging; " +
336
+ "pass closeInput: false to run() to stream input into it.", "sendInput");
337
+ }
319
338
  if (this._control) {
320
339
  this._control.sendInput(data);
321
340
  }
322
341
  }
342
+ /**
343
+ * Half-close stdin so the command reads EOF.
344
+ *
345
+ * Idempotent, and a no-op under a PTY, where input and output share one
346
+ * terminal file descriptor and there is no write end to close -- send an
347
+ * EOT byte (0x04) with {@link sendInput} instead.
348
+ */
349
+ closeInput() {
350
+ if (this._pty || this._stdinClosed)
351
+ return;
352
+ this._stdinClosed = true;
353
+ this._control?.sendCloseStdin();
354
+ }
323
355
  /** Last known stdout byte offset (for manual reconnection). */
324
356
  get lastStdoutOffset() {
325
357
  return this._lastStdoutOffset;
@@ -341,6 +373,8 @@ export class CommandHandle {
341
373
  return this._sandbox.reconnect(this._commandId, {
342
374
  stdoutOffset: this._lastStdoutOffset,
343
375
  stderrOffset: this._lastStderrOffset,
376
+ stdinClosed: this._stdinClosed,
377
+ pty: this._pty,
344
378
  });
345
379
  }
346
380
  }
@@ -14,6 +14,12 @@ exports.handleSandboxCreationError = handleSandboxCreationError;
14
14
  exports.throwIfNotReady = throwIfNotReady;
15
15
  exports.handleClientHttpError = handleClientHttpError;
16
16
  exports.handleSandboxHttpError = handleSandboxHttpError;
17
+ exports.assertRunConfigNotCombined = assertRunConfigNotCombined;
18
+ exports.resolveCloseInput = resolveCloseInput;
19
+ exports.buildRangeHeader = buildRangeHeader;
20
+ exports.fileStatFromResponse = fileStatFromResponse;
21
+ exports.fileChunkFromResponse = fileChunkFromResponse;
22
+ exports.handleFileHttpError = handleFileHttpError;
17
23
  const errors_js_1 = require("./errors.cjs");
18
24
  // =============================================================================
19
25
  // Input validation
@@ -268,3 +274,126 @@ async function handleSandboxHttpError(response) {
268
274
  }
269
275
  throw new errors_js_1.LangSmithSandboxError(message);
270
276
  }
277
+ // =============================================================================
278
+ // Run configuration
279
+ // =============================================================================
280
+ /**
281
+ * Reject a per-command `runConfig` alongside the deprecated `env`/`cwd`.
282
+ *
283
+ * The server answers 400 for a request carrying both spellings rather than
284
+ * letting one silently win, so refuse it here where the message can name the
285
+ * replacement.
286
+ */
287
+ function assertRunConfigNotCombined(options) {
288
+ if (options.runConfig !== undefined &&
289
+ (options.env !== undefined || options.cwd !== undefined)) {
290
+ throw new errors_js_1.LangSmithValidationError("Cannot combine runConfig with the deprecated env/cwd options. " +
291
+ "Use runConfig.env_vars and runConfig.work_dir instead.");
292
+ }
293
+ }
294
+ /**
295
+ * Whether to half-close stdin at spawn.
296
+ *
297
+ * Defaults on for a non-PTY command: a command that reads stdin otherwise
298
+ * blocks on a pipe nobody writes to until the timeout kills it. A PTY has no
299
+ * separate write end to close, so the server ignores the flag there.
300
+ */
301
+ function resolveCloseInput(closeInput, pty) {
302
+ if (pty)
303
+ return false;
304
+ return closeInput ?? true;
305
+ }
306
+ // =============================================================================
307
+ // Ranged downloads
308
+ // =============================================================================
309
+ /** Render a byte range as an RFC 9110 `Range` header value. */
310
+ function buildRangeHeader(options) {
311
+ const { start, end, suffixBytes } = options;
312
+ if (suffixBytes !== undefined) {
313
+ if (start !== undefined || end !== undefined) {
314
+ throw new errors_js_1.LangSmithValidationError("Cannot combine suffixBytes with start/end.");
315
+ }
316
+ if (suffixBytes <= 0) {
317
+ throw new errors_js_1.LangSmithValidationError("suffixBytes must be positive.");
318
+ }
319
+ return `bytes=-${suffixBytes}`;
320
+ }
321
+ if (start === undefined) {
322
+ throw new errors_js_1.LangSmithValidationError("Provide start (with optional end), or suffixBytes.");
323
+ }
324
+ if (start < 0) {
325
+ throw new errors_js_1.LangSmithValidationError("start must not be negative.");
326
+ }
327
+ if (end === undefined) {
328
+ return `bytes=${start}-`;
329
+ }
330
+ if (end < start) {
331
+ throw new errors_js_1.LangSmithValidationError("end must not precede start.");
332
+ }
333
+ return `bytes=${start}-${end}`;
334
+ }
335
+ /** Read the first byte offset and total size out of `Content-Range`. */
336
+ function parseContentRange(value) {
337
+ if (!value || !value.startsWith("bytes "))
338
+ return { start: 0 };
339
+ const spec = value.slice("bytes ".length).trim();
340
+ const [rangePart, totalPart] = spec.split("/");
341
+ const first = rangePart.split("-")[0].trim();
342
+ const start = /^\d+$/.test(first) ? Number(first) : 0;
343
+ const total = /^\d+$/.test((totalPart ?? "").trim())
344
+ ? Number(totalPart)
345
+ : undefined;
346
+ return { start, total };
347
+ }
348
+ /** Build a FileStat from a HEAD response's headers. */
349
+ function fileStatFromResponse(response) {
350
+ const length = response.headers.get("content-length");
351
+ return {
352
+ size_bytes: length && /^\d+$/.test(length) ? Number(length) : 0,
353
+ etag: response.headers.get("etag") ?? undefined,
354
+ last_modified: response.headers.get("last-modified") ?? undefined,
355
+ content_type: response.headers.get("content-type") ?? undefined,
356
+ };
357
+ }
358
+ /** Build a FileChunk from a ranged download response. */
359
+ async function fileChunkFromResponse(response) {
360
+ const etag = response.headers.get("etag") ?? undefined;
361
+ const lastModified = response.headers.get("last-modified") ?? undefined;
362
+ if (response.status === 304) {
363
+ return {
364
+ content: new Uint8Array(0),
365
+ etag,
366
+ start: 0,
367
+ partial: false,
368
+ unchanged: true,
369
+ last_modified: lastModified,
370
+ };
371
+ }
372
+ const content = new Uint8Array(await response.arrayBuffer());
373
+ const partial = response.status === 206;
374
+ // A stale If-Range answers 200 with the whole file; the caller has to
375
+ // restart rather than append, so report it from byte zero.
376
+ const { start, total } = partial
377
+ ? parseContentRange(response.headers.get("content-range"))
378
+ : { start: 0, total: content.length };
379
+ return {
380
+ content,
381
+ etag,
382
+ total_bytes: total,
383
+ start,
384
+ partial,
385
+ unchanged: false,
386
+ last_modified: lastModified,
387
+ };
388
+ }
389
+ /** Map a file-operation HTTP error, including the non-JSON 416. */
390
+ async function handleFileHttpError(response, path, sandboxName) {
391
+ if (response.status === 404) {
392
+ throw new errors_js_1.LangSmithResourceNotFoundError(`File '${path}' not found in sandbox '${sandboxName}'`, "file");
393
+ }
394
+ if (response.status === 416) {
395
+ throw new errors_js_1.LangSmithSandboxOperationError(`Requested range for '${path}' starts past the end of the file ` +
396
+ `(${response.headers.get("content-range") ?? "unknown size"}).`, "read");
397
+ }
398
+ return handleSandboxHttpError(response);
399
+ }
@@ -4,6 +4,7 @@
4
4
  * These functions are used to parse error responses and raise appropriate
5
5
  * exceptions. They contain no I/O operations.
6
6
  */
7
+ import type { FileChunk, FileStat, ReadRangeOptions, RunConfig } from "./types.js";
7
8
  /**
8
9
  * Validate TTL values for sandbox create/update (minute resolution).
9
10
  *
@@ -76,4 +77,32 @@ export declare function handleClientHttpError(response: Response): Promise<never
76
77
  * - 403 -> LangSmithSandboxOperationError (permission denied)
77
78
  */
78
79
  export declare function handleSandboxHttpError(response: Response): Promise<never>;
80
+ /**
81
+ * Reject a per-command `runConfig` alongside the deprecated `env`/`cwd`.
82
+ *
83
+ * The server answers 400 for a request carrying both spellings rather than
84
+ * letting one silently win, so refuse it here where the message can name the
85
+ * replacement.
86
+ */
87
+ export declare function assertRunConfigNotCombined(options: {
88
+ runConfig?: RunConfig;
89
+ env?: Record<string, string>;
90
+ cwd?: string;
91
+ }): void;
92
+ /**
93
+ * Whether to half-close stdin at spawn.
94
+ *
95
+ * Defaults on for a non-PTY command: a command that reads stdin otherwise
96
+ * blocks on a pipe nobody writes to until the timeout kills it. A PTY has no
97
+ * separate write end to close, so the server ignores the flag there.
98
+ */
99
+ export declare function resolveCloseInput(closeInput: boolean | undefined, pty: boolean): boolean;
100
+ /** Render a byte range as an RFC 9110 `Range` header value. */
101
+ export declare function buildRangeHeader(options: ReadRangeOptions): string;
102
+ /** Build a FileStat from a HEAD response's headers. */
103
+ export declare function fileStatFromResponse(response: Response): FileStat;
104
+ /** Build a FileChunk from a ranged download response. */
105
+ export declare function fileChunkFromResponse(response: Response): Promise<FileChunk>;
106
+ /** Map a file-operation HTTP error, including the non-JSON 416. */
107
+ export declare function handleFileHttpError(response: Response, path: string, sandboxName: string): Promise<never>;
79
108
  export {};