@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.
@@ -0,0 +1,127 @@
1
+ import { RequestError, } from "@agentclientprotocol/sdk";
2
+ /** Minimal bidirectional JSON-RPC peer used only by router-owned discovery connections. */
3
+ export class RawRpcPeer {
4
+ handler;
5
+ reader;
6
+ writer;
7
+ pending = new Map();
8
+ incoming = new Map();
9
+ nextRequestId = 0;
10
+ writeQueue = Promise.resolve();
11
+ closeReason;
12
+ closed;
13
+ constructor(stream, handler) {
14
+ this.handler = handler;
15
+ this.reader = stream.readable.getReader();
16
+ this.writer = stream.writable.getWriter();
17
+ this.closed = this.receive().finally(() => {
18
+ const reason = this.closeReason ?? new Error("ACP JSON-RPC connection closed");
19
+ for (const pending of this.pending.values())
20
+ pending.reject(reason);
21
+ this.pending.clear();
22
+ for (const controller of this.incoming.values())
23
+ controller.abort(reason);
24
+ this.incoming.clear();
25
+ });
26
+ this.closed.catch(() => { });
27
+ }
28
+ request(method, params, signal) {
29
+ if (signal?.aborted)
30
+ return Promise.reject(signal.reason);
31
+ const id = `agentprism-${(this.nextRequestId += 1)}`;
32
+ const message = {
33
+ jsonrpc: "2.0",
34
+ id,
35
+ method,
36
+ ...(params === undefined ? {} : { params }),
37
+ };
38
+ const onAbort = () => {
39
+ void this.notify("$/cancel_request", { requestId: id });
40
+ };
41
+ signal?.addEventListener("abort", onAbort, { once: true });
42
+ return new Promise((resolve, reject) => {
43
+ this.pending.set(id, { resolve, reject });
44
+ void this.write(message).catch((error) => {
45
+ this.pending.delete(id);
46
+ reject(error);
47
+ });
48
+ }).finally(() => signal?.removeEventListener("abort", onAbort));
49
+ }
50
+ notify(method, params) {
51
+ return this.write({
52
+ jsonrpc: "2.0",
53
+ method,
54
+ ...(params === undefined ? {} : { params }),
55
+ });
56
+ }
57
+ async close(reason) {
58
+ this.closeReason = reason;
59
+ await Promise.allSettled([
60
+ this.reader.cancel(reason),
61
+ this.writeQueue.then(() => this.writer.close()),
62
+ ]);
63
+ await this.closed.catch(() => { });
64
+ try {
65
+ this.writer.releaseLock();
66
+ }
67
+ catch { }
68
+ }
69
+ write(message) {
70
+ const operation = this.writeQueue.then(() => this.writer.write(message));
71
+ this.writeQueue = operation.catch(() => { });
72
+ return operation;
73
+ }
74
+ async receive() {
75
+ try {
76
+ while (true) {
77
+ const item = await this.reader.read();
78
+ if (item.done)
79
+ return;
80
+ const message = item.value;
81
+ if ("result" in message || "error" in message) {
82
+ const pending = this.pending.get(message.id);
83
+ if (!pending)
84
+ continue;
85
+ this.pending.delete(message.id);
86
+ pending.resolve("result" in message ? { result: message.result } : { error: message.error });
87
+ continue;
88
+ }
89
+ if (!("id" in message)) {
90
+ if (message.method === "$/cancel_request") {
91
+ const requestId = cancelRequestId(message.params);
92
+ if (requestId !== undefined)
93
+ this.incoming.get(requestId)?.abort(RequestError.requestCancelled());
94
+ continue;
95
+ }
96
+ void Promise.resolve(this.handler.notification(message)).catch(() => { });
97
+ continue;
98
+ }
99
+ this.dispatchRequest(message);
100
+ }
101
+ }
102
+ finally {
103
+ this.reader.releaseLock();
104
+ }
105
+ }
106
+ dispatchRequest(message) {
107
+ const controller = new AbortController();
108
+ this.incoming.set(message.id, controller);
109
+ void Promise.resolve()
110
+ .then(() => this.handler.request(message, controller.signal))
111
+ .catch((error) => ({ error: errorResponse(error) }))
112
+ .then((result) => this.write({ jsonrpc: "2.0", id: message.id, ...result }))
113
+ .catch(() => { })
114
+ .finally(() => this.incoming.delete(message.id));
115
+ }
116
+ }
117
+ export function errorResponse(error) {
118
+ if (error instanceof RequestError)
119
+ return error.toErrorResponse();
120
+ return RequestError.internalError(undefined, error instanceof Error ? error.message : String(error)).toErrorResponse();
121
+ }
122
+ function cancelRequestId(params) {
123
+ if (params === null || typeof params !== "object" || Array.isArray(params))
124
+ return undefined;
125
+ const value = params.requestId;
126
+ return typeof value === "string" || typeof value === "number" || value === null ? value : undefined;
127
+ }
@@ -0,0 +1,18 @@
1
+ import { type Stream } from "@agentclientprotocol/sdk";
2
+ import type { CustomBackendConfig } from "@automatalabs/acp-agents";
3
+ import { type BackendTarget } from "./backends.js";
4
+ export interface ServeAcpServerOptions {
5
+ /** Outer ACP stream. Defaults to process stdin/stdout. */
6
+ stream?: Stream;
7
+ /** Programmatic custom backends merged over AGENTPRISM_BACKENDS. */
8
+ backends?: Record<string, CustomBackendConfig>;
9
+ /** Exact backend targets, primarily for embedding and deterministic tests. */
10
+ targets?: readonly BackendTarget[];
11
+ /** Package version advertised on discovery connections. */
12
+ version?: string;
13
+ /** Closes the active outer and downstream connections when aborted. */
14
+ signal?: AbortSignal;
15
+ }
16
+ /** Serve one connection-pinned AgentPrism ACP V1 stream until either side closes. */
17
+ export declare function serveAcpServer(options?: ServeAcpServerOptions): Promise<void>;
18
+ //# sourceMappingURL=server.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AACA,OAAO,EAYL,KAAK,MAAM,EACZ,MAAM,0BAA0B,CAAC;AAElC,OAAO,KAAK,EAAE,mBAAmB,EAAwB,MAAM,0BAA0B,CAAC;AAC1F,OAAO,EAAyB,KAAK,aAAa,EAAE,MAAM,eAAe,CAAC;AAgB1E,MAAM,WAAW,qBAAqB;IACpC,0DAA0D;IAC1D,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,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,uEAAuE;IACvE,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAED,qFAAqF;AACrF,wBAAsB,cAAc,CAAC,OAAO,GAAE,qBAA0B,GAAG,OAAO,CAAC,IAAI,CAAC,CAiEvF"}
package/dist/server.js ADDED
@@ -0,0 +1,372 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { PROTOCOL_VERSION, RequestError, methods, ndJsonStream, } from "@agentclientprotocol/sdk";
3
+ import { Readable, Writable } from "node:stream";
4
+ import { resolveBackendTargets } from "./backends.js";
5
+ import { ACP_BACKENDS_PROBE_METHOD, assertSessionBackend, discoveryInitializeResponse, isRecord, mergeBackendInitializeResponse, parseProbeBackendsParams, parseRouterInitialize, } from "./protocol.js";
6
+ import { RawRpcPeer, errorResponse } from "./raw-rpc.js";
7
+ /** Serve one connection-pinned AgentPrism ACP V1 stream until either side closes. */
8
+ export async function serveAcpServer(options = {}) {
9
+ const stream = options.stream ?? stdioStream();
10
+ const targets = options.targets ? [...options.targets] : resolveBackendTargets({ backends: options.backends });
11
+ const targetsById = new Map(targets.map((target) => [target.id, target]));
12
+ if (targetsById.size !== targets.length)
13
+ throw new Error("ACP backend target ids must be unique");
14
+ const outerReader = stream.readable.getReader();
15
+ const first = await readFirst(outerReader, options.signal);
16
+ if (!first) {
17
+ outerReader.releaseLock();
18
+ return;
19
+ }
20
+ const remainingReadable = readableFromReader(outerReader);
21
+ const firstRequest = asInitializeRequest(first);
22
+ if (!firstRequest) {
23
+ await rejectFirstMessage(stream.writable, first);
24
+ await remainingReadable.cancel();
25
+ return;
26
+ }
27
+ let parsed;
28
+ try {
29
+ parsed = parseRouterInitialize(firstRequest.params);
30
+ }
31
+ catch (error) {
32
+ await writeResponseAndClose(stream.writable, {
33
+ jsonrpc: "2.0",
34
+ id: firstRequest.id,
35
+ error: errorResponse(error),
36
+ });
37
+ await remainingReadable.cancel();
38
+ return;
39
+ }
40
+ if (parsed.selection.mode === "discovery") {
41
+ await serveDiscoveryConnection({ readable: remainingReadable, writable: stream.writable }, firstRequest, parsed.request, targets, options.version ?? packageVersion(), options.signal);
42
+ return;
43
+ }
44
+ const target = targetsById.get(parsed.selection.backend);
45
+ if (!target) {
46
+ await writeResponseAndClose(stream.writable, {
47
+ jsonrpc: "2.0",
48
+ id: firstRequest.id,
49
+ error: RequestError.invalidParams(undefined, `unknown AgentPrism ACP backend ${JSON.stringify(parsed.selection.backend)}`).toErrorResponse(),
50
+ });
51
+ await remainingReadable.cancel();
52
+ return;
53
+ }
54
+ await serveBackendConnection({ readable: remainingReadable, writable: stream.writable }, firstRequest, target, options.signal);
55
+ }
56
+ function stdioStream() {
57
+ return ndJsonStream(Writable.toWeb(process.stdout), Readable.toWeb(process.stdin));
58
+ }
59
+ async function serveDiscoveryConnection(stream, initializeMessage, initializeRequest, targets, version, signal) {
60
+ const writer = stream.writable.getWriter();
61
+ await writer.write({
62
+ jsonrpc: "2.0",
63
+ id: initializeMessage.id,
64
+ result: discoveryInitializeResponse(version),
65
+ });
66
+ writer.releaseLock();
67
+ let peer;
68
+ const handler = {
69
+ request: async (message, requestSignal) => {
70
+ if (message.method !== ACP_BACKENDS_PROBE_METHOD) {
71
+ return { error: RequestError.methodNotFound(message.method).toErrorResponse() };
72
+ }
73
+ try {
74
+ const params = parseProbeBackendsParams(message.params);
75
+ return {
76
+ result: await probeBackends(targets, initializeRequest, params, peer, requestSignal),
77
+ };
78
+ }
79
+ catch (error) {
80
+ return { error: errorResponse(error) };
81
+ }
82
+ },
83
+ notification: () => { },
84
+ };
85
+ peer = new RawRpcPeer(stream, handler);
86
+ const onAbort = () => void peer.close(signal?.reason);
87
+ signal?.addEventListener("abort", onAbort, { once: true });
88
+ try {
89
+ await peer.closed;
90
+ }
91
+ finally {
92
+ signal?.removeEventListener("abort", onAbort);
93
+ await peer.close();
94
+ }
95
+ }
96
+ async function probeBackends(targets, initializeRequest, params, outer, signal) {
97
+ const backends = await Promise.all(targets.map((target) => probeBackend(target, initializeRequest, params, outer, signal)));
98
+ return { backends };
99
+ }
100
+ async function probeBackend(target, initializeRequest, params, outer, signal) {
101
+ let downstream;
102
+ let peer;
103
+ let sessionId;
104
+ let stage = "initialize";
105
+ try {
106
+ signal.throwIfAborted();
107
+ downstream = await target.open();
108
+ peer = new RawRpcPeer(downstream.stream, {
109
+ request: (message, requestSignal) => outer.request(message.method, message.params, requestSignal),
110
+ notification: (message) => outer.notify(message.method, message.params),
111
+ });
112
+ const initialized = await peer.request(methods.agent.initialize, initializeRequest, signal);
113
+ if ("error" in initialized)
114
+ throw new RequestError(initialized.error.code, initialized.error.message, initialized.error.data);
115
+ const initializeResponse = requireInitializeResponse(initialized.result);
116
+ stage = "session/new";
117
+ const created = await peer.request(methods.agent.session.new, params, signal);
118
+ if ("error" in created)
119
+ throw new RequestError(created.error.code, created.error.message, created.error.data);
120
+ const sessionResponse = requireNewSessionResponse(created.result);
121
+ sessionId = sessionResponse.sessionId;
122
+ return {
123
+ id: target.id,
124
+ name: target.name,
125
+ available: true,
126
+ ...(initializeResponse.agentInfo === undefined ? {} : { agentInfo: initializeResponse.agentInfo }),
127
+ ...(initializeResponse.agentCapabilities === undefined
128
+ ? {}
129
+ : { agentCapabilities: initializeResponse.agentCapabilities }),
130
+ ...(sessionResponse.modes === undefined ? {} : { modes: sessionResponse.modes }),
131
+ ...(sessionResponse.configOptions === undefined ? {} : { configOptions: sessionResponse.configOptions }),
132
+ ...(initializeResponse._meta === undefined ? {} : { initializeMeta: initializeResponse._meta }),
133
+ ...(sessionResponse._meta === undefined ? {} : { sessionMeta: sessionResponse._meta }),
134
+ };
135
+ }
136
+ catch (error) {
137
+ return {
138
+ id: target.id,
139
+ name: target.name,
140
+ available: false,
141
+ stage,
142
+ error: error instanceof Error ? error.message : String(error),
143
+ };
144
+ }
145
+ finally {
146
+ if (peer && sessionId) {
147
+ await peer.request(methods.agent.session.close, { sessionId }).catch(() => undefined);
148
+ }
149
+ await peer?.close();
150
+ await downstream?.close();
151
+ }
152
+ }
153
+ async function serveBackendConnection(outerStream, initializeMessage, target, signal) {
154
+ let downstream;
155
+ const outerWriter = outerStream.writable.getWriter();
156
+ const outerReader = outerStream.readable.getReader();
157
+ let innerWriter;
158
+ let innerReader;
159
+ let initializeResponded = false;
160
+ const onAbort = () => {
161
+ void outerReader.cancel(signal?.reason);
162
+ void innerReader?.cancel(signal?.reason);
163
+ };
164
+ signal?.addEventListener("abort", onAbort, { once: true });
165
+ try {
166
+ downstream = await target.open();
167
+ innerWriter = downstream.stream.writable.getWriter();
168
+ innerReader = downstream.stream.readable.getReader();
169
+ await innerWriter.write(initializeMessage);
170
+ const initialized = await readInitializeResponse(innerReader, outerWriter, initializeMessage.id);
171
+ if ("error" in initialized) {
172
+ await outerWriter.write(initialized);
173
+ initializeResponded = true;
174
+ return;
175
+ }
176
+ const response = requireInitializeResponse(initialized.result);
177
+ await outerWriter.write({
178
+ ...initialized,
179
+ result: mergeBackendInitializeResponse(response, target.id),
180
+ });
181
+ initializeResponded = true;
182
+ const clientToBackend = pumpClientToBackend(outerReader, innerWriter, outerWriter, target.id, signal);
183
+ const backendToClient = pumpUnchanged(innerReader, outerWriter, signal);
184
+ await Promise.race([clientToBackend, backendToClient]);
185
+ await Promise.allSettled([
186
+ outerReader.cancel(),
187
+ innerReader.cancel(),
188
+ clientToBackend,
189
+ backendToClient,
190
+ ]);
191
+ }
192
+ catch (error) {
193
+ if (!initializeResponded && !signal?.aborted) {
194
+ await outerWriter.write({
195
+ jsonrpc: "2.0",
196
+ id: initializeMessage.id,
197
+ error: errorResponse(error),
198
+ }).catch(() => undefined);
199
+ initializeResponded = true;
200
+ }
201
+ else if (!signal?.aborted) {
202
+ throw error;
203
+ }
204
+ }
205
+ finally {
206
+ signal?.removeEventListener("abort", onAbort);
207
+ await Promise.allSettled([
208
+ innerWriter?.close() ?? Promise.resolve(),
209
+ outerWriter.close(),
210
+ ]);
211
+ try {
212
+ innerReader?.releaseLock();
213
+ }
214
+ catch { }
215
+ try {
216
+ outerReader.releaseLock();
217
+ }
218
+ catch { }
219
+ try {
220
+ innerWriter?.releaseLock();
221
+ }
222
+ catch { }
223
+ try {
224
+ outerWriter.releaseLock();
225
+ }
226
+ catch { }
227
+ await downstream?.close();
228
+ }
229
+ }
230
+ async function readInitializeResponse(reader, outerWriter, initializeId) {
231
+ while (true) {
232
+ const item = await reader.read();
233
+ if (item.done)
234
+ throw new Error("ACP backend closed before initialize completed");
235
+ const message = item.value;
236
+ if (("result" in message || "error" in message) && message.id === initializeId)
237
+ return message;
238
+ await outerWriter.write(message);
239
+ }
240
+ }
241
+ async function pumpClientToBackend(reader, backendWriter, clientWriter, backendId, signal) {
242
+ while (!signal?.aborted) {
243
+ const item = await reader.read();
244
+ if (item.done)
245
+ return;
246
+ const message = item.value;
247
+ if ("method" in message && message.method === methods.agent.session.new && "id" in message) {
248
+ try {
249
+ assertSessionBackend(message.params, backendId);
250
+ }
251
+ catch (error) {
252
+ await clientWriter.write({ jsonrpc: "2.0", id: message.id, error: errorResponse(error) });
253
+ continue;
254
+ }
255
+ }
256
+ if ("method" in message && message.method.startsWith("_automatalabs/agentprism/")) {
257
+ if ("id" in message) {
258
+ await clientWriter.write({
259
+ jsonrpc: "2.0",
260
+ id: message.id,
261
+ error: RequestError.methodNotFound(message.method).toErrorResponse(),
262
+ });
263
+ }
264
+ continue;
265
+ }
266
+ await backendWriter.write(message);
267
+ }
268
+ }
269
+ async function pumpUnchanged(reader, writer, signal) {
270
+ while (!signal?.aborted) {
271
+ const item = await reader.read();
272
+ if (item.done)
273
+ return;
274
+ await writer.write(item.value);
275
+ }
276
+ }
277
+ function requireInitializeResponse(value) {
278
+ if (!isRecord(value) || value.protocolVersion !== PROTOCOL_VERSION) {
279
+ throw new Error(`ACP backend returned an invalid protocol-${PROTOCOL_VERSION} initialize response`);
280
+ }
281
+ return value;
282
+ }
283
+ function requireNewSessionResponse(value) {
284
+ if (!isRecord(value) || typeof value.sessionId !== "string" || value.sessionId.length === 0) {
285
+ throw new Error("ACP backend returned an invalid session/new response");
286
+ }
287
+ return value;
288
+ }
289
+ function asInitializeRequest(message) {
290
+ return "method" in message && "id" in message && message.method === methods.agent.initialize
291
+ ? message
292
+ : undefined;
293
+ }
294
+ async function rejectFirstMessage(writable, message) {
295
+ if ("method" in message && "id" in message) {
296
+ await writeResponseAndClose(writable, {
297
+ jsonrpc: "2.0",
298
+ id: message.id,
299
+ error: RequestError.invalidRequest(undefined, "first ACP request must be initialize").toErrorResponse(),
300
+ });
301
+ return;
302
+ }
303
+ const writer = writable.getWriter();
304
+ await writer.close();
305
+ writer.releaseLock();
306
+ }
307
+ async function writeResponseAndClose(writable, response) {
308
+ const writer = writable.getWriter();
309
+ try {
310
+ await writer.write(response);
311
+ await writer.close();
312
+ }
313
+ finally {
314
+ writer.releaseLock();
315
+ }
316
+ }
317
+ async function readFirst(reader, signal) {
318
+ if (!signal) {
319
+ const item = await reader.read();
320
+ return item.done ? undefined : item.value;
321
+ }
322
+ if (signal.aborted)
323
+ return undefined;
324
+ return new Promise((resolve, reject) => {
325
+ const onAbort = () => {
326
+ void reader.cancel(signal.reason);
327
+ resolve(undefined);
328
+ };
329
+ signal.addEventListener("abort", onAbort, { once: true });
330
+ reader.read().then((item) => {
331
+ signal.removeEventListener("abort", onAbort);
332
+ resolve(item.done ? undefined : item.value);
333
+ }, (error) => {
334
+ signal.removeEventListener("abort", onAbort);
335
+ reject(error);
336
+ });
337
+ });
338
+ }
339
+ function readableFromReader(reader) {
340
+ return new ReadableStream({
341
+ async pull(controller) {
342
+ try {
343
+ const item = await reader.read();
344
+ if (item.done) {
345
+ controller.close();
346
+ reader.releaseLock();
347
+ }
348
+ else {
349
+ controller.enqueue(item.value);
350
+ }
351
+ }
352
+ catch (error) {
353
+ controller.error(error);
354
+ try {
355
+ reader.releaseLock();
356
+ }
357
+ catch { }
358
+ }
359
+ },
360
+ async cancel(reason) {
361
+ await reader.cancel(reason);
362
+ try {
363
+ reader.releaseLock();
364
+ }
365
+ catch { }
366
+ },
367
+ });
368
+ }
369
+ function packageVersion() {
370
+ const manifest = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
371
+ return typeof manifest.version === "string" ? manifest.version : "0.0.0";
372
+ }
package/package.json CHANGED
@@ -1,21 +1,45 @@
1
1
  {
2
2
  "name": "@automatalabs/acp-server",
3
- "version": "0.0.0",
4
- "description": "Bootstrap package record for AgentPrism's ACP server.",
3
+ "version": "0.2.0",
5
4
  "license": "Apache-2.0",
5
+ "engines": {
6
+ "node": ">=22"
7
+ },
6
8
  "repository": {
7
9
  "type": "git",
8
10
  "url": "git+https://github.com/agentprism/agentprism-workflows.git",
9
11
  "directory": "packages/acp-server"
10
12
  },
11
- "homepage": "https://github.com/agentprism/agentprism-workflows/tree/main/packages/acp-server#readme",
12
- "bugs": {
13
- "url": "https://github.com/agentprism/agentprism-workflows/issues"
13
+ "type": "module",
14
+ "bin": {
15
+ "agentprism-acp-server": "./dist/cli.js"
16
+ },
17
+ "main": "./dist/index.js",
18
+ "types": "./dist/index.d.ts",
19
+ "exports": {
20
+ ".": {
21
+ "types": "./dist/index.d.ts",
22
+ "import": "./dist/index.js",
23
+ "default": "./dist/index.js"
24
+ }
14
25
  },
15
26
  "files": [
16
- "README.md"
27
+ "dist"
17
28
  ],
18
29
  "publishConfig": {
19
30
  "access": "public"
31
+ },
32
+ "dependencies": {
33
+ "@agentclientprotocol/sdk": "^1.4.0",
34
+ "ws": "^8.21.3",
35
+ "@automatalabs/acp-agents": "1.1.0"
36
+ },
37
+ "devDependencies": {
38
+ "@types/ws": "^8.18.1"
39
+ },
40
+ "scripts": {
41
+ "build": "tsc -b",
42
+ "typecheck": "tsc --noEmit",
43
+ "test": "tsx --test \"test/**/*.test.ts\""
20
44
  }
21
- }
45
+ }