@multi-agent-protocol/sdk 0.0.4 → 0.0.6

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,85 @@
1
+ import { TunnelStream } from 'agentic-mesh';
1
2
  import { ulid } from 'ulid';
3
+ import { EventEmitter } from 'events';
4
+
5
+ var __defProp = Object.defineProperty;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __esm = (fn, res) => function __init() {
8
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
9
+ };
10
+ var __export = (target, all) => {
11
+ for (var name in all)
12
+ __defProp(target, name, { get: all[name], enumerable: true });
13
+ };
14
+
15
+ // src/stream/agentic-mesh.ts
16
+ var agentic_mesh_exports = {};
17
+ __export(agentic_mesh_exports, {
18
+ agenticMeshStream: () => agenticMeshStream
19
+ });
20
+ async function agenticMeshStream(config) {
21
+ if (!config.transport.active) {
22
+ await config.transport.start();
23
+ }
24
+ const connected = await config.transport.connect(config.peer);
25
+ if (!connected) {
26
+ throw new Error(`Failed to connect to peer: ${config.peer.peerId}`);
27
+ }
28
+ const streamId = `map-${config.localPeerId}-${Date.now()}`;
29
+ const tunnelStream = new TunnelStream({
30
+ transport: config.transport,
31
+ peerId: config.peer.peerId,
32
+ streamId
33
+ });
34
+ await tunnelStream.open();
35
+ return tunnelStreamToMapStream(tunnelStream);
36
+ }
37
+ function tunnelStreamToMapStream(tunnel) {
38
+ let readingAborted = false;
39
+ const readable = new ReadableStream({
40
+ async start(controller) {
41
+ try {
42
+ for await (const frame of tunnel) {
43
+ if (readingAborted) break;
44
+ controller.enqueue(frame);
45
+ }
46
+ if (!readingAborted) {
47
+ controller.close();
48
+ }
49
+ } catch (error) {
50
+ if (!readingAborted) {
51
+ controller.error(error);
52
+ }
53
+ }
54
+ },
55
+ cancel() {
56
+ readingAborted = true;
57
+ tunnel.close().catch(() => {
58
+ });
59
+ }
60
+ });
61
+ const writable = new WritableStream({
62
+ async write(message) {
63
+ if (!tunnel.isOpen) {
64
+ throw new Error("Stream is not open");
65
+ }
66
+ await tunnel.write(message);
67
+ },
68
+ async close() {
69
+ await tunnel.close();
70
+ },
71
+ abort() {
72
+ readingAborted = true;
73
+ tunnel.close().catch(() => {
74
+ });
75
+ }
76
+ });
77
+ return { readable, writable };
78
+ }
79
+ var init_agentic_mesh = __esm({
80
+ "src/stream/agentic-mesh.ts"() {
81
+ }
82
+ });
2
83
 
3
84
  // src/jsonrpc/index.ts
