@multi-agent-protocol/sdk 0.0.3 → 0.0.5

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/dist/testing.cjs CHANGED
@@ -1,6 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  var ulid = require('ulid');
4
+ var events = require('events');
4
5
 
5
6
  // src/jsonrpc/index.ts
6
7
  function isRequest(message) {
@@ -63,6 +64,7 @@ var EVENT_TYPES = {
63
64
  PARTICIPANT_DISCONNECTED: "participant_disconnected",
64
65
  // Message events
65
66
  MESSAGE_SENT: "message_sent",
67
+ MESSAGE_DELIVERED: "message_delivered",
66
68
  // Scope events
67
69
  SCOPE_CREATED: "scope_created",
68
70
  SCOPE_MEMBER_JOINED: "scope_member_joined",
@@ -1398,6 +1400,11 @@ var TestServer = class {
1398
1400
  if (participant) {
1399
1401
  this.deliverMessage(recipientId, message);
1400
1402
  delivered.push(recipientId);
1403
+ this.emitEvent({
1404
+ type: EVENT_TYPES.MESSAGE_DELIVERED,
1405
+ source: senderAgentId ?? senderId,
1406
+ data: { messageId, message, correlationId: params.meta?.correlationId }
1407
+ });
1401
1408
  }
1402
1409
  }
1403
1410
  this.emitEvent({
@@ -1758,6 +1765,9 @@ var TestServer = class {
1758
1765
  if (typeof address === "string") {
1759
1766
  return [this.#findParticipantForAgent(address) ?? address];
1760
1767
  }
1768
+ if ("participant" in address && address.participant) {
1769
+ return this.#participants.has(address.participant) ? [address.participant] : [];
1770
+ }
1761
1771
  if ("agent" in address && !("system" in address)) {
1762
1772
  const participantId = this.#findParticipantForAgent(address.agent);
1763
1773
  return participantId ? [participantId] : [];
@@ -2034,6 +2044,211 @@ var TestServer = class {
2034
2044
  }
2035
2045
  };
2036
2046
 
2047
+ // src/stream/index.ts
2048
+ function websocketStream(ws) {
2049
+ const messageQueue = [];
2050
+ let messageResolver = null;
2051
+ let closed = false;
2052
+ let closeError = null;
2053
+ ws.addEventListener("message", (event) => {
2054
+ try {
2055
+ const message = JSON.parse(event.data);
2056
+ if (messageResolver) {
2057
+ messageResolver({ value: message, done: false });
2058
+ messageResolver = null;
2059
+ } else {
2060
+ messageQueue.push(message);
2061
+ }
2062
+ } catch {
2063
+ console.error("MAP: Failed to parse WebSocket message:", event.data);
2064
+ }
2065
+ });
2066
+ ws.addEventListener("close", () => {
2067
+ closed = true;
2068
+ if (messageResolver) {
2069
+ messageResolver({ value: void 0, done: true });
2070
+ messageResolver = null;
2071
+ }
2072
+ });
2073
+ ws.addEventListener("error", () => {
2074
+ closeError = new Error("WebSocket error");
2075
+ closed = true;
2076
+ if (messageResolver) {
2077
+ messageResolver({ value: void 0, done: true });
2078
+ messageResolver = null;
2079
+ }
2080
+ });
2081
+ const readable = new ReadableStream({
2082
+ async pull(controller) {
2083
+ if (messageQueue.length > 0) {
2084
+ controller.enqueue(messageQueue.shift());
2085
+ return;
2086
+ }
2087
+ if (closed) {
2088
+ if (closeError) {
2089
+ controller.error(closeError);
2090
+ } else {
2091
+ controller.close();
2092
+ }
2093
+ return;
2094
+ }
2095
+ await new Promise((resolve) => {
2096
+ messageResolver = resolve;
2097
+ }).then((result) => {
2098
+ if (result.done) {
2099
+ controller.close();
2100
+ } else {
2101
+ controller.enqueue(result.value);
2102
+ }
2103
+ });
2104
+ }
2105
+ });
2106
+ const writable = new WritableStream({
2107
+ async write(message) {
2108
+ if (ws.readyState === WebSocket.CONNECTING) {
2109
+ await new Promise((resolve, reject) => {
2110
+ const onOpen = () => {
2111
+ ws.removeEventListener("error", onError);
2112
+ resolve();
2113
+ };
2114
+ const onError = () => {
2115
+ ws.removeEventListener("open", onOpen);
2116
+ reject(new Error("WebSocket failed to connect"));
2117
+ };
2118
+ ws.addEventListener("open", onOpen, { once: true });
2119
+ ws.addEventListener("error", onError, { once: true });
2120
+ });
2121
+ }
2122
+ if (ws.readyState !== WebSocket.OPEN) {
2123
+ throw new Error("WebSocket is not open");
2124
+ }
2125
+ ws.send(JSON.stringify(message));
2126
+ },
2127
+ close() {
2128
+ ws.close();
2129
+ },
2130
+ abort() {
2131
+ ws.close();
2132
+ }
2133
+ });
2134
+ return { readable, writable };
2135
+ }
2136
+ function waitForOpen(ws, timeoutMs = 1e4) {
2137
+ return new Promise((resolve, reject) => {
2138
+ if (ws.readyState === WebSocket.OPEN) {
2139
+ resolve();
2140
+ return;
2141
+ }
2142
+ const timeout = setTimeout(() => {
2143
+ ws.close();
2144
+ reject(new Error(`WebSocket connection timeout after ${timeoutMs}ms`));
2145
+ }, timeoutMs);
2146
+ const onOpen = () => {
2147
+ clearTimeout(timeout);
2148
+ ws.removeEventListener("error", onError);
2149
+ resolve();
2150
+ };
2151
+ const onError = () => {
2152
+ clearTimeout(timeout);
2153
+ ws.removeEventListener("open", onOpen);
2154
+ reject(new Error("WebSocket connection failed"));
2155
+ };
2156
+ ws.addEventListener("open", onOpen, { once: true });
2157
+ ws.addEventListener("error", onError, { once: true });
2158
+ });
2159
+ }
2160
+ function createStreamPair() {
2161
+ const clientToServer = [];
2162
+ const serverToClient = [];
2163
+ let clientToServerResolver = null;
2164
+ let serverToClientResolver = null;
2165
+ let clientToServerClosed = false;
2166
+ let serverToClientClosed = false;
2167
+ function createReadable(queue, _getResolver, setResolver, isClosed) {
2168
+ return new ReadableStream({
2169
+ async pull(controller) {
2170
+ if (queue.length > 0) {
2171
+ controller.enqueue(queue.shift());
2172
+ return;
2173
+ }
2174
+ if (isClosed()) {
2175
+ controller.close();
2176
+ return;
2177
+ }
2178
+ const message = await new Promise((resolve) => {
2179
+ setResolver((msg) => {
2180
+ setResolver(null);
2181
+ resolve(msg);
2182
+ });
2183
+ });
2184
+ if (message === null) {
2185
+ controller.close();
2186
+ } else {
2187
+ controller.enqueue(message);
2188
+ }
2189
+ }
2190
+ });
2191
+ }
2192
+ function createWritable(queue, getResolver, setClosed) {
2193
+ return new WritableStream({
2194
+ write(message) {
2195
+ const resolver = getResolver();
2196
+ if (resolver) {
2197
+ resolver(message);
2198
+ } else {
2199
+ queue.push(message);
2200
+ }
2201
+ },
2202
+ close() {
2203
+ setClosed();
2204
+ const resolver = getResolver();
2205
+ if (resolver) {
2206
+ resolver(null);
2207
+ }
2208
+ }
2209
+ });
2210
+ }
2211
+ const clientStream = {
2212
+ // Client writes to server
2213
+ writable: createWritable(
2214
+ clientToServer,
2215
+ () => clientToServerResolver,
2216
+ () => {
2217
+ clientToServerClosed = true;
2218
+ }
2219
+ ),
2220
+ // Client reads from server
2221
+ readable: createReadable(
2222
+ serverToClient,
2223
+ () => serverToClientResolver,
2224
+ (r) => {
2225
+ serverToClientResolver = r;
2226
+ },
2227
+ () => serverToClientClosed
2228
+ )
2229
+ };
2230
+ const serverStream = {
2231
+ // Server writes to client
2232
+ writable: createWritable(
2233
+ serverToClient,
2234
+ () => serverToClientResolver,
2235
+ () => {
2236
+ serverToClientClosed = true;
2237
+ }
2238
+ ),
2239
+ // Server reads from client
2240
+ readable: createReadable(
2241
+ clientToServer,
2242
+ () => clientToServerResolver,
2243
+ (r) => {
2244
+ clientToServerResolver = r;
2245
+ },
2246
+ () => clientToServerClosed
2247
+ )
2248
+ };
2249
+ return [clientStream, serverStream];
2250
+ }
2251
+
2037
2252
  // src/subscription/index.ts
2038
2253
  var Subscription = class {
2039
2254
  id;
@@ -2349,92 +2564,732 @@ function createSubscription(id, unsubscribe, options, sendAck) {
2349
2564
  return new Subscription(id, unsubscribe, options, sendAck);
2350
2565
  }
2351
2566
 
2352
- // src/connection/client.ts
2353
- var ClientConnection = class {
2354
- #connection;
2355
- #subscriptions = /* @__PURE__ */ new Map();
2356
- #subscriptionStates = /* @__PURE__ */ new Map();
2357
- #reconnectionHandlers = /* @__PURE__ */ new Set();
2567
+ // src/acp/types.ts
2568
+ var ACPError = class _ACPError extends Error {
2569
+ code;
2570
+ data;
2571
+ constructor(code, message, data) {
2572
+ super(message);
2573
+ this.name = "ACPError";
2574
+ this.code = code;
2575
+ this.data = data;
2576
+ }
2577
+ /**
2578
+ * Create an ACPError from an error response.
2579
+ */
2580
+ static fromResponse(error) {
2581
+ return new _ACPError(error.code, error.message, error.data);
2582
+ }
2583
+ /**
2584
+ * Convert to JSON-RPC error object.
2585
+ */
2586
+ toErrorObject() {
2587
+ return {
2588
+ code: this.code,
2589
+ message: this.message,
2590
+ ...this.data !== void 0 && { data: this.data }
2591
+ };
2592
+ }
2593
+ };
2594
+ var ACP_METHODS = {
2595
+ // Lifecycle
2596
+ INITIALIZE: "initialize",
2597
+ AUTHENTICATE: "authenticate",
2598
+ // Session management
2599
+ SESSION_NEW: "session/new",
2600
+ SESSION_LOAD: "session/load",
2601
+ SESSION_SET_MODE: "session/set_mode",
2602
+ // Prompt
2603
+ SESSION_PROMPT: "session/prompt",
2604
+ SESSION_CANCEL: "session/cancel",
2605
+ // Notifications
2606
+ SESSION_UPDATE: "session/update",
2607
+ // Agent→Client requests
2608
+ REQUEST_PERMISSION: "request_permission",
2609
+ FS_READ_TEXT_FILE: "fs/read_text_file",
2610
+ FS_WRITE_TEXT_FILE: "fs/write_text_file",
2611
+ TERMINAL_CREATE: "terminal/create",
2612
+ TERMINAL_OUTPUT: "terminal/output",
2613
+ TERMINAL_RELEASE: "terminal/release",
2614
+ TERMINAL_WAIT_FOR_EXIT: "terminal/wait_for_exit",
2615
+ TERMINAL_KILL: "terminal/kill"
2616
+ };
2617
+ function isACPEnvelope(payload) {
2618
+ if (typeof payload !== "object" || payload === null) {
2619
+ return false;
2620
+ }
2621
+ const envelope = payload;
2622
+ if (typeof envelope.acp !== "object" || envelope.acp === null || typeof envelope.acpContext !== "object" || envelope.acpContext === null) {
2623
+ return false;
2624
+ }
2625
+ const acpContext = envelope.acpContext;
2626
+ return typeof acpContext.streamId === "string";
2627
+ }
2628
+
2629
+ // src/acp/stream.ts
2630
+ var ACPStreamConnection = class extends events.EventEmitter {
2631
+ #mapClient;
2358
2632
  #options;
2633
+ #streamId;
2634
+ #pendingRequests = /* @__PURE__ */ new Map();
2635
+ #subscription = null;
2359
2636
  #sessionId = null;
2360
- #serverCapabilities = null;
2361
- #connected = false;
2362
- #lastConnectOptions;
2637
+ #initialized = false;
2638
+ #capabilities = null;
2639
+ #closed = false;
2640
+ #lastEventId = null;
2363
2641
  #isReconnecting = false;
2364
- constructor(stream, options = {}) {
2365
- this.#connection = new BaseConnection(stream, options);
2642
+ #unsubscribeReconnection = null;
2643
+ /**
2644
+ * Create a new ACP stream connection.
2645
+ *
2646
+ * @param mapClient - The underlying MAP client connection
2647
+ * @param options - Stream configuration options
2648
+ */
2649
+ constructor(mapClient, options) {
2650
+ super();
2651
+ this.#mapClient = mapClient;
2366
2652
  this.#options = options;
2367
- this.#connection.setNotificationHandler(this.#handleNotification.bind(this));
2368
- if (options.reconnection?.enabled && options.createStream) {
2369
- this.#connection.onStateChange((newState) => {
2370
- if (newState === "closed" && this.#connected && !this.#isReconnecting) {
2371
- void this.#handleDisconnect();
2372
- }
2373
- });
2374
- }
2653
+ this.#streamId = `acp-${Date.now()}-${Math.random().toString(36).slice(2)}`;
2654
+ this.#unsubscribeReconnection = mapClient.onReconnection((event) => {
2655
+ void this.#handleReconnectionEvent(event);
2656
+ });
2375
2657
  }
2376
2658
  // ===========================================================================
2377
- // Connection Lifecycle
2659
+ // Public Properties
2660
+ // ===========================================================================
2661
+ /** Unique identifier for this ACP stream */
2662
+ get streamId() {
2663
+ return this.#streamId;
2664
+ }
2665
+ /** Target agent this stream connects to */
2666
+ get targetAgent() {
2667
+ return this.#options.targetAgent;
2668
+ }
2669
+ /** Current ACP session ID (null until newSession called) */
2670
+ get sessionId() {
2671
+ return this.#sessionId;
2672
+ }
2673
+ /** Whether initialize() has been called */
2674
+ get initialized() {
2675
+ return this.#initialized;
2676
+ }
2677
+ /** Agent capabilities from initialize response */
2678
+ get capabilities() {
2679
+ return this.#capabilities;
2680
+ }
2681
+ /** Whether the stream is closed */
2682
+ get isClosed() {
2683
+ return this.#closed;
2684
+ }
2685
+ /** Last processed event ID (for reconnection support) */
2686
+ get lastEventId() {
2687
+ return this.#lastEventId;
2688
+ }
2689
+ /** Whether the stream is currently reconnecting */
2690
+ get isReconnecting() {
2691
+ return this.#isReconnecting;
2692
+ }
2693
+ // ===========================================================================
2694
+ // Reconnection Handling
2378
2695
  // ===========================================================================
2379
2696
  /**
2380
- * Connect to the MAP system
2697
+ * Handle MAP reconnection events.
2381
2698
  */
2382
- async connect(options) {
2383
- const params = {
2384
- protocolVersion: PROTOCOL_VERSION,
2385
- participantType: "client",
2386
- name: this.#options.name,
2387
- capabilities: this.#options.capabilities,
2388
- sessionId: options?.sessionId,
2389
- auth: options?.auth
2390
- };
2391
- const result = await this.#connection.sendRequest(CORE_METHODS.CONNECT, params);
2392
- this.#sessionId = result.sessionId;
2393
- this.#serverCapabilities = result.capabilities;
2394
- this.#connected = true;
2395
- this.#connection._transitionTo("connected");
2396
- this.#lastConnectOptions = options;
2397
- return result;
2699
+ async #handleReconnectionEvent(event) {
2700
+ if (this.#closed) return;
2701
+ switch (event.type) {
2702
+ case "disconnected":
2703
+ this.#isReconnecting = true;
2704
+ this.emit("reconnecting");
2705
+ for (const [id, pending] of this.#pendingRequests) {
2706
+ clearTimeout(pending.timeout);
2707
+ pending.reject(new Error("Connection lost during ACP request"));
2708
+ this.#pendingRequests.delete(id);
2709
+ }
2710
+ break;
2711
+ case "reconnected":
2712
+ await this.#handleReconnected();
2713
+ break;
2714
+ case "reconnectFailed":
2715
+ this.#isReconnecting = false;
2716
+ this.emit("error", event.error ?? new Error("MAP reconnection failed"));
2717
+ break;
2718
+ }
2398
2719
  }
2399
2720
  /**
2400
- * Disconnect from the MAP system
2721
+ * Handle successful MAP reconnection.
2401
2722
  */
2402
- async disconnect(reason) {
2403
- if (!this.#connected) return;
2723
+ async #handleReconnected() {
2724
+ this.#subscription = null;
2404
2725
  try {
2405
- await this.#connection.sendRequest(
2406
- CORE_METHODS.DISCONNECT,
2407
- reason ? { reason } : void 0
2408
- );
2409
- } finally {
2410
- for (const subscription of this.#subscriptions.values()) {
2411
- subscription._close();
2726
+ await this.#setupSubscription();
2727
+ if (this.#sessionId) {
2728
+ const sessionValid = await this.#verifySessionValid();
2729
+ if (!sessionValid) {
2730
+ const lostSessionId = this.#sessionId;
2731
+ this.#sessionId = null;
2732
+ this.emit("sessionLost", {
2733
+ sessionId: lostSessionId,
2734
+ reason: "Session no longer valid after reconnection"
2735
+ });
2736
+ }
2412
2737
  }
2413
- this.#subscriptions.clear();
2414
- await this.#connection.close();
2415
- this.#connected = false;
2738
+ this.#isReconnecting = false;
2739
+ this.emit("reconnected");
2740
+ } catch (error) {
2741
+ this.#isReconnecting = false;
2742
+ this.emit("error", error instanceof Error ? error : new Error(String(error)));
2416
2743
  }
2417
2744
  }
2418
2745
  /**
2419
- * Whether the client is connected
2420
- */
2421
- get isConnected() {
2422
- return this.#connected && !this.#connection.isClosed;
2423
- }
2424
- /**
2425
- * Current session ID
2746
+ * Verify that the current ACP session is still valid.
2747
+ *
2748
+ * Since ACP doesn't have a dedicated status check method, we attempt
2749
+ * a lightweight operation (cancel with no effect) to verify the session.
2426
2750
  */
2427
- get sessionId() {
2428
- return this.#sessionId;
2751
+ async #verifySessionValid() {
2752
+ if (!this.#sessionId) return false;
2753
+ try {
2754
+ await this.#sendNotification(ACP_METHODS.SESSION_CANCEL, {
2755
+ sessionId: this.#sessionId,
2756
+ reason: "session_verification"
2757
+ });
2758
+ return true;
2759
+ } catch {
2760
+ return false;
2761
+ }
2429
2762
  }
2763
+ // ===========================================================================
2764
+ // Internal Methods
2765
+ // ===========================================================================
2430
2766
  /**
2431
- * Server capabilities
2767
+ * Set up the subscription for receiving messages from the target agent.
2432
2768
  */
2433
- get serverCapabilities() {
2434
- return this.#serverCapabilities;
2769
+ async #setupSubscription() {
2770
+ if (this.#subscription) return;
2771
+ this.#subscription = await this.#mapClient.subscribe({
2772
+ fromAgents: [this.#options.targetAgent]
2773
+ });
2774
+ void this.#processEvents();
2435
2775
  }
2436
2776
  /**
2437
- * AbortSignal that triggers when the connection closes
2777
+ * Process incoming events from the subscription.
2778
+ */
2779
+ async #processEvents() {
2780
+ if (!this.#subscription) return;
2781
+ try {
2782
+ for await (const event of this.#subscription) {
2783
+ if (this.#closed) break;
2784
+ if (event.id) {
2785
+ this.#lastEventId = event.id;
2786
+ }
2787
+ if (event.type === "message_delivered" && event.data) {
2788
+ const message = event.data.message;
2789
+ if (message?.payload) {
2790
+ await this.#handleIncomingMessage(message);
2791
+ }
2792
+ }
2793
+ }
2794
+ } catch (error) {
2795
+ if (!this.#closed) {
2796
+ this.emit("error", error);
2797
+ }
2798
+ }
2799
+ }
2800
+ /**
2801
+ * Handle an incoming message from the target agent.
2802
+ */
2803
+ async #handleIncomingMessage(message) {
2804
+ const payload = message.payload;
2805
+ if (!isACPEnvelope(payload)) return;
2806
+ const envelope = payload;
2807
+ const { acp, acpContext } = envelope;
2808
+ if (acpContext.streamId !== this.#streamId) return;
2809
+ if (acp.id !== void 0 && !acp.method) {
2810
+ const requestId = String(acp.id);
2811
+ const pending = this.#pendingRequests.get(requestId);
2812
+ if (pending) {
2813
+ clearTimeout(pending.timeout);
2814
+ this.#pendingRequests.delete(requestId);
2815
+ if (acp.error) {
2816
+ pending.reject(ACPError.fromResponse(acp.error));
2817
+ } else {
2818
+ pending.resolve(acp.result);
2819
+ }
2820
+ }
2821
+ return;
2822
+ }
2823
+ if (acp.method && acp.id === void 0) {
2824
+ await this.#handleNotification(acp.method, acp.params, acpContext);
2825
+ return;
2826
+ }
2827
+ if (acp.method && acp.id !== void 0) {
2828
+ await this.#handleAgentRequest(acp.id, acp.method, acp.params, acpContext, message);
2829
+ }
2830
+ }
2831
+ /**
2832
+ * Handle an ACP notification from the agent.
2833
+ */
2834
+ async #handleNotification(method, params, _acpContext) {
2835
+ if (method === ACP_METHODS.SESSION_UPDATE) {
2836
+ await this.#options.client.sessionUpdate(params);
2837
+ }
2838
+ }
2839
+ /**
2840
+ * Handle an agent→client request.
2841
+ */
2842
+ async #handleAgentRequest(requestId, method, params, _ctx, originalMessage) {
2843
+ let result;
2844
+ let error;
2845
+ try {
2846
+ switch (method) {
2847
+ case ACP_METHODS.REQUEST_PERMISSION:
2848
+ result = await this.#options.client.requestPermission(
2849
+ params
2850
+ );
2851
+ break;
2852
+ case ACP_METHODS.FS_READ_TEXT_FILE:
2853
+ if (!this.#options.client.readTextFile) {
2854
+ throw new ACPError(-32601, "Method not supported: fs/read_text_file");
2855
+ }
2856
+ result = await this.#options.client.readTextFile(
2857
+ params
2858
+ );
2859
+ break;
2860
+ case ACP_METHODS.FS_WRITE_TEXT_FILE:
2861
+ if (!this.#options.client.writeTextFile) {
2862
+ throw new ACPError(-32601, "Method not supported: fs/write_text_file");
2863
+ }
2864
+ result = await this.#options.client.writeTextFile(
2865
+ params
2866
+ );
2867
+ break;
2868
+ case ACP_METHODS.TERMINAL_CREATE:
2869
+ if (!this.#options.client.createTerminal) {
2870
+ throw new ACPError(-32601, "Method not supported: terminal/create");
2871
+ }
2872
+ result = await this.#options.client.createTerminal(
2873
+ params
2874
+ );
2875
+ break;
2876
+ case ACP_METHODS.TERMINAL_OUTPUT:
2877
+ if (!this.#options.client.terminalOutput) {
2878
+ throw new ACPError(-32601, "Method not supported: terminal/output");
2879
+ }
2880
+ result = await this.#options.client.terminalOutput(
2881
+ params
2882
+ );
2883
+ break;
2884
+ case ACP_METHODS.TERMINAL_RELEASE:
2885
+ if (!this.#options.client.releaseTerminal) {
2886
+ throw new ACPError(-32601, "Method not supported: terminal/release");
2887
+ }
2888
+ result = await this.#options.client.releaseTerminal(
2889
+ params
2890
+ );
2891
+ break;
2892
+ case ACP_METHODS.TERMINAL_WAIT_FOR_EXIT:
2893
+ if (!this.#options.client.waitForTerminalExit) {
2894
+ throw new ACPError(-32601, "Method not supported: terminal/wait_for_exit");
2895
+ }
2896
+ result = await this.#options.client.waitForTerminalExit(
2897
+ params
2898
+ );
2899
+ break;
2900
+ case ACP_METHODS.TERMINAL_KILL:
2901
+ if (!this.#options.client.killTerminal) {
2902
+ throw new ACPError(-32601, "Method not supported: terminal/kill");
2903
+ }
2904
+ result = await this.#options.client.killTerminal(
2905
+ params
2906
+ );
2907
+ break;
2908
+ default:
2909
+ throw new ACPError(-32601, `Unknown method: ${method}`);
2910
+ }
2911
+ } catch (e) {
2912
+ if (e instanceof ACPError) {
2913
+ error = e;
2914
+ } else {
2915
+ error = new ACPError(-32603, e.message);
2916
+ }
2917
+ }
2918
+ const responseEnvelope = {
2919
+ acp: {
2920
+ jsonrpc: "2.0",
2921
+ id: requestId,
2922
+ ...error ? { error: error.toErrorObject() } : { result }
2923
+ },
2924
+ acpContext: {
2925
+ streamId: this.#streamId,
2926
+ sessionId: this.#sessionId,
2927
+ direction: "client-to-agent"
2928
+ }
2929
+ };
2930
+ await this.#mapClient.send(
2931
+ { agent: this.#options.targetAgent },
2932
+ responseEnvelope,
2933
+ {
2934
+ protocol: "acp",
2935
+ correlationId: originalMessage.id
2936
+ }
2937
+ );
2938
+ }
2939
+ /**
2940
+ * Send an ACP request and wait for response.
2941
+ */
2942
+ async #sendRequest(method, params) {
2943
+ if (this.#closed) {
2944
+ throw new Error("ACP stream is closed");
2945
+ }
2946
+ await this.#setupSubscription();
2947
+ if (this.#closed) {
2948
+ throw new Error("ACP stream closed");
2949
+ }
2950
+ const correlationId = `${this.#streamId}-${Date.now()}-${Math.random().toString(36).slice(2)}`;
2951
+ const timeout = this.#options.timeout ?? 3e4;
2952
+ const envelope = {
2953
+ acp: {
2954
+ jsonrpc: "2.0",
2955
+ id: correlationId,
2956
+ method,
2957
+ ...params !== void 0 && { params }
2958
+ },
2959
+ acpContext: {
2960
+ streamId: this.#streamId,
2961
+ sessionId: this.#sessionId,
2962
+ direction: "client-to-agent"
2963
+ }
2964
+ };
2965
+ const resultPromise = new Promise((resolve, reject) => {
2966
+ const timeoutHandle = setTimeout(() => {
2967
+ this.#pendingRequests.delete(correlationId);
2968
+ reject(new Error(`ACP request timed out after ${timeout}ms: ${method}`));
2969
+ }, timeout);
2970
+ this.#pendingRequests.set(correlationId, {
2971
+ resolve,
2972
+ reject,
2973
+ timeout: timeoutHandle,
2974
+ method
2975
+ });
2976
+ });
2977
+ try {
2978
+ await this.#mapClient.send({ agent: this.#options.targetAgent }, envelope, {
2979
+ protocol: "acp",
2980
+ correlationId
2981
+ });
2982
+ } catch (err) {
2983
+ const pending = this.#pendingRequests.get(correlationId);
2984
+ if (pending) {
2985
+ clearTimeout(pending.timeout);
2986
+ this.#pendingRequests.delete(correlationId);
2987
+ }
2988
+ throw err;
2989
+ }
2990
+ if (this.#closed && !this.#pendingRequests.has(correlationId)) {
2991
+ throw new Error("ACP stream closed");
2992
+ }
2993
+ return resultPromise;
2994
+ }
2995
+ /**
2996
+ * Send an ACP notification (no response expected).
2997
+ */
2998
+ async #sendNotification(method, params) {
2999
+ if (this.#closed) {
3000
+ throw new Error("ACP stream is closed");
3001
+ }
3002
+ await this.#setupSubscription();
3003
+ const envelope = {
3004
+ acp: {
3005
+ jsonrpc: "2.0",
3006
+ method,
3007
+ ...params !== void 0 && { params }
3008
+ },
3009
+ acpContext: {
3010
+ streamId: this.#streamId,
3011
+ sessionId: this.#sessionId,
3012
+ direction: "client-to-agent"
3013
+ }
3014
+ };
3015
+ await this.#mapClient.send({ agent: this.#options.targetAgent }, envelope, {
3016
+ protocol: "acp"
3017
+ });
3018
+ }
3019
+ // ===========================================================================
3020
+ // ACP Lifecycle Methods
3021
+ // ===========================================================================
3022
+ /**
3023
+ * Initialize the ACP connection with the target agent.
3024
+ */
3025
+ async initialize(params) {
3026
+ if (this.#initialized) {
3027
+ throw new Error("ACP stream already initialized");
3028
+ }
3029
+ const result = await this.#sendRequest(
3030
+ ACP_METHODS.INITIALIZE,
3031
+ params
3032
+ );
3033
+ this.#initialized = true;
3034
+ this.#capabilities = result.agentCapabilities ?? null;
3035
+ return result;
3036
+ }
3037
+ /**
3038
+ * Authenticate with the agent.
3039
+ */
3040
+ async authenticate(params) {
3041
+ if (!this.#initialized) {
3042
+ throw new Error("Must call initialize() before authenticate()");
3043
+ }
3044
+ return this.#sendRequest(
3045
+ ACP_METHODS.AUTHENTICATE,
3046
+ params
3047
+ );
3048
+ }
3049
+ // ===========================================================================
3050
+ // ACP Session Methods
3051
+ // ===========================================================================
3052
+ /**
3053
+ * Create a new ACP session.
3054
+ */
3055
+ async newSession(params) {
3056
+ if (!this.#initialized) {
3057
+ throw new Error("Must call initialize() before newSession()");
3058
+ }
3059
+ const result = await this.#sendRequest(
3060
+ ACP_METHODS.SESSION_NEW,
3061
+ params
3062
+ );
3063
+ this.#sessionId = result.sessionId;
3064
+ return result;
3065
+ }
3066
+ /**
3067
+ * Load an existing ACP session.
3068
+ */
3069
+ async loadSession(params) {
3070
+ if (!this.#initialized) {
3071
+ throw new Error("Must call initialize() before loadSession()");
3072
+ }
3073
+ const result = await this.#sendRequest(
3074
+ ACP_METHODS.SESSION_LOAD,
3075
+ params
3076
+ );
3077
+ this.#sessionId = params.sessionId;
3078
+ return result;
3079
+ }
3080
+ /**
3081
+ * Set the session mode.
3082
+ */
3083
+ async setSessionMode(params) {
3084
+ return this.#sendRequest(
3085
+ ACP_METHODS.SESSION_SET_MODE,
3086
+ params
3087
+ );
3088
+ }
3089
+ // ===========================================================================
3090
+ // ACP Prompt Methods
3091
+ // ===========================================================================
3092
+ /**
3093
+ * Send a prompt to the agent.
3094
+ * Updates are received via the sessionUpdate handler.
3095
+ */
3096
+ async prompt(params) {
3097
+ if (!this.#sessionId) {
3098
+ throw new Error("Must call newSession() or loadSession() before prompt()");
3099
+ }
3100
+ return this.#sendRequest(
3101
+ ACP_METHODS.SESSION_PROMPT,
3102
+ params
3103
+ );
3104
+ }
3105
+ /**
3106
+ * Cancel ongoing operations for the current session.
3107
+ */
3108
+ async cancel(params) {
3109
+ if (!this.#sessionId) {
3110
+ throw new Error("No active session to cancel");
3111
+ }
3112
+ await this.#sendNotification(ACP_METHODS.SESSION_CANCEL, {
3113
+ sessionId: this.#sessionId,
3114
+ ...params
3115
+ });
3116
+ }
3117
+ // ===========================================================================
3118
+ // Lifecycle
3119
+ // ===========================================================================
3120
+ /**
3121
+ * Close this ACP stream and clean up resources.
3122
+ */
3123
+ async close() {
3124
+ if (this.#closed) return;
3125
+ this.#closed = true;
3126
+ if (this.#unsubscribeReconnection) {
3127
+ this.#unsubscribeReconnection();
3128
+ this.#unsubscribeReconnection = null;
3129
+ }
3130
+ for (const [id, pending] of this.#pendingRequests) {
3131
+ clearTimeout(pending.timeout);
3132
+ pending.reject(new Error("ACP stream closed"));
3133
+ this.#pendingRequests.delete(id);
3134
+ }
3135
+ if (this.#subscription) {
3136
+ await this.#subscription.unsubscribe();
3137
+ this.#subscription = null;
3138
+ }
3139
+ this.emit("close");
3140
+ }
3141
+ };
3142
+
3143
+ // src/connection/client.ts
3144
+ var ClientConnection = class _ClientConnection {
3145
+ #connection;
3146
+ #subscriptions = /* @__PURE__ */ new Map();
3147
+ #subscriptionStates = /* @__PURE__ */ new Map();
3148
+ #reconnectionHandlers = /* @__PURE__ */ new Set();
3149
+ #acpStreams = /* @__PURE__ */ new Map();
3150
+ #options;
3151
+ #sessionId = null;
3152
+ #serverCapabilities = null;
3153
+ #connected = false;
3154
+ #lastConnectOptions;
3155
+ #isReconnecting = false;
3156
+ constructor(stream, options = {}) {
3157
+ this.#connection = new BaseConnection(stream, options);
3158
+ this.#options = options;
3159
+ this.#connection.setNotificationHandler(this.#handleNotification.bind(this));
3160
+ if (options.reconnection?.enabled && options.createStream) {
3161
+ this.#connection.onStateChange((newState) => {
3162
+ if (newState === "closed" && this.#connected && !this.#isReconnecting) {
3163
+ void this.#handleDisconnect();
3164
+ }
3165
+ });
3166
+ }
3167
+ }
3168
+ // ===========================================================================
3169
+ // Static Factory Methods
3170
+ // ===========================================================================
3171
+ /**
3172
+ * Connect to a MAP server via WebSocket URL.
3173
+ *
3174
+ * Handles:
3175
+ * - WebSocket creation and connection
3176
+ * - Stream wrapping
3177
+ * - Auto-configuration of createStream for reconnection
3178
+ * - Initial MAP protocol connect handshake
3179
+ *
3180
+ * @param url - WebSocket URL (ws:// or wss://)
3181
+ * @param options - Connection options
3182
+ * @returns Connected ClientConnection instance
3183
+ *
3184
+ * @example
3185
+ * ```typescript
3186
+ * const client = await ClientConnection.connect('ws://localhost:8080', {
3187
+ * name: 'MyClient',
3188
+ * reconnection: true
3189
+ * });
3190
+ *
3191
+ * // Already connected, ready to use
3192
+ * const agents = await client.listAgents();
3193
+ * ```
3194
+ */
3195
+ static async connect(url, options) {
3196
+ const parsedUrl = new URL(url);
3197
+ if (!["ws:", "wss:"].includes(parsedUrl.protocol)) {
3198
+ throw new Error(
3199
+ `Unsupported protocol: ${parsedUrl.protocol}. Use ws: or wss:`
3200
+ );
3201
+ }
3202
+ const timeout = options?.connectTimeout ?? 1e4;
3203
+ const ws = new WebSocket(url);
3204
+ await waitForOpen(ws, timeout);
3205
+ const stream = websocketStream(ws);
3206
+ const createStream = async () => {
3207
+ const newWs = new WebSocket(url);
3208
+ await waitForOpen(newWs, timeout);
3209
+ return websocketStream(newWs);
3210
+ };
3211
+ const reconnection = options?.reconnection === true ? { enabled: true } : typeof options?.reconnection === "object" ? options.reconnection : void 0;
3212
+ const client = new _ClientConnection(stream, {
3213
+ name: options?.name,
3214
+ capabilities: options?.capabilities,
3215
+ createStream,
3216
+ reconnection
3217
+ });
3218
+ await client.connect({ auth: options?.auth });
3219
+ return client;
3220
+ }
3221
+ // ===========================================================================
3222
+ // Connection Lifecycle
3223
+ // ===========================================================================
3224
+ /**
3225
+ * Connect to the MAP system
3226
+ */
3227
+ async connect(options) {
3228
+ const params = {
3229
+ protocolVersion: PROTOCOL_VERSION,
3230
+ participantType: "client",
3231
+ name: this.#options.name,
3232
+ capabilities: this.#options.capabilities,
3233
+ sessionId: options?.sessionId,
3234
+ resumeToken: options?.resumeToken,
3235
+ auth: options?.auth
3236
+ };
3237
+ const result = await this.#connection.sendRequest(CORE_METHODS.CONNECT, params);
3238
+ this.#sessionId = result.sessionId;
3239
+ this.#serverCapabilities = result.capabilities;
3240
+ this.#connected = true;
3241
+ this.#connection._transitionTo("connected");
3242
+ this.#lastConnectOptions = options;
3243
+ return result;
3244
+ }
3245
+ /**
3246
+ * Disconnect from the MAP system
3247
+ * @param reason - Optional reason for disconnecting
3248
+ * @returns Resume token that can be used to resume this session later
3249
+ */
3250
+ async disconnect(reason) {
3251
+ if (!this.#connected) return void 0;
3252
+ let resumeToken;
3253
+ try {
3254
+ const result = await this.#connection.sendRequest(
3255
+ CORE_METHODS.DISCONNECT,
3256
+ reason ? { reason } : void 0
3257
+ );
3258
+ resumeToken = result.resumeToken;
3259
+ } finally {
3260
+ for (const stream of this.#acpStreams.values()) {
3261
+ await stream.close();
3262
+ }
3263
+ this.#acpStreams.clear();
3264
+ for (const subscription of this.#subscriptions.values()) {
3265
+ subscription._close();
3266
+ }
3267
+ this.#subscriptions.clear();
3268
+ await this.#connection.close();
3269
+ this.#connected = false;
3270
+ }
3271
+ return resumeToken;
3272
+ }
3273
+ /**
3274
+ * Whether the client is connected
3275
+ */
3276
+ get isConnected() {
3277
+ return this.#connected && !this.#connection.isClosed;
3278
+ }
3279
+ /**
3280
+ * Current session ID
3281
+ */
3282
+ get sessionId() {
3283
+ return this.#sessionId;
3284
+ }
3285
+ /**
3286
+ * Server capabilities
3287
+ */
3288
+ get serverCapabilities() {
3289
+ return this.#serverCapabilities;
3290
+ }
3291
+ /**
3292
+ * AbortSignal that triggers when the connection closes
2438
3293
  */
