@ai-sdk/harness-acp 1.0.38 → 1.0.40
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 +24 -0
- package/README.md +4 -3
- package/dist/bridge/host-tool-mcp.mjs +1 -1
- package/dist/bridge/index.mjs +556 -43
- package/dist/bridge/index.mjs.map +1 -1
- package/dist/index.d.ts +56 -6
- package/dist/index.js +280 -72
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/acp-harness.ts +51 -9
- package/src/index.ts +2 -0
- package/src/v1/acp-v1-bridge-protocol.ts +19 -0
- package/src/v1/acp-v1-harness.ts +307 -19
- package/src/v1/acp-v1-settings.ts +42 -0
- package/src/v1/acp-v1-skills.ts +4 -65
- package/src/v1/bridge/acp-v1-bridge-environment.ts +16 -0
- package/src/v1/bridge/create-emit-stream-event.ts +6 -0
- package/src/v1/bridge/host-tool-correlation.ts +34 -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 +111 -24
- package/src/v1/bridge/instruction-mapping.ts +12 -0
- package/src/v1/bridge/stream-translator.ts +41 -2
- package/src/v1/index.ts +2 -0
|
@@ -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,
|
|
@@ -104,6 +104,13 @@ let coldRestorationMethod: ACPSessionRestorationMethod | undefined;
|
|
|
104
104
|
let activePermissionController:
|
|
105
105
|
| ReturnType<typeof createACPPermissionController>
|
|
106
106
|
| undefined;
|
|
107
|
+
let activeQuestionRequest:
|
|
108
|
+
| {
|
|
109
|
+
method: string;
|
|
110
|
+
turn: BridgeTurn;
|
|
111
|
+
emitStreamEvent: ReturnType<typeof createEmitStreamEvent>;
|
|
112
|
+
}
|
|
113
|
+
| undefined;
|
|
107
114
|
|
|
108
115
|
await runBridge<StartMessage>({
|
|
109
116
|
bridgeType,
|
|
@@ -252,15 +259,36 @@ async function runTurn(start: StartMessage, turn: BridgeTurn): Promise<void> {
|
|
|
252
259
|
const emitStreamEvent = createEmitStreamEvent({
|
|
253
260
|
emit: event => turn.emit(event as BridgeEvent),
|
|
254
261
|
emitToolCallCandidate: ({ toolCall }) => {
|
|
262
|
+
const requestId = crypto.randomUUID();
|
|
255
263
|
turn.emit({
|
|
256
264
|
type: 'acp-tool-call-candidate',
|
|
265
|
+
requestId,
|
|
257
266
|
toolCall,
|
|
258
267
|
});
|
|
268
|
+
void turn.requestToolResult(requestId).then(result => {
|
|
269
|
+
if (
|
|
270
|
+
result.output != null &&
|
|
271
|
+
typeof result.output === 'object' &&
|
|
272
|
+
'suppress' in result.output &&
|
|
273
|
+
result.output.suppress === true
|
|
274
|
+
) {
|
|
275
|
+
emitStreamEvent.suppressToolCall({
|
|
276
|
+
toolCallId: toolCall.toolCallId,
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
});
|
|
259
280
|
},
|
|
260
281
|
builtinTools: start.builtinTools,
|
|
261
282
|
hostToolServerName: HOST_TOOL_MCP_SERVER_NAME,
|
|
262
283
|
hostTools: start.tools ?? [],
|
|
263
284
|
});
|
|
285
|
+
if (bridgeConfiguration.askUserQuestionsRequestMethod != null) {
|
|
286
|
+
activeQuestionRequest = {
|
|
287
|
+
method: bridgeConfiguration.askUserQuestionsRequestMethod,
|
|
288
|
+
turn,
|
|
289
|
+
emitStreamEvent,
|
|
290
|
+
};
|
|
291
|
+
}
|
|
264
292
|
const permissionController = createACPPermissionController({
|
|
265
293
|
turn,
|
|
266
294
|
sessionId: activeSession.sessionId,
|
|
@@ -353,6 +381,9 @@ async function runTurn(start: StartMessage, turn: BridgeTurn): Promise<void> {
|
|
|
353
381
|
if (emitStreamEvent.message({ message })) return;
|
|
354
382
|
}
|
|
355
383
|
} finally {
|
|
384
|
+
if (activeQuestionRequest?.turn === turn) {
|
|
385
|
+
activeQuestionRequest = undefined;
|
|
386
|
+
}
|
|
356
387
|
permissionController.cancelAll();
|
|
357
388
|
activePermissionController = undefined;
|
|
358
389
|
activeHostToolRelay.unbindTurn({ turn: relayTurn });
|
|
@@ -458,7 +489,7 @@ async function ensureSession({
|
|
|
458
489
|
stream: acp.ndJsonStream(input, output),
|
|
459
490
|
});
|
|
460
491
|
streamCapture = capturedStream.capture;
|
|
461
|
-
|
|
492
|
+
let client = acp
|
|
462
493
|
.client({ name: clientApp.name })
|
|
463
494
|
.onRequest(
|
|
464
495
|
acp.methods.client.session.requestPermission,
|
|
@@ -475,8 +506,58 @@ async function ensureSession({
|
|
|
475
506
|
notification: params,
|
|
476
507
|
update: params.update,
|
|
477
508
|
});
|
|
478
|
-
})
|
|
479
|
-
|
|
509
|
+
});
|
|
510
|
+
if (bridgeConfiguration.askUserQuestionsRequestMethod != null) {
|
|
511
|
+
const method = bridgeConfiguration.askUserQuestionsRequestMethod;
|
|
512
|
+
client = client.onRequest<unknown, unknown>(
|
|
513
|
+
method,
|
|
514
|
+
value => value,
|
|
515
|
+
async ({ params }) => {
|
|
516
|
+
const active = activeQuestionRequest;
|
|
517
|
+
if (active == null || active.method !== method) {
|
|
518
|
+
throw acp.RequestError.methodNotFound(method);
|
|
519
|
+
}
|
|
520
|
+
const requestId = crypto.randomUUID();
|
|
521
|
+
const nativeToolCallId = getNativeQuestionToolCallId({ params });
|
|
522
|
+
active.turn.emit({
|
|
523
|
+
type: 'acp-question-request',
|
|
524
|
+
requestId,
|
|
525
|
+
nativeRequest: params,
|
|
526
|
+
...(nativeToolCallId == null
|
|
527
|
+
? {}
|
|
528
|
+
: {
|
|
529
|
+
nativeToolCall:
|
|
530
|
+
active.emitStreamEvent.getToolCall({
|
|
531
|
+
toolCallId: nativeToolCallId,
|
|
532
|
+
}) ?? undefined,
|
|
533
|
+
}),
|
|
534
|
+
});
|
|
535
|
+
const result = await active.turn.requestToolResult(requestId);
|
|
536
|
+
const classification = result.output as
|
|
537
|
+
| { type: 'unhandled' }
|
|
538
|
+
| { type: 'handled'; toolCallId: string }
|
|
539
|
+
| undefined;
|
|
540
|
+
if (classification?.type !== 'handled') {
|
|
541
|
+
throw acp.RequestError.methodNotFound(method);
|
|
542
|
+
}
|
|
543
|
+
active.emitStreamEvent.suppressToolCall({
|
|
544
|
+
toolCallId: nativeToolCallId ?? classification.toolCallId,
|
|
545
|
+
});
|
|
546
|
+
try {
|
|
547
|
+
const nativeResult = await active.turn.requestToolResult(
|
|
548
|
+
classification.toolCallId,
|
|
549
|
+
);
|
|
550
|
+
return nativeResult.output;
|
|
551
|
+
} finally {
|
|
552
|
+
active.turn.emit({
|
|
553
|
+
type: 'acp-question-resolved',
|
|
554
|
+
requestId,
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
},
|
|
558
|
+
);
|
|
559
|
+
}
|
|
560
|
+
connection = client.connect(capturedStream.stream);
|
|
480
561
|
|
|
481
562
|
const initializeRequest = createACPInitializeRequest({
|
|
482
563
|
protocolVersion: acp.PROTOCOL_VERSION,
|
|
@@ -512,34 +593,27 @@ async function ensureSession({
|
|
|
512
593
|
initialization,
|
|
513
594
|
});
|
|
514
595
|
const tools = start.tools ?? [];
|
|
596
|
+
const mcpTransport = bridgeConfiguration.hostToolMcpTransport ?? 'stdio';
|
|
515
597
|
const catalogPath = `${bridgeStateDir}/host-tools.json`;
|
|
516
|
-
|
|
598
|
+
if (mcpTransport === 'stdio') {
|
|
599
|
+
await writeFile(catalogPath, JSON.stringify(tools), { mode: 0o600 });
|
|
600
|
+
}
|
|
517
601
|
hostToolRelay = await startHostToolRelay({
|
|
518
602
|
tools,
|
|
519
603
|
serverName: HOST_TOOL_MCP_SERVER_NAME,
|
|
604
|
+
mcpTransport,
|
|
520
605
|
});
|
|
521
606
|
|
|
522
607
|
const mcpServers: acp.McpServer[] = [
|
|
523
608
|
...externalMcpServers,
|
|
524
|
-
{
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
},
|
|
533
|
-
{
|
|
534
|
-
name: 'AI_SDK_ACP_HOST_TOOL_RELAY_URL',
|
|
535
|
-
value: hostToolRelay.url,
|
|
536
|
-
},
|
|
537
|
-
{
|
|
538
|
-
name: 'AI_SDK_ACP_HOST_TOOL_RELAY_CREDENTIAL',
|
|
539
|
-
value: hostToolRelay.credential,
|
|
540
|
-
},
|
|
541
|
-
],
|
|
542
|
-
},
|
|
609
|
+
createHostToolMcpServerDefinition({
|
|
610
|
+
mcpTransport,
|
|
611
|
+
relay: hostToolRelay,
|
|
612
|
+
serverName: HOST_TOOL_MCP_SERVER_NAME,
|
|
613
|
+
catalogPath,
|
|
614
|
+
initialization,
|
|
615
|
+
harnessId: bridgeType,
|
|
616
|
+
}),
|
|
543
617
|
];
|
|
544
618
|
let createdSession: ACPActiveSession;
|
|
545
619
|
if (start.recoveryMode?.type === 'lossy-rerun') {
|
|
@@ -627,6 +701,19 @@ async function ensureSession({
|
|
|
627
701
|
return { initialHostToolCatalogRefreshRequired: tools.length > 0 };
|
|
628
702
|
}
|
|
629
703
|
|
|
704
|
+
function getNativeQuestionToolCallId({
|
|
705
|
+
params,
|
|
706
|
+
}: {
|
|
707
|
+
params: unknown;
|
|
708
|
+
}): string | undefined {
|
|
709
|
+
if (!isRecord(params)) return undefined;
|
|
710
|
+
if (typeof params.toolCallId === 'string') return params.toolCallId;
|
|
711
|
+
const nestedParams = params.params;
|
|
712
|
+
return isRecord(nestedParams) && typeof nestedParams.toolCallId === 'string'
|
|
713
|
+
? nestedParams.toolCallId
|
|
714
|
+
: undefined;
|
|
715
|
+
}
|
|
716
|
+
|
|
630
717
|
function createExternalMcpServers({
|
|
631
718
|
mcpServers,
|
|
632
719
|
initialization,
|
|
@@ -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') {
|
|
@@ -68,6 +68,7 @@ export function createACPStreamTranslator({
|
|
|
68
68
|
output: unknown;
|
|
69
69
|
isError?: boolean;
|
|
70
70
|
}) => void;
|
|
71
|
+
getToolCall: (options: { toolCallId: string }) => ACPToolCall | undefined;
|
|
71
72
|
} {
|
|
72
73
|
let openBlock:
|
|
73
74
|
| {
|
|
@@ -215,6 +216,7 @@ export function createACPStreamTranslator({
|
|
|
215
216
|
builtinTools,
|
|
216
217
|
builtinToolsByName,
|
|
217
218
|
});
|
|
219
|
+
emitToolCallCandidate?.({ toolCall: createACPToolCall({ state }) });
|
|
218
220
|
if (
|
|
219
221
|
!forceEmit &&
|
|
220
222
|
builtin?.inputSchema != null &&
|
|
@@ -238,7 +240,6 @@ export function createACPStreamTranslator({
|
|
|
238
240
|
programmaticName,
|
|
239
241
|
toolCallId: state.toolCallId,
|
|
240
242
|
});
|
|
241
|
-
emitToolCallCandidate?.({ toolCall: createACPToolCall({ state }) });
|
|
242
243
|
}
|
|
243
244
|
emit({
|
|
244
245
|
type: 'tool-call',
|
|
@@ -381,6 +382,10 @@ export function createACPStreamTranslator({
|
|
|
381
382
|
close: closeBlock,
|
|
382
383
|
hostToolCall,
|
|
383
384
|
hostToolResult,
|
|
385
|
+
getToolCall: ({ toolCallId }) => {
|
|
386
|
+
const state = toolStates.get(toolCallId);
|
|
387
|
+
return state == null ? undefined : createACPToolCall({ state });
|
|
388
|
+
},
|
|
384
389
|
finish: response => {
|
|
385
390
|
if (finished) return;
|
|
386
391
|
finished = true;
|
|
@@ -521,7 +526,41 @@ function resolveBuiltinTool({
|
|
|
521
526
|
const schemaMatches = builtinTools.filter(tool =>
|
|
522
527
|
matchesBuiltinToolInput({ rawInput, inputSchema: tool.inputSchema }),
|
|
523
528
|
);
|
|
524
|
-
|
|
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;
|
|
525
564
|
}
|
|
526
565
|
|
|
527
566
|
function findBuiltinToolByTitle({
|
package/src/v1/index.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
export { createACPV1 } from './acp-v1-harness';
|
|
2
2
|
export type {
|
|
3
|
+
ACPAskUserQuestionsSettings,
|
|
3
4
|
ACPCredentialBrokering,
|
|
4
5
|
ACPAuthentication,
|
|
5
6
|
ACPAuthenticationMode,
|
|
7
|
+
ACPHostToolMCPTransport,
|
|
6
8
|
ACPInstallCommandSource,
|
|
7
9
|
ACPInstructionMapping,
|
|
8
10
|
ACPModelMapping,
|