@ai-sdk/harness-acp 1.0.55 → 1.0.56

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-sdk/harness-acp",
3
- "version": "1.0.55",
3
+ "version": "1.0.56",
4
4
  "type": "module",
5
5
  "license": "Apache-2.0",
6
6
  "sideEffects": false,
@@ -26,7 +26,7 @@
26
26
  }
27
27
  },
28
28
  "dependencies": {
29
- "@ai-sdk/harness": "1.0.117",
29
+ "@ai-sdk/harness": "1.0.118",
30
30
  "@ai-sdk/provider-utils": "5.0.45",
31
31
  "ws": "^8.21.0"
32
32
  },
@@ -5,6 +5,7 @@ import {
5
5
  type HarnessV1MintBridgeTokenCallback,
6
6
  type HarnessV1PortEndpoint,
7
7
  } from '@ai-sdk/harness';
8
+ import type { SandboxChannelReconnectOptions } from '@ai-sdk/harness/utils';
8
9
  import type { ToolSet } from '@ai-sdk/provider-utils';
9
10
  import { z } from 'zod/v4';
10
11
  import type { ACPAuthenticationMode, ACPClientApp } from './acp-auth';
@@ -45,6 +46,13 @@ export type ACPHarnessSettings<
45
46
  */
46
47
  readonly portEndpoint?: HarnessV1PortEndpoint;
47
48
  readonly startupTimeoutMs?: number;
49
+ /**
50
+ * Configures reconnection attempts after an established bridge connection
51
+ * drops. The reconnect window includes connection establishment and
52
+ * backoff delays. Defaults to 30 seconds with exponential backoff from 50
53
+ * milliseconds up to 2 seconds.
54
+ */
55
+ readonly reconnect?: SandboxChannelReconnectOptions;
48
56
  readonly clientApp?: ACPClientApp;
49
57
  readonly version?: ACPV1Settings['version'];
50
58
  readonly harnessId: ACPV1Settings['harnessId'];
@@ -192,6 +200,7 @@ export function createACP<
192
200
  port: settings.port,
193
201
  portEndpoint: settings.portEndpoint,
194
202
  startupTimeoutMs: settings.startupTimeoutMs,
203
+ reconnect: settings.reconnect,
195
204
  clientApp,
196
205
  lifecycleStateSchema: acpResumeStateSchema satisfies z.ZodType<{
197
206
  bridge?: ACPBridgeCoords;
@@ -34,6 +34,7 @@ import {
34
34
  withBridgeToken,
35
35
  writeInstructions,
36
36
  writeSkills,
37
+ type SandboxChannelReconnectOptions,
37
38
  } from '@ai-sdk/harness/utils';
38
39
  import {
39
40
  asSchema,
@@ -130,6 +131,7 @@ export function createACPV1<TBuiltinTools extends ToolSet = {}>({
130
131
  port: portOverride,
131
132
  portEndpoint: portEndpointOverride,
132
133
  startupTimeoutMs,
134
+ reconnect,
133
135
  clientApp,
134
136
  lifecycleStateSchema,
135
137
  }: {
@@ -138,6 +140,7 @@ export function createACPV1<TBuiltinTools extends ToolSet = {}>({
138
140
  port?: number;
139
141
  portEndpoint?: HarnessV1PortEndpoint;
140
142
  startupTimeoutMs?: number;
143
+ reconnect?: SandboxChannelReconnectOptions;
141
144
  clientApp: ACPClientApp;
142
145
  lifecycleStateSchema: NonNullable<
143
146
  HarnessV1<TBuiltinTools>['lifecycleStateSchema']
@@ -441,11 +444,13 @@ export function createACPV1<TBuiltinTools extends ToolSet = {}>({
441
444
  token: coords.token,
442
445
  });
443
446
  const attachChannel: ACPChannel = new SandboxChannel({
444
- connect: () => openWebSocket(attachEndpoint),
447
+ connect: ({ abortSignal }) =>
448
+ openWebSocket({ ...attachEndpoint, abortSignal }),
445
449
  outboundSchema: outboundMessageSchema,
446
450
  initialLastSeenEventId: coords.lastSeenEventId,
447
451
  onDiagnostic,
448
452
  onBridgeError,
453
+ reconnect,
449
454
  });
450
455
  await attachChannel.open(isContinue ? { resume: true } : undefined);
451
456
  return createSession({
@@ -700,13 +705,15 @@ export function createACPV1<TBuiltinTools extends ToolSet = {}>({
700
705
  });
701
706
  const bridgeEndpoint = withBridgeToken({ endpoint, token });
702
707
  const channel: ACPChannel = new SandboxChannel({
703
- connect: () => openWebSocket(bridgeEndpoint),
708
+ connect: ({ abortSignal }) =>
709
+ openWebSocket({ ...bridgeEndpoint, abortSignal }),
704
710
  outboundSchema: outboundMessageSchema,
705
711
  ...(respawnStrategy?.mode === 'disk-replay'
706
712
  ? { initialLastSeenEventId: respawnStrategy.afterSeq }
707
713
  : {}),
708
714
  onDiagnostic,
709
715
  onBridgeError,
716
+ reconnect,
710
717
  });
711
718
  await channel.open(
712
719
  respawnStrategy?.mode === 'disk-replay' ? { resume: true } : undefined,
@@ -1009,21 +1016,57 @@ async function resolveBridgeEndpoint({
1009
1016
  function openWebSocket({
1010
1017
  url,
1011
1018
  headers,
1012
- }: HarnessV1PortEndpoint): Promise<WebSocket> {
1019
+ abortSignal,
1020
+ }: HarnessV1PortEndpoint & { abortSignal: AbortSignal }): Promise<WebSocket> {
1013
1021
  return new Promise((resolve, reject) => {
1022
+ if (abortSignal.aborted) {
1023
+ reject(abortSignal.reason ?? new Error('WebSocket connection aborted'));
1024
+ return;
1025
+ }
1026
+
1014
1027
  const ws = new WebSocket(url, {
1015
1028
  headers: headers == null ? undefined : { ...headers },
1016
1029
  });
1017
- const onOpen = () => {
1030
+
1031
+ let settled = false;
1032
+ const cleanup = () => {
1033
+ abortSignal.removeEventListener('abort', onAbort);
1034
+ ws.off('open', onOpen);
1018
1035
  ws.off('error', onError);
1036
+ };
1037
+ const rejectWithCleanup = (error: unknown) => {
1038
+ if (settled) return;
1039
+ settled = true;
1040
+ cleanup();
1041
+ ws.once('error', () => {});
1042
+ try {
1043
+ ws.terminate();
1044
+ } catch {
1045
+ try {
1046
+ ws.close();
1047
+ } catch {
1048
+ // best-effort
1049
+ }
1050
+ }
1051
+ reject(error);
1052
+ };
1053
+ const onOpen = () => {
1054
+ if (settled) return;
1055
+ settled = true;
1056
+ cleanup();
1019
1057
  resolve(ws);
1020
1058
  };
1021
1059
  const onError = (error: Error) => {
1022
- ws.off('open', onOpen);
1023
- reject(error);
1060
+ rejectWithCleanup(error);
1024
1061
  };
1062
+ const onAbort = () =>
1063
+ rejectWithCleanup(
1064
+ abortSignal.reason ?? new Error('WebSocket connection aborted'),
1065
+ );
1066
+
1025
1067
  ws.once('open', onOpen);
1026
1068
  ws.once('error', onError);
1069
+ abortSignal.addEventListener('abort', onAbort, { once: true });
1027
1070
  });
1028
1071
  }
1029
1072