@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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ai-sdk/harness-claude-code",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.105",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
}
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@ai-sdk/harness": "1.0.
|
|
29
|
+
"@ai-sdk/harness": "1.0.101",
|
|
30
30
|
"@ai-sdk/provider-utils": "5.0.36",
|
|
31
31
|
"ws": "^8.21.0"
|
|
32
32
|
},
|
|
@@ -38,6 +38,9 @@ export type ClaudeMessage = {
|
|
|
38
38
|
usage?: Record<string, unknown>;
|
|
39
39
|
};
|
|
40
40
|
result?: string;
|
|
41
|
+
/** Result-message error flags; distinct from `error_status` (`api_retry`). */
|
|
42
|
+
is_error?: boolean;
|
|
43
|
+
api_error_status?: number | null;
|
|
41
44
|
errors?: ReadonlyArray<string>;
|
|
42
45
|
usage?: Record<string, unknown>;
|
|
43
46
|
total_cost_usd?: number;
|
|
@@ -246,6 +249,11 @@ export function createEmitStreamEvent({
|
|
|
246
249
|
continue;
|
|
247
250
|
}
|
|
248
251
|
state.nativeToolCallNames.set(block.id, block.name);
|
|
252
|
+
if (block.name === 'AskUserQuestion') {
|
|
253
|
+
state.pendingStepToolUseIds.add(block.id);
|
|
254
|
+
opensStep = true;
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
249
257
|
const dynamic = isExternalMcpTool(block.name);
|
|
250
258
|
if (dynamic) state.externalMcpToolUseIds.add(block.id);
|
|
251
259
|
if (state.approvalRequestedToolUseIds.has(block.id)) {
|
|
@@ -428,6 +436,9 @@ function handleStreamEvent({
|
|
|
428
436
|
if (nativeName === 'StructuredOutput') {
|
|
429
437
|
return;
|
|
430
438
|
}
|
|
439
|
+
if (nativeName === 'AskUserQuestion') {
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
431
442
|
const hostToolName = nativeName.startsWith(HOST_TOOL_PREFIX)
|
|
432
443
|
? nativeName.slice(HOST_TOOL_PREFIX.length)
|
|
433
444
|
: undefined;
|
package/src/bridge/index.ts
CHANGED
|
@@ -34,6 +34,10 @@ import { argv, env as procEnv, stdout } from 'node:process';
|
|
|
34
34
|
* 3. the dependency entry in `src/bridge/package.json`.
|
|
35
35
|
*/
|
|
36
36
|
import * as claudeAgentSdk from '@anthropic-ai/claude-agent-sdk';
|
|
37
|
+
import type {
|
|
38
|
+
HookCallback,
|
|
39
|
+
HookJSONOutput,
|
|
40
|
+
} from '@anthropic-ai/claude-agent-sdk';
|
|
37
41
|
import * as mcpServerModule from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
38
42
|
import { createClaudeCodeSystemPrompt } from './claude-code-system-prompt';
|
|
39
43
|
import { toClaudeSkillsOption } from './claude-skills-option';
|
|
@@ -52,6 +56,11 @@ import {
|
|
|
52
56
|
resolveInactiveNativeTools,
|
|
53
57
|
resolveNativeTools,
|
|
54
58
|
} from './tool-filtering';
|
|
59
|
+
import {
|
|
60
|
+
claudeCodeQuestionKey,
|
|
61
|
+
toClaudeCodeQuestionResult,
|
|
62
|
+
toHarnessQuestionsInput,
|
|
63
|
+
} from './question-tool';
|
|
55
64
|
|
|
56
65
|
/*
|
|
57
66
|
* Native Claude Code tool name → cross-harness common name. Tools outside this
|
|
@@ -65,7 +74,8 @@ type CommonBuiltinToolName =
|
|
|
65
74
|
| 'bash'
|
|
66
75
|
| 'glob'
|
|
67
76
|
| 'grep'
|
|
68
|
-
| 'webSearch'
|
|
77
|
+
| 'webSearch'
|
|
78
|
+
| 'askUserQuestions';
|
|
69
79
|
|
|
70
80
|
const NATIVE_TO_COMMON: Readonly<Record<string, CommonBuiltinToolName>> = {
|
|
71
81
|
Read: 'read',
|
|
@@ -75,6 +85,7 @@ const NATIVE_TO_COMMON: Readonly<Record<string, CommonBuiltinToolName>> = {
|
|
|
75
85
|
Glob: 'glob',
|
|
76
86
|
Grep: 'grep',
|
|
77
87
|
WebSearch: 'webSearch',
|
|
88
|
+
AskUserQuestion: 'askUserQuestions',
|
|
78
89
|
};
|
|
79
90
|
|
|
80
91
|
const NATIVE_TOOL_KINDS: Readonly<
|
|
@@ -162,17 +173,15 @@ function createPermissionOptions(input: {
|
|
|
162
173
|
permissionMode,
|
|
163
174
|
inactiveNativeTools,
|
|
164
175
|
});
|
|
165
|
-
if (permissionMode === 'allow-all' && inactiveNativeTools.size === 0) {
|
|
166
|
-
return {
|
|
167
|
-
permissionMode: 'bypassPermissions',
|
|
168
|
-
allowDangerouslySkipPermissions: true,
|
|
169
|
-
};
|
|
170
|
-
}
|
|
171
176
|
|
|
172
177
|
return {
|
|
173
178
|
permissionMode:
|
|
174
|
-
permissionMode === 'allow-
|
|
175
|
-
|
|
179
|
+
permissionMode === 'allow-all'
|
|
180
|
+
? 'bypassPermissions'
|
|
181
|
+
: permissionMode === 'allow-edits'
|
|
182
|
+
? 'acceptEdits'
|
|
183
|
+
: 'default',
|
|
184
|
+
allowDangerouslySkipPermissions: permissionMode === 'allow-all',
|
|
176
185
|
...(permissionSettings ? { settings: permissionSettings } : {}),
|
|
177
186
|
canUseTool: async (
|
|
178
187
|
toolName: string,
|
|
@@ -223,6 +232,80 @@ function createPermissionOptions(input: {
|
|
|
223
232
|
};
|
|
224
233
|
}
|
|
225
234
|
|
|
235
|
+
function createQuestionPreToolUseHook(input: {
|
|
236
|
+
turn: BridgeTurn;
|
|
237
|
+
emit: Emit;
|
|
238
|
+
nativeToolCallNames: Map<string, string>;
|
|
239
|
+
}): HookCallback {
|
|
240
|
+
return async (hookInput, toolUseID): Promise<HookJSONOutput> => {
|
|
241
|
+
if (
|
|
242
|
+
hookInput.hook_event_name !== 'PreToolUse' ||
|
|
243
|
+
hookInput.tool_name !== 'AskUserQuestion'
|
|
244
|
+
) {
|
|
245
|
+
return {};
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const nativeInput = hookInput.tool_input as Parameters<
|
|
249
|
+
typeof toHarnessQuestionsInput
|
|
250
|
+
>[0];
|
|
251
|
+
const canonicalInput = toHarnessQuestionsInput(nativeInput);
|
|
252
|
+
const toolCallId = toolUseID ?? hookInput.tool_use_id;
|
|
253
|
+
input.nativeToolCallNames.set(toolCallId, hookInput.tool_name);
|
|
254
|
+
input.emit({
|
|
255
|
+
type: 'tool-call',
|
|
256
|
+
toolCallId,
|
|
257
|
+
toolName: 'askUserQuestions',
|
|
258
|
+
nativeName: hookInput.tool_name,
|
|
259
|
+
input: JSON.stringify(canonicalInput),
|
|
260
|
+
providerExecuted: false,
|
|
261
|
+
providerMetadata: {
|
|
262
|
+
'claude-code': {
|
|
263
|
+
nativeRequest: nativeInput,
|
|
264
|
+
},
|
|
265
|
+
},
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
const questionKey = claudeCodeQuestionKey(nativeInput);
|
|
269
|
+
const result = await input.turn.requestToolResult({
|
|
270
|
+
toolCallId,
|
|
271
|
+
matches: candidate => {
|
|
272
|
+
const nativeRequest = candidate.toolResult?.providerOptions?.[
|
|
273
|
+
'claude-code'
|
|
274
|
+
]?.nativeRequest as
|
|
275
|
+
| Parameters<typeof claudeCodeQuestionKey>[0]
|
|
276
|
+
| undefined;
|
|
277
|
+
return (
|
|
278
|
+
nativeRequest != null &&
|
|
279
|
+
claudeCodeQuestionKey(nativeRequest) === questionKey
|
|
280
|
+
);
|
|
281
|
+
},
|
|
282
|
+
});
|
|
283
|
+
const nativeResult = toClaudeCodeQuestionResult({
|
|
284
|
+
nativeInput,
|
|
285
|
+
output: result.output as Parameters<
|
|
286
|
+
typeof toClaudeCodeQuestionResult
|
|
287
|
+
>[0]['output'],
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
return {
|
|
291
|
+
hookSpecificOutput: {
|
|
292
|
+
hookEventName: 'PreToolUse',
|
|
293
|
+
...(nativeResult.behavior === 'allow'
|
|
294
|
+
? {
|
|
295
|
+
permissionDecision: 'allow',
|
|
296
|
+
updatedInput: {
|
|
297
|
+
...nativeResult.updatedInput,
|
|
298
|
+
} as Record<string, unknown>,
|
|
299
|
+
}
|
|
300
|
+
: {
|
|
301
|
+
permissionDecision: 'deny',
|
|
302
|
+
permissionDecisionReason: nativeResult.message,
|
|
303
|
+
}),
|
|
304
|
+
},
|
|
305
|
+
};
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
|
|
226
309
|
function createPermissionSettings(input: {
|
|
227
310
|
permissionMode: 'allow-reads' | 'allow-edits' | 'allow-all';
|
|
228
311
|
inactiveNativeTools: ReadonlySet<string>;
|
|
@@ -372,6 +455,11 @@ async function runTurn(start: StartMessage, turn: BridgeTurn): Promise<void> {
|
|
|
372
455
|
nativeToolCallNames: streamEventState.nativeToolCallNames,
|
|
373
456
|
approvalRequestedToolUseIds: streamEventState.approvalRequestedToolUseIds,
|
|
374
457
|
});
|
|
458
|
+
const questionPreToolUseHook = createQuestionPreToolUseHook({
|
|
459
|
+
turn,
|
|
460
|
+
emit,
|
|
461
|
+
nativeToolCallNames: streamEventState.nativeToolCallNames,
|
|
462
|
+
});
|
|
375
463
|
|
|
376
464
|
const q = claudeSdk.query({
|
|
377
465
|
prompt: queryInput.input,
|
|
@@ -401,6 +489,12 @@ async function runTurn(start: StartMessage, turn: BridgeTurn): Promise<void> {
|
|
|
401
489
|
// `compact_boundary` system message does not. Latch it for the unified
|
|
402
490
|
// `compaction` event; return an empty output so compaction proceeds.
|
|
403
491
|
hooks: {
|
|
492
|
+
PreToolUse: [
|
|
493
|
+
{
|
|
494
|
+
matcher: 'AskUserQuestion',
|
|
495
|
+
hooks: [questionPreToolUseHook],
|
|
496
|
+
},
|
|
497
|
+
],
|
|
404
498
|
PostCompact: [
|
|
405
499
|
{
|
|
406
500
|
hooks: [
|
|
@@ -507,6 +601,19 @@ async function runTurn(start: StartMessage, turn: BridgeTurn): Promise<void> {
|
|
|
507
601
|
|
|
508
602
|
if (type === 'result') {
|
|
509
603
|
if (msg.subtype === 'success') {
|
|
604
|
+
// `success` does not mean the turn succeeded: the CLI flags a rejected
|
|
605
|
+
// request with `is_error` and puts the message in `result`, which the
|
|
606
|
+
// empty-result rescue below cannot catch.
|
|
607
|
+
if (msg.is_error) {
|
|
608
|
+
emitTerminalError(
|
|
609
|
+
msg.result?.trim() ||
|
|
610
|
+
streamEventState.observedTerminalError ||
|
|
611
|
+
(typeof msg.api_error_status === 'number'
|
|
612
|
+
? `Claude Code reported an API error (HTTP ${msg.api_error_status})`
|
|
613
|
+
: 'Claude Code reported a failed result'),
|
|
614
|
+
);
|
|
615
|
+
continue;
|
|
616
|
+
}
|
|
510
617
|
const emptyResult = !msg.result?.trim?.();
|
|
511
618
|
if (emptyResult && streamEventState.observedTerminalError) {
|
|
512
619
|
emitTerminalError(streamEventState.observedTerminalError);
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
HarnessV1QuestionsToolInput,
|
|
3
|
+
HarnessV1QuestionsToolOutput,
|
|
4
|
+
} from '@ai-sdk/harness';
|
|
5
|
+
import type { AskUserQuestionInput } from '@anthropic-ai/claude-agent-sdk/sdk-tools.js';
|
|
6
|
+
|
|
7
|
+
type ClaudeCodeQuestion = AskUserQuestionInput['questions'][number];
|
|
8
|
+
|
|
9
|
+
export function claudeCodeQuestionKey(
|
|
10
|
+
nativeInput: AskUserQuestionInput,
|
|
11
|
+
): string {
|
|
12
|
+
return JSON.stringify(nativeInput.questions);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function toHarnessQuestionsInput(
|
|
16
|
+
nativeInput: AskUserQuestionInput,
|
|
17
|
+
): HarnessV1QuestionsToolInput {
|
|
18
|
+
return {
|
|
19
|
+
allowPartialAnswers: true,
|
|
20
|
+
questions: nativeInput.questions.map((question, questionIndex) => ({
|
|
21
|
+
id: `question-${questionIndex + 1}`,
|
|
22
|
+
question: question.question,
|
|
23
|
+
header: question.header,
|
|
24
|
+
options: question.options.map((option, optionIndex) => ({
|
|
25
|
+
id: `option-${optionIndex + 1}`,
|
|
26
|
+
label: option.label,
|
|
27
|
+
description: option.description,
|
|
28
|
+
...(option.preview !== undefined ? { preview: option.preview } : {}),
|
|
29
|
+
})),
|
|
30
|
+
allowMultiple: question.multiSelect,
|
|
31
|
+
allowFreeForm: true,
|
|
32
|
+
})),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function toClaudeCodeQuestionResult(input: {
|
|
37
|
+
nativeInput: AskUserQuestionInput;
|
|
38
|
+
output: HarnessV1QuestionsToolOutput;
|
|
39
|
+
}):
|
|
40
|
+
| {
|
|
41
|
+
behavior: 'allow';
|
|
42
|
+
updatedInput: AskUserQuestionInput & { answers: Record<string, string> };
|
|
43
|
+
}
|
|
44
|
+
| { behavior: 'deny'; message: string } {
|
|
45
|
+
const output = input.output;
|
|
46
|
+
if (output.action === 'declined' || output.action === 'cancelled') {
|
|
47
|
+
return {
|
|
48
|
+
behavior: 'deny',
|
|
49
|
+
message: `The user ${output.action} the questions.`,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const canonicalInput = toHarnessQuestionsInput(input.nativeInput);
|
|
54
|
+
const answers = Object.fromEntries(
|
|
55
|
+
canonicalInput.questions.flatMap((question, questionIndex) => {
|
|
56
|
+
const answer = output.answers[question.id];
|
|
57
|
+
if (answer == null) return [];
|
|
58
|
+
const nativeQuestion = input.nativeInput.questions[questionIndex];
|
|
59
|
+
return [
|
|
60
|
+
[
|
|
61
|
+
nativeQuestion.question,
|
|
62
|
+
toClaudeCodeAnswer({
|
|
63
|
+
nativeQuestion,
|
|
64
|
+
optionIds: answer.optionIds,
|
|
65
|
+
freeform: answer.freeform,
|
|
66
|
+
}),
|
|
67
|
+
],
|
|
68
|
+
];
|
|
69
|
+
}),
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
behavior: 'allow',
|
|
74
|
+
updatedInput: { ...input.nativeInput, answers },
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function toClaudeCodeAnswer(input: {
|
|
79
|
+
nativeQuestion: ClaudeCodeQuestion;
|
|
80
|
+
optionIds: string[];
|
|
81
|
+
freeform: string | undefined;
|
|
82
|
+
}): string {
|
|
83
|
+
if (input.freeform !== undefined) return input.freeform;
|
|
84
|
+
const labels = input.optionIds.flatMap(optionId => {
|
|
85
|
+
const index = positionalIdIndex({ id: optionId, prefix: 'option-' });
|
|
86
|
+
const option =
|
|
87
|
+
index == null ? undefined : input.nativeQuestion.options[index];
|
|
88
|
+
return option == null ? [] : [option.label];
|
|
89
|
+
});
|
|
90
|
+
return input.nativeQuestion.multiSelect
|
|
91
|
+
? labels.join(', ')
|
|
92
|
+
: (labels[0] ?? '');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function positionalIdIndex(input: {
|
|
96
|
+
id: string;
|
|
97
|
+
prefix: string;
|
|
98
|
+
}): number | undefined {
|
|
99
|
+
if (!input.id.startsWith(input.prefix)) return undefined;
|
|
100
|
+
const oneBasedIndex = Number(input.id.slice(input.prefix.length));
|
|
101
|
+
return Number.isInteger(oneBasedIndex) && oneBasedIndex > 0
|
|
102
|
+
? oneBasedIndex - 1
|
|
103
|
+
: undefined;
|
|
104
|
+
}
|
|
@@ -26,7 +26,7 @@ const PUBLIC_TO_NATIVE: Readonly<Record<string, string>> = {
|
|
|
26
26
|
ExitPlanMode: 'ExitPlanMode',
|
|
27
27
|
EnterWorktree: 'EnterWorktree',
|
|
28
28
|
ExitWorktree: 'ExitWorktree',
|
|
29
|
-
|
|
29
|
+
askUserQuestions: 'AskUserQuestion',
|
|
30
30
|
Skill: 'Skill',
|
|
31
31
|
ToolSearch: 'ToolSearch',
|
|
32
32
|
};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { posix } from 'node:path';
|
|
2
2
|
import {
|
|
3
3
|
commonTool,
|
|
4
|
+
HARNESS_V1_BUILTIN_TOOLS,
|
|
4
5
|
harnessV1DiagnosticFromBridgeFrame,
|
|
5
6
|
HarnessCapabilityUnsupportedError,
|
|
6
7
|
type HarnessV1,
|
|
@@ -461,39 +462,14 @@ const CLAUDE_CODE_BUILTIN_TOOLS = {
|
|
|
461
462
|
discard_changes: z.boolean().optional(),
|
|
462
463
|
}),
|
|
463
464
|
}),
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
options: z.array(
|
|
473
|
-
z.object({
|
|
474
|
-
label: z.string(),
|
|
475
|
-
description: z.string(),
|
|
476
|
-
preview: z.string().optional(),
|
|
477
|
-
}),
|
|
478
|
-
),
|
|
479
|
-
multiSelect: z.boolean(),
|
|
480
|
-
}),
|
|
481
|
-
)
|
|
482
|
-
.min(1)
|
|
483
|
-
.max(4),
|
|
484
|
-
answers: z.record(z.string(), z.string()).optional(),
|
|
485
|
-
annotations: z
|
|
486
|
-
.record(
|
|
487
|
-
z.string(),
|
|
488
|
-
z.object({
|
|
489
|
-
preview: z.string().optional(),
|
|
490
|
-
notes: z.string().optional(),
|
|
491
|
-
}),
|
|
492
|
-
)
|
|
493
|
-
.optional(),
|
|
494
|
-
metadata: z.object({ source: z.string().optional() }).optional(),
|
|
495
|
-
}),
|
|
496
|
-
}),
|
|
465
|
+
askUserQuestions: {
|
|
466
|
+
...HARNESS_V1_BUILTIN_TOOLS.askUserQuestions,
|
|
467
|
+
nativeName: 'AskUserQuestion',
|
|
468
|
+
toolUseKind: 'readonly',
|
|
469
|
+
} as typeof HARNESS_V1_BUILTIN_TOOLS.askUserQuestions & {
|
|
470
|
+
readonly nativeName: 'AskUserQuestion';
|
|
471
|
+
readonly toolUseKind: 'readonly';
|
|
472
|
+
},
|
|
497
473
|
Skill: {
|
|
498
474
|
...tool({
|
|
499
475
|
description: 'Activate a skill by name',
|
|
@@ -1697,6 +1673,9 @@ function createSession({
|
|
|
1697
1673
|
toolCallId: input.toolCallId,
|
|
1698
1674
|
output: input.output,
|
|
1699
1675
|
isError: input.isError,
|
|
1676
|
+
...(input.toolResult !== undefined
|
|
1677
|
+
? { toolResult: input.toolResult }
|
|
1678
|
+
: {}),
|
|
1700
1679
|
});
|
|
1701
1680
|
},
|
|
1702
1681
|
submitToolApproval: async input => {
|