@ai-sdk/harness-acp 0.0.0 → 1.0.1

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.
Files changed (49) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/LICENSE +13 -0
  3. package/README.md +69 -3
  4. package/dist/bridge/host-tool-mcp.mjs +302 -0
  5. package/dist/bridge/host-tool-mcp.mjs.map +1 -0
  6. package/dist/bridge/index.mjs +3317 -0
  7. package/dist/bridge/index.mjs.map +1 -0
  8. package/dist/bridge/package.json +12 -0
  9. package/dist/bridge/pnpm-lock.yaml +816 -0
  10. package/dist/index.d.ts +104 -0
  11. package/dist/index.js +2089 -0
  12. package/dist/index.js.map +1 -0
  13. package/package.json +64 -10
  14. package/src/acp-auth.ts +254 -0
  15. package/src/acp-bridge-protocol.ts +13 -0
  16. package/src/acp-harness.ts +107 -0
  17. package/src/index.ts +19 -0
  18. package/src/v1/acp-v1-bridge-protocol.ts +151 -0
  19. package/src/v1/acp-v1-harness.ts +1461 -0
  20. package/src/v1/acp-v1-lifecycle.ts +115 -0
  21. package/src/v1/acp-v1-prompt.ts +122 -0
  22. package/src/v1/acp-v1-settings.ts +97 -0
  23. package/src/v1/acp-v1-skills.ts +228 -0
  24. package/src/v1/acp-v1-turn-start-config.ts +93 -0
  25. package/src/v1/bridge/acp-diagnostics.ts +83 -0
  26. package/src/v1/bridge/acp-stream-capture.ts +152 -0
  27. package/src/v1/bridge/acp-v1-bridge-environment.ts +95 -0
  28. package/src/v1/bridge/agent-stderr-monitor.ts +52 -0
  29. package/src/v1/bridge/create-emit-stream-event.ts +97 -0
  30. package/src/v1/bridge/host-tool-correlation.ts +640 -0
  31. package/src/v1/bridge/host-tool-mcp-server.ts +180 -0
  32. package/src/v1/bridge/host-tool-mcp.ts +185 -0
  33. package/src/v1/bridge/host-tool-relay-client.ts +53 -0
  34. package/src/v1/bridge/host-tool-relay.ts +510 -0
  35. package/src/v1/bridge/index.ts +714 -0
  36. package/src/v1/bridge/package.json +12 -0
  37. package/src/v1/bridge/permission-controller.ts +152 -0
  38. package/src/v1/bridge/permission-mode.ts +163 -0
  39. package/src/v1/bridge/pnpm-lock.yaml +816 -0
  40. package/src/v1/bridge/profile-values.ts +97 -0
  41. package/src/v1/bridge/protocol-configuration.ts +146 -0
  42. package/src/v1/bridge/recovered-session.ts +145 -0
  43. package/src/v1/bridge/refresh-host-tool-catalog.ts +33 -0
  44. package/src/v1/bridge/session-lifecycle.ts +83 -0
  45. package/src/v1/bridge/stream-translator.ts +901 -0
  46. package/src/v1/implementation.ts +308 -0
  47. package/src/v1/index.ts +16 -0
  48. package/src/version.ts +6 -0
  49. package/index.js +0 -1
