@ai-sdk/harness-claude-code 1.0.167 → 1.0.169

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-claude-code",
3
- "version": "1.0.167",
3
+ "version": "1.0.169",
4
4
  "type": "module",
5
5
  "license": "Apache-2.0",
6
6
  "sideEffects": false,
@@ -26,9 +26,9 @@
26
26
  }
27
27
  },
28
28
  "dependencies": {
29
- "@ai-sdk/harness": "1.0.163",
30
- "@ai-sdk/provider": "4.0.33",
31
- "@ai-sdk/provider-utils": "5.0.67",
29
+ "@ai-sdk/harness": "1.0.165",
30
+ "@ai-sdk/provider": "4.0.34",
31
+ "@ai-sdk/provider-utils": "5.0.69",
32
32
  "ws": "^8.21.0"
33
33
  },
34
34
  "peerDependencies": {
@@ -194,7 +194,14 @@ export function createEmitStreamEvent({
194
194
  // Emit `stream-start` once, on the first message, carrying the model the
195
195
  // CLI resolved to (the `system`/`init` message reports it — this is the
196
196
  // default model when none was configured).
197
- if (!streamStarted) {
197
+ /*
198
+ * `command_lifecycle` frames are skipped here. The CLI's stdin reader
199
+ * forwards them (e.g. `queued` and `started` for the prompt) straight to
200
+ * the output stream, independently of the engine that yields
201
+ * `system`/`init` at the start of each turn, so they can arrive before
202
+ * `init`. They produce no stream parts.
203
+ */
204
+ if (!streamStarted && type !== 'command_lifecycle') {
198
205
  const initModel =
199
206
  type === 'system' &&
200
207
  msg.subtype === 'init' &&
@@ -16,7 +16,6 @@ import {
16
16
  type HarnessV1ResponseFormat,
17
17
  type HarnessV1PortEndpoint,
18
18
  type HarnessV1ResumeSessionState,
19
- type HarnessV1NetworkSandboxSession,
20
19
  type HarnessV1Session,
21
20
  type HarnessV1Skill,
22
21
  type HarnessV1StreamPart,
@@ -28,6 +27,7 @@ import {
28
27
  classifyDiskLog,
29
28
  resolveSandboxCredentialEnvironment,
30
29
  createBridgeToken,
30
+ createBridgeWebSocketConnect,
31
31
  experimental_createBridgeUserMessageSubmitter,
32
32
  createBridgeErrorHandler,
33
33
  createBridgeStartupError,
@@ -36,18 +36,20 @@ import {
36
36
  forwardBridgeProcessStream,
37
37
  getRestrictedSandboxSession,
38
38
  markBridgeStarting,
39
+ resolveBridgeEndpoint,
40
+ resolveBridgePort,
39
41
  resolveSandboxHomeDir,
40
42
  SandboxChannel,
41
43
  shellQuote,
42
- sleep,
44
+ validateBridgeSandboxSettings,
43
45
  warnCredentialBrokeringUnavailable,
44
46
  waitForBridgeReady,
45
- withBridgeToken,
46
47
  writeSkills,
48
+ type BridgeHello,
49
+ type SandboxChannelDebugEvent,
47
50
  type SandboxChannelReconnectOptions,
48
51
  } from '@ai-sdk/harness/utils';
49
52
  import {
50
- safeParseJSON,
51
53
  tool,
52
54
  type Experimental_SandboxSession as SandboxSession,
53
55
  type Experimental_SandboxProcess,
@@ -148,6 +150,14 @@ export type ClaudeCodeHarnessSettings = {
148
150
  * milliseconds up to 2 seconds.
149
151
  */
150
152
  readonly reconnect?: SandboxChannelReconnectOptions;
153
+ /**
154
+ * Called with bridge connection lifecycle events: reconnect attempts,
155
+ * successful reconnects, and `reconnect-failed` once the connection is
156
+ * closed for good. Hosts that cache idle sessions can use it to reattach
157
+ * before sending the next prompt. The `cause` of a failed reconnect can
158
+ * contain connection details, so avoid logging it as is.
159
+ */
160
+ readonly onDebug?: (event: SandboxChannelDebugEvent) => void;
151
161
  /**
152
162
  * Creates the authentication token used by the sandbox bridge. Defaults to
153
163
  * a random 32-byte hexadecimal token.
@@ -862,18 +872,13 @@ export function createClaudeCode(
862
872
  const toolSafeSandboxSession =
863
873
  getRestrictedSandboxSession(sandboxSession);
864
874
  const sandboxId = 'id' in sandboxSession ? sandboxSession.id : undefined;
865
- validateBasicSandboxSettings({
875
+ validateBridgeSandboxSettings({
866
876
  sandboxSession,
867
- port: settings.port,
868
- portEndpoint: settings.portEndpoint,
877
+ sandboxId,
878
+ portOverride: settings.port,
879
+ portEndpointOverride: settings.portEndpoint,
880
+ mintBridgeToken: settings.mintBridgeToken,
869
881
  });
870
- if (settings.mintBridgeToken != null && sandboxId == null) {
871
- throw new HarnessCapabilityUnsupportedError({
872
- harnessId: 'claude-code',
873
- message:
874
- 'The Claude Code harness cannot use `mintBridgeToken` with a sandbox session that does not expose an id.',
875
- });
876
- }
877
882
  const lifecycleState = startOpts.continueFrom ?? startOpts.resumeFrom;
878
883
  const isResume = lifecycleState != null;
879
884
  const isContinue = startOpts.continueFrom != null;
@@ -1000,26 +1005,10 @@ export function createClaudeCode(
1000
1005
  sessionId: startOpts.sessionId,
1001
1006
  });
1002
1007
  let supportsUserMessageResponses = false;
1003
-
1004
- // Builds the `connect` thunk a `SandboxChannel` re-invokes on every
1005
- // (re)connect: open the socket, then wait for `bridge-hello` so the
1006
- // end-to-end link is proven live before any frame is sent.
1007
- const buildConnect =
1008
- (endpoint: HarnessV1PortEndpoint) =>
1009
- async ({
1010
- abortSignal,
1011
- }: {
1012
- abortSignal: AbortSignal;
1013
- }): Promise<WebSocket> => {
1014
- return openBridgeWebSocket({
1015
- endpoint,
1016
- timeoutMs,
1017
- abortSignal,
1018
- onHello: supportsResponses => {
1019
- supportsUserMessageResponses = supportsResponses;
1020
- },
1021
- });
1022
- };
1008
+ const onHello = (hello: BridgeHello) => {
1009
+ supportsUserMessageResponses =
1010
+ hello.capabilities?.experimental_userMessageResponses === true;
1011
+ };
1023
1012
 
1024
1013
  /*
1025
1014
  * Rung 1 — ATTACH. When lifecycle state carries live bridge coordinates,
@@ -1031,22 +1020,24 @@ export function createClaudeCode(
1031
1020
  */
1032
1021
  if (coords) {
1033
1022
  try {
1034
- const endpoint = await resolveBridgeEndpoint({
1023
+ const attachEndpoint = await resolveBridgeEndpoint({
1035
1024
  sandboxSession,
1036
- override: settings.portEndpoint,
1037
1025
  port: coords.port,
1038
- });
1039
- const attachEndpoint = withBridgeToken({
1040
- endpoint,
1026
+ portEndpointOverride: settings.portEndpoint,
1041
1027
  token: coords.token,
1042
1028
  });
1043
1029
  const attachChannel: ClaudeCodeChannel = new SandboxChannel({
1044
- connect: buildConnect(attachEndpoint),
1030
+ connect: createBridgeWebSocketConnect({
1031
+ endpoint: attachEndpoint,
1032
+ webSocketConstructor: WebSocket,
1033
+ onHello,
1034
+ }),
1045
1035
  outboundSchema: outboundMessageSchema,
1046
1036
  initialLastSeenEventId: coords.lastSeenEventId,
1047
1037
  onDiagnostic,
1048
1038
  onBridgeError,
1049
1039
  reconnect: settings.reconnect,
1040
+ onDebug: settings.onDebug,
1050
1041
  });
1051
1042
  const finishListenerAttachment = isContinue
1052
1043
  ? attachChannel.beginListenerAttachment()
@@ -1118,7 +1109,7 @@ export function createClaudeCode(
1118
1109
 
1119
1110
  const port = resolveBridgePort({
1120
1111
  sandboxSession,
1121
- override: settings.port,
1112
+ portOverride: settings.port,
1122
1113
  });
1123
1114
  const token =
1124
1115
  settings.mintBridgeToken == null
@@ -1191,19 +1182,26 @@ export function createClaudeCode(
1191
1182
  });
1192
1183
  void drainBridgeProcessStream(proc.stdout);
1193
1184
 
1194
- const endpoint = await resolveBridgeEndpoint({
1185
+ const bridgeEndpoint = await resolveBridgeEndpoint({
1195
1186
  sandboxSession,
1196
- override: settings.portEndpoint,
1197
1187
  port: boundPort,
1188
+ portEndpointOverride: settings.portEndpoint,
1189
+ token,
1198
1190
  });
1199
- const bridgeEndpoint = withBridgeToken({ endpoint, token });
1200
1191
 
1201
1192
  const channel: ClaudeCodeChannel = new SandboxChannel({
1202
- connect: buildConnect(bridgeEndpoint),
1193
+ connect: createBridgeWebSocketConnect({
1194
+ endpoint: bridgeEndpoint,
1195
+ webSocketConstructor: WebSocket,
1196
+ openRetryTimeoutMs: timeoutMs,
1197
+ startAbortSignal: startOpts.abortSignal,
1198
+ onHello,
1199
+ }),
1203
1200
  outboundSchema: outboundMessageSchema,
1204
1201
  onDiagnostic,
1205
1202
  onBridgeError,
1206
1203
  reconnect: settings.reconnect,
1204
+ onDebug: settings.onDebug,
1207
1205
  // In replay mode the respawned bridge reloaded the finished turn from
1208
1206
  // disk; seed the cursor and resume so it streams the tail (incl.
1209
1207
  // `finish`) rather than starting empty.
@@ -1255,71 +1253,6 @@ export function createClaudeCode(
1255
1253
  };
1256
1254
  }
1257
1255
 
1258
- function resolveBridgePort({
1259
- sandboxSession,
1260
- override,
1261
- }: {
1262
- sandboxSession: HarnessV1NetworkSandboxSession | SandboxSession;
1263
- override: number | undefined;
1264
- }): number {
1265
- if (override !== undefined) return override;
1266
- if ('ports' in sandboxSession && sandboxSession.ports.length > 0) {
1267
- return sandboxSession.ports[0];
1268
- }
1269
- throw new HarnessCapabilityUnsupportedError({
1270
- harnessId: 'claude-code',
1271
- message:
1272
- 'The claude-code harness needs a TCP port exposed by the sandbox. ' +
1273
- 'Create the sandbox with `ports: [<port>]` or pass `createClaudeCode({ port })`.',
1274
- });
1275
- }
1276
-
1277
- function validateBasicSandboxSettings({
1278
- sandboxSession,
1279
- port,
1280
- portEndpoint,
1281
- }: {
1282
- sandboxSession: HarnessV1NetworkSandboxSession | SandboxSession;
1283
- port: number | undefined;
1284
- portEndpoint: HarnessV1PortEndpoint | undefined;
1285
- }): void {
1286
- if ('getPortEndpoint' in sandboxSession) return;
1287
- if (port == null) {
1288
- throw new HarnessCapabilityUnsupportedError({
1289
- harnessId: 'claude-code',
1290
- message:
1291
- 'The Claude Code harness requires an explicit `port` when using a basic sandbox session.',
1292
- });
1293
- }
1294
- if (portEndpoint == null) {
1295
- throw new HarnessCapabilityUnsupportedError({
1296
- harnessId: 'claude-code',
1297
- message:
1298
- 'The Claude Code harness requires an explicit `portEndpoint` when using a basic sandbox session.',
1299
- });
1300
- }
1301
- }
1302
-
1303
- async function resolveBridgeEndpoint({
1304
- sandboxSession,
1305
- override,
1306
- port,
1307
- }: {
1308
- sandboxSession: HarnessV1NetworkSandboxSession | SandboxSession;
1309
- override: HarnessV1PortEndpoint | undefined;
1310
- port: number;
1311
- }): Promise<HarnessV1PortEndpoint> {
1312
- if (override != null) return override;
1313
- if ('getPortEndpoint' in sandboxSession) {
1314
- return sandboxSession.getPortEndpoint({ port, protocol: 'ws' });
1315
- }
1316
- throw new HarnessCapabilityUnsupportedError({
1317
- harnessId: 'claude-code',
1318
- message:
1319
- 'The Claude Code harness requires an explicit `portEndpoint` when using a basic sandbox session.',
1320
- });
1321
- }
1322
-
1323
1256
  /**
1324
1257
  * Materialise skill files into
1325
1258
  * `$HOME/.claude/skills/<name>/SKILL.md`. The `claude` CLI
@@ -1327,221 +1260,6 @@ async function resolveBridgeEndpoint({
1327
1260
  * be in place before the bridge is spawned without mutating the session
1328
1261
  * workdir. Each file uses the YAML-frontmatter shape the CLI expects.
1329
1262
  */
1330
- /**
1331
- * Wait for the bridge's `bridge-hello` message to arrive on the freshly
1332
- * opened WebSocket before any other host-side code touches it.
1333
- *
1334
- * Some sandbox runtimes (Vercel in particular) complete the WS upgrade
1335
- * with the host long before the connection is actually forwarded to the
1336
- * sandbox-side bridge. Anything sent in that gap is silently dropped —
1337
- * including the `start` message we send next. The bridge emits
1338
- * `bridge-hello` the instant it accepts the connection, so receiving it
1339
- * is the only reliable evidence that the end-to-end link is live.
1340
- */
1341
- function openWebSocketAndWaitForBridgeHello({
1342
- endpoint,
1343
- openTimeoutMs,
1344
- getHelloTimeoutMs,
1345
- abortSignal,
1346
- onHello,
1347
- }: {
1348
- endpoint: HarnessV1PortEndpoint;
1349
- openTimeoutMs: number;
1350
- getHelloTimeoutMs: () => number;
1351
- abortSignal: AbortSignal;
1352
- onHello: (supportsUserMessageResponses: boolean) => void;
1353
- }): Promise<WebSocket> {
1354
- return new Promise<WebSocket>((resolve, reject) => {
1355
- const abortReason = () =>
1356
- abortSignal.reason ?? new Error('WebSocket connection aborted');
1357
- if (abortSignal.aborted) {
1358
- reject(abortReason());
1359
- return;
1360
- }
1361
-
1362
- const ws = new WebSocket(endpoint.url, {
1363
- headers: endpoint.headers == null ? undefined : { ...endpoint.headers },
1364
- });
1365
- let opened = false;
1366
- let sawBridgeHello = false;
1367
- let settled = false;
1368
- let openTimer: ReturnType<typeof setTimeout> | undefined;
1369
- let helloTimer: ReturnType<typeof setTimeout> | undefined;
1370
- let onAbort: (() => void) | undefined;
1371
-
1372
- const cleanup = ({
1373
- keepTerminationListeners = false,
1374
- }: {
1375
- keepTerminationListeners?: boolean;
1376
- } = {}) => {
1377
- if (openTimer) clearTimeout(openTimer);
1378
- if (helloTimer) clearTimeout(helloTimer);
1379
- ws.off('open', onOpen);
1380
- ws.off('message', onMessage);
1381
- if (onAbort != null) {
1382
- abortSignal.removeEventListener('abort', onAbort);
1383
- }
1384
- if (!keepTerminationListeners) {
1385
- ws.off('close', onClose);
1386
- ws.off('error', onError);
1387
- }
1388
- };
1389
- const settle = (err?: unknown) => {
1390
- if (settled) return;
1391
- settled = true;
1392
- if (err) {
1393
- cleanup({ keepTerminationListeners: true });
1394
- try {
1395
- ws.terminate();
1396
- } catch {
1397
- cleanup();
1398
- }
1399
- reject(err);
1400
- } else {
1401
- cleanup();
1402
- resolve(ws);
1403
- }
1404
- };
1405
- const tryResolve = () => {
1406
- if (opened && sawBridgeHello) settle();
1407
- };
1408
- const startHelloTimer = () => {
1409
- if (helloTimer) return;
1410
- const helloTimeoutMs = getHelloTimeoutMs();
1411
- helloTimer = setTimeout(
1412
- () =>
1413
- settle(
1414
- new Error(
1415
- `claude-code bridge did not send bridge-hello within ${helloTimeoutMs}ms`,
1416
- ),
1417
- ),
1418
- helloTimeoutMs,
1419
- );
1420
- helloTimer.unref?.();
1421
- };
1422
- const onOpen = () => {
1423
- opened = true;
1424
- if (openTimer) {
1425
- clearTimeout(openTimer);
1426
- openTimer = undefined;
1427
- }
1428
- startHelloTimer();
1429
- tryResolve();
1430
- };
1431
- const onMessage = (raw: unknown) => {
1432
- void (async () => {
1433
- const parsed = await safeParseJSON({
1434
- text: webSocketMessageToString(raw),
1435
- });
1436
- if (!parsed.success || settled) return;
1437
- const value = parsed.value;
1438
- if (
1439
- typeof value === 'object' &&
1440
- value !== null &&
1441
- !Array.isArray(value) &&
1442
- (value as { type?: unknown }).type === 'bridge-hello'
1443
- ) {
1444
- const capabilities = (
1445
- value as {
1446
- capabilities?: { experimental_userMessageResponses?: unknown };
1447
- }
1448
- ).capabilities;
1449
- onHello(capabilities?.experimental_userMessageResponses === true);
1450
- sawBridgeHello = true;
1451
- tryResolve();
1452
- }
1453
- })();
1454
- };
1455
- const onClose = () => {
1456
- settle(
1457
- new Error('claude-code bridge closed before sending bridge-hello'),
1458
- );
1459
- cleanup();
1460
- };
1461
- const onError = (err: Error) => settle(err);
1462
- onAbort = () => settle(abortReason());
1463
- openTimer = setTimeout(
1464
- () =>
1465
- settle(new Error(`WebSocket open timed out after ${openTimeoutMs}ms`)),
1466
- openTimeoutMs,
1467
- );
1468
- openTimer.unref?.();
1469
- ws.on('open', onOpen);
1470
- ws.on('message', onMessage);
1471
- ws.on('close', onClose);
1472
- ws.on('error', onError);
1473
- abortSignal.addEventListener('abort', onAbort, { once: true });
1474
- });
1475
- }
1476
-
1477
- async function openBridgeWebSocket({
1478
- endpoint,
1479
- timeoutMs,
1480
- abortSignal,
1481
- onHello,
1482
- }: {
1483
- endpoint: HarnessV1PortEndpoint;
1484
- timeoutMs: number;
1485
- abortSignal: AbortSignal;
1486
- onHello: (supportsUserMessageResponses: boolean) => void;
1487
- }): Promise<WebSocket> {
1488
- const deadline = Date.now() + timeoutMs;
1489
- let attempt = 0;
1490
- let lastError: unknown;
1491
-
1492
- while (!abortSignal.aborted && Date.now() < deadline) {
1493
- attempt++;
1494
- try {
1495
- const remaining = Math.max(1, deadline - Date.now());
1496
- return await openWebSocketAndWaitForBridgeHello({
1497
- endpoint,
1498
- openTimeoutMs: Math.min(10_000, remaining),
1499
- getHelloTimeoutMs: () =>
1500
- Math.min(5_000, Math.max(1, deadline - Date.now())),
1501
- abortSignal,
1502
- onHello,
1503
- });
1504
- } catch (err) {
1505
- if (abortSignal.aborted) {
1506
- throw abortSignal.reason ?? new Error('WebSocket connection aborted');
1507
- }
1508
- lastError = err;
1509
- const remaining = deadline - Date.now();
1510
- if (remaining <= 0) break;
1511
- await sleep({
1512
- ms: Math.min(250 * attempt, 1_000, remaining),
1513
- abortSignal,
1514
- });
1515
- }
1516
- }
1517
-
1518
- if (abortSignal.aborted) {
1519
- throw abortSignal.reason ?? new Error('WebSocket connection aborted');
1520
- }
1521
-
1522
- throw new Error(
1523
- `claude-code bridge did not complete WebSocket handshake within ${timeoutMs}ms after ${attempt} attempt(s). Last error: ${formatUnknownError(lastError)}`,
1524
- );
1525
- }
1526
-
1527
- function webSocketMessageToString(raw: unknown): string {
1528
- if (typeof raw === 'string') return raw;
1529
- if (Buffer.isBuffer(raw)) return raw.toString('utf8');
1530
- if (Array.isArray(raw)) return Buffer.concat(raw).toString('utf8');
1531
- if (raw instanceof ArrayBuffer) return Buffer.from(raw).toString('utf8');
1532
- if (ArrayBuffer.isView(raw)) {
1533
- return Buffer.from(raw.buffer, raw.byteOffset, raw.byteLength).toString(
1534
- 'utf8',
1535
- );
1536
- }
1537
- return String(raw);
1538
- }
1539
-
1540
- function formatUnknownError(error: unknown): string {
1541
- if (error instanceof Error) return error.message;
1542
- return String(error);
1543
- }
1544
-
1545
1263
  function createSession({
1546
1264
  sessionId,
1547
1265
  channel,