@ai-sdk/harness-claude-code 1.0.104 → 1.0.106

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,26 @@
1
1
  # @ai-sdk/harness-claude-code
2
2
 
3
+ ## 1.0.106
4
+
5
+ ### Patch Changes
6
+
7
+ - eeed977: feat(harness): allow passing arbitrary headers with inference requests via `headers` property in `HarnessAgentSettings`
8
+ - a39c8bf: chore(harness): clarify `writeSkills` helper intent to require materializing skills in HOME directory
9
+ - Updated dependencies [fe86f8f]
10
+ - Updated dependencies [255cccf]
11
+ - Updated dependencies [eeed977]
12
+ - Updated dependencies [a39c8bf]
13
+ - @ai-sdk/harness@1.0.102
14
+
15
+ ## 1.0.105
16
+
17
+ ### Patch Changes
18
+
19
+ - 951c54d: feat(harness): support `askUserQuestions` tool including support for normalization across harness adapters
20
+ - a554cc8: fix (harness-claude-code): treat a `result` message flagged `is_error` as a terminal error
21
+ - Updated dependencies [951c54d]
22
+ - @ai-sdk/harness@1.0.101
23
+
3
24
  ## 1.0.104
4
25
 
5
26
  ### 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)) {
@@ -904,6 +939,9 @@ function handleStreamEvent({
904
939
  if (nativeName === "StructuredOutput") {
905
940
  return;
906
941
  }
942
+ if (nativeName === "AskUserQuestion") {
943
+ return;
944
+ }
907
945
  const hostToolName = nativeName.startsWith(HOST_TOOL_PREFIX) ? nativeName.slice(HOST_TOOL_PREFIX.length) : void 0;
908
946
  const dynamic = isExternalMcpTool(nativeName);
909
947
  partialBlocks.set(index, { id, kind: "tool-input" });
@@ -1121,7 +1159,7 @@ var PUBLIC_TO_NATIVE = {
1121
1159
  ExitPlanMode: "ExitPlanMode",
1122
1160
  EnterWorktree: "EnterWorktree",
1123
1161
  ExitWorktree: "ExitWorktree",
1124
- AskUserQuestion: "AskUserQuestion",
1162
+ askUserQuestions: "AskUserQuestion",
1125
1163
  Skill: "Skill",
1126
1164
  ToolSearch: "ToolSearch"
1127
1165
  };
@@ -1141,6 +1179,74 @@ function resolveInactiveNativeTools(toolFiltering) {
1141
1179
  return inactiveToolNames.map((name) => toNativeName(name));
1142
1180
  }
1143
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
+
1144
1250
  // src/bridge/index.ts
1145
1251
  var NATIVE_TO_COMMON = {
1146
1252
  Read: "read",
@@ -1149,7 +1255,8 @@ var NATIVE_TO_COMMON = {
1149
1255
  Bash: "bash",
1150
1256
  Glob: "glob",
1151
1257
  Grep: "grep",
1152
- WebSearch: "webSearch"
1258
+ WebSearch: "webSearch",
1259
+ AskUserQuestion: "askUserQuestions"
1153
1260
  };
1154
1261
  var NATIVE_TOOL_KINDS = {
1155
1262
  Read: "readonly",
@@ -1209,15 +1316,9 @@ function createPermissionOptions(input) {
1209
1316
  permissionMode,
1210
1317
  inactiveNativeTools
1211
1318
  });
1212
- if (permissionMode === "allow-all" && inactiveNativeTools.size === 0) {
1213
- return {
1214
- permissionMode: "bypassPermissions",
1215
- allowDangerouslySkipPermissions: true
1216
- };
1217
- }
1218
1319
  return {
1219
- permissionMode: permissionMode === "allow-edits" ? "acceptEdits" : "default",
1220
- allowDangerouslySkipPermissions: false,
1320
+ permissionMode: permissionMode === "allow-all" ? "bypassPermissions" : permissionMode === "allow-edits" ? "acceptEdits" : "default",
1321
+ allowDangerouslySkipPermissions: permissionMode === "allow-all",
1221
1322
  ...permissionSettings ? { settings: permissionSettings } : {},
1222
1323
  canUseTool: async (toolName, toolInput, options) => {
1223
1324
  if (toolName.startsWith("mcp__harness-tools__")) {
@@ -1256,6 +1357,56 @@ function createPermissionOptions(input) {
1256
1357
  }
1257
1358
  };
1258
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
+ }
1259
1410
  function createPermissionSettings(input) {
1260
1411
  const askRules = /* @__PURE__ */ new Set();
1261
1412
  for (const [nativeName, kind] of Object.entries(NATIVE_TOOL_KINDS)) {
@@ -1359,6 +1510,11 @@ async function runTurn(start, turn) {
1359
1510
  nativeToolCallNames: streamEventState.nativeToolCallNames,
1360
1511
  approvalRequestedToolUseIds: streamEventState.approvalRequestedToolUseIds
1361
1512
  });
1513
+ const questionPreToolUseHook = createQuestionPreToolUseHook({
1514
+ turn,
1515
+ emit,
1516
+ nativeToolCallNames: streamEventState.nativeToolCallNames
1517
+ });
1362
1518
  const q = claudeSdk.query({
1363
1519
  prompt: queryInput.input,
1364
1520
  options: {
@@ -1382,6 +1538,12 @@ async function runTurn(start, turn) {
1382
1538
  // `compact_boundary` system message does not. Latch it for the unified
1383
1539
  // `compaction` event; return an empty output so compaction proceeds.
1384
1540
  hooks: {
1541
+ PreToolUse: [
1542
+ {
1543
+ matcher: "AskUserQuestion",
1544
+ hooks: [questionPreToolUseHook]
1545
+ }
1546
+ ],
1385
1547
  PostCompact: [
1386
1548
  {
1387
1549
  hooks: [
@@ -1460,6 +1622,12 @@ async function runTurn(start, turn) {
1460
1622
  emitStreamEvent(msg);
1461
1623
  if (type === "result") {
1462
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
+ }
1463
1631
  const emptyResult = !msg.result?.trim?.();
1464
1632
  if (emptyResult && streamEventState.observedTerminalError) {
1465
1633
  emitTerminalError(streamEventState.observedTerminalError);