@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/CHANGELOG.md +12 -0
- package/dist/bridge/index.mjs +22 -13
- package/dist/bridge/index.mjs.map +1 -1
- package/dist/index.d.ts +8 -0
- package/dist/index.js +34 -11
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/bridge/create-emit-stream-event.ts +8 -7
- package/src/bridge/index.ts +6 -5
- package/src/bridge/json-schema-to-zod.ts +16 -3
- package/src/claude-code-harness.ts +48 -10
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ai-sdk/harness-claude-code",
|
|
3
|
-
"version": "1.0.
|
|
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.
|
|
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
|
-
|
|
236
|
-
|
|
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
|
|
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' &&
|
|
291
|
-
const toolResultBlocks =
|
|
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
|
|
298
|
+
for (const block of messageContent) {
|
|
298
299
|
if (
|
|
299
300
|
block.type === 'tool_result' &&
|
|
300
301
|
typeof block.tool_use_id === 'string'
|
package/src/bridge/index.ts
CHANGED
|
@@ -51,7 +51,7 @@ import {
|
|
|
51
51
|
mapUsage,
|
|
52
52
|
type ClaudeMessage,
|
|
53
53
|
} from './create-emit-stream-event';
|
|
54
|
-
import {
|
|
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
|
-
|
|
384
|
-
server.tool(
|
|
383
|
+
server.registerTool(
|
|
385
384
|
tool.name,
|
|
386
|
-
|
|
387
|
-
|
|
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
|
|
21
|
+
export function jsonSchemaToZodObject(input: unknown) {
|
|
21
22
|
const schema = isJsonSchemaObject(input) ? input : {};
|
|
22
|
-
return
|
|
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
|
|
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) =>
|
|
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(
|
|
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);
|