2439
3294
  get signal() {
2440
3295
  return this.#connection.signal;
@@ -2587,6 +3442,65 @@ var ClientConnection = class {
2587
3442
  }
2588
3443
  }
2589
3444
  // ===========================================================================
3445
+ // ACP Streams
3446
+ // ===========================================================================
3447
+ /**
3448
+ * Create a virtual ACP stream connection to an agent.
3449
+ *
3450
+ * This allows clients to interact with ACP-compatible agents using the
3451
+ * familiar ACP interface while routing all messages through MAP.
3452
+ *
3453
+ * @param options - Stream configuration options
3454
+ * @returns ACPStreamConnection instance ready for initialize()
3455
+ *
3456
+ * @example
3457
+ * ```typescript
3458
+ * const acp = client.createACPStream({
3459
+ * targetAgent: 'coding-agent-1',
3460
+ * client: {
3461
+ * requestPermission: async (req) => ({
3462
+ * outcome: { outcome: 'selected', optionId: 'allow' }
3463
+ * }),
3464
+ * sessionUpdate: async (update) => {
3465
+ * console.log('Agent update:', update);
3466
+ * }
3467
+ * }
3468
+ * });
3469
+ *
3470
+ * await acp.initialize({
3471
+ * protocolVersion: 20241007,
3472
+ * clientInfo: { name: 'IDE', version: '1.0' }
3473
+ * });
3474
+ * const { sessionId } = await acp.newSession({ cwd: '/project', mcpServers: [] });
3475
+ * const result = await acp.prompt({
3476
+ * sessionId,
3477
+ * prompt: [{ type: 'text', text: 'Hello' }]
3478
+ * });
3479
+ *
3480
+ * await acp.close();
3481
+ * ```
3482
+ */
3483
+ createACPStream(options) {
3484
+ const stream = new ACPStreamConnection(this, options);
3485
+ this.#acpStreams.set(stream.streamId, stream);
3486
+ stream.on("close", () => {
3487
+ this.#acpStreams.delete(stream.streamId);
3488
+ });
3489
+ return stream;
3490
+ }
3491
+ /**
3492
+ * Get an active ACP stream by ID.
3493
+ */
3494
+ getACPStream(streamId) {
3495
+ return this.#acpStreams.get(streamId);
3496
+ }
3497
+ /**
3498
+ * Get all active ACP streams.
3499
+ */
3500
+ get acpStreams() {
3501
+ return this.#acpStreams;
3502
+ }
3503
+ // ===========================================================================
2590
3504
  // Subscriptions
