@ai-sdk/harness-acp 1.0.24 → 1.0.25

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.24",
3
+ "version": "1.0.25",
4
4
  "type": "module",
5
5
  "license": "Apache-2.0",
6
6
  "sideEffects": false,
@@ -26,9 +26,9 @@
26
26
  }
27
27
  },
28
28
  "dependencies": {
29
- "ws": "^8.21.0",
30
- "@ai-sdk/harness": "1.0.86",
31
- "@ai-sdk/provider-utils": "5.0.29"
29
+ "@ai-sdk/harness": "1.0.87",
30
+ "@ai-sdk/provider-utils": "5.0.30",
31
+ "ws": "^8.21.0"
32
32
  },
33
33
  "peerDependencies": {
34
34
  "zod": "^3.25.76 || ^4.1.8"
@@ -38,10 +38,10 @@
38
38
  "@modelcontextprotocol/sdk": "1.29.0",
39
39
  "@types/node": "22.19.19",
40
40
  "@types/ws": "^8.5.13",
41
+ "@vercel/ai-tsconfig": "0.0.0",
41
42
  "tsup": "^8.5.1",
42
43
  "typescript": "5.8.3",
43
- "zod": "3.25.76",
44
- "@vercel/ai-tsconfig": "0.0.0"
44
+ "zod": "3.25.76"
45
45
  },