@@ -0,0 +1,83 @@
1
+ import type { InitializeResponse } from '@agentclientprotocol/sdk';
2
+
3
+ export function createACPInitializationDiagnostic({
4
+ initialization,
5
+ sessionId,
6
+ }: {
7
+ initialization: InitializeResponse;
8
+ sessionId: string;
9
+ }): Record<string, unknown> {
10
+ const agentInfo = initialization.agentInfo;
11
+ return {
12
+ protocolVersion: initialization.protocolVersion,
13
+ sessionId,
14
+ agent:
15
+ agentInfo == null
16
+ ? null
17
+ : {
18
+ name: agentInfo.name,
19
+ version: agentInfo.version,
20
+ ...(agentInfo.title == null ? {} : { title: agentInfo.title }),
21
+ },
22
+ capabilities: stripMetadata({
23
+ value: initialization.agentCapabilities ?? {},
24
+ }),
25
+ authMethods: (initialization.authMethods ?? []).map(method => ({
26
+ id: method.id,
27
+ type: 'type' in method ? method.type : 'agent',
28
+ })),
29
+ };
30
+ }
31
+
32
+ export function createACPBridgeError({
33
+ stage,
34
+ cause,
35
+ }: {
36
+ stage:
37
+ | 'session initialization'
38
+ | 'session cancellation'
39
+ | 'prompt update stream';
40
+ cause: unknown;
41
+ }): Error {
42
+ const causeMessage = getErrorMessage({ error: cause });
43
+ const error = new Error(
44
+ causeMessage == null
45
+ ? `ACP ${stage} failed.`
46
+ : `ACP ${stage} failed: ${causeMessage}`,
47
+ );
48
+ (error as Error & { cause?: unknown }).cause = cause;
49
+ return error;
50
+ }
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
+
73
+ function stripMetadata({ value }: { value: unknown }): unknown {
74
+ if (Array.isArray(value)) {
75
+ return value.map(item => stripMetadata({ value: item }));
76
+ }
77
+ if (value == null || typeof value !== 'object') return value;
78
+ const result: Record<string, unknown> = {};
79
+ for (const [key, item] of Object.entries(value)) {
80
+ if (key !== '_meta') result[key] = stripMetadata({ value: item });
81
+ }
82
+ return result;
83
+ }
@@ -0,0 +1,152 @@
1
+ import type {
2
+ AnyMessage,
3
+ SessionUpdate,
4
+ Stream as ACPStream,
5
+ } from '@agentclientprotocol/sdk';
6
+
7
+ const KNOWN_SESSION_UPDATES = new Set([
8
+ 'user_message_chunk',
9
+ 'agent_message_chunk',
10
+ 'agent_thought_chunk',
11
+ 'tool_call',
12
+ 'tool_call_update',
13
+ 'plan',
14
+ 'plan_update',
15
+ 'plan_removed',
16
+ 'available_commands_update',
17
+ 'current_mode_update',
18
+ 'config_option_update',
19
+ 'session_info_update',
20
+ 'usage_update',
21
+ ]);
22
+
23
+ type CapturedSessionUpdate = {
24
+ readonly rawUpdate: unknown;
25
+ readonly forwarded: boolean;
26
+ };
27
+
28
+ export type ACPStreamCapture = {
29
+ takeForUpdate(options: { update: SessionUpdate }): {
30
+ readonly precedingRawValues: ReadonlyArray<unknown>;
31
+ readonly rawUpdate: unknown;
32
+ };
33
+ drainRawValues(): ReadonlyArray<unknown>;
34
+ };
35
+
36
+ export function captureACPStream({ stream }: { stream: ACPStream }): {
37
+ stream: ACPStream;
38
+ capture: ACPStreamCapture;
39
+ } {
40
+ const updates: CapturedSessionUpdate[] = [];
41
+ const readable = stream.readable.pipeThrough(
42
+ new TransformStream<AnyMessage, AnyMessage>({
43
+ transform(message, controller) {
44
+ const rawUpdate = getRawSessionUpdate({ message });
45
+ if (!rawUpdate.isSessionUpdate) {
46
+ controller.enqueue(message);
47
+ return;
48
+ }
49
+
50
+ const sessionUpdate = getStringProperty({
51
+ value: rawUpdate.value,
52
+ property: 'sessionUpdate',
53
+ });
54
+ const forwarded =
55
+ !rawUpdate.canFilterUnknown ||
56
+ sessionUpdate == null ||
57
+ KNOWN_SESSION_UPDATES.has(sessionUpdate);
58
+ updates.push({ rawUpdate: rawUpdate.value, forwarded });
59
+ if (forwarded) controller.enqueue(message);
60
+ },
61
+ }),
62
+ );
63
+
64
+ return {
65
+ stream: { ...stream, readable },
66
+ capture: {
67
+ takeForUpdate: ({ update }) => {
68
+ const index = updates.findIndex(
69
+ candidate =>
70
+ candidate.forwarded &&
71
+ sessionUpdatesMatch({
72
+ rawUpdate: candidate.rawUpdate,
73
+ update,
74
+ }),
75
+ );
76
+ if (index === -1) {
77
+ return { precedingRawValues: [], rawUpdate: update };
78
+ }
79
+ const consumed = updates.splice(0, index + 1);
80
+ return {
81
+ precedingRawValues: consumed
82
+ .slice(0, -1)
83
+ .map(candidate => candidate.rawUpdate),
84
+ rawUpdate: consumed.at(-1)!.rawUpdate,
85
+ };
86
+ },
87
+ drainRawValues: () =>
88
+ updates.splice(0).map(candidate => candidate.rawUpdate),
89
+ },
90
+ };
91
+ }
92
+
93
+ function getRawSessionUpdate({ message }: { message: AnyMessage }):
94
+ | { readonly isSessionUpdate: false }
95
+ | {
96
+ readonly isSessionUpdate: true;
97
+ readonly value: unknown;
98
+ readonly canFilterUnknown: boolean;
99
+ } {
100
+ const record = message as unknown as Record<string, unknown>;
101
+ if (record.method !== 'session/update' || !isRecord(record.params)) {
102
+ return { isSessionUpdate: false };
103
+ }
104
+ const params = record.params;
105
+ return {
106
+ isSessionUpdate: true,
107
+ value: params.update,
108
+ canFilterUnknown:
109
+ typeof params.sessionId === 'string' && isRecord(params.update),
110
+ };
111
+ }
112
+
113
+ function sessionUpdatesMatch({
114
+ rawUpdate,
115
+ update,
116
+ }: {
117
+ rawUpdate: unknown;
118
+ update: SessionUpdate;
119
+ }): boolean {
120
+ if (!isRecord(rawUpdate)) return false;
121
+ if (rawUpdate.sessionUpdate !== update.sessionUpdate) return false;
122
+ if (
123
+ update.sessionUpdate === 'tool_call' ||
124
+ update.sessionUpdate === 'tool_call_update'
125
+ ) {
126
+ return rawUpdate.toolCallId === update.toolCallId;
127
+ }
128
+ if (
129
+ update.sessionUpdate === 'user_message_chunk' ||
130
+ update.sessionUpdate === 'agent_message_chunk' ||
131
+ update.sessionUpdate === 'agent_thought_chunk'
132
+ ) {
133
+ return rawUpdate.messageId === update.messageId;
134
+ }
135
+ return true;
136
+ }
137
+
138
+ function getStringProperty({
139
+ value,
140
+ property,
141
+ }: {
142
+ value: unknown;
143
+ property: string;
144
+ }): string | undefined {
145
+ if (!isRecord(value)) return undefined;
146
+ const propertyValue = value[property];
147
+ return typeof propertyValue === 'string' ? propertyValue : undefined;
148
+ }
149
+
150
+ function isRecord(value: unknown): value is Record<string, unknown> {
151
+ return value != null && typeof value === 'object' && !Array.isArray(value);
152
+ }
@@ -0,0 +1,95 @@
1
+ import { z } from 'zod/v4';
2
+ import type {
3
+ ACPAuthentication,
4
+ ACPProfileValue,
5
+ ACPSerializableValue,
6
+ } from '../acp-v1-settings';
7
+
8
+ export const ACP_BRIDGE_CONFIGURATION_ENV = 'AI_SDK_ACP_BRIDGE_CONFIGURATION';
9
+
10
+ const serializableValueSchema: z.ZodType<ACPSerializableValue> = z.json();
11
+
12
+ const profileValueSchema: z.ZodType<ACPProfileValue> = z.lazy(() =>
13
+ z.union([
14
+ z.string(),
15
+ z.number(),
16
+ z.boolean(),
17
+ z.null(),
18
+ z.object({
19
+ $source: z.enum([
20
+ 'gateway-api-key',
21
+ 'gateway-base-url',
22
+ 'gateway-authorization',
23
+ 'client-app',
24
+ 'client-app-name',
25
+ 'client-app-version',
26
+ ]),
27
+ prefix: z.string().optional(),
28
+ suffix: z.string().optional(),
29
+ ensureSuffix: z.string().optional(),
30
+ }),
31
+ z.array(profileValueSchema),
32
+ z.record(z.string(), profileValueSchema),
33
+ ]),
34
+ );
35
+
36
+ const serializableRecordSchema = z.record(z.string(), serializableValueSchema);
37
+ const profileRecordSchema = z.record(z.string(), profileValueSchema);
38
+
39
+ const authenticationSchema: z.ZodType<ACPAuthentication> = z.object({
40
+ methodId: z.string(),
41
+ meta: serializableRecordSchema.optional(),
42
+ clientCapabilities: serializableRecordSchema.optional(),
43
+ });
44
+
45
+ const providerAuthenticationSchema = z.discriminatedUnion('type', [
46
+ z.object({ type: z.literal('direct') }),
47
+ z.object({
48
+ type: z.literal('ai-gateway'),
49
+ env: profileRecordSchema,
50
+ }),
51
+ ]);
52
+
53
+ export type ACPResolvedProviderAuthentication = z.infer<
54
+ typeof providerAuthenticationSchema
55
+ >;
56
+
57
+ export type ACPBridgeConfiguration = {
58
+ readonly authentication?: ACPAuthentication;
59
+ readonly providerAuthentication?: ACPResolvedProviderAuthentication;
60
+ readonly sessionMeta?: Readonly<Record<string, ACPSerializableValue>>;
61
+ };
62
+
63
+ const bridgeConfigurationSchema: z.ZodType<ACPBridgeConfiguration> = z.object({
64
+ authentication: authenticationSchema.optional(),
65
+ providerAuthentication: providerAuthenticationSchema.optional(),
66
+ sessionMeta: serializableRecordSchema.optional(),
67
+ });
68
+
69
+ export function createACPBridgeEnvironment({
70
+ authentication,
71
+ providerAuthentication,
72
+ sessionMeta,
73
+ }: ACPBridgeConfiguration): Record<string, string> {
74
+ return {
75
+ [ACP_BRIDGE_CONFIGURATION_ENV]: JSON.stringify({
76
+ ...(authentication == null ? {} : { authentication }),
77
+ ...(providerAuthentication == null ? {} : { providerAuthentication }),
78
+ ...(sessionMeta == null ? {} : { sessionMeta }),
79
+ }),
80
+ };
81
+ }
82
+
83
+ export async function readACPBridgeEnvironment({
84
+ env,
85
+ }: {
86
+ env: Readonly<Record<string, string | undefined>>;
87
+ }): Promise<ACPBridgeConfiguration> {
88
+ const serialized = env[ACP_BRIDGE_CONFIGURATION_ENV];
89
+ if (serialized == null) return {};
90
+ try {
91
+ const result = bridgeConfigurationSchema.safeParse(JSON.parse(serialized));
92
+ if (result.success) return result.data;
93
+ } catch {}
94
+ throw new Error('ACP bridge configuration environment is invalid.');
95
+ }
@@ -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
+ }
@@ -0,0 +1,97 @@
1
+ import type {
2
+ ActiveSessionMessage,
3
+ ToolCallUpdate,
4
+ } from '@agentclientprotocol/sdk';
5
+ import type {
6
+ HarnessV1BridgeToolWire,
7
+ HarnessV1StreamPart,
8
+ } from '@ai-sdk/harness';
9
+ import type { ACPBuiltinToolMapping } from '../acp-v1-bridge-protocol';
10
+ import { createACPStreamTranslator } from './stream-translator';
11
+ import { createHostToolCorrelation } from './host-tool-correlation';
12
+
13
+ export function createEmitStreamEvent({
14
+ emit,
15
+ builtinTools,
16
+ hostToolServerName,
17
+ hostTools,
18
+ }: {
19
+ emit: (event: HarnessV1StreamPart) => void;
20
+ builtinTools: ReadonlyArray<ACPBuiltinToolMapping>;
21
+ hostToolServerName: string;
22
+ hostTools: ReadonlyArray<HarnessV1BridgeToolWire>;
23
+ }): {
24
+ message: (options: {
25
+ message: ActiveSessionMessage;
26
+ rawUpdate?: unknown;
27
+ }) => boolean;
28
+ raw: (options: { rawValue: unknown }) => void;
29
+ close: () => void;
30
+ permissionToolCall: (options: { toolCall: ToolCallUpdate }) => void;
31
+ claimHostToolPermission: (options: { toolCall: ToolCallUpdate }) => boolean;
32
+ hostToolCall: (options: {
33
+ toolCallId: string;
34
+ toolName: string;
35
+ input: Readonly<Record<string, unknown>>;
36
+ }) => void;
37
+ hostToolResult: (options: {
38
+ toolCallId: string;
39
+ toolName: string;
40
+ output: unknown;
41
+ isError?: boolean;
42
+ }) => void;
43
+ registerHostToolCorrelationInvocation: (options: {
44
+ token: string;
45
+ serverName: string;
46
+ toolName: string;
47
+ input: Readonly<Record<string, unknown>>;
48
+ order: number;
49
+ }) => void;
50
+ removeHostToolCorrelationInvocation: (options: { token: string }) => void;
51
+ } {
52
+ const translator = createACPStreamTranslator({ emit, builtinTools });
53
+ const correlation = createHostToolCorrelation({
54
+ emitSemanticUpdate: ({ message, rawUpdate }) => {
55
+ translator.update({
56
+ update: message.update,
57
+ rawUpdate,
58
+ preserveRaw: false,
59
+ });
60
+ },
61
+ emitRawUpdate: ({ rawUpdate }) => {
62
+ translator.raw({ rawValue: rawUpdate });
63
+ },
64
+ hostToolServerName,
65
+ hostTools,
66
+ });
67
+
68
+ return {
69
+ message: ({ message, rawUpdate }) => {
70
+ if (message.kind === 'stop') {
71
+ correlation.close();
72
+ translator.finish(message.response);
73
+ return true;
74
+ }
75
+ correlation.update({ message, rawUpdate });
76
+ return false;
77
+ },
78
+ raw: translator.raw,
79
+ close: () => {
80
+ correlation.close();
81
+ translator.close();
82
+ },
83
+ permissionToolCall: ({ toolCall }) => {
84
+ translator.permissionToolCall({
85
+ toolCall: {
86
+ sessionUpdate: 'tool_call_update',
87
+ ...toolCall,
88
+ },
89
+ });
90
+ },
91
+ claimHostToolPermission: correlation.claimHostToolPermission,
92
+ hostToolCall: translator.hostToolCall,
93
+ hostToolResult: translator.hostToolResult,
94
+ registerHostToolCorrelationInvocation: correlation.registerInvocation,
95
+ removeHostToolCorrelationInvocation: correlation.removeInvocation,
96
+ };
97
+ }