4
85
  function isRequest(message) {
@@ -61,6 +142,7 @@ var EVENT_TYPES = {
61
142
  PARTICIPANT_DISCONNECTED: "participant_disconnected",
62
143
  // Message events
63
144
  MESSAGE_SENT: "message_sent",
145
+ MESSAGE_DELIVERED: "message_delivered",
64
146
  // Scope events
65
147
  SCOPE_CREATED: "scope_created",
66
148
  SCOPE_MEMBER_JOINED: "scope_member_joined",
@@ -108,6 +190,10 @@ var SESSION_METHODS = {
108
190
  SESSION_LOAD: "map/session/load",
109
191
  SESSION_CLOSE: "map/session/close"
110
192
  };
193
+ var AUTH_METHODS = {
194
+ AUTHENTICATE: "map/authenticate",
195
+ AUTH_REFRESH: "map/auth/refresh"
196
+ };
111
197
  var PERMISSION_METHODS = {
112
198
  PERMISSIONS_UPDATE: "map/permissions/update"
113
199
  };
@@ -118,8 +204,7 @@ var NOTIFICATION_METHODS = {
118
204
  EVENT: "map/event",
119
205
  MESSAGE: "map/message",
120
206
  /** Client acknowledges received events (for backpressure) */
121
- SUBSCRIBE_ACK: "map/subscribe.ack"
122
- };
207
+ SUBSCRIBE_ACK: "map/subscribe.ack"};
123
208
  var PROTOCOL_ERROR_CODES = {
124
209
  PARSE_ERROR: -32700,
125
210
  INVALID_REQUEST: -32600,
@@ -131,7 +216,10 @@ var AUTH_ERROR_CODES = {
131
216
  AUTH_REQUIRED: 1e3,
132
217
  AUTH_FAILED: 1001,
133
218
  TOKEN_EXPIRED: 1002,
134
- PERMISSION_DENIED: 1003
219
+ PERMISSION_DENIED: 1003,
220
+ INSUFFICIENT_SCOPE: 1004,
221
+ METHOD_NOT_SUPPORTED: 1005,
222
+ INVALID_CREDENTIALS: 1006
135
223
  };
136
224
  var ROUTING_ERROR_CODES = {
137
225
  ADDRESS_NOT_FOUND: 2e3,
@@ -270,6 +358,27 @@ var MAPRequestError = class _MAPRequestError extends Error {
270
358
  { category: "auth" }
271
359
  );
272
360
  }
361
+ static insufficientScope(required) {
362
+ return new _MAPRequestError(
363
+ AUTH_ERROR_CODES.INSUFFICIENT_SCOPE,
364
+ required ? `Insufficient scope: ${required}` : "Insufficient scope",
365
+ { category: "auth" }
366
+ );
367
+ }
368
+ static methodNotSupported(method) {
369
+ return new _MAPRequestError(
370
+ AUTH_ERROR_CODES.METHOD_NOT_SUPPORTED,
371
+ `Authentication method not supported: ${method}`,
372
+ { category: "auth" }
373
+ );
374
+ }
375
+ static invalidCredentials(details) {
376
+ return new _MAPRequestError(
377
+ AUTH_ERROR_CODES.INVALID_CREDENTIALS,
378
+ details ?? "Invalid credentials",
379
+ { category: "auth" }
380
+ );
381
+ }
273
382
  // ==========================================================================
274
383
  // Routing Errors (2xxx)
275
384
  // ==========================================================================
@@ -1396,6 +1505,11 @@ var TestServer = class {
1396
1505
  if (participant) {
1397
1506
  this.deliverMessage(recipientId, message);
1398
1507
  delivered.push(recipientId);
1508
+ this.emitEvent({
1509
+ type: EVENT_TYPES.MESSAGE_DELIVERED,
1510
+ source: senderAgentId ?? senderId,
1511
+ data: { messageId, message, correlationId: params.meta?.correlationId }
1512
+ });
1399
1513
  }
1400
1514
  }
1401
1515
  this.emitEvent({
@@ -1756,6 +1870,9 @@ var TestServer = class {
1756
1870
  if (typeof address === "string") {
1757
1871
  return [this.#findParticipantForAgent(address) ?? address];
1758
1872
  }
1873
+ if ("participant" in address && address.participant) {
1874
+ return this.#participants.has(address.participant) ? [address.participant] : [];
1875
+ }
1759
1876
  if ("agent" in address && !("system" in address)) {
1760
1877
  const participantId = this.#findParticipantForAgent(address.agent);
1761
1878
  return participantId ? [participantId] : [];
@@ -1841,11 +1958,6 @@ var TestServer = class {
1841
1958
  return false;
1842
1959
  }
1843
1960
  }
1844
- if (filter.agents?.length && event.source) {
1845
- if (!filter.agents.includes(event.source)) {
1846
- return false;
1847
- }
1848
- }
1849
1961
  if (filter.fromAgents?.length && event.source) {
1850
1962
  if (!filter.fromAgents.includes(event.source)) {
1851
1963
  return false;
@@ -2033,6 +2145,7 @@ var TestServer = class {
2033
2145
  };
2034
2146
 
2035
2147
  // src/stream/index.ts
2148
+ init_agentic_mesh();
2036
2149
  function websocketStream(ws) {
2037
2150
  const messageQueue = [];
2038
2151
  let messageResolver = null;
@@ -2552,18 +2665,624 @@ function createSubscription(id, unsubscribe, options, sendAck) {
2552
2665
  return new Subscription(id, unsubscribe, options, sendAck);
2553
2666
  }
2554
2667
 
2668
+ // src/acp/types.ts
2669
+ var ACPError = class _ACPError extends Error {
2670
+ code;
2671
+ data;
2672
+ constructor(code, message, data) {
2673
+ super(message);
2674
+ this.name = "ACPError";
2675
+ this.code = code;
2676
+ this.data = data;
2677
+ }
2678
+ /**
2679
+ * Create an ACPError from an error response.
2680
+ */
2681
+ static fromResponse(error) {
2682
+ return new _ACPError(error.code, error.message, error.data);
2683
+ }
2684
+ /**
2685
+ * Convert to JSON-RPC error object.
2686
+ */
2687
+ toErrorObject() {
2688
+ return {
2689
+ code: this.code,
2690
+ message: this.message,
2691
+ ...this.data !== void 0 && { data: this.data }
2692
+ };
2693
+ }
2694
+ };
2695
+ var ACP_METHODS = {
2696
+ // Lifecycle
2697
+ INITIALIZE: "initialize",
2698
+ AUTHENTICATE: "authenticate",
2699
+ // Session management
2700
+ SESSION_NEW: "session/new",
2701
+ SESSION_LOAD: "session/load",
2702
+ SESSION_SET_MODE: "session/set_mode",
2703
+ // Prompt
2704
+ SESSION_PROMPT: "session/prompt",
2705
+ SESSION_CANCEL: "session/cancel",
2706
+ // Notifications
2707
+ SESSION_UPDATE: "session/update",
2708
+ // Agent→Client requests
2709
+ REQUEST_PERMISSION: "request_permission",
2710
+ FS_READ_TEXT_FILE: "fs/read_text_file",
2711
+ FS_WRITE_TEXT_FILE: "fs/write_text_file",
2712
+ TERMINAL_CREATE: "terminal/create",
2713
+ TERMINAL_OUTPUT: "terminal/output",
2714
+ TERMINAL_RELEASE: "terminal/release",
2715
+ TERMINAL_WAIT_FOR_EXIT: "terminal/wait_for_exit",
2716
+ TERMINAL_KILL: "terminal/kill"
2717
+ };
2718
+ function isACPEnvelope(payload) {
2719
+ if (typeof payload !== "object" || payload === null) {
2720
+ return false;
2721
+ }
2722
+ const envelope = payload;
2723
+ if (typeof envelope.acp !== "object" || envelope.acp === null || typeof envelope.acpContext !== "object" || envelope.acpContext === null) {
2724
+ return false;
2725
+ }
2726
+ const acpContext = envelope.acpContext;
2727
+ return typeof acpContext.streamId === "string";
2728
+ }
2729
+
2730
+ // src/acp/stream.ts
2731
+ var ACPStreamConnection = class extends EventEmitter {
2732
+ #mapClient;
2733
+ #options;
2734
+ #streamId;
2735
+ #pendingRequests = /* @__PURE__ */ new Map();
2736
+ #subscription = null;
2737
+ #sessionId = null;
2738
+ #initialized = false;
2739
+ #capabilities = null;
2740
+ #closed = false;
2741
+ #lastEventId = null;
2742
+ #isReconnecting = false;
2743
+ #unsubscribeReconnection = null;
2744
+ /**
2745
+ * Create a new ACP stream connection.
2746
+ *
2747
+ * @param mapClient - The underlying MAP client connection
2748
+ * @param options - Stream configuration options
2749
+ */
2750
+ constructor(mapClient, options) {
2751
+ super();
2752
+ this.#mapClient = mapClient;
2753
+ this.#options = options;
2754
+ this.#streamId = `acp-${Date.now()}-${Math.random().toString(36).slice(2)}`;
2755
+ this.#unsubscribeReconnection = mapClient.onReconnection((event) => {
2756
+ void this.#handleReconnectionEvent(event);
2757
+ });
2758
+ }
2759
+ // ===========================================================================
2760
+ // Public Properties
2761
+ // ===========================================================================
2762
+ /** Unique identifier for this ACP stream */
2763
+ get streamId() {
2764
+ return this.#streamId;
2765
+ }
2766
+ /** Target agent this stream connects to */
2767
+ get targetAgent() {
2768
+ return this.#options.targetAgent;
2769
+ }
2770
+ /** Current ACP session ID (null until newSession called) */
2771
+ get sessionId() {
2772
+ return this.#sessionId;
2773
+ }
2774
+ /** Whether initialize() has been called */
2775
+ get initialized() {
2776
+ return this.#initialized;
2777
+ }
2778
+ /** Agent capabilities from initialize response */
2779
+ get capabilities() {
2780
+ return this.#capabilities;
2781
+ }
2782
+ /** Whether the stream is closed */
2783
+ get isClosed() {
2784
+ return this.#closed;
2785
+ }
2786
+ /** Last processed event ID (for reconnection support) */
2787
+ get lastEventId() {
2788
+ return this.#lastEventId;
2789
+ }
2790
+ /** Whether the stream is currently reconnecting */
2791
+ get isReconnecting() {
2792
+ return this.#isReconnecting;
2793
+ }
2794
+ // ===========================================================================
2795
+ // Reconnection Handling
2796
+ // ===========================================================================
2797
+ /**
2798
+ * Handle MAP reconnection events.
2799
+ */
2800
+ async #handleReconnectionEvent(event) {
2801
+ if (this.#closed) return;
2802
+ switch (event.type) {
2803
+ case "disconnected":
2804
+ this.#isReconnecting = true;
2805
+ this.emit("reconnecting");
2806
+ for (const [id, pending] of this.#pendingRequests) {
2807
+ clearTimeout(pending.timeout);
2808
+ pending.reject(new Error("Connection lost during ACP request"));
2809
+ this.#pendingRequests.delete(id);
2810
+ }
2811
+ break;
2812
+ case "reconnected":
2813
+ await this.#handleReconnected();
2814
+ break;
2815
+ case "reconnectFailed":
2816
+ this.#isReconnecting = false;
2817
+ this.emit("error", event.error ?? new Error("MAP reconnection failed"));
2818
+ break;
2819
+ }
2820
+ }
2821
+ /**
2822
+ * Handle successful MAP reconnection.
2823
+ */
2824
+ async #handleReconnected() {
2825
+ this.#subscription = null;
2826
+ try {
2827
+ await this.#setupSubscription();
2828
+ if (this.#sessionId) {
2829
+ const sessionValid = await this.#verifySessionValid();
2830
+ if (!sessionValid) {
2831
+ const lostSessionId = this.#sessionId;
2832
+ this.#sessionId = null;
2833
+ this.emit("sessionLost", {
2834
+ sessionId: lostSessionId,
2835
+ reason: "Session no longer valid after reconnection"
2836
+ });
2837
+ }
2838
+ }
2839
+ this.#isReconnecting = false;
2840
+ this.emit("reconnected");
2841
+ } catch (error) {
2842
+ this.#isReconnecting = false;
2843
+ this.emit("error", error instanceof Error ? error : new Error(String(error)));
2844
+ }
2845
+ }
2846
+ /**
2847
+ * Verify that the current ACP session is still valid.
2848
+ *
2849
+ * Since ACP doesn't have a dedicated status check method, we attempt
2850
+ * a lightweight operation (cancel with no effect) to verify the session.
2851
+ */
2852
+ async #verifySessionValid() {
2853
+ if (!this.#sessionId) return false;
2854
+ try {
2855
+ await this.#sendNotification(ACP_METHODS.SESSION_CANCEL, {
2856
+ sessionId: this.#sessionId,
2857
+ reason: "session_verification"
2858
+ });
2859
+ return true;
2860
+ } catch {
2861
+ return false;
2862
+ }
2863
+ }
2864
+ // ===========================================================================
2865
+ // Internal Methods
2866
+ // ===========================================================================
2867
+ /**
2868
+ * Set up the subscription for receiving messages from the target agent.
2869
+ */
2870
+ async #setupSubscription() {
2871
+ if (this.#subscription) return;
2872
+ this.#subscription = await this.#mapClient.subscribe({
2873
+ fromAgents: [this.#options.targetAgent]
2874
+ });
2875
+ void this.#processEvents();
2876
+ }
2877
+ /**
2878
+ * Process incoming events from the subscription.
2879
+ */
2880
+ async #processEvents() {
2881
+ if (!this.#subscription) return;
2882
+ try {
2883
+ for await (const event of this.#subscription) {
2884
+ if (this.#closed) break;
2885
+ if (event.id) {
2886
+ this.#lastEventId = event.id;
2887
+ }
2888
+ if (event.type === "message_delivered" && event.data) {
2889
+ const message = event.data.message;
2890
+ if (message?.payload) {
2891
+ await this.#handleIncomingMessage(message);
2892
+ }
2893
+ }
2894
+ }
2895
+ } catch (error) {
2896
+ if (!this.#closed) {
2897
+ this.emit("error", error);
2898
+ }
2899
+ }
2900
+ }
2901
+ /**
2902
+ * Handle an incoming message from the target agent.
2903
+ */
2904
+ async #handleIncomingMessage(message) {
2905
+ const payload = message.payload;
2906
+ if (!isACPEnvelope(payload)) return;
2907
+ const envelope = payload;
2908
+ const { acp, acpContext } = envelope;
2909
+ if (acpContext.streamId !== this.#streamId) return;
2910
+ if (acp.id !== void 0 && !acp.method) {
2911
+ const requestId = String(acp.id);
2912
+ const pending = this.#pendingRequests.get(requestId);
2913
+ if (pending) {
2914
+ clearTimeout(pending.timeout);
2915
+ this.#pendingRequests.delete(requestId);
2916
+ if (acp.error) {
2917
+ pending.reject(ACPError.fromResponse(acp.error));
2918
+ } else {
2919
+ pending.resolve(acp.result);
2920
+ }
2921
+ }
2922
+ return;
2923
+ }
2924
+ if (acp.method && acp.id === void 0) {
2925
+ await this.#handleNotification(acp.method, acp.params, acpContext);
2926
+ return;
2927
+ }
2928
+ if (acp.method && acp.id !== void 0) {
2929
+ await this.#handleAgentRequest(acp.id, acp.method, acp.params, acpContext, message);
2930
+ }
2931
+ }
2932
+ /**
2933
+ * Handle an ACP notification from the agent.
2934
+ */
2935
+ async #handleNotification(method, params, _acpContext) {
2936
+ if (method === ACP_METHODS.SESSION_UPDATE) {
2937
+ await this.#options.client.sessionUpdate(params);
2938
+ }
2939
+ }
2940
+ /**
2941
+ * Handle an agent→client request.
2942
+ */
2943
+ async #handleAgentRequest(requestId, method, params, _ctx, originalMessage) {
2944
+ let result;
2945
+ let error;
2946
+ try {
2947
+ switch (method) {
2948
+ case ACP_METHODS.REQUEST_PERMISSION:
2949
+ result = await this.#options.client.requestPermission(
2950
+ params
2951
+ );
2952
+ break;
2953
+ case ACP_METHODS.FS_READ_TEXT_FILE:
2954
+ if (!this.#options.client.readTextFile) {
2955
+ throw new ACPError(-32601, "Method not supported: fs/read_text_file");
2956
+ }
2957
+ result = await this.#options.client.readTextFile(
2958
+ params
2959
+ );
2960
+ break;
2961
+ case ACP_METHODS.FS_WRITE_TEXT_FILE:
2962
+ if (!this.#options.client.writeTextFile) {
2963
+ throw new ACPError(-32601, "Method not supported: fs/write_text_file");
2964
+ }
2965
+ result = await this.#options.client.writeTextFile(
2966
+ params
2967
+ );
2968
+ break;
2969
+ case ACP_METHODS.TERMINAL_CREATE:
2970
+ if (!this.#options.client.createTerminal) {
2971
+ throw new ACPError(-32601, "Method not supported: terminal/create");
2972
+ }
2973
+ result = await this.#options.client.createTerminal(
2974
+ params
2975
+ );
2976
+ break;
2977
+ case ACP_METHODS.TERMINAL_OUTPUT:
2978
+ if (!this.#options.client.terminalOutput) {
2979
+ throw new ACPError(-32601, "Method not supported: terminal/output");
2980
+ }
2981
+ result = await this.#options.client.terminalOutput(
2982
+ params
2983
+ );
2984
+ break;
2985
+ case ACP_METHODS.TERMINAL_RELEASE:
2986
+ if (!this.#options.client.releaseTerminal) {
2987
+ throw new ACPError(-32601, "Method not supported: terminal/release");
2988
+ }
2989
+ result = await this.#options.client.releaseTerminal(
2990
+ params
2991
+ );
2992
+ break;
2993
+ case ACP_METHODS.TERMINAL_WAIT_FOR_EXIT:
2994
+ if (!this.#options.client.waitForTerminalExit) {
2995
+ throw new ACPError(-32601, "Method not supported: terminal/wait_for_exit");
2996
+ }
2997
+ result = await this.#options.client.waitForTerminalExit(
2998
+ params
2999
+ );
3000
+ break;
3001
+ case ACP_METHODS.TERMINAL_KILL:
3002
+ if (!this.#options.client.killTerminal) {
3003
+ throw new ACPError(-32601, "Method not supported: terminal/kill");
3004
+ }
3005
+ result = await this.#options.client.killTerminal(
3006
+ params
3007
+ );
3008
+ break;
3009
+ default:
3010
+ throw new ACPError(-32601, `Unknown method: ${method}`);
3011
+ }
3012
+ } catch (e) {
3013
+ if (e instanceof ACPError) {
3014
+ error = e;
3015
+ } else {
3016
+ error = new ACPError(-32603, e.message);
3017
+ }
3018
+ }
3019
+ const responseEnvelope = {
3020
+ acp: {
3021
+ jsonrpc: "2.0",
3022
+ id: requestId,
3023
+ ...error ? { error: error.toErrorObject() } : { result }
3024
+ },
3025
+ acpContext: {
3026
+ streamId: this.#streamId,
3027
+ sessionId: this.#sessionId,
3028
+ direction: "client-to-agent"
3029
+ }
3030
+ };
3031
+ await this.#mapClient.send(
3032
+ { agent: this.#options.targetAgent },
3033
+ responseEnvelope,
3034
+ {
3035
+ protocol: "acp",
3036
+ correlationId: originalMessage.id
3037
+ }
3038
+ );
3039
+ }
3040
+ /**
3041
+ * Send an ACP request and wait for response.
3042
+ */
3043
+ async #sendRequest(method, params) {
3044
+ if (this.#closed) {
3045
+ throw new Error("ACP stream is closed");
3046
+ }
3047
+ await this.#setupSubscription();
3048
+ if (this.#closed) {
3049
+ throw new Error("ACP stream closed");
3050
+ }
3051
+ const correlationId = `${this.#streamId}-${Date.now()}-${Math.random().toString(36).slice(2)}`;
3052
+ const timeout = this.#options.timeout ?? 3e4;
3053
+ const envelope = {
3054
+ acp: {
3055
+ jsonrpc: "2.0",
3056
+ id: correlationId,
3057
+ method,
3058
+ ...params !== void 0 && { params }
3059
+ },
3060
+ acpContext: {
3061
+ streamId: this.#streamId,
3062
+ sessionId: this.#sessionId,
3063
+ direction: "client-to-agent"
3064
+ }
3065
+ };
3066
+ const resultPromise = new Promise((resolve, reject) => {
3067
+ const timeoutHandle = setTimeout(() => {
3068
+ this.#pendingRequests.delete(correlationId);
3069
+ reject(new Error(`ACP request timed out after ${timeout}ms: ${method}`));
3070
+ }, timeout);
3071
+ this.#pendingRequests.set(correlationId, {
3072
+ resolve,
3073
+ reject,
3074
+ timeout: timeoutHandle,
3075
+ method
3076
+ });
3077
+ });
3078
+ try {
3079
+ await this.#mapClient.send({ agent: this.#options.targetAgent }, envelope, {
3080
+ protocol: "acp",
3081
+ correlationId
3082
+ });
3083
+ } catch (err) {
3084
+ const pending = this.#pendingRequests.get(correlationId);
3085
+ if (pending) {
3086
+ clearTimeout(pending.timeout);
3087
+ this.#pendingRequests.delete(correlationId);
3088
+ }
3089
+ throw err;
3090
+ }
3091
+ if (this.#closed && !this.#pendingRequests.has(correlationId)) {
3092
+ throw new Error("ACP stream closed");
3093
+ }
3094
+ return resultPromise;
3095
+ }
3096
+ /**
3097
+ * Send an ACP notification (no response expected).
3098
+ */
3099
+ async #sendNotification(method, params) {
3100
+ if (this.#closed) {
3101
+ throw new Error("ACP stream is closed");
3102
+ }
3103
+ await this.#setupSubscription();
3104
+ const envelope = {
3105
+ acp: {
3106
+ jsonrpc: "2.0",
3107
+ method,
3108
+ ...params !== void 0 && { params }
3109
+ },
3110
+ acpContext: {
3111
+ streamId: this.#streamId,
3112
+ sessionId: this.#sessionId,
3113
+ direction: "client-to-agent"
3114
+ }
3115
+ };
3116
+ await this.#mapClient.send({ agent: this.#options.targetAgent }, envelope, {
3117
+ protocol: "acp"
3118
+ });
3119
+ }
3120
+ // ===========================================================================
3121
+ // ACP Lifecycle Methods
3122
+ // ===========================================================================
3123
+ /**
3124
+ * Initialize the ACP connection with the target agent.
3125
+ */
3126
+ async initialize(params) {
3127
+ if (this.#initialized) {
3128
+ throw new Error("ACP stream already initialized");
3129
+ }
3130
+ const result = await this.#sendRequest(
3131
+ ACP_METHODS.INITIALIZE,
3132
+ params
3133
+ );
3134
+ this.#initialized = true;
3135
+ this.#capabilities = result.agentCapabilities ?? null;
3136
+ return result;
3137
+ }
3138
+ /**
3139
+ * Authenticate with the agent.
3140
+ */
3141
+ async authenticate(params) {
3142
+ if (!this.#initialized) {
3143
+ throw new Error("Must call initialize() before authenticate()");
3144
+ }
3145
+ return this.#sendRequest(
3146
+ ACP_METHODS.AUTHENTICATE,
3147
+ params
3148
+ );
3149
+ }
3150
+ // ===========================================================================
3151
+ // ACP Session Methods
3152
+ // ===========================================================================
3153
+ /**
3154
+ * Create a new ACP session.
3155
+ */
3156
+ async newSession(params) {
3157
+ if (!this.#initialized) {
3158
+ throw new Error("Must call initialize() before newSession()");
3159
+ }
3160
+ const result = await this.#sendRequest(
3161
+ ACP_METHODS.SESSION_NEW,
3162
+ params
3163
+ );
3164
+ this.#sessionId = result.sessionId;
3165
+ return result;
3166
+ }
3167
+ /**
3168
+ * Load an existing ACP session.
3169
+ */
3170
+ async loadSession(params) {
3171
+ if (!this.#initialized) {
3172
+ throw new Error("Must call initialize() before loadSession()");
3173
+ }
3174
+ const result = await this.#sendRequest(
3175
+ ACP_METHODS.SESSION_LOAD,
3176
+ params
3177
+ );
3178
+ this.#sessionId = params.sessionId;
3179
+ return result;
3180
+ }
3181
+ /**
3182
+ * Set the session mode.
3183
+ */
3184
+ async setSessionMode(params) {
3185
+ return this.#sendRequest(
3186
+ ACP_METHODS.SESSION_SET_MODE,
3187
+ params
3188
+ );
3189
+ }
3190
+ // ===========================================================================
3191
+ // ACP Prompt Methods
3192
+ // ===========================================================================
3193
+ /**
3194
+ * Send a prompt to the agent.
3195
+ * Updates are received via the sessionUpdate handler.
3196
+ */
3197
+ async prompt(params) {
3198
+ if (!this.#sessionId) {
3199
+ throw new Error("Must call newSession() or loadSession() before prompt()");
3200
+ }
3201
+ return this.#sendRequest(
3202
+ ACP_METHODS.SESSION_PROMPT,
3203
+ params
3204
+ );
3205
+ }
3206
+ /**
3207
+ * Cancel ongoing operations for the current session.
3208
+ */
3209
+ async cancel(params) {
3210
+ if (!this.#sessionId) {
3211
+ throw new Error("No active session to cancel");
3212
+ }
3213
+ await this.#sendNotification(ACP_METHODS.SESSION_CANCEL, {
3214
+ sessionId: this.#sessionId,
3215
+ ...params
3216
+ });
3217
+ }
3218
+ // ===========================================================================
3219
+ // Extension Methods
3220
+ // ===========================================================================
3221
+ /**
3222
+ * Call an ACP extension method on the target agent.
3223
+ *
3224
+ * Extension methods are prefixed with "_" (e.g., "_macro/spawnAgent").
3225
+ * The agent must support the extension for this to succeed.
3226
+ *
3227
+ * @param method - The extension method name (e.g., "_macro/spawnAgent")
3228
+ * @param params - Parameters to pass to the extension method
3229
+ * @returns The result from the extension method
3230
+ *
3231
+ * @example
3232
+ * ```typescript
3233
+ * const result = await acp.callExtension("_macro/spawnAgent", {
3234
+ * task: "Implement feature X",
3235
+ * cwd: "/project"
3236
+ * });
3237
+ * ```
3238
+ */
3239
+ async callExtension(method, params) {
3240
+ if (!this.#initialized) {
3241
+ throw new Error("Must call initialize() before callExtension()");
3242
+ }
3243
+ return this.#sendRequest(method, params);
3244
+ }
3245
+ // ===========================================================================
3246
+ // Lifecycle
3247
+ // ===========================================================================
3248
+ /**
3249
+ * Close this ACP stream and clean up resources.
3250
+ */
3251
+ async close() {
3252
+ if (this.#closed) return;
3253
+ this.#closed = true;
3254
+ if (this.#unsubscribeReconnection) {
3255
+ this.#unsubscribeReconnection();
3256
+ this.#unsubscribeReconnection = null;
3257
+ }
3258
+ for (const [id, pending] of this.#pendingRequests) {
3259
+ clearTimeout(pending.timeout);
3260
+ pending.reject(new Error("ACP stream closed"));
3261
+ this.#pendingRequests.delete(id);
3262
+ }
3263
+ if (this.#subscription) {
3264
+ await this.#subscription.unsubscribe();
3265
+ this.#subscription = null;
3266
+ }
3267
+ this.emit("close");
3268
+ }
3269
+ };
3270
+
2555
3271
  // src/connection/client.ts
