@ai-sdk/harness-acp 1.0.39 → 1.0.41
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 +25 -0
- package/README.md +4 -3
- package/dist/bridge/host-tool-mcp.mjs +1 -1
- package/dist/bridge/index.mjs +408 -32
- package/dist/bridge/index.mjs.map +1 -1
- package/dist/index.d.ts +27 -2
- package/dist/index.js +68 -69
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/acp-harness.ts +8 -0
- package/src/index.ts +1 -0
- package/src/v1/acp-v1-bridge-protocol.ts +4 -0
- package/src/v1/acp-v1-harness.ts +62 -19
- package/src/v1/acp-v1-settings.ts +20 -0
- package/src/v1/acp-v1-skills.ts +4 -65
- package/src/v1/bridge/acp-v1-bridge-environment.ts +10 -0
- package/src/v1/bridge/host-tool-mcp-definition.ts +69 -0
- package/src/v1/bridge/host-tool-mcp-http.ts +182 -0
- package/src/v1/bridge/host-tool-relay.ts +87 -7
- package/src/v1/bridge/index.ts +14 -21
- package/src/v1/bridge/instruction-mapping.ts +12 -0
- package/src/v1/bridge/stream-translator.ts +35 -1
- package/src/v1/index.ts +1 -0
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
3
|
+
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
4
|
+
import { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js';
|
|
5
|
+
import type { HarnessV1BridgeToolWire } from '@ai-sdk/harness';
|
|
6
|
+
import {
|
|
7
|
+
createHostToolMCPServer,
|
|
8
|
+
type HostToolMCPInvocationResult,
|
|
9
|
+
type HostToolMCPServer,
|
|
10
|
+
} from './host-tool-mcp-server';
|
|
11
|
+
|
|
12
|
+
export const HOST_TOOL_MCP_ENDPOINT_PATH = '/mcp';
|
|
13
|
+
|
|
14
|
+
export type HostToolMCPHttpEndpoint = {
|
|
15
|
+
handleRequest(options: {
|
|
16
|
+
request: IncomingMessage;
|
|
17
|
+
response: ServerResponse;
|
|
18
|
+
body?: unknown;
|
|
19
|
+
}): Promise<void>;
|
|
20
|
+
updateCatalog(options: {
|
|
21
|
+
revision: number;
|
|
22
|
+
tools: ReadonlyArray<HarnessV1BridgeToolWire>;
|
|
23
|
+
}): Promise<void>;
|
|
24
|
+
close(): Promise<void>;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
type EndpointSession = {
|
|
28
|
+
readonly transport: StreamableHTTPServerTransport;
|
|
29
|
+
readonly hostToolServer: HostToolMCPServer;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/*
|
|
33
|
+
* The ACP host tool catalog is shared by every MCP session opened against this
|
|
34
|
+
* endpoint. Each session needs its own MCP `Server` instance because a server
|
|
35
|
+
* can be connected to a single transport at a time, so catalog updates are
|
|
36
|
+
* fanned out to all live sessions and also retained for sessions opened later.
|
|
37
|
+
*/
|
|
38
|
+
export function createHostToolMCPHttpEndpoint({
|
|
39
|
+
tools,
|
|
40
|
+
revision,
|
|
41
|
+
invoke,
|
|
42
|
+
onListTools,
|
|
43
|
+
}: {
|
|
44
|
+
tools: ReadonlyArray<HarnessV1BridgeToolWire>;
|
|
45
|
+
revision: number;
|
|
46
|
+
invoke: (options: {
|
|
47
|
+
toolName: string;
|
|
48
|
+
input: Readonly<Record<string, unknown>>;
|
|
49
|
+
catalogRevision: number;
|
|
50
|
+
}) => Promise<HostToolMCPInvocationResult>;
|
|
51
|
+
onListTools: (options: { revision: number }) => Promise<void>;
|
|
52
|
+
}): HostToolMCPHttpEndpoint {
|
|
53
|
+
let catalog: {
|
|
54
|
+
revision: number;
|
|
55
|
+
tools: ReadonlyArray<HarnessV1BridgeToolWire>;
|
|
56
|
+
} = { revision, tools: [...tools] };
|
|
57
|
+
const sessions = new Map<string, EndpointSession>();
|
|
58
|
+
let closed = false;
|
|
59
|
+
|
|
60
|
+
async function openSession({
|
|
61
|
+
request,
|
|
62
|
+
response,
|
|
63
|
+
body,
|
|
64
|
+
}: {
|
|
65
|
+
request: IncomingMessage;
|
|
66
|
+
response: ServerResponse;
|
|
67
|
+
body: unknown;
|
|
68
|
+
}): Promise<void> {
|
|
69
|
+
const hostToolServer = createHostToolMCPServer({
|
|
70
|
+
tools: catalog.tools,
|
|
71
|
+
revision: catalog.revision,
|
|
72
|
+
invoke,
|
|
73
|
+
onListTools,
|
|
74
|
+
});
|
|
75
|
+
const transport = new StreamableHTTPServerTransport({
|
|
76
|
+
sessionIdGenerator: () => randomUUID(),
|
|
77
|
+
onsessioninitialized: sessionId => {
|
|
78
|
+
sessions.set(sessionId, { transport, hostToolServer });
|
|
79
|
+
},
|
|
80
|
+
onsessionclosed: sessionId => {
|
|
81
|
+
sessions.delete(sessionId);
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
transport.onclose = () => {
|
|
85
|
+
const { sessionId } = transport;
|
|
86
|
+
if (sessionId != null) sessions.delete(sessionId);
|
|
87
|
+
};
|
|
88
|
+
await hostToolServer.server.connect(transport);
|
|
89
|
+
await transport.handleRequest(request, response, body);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
handleRequest: async ({ request, response, body }) => {
|
|
94
|
+
if (closed) {
|
|
95
|
+
respondWithJSONRPCError({
|
|
96
|
+
response,
|
|
97
|
+
status: 503,
|
|
98
|
+
code: -32000,
|
|
99
|
+
message: 'The host tool MCP endpoint is closed.',
|
|
100
|
+
});
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
const sessionId = readSessionId({ request });
|
|
104
|
+
if (sessionId != null) {
|
|
105
|
+
const session = sessions.get(sessionId);
|
|
106
|
+
if (session == null) {
|
|
107
|
+
respondWithJSONRPCError({
|
|
108
|
+
response,
|
|
109
|
+
status: 404,
|
|
110
|
+
code: -32001,
|
|
111
|
+
message: 'Unknown host tool MCP session.',
|
|
112
|
+
});
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
await session.transport.handleRequest(request, response, body);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
if (request.method !== 'POST' || !isInitializeRequest(body)) {
|
|
119
|
+
respondWithJSONRPCError({
|
|
120
|
+
response,
|
|
121
|
+
status: 400,
|
|
122
|
+
code: -32000,
|
|
123
|
+
message:
|
|
124
|
+
'Host tool MCP requests without a session id must be an initialize request.',
|
|
125
|
+
});
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
await openSession({ request, response, body });
|
|
129
|
+
},
|
|
130
|
+
updateCatalog: async ({ revision: nextRevision, tools: nextTools }) => {
|
|
131
|
+
catalog = { revision: nextRevision, tools: [...nextTools] };
|
|
132
|
+
await Promise.all(
|
|
133
|
+
[...sessions.values()].map(session =>
|
|
134
|
+
session.hostToolServer.updateCatalog({
|
|
135
|
+
revision: nextRevision,
|
|
136
|
+
tools: nextTools,
|
|
137
|
+
}),
|
|
138
|
+
),
|
|
139
|
+
);
|
|
140
|
+
},
|
|
141
|
+
close: async () => {
|
|
142
|
+
if (closed) return;
|
|
143
|
+
closed = true;
|
|
144
|
+
const live = [...sessions.values()];
|
|
145
|
+
sessions.clear();
|
|
146
|
+
await Promise.all(
|
|
147
|
+
live.map(session => session.hostToolServer.server.close()),
|
|
148
|
+
);
|
|
149
|
+
},
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function readSessionId({
|
|
154
|
+
request,
|
|
155
|
+
}: {
|
|
156
|
+
request: IncomingMessage;
|
|
157
|
+
}): string | undefined {
|
|
158
|
+
const value = request.headers['mcp-session-id'];
|
|
159
|
+
const sessionId = Array.isArray(value) ? value[0] : value;
|
|
160
|
+
return sessionId == null || sessionId.length === 0 ? undefined : sessionId;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function respondWithJSONRPCError({
|
|
164
|
+
response,
|
|
165
|
+
status,
|
|
166
|
+
code,
|
|
167
|
+
message,
|
|
168
|
+
}: {
|
|
169
|
+
response: ServerResponse;
|
|
170
|
+
status: number;
|
|
171
|
+
code: number;
|
|
172
|
+
message: string;
|
|
173
|
+
}): void {
|
|
174
|
+
response.writeHead(status, { 'content-type': 'application/json' });
|
|
175
|
+
response.end(
|
|
176
|
+
JSON.stringify({
|
|
177
|
+
jsonrpc: '2.0',
|
|
178
|
+
error: { code, message },
|
|
179
|
+
id: null,
|
|
180
|
+
}),
|
|
181
|
+
);
|
|
182
|
+
}
|
|
@@ -1,7 +1,13 @@
|
|
|
1
|
-
import { randomBytes, timingSafeEqual } from 'node:crypto';
|
|
1
|
+
import { randomBytes, randomUUID, timingSafeEqual } from 'node:crypto';
|
|
2
2
|
import { createServer, type IncomingMessage, type Server } from 'node:http';
|
|
3
3
|
import type { AddressInfo } from 'node:net';
|
|
4
4
|
import type { HarnessV1BridgeToolWire } from '@ai-sdk/harness';
|
|
5
|
+
import type { ACPHostToolMCPTransport } from '../acp-v1-settings';
|
|
6
|
+
import {
|
|
7
|
+
createHostToolMCPHttpEndpoint,
|
|
8
|
+
HOST_TOOL_MCP_ENDPOINT_PATH,
|
|
9
|
+
type HostToolMCPHttpEndpoint,
|
|
10
|
+
} from './host-tool-mcp-http';
|
|
5
11
|
|
|
6
12
|
export type HostToolCorrelationInvocation = {
|
|
7
13
|
readonly token: string;
|
|
@@ -34,6 +40,11 @@ export type HostToolRelayTurn = {
|
|
|
34
40
|
|
|
35
41
|
export type HostToolRelay = {
|
|
36
42
|
readonly url: string;
|
|
43
|
+
/**
|
|
44
|
+
* MCP endpoint that exposes the host tool catalog over Streamable HTTP.
|
|
45
|
+
* Only present when the relay was started with the `http` MCP transport.
|
|
46
|
+
*/
|
|
47
|
+
readonly mcpUrl?: string;
|
|
37
48
|
readonly credential: string;
|
|
38
49
|
bindTurn(options: { turn: HostToolRelayTurn }): void;
|
|
39
50
|
unbindTurn(options: { turn: HostToolRelayTurn }): void;
|
|
@@ -64,9 +75,11 @@ type CatalogState = {
|
|
|
64
75
|
export async function startHostToolRelay({
|
|
65
76
|
tools,
|
|
66
77
|
serverName,
|
|
78
|
+
mcpTransport = 'stdio',
|
|
67
79
|
}: {
|
|
68
80
|
tools: ReadonlyArray<HarnessV1BridgeToolWire>;
|
|
69
81
|
serverName: string;
|
|
82
|
+
mcpTransport?: ACPHostToolMCPTransport;
|
|
70
83
|
}): Promise<HostToolRelay> {
|
|
71
84
|
const state: CatalogState = {
|
|
72
85
|
tools: [...tools],
|
|
@@ -81,8 +94,52 @@ export async function startHostToolRelay({
|
|
|
81
94
|
let activeTurn: HostToolRelayTurn | undefined;
|
|
82
95
|
let invocationOrder = 0;
|
|
83
96
|
let closePromise: Promise<void> | undefined;
|
|
97
|
+
const mcpEndpoint: HostToolMCPHttpEndpoint | undefined =
|
|
98
|
+
mcpTransport === 'http'
|
|
99
|
+
? createHostToolMCPHttpEndpoint({
|
|
100
|
+
tools: state.tools,
|
|
101
|
+
revision: state.revision,
|
|
102
|
+
invoke: ({ toolName, input, catalogRevision }) =>
|
|
103
|
+
handleInvocation({
|
|
104
|
+
body: {
|
|
105
|
+
requestId: randomUUID(),
|
|
106
|
+
toolName,
|
|
107
|
+
input,
|
|
108
|
+
catalogRevision,
|
|
109
|
+
},
|
|
110
|
+
state,
|
|
111
|
+
serverName,
|
|
112
|
+
turn: activeTurn,
|
|
113
|
+
nextInvocationOrder: () => ++invocationOrder,
|
|
114
|
+
}),
|
|
115
|
+
onListTools: async ({ revision }) => {
|
|
116
|
+
acknowledgeCatalog({ state, revision });
|
|
117
|
+
},
|
|
118
|
+
})
|
|
119
|
+
: undefined;
|
|
84
120
|
const server = createServer(async (request, response) => {
|
|
85
121
|
try {
|
|
122
|
+
if (mcpEndpoint != null && request.url === HOST_TOOL_MCP_ENDPOINT_PATH) {
|
|
123
|
+
if (
|
|
124
|
+
!credentialsMatch({
|
|
125
|
+
expected: credential,
|
|
126
|
+
actual: request.headers.authorization,
|
|
127
|
+
})
|
|
128
|
+
) {
|
|
129
|
+
throw new RelayRequestError({
|
|
130
|
+
status: 401,
|
|
131
|
+
message: 'Invalid host tool relay credential.',
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
await mcpEndpoint.handleRequest({
|
|
135
|
+
request,
|
|
136
|
+
response,
|
|
137
|
+
...(request.method === 'POST'
|
|
138
|
+
? { body: await readJSONBody({ request }) }
|
|
139
|
+
: {}),
|
|
140
|
+
});
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
86
143
|
const result = await handleRequest({
|
|
87
144
|
request,
|
|
88
145
|
credential,
|
|
@@ -94,6 +151,10 @@ export async function startHostToolRelay({
|
|
|
94
151
|
response.writeHead(200, { 'content-type': 'application/json' });
|
|
95
152
|
response.end(JSON.stringify(result));
|
|
96
153
|
} catch (error) {
|
|
154
|
+
if (response.headersSent) {
|
|
155
|
+
response.end();
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
97
158
|
const status = error instanceof RelayRequestError ? error.status : 500;
|
|
98
159
|
response.writeHead(status, { 'content-type': 'application/json' });
|
|
99
160
|
response.end(
|
|
@@ -108,6 +169,11 @@ export async function startHostToolRelay({
|
|
|
108
169
|
|
|
109
170
|
return {
|
|
110
171
|
url: `http://127.0.0.1:${address.port}/invoke`,
|
|
172
|
+
...(mcpEndpoint == null
|
|
173
|
+
? {}
|
|
174
|
+
: {
|
|
175
|
+
mcpUrl: `http://127.0.0.1:${address.port}${HOST_TOOL_MCP_ENDPOINT_PATH}`,
|
|
176
|
+
}),
|
|
111
177
|
credential,
|
|
112
178
|
bindTurn: ({ turn }) => {
|
|
113
179
|
if (activeTurn != null && activeTurn !== turn) {
|
|
@@ -127,6 +193,10 @@ export async function startHostToolRelay({
|
|
|
127
193
|
state.fingerprint = fingerprint;
|
|
128
194
|
state.revision += 1;
|
|
129
195
|
resolveCatalogChanges({ state });
|
|
196
|
+
void mcpEndpoint?.updateCatalog({
|
|
197
|
+
revision: state.revision,
|
|
198
|
+
tools: state.tools,
|
|
199
|
+
});
|
|
130
200
|
return { changed: true, revision: state.revision };
|
|
131
201
|
},
|
|
132
202
|
waitForCatalogRefresh: ({ revision, timeoutMs }) =>
|
|
@@ -136,7 +206,10 @@ export async function startHostToolRelay({
|
|
|
136
206
|
state.closed = true;
|
|
137
207
|
resolveCatalogChanges({ state });
|
|
138
208
|
resolveRefreshWaiters({ state, closing: true });
|
|
139
|
-
closePromise =
|
|
209
|
+
closePromise = (async () => {
|
|
210
|
+
await mcpEndpoint?.close();
|
|
211
|
+
await closeServer({ server });
|
|
212
|
+
})();
|
|
140
213
|
return closePromise;
|
|
141
214
|
},
|
|
142
215
|
};
|
|
@@ -243,14 +316,21 @@ function handleCatalogSeen({
|
|
|
243
316
|
message: 'Invalid host tool catalog acknowledgment.',
|
|
244
317
|
});
|
|
245
318
|
}
|
|
246
|
-
state.
|
|
247
|
-
state.servedRevision,
|
|
248
|
-
body.revision as number,
|
|
249
|
-
);
|
|
250
|
-
resolveRefreshWaiters({ state, closing: false });
|
|
319
|
+
acknowledgeCatalog({ state, revision: body.revision as number });
|
|
251
320
|
return { acknowledged: true };
|
|
252
321
|
}
|
|
253
322
|
|
|
323
|
+
function acknowledgeCatalog({
|
|
324
|
+
state,
|
|
325
|
+
revision,
|
|
326
|
+
}: {
|
|
327
|
+
state: CatalogState;
|
|
328
|
+
revision: number;
|
|
329
|
+
}): void {
|
|
330
|
+
state.servedRevision = Math.max(state.servedRevision, revision);
|
|
331
|
+
resolveRefreshWaiters({ state, closing: false });
|
|
332
|
+
}
|
|
333
|
+
|
|
254
334
|
async function handleInvocation({
|
|
255
335
|
body,
|
|
256
336
|
state,
|
package/src/v1/bridge/index.ts
CHANGED
|
@@ -7,7 +7,6 @@ import {
|
|
|
7
7
|
import * as acp from '@agentclientprotocol/sdk';
|
|
8
8
|
import { spawn, type ChildProcessWithoutNullStreams } from 'node:child_process';
|
|
9
9
|
import { readFile, writeFile } from 'node:fs/promises';
|
|
10
|
-
import { fileURLToPath } from 'node:url';
|
|
11
10
|
import { Readable, Writable } from 'node:stream';
|
|
12
11
|
import { argv, env as processEnv } from 'node:process';
|
|
13
12
|
import {
|
|
@@ -38,6 +37,7 @@ import {
|
|
|
38
37
|
type HostToolRelay,
|
|
39
38
|
type HostToolRelayTurn,
|
|
40
39
|
} from './host-tool-relay';
|
|
40
|
+
import { createHostToolMcpServerDefinition } from './host-tool-mcp-definition';
|
|
41
41
|
import {
|
|
42
42
|
promptAndRefreshInitialHostToolCatalog,
|
|
43
43
|
refreshHostToolCatalog,
|
|
@@ -593,34 +593,27 @@ async function ensureSession({
|
|
|
593
593
|
initialization,
|
|
594
594
|
});
|
|
595
595
|
const tools = start.tools ?? [];
|
|
596
|
+
const mcpTransport = bridgeConfiguration.hostToolMcpTransport ?? 'stdio';
|
|
596
597
|
const catalogPath = `${bridgeStateDir}/host-tools.json`;
|
|
597
|
-
|
|
598
|
+
if (mcpTransport === 'stdio') {
|
|
599
|
+
await writeFile(catalogPath, JSON.stringify(tools), { mode: 0o600 });
|
|
600
|
+
}
|
|
598
601
|
hostToolRelay = await startHostToolRelay({
|
|
599
602
|
tools,
|
|
600
603
|
serverName: HOST_TOOL_MCP_SERVER_NAME,
|
|
604
|
+
mcpTransport,
|
|
601
605
|
});
|
|
602
606
|
|
|
603
607
|
const mcpServers: acp.McpServer[] = [
|
|
604
608
|
...externalMcpServers,
|
|
605
|
-
{
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
},
|
|
614
|
-
{
|
|
615
|
-
name: 'AI_SDK_ACP_HOST_TOOL_RELAY_URL',
|
|
616
|
-
value: hostToolRelay.url,
|
|
617
|
-
},
|
|
618
|
-
{
|
|
619
|
-
name: 'AI_SDK_ACP_HOST_TOOL_RELAY_CREDENTIAL',
|
|
620
|
-
value: hostToolRelay.credential,
|
|
621
|
-
},
|
|
622
|
-
],
|
|
623
|
-
},
|
|
609
|
+
createHostToolMcpServerDefinition({
|
|
610
|
+
mcpTransport,
|
|
611
|
+
relay: hostToolRelay,
|
|
612
|
+
serverName: HOST_TOOL_MCP_SERVER_NAME,
|
|
613
|
+
catalogPath,
|
|
614
|
+
initialization,
|
|
615
|
+
harnessId: bridgeType,
|
|
616
|
+
}),
|
|
624
617
|
];
|
|
625
618
|
let createdSession: ACPActiveSession;
|
|
626
619
|
if (start.recoveryMode?.type === 'lossy-rerun') {
|
|
@@ -33,6 +33,18 @@ export async function resolveACPInstructionConfiguration({
|
|
|
33
33
|
return { sessionMeta, environment: resolvedEnvironment };
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
if (instructionMapping.type === 'filesystem') {
|
|
37
|
+
if (
|
|
38
|
+
typeof instructionMapping.path !== 'string' ||
|
|
39
|
+
instructionMapping.path.trim().length === 0
|
|
40
|
+
) {
|
|
41
|
+
throw new Error(
|
|
42
|
+
'ACP instruction mapping filesystem path must be a non-empty string.',
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
return { sessionMeta, environment: resolvedEnvironment };
|
|
46
|
+
}
|
|
47
|
+
|
|
36
48
|
assertSafePath({ path: instructionMapping.path });
|
|
37
49
|
|
|
38
50
|
if (instructionMapping.type === 'session-meta') {
|
|
@@ -526,7 +526,41 @@ function resolveBuiltinTool({
|
|
|
526
526
|
const schemaMatches = builtinTools.filter(tool =>
|
|
527
527
|
matchesBuiltinToolInput({ rawInput, inputSchema: tool.inputSchema }),
|
|
528
528
|
);
|
|
529
|
-
|
|
529
|
+
if (schemaMatches.length <= 1) return schemaMatches[0];
|
|
530
|
+
return findMostSpecificSchemaMatch({ rawInput, schemaMatches });
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
/*
|
|
534
|
+
* Multiple schemas can pass `matchesBuiltinToolInput` at once: a tool whose
|
|
535
|
+
* only required field is optional on a richer sibling tool (e.g. `bash`'s
|
|
536
|
+
* optional `shellId` versus `stop_bash`'s required `shellId`) matches any
|
|
537
|
+
* rawInput the richer tool also matches. Preferring whichever candidate
|
|
538
|
+
* declares the most of rawInput's own keys resolves that in favor of the
|
|
539
|
+
* richer, more specific schema, while two equally specific schemas (e.g.
|
|
540
|
+
* identical schemas under different names) still resolve to `undefined`.
|
|
541
|
+
*/
|
|
542
|
+
function findMostSpecificSchemaMatch({
|
|
543
|
+
rawInput,
|
|
544
|
+
schemaMatches,
|
|
545
|
+
}: {
|
|
546
|
+
rawInput: unknown;
|
|
547
|
+
schemaMatches: ReadonlyArray<ACPBuiltinToolMapping>;
|
|
548
|
+
}): ACPBuiltinToolMapping | undefined {
|
|
549
|
+
if (!isRecord(rawInput)) return undefined;
|
|
550
|
+
const rawInputKeys = Object.keys(rawInput);
|
|
551
|
+
const coverageOf = (tool: ACPBuiltinToolMapping): number => {
|
|
552
|
+
const properties = isRecord(tool.inputSchema)
|
|
553
|
+
? isRecord(tool.inputSchema.properties)
|
|
554
|
+
? tool.inputSchema.properties
|
|
555
|
+
: {}
|
|
556
|
+
: {};
|
|
557
|
+
return rawInputKeys.filter(key => key in properties).length;
|
|
558
|
+
};
|
|
559
|
+
const highestCoverage = Math.max(...schemaMatches.map(coverageOf));
|
|
560
|
+
const mostSpecificMatches = schemaMatches.filter(
|
|
561
|
+
tool => coverageOf(tool) === highestCoverage,
|
|
562
|
+
);
|
|
563
|
+
return mostSpecificMatches.length === 1 ? mostSpecificMatches[0] : undefined;
|
|
530
564
|
}
|
|
531
565
|
|
|
532
566
|
function findBuiltinToolByTitle({
|