@ai-sdk/harness-claude-code 1.0.153 → 1.0.155

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,27 @@
1
1
  # @ai-sdk/harness-claude-code
2
2
 
3
+ ## 1.0.155
4
+
5
+ ### Patch Changes
6
+
7
+ - 6c65131: feat(harness): consistently support parallel and sequential tool approvals
8
+ - 1bebfcf: feat(harness): add support for tools with `toModelOutput()` transformation
9
+ - 0291550: fix(harness-claude-code): wait for response completion before finishing tool steps
10
+ - Updated dependencies [6c65131]
11
+ - Updated dependencies [1bebfcf]
12
+ - @ai-sdk/harness@1.0.151
13
+
14
+ ## 1.0.154
15
+
16
+ ### Patch Changes
17
+
18
+ - Updated dependencies [6617f11]
19
+ - Updated dependencies [aae69cb]
20
+ - Updated dependencies [a7a66f5]
21
+ - Updated dependencies [52e3f53]
22
+ - Updated dependencies [6c0295c]
23
+ - @ai-sdk/harness@1.0.150
24
+
3
25
  ## 1.0.153
4
26
 
5
27
  ### Patch Changes
@@ -8,6 +8,71 @@ import * as mcpServerModule from "@modelcontextprotocol/sdk/server/mcp.js";
8
8
  import { isDeepStrictEqual } from "node:util";
9
9
  import { z } from "zod/v4";
10
10
  //#region ../harness/dist/bridge/index.js
11
+ const name = "AI_HarnessBridgeCapabilityUnsupportedError";
12
+ /**
13
+ * Signals an unsupported capability discovered inside a sandbox bridge.
14
+ * `isInstance` also recognizes the serialized error shape received by the
15
+ * host, where it can be translated to `HarnessCapabilityUnsupportedError`.
16
+ */
17
+ var HarnessBridgeCapabilityUnsupportedError = class extends Error {
18
+ constructor({ message, harnessId, cause }) {
19
+ super(message);
20
+ Object.defineProperty(this, "name", { value: name });
21
+ this.harnessId = harnessId;
22
+ this.cause = cause;
23
+ }
24
+ static isInstance(error) {
25
+ return error != null && typeof error === "object" && "name" in error && error.name === name && "message" in error && typeof error.message === "string";
26
+ }
27
+ };
28
+ function getNormalizedInlineImage(part) {
29
+ if (part.type === "file" && part.mediaType.startsWith("image/") && part.mediaType !== "image/*" && part.data.type === "data" && typeof part.data.data === "string") return {
30
+ data: part.data.data,
31
+ mediaType: part.mediaType
32
+ };
33
+ }
34
+ function convertHarnessToolModelOutput({ output }) {
35
+ switch (output.type) {
36
+ case "text":
37
+ case "error-text": return {
38
+ content: [{
39
+ type: "text",
40
+ text: output.value
41
+ }],
42
+ isError: output.type === "error-text"
43
+ };
44
+ case "json":
45
+ case "error-json": return {
46
+ content: [{
47
+ type: "text",
48
+ text: JSON.stringify(output.value)
49
+ }],
50
+ isError: output.type === "error-json"
51
+ };
52
+ case "execution-denied": return {
53
+ content: [{
54
+ type: "text",
55
+ text: output.reason ?? "Tool execution denied."
56
+ }],
57
+ isError: true
58
+ };
59
+ case "content": return {
60
+ content: output.value.map((part) => {
61
+ if (part.type === "text") return {
62
+ type: "text",
63
+ text: part.text
64
+ };
65
+ const image = getNormalizedInlineImage(part);
66
+ if (image != null) return {
67
+ type: "image",
68
+ ...image
69
+ };
70
+ throw new HarnessBridgeCapabilityUnsupportedError({ message: `Harnesses support only text and inline images in tool model output; unsupported content '${part.type}'.` });
71
+ }),
72
+ isError: false
73
+ };
74
+ }
75
+ }
11
76
  const DEBUG_LEVEL_WEIGHT = {
12
77
  error: 0,
13
78
  warn: 1,
@@ -632,12 +697,17 @@ function createClaudeStreamEventState() {
632
697
  pendingStepDeltaUsage: void 0,
633
698
  pendingStepUsage: void 0,
634
699
  pendingResponseUsage: void 0,
700
+ responseOpen: false,
635
701
  stepOpen: false,
636
702
  mcpToolCalls: /* @__PURE__ */ new Map(),
637
703
  mcpHandlerCalls: /* @__PURE__ */ new Map(),
638
704
  externalMcpToolUseIds: /* @__PURE__ */ new Set(),
639
705
  structuredOutputToolUseIds: /* @__PURE__ */ new Set(),
640
- observedTerminalError: void 0
706
+ observedTerminalError: void 0,
707
+ hostInputWaitingToolUseIds: /* @__PURE__ */ new Set(),
708
+ earlyFinishStepToolUseIds: /* @__PURE__ */ new Set(),
709
+ emittedToolCallIds: /* @__PURE__ */ new Set(),
710
+ modelResponseStreaming: false
641
711
  };
642
712
  }
