@ai-sdk/harness-claude-code 1.0.121 → 1.0.122

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.121",
3
+ "version": "1.0.122",
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
  },
@@ -35,7 +35,7 @@ export type ClaudeMessage = {
35
35
  };
36
36
  };
37
37
  message?: {
38
- content?: ReadonlyArray<MessageBlock>;
38
+ content?: string | ReadonlyArray<MessageBlock>;
39
39
  usage?: Record<string, unknown>;
40
40
  };
41
41
  result?: string;
@@ -232,11 +232,12 @@ export function createEmitStreamEvent({
232
232
  return;
233
233
  }
234
234
 
235
- if (type === 'assistant' && msg.message?.content) {
236
- const usage = toUsageRecord(msg.message.usage);
235
+ const messageContent = msg.message?.content;
236
+ if (type === 'assistant' && Array.isArray(messageContent)) {
237
+ const usage = toUsageRecord(msg.message?.usage);
237
238
  const toolUseIds: string[] = [];
238
239
  let opensStep = false;
239
- for (const block of msg.message.content) {
240
+ for (const block of messageContent) {
240
241
  if (
241
242
  block.type === 'tool_use' &&
242
243
  typeof block.id === 'string' &&
@@ -287,14 +288,14 @@ export function createEmitStreamEvent({
287
288
  return;
288
289
  }
289
290
 
290
- if (type === 'user' && msg.message?.content) {
291
- const toolResultBlocks = msg.message.content.filter(
291
+ if (type === 'user' && Array.isArray(messageContent)) {
292
+ const toolResultBlocks = messageContent.filter(
292
293
  block => block.type === 'tool_result',
293
294
  );
294
295
  const toolUseResult =
295
296
  toolResultBlocks.length === 1 ? msg.tool_use_result : undefined;
296
297
 
297
- for (const block of msg.message.content) {
298
+ for (const block of messageContent) {
298
299
  if (
299
300
  block.type === 'tool_result' &&
300
301
  typeof block.tool_use_id === 'string'
@@ -51,7 +51,7 @@ import {
51
51
  mapUsage,
52
52
  type ClaudeMessage,
53
53
  } from './create-emit-stream-event';
54
- import { jsonSchemaToZodShape } from './json-schema-to-zod';
54
+ import { jsonSchemaToZodObject } from './json-schema-to-zod';
55
55
  import {
56
56
  resolveInactiveNativeTools,
57
57
  resolveNativeTools,
@@ -380,11 +380,12 @@ async function runTurn(start: StartMessage, turn: BridgeTurn): Promise<void> {
380
380
  version: '1.0.0',
381
381
  });
382
382
  for (const tool of start.tools) {
383
- const shape = jsonSchemaToZodShape(tool.inputSchema);
384
- server.tool(
383
+ server.registerTool(
385
384
  tool.name,
386
- tool.description ?? '',
387
- shape,
385
+ {
386
+ description: tool.description ?? '',
387
+ inputSchema: jsonSchemaToZodObject(tool.inputSchema),
388
+ },
388
389
  async (
389
390
  ...handlerArgs: [
390
391
  Record<string, unknown>,
@@ -6,6 +6,7 @@ export type JsonSchemaObject = {
6
6
  type?: string | string[];
7
7
  description?: string;
8
8
  properties?: Record<string, JsonSchemaObject>;
9
+ additionalProperties?: boolean | JsonSchemaObject;
9
10
  required?: string[];
10
11
  items?: JsonSchemaObject | JsonSchemaObject[];
11
12
  enum?: unknown[];
@@ -17,9 +18,21 @@ export type JsonSchemaObject = {
17
18
 
18
19
  type ZodShape = Record<string, z.ZodTypeAny>;
19
20
 
20
- export function jsonSchemaToZodShape(input: unknown): ZodShape {
21
+ export function jsonSchemaToZodObject(input: unknown) {
21
22
  const schema = isJsonSchemaObject(input) ? input : {};
22
- return toZodShape(schema);
23
+ return toZodObject(schema);
24
+ }
25
+
26
+ function toZodObject(schema: JsonSchemaObject) {
27
+ const object = z.object(toZodShape(schema));
28
+ if (schema.additionalProperties === false) return object.strict();
29
+
30
+ // JSON Schema allows additional properties by default; Zod strips them.
31
+ return object.catchall(
32
+ isJsonSchemaObject(schema.additionalProperties)
33
+ ? toZodType(schema.additionalProperties)
34
+ : z.unknown(),
35
+ );
23
36
  }
24
37
 
25
38
  function toZodShape(schema: JsonSchemaObject | undefined): ZodShape {
@@ -71,7 +84,7 @@ function zodForType(schema: JsonSchemaObject): z.ZodTypeAny {
71
84
  Array.isArray(schema.items) ? z.any() : toZodType(schema.items),
72
85
  );
73
86
  case 'object':
74
- return z.object(toZodShape(schema));
87
+ return toZodObject(schema);
75
88
  case 'null':
76
89
  return z.null();
77
90
  default:
@@ -38,10 +38,12 @@ import {
38
38
  resolveSandboxHomeDir,
39
39
  SandboxChannel,
40
40
  shellQuote,
41
+ sleep,
41
42
  warnCredentialBrokeringUnavailable,
42
43
  waitForBridgeReady,
43
44
  withBridgeToken,
44
45
  writeSkills,
46
+ type SandboxChannelReconnectOptions,
45
47
  } from '@ai-sdk/harness/utils';
46
48
  import {
47
49
  safeParseJSON,
@@ -128,6 +130,13 @@ export type ClaudeCodeHarnessSettings = {
128
130
  readonly portEndpoint?: HarnessV1PortEndpoint;
129
131
  /** Maximum milliseconds to wait for the bridge to advertise its port. Defaults to 120000. */
130
132
  readonly startupTimeoutMs?: number;
133
+ /**
134
+ * Configures reconnection attempts after an established bridge connection
135
+ * drops. The reconnect window includes connection establishment and
136
+ * backoff delays. Defaults to 30 seconds with exponential backoff from 50
137
+ * milliseconds up to 2 seconds.
138
+ */
139
+ readonly reconnect?: SandboxChannelReconnectOptions;
131
140
  /**
132
141
  * Creates the authentication token used by the sandbox bridge. Defaults to
133
142
  * a random 32-byte hexadecimal token.
@@ -964,10 +973,16 @@ export function createClaudeCode(
964
973
  // (re)connect: open the socket, then wait for `bridge-hello` so the
965
974
  // end-to-end link is proven live before any frame is sent.
966
975
  const buildConnect =
967
- (endpoint: HarnessV1PortEndpoint) => async (): Promise<WebSocket> => {
976
+ (endpoint: HarnessV1PortEndpoint) =>
977
+ async ({
978
+ abortSignal,
979
+ }: {
980
+ abortSignal: AbortSignal;
981
+ }): Promise<WebSocket> => {
968
982
  return openBridgeWebSocket({
969
983
  endpoint,
970
984
  timeoutMs,
985
+ abortSignal,
971
986
  onHello: supportsResponses => {
972
987
  supportsUserMessageResponses = supportsResponses;
973
988
  },
@@ -999,6 +1014,7 @@ export function createClaudeCode(
999
1014
  initialLastSeenEventId: coords.lastSeenEventId,
1000
1015
  onDiagnostic,
1001
1016
  onBridgeError,
1017
+ reconnect: settings.reconnect,
1002
1018
  });
1003
1019
  await attachChannel.open(isContinue ? { resume: true } : undefined);
1004
1020
  return createSession({
@@ -1144,6 +1160,7 @@ export function createClaudeCode(
1144
1160
  outboundSchema: outboundMessageSchema,
1145
1161
  onDiagnostic,
1146
1162
  onBridgeError,
1163
+ reconnect: settings.reconnect,
1147
1164
  // In replay mode the respawned bridge reloaded the finished turn from
1148
1165
  // disk; seed the cursor and resume so it streams the tail (incl.
1149
1166
  // `finish`) rather than starting empty.
@@ -1270,14 +1287,23 @@ function openWebSocketAndWaitForBridgeHello({
1270
1287
  endpoint,
1271
1288
  openTimeoutMs,
1272
1289
  getHelloTimeoutMs,
1290
+ abortSignal,
1273
1291
  onHello,
1274
1292
  }: {
1275
1293
  endpoint: HarnessV1PortEndpoint;
1276
1294
  openTimeoutMs: number;
1277
1295
  getHelloTimeoutMs: () => number;
1296
+ abortSignal: AbortSignal;
1278
1297
  onHello: (supportsUserMessageResponses: boolean) => void;
1279
1298
  }): Promise<WebSocket> {
1280
1299
  return new Promise<WebSocket>((resolve, reject) => {
1300
+ const abortReason = () =>
1301
+ abortSignal.reason ?? new Error('WebSocket connection aborted');
1302
+ if (abortSignal.aborted) {
1303
+ reject(abortReason());
1304
+ return;
1305
+ }
1306
+
1281
1307
  const ws = new WebSocket(endpoint.url, {
1282
1308
  headers: endpoint.headers == null ? undefined : { ...endpoint.headers },
1283
1309
  });
@@ -1286,6 +1312,7 @@ function openWebSocketAndWaitForBridgeHello({
1286
1312
  let settled = false;
1287
1313
  let openTimer: ReturnType<typeof setTimeout> | undefined;
1288
1314
  let helloTimer: ReturnType<typeof setTimeout> | undefined;
1315
+ let onAbort: (() => void) | undefined;
1289
1316
 
1290
1317
  const cleanup = ({
1291
1318
  keepTerminationListeners = false,
@@ -1296,6 +1323,9 @@ function openWebSocketAndWaitForBridgeHello({
1296
1323
  if (helloTimer) clearTimeout(helloTimer);
1297
1324
  ws.off('open', onOpen);
1298
1325
  ws.off('message', onMessage);
1326
+ if (onAbort != null) {
1327
+ abortSignal.removeEventListener('abort', onAbort);
1328
+ }
1299
1329
  if (!keepTerminationListeners) {
1300
1330
  ws.off('close', onClose);
1301
1331
  ws.off('error', onError);
@@ -1374,6 +1404,7 @@ function openWebSocketAndWaitForBridgeHello({
1374
1404
  cleanup();
1375
1405
  };
1376
1406
  const onError = (err: Error) => settle(err);
1407
+ onAbort = () => settle(abortReason());
1377
1408
  openTimer = setTimeout(
1378
1409
  () =>
1379
1410
  settle(new Error(`WebSocket open timed out after ${openTimeoutMs}ms`)),
@@ -1384,23 +1415,26 @@ function openWebSocketAndWaitForBridgeHello({
1384
1415
  ws.on('message', onMessage);
1385
1416
  ws.on('close', onClose);
1386
1417
  ws.on('error', onError);
1418
+ abortSignal.addEventListener('abort', onAbort, { once: true });
1387
1419
  });
1388
1420
  }
1389
1421
 
1390
1422
  async function openBridgeWebSocket({
1391
1423
  endpoint,
1392
1424
  timeoutMs,
1425
+ abortSignal,
1393
1426
  onHello,
1394
1427
  }: {
1395
1428
  endpoint: HarnessV1PortEndpoint;
1396
1429
  timeoutMs: number;
1430
+ abortSignal: AbortSignal;
1397
1431
  onHello: (supportsUserMessageResponses: boolean) => void;
1398
1432
  }): Promise<WebSocket> {
1399
1433
  const deadline = Date.now() + timeoutMs;
1400
1434
  let attempt = 0;
1401
1435
  let lastError: unknown;
1402
1436
 
1403
- while (Date.now() < deadline) {
1437
+ while (!abortSignal.aborted && Date.now() < deadline) {
1404
1438
  attempt++;
1405
1439
  try {
1406
1440
  const remaining = Math.max(1, deadline - Date.now());
@@ -1409,16 +1443,27 @@ async function openBridgeWebSocket({
1409
1443
  openTimeoutMs: Math.min(10_000, remaining),
1410
1444
  getHelloTimeoutMs: () =>
1411
1445
  Math.min(5_000, Math.max(1, deadline - Date.now())),
1446
+ abortSignal,
1412
1447
  onHello,
1413
1448
  });
1414
1449
  } catch (err) {
1450
+ if (abortSignal.aborted) {
1451
+ throw abortSignal.reason ?? new Error('WebSocket connection aborted');
1452
+ }
1415
1453
  lastError = err;
1416
1454
  const remaining = deadline - Date.now();
1417
1455
  if (remaining <= 0) break;
1418
- await sleep(Math.min(250 * attempt, 1_000, remaining));
1456
+ await sleep({
1457
+ ms: Math.min(250 * attempt, 1_000, remaining),
1458
+ abortSignal,
1459
+ });
1419
1460
  }
1420
1461
  }
1421
1462
 
1463
+ if (abortSignal.aborted) {
1464
+ throw abortSignal.reason ?? new Error('WebSocket connection aborted');
1465
+ }
1466
+
1422
1467
  throw new Error(
1423
1468
  `claude-code bridge did not complete WebSocket handshake within ${timeoutMs}ms after ${attempt} attempt(s). Last error: ${formatUnknownError(lastError)}`,
1424
1469
  );
@@ -1437,13 +1482,6 @@ function webSocketMessageToString(raw: unknown): string {
1437
1482
  return String(raw);
1438
1483
  }
1439
1484
 
1440
- function sleep(ms: number): Promise<void> {
1441
- return new Promise(resolve => {
1442
- const timer = setTimeout(resolve, ms);
1443
- timer.unref?.();
1444
- });
1445
- }
1446
-
1447
1485
  function formatUnknownError(error: unknown): string {
1448
1486
  if (error instanceof Error) return error.message;
1449
1487
  return String(error);