@ai-sdk/harness-acp 1.0.37 → 1.0.39

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.
@@ -2,6 +2,7 @@ import type {
2
2
  ActiveSessionMessage,
3
3
  ToolCallUpdate,
4
4
  } from '@agentclientprotocol/sdk';
5
+ import type { ACPToolCall } from '../../acp-tool-call';
5
6
 
6
7
  type SessionUpdateMessage = Extract<
7
8
  ActiveSessionMessage,
@@ -55,6 +56,8 @@ export function createHostToolCorrelation({
55
56
  order: number;
56
57
  }): void;
57
58
  claimHostToolPermission(options: { toolCall: ToolCallUpdate }): boolean;
59
+ suppressToolCall(options: { toolCallId: string }): void;
60
+ getToolCall(options: { toolCallId: string }): ACPToolCall | undefined;
58
61
  flush(): void;
59
62
  removeInvocation(options: { token: string }): void;
60
63
  close(): void;
@@ -63,6 +66,7 @@ export function createHostToolCorrelation({
63
66
  const candidates = new Map<string, CorrelationCandidate>();
64
67
  const suppressedToolCallIds = new Set<string>();
65
68
  const releasedToolCallIds = new Set<string>();
69
+ const observedToolCalls = new Map<string, ACPToolCall>();
66
70
  let nextCandidateOrder = 0;
67
71
  let buffered: BufferedUpdate[] = [];
68
72
  let flushTimer: ReturnType<typeof setTimeout> | undefined;
@@ -155,6 +159,13 @@ export function createHostToolCorrelation({
155
159
  }
156
160
 
157
161
  const toolCallId = message.update.toolCallId;
162
+ observedToolCalls.set(
163
+ toolCallId,
164
+ mergeObservedToolCall({
165
+ previous: observedToolCalls.get(toolCallId),
166
+ update: message.update,
167
+ }),
168
+ );
158
169
  if (suppressedToolCallIds.has(toolCallId)) {
159
170
  if (isTerminalToolUpdate(message)) {
160
171
  suppressedToolCallIds.delete(toolCallId);
@@ -242,6 +253,8 @@ export function createHostToolCorrelation({
242
253
  suppressToolCallUpdate({ toolCallId: toolCall.toolCallId });
243
254
  return true;
244
255
  },
256
+ suppressToolCall: suppressToolCallUpdate,
257
+ getToolCall: ({ toolCallId }) => observedToolCalls.get(toolCallId),
245
258
  flush,
246
259
  removeInvocation: ({ token }) => {
247
260
  deleteInvocation({ invocations, token });
@@ -257,10 +270,31 @@ export function createHostToolCorrelation({
257
270
  candidates.clear();
258
271
  suppressedToolCallIds.clear();
259
272
  releasedToolCallIds.clear();
273
+ observedToolCalls.clear();
260
274
  },
261
275
  };
262
276
  }
263
277
 
278
+ function mergeObservedToolCall({
279
+ previous,
280
+ update,
281
+ }: {
282
+ previous: ACPToolCall | undefined;
283
+ update: ToolCallUpdate;
284
+ }): ACPToolCall {
285
+ return {
286
+ toolCallId: update.toolCallId,
287
+ title: update.title ?? previous?.title ?? `Tool ${update.toolCallId}`,
288
+ ...(update.kind == null ? {} : { kind: update.kind }),
289
+ ...(update.status == null ? {} : { status: update.status }),
290
+ ...(update.content == null ? {} : { content: update.content }),
291
+ ...(update.locations == null ? {} : { locations: update.locations }),
292
+ ...(update.rawInput === undefined ? {} : { rawInput: update.rawInput }),
293
+ ...(update.rawOutput === undefined ? {} : { rawOutput: update.rawOutput }),
294
+ ...(update._meta === undefined ? {} : { _meta: update._meta }),
295
+ };
296
+ }
297
+
264
298
  function hasPortableEvidence({
265
299
  candidate,
266
300
  invocation,
@@ -104,6 +104,13 @@ let coldRestorationMethod: ACPSessionRestorationMethod | undefined;
104
104
  let activePermissionController:
105
105
  | ReturnType<typeof createACPPermissionController>
106
106
  | undefined;
107
+ let activeQuestionRequest:
108
+ | {
109
+ method: string;
110
+ turn: BridgeTurn;
111
+ emitStreamEvent: ReturnType<typeof createEmitStreamEvent>;
112
+ }
113
+ | undefined;
107
114
 
108
115
  await runBridge<StartMessage>({
109
116
  bridgeType,
@@ -252,15 +259,36 @@ async function runTurn(start: StartMessage, turn: BridgeTurn): Promise<void> {
252
259
  const emitStreamEvent = createEmitStreamEvent({
253
260
  emit: event => turn.emit(event as BridgeEvent),
254
261
  emitToolCallCandidate: ({ toolCall }) => {
262
+ const requestId = crypto.randomUUID();
255
263
  turn.emit({
256
264
  type: 'acp-tool-call-candidate',
265
+ requestId,
257
266
  toolCall,
258
267
  });
268
+ void turn.requestToolResult(requestId).then(result => {
269
+ if (
270
+ result.output != null &&
271
+ typeof result.output === 'object' &&
272
+ 'suppress' in result.output &&
273
+ result.output.suppress === true
274
+ ) {
275
+ emitStreamEvent.suppressToolCall({
276
+ toolCallId: toolCall.toolCallId,
277
+ });
278
+ }
279
+ });
259
280
  },
260
281
  builtinTools: start.builtinTools,
261
282
  hostToolServerName: HOST_TOOL_MCP_SERVER_NAME,
262
283
  hostTools: start.tools ?? [],
263
284
  });
285
+ if (bridgeConfiguration.askUserQuestionsRequestMethod != null) {
286
+ activeQuestionRequest = {
287
+ method: bridgeConfiguration.askUserQuestionsRequestMethod,
288
+ turn,
289
+ emitStreamEvent,
290
+ };
291
+ }
264
292
  const permissionController = createACPPermissionController({
265
293
  turn,
266
294
  sessionId: activeSession.sessionId,
@@ -353,6 +381,9 @@ async function runTurn(start: StartMessage, turn: BridgeTurn): Promise<void> {
353
381
  if (emitStreamEvent.message({ message })) return;
354
382
  }
355
383
  } finally {
384
+ if (activeQuestionRequest?.turn === turn) {
385
+ activeQuestionRequest = undefined;
386
+ }
356
387
  permissionController.cancelAll();
357
388
  activePermissionController = undefined;
358
389
  activeHostToolRelay.unbindTurn({ turn: relayTurn });
@@ -458,7 +489,7 @@ async function ensureSession({
458
489
  stream: acp.ndJsonStream(input, output),
459
490
  });
460
491
  streamCapture = capturedStream.capture;
461
- connection = acp
492
+ let client = acp
462
493
  .client({ name: clientApp.name })
463
494
  .onRequest(
464
495
  acp.methods.client.session.requestPermission,
@@ -475,8 +506,58 @@ async function ensureSession({
475
506
  notification: params,
476
507
  update: params.update,
477
508
  });
478
- })
479
- .connect(capturedStream.stream);
509
+ });
510
+ if (bridgeConfiguration.askUserQuestionsRequestMethod != null) {
511
+ const method = bridgeConfiguration.askUserQuestionsRequestMethod;
512
+ client = client.onRequest<unknown, unknown>(
513
+ method,
514
+ value => value,
515
+ async ({ params }) => {
516
+ const active = activeQuestionRequest;
517
+ if (active == null || active.method !== method) {
518
+ throw acp.RequestError.methodNotFound(method);
519
+ }
520
+ const requestId = crypto.randomUUID();
521
+ const nativeToolCallId = getNativeQuestionToolCallId({ params });
522
+ active.turn.emit({
523
+ type: 'acp-question-request',
524
+ requestId,
525
+ nativeRequest: params,
526
+ ...(nativeToolCallId == null
527
+ ? {}
528
+ : {
529
+ nativeToolCall:
530
+ active.emitStreamEvent.getToolCall({
531
+ toolCallId: nativeToolCallId,
532
+ }) ?? undefined,
533
+ }),
534
+ });
535
+ const result = await active.turn.requestToolResult(requestId);
536
+ const classification = result.output as
537
+ | { type: 'unhandled' }
538
+ | { type: 'handled'; toolCallId: string }
539
+ | undefined;
540
+ if (classification?.type !== 'handled') {
541
+ throw acp.RequestError.methodNotFound(method);
542
+ }
543
+ active.emitStreamEvent.suppressToolCall({
544
+ toolCallId: nativeToolCallId ?? classification.toolCallId,
545
+ });
546
+ try {
547
+ const nativeResult = await active.turn.requestToolResult(
548
+ classification.toolCallId,
549
+ );
550
+ return nativeResult.output;
551
+ } finally {
552
+ active.turn.emit({
553
+ type: 'acp-question-resolved',
554
+ requestId,
555
+ });
556
+ }
557
+ },
558
+ );
559
+ }
560
+ connection = client.connect(capturedStream.stream);
480
561
 
481
562
  const initializeRequest = createACPInitializeRequest({
482
563
  protocolVersion: acp.PROTOCOL_VERSION,
@@ -627,6 +708,19 @@ async function ensureSession({
627
708
  return { initialHostToolCatalogRefreshRequired: tools.length > 0 };
628
709
  }
629
710
 
711
+ function getNativeQuestionToolCallId({
712
+ params,
713
+ }: {
714
+ params: unknown;
715
+ }): string | undefined {
716
+ if (!isRecord(params)) return undefined;
717
+ if (typeof params.toolCallId === 'string') return params.toolCallId;
718
+ const nestedParams = params.params;
719
+ return isRecord(nestedParams) && typeof nestedParams.toolCallId === 'string'
720
+ ? nestedParams.toolCallId
721
+ : undefined;
722
+ }
723
+
630
724
  function createExternalMcpServers({
631
725
  mcpServers,
632
726
  initialization,
@@ -68,6 +68,7 @@ export function createACPStreamTranslator({
68
68
  output: unknown;
69
69
  isError?: boolean;
70
70
  }) => void;
71
+ getToolCall: (options: { toolCallId: string }) => ACPToolCall | undefined;
71
72
  } {
72
73
  let openBlock:
73
74
  | {
@@ -215,6 +216,7 @@ export function createACPStreamTranslator({
215
216
  builtinTools,
216
217
  builtinToolsByName,
217
218
  });
219
+ emitToolCallCandidate?.({ toolCall: createACPToolCall({ state }) });
218
220
  if (
219
221
  !forceEmit &&
220
222
  builtin?.inputSchema != null &&
@@ -238,7 +240,6 @@ export function createACPStreamTranslator({
238
240
  programmaticName,
239
241
  toolCallId: state.toolCallId,
240
242
  });
241
- emitToolCallCandidate?.({ toolCall: createACPToolCall({ state }) });
242
243
  }
243
244
  emit({
244
245
  type: 'tool-call',
@@ -381,6 +382,10 @@ export function createACPStreamTranslator({
381
382
  close: closeBlock,
382
383
  hostToolCall,
383
384
  hostToolResult,
385
+ getToolCall: ({ toolCallId }) => {
386
+ const state = toolStates.get(toolCallId);
387
+ return state == null ? undefined : createACPToolCall({ state });
388
+ },
384
389
  finish: response => {
385
390
  if (finished) return;
386
391
  finished = true;
package/src/v1/index.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export { createACPV1 } from './acp-v1-harness';
2
2
  export type {
3
+ ACPAskUserQuestionsSettings,
3
4
  ACPCredentialBrokering,
4
5
  ACPAuthentication,
5
6
  ACPAuthenticationMode,