643
713
  const UNRECOVERABLE_API_RETRY_STATUSES = /* @__PURE__ */ new Set([
@@ -730,12 +800,34 @@ function createEmitStreamEvent({ state, emit, emitWarning, emitTerminalError, on
730
800
  continue;
731
801
  }
732
802
  if (block.name.startsWith(HOST_TOOL_PREFIX)) {
803
+ const hostToolName = block.name.slice(20);
733
804
  state.pendingStepToolUseIds.add(block.id);
734
805
  state.mcpToolCalls.set(block.id, {
735
- toolName: block.name.slice(20),
806
+ toolName: hostToolName,
736
807
  input: block.input ?? {}
737
808
  });
738
809
  opensStep = true;
810
+ const handlerCallId = findMcpHandlerCallId({
811
+ state,
812
+ toolCallId: block.id,
813
+ toolCall: state.mcpToolCalls.get(block.id)
814
+ });
815
+ if (handlerCallId != null) {
816
+ state.mcpHandlerCalls.get(handlerCallId).toolUseId = block.id;
817
+ state.emittedToolCallIds.add(block.id);
818
+ continue;
819
+ }
820
+ state.hostInputWaitingToolUseIds.add(block.id);
821
+ if (!state.emittedToolCallIds.has(block.id)) {
822
+ state.emittedToolCallIds.add(block.id);
823
+ emit({
824
+ type: "tool-call",
825
+ toolCallId: block.id,
826
+ toolName: hostToolName,
827
+ input: JSON.stringify(block.input ?? {}),
828
+ providerExecuted: false
829
+ });
830
+ }
739
831
  continue;
740
832
  }
741
833
  state.nativeToolCallNames.set(block.id, block.name);
@@ -746,9 +838,10 @@ function createEmitStreamEvent({ state, emit, emitWarning, emitTerminalError, on
746
838
  }
747
839
  const dynamic = isExternalMcpTool(block.name);
748
840
  if (dynamic) state.externalMcpToolUseIds.add(block.id);
749
- if (state.approvalRequestedToolUseIds.has(block.id)) continue;
750
841
  state.pendingStepToolUseIds.add(block.id);
751
842
  opensStep = true;
843
+ if (state.emittedToolCallIds.has(block.id)) continue;
844
+ state.emittedToolCallIds.add(block.id);
752
845
  emit({
753
846
  type: "tool-call",
754
847
  toolCallId: block.id,
@@ -765,6 +858,10 @@ function createEmitStreamEvent({ state, emit, emitWarning, emitTerminalError, on
765
858
  state.pendingStepAssistantUsage = usage;
766
859
  updatePendingStepUsage(state);
767
860
  }
861
+ if (state.hostInputWaitingToolUseIds.size > 0) closeStepIfReady({
862
+ state,
863
+ emit
864
+ });
768
865
  }
769
866
  return;
770
867
  }
@@ -781,6 +878,7 @@ function createEmitStreamEvent({ state, emit, emitWarning, emitTerminalError, on
781
878
  });
782
879
  state.mcpToolCalls.delete(block.tool_use_id);
783
880
  state.pendingStepToolUseIds.delete(block.tool_use_id);
881
+ state.hostInputWaitingToolUseIds.delete(block.tool_use_id);
784
882
  if (handlerReportedCall) continue;
785
883
  const isError = !!block.is_error;
786
884
  const result = toolUseResult !== void 0 ? toolUseResult : resolveToolResult({
@@ -789,13 +887,16 @@ function createEmitStreamEvent({ state, emit, emitWarning, emitTerminalError, on
789
887
  isError,
790
888
  rawContent: block.content
791
889
  });
792
- emit({
793
- type: "tool-call",
794
- toolCallId: block.tool_use_id,
795
- toolName: mcpToolCall.toolName,
796
- input: JSON.stringify(mcpToolCall.input),
797
- providerExecuted: true
798
- });
890
+ if (!state.emittedToolCallIds.has(block.tool_use_id)) {
891
+ state.emittedToolCallIds.add(block.tool_use_id);
892
+ emit({
893
+ type: "tool-call",
894
+ toolCallId: block.tool_use_id,
895
+ toolName: mcpToolCall.toolName,
896
+ input: JSON.stringify(mcpToolCall.input),
897
+ providerExecuted: true
898
+ });
899
+ }
799
900
  emit({
800
901
  type: "tool-result",
801
902
  toolCallId: block.tool_use_id,
@@ -835,27 +936,40 @@ function createEmitStreamEvent({ state, emit, emitWarning, emitTerminalError, on
835
936
  };
836
937
  }
837
938
  function takeMcpHandlerCall({ state, toolCallId, toolCall }) {
838
- if (state.mcpHandlerCalls.delete(toolCallId)) return true;
839
- for (const [handlerCallId, handlerCall] of state.mcpHandlerCalls) if (handlerCall.toolName === toolCall.toolName && isDeepStrictEqual(handlerCall.input, toolCall.input)) {
840
- state.mcpHandlerCalls.delete(handlerCallId);
841
- return true;
842
- }
939
+ const handlerCallId = findMcpHandlerCallId({
940
+ state,
941
+ toolCallId,
942
+ toolCall
943
+ });
944
+ if (handlerCallId != null) return state.mcpHandlerCalls.delete(handlerCallId);
843
945
  const matchingHandlerCallIds = Array.from(state.mcpHandlerCalls).filter(([, handlerCall]) => handlerCall.toolName === toolCall.toolName).map(([handlerCallId]) => handlerCallId);
844
946
  const matchingToolCallCount = Array.from(state.mcpToolCalls.values()).filter((pendingCall) => pendingCall.toolName === toolCall.toolName).length;
845
- if (matchingHandlerCallIds.length > 0 && matchingHandlerCallIds.length === matchingToolCallCount) {
846
- state.mcpHandlerCalls.delete(matchingHandlerCallIds[0]);
847
- return true;
848
- }
947
+ if (matchingHandlerCallIds.length > 0 && matchingHandlerCallIds.length === matchingToolCallCount) return state.mcpHandlerCalls.delete(matchingHandlerCallIds[0]);
849
948
  return false;
850
949
  }
851
- function finishApprovalStep({ state, emit, approvalId }) {
950
+ function findMcpHandlerCallId({ state, toolCallId, toolCall }) {
951
+ if (state.mcpHandlerCalls.has(toolCallId)) return toolCallId;
952
+ for (const [handlerCallId, handlerCall] of state.mcpHandlerCalls) {
953
+ if (handlerCall.toolUseId != null && handlerCall.toolUseId !== toolCallId || handlerCallId !== toolCallId && state.mcpToolCalls.has(handlerCallId)) continue;
954
+ if (handlerCall.toolName === toolCall.toolName && isDeepStrictEqual(handlerCall.input, toolCall.input)) return handlerCallId;
955
+ }
956
+ }
957
+ function markWaitingOnHostInput({ state, emit, toolUseId }) {
852
958
  state.stepOpen = true;
853
- state.pendingStepToolUseIds.delete(approvalId);
959
+ state.pendingStepToolUseIds.add(toolUseId);
960
+ state.hostInputWaitingToolUseIds.add(toolUseId);
854
961
  closeStepIfReady({
855
962
  state,
856
963
  emit
857
964
  });
858
965
  }
966
+ function clearWaitingOnHostInput({ state, toolUseId }) {
967
+ state.hostInputWaitingToolUseIds.delete(toolUseId);
968
+ }
969
+ function resolveMcpHandlerToolUseId({ state, metadataToolUseId, toolName, input }) {
970
+ if (metadataToolUseId != null) return metadataToolUseId;
971
+ for (const [toolUseId, toolCall] of state.mcpToolCalls) if (!state.mcpHandlerCalls.has(toolUseId) && toolCall.toolName === toolName && isDeepStrictEqual(toolCall.input, input)) return toolUseId;
972
+ }
859
973
  function emitFinishStep({ state, emit, usage }) {
860
974
  emit({
861
975
  type: "finish-step",
@@ -870,14 +984,37 @@ function emitFinishStep({ state, emit, usage }) {
870
984
  state.pendingStepDeltaUsage = void 0;
871
985
  state.pendingStepUsage = void 0;
872
986
  state.pendingStepToolUseIds = /* @__PURE__ */ new Set();
987
+ state.earlyFinishStepToolUseIds = /* @__PURE__ */ new Set();
873
988
  state.stepOpen = false;
874
989
  }
875
990
  function closeStepIfReady({ state, emit }) {
876
- if (!state.stepOpen || state.pendingStepToolUseIds.size > 0 || state.partialBlocks.size > 0) return;
877
- emitFinishStep({
991
+ if (!state.stepOpen || state.responseOpen || state.partialBlocks.size > 0) return;
992
+ if (state.pendingStepToolUseIds.size === 0) {
993
+ emitFinishStep({
994
+ state,
995
+ emit,
996
+ usage: state.pendingStepUsage
997
+ });
998
+ return;
999
+ }
1000
+ emitEarlyFinishStepIfReady({
878
1001
  state,
879
- emit,
880
- usage: state.pendingStepUsage
1002
+ emit
1003
+ });
1004
+ }
1005
+ function emitEarlyFinishStepIfReady({ state, emit }) {
1006
+ if (!state.stepOpen || state.partialBlocks.size > 0 || state.modelResponseStreaming || state.pendingStepToolUseIds.size === 0) return;
1007
+ const uncovered = [...state.hostInputWaitingToolUseIds].filter((toolUseId) => state.pendingStepToolUseIds.has(toolUseId) && !state.earlyFinishStepToolUseIds.has(toolUseId));
1008
+ if (uncovered.length === 0) return;
1009
+ for (const toolUseId of uncovered) state.earlyFinishStepToolUseIds.add(toolUseId);
1010
+ emit({
1011
+ type: "finish-step",
1012
+ finishReason: {
1013
+ unified: "tool-calls",
1014
+ raw: "host-input"
1015
+ },
1016
+ usage: defaultUsage(),
1017
+ harnessMetadata: { "claude-code": { inferredStep: true } }
881
1018
  });
882
1019
  }
883
1020
  function formatApiRetryWarning(msg) {
@@ -894,7 +1031,9 @@ function formatApiRetryWarning(msg) {
894
1031
  function handleStreamEvent({ event, message, state, send, toCommonName }) {
895
1032
  if (!event) return;
896
1033
  if (event.type === "message_start") {
1034
+ state.modelResponseStreaming = true;
897
1035
  state.pendingResponseUsage = toUsageRecord(event.message?.usage);
1036
+ state.responseOpen = true;
898
1037
  return;
899
1038
  }
900
1039
  if (event.type === "message_delta") {
@@ -915,6 +1054,12 @@ function handleStreamEvent({ event, message, state, send, toCommonName }) {
915
1054
  }
916
1055
  });
917
1056
  state.pendingResponseUsage = void 0;
1057
+ state.modelResponseStreaming = false;
1058
+ state.responseOpen = false;
1059
+ closeStepIfReady({
1060
+ state,
1061
+ emit: send
1062
+ });
918
1063
  return;
919
1064
  }
920
1065
  if (typeof event.index !== "number") return;
@@ -1355,22 +1500,26 @@ function createPermissionOptions(input) {
1355
1500
  const approvalId = options.toolUseID;
1356
1501
  input.approvalRequestedToolUseIds.add(approvalId);
1357
1502
  input.nativeToolCallNames.set(approvalId, toolName);
1358
- input.emit({
1359
- type: "tool-call",
1360
- toolCallId: approvalId,
1361
- toolName: toCommonName(toolName),
1362
- nativeName: toolName,
1363
- input: JSON.stringify(toolInput ?? {}),
1364
- providerExecuted: true,
1365
- ...isExternalMcpTool(toolName) ? { dynamic: true } : {}
1366
- });
1503
+ if (!input.emittedToolCallIds.has(approvalId)) {
1504
+ input.emittedToolCallIds.add(approvalId);
1505
+ input.emit({
1506
+ type: "tool-call",
1507
+ toolCallId: approvalId,
1508
+ toolName: toCommonName(toolName),
1509
+ nativeName: toolName,
1510
+ input: JSON.stringify(toolInput ?? {}),
1511
+ providerExecuted: true,
1512
+ ...isExternalMcpTool(toolName) ? { dynamic: true } : {}
1513
+ });
1514
+ }
1515
+ const decisionPromise = input.turn.requestToolApproval(approvalId);
1367
1516
  input.emit({
1368
1517
  type: "tool-approval-request",
1369
1518
  approvalId,
1370
1519
  toolCallId: approvalId
1371
1520
  });
1372
- input.finishApprovalStep(approvalId);
1373
- const decision = await input.turn.requestToolApproval(approvalId);
1521
+ input.markWaitingOnHostInput(approvalId);
1522
+ const decision = await decisionPromise.finally(() => input.clearWaitingOnHostInput(approvalId));
1374
1523
  return decision.approved ? {
1375
1524
  behavior: "allow",
1376
1525
  updatedInput: toolInput,
@@ -1400,6 +1549,7 @@ function createQuestionPreToolUseHook(input) {
1400
1549
  providerMetadata: { "claude-code": { nativeRequest: nativeInput } }
1401
1550
  });
1402
1551
  const questionKey = claudeCodeQuestionKey(nativeInput);
1552
+ input.markWaitingOnHostInput(toolCallId);
1403
1553
  const nativeResult = toClaudeCodeQuestionResult({
1404
1554
  nativeInput,
1405
1555
  output: (await input.turn.requestToolResult({
@@ -1408,7 +1558,7 @@ function createQuestionPreToolUseHook(input) {
1408
1558
  const nativeRequest = candidate.toolResult?.providerOptions?.["claude-code"]?.nativeRequest;
1409
1559
  return nativeRequest != null && claudeCodeQuestionKey(nativeRequest) === questionKey;
1410
1560
  }
1411
- })).output
1561
+ }).finally(() => input.clearWaitingOnHostInput(toolCallId))).output
1412
1562
  });
1413
1563
  return { hookSpecificOutput: {
1414
1564
  hookEventName: "PreToolUse",
@@ -1461,19 +1611,38 @@ async function runTurn(start, turn) {
1461
1611
  }, async (...handlerArgs) => {
1462
1612
  const [input, extra] = handlerArgs;
1463
1613
  const metadataToolCallId = extra._meta?.["claudecode/toolUseId"];
1464
- const toolCallId = typeof metadataToolCallId === "string" ? metadataToolCallId : randomUUID();
1614
+ const toolCallId = resolveMcpHandlerToolUseId({
1615
+ state: streamEventState,
1616
+ metadataToolUseId: typeof metadataToolCallId === "string" ? metadataToolCallId : void 0,
1617
+ toolName: tool.name,
1618
+ input
1619
+ }) ?? randomUUID();
1465
1620
  streamEventState.mcpHandlerCalls.set(toolCallId, {
1466
1621
  toolName: tool.name,
1467
1622
  input
1468
1623
  });
1469
- emit({
1470
- type: "tool-call",
1471
- toolCallId,
1472
- toolName: tool.name,
1473
- input: JSON.stringify(input),
1474
- providerExecuted: false
1624
+ if (!streamEventState.emittedToolCallIds.has(toolCallId)) {
1625
+ streamEventState.emittedToolCallIds.add(toolCallId);
1626
+ emit({
1627
+ type: "tool-call",
1628
+ toolCallId,
1629
+ toolName: tool.name,
1630
+ input: JSON.stringify(input),
1631
+ providerExecuted: false
1632
+ });
1633
+ markWaitingOnHostInput({
1634
+ state: streamEventState,
1635
+ emit,
1636
+ toolUseId: toolCallId
1637
+ });
1638
+ }
1639
+ const { output, isError, toolResult } = await turn.requestToolResult(toolCallId).finally(() => {
1640
+ clearWaitingOnHostInput({
1641
+ state: streamEventState,
1642
+ toolUseId: toolCallId
1643
+ });
1644
+ if (!streamEventState.mcpToolCalls.has(toolCallId)) streamEventState.pendingStepToolUseIds.delete(toolCallId);
1475
1645
  });
1476
- const { output, isError } = await turn.requestToolResult(toolCallId);
1477
1646
  emit({
1478
1647
  type: "tool-result",
1479
1648
  toolCallId,
@@ -1481,6 +1650,17 @@ async function runTurn(start, turn) {
1481
1650
  result: output ?? null,
1482
1651
  isError: !!isError
1483
1652
  });
1653
+ if (toolResult != null) {
1654
+ const converted = convertHarnessToolModelOutput({ output: toolResult.output });
1655
+ return {
1656
+ content: converted.content.map((part) => part.type === "text" ? part : {
1657
+ type: "image",
1658
+ data: part.data,
1659
+ mimeType: part.mediaType
1660
+ }),
1661
+ isError: isError === true || converted.isError
1662
+ };
1663
+ }
1484
1664
  return {
1485
1665
  content: [{
1486
1666
  type: "text",
@@ -1517,20 +1697,32 @@ async function runTurn(start, turn) {
1517
1697
  inactiveNativeTools,
1518
1698
  turn,
1519
1699
  emit,
1520
- finishApprovalStep: (approvalId) => {
1521
- finishApprovalStep({
1522
- state: streamEventState,
1523
- emit,
1524
- approvalId
1525
- });
1526
- },
1700
+ markWaitingOnHostInput: (toolUseId) => markWaitingOnHostInput({
1701
+ state: streamEventState,
1702
+ emit,
1703
+ toolUseId
1704
+ }),
1705
+ clearWaitingOnHostInput: (toolUseId) => clearWaitingOnHostInput({
1706
+ state: streamEventState,
1707
+ toolUseId
1708
+ }),
1527
1709
  nativeToolCallNames: streamEventState.nativeToolCallNames,
1528
- approvalRequestedToolUseIds: streamEventState.approvalRequestedToolUseIds
1710
+ approvalRequestedToolUseIds: streamEventState.approvalRequestedToolUseIds,
1711
+ emittedToolCallIds: streamEventState.emittedToolCallIds
1529
1712
  });
1530
1713
  const questionPreToolUseHook = createQuestionPreToolUseHook({
1531
1714
  turn,
1532
1715
  emit,
1533
- nativeToolCallNames: streamEventState.nativeToolCallNames
1716
+ nativeToolCallNames: streamEventState.nativeToolCallNames,
1717
+ markWaitingOnHostInput: (toolUseId) => markWaitingOnHostInput({
1718
+ state: streamEventState,
1719
+ emit,
1720
+ toolUseId
1721
+ }),
1722
+ clearWaitingOnHostInput: (toolUseId) => clearWaitingOnHostInput({
1723
+ state: streamEventState,
1724
+ toolUseId
1725
+ })
1534
1726
  });
1535
1727
  const q = claudeSdk.query({
1536
1728
  prompt: queryInput.input,