@ai-sdk/harness-acp 1.0.0 → 1.0.2
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 +22 -0
- package/README.md +5 -6
- package/dist/bridge/host-tool-mcp.mjs +1 -1
- package/dist/bridge/index.mjs +145 -39
- package/dist/bridge/index.mjs.map +1 -1
- package/dist/index.d.ts +137 -19
- package/dist/index.js +187 -110
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/acp-harness.ts +26 -60
- package/src/acp-tool-call.ts +124 -0
- package/src/index.ts +21 -3
- package/src/v1/acp-v1-bridge-protocol.ts +17 -1
- package/src/v1/acp-v1-harness.ts +111 -21
- package/src/v1/acp-v1-settings.ts +27 -20
- package/src/v1/acp-v1-turn-start-config.ts +3 -0
- package/src/v1/bridge/acp-diagnostics.ts +27 -1
- package/src/v1/bridge/agent-stderr-monitor.ts +52 -0
- package/src/v1/bridge/create-emit-stream-event.ts +8 -1
- package/src/v1/bridge/index.ts +66 -34
- package/src/v1/bridge/stream-translator.ts +62 -5
- package/src/v1/implementation.ts +73 -47
- package/src/v1/index.ts +3 -3
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { HarnessV1PermissionMode } from '@ai-sdk/harness';
|
|
2
|
+
import type { ACPToolCall } from '../acp-tool-call';
|
|
2
3
|
|
|
3
4
|
export type ACPSerializablePrimitive = string | number | boolean | null;
|
|
4
5
|
|
|
@@ -26,33 +27,24 @@ export type ACPProfileValue =
|
|
|
26
27
|
| ReadonlyArray<ACPProfileValue>
|
|
27
28
|
| { readonly [key: string]: ACPProfileValue };
|
|
28
29
|
|
|
29
|
-
type
|
|
30
|
-
readonly type: 'npm';
|
|
31
|
-
readonly
|
|
32
|
-
readonly args?: ReadonlyArray<string>;
|
|
33
|
-
readonly forwardEnv?: ReadonlyArray<string>;
|
|
30
|
+
export type ACPNpmSimpleSource = {
|
|
31
|
+
readonly type: 'npm-simple';
|
|
32
|
+
readonly packageName: string;
|
|
34
33
|
/**
|
|
35
|
-
*
|
|
36
|
-
*
|
|
34
|
+
* Exact version to install. When omitted, the package's `latest` dist-tag is
|
|
35
|
+
* installed and no version takes part in the implementation identity, so a
|
|
36
|
+
* new upstream release does not invalidate existing lifecycle state.
|
|
37
37
|
*/
|
|
38
|
-
readonly
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
export type ACPSimpleNpmImplementation = ACPNpmLaunchSettings & {
|
|
42
|
-
readonly mode: 'simple';
|
|
43
|
-
readonly packageName: string;
|
|
44
|
-
readonly version: string;
|
|
38
|
+
readonly packageVersion?: string;
|
|
45
39
|
};
|
|
46
40
|
|
|
47
|
-
export type
|
|
48
|
-
readonly
|
|
41
|
+
export type ACPNpmLockedSource = {
|
|
42
|
+
readonly type: 'npm-locked';
|
|
49
43
|
readonly packageJson: string;
|
|
50
44
|
readonly pnpmLockYaml: string;
|
|
51
45
|
};
|
|
52
46
|
|
|
53
|
-
export type
|
|
54
|
-
| ACPSimpleNpmImplementation
|
|
55
|
-
| ACPLockedNpmImplementation;
|
|
47
|
+
export type ACPSource = ACPNpmSimpleSource | ACPNpmLockedSource;
|
|
56
48
|
|
|
57
49
|
export type ACPAuthentication = {
|
|
58
50
|
readonly methodId: string;
|
|
@@ -86,8 +78,18 @@ export type ACPPermissionModeMapping = Readonly<
|
|
|
86
78
|
export type ACPV1Settings = {
|
|
87
79
|
readonly version?: 'v1';
|
|
88
80
|
readonly harnessId: string;
|
|
81
|
+
readonly mcpServers?: Record<string, unknown>;
|
|
82
|
+
readonly isMcpToolCall?: (toolCall: ACPToolCall) => boolean;
|
|
89
83
|
readonly auth?: ACPProviderAuthenticationMode;
|
|
90
|
-
readonly
|
|
84
|
+
readonly source: ACPSource;
|
|
85
|
+
readonly executable: string;
|
|
86
|
+
readonly args?: ReadonlyArray<string>;
|
|
87
|
+
readonly forwardEnv?: ReadonlyArray<string>;
|
|
88
|
+
/**
|
|
89
|
+
* Runtime environment values that are safe to persist in bootstrap and
|
|
90
|
+
* lifecycle compatibility identity.
|
|
91
|
+
*/
|
|
92
|
+
readonly env?: Readonly<Record<string, string>>;
|
|
91
93
|
readonly authentication?: ACPAuthentication;
|
|
92
94
|
readonly providerAuthentication?: ACPProviderAuthentication;
|
|
93
95
|
readonly modelId?: string;
|
|
@@ -95,4 +97,9 @@ export type ACPV1Settings = {
|
|
|
95
97
|
readonly session?: {
|
|
96
98
|
readonly meta?: Readonly<Record<string, ACPSerializableValue>>;
|
|
97
99
|
};
|
|
100
|
+
/**
|
|
101
|
+
* Creates the authentication token used by the sandbox bridge. Defaults to
|
|
102
|
+
* a random 32-byte hexadecimal token.
|
|
103
|
+
*/
|
|
104
|
+
readonly mintBridgeToken?: (sandboxId: string) => string;
|
|
98
105
|
};
|
|
@@ -20,6 +20,7 @@ export function createACPTurnStartConfig({
|
|
|
20
20
|
builtinTools,
|
|
21
21
|
permissionMode,
|
|
22
22
|
permissionModeMapping,
|
|
23
|
+
mcpServers,
|
|
23
24
|
debug,
|
|
24
25
|
authenticationProfile,
|
|
25
26
|
sessionMeta,
|
|
@@ -29,6 +30,7 @@ export function createACPTurnStartConfig({
|
|
|
29
30
|
builtinTools: ReadonlyArray<ACPBuiltinToolMapping>;
|
|
30
31
|
permissionMode: NonNullable<StartMessage['permissionMode']>;
|
|
31
32
|
permissionModeMapping: ACPPermissionModeMapping | undefined;
|
|
33
|
+
mcpServers: Record<string, unknown> | undefined;
|
|
32
34
|
debug: HarnessV1DebugConfig | undefined;
|
|
33
35
|
authenticationProfile: ACPAuthenticationProfileIdentity;
|
|
34
36
|
sessionMeta: Readonly<Record<string, ACPSerializableValue>> | undefined;
|
|
@@ -42,6 +44,7 @@ export function createACPTurnStartConfig({
|
|
|
42
44
|
sessionMeta: sessionMeta ?? null,
|
|
43
45
|
builtinTools,
|
|
44
46
|
permissionModeMapping: permissionModeMapping ?? null,
|
|
47
|
+
mcpServers: mcpServers ?? null,
|
|
45
48
|
}),
|
|
46
49
|
)
|
|
47
50
|
.digest('hex'),
|
|
@@ -39,11 +39,37 @@ export function createACPBridgeError({
|
|
|
39
39
|
| 'prompt update stream';
|
|
40
40
|
cause: unknown;
|
|
41
41
|
}): Error {
|
|
42
|
-
const
|
|
42
|
+
const causeMessage = getErrorMessage({ error: cause });
|
|
43
|
+
const error = new Error(
|
|
44
|
+
causeMessage == null
|
|
45
|
+
? `ACP ${stage} failed.`
|
|
46
|
+
: `ACP ${stage} failed: ${causeMessage}`,
|
|
47
|
+
);
|
|
43
48
|
(error as Error & { cause?: unknown }).cause = cause;
|
|
44
49
|
return error;
|
|
45
50
|
}
|
|
46
51
|
|
|
52
|
+
function getErrorMessage({ error }: { error: unknown }): string | undefined {
|
|
53
|
+
if (error instanceof Error) {
|
|
54
|
+
return error.message;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (
|
|
58
|
+
error != null &&
|
|
59
|
+
typeof error === 'object' &&
|
|
60
|
+
'message' in error &&
|
|
61
|
+
typeof error.message === 'string'
|
|
62
|
+
) {
|
|
63
|
+
return error.message;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (typeof error === 'string') {
|
|
67
|
+
return error;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
|
|
47
73
|
function stripMetadata({ value }: { value: unknown }): unknown {
|
|
48
74
|
if (Array.isArray(value)) {
|
|
49
75
|
return value.map(item => stripMetadata({ value: item }));
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { Readable } from 'node:stream';
|
|
2
|
+
import { stripVTControlCharacters } from 'node:util';
|
|
3
|
+
|
|
4
|
+
export function monitorACPAgentStderr({
|
|
5
|
+
stderr,
|
|
6
|
+
onStderrLine,
|
|
7
|
+
}: {
|
|
8
|
+
stderr: Readable;
|
|
9
|
+
onStderrLine: (line: string) => void;
|
|
10
|
+
}): Promise<never> {
|
|
11
|
+
let rejectFailure!: (error: unknown) => void;
|
|
12
|
+
const failure = new Promise<never>((_, reject) => {
|
|
13
|
+
rejectFailure = reject;
|
|
14
|
+
});
|
|
15
|
+
void failure.catch(() => {});
|
|
16
|
+
|
|
17
|
+
void (async () => {
|
|
18
|
+
stderr.setEncoding('utf8');
|
|
19
|
+
let pending = '';
|
|
20
|
+
for await (const chunk of stderr) {
|
|
21
|
+
pending += chunk;
|
|
22
|
+
const lines = pending.split('\n');
|
|
23
|
+
pending = lines.pop() ?? '';
|
|
24
|
+
for (const line of lines) handleStderrLine({ line });
|
|
25
|
+
}
|
|
26
|
+
if (pending.length > 0) handleStderrLine({ line: pending });
|
|
27
|
+
})().catch(error => rejectFailure(error));
|
|
28
|
+
|
|
29
|
+
return failure;
|
|
30
|
+
|
|
31
|
+
function handleStderrLine({ line }: { line: string }): void {
|
|
32
|
+
if (line.length === 0) return;
|
|
33
|
+
onStderrLine(line);
|
|
34
|
+
const normalizedLine = stripVTControlCharacters(line);
|
|
35
|
+
|
|
36
|
+
/*
|
|
37
|
+
* An ACP agent can log a response-stream decoding failure without rejecting
|
|
38
|
+
* its pending JSON-RPC prompt. Once the provider stream cannot be decoded,
|
|
39
|
+
* no valid prompt completion can reach the ACP client, so the bridge must
|
|
40
|
+
* fail the turn instead of waiting indefinitely.
|
|
41
|
+
*/
|
|
42
|
+
if (
|
|
43
|
+
normalizedLine
|
|
44
|
+
.toLowerCase()
|
|
45
|
+
.includes('failed to deserialize responsestreamevent from stream')
|
|
46
|
+
) {
|
|
47
|
+
rejectFailure(
|
|
48
|
+
new Error('ACP agent failed to deserialize a streamed response.'),
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -6,17 +6,20 @@ import type {
|
|
|
6
6
|
HarnessV1BridgeToolWire,
|
|
7
7
|
HarnessV1StreamPart,
|
|
8
8
|
} from '@ai-sdk/harness';
|
|
9
|
+
import type { ACPToolCall } from '../../acp-tool-call';
|
|
9
10
|
import type { ACPBuiltinToolMapping } from '../acp-v1-bridge-protocol';
|
|
10
11
|
import { createACPStreamTranslator } from './stream-translator';
|
|
11
12
|
import { createHostToolCorrelation } from './host-tool-correlation';
|
|
12
13
|
|
|
13
14
|
export function createEmitStreamEvent({
|
|
14
15
|
emit,
|
|
16
|
+
emitToolCallCandidate,
|
|
15
17
|
builtinTools,
|
|
16
18
|
hostToolServerName,
|
|
17
19
|
hostTools,
|
|
18
20
|
}: {
|
|
19
21
|
emit: (event: HarnessV1StreamPart) => void;
|
|
22
|
+
emitToolCallCandidate: (options: { toolCall: ACPToolCall }) => void;
|
|
20
23
|
builtinTools: ReadonlyArray<ACPBuiltinToolMapping>;
|
|
21
24
|
hostToolServerName: string;
|
|
22
25
|
hostTools: ReadonlyArray<HarnessV1BridgeToolWire>;
|
|
@@ -49,7 +52,11 @@ export function createEmitStreamEvent({
|
|
|
49
52
|
}) => void;
|
|
50
53
|
removeHostToolCorrelationInvocation: (options: { token: string }) => void;
|
|
51
54
|
} {
|
|
52
|
-
const translator = createACPStreamTranslator({
|
|
55
|
+
const translator = createACPStreamTranslator({
|
|
56
|
+
emit,
|
|
57
|
+
emitToolCallCandidate,
|
|
58
|
+
builtinTools,
|
|
59
|
+
});
|
|
53
60
|
const correlation = createHostToolCorrelation({
|
|
54
61
|
emitSemanticUpdate: ({ message, rawUpdate }) => {
|
|
55
62
|
translator.update({
|
package/src/v1/bridge/index.ts
CHANGED
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
createACPBridgeError,
|
|
31
31
|
createACPInitializationDiagnostic,
|
|
32
32
|
} from './acp-diagnostics';
|
|
33
|
+
import { monitorACPAgentStderr } from './agent-stderr-monitor';
|
|
33
34
|
import { createEmitStreamEvent } from './create-emit-stream-event';
|
|
34
35
|
import {
|
|
35
36
|
startHostToolRelay,
|
|
@@ -79,6 +80,7 @@ const implementation = await readImplementationDescriptor({
|
|
|
79
80
|
const bridgeConfiguration = await readACPBridgeEnvironment({ env: processEnv });
|
|
80
81
|
|
|
81
82
|
let child: ChildProcessWithoutNullStreams | undefined;
|
|
83
|
+
let agentResponseStreamFailure: Promise<never> | undefined;
|
|
82
84
|
let connection: acp.ClientConnection | undefined;
|
|
83
85
|
let session: ACPActiveSession | undefined;
|
|
84
86
|
let recoveredSessionUpdates:
|
|
@@ -139,6 +141,12 @@ async function runTurn(start: StartMessage, turn: BridgeTurn): Promise<void> {
|
|
|
139
141
|
if (activeSession == null) {
|
|
140
142
|
throw new Error('ACP session initialization did not produce a session.');
|
|
141
143
|
}
|
|
144
|
+
const activeAgentResponseStreamFailure = agentResponseStreamFailure;
|
|
145
|
+
if (activeAgentResponseStreamFailure == null) {
|
|
146
|
+
throw new Error(
|
|
147
|
+
'ACP session initialization did not start stderr monitoring.',
|
|
148
|
+
);
|
|
149
|
+
}
|
|
142
150
|
if (start.recoveryMode?.type === 'lossy-rerun') {
|
|
143
151
|
const marker = {
|
|
144
152
|
type: 'acp-recovery',
|
|
@@ -222,6 +230,12 @@ async function runTurn(start: StartMessage, turn: BridgeTurn): Promise<void> {
|
|
|
222
230
|
turn.emit({ type: 'stream-start' });
|
|
223
231
|
const emitStreamEvent = createEmitStreamEvent({
|
|
224
232
|
emit: event => turn.emit(event as BridgeEvent),
|
|
233
|
+
emitToolCallCandidate: ({ toolCall }) => {
|
|
234
|
+
turn.emit({
|
|
235
|
+
type: 'acp-tool-call-candidate',
|
|
236
|
+
toolCall,
|
|
237
|
+
});
|
|
238
|
+
},
|
|
225
239
|
builtinTools: start.builtinTools,
|
|
226
240
|
hostToolServerName: HOST_TOOL_MCP_SERVER_NAME,
|
|
227
241
|
hostTools: start.tools ?? [],
|
|
@@ -260,6 +274,7 @@ async function runTurn(start: StartMessage, turn: BridgeTurn): Promise<void> {
|
|
|
260
274
|
message = await Promise.race([
|
|
261
275
|
activeSession.nextUpdate(),
|
|
262
276
|
cancellationFailure,
|
|
277
|
+
activeAgentResponseStreamFailure,
|
|
263
278
|
]);
|
|
264
279
|
} catch (error) {
|
|
265
280
|
for (const rawValue of streamCapture?.drainRawValues() ?? []) {
|
|
@@ -313,6 +328,7 @@ async function ensureSession({
|
|
|
313
328
|
sessionMeta: bridgeConfiguration.sessionMeta,
|
|
314
329
|
permissionMode: start.permissionMode,
|
|
315
330
|
permissionModeMapping: start.permissionModeMapping,
|
|
331
|
+
mcpServers: start.mcpServers,
|
|
316
332
|
});
|
|
317
333
|
if (session != null) {
|
|
318
334
|
if (catalogRefreshError != null) throw catalogRefreshError;
|
|
@@ -361,7 +377,16 @@ async function ensureSession({
|
|
|
361
377
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
362
378
|
},
|
|
363
379
|
);
|
|
364
|
-
|
|
380
|
+
agentResponseStreamFailure = monitorACPAgentStderr({
|
|
381
|
+
stderr: child.stderr,
|
|
382
|
+
onStderrLine: line => {
|
|
383
|
+
turn.bridgeLog({
|
|
384
|
+
level: 'warn',
|
|
385
|
+
subsystem: 'acp.agent.stderr',
|
|
386
|
+
message: line,
|
|
387
|
+
});
|
|
388
|
+
},
|
|
389
|
+
});
|
|
365
390
|
|
|
366
391
|
const input = Writable.toWeb(child.stdin);
|
|
367
392
|
const output = Readable.toWeb(child.stdout) as ReadableStream<
|
|
@@ -419,6 +444,10 @@ async function ensureSession({
|
|
|
419
444
|
authentication,
|
|
420
445
|
});
|
|
421
446
|
}
|
|
447
|
+
const externalMcpServers = createExternalMcpServers({
|
|
448
|
+
mcpServers: start.mcpServers,
|
|
449
|
+
initialization,
|
|
450
|
+
});
|
|
422
451
|
const tools = start.tools ?? [];
|
|
423
452
|
const catalogPath = `${bridgeStateDir}/host-tools.json`;
|
|
424
453
|
await writeFile(catalogPath, JSON.stringify(tools), { mode: 0o600 });
|
|
@@ -428,6 +457,7 @@ async function ensureSession({
|
|
|
428
457
|
});
|
|
429
458
|
|
|
430
459
|
const mcpServers: acp.McpServer[] = [
|
|
460
|
+
...externalMcpServers,
|
|
431
461
|
{
|
|
432
462
|
name: HOST_TOOL_MCP_SERVER_NAME,
|
|
433
463
|
command: process.execPath,
|
|
@@ -539,6 +569,41 @@ async function ensureSession({
|
|
|
539
569
|
sessionConfigurationFingerprint = fingerprint;
|
|
540
570
|
}
|
|
541
571
|
|
|
572
|
+
function createExternalMcpServers({
|
|
573
|
+
mcpServers,
|
|
574
|
+
initialization,
|
|
575
|
+
}: {
|
|
576
|
+
mcpServers: Record<string, unknown> | undefined;
|
|
577
|
+
initialization: acp.InitializeResponse;
|
|
578
|
+
}): acp.McpServer[] {
|
|
579
|
+
if (mcpServers == null) return [];
|
|
580
|
+
return Object.entries(mcpServers).map(([name, value]) => {
|
|
581
|
+
if (!isRecord(value)) {
|
|
582
|
+
throw new Error(
|
|
583
|
+
`ACP MCP server ${JSON.stringify(name)} must be configured with an object value.`,
|
|
584
|
+
);
|
|
585
|
+
}
|
|
586
|
+
if (value.type === 'acp') {
|
|
587
|
+
throw new HarnessBridgeCapabilityUnsupportedError({
|
|
588
|
+
harnessId: bridgeType,
|
|
589
|
+
message:
|
|
590
|
+
'ACP-transport MCP servers require client-side mcp/connect handling, which this harness does not provide.',
|
|
591
|
+
});
|
|
592
|
+
}
|
|
593
|
+
const mcpCapabilities = initialization.agentCapabilities?.mcpCapabilities;
|
|
594
|
+
if (
|
|
595
|
+
(value.type === 'http' && mcpCapabilities?.http !== true) ||
|
|
596
|
+
(value.type === 'sse' && mcpCapabilities?.sse !== true)
|
|
597
|
+
) {
|
|
598
|
+
throw new HarnessBridgeCapabilityUnsupportedError({
|
|
599
|
+
harnessId: bridgeType,
|
|
600
|
+
message: `The ACP agent does not advertise support for ${value.type.toUpperCase()} MCP servers.`,
|
|
601
|
+
});
|
|
602
|
+
}
|
|
603
|
+
return { ...value, name } as acp.McpServer;
|
|
604
|
+
});
|
|
605
|
+
}
|
|
606
|
+
|
|
542
607
|
function unknownUsage() {
|
|
543
608
|
return {
|
|
544
609
|
inputTokens: {
|
|
@@ -631,39 +696,6 @@ function isRecord(value: unknown): value is Readonly<Record<string, unknown>> {
|
|
|
631
696
|
return value != null && typeof value === 'object' && !Array.isArray(value);
|
|
632
697
|
}
|
|
633
698
|
|
|
634
|
-
function forwardAgentStderr({
|
|
635
|
-
child,
|
|
636
|
-
turn,
|
|
637
|
-
}: {
|
|
638
|
-
child: ChildProcessWithoutNullStreams;
|
|
639
|
-
turn: BridgeTurn;
|
|
640
|
-
}): void {
|
|
641
|
-
void (async () => {
|
|
642
|
-
child.stderr.setEncoding('utf8');
|
|
643
|
-
let pending = '';
|
|
644
|
-
for await (const chunk of child.stderr) {
|
|
645
|
-
pending += chunk;
|
|
646
|
-
const lines = pending.split('\n');
|
|
647
|
-
pending = lines.pop() ?? '';
|
|
648
|
-
for (const line of lines) {
|
|
649
|
-
if (line.length === 0) continue;
|
|
650
|
-
turn.bridgeLog({
|
|
651
|
-
level: 'warn',
|
|
652
|
-
subsystem: 'acp.agent.stderr',
|
|
653
|
-
message: line,
|
|
654
|
-
});
|
|
655
|
-
}
|
|
656
|
-
}
|
|
657
|
-
if (pending.length > 0) {
|
|
658
|
-
turn.bridgeLog({
|
|
659
|
-
level: 'warn',
|
|
660
|
-
subsystem: 'acp.agent.stderr',
|
|
661
|
-
message: pending,
|
|
662
|
-
});
|
|
663
|
-
}
|
|
664
|
-
})();
|
|
665
|
-
}
|
|
666
|
-
|
|
667
699
|
async function readImplementationDescriptor({
|
|
668
700
|
path,
|
|
669
701
|
}: {
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import type { HarnessV1StreamPart } from '@ai-sdk/harness';
|
|
2
2
|
import type { PromptResponse, SessionUpdate } from '@agentclientprotocol/sdk';
|
|
3
|
+
import type {
|
|
4
|
+
ACPToolCall,
|
|
5
|
+
ACPToolCallStatus,
|
|
6
|
+
ACPToolKind,
|
|
7
|
+
} from '../../acp-tool-call';
|
|
3
8
|
import type { ACPBuiltinToolMapping } from '../acp-v1-bridge-protocol';
|
|
4
9
|
|
|
5
10
|
type HarnessUsage = Extract<
|
|
@@ -20,7 +25,6 @@ type ToolState = {
|
|
|
20
25
|
emittedResult: boolean;
|
|
21
26
|
toolName?: string;
|
|
22
27
|
nativeName?: string;
|
|
23
|
-
dynamic?: boolean;
|
|
24
28
|
};
|
|
25
29
|
|
|
26
30
|
export type ACPSessionUpdate = SessionUpdate;
|
|
@@ -28,9 +32,11 @@ export type ACPPromptResponse = PromptResponse;
|
|
|
28
32
|
|
|
29
33
|
export function createACPStreamTranslator({
|
|
30
34
|
emit,
|
|
35
|
+
emitToolCallCandidate,
|
|
31
36
|
builtinTools = [],
|
|
32
37
|
}: {
|
|
33
38
|
emit: (event: HarnessV1StreamPart) => void;
|
|
39
|
+
emitToolCallCandidate?: (options: { toolCall: ACPToolCall }) => void;
|
|
34
40
|
builtinTools?: ReadonlyArray<ACPBuiltinToolMapping>;
|
|
35
41
|
}): {
|
|
36
42
|
update: (
|
|
@@ -228,13 +234,12 @@ export function createACPStreamTranslator({
|
|
|
228
234
|
builtin.nativeName != null && builtin.toolName !== builtin.nativeName
|
|
229
235
|
? builtin.nativeName
|
|
230
236
|
: undefined;
|
|
231
|
-
state.dynamic = false;
|
|
232
237
|
} else {
|
|
233
238
|
state.toolName = createDynamicToolName({
|
|
234
239
|
programmaticName,
|
|
235
240
|
toolCallId: state.toolCallId,
|
|
236
241
|
});
|
|
237
|
-
state
|
|
242
|
+
emitToolCallCandidate?.({ toolCall: createACPToolCall({ state }) });
|
|
238
243
|
}
|
|
239
244
|
emit({
|
|
240
245
|
type: 'tool-call',
|
|
@@ -242,7 +247,6 @@ export function createACPStreamTranslator({
|
|
|
242
247
|
toolName: state.toolName,
|
|
243
248
|
input: stringifyToolInput({ input: state.values.rawInput }),
|
|
244
249
|
providerExecuted: true,
|
|
245
|
-
...(state.dynamic ? { dynamic: true } : {}),
|
|
246
250
|
...(state.nativeName == null ? {} : { nativeName: state.nativeName }),
|
|
247
251
|
});
|
|
248
252
|
state.emittedCall = true;
|
|
@@ -263,7 +267,6 @@ export function createACPStreamTranslator({
|
|
|
263
267
|
toolName: state.toolName!,
|
|
264
268
|
result: createToolResult({ state }),
|
|
265
269
|
...(state.values.status === 'failed' ? { isError: true } : {}),
|
|
266
|
-
...(state.dynamic ? { dynamic: true } : {}),
|
|
267
270
|
});
|
|
268
271
|
state.emittedResult = true;
|
|
269
272
|
pendingToolCallIds.delete(state.toolCallId);
|
|
@@ -697,6 +700,60 @@ function mergeToolUpdate({
|
|
|
697
700
|
if (programmaticName != null) state.values.name = programmaticName;
|
|
698
701
|
}
|
|
699
702
|
|
|
703
|
+
function createACPToolCall({ state }: { state: ToolState }): ACPToolCall {
|
|
704
|
+
const title = getStringProperty({
|
|
705
|
+
value: state.values,
|
|
706
|
+
property: 'title',
|
|
707
|
+
});
|
|
708
|
+
const kind = state.values.kind;
|
|
709
|
+
const status = state.values.status;
|
|
710
|
+
return {
|
|
711
|
+
toolCallId: state.toolCallId,
|
|
712
|
+
title: title ?? state.toolName ?? `Tool ${state.toolCallId}`,
|
|
713
|
+
...(isACPToolKind(kind) ? { kind } : {}),
|
|
714
|
+
...(isACPToolCallStatus(status) ? { status } : {}),
|
|
715
|
+
...(Array.isArray(state.values.content)
|
|
716
|
+
? { content: state.values.content as ACPToolCall['content'] }
|
|
717
|
+
: {}),
|
|
718
|
+
...(Array.isArray(state.values.locations)
|
|
719
|
+
? { locations: state.values.locations as ACPToolCall['locations'] }
|
|
720
|
+
: {}),
|
|
721
|
+
...(Object.prototype.hasOwnProperty.call(state.values, 'rawInput')
|
|
722
|
+
? { rawInput: state.values.rawInput }
|
|
723
|
+
: {}),
|
|
724
|
+
...(Object.prototype.hasOwnProperty.call(state.values, 'rawOutput')
|
|
725
|
+
? { rawOutput: state.values.rawOutput }
|
|
726
|
+
: {}),
|
|
727
|
+
...(state.values._meta === null || isRecord(state.values._meta)
|
|
728
|
+
? { _meta: state.values._meta }
|
|
729
|
+
: {}),
|
|
730
|
+
};
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
function isACPToolKind(value: unknown): value is ACPToolKind {
|
|
734
|
+
return (
|
|
735
|
+
value === 'read' ||
|
|
736
|
+
value === 'edit' ||
|
|
737
|
+
value === 'delete' ||
|
|
738
|
+
value === 'move' ||
|
|
739
|
+
value === 'search' ||
|
|
740
|
+
value === 'execute' ||
|
|
741
|
+
value === 'think' ||
|
|
742
|
+
value === 'fetch' ||
|
|
743
|
+
value === 'switch_mode' ||
|
|
744
|
+
value === 'other'
|
|
745
|
+
);
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
function isACPToolCallStatus(value: unknown): value is ACPToolCallStatus {
|
|
749
|
+
return (
|
|
750
|
+
value === 'pending' ||
|
|
751
|
+
value === 'in_progress' ||
|
|
752
|
+
value === 'completed' ||
|
|
753
|
+
value === 'failed'
|
|
754
|
+
);
|
|
755
|
+
}
|
|
756
|
+
|
|
700
757
|
function emitFileChanges({
|
|
701
758
|
state,
|
|
702
759
|
emit,
|