@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,97 @@
1
+ import type { ACPProfileValue, ACPSerializableValue } from '../acp-v1-settings';
2
+
3
+ export type ACPGatewayValues = {
4
+ readonly apiKey: string;
5
+ readonly baseUrl: string;
6
+ readonly clientAppName: string;
7
+ readonly clientAppVersion: string;
8
+ };
9
+
10
+ export function resolveACPProfileValue({
11
+ value,
12
+ gateway,
13
+ }: {
14
+ value: ACPProfileValue;
15
+ gateway: ACPGatewayValues;
16
+ }): ACPSerializableValue {
17
+ if (Array.isArray(value)) {
18
+ return value.map(item => resolveACPProfileValue({ value: item, gateway }));
19
+ }
20
+ if (value !== null && typeof value === 'object') {
21
+ if (isValueSource(value)) {
22
+ const sourceValue = resolveSource({ source: value.$source, gateway });
23
+ const resolved = `${value.prefix ?? ''}${sourceValue}`;
24
+ return value.ensureSuffix == null
25
+ ? `${resolved}${value.suffix ?? ''}`
26
+ : ensureSuffix({
27
+ value: resolved,
28
+ suffix: value.ensureSuffix,
29
+ });
30
+ }
31
+ return Object.fromEntries(
32
+ Object.entries(value).map(([key, item]) => [
33
+ key,
34
+ resolveACPProfileValue({ value: item, gateway }),
35
+ ]),
36
+ );
37
+ }
38
+ return value;
39
+ }
40
+
41
+ function isValueSource(value: object): value is {
42
+ readonly $source:
43
+ | 'gateway-api-key'
44
+ | 'gateway-base-url'
45
+ | 'gateway-authorization'
46
+ | 'client-app'
47
+ | 'client-app-name'
48
+ | 'client-app-version';
49
+ readonly prefix?: string;
50
+ readonly suffix?: string;
51
+ readonly ensureSuffix?: string;
52
+ } {
53
+ return (
54
+ '$source' in value &&
55
+ typeof (value as { $source?: unknown }).$source === 'string'
56
+ );
57
+ }
58
+
59
+ function ensureSuffix({
60
+ value,
61
+ suffix,
62
+ }: {
63
+ value: string;
64
+ suffix: string;
65
+ }): string {
66
+ const normalized = value.replace(/\/+$/, '');
67
+ return normalized.endsWith(suffix) ? normalized : `${normalized}${suffix}`;
68
+ }
69
+
70
+ function resolveSource({
71
+ source,
72
+ gateway,
73
+ }: {
74
+ source:
75
+ | 'gateway-api-key'
76
+ | 'gateway-base-url'
77
+ | 'gateway-authorization'
78
+ | 'client-app'
79
+ | 'client-app-name'
80
+ | 'client-app-version';
81
+ gateway: ACPGatewayValues;
82
+ }): string {
83
+ switch (source) {
84
+ case 'gateway-api-key':
85
+ return gateway.apiKey;
86
+ case 'gateway-base-url':
87
+ return gateway.baseUrl;
88
+ case 'gateway-authorization':
89
+ return `Bearer ${gateway.apiKey}`;
90
+ case 'client-app':
91
+ return `${gateway.clientAppName}/${gateway.clientAppVersion}`;
92
+ case 'client-app-name':
93
+ return gateway.clientAppName;
94
+ case 'client-app-version':
95
+ return gateway.clientAppVersion;
96
+ }
97
+ }
@@ -0,0 +1,146 @@
1
+ import type { ACPResolvedProviderAuthentication } from './acp-v1-bridge-environment';
2
+ import type { ACPClientApp } from '../../acp-auth';
3
+ import type { ACPAuthentication } from '../acp-v1-settings';
4
+ import {
5
+ resolveACPProfileValue,
6
+ type ACPGatewayValues,
7
+ } from './profile-values';
8
+
9
+ export type ACPInitializeResult = {
10
+ readonly protocolVersion: number;
11
+ readonly authMethods?: ReadonlyArray<{ readonly id: string }> | null;
12
+ readonly agentCapabilities?: Readonly<Record<string, unknown>> | null;
13
+ };
14
+
15
+ export function createACPInitializeRequest({
16
+ protocolVersion,
17
+ clientApp,
18
+ authentication,
19
+ supportsBooleanSessionConfigOptions = false,
20
+ }: {
21
+ protocolVersion: number;
22
+ clientApp: ACPClientApp;
23
+ authentication: ACPAuthentication | undefined;
24
+ supportsBooleanSessionConfigOptions?: boolean;
25
+ }): {
26
+ readonly protocolVersion: number;
27
+ readonly clientInfo: {
28
+ readonly name: string;
29
+ readonly version: string;
30
+ };
31
+ readonly clientCapabilities: Readonly<Record<string, unknown>>;
32
+ } {
33
+ let clientCapabilities: Readonly<Record<string, unknown>> =
34
+ authentication?.clientCapabilities ?? {};
35
+ if (supportsBooleanSessionConfigOptions) {
36
+ clientCapabilities = mergeRecords({
37
+ left: clientCapabilities,
38
+ right: {
39
+ session: {
40
+ configOptions: {
41
+ boolean: {},
42
+ },
43
+ },
44
+ },
45
+ });
46
+ }
47
+ return {
48
+ protocolVersion,
49
+ clientInfo: clientApp,
50
+ clientCapabilities,
51
+ };
52
+ }
53
+
54
+ export function resolveACPLaunchEnvironment({
55
+ providerAuthentication,
56
+ gateway,
57
+ }: {
58
+ providerAuthentication: ACPResolvedProviderAuthentication | undefined;
59
+ gateway: ACPGatewayValues | undefined;
60
+ }): Record<string, string> {
61
+ if (providerAuthentication?.type !== 'ai-gateway') {
62
+ return {};
63
+ }
64
+ const resolved = asRecord(
65
+ resolveACPProfileValue({
66
+ value: providerAuthentication.env,
67
+ gateway: requireGateway({ gateway }),
68
+ }),
69
+ );
70
+ return Object.fromEntries(
71
+ Object.entries(resolved).map(([key, value]) => [
72
+ key,
73
+ typeof value === 'string' ? value : JSON.stringify(value),
74
+ ]),
75
+ );
76
+ }
77
+
78
+ export function validateACPProtocolVersion({
79
+ requested,
80
+ initialization,
81
+ }: {
82
+ requested: number;
83
+ initialization: ACPInitializeResult;
84
+ }): void {
85
+ if (initialization.protocolVersion !== requested) {
86
+ throw new Error(
87
+ `ACP protocol negotiation failed: requested v${requested}, agent selected v${initialization.protocolVersion}.`,
88
+ );
89
+ }
90
+ }
91
+
92
+ export function assertACPAuthenticationMethod({
93
+ initialization,
94
+ methodId,
95
+ }: {
96
+ initialization: ACPInitializeResult;
97
+ methodId: string;
98
+ }): void {
99
+ if (!initialization.authMethods?.some(method => method.id === methodId)) {
100
+ const advertised =
101
+ initialization.authMethods?.map(method => method.id).join(', ') || 'none';
102
+ throw new Error(
103
+ `ACP authentication method ${JSON.stringify(methodId)} is not advertised by the agent. Advertised methods: ${advertised}.`,
104
+ );
105
+ }
106
+ }
107
+
108
+ function mergeRecords({
109
+ left,
110
+ right,
111
+ }: {
112
+ left: Readonly<Record<string, unknown>>;
113
+ right: Readonly<Record<string, unknown>>;
114
+ }): Readonly<Record<string, unknown>> {
115
+ const result: Record<string, unknown> = { ...left };
116
+ for (const [key, value] of Object.entries(right)) {
117
+ const previous = result[key];
118
+ result[key] =
119
+ isRecord(previous) && isRecord(value)
120
+ ? mergeRecords({ left: previous, right: value })
121
+ : value;
122
+ }
123
+ return result;
124
+ }
125
+
126
+ function requireGateway({
127
+ gateway,
128
+ }: {
129
+ gateway: ACPGatewayValues | undefined;
130
+ }): ACPGatewayValues {
131
+ if (gateway == null) {
132
+ throw new Error('ACP Gateway profile values are unavailable.');
133
+ }
134
+ return gateway;
135
+ }
136
+
137
+ function asRecord(value: unknown): Readonly<Record<string, unknown>> {
138
+ if (!isRecord(value)) {
139
+ throw new Error('ACP profile data must resolve to an object.');
140
+ }
141
+ return value;
142
+ }
143
+
144
+ function isRecord(value: unknown): value is Readonly<Record<string, unknown>> {
145
+ return value != null && typeof value === 'object' && !Array.isArray(value);
146
+ }
@@ -0,0 +1,145 @@
1
+ import { HarnessBridgeCapabilityUnsupportedError } from '@ai-sdk/harness/bridge';
2
+ import * as acp from '@agentclientprotocol/sdk';
3
+ import type { ACPInitializeResult } from './protocol-configuration';
4
+
5
+ export type ACPActiveSession = {
6
+ readonly sessionId: string;
7
+ readonly newSessionResponse: acp.NewSessionResponse;
8
+ prompt(prompt: Array<acp.ContentBlock>): Promise<acp.PromptResponse>;
9
+ nextUpdate(): Promise<acp.ActiveSessionMessage>;
10
+ dispose(): void;
11
+ };
12
+
13
+ export function assertACPResumeCapability({
14
+ initialization,
15
+ harnessId,
16
+ }: {
17
+ initialization: ACPInitializeResult;
18
+ harnessId: string;
19
+ }): void {
20
+ const sessionCapabilities = initialization.agentCapabilities
21
+ ?.sessionCapabilities as Readonly<Record<string, unknown>> | undefined;
22
+ if (
23
+ sessionCapabilities?.resume == null ||
24
+ sessionCapabilities.resume === false
25
+ ) {
26
+ throw new HarnessBridgeCapabilityUnsupportedError({
27
+ harnessId,
28
+ message:
29
+ 'ACP process-loss rerun requires the agent to advertise sessionCapabilities.resume; a fresh unrelated ACP session will not be created.',
30
+ });
31
+ }
32
+ }
33
+
34
+ export function createACPRecoveredSession({
35
+ agent,
36
+ sessionId,
37
+ restorationResponse,
38
+ updates,
39
+ }: {
40
+ agent: acp.ClientContext;
41
+ sessionId: string;
42
+ restorationResponse: acp.ResumeSessionResponse | acp.LoadSessionResponse;
43
+ updates: ReturnType<typeof createACPRecoveredSessionUpdates>;
44
+ }): ACPActiveSession {
45
+ let disposed = false;
46
+ return {
47
+ sessionId,
48
+ newSessionResponse: {
49
+ sessionId,
50
+ ...restorationResponse,
51
+ },
52
+ prompt: async prompt => {
53
+ if (disposed) {
54
+ throw new Error('Recovered ACP session is disposed.');
55
+ }
56
+ updates.clearErrors();
57
+ const response = agent.request<acp.PromptResponse, acp.PromptRequest>(
58
+ acp.methods.agent.session.prompt,
59
+ { sessionId, prompt },
60
+ );
61
+ void response.then(
62
+ value => {
63
+ updates.enqueue({
64
+ kind: 'stop',
65
+ response: value,
66
+ stopReason: value.stopReason,
67
+ });
68
+ },
69
+ error => updates.reject(error),
70
+ );
71
+ return response;
72
+ },
73
+ nextUpdate: () => updates.next(),
74
+ dispose: () => {
75
+ if (disposed) return;
76
+ disposed = true;
77
+ updates.fail(new Error('Recovered ACP session disposed.'));
78
+ },
79
+ };
80
+ }
81
+
82
+ export function createACPRecoveredSessionUpdates(): {
83
+ enqueue(value: acp.ActiveSessionMessage): void;
84
+ reject(error: unknown): void;
85
+ clearErrors(): void;
86
+ fail(error: unknown): void;
87
+ next(): Promise<acp.ActiveSessionMessage>;
88
+ } {
89
+ type Entry =
90
+ | { readonly type: 'value'; readonly value: acp.ActiveSessionMessage }
91
+ | { readonly type: 'error'; readonly error: unknown };
92
+ const values: Entry[] = [];
93
+ const waiters: Array<{
94
+ resolve(value: acp.ActiveSessionMessage): void;
95
+ reject(error: unknown): void;
96
+ }> = [];
97
+ let failure: unknown;
98
+ let failed = false;
99
+ return {
100
+ enqueue(value) {
101
+ if (failed) return;
102
+ const waiter = waiters.shift();
103
+ if (waiter == null) values.push({ type: 'value', value });
104
+ else waiter.resolve(value);
105
+ },
106
+ reject(error) {
107
+ if (failed) return;
108
+ const waiter = waiters.shift();
109
+ if (waiter == null) values.push({ type: 'error', error });
110
+ else waiter.reject(error);
111
+ },
112
+ clearErrors() {
113
+ for (let index = values.length - 1; index >= 0; index--) {
114
+ if (values[index]?.type === 'error') values.splice(index, 1);
115
+ }
116
+ },
117
+ fail(error) {
118
+ if (failed) return;
119
+ failed = true;
120
+ failure = error;
121
+ for (const waiter of waiters.splice(0)) waiter.reject(error);
122
+ },
123
+ next() {
124
+ const entry = values.shift();
125
+ if (entry?.type === 'value') return Promise.resolve(entry.value);
126
+ if (entry?.type === 'error') {
127
+ return Promise.reject(
128
+ entry.error instanceof Error
129
+ ? entry.error
130
+ : new Error('Recovered ACP session update failed.'),
131
+ );
132
+ }
133
+ if (failed) {
134
+ return Promise.reject(
135
+ failure instanceof Error
136
+ ? failure
137
+ : new Error('Recovered ACP session update stream failed.'),
138
+ );
139
+ }
140
+ return new Promise((resolve, reject) => {
141
+ waiters.push({ resolve, reject });
142
+ });
143
+ },
144
+ };
145
+ }
@@ -0,0 +1,33 @@
1
+ import type { HarnessV1BridgeToolWire } from '@ai-sdk/harness';
2
+ import { HarnessBridgeCapabilityUnsupportedError } from '@ai-sdk/harness/bridge';
3
+ import type { HostToolRelay } from './host-tool-relay';
4
+
5
+ export async function refreshHostToolCatalog({
6
+ relay,
7
+ tools,
8
+ harnessId,
9
+ timeoutMs,
10
+ }: {
11
+ relay: Pick<HostToolRelay, 'updateCatalog' | 'waitForCatalogRefresh'>;
12
+ tools: ReadonlyArray<HarnessV1BridgeToolWire>;
13
+ harnessId: string;
14
+ timeoutMs: number;
15
+ }): Promise<void> {
16
+ const catalog = relay.updateCatalog({ tools });
17
+ if (!catalog.changed && tools.length === 0) return;
18
+ if (
19
+ await relay.waitForCatalogRefresh({
20
+ revision: catalog.revision,
21
+ timeoutMs,
22
+ })
23
+ ) {
24
+ return;
25
+ }
26
+ throw new HarnessBridgeCapabilityUnsupportedError({
27
+ harnessId,
28
+ message:
29
+ 'The ACP implementation did not load the active harness-owned MCP ' +
30
+ `tool catalog to revision ${catalog.revision} before the next ` +
31
+ 'prompt, so host tools cannot be exposed safely.',
32
+ });
33
+ }
@@ -0,0 +1,83 @@
1
+ import { HarnessBridgeCapabilityUnsupportedError } from '@ai-sdk/harness/bridge';
2
+ import * as acp from '@agentclientprotocol/sdk';
3
+ import type { ACPInitializeResult } from './protocol-configuration';
4
+
5
+ export type ACPSessionRestorationMethod = 'resume' | 'load';
6
+
7
+ export function resolveACPSessionRestorationMethod({
8
+ initialization,
9
+ harnessId,
10
+ }: {
11
+ initialization: ACPInitializeResult;
12
+ harnessId: string;
13
+ }): ACPSessionRestorationMethod {
14
+ const sessionCapabilities = initialization.agentCapabilities
15
+ ?.sessionCapabilities as Readonly<Record<string, unknown>> | undefined;
16
+ if (
17
+ sessionCapabilities?.resume != null &&
18
+ sessionCapabilities.resume !== false
19
+ ) {
20
+ return 'resume';
21
+ }
22
+ if (initialization.agentCapabilities?.loadSession === true) {
23
+ return 'load';
24
+ }
25
+ throw new HarnessBridgeCapabilityUnsupportedError({
26
+ harnessId,
27
+ message:
28
+ 'Cold ACP session restoration requires the agent to advertise sessionCapabilities.resume or loadSession; a fresh unrelated ACP session will not be created.',
29
+ });
30
+ }
31
+
32
+ export async function restoreACPBridgeSession({
33
+ agent,
34
+ initialization,
35
+ sessionId,
36
+ cwd,
37
+ mcpServers,
38
+ meta,
39
+ harnessId,
40
+ setHistoricalUpdatesSuppressed,
41
+ discardCapturedHistory,
42
+ }: {
43
+ agent: acp.ClientContext;
44
+ initialization: ACPInitializeResult;
45
+ sessionId: string;
46
+ cwd: string;
47
+ mcpServers: ReadonlyArray<acp.McpServer>;
48
+ meta: Readonly<Record<string, unknown>> | undefined;
49
+ harnessId: string;
50
+ setHistoricalUpdatesSuppressed(options: { suppressed: boolean }): void;
51
+ discardCapturedHistory(): void;
52
+ }): Promise<{
53
+ method: ACPSessionRestorationMethod;
54
+ response: acp.ResumeSessionResponse | acp.LoadSessionResponse;
55
+ }> {
56
+ const method = resolveACPSessionRestorationMethod({
57
+ initialization,
58
+ harnessId,
59
+ });
60
+ setHistoricalUpdatesSuppressed({ suppressed: true });
61
+ try {
62
+ const request = {
63
+ sessionId,
64
+ cwd,
65
+ mcpServers: [...mcpServers],
66
+ ...(meta == null ? {} : { _meta: meta }),
67
+ };
68
+ const response =
69
+ method === 'resume'
70
+ ? await agent.request<
71
+ acp.ResumeSessionResponse,
72
+ acp.ResumeSessionRequest
73
+ >(acp.methods.agent.session.resume, request)
74
+ : await agent.request<acp.LoadSessionResponse, acp.LoadSessionRequest>(
75
+ acp.methods.agent.session.load,
76
+ request,
77
+ );
78
+ discardCapturedHistory();
79
+ return { method, response };
80
+ } finally {
81
+ setHistoricalUpdatesSuppressed({ suppressed: false });
82
+ }
83
+ }