@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 +18 -0
- package/dist/bridge/index.mjs +213 -22
- package/dist/bridge/index.mjs.map +1 -1
- package/dist/index.d.ts +37 -36
- package/dist/index.js +14 -31
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/bridge/create-emit-stream-event.ts +61 -22
- 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 +18 -34
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
|
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)) {
|
|
@@ -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
|
|
814
|
-
|
|
815
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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);
|