@ai-sdk/harness-acp 0.0.0 → 1.0.0
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 +13 -0
- package/LICENSE +13 -0
- package/README.md +70 -3
- package/dist/bridge/host-tool-mcp.mjs +302 -0
- package/dist/bridge/host-tool-mcp.mjs.map +1 -0
- package/dist/bridge/index.mjs +3278 -0
- package/dist/bridge/index.mjs.map +1 -0
- package/dist/bridge/package.json +12 -0
- package/dist/bridge/pnpm-lock.yaml +816 -0
- package/dist/index.d.ts +98 -0
- package/dist/index.js +2094 -0
- package/dist/index.js.map +1 -0
- package/package.json +64 -10
- package/src/acp-auth.ts +254 -0
- package/src/acp-bridge-protocol.ts +13 -0
- package/src/acp-harness.ts +160 -0
- package/src/index.ts +19 -0
- package/src/v1/acp-v1-bridge-protocol.ts +151 -0
- package/src/v1/acp-v1-harness.ts +1447 -0
- package/src/v1/acp-v1-lifecycle.ts +115 -0
- package/src/v1/acp-v1-prompt.ts +122 -0
- package/src/v1/acp-v1-settings.ts +98 -0
- package/src/v1/acp-v1-skills.ts +228 -0
- package/src/v1/acp-v1-turn-start-config.ts +93 -0
- package/src/v1/bridge/acp-diagnostics.ts +57 -0
- package/src/v1/bridge/acp-stream-capture.ts +152 -0
- package/src/v1/bridge/acp-v1-bridge-environment.ts +95 -0
- package/src/v1/bridge/create-emit-stream-event.ts +97 -0
- package/src/v1/bridge/host-tool-correlation.ts +640 -0
- package/src/v1/bridge/host-tool-mcp-server.ts +180 -0
- package/src/v1/bridge/host-tool-mcp.ts +185 -0
- package/src/v1/bridge/host-tool-relay-client.ts +53 -0
- package/src/v1/bridge/host-tool-relay.ts +510 -0
- package/src/v1/bridge/index.ts +729 -0
- package/src/v1/bridge/package.json +12 -0
- package/src/v1/bridge/permission-controller.ts +152 -0
- package/src/v1/bridge/permission-mode.ts +163 -0
- package/src/v1/bridge/pnpm-lock.yaml +816 -0
- package/src/v1/bridge/profile-values.ts +97 -0
- package/src/v1/bridge/protocol-configuration.ts +146 -0
- package/src/v1/bridge/recovered-session.ts +145 -0
- package/src/v1/bridge/refresh-host-tool-catalog.ts +33 -0
- package/src/v1/bridge/session-lifecycle.ts +83 -0
- package/src/v1/bridge/stream-translator.ts +901 -0
- package/src/v1/implementation.ts +282 -0
- package/src/v1/index.ts +16 -0
- package/src/version.ts +6 -0
- package/index.js +0 -1
|
@@ -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,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
|
+
}
|