langsmith 0.8.7 → 0.8.8

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 (71) hide show
  1. package/dist/_openapi_client/client.cjs +8 -0
  2. package/dist/_openapi_client/client.d.ts +3 -0
  3. package/dist/_openapi_client/client.js +8 -0
  4. package/dist/_openapi_client/resources/annotation-queues/annotation-queues.cjs +147 -0
  5. package/dist/_openapi_client/resources/annotation-queues/annotation-queues.d.ts +372 -0
  6. package/dist/_openapi_client/resources/annotation-queues/annotation-queues.js +110 -0
  7. package/dist/_openapi_client/resources/annotation-queues/items.cjs +84 -0
  8. package/dist/_openapi_client/resources/annotation-queues/items.d.ts +207 -0
  9. package/dist/_openapi_client/resources/annotation-queues/items.js +80 -0
  10. package/dist/_openapi_client/resources/annotation-queues/runs.cjs +61 -0
  11. package/dist/_openapi_client/resources/annotation-queues/runs.d.ts +185 -0
  12. package/dist/_openapi_client/resources/annotation-queues/runs.js +57 -0
  13. package/dist/_openapi_client/resources/datasets/experiment-runs.cjs +1 -1
  14. package/dist/_openapi_client/resources/datasets/experiment-runs.js +1 -1
  15. package/dist/_openapi_client/resources/index.cjs +3 -1
  16. package/dist/_openapi_client/resources/index.d.ts +1 -0
  17. package/dist/_openapi_client/resources/index.js +1 -0
  18. package/dist/_openapi_client/resources/info.cjs +3 -1
  19. package/dist/_openapi_client/resources/info.d.ts +16 -22
  20. package/dist/_openapi_client/resources/info.js +3 -1
  21. package/dist/_openapi_client/resources/issues.cjs +2 -2
  22. package/dist/_openapi_client/resources/issues.js +2 -2
  23. package/dist/_openapi_client/resources/online-evaluators.cjs +7 -7
  24. package/dist/_openapi_client/resources/online-evaluators.d.ts +8 -0
  25. package/dist/_openapi_client/resources/online-evaluators.js +7 -7
  26. package/dist/_openapi_client/resources/public/runs.cjs +2 -2
  27. package/dist/_openapi_client/resources/public/runs.d.ts +1 -1
  28. package/dist/_openapi_client/resources/public/runs.js +2 -2
  29. package/dist/_openapi_client/resources/runs/runs.cjs +3 -3
  30. package/dist/_openapi_client/resources/runs/runs.d.ts +9 -4
  31. package/dist/_openapi_client/resources/runs/runs.js +3 -3
  32. package/dist/_openapi_client/resources/runs/share.cjs +2 -2
  33. package/dist/_openapi_client/resources/runs/share.js +2 -2
  34. package/dist/_openapi_client/resources/sandboxes/boxes.cjs +10 -10
  35. package/dist/_openapi_client/resources/sandboxes/boxes.d.ts +40 -0
  36. package/dist/_openapi_client/resources/sandboxes/boxes.js +10 -10
  37. package/dist/_openapi_client/resources/sandboxes/registries.cjs +5 -5
  38. package/dist/_openapi_client/resources/sandboxes/registries.js +5 -5
  39. package/dist/_openapi_client/resources/sandboxes/sandboxes.d.ts +17 -0
  40. package/dist/_openapi_client/resources/sandboxes/snapshots.cjs +4 -4
  41. package/dist/_openapi_client/resources/sandboxes/snapshots.d.ts +12 -0
  42. package/dist/_openapi_client/resources/sandboxes/snapshots.js +4 -4
  43. package/dist/_openapi_client/resources/threads.cjs +3 -3
  44. package/dist/_openapi_client/resources/threads.d.ts +2 -1
  45. package/dist/_openapi_client/resources/threads.js +3 -3
  46. package/dist/_openapi_client/resources/traces.cjs +2 -2
  47. package/dist/_openapi_client/resources/traces.d.ts +1 -1
  48. package/dist/_openapi_client/resources/traces.js +2 -2
  49. package/dist/client.cjs +57 -21
  50. package/dist/client.d.ts +58 -22
  51. package/dist/client.js +57 -21
  52. package/dist/index.cjs +1 -1
  53. package/dist/index.d.ts +1 -1
  54. package/dist/index.js +1 -1
  55. package/dist/sandbox/command_handle.cjs +4 -4
  56. package/dist/sandbox/command_handle.js +5 -5
  57. package/dist/sandbox/errors.cjs +16 -1
  58. package/dist/sandbox/errors.d.ts +11 -0
  59. package/dist/sandbox/errors.js +14 -0
  60. package/dist/sandbox/index.cjs +2 -1
  61. package/dist/sandbox/index.d.ts +1 -1
  62. package/dist/sandbox/index.js +1 -1
  63. package/dist/sandbox/sandbox.cjs +22 -4
  64. package/dist/sandbox/sandbox.js +24 -6
  65. package/dist/sandbox/ws_execute.cjs +75 -8
  66. package/dist/sandbox/ws_execute.d.ts +8 -0
  67. package/dist/sandbox/ws_execute.js +72 -8
  68. package/package.json +1 -1
  69. package/dist/utils/constants.cjs +0 -4
  70. package/dist/utils/constants.d.ts +0 -1
  71. package/dist/utils/constants.js +0 -1