46
46
  "engines": {
47
47
  "node": ">=22"
@@ -40,6 +40,12 @@ export type ACPHarnessSettings<TBuiltinTools extends ToolSet = {}> = {
40
40
  readonly forwardEnv?: ACPV1Settings['forwardEnv'];
41
41
  readonly credentialEnv?: ACPV1Settings['credentialEnv'];
42
42
  readonly credentialBrokering?: ACPV1Settings['credentialBrokering'];
43
+ /**
44
+ * Customizes each credential value before it is forwarded into a sandbox
45
+ * process. This does not restrict which credentials the harness adapter can
46
+ * discover, read, or otherwise access in the host process.
47
+ */
48
+ readonly credentialForwarding?: ACPV1Settings['credentialForwarding'];
43
49
  readonly env?: ACPV1Settings['env'];
44
50
  readonly authentication?: ACPV1Settings['authentication'];
45
51
  readonly providerAuthentication?: ACPV1Settings['providerAuthentication'];
@@ -7,6 +7,7 @@ import {
7
7
  createImplementationManifest,
8
8
  getImplementationInstallScript,
9
9
  getImplementationLockfile,
10
+ getImplementationWorkspaceFile,
10
11
  type ACPImplementation,
11
12
  } from './implementation';
12
13
 
@@ -40,6 +41,9 @@ export function createACPBootstrap({
40
41
  const implementationLock = getImplementationLockfile({
41
42
  implementation,
42
43
  });
44
+ const implementationWorkspace = getImplementationWorkspaceFile({
45
+ implementation,
46
+ });
43
47
  const implementationInstallScript = getImplementationInstallScript({
44
48
  implementation,
45
49
  });
@@ -80,6 +84,14 @@ export function createACPBootstrap({
80
84
  content: implementationLock,
81
85
  },
82
86
  ]),
87
+ ...(implementationWorkspace == null
88
+ ? []
89
+ : [
90
+ {
91
+ path: `${bootstrapDir}/implementation/pnpm-workspace.yaml`,
92
+ content: implementationWorkspace,
93
+ },
94
+ ]),
83
95
  ...(implementationInstallScript == null
84
96
  ? []
85
97
  : [
@@ -32,6 +32,18 @@ export type ACPBuiltinToolMapping = {
32
32
  */
33
33
  readonly nativeName?: HarnessV1BuiltinTool['nativeName'];
34
34
 
35
+ /**
36
+ * Stable prefix of the display title emitted by an ACP implementation.
37
+ * Used only when the implementation omits a programmatic tool name.
38
+ */
39
+ readonly title?: HarnessV1BuiltinTool['title'];
40
+
41
+ /**
42
+ * Broad capability category used to compare a built-in with the ACP tool
43
+ * kind when resolving unnamed calls.
44
+ */
45
+ readonly toolUseKind?: HarnessV1BuiltinTool['toolUseKind'];
46
+
35
47
  /**
36
48
  * JSON Schema used to identify tool calls when an ACP implementation does
37
49
  * not provide a programmatic tool name. It is optional because built-in
@@ -43,6 +55,8 @@ export type ACPBuiltinToolMapping = {
43
55
  const builtinToolSchema: z.ZodType<ACPBuiltinToolMapping> = z.object({
44
56
  toolName: z.string(),
45
57
  nativeName: z.string().optional(),
58
+ title: z.string().optional(),
59
+ toolUseKind: z.enum(['readonly', 'edit', 'bash']).optional(),
46
60
  inputSchema: z.json().optional(),
47
61
  });
48
62
 
@@ -14,6 +14,7 @@ import {
14
14
  } from '@ai-sdk/harness';
15
15
  import { HarnessBridgeCapabilityUnsupportedError } from '@ai-sdk/harness/bridge';
16
16
  import {
17
+ applyCredentialForwarding,
17
18
  createBridgeErrorHandler,
18
19
  createBridgeStartupError,
19
20
  classifyDiskLog,
@@ -82,6 +83,7 @@ import type {
82
83
  ACPOutputSchemaMapping,
83
84
  ACPPermissionModeMapping,
84
85
  ACPPermissionModeTarget,
86
+ ACPProfileValue,
85
87
  ACPSerializableValue,
86
88
  ACPV1Settings,
87
89
  } from './acp-v1-settings';
@@ -243,6 +245,22 @@ export function createACPV1<TBuiltinTools extends ToolSet = {}>({
243
245
  let sandboxProviderAuthenticationEnvironment =
244
246
  resolvedProviderAuthentication.env;
245
247
  let sandboxProviderEnvironment: Record<string, string> | undefined;
248
+ const credentialEnvironmentVariables = [
249
+ ...new Set([
250
+ ...(settings.credentialEnv ?? []),
251
+ 'AI_GATEWAY_API_KEY',
252
+ 'VERCEL_OIDC_TOKEN',
253
+ ]),
254
+ ];
255
+ const credentialForwardingEnvironmentVariables = [
256
+ ...new Set([
257
+ ...credentialEnvironmentVariables,
258
+ ...resolveACPProviderCredentialEnvironmentVariables({
259
+ providerAuthentication:
260
+ resolvedProviderAuthentication.providerAuthentication,
261
+ }),
262
+ ]),
263
+ ];
246
264
 
247
265
  if (
248
266
  settings.credentialBrokering != null &&
@@ -253,13 +271,6 @@ export function createACPV1<TBuiltinTools extends ToolSet = {}>({
253
271
  resolvedProviderAuthentication,
254
272
  clientApp,
255
273
  });
256
- const credentialEnvironmentVariables = [
257
- ...new Set([
258
- ...(settings.credentialEnv ?? []),
259
- 'AI_GATEWAY_API_KEY',
260
- 'VERCEL_OIDC_TOKEN',
261
- ]),
262
- ];
263
274
  const requestTransformations = settings.credentialBrokering({
264
275
  env: {
265
276
  ...implementationEnvironment,
@@ -277,20 +288,33 @@ export function createACPV1<TBuiltinTools extends ToolSet = {}>({
277
288
  });
278
289
 
279
290
  /*
280
- * Gateway profiles are resolved on the host twice: real values feed
281
- * the transformation callback, while the bridge receives only a
282
- * structurally equivalent environment with credential placeholders.
283
- * Resolving the profile inside the sandbox would require serializing
284
- * the Gateway credential into the bridge process environment.
291
+ * Each profile environment variable is resolved with its own name as
292
+ * the fake Gateway credential. This preserves profile transformations
293
+ * without exposing the real credential to the sandbox process.
285
294
  */
286
- sandboxProviderEnvironment = maskSandboxCredentials({
287
- environment: resolveProviderEnvironment({
288
- resolvedProviderAuthentication,
289
- clientApp,
290
- gatewayApiKey: 'AI_GATEWAY_API_KEY',
291
- }),
292
- credentialEnvironmentVariables,
295
+ sandboxProviderEnvironment = resolveBrokeredProviderEnvironment({
296
+ resolvedProviderAuthentication,
297
+ clientApp,
298
+ });
299
+ } else if (settings.credentialBrokering != null) {
300
+ warnCredentialBrokeringUnavailable();
301
+ }
302
+ if (
303
+ settings.credentialForwarding != null &&
304
+ sandboxProviderEnvironment == null &&
305
+ resolvedProviderAuthentication.providerAuthentication?.type ===
306
+ 'ai-gateway'
307
+ ) {
308
+ sandboxProviderEnvironment = resolveProviderEnvironment({
309
+ resolvedProviderAuthentication,
310
+ clientApp,
293
311
  });
312
+ }
313
+ if (sandboxProviderEnvironment != null) {
314
+ sandboxImplementationEnvironment = {
315
+ ...sandboxImplementationEnvironment,
316
+ ...sandboxProviderEnvironment,
317
+ };
294
318
  sandboxProviderAuthenticationEnvironment = Object.fromEntries(
295
319
  Object.entries(resolvedProviderAuthentication.env).filter(
296
320
  ([key]) =>
@@ -298,8 +322,6 @@ export function createACPV1<TBuiltinTools extends ToolSet = {}>({
298
322
  key !== 'AI_SDK_ACP_GATEWAY_BASE_URL',
299
323
  ),
300
324
  );
301
- } else if (settings.credentialBrokering != null) {
302
- warnCredentialBrokeringUnavailable();
303
325
  }
304
326
  const resolvedBridgeDir = posix.resolve(
305
327
  defaultWorkingDirectory,
@@ -521,6 +543,13 @@ export function createACPV1<TBuiltinTools extends ToolSet = {}>({
521
543
  }
522
544
  }
523
545
 
546
+ const forwardedImplementationEnvironment =
547
+ await applyCredentialForwarding({
548
+ environment: sandboxImplementationEnvironment,
549
+ credentialEnvironmentVariables:
550
+ credentialForwardingEnvironmentVariables,
551
+ credentialForwarding: settings.credentialForwarding,
552
+ });
524
553
  const port = resolveBridgePort({
525
554
  sandboxSession,
526
555
  override: portOverride,
@@ -549,12 +578,13 @@ export function createACPV1<TBuiltinTools extends ToolSet = {}>({
549
578
  ` --implementation-dir ${shellQuote(resolvedImplementationDir)}` +
550
579
  ` --bridge-type ${shellQuote(settings.harnessId)}`,
551
580
  env: {
552
- ...sandboxImplementationEnvironment,
581
+ ...forwardedImplementationEnvironment,
553
582
  ...createACPBridgeEnvironment({
554
583
  authentication: settings.authentication,
555
584
  providerAuthentication:
556
585
  resolvedProviderAuthentication.providerAuthentication,
557
- providerEnvironment: sandboxProviderEnvironment,
586
+ providerEnvironment:
587
+ sandboxProviderEnvironment == null ? undefined : {},
558
588
  sessionMeta: settings.session?.meta,
559
589
  }),
560
590
  ...sandboxProviderAuthenticationEnvironment,
@@ -722,13 +752,11 @@ export function createACPV1<TBuiltinTools extends ToolSet = {}>({
722
752
  function resolveProviderEnvironment({
723
753
  resolvedProviderAuthentication,
724
754
  clientApp,
725
- gatewayApiKey,
726
755
  }: {
727
756
  resolvedProviderAuthentication: ReturnType<
728
757
  typeof resolveACPProviderAuthentication
729
758
  >;
730
759
  clientApp: ACPClientApp;
731
- gatewayApiKey?: string;
732
760
  }): Record<string, string> {
733
761
  if (
734
762
  resolvedProviderAuthentication.providerAuthentication?.type !== 'ai-gateway'
@@ -739,12 +767,10 @@ function resolveProviderEnvironment({
739
767
  providerAuthentication:
740
768
  resolvedProviderAuthentication.providerAuthentication,
741
769
  gateway: {
742
- apiKey:
743
- gatewayApiKey ??
744
- requireResolvedEnvironmentValue({
745
- environment: resolvedProviderAuthentication.env,
746
- name: 'AI_SDK_ACP_GATEWAY_API_KEY',
747
- }),
770
+ apiKey: requireResolvedEnvironmentValue({
771
+ environment: resolvedProviderAuthentication.env,
772
+ name: 'AI_SDK_ACP_GATEWAY_API_KEY',
773
+ }),
748
774
  baseUrl: requireResolvedEnvironmentValue({
749
775
  environment: resolvedProviderAuthentication.env,
750
776
  name: 'AI_SDK_ACP_GATEWAY_BASE_URL',
@@ -755,6 +781,79 @@ function resolveProviderEnvironment({
755
781
  });
756
782
  }
757
783
 
784
+ function resolveBrokeredProviderEnvironment({
785
+ resolvedProviderAuthentication,
786
+ clientApp,
787
+ }: {
788
+ resolvedProviderAuthentication: ReturnType<
789
+ typeof resolveACPProviderAuthentication
790
+ >;
791
+ clientApp: ACPClientApp;
792
+ }): Record<string, string> {
793
+ const providerAuthentication =
794
+ resolvedProviderAuthentication.providerAuthentication;
795
+ if (providerAuthentication?.type !== 'ai-gateway') return {};
796
+
797
+ const baseUrl = requireResolvedEnvironmentValue({
798
+ environment: resolvedProviderAuthentication.env,
799
+ name: 'AI_SDK_ACP_GATEWAY_BASE_URL',
800
+ });
801
+
802
+ return Object.fromEntries(
803
+ Object.entries(providerAuthentication.env).map(
804
+ ([environmentVariableName, value]) => {
805
+ const environment = resolveACPLaunchEnvironment({
806
+ providerAuthentication: {
807
+ type: 'ai-gateway',
808
+ env: { [environmentVariableName]: value },
809
+ },
810
+ gateway: {
811
+ apiKey: environmentVariableName,
812
+ baseUrl,
813
+ clientAppName: clientApp.name,
814
+ clientAppVersion: clientApp.version,
815
+ },
816
+ });
817
+ return [environmentVariableName, environment[environmentVariableName]!];
818
+ },
819
+ ),
820
+ );
821
+ }
822
+
823
+ function resolveACPProviderCredentialEnvironmentVariables({
824
+ providerAuthentication,
825
+ }: {
826
+ providerAuthentication: ReturnType<
827
+ typeof resolveACPProviderAuthentication
828
+ >['providerAuthentication'];
829
+ }): string[] {
830
+ if (providerAuthentication?.type !== 'ai-gateway') return [];
831
+
832
+ return Object.entries(providerAuthentication.env)
833
+ .filter(([, value]) => containsACPProviderCredential({ value }))
834
+ .map(([name]) => name);
835
+ }
836
+
837
+ function containsACPProviderCredential({
838
+ value,
839
+ }: {
840
+ value: ACPProfileValue;
841
+ }): boolean {
842
+ if (Array.isArray(value)) {
843
+ return value.some(item => containsACPProviderCredential({ value: item }));
844
+ }
845
+ if (value == null || typeof value !== 'object') return false;
846
+ if ('$source' in value) {
847
+ return (
848
+ value.$source === 'gateway-api-key' ||
849
+ value.$source === 'gateway-authorization'
850
+ );
851
+ }
852
+ return Object.values(value).some(item =>
853
+ containsACPProviderCredential({ value: item }),
854
+ );
855
+ }
856
+
758
857
  function requireResolvedEnvironmentValue({
759
858
  environment,
760
859
  name,
@@ -1583,6 +1682,22 @@ export function serializeBuiltinTools({
1583
1682
  typeof tool.nativeName === 'string'
1584
1683
  ? tool.nativeName
1585
1684
  : undefined;
1685
+ const title =
1686
+ tool != null &&
1687
+ typeof tool === 'object' &&
1688
+ 'title' in tool &&
1689
+ typeof tool.title === 'string'
1690
+ ? tool.title
1691
+ : undefined;
1692
+ const toolUseKind =
1693
+ tool != null &&
1694
+ typeof tool === 'object' &&
1695
+ 'toolUseKind' in tool &&
1696
+ (tool.toolUseKind === 'readonly' ||
1697
+ tool.toolUseKind === 'edit' ||
1698
+ tool.toolUseKind === 'bash')
1699
+ ? tool.toolUseKind
1700
+ : undefined;
1586
1701
  let inputSchema: ACPBuiltinToolMapping['inputSchema'];
1587
1702
  if (
1588
1703
  tool != null &&
@@ -1599,6 +1714,8 @@ export function serializeBuiltinTools({
1599
1714
  return {
1600
1715
  toolName,
1601
1716
  ...(nativeName == null ? {} : { nativeName }),
1717
+ ...(title == null ? {} : { title }),
1718
+ ...(toolUseKind == null ? {} : { toolUseKind }),
1602
1719
  ...(inputSchema == null ? {} : { inputSchema }),
1603
1720
  };
1604
1721
  });
@@ -1,4 +1,5 @@
1
1
  import type {
2
+ HarnessV1CredentialForwarding,
2
3
  HarnessV1PermissionMode,
3
4
  HarnessV1RequestTransformation,
4
5
  } from '@ai-sdk/harness';
@@ -45,6 +46,8 @@ export type ACPNpmLockedSource = {
45
46
  readonly type: 'npm-locked';
46
47
  readonly packageJson: string;
47
48
  readonly pnpmLockYaml: string;
49
+ /** Optional pnpm workspace configuration required by the locked install. */
50
+ readonly pnpmWorkspaceYaml?: string;
48
51
  };
49
52
 
50
53
  export type ACPInstallCommandSource = {
@@ -125,6 +128,12 @@ export type ACPV1Settings = {
125
128
  readonly forwardEnv?: ReadonlyArray<string>;
126
129
  readonly credentialEnv?: ReadonlyArray<string>;
127
130
  readonly credentialBrokering?: ACPCredentialBrokering;
131
+ /**
132
+ * Customizes each credential value before it is forwarded into a sandbox
133
+ * process. This does not restrict which credentials the harness adapter can
134
+ * discover, read, or otherwise access in the host process.
135
+ */
136
+ readonly credentialForwarding?: HarnessV1CredentialForwarding;
128
137
  /**
129
138
  * Runtime environment values that are safe to persist in bootstrap and
130
139
  * lifecycle compatibility identity.
@@ -38,7 +38,10 @@ import {
38
38
  type HostToolRelay,
39
39
  type HostToolRelayTurn,
40
40
  } from './host-tool-relay';
41
- import { refreshHostToolCatalog } from './refresh-host-tool-catalog';
41
+ import {
42
+ promptAndRefreshInitialHostToolCatalog,
43
+ refreshHostToolCatalog,
44
+ } from './refresh-host-tool-catalog';
42
45
  import { createACPPermissionController } from './permission-controller';
43
46
  import { configureACPPermissionMode } from './permission-mode';
44
47
  import {
@@ -130,8 +133,12 @@ await runBridge<StartMessage>({
130
133
  });
131
134
 
132
135
  async function runTurn(start: StartMessage, turn: BridgeTurn): Promise<void> {
136
+ let initialHostToolCatalogRefreshRequired: boolean;
133
137
  try {
134
- await ensureSession({ start, turn });
138
+ ({ initialHostToolCatalogRefreshRequired } = await ensureSession({
139
+ start,
140
+ turn,
141
+ }));
135
142
  } catch (error) {
136
143
  if (HarnessBridgeCapabilityUnsupportedError.isInstance(error)) throw error;
137
144
  throw createACPBridgeError({
@@ -149,6 +156,10 @@ async function runTurn(start: StartMessage, turn: BridgeTurn): Promise<void> {
149
156
  'ACP session initialization did not start stderr monitoring.',
150
157
  );
151
158
  }
159
+ const activeHostToolRelay = hostToolRelay;
160
+ if (activeHostToolRelay == null) {
161
+ throw new Error('The host tool MCP relay is unavailable.');
162
+ }
152
163
  if (start.recoveryMode?.type === 'lossy-rerun') {
153
164
  const marker = {
154
165
  type: 'acp-recovery',
@@ -260,15 +271,35 @@ async function runTurn(start: StartMessage, turn: BridgeTurn): Promise<void> {
260
271
  removeCorrelationInvocation:
261
272
  emitStreamEvent.removeHostToolCorrelationInvocation,
262
273
  };
263
- hostToolRelay?.bindTurn({ turn: relayTurn });
274
+ activeHostToolRelay.bindTurn({ turn: relayTurn });
264
275
  try {
265
276
  const promptMeta = createOutputSchemaPromptMeta({ start });
266
- void promptActiveSession({
267
- session: activeSession,
268
- agent: connection!.agent,
269
- prompt: start.prompt,
270
- meta: promptMeta,
271
- });
277
+ const startPrompt = () =>
278
+ promptActiveSession({
279
+ session: activeSession,
280
+ agent: connection!.agent,
281
+ prompt: start.prompt,
282
+ meta: promptMeta,
283
+ });
284
+ if (initialHostToolCatalogRefreshRequired) {
285
+ try {
286
+ await promptAndRefreshInitialHostToolCatalog({
287
+ startPrompt,
288
+ relay: activeHostToolRelay,
289
+ tools: start.tools ?? [],
290
+ harnessId: bridgeType,
291
+ timeoutMs: CATALOG_REFRESH_TIMEOUT_MS,
292
+ });
293
+ } catch (error) {
294
+ if (HarnessBridgeCapabilityUnsupportedError.isInstance(error)) {
295
+ catalogRefreshError = error;
296
+ }
297
+ sessionConfigurationFailure = { error };
298
+ throw error;
299
+ }
300
+ } else {
301
+ void startPrompt();
302
+ }
272
303
  if (turn.abortSignal.aborted) {
273
304
  await cancel();
274
305
  } else {
@@ -316,7 +347,7 @@ async function runTurn(start: StartMessage, turn: BridgeTurn): Promise<void> {
316
347
  } finally {
317
348
  permissionController.cancelAll();
318
349
  activePermissionController = undefined;
319
- hostToolRelay?.unbindTurn({ turn: relayTurn });
350
+ activeHostToolRelay.unbindTurn({ turn: relayTurn });
320
351
  }
321
352
  }
322
353
 
@@ -326,7 +357,7 @@ async function ensureSession({
326
357
  }: {
327
358
  start: StartMessage;
328
359
  turn: BridgeTurn;
329
- }): Promise<void> {
360
+ }): Promise<{ initialHostToolCatalogRefreshRequired: boolean }> {
330
361
  if (sessionConfigurationFailure != null) {
331
362
  throw sessionConfigurationFailure.error;
332
363
  }
@@ -364,7 +395,7 @@ async function ensureSession({
364
395
  }
365
396
  throw error;
366
397
  }
367
- return;
398
+ return { initialHostToolCatalogRefreshRequired: false };
368
399
  }
369
400
 
370
401
  const clientApp = resolveClientApp();
@@ -558,12 +589,6 @@ async function ensureSession({
558
589
  .start();
559
590
  }
560
591
  try {
561
- await refreshHostToolCatalog({
562
- relay: hostToolRelay,
563
- tools,
564
- harnessId: bridgeType,
565
- timeoutMs: CATALOG_REFRESH_TIMEOUT_MS,
566
- });
567
592
  if (start.permissionModeMapping != null) {
568
593
  await configureACPPermissionMode({
569
594
  agent: connection.agent,
@@ -588,6 +613,7 @@ async function ensureSession({
588
613
  sessionId: createdSession.sessionId,
589
614
  });
590
615
  sessionConfigurationFingerprint = fingerprint;
616
+ return { initialHostToolCatalogRefreshRequired: tools.length > 0 };
591
617
  }
592
618
 
593
619
  function createExternalMcpServers({
@@ -2,6 +2,34 @@ import type { HarnessV1BridgeToolWire } from '@ai-sdk/harness';
2
2
  import { HarnessBridgeCapabilityUnsupportedError } from '@ai-sdk/harness/bridge';
3
3
  import type { HostToolRelay } from './host-tool-relay';
4
4
 
5
+ /*
6
+ * Some ACP agents start session MCP servers only after receiving the first
7
+ * prompt. Starting that prompt before waiting lets those agents discover the
8
+ * initial catalog, while the relay's revision checks still reject stale or
9
+ * unknown tool invocations.
10
+ */
11
+ export async function promptAndRefreshInitialHostToolCatalog({
12
+ startPrompt,
13
+ relay,
14
+ tools,
15
+ harnessId,
16
+ timeoutMs,
17
+ }: {
18
+ startPrompt: () => Promise<unknown>;
19
+ relay: Pick<HostToolRelay, 'updateCatalog' | 'waitForCatalogRefresh'>;
20
+ tools: ReadonlyArray<HarnessV1BridgeToolWire>;
21
+ harnessId: string;
22
+ timeoutMs: number;
23
+ }): Promise<void> {
24
+ void startPrompt();
25
+ await refreshHostToolCatalog({
26
+ relay,
27
+ tools,
28
+ harnessId,
29
+ timeoutMs,
30
+ });
31
+ }
32
+
5
33
  export async function refreshHostToolCatalog({
6
34
  relay,
7
35
  tools,
@@ -206,6 +206,11 @@ export function createACPStreamTranslator({
206
206
  const builtin = resolveBuiltinTool({
207
207
  programmaticName,
208
208
  metadata: state.values._meta,
209
+ title: getStringProperty({
210
+ value: state.values,
211
+ property: 'title',
212
+ }),
213
+ kind: isACPToolKind(state.values.kind) ? state.values.kind : undefined,
209
214
  rawInput: state.values.rawInput,
210
215
  builtinTools,
211
216
  builtinToolsByName,
@@ -478,12 +483,16 @@ function addBuiltinToolMatch({
478
483
  function resolveBuiltinTool({
479
484
  programmaticName,
480
485
  metadata,
486
+ title,
487
+ kind,
481
488
  rawInput,
482
489
  builtinTools,
483
490
  builtinToolsByName,
484
491
  }: {
485
492
  programmaticName: string | undefined;
486
493
  metadata: unknown;
494
+ title: string | undefined;
495
+ kind: ACPToolKind | undefined;
487
496
  rawInput: unknown;
488
497
  builtinTools: ReadonlyArray<ACPBuiltinToolMapping>;
489
498
  builtinToolsByName: ReadonlyMap<string, ACPBuiltinToolMapping>;
@@ -502,12 +511,68 @@ function resolveBuiltinTool({
502
511
  : undefined;
503
512
  }
504
513
 
514
+ const titleMatch = findBuiltinToolByTitle({
515
+ title,
516
+ kind,
517
+ builtinTools,
518
+ });
519
+ if (titleMatch != null) return titleMatch;
520
+
505
521
  const schemaMatches = builtinTools.filter(tool =>
506
522
  matchesBuiltinToolInput({ rawInput, inputSchema: tool.inputSchema }),
507
523
  );
508
524
  return schemaMatches.length === 1 ? schemaMatches[0] : undefined;
509
525
  }
510
526
 
527
+ function findBuiltinToolByTitle({
528
+ title,
529
+ kind,
530
+ builtinTools,
531
+ }: {
532
+ title: string | undefined;
533
+ kind: ACPToolKind | undefined;
534
+ builtinTools: ReadonlyArray<ACPBuiltinToolMapping>;
535
+ }): ACPBuiltinToolMapping | undefined {
536
+ if (title == null) return undefined;
537
+ const matches = builtinTools.filter(
538
+ tool =>
539
+ tool.title != null &&
540
+ title.startsWith(tool.title) &&
541
+ isCompatibleToolKind({ toolUseKind: tool.toolUseKind, kind }),
542
+ );
543
+ if (matches.length === 0) return undefined;
544
+ const longestPrefixLength = Math.max(
545
+ ...matches.map(tool => tool.title!.length),
546
+ );
547
+ const longestMatches = matches.filter(
548
+ tool => tool.title!.length === longestPrefixLength,
549
+ );
550
+ return longestMatches.length === 1 ? longestMatches[0] : undefined;
551
+ }
552
+
553
+ function isCompatibleToolKind({
554
+ toolUseKind,
555
+ kind,
556
+ }: {
557
+ toolUseKind: ACPBuiltinToolMapping['toolUseKind'];
558
+ kind: ACPToolKind | undefined;
559
+ }): boolean {
560
+ if (toolUseKind == null || kind == null) return true;
561
+ switch (toolUseKind) {
562
+ case 'readonly':
563
+ return (
564
+ kind === 'read' ||
565
+ kind === 'search' ||
566
+ kind === 'fetch' ||
567
+ kind === 'think'
568
+ );
569
+ case 'edit':
570
+ return kind === 'edit' || kind === 'delete' || kind === 'move';
571
+ case 'bash':
572
+ return kind === 'execute';
573
+ }
574
+ }
575
+
511
576
  function findBuiltinToolsInMetadata({
512
577
  metadata,
513
578
  builtinToolsByName,