@ai-sdk/harness-claude-code 1.0.96 → 1.0.98

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 CHANGED
@@ -1,5 +1,33 @@
1
1
  # @ai-sdk/harness-claude-code
2
2
 
3
+ ## 1.0.98
4
+
5
+ ### Patch Changes
6
+
7
+ - e79bc7a: fix(harness-claude-code): resume the exact conversation instead of the most recent one in the working directory
8
+ - 8961fde: feat(harness): allow changing `model` between turns via call options
9
+ - eb59f2a: fix(harness): ensure harness adapters can stream tool input deltas before the complete tool call arrives
10
+ - Updated dependencies [8961fde]
11
+ - Updated dependencies [eb59f2a]
12
+ - @ai-sdk/harness@1.0.94
13
+ - @ai-sdk/provider-utils@5.0.34
14
+
15
+ ## 1.0.97
16
+
17
+ ### Patch Changes
18
+
19
+ - cc9f6ce: fix(harness): stop diagnosing caller-initiated aborts as bridge errors, and serialize bridge turns so a start racing an aborted turn's teardown no longer overlaps it (bounded by a teardown grace period, after which the start proceeds as before)
20
+ - 7608210: feat(harness): add `model` parameter to `HarnessAgent` instead of having each harness adapter support it on their own constructor functions
21
+ - f7bd978: chore(harness): update underlying adapter SDKs to their latest versions
22
+ - 14d4fc0: feat(harness): allow changing harness settings between turns via `prepareCall()` support on `HarnessAgent`
23
+ - Updated dependencies [cc9f6ce]
24
+ - Updated dependencies [7608210]
25
+ - Updated dependencies [6f8a2d7]
26
+ - Updated dependencies [14d4fc0]
27
+ - Updated dependencies [90192f1]
28
+ - @ai-sdk/harness@1.0.93
29
+ - @ai-sdk/provider-utils@5.0.33
30
+
3
31
  ## 1.0.96
4
32
 
5
33
  ### Patch Changes
@@ -141,6 +141,7 @@ var ENV_TRUTHY = /* @__PURE__ */ new Set(["1", "true", "yes", "on"]);
141
141
  var WS_OPEN = 1;
