@ai-sdk/harness-claude-code 1.0.103 → 1.0.105

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,23 @@
1
1
  # @ai-sdk/harness-claude-code
2
2
 
3
+ ## 1.0.105
4
+
5
+ ### Patch Changes
6
+
7
+ - 951c54d: feat(harness): support `askUserQuestions` tool including support for normalization across harness adapters
8
+ - a554cc8: fix (harness-claude-code): treat a `result` message flagged `is_error` as a terminal error
9
+ - Updated dependencies [951c54d]
10
+ - @ai-sdk/harness@1.0.101
11
+
12
+ ## 1.0.104
13
+
14
+ ### Patch Changes
15
+
16
+ - f21945b: fix(harness-claude-code): keep non-text tool result content and scalar MCP tool results intact instead of flattening them to a stringified or re-parsed value
17
+ - 0c37bf0: fix(harness): avoid warning about lack of credential brokering support when `credentialForwarding` callback is used to replace all credentials with ephemeral fake secrets
18
+ - Updated dependencies [0c37bf0]
19
+ - @ai-sdk/harness@1.0.100
20
+
3
21
  ## 1.0.103
4
22
 
5
23
  ### Patch Changes
@@ -214,6 +214,7 @@ async function runBridge(options) {
214
214
  }
215
215
  }
216
216
  const pendingToolResults = /* @__PURE__ */ new Map();
217
+ const bufferedToolResults = [];
217
218
  const pendingToolApprovals = /* @__PURE__ */ new Map();
