@automatalabs/acp-server 0.0.0 → 0.2.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/LICENSE +202 -0
- package/README.md +357 -2
- package/dist/backends.d.ts +15 -0
- package/dist/backends.d.ts.map +1 -0
- package/dist/backends.js +30 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +114 -0
- package/dist/http-server.d.ts +41 -0
- package/dist/http-server.d.ts.map +1 -0
- package/dist/http-server.js +200 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -0
- package/dist/protocol.d.ts +54 -0
- package/dist/protocol.d.ts.map +1 -0
- package/dist/protocol.js +138 -0
- package/dist/raw-rpc.d.ts +30 -0
- package/dist/raw-rpc.d.ts.map +1 -0
- package/dist/raw-rpc.js +127 -0
- package/dist/server.d.ts +18 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +372 -0
- package/package.json +31 -7
package/dist/cli.js
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Keep stdout reserved for ACP before importing modules that may log during evaluation.
|
|
3
|
+
if (process.argv.includes("--version") || process.argv.includes("-v")) {
|
|
4
|
+
const manifest = await import("../package.json", { with: { type: "json" } }).then((module) => module.default);
|
|
5
|
+
process.stdout.write(`${manifest.version}\n`);
|
|
6
|
+
process.exit(0);
|
|
7
|
+
}
|
|
8
|
+
if (process.argv.includes("--help") || process.argv.includes("-h")) {
|
|
9
|
+
process.stdout.write(`Usage:
|
|
10
|
+
agentprism-acp-server
|
|
11
|
+
agentprism-acp-server --http [--host <host>] [--port <port>] [--path <path>]
|
|
12
|
+
|
|
13
|
+
With no transport flag, the server speaks ACP over stdio. --http starts both
|
|
14
|
+
Streamable HTTP and WebSocket transports on one endpoint (default:
|
|
15
|
+
http://127.0.0.1:7331/acp and ws://127.0.0.1:7331/acp).
|
|
16
|
+
`);
|
|
17
|
+
process.exit(0);
|
|
18
|
+
}
|
|
19
|
+
console.log = console.error;
|
|
20
|
+
console.info = console.error;
|
|
21
|
+
console.warn = console.error;
|
|
22
|
+
console.debug = console.error;
|
|
23
|
+
process.on("unhandledRejection", (reason) => {
|
|
24
|
+
console.error("unhandledRejection:", reason);
|
|
25
|
+
});
|
|
26
|
+
const abortController = new AbortController();
|
|
27
|
+
process.once("SIGTERM", () => abortController.abort(new Error("SIGTERM")));
|
|
28
|
+
process.once("SIGINT", () => abortController.abort(new Error("SIGINT")));
|
|
29
|
+
try {
|
|
30
|
+
const options = parseCliOptions(process.argv.slice(2));
|
|
31
|
+
if (options.mode === "stdio") {
|
|
32
|
+
const { serveAcpServer } = await import("./server.js");
|
|
33
|
+
await serveAcpServer({ signal: abortController.signal });
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
const { listenAcpHttpServer } = await import("./http-server.js");
|
|
37
|
+
const server = await listenAcpHttpServer({
|
|
38
|
+
host: options.host,
|
|
39
|
+
port: options.port,
|
|
40
|
+
path: options.path,
|
|
41
|
+
signal: abortController.signal,
|
|
42
|
+
});
|
|
43
|
+
console.error(`ACP Streamable HTTP endpoint listening at ${server.url}`);
|
|
44
|
+
console.error(`ACP WebSocket endpoint listening at ${server.webSocketUrl}`);
|
|
45
|
+
await server.closed;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
if (!abortController.signal.aborted) {
|
|
50
|
+
console.error("ACP server failed:", error);
|
|
51
|
+
process.exitCode = 1;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
function parseCliOptions(args) {
|
|
55
|
+
let http = false;
|
|
56
|
+
let host = "127.0.0.1";
|
|
57
|
+
let port = 7331;
|
|
58
|
+
let path = "/acp";
|
|
59
|
+
let networkOption = false;
|
|
60
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
61
|
+
const argument = args[index];
|
|
62
|
+
if (argument === "--http") {
|
|
63
|
+
http = true;
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if (argument === "--host" || argument === "--port" || argument === "--path") {
|
|
67
|
+
const value = args[index + 1];
|
|
68
|
+
if (value === undefined || value.startsWith("--")) {
|
|
69
|
+
throw new Error(`${argument} requires a value`);
|
|
70
|
+
}
|
|
71
|
+
networkOption = true;
|
|
72
|
+
index += 1;
|
|
73
|
+
if (argument === "--host")
|
|
74
|
+
host = value;
|
|
75
|
+
if (argument === "--path")
|
|
76
|
+
path = value;
|
|
77
|
+
if (argument === "--port")
|
|
78
|
+
port = parsePort(value);
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
if (argument.startsWith("--host=")) {
|
|
82
|
+
host = argument.slice("--host=".length);
|
|
83
|
+
networkOption = true;
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
if (argument.startsWith("--port=")) {
|
|
87
|
+
port = parsePort(argument.slice("--port=".length));
|
|
88
|
+
networkOption = true;
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (argument.startsWith("--path=")) {
|
|
92
|
+
path = argument.slice("--path=".length);
|
|
93
|
+
networkOption = true;
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
throw new Error(`unknown argument ${JSON.stringify(argument)}; run with --help for usage`);
|
|
97
|
+
}
|
|
98
|
+
if (!http) {
|
|
99
|
+
if (networkOption)
|
|
100
|
+
throw new Error("--host, --port, and --path require --http");
|
|
101
|
+
return { mode: "stdio" };
|
|
102
|
+
}
|
|
103
|
+
return { mode: "http", host, port, path };
|
|
104
|
+
}
|
|
105
|
+
function parsePort(value) {
|
|
106
|
+
if (!/^\d+$/.test(value))
|
|
107
|
+
throw new Error("--port must be an integer from 0 through 65535");
|
|
108
|
+
const port = Number(value);
|
|
109
|
+
if (!Number.isSafeInteger(port) || port < 0 || port > 65_535) {
|
|
110
|
+
throw new Error("--port must be an integer from 0 through 65535");
|
|
111
|
+
}
|
|
112
|
+
return port;
|
|
113
|
+
}
|
|
114
|
+
export {};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { CustomBackendConfig } from "@automatalabs/acp-agents";
|
|
2
|
+
import { type BackendTarget } from "./backends.js";
|
|
3
|
+
export declare const DEFAULT_ACP_HTTP_HOST: "127.0.0.1";
|
|
4
|
+
export declare const DEFAULT_ACP_HTTP_PORT: 7331;
|
|
5
|
+
export declare const DEFAULT_ACP_HTTP_PATH: "/acp";
|
|
6
|
+
export interface ListenAcpHttpServerOptions {
|
|
7
|
+
/** Interface to bind. Defaults to loopback. */
|
|
8
|
+
host?: string;
|
|
9
|
+
/** TCP port to bind. Pass zero to allocate an ephemeral port. */
|
|
10
|
+
port?: number;
|
|
11
|
+
/** Exact HTTP and WebSocket endpoint path. Defaults to /acp. */
|
|
12
|
+
path?: string;
|
|
13
|
+
/** Maximum JSON request body accepted by the Streamable HTTP adapter. */
|
|
14
|
+
maxRequestBodyBytes?: number;
|
|
15
|
+
/** Programmatic custom backends merged over AGENTPRISM_BACKENDS. */
|
|
16
|
+
backends?: Record<string, CustomBackendConfig>;
|
|
17
|
+
/** Exact backend targets, primarily for embedding and deterministic tests. */
|
|
18
|
+
targets?: readonly BackendTarget[];
|
|
19
|
+
/** Package version advertised on discovery connections. */
|
|
20
|
+
version?: string;
|
|
21
|
+
/** Stops the listener and every active transport connection when aborted. */
|
|
22
|
+
signal?: AbortSignal;
|
|
23
|
+
}
|
|
24
|
+
export interface AcpHttpServerHandle {
|
|
25
|
+
readonly host: string;
|
|
26
|
+
readonly port: number;
|
|
27
|
+
readonly path: string;
|
|
28
|
+
readonly url: string;
|
|
29
|
+
readonly webSocketUrl: string;
|
|
30
|
+
readonly closed: Promise<void>;
|
|
31
|
+
close(): Promise<void>;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Listen for ACP V1 Streamable HTTP and WebSocket connections on one endpoint.
|
|
35
|
+
*
|
|
36
|
+
* Each accepted transport connection receives an independent connection-pinned router. The
|
|
37
|
+
* experimental SDK surface used here is the official ACP HTTP/WebSocket transport implementation;
|
|
38
|
+
* the router protocol carried over it remains ACP V1.
|
|
39
|
+
*/
|
|
40
|
+
export declare function listenAcpHttpServer(options?: ListenAcpHttpServerOptions): Promise<AcpHttpServerHandle>;
|
|
41
|
+
//# sourceMappingURL=http-server.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"http-server.d.ts","sourceRoot":"","sources":["../src/http-server.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAC;AAEpE,OAAO,EAAyB,KAAK,aAAa,EAAE,MAAM,eAAe,CAAC;AAG1E,eAAO,MAAM,qBAAqB,EAAG,WAAoB,CAAC;AAC1D,eAAO,MAAM,qBAAqB,EAAG,IAAa,CAAC;AACnD,eAAO,MAAM,qBAAqB,EAAG,MAAe,CAAC;AAErD,MAAM,WAAW,0BAA0B;IACzC,+CAA+C;IAC/C,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,iEAAiE;IACjE,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,gEAAgE;IAChE,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,yEAAyE;IACzE,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,oEAAoE;IACpE,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,mBAAmB,CAAC,CAAC;IAC/C,8EAA8E;IAC9E,OAAO,CAAC,EAAE,SAAS,aAAa,EAAE,CAAC;IACnC,2DAA2D;IAC3D,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,6EAA6E;IAC7E,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAED,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/B,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB;AAED;;;;;;GAMG;AACH,wBAAsB,mBAAmB,CACvC,OAAO,GAAE,0BAA+B,GACvC,OAAO,CAAC,mBAAmB,CAAC,CA8H9B"}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { createServer } from "node:http";
|
|
2
|
+
import { AcpServer } from "@agentclientprotocol/sdk/experimental/server";
|
|
3
|
+
import { createNodeHttpHandler, createNodeWebSocketUpgradeHandler, } from "@agentclientprotocol/sdk/experimental/node";
|
|
4
|
+
import { WebSocketServer } from "ws";
|
|
5
|
+
import { resolveBackendTargets } from "./backends.js";
|
|
6
|
+
import { serveAcpServer } from "./server.js";
|
|
7
|
+
export const DEFAULT_ACP_HTTP_HOST = "127.0.0.1";
|
|
8
|
+
export const DEFAULT_ACP_HTTP_PORT = 7331;
|
|
9
|
+
export const DEFAULT_ACP_HTTP_PATH = "/acp";
|
|
10
|
+
/**
|
|
11
|
+
* Listen for ACP V1 Streamable HTTP and WebSocket connections on one endpoint.
|
|
12
|
+
*
|
|
13
|
+
* Each accepted transport connection receives an independent connection-pinned router. The
|
|
14
|
+
* experimental SDK surface used here is the official ACP HTTP/WebSocket transport implementation;
|
|
15
|
+
* the router protocol carried over it remains ACP V1.
|
|
16
|
+
*/
|
|
17
|
+
export async function listenAcpHttpServer(options = {}) {
|
|
18
|
+
options.signal?.throwIfAborted();
|
|
19
|
+
const host = requireHost(options.host ?? DEFAULT_ACP_HTTP_HOST);
|
|
20
|
+
const port = requirePort(options.port ?? DEFAULT_ACP_HTTP_PORT);
|
|
21
|
+
const path = requirePath(options.path ?? DEFAULT_ACP_HTTP_PATH);
|
|
22
|
+
const targets = options.targets
|
|
23
|
+
? [...options.targets]
|
|
24
|
+
: resolveBackendTargets({ backends: options.backends });
|
|
25
|
+
requireUniqueTargets(targets);
|
|
26
|
+
const transportServer = new AcpServer({
|
|
27
|
+
createAgent: () => ({
|
|
28
|
+
connect(stream) {
|
|
29
|
+
// AcpServer exposes a batch-capable transport stream so it can also host draft ACP V2.
|
|
30
|
+
// This router rejects non-V1 initialize requests, after which the SDK guarantees that V1
|
|
31
|
+
// connections contain individual messages only.
|
|
32
|
+
const closed = serveAcpServer({
|
|
33
|
+
stream: stream,
|
|
34
|
+
targets,
|
|
35
|
+
...(options.version === undefined ? {} : { version: options.version }),
|
|
36
|
+
...(options.signal === undefined ? {} : { signal: options.signal }),
|
|
37
|
+
}).catch((error) => {
|
|
38
|
+
// One failed client connection must not become a process-level unhandled rejection or
|
|
39
|
+
// stop the shared listener. AcpServer observes this lifecycle promise to tear down only
|
|
40
|
+
// the affected transport connection.
|
|
41
|
+
console.error("ACP network connection failed:", error);
|
|
42
|
+
});
|
|
43
|
+
return { closed };
|
|
44
|
+
},
|
|
45
|
+
}),
|
|
46
|
+
});
|
|
47
|
+
const httpHandler = createNodeHttpHandler(transportServer, {
|
|
48
|
+
...(options.maxRequestBodyBytes === undefined
|
|
49
|
+
? {}
|
|
50
|
+
: { maxRequestBodyBytes: options.maxRequestBodyBytes }),
|
|
51
|
+
});
|
|
52
|
+
const webSocketServer = new WebSocketServer({ noServer: true });
|
|
53
|
+
const upgradeHandler = createNodeWebSocketUpgradeHandler(transportServer, webSocketServer);
|
|
54
|
+
const httpServer = createServer((request, response) => {
|
|
55
|
+
if (!isAcpPath(request, path)) {
|
|
56
|
+
notFound(response);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
httpHandler(request, response);
|
|
60
|
+
});
|
|
61
|
+
httpServer.on("upgrade", (request, socket, head) => {
|
|
62
|
+
if (!isAcpPath(request, path)) {
|
|
63
|
+
socket.destroy();
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
upgradeHandler(request, socket, head);
|
|
67
|
+
});
|
|
68
|
+
await new Promise((resolve, reject) => {
|
|
69
|
+
const onError = (error) => {
|
|
70
|
+
httpServer.off("listening", onListening);
|
|
71
|
+
reject(error);
|
|
72
|
+
};
|
|
73
|
+
const onListening = () => {
|
|
74
|
+
httpServer.off("error", onError);
|
|
75
|
+
resolve();
|
|
76
|
+
};
|
|
77
|
+
httpServer.once("error", onError);
|
|
78
|
+
httpServer.once("listening", onListening);
|
|
79
|
+
httpServer.listen(port, host);
|
|
80
|
+
});
|
|
81
|
+
const address = httpServer.address();
|
|
82
|
+
if (!isAddressInfo(address)) {
|
|
83
|
+
await closeNodeServer(httpServer).catch(() => undefined);
|
|
84
|
+
await transportServer.close();
|
|
85
|
+
throw new Error("ACP HTTP server did not bind to a TCP address");
|
|
86
|
+
}
|
|
87
|
+
let resolveClosed;
|
|
88
|
+
let rejectClosed;
|
|
89
|
+
const closed = new Promise((resolve, reject) => {
|
|
90
|
+
resolveClosed = resolve;
|
|
91
|
+
rejectClosed = reject;
|
|
92
|
+
});
|
|
93
|
+
closed.catch(() => { });
|
|
94
|
+
let closePromise;
|
|
95
|
+
let runtimeError;
|
|
96
|
+
const close = () => {
|
|
97
|
+
closePromise ??= (async () => {
|
|
98
|
+
options.signal?.removeEventListener("abort", onAbort);
|
|
99
|
+
const stopListening = closeNodeServer(httpServer);
|
|
100
|
+
const results = await Promise.allSettled([
|
|
101
|
+
transportServer.close(),
|
|
102
|
+
stopListening,
|
|
103
|
+
]);
|
|
104
|
+
for (const client of webSocketServer.clients)
|
|
105
|
+
client.terminate();
|
|
106
|
+
await closeWebSocketServer(webSocketServer).catch((error) => {
|
|
107
|
+
results.push({ status: "rejected", reason: error });
|
|
108
|
+
});
|
|
109
|
+
const errors = results
|
|
110
|
+
.filter((result) => result.status === "rejected")
|
|
111
|
+
.map((result) => result.reason);
|
|
112
|
+
if (runtimeError !== undefined)
|
|
113
|
+
errors.unshift(runtimeError);
|
|
114
|
+
if (errors.length > 0)
|
|
115
|
+
throw new AggregateError(errors, "Failed to close ACP HTTP server");
|
|
116
|
+
})();
|
|
117
|
+
closePromise.then(resolveClosed, rejectClosed);
|
|
118
|
+
return closePromise;
|
|
119
|
+
};
|
|
120
|
+
const onAbort = () => {
|
|
121
|
+
void close();
|
|
122
|
+
};
|
|
123
|
+
options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
124
|
+
httpServer.on("error", (error) => {
|
|
125
|
+
runtimeError = error;
|
|
126
|
+
void close();
|
|
127
|
+
});
|
|
128
|
+
if (options.signal?.aborted)
|
|
129
|
+
void close();
|
|
130
|
+
const endpointHost = formatUrlHost(host);
|
|
131
|
+
return {
|
|
132
|
+
host,
|
|
133
|
+
port: address.port,
|
|
134
|
+
path,
|
|
135
|
+
url: `http://${endpointHost}:${address.port}${path}`,
|
|
136
|
+
webSocketUrl: `ws://${endpointHost}:${address.port}${path}`,
|
|
137
|
+
closed,
|
|
138
|
+
close,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
function requireHost(value) {
|
|
142
|
+
const host = value.trim();
|
|
143
|
+
if (host.length === 0)
|
|
144
|
+
throw new TypeError("ACP HTTP host must not be empty");
|
|
145
|
+
return host;
|
|
146
|
+
}
|
|
147
|
+
function requirePort(value) {
|
|
148
|
+
if (!Number.isSafeInteger(value) || value < 0 || value > 65_535) {
|
|
149
|
+
throw new RangeError("ACP HTTP port must be an integer from 0 through 65535");
|
|
150
|
+
}
|
|
151
|
+
return value;
|
|
152
|
+
}
|
|
153
|
+
function requirePath(value) {
|
|
154
|
+
if (!value.startsWith("/") || value.includes("?") || value.includes("#")) {
|
|
155
|
+
throw new TypeError("ACP HTTP path must be an absolute path without a query or fragment");
|
|
156
|
+
}
|
|
157
|
+
const parsed = new URL(value, "http://localhost");
|
|
158
|
+
if (parsed.pathname !== value) {
|
|
159
|
+
throw new TypeError("ACP HTTP path must be a canonical URL path");
|
|
160
|
+
}
|
|
161
|
+
return value;
|
|
162
|
+
}
|
|
163
|
+
function requireUniqueTargets(targets) {
|
|
164
|
+
const ids = new Set(targets.map((target) => target.id));
|
|
165
|
+
if (ids.size !== targets.length)
|
|
166
|
+
throw new Error("ACP backend target ids must be unique");
|
|
167
|
+
}
|
|
168
|
+
function isAcpPath(request, path) {
|
|
169
|
+
return new URL(request.url ?? "/", "http://localhost").pathname === path;
|
|
170
|
+
}
|
|
171
|
+
function notFound(response) {
|
|
172
|
+
response.writeHead(404, { "Content-Type": "text/plain" });
|
|
173
|
+
response.end("Not Found");
|
|
174
|
+
}
|
|
175
|
+
function isAddressInfo(value) {
|
|
176
|
+
return value !== null && typeof value === "object";
|
|
177
|
+
}
|
|
178
|
+
function formatUrlHost(host) {
|
|
179
|
+
return host.includes(":") && !host.startsWith("[") ? `[${host}]` : host;
|
|
180
|
+
}
|
|
181
|
+
function closeNodeServer(server) {
|
|
182
|
+
return new Promise((resolve, reject) => {
|
|
183
|
+
server.close((error) => {
|
|
184
|
+
if (error)
|
|
185
|
+
reject(error);
|
|
186
|
+
else
|
|
187
|
+
resolve();
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
function closeWebSocketServer(server) {
|
|
192
|
+
return new Promise((resolve, reject) => {
|
|
193
|
+
server.close((error) => {
|
|
194
|
+
if (error)
|
|
195
|
+
reject(error);
|
|
196
|
+
else
|
|
197
|
+
resolve();
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export { serveAcpServer } from "./server.js";
|
|
2
|
+
export type { ServeAcpServerOptions } from "./server.js";
|
|
3
|
+
export { DEFAULT_ACP_HTTP_HOST, DEFAULT_ACP_HTTP_PATH, DEFAULT_ACP_HTTP_PORT, listenAcpHttpServer, } from "./http-server.js";
|
|
4
|
+
export type { AcpHttpServerHandle, ListenAcpHttpServerOptions } from "./http-server.js";
|
|
5
|
+
export { resolveBackendTargets } from "./backends.js";
|
|
6
|
+
export type { BackendTarget, ResolveBackendTargetsOptions } from "./backends.js";
|
|
7
|
+
export { ACP_BACKENDS_PROBE_METHOD, ACP_ROUTER_META_NAMESPACE, ACP_ROUTER_VERSION, assertSessionBackend, discoveryInitializeResponse, mergeBackendInitializeResponse, parseProbeBackendsParams, parseRouterInitialize, } from "./protocol.js";
|
|
8
|
+
export type { BackendProbe, ParsedRouterInitialize, ProbeBackendsParams, ProbeBackendsResult, RouterSelection, } from "./protocol.js";
|
|
9
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAC7C,YAAY,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAC;AAEzD,OAAO,EACL,qBAAqB,EACrB,qBAAqB,EACrB,qBAAqB,EACrB,mBAAmB,GACpB,MAAM,kBAAkB,CAAC;AAC1B,YAAY,EAAE,mBAAmB,EAAE,0BAA0B,EAAE,MAAM,kBAAkB,CAAC;AAExF,OAAO,EAAE,qBAAqB,EAAE,MAAM,eAAe,CAAC;AACtD,YAAY,EAAE,aAAa,EAAE,4BAA4B,EAAE,MAAM,eAAe,CAAC;AAEjF,OAAO,EACL,yBAAyB,EACzB,yBAAyB,EACzB,kBAAkB,EAClB,oBAAoB,EACpB,2BAA2B,EAC3B,8BAA8B,EAC9B,wBAAwB,EACxB,qBAAqB,GACtB,MAAM,eAAe,CAAC;AACvB,YAAY,EACV,YAAY,EACZ,sBAAsB,EACtB,mBAAmB,EACnB,mBAAmB,EACnB,eAAe,GAChB,MAAM,eAAe,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { serveAcpServer } from "./server.js";
|
|
2
|
+
export { DEFAULT_ACP_HTTP_HOST, DEFAULT_ACP_HTTP_PATH, DEFAULT_ACP_HTTP_PORT, listenAcpHttpServer, } from "./http-server.js";
|
|
3
|
+
export { resolveBackendTargets } from "./backends.js";
|
|
4
|
+
export { ACP_BACKENDS_PROBE_METHOD, ACP_ROUTER_META_NAMESPACE, ACP_ROUTER_VERSION, assertSessionBackend, discoveryInitializeResponse, mergeBackendInitializeResponse, parseProbeBackendsParams, parseRouterInitialize, } from "./protocol.js";
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { type AgentCapabilities, type Implementation, type InitializeRequest, type InitializeResponse, type McpServer, type SessionConfigOption, type SessionModeState } from "@agentclientprotocol/sdk";
|
|
2
|
+
export declare const ACP_ROUTER_META_NAMESPACE: "@automatalabs/agentprism";
|
|
3
|
+
export declare const ACP_ROUTER_VERSION: 1;
|
|
4
|
+
export declare const ACP_BACKENDS_PROBE_METHOD: "_automatalabs/agentprism/backends/probe";
|
|
5
|
+
export type RouterSelection = {
|
|
6
|
+
readonly version: 1;
|
|
7
|
+
readonly mode: "discovery";
|
|
8
|
+
} | {
|
|
9
|
+
readonly version: 1;
|
|
10
|
+
readonly mode: "backend";
|
|
11
|
+
readonly backend: string;
|
|
12
|
+
};
|
|
13
|
+
export interface ParsedRouterInitialize {
|
|
14
|
+
readonly request: InitializeRequest;
|
|
15
|
+
readonly selection: RouterSelection;
|
|
16
|
+
}
|
|
17
|
+
export interface ProbeBackendsParams {
|
|
18
|
+
cwd: string;
|
|
19
|
+
additionalDirectories?: string[];
|
|
20
|
+
mcpServers: McpServer[];
|
|
21
|
+
_meta?: Record<string, unknown> | null;
|
|
22
|
+
}
|
|
23
|
+
export type BackendProbe = {
|
|
24
|
+
id: string;
|
|
25
|
+
name: string;
|
|
26
|
+
available: true;
|
|
27
|
+
agentInfo?: Implementation | null;
|
|
28
|
+
agentCapabilities?: AgentCapabilities;
|
|
29
|
+
modes?: SessionModeState | null;
|
|
30
|
+
configOptions?: SessionConfigOption[] | null;
|
|
31
|
+
initializeMeta?: Record<string, unknown> | null;
|
|
32
|
+
sessionMeta?: Record<string, unknown> | null;
|
|
33
|
+
} | {
|
|
34
|
+
id: string;
|
|
35
|
+
name: string;
|
|
36
|
+
available: false;
|
|
37
|
+
stage: "initialize" | "session/new";
|
|
38
|
+
error: string;
|
|
39
|
+
};
|
|
40
|
+
export interface ProbeBackendsResult {
|
|
41
|
+
backends: BackendProbe[];
|
|
42
|
+
}
|
|
43
|
+
/** Validate the ACP and AgentPrism portions of the connection's first initialize request. */
|
|
44
|
+
export declare function parseRouterInitialize(params: unknown): ParsedRouterInitialize;
|
|
45
|
+
/** Validate the redundant backend assertion required on every session/new request. */
|
|
46
|
+
export declare function assertSessionBackend(params: unknown, selectedBackend: string): void;
|
|
47
|
+
/** Validate and copy the temporary session inputs accepted by the discovery probe. */
|
|
48
|
+
export declare function parseProbeBackendsParams(params: unknown): ProbeBackendsParams;
|
|
49
|
+
/** Router-owned initialize response for a discovery connection. */
|
|
50
|
+
export declare function discoveryInitializeResponse(version: string): InitializeResponse;
|
|
51
|
+
/** Add the proxy's negotiated capability without changing the selected backend's response fields. */
|
|
52
|
+
export declare function mergeBackendInitializeResponse(response: InitializeResponse, backend: string): InitializeResponse;
|
|
53
|
+
export declare function isRecord(value: unknown): value is Record<string, unknown>;
|
|
54
|
+
//# sourceMappingURL=protocol.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"protocol.d.ts","sourceRoot":"","sources":["../src/protocol.ts"],"names":[],"mappings":"AACA,OAAO,EAGL,KAAK,iBAAiB,EACtB,KAAK,cAAc,EACnB,KAAK,iBAAiB,EACtB,KAAK,kBAAkB,EACvB,KAAK,SAAS,EACd,KAAK,mBAAmB,EACxB,KAAK,gBAAgB,EACtB,MAAM,0BAA0B,CAAC;AAElC,eAAO,MAAM,yBAAyB,EAAG,0BAAmC,CAAC;AAC7E,eAAO,MAAM,kBAAkB,EAAG,CAAU,CAAC;AAC7C,eAAO,MAAM,yBAAyB,EAAG,yCAAkD,CAAC;AAI5F,MAAM,MAAM,eAAe,GACvB;IAAE,QAAQ,CAAC,OAAO,EAAE,CAAC,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAA;CAAE,GACnD;IAAE,QAAQ,CAAC,OAAO,EAAE,CAAC,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC;AAEhF,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,OAAO,EAAE,iBAAiB,CAAC;IACpC,QAAQ,CAAC,SAAS,EAAE,eAAe,CAAC;CACrC;AAED,MAAM,WAAW,mBAAmB;IAClC,GAAG,EAAE,MAAM,CAAC;IACZ,qBAAqB,CAAC,EAAE,MAAM,EAAE,CAAC;IACjC,UAAU,EAAE,SAAS,EAAE,CAAC;IACxB,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;CACxC;AAED,MAAM,MAAM,YAAY,GACpB;IACE,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,IAAI,CAAC;IAChB,SAAS,CAAC,EAAE,cAAc,GAAG,IAAI,CAAC;IAClC,iBAAiB,CAAC,EAAE,iBAAiB,CAAC;IACtC,KAAK,CAAC,EAAE,gBAAgB,GAAG,IAAI,CAAC;IAChC,aAAa,CAAC,EAAE,mBAAmB,EAAE,GAAG,IAAI,CAAC;IAC7C,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IAChD,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;CAC9C,GACD;IACE,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,KAAK,CAAC;IACjB,KAAK,EAAE,YAAY,GAAG,aAAa,CAAC;IACpC,KAAK,EAAE,MAAM,CAAC;CACf,CAAC;AAEN,MAAM,WAAW,mBAAmB;IAClC,QAAQ,EAAE,YAAY,EAAE,CAAC;CAC1B;AAED,6FAA6F;AAC7F,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,OAAO,GAAG,sBAAsB,CA+C7E;AAED,sFAAsF;AACtF,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,OAAO,EAAE,eAAe,EAAE,MAAM,GAAG,IAAI,CAanF;AAED,sFAAsF;AACtF,wBAAgB,wBAAwB,CAAC,MAAM,EAAE,OAAO,GAAG,mBAAmB,CA0B7E;AAED,mEAAmE;AACnE,wBAAgB,2BAA2B,CAAC,OAAO,EAAE,MAAM,GAAG,kBAAkB,CAoB/E;AAED,qGAAqG;AACrG,wBAAgB,8BAA8B,CAC5C,QAAQ,EAAE,kBAAkB,EAC5B,OAAO,EAAE,MAAM,GACd,kBAAkB,CAsBpB;AAED,wBAAgB,QAAQ,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAEzE"}
|
package/dist/protocol.js
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { isAbsolute } from "node:path";
|
|
2
|
+
import { PROTOCOL_VERSION, RequestError, } from "@agentclientprotocol/sdk";
|
|
3
|
+
export const ACP_ROUTER_META_NAMESPACE = "@automatalabs/agentprism";
|
|
4
|
+
export const ACP_ROUTER_VERSION = 1;
|
|
5
|
+
export const ACP_BACKENDS_PROBE_METHOD = "_automatalabs/agentprism/backends/probe";
|
|
6
|
+
const BACKEND_ID_PATTERN = /^[a-z][a-z0-9._-]*$/;
|
|
7
|
+
/** Validate the ACP and AgentPrism portions of the connection's first initialize request. */
|
|
8
|
+
export function parseRouterInitialize(params) {
|
|
9
|
+
const request = requireRecord(params, "initialize params");
|
|
10
|
+
if (request.protocolVersion !== PROTOCOL_VERSION) {
|
|
11
|
+
throw invalidParams(`AgentPrism ACP server supports protocol version ${PROTOCOL_VERSION}`);
|
|
12
|
+
}
|
|
13
|
+
const clientCapabilities = requireRecord(request.clientCapabilities, "clientCapabilities");
|
|
14
|
+
const capabilityMeta = requireRecord(clientCapabilities._meta, "clientCapabilities._meta");
|
|
15
|
+
const capabilityNamespace = requireRecord(capabilityMeta[ACP_ROUTER_META_NAMESPACE], `clientCapabilities._meta[${JSON.stringify(ACP_ROUTER_META_NAMESPACE)}]`);
|
|
16
|
+
const capability = requireRecord(capabilityNamespace.acpRouter, "acpRouter client capability");
|
|
17
|
+
if (!Array.isArray(capability.versions) ||
|
|
18
|
+
!capability.versions.every((value) => Number.isSafeInteger(value)) ||
|
|
19
|
+
!capability.versions.includes(ACP_ROUTER_VERSION)) {
|
|
20
|
+
throw invalidParams(`client must advertise AgentPrism ACP router version ${ACP_ROUTER_VERSION}`);
|
|
21
|
+
}
|
|
22
|
+
const requestMeta = requireRecord(request._meta, "initialize._meta");
|
|
23
|
+
const requestNamespace = requireRecord(requestMeta[ACP_ROUTER_META_NAMESPACE], `initialize._meta[${JSON.stringify(ACP_ROUTER_META_NAMESPACE)}]`);
|
|
24
|
+
const selection = requireRecord(requestNamespace.acpRouter, "initialize acpRouter selection");
|
|
25
|
+
if (selection.version !== ACP_ROUTER_VERSION) {
|
|
26
|
+
throw invalidParams(`initialize must select AgentPrism ACP router version ${ACP_ROUTER_VERSION}`);
|
|
27
|
+
}
|
|
28
|
+
if (selection.mode === "discovery") {
|
|
29
|
+
return { request, selection: { version: ACP_ROUTER_VERSION, mode: "discovery" } };
|
|
30
|
+
}
|
|
31
|
+
if (selection.mode !== "backend") {
|
|
32
|
+
throw invalidParams('initialize acpRouter mode must be "discovery" or "backend"');
|
|
33
|
+
}
|
|
34
|
+
if (typeof selection.backend !== "string" || !BACKEND_ID_PATTERN.test(selection.backend)) {
|
|
35
|
+
throw invalidParams(`initialize acpRouter backend must match ${BACKEND_ID_PATTERN}`);
|
|
36
|
+
}
|
|
37
|
+
return {
|
|
38
|
+
request,
|
|
39
|
+
selection: {
|
|
40
|
+
version: ACP_ROUTER_VERSION,
|
|
41
|
+
mode: "backend",
|
|
42
|
+
backend: selection.backend,
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
/** Validate the redundant backend assertion required on every session/new request. */
|
|
47
|
+
export function assertSessionBackend(params, selectedBackend) {
|
|
48
|
+
const request = requireRecord(params, "session/new params");
|
|
49
|
+
const meta = requireRecord(request._meta, "session/new._meta");
|
|
50
|
+
const namespace = requireRecord(meta[ACP_ROUTER_META_NAMESPACE], `session/new._meta[${JSON.stringify(ACP_ROUTER_META_NAMESPACE)}]`);
|
|
51
|
+
const selection = requireRecord(namespace.acpRouter, "session/new acpRouter selection");
|
|
52
|
+
if (selection.version !== ACP_ROUTER_VERSION || selection.backend !== selectedBackend) {
|
|
53
|
+
throw invalidParams(`session/new must select router version ${ACP_ROUTER_VERSION} and backend ${JSON.stringify(selectedBackend)}`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/** Validate and copy the temporary session inputs accepted by the discovery probe. */
|
|
57
|
+
export function parseProbeBackendsParams(params) {
|
|
58
|
+
const value = requireRecord(params, "probe params");
|
|
59
|
+
if (typeof value.cwd !== "string" || !isAbsolute(value.cwd)) {
|
|
60
|
+
throw invalidParams("probe cwd must be an absolute path");
|
|
61
|
+
}
|
|
62
|
+
if (!Array.isArray(value.mcpServers)) {
|
|
63
|
+
throw invalidParams("probe mcpServers must be an array");
|
|
64
|
+
}
|
|
65
|
+
if (value.additionalDirectories !== undefined &&
|
|
66
|
+
(!Array.isArray(value.additionalDirectories) ||
|
|
67
|
+
!value.additionalDirectories.every((directory) => typeof directory === "string" && isAbsolute(directory)))) {
|
|
68
|
+
throw invalidParams("probe additionalDirectories must contain only absolute paths");
|
|
69
|
+
}
|
|
70
|
+
if (value._meta !== undefined && value._meta !== null && !isRecord(value._meta)) {
|
|
71
|
+
throw invalidParams("probe _meta must be an object or null");
|
|
72
|
+
}
|
|
73
|
+
return {
|
|
74
|
+
cwd: value.cwd,
|
|
75
|
+
mcpServers: value.mcpServers,
|
|
76
|
+
...(value.additionalDirectories === undefined
|
|
77
|
+
? {}
|
|
78
|
+
: { additionalDirectories: [...value.additionalDirectories] }),
|
|
79
|
+
...(value._meta === undefined ? {} : { _meta: value._meta }),
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
/** Router-owned initialize response for a discovery connection. */
|
|
83
|
+
export function discoveryInitializeResponse(version) {
|
|
84
|
+
return {
|
|
85
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
86
|
+
agentInfo: {
|
|
87
|
+
name: "agentprism-acp-server",
|
|
88
|
+
title: "AgentPrism ACP Server",
|
|
89
|
+
version,
|
|
90
|
+
},
|
|
91
|
+
agentCapabilities: {
|
|
92
|
+
_meta: {
|
|
93
|
+
[ACP_ROUTER_META_NAMESPACE]: {
|
|
94
|
+
acpRouter: {
|
|
95
|
+
version: ACP_ROUTER_VERSION,
|
|
96
|
+
mode: "discovery",
|
|
97
|
+
methods: { probeBackends: ACP_BACKENDS_PROBE_METHOD },
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
/** Add the proxy's negotiated capability without changing the selected backend's response fields. */
|
|
105
|
+
export function mergeBackendInitializeResponse(response, backend) {
|
|
106
|
+
const capabilities = response.agentCapabilities ?? {};
|
|
107
|
+
const capabilityMeta = capabilities._meta ?? {};
|
|
108
|
+
const namespaceValue = capabilityMeta[ACP_ROUTER_META_NAMESPACE];
|
|
109
|
+
const namespace = isRecord(namespaceValue) ? namespaceValue : {};
|
|
110
|
+
return {
|
|
111
|
+
...response,
|
|
112
|
+
agentCapabilities: {
|
|
113
|
+
...capabilities,
|
|
114
|
+
_meta: {
|
|
115
|
+
...capabilityMeta,
|
|
116
|
+
[ACP_ROUTER_META_NAMESPACE]: {
|
|
117
|
+
...namespace,
|
|
118
|
+
acpRouter: {
|
|
119
|
+
version: ACP_ROUTER_VERSION,
|
|
120
|
+
mode: "backend",
|
|
121
|
+
backend,
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
export function isRecord(value) {
|
|
129
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
130
|
+
}
|
|
131
|
+
function requireRecord(value, field) {
|
|
132
|
+
if (!isRecord(value))
|
|
133
|
+
throw invalidParams(`${field} must be an object`);
|
|
134
|
+
return value;
|
|
135
|
+
}
|
|
136
|
+
function invalidParams(message) {
|
|
137
|
+
return RequestError.invalidParams(undefined, message);
|
|
138
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { type AnyNotification, type AnyRequest, type Result, type Stream } from "@agentclientprotocol/sdk";
|
|
2
|
+
export interface RawRpcHandler {
|
|
3
|
+
request(message: AnyRequest, signal: AbortSignal): Promise<Result<unknown>> | Result<unknown>;
|
|
4
|
+
notification(message: AnyNotification): Promise<void> | void;
|
|
5
|
+
}
|
|
6
|
+
/** Minimal bidirectional JSON-RPC peer used only by router-owned discovery connections. */
|
|
7
|
+
export declare class RawRpcPeer {
|
|
8
|
+
private readonly handler;
|
|
9
|
+
private readonly reader;
|
|
10
|
+
private readonly writer;
|
|
11
|
+
private readonly pending;
|
|
12
|
+
private readonly incoming;
|
|
13
|
+
private nextRequestId;
|
|
14
|
+
private writeQueue;
|
|
15
|
+
private closeReason;
|
|
16
|
+
readonly closed: Promise<void>;
|
|
17
|
+
constructor(stream: Stream, handler: RawRpcHandler);
|
|
18
|
+
request(method: string, params?: unknown, signal?: AbortSignal): Promise<Result<unknown>>;
|
|
19
|
+
notify(method: string, params?: unknown): Promise<void>;
|
|
20
|
+
close(reason?: unknown): Promise<void>;
|
|
21
|
+
private write;
|
|
22
|
+
private receive;
|
|
23
|
+
private dispatchRequest;
|
|
24
|
+
}
|
|
25
|
+
export declare function errorResponse(error: unknown): {
|
|
26
|
+
code: number;
|
|
27
|
+
message: string;
|
|
28
|
+
data?: unknown;
|
|
29
|
+
};
|
|
30
|
+
//# sourceMappingURL=raw-rpc.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"raw-rpc.d.ts","sourceRoot":"","sources":["../src/raw-rpc.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,KAAK,eAAe,EACpB,KAAK,UAAU,EAEf,KAAK,MAAM,EACX,KAAK,MAAM,EACZ,MAAM,0BAA0B,CAAC;AAElC,MAAM,WAAW,aAAa;IAC5B,OAAO,CAAC,OAAO,EAAE,UAAU,EAAE,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,GAAG,MAAM,CAAC,OAAO,CAAC,CAAC;IAC9F,YAAY,CAAC,OAAO,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;CAC9D;AAOD,2FAA2F;AAC3F,qBAAa,UAAU;IAUO,OAAO,CAAC,QAAQ,CAAC,OAAO;IATpD,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA0C;IACjE,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA0C;IACjE,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAwC;IAChE,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAyC;IAClE,OAAO,CAAC,aAAa,CAAK;IAC1B,OAAO,CAAC,UAAU,CAAoC;IACtD,OAAO,CAAC,WAAW,CAAU;IAC7B,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC;gBAEnB,MAAM,EAAE,MAAM,EAAmB,OAAO,EAAE,aAAa;IAanE,OAAO,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;IAuBzF,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC;IAQjD,KAAK,CAAC,MAAM,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC;IAY5C,OAAO,CAAC,KAAK;YAMC,OAAO;IA6BrB,OAAO,CAAC,eAAe;CAUxB;AAED,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,OAAO,CAAA;CAAE,CAG/F"}
|