2591
3505
  // ===========================================================================
2592
3506
  /**
@@ -2948,99 +3862,6 @@ var ClientConnection = class {
2948
3862
  }
2949
3863
  };
2950
3864
 
2951
- // src/stream/index.ts
2952
- function createStreamPair() {
2953
- const clientToServer = [];
2954
- const serverToClient = [];
2955
- let clientToServerResolver = null;
2956
- let serverToClientResolver = null;
2957
- let clientToServerClosed = false;
2958
- let serverToClientClosed = false;
2959
- function createReadable(queue, _getResolver, setResolver, isClosed) {
2960
- return new ReadableStream({
2961
- async pull(controller) {
2962
- if (queue.length > 0) {
2963
- controller.enqueue(queue.shift());
2964
- return;
2965
- }
2966
- if (isClosed()) {
2967
- controller.close();
2968
- return;
2969
- }
2970
- const message = await new Promise((resolve) => {
2971
- setResolver((msg) => {
2972
- setResolver(null);
2973
- resolve(msg);
2974
- });
2975
- });
2976
- if (message === null) {
2977
- controller.close();
2978
- } else {
2979
- controller.enqueue(message);
2980
- }
2981
- }
2982
- });
2983
- }
2984
- function createWritable(queue, getResolver, setClosed) {
2985
- return new WritableStream({
2986
- write(message) {
2987
- const resolver = getResolver();
2988
- if (resolver) {
2989
- resolver(message);
2990
- } else {
2991
- queue.push(message);
2992
- }
2993
- },
2994
- close() {
2995
- setClosed();
2996
- const resolver = getResolver();
2997
- if (resolver) {
2998
- resolver(null);
2999
- }
3000
- }
3001
- });
3002
- }
3003
- const clientStream = {
3004
- // Client writes to server
3005
- writable: createWritable(
3006
- clientToServer,
3007
- () => clientToServerResolver,
3008
- () => {
3009
- clientToServerClosed = true;
3010
- }
3011
- ),
3012
- // Client reads from server
3013
- readable: createReadable(
3014
- serverToClient,
3015
- () => serverToClientResolver,
3016
- (r) => {
3017
- serverToClientResolver = r;
3018
- },
3019
- () => serverToClientClosed
3020
- )
3021
- };
3022
- const serverStream = {
3023
- // Server writes to client
3024
- writable: createWritable(
3025
- serverToClient,
3026
- () => serverToClientResolver,
3027
- () => {
3028
- serverToClientClosed = true;
3029
- }
3030
- ),
3031
- // Server reads from client
3032
- readable: createReadable(
3033
- clientToServer,
3034
- () => clientToServerResolver,
3035
- (r) => {
3036
- clientToServerResolver = r;
3037
- },
3038
- () => clientToServerClosed
3039
- )
3040
- };
3041
- return [clientStream, serverStream];
3042
- }
3043
-
3044
3865
  // src/testing/client.ts
3045
3866
  var TestClient = class _TestClient {
3046
3867
  #connection;
@@ -3233,7 +4054,7 @@ var TestClient = class _TestClient {
3233
4054
  };
3234
4055
 
3235
4056
  // src/connection/agent.ts
3236
- var AgentConnection = class {
4057
+ var AgentConnection = class _AgentConnection {
3237
4058
  #connection;
3238
4059
  #subscriptions = /* @__PURE__ */ new Map();
