@ai-sdk/harness-claude-code 1.0.104 → 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 +9 -0
- package/dist/bridge/index.mjs +185 -17
- package/dist/bridge/index.mjs.map +1 -1
- package/dist/index.d.ts +37 -36
- package/dist/index.js +9 -30
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/bridge/create-emit-stream-event.ts +11 -0
- package/src/bridge/index.ts +116 -9
- package/src/bridge/question-tool.ts +104 -0
- package/src/bridge/tool-filtering.ts +1 -1
- package/src/claude-code-harness.ts +12 -33
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
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
|
+
|
|
3
12
|
## 1.0.104
|
|
4
13
|
|
|
5
14
|
### Patch Changes
|
package/dist/bridge/index.mjs
CHANGED
|
@@ -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: (
|
|
397
|
-
|
|
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
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
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
|
-
|
|
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:
|
|
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);
|