@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,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
|
+
}
|