@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/CHANGELOG.md +11 -0
- package/dist/bridge/host-tool-mcp.mjs +1 -1
- package/dist/bridge/index.mjs +93 -11
- package/dist/bridge/index.mjs.map +1 -1
- package/dist/index.d.ts +15 -1
- package/dist/index.js +119 -23
- package/dist/index.js.map +1 -1
- package/package.json +6 -6
- package/src/acp-harness.ts +6 -0
- package/src/v1/acp-bootstrap.ts +12 -0
- package/src/v1/acp-v1-bridge-protocol.ts +14 -0
- package/src/v1/acp-v1-harness.ts +148 -31
- package/src/v1/acp-v1-settings.ts +9 -0
- package/src/v1/bridge/index.ts +44 -18
- package/src/v1/bridge/refresh-host-tool-catalog.ts +28 -0
- package/src/v1/bridge/stream-translator.ts +65 -0
- package/src/v1/implementation.ts +13 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ai-sdk/harness-acp",
|
|
3
|
-
"version": "1.0.
|
|
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
|
-
"
|
|
30
|
-
"@ai-sdk/
|
|
31
|
-
"
|
|
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"
|
package/src/acp-harness.ts
CHANGED
|
@@ -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'];
|
package/src/v1/acp-bootstrap.ts
CHANGED
|
@@ -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
|
|
package/src/v1/acp-v1-harness.ts
CHANGED
|
@@ -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
|
-
*
|
|
281
|
-
* the
|
|
282
|
-
*
|
|
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 =
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
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
|
-
...
|
|
581
|
+
...forwardedImplementationEnvironment,
|
|
553
582
|
...createACPBridgeEnvironment({
|
|
554
583
|
authentication: settings.authentication,
|
|
555
584
|
providerAuthentication:
|
|
556
585
|
resolvedProviderAuthentication.providerAuthentication,
|
|
557
|
-
providerEnvironment:
|
|
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
|
-
|
|
744
|
-
|
|
745
|
-
|
|
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.
|
package/src/v1/bridge/index.ts
CHANGED
|
@@ -38,7 +38,10 @@ import {
|
|
|
38
38
|
type HostToolRelay,
|
|
39
39
|
type HostToolRelayTurn,
|
|
40
40
|
} from './host-tool-relay';
|
|
41
|
-
import {
|
|
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({
|
|
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
|
-
|
|
274
|
+
activeHostToolRelay.bindTurn({ turn: relayTurn });
|
|
264
275
|
try {
|
|
265
276
|
const promptMeta = createOutputSchemaPromptMeta({ start });
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
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
|
-
|
|
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<
|
|
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,
|