3239
4060
  #options;
@@ -3260,6 +4081,66 @@ var AgentConnection = class {
3260
4081
  }
3261
4082
  }
3262
4083
  // ===========================================================================
4084
+ // Static Factory Methods
4085
+ // ===========================================================================
4086
+ /**
4087
+ * Connect and register an agent via WebSocket URL.
4088
+ *
4089
+ * Handles:
4090
+ * - WebSocket creation and connection
4091
+ * - Stream wrapping
4092
+ * - Auto-configuration of createStream for reconnection
4093
+ * - Initial MAP protocol connect handshake
4094
+ * - Agent registration
4095
+ *
4096
+ * @param url - WebSocket URL (ws:// or wss://)
4097
+ * @param options - Connection and agent options
4098
+ * @returns Connected and registered AgentConnection instance
4099
+ *
4100
+ * @example
4101
+ * ```typescript
4102
+ * const agent = await AgentConnection.connect('ws://localhost:8080', {
4103
+ * name: 'Worker',
4104
+ * role: 'processor',
4105
+ * reconnection: true
4106
+ * });
4107
+ *
4108
+ * // Already registered, ready to work
4109
+ * agent.onMessage(handleMessage);
4110
+ * await agent.busy();
4111
+ * ```
4112
+ */
4113
+ static async connect(url, options) {
4114
+ const parsedUrl = new URL(url);
4115
+ if (!["ws:", "wss:"].includes(parsedUrl.protocol)) {
4116
+ throw new Error(
4117
+ `Unsupported protocol: ${parsedUrl.protocol}. Use ws: or wss:`
4118
+ );
4119
+ }
4120
+ const timeout = options?.connectTimeout ?? 1e4;
4121
+ const ws = new WebSocket(url);
4122
+ await waitForOpen(ws, timeout);
4123
+ const stream = websocketStream(ws);
4124
+ const createStream = async () => {
4125
+ const newWs = new WebSocket(url);
4126
+ await waitForOpen(newWs, timeout);
4127
+ return websocketStream(newWs);
4128
+ };
4129
+ const reconnection = options?.reconnection === true ? { enabled: true } : typeof options?.reconnection === "object" ? options.reconnection : void 0;
4130
+ const agent = new _AgentConnection(stream, {
4131
+ name: options?.name,
4132
+ role: options?.role,
4133
+ capabilities: options?.capabilities,
4134
+ visibility: options?.visibility,
4135
+ parent: options?.parent,
4136
+ scopes: options?.scopes,
4137
+ createStream,
4138
+ reconnection
4139
+ });
4140
+ await agent.connect({ auth: options?.auth });
4141
+ return agent;
4142
+ }
4143
+ // ===========================================================================
3263
4144
  // Connection Lifecycle