@@ -351,6 +351,20 @@ export class LangSmithStreamEndedBeforeStartedError extends LangSmithSandboxOper
351
351
  this.name = "LangSmithStreamEndedBeforeStartedError";
352
352
  }
353
353
  }
354
+ /**
355
+ * Thrown when the socket fails or times out before the WebSocket handshake.
356
+ *
357
+ * Distinct from its parent because it is safely retryable: the execute frame
358
+ * was never sent, so re-issuing the same command_id cannot double-run a
359
+ * command. run() retries this with backoff; a plain connection error (a
360
+ * rejected handshake) is permanent and propagates immediately.
361
+ */
362
+ export class LangSmithSandboxConnectTimeoutError extends LangSmithSandboxConnectionError {
363
+ constructor(message) {
364
+ super(message);
365
+ this.name = "LangSmithSandboxConnectTimeoutError";
366
+ }
367
+ }
354
368
  /**
355
369
  * Raised when the sandbox server is reloading (close code 1001).
356
370
  *
@@ -29,7 +29,7 @@
29
29
  * @packageDocumentation
30
30
  */
31
31
  Object.defineProperty(exports, "__esModule", { value: true });
32
- exports.LangSmithDataplaneNotConfiguredError = exports.LangSmithCommandTimeoutError = exports.LangSmithSandboxOperationError = exports.LangSmithSandboxNotReadyError = exports.LangSmithSandboxCreationError = exports.LangSmithResourceCreationError = exports.LangSmithQuotaExceededError = exports.LangSmithValidationError = exports.LangSmithResourceNameConflictError = exports.LangSmithResourceAlreadyExistsError = exports.LangSmithResourceInUseError = exports.LangSmithResourceTimeoutError = exports.LangSmithResourceNotFoundError = exports.LangSmithSandboxServerReloadError = exports.LangSmithSandboxConnectionError = exports.LangSmithSandboxAuthenticationError = exports.LangSmithSandboxAPIError = exports.LangSmithSandboxError = exports.s3Mount = exports.mountConfig = exports.gitMount = exports.gcsMount = exports.workspaceSecret = exports.proxyConfig = exports.opaqueSecret = exports.gcpAuth = exports.awsAuth = exports.CommandHandle = exports.Sandbox = exports.SandboxClient = void 0;
32
+ exports.LangSmithDataplaneNotConfiguredError = exports.LangSmithCommandTimeoutError = exports.LangSmithSandboxOperationError = exports.LangSmithSandboxNotReadyError = exports.LangSmithSandboxCreationError = exports.LangSmithResourceCreationError = exports.LangSmithQuotaExceededError = exports.LangSmithValidationError = exports.LangSmithResourceNameConflictError = exports.LangSmithResourceAlreadyExistsError = exports.LangSmithResourceInUseError = exports.LangSmithResourceTimeoutError = exports.LangSmithResourceNotFoundError = exports.LangSmithSandboxServerReloadError = exports.LangSmithSandboxConnectTimeoutError = exports.LangSmithSandboxConnectionError = exports.LangSmithSandboxAuthenticationError = exports.LangSmithSandboxAPIError = exports.LangSmithSandboxError = exports.s3Mount = exports.mountConfig = exports.gitMount = exports.gcsMount = exports.workspaceSecret = exports.proxyConfig = exports.opaqueSecret = exports.gcpAuth = exports.awsAuth = exports.CommandHandle = exports.Sandbox = exports.SandboxClient = void 0;
33
33
  // Main classes
34
34
  var client_js_1 = require("./client.cjs");
35
35
  Object.defineProperty(exports, "SandboxClient", { enumerable: true, get: function () { return client_js_1.SandboxClient; } });
