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