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