@ai-sdk/harness 1.0.117 → 1.0.119

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.
@@ -22,13 +22,19 @@ type SandboxChannelDebugEvent = {
22
22
  cause: unknown;
23
23
  };
24
24
  interface SandboxChannelReconnectOptions {
25
- /** Give up reconnecting after this many milliseconds. Default 30_000. */
25
+ /**
26
+ * Give up reconnecting after this many milliseconds, including connection
27
+ * establishment and backoff delays. Default 30_000.
28
+ */
26
29
  readonly maxElapsedMs?: number;
27
30
  /** First backoff delay. Default 50. */
28
31
  readonly initialDelayMs?: number;
29
32
  /** Backoff ceiling. Default 2_000. */
30
33
  readonly maxDelayMs?: number;
31
34
  }
35
+ interface SandboxChannelConnectOptions {
36
+ readonly abortSignal: AbortSignal;
37
+ }
32
38
  interface SandboxChannelOptions<TOut> {
33
39
  /**
34
40
  * Open a fresh WebSocket to the bridge and resolve once it is ready to carry
@@ -37,7 +43,7 @@ interface SandboxChannelOptions<TOut> {
37
43
  * every transient reconnect. Must reject if the connection cannot be
38
44
  * established.
39
45
  */
40
- connect: () => Promise<WebSocket>;
46
+ connect: (options: SandboxChannelConnectOptions) => Promise<WebSocket>;
41
47
  /** Schema validating inbound (bridge → host) frames. */
42
48
  outboundSchema: FlexibleSchema<TOut>;
43
49
  reconnect?: SandboxChannelReconnectOptions;
@@ -123,6 +129,8 @@ declare class SandboxChannel<TOut extends {
123
129
  private _lastSeenEventId;
124
130
  private readonly pendingSends;
125
131
  private dispatchChain;
132
+ private activeConnectAbortController;
133
+ private reconnectAbortController;
126
134
  constructor(options: SandboxChannelOptions<TOut>);
127
135
  /**
128
136
  * Highest bridge event `seq` this channel has observed. Persist it (e.g. via
@@ -174,6 +182,9 @@ declare class SandboxChannel<TOut extends {
174
182
  isClosed(): boolean;
175
183
  private wire;
176
184
  private reconnectLoop;
185
+ private connect;
186
+ private abortActiveConnect;
187
+ private failReconnect;
177
188
  private rawSend;
178
189
  private flushPending;
179
190
  private enqueue;
@@ -700,4 +711,9 @@ declare function resolveSandboxDefaultWorkingDirectory({ sandboxSession, abortSi
700
711
 
701
712
  declare function getRestrictedSandboxSession(sandboxSession: HarnessV1NetworkSandboxSession | Experimental_SandboxSession): Experimental_SandboxSession;
702
713
 
703
- export { type BridgeReadyErrorContext, type BridgeReadySource, type DiskLogRecoveryMode, type Experimental_BridgeUserMessageRequest, type Experimental_BridgeUserMessageResponse, type Experimental_BridgeUserMessageSubmitter, type OAuthCredential, type RefreshOAuthAccessTokenResult, SandboxChannel, type SandboxChannelDebugEvent, type SandboxChannelOptions, type SandboxChannelReconnectOptions, type SkillFilePathMode, type WaitForBridgeReadyOptions, type WaitForBridgeReadyResult, type WriteInstructionsOptions, type WriteInstructionsResult, type WriteSkillsOptions, type WriteSkillsResult, applyCredentialForwarding, classifyDiskLog, createBridgeErrorHandler, createBridgeStartupError, createBridgeToken, createCredentialRequestTransformation, createReadBridgeAsset, createSandboxCredentialEnvironment, drainBridgeProcessStream, experimental_createBridgeUserMessageSubmitter, formatBridgeError, forwardBridgeProcessStream, generateSandboxCredentialPlaceholder, getAiGatewayAuthFromEnv, getJwtExpiresAt, getRestrictedSandboxSession, isAccessTokenExpiringSoon, isHarnessAuthenticationEnvironment, isLinux, isMacOS, isSandboxCredentialPlaceholder, isWindows, logBridgeError, markBridgeStarting, maskSandboxCredentials, parseJwtPayload, readLinuxSecretServicePassword, readMacOSKeychainPassword, readWindowsCredentialManagerPassword, refreshOAuthAccessToken, resolveSandboxDefaultWorkingDirectory, resolveSandboxHomeDir, shellQuote, shouldResolveNativeSubscription, waitForBridgeReady, warnCredentialBrokeringUnavailable, withBridgeToken, writeInstructions, writeSkills };
714
+ declare function sleep({ ms, abortSignal, }: {
715
+ ms: number;
716
+ abortSignal?: AbortSignal;
717
+ }): Promise<void>;
718
+
719
+ export { type BridgeReadyErrorContext, type BridgeReadySource, type DiskLogRecoveryMode, type Experimental_BridgeUserMessageRequest, type Experimental_BridgeUserMessageResponse, type Experimental_BridgeUserMessageSubmitter, type OAuthCredential, type RefreshOAuthAccessTokenResult, SandboxChannel, type SandboxChannelConnectOptions, type SandboxChannelDebugEvent, type SandboxChannelOptions, type SandboxChannelReconnectOptions, type SkillFilePathMode, type WaitForBridgeReadyOptions, type WaitForBridgeReadyResult, type WriteInstructionsOptions, type WriteInstructionsResult, type WriteSkillsOptions, type WriteSkillsResult, applyCredentialForwarding, classifyDiskLog, createBridgeErrorHandler, createBridgeStartupError, createBridgeToken, createCredentialRequestTransformation, createReadBridgeAsset, createSandboxCredentialEnvironment, drainBridgeProcessStream, experimental_createBridgeUserMessageSubmitter, formatBridgeError, forwardBridgeProcessStream, generateSandboxCredentialPlaceholder, getAiGatewayAuthFromEnv, getJwtExpiresAt, getRestrictedSandboxSession, isAccessTokenExpiringSoon, isHarnessAuthenticationEnvironment, isLinux, isMacOS, isSandboxCredentialPlaceholder, isWindows, logBridgeError, markBridgeStarting, maskSandboxCredentials, parseJwtPayload, readLinuxSecretServicePassword, readMacOSKeychainPassword, readWindowsCredentialManagerPassword, refreshOAuthAccessToken, resolveSandboxDefaultWorkingDirectory, resolveSandboxHomeDir, shellQuote, shouldResolveNativeSubscription, sleep, waitForBridgeReady, warnCredentialBrokeringUnavailable, withBridgeToken, writeInstructions, writeSkills };
@@ -3,14 +3,95 @@ import {
3
3
  safeParseJSON,
4
4
  safeValidateTypes
5
5
  } from "@ai-sdk/provider-utils";
6
+
7
+ // src/utils/sleep.ts
8
+ function sleep({
9
+ ms,
10
+ abortSignal
11
+ }) {
12
+ return new Promise((resolve) => {
13
+ var _a;
14
+ if (abortSignal == null ? void 0 : abortSignal.aborted) {
15
+ resolve();
16
+ return;
17
+ }
18
+ let settled = false;
19
+ let onAbort = () => {
20
+ };
21
+ const finish = () => {
22
+ if (settled) return;
23
+ settled = true;
24
+ abortSignal == null ? void 0 : abortSignal.removeEventListener("abort", onAbort);
25
+ resolve();
26
+ };
27
+ const timer = setTimeout(finish, ms);
28
+ (_a = timer.unref) == null ? void 0 : _a.call(timer);
29
+ onAbort = () => {
30
+ clearTimeout(timer);
31
+ finish();
32
+ };
33
+ abortSignal == null ? void 0 : abortSignal.addEventListener("abort", onAbort, { once: true });
34
+ });
35
+ }
36
+
37
+ // src/utils/sandbox-channel.ts
6
38
  var sandboxChannelEventCheckpointSymbol = /* @__PURE__ */ Symbol.for(
7
39
  "vercel.ai.harness.sandboxChannelEventCheckpoint"
8
40
  );
9
- var sleep = (ms) => new Promise((resolve) => {
41
+ function getAbortReason({
42
+ abortSignal
43
+ }) {
10
44
  var _a;
11
- const t = setTimeout(resolve, ms);
12
- (_a = t.unref) == null ? void 0 : _a.call(t);
13
- });
45
+ return (_a = abortSignal.reason) != null ? _a : new Error("SandboxChannel connection aborted");
46
+ }
47
+ function closeWebSocket({ ws }) {
48
+ try {
49
+ const terminable = ws;
50
+ if (terminable.terminate != null) {
51
+ terminable.terminate();
52
+ } else {
53
+ ws.close();
54
+ }
55
+ } catch (e) {
56
+ try {
57
+ ws.close();
58
+ } catch (e2) {
59
+ }
60
+ }
61
+ }
62
+ async function awaitWebSocketConnection({
63
+ connection,
64
+ abortSignal
65
+ }) {
66
+ if (abortSignal.aborted) {
67
+ void connection.then((ws) => closeWebSocket({ ws })).catch(() => {
68
+ });
69
+ throw getAbortReason({ abortSignal });
70
+ }
71
+ let onAbort;
72
+ const aborted = new Promise((_, reject) => {
73
+ onAbort = () => reject(getAbortReason({ abortSignal }));
74
+ abortSignal.addEventListener("abort", onAbort, { once: true });
75
+ });
76
+ try {
77
+ const ws = await Promise.race([connection, aborted]);
78
+ if (abortSignal.aborted) {
79
+ closeWebSocket({ ws });
80
+ throw getAbortReason({ abortSignal });
81
+ }
82
+ return ws;
83
+ } catch (error) {
84
+ if (abortSignal.aborted) {
85
+ void connection.then((ws) => closeWebSocket({ ws })).catch(() => {
86
+ });
87
+ }
88
+ throw error;
89
+ } finally {
90
+ if (onAbort != null) {
91
+ abortSignal.removeEventListener("abort", onAbort);
92
+ }
93
+ }
94
+ }
14
95
  var SandboxChannel = class {
15
96
  constructor(options) {
16
97
  this.listeners = /* @__PURE__ */ new Map();
@@ -66,17 +147,27 @@ var SandboxChannel = class {
66
147
  if (this.terminal) {
67
148
  throw new Error("SandboxChannel: cannot open a closed channel.");
68
149
  }
69
- const ws = await this.connectThunk();
70
- this.wire(ws);
71
- this.ws = ws;
72
- this.connected = true;
73
- if (opts == null ? void 0 : opts.resume) {
74
- this.rawSend(
75
- JSON.stringify({
76
- type: "resume",
77
- lastSeenEventId: this._lastSeenEventId
78
- })
79
- );
150
+ const abortController = new AbortController();
151
+ this.activeConnectAbortController = abortController;
152
+ try {
153
+ const ws = await this.connect({
154
+ abortSignal: abortController.signal
155
+ });
156
+ this.wire(ws);
157
+ this.ws = ws;
158
+ this.connected = true;
159
+ if (opts == null ? void 0 : opts.resume) {
160
+ this.rawSend(
161
+ JSON.stringify({
162
+ type: "resume",
163
+ lastSeenEventId: this._lastSeenEventId
164
+ })
165
+ );
166
+ }
167
+ } finally {
168
+ if (this.activeConnectAbortController === abortController) {
169
+ this.activeConnectAbortController = void 0;
170
+ }
80
171
  }
81
172
  }
82
173
  on(type, listener) {
@@ -122,11 +213,13 @@ var SandboxChannel = class {
122
213
  */
123
214
  beginClose() {
124
215
  this.closing = true;
216
+ this.abortActiveConnect();
125
217
  }
126
218
  close() {
127
219
  var _a;
128
220
  if (this.terminal) return;
129
221
  this.closing = true;
222
+ this.abortActiveConnect();
130
223
  try {
131
224
  (_a = this.ws) == null ? void 0 : _a.close();
132
225
  } catch (e) {
@@ -157,6 +250,7 @@ var SandboxChannel = class {
157
250
  }
158
251
  this.suspended = true;
159
252
  this.closing = true;
253
+ this.abortActiveConnect();
160
254
  this.onClose(
161
255
  () => resolve(pinnedSuspensionCursor != null ? pinnedSuspensionCursor : this._lastSeenEventId)
162
256
  );
@@ -204,58 +298,129 @@ var SandboxChannel = class {
204
298
  async reconnectLoop() {
205
299
  var _a, _b, _c;
206
300
  if (this.terminal || this.closing) return;
207
- const start = Date.now();
301
+ const deadline = Date.now() + this.maxElapsedMs;
208
302
  let attempt = 0;
209
303
  let delay = this.initialDelayMs;
210
- while (!this.terminal && !this.closing) {
211
- attempt++;
212
- (_a = this.onDebug) == null ? void 0 : _a.call(this, {
213
- event: "reconnect-attempt",
214
- attempt,
215
- lastSeenEventId: this._lastSeenEventId
216
- });
217
- try {
218
- const ws = await this.connectThunk();
219
- if (this.terminal || this.closing) {
220
- try {
221
- ws.close();
222
- } catch (e) {
223
- }
304
+ const reconnectAbortController = new AbortController();
305
+ this.reconnectAbortController = reconnectAbortController;
306
+ try {
307
+ while (!this.terminal && !this.closing) {
308
+ const remaining = deadline - Date.now();
309
+ if (remaining <= 0) {
310
+ this.failReconnect({
311
+ attempts: attempt,
312
+ cause: new Error("Reconnect deadline expired")
313
+ });
224
314
  return;
225
315
  }
226
- this.wire(ws);
227
- this.ws = ws;
228
- this.connected = true;
229
- this.rawSend(
230
- JSON.stringify({
231
- type: "resume",
232
- lastSeenEventId: this._lastSeenEventId
233
- })
234
- );
235
- this.flushPending();
236
- for (const handler of this.onReconnectHandlers) handler();
237
- (_b = this.onDebug) == null ? void 0 : _b.call(this, {
238
- event: "reconnected",
316
+ attempt++;
317
+ (_a = this.onDebug) == null ? void 0 : _a.call(this, {
318
+ event: "reconnect-attempt",
239
319
  attempt,
240
320
  lastSeenEventId: this._lastSeenEventId
241
321
  });
242
- return;
243
- } catch (cause) {
244
- if (Date.now() - start >= this.maxElapsedMs) {
245
- this.finalizeClose(1006, "reconnect failed");
322
+ const abortController = new AbortController();
323
+ this.activeConnectAbortController = abortController;
324
+ const abortConnect = () => abortController.abort(reconnectAbortController.signal.reason);
325
+ reconnectAbortController.signal.addEventListener(
326
+ "abort",
327
+ abortConnect,
328
+ {
329
+ once: true
330
+ }
331
+ );
332
+ const timeout = setTimeout(() => {
333
+ abortController.abort(new Error("Reconnect deadline expired"));
334
+ }, remaining);
335
+ (_b = timeout.unref) == null ? void 0 : _b.call(timeout);
336
+ try {
337
+ const ws = await this.connect({
338
+ abortSignal: abortController.signal
339
+ });
340
+ if (this.terminal || this.closing) {
341
+ closeWebSocket({ ws });
342
+ return;
343
+ }
344
+ this.wire(ws);
345
+ this.ws = ws;
346
+ this.connected = true;
347
+ this.rawSend(
348
+ JSON.stringify({
349
+ type: "resume",
350
+ lastSeenEventId: this._lastSeenEventId
351
+ })
352
+ );
353
+ this.flushPending();
354
+ for (const handler of this.onReconnectHandlers) handler();
246
355
  (_c = this.onDebug) == null ? void 0 : _c.call(this, {
247
- event: "reconnect-failed",
248
- attempts: attempt,
249
- lastSeenEventId: this._lastSeenEventId,
250
- cause
356
+ event: "reconnected",
357
+ attempt,
358
+ lastSeenEventId: this._lastSeenEventId
251
359
  });
252
360
  return;
361
+ } catch (cause) {
362
+ if (this.terminal || this.closing || reconnectAbortController.signal.aborted) {
363
+ return;
364
+ }
365
+ const remainingAfterFailure = deadline - Date.now();
366
+ if (remainingAfterFailure <= 0) {
367
+ this.failReconnect({ attempts: attempt, cause });
368
+ return;
369
+ }
370
+ await sleep({
371
+ ms: Math.min(delay, remainingAfterFailure),
372
+ abortSignal: reconnectAbortController.signal
373
+ });
374
+ delay = Math.min(delay * 1.5, this.maxDelayMs);
375
+ } finally {
376
+ clearTimeout(timeout);
377
+ reconnectAbortController.signal.removeEventListener(
378
+ "abort",
379
+ abortConnect
380
+ );
381
+ if (this.activeConnectAbortController === abortController) {
382
+ this.activeConnectAbortController = void 0;
383
+ }
253
384
  }
254
- await sleep(delay);
255
- delay = Math.min(delay * 1.5, this.maxDelayMs);
385
+ }
386
+ } finally {
387
+ if (this.reconnectAbortController === reconnectAbortController) {
388
+ this.reconnectAbortController = void 0;
256
389
  }
257
390
  }
258
391
  }
392
+ connect({
393
+ abortSignal
394
+ }) {
395
+ return awaitWebSocketConnection({
396
+ connection: Promise.resolve().then(
397
+ () => this.connectThunk({ abortSignal })
398
+ ),
399
+ abortSignal
400
+ });
401
+ }
402
+ abortActiveConnect() {
403
+ var _a;
404
+ const controller = this.activeConnectAbortController;
405
+ this.activeConnectAbortController = void 0;
406
+ controller == null ? void 0 : controller.abort(new Error("SandboxChannel connection aborted"));
407
+ (_a = this.reconnectAbortController) == null ? void 0 : _a.abort(
408
+ new Error("SandboxChannel connection aborted")
409
+ );
410
+ }
411
+ failReconnect({
412
+ attempts,
413
+ cause
414
+ }) {
415
+ var _a;
416
+ this.finalizeClose(1006, "reconnect failed");
417
+ (_a = this.onDebug) == null ? void 0 : _a.call(this, {
418
+ event: "reconnect-failed",
419
+ attempts,
420
+ lastSeenEventId: this._lastSeenEventId,
421
+ cause
422
+ });
423
+ }
259
424
  rawSend(text) {
260
425
  if (this.connected && this.ws) {
261
426
  this.ws.send(text);
@@ -342,6 +507,7 @@ var SandboxChannel = class {
342
507
  }
343
508
  finalizeClose(code, reason) {
344
509
  if (this.terminal) return;
510
+ this.abortActiveConnect();
345
511
  this.terminal = true;
346
512
  this.connected = false;
347
513
  for (const h of this.onCloseHandlers) h(code, reason);
@@ -2315,6 +2481,7 @@ export {
2315
2481
  resolveSandboxHomeDir,
2316
2482
  shellQuote,
2317
2483
  shouldResolveNativeSubscription,
2484
+ sleep,
2318
2485
  waitForBridgeReady,
2319
2486
  warnCredentialBrokeringUnavailable,
2320
2487
  withBridgeToken,