142
142
  async function runBridge(options) {
143
143
  const { bridgeType, bridgeStateDir: bridgeStateDir2, onStart, onStop, onDestroy } = options;
144
+ const teardownGraceMs = options.turnTeardownGraceMs ?? 1e4;
144
145
  const expectedToken = options.token ?? procEnv.BRIDGE_CHANNEL_TOKEN ?? "";
145
146
  const bridgeWsPort = options.port ?? parseInt(procEnv.BRIDGE_WS_PORT ?? "0", 10);
146
147
  const bridgeMetaPath = `${bridgeStateDir2}/bridge-meta.json`;
@@ -157,6 +158,7 @@ async function runBridge(options) {
157
158
  let isFirstTurn = true;
158
159
  let turnAbort;
159
160
  let currentUserMessages;
161
+ let activeTurn;
160
162
  let debugConfig;
161
163
  let consoleCaptureInstalled = false;
162
164
  const envDebugEnabled = ENV_TRUTHY.has(
@@ -347,10 +349,28 @@ async function runBridge(options) {
347
349
  const handleInbound = async (msg, ws) => {
348
350
  switch (msg.type) {
349
351
  case "start": {
352
+ for (; ; ) {
353
+ const pendingTurn = activeTurn;
354
+ if (pendingTurn == null) break;
355
+ turnAbort?.abort();
356
+ currentUserMessages?.close(
357
+ new Error("A new bridge turn replaced the active turn.")
358
+ );
359
+ let graceTimer;
360
+ const settled = await Promise.race([
361
+ pendingTurn.then(() => true),
362
+ new Promise((resolve) => {
363
+ graceTimer = setTimeout(() => resolve(false), teardownGraceMs);
364
+ graceTimer.unref?.();
365
+ })
366
+ ]);
367
+ clearTimeout(graceTimer);
368
+ if (!settled) break;
369
+ }
370
+ let turnFinished;
371
+ const thisTurn = new Promise((resolve) => turnFinished = resolve);
372
+ activeTurn = thisTurn;
350
373
  activeSocket = ws;
351
- currentUserMessages?.close(
352
- new Error("A new bridge turn replaced the active turn.")
353
- );
354
374
  const firstTurn = isFirstTurn;
355
375
  isFirstTurn = false;
356
376
  eventLog = [];
@@ -407,8 +427,12 @@ async function runBridge(options) {
407
427
  if (currentUserMessages === userMessages) {
408
428
  currentUserMessages = void 0;
409
429
  }
410
- currentTurnState = "waiting";
411
- void writeBridgeMeta("waiting");
430
+ if (activeTurn === thisTurn) {
431
+ activeTurn = void 0;
432
+ currentTurnState = "waiting";
433
+ void writeBridgeMeta("waiting");
434
+ }
435
+ turnFinished();
412
436
  }
413
437
  return;
414
438
  }
@@ -711,7 +735,12 @@ function createEmitStreamEvent({
711
735
  return;
712
736
  }
713
737
  if (type === "stream_event") {
714
- handleStreamEvent(msg.event, state.partialBlocks, emit);
738
+ handleStreamEvent({
739
+ event: msg.event,
740
+ state,
741
+ send: emit,
742
+ toCommonName: toCommonName2
743
+ });
715
744
  return;
716
745
  }
717
746
  if (type === "assistant" && msg.message?.content) {
@@ -839,9 +868,16 @@ function formatApiRetryWarning(msg) {
839
868
  if (msg.error) details.push(msg.error);
840
869
  return details.length > 0 ? `Claude Code API retry: ${details.join("; ")}` : "Claude Code API retry";
841
870
  }
842
- function handleStreamEvent(event, partialBlocks, send) {
871
+ var HOST_TOOL_PREFIX = "mcp__harness-tools__";
872
+ function handleStreamEvent({
873
+ event,
874
+ state,
875
+ send,
876
+ toCommonName: toCommonName2
877
+ }) {
843
878
  if (!event || typeof event.index !== "number") return;
844
879
  const index = event.index;
880
+ const partialBlocks = state.partialBlocks;
845
881
  if (event.type === "content_block_start") {
846
882
  const blockType = event.content_block?.type;
847
883
  if (blockType === "text") {
@@ -852,6 +888,22 @@ function handleStreamEvent(event, partialBlocks, send) {
852
888
  const id = randomUUID2();
853
889
  partialBlocks.set(index, { id, kind: "thinking" });
854
890
  send({ type: "reasoning-start", id });
891
+ } else if (blockType === "tool_use" && typeof event.content_block?.id === "string" && typeof event.content_block.name === "string") {
892
+ const id = event.content_block.id;
893
+ const nativeName = event.content_block.name;
894
+ if (nativeName === "StructuredOutput") {
895
+ return;
896
+ }
897
+ const hostToolName = nativeName.startsWith(HOST_TOOL_PREFIX) ? nativeName.slice(HOST_TOOL_PREFIX.length) : void 0;
898
+ const dynamic = hostToolName === void 0 && nativeName.startsWith("mcp__");
899
+ partialBlocks.set(index, { id, kind: "tool-input" });
900
+ send({
901
+ type: "tool-input-start",
902
+ id,
903
+ toolName: hostToolName ?? toCommonName2(nativeName),
904
+ providerExecuted: hostToolName === void 0,
905
+ ...dynamic ? { dynamic: true } : {}
906
+ });
855
907
  }
856
908
  return;
857
909
  }
@@ -866,6 +918,12 @@ function handleStreamEvent(event, partialBlocks, send) {
866
918
  id: block.id,
867
919
  delta: event.delta.thinking
868
920
  });
921
+ } else if (block.kind === "tool-input" && event.delta?.type === "input_json_delta" && typeof event.delta.partial_json === "string") {
922
+ send({
923
+ type: "tool-input-delta",
924
+ id: block.id,
925
+ delta: event.delta.partial_json
926
+ });
869
927
  }
870
928
  return;
871
929
  }
@@ -875,8 +933,10 @@ function handleStreamEvent(event, partialBlocks, send) {
875
933
  partialBlocks.delete(index);
876
934
  if (block.kind === "text") {
877
935
  send({ type: "text-end", id: block.id });
878
- } else {
936
+ } else if (block.kind === "thinking") {
879
937
  send({ type: "reasoning-end", id: block.id });
938
+ } else {
939
+ send({ type: "tool-input-end", id: block.id });
880
940
  }
881
941
  }
882
942
  }
@@ -1102,13 +1162,15 @@ if (!bridgeStateDir) {
1102
1162
  }
1103
1163
  var claudeSdk = claudeAgentSdk;
1104
1164
  var mcpModule = mcpServerModule;
1165
+ var lastClaudeSessionId;
1105
1166
  await runBridge({
1106
1167
  bridgeType: "claude-code",
1107
1168
  bridgeStateDir,
1108
1169
  onStart: runTurn,
1109
- // Claude Code's session state lives in the workdir on the sandbox filesystem
1110
- // (captured by the sandbox snapshot on stop); the resume payload is empty.
1111
- onStop: () => ({})
1170
+ // Claude Code's conversation state lives in the runtime's own store, keyed
1171
+ // by working directory. The resume payload names the exact conversation so a
1172
+ // later resume does not have to fall back to "most recent in this workdir".
1173
+ onStop: () => lastClaudeSessionId == null ? {} : { claudeSessionId: lastClaudeSessionId }
1112
1174
  });
1113
1175
  function createPermissionOptions(input) {
1114
1176
  const permissionMode = input.start.permissionMode ?? "allow-all";
@@ -1185,12 +1247,19 @@ function nativeToolRequiresApproval(input) {
1185
1247
  async function runTurn(start, turn) {
1186
1248
  const emit = (msg) => turn.emit(msg);
1187
1249
  const abortCtl = new AbortController();
1250
+ let gracefulAbort;
1251
+ let hardAbortTimer;
1252
+ const onHostAbort = () => {
1253
+ if (gracefulAbort) {
1254
+ gracefulAbort();
1255
+ } else {
1256
+ abortCtl.abort();
1257
+ }
1258
+ };
1188
1259
  if (turn.abortSignal.aborted) {
1189
1260
  abortCtl.abort();
1190
1261
  } else {
1191
- turn.abortSignal.addEventListener("abort", () => abortCtl.abort(), {
1192
- once: true
1193
- });
1262
+ turn.abortSignal.addEventListener("abort", onHostAbort, { once: true });
1194
1263
  }
1195
1264
  const streamEventState = createClaudeStreamEventState();
1196
1265
  const mcpServers = { ...start.mcpServers ?? {} };
@@ -1205,8 +1274,10 @@ async function runTurn(start, turn) {
1205
1274
  tool.name,
1206
1275
  tool.description ?? "",
1207
1276
  shape,
1208
- async (input) => {
1209
- const toolCallId = randomUUID3();
1277
+ async (...handlerArgs) => {
1278
+ const [input, extra] = handlerArgs;
1279
+ const metadataToolCallId = extra._meta?.["claudecode/toolUseId"];
1280
+ const toolCallId = typeof metadataToolCallId === "string" ? metadataToolCallId : randomUUID3();
1210
1281
  emit({
1211
1282
  type: "tool-call",
1212
1283
  toolCallId,
@@ -1293,17 +1364,31 @@ async function runTurn(start, turn) {
1293
1364
  }
1294
1365
  ]
1295
1366
  },
1296
- // Continuation rule: the host can force-continue (resume after a
1297
- // cross-process detach) by setting `start.continue: true`; otherwise
1298
- // we continue every subsequent turn after the first one in this
1299
- // bridge process.
1300
- ...start.continue === true || !turn.firstTurn ? { continue: true } : {},
1367
+ // Continuation rule, most specific first.
1368
+ //
1369
+ // `resumeSessionId` names the exact conversation and is what a
1370
+ // cross-process resume should use: `continue` means "most recent thread
1371
+ // in this workdir", which silently picks the wrong one once anything
1372
+ // else has run there. The bridge also retains the id observed during its
1373
+ // previous query, so every later query stays pinned to that conversation
1374
+ // even when the host detached and reattached between turns. `resume` and
1375
+ // `continue` are mutually exclusive in the SDK.
1376
+ //
1377
+ // Otherwise the host can force-continue by setting `start.continue`,
1378
+ // and turns after the first fall back to the legacy cwd-based behavior
1379
+ // when no exact id was observed.
1380
+ ...start.resumeSessionId ?? lastClaudeSessionId ? { resume: start.resumeSessionId ?? lastClaudeSessionId } : start.continue === true || !turn.firstTurn ? { continue: true } : {},
1301
1381
  ...permissionOptions,
1302
1382
  mcpServers,
1303
1383
  cwd: workdir,
1304
1384
  abortSignal: abortCtl.signal
1305
1385
  }
1306
1386
  });
1387
+ gracefulAbort = () => {
1388
+ hardAbortTimer = setTimeout(() => abortCtl.abort(), 5e3);
1389
+ hardAbortTimer.unref?.();
1390
+ void Promise.resolve().then(() => q.interrupt()).catch(() => abortCtl.abort());
1391
+ };
1307
1392
  let turnUsage;
1308
1393
  let totalCostUsd;
1309
1394
  let emittedTerminalError = false;
@@ -1313,10 +1398,12 @@ async function runTurn(start, turn) {
1313
1398
  if (!normalized || emittedTerminalError || emittedTerminalFinish) return;
1314
1399
  streamEventState.observedTerminalError = normalized;
1315
1400
  emittedTerminalError = true;
1316
- turn.emitError({
1317
- error: normalized,
1318
- message: "claude-code terminal error"
1319
- });
1401
+ if (!turn.abortSignal.aborted) {
1402
+ turn.emitError({
1403
+ error: normalized,
1404
+ message: "claude-code terminal error"
1405
+ });
1406
+ }
1320
1407
  queryInput.close();
1321
1408
  abortCtl.abort();
1322
1409
  };
@@ -1335,6 +1422,10 @@ async function runTurn(start, turn) {
1335
1422
  if (type === "command_lifecycle") {
1336
1423
  queryInput.handleLifecycle(msg);
1337
1424
  }
1425
+ const sessionId = msg.session_id;
1426
+ if (typeof sessionId === "string" && sessionId.length > 0) {
1427
+ lastClaudeSessionId = sessionId;
1428
+ }
1338
1429
  emitStreamEvent(msg);
1339
1430
  if (type === "result") {
1340
1431
  if (msg.subtype === "success") {
@@ -1385,12 +1476,21 @@ async function runTurn(start, turn) {
1385
1476
  }
1386
1477
  }
1387
1478
  } catch (err) {
1388
- if (!(abortCtl.signal.aborted && emittedTerminalError)) {
1479
+ if (!turn.abortSignal.aborted && !(abortCtl.signal.aborted && emittedTerminalError)) {
1389
1480
  turn.emitError({ error: err, message: "claude-code turn failed" });
1390
1481
  }
1391
1482
  return;
1392
1483
  } finally {
1484
+ gracefulAbort = void 0;
1485
+ if (hardAbortTimer != null) clearTimeout(hardAbortTimer);
1486
+ turn.abortSignal.removeEventListener("abort", onHostAbort);
1393
1487
  queryInput.close();
1488
+ try {
1489
+ await q.return?.(
1490
+ void 0
1491
+ );
1492
+ } catch {
1493
+ }
1394
1494
  }
1395
1495
  if (emittedTerminalError) return;
1396
1496
  emittedTerminalFinish = true;
@@ -1399,7 +1499,17 @@ async function runTurn(start, turn) {
1399
1499
  type: "finish",
1400
1500
  finishReason: { unified: "stop", raw: "stop" },
1401
1501
  totalUsage: turnUsage ?? streamEventState.stepUsage ?? defaultUsage(),
1402
- ...totalCostUsd !== void 0 ? { harnessMetadata: { "claude-code": { costUsd: totalCostUsd } } } : {}
1502
+ ...totalCostUsd !== void 0 || lastClaudeSessionId !== void 0 ? {
1503
+ harnessMetadata: {
1504
+ "claude-code": {
1505
+ ...totalCostUsd !== void 0 ? { costUsd: totalCostUsd } : {},
1506
+ // The conversation this turn belongs to, resumable outside the
1507
+ // SDK with `claude --resume <sessionId>` and captured by the
1508
+ // adapter for exact cross-process resume.
1509
+ ...lastClaudeSessionId !== void 0 ? { sessionId: lastClaudeSessionId } : {}
1510
+ }
1511
+ }
1512
+ } : {}
1403
1513
  });
1404
1514
  }
1405
1515
  function createQueryInput({