@ai-sdk/harness-acp 1.0.38 → 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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-sdk/harness-acp",
3
- "version": "1.0.38",
3
+ "version": "1.0.39",
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
  },
@@ -1,9 +1,17 @@
1
- import type { HarnessV1, HarnessV1PortEndpoint } from '@ai-sdk/harness';
1
+ import {
2
+ HARNESS_V1_BUILTIN_TOOLS,
3
+ type HarnessV1,
4
+ type HarnessV1PortEndpoint,
5
+ } from '@ai-sdk/harness';
2
6
  import type { ToolSet } from '@ai-sdk/provider-utils';
3
7
  import { z } from 'zod/v4';
4
8
  import type { ACPClientApp } from './acp-auth';
5
9
  import type { ACPToolCall } from './acp-tool-call';
6
- import { createACPV1, type ACPV1Settings } from './v1';
10
+ import {
11
+ createACPV1,
12
+ type ACPAskUserQuestionsSettings,
13
+ type ACPV1Settings,
14
+ } from './v1';
7
15
  import {
8
16
  acpColdSessionStateSchema,
9
17
  acpTurnStartConfigSchema,
@@ -15,7 +23,12 @@ const ACP_CLIENT_APP = {
15
23
  version: VERSION,
16
24
  } as const satisfies ACPClientApp;
17
25
 
18
- export type ACPHarnessSettings<TBuiltinTools extends ToolSet = {}> = {
26
+ export type ACPHarnessSettings<
27
+ TBuiltinTools extends ToolSet = {},
28
+ TAskUserQuestions extends ACPAskUserQuestionsSettings | undefined =
29
+ | ACPAskUserQuestionsSettings
30
+ | undefined,
31
+ > = {
19
32
  readonly builtinTools?: TBuiltinTools;
20
33
  /**
21
34
  * MCP server definitions keyed by server name. Each definition uses the
@@ -61,6 +74,7 @@ export type ACPHarnessSettings<TBuiltinTools extends ToolSet = {}> = {
61
74
  readonly skillsDirectory?: ACPV1Settings['skillsDirectory'];
62
75
  readonly instructionMapping?: ACPV1Settings['instructionMapping'];
63
76
  readonly outputSchemaMapping?: ACPV1Settings['outputSchemaMapping'];
77
+ readonly askUserQuestions?: TAskUserQuestions;
64
78
  readonly permissionModeMapping?: ACPV1Settings['permissionModeMapping'];
65
79
  readonly session?: ACPV1Settings['session'];
66
80
  readonly mintBridgeToken?: ACPV1Settings['mintBridgeToken'];
@@ -117,9 +131,22 @@ const acpResumeStateSchema = z.object({
117
131
 
118
132
  type ACPBridgeCoords = z.infer<typeof acpBridgeCoordsSchema>;
119
133
 
120
- export function createACP<TBuiltinTools extends ToolSet = {}>(
121
- settings: ACPHarnessSettings<TBuiltinTools>,
122
- ): HarnessV1<TBuiltinTools> {
134
+ type ACPHarnessTools<
135
+ TBuiltinTools extends ToolSet,
136
+ TAskUserQuestions,
137
+ > = TBuiltinTools &
138
+ ([TAskUserQuestions] extends [undefined]
139
+ ? {}
140
+ : {
141
+ askUserQuestions: typeof HARNESS_V1_BUILTIN_TOOLS.askUserQuestions;
142
+ });
143
+
144
+ export function createACP<
145
+ TBuiltinTools extends ToolSet = {},
146
+ TAskUserQuestions extends ACPAskUserQuestionsSettings | undefined = undefined,
147
+ >(
148
+ settings: ACPHarnessSettings<TBuiltinTools, TAskUserQuestions>,
149
+ ): HarnessV1<ACPHarnessTools<TBuiltinTools, TAskUserQuestions>> {
123
150
  if (
124
151
  (settings.credentialEnv == null) !==
125
152
  (settings.credentialBrokering == null)
@@ -143,10 +170,17 @@ export function createACP<TBuiltinTools extends ToolSet = {}>(
143
170
  switch (version) {
144
171
  case 'v1': {
145
172
  const clientApp = settings.clientApp ?? ACP_CLIENT_APP;
173
+ const builtinTools =
174
+ settings.askUserQuestions == null
175
+ ? (settings.builtinTools ?? (ACP_BUILTIN_TOOLS as TBuiltinTools))
176
+ : {
177
+ ...(settings.builtinTools ??
178
+ (ACP_BUILTIN_TOOLS as TBuiltinTools)),
179
+ askUserQuestions: HARNESS_V1_BUILTIN_TOOLS.askUserQuestions,
180
+ };
146
181
  return createACPV1({
147
182
  settings,
148
- builtinTools:
149
- settings.builtinTools ?? (ACP_BUILTIN_TOOLS as TBuiltinTools),
183
+ builtinTools,
150
184
  port: settings.port,
151
185
  portEndpoint: settings.portEndpoint,
152
186
  startupTimeoutMs: settings.startupTimeoutMs,
@@ -154,7 +188,7 @@ export function createACP<TBuiltinTools extends ToolSet = {}>(
154
188
  lifecycleStateSchema: acpResumeStateSchema satisfies z.ZodType<{
155
189
  bridge?: ACPBridgeCoords;
156
190
  }>,
157
- });
191
+ }) as HarnessV1<ACPHarnessTools<TBuiltinTools, TAskUserQuestions>>;
158
192
  }
159
193
  default:
160
194
  throw new Error(
package/src/index.ts CHANGED
@@ -20,6 +20,7 @@ export type {
20
20
  ACPToolKind,
21
21
  } from './acp-tool-call';
22
22
  export type {
23
+ ACPAskUserQuestionsSettings,
23
24
  ACPCredentialBrokering,
24
25
  ACPAuthentication,
25
26
  ACPInstallCommandSource,
@@ -199,6 +199,7 @@ const coldRestoreSchema = z.object({
199
199
 
200
200
  const acpToolCallCandidateSchema = z.object({
201
201
  type: z.literal('acp-tool-call-candidate'),
202
+ requestId: z.string(),
202
203
  toolCall: z.custom<ACPToolCall>(
203
204
  value =>
204
205
  value != null &&
@@ -208,9 +209,23 @@ const acpToolCallCandidateSchema = z.object({
208
209
  ),
209
210
  });
210
211
 
212
+ const acpQuestionRequestSchema = z.object({
213
+ type: z.literal('acp-question-request'),
214
+ requestId: z.string(),
215
+ nativeRequest: z.unknown(),
216
+ nativeToolCall: acpToolCallCandidateSchema.shape.toolCall.optional(),
217
+ });
218
+
219
+ const acpQuestionResolvedSchema = z.object({
220
+ type: z.literal('acp-question-resolved'),
221
+ requestId: z.string(),
222
+ });
223
+
211
224
  export const outboundMessageSchema = z.union([
212
225
  harnessV1BridgeOutboundMessageSchema,
213
226
  acpToolCallCandidateSchema,
227
+ acpQuestionRequestSchema,
228
+ acpQuestionResolvedSchema,
214
229
  ]);
215
230
  export type OutboundMessage = z.infer<typeof outboundMessageSchema>;
216
231
 
@@ -37,6 +37,7 @@ import {
37
37
  asSchema,
38
38
  type Experimental_SandboxProcess,
39
39
  type Experimental_SandboxSession as SandboxSession,
40
+ type ToolResultPart,
40
41
  type ToolSet,
41
42
  } from '@ai-sdk/provider-utils';
42
43
  import { WebSocket } from 'ws';
@@ -81,6 +82,7 @@ import {
81
82
  prependACPInstructionGuidance,
82
83
  } from './acp-v1-prompt';
83
84
  import type {
85
+ ACPAskUserQuestionsSettings,
84
86
  ACPInstructionMapping,
85
87
  ACPModelMapping,
86
88
  ACPOutputSchemaMapping,
@@ -442,6 +444,7 @@ export function createACPV1<TBuiltinTools extends ToolSet = {}>({
442
444
  sessionMeta: settings.session?.meta,
443
445
  instructionMapping: settings.instructionMapping,
444
446
  outputSchemaMapping: settings.outputSchemaMapping,
447
+ askUserQuestions: settings.askUserQuestions,
445
448
  debug: startOptions.observability?.debug,
446
449
  implementationIdentity,
447
450
  authenticationProfile,
@@ -615,6 +618,8 @@ export function createACPV1<TBuiltinTools extends ToolSet = {}>({
615
618
  sandboxProviderEnvironment == null ? undefined : {},
616
619
  sessionMeta: settings.session?.meta,
617
620
  clientCapabilities: settings.clientCapabilities,
621
+ askUserQuestionsRequestMethod:
622
+ settings.askUserQuestions?.requestMethod,
618
623
  }),
619
624
  ...sandboxProviderAuthenticationEnvironment,
620
625
  BRIDGE_CHANNEL_TOKEN: token,
@@ -732,6 +737,7 @@ export function createACPV1<TBuiltinTools extends ToolSet = {}>({
732
737
  sessionMeta: settings.session?.meta,
733
738
  instructionMapping: settings.instructionMapping,
734
739
  outputSchemaMapping: settings.outputSchemaMapping,
740
+ askUserQuestions: settings.askUserQuestions,
735
741
  debug: startOptions.observability?.debug,
736
742
  implementationIdentity,
737
743
  authenticationProfile,
@@ -1045,6 +1051,7 @@ function createSession({
1045
1051
  sessionMeta,
1046
1052
  instructionMapping,
1047
1053
  outputSchemaMapping,
1054
+ askUserQuestions,
1048
1055
  debug,
1049
1056
  implementationIdentity,
1050
1057
  authenticationProfile,
@@ -1081,6 +1088,7 @@ function createSession({
1081
1088
  sessionMeta: Readonly<Record<string, ACPSerializableValue>> | undefined;
1082
1089
  instructionMapping: ACPInstructionMapping | undefined;
1083
1090
  outputSchemaMapping: ACPOutputSchemaMapping | undefined;
1091
+ askUserQuestions: ACPAskUserQuestionsSettings | undefined;
1084
1092
  debug: HarnessV1DebugConfig | undefined;
1085
1093
  implementationIdentity: string;
1086
1094
  authenticationProfile: ACPAuthenticationProfileIdentity;
@@ -1114,6 +1122,14 @@ function createSession({
1114
1122
  let instructionsFingerprint = instructionsFingerprintAtStart;
1115
1123
  let latestACPSessionId = acpSessionIdAtStart;
1116
1124
  let latestTurnStartConfig = turnStartConfigAtStart;
1125
+ const bufferedQuestionResults = new Map<
1126
+ string,
1127
+ {
1128
+ readonly output: unknown;
1129
+ readonly isError?: boolean;
1130
+ readonly toolResult: ToolResultPart;
1131
+ }
1132
+ >();
1117
1133
 
1118
1134
  const markTurnFinished = () => {
1119
1135
  turnInFlight = false;
@@ -1154,6 +1170,14 @@ function createSession({
1154
1170
  | undefined;
1155
1171
  const dynamicToolCalls = new Map<string, boolean>();
1156
1172
  const toolCallClassificationErrors = new Map<string, unknown>();
1173
+ const activeQuestionRequests = new Map<
1174
+ string,
1175
+ {
1176
+ readonly requestId: string;
1177
+ readonly nativeRequest: unknown;
1178
+ }
1179
+ >();
1180
+ const questionToolCallIdsByRequestId = new Map<string, string>();
1157
1181
  const subscriptions: Array<() => void> = [];
1158
1182
  const forward = (event: HarnessV1StreamPart) => {
1159
1183
  if (event.type === 'text-start' || event.type === 'reasoning-start') {
@@ -1220,12 +1244,120 @@ function createSession({
1220
1244
  subscriptions.push(
1221
1245
  channel.on('acp-tool-call-candidate', event => {
1222
1246
  try {
1247
+ const suppress =
1248
+ askUserQuestions?.isNativeToolCall?.({
1249
+ nativeToolCall: event.toolCall,
1250
+ }) === true;
1223
1251
  dynamicToolCalls.set(
1224
1252
  event.toolCall.toolCallId,
1225
1253
  isMcpToolCall?.(event.toolCall) === true,
1226
1254
  );
1255
+ channel.send({
1256
+ type: 'tool-result',
1257
+ toolCallId: event.requestId,
1258
+ output: { suppress },
1259
+ });
1227
1260
  } catch (error) {
1228
1261
  toolCallClassificationErrors.set(event.toolCall.toolCallId, error);
1262
+ channel.send({
1263
+ type: 'tool-result',
1264
+ toolCallId: event.requestId,
1265
+ output: { suppress: false },
1266
+ });
1267
+ }
1268
+ }),
1269
+ );
1270
+ subscriptions.push(
1271
+ channel.on('acp-question-request', event => {
1272
+ if (askUserQuestions == null) {
1273
+ channel.send({
1274
+ type: 'tool-result',
1275
+ toolCallId: event.requestId,
1276
+ output: { type: 'unhandled' },
1277
+ });
1278
+ return;
1279
+ }
1280
+ try {
1281
+ const nativeToolCall = askUserQuestions.fromNativeRequest({
1282
+ nativeRequest: event.nativeRequest,
1283
+ nativeToolCall: event.nativeToolCall,
1284
+ });
1285
+ if (nativeToolCall == null) {
1286
+ channel.send({
1287
+ type: 'tool-result',
1288
+ toolCallId: event.requestId,
1289
+ output: { type: 'unhandled' },
1290
+ });
1291
+ return;
1292
+ }
1293
+ if (
1294
+ nativeToolCall.toolName !== 'askUserQuestions' ||
1295
+ nativeToolCall.providerExecuted !== false
1296
+ ) {
1297
+ throw new Error(
1298
+ `${harnessId} ACP askUserQuestions.fromNativeRequest must return a client-executed askUserQuestions tool call.`,
1299
+ );
1300
+ }
1301
+
1302
+ const toolCall = withNativeQuestionRequest({
1303
+ harnessId,
1304
+ nativeRequest: event.nativeRequest,
1305
+ toolCall: nativeToolCall,
1306
+ });
1307
+ const bufferedResult = takeBufferedQuestionResult({
1308
+ bufferedQuestionResults,
1309
+ toolCallId: toolCall.toolCallId,
1310
+ nativeRequest: event.nativeRequest,
1311
+ matchesNativeRequest: askUserQuestions.matchesNativeRequest,
1312
+ harnessId,
1313
+ });
1314
+ activeQuestionRequests.set(toolCall.toolCallId, {
1315
+ requestId: event.requestId,
1316
+ nativeRequest: event.nativeRequest,
1317
+ });
1318
+ questionToolCallIdsByRequestId.set(
1319
+ event.requestId,
1320
+ toolCall.toolCallId,
1321
+ );
1322
+ channel.send({
1323
+ type: 'tool-result',
1324
+ toolCallId: event.requestId,
1325
+ output: {
1326
+ type: 'handled',
1327
+ toolCallId: toolCall.toolCallId,
1328
+ },
1329
+ });
1330
+
1331
+ if (bufferedResult == null) {
1332
+ forward(toolCall);
1333
+ return;
1334
+ }
1335
+ channel.send({
1336
+ type: 'tool-result',
1337
+ toolCallId: toolCall.toolCallId,
1338
+ output: askUserQuestions.toNativeResponse({
1339
+ nativeRequest: event.nativeRequest,
1340
+ toolResult: bufferedResult.toolResult,
1341
+ }),
1342
+ isError: bufferedResult.isError,
1343
+ toolResult: bufferedResult.toolResult,
1344
+ });
1345
+ } catch (error) {
1346
+ closeForwardedBlock();
1347
+ forward({ type: 'error', error });
1348
+ try {
1349
+ channel.send({ type: 'abort' });
1350
+ } catch {}
1351
+ settle({ error });
1352
+ }
1353
+ }),
1354
+ );
1355
+ subscriptions.push(
1356
+ channel.on('acp-question-resolved', event => {
1357
+ const toolCallId = questionToolCallIdsByRequestId.get(event.requestId);
1358
+ if (toolCallId != null) {
1359
+ activeQuestionRequests.delete(toolCallId);
1360
+ questionToolCallIdsByRequestId.delete(event.requestId);
1229
1361
  }
1230
1362
  }),
1231
1363
  );
@@ -1315,11 +1447,52 @@ function createSession({
1315
1447
  });
1316
1448
  },
1317
1449
  submitToolResult: async input => {
1450
+ if (
1451
+ askUserQuestions != null &&
1452
+ input.toolResult?.toolName === 'askUserQuestions'
1453
+ ) {
1454
+ const activeRequest = activeQuestionRequests.get(input.toolCallId);
1455
+ if (activeRequest == null) {
1456
+ const previousNativeRequest =
1457
+ input.toolResult.providerOptions?.[harnessId]?.nativeRequest;
1458
+ if (!lossyRerun && previousNativeRequest !== undefined) {
1459
+ channel.send({
1460
+ type: 'tool-result',
1461
+ toolCallId: input.toolCallId,
1462
+ output: askUserQuestions.toNativeResponse({
1463
+ nativeRequest: previousNativeRequest,
1464
+ toolResult: input.toolResult,
1465
+ }),
1466
+ isError: input.isError,
1467
+ toolResult: input.toolResult,
1468
+ });
1469
+ return;
1470
+ }
1471
+ bufferedQuestionResults.set(input.toolCallId, {
1472
+ output: input.output,
1473
+ ...(input.isError == null ? {} : { isError: input.isError }),
1474
+ toolResult: input.toolResult,
1475
+ });
1476
+ return;
1477
+ }
1478
+ channel.send({
1479
+ type: 'tool-result',
1480
+ toolCallId: input.toolCallId,
1481
+ output: askUserQuestions.toNativeResponse({
1482
+ nativeRequest: activeRequest.nativeRequest,
1483
+ toolResult: input.toolResult,
1484
+ }),
1485
+ isError: input.isError,
1486
+ toolResult: input.toolResult,
1487
+ });
1488
+ return;
1489
+ }
1318
1490
  channel.send({
1319
1491
  type: 'tool-result',
1320
1492
  toolCallId: input.toolCallId,
1321
1493
  output: input.output,
1322
1494
  isError: input.isError,
1495
+ toolResult: input.toolResult,
1323
1496
  });
1324
1497
  },
1325
1498
  done,
@@ -1682,6 +1855,78 @@ function isCompletePermissionModeMapping({
1682
1855
  );
1683
1856
  }
1684
1857
 
1858
+ function withNativeQuestionRequest({
1859
+ harnessId,
1860
+ nativeRequest,
1861
+ toolCall,
1862
+ }: {
1863
+ harnessId: string;
1864
+ nativeRequest: unknown;
1865
+ toolCall: Extract<HarnessV1StreamPart, { type: 'tool-call' }>;
1866
+ }): Extract<HarnessV1StreamPart, { type: 'tool-call' }> {
1867
+ const harnessMetadata = toolCall.providerMetadata?.[harnessId];
1868
+ return {
1869
+ ...toolCall,
1870
+ providerMetadata: {
1871
+ ...toolCall.providerMetadata,
1872
+ [harnessId]: {
1873
+ ...(harnessMetadata ?? {}),
1874
+ nativeRequest,
1875
+ } as NonNullable<
1876
+ Extract<HarnessV1StreamPart, { type: 'tool-call' }>['providerMetadata']
1877
+ >[string],
1878
+ },
1879
+ };
1880
+ }
1881
+
1882
+ function takeBufferedQuestionResult({
1883
+ bufferedQuestionResults,
1884
+ toolCallId,
1885
+ nativeRequest,
1886
+ matchesNativeRequest,
1887
+ harnessId,
1888
+ }: {
1889
+ bufferedQuestionResults: Map<
1890
+ string,
1891
+ {
1892
+ readonly output: unknown;
1893
+ readonly isError?: boolean;
1894
+ readonly toolResult: ToolResultPart;
1895
+ }
1896
+ >;
1897
+ toolCallId: string;
1898
+ nativeRequest: unknown;
1899
+ matchesNativeRequest:
1900
+ | ACPAskUserQuestionsSettings['matchesNativeRequest']
1901
+ | undefined;
1902
+ harnessId: string;
1903
+ }):
1904
+ | {
1905
+ readonly output: unknown;
1906
+ readonly isError?: boolean;
1907
+ readonly toolResult: ToolResultPart;
1908
+ }
1909
+ | undefined {
1910
+ const exact = bufferedQuestionResults.get(toolCallId);
1911
+ if (exact != null) {
1912
+ bufferedQuestionResults.delete(toolCallId);
1913
+ return exact;
1914
+ }
1915
+ if (matchesNativeRequest == null) return undefined;
1916
+ for (const [bufferedToolCallId, buffered] of bufferedQuestionResults) {
1917
+ const previousNativeRequest =
1918
+ buffered.toolResult.providerOptions?.[harnessId]?.nativeRequest;
1919
+ if (
1920
+ previousNativeRequest !== undefined &&
1921
+ matchesNativeRequest({ previousNativeRequest, nativeRequest })
1922
+ ) {
1923
+ bufferedQuestionResults.delete(bufferedToolCallId);
1924
+ return buffered;
1925
+ }
1926
+ }
1927
+ return undefined;
1928
+ }
1929
+
1685
1930
  function isPermissionModeMappingValue({
1686
1931
  value,
1687
1932
  }: {
@@ -3,7 +3,9 @@ import type {
3
3
  HarnessV1CredentialForwarding,
4
4
  HarnessV1PermissionMode,
5
5
  HarnessV1RequestTransformation,
6
+ HarnessV1StreamPart,
6
7
  } from '@ai-sdk/harness';
8
+ import type { ToolResultPart } from '@ai-sdk/provider-utils';
7
9
  import type { ACPToolCall } from '../acp-tool-call';
8
10
 
9
11
  export type ACPSerializablePrimitive = string | number | boolean | null;
@@ -129,6 +131,25 @@ export type ACPOutputSchemaMapping = {
129
131
  readonly path: ReadonlyArray<string>;
130
132
  };
131
133
 
134
+ export type ACPAskUserQuestionsSettings = {
135
+ readonly requestMethod: string;
136
+ readonly isNativeToolCall?: (options: {
137
+ nativeToolCall: ACPToolCall;
138
+ }) => boolean;
139
+ readonly fromNativeRequest: (options: {
140
+ nativeRequest: unknown;
141
+ nativeToolCall?: ACPToolCall;
142
+ }) => Extract<HarnessV1StreamPart, { type: 'tool-call' }> | null;
143
+ readonly toNativeResponse: (options: {
144
+ nativeRequest: unknown;
145
+ toolResult: ToolResultPart;
146
+ }) => unknown;
147
+ readonly matchesNativeRequest?: (options: {
148
+ previousNativeRequest: unknown;
149
+ nativeRequest: unknown;
150
+ }) => boolean;
151
+ };
152
+
132
153
  export type ACPV1Settings = {
133
154
  readonly version?: 'v1';
134
155
  readonly harnessId: string;
@@ -179,6 +200,7 @@ export type ACPV1Settings = {
179
200
  * below the ACP session prompt's `_meta` field.
180
201
  */
181
202
  readonly outputSchemaMapping?: ACPOutputSchemaMapping;
203
+ readonly askUserQuestions?: ACPAskUserQuestionsSettings;
182
204
  readonly permissionModeMapping?: ACPPermissionModeMapping;
183
205
  readonly session?: {
184
206
  readonly meta?: Readonly<Record<string, ACPSerializableValue>>;
@@ -60,6 +60,7 @@ export type ACPBridgeConfiguration = {
60
60
  readonly providerEnvironment?: Readonly<Record<string, string>>;
61
61
  readonly sessionMeta?: Readonly<Record<string, ACPSerializableValue>>;
62
62
  readonly clientCapabilities?: Readonly<Record<string, ACPSerializableValue>>;
63
+ readonly askUserQuestionsRequestMethod?: string;
63
64
  };
64
65
 
65
66
  const bridgeConfigurationSchema: z.ZodType<ACPBridgeConfiguration> = z.object({
@@ -68,6 +69,7 @@ const bridgeConfigurationSchema: z.ZodType<ACPBridgeConfiguration> = z.object({
68
69
  providerEnvironment: z.record(z.string(), z.string()).optional(),
69
70
  sessionMeta: serializableRecordSchema.optional(),
70
71
  clientCapabilities: serializableRecordSchema.optional(),
72
+ askUserQuestionsRequestMethod: z.string().optional(),
71
73
  });
72
74
 
73
75
  export function createACPBridgeEnvironment({
@@ -76,6 +78,7 @@ export function createACPBridgeEnvironment({
76
78
  providerEnvironment,
77
79
  sessionMeta,
78
80
  clientCapabilities,
81
+ askUserQuestionsRequestMethod,
79
82
  }: ACPBridgeConfiguration): Record<string, string> {
80
83
  return {
81
84
  [ACP_BRIDGE_CONFIGURATION_ENV]: JSON.stringify({
@@ -84,6 +87,9 @@ export function createACPBridgeEnvironment({
84
87
  ...(providerEnvironment == null ? {} : { providerEnvironment }),
85
88
  ...(sessionMeta == null ? {} : { sessionMeta }),
86
89
  ...(clientCapabilities == null ? {} : { clientCapabilities }),
90
+ ...(askUserQuestionsRequestMethod == null
91
+ ? {}
92
+ : { askUserQuestionsRequestMethod }),
87
93
  }),
88
94
  };
89
95
  }
@@ -51,6 +51,8 @@ export function createEmitStreamEvent({
51
51
  order: number;
52
52
  }) => void;
53
53
  removeHostToolCorrelationInvocation: (options: { token: string }) => void;
54
+ suppressToolCall: (options: { toolCallId: string }) => void;
55
+ getToolCall: (options: { toolCallId: string }) => ACPToolCall | undefined;
54
56
  } {
55
57
  const translator = createACPStreamTranslator({
56
58
  emit,
@@ -100,5 +102,9 @@ export function createEmitStreamEvent({
100
102
  hostToolResult: translator.hostToolResult,
101
103
  registerHostToolCorrelationInvocation: correlation.registerInvocation,
102
104
  removeHostToolCorrelationInvocation: correlation.removeInvocation,
105
+ suppressToolCall: correlation.suppressToolCall,
106
+ getToolCall: ({ toolCallId }) =>
107
+ correlation.getToolCall({ toolCallId }) ??
108
+ translator.getToolCall({ toolCallId }),
103
109
  };
104
110
  }
@@ -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,