@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-sdk/harness-claude-code",
3
- "version": "1.0.104",
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.100",
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;
@@ -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-edits' ? 'acceptEdits' : 'default',
175
- allowDangerouslySkipPermissions: false,
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
- AskUserQuestion: 'AskUserQuestion',
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
- AskUserQuestion: tool({
465
- description: 'Ask the user multiple-choice questions via a structured UI',
466
- inputSchema: z.object({
467
- questions: z
468
- .array(
469
- z.object({
470
- question: z.string(),
471
- header: z.string(),
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 => {