@multi-agent-protocol/sdk 0.0.4 → 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] : [];
@@ -2554,12 +2564,589 @@ function createSubscription(id, unsubscribe, options, sendAck) {
2554
2564
  return new Subscription(id, unsubscribe, options, sendAck);
2555
2565
  }
2556
2566
 
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;
2632
+ #options;
2633
+ #streamId;
2634
+ #pendingRequests = /* @__PURE__ */ new Map();
2635
+ #subscription = null;
2636
+ #sessionId = null;
2637
+ #initialized = false;
2638
+ #capabilities = null;
2639
+ #closed = false;
2640
+ #lastEventId = null;
2641
+ #isReconnecting = false;
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;
2652
+ this.#options = options;
2653
+ this.#streamId = `acp-${Date.now()}-${Math.random().toString(36).slice(2)}`;
2654
+ this.#unsubscribeReconnection = mapClient.onReconnection((event) => {
2655
+ void this.#handleReconnectionEvent(event);
2656
+ });
2657
+ }
2658
+ // ===========================================================================
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
2695
+ // ===========================================================================
2696
+ /**
2697
+ * Handle MAP reconnection events.
2698
+ */
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
+ }
2719
+ }
2720
+ /**
2721
+ * Handle successful MAP reconnection.
2722
+ */
2723
+ async #handleReconnected() {
2724
+ this.#subscription = null;
2725
+ try {
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
+ }
2737
+ }
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)));
2743
+ }
2744
+ }
2745
+ /**
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.
2750
+ */
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
+ }
2762
+ }
2763
+ // ===========================================================================
2764
+ // Internal Methods
2765
+ // ===========================================================================
2766
+ /**
2767
+ * Set up the subscription for receiving messages from the target agent.
2768
+ */
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();
2775
+ }
2776
+ /**
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
+
2557
3143
  // src/connection/client.ts
2558
3144
  var ClientConnection = class _ClientConnection {
2559
3145
  #connection;
2560
3146
  #subscriptions = /* @__PURE__ */ new Map();
2561
3147
  #subscriptionStates = /* @__PURE__ */ new Map();
2562
3148
  #reconnectionHandlers = /* @__PURE__ */ new Set();
3149
+ #acpStreams = /* @__PURE__ */ new Map();
2563
3150
  #options;
2564
3151
  #sessionId = null;
2565
3152
  #serverCapabilities = null;
@@ -2670,6 +3257,10 @@ var ClientConnection = class _ClientConnection {
2670
3257
  );
2671
3258
  resumeToken = result.resumeToken;
2672
3259
  } finally {
3260
+ for (const stream of this.#acpStreams.values()) {
3261
+ await stream.close();
3262
+ }
3263
+ this.#acpStreams.clear();
2673
3264
  for (const subscription of this.#subscriptions.values()) {
2674
3265
  subscription._close();
2675
3266
  }
@@ -2851,6 +3442,65 @@ var ClientConnection = class _ClientConnection {
2851
3442
  }
2852
3443
  }
2853
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
+ // ===========================================================================
2854
3504
  // Subscriptions
2855
3505
  // ===========================================================================
2856
3506
  /**