3264
4145
  // ===========================================================================
3265
4146
  /**
@@ -3272,6 +4153,7 @@ var AgentConnection = class {
3272
4153
  participantId: options?.agentId,
3273
4154
  name: this.#options.name,
3274
4155
  capabilities: this.#options.capabilities,
4156
+ resumeToken: options?.resumeToken,
3275
4157
  auth: options?.auth
3276
4158
  };
3277
4159
  const connectResult = await this.#connection.sendRequest(CORE_METHODS.CONNECT, connectParams);
@@ -3296,9 +4178,12 @@ var AgentConnection = class {
3296
4178
  }
3297
4179
  /**
3298
4180
  * Disconnect from the MAP system
4181
+ * @param reason - Optional reason for disconnecting
4182
+ * @returns Resume token that can be used to resume this session later
3299
4183
  */
3300
4184
  async disconnect(reason) {
3301
- if (!this.#connected) return;
4185
+ if (!this.#connected) return void 0;
4186
+ let resumeToken;
3302
4187
  try {
3303
4188
  if (this.#agentId) {
3304
4189
  await this.#connection.sendRequest(LIFECYCLE_METHODS.AGENTS_UNREGISTER, {
@@ -3306,10 +4191,11 @@ var AgentConnection = class {
3306
4191
  reason
3307
4192
  });
3308
4193
  }
3309
- await this.#connection.sendRequest(
4194
+ const result = await this.#connection.sendRequest(
3310
4195
  CORE_METHODS.DISCONNECT,
3311
4196
  reason ? { reason } : void 0
3312
4197
  );
4198
+ resumeToken = result.resumeToken;
3313
4199
  } finally {
3314
4200
  for (const subscription of this.#subscriptions.values()) {
3315
4201
  subscription._close();
@@ -3318,6 +4204,7 @@ var AgentConnection = class {
3318
4204
  await this.#connection.close();
3319
4205
  this.#connected = false;
3320
4206
  }
4207
+ return resumeToken;
3321
4208
  }
3322
4209
  /**
3323
4210
  * Whether the agent is connected