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