2556
3272
  var ClientConnection = class _ClientConnection {
2557
3273
  #connection;
2558
3274
  #subscriptions = /* @__PURE__ */ new Map();
2559
3275
  #subscriptionStates = /* @__PURE__ */ new Map();
2560
3276
  #reconnectionHandlers = /* @__PURE__ */ new Set();
3277
+ #acpStreams = /* @__PURE__ */ new Map();
2561
3278
  #options;
2562
3279
  #sessionId = null;
2563
3280
  #serverCapabilities = null;
2564
3281
  #connected = false;
2565
3282
  #lastConnectOptions;
2566
3283
  #isReconnecting = false;
3284
+ #lastResumeToken;
3285
+ #onTokenExpiring;
2567
3286
  constructor(stream, options = {}) {
2568
3287
  this.#connection = new BaseConnection(stream, options);
2569
3288
  this.#options = options;
@@ -2629,11 +3348,70 @@ var ClientConnection = class _ClientConnection {
2629
3348
  await client.connect({ auth: options?.auth });
2630
3349
  return client;
2631
3350
  }
3351
+ /**
3352
+ * Connect to a MAP server via agentic-mesh transport.
3353
+ *
3354
+ * Handles:
3355
+ * - Dynamic import of agentic-mesh (optional peer dependency)
3356
+ * - Stream creation over encrypted mesh tunnel
3357
+ * - Auto-configuration of createStream for reconnection
3358
+ * - Initial MAP protocol connect handshake
3359
+ *
3360
+ * Requires `agentic-mesh` to be installed as a peer dependency.
3361
+ *
3362
+ * @param options - Mesh connection options
3363
+ * @returns Connected ClientConnection instance
3364
+ *
3365
+ * @example
3366
+ * ```typescript
3367
+ * import { createNebulaTransport } from 'agentic-mesh';
3368
+ *
3369
+ * const transport = createNebulaTransport({
3370
+ * configPath: '/etc/nebula/config.yml',
3371
+ * });
3372
+ *
3373
+ * const client = await ClientConnection.connectMesh({
3374
+ * transport,
3375
+ * peer: { peerId: 'server', address: '10.0.0.1', port: 4242 },
3376
+ * localPeerId: 'my-client',
3377
+ * name: 'MeshClient',
3378
+ * reconnection: true
3379
+ * });
3380
+ *
3381
+ * const agents = await client.listAgents();
3382
+ * ```
3383
+ */
3384
+ static async connectMesh(options) {
3385
+ const { agenticMeshStream: agenticMeshStream2 } = await Promise.resolve().then(() => (init_agentic_mesh(), agentic_mesh_exports));
3386
+ const streamConfig = {
3387
+ transport: options.transport,
3388
+ peer: options.peer,
3389
+ localPeerId: options.localPeerId,
3390
+ timeout: options.timeout
3391
+ };
3392
+ const stream = await agenticMeshStream2(streamConfig);
3393
+ const createStream = async () => agenticMeshStream2(streamConfig);
3394
+ const reconnection = options.reconnection === true ? { enabled: true } : typeof options.reconnection === "object" ? options.reconnection : void 0;
3395
+ const client = new _ClientConnection(stream, {
3396
+ name: options.name,
3397
+ capabilities: options.capabilities,
3398
+ createStream,
3399
+ reconnection
3400
+ });
3401
+ await client.connect({ auth: options.auth });
3402
+ return client;
3403
+ }
2632
3404
  // ===========================================================================
2633
3405
  // Connection Lifecycle
2634
3406
  // ===========================================================================
2635
3407
  /**
2636
3408
  * Connect to the MAP system
3409
+ *
3410
+ * @param options - Connection options
3411
+ * @param options.sessionId - Specific session ID to use
3412
+ * @param options.resumeToken - Token to resume a previously disconnected session
3413
+ * @param options.auth - Authentication credentials
3414
+ * @param options.onTokenExpiring - Callback invoked before token expires for proactive refresh
2637
3415
  */
2638
3416
  async connect(options) {
2639
3417
  const params = {
@@ -2649,10 +3427,134 @@ var ClientConnection = class _ClientConnection {
2649
3427
  this.#sessionId = result.sessionId;
2650
3428
  this.#serverCapabilities = result.capabilities;
2651
3429
  this.#connected = true;
3430
+ if (result.resumeToken) {
3431
+ this.#lastResumeToken = result.resumeToken;
3432
+ }
3433
+ if (options?.onTokenExpiring) {
3434
+ this.#onTokenExpiring = options.onTokenExpiring;
3435
+ this.#setupTokenExpiryMonitoring(result);
3436
+ }
2652
3437
  this.#connection._transitionTo("connected");
2653
3438
  this.#lastConnectOptions = options;
2654
3439
  return result;
2655
3440
  }
3441
+ /**
3442
+ * Get the resume token for this session.
3443
+ * Can be used to reconnect and restore session state after disconnection.
3444
+ *
3445
+ * @returns The resume token, or undefined if not available
3446
+ */
3447
+ getResumeToken() {
3448
+ return this.#lastResumeToken;
3449
+ }
3450
+ /**
3451
+ * Reconnect to the server, optionally using a resume token to restore session.
3452
+ *
3453
+ * @param resumeToken - Token to resume previous session. If not provided, uses the last known token.
3454
+ * @returns Connect response result
3455
+ *
3456
+ * @example
3457
+ * ```typescript
3458
+ * // Save token before disconnect
3459
+ * const token = await client.disconnect();
3460
+ *
3461
+ * // Later, reconnect with the token
3462
+ * const result = await client.reconnect(token);
3463
+ * console.log('Reconnected:', result.reconnected);
3464
+ * ```
3465
+ */
3466
+ async reconnect(resumeToken) {
3467
+ const tokenToUse = resumeToken ?? this.#lastResumeToken;
3468
+ return this.connect({
3469
+ ...this.#lastConnectOptions,
3470
+ resumeToken: tokenToUse
3471
+ });
3472
+ }
3473
+ /**
3474
+ * Set up monitoring for token expiration
3475
+ */
3476
+ #setupTokenExpiryMonitoring(connectResult) {
3477
+ const principal = connectResult.principal;
3478
+ if (!principal?.expiresAt || !this.#onTokenExpiring) {
3479
+ return;
3480
+ }
3481
+ const expiresAt = principal.expiresAt;
3482
+ const now = Date.now();
3483
+ const warningTime = expiresAt - 6e4;
3484
+ const delay = warningTime - now;
3485
+ if (delay > 0) {
3486
+ setTimeout(async () => {
3487
+ if (!this.#connected || !this.#onTokenExpiring) return;
3488
+ try {
3489
+ const newCredentials = await this.#onTokenExpiring(expiresAt);
3490
+ if (newCredentials) {
3491
+ const refreshResult = await this.refreshAuth({
3492
+ method: newCredentials.method,
3493
+ credential: newCredentials.credential
3494
+ });
3495
+ if (refreshResult.success && refreshResult.principal?.expiresAt) {
3496
+ this.#setupTokenExpiryMonitoring({
3497
+ ...connectResult,
3498
+ principal: refreshResult.principal
3499
+ });
3500
+ }
3501
+ }
3502
+ } catch {
3503
+ }
3504
+ }, delay);
3505
+ }
3506
+ }
3507
+ /**
3508
+ * Authenticate with the server after connection.
3509
+ *
3510
+ * Use this when the server returns `authRequired` in the connect response,
3511
+ * indicating that authentication is needed before accessing protected resources.
3512
+ *
3513
+ * @param auth - Authentication credentials
3514
+ * @returns Authentication result with principal if successful
3515
+ *
3516
+ * @example
3517
+ * ```typescript
3518
+ * const connectResult = await client.connect();
3519
+ *
3520
+ * if (connectResult.authRequired) {
3521
+ * const authResult = await client.authenticate({
3522
+ * method: 'api-key',
3523
+ * credential: process.env.API_KEY,
3524
+ * });
3525
+ *
3526
+ * if (authResult.success) {
3527
+ * console.log('Authenticated as:', authResult.principal?.id);
3528
+ * }
3529
+ * }
3530
+ * ```
3531
+ */
3532
+ async authenticate(auth) {
3533
+ const params = {
3534
+ method: auth.method,
3535
+ credential: auth.credential
3536
+ };
3537
+ const result = await this.#connection.sendRequest(AUTH_METHODS.AUTHENTICATE, params);
3538
+ if (result.success && result.sessionId) {
3539
+ this.#sessionId = result.sessionId;
3540
+ }
3541
+ return result;
3542
+ }
3543
+ /**
3544
+ * Refresh authentication credentials.
3545
+ *
3546
+ * Use this to update credentials before they expire for long-lived connections.
3547
+ *
3548
+ * @param auth - New authentication credentials
3549
+ * @returns Updated principal information
3550
+ */
3551
+ async refreshAuth(auth) {
3552
+ const params = {
3553
+ method: auth.method,
3554
+ credential: auth.credential
3555
+ };
3556
+ return this.#connection.sendRequest(AUTH_METHODS.AUTH_REFRESH, params);
3557
+ }
2656
3558
  /**
2657
3559
  * Disconnect from the MAP system
2658
3560
  * @param reason - Optional reason for disconnecting
@@ -2667,7 +3569,14 @@ var ClientConnection = class _ClientConnection {
2667
3569
  reason ? { reason } : void 0
2668
3570
  );
2669
3571
  resumeToken = result.resumeToken;
3572
+ if (resumeToken) {
3573
+ this.#lastResumeToken = resumeToken;
3574
+ }
2670
3575
  } finally {
3576
+ for (const stream of this.#acpStreams.values()) {
3577
+ await stream.close();
3578
+ }
3579
+ this.#acpStreams.clear();
2671
3580
  for (const subscription of this.#subscriptions.values()) {
2672
3581
  subscription._close();
2673
3582
  }
@@ -2849,6 +3758,65 @@ var ClientConnection = class _ClientConnection {
2849
3758
  }
2850
3759
  }
2851
3760
  // ===========================================================================
3761
+ // ACP Streams
3762
+ // ===========================================================================
3763
+ /**
3764
+ * Create a virtual ACP stream connection to an agent.
3765
+ *
3766
+ * This allows clients to interact with ACP-compatible agents using the
3767
+ * familiar ACP interface while routing all messages through MAP.
3768
+ *
3769
+ * @param options - Stream configuration options
3770
+ * @returns ACPStreamConnection instance ready for initialize()
3771
+ *
3772
+ * @example
3773
+ * ```typescript
3774
+ * const acp = client.createACPStream({
3775
+ * targetAgent: 'coding-agent-1',
3776
+ * client: {
3777
+ * requestPermission: async (req) => ({
3778
+ * outcome: { outcome: 'selected', optionId: 'allow' }
3779
+ * }),
3780
+ * sessionUpdate: async (update) => {
3781
+ * console.log('Agent update:', update);
3782
+ * }
3783
+ * }
3784
+ * });
3785
+ *
3786
+ * await acp.initialize({
3787
+ * protocolVersion: 20241007,
3788
+ * clientInfo: { name: 'IDE', version: '1.0' }
3789
+ * });
3790
+ * const { sessionId } = await acp.newSession({ cwd: '/project', mcpServers: [] });
3791
+ * const result = await acp.prompt({
3792
+ * sessionId,
3793
+ * prompt: [{ type: 'text', text: 'Hello' }]
3794
+ * });
3795
+ *
3796
+ * await acp.close();
3797
+ * ```
3798
+ */
3799
+ createACPStream(options) {
3800
+ const stream = new ACPStreamConnection(this, options);
3801
+ this.#acpStreams.set(stream.streamId, stream);
3802
+ stream.on("close", () => {
3803
+ this.#acpStreams.delete(stream.streamId);
3804
+ });
3805
+ return stream;
3806
+ }
3807
+ /**
3808
+ * Get an active ACP stream by ID.
3809
+ */
3810
+ getACPStream(streamId) {
3811
+ return this.#acpStreams.get(streamId);
3812
+ }
3813
+ /**
3814
+ * Get all active ACP streams.
3815
+ */
3816
+ get acpStreams() {
3817
+ return this.#acpStreams;
3818
+ }
3819
+ // ===========================================================================
2852
3820
  // Subscriptions
2853
3821
  // ===========================================================================
2854
3822
  /**
@@ -3488,6 +4456,66 @@ var AgentConnection = class _AgentConnection {
3488
4456
  await agent.connect({ auth: options?.auth });
3489
4457
  return agent;
3490
4458
  }
4459
+ /**
4460
+ * Connect and register an agent via agentic-mesh transport.
4461
+ *
4462
+ * Handles:
4463
+ * - Dynamic import of agentic-mesh (optional peer dependency)
4464
+ * - Stream creation over encrypted mesh tunnel
4465
+ * - Auto-configuration of createStream for reconnection
4466
+ * - Initial MAP protocol connect handshake
4467
+ * - Agent registration
4468
+ *
4469
+ * Requires `agentic-mesh` to be installed as a peer dependency.
4470
+ *
4471
+ * @param options - Mesh connection and agent options
4472
+ * @returns Connected and registered AgentConnection instance
4473
+ *
4474
+ * @example
4475
+ * ```typescript
4476
+ * import { createNebulaTransport } from 'agentic-mesh';
4477
+ *
4478
+ * const transport = createNebulaTransport({
4479
+ * configPath: '/etc/nebula/config.yml',
4480
+ * });
4481
+ *
4482
+ * const agent = await AgentConnection.connectMesh({
4483
+ * transport,
4484
+ * peer: { peerId: 'server', address: '10.0.0.1', port: 4242 },
4485
+ * localPeerId: 'my-agent',
4486
+ * name: 'MeshWorker',
4487
+ * role: 'processor',
4488
+ * reconnection: true
4489
+ * });
4490
+ *
4491
+ * agent.onMessage(handleMessage);
4492
+ * await agent.busy();
4493
+ * ```
4494
+ */
4495
+ static async connectMesh(options) {
4496
+ const { agenticMeshStream: agenticMeshStream2 } = await Promise.resolve().then(() => (init_agentic_mesh(), agentic_mesh_exports));
4497
+ const streamConfig = {
4498
+ transport: options.transport,
4499
+ peer: options.peer,
4500
+ localPeerId: options.localPeerId,
4501
+ timeout: options.timeout
4502
+ };
4503
+ const stream = await agenticMeshStream2(streamConfig);
4504
+ const createStream = async () => agenticMeshStream2(streamConfig);
4505
+ const reconnection = options.reconnection === true ? { enabled: true } : typeof options.reconnection === "object" ? options.reconnection : void 0;
4506
+ const agent = new _AgentConnection(stream, {
4507
+ name: options.name,
4508
+ role: options.role,
4509
+ capabilities: options.capabilities,
4510
+ visibility: options.visibility,
4511
+ parent: options.parent,
4512
+ scopes: options.scopes,
4513
+ createStream,
4514
+ reconnection
4515
+ });
4516
+ await agent.connect({ auth: options.auth });
4517
+ return agent;
4518
+ }
3491
4519
  // ===========================================================================
3492
4520
  // Connection Lifecycle
3493
4521
  // ===========================================================================
@@ -3524,6 +4552,62 @@ var AgentConnection = class _AgentConnection {
3524
4552
  this.#connection._transitionTo("connected");
3525
4553
  return { connection: connectResult, agent: registerResult.agent };
3526
4554
  }
4555
+ /**
4556
+ * Authenticate with the server after connection.
4557
+ *
4558
+ * Use this when the server returns `authRequired` in the connect response,
4559
+ * indicating that authentication is needed before registering or accessing
4560
+ * protected resources.
4561
+ *
4562
+ * @param auth - Authentication credentials
4563
+ * @returns Authentication result with principal if successful
4564
+ *
4565
+ * @example
4566
+ * ```typescript
4567
+ * const agent = new AgentConnection(stream, { name: 'MyAgent' });
4568
+ *
4569
+ * // First connect to get auth requirements
4570
+ * const connectResult = await agent.connectOnly();
4571
+ *
4572
+ * if (connectResult.authRequired) {
4573
+ * const authResult = await agent.authenticate({
4574
+ * method: 'api-key',
4575
+ * token: process.env.AGENT_API_KEY,
4576
+ * });
4577
+ *
4578
+ * if (authResult.success) {
4579
+ * // Now register the agent
4580
+ * await agent.register({ name: 'MyAgent', role: 'worker' });
4581
+ * }
4582
+ * }
4583
+ * ```
4584
+ */
4585
+ async authenticate(auth) {
4586
+ const params = {
4587
+ method: auth.method,
4588
+ credential: auth.token
4589
+ };
4590
+ const result = await this.#connection.sendRequest(AUTH_METHODS.AUTHENTICATE, params);
4591
+ if (result.success && result.sessionId) {
4592
+ this.#sessionId = result.sessionId;
4593
+ }
4594
+ return result;
4595
+ }
4596
+ /**
4597
+ * Refresh authentication credentials.
4598
+ *
4599
+ * Use this to update credentials before they expire for long-lived connections.
4600
+ *
4601
+ * @param auth - New authentication credentials
4602
+ * @returns Updated principal information
4603
+ */
4604
+ async refreshAuth(auth) {
4605
+ const params = {
4606
+ method: auth.method,
4607
+ credential: auth.token
4608
+ };
4609
+ return this.#connection.sendRequest(AUTH_METHODS.AUTH_REFRESH, params);
4610
+ }
3527
4611
  /**
3528
4612
  * Disconnect from the MAP system
3529
4613
  * @param reason - Optional reason for disconnecting