218
219
  const writeBridgeMeta = async (state) => {
219
220
  try {
@@ -393,9 +394,23 @@ async function runBridge(options) {
393
394
  const userMessages = createBridgeUserMessageQueue({ respond: emit });
394
395
  const turn = {
395
396
  emit,
396
- requestToolResult: (toolCallId) => new Promise((resolve) => {
397
- pendingToolResults.set(toolCallId, resolve);
398
- }),
397
+ requestToolResult: (requestInput) => {
398
+ const request = typeof requestInput === "string" ? { toolCallId: requestInput } : requestInput;
399
+ const bufferedIndex = bufferedToolResults.findIndex(
400
+ (buffered) => buffered.toolCallId === request.toolCallId || request.matches?.(buffered.result) === true
401
+ );
402
+ if (bufferedIndex >= 0) {
403
+ return Promise.resolve(
404
+ bufferedToolResults.splice(bufferedIndex, 1)[0].result
405
+ );
406
+ }
407
+ return new Promise((resolve) => {
408
+ pendingToolResults.set(request.toolCallId, {
409
+ resolve,
410
+ matches: request.matches
411
+ });
412
+ });
413
+ },
399
414
  requestToolApproval: (approvalId) => new Promise((resolve) => {
400
415
  pendingToolApprovals.set(approvalId, resolve);
401
416
  }),
@@ -437,10 +452,25 @@ async function runBridge(options) {
437
452
  return;
438
453
  }
439
454
  case "tool-result": {
440
- const resolver = pendingToolResults.get(msg.toolCallId);
441
- if (resolver) {
442
- pendingToolResults.delete(msg.toolCallId);
443
- resolver({ output: msg.output, isError: msg.isError });
455
+ const result = {
456
+ output: msg.output,
457
+ isError: msg.isError,
458
+ toolResult: msg.toolResult
459
+ };
460
+ const exactPending = pendingToolResults.get(msg.toolCallId);
461
+ const matchingPending = exactPending == null ? Array.from(pendingToolResults.entries()).find(
462
+ ([, pending2]) => pending2.matches?.(result) === true
463
+ ) : void 0;
464
+ const pending = exactPending ?? matchingPending?.[1];
465
+ const pendingId = exactPending != null ? msg.toolCallId : matchingPending?.[0];
466
+ if (pending != null && pendingId != null) {
467
+ pendingToolResults.delete(pendingId);
468
+ pending.resolve(result);
469
+ } else {
470
+ bufferedToolResults.push({
471
+ toolCallId: msg.toolCallId,
472
+ result
473
+ });
444
474
  }
445
475
  return;
446
476
  }
@@ -765,6 +795,11 @@ function createEmitStreamEvent({
765
795
  continue;
766
796
  }
767
797
  state.nativeToolCallNames.set(block.id, block.name);
798
+ if (block.name === "AskUserQuestion") {
799
+ state.pendingStepToolUseIds.add(block.id);
800
+ opensStep = true;
801
+ continue;
802
+ }
768
803
  const dynamic = isExternalMcpTool(block.name);
769
804
  if (dynamic) state.externalMcpToolUseIds.add(block.id);
770
805
  if (state.approvalRequestedToolUseIds.has(block.id)) {
@@ -810,9 +845,12 @@ function createEmitStreamEvent({
810
845
  const toolName = toCommonName2(nativeName);
811
846
  const dynamic = state.externalMcpToolUseIds.delete(block.tool_use_id);
812
847
  const isError = !!block.is_error;
813
- const content = stringifyContent(block.content);
814
- const contentResult = toolName === "bash" ? { exitCode: isError ? 1 : 0, stdout: content } : dynamic ? parseMcpToolResult(content) : content;
815
- const result = toolUseResult !== void 0 ? toolUseResult : contentResult;
848
+ const result = toolUseResult !== void 0 ? toolUseResult : resolveToolResult({
849
+ toolName,
850
+ dynamic,
851
+ isError,
852
+ rawContent: block.content
853
+ });
816
854
  emit({
817
855
  type: "tool-result",
818
856
  toolCallId: block.tool_use_id,
@@ -901,6 +939,9 @@ function handleStreamEvent({
901
939
  if (nativeName === "StructuredOutput") {
902
940
  return;
903
941
  }
942
+ if (nativeName === "AskUserQuestion") {
943
+ return;
944
+ }
904
945
  const hostToolName = nativeName.startsWith(HOST_TOOL_PREFIX) ? nativeName.slice(HOST_TOOL_PREFIX.length) : void 0;
905
946
  const dynamic = isExternalMcpTool(nativeName);
906
947
  partialBlocks.set(index, { id, kind: "tool-input" });
@@ -947,18 +988,38 @@ function handleStreamEvent({
947
988
  }
948
989
  }
949
990
  }
991
+ function isTextEntry(entry) {
992
+ return entry != null && typeof entry === "object" && "text" in entry;
993
+ }
950
994
  function stringifyContent(content) {
951
995
  if (typeof content === "string") return content;
952
996
  if (Array.isArray(content)) {
953
997
  return content.map(
954
- (entry) => entry && typeof entry === "object" && "text" in entry ? String(entry.text ?? "") : JSON.stringify(entry)
998
+ (entry) => isTextEntry(entry) ? String(entry.text ?? "") : JSON.stringify(entry)
955
999
  ).join("");
956
1000
  }
957
1001
  return JSON.stringify(content);
958
1002
  }
1003
+ function hasNonTextContent(content) {
1004
+ return Array.isArray(content) && content.some((entry) => !isTextEntry(entry));
1005
+ }
1006
+ function resolveToolResult({
1007
+ toolName,
1008
+ dynamic,
1009
+ isError,
1010
+ rawContent
1011
+ }) {
1012
+ if (toolName === "bash") {
1013
+ return { exitCode: isError ? 1 : 0, stdout: stringifyContent(rawContent) };
1014
+ }
1015
+ if (hasNonTextContent(rawContent)) return rawContent;
1016
+ const content = stringifyContent(rawContent);
1017
+ return dynamic ? parseMcpToolResult(content) : content;
1018
+ }
959
1019
  function parseMcpToolResult(content) {
960
1020
  try {
961
- return JSON.parse(content);
1021
+ const parsed = JSON.parse(content);
1022
+ return parsed !== null && typeof parsed === "object" ? parsed : content;
962
1023
  } catch {
963
1024
  return content;
964
1025
  }
@@ -1098,7 +1159,7 @@ var PUBLIC_TO_NATIVE = {
1098
1159
  ExitPlanMode: "ExitPlanMode",
1099
1160
  EnterWorktree: "EnterWorktree",
1100
1161
  ExitWorktree: "ExitWorktree",
1101
- AskUserQuestion: "AskUserQuestion",
1162
+ askUserQuestions: "AskUserQuestion",
1102
1163
  Skill: "Skill",
1103
1164
  ToolSearch: "ToolSearch"
1104
1165
  };
@@ -1118,6 +1179,74 @@ function resolveInactiveNativeTools(toolFiltering) {
1118
1179
  return inactiveToolNames.map((name) => toNativeName(name));
1119
1180
  }
1120
1181
 
1182
+ // src/bridge/question-tool.ts
1183
+ function claudeCodeQuestionKey(nativeInput) {
1184
+ return JSON.stringify(nativeInput.questions);
1185
+ }
1186
+ function toHarnessQuestionsInput(nativeInput) {
1187
+ return {
1188
+ allowPartialAnswers: true,
1189
+ questions: nativeInput.questions.map((question, questionIndex) => ({
1190
+ id: `question-${questionIndex + 1}`,
1191
+ question: question.question,
1192
+ header: question.header,
1193
+ options: question.options.map((option, optionIndex) => ({
1194
+ id: `option-${optionIndex + 1}`,
1195
+ label: option.label,
1196
+ description: option.description,
1197
+ ...option.preview !== void 0 ? { preview: option.preview } : {}
1198
+ })),
1199
+ allowMultiple: question.multiSelect,
1200
+ allowFreeForm: true
1201
+ }))
1202
+ };
1203
+ }
1204
+ function toClaudeCodeQuestionResult(input) {
1205
+ const output = input.output;
1206
+ if (output.action === "declined" || output.action === "cancelled") {
1207
+ return {
1208
+ behavior: "deny",
1209
+ message: `The user ${output.action} the questions.`
1210
+ };
1211
+ }
1212
+ const canonicalInput = toHarnessQuestionsInput(input.nativeInput);
1213
+ const answers = Object.fromEntries(
1214
+ canonicalInput.questions.flatMap((question, questionIndex) => {
1215
+ const answer = output.answers[question.id];
1216
+ if (answer == null) return [];
1217
+ const nativeQuestion = input.nativeInput.questions[questionIndex];
1218
+ return [
1219
+ [
1220
+ nativeQuestion.question,
1221
+ toClaudeCodeAnswer({
1222
+ nativeQuestion,
1223
+ optionIds: answer.optionIds,
1224
+ freeform: answer.freeform
1225
+ })
1226
+ ]
1227
+ ];
1228
+ })
1229
+ );
1230
+ return {
1231
+ behavior: "allow",
1232
+ updatedInput: { ...input.nativeInput, answers }
1233
+ };
1234
+ }
1235
+ function toClaudeCodeAnswer(input) {
1236
+ if (input.freeform !== void 0) return input.freeform;
1237
+ const labels = input.optionIds.flatMap((optionId) => {
1238
+ const index = positionalIdIndex({ id: optionId, prefix: "option-" });
1239
+ const option = index == null ? void 0 : input.nativeQuestion.options[index];
1240
+ return option == null ? [] : [option.label];
1241
+ });
1242
+ return input.nativeQuestion.multiSelect ? labels.join(", ") : labels[0] ?? "";
1243
+ }
1244
+ function positionalIdIndex(input) {
1245
+ if (!input.id.startsWith(input.prefix)) return void 0;
1246
+ const oneBasedIndex = Number(input.id.slice(input.prefix.length));
1247
+ return Number.isInteger(oneBasedIndex) && oneBasedIndex > 0 ? oneBasedIndex - 1 : void 0;
1248
+ }
1249
+
1121
1250
  // src/bridge/index.ts
1122
1251
  var NATIVE_TO_COMMON = {
1123
1252
  Read: "read",
@@ -1126,7 +1255,8 @@ var NATIVE_TO_COMMON = {
1126
1255
  Bash: "bash",
1127
1256
  Glob: "glob",
1128
1257
  Grep: "grep",
1129
- WebSearch: "webSearch"
1258
+ WebSearch: "webSearch",
1259
+ AskUserQuestion: "askUserQuestions"
1130
1260
  };
1131
1261
  var NATIVE_TOOL_KINDS = {
1132
1262
  Read: "readonly",
@@ -1186,15 +1316,9 @@ function createPermissionOptions(input) {
1186
1316
  permissionMode,
1187
1317
  inactiveNativeTools
1188
1318
  });
1189
- if (permissionMode === "allow-all" && inactiveNativeTools.size === 0) {
1190
- return {
1191
- permissionMode: "bypassPermissions",
1192
- allowDangerouslySkipPermissions: true
1193
- };
1194
- }
1195
1319
  return {
1196
- permissionMode: permissionMode === "allow-edits" ? "acceptEdits" : "default",
1197
- allowDangerouslySkipPermissions: false,
1320
+ permissionMode: permissionMode === "allow-all" ? "bypassPermissions" : permissionMode === "allow-edits" ? "acceptEdits" : "default",
1321
+ allowDangerouslySkipPermissions: permissionMode === "allow-all",
1198
1322
  ...permissionSettings ? { settings: permissionSettings } : {},
1199
1323
  canUseTool: async (toolName, toolInput, options) => {
1200
1324
  if (toolName.startsWith("mcp__harness-tools__")) {
@@ -1233,6 +1357,56 @@ function createPermissionOptions(input) {
1233
1357
  }
1234
1358
  };
1235
1359
  }
1360
+ function createQuestionPreToolUseHook(input) {
1361
+ return async (hookInput, toolUseID) => {
1362
+ if (hookInput.hook_event_name !== "PreToolUse" || hookInput.tool_name !== "AskUserQuestion") {
1363
+ return {};
1364
+ }
1365
+ const nativeInput = hookInput.tool_input;
1366
+ const canonicalInput = toHarnessQuestionsInput(nativeInput);
1367
+ const toolCallId = toolUseID ?? hookInput.tool_use_id;
1368
+ input.nativeToolCallNames.set(toolCallId, hookInput.tool_name);
1369
+ input.emit({
1370
+ type: "tool-call",
1371
+ toolCallId,
1372
+ toolName: "askUserQuestions",
1373
+ nativeName: hookInput.tool_name,
1374
+ input: JSON.stringify(canonicalInput),
1375
+ providerExecuted: false,
1376
+ providerMetadata: {
1377
+ "claude-code": {
1378
+ nativeRequest: nativeInput
1379
+ }
1380
+ }
1381
+ });
1382
+ const questionKey = claudeCodeQuestionKey(nativeInput);
1383
+ const result = await input.turn.requestToolResult({
1384
+ toolCallId,
1385
+ matches: (candidate) => {
1386
+ const nativeRequest = candidate.toolResult?.providerOptions?.["claude-code"]?.nativeRequest;
1387
+ return nativeRequest != null && claudeCodeQuestionKey(nativeRequest) === questionKey;
1388
+ }
1389
+ });
1390
+ const nativeResult = toClaudeCodeQuestionResult({
1391
+ nativeInput,
1392
+ output: result.output
1393
+ });
1394
+ return {
1395
+ hookSpecificOutput: {
1396
+ hookEventName: "PreToolUse",
1397
+ ...nativeResult.behavior === "allow" ? {
1398
+ permissionDecision: "allow",
1399
+ updatedInput: {
1400
+ ...nativeResult.updatedInput
1401
+ }
1402
+ } : {
1403
+ permissionDecision: "deny",
1404
+ permissionDecisionReason: nativeResult.message
1405
+ }
1406
+ }
1407
+ };
1408
+ };
1409
+ }
1236
1410
  function createPermissionSettings(input) {
1237
1411
  const askRules = /* @__PURE__ */ new Set();
1238
1412
  for (const [nativeName, kind] of Object.entries(NATIVE_TOOL_KINDS)) {
@@ -1336,6 +1510,11 @@ async function runTurn(start, turn) {
1336
1510
  nativeToolCallNames: streamEventState.nativeToolCallNames,
1337
1511
  approvalRequestedToolUseIds: streamEventState.approvalRequestedToolUseIds
1338
1512
  });
1513
+ const questionPreToolUseHook = createQuestionPreToolUseHook({
1514
+ turn,
1515
+ emit,
1516
+ nativeToolCallNames: streamEventState.nativeToolCallNames
1517
+ });
1339
1518
  const q = claudeSdk.query({
1340
1519
  prompt: queryInput.input,
1341
1520
  options: {
@@ -1359,6 +1538,12 @@ async function runTurn(start, turn) {
1359
1538
  // `compact_boundary` system message does not. Latch it for the unified
1360
1539
  // `compaction` event; return an empty output so compaction proceeds.
1361
1540
  hooks: {
1541
+ PreToolUse: [
1542
+ {
1543
+ matcher: "AskUserQuestion",
1544
+ hooks: [questionPreToolUseHook]
1545
+ }
1546
+ ],
1362
1547
  PostCompact: [
1363
1548
  {
1364
1549
  hooks: [
@@ -1437,6 +1622,12 @@ async function runTurn(start, turn) {
1437
1622
  emitStreamEvent(msg);
1438
1623
  if (type === "result") {
1439
1624
  if (msg.subtype === "success") {
1625
+ if (msg.is_error) {
1626
+ emitTerminalError(
1627
+ msg.result?.trim() || streamEventState.observedTerminalError || (typeof msg.api_error_status === "number" ? `Claude Code reported an API error (HTTP ${msg.api_error_status})` : "Claude Code reported a failed result")
1628
+ );
1629
+ continue;
1630
+ }
1440
1631
  const emptyResult = !msg.result?.trim?.();
1441
1632
  if (emptyResult && streamEventState.observedTerminalError) {
1442
1633
  emitTerminalError(streamEventState.observedTerminalError);