@ai-sdk/harness-claude-code 1.0.154 → 1.0.156

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.
@@ -6,6 +6,7 @@
6
6
 
7
7
  import {
8
8
  runBridge,
9
+ convertHarnessToolModelOutput,
9
10
  type BridgeEvent,
10
11
  type BridgeTurn,
11
12
  type Experimental_BridgeUserMessage,
@@ -46,7 +47,9 @@ import {
46
47
  createEmitStreamEvent,
47
48
  defaultUsage,
48
49
  emitFinishStep,
49
- finishApprovalStep,
50
+ markWaitingOnHostInput,
51
+ clearWaitingOnHostInput,
52
+ resolveMcpHandlerToolUseId,
50
53
  isExternalMcpTool,
51
54
  mapUsage,
52
55
  type ClaudeMessage,
@@ -164,9 +167,11 @@ function createPermissionOptions(input: {
164
167
  inactiveNativeTools: readonly string[];
165
168
  turn: BridgeTurn;
166
169
  emit: Emit;
167
- finishApprovalStep: (approvalId: string) => void;
170
+ markWaitingOnHostInput: (toolUseId: string) => void;
171
+ clearWaitingOnHostInput: (toolUseId: string) => void;
168
172
  nativeToolCallNames: Map<string, string>;
169
173
  approvalRequestedToolUseIds: Set<string>;
174
+ emittedToolCallIds: Set<string>;
170
175
  }): Record<string, unknown> {
171
176
  const permissionMode = input.start.permissionMode ?? 'allow-all';
172
177
  const inactiveNativeTools = new Set(input.inactiveNativeTools);
@@ -222,23 +227,29 @@ function createPermissionOptions(input: {
222
227
  const approvalId = options.toolUseID;
223
228
  input.approvalRequestedToolUseIds.add(approvalId);
224
229
  input.nativeToolCallNames.set(approvalId, toolName);
225
- input.emit({
226
- type: 'tool-call',
227
- toolCallId: approvalId,
228
- toolName: toCommonName(toolName),
229
- nativeName: toolName,
230
- input: JSON.stringify(toolInput ?? {}),
231
- providerExecuted: true,
232
- ...(isExternalMcpTool(toolName) ? { dynamic: true } : {}),
233
- });
230
+ if (!input.emittedToolCallIds.has(approvalId)) {
231
+ input.emittedToolCallIds.add(approvalId);
232
+ input.emit({
233
+ type: 'tool-call',
234
+ toolCallId: approvalId,
235
+ toolName: toCommonName(toolName),
236
+ nativeName: toolName,
237
+ input: JSON.stringify(toolInput ?? {}),
238
+ providerExecuted: true,
239
+ ...(isExternalMcpTool(toolName) ? { dynamic: true } : {}),
240
+ });
241
+ }
242
+ const decisionPromise = input.turn.requestToolApproval(approvalId);
234
243
  input.emit({
235
244
  type: 'tool-approval-request',
236
245
  approvalId,
237
246
  toolCallId: approvalId,
238
247
  });
239
- input.finishApprovalStep(approvalId);
248
+ input.markWaitingOnHostInput(approvalId);
240
249
 
241
- const decision = await input.turn.requestToolApproval(approvalId);
250
+ const decision = await decisionPromise.finally(() =>
251
+ input.clearWaitingOnHostInput(approvalId),
252
+ );
242
253
  return decision.approved
243
254
  ? { behavior: 'allow', updatedInput: toolInput, toolUseID: approvalId }
244
255
  : {
@@ -254,6 +265,8 @@ function createQuestionPreToolUseHook(input: {
254
265
  turn: BridgeTurn;
255
266
  emit: Emit;
256
267
  nativeToolCallNames: Map<string, string>;
268
+ markWaitingOnHostInput: (toolUseId: string) => void;
269
+ clearWaitingOnHostInput: (toolUseId: string) => void;
257
270
  }): HookCallback {
258
271
  return async (hookInput, toolUseID): Promise<HookJSONOutput> => {
259
272
  if (
@@ -284,20 +297,23 @@ function createQuestionPreToolUseHook(input: {
284
297
  });
285
298
 
286
299
  const questionKey = claudeCodeQuestionKey(nativeInput);
287
- const result = await input.turn.requestToolResult({
288
- toolCallId,
289
- matches: candidate => {
290
- const nativeRequest = candidate.toolResult?.providerOptions?.[
291
- 'claude-code'
292
- ]?.nativeRequest as
293
- | Parameters<typeof claudeCodeQuestionKey>[0]
294
- | undefined;
295
- return (
296
- nativeRequest != null &&
297
- claudeCodeQuestionKey(nativeRequest) === questionKey
298
- );
299
- },
300
- });
300
+ input.markWaitingOnHostInput(toolCallId);
301
+ const result = await input.turn
302
+ .requestToolResult({
303
+ toolCallId,
304
+ matches: candidate => {
305
+ const nativeRequest = candidate.toolResult?.providerOptions?.[
306
+ 'claude-code'
307
+ ]?.nativeRequest as
308
+ | Parameters<typeof claudeCodeQuestionKey>[0]
309
+ | undefined;
310
+ return (
311
+ nativeRequest != null &&
312
+ claudeCodeQuestionKey(nativeRequest) === questionKey
313
+ );
314
+ },
315
+ })
316
+ .finally(() => input.clearWaitingOnHostInput(toolCallId));
301
317
  const nativeResult = toClaudeCodeQuestionResult({
302
318
  nativeInput,
303
319
  output: result.output as Parameters<
@@ -413,21 +429,50 @@ async function runTurn(start: StartMessage, turn: BridgeTurn): Promise<void> {
413
429
  const [input, extra] = handlerArgs;
414
430
  const metadataToolCallId = extra._meta?.['claudecode/toolUseId'];
415
431
  const toolCallId =
416
- typeof metadataToolCallId === 'string'
417
- ? metadataToolCallId
418
- : randomUUID();
432
+ resolveMcpHandlerToolUseId({
433
+ state: streamEventState,
434
+ metadataToolUseId:
435
+ typeof metadataToolCallId === 'string'
436
+ ? metadataToolCallId
437
+ : undefined,
438
+ toolName: tool.name,
439
+ input,
440
+ }) ?? randomUUID();
419
441
  streamEventState.mcpHandlerCalls.set(toolCallId, {
420
442
  toolName: tool.name,
421
443
  input,
422
444
  });
423
- emit({
424
- type: 'tool-call',
425
- toolCallId,
426
- toolName: tool.name,
427
- input: JSON.stringify(input),
428
- providerExecuted: false,
429
- });
430
- const { output, isError } = await turn.requestToolResult(toolCallId);
445
+ if (!streamEventState.emittedToolCallIds.has(toolCallId)) {
446
+ streamEventState.emittedToolCallIds.add(toolCallId);
447
+ emit({
448
+ type: 'tool-call',
449
+ toolCallId,
450
+ toolName: tool.name,
451
+ input: JSON.stringify(input),
452
+ providerExecuted: false,
453
+ });
454
+ markWaitingOnHostInput({
455
+ state: streamEventState,
456
+ emit,
457
+ toolUseId: toolCallId,
458
+ });
459
+ }
460
+ const { output, isError, toolResult } = await turn
461
+ .requestToolResult(toolCallId)
462
+ .finally(() => {
463
+ clearWaitingOnHostInput({
464
+ state: streamEventState,
465
+ toolUseId: toolCallId,
466
+ });
467
+ /*
468
+ * A handler call that could not be matched to a reported
469
+ * `tool_use` block never receives a native tool result, so it
470
+ * must not keep the step open once the host has answered.
471
+ */
472
+ if (!streamEventState.mcpToolCalls.has(toolCallId)) {
473
+ streamEventState.pendingStepToolUseIds.delete(toolCallId);
474
+ }
475
+ });
431
476
  emit({
432
477
  type: 'tool-result',
433
478
  toolCallId,
@@ -435,6 +480,23 @@ async function runTurn(start: StartMessage, turn: BridgeTurn): Promise<void> {
435
480
  result: output ?? null,
436
481
  isError: !!isError,
437
482
  });
483
+ if (toolResult != null) {
484
+ const converted = convertHarnessToolModelOutput({
485
+ output: toolResult.output,
486
+ });
487
+ return {
488
+ content: converted.content.map(part =>
489
+ part.type === 'text'
490
+ ? part
491
+ : {
492
+ type: 'image' as const,
493
+ data: part.data,
494
+ mimeType: part.mediaType,
495
+ },
496
+ ),
497
+ isError: isError === true || converted.isError,
498
+ };
499
+ }
438
500
  return {
439
501
  content: [{ type: 'text', text: JSON.stringify(output ?? null) }],
440
502
  isError,
@@ -485,16 +547,22 @@ async function runTurn(start: StartMessage, turn: BridgeTurn): Promise<void> {
485
547
  inactiveNativeTools,
486
548
  turn,
487
549
  emit,
488
- finishApprovalStep: approvalId => {
489
- finishApprovalStep({ state: streamEventState, emit, approvalId });
490
- },
550
+ markWaitingOnHostInput: toolUseId =>
551
+ markWaitingOnHostInput({ state: streamEventState, emit, toolUseId }),
552
+ clearWaitingOnHostInput: toolUseId =>
553
+ clearWaitingOnHostInput({ state: streamEventState, toolUseId }),
491
554
  nativeToolCallNames: streamEventState.nativeToolCallNames,
492
555
  approvalRequestedToolUseIds: streamEventState.approvalRequestedToolUseIds,
556
+ emittedToolCallIds: streamEventState.emittedToolCallIds,
493
557
  });
494
558
  const questionPreToolUseHook = createQuestionPreToolUseHook({
495
559
  turn,
496
560
  emit,
497
561
  nativeToolCallNames: streamEventState.nativeToolCallNames,
562
+ markWaitingOnHostInput: toolUseId =>
563
+ markWaitingOnHostInput({ state: streamEventState, emit, toolUseId }),
564
+ clearWaitingOnHostInput: toolUseId =>
565
+ clearWaitingOnHostInput({ state: streamEventState, toolUseId }),
498
566
  });
499
567
 
500
568
  const q = claudeSdk.query({