@@ -55,6 +55,7 @@ Object.defineProperty(exports, "LangSmithSandboxError", { enumerable: true, get:
55
55
  Object.defineProperty(exports, "LangSmithSandboxAPIError", { enumerable: true, get: function () { return errors_js_1.LangSmithSandboxAPIError; } });
56
56
  Object.defineProperty(exports, "LangSmithSandboxAuthenticationError", { enumerable: true, get: function () { return errors_js_1.LangSmithSandboxAuthenticationError; } });
57
57
  Object.defineProperty(exports, "LangSmithSandboxConnectionError", { enumerable: true, get: function () { return errors_js_1.LangSmithSandboxConnectionError; } });
58
+ Object.defineProperty(exports, "LangSmithSandboxConnectTimeoutError", { enumerable: true, get: function () { return errors_js_1.LangSmithSandboxConnectTimeoutError; } });
58
59
  Object.defineProperty(exports, "LangSmithSandboxServerReloadError", { enumerable: true, get: function () { return errors_js_1.LangSmithSandboxServerReloadError; } });
59
60
  // Resource errors (type-based with resourceType attribute)
60
61
  Object.defineProperty(exports, "LangSmithResourceNotFoundError", { enumerable: true, get: function () { return errors_js_1.LangSmithResourceNotFoundError; } });
@@ -33,4 +33,4 @@ export { CommandHandle } from "./command_handle.js";
33
33
  export { awsAuth, gcpAuth, opaqueSecret, proxyConfig, workspaceSecret, } from "./proxy_config.js";
34
34
  export { gcsMount, gitMount, mountConfig, s3Mount } from "./mounts.js";
35
35
  export type { ExecutionResult, OutputChunk, WsMessage, WsRunOptions, ResourceStatus, Snapshot, SandboxData, SandboxClientConfig, RunOptions, CreateSandboxOptions, SandboxAccessControl, SandboxAwsAuthRule, SandboxAwsMountAuthConfig, SandboxGcpAuthRule, SandboxGcpMountAuthConfig, SandboxMountAuth, SandboxMountAuthConfig, SandboxMountConfig, SandboxProxyConfig, SandboxProxyRule, SandboxProxySecret, SandboxMount, MountCacheConfig, GCSMountConfig, GCSMountSpec, GitMountConfig, GitMountRefSpec, GitMountSpec, S3MountConfig, S3MountSpec, CreateSnapshotOptions, CreateDockerfileSnapshotOptions, CaptureSnapshotOptions, ListSnapshotsOptions, WaitForSnapshotOptions, StartSandboxOptions, UpdateSandboxOptions, WaitForSandboxOptions, } from "./types.js";
36
- export { LangSmithSandboxError, LangSmithSandboxAPIError, LangSmithSandboxAuthenticationError, LangSmithSandboxConnectionError, LangSmithSandboxServerReloadError, LangSmithResourceNotFoundError, LangSmithResourceTimeoutError, LangSmithResourceInUseError, LangSmithResourceAlreadyExistsError, LangSmithResourceNameConflictError, LangSmithValidationError, LangSmithQuotaExceededError, LangSmithResourceCreationError, LangSmithSandboxCreationError, LangSmithSandboxNotReadyError, LangSmithSandboxOperationError, LangSmithCommandTimeoutError, LangSmithDataplaneNotConfiguredError, } from "./errors.js";
36
+ export { LangSmithSandboxError, LangSmithSandboxAPIError, LangSmithSandboxAuthenticationError, LangSmithSandboxConnectionError, LangSmithSandboxConnectTimeoutError, LangSmithSandboxServerReloadError, LangSmithResourceNotFoundError, LangSmithResourceTimeoutError, LangSmithResourceInUseError, LangSmithResourceAlreadyExistsError, LangSmithResourceNameConflictError, LangSmithValidationError, LangSmithQuotaExceededError, LangSmithResourceCreationError, LangSmithSandboxCreationError, LangSmithSandboxNotReadyError, LangSmithSandboxOperationError, LangSmithCommandTimeoutError, LangSmithDataplaneNotConfiguredError, } from "./errors.js";
@@ -36,7 +36,7 @@ export { gcsMount, gitMount, mountConfig, s3Mount } from "./mounts.js";
36
36
  // Errors
37
37
  export {
38
38
  // Base and connection errors
39
- LangSmithSandboxError, LangSmithSandboxAPIError, LangSmithSandboxAuthenticationError, LangSmithSandboxConnectionError, LangSmithSandboxServerReloadError,
39
+ LangSmithSandboxError, LangSmithSandboxAPIError, LangSmithSandboxAuthenticationError, LangSmithSandboxConnectionError, LangSmithSandboxConnectTimeoutError, LangSmithSandboxServerReloadError,
40
40
  // Resource errors (type-based with resourceType attribute)
41
41
  LangSmithResourceNotFoundError, LangSmithResourceTimeoutError, LangSmithResourceInUseError, LangSmithResourceAlreadyExistsError, LangSmithResourceNameConflictError,
42
42
  // Validation and quota errors
@@ -234,6 +234,7 @@ class Sandbox {
234
234
  // spawning a second one.
235
235
  const execCommandId = commandId ?? (0, uuid_js_1.uuid7)();
236
236
  let attempt = 0;
237
+ const deadline = (0, ws_execute_js_1.connectDeadline)();
237
238
  // eslint-disable-next-line no-constant-condition
238
239
  while (true) {
239
240
  const [stream, control] = await (0, ws_execute_js_1.runWsStream)(dataplaneUrl, this._client.getApiKey(), command, {
@@ -246,6 +247,7 @@ class Sandbox {
246
247
  killOnDisconnect,
247
248
  ttlSeconds,
248
249
  pty,
250
+ openTimeout: (0, ws_execute_js_1.openTimeoutFor)(deadline),
249
251
  ...(Object.keys(clientHeaders).length > 0
250
252
  ? { headers: clientHeaders }
251
253
  : {}),
@@ -259,16 +261,31 @@ class Sandbox {
259
261
  return handle;
260
262
  }
261
263
  catch (e) {
262
- // Idempotent re-issue (same command_id) after an early close.
263
- if (!(e instanceof errors_js_1.LangSmithStreamEndedBeforeStartedError)) {
264
+ // Idempotent re-issue (same command_id): neither an early close nor a
265
+ // failed connect can have started a second command.
266
+ if (!(e instanceof errors_js_1.LangSmithStreamEndedBeforeStartedError) &&
267
+ !(e instanceof errors_js_1.LangSmithSandboxConnectTimeoutError)) {
264
268
  throw e;
265
269
  }
266
270
  attempt++;
267
271
  if (attempt > command_handle_js_1.CommandHandle.MAX_AUTO_RECONNECTS) {
268
272
  throw e;
269
273
  }
270
- const delay = Math.min(command_handle_js_1.CommandHandle.BACKOFF_BASE * 2 ** (attempt - 1), command_handle_js_1.CommandHandle.BACKOFF_MAX);
274
+ let delay = Math.min(command_handle_js_1.CommandHandle.BACKOFF_BASE * 2 ** (attempt - 1), command_handle_js_1.CommandHandle.BACKOFF_MAX);
275
+ const remaining = (0, ws_execute_js_1.remainingBudget)(deadline);
276
+ if (remaining !== undefined) {
277
+ if (remaining <= 0) {
278
+ throw e;
279
+ }
280
+ delay = Math.min(delay, remaining);
281
+ }
271
282
  await new Promise((r) => setTimeout(r, delay * 1000));
283
+ // Never start an attempt with no budget left: openTimeoutFor would
284
+ // return 0, which ws forwards to Node as "no timeout at all".
285
+ const left = (0, ws_execute_js_1.remainingBudget)(deadline);
286
+ if (left !== undefined && left <= 0) {
287
+ throw e;
288
+ }
272
289
  }
273
290
  }
274
291
  }
@@ -426,8 +443,9 @@ class Sandbox {
426
443
  */
427
444
  async stop() {
428
445
  await this._client.stopSandbox(this.name);
446
+ // dataplane_url stays set: it is stable across stop/start and a request on
447
+ // it resumes the sandbox.
429
448
  this.status = "stopped";
430
- this.dataplane_url = undefined;
431
449
  }
432
450
  /**
433
451
  * Capture a snapshot from this sandbox.
@@ -2,10 +2,10 @@
2
2
  * Sandbox class for interacting with a specific sandbox instance.
3
3
  */
4
4
  import { uuid7 } from "../uuid.js";
5
- import { LangSmithDataplaneNotConfiguredError, LangSmithStreamEndedBeforeStartedError, } from "./errors.js";
5
+ import { LangSmithDataplaneNotConfiguredError, LangSmithSandboxConnectTimeoutError, LangSmithStreamEndedBeforeStartedError, } from "./errors.js";
6
6
  import { handleSandboxHttpError } from "./helpers.js";
7
7
  import { CommandHandle } from "./command_handle.js";
8
- import { isWsAvailable, reconnectWsStream, runWsStream } from "./ws_execute.js";
8
+ import { connectDeadline, isWsAvailable, openTimeoutFor, reconnectWsStream, remainingBudget, runWsStream, } from "./ws_execute.js";
9
9
  /**
10
10
  * Represents an active sandbox for running commands and file operations.
11
11
  *
@@ -231,6 +231,7 @@ export class Sandbox {
231
231
  // spawning a second one.
232
232
  const execCommandId = commandId ?? uuid7();
233
233
  let attempt = 0;
234
+ const deadline = connectDeadline();
234
235
  // eslint-disable-next-line no-constant-condition
235
236
  while (true) {
236
237
  const [stream, control] = await runWsStream(dataplaneUrl, this._client.getApiKey(), command, {
@@ -243,6 +244,7 @@ export class Sandbox {
243
244
  killOnDisconnect,
244
245
  ttlSeconds,
245
246
  pty,
247
+ openTimeout: openTimeoutFor(deadline),
246
248
  ...(Object.keys(clientHeaders).length > 0
247
249
  ? { headers: clientHeaders }
248
250
  : {}),
@@ -256,16 +258,31 @@ export class Sandbox {
256
258
  return handle;
257
259
  }
258
260
  catch (e) {
259
- // Idempotent re-issue (same command_id) after an early close.
260
- if (!(e instanceof LangSmithStreamEndedBeforeStartedError)) {
261
+ // Idempotent re-issue (same command_id): neither an early close nor a
262
+ // failed connect can have started a second command.
263
+ if (!(e instanceof LangSmithStreamEndedBeforeStartedError) &&
264
+ !(e instanceof LangSmithSandboxConnectTimeoutError)) {
261
265
  throw e;
262
266
  }
263
267
  attempt++;
264
268
  if (attempt > CommandHandle.MAX_AUTO_RECONNECTS) {
265
269
  throw e;
266
270
  }
267
- const delay = Math.min(CommandHandle.BACKOFF_BASE * 2 ** (attempt - 1), CommandHandle.BACKOFF_MAX);
271
+ let delay = Math.min(CommandHandle.BACKOFF_BASE * 2 ** (attempt - 1), CommandHandle.BACKOFF_MAX);
272
+ const remaining = remainingBudget(deadline);
273
+ if (remaining !== undefined) {
274
+ if (remaining <= 0) {
275
+ throw e;
276
+ }
277
+ delay = Math.min(delay, remaining);
278
+ }
268
279
  await new Promise((r) => setTimeout(r, delay * 1000));
280
+ // Never start an attempt with no budget left: openTimeoutFor would
281
+ // return 0, which ws forwards to Node as "no timeout at all".
282
+ const left = remainingBudget(deadline);
283
+ if (left !== undefined && left <= 0) {
284
+ throw e;
285
+ }
269
286
  }
270
287
  }
271
288
  }
@@ -423,8 +440,9 @@ export class Sandbox {
423
440
  */
424
441
  async stop() {
425
442
  await this._client.stopSandbox(this.name);
443
+ // dataplane_url stays set: it is stable across stop/start and a request on
444
+ // it resumes the sandbox.
426
445
  this.status = "stopped";
427
- this.dataplane_url = undefined;
428
446
  }
429
447
  /**
430
448
  * Capture a snapshot from this sandbox.
@@ -6,7 +6,10 @@
6
6
  * Install with: npm install ws
7
7
  */
8
8
  Object.defineProperty(exports, "__esModule", { value: true });
9
- exports.WSStreamControl = void 0;
9
+ exports.WSStreamControl = exports.WS_CONNECT_BUDGET = exports.WS_OPEN_TIMEOUT = void 0;
10
+ exports.connectDeadline = connectDeadline;
11
+ exports.remainingBudget = remainingBudget;
12
+ exports.openTimeoutFor = openTimeoutFor;
10
13
  exports.isWsAvailable = isWsAvailable;
11
14
  exports.buildWsUrl = buildWsUrl;
12
15
  exports.buildAuthHeaders = buildAuthHeaders;
@@ -14,6 +17,46 @@ exports.raiseForWsError = raiseForWsError;
14
17
  exports.runWsStream = runWsStream;
15
18
  exports.reconnectWsStream = reconnectWsStream;
16
19
  const errors_js_1 = require("./errors.cjs");
20
+ const env_js_1 = require("../utils/env.cjs");
21
+ /** Read a WebSocket timeout override, in seconds. `<= 0` disables it. */
22
+ function envTimeout(name, defaultSeconds) {
23
+ const raw = (0, env_js_1.getLangSmithEnvironmentVariable)(name);
24
+ if (raw === undefined || raw.trim() === "")
25
+ return defaultSeconds;
26
+ const value = Number(raw);
27
+ if (!Number.isFinite(value)) {
28
+ console.warn(`Ignoring invalid LANGSMITH_${name}=${raw}, using ${defaultSeconds}`);
29
+ return defaultSeconds;
30
+ }
31
+ return value > 0 ? value : undefined;
32
+ }
33
+ // A slow cold start is recovered by the retry loop in run(), not by waiting here.
34
+ exports.WS_OPEN_TIMEOUT = envTimeout("SANDBOX_WS_TIMEOUT_OPEN", 30);
35
+ // Ceiling on the whole connect phase. Without it, retrying a blackholed
36
+ // handshake costs MAX_AUTO_RECONNECTS + 1 full open timeouts plus backoff.
37
+ exports.WS_CONNECT_BUDGET = envTimeout("SANDBOX_WS_TIMEOUT_CONNECT_BUDGET", 120);
38
+ function nowSeconds() {
39
+ return performance.now() / 1000;
40
+ }
41
+ /** Instant after which connect attempts must stop, if bounded. */
42
+ function connectDeadline() {
43
+ return exports.WS_CONNECT_BUDGET === undefined
44
+ ? undefined
45
+ : nowSeconds() + exports.WS_CONNECT_BUDGET;
46
+ }
47
+ /** Seconds left in the connect budget, or undefined when unbounded. */
48
+ function remainingBudget(deadline) {
49
+ return deadline === undefined ? undefined : deadline - nowSeconds();
50
+ }
51
+ /** Per-attempt open timeout, clamped so it cannot outlive `deadline`. */
52
+ function openTimeoutFor(deadline) {
53
+ if (deadline === undefined)
54
+ return exports.WS_OPEN_TIMEOUT;
55
+ const remaining = Math.max(deadline - nowSeconds(), 0);
56
+ return exports.WS_OPEN_TIMEOUT === undefined
57
+ ? remaining
58
+ : Math.min(exports.WS_OPEN_TIMEOUT, remaining);
59
+ }
17
60
  // Load the optional 'ws' package once, at module load, instead of importing on
18
61
  // every call. Resolves to the WebSocket constructor, or null if 'ws' isn't
19
62
  // installed. run() reads isWsAvailable() to choose WS vs the HTTP fallback.
@@ -140,18 +183,42 @@ function raiseForWsError(msg, commandId = "") {
140
183
  /**
141
184
  * Create a ws WebSocket connection and return a promise that resolves when open
142
185
  * or rejects on error.
186
+ *
187
+ * A socket-level failure rejects with the retryable connect-timeout error; a
188
+ * non-101 response rejects with the permanent connection error.
143
189
  */
144
- async function connectWs(url, headers) {
190
+ async function connectWs(url, headers, openTimeout = exports.WS_OPEN_TIMEOUT) {
145
191
  const { WebSocket: WS } = await ensureWs();
146
192
  return new Promise((resolve, reject) => {
147
- const ws = new WS(url, { headers });
193
+ const ws = new WS(url, {
194
+ headers,
195
+ handshakeTimeout: openTimeout === undefined ? undefined : openTimeout * 1000,
196
+ });
197
+ let settled = false;
148
198
  ws.on("open", () => {
149
- ws.removeAllListeners("error");
199
+ if (settled)
200
+ return;
201
+ settled = true;
150
202
  resolve(ws);
151
203
  });
204
+ // Attaching this suppresses ws's own "error" for a non-101 response, so the
205
+ // rejected upgrade cannot be misreported as a retryable socket failure. ws
206
+ // leaves cleanup to the handler once a listener is present.
207
+ ws.on("unexpected-response", (req, res) => {
208
+ res.resume?.();
209
+ req.destroy?.();
210
+ if (settled)
211
+ return;
212
+ settled = true;
213
+ reject(new errors_js_1.LangSmithSandboxConnectionError(`WebSocket upgrade to ${url} rejected by server (HTTP ${res.statusCode})`));
214
+ });
215
+ // Stays attached after settling so a late error has a listener and cannot
216
+ // become an unhandled 'error' event.
152
217
  ws.on("error", (err) => {
153
- ws.removeAllListeners("open");
154
- reject(new errors_js_1.LangSmithSandboxConnectionError(`Failed to connect to sandbox WebSocket: ${err.message}`));
218
+ if (settled)
219
+ return;
220
+ settled = true;
221
+ reject(new errors_js_1.LangSmithSandboxConnectTimeoutError(`Failed to connect to sandbox WebSocket: ${err.message}`));
155
222
  });
156
223
  });
157
224
  }
@@ -249,14 +316,14 @@ async function* readWsMessages(ws) {
249
316
  * data arrives in addition to yielding the messages.
250
317
  */
251
318
  async function runWsStream(dataplaneUrl, apiKey, command, options = {}) {
252
- const { timeout = 60, env, cwd, shell = "/bin/bash", onStdout, onStderr, commandId, idleTimeout = 300, killOnDisconnect = false, ttlSeconds = 600, pty, headers: extraHeaders, } = options;
319
+ 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;
253
320
  const wsUrl = buildWsUrl(dataplaneUrl);
254
321
  const headers = buildAuthHeaders(apiKey, extraHeaders);
255
322
  const control = new WSStreamControl();
256
323
  async function* stream() {
257
324
  let ws;
258
325
  try {
259
- ws = await connectWs(wsUrl, headers);
326
+ ws = await connectWs(wsUrl, headers, openTimeout);
260
327
  control._bind(ws);
261
328
  // Send execute request
262
329
  const payload = {
@@ -5,6 +5,14 @@
5
5
  * Install with: npm install ws
6
6
  */
7
7
  import type { WsMessage, WsRunOptions } from "./types.js";
8
+ export declare const WS_OPEN_TIMEOUT: number | undefined;
9
+ export declare const WS_CONNECT_BUDGET: number | undefined;
10
+ /** Instant after which connect attempts must stop, if bounded. */
11
+ export declare function connectDeadline(): number | undefined;
12
+ /** Seconds left in the connect budget, or undefined when unbounded. */
13
+ export declare function remainingBudget(deadline: number | undefined): number | undefined;
14
+ /** Per-attempt open timeout, clamped so it cannot outlive `deadline`. */
15
+ export declare function openTimeoutFor(deadline: number | undefined): number | undefined;
8
16
  type WsWebSocket = any;
9
17
  /** Whether the optional `ws` package could be loaded (resolved once). */
10
18
  export declare function isWsAvailable(): Promise<boolean>;
@@ -4,7 +4,47 @@
4
4
  * Uses the `ws` npm package (optional peer dependency).
5
5
  * Install with: npm install ws
6
6
  */
7
- import { LangSmithCommandTimeoutError, LangSmithSandboxConnectionError, LangSmithSandboxOperationError, LangSmithSandboxServerReloadError, } from "./errors.js";
7
+ import { LangSmithCommandTimeoutError, LangSmithSandboxConnectionError, LangSmithSandboxConnectTimeoutError, LangSmithSandboxOperationError, LangSmithSandboxServerReloadError, } from "./errors.js";
8
+ import { getLangSmithEnvironmentVariable } from "../utils/env.js";
9
+ /** Read a WebSocket timeout override, in seconds. `<= 0` disables it. */
10
+ function envTimeout(name, defaultSeconds) {
11
+ const raw = getLangSmithEnvironmentVariable(name);
12
+ if (raw === undefined || raw.trim() === "")
13
+ return defaultSeconds;
14
+ const value = Number(raw);
15
+ if (!Number.isFinite(value)) {
16
+ console.warn(`Ignoring invalid LANGSMITH_${name}=${raw}, using ${defaultSeconds}`);
17
+ return defaultSeconds;
18
+ }
19
+ return value > 0 ? value : undefined;
20
+ }
21
+ // A slow cold start is recovered by the retry loop in run(), not by waiting here.
22
+ export const WS_OPEN_TIMEOUT = envTimeout("SANDBOX_WS_TIMEOUT_OPEN", 30);
23
+ // Ceiling on the whole connect phase. Without it, retrying a blackholed
24
+ // handshake costs MAX_AUTO_RECONNECTS + 1 full open timeouts plus backoff.
25
+ export const WS_CONNECT_BUDGET = envTimeout("SANDBOX_WS_TIMEOUT_CONNECT_BUDGET", 120);
26
+ function nowSeconds() {
27
+ return performance.now() / 1000;
28
+ }
29
+ /** Instant after which connect attempts must stop, if bounded. */
30
+ export function connectDeadline() {
31
+ return WS_CONNECT_BUDGET === undefined
32
+ ? undefined
33
+ : nowSeconds() + WS_CONNECT_BUDGET;
34
+ }
35
+ /** Seconds left in the connect budget, or undefined when unbounded. */
36
+ export function remainingBudget(deadline) {
37
+ return deadline === undefined ? undefined : deadline - nowSeconds();
38
+ }
39
+ /** Per-attempt open timeout, clamped so it cannot outlive `deadline`. */
40
+ export function openTimeoutFor(deadline) {
41
+ if (deadline === undefined)
42
+ return WS_OPEN_TIMEOUT;
43
+ const remaining = Math.max(deadline - nowSeconds(), 0);
44
+ return WS_OPEN_TIMEOUT === undefined
45
+ ? remaining
46
+ : Math.min(WS_OPEN_TIMEOUT, remaining);
47
+ }
8
48
  // Load the optional 'ws' package once, at module load, instead of importing on
9
49
  // every call. Resolves to the WebSocket constructor, or null if 'ws' isn't
10
50
  // installed. run() reads isWsAvailable() to choose WS vs the HTTP fallback.
@@ -130,18 +170,42 @@ export function raiseForWsError(msg, commandId = "") {
130
170
  /**
131
171
  * Create a ws WebSocket connection and return a promise that resolves when open
132
172
  * or rejects on error.
173
+ *
174
+ * A socket-level failure rejects with the retryable connect-timeout error; a
175
+ * non-101 response rejects with the permanent connection error.
133
176
  */
134
- async function connectWs(url, headers) {
177
+ async function connectWs(url, headers, openTimeout = WS_OPEN_TIMEOUT) {
135
178
  const { WebSocket: WS } = await ensureWs();
136
179
  return new Promise((resolve, reject) => {
137
- const ws = new WS(url, { headers });
180
+ const ws = new WS(url, {
181
+ headers,
182
+ handshakeTimeout: openTimeout === undefined ? undefined : openTimeout * 1000,
183
+ });
184
+ let settled = false;
138
185
  ws.on("open", () => {
139
- ws.removeAllListeners("error");
186
+ if (settled)
187
+ return;
188
+ settled = true;
140
189
  resolve(ws);
141
190
  });
191
+ // Attaching this suppresses ws's own "error" for a non-101 response, so the
192
+ // rejected upgrade cannot be misreported as a retryable socket failure. ws
193
+ // leaves cleanup to the handler once a listener is present.
194
+ ws.on("unexpected-response", (req, res) => {
195
+ res.resume?.();
196
+ req.destroy?.();
197
+ if (settled)
198
+ return;
199
+ settled = true;
200
+ reject(new LangSmithSandboxConnectionError(`WebSocket upgrade to ${url} rejected by server (HTTP ${res.statusCode})`));
201
+ });
202
+ // Stays attached after settling so a late error has a listener and cannot
203
+ // become an unhandled 'error' event.
142
204
  ws.on("error", (err) => {
143
- ws.removeAllListeners("open");
144
- reject(new LangSmithSandboxConnectionError(`Failed to connect to sandbox WebSocket: ${err.message}`));
205
+ if (settled)
206
+ return;
207
+ settled = true;
208
+ reject(new LangSmithSandboxConnectTimeoutError(`Failed to connect to sandbox WebSocket: ${err.message}`));
145
209
  });
146
210
  });
147
211
  }
@@ -239,14 +303,14 @@ async function* readWsMessages(ws) {
239
303
  * data arrives in addition to yielding the messages.
240
304
  */
241
305
  export async function runWsStream(dataplaneUrl, apiKey, command, options = {}) {
242
- const { timeout = 60, env, cwd, shell = "/bin/bash", onStdout, onStderr, commandId, idleTimeout = 300, killOnDisconnect = false, ttlSeconds = 600, pty, headers: extraHeaders, } = options;
306
+ 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;
243
307
  const wsUrl = buildWsUrl(dataplaneUrl);
244
308
  const headers = buildAuthHeaders(apiKey, extraHeaders);
245
309
  const control = new WSStreamControl();
246
310
  async function* stream() {
247
311
  let ws;
248
312
  try {
249
- ws = await connectWs(wsUrl, headers);
313
+ ws = await connectWs(wsUrl, headers, openTimeout);
250
314
  control._bind(ws);
251
315
  // Send execute request
252
316
  const payload = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "langsmith",
3
- "version": "0.8.7",
3
+ "version": "0.8.8",
4
4
  "description": "Client library to connect to the LangSmith Observability and Evaluation Platform.",
5
5
  "packageManager": "pnpm@10.33.0",
6
6
  "files": [
@@ -1,4 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports._MIN_BACKEND_VERSION = void 0;
4
- exports._MIN_BACKEND_VERSION = "0.16.14rc1";
@@ -1 +0,0 @@
1
- export declare const _MIN_BACKEND_VERSION = "0.16.14rc1";
@@ -1 +0,0 @@
1
- export const _MIN_BACKEND_VERSION = "0.16.14rc1";