@multi-agent-protocol/sdk 0.0.2
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/README.md +248 -0
- package/dist/index-C7XPWnxS.d.cts +3052 -0
- package/dist/index-C7XPWnxS.d.ts +3052 -0
- package/dist/index.cjs +4528 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +2288 -0
- package/dist/index.d.ts +2288 -0
- package/dist/index.js +4353 -0
- package/dist/index.js.map +1 -0
- package/dist/testing.cjs +4004 -0
- package/dist/testing.cjs.map +1 -0
- package/dist/testing.d.cts +367 -0
- package/dist/testing.d.ts +367 -0
- package/dist/testing.js +4000 -0
- package/dist/testing.js.map +1 -0
- package/package.json +79 -0
package/dist/testing.js
ADDED
|
@@ -0,0 +1,4000 @@
|
|
|
1
|
+
import { ulid } from 'ulid';
|
|
2
|
+
|
|
3
|
+
// src/jsonrpc/index.ts
|
|
4
|
+
function isRequest(message) {
|
|
5
|
+
return typeof message === "object" && message !== null && "jsonrpc" in message && message.jsonrpc === "2.0" && "id" in message && "method" in message;
|
|
6
|
+
}
|
|
7
|
+
function isNotification(message) {
|
|
8
|
+
return typeof message === "object" && message !== null && "jsonrpc" in message && message.jsonrpc === "2.0" && "method" in message && !("id" in message);
|
|
9
|
+
}
|
|
10
|
+
function isResponse(message) {
|
|
11
|
+
return typeof message === "object" && message !== null && "jsonrpc" in message && message.jsonrpc === "2.0" && "id" in message && !("method" in message);
|
|
12
|
+
}
|
|
13
|
+
function isErrorResponse(response) {
|
|
14
|
+
return "error" in response;
|
|
15
|
+
}
|
|
16
|
+
function createRequest(id, method, params) {
|
|
17
|
+
const request = {
|
|
18
|
+
jsonrpc: "2.0",
|
|
19
|
+
id,
|
|
20
|
+
method
|
|
21
|
+
};
|
|
22
|
+
if (params !== void 0) {
|
|
23
|
+
request.params = params;
|
|
24
|
+
}
|
|
25
|
+
return request;
|
|
26
|
+
}
|
|
27
|
+
function createNotification(method, params) {
|
|
28
|
+
const notification = {
|
|
29
|
+
jsonrpc: "2.0",
|
|
30
|
+
method
|
|
31
|
+
};
|
|
32
|
+
if (params !== void 0) {
|
|
33
|
+
notification.params = params;
|
|
34
|
+
}
|
|
35
|
+
return notification;
|
|
36
|
+
}
|
|
37
|
+
function createSuccessResponse(id, result) {
|
|
38
|
+
return {
|
|
39
|
+
jsonrpc: "2.0",
|
|
40
|
+
id,
|
|
41
|
+
result
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
function createErrorResponse(id, error) {
|
|
45
|
+
return {
|
|
46
|
+
jsonrpc: "2.0",
|
|
47
|
+
id,
|
|
48
|
+
error
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// src/types/index.ts
|
|
53
|
+
var EVENT_TYPES = {
|
|
54
|
+
// Agent lifecycle events
|
|
55
|
+
AGENT_REGISTERED: "agent_registered",
|
|
56
|
+
AGENT_UNREGISTERED: "agent_unregistered",
|
|
57
|
+
AGENT_STATE_CHANGED: "agent_state_changed",
|
|
58
|
+
AGENT_ORPHANED: "agent_orphaned",
|
|
59
|
+
// Participant lifecycle events
|
|
60
|
+
PARTICIPANT_CONNECTED: "participant_connected",
|
|
61
|
+
PARTICIPANT_DISCONNECTED: "participant_disconnected",
|
|
62
|
+
// Message events
|
|
63
|
+
MESSAGE_SENT: "message_sent",
|
|
64
|
+
// Scope events
|
|
65
|
+
SCOPE_CREATED: "scope_created",
|
|
66
|
+
SCOPE_MEMBER_JOINED: "scope_member_joined",
|
|
67
|
+
SCOPE_MEMBER_LEFT: "scope_member_left",
|
|
68
|
+
// Permission events
|
|
69
|
+
PERMISSIONS_CLIENT_UPDATED: "permissions_client_updated",
|
|
70
|
+
PERMISSIONS_AGENT_UPDATED: "permissions_agent_updated"};
|
|
71
|
+
var CORE_METHODS = {
|
|
72
|
+
CONNECT: "map/connect",
|
|
73
|
+
DISCONNECT: "map/disconnect",
|
|
74
|
+
SEND: "map/send",
|
|
75
|
+
SUBSCRIBE: "map/subscribe",
|
|
76
|
+
UNSUBSCRIBE: "map/unsubscribe",
|
|
77
|
+
REPLAY: "map/replay"
|
|
78
|
+
};
|
|
79
|
+
var OBSERVATION_METHODS = {
|
|
80
|
+
AGENTS_LIST: "map/agents/list",
|
|
81
|
+
AGENTS_GET: "map/agents/get",
|
|
82
|
+
SCOPES_LIST: "map/scopes/list",
|
|
83
|
+
SCOPES_GET: "map/scopes/get",
|
|
84
|
+
SCOPES_MEMBERS: "map/scopes/members",
|
|
85
|
+
STRUCTURE_GRAPH: "map/structure/graph"
|
|
86
|
+
};
|
|
87
|
+
var LIFECYCLE_METHODS = {
|
|
88
|
+
AGENTS_REGISTER: "map/agents/register",
|
|
89
|
+
AGENTS_UNREGISTER: "map/agents/unregister",
|
|
90
|
+
AGENTS_SPAWN: "map/agents/spawn"
|
|
91
|
+
};
|
|
92
|
+
var STATE_METHODS = {
|
|
93
|
+
AGENTS_UPDATE: "map/agents/update",
|
|
94
|
+
AGENTS_SUSPEND: "map/agents/suspend",
|
|
95
|
+
AGENTS_RESUME: "map/agents/resume",
|
|
96
|
+
AGENTS_STOP: "map/agents/stop"
|
|
97
|
+
};
|
|
98
|
+
var STEERING_METHODS = {
|
|
99
|
+
INJECT: "map/inject"
|
|
100
|
+
};
|
|
101
|
+
var SCOPE_METHODS = {
|
|
102
|
+
SCOPES_CREATE: "map/scopes/create",
|
|
103
|
+
SCOPES_JOIN: "map/scopes/join",
|
|
104
|
+
SCOPES_LEAVE: "map/scopes/leave"
|
|
105
|
+
};
|
|
106
|
+
var SESSION_METHODS = {
|
|
107
|
+
SESSION_LIST: "map/session/list",
|
|
108
|
+
SESSION_LOAD: "map/session/load",
|
|
109
|
+
SESSION_CLOSE: "map/session/close"
|
|
110
|
+
};
|
|
111
|
+
var PERMISSION_METHODS = {
|
|
112
|
+
PERMISSIONS_UPDATE: "map/permissions/update"
|
|
113
|
+
};
|
|
114
|
+
var FEDERATION_METHODS = {
|
|
115
|
+
FEDERATION_ROUTE: "map/federation/route"
|
|
116
|
+
};
|
|
117
|
+
var NOTIFICATION_METHODS = {
|
|
118
|
+
EVENT: "map/event",
|
|
119
|
+
MESSAGE: "map/message",
|
|
120
|
+
/** Client acknowledges received events (for backpressure) */
|
|
121
|
+
SUBSCRIBE_ACK: "map/subscribe.ack"
|
|
122
|
+
};
|
|
123
|
+
var PROTOCOL_ERROR_CODES = {
|
|
124
|
+
PARSE_ERROR: -32700,
|
|
125
|
+
INVALID_REQUEST: -32600,
|
|
126
|
+
METHOD_NOT_FOUND: -32601,
|
|
127
|
+
INVALID_PARAMS: -32602,
|
|
128
|
+
INTERNAL_ERROR: -32603
|
|
129
|
+
};
|
|
130
|
+
var AUTH_ERROR_CODES = {
|
|
131
|
+
AUTH_REQUIRED: 1e3,
|
|
132
|
+
AUTH_FAILED: 1001,
|
|
133
|
+
TOKEN_EXPIRED: 1002,
|
|
134
|
+
PERMISSION_DENIED: 1003
|
|
135
|
+
};
|
|
136
|
+
var ROUTING_ERROR_CODES = {
|
|
137
|
+
ADDRESS_NOT_FOUND: 2e3,
|
|
138
|
+
AGENT_NOT_FOUND: 2001,
|
|
139
|
+
SCOPE_NOT_FOUND: 2002,
|
|
140
|
+
DELIVERY_FAILED: 2003,
|
|
141
|
+
ADDRESS_AMBIGUOUS: 2004
|
|
142
|
+
};
|
|
143
|
+
var AGENT_ERROR_CODES = {
|
|
144
|
+
AGENT_EXISTS: 3e3,
|
|
145
|
+
STATE_INVALID: 3001,
|
|
146
|
+
NOT_RESPONDING: 3002,
|
|
147
|
+
TERMINATED: 3003,
|
|
148
|
+
SPAWN_FAILED: 3004
|
|
149
|
+
};
|
|
150
|
+
var RESOURCE_ERROR_CODES = {
|
|
151
|
+
EXHAUSTED: 4e3,
|
|
152
|
+
RATE_LIMITED: 4001,
|
|
153
|
+
QUOTA_EXCEEDED: 4002
|
|
154
|
+
};
|
|
155
|
+
var FEDERATION_ERROR_CODES = {
|
|
156
|
+
FEDERATION_UNAVAILABLE: 5e3,
|
|
157
|
+
FEDERATION_SYSTEM_NOT_FOUND: 5001,
|
|
158
|
+
FEDERATION_AUTH_FAILED: 5002,
|
|
159
|
+
FEDERATION_ROUTE_REJECTED: 5003,
|
|
160
|
+
/** Message has already visited this system (loop detected) */
|
|
161
|
+
FEDERATION_LOOP_DETECTED: 5010,
|
|
162
|
+
/** Message exceeded maximum hop count */
|
|
163
|
+
FEDERATION_MAX_HOPS_EXCEEDED: 5011
|
|
164
|
+
};
|
|
165
|
+
var ERROR_CODES = {
|
|
166
|
+
...PROTOCOL_ERROR_CODES,
|
|
167
|
+
...AUTH_ERROR_CODES,
|
|
168
|
+
...ROUTING_ERROR_CODES,
|
|
169
|
+
...AGENT_ERROR_CODES,
|
|
170
|
+
...RESOURCE_ERROR_CODES,
|
|
171
|
+
...FEDERATION_ERROR_CODES
|
|
172
|
+
};
|
|
173
|
+
var PROTOCOL_VERSION = 1;
|
|
174
|
+
|
|
175
|
+
// src/errors/index.ts
|
|
176
|
+
var MAPRequestError = class _MAPRequestError extends Error {
|
|
177
|
+
code;
|
|
178
|
+
data;
|
|
179
|
+
constructor(code, message, data) {
|
|
180
|
+
super(message);
|
|
181
|
+
this.name = "MAPRequestError";
|
|
182
|
+
this.code = code;
|
|
183
|
+
this.data = data;
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Convert to MAP error object
|
|
187
|
+
*/
|
|
188
|
+
toError() {
|
|
189
|
+
const error = {
|
|
190
|
+
code: this.code,
|
|
191
|
+
message: this.message
|
|
192
|
+
};
|
|
193
|
+
if (this.data) {
|
|
194
|
+
error.data = this.data;
|
|
195
|
+
}
|
|
196
|
+
return error;
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Convert to JSON-RPC error response
|
|
200
|
+
*/
|
|
201
|
+
toResponse(id) {
|
|
202
|
+
return createErrorResponse(id, this.toError());
|
|
203
|
+
}
|
|
204
|
+
// ==========================================================================
|
|
205
|
+
// Protocol Errors (-32xxx)
|
|
206
|
+
// ==========================================================================
|
|
207
|
+
static parseError(details) {
|
|
208
|
+
return new _MAPRequestError(
|
|
209
|
+
PROTOCOL_ERROR_CODES.PARSE_ERROR,
|
|
210
|
+
details ?? "Parse error",
|
|
211
|
+
{ category: "protocol" }
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
static invalidRequest(details) {
|
|
215
|
+
return new _MAPRequestError(
|
|
216
|
+
PROTOCOL_ERROR_CODES.INVALID_REQUEST,
|
|
217
|
+
details ?? "Invalid request",
|
|
218
|
+
{ category: "protocol" }
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
static methodNotFound(method) {
|
|
222
|
+
return new _MAPRequestError(
|
|
223
|
+
PROTOCOL_ERROR_CODES.METHOD_NOT_FOUND,
|
|
224
|
+
`Method not found: ${method}`,
|
|
225
|
+
{ category: "protocol" }
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
static invalidParams(details) {
|
|
229
|
+
return new _MAPRequestError(
|
|
230
|
+
PROTOCOL_ERROR_CODES.INVALID_PARAMS,
|
|
231
|
+
"Invalid params",
|
|
232
|
+
{ category: "protocol", details }
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
static internalError(details) {
|
|
236
|
+
return new _MAPRequestError(
|
|
237
|
+
PROTOCOL_ERROR_CODES.INTERNAL_ERROR,
|
|
238
|
+
details ?? "Internal error",
|
|
239
|
+
{ category: "internal" }
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
// ==========================================================================
|
|
243
|
+
// Auth Errors (1xxx)
|
|
244
|
+
// ==========================================================================
|
|
245
|
+
static authRequired() {
|
|
246
|
+
return new _MAPRequestError(
|
|
247
|
+
AUTH_ERROR_CODES.AUTH_REQUIRED,
|
|
248
|
+
"Authentication required",
|
|
249
|
+
{ category: "auth" }
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
static authFailed(details) {
|
|
253
|
+
return new _MAPRequestError(
|
|
254
|
+
AUTH_ERROR_CODES.AUTH_FAILED,
|
|
255
|
+
details ?? "Authentication failed",
|
|
256
|
+
{ category: "auth" }
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
static tokenExpired() {
|
|
260
|
+
return new _MAPRequestError(
|
|
261
|
+
AUTH_ERROR_CODES.TOKEN_EXPIRED,
|
|
262
|
+
"Token expired",
|
|
263
|
+
{ category: "auth", retryable: true }
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
static permissionDenied(required) {
|
|
267
|
+
return new _MAPRequestError(
|
|
268
|
+
AUTH_ERROR_CODES.PERMISSION_DENIED,
|
|
269
|
+
required ? `Permission denied: ${required}` : "Permission denied",
|
|
270
|
+
{ category: "auth" }
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
// ==========================================================================
|
|
274
|
+
// Routing Errors (2xxx)
|
|
275
|
+
// ==========================================================================
|
|
276
|
+
static addressNotFound(address) {
|
|
277
|
+
return new _MAPRequestError(
|
|
278
|
+
ROUTING_ERROR_CODES.ADDRESS_NOT_FOUND,
|
|
279
|
+
`Address not found: ${address}`,
|
|
280
|
+
{ category: "routing" }
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
static agentNotFound(agentId) {
|
|
284
|
+
return new _MAPRequestError(
|
|
285
|
+
ROUTING_ERROR_CODES.AGENT_NOT_FOUND,
|
|
286
|
+
`Agent not found: ${agentId}`,
|
|
287
|
+
{ category: "routing" }
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
static scopeNotFound(scopeId) {
|
|
291
|
+
return new _MAPRequestError(
|
|
292
|
+
ROUTING_ERROR_CODES.SCOPE_NOT_FOUND,
|
|
293
|
+
`Scope not found: ${scopeId}`,
|
|
294
|
+
{ category: "routing" }
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
static deliveryFailed(details) {
|
|
298
|
+
return new _MAPRequestError(
|
|
299
|
+
ROUTING_ERROR_CODES.DELIVERY_FAILED,
|
|
300
|
+
details ?? "Message delivery failed",
|
|
301
|
+
{ category: "routing", retryable: true }
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
static addressAmbiguous(address) {
|
|
305
|
+
return new _MAPRequestError(
|
|
306
|
+
ROUTING_ERROR_CODES.ADDRESS_AMBIGUOUS,
|
|
307
|
+
`Address is ambiguous: ${address}`,
|
|
308
|
+
{ category: "routing" }
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
// ==========================================================================
|
|
312
|
+
// Agent Errors (3xxx)
|
|
313
|
+
// ==========================================================================
|
|
314
|
+
static agentExists(agentId) {
|
|
315
|
+
return new _MAPRequestError(
|
|
316
|
+
AGENT_ERROR_CODES.AGENT_EXISTS,
|
|
317
|
+
`Agent already exists: ${agentId}`,
|
|
318
|
+
{ category: "agent" }
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
static stateInvalid(currentState, requestedAction) {
|
|
322
|
+
return new _MAPRequestError(
|
|
323
|
+
AGENT_ERROR_CODES.STATE_INVALID,
|
|
324
|
+
`Cannot ${requestedAction} agent in state: ${currentState}`,
|
|
325
|
+
{ category: "agent" }
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
static agentNotResponding(agentId) {
|
|
329
|
+
return new _MAPRequestError(
|
|
330
|
+
AGENT_ERROR_CODES.NOT_RESPONDING,
|
|
331
|
+
`Agent not responding: ${agentId}`,
|
|
332
|
+
{ category: "agent", retryable: true }
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
static agentTerminated(agentId) {
|
|
336
|
+
return new _MAPRequestError(
|
|
337
|
+
AGENT_ERROR_CODES.TERMINATED,
|
|
338
|
+
`Agent terminated: ${agentId}`,
|
|
339
|
+
{ category: "agent" }
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
static spawnFailed(details) {
|
|
343
|
+
return new _MAPRequestError(
|
|
344
|
+
AGENT_ERROR_CODES.SPAWN_FAILED,
|
|
345
|
+
details ?? "Failed to spawn agent",
|
|
346
|
+
{ category: "agent" }
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
// ==========================================================================
|
|
350
|
+
// Resource Errors (4xxx)
|
|
351
|
+
// ==========================================================================
|
|
352
|
+
static resourceExhausted(resource) {
|
|
353
|
+
return new _MAPRequestError(
|
|
354
|
+
RESOURCE_ERROR_CODES.EXHAUSTED,
|
|
355
|
+
resource ? `Resource exhausted: ${resource}` : "Resource exhausted",
|
|
356
|
+
{ category: "resource", retryable: true }
|
|
357
|
+
);
|
|
358
|
+
}
|
|
359
|
+
static rateLimited(retryAfterMs) {
|
|
360
|
+
return new _MAPRequestError(
|
|
361
|
+
RESOURCE_ERROR_CODES.RATE_LIMITED,
|
|
362
|
+
"Rate limited",
|
|
363
|
+
{ category: "resource", retryable: true, retryAfterMs }
|
|
364
|
+
);
|
|
365
|
+
}
|
|
366
|
+
static quotaExceeded(quota) {
|
|
367
|
+
return new _MAPRequestError(
|
|
368
|
+
RESOURCE_ERROR_CODES.QUOTA_EXCEEDED,
|
|
369
|
+
quota ? `Quota exceeded: ${quota}` : "Quota exceeded",
|
|
370
|
+
{ category: "resource" }
|
|
371
|
+
);
|
|
372
|
+
}
|
|
373
|
+
// ==========================================================================
|
|
374
|
+
// Federation Errors (5xxx)
|
|
375
|
+
// ==========================================================================
|
|
376
|
+
static federationUnavailable(systemId) {
|
|
377
|
+
return new _MAPRequestError(
|
|
378
|
+
FEDERATION_ERROR_CODES.FEDERATION_UNAVAILABLE,
|
|
379
|
+
systemId ? `Federation unavailable: ${systemId}` : "Federation unavailable",
|
|
380
|
+
{ category: "federation", retryable: true }
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
static federationSystemNotFound(systemId) {
|
|
384
|
+
return new _MAPRequestError(
|
|
385
|
+
FEDERATION_ERROR_CODES.FEDERATION_SYSTEM_NOT_FOUND,
|
|
386
|
+
`System not found: ${systemId}`,
|
|
387
|
+
{ category: "federation" }
|
|
388
|
+
);
|
|
389
|
+
}
|
|
390
|
+
static federationAuthFailed(systemId) {
|
|
391
|
+
return new _MAPRequestError(
|
|
392
|
+
FEDERATION_ERROR_CODES.FEDERATION_AUTH_FAILED,
|
|
393
|
+
`Federation authentication failed: ${systemId}`,
|
|
394
|
+
{ category: "federation" }
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
static federationRouteRejected(systemId, reason) {
|
|
398
|
+
return new _MAPRequestError(
|
|
399
|
+
FEDERATION_ERROR_CODES.FEDERATION_ROUTE_REJECTED,
|
|
400
|
+
reason ? `Route rejected by ${systemId}: ${reason}` : `Route rejected by ${systemId}`,
|
|
401
|
+
{ category: "federation" }
|
|
402
|
+
);
|
|
403
|
+
}
|
|
404
|
+
// ==========================================================================
|
|
405
|
+
// Utility
|
|
406
|
+
// ==========================================================================
|
|
407
|
+
/**
|
|
408
|
+
* Create from a MAP error object
|
|
409
|
+
*/
|
|
410
|
+
static fromError(error) {
|
|
411
|
+
return new _MAPRequestError(error.code, error.message, error.data);
|
|
412
|
+
}
|
|
413
|
+
/**
|
|
414
|
+
* Check if this error is retryable
|
|
415
|
+
*/
|
|
416
|
+
get retryable() {
|
|
417
|
+
return this.data?.retryable ?? false;
|
|
418
|
+
}
|
|
419
|
+
/**
|
|
420
|
+
* Get retry delay in milliseconds, if specified
|
|
421
|
+
*/
|
|
422
|
+
get retryAfterMs() {
|
|
423
|
+
return this.data?.retryAfterMs;
|
|
424
|
+
}
|
|
425
|
+
/**
|
|
426
|
+
* Get error category
|
|
427
|
+
*/
|
|
428
|
+
get category() {
|
|
429
|
+
return this.data?.category;
|
|
430
|
+
}
|
|
431
|
+
};
|
|
432
|
+
var MAPConnectionError = class _MAPConnectionError extends Error {
|
|
433
|
+
constructor(message) {
|
|
434
|
+
super(message);
|
|
435
|
+
this.name = "MAPConnectionError";
|
|
436
|
+
}
|
|
437
|
+
static closed() {
|
|
438
|
+
return new _MAPConnectionError("Connection closed");
|
|
439
|
+
}
|
|
440
|
+
static timeout() {
|
|
441
|
+
return new _MAPConnectionError("Connection timeout");
|
|
442
|
+
}
|
|
443
|
+
};
|
|
444
|
+
var MAPTimeoutError = class extends Error {
|
|
445
|
+
timeoutMs;
|
|
446
|
+
constructor(operation, timeoutMs) {
|
|
447
|
+
super(`Operation timed out after ${timeoutMs}ms: ${operation}`);
|
|
448
|
+
this.name = "MAPTimeoutError";
|
|
449
|
+
this.timeoutMs = timeoutMs;
|
|
450
|
+
}
|
|
451
|
+
};
|
|
452
|
+
|
|
453
|
+
// src/connection/base.ts
|
|
454
|
+
var BaseConnection = class {
|
|
455
|
+
#stream;
|
|
456
|
+
#pendingResponses = /* @__PURE__ */ new Map();
|
|
457
|
+
#abortController = new AbortController();
|
|
458
|
+
#closedPromise;
|
|
459
|
+
#defaultTimeout;
|
|
460
|
+
#stateChangeHandlers = /* @__PURE__ */ new Set();
|
|
461
|
+
#nextRequestId = 1;
|
|
462
|
+
#writeQueue = Promise.resolve();
|
|
463
|
+
#requestHandler = null;
|
|
464
|
+
#notificationHandler = null;
|
|
465
|
+
#closed = false;
|
|
466
|
+
#closeResolver;
|
|
467
|
+
#state = "initial";
|
|
468
|
+
constructor(stream, options = {}) {
|
|
469
|
+
this.#stream = stream;
|
|
470
|
+
this.#defaultTimeout = options.defaultTimeout ?? 3e4;
|
|
471
|
+
this.#closedPromise = new Promise((resolve) => {
|
|
472
|
+
this.#closeResolver = resolve;
|
|
473
|
+
});
|
|
474
|
+
void this.#startReceiving();
|
|
475
|
+
}
|
|
476
|
+
/**
|
|
477
|
+
* AbortSignal that triggers when the connection closes.
|
|
478
|
+
* Useful for cancelling operations tied to this connection.
|
|
479
|
+
*/
|
|
480
|
+
get signal() {
|
|
481
|
+
return this.#abortController.signal;
|
|
482
|
+
}
|
|
483
|
+
/**
|
|
484
|
+
* Promise that resolves when the connection is closed.
|
|
485
|
+
*/
|
|
486
|
+
get closed() {
|
|
487
|
+
return this.#closedPromise;
|
|
488
|
+
}
|
|
489
|
+
/**
|
|
490
|
+
* Whether the connection is closed
|
|
491
|
+
*/
|
|
492
|
+
get isClosed() {
|
|
493
|
+
return this.#closed;
|
|
494
|
+
}
|
|
495
|
+
/**
|
|
496
|
+
* Set the handler for incoming requests
|
|
497
|
+
*/
|
|
498
|
+
setRequestHandler(handler) {
|
|
499
|
+
this.#requestHandler = handler;
|
|
500
|
+
}
|
|
501
|
+
/**
|
|
502
|
+
* Set the handler for incoming notifications
|
|
503
|
+
*/
|
|
504
|
+
setNotificationHandler(handler) {
|
|
505
|
+
this.#notificationHandler = handler;
|
|
506
|
+
}
|
|
507
|
+
/**
|
|
508
|
+
* Current connection state
|
|
509
|
+
*/
|
|
510
|
+
get state() {
|
|
511
|
+
return this.#state;
|
|
512
|
+
}
|
|
513
|
+
/**
|
|
514
|
+
* Register a handler for state changes.
|
|
515
|
+
*
|
|
516
|
+
* @param handler - Function called when state changes
|
|
517
|
+
* @returns Unsubscribe function to remove the handler
|
|
518
|
+
*/
|
|
519
|
+
onStateChange(handler) {
|
|
520
|
+
this.#stateChangeHandlers.add(handler);
|
|
521
|
+
return () => this.#stateChangeHandlers.delete(handler);
|
|
522
|
+
}
|
|
523
|
+
/**
|
|
524
|
+
* Transition to a new state and notify handlers.
|
|
525
|
+
* @internal
|
|
526
|
+
*/
|
|
527
|
+
_transitionTo(newState) {
|
|
528
|
+
if (this.#state === newState) return;
|
|
529
|
+
const oldState = this.#state;
|
|
530
|
+
this.#state = newState;
|
|
531
|
+
for (const handler of this.#stateChangeHandlers) {
|
|
532
|
+
try {
|
|
533
|
+
handler(newState, oldState);
|
|
534
|
+
} catch (error) {
|
|
535
|
+
console.error("MAP: State change handler error:", error);
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
/**
|
|
540
|
+
* Reconnect with a new stream.
|
|
541
|
+
*
|
|
542
|
+
* This method is used by role-specific connections to replace the
|
|
543
|
+
* underlying transport after a disconnect.
|
|
544
|
+
*
|
|
545
|
+
* @param newStream - The new stream to use
|
|
546
|
+
* @throws If the connection is permanently closed
|
|
547
|
+
*/
|
|
548
|
+
async reconnect(newStream) {
|
|
549
|
+
if (this.#state === "closed") {
|
|
550
|
+
throw new Error("Cannot reconnect a permanently closed connection");
|
|
551
|
+
}
|
|
552
|
+
this.#stream = newStream;
|
|
553
|
+
this.#closed = false;
|
|
554
|
+
this.#writeQueue = Promise.resolve();
|
|
555
|
+
void this.#startReceiving();
|
|
556
|
+
this._transitionTo("connected");
|
|
557
|
+
}
|
|
558
|
+
/**
|
|
559
|
+
* Send a request and wait for response
|
|
560
|
+
*/
|
|
561
|
+
async sendRequest(method, params, options) {
|
|
562
|
+
if (this.#closed) {
|
|
563
|
+
throw MAPConnectionError.closed();
|
|
564
|
+
}
|
|
565
|
+
const id = this.#nextRequestId++;
|
|
566
|
+
const request = createRequest(id, method, params);
|
|
567
|
+
const responsePromise = new Promise((resolve, reject) => {
|
|
568
|
+
const pending = { resolve, reject };
|
|
569
|
+
const timeout = options?.timeout ?? this.#defaultTimeout;
|
|
570
|
+
if (timeout > 0) {
|
|
571
|
+
pending.timeoutId = setTimeout(() => {
|
|
572
|
+
this.#pendingResponses.delete(id);
|
|
573
|
+
reject(new MAPTimeoutError(method, timeout));
|
|
574
|
+
}, timeout);
|
|
575
|
+
}
|
|
576
|
+
this.#pendingResponses.set(id, pending);
|
|
577
|
+
});
|
|
578
|
+
await this.#sendMessage(request);
|
|
579
|
+
return responsePromise;
|
|
580
|
+
}
|
|
581
|
+
/**
|
|
582
|
+
* Send a notification (no response expected)
|
|
583
|
+
*/
|
|
584
|
+
async sendNotification(method, params) {
|
|
585
|
+
if (this.#closed) {
|
|
586
|
+
throw MAPConnectionError.closed();
|
|
587
|
+
}
|
|
588
|
+
const notification = createNotification(method, params);
|
|
589
|
+
await this.#sendMessage(notification);
|
|
590
|
+
}
|
|
591
|
+
/**
|
|
592
|
+
* Send a response to a request
|
|
593
|
+
*/
|
|
594
|
+
async sendResponse(id, result) {
|
|
595
|
+
if (this.#closed) {
|
|
596
|
+
throw MAPConnectionError.closed();
|
|
597
|
+
}
|
|
598
|
+
const response = createSuccessResponse(id, result);
|
|
599
|
+
await this.#sendMessage(response);
|
|
600
|
+
}
|
|
601
|
+
/**
|
|
602
|
+
* Send an error response to a request
|
|
603
|
+
*/
|
|
604
|
+
async sendErrorResponse(id, error) {
|
|
605
|
+
if (this.#closed) {
|
|
606
|
+
throw MAPConnectionError.closed();
|
|
607
|
+
}
|
|
608
|
+
const response = createErrorResponse(id, error);
|
|
609
|
+
await this.#sendMessage(response);
|
|
610
|
+
}
|
|
611
|
+
/**
|
|
612
|
+
* Close the connection
|
|
613
|
+
*/
|
|
614
|
+
async close() {
|
|
615
|
+
if (this.#closed) return;
|
|
616
|
+
this.#closed = true;
|
|
617
|
+
this._transitionTo("closed");
|
|
618
|
+
this.#abortController.abort();
|
|
619
|
+
for (const [, pending] of this.#pendingResponses) {
|
|
620
|
+
if (pending.timeoutId) {
|
|
621
|
+
clearTimeout(pending.timeoutId);
|
|
622
|
+
}
|
|
623
|
+
pending.reject(MAPConnectionError.closed());
|
|
624
|
+
}
|
|
625
|
+
this.#pendingResponses.clear();
|
|
626
|
+
try {
|
|
627
|
+
const writer = this.#stream.writable.getWriter();
|
|
628
|
+
await writer.close();
|
|
629
|
+
writer.releaseLock();
|
|
630
|
+
} catch {
|
|
631
|
+
}
|
|
632
|
+
this.#closeResolver();
|
|
633
|
+
}
|
|
634
|
+
/**
|
|
635
|
+
* Start receiving messages from the stream
|
|
636
|
+
*/
|
|
637
|
+
async #startReceiving() {
|
|
638
|
+
const reader = this.#stream.readable.getReader();
|
|
639
|
+
try {
|
|
640
|
+
while (!this.#closed) {
|
|
641
|
+
const { done, value } = await reader.read();
|
|
642
|
+
if (done) {
|
|
643
|
+
break;
|
|
644
|
+
}
|
|
645
|
+
await this.#handleMessage(value);
|
|
646
|
+
}
|
|
647
|
+
} catch (error) {
|
|
648
|
+
if (!this.#closed) {
|
|
649
|
+
console.error("MAP: Error receiving message:", error);
|
|
650
|
+
}
|
|
651
|
+
} finally {
|
|
652
|
+
reader.releaseLock();
|
|
653
|
+
await this.close();
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
/**
|
|
657
|
+
* Handle an incoming message
|
|
658
|
+
*/
|
|
659
|
+
async #handleMessage(message) {
|
|
660
|
+
try {
|
|
661
|
+
if (isRequest(message)) {
|
|
662
|
+
await this.#handleRequest(message);
|
|
663
|
+
} else if (isNotification(message)) {
|
|
664
|
+
await this.#handleNotification(message);
|
|
665
|
+
} else if (isResponse(message)) {
|
|
666
|
+
this.#handleResponse(message);
|
|
667
|
+
} else {
|
|
668
|
+
console.error("MAP: Unknown message type:", message);
|
|
669
|
+
}
|
|
670
|
+
} catch (error) {
|
|
671
|
+
console.error("MAP: Error handling message:", error);
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
/**
|
|
675
|
+
* Handle an incoming request
|
|
676
|
+
*/
|
|
677
|
+
async #handleRequest(request) {
|
|
678
|
+
const { id, method, params } = request;
|
|
679
|
+
if (!this.#requestHandler) {
|
|
680
|
+
await this.sendErrorResponse(
|
|
681
|
+
id,
|
|
682
|
+
MAPRequestError.methodNotFound(method).toError()
|
|
683
|
+
);
|
|
684
|
+
return;
|
|
685
|
+
}
|
|
686
|
+
try {
|
|
687
|
+
const result = await this.#requestHandler(method, params);
|
|
688
|
+
await this.sendResponse(id, result ?? null);
|
|
689
|
+
} catch (error) {
|
|
690
|
+
if (error instanceof MAPRequestError) {
|
|
691
|
+
await this.sendErrorResponse(id, error.toError());
|
|
692
|
+
} else {
|
|
693
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
694
|
+
await this.sendErrorResponse(
|
|
695
|
+
id,
|
|
696
|
+
MAPRequestError.internalError(message).toError()
|
|
697
|
+
);
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
/**
|
|
702
|
+
* Handle an incoming notification
|
|
703
|
+
*/
|
|
704
|
+
async #handleNotification(notification) {
|
|
705
|
+
const { method, params } = notification;
|
|
706
|
+
if (!this.#notificationHandler) {
|
|
707
|
+
console.warn("MAP: No notification handler for:", method);
|
|
708
|
+
return;
|
|
709
|
+
}
|
|
710
|
+
try {
|
|
711
|
+
await this.#notificationHandler(method, params);
|
|
712
|
+
} catch (error) {
|
|
713
|
+
console.error("MAP: Error handling notification:", method, error);
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
/**
|
|
717
|
+
* Handle an incoming response
|
|
718
|
+
*/
|
|
719
|
+
#handleResponse(response) {
|
|
720
|
+
const { id } = response;
|
|
721
|
+
const pending = this.#pendingResponses.get(id);
|
|
722
|
+
if (!pending) {
|
|
723
|
+
console.warn("MAP: Received response for unknown request:", id);
|
|
724
|
+
return;
|
|
725
|
+
}
|
|
726
|
+
this.#pendingResponses.delete(id);
|
|
727
|
+
if (pending.timeoutId) {
|
|
728
|
+
clearTimeout(pending.timeoutId);
|
|
729
|
+
}
|
|
730
|
+
if (isErrorResponse(response)) {
|
|
731
|
+
pending.reject(MAPRequestError.fromError(response.error));
|
|
732
|
+
} else {
|
|
733
|
+
pending.resolve(response.result);
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
/**
|
|
737
|
+
* Send a message through the stream with write queue serialization
|
|
738
|
+
*/
|
|
739
|
+
async #sendMessage(message) {
|
|
740
|
+
this.#writeQueue = this.#writeQueue.then(async () => {
|
|
741
|
+
if (this.#closed) return;
|
|
742
|
+
const writer = this.#stream.writable.getWriter();
|
|
743
|
+
try {
|
|
744
|
+
await writer.write(message);
|
|
745
|
+
} finally {
|
|
746
|
+
writer.releaseLock();
|
|
747
|
+
}
|
|
748
|
+
}).catch((error) => {
|
|
749
|
+
console.error("MAP: Write error:", error);
|
|
750
|
+
});
|
|
751
|
+
return this.#writeQueue;
|
|
752
|
+
}
|
|
753
|
+
};
|
|
754
|
+
|
|
755
|
+
// src/federation/envelope.ts
|
|
756
|
+
function processFederationEnvelope(envelope, config) {
|
|
757
|
+
const { federation } = envelope;
|
|
758
|
+
const maxHops = federation.maxHops ?? config.maxHops ?? 10;
|
|
759
|
+
if (federation.hopCount >= maxHops) {
|
|
760
|
+
return {
|
|
761
|
+
success: false,
|
|
762
|
+
errorCode: ERROR_CODES.FEDERATION_MAX_HOPS_EXCEEDED,
|
|
763
|
+
errorMessage: `Message exceeded maximum hop count of ${maxHops}`
|
|
764
|
+
};
|
|
765
|
+
}
|
|
766
|
+
if (federation.path?.includes(config.systemId)) {
|
|
767
|
+
return {
|
|
768
|
+
success: false,
|
|
769
|
+
errorCode: ERROR_CODES.FEDERATION_LOOP_DETECTED,
|
|
770
|
+
errorMessage: `Loop detected: message already visited ${config.systemId}`
|
|
771
|
+
};
|
|
772
|
+
}
|
|
773
|
+
if (config.allowedSources && !config.allowedSources.includes(federation.sourceSystem)) {
|
|
774
|
+
return {
|
|
775
|
+
success: false,
|
|
776
|
+
errorCode: ERROR_CODES.FEDERATION_ROUTE_REJECTED,
|
|
777
|
+
errorMessage: `Source system ${federation.sourceSystem} not in allowed sources`
|
|
778
|
+
};
|
|
779
|
+
}
|
|
780
|
+
if (config.allowedTargets && !config.allowedTargets.includes(federation.targetSystem)) {
|
|
781
|
+
return {
|
|
782
|
+
success: false,
|
|
783
|
+
errorCode: ERROR_CODES.FEDERATION_ROUTE_REJECTED,
|
|
784
|
+
errorMessage: `Target system ${federation.targetSystem} not in allowed targets`
|
|
785
|
+
};
|
|
786
|
+
}
|
|
787
|
+
return {
|
|
788
|
+
success: true,
|
|
789
|
+
envelope: {
|
|
790
|
+
payload: envelope.payload,
|
|
791
|
+
federation: {
|
|
792
|
+
...federation,
|
|
793
|
+
hopCount: federation.hopCount + 1,
|
|
794
|
+
path: config.trackPath ? [...federation.path ?? [], config.systemId] : federation.path
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
};
|
|
798
|
+
}
|
|
799
|
+
function unwrapEnvelope(envelope) {
|
|
800
|
+
return envelope.payload;
|
|
801
|
+
}
|
|
802
|
+
function isValidEnvelope(obj) {
|
|
803
|
+
if (!obj || typeof obj !== "object") return false;
|
|
804
|
+
const envelope = obj;
|
|
805
|
+
if (!("payload" in envelope) || !("federation" in envelope)) return false;
|
|
806
|
+
const federation = envelope.federation;
|
|
807
|
+
if (!federation || typeof federation !== "object") return false;
|
|
808
|
+
return typeof federation.sourceSystem === "string" && typeof federation.targetSystem === "string" && typeof federation.hopCount === "number" && typeof federation.originTimestamp === "number";
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
// src/protocol/index.ts
|
|
812
|
+
function buildConnectResponse(params) {
|
|
813
|
+
return {
|
|
814
|
+
protocolVersion: params.protocolVersion,
|
|
815
|
+
sessionId: params.sessionId,
|
|
816
|
+
participantId: params.participantId,
|
|
817
|
+
capabilities: params.capabilities,
|
|
818
|
+
systemInfo: params.systemInfo,
|
|
819
|
+
reconnected: params.reconnected,
|
|
820
|
+
reclaimedAgents: params.reclaimedAgents,
|
|
821
|
+
ownedAgents: params.ownedAgents
|
|
822
|
+
};
|
|
823
|
+
}
|
|
824
|
+
function buildSendResponse(messageId, delivered) {
|
|
825
|
+
return { messageId, delivered };
|
|
826
|
+
}
|
|
827
|
+
function buildAgentsRegisterResponse(agent) {
|
|
828
|
+
return { agent };
|
|
829
|
+
}
|
|
830
|
+
function buildAgentsUnregisterResponse(agent) {
|
|
831
|
+
return { agent };
|
|
832
|
+
}
|
|
833
|
+
function buildAgentsListResponse(agents) {
|
|
834
|
+
return { agents };
|
|
835
|
+
}
|
|
836
|
+
function buildAgentsGetResponse(agent, children, descendants) {
|
|
837
|
+
const result = { agent };
|
|
838
|
+
if (children) result.children = children;
|
|
839
|
+
if (descendants) result.descendants = descendants;
|
|
840
|
+
return result;
|
|
841
|
+
}
|
|
842
|
+
function buildAgentsUpdateResponse(agent) {
|
|
843
|
+
return { agent };
|
|
844
|
+
}
|
|
845
|
+
function buildScopesCreateResponse(scope) {
|
|
846
|
+
return { scope };
|
|
847
|
+
}
|
|
848
|
+
function buildScopesListResponse(scopes) {
|
|
849
|
+
return { scopes };
|
|
850
|
+
}
|
|
851
|
+
function buildScopesJoinResponse(scope, agent) {
|
|
852
|
+
return { scope, agent };
|
|
853
|
+
}
|
|
854
|
+
function buildScopesLeaveResponse(scope, agent) {
|
|
855
|
+
return { scope, agent };
|
|
856
|
+
}
|
|
857
|
+
function buildSubscribeResponse(subscriptionId) {
|
|
858
|
+
return { subscriptionId };
|
|
859
|
+
}
|
|
860
|
+
function buildUnsubscribeResponse(subscriptionId, closedAt = Date.now()) {
|
|
861
|
+
return {
|
|
862
|
+
subscription: {
|
|
863
|
+
id: subscriptionId,
|
|
864
|
+
closedAt
|
|
865
|
+
}
|
|
866
|
+
};
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
// src/permissions/index.ts
|
|
870
|
+
function isAgentExposed(exposure, agentId) {
|
|
871
|
+
if (!exposure?.agents) return true;
|
|
872
|
+
const {
|
|
873
|
+
publicByDefault = true,
|
|
874
|
+
publicAgents = [],
|
|
875
|
+
hiddenAgents = []
|
|
876
|
+
} = exposure.agents;
|
|
877
|
+
if (matchesPatterns(agentId, hiddenAgents)) return false;
|
|
878
|
+
if (matchesPatterns(agentId, publicAgents)) return true;
|
|
879
|
+
return publicByDefault;
|
|
880
|
+
}
|
|
881
|
+
function isEventTypeExposed(exposure, eventType) {
|
|
882
|
+
if (!exposure?.events) return true;
|
|
883
|
+
const { exposedTypes, hiddenTypes = [] } = exposure.events;
|
|
884
|
+
if (hiddenTypes.includes(eventType)) return false;
|
|
885
|
+
if (exposedTypes && !exposedTypes.includes(eventType)) return false;
|
|
886
|
+
return true;
|
|
887
|
+
}
|
|
888
|
+
function isScopeExposed(exposure, scopeId) {
|
|
889
|
+
if (!exposure?.scopes) return true;
|
|
890
|
+
const {
|
|
891
|
+
publicByDefault = true,
|
|
892
|
+
publicScopes = [],
|
|
893
|
+
hiddenScopes = []
|
|
894
|
+
} = exposure.scopes;
|
|
895
|
+
if (matchesPatterns(scopeId, hiddenScopes)) return false;
|
|
896
|
+
if (matchesPatterns(scopeId, publicScopes)) return true;
|
|
897
|
+
return publicByDefault;
|
|
898
|
+
}
|
|
899
|
+
function canSeeScope(scope, participant, memberAgentIds = []) {
|
|
900
|
+
const visibility = scope.visibility ?? "public";
|
|
901
|
+
switch (visibility) {
|
|
902
|
+
case "public":
|
|
903
|
+
return true;
|
|
904
|
+
case "members":
|
|
905
|
+
return memberAgentIds.length > 0;
|
|
906
|
+
case "system":
|
|
907
|
+
return participant.type === "system";
|
|
908
|
+
default:
|
|
909
|
+
return false;
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
function canSeeAgent(agent, participant, ownedAgentIds = []) {
|
|
913
|
+
const visibility = agent.visibility ?? "public";
|
|
914
|
+
switch (visibility) {
|
|
915
|
+
case "public":
|
|
916
|
+
return true;
|
|
917
|
+
case "parent-only":
|
|
918
|
+
if (ownedAgentIds.includes(agent.id)) return true;
|
|
919
|
+
return agent.parent ? ownedAgentIds.includes(agent.parent) : false;
|
|
920
|
+
case "scope":
|
|
921
|
+
return true;
|
|
922
|
+
case "system":
|
|
923
|
+
return participant.type === "system";
|
|
924
|
+
default:
|
|
925
|
+
return false;
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
function filterVisibleAgents(agents, context) {
|
|
929
|
+
const ownedAgentIds = context.ownedAgentIds ?? [];
|
|
930
|
+
return agents.filter((agent) => {
|
|
931
|
+
if (!isAgentExposed(context.system.exposure, agent.id)) {
|
|
932
|
+
return false;
|
|
933
|
+
}
|
|
934
|
+
if (!canSeeAgent(agent, context.participant, ownedAgentIds)) {
|
|
935
|
+
return false;
|
|
936
|
+
}
|
|
937
|
+
return true;
|
|
938
|
+
});
|
|
939
|
+
}
|
|
940
|
+
function filterVisibleScopes(scopes, context) {
|
|
941
|
+
const scopeMembership = context.scopeMembership ?? /* @__PURE__ */ new Map();
|
|
942
|
+
return scopes.filter((scope) => {
|
|
943
|
+
if (!isScopeExposed(context.system.exposure, scope.id)) {
|
|
944
|
+
return false;
|
|
945
|
+
}
|
|
946
|
+
const memberAgentIds = scopeMembership.get(scope.id) ?? [];
|
|
947
|
+
if (!canSeeScope(scope, context.participant, memberAgentIds)) {
|
|
948
|
+
return false;
|
|
949
|
+
}
|
|
950
|
+
return true;
|
|
951
|
+
});
|
|
952
|
+
}
|
|
953
|
+
function matchesPatterns(value, patterns) {
|
|
954
|
+
return patterns.some((pattern) => matchGlob(value, pattern));
|
|
955
|
+
}
|
|
956
|
+
function matchGlob(value, pattern) {
|
|
957
|
+
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".");
|
|
958
|
+
const regex = new RegExp(`^${escaped}$`);
|
|
959
|
+
return regex.test(value);
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
// src/utils/retry.ts
|
|
963
|
+
var DEFAULT_RETRY_POLICY = {
|
|
964
|
+
maxRetries: 10,
|
|
965
|
+
baseDelayMs: 1e3,
|
|
966
|
+
maxDelayMs: 3e4,
|
|
967
|
+
jitter: true
|
|
968
|
+
};
|
|
969
|
+
function calculateDelay(attempt, policy) {
|
|
970
|
+
let delay = Math.min(
|
|
971
|
+
policy.baseDelayMs * Math.pow(2, attempt - 1),
|
|
972
|
+
policy.maxDelayMs
|
|
973
|
+
);
|
|
974
|
+
if (policy.jitter) {
|
|
975
|
+
delay = delay * (0.5 + Math.random());
|
|
976
|
+
}
|
|
977
|
+
return Math.floor(delay);
|
|
978
|
+
}
|
|
979
|
+
async function withRetry(operation, policy = DEFAULT_RETRY_POLICY, callbacks) {
|
|
980
|
+
let lastError;
|
|
981
|
+
for (let attempt = 1; attempt <= policy.maxRetries + 1; attempt++) {
|
|
982
|
+
try {
|
|
983
|
+
const result = await operation();
|
|
984
|
+
callbacks?.onSuccess?.(result, attempt);
|
|
985
|
+
return result;
|
|
986
|
+
} catch (error) {
|
|
987
|
+
lastError = error;
|
|
988
|
+
const isRetryable = policy.isRetryable?.(lastError) ?? true;
|
|
989
|
+
const hasMoreAttempts = attempt <= policy.maxRetries;
|
|
990
|
+
if (!isRetryable || !hasMoreAttempts) {
|
|
991
|
+
break;
|
|
992
|
+
}
|
|
993
|
+
const delay = calculateDelay(attempt, policy);
|
|
994
|
+
callbacks?.onRetry?.({
|
|
995
|
+
attempt,
|
|
996
|
+
nextDelayMs: delay,
|
|
997
|
+
lastError
|
|
998
|
+
});
|
|
999
|
+
await sleep(delay);
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
callbacks?.onFailure?.(lastError, policy.maxRetries + 1);
|
|
1003
|
+
throw lastError;
|
|
1004
|
+
}
|
|
1005
|
+
function sleep(ms) {
|
|
1006
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
// src/testing/server.ts
|
|
1010
|
+
var TestServer = class {
|
|
1011
|
+
#options;
|
|
1012
|
+
#participants = /* @__PURE__ */ new Map();
|
|
1013
|
+
#agents = /* @__PURE__ */ new Map();
|
|
1014
|
+
#scopes = /* @__PURE__ */ new Map();
|
|
1015
|
+
#subscriptions = /* @__PURE__ */ new Map();
|
|
1016
|
+
#messages = [];
|
|
1017
|
+
#eventHistory = [];
|
|
1018
|
+
#maxEventHistory;
|
|
1019
|
+
/**
|
|
1020
|
+
* Tracks recent event IDs for causal linking.
|
|
1021
|
+
* Key format: "type:source" or "type:messageId" for message events
|
|
1022
|
+
*/
|
|
1023
|
+
#recentEventIds = /* @__PURE__ */ new Map();
|
|
1024
|
+
#sessionId;
|
|
1025
|
+
#nextParticipantId = 1;
|
|
1026
|
+
#nextAgentId = 1;
|
|
1027
|
+
#nextScopeId = 1;
|
|
1028
|
+
#nextSubscriptionId = 1;
|
|
1029
|
+
#nextMessageId = 1;
|
|
1030
|
+
constructor(options = {}) {
|
|
1031
|
+
this.#options = options;
|
|
1032
|
+
this.#sessionId = `session-${Date.now()}`;
|
|
1033
|
+
this.#maxEventHistory = options.maxEventHistory ?? 1e4;
|
|
1034
|
+
}
|
|
1035
|
+
/**
|
|
1036
|
+
* Accept a new connection
|
|
1037
|
+
*/
|
|
1038
|
+
acceptConnection(stream) {
|
|
1039
|
+
const connection = new BaseConnection(stream);
|
|
1040
|
+
connection.setRequestHandler(this.#handleRequest.bind(this, connection));
|
|
1041
|
+
return connection;
|
|
1042
|
+
}
|
|
1043
|
+
/**
|
|
1044
|
+
* Get current session ID
|
|
1045
|
+
*/
|
|
1046
|
+
get sessionId() {
|
|
1047
|
+
return this.#sessionId;
|
|
1048
|
+
}
|
|
1049
|
+
/**
|
|
1050
|
+
* Get all registered agents
|
|
1051
|
+
*/
|
|
1052
|
+
get agents() {
|
|
1053
|
+
return this.#agents;
|
|
1054
|
+
}
|
|
1055
|
+
/**
|
|
1056
|
+
* Get all scopes
|
|
1057
|
+
*/
|
|
1058
|
+
get scopes() {
|
|
1059
|
+
return this.#scopes;
|
|
1060
|
+
}
|
|
1061
|
+
/**
|
|
1062
|
+
* Get all messages sent through the server
|
|
1063
|
+
*/
|
|
1064
|
+
get messages() {
|
|
1065
|
+
return this.#messages;
|
|
1066
|
+
}
|
|
1067
|
+
/**
|
|
1068
|
+
* Get connected participant count
|
|
1069
|
+
*/
|
|
1070
|
+
get participantCount() {
|
|
1071
|
+
return this.#participants.size;
|
|
1072
|
+
}
|
|
1073
|
+
/**
|
|
1074
|
+
* Get event history for replay testing
|
|
1075
|
+
*/
|
|
1076
|
+
get eventHistory() {
|
|
1077
|
+
return this.#eventHistory;
|
|
1078
|
+
}
|
|
1079
|
+
/**
|
|
1080
|
+
* Emit an event to subscribers
|
|
1081
|
+
* @param event - The event to emit (without id/timestamp)
|
|
1082
|
+
* @param causedBy - Optional array of event IDs that caused this event
|
|
1083
|
+
* @returns The generated event ID
|
|
1084
|
+
*/
|
|
1085
|
+
emitEvent(event, causedBy) {
|
|
1086
|
+
const eventId = ulid();
|
|
1087
|
+
const timestamp = Date.now();
|
|
1088
|
+
const fullEvent = {
|
|
1089
|
+
id: `evt-${timestamp}-${Math.random().toString(36).slice(2)}`,
|
|
1090
|
+
timestamp,
|
|
1091
|
+
...event
|
|
1092
|
+
};
|
|
1093
|
+
if (!isEventTypeExposed(this.#options.exposure, event.type)) {
|
|
1094
|
+
return eventId;
|
|
1095
|
+
}
|
|
1096
|
+
this.#eventHistory.push({ eventId, timestamp, event: fullEvent, causedBy });
|
|
1097
|
+
const trackingKey = event.source ? `${event.type}:${event.source}` : event.type;
|
|
1098
|
+
this.#recentEventIds.set(trackingKey, eventId);
|
|
1099
|
+
const eventData = event.data;
|
|
1100
|
+
if (eventData?.messageId) {
|
|
1101
|
+
this.#recentEventIds.set(`message:${eventData.messageId}`, eventId);
|
|
1102
|
+
}
|
|
1103
|
+
if (eventData?.agentId) {
|
|
1104
|
+
this.#recentEventIds.set(`${event.type}:agent:${eventData.agentId}`, eventId);
|
|
1105
|
+
}
|
|
1106
|
+
while (this.#eventHistory.length > this.#maxEventHistory) {
|
|
1107
|
+
this.#eventHistory.shift();
|
|
1108
|
+
}
|
|
1109
|
+
for (const subscription of this.#subscriptions.values()) {
|
|
1110
|
+
if (this.#matchesFilter(fullEvent, subscription.filter)) {
|
|
1111
|
+
const participant = this.#participants.get(subscription.participantId);
|
|
1112
|
+
if (participant) {
|
|
1113
|
+
subscription.sequenceNumber++;
|
|
1114
|
+
participant.connection.sendNotification(NOTIFICATION_METHODS.EVENT, {
|
|
1115
|
+
subscriptionId: subscription.id,
|
|
1116
|
+
sequenceNumber: subscription.sequenceNumber,
|
|
1117
|
+
eventId,
|
|
1118
|
+
timestamp,
|
|
1119
|
+
event: fullEvent,
|
|
1120
|
+
causedBy
|
|
1121
|
+
});
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
return eventId;
|
|
1126
|
+
}
|
|
1127
|
+
/**
|
|
1128
|
+
* Deliver a message notification to a participant
|
|
1129
|
+
*/
|
|
1130
|
+
deliverMessage(participantId, message) {
|
|
1131
|
+
const participant = this.#participants.get(participantId);
|
|
1132
|
+
if (participant) {
|
|
1133
|
+
participant.connection.sendNotification(NOTIFICATION_METHODS.MESSAGE, {
|
|
1134
|
+
message
|
|
1135
|
+
});
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
/**
|
|
1139
|
+
* Handle incoming requests
|
|
1140
|
+
*/
|
|
1141
|
+
async #handleRequest(connection, method, params) {
|
|
1142
|
+
switch (method) {
|
|
1143
|
+
// =======================================================================
|
|
1144
|
+
// Core Methods
|
|
1145
|
+
// =======================================================================
|
|
1146
|
+
case CORE_METHODS.CONNECT:
|
|
1147
|
+
return this.#handleConnect(connection, params);
|
|
1148
|
+
case CORE_METHODS.DISCONNECT:
|
|
1149
|
+
return this.#handleDisconnect(connection, params);
|
|
1150
|
+
case CORE_METHODS.SEND:
|
|
1151
|
+
return this.#handleSend(connection, params);
|
|
1152
|
+
case CORE_METHODS.SUBSCRIBE:
|
|
1153
|
+
return this.#handleSubscribe(connection, params);
|
|
1154
|
+
case CORE_METHODS.UNSUBSCRIBE:
|
|
1155
|
+
return this.#handleUnsubscribe(connection, params);
|
|
1156
|
+
case CORE_METHODS.REPLAY:
|
|
1157
|
+
return this.#handleReplay(params);
|
|
1158
|
+
// =======================================================================
|
|
1159
|
+
// Observation Methods
|
|
1160
|
+
// =======================================================================
|
|
1161
|
+
case OBSERVATION_METHODS.AGENTS_LIST:
|
|
1162
|
+
return this.#handleAgentsList(connection, params);
|
|
1163
|
+
case OBSERVATION_METHODS.AGENTS_GET:
|
|
1164
|
+
return this.#handleAgentsGet(params);
|
|
1165
|
+
case OBSERVATION_METHODS.SCOPES_LIST:
|
|
1166
|
+
return this.#handleScopesList(connection);
|
|
1167
|
+
// =======================================================================
|
|
1168
|
+
// Lifecycle Methods
|
|
1169
|
+
// =======================================================================
|
|
1170
|
+
case LIFECYCLE_METHODS.AGENTS_REGISTER:
|
|
1171
|
+
return this.#handleAgentsRegister(connection, params);
|
|
1172
|
+
case LIFECYCLE_METHODS.AGENTS_UNREGISTER:
|
|
1173
|
+
return this.#handleAgentsUnregister(params);
|
|
1174
|
+
case LIFECYCLE_METHODS.AGENTS_SPAWN:
|
|
1175
|
+
return this.#handleAgentsSpawn(
|
|
1176
|
+
connection,
|
|
1177
|
+
params
|
|
1178
|
+
);
|
|
1179
|
+
// =======================================================================
|
|
1180
|
+
// State Methods
|
|
1181
|
+
// =======================================================================
|
|
1182
|
+
case STATE_METHODS.AGENTS_UPDATE:
|
|
1183
|
+
return this.#handleAgentsUpdate(connection, params);
|
|
1184
|
+
case STATE_METHODS.AGENTS_STOP:
|
|
1185
|
+
return this.#handleAgentsStop(params);
|
|
1186
|
+
// =======================================================================
|
|
1187
|
+
// Scope Methods
|
|
1188
|
+
// =======================================================================
|
|
1189
|
+
case SCOPE_METHODS.SCOPES_CREATE:
|
|
1190
|
+
return this.#handleScopesCreate(params);
|
|
1191
|
+
case SCOPE_METHODS.SCOPES_JOIN:
|
|
1192
|
+
return this.#handleScopesJoin(params);
|
|
1193
|
+
case SCOPE_METHODS.SCOPES_LEAVE:
|
|
1194
|
+
return this.#handleScopesLeave(params);
|
|
1195
|
+
// =======================================================================
|
|
1196
|
+
// Steering Methods
|
|
1197
|
+
// =======================================================================
|
|
1198
|
+
case STEERING_METHODS.INJECT:
|
|
1199
|
+
return this.#handleInject(params);
|
|
1200
|
+
// =======================================================================
|
|
1201
|
+
// Federation Methods
|
|
1202
|
+
// =======================================================================
|
|
1203
|
+
case FEDERATION_METHODS.FEDERATION_ROUTE:
|
|
1204
|
+
return this.#handleFederationRoute(params);
|
|
1205
|
+
// =======================================================================
|
|
1206
|
+
// Notification Methods (client → server)
|
|
1207
|
+
// =======================================================================
|
|
1208
|
+
case NOTIFICATION_METHODS.SUBSCRIBE_ACK:
|
|
1209
|
+
return this.#handleSubscriptionAck(params);
|
|
1210
|
+
// =======================================================================
|
|
1211
|
+
// Permission Methods
|
|
1212
|
+
// =======================================================================
|
|
1213
|
+
case PERMISSION_METHODS.PERMISSIONS_UPDATE:
|
|
1214
|
+
return this.#handlePermissionsUpdate(
|
|
1215
|
+
connection,
|
|
1216
|
+
params
|
|
1217
|
+
);
|
|
1218
|
+
default:
|
|
1219
|
+
throw MAPRequestError.methodNotFound(method);
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
// ===========================================================================
|
|
1223
|
+
// Core Method Handlers
|
|
1224
|
+
// ===========================================================================
|
|
1225
|
+
#handleConnect(connection, params) {
|
|
1226
|
+
const participantId = params.participantId || `participant-${this.#nextParticipantId++}`;
|
|
1227
|
+
const participant = {
|
|
1228
|
+
id: participantId,
|
|
1229
|
+
type: params.participantType,
|
|
1230
|
+
name: params.name,
|
|
1231
|
+
connection,
|
|
1232
|
+
subscriptions: /* @__PURE__ */ new Set(),
|
|
1233
|
+
capabilities: params.capabilities
|
|
1234
|
+
};
|
|
1235
|
+
this.#participants.set(participantId, participant);
|
|
1236
|
+
connection.closed.then(() => {
|
|
1237
|
+
this.#handleParticipantDisconnect(participantId);
|
|
1238
|
+
});
|
|
1239
|
+
this.emitEvent({
|
|
1240
|
+
type: EVENT_TYPES.PARTICIPANT_CONNECTED,
|
|
1241
|
+
source: participantId,
|
|
1242
|
+
data: { participantId, participantType: params.participantType, name: params.name }
|
|
1243
|
+
});
|
|
1244
|
+
return buildConnectResponse({
|
|
1245
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
1246
|
+
sessionId: this.#sessionId,
|
|
1247
|
+
participantId,
|
|
1248
|
+
capabilities: this.#options.capabilities ?? {
|
|
1249
|
+
observation: { canObserve: true, canQuery: true },
|
|
1250
|
+
messaging: { canSend: true, canReceive: true, canBroadcast: true },
|
|
1251
|
+
lifecycle: { canSpawn: true, canRegister: true, canUnregister: true, canStop: true, canSteer: true },
|
|
1252
|
+
scopes: { canCreateScopes: true, canManageScopes: true },
|
|
1253
|
+
streaming: { supportsAck: true, supportsPause: true }
|
|
1254
|
+
},
|
|
1255
|
+
systemInfo: {
|
|
1256
|
+
name: this.#options.name ?? "Test MAP Server",
|
|
1257
|
+
version: this.#options.version ?? "1.0.0"
|
|
1258
|
+
}
|
|
1259
|
+
});
|
|
1260
|
+
}
|
|
1261
|
+
#handleDisconnect(connection, params) {
|
|
1262
|
+
for (const [id, participant] of this.#participants) {
|
|
1263
|
+
if (participant.connection === connection) {
|
|
1264
|
+
this.#handleParticipantDisconnect(id, params?.policy);
|
|
1265
|
+
break;
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
return { acknowledged: true };
|
|
1269
|
+
}
|
|
1270
|
+
#handleParticipantDisconnect(participantId, policy) {
|
|
1271
|
+
const participant = this.#participants.get(participantId);
|
|
1272
|
+
if (!participant) return;
|
|
1273
|
+
for (const subId of participant.subscriptions) {
|
|
1274
|
+
this.#subscriptions.delete(subId);
|
|
1275
|
+
}
|
|
1276
|
+
const disconnectEventId = this.emitEvent({
|
|
1277
|
+
type: EVENT_TYPES.PARTICIPANT_DISCONNECTED,
|
|
1278
|
+
source: participantId,
|
|
1279
|
+
data: { participantId, participantType: participant.type }
|
|
1280
|
+
});
|
|
1281
|
+
const ownedAgents = Array.from(this.#agents.values()).filter((a) => a.ownerId === participantId);
|
|
1282
|
+
for (const agent of ownedAgents) {
|
|
1283
|
+
const agentBehavior = policy?.agentBehavior ?? "unregister";
|
|
1284
|
+
if (agentBehavior === "unregister") {
|
|
1285
|
+
this.#terminateAgentTree(agent.id, [disconnectEventId]);
|
|
1286
|
+
} else if (agentBehavior === "orphan") {
|
|
1287
|
+
const previousOwner = agent.ownerId;
|
|
1288
|
+
agent.ownerId = null;
|
|
1289
|
+
this.emitEvent(
|
|
1290
|
+
{
|
|
1291
|
+
type: EVENT_TYPES.AGENT_ORPHANED,
|
|
1292
|
+
source: agent.id,
|
|
1293
|
+
data: { agentId: agent.id, previousOwner }
|
|
1294
|
+
},
|
|
1295
|
+
[disconnectEventId]
|
|
1296
|
+
// Caused by the disconnect event
|
|
1297
|
+
);
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
this.#participants.delete(participantId);
|
|
1301
|
+
}
|
|
1302
|
+
/**
|
|
1303
|
+
* Terminate an agent and all its descendants
|
|
1304
|
+
* @param agentId - Agent to terminate
|
|
1305
|
+
* @param causedBy - Event IDs that caused this termination (e.g., disconnect event)
|
|
1306
|
+
*/
|
|
1307
|
+
#terminateAgentTree(agentId, causedBy) {
|
|
1308
|
+
const children = Array.from(this.#agents.values()).filter((a) => a.parent === agentId);
|
|
1309
|
+
for (const child of children) {
|
|
1310
|
+
this.#terminateAgentTree(child.id, causedBy);
|
|
1311
|
+
}
|
|
1312
|
+
const agent = this.#agents.get(agentId);
|
|
1313
|
+
if (agent) {
|
|
1314
|
+
this.#agents.delete(agentId);
|
|
1315
|
+
for (const scope of this.#scopes.values()) {
|
|
1316
|
+
scope.members.delete(agentId);
|
|
1317
|
+
}
|
|
1318
|
+
this.emitEvent(
|
|
1319
|
+
{
|
|
1320
|
+
type: EVENT_TYPES.AGENT_UNREGISTERED,
|
|
1321
|
+
source: agentId,
|
|
1322
|
+
data: { agentId, reason: "owner_disconnected" }
|
|
1323
|
+
},
|
|
1324
|
+
causedBy
|
|
1325
|
+
);
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
#handleAgentsList(connection, params) {
|
|
1329
|
+
let agents = Array.from(this.#agents.values());
|
|
1330
|
+
const permissionContext = this.#buildPermissionContext(connection);
|
|
1331
|
+
if (permissionContext) {
|
|
1332
|
+
agents = filterVisibleAgents(agents, permissionContext);
|
|
1333
|
+
}
|
|
1334
|
+
if (params?.filter) {
|
|
1335
|
+
const { states, roles, scopes, parent } = params.filter;
|
|
1336
|
+
if (states?.length) {
|
|
1337
|
+
agents = agents.filter((a) => states.includes(a.state));
|
|
1338
|
+
}
|
|
1339
|
+
if (roles?.length) {
|
|
1340
|
+
agents = agents.filter((a) => a.role && roles.includes(a.role));
|
|
1341
|
+
}
|
|
1342
|
+
if (scopes?.length) {
|
|
1343
|
+
agents = agents.filter((a) => a.scopes?.some((s) => scopes.includes(s)));
|
|
1344
|
+
}
|
|
1345
|
+
if (parent) {
|
|
1346
|
+
agents = agents.filter((a) => a.parent === parent);
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
return buildAgentsListResponse(agents);
|
|
1350
|
+
}
|
|
1351
|
+
#handleAgentsGet(params) {
|
|
1352
|
+
const agent = this.#agents.get(params.agentId);
|
|
1353
|
+
if (!agent) {
|
|
1354
|
+
throw MAPRequestError.agentNotFound(params.agentId);
|
|
1355
|
+
}
|
|
1356
|
+
const children = params.include?.children ? Array.from(this.#agents.values()).filter((a) => a.parent === params.agentId) : void 0;
|
|
1357
|
+
const descendants = params.include?.descendants ? this.#getDescendants(params.agentId) : void 0;
|
|
1358
|
+
return buildAgentsGetResponse(agent, children, descendants);
|
|
1359
|
+
}
|
|
1360
|
+
/**
|
|
1361
|
+
* Get all descendants of an agent recursively
|
|
1362
|
+
*/
|
|
1363
|
+
#getDescendants(agentId) {
|
|
1364
|
+
const descendants = [];
|
|
1365
|
+
const children = Array.from(this.#agents.values()).filter((a) => a.parent === agentId);
|
|
1366
|
+
for (const child of children) {
|
|
1367
|
+
descendants.push(child);
|
|
1368
|
+
descendants.push(...this.#getDescendants(child.id));
|
|
1369
|
+
}
|
|
1370
|
+
return descendants;
|
|
1371
|
+
}
|
|
1372
|
+
#handleSend(connection, params) {
|
|
1373
|
+
const messageId = `msg-${this.#nextMessageId++}`;
|
|
1374
|
+
const delivered = [];
|
|
1375
|
+
let senderId;
|
|
1376
|
+
let senderAgentId;
|
|
1377
|
+
for (const [id, participant] of this.#participants) {
|
|
1378
|
+
if (participant.connection === connection) {
|
|
1379
|
+
senderId = id;
|
|
1380
|
+
senderAgentId = participant.agentId;
|
|
1381
|
+
break;
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1384
|
+
const message = {
|
|
1385
|
+
id: messageId,
|
|
1386
|
+
from: senderId ?? "unknown",
|
|
1387
|
+
to: params.to,
|
|
1388
|
+
timestamp: Date.now(),
|
|
1389
|
+
payload: params.payload,
|
|
1390
|
+
meta: params.meta
|
|
1391
|
+
};
|
|
1392
|
+
this.#messages.push(message);
|
|
1393
|
+
const recipients = this.#resolveAddress(params.to, senderAgentId);
|
|
1394
|
+
for (const recipientId of recipients) {
|
|
1395
|
+
const participant = this.#participants.get(recipientId);
|
|
1396
|
+
if (participant) {
|
|
1397
|
+
this.deliverMessage(recipientId, message);
|
|
1398
|
+
delivered.push(recipientId);
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
this.emitEvent({
|
|
1402
|
+
type: EVENT_TYPES.MESSAGE_SENT,
|
|
1403
|
+
source: senderId,
|
|
1404
|
+
data: { messageId, to: params.to }
|
|
1405
|
+
});
|
|
1406
|
+
return buildSendResponse(messageId, delivered);
|
|
1407
|
+
}
|
|
1408
|
+
#handleSubscribe(connection, params) {
|
|
1409
|
+
let participantId;
|
|
1410
|
+
for (const [id, participant] of this.#participants) {
|
|
1411
|
+
if (participant.connection === connection) {
|
|
1412
|
+
participantId = id;
|
|
1413
|
+
break;
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
if (!participantId) {
|
|
1417
|
+
throw MAPRequestError.authRequired();
|
|
1418
|
+
}
|
|
1419
|
+
const subscriptionId = `sub-${this.#nextSubscriptionId++}`;
|
|
1420
|
+
const subscription = {
|
|
1421
|
+
id: subscriptionId,
|
|
1422
|
+
participantId,
|
|
1423
|
+
filter: params?.filter,
|
|
1424
|
+
sequenceNumber: 0
|
|
1425
|
+
};
|
|
1426
|
+
this.#subscriptions.set(subscriptionId, subscription);
|
|
1427
|
+
this.#participants.get(participantId).subscriptions.add(subscriptionId);
|
|
1428
|
+
return buildSubscribeResponse(subscriptionId);
|
|
1429
|
+
}
|
|
1430
|
+
#handleUnsubscribe(_connection, params) {
|
|
1431
|
+
const subscription = this.#subscriptions.get(params.subscriptionId);
|
|
1432
|
+
if (subscription) {
|
|
1433
|
+
this.#subscriptions.delete(params.subscriptionId);
|
|
1434
|
+
const participant = this.#participants.get(subscription.participantId);
|
|
1435
|
+
if (participant) {
|
|
1436
|
+
participant.subscriptions.delete(params.subscriptionId);
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1439
|
+
return buildUnsubscribeResponse(params.subscriptionId);
|
|
1440
|
+
}
|
|
1441
|
+
#handleReplay(params) {
|
|
1442
|
+
const {
|
|
1443
|
+
afterEventId,
|
|
1444
|
+
fromTimestamp,
|
|
1445
|
+
toTimestamp,
|
|
1446
|
+
filter,
|
|
1447
|
+
limit = 100
|
|
1448
|
+
} = params ?? {};
|
|
1449
|
+
let events = [...this.#eventHistory];
|
|
1450
|
+
if (afterEventId) {
|
|
1451
|
+
const idx = events.findIndex((e) => e.eventId === afterEventId);
|
|
1452
|
+
if (idx >= 0) {
|
|
1453
|
+
events = events.slice(idx + 1);
|
|
1454
|
+
}
|
|
1455
|
+
}
|
|
1456
|
+
if (fromTimestamp !== void 0) {
|
|
1457
|
+
events = events.filter((e) => e.timestamp >= fromTimestamp);
|
|
1458
|
+
}
|
|
1459
|
+
if (toTimestamp !== void 0) {
|
|
1460
|
+
events = events.filter((e) => e.timestamp <= toTimestamp);
|
|
1461
|
+
}
|
|
1462
|
+
if (filter) {
|
|
1463
|
+
events = events.filter((e) => this.#matchesFilter(e.event, filter));
|
|
1464
|
+
}
|
|
1465
|
+
const hasMore = events.length > limit;
|
|
1466
|
+
events = events.slice(0, Math.min(limit, 1e3));
|
|
1467
|
+
return {
|
|
1468
|
+
events: events.map((e) => ({
|
|
1469
|
+
eventId: e.eventId,
|
|
1470
|
+
timestamp: e.timestamp,
|
|
1471
|
+
event: e.event,
|
|
1472
|
+
causedBy: e.causedBy
|
|
1473
|
+
})),
|
|
1474
|
+
hasMore,
|
|
1475
|
+
totalCount: this.#eventHistory.length
|
|
1476
|
+
};
|
|
1477
|
+
}
|
|
1478
|
+
// ===========================================================================
|
|
1479
|
+
// Structure Method Handlers
|
|
1480
|
+
// ===========================================================================
|
|
1481
|
+
#handleAgentsRegister(connection, params) {
|
|
1482
|
+
const agentId = params.agentId || `agent-${this.#nextAgentId++}`;
|
|
1483
|
+
if (this.#agents.has(agentId)) {
|
|
1484
|
+
throw MAPRequestError.agentExists(agentId);
|
|
1485
|
+
}
|
|
1486
|
+
let ownerId = "unknown";
|
|
1487
|
+
for (const [participantId, participant] of this.#participants) {
|
|
1488
|
+
if (participant.connection === connection) {
|
|
1489
|
+
ownerId = participantId;
|
|
1490
|
+
participant.agentId = agentId;
|
|
1491
|
+
break;
|
|
1492
|
+
}
|
|
1493
|
+
}
|
|
1494
|
+
const agent = {
|
|
1495
|
+
id: agentId,
|
|
1496
|
+
name: params.name,
|
|
1497
|
+
description: params.description,
|
|
1498
|
+
role: params.role,
|
|
1499
|
+
parent: params.parent,
|
|
1500
|
+
ownerId,
|
|
1501
|
+
// v2: Track which participant owns this agent
|
|
1502
|
+
state: "registered",
|
|
1503
|
+
scopes: params.scopes,
|
|
1504
|
+
visibility: params.visibility,
|
|
1505
|
+
capabilities: params.capabilities,
|
|
1506
|
+
metadata: params.metadata,
|
|
1507
|
+
permissionOverrides: params.permissionOverrides,
|
|
1508
|
+
lifecycle: {
|
|
1509
|
+
createdAt: Date.now()
|
|
1510
|
+
}
|
|
1511
|
+
};
|
|
1512
|
+
this.#agents.set(agentId, agent);
|
|
1513
|
+
this.emitEvent({
|
|
1514
|
+
type: EVENT_TYPES.AGENT_REGISTERED,
|
|
1515
|
+
source: agentId,
|
|
1516
|
+
data: { agentId, name: agent.name, role: agent.role, ownerId }
|
|
1517
|
+
});
|
|
1518
|
+
return buildAgentsRegisterResponse(agent);
|
|
1519
|
+
}
|
|
1520
|
+
#handleAgentsUnregister(params) {
|
|
1521
|
+
const agent = this.#agents.get(params.agentId);
|
|
1522
|
+
if (!agent) {
|
|
1523
|
+
throw MAPRequestError.agentNotFound(params.agentId);
|
|
1524
|
+
}
|
|
1525
|
+
const agentCopy = { ...agent };
|
|
1526
|
+
this.#agents.delete(params.agentId);
|
|
1527
|
+
for (const scope of this.#scopes.values()) {
|
|
1528
|
+
scope.members.delete(params.agentId);
|
|
1529
|
+
}
|
|
1530
|
+
this.emitEvent({
|
|
1531
|
+
type: EVENT_TYPES.AGENT_UNREGISTERED,
|
|
1532
|
+
source: params.agentId,
|
|
1533
|
+
data: { agentId: params.agentId, reason: params.reason }
|
|
1534
|
+
});
|
|
1535
|
+
return buildAgentsUnregisterResponse(agentCopy);
|
|
1536
|
+
}
|
|
1537
|
+
#handleAgentsUpdate(connection, params) {
|
|
1538
|
+
const agent = this.#agents.get(params.agentId);
|
|
1539
|
+
if (!agent) {
|
|
1540
|
+
throw MAPRequestError.agentNotFound(params.agentId);
|
|
1541
|
+
}
|
|
1542
|
+
let requestingParticipantId;
|
|
1543
|
+
for (const [id, p] of this.#participants) {
|
|
1544
|
+
if (p.connection === connection) {
|
|
1545
|
+
requestingParticipantId = id;
|
|
1546
|
+
break;
|
|
1547
|
+
}
|
|
1548
|
+
}
|
|
1549
|
+
const previousState = agent.state;
|
|
1550
|
+
if (params.state) {
|
|
1551
|
+
agent.state = params.state;
|
|
1552
|
+
}
|
|
1553
|
+
if (params.metadata) {
|
|
1554
|
+
agent.metadata = { ...agent.metadata, ...params.metadata };
|
|
1555
|
+
}
|
|
1556
|
+
if (params.permissionOverrides) {
|
|
1557
|
+
const existingOverrides = agent.permissionOverrides ?? {};
|
|
1558
|
+
const newOverrides = {};
|
|
1559
|
+
if (existingOverrides.canSee || params.permissionOverrides.canSee) {
|
|
1560
|
+
newOverrides.canSee = {
|
|
1561
|
+
...existingOverrides.canSee,
|
|
1562
|
+
...params.permissionOverrides.canSee
|
|
1563
|
+
};
|
|
1564
|
+
}
|
|
1565
|
+
if (existingOverrides.canMessage || params.permissionOverrides.canMessage) {
|
|
1566
|
+
newOverrides.canMessage = {
|
|
1567
|
+
...existingOverrides.canMessage,
|
|
1568
|
+
...params.permissionOverrides.canMessage
|
|
1569
|
+
};
|
|
1570
|
+
}
|
|
1571
|
+
if (existingOverrides.acceptsFrom || params.permissionOverrides.acceptsFrom) {
|
|
1572
|
+
newOverrides.acceptsFrom = {
|
|
1573
|
+
...existingOverrides.acceptsFrom,
|
|
1574
|
+
...params.permissionOverrides.acceptsFrom
|
|
1575
|
+
};
|
|
1576
|
+
}
|
|
1577
|
+
agent.permissionOverrides = newOverrides;
|
|
1578
|
+
this.emitEvent({
|
|
1579
|
+
type: EVENT_TYPES.PERMISSIONS_AGENT_UPDATED,
|
|
1580
|
+
source: params.agentId,
|
|
1581
|
+
data: {
|
|
1582
|
+
agentId: params.agentId,
|
|
1583
|
+
changes: params.permissionOverrides,
|
|
1584
|
+
effectivePermissions: newOverrides,
|
|
1585
|
+
updatedBy: requestingParticipantId
|
|
1586
|
+
}
|
|
1587
|
+
});
|
|
1588
|
+
}
|
|
1589
|
+
if (params.state && params.state !== previousState) {
|
|
1590
|
+
this.emitEvent({
|
|
1591
|
+
type: EVENT_TYPES.AGENT_STATE_CHANGED,
|
|
1592
|
+
source: params.agentId,
|
|
1593
|
+
data: { agentId: params.agentId, previousState, newState: params.state }
|
|
1594
|
+
});
|
|
1595
|
+
}
|
|
1596
|
+
return buildAgentsUpdateResponse(agent);
|
|
1597
|
+
}
|
|
1598
|
+
#handleAgentsSpawn(connection, params) {
|
|
1599
|
+
if (!this.#agents.has(params.parent)) {
|
|
1600
|
+
throw MAPRequestError.agentNotFound(params.parent);
|
|
1601
|
+
}
|
|
1602
|
+
return this.#handleAgentsRegister(connection, {
|
|
1603
|
+
name: params.name,
|
|
1604
|
+
role: params.role,
|
|
1605
|
+
parent: params.parent
|
|
1606
|
+
});
|
|
1607
|
+
}
|
|
1608
|
+
#handleAgentsStop(params) {
|
|
1609
|
+
const agent = this.#agents.get(params.agentId);
|
|
1610
|
+
if (!agent) {
|
|
1611
|
+
throw MAPRequestError.agentNotFound(params.agentId);
|
|
1612
|
+
}
|
|
1613
|
+
agent.state = "stopping";
|
|
1614
|
+
this.emitEvent({
|
|
1615
|
+
type: EVENT_TYPES.AGENT_STATE_CHANGED,
|
|
1616
|
+
source: params.agentId,
|
|
1617
|
+
data: { agentId: params.agentId, previousState: agent.state, newState: "stopping" }
|
|
1618
|
+
});
|
|
1619
|
+
return { stopping: true, agent };
|
|
1620
|
+
}
|
|
1621
|
+
#handleScopesList(connection) {
|
|
1622
|
+
let scopes = Array.from(this.#scopes.values()).map(({ members, ...scope }) => scope);
|
|
1623
|
+
const permissionContext = this.#buildPermissionContext(connection);
|
|
1624
|
+
if (permissionContext) {
|
|
1625
|
+
scopes = filterVisibleScopes(scopes, permissionContext);
|
|
1626
|
+
}
|
|
1627
|
+
return buildScopesListResponse(scopes);
|
|
1628
|
+
}
|
|
1629
|
+
#handleScopesCreate(params) {
|
|
1630
|
+
const scopeId = params.scopeId || `scope-${this.#nextScopeId++}`;
|
|
1631
|
+
const scope = {
|
|
1632
|
+
id: scopeId,
|
|
1633
|
+
name: params.name,
|
|
1634
|
+
description: params.description,
|
|
1635
|
+
parent: params.parent,
|
|
1636
|
+
joinPolicy: params.joinPolicy,
|
|
1637
|
+
visibility: params.visibility,
|
|
1638
|
+
members: /* @__PURE__ */ new Set()
|
|
1639
|
+
};
|
|
1640
|
+
this.#scopes.set(scopeId, scope);
|
|
1641
|
+
this.emitEvent({
|
|
1642
|
+
type: EVENT_TYPES.SCOPE_CREATED,
|
|
1643
|
+
data: { scopeId, name: scope.name }
|
|
1644
|
+
});
|
|
1645
|
+
const { members, ...scopeWithoutMembers } = scope;
|
|
1646
|
+
return buildScopesCreateResponse(scopeWithoutMembers);
|
|
1647
|
+
}
|
|
1648
|
+
#handleScopesJoin(params) {
|
|
1649
|
+
const scope = this.#scopes.get(params.scopeId);
|
|
1650
|
+
if (!scope) {
|
|
1651
|
+
throw MAPRequestError.scopeNotFound(params.scopeId);
|
|
1652
|
+
}
|
|
1653
|
+
const agent = this.#agents.get(params.agentId);
|
|
1654
|
+
if (!agent) {
|
|
1655
|
+
throw MAPRequestError.agentNotFound(params.agentId);
|
|
1656
|
+
}
|
|
1657
|
+
scope.members.add(params.agentId);
|
|
1658
|
+
if (!agent.scopes) {
|
|
1659
|
+
agent.scopes = [];
|
|
1660
|
+
}
|
|
1661
|
+
if (!agent.scopes.includes(params.scopeId)) {
|
|
1662
|
+
agent.scopes.push(params.scopeId);
|
|
1663
|
+
}
|
|
1664
|
+
this.emitEvent({
|
|
1665
|
+
type: EVENT_TYPES.SCOPE_MEMBER_JOINED,
|
|
1666
|
+
source: params.agentId,
|
|
1667
|
+
data: { scopeId: params.scopeId, agentId: params.agentId }
|
|
1668
|
+
});
|
|
1669
|
+
const { members, ...scopeWithoutMembers } = scope;
|
|
1670
|
+
return buildScopesJoinResponse(scopeWithoutMembers, agent);
|
|
1671
|
+
}
|
|
1672
|
+
#handleScopesLeave(params) {
|
|
1673
|
+
const scope = this.#scopes.get(params.scopeId);
|
|
1674
|
+
if (!scope) {
|
|
1675
|
+
throw MAPRequestError.scopeNotFound(params.scopeId);
|
|
1676
|
+
}
|
|
1677
|
+
scope.members.delete(params.agentId);
|
|
1678
|
+
const agent = this.#agents.get(params.agentId);
|
|
1679
|
+
if (!agent) {
|
|
1680
|
+
throw MAPRequestError.agentNotFound(params.agentId);
|
|
1681
|
+
}
|
|
1682
|
+
if (agent.scopes) {
|
|
1683
|
+
agent.scopes = agent.scopes.filter((s) => s !== params.scopeId);
|
|
1684
|
+
}
|
|
1685
|
+
this.emitEvent({
|
|
1686
|
+
type: EVENT_TYPES.SCOPE_MEMBER_LEFT,
|
|
1687
|
+
source: params.agentId,
|
|
1688
|
+
data: { scopeId: params.scopeId, agentId: params.agentId }
|
|
1689
|
+
});
|
|
1690
|
+
const { members, ...scopeWithoutMembers } = scope;
|
|
1691
|
+
return buildScopesLeaveResponse(scopeWithoutMembers, agent);
|
|
1692
|
+
}
|
|
1693
|
+
// ===========================================================================
|
|
1694
|
+
// Extension Method Handlers
|
|
1695
|
+
// ===========================================================================
|
|
1696
|
+
#handleInject(params) {
|
|
1697
|
+
const agent = this.#agents.get(params.agentId);
|
|
1698
|
+
if (!agent) {
|
|
1699
|
+
throw MAPRequestError.agentNotFound(params.agentId);
|
|
1700
|
+
}
|
|
1701
|
+
return { injected: true, delivery: params.delivery ?? "queue" };
|
|
1702
|
+
}
|
|
1703
|
+
// ===========================================================================
|
|
1704
|
+
// Helpers
|
|
1705
|
+
// ===========================================================================
|
|
1706
|
+
/**
|
|
1707
|
+
* Build a permission context for a connection.
|
|
1708
|
+
* Returns undefined if no exposure configuration is set.
|
|
1709
|
+
*/
|
|
1710
|
+
#buildPermissionContext(connection) {
|
|
1711
|
+
if (!this.#options.exposure) {
|
|
1712
|
+
return void 0;
|
|
1713
|
+
}
|
|
1714
|
+
let participant;
|
|
1715
|
+
for (const p of this.#participants.values()) {
|
|
1716
|
+
if (p.connection === connection) {
|
|
1717
|
+
participant = p;
|
|
1718
|
+
break;
|
|
1719
|
+
}
|
|
1720
|
+
}
|
|
1721
|
+
if (!participant) {
|
|
1722
|
+
return void 0;
|
|
1723
|
+
}
|
|
1724
|
+
const ownedAgentIds = [];
|
|
1725
|
+
for (const agent of this.#agents.values()) {
|
|
1726
|
+
if (agent.ownerId === participant.id) {
|
|
1727
|
+
ownedAgentIds.push(agent.id);
|
|
1728
|
+
}
|
|
1729
|
+
}
|
|
1730
|
+
const scopeMembership = /* @__PURE__ */ new Map();
|
|
1731
|
+
for (const [scopeId, scope] of this.#scopes) {
|
|
1732
|
+
const memberAgentIds = Array.from(scope.members).filter(
|
|
1733
|
+
(agentId) => ownedAgentIds.includes(agentId)
|
|
1734
|
+
);
|
|
1735
|
+
if (memberAgentIds.length > 0) {
|
|
1736
|
+
scopeMembership.set(scopeId, memberAgentIds);
|
|
1737
|
+
}
|
|
1738
|
+
}
|
|
1739
|
+
return {
|
|
1740
|
+
system: { exposure: this.#options.exposure },
|
|
1741
|
+
participant: {
|
|
1742
|
+
id: participant.id,
|
|
1743
|
+
type: participant.type,
|
|
1744
|
+
capabilities: this.#options.capabilities ?? {
|
|
1745
|
+
observation: { canObserve: true, canQuery: true },
|
|
1746
|
+
messaging: { canSend: true, canReceive: true, canBroadcast: true },
|
|
1747
|
+
lifecycle: { canSpawn: true, canRegister: true, canUnregister: true, canStop: true, canSteer: true },
|
|
1748
|
+
scopes: { canCreateScopes: true, canManageScopes: true }
|
|
1749
|
+
}
|
|
1750
|
+
},
|
|
1751
|
+
ownedAgentIds,
|
|
1752
|
+
scopeMembership
|
|
1753
|
+
};
|
|
1754
|
+
}
|
|
1755
|
+
#resolveAddress(address, senderAgentId) {
|
|
1756
|
+
if (typeof address === "string") {
|
|
1757
|
+
return [this.#findParticipantForAgent(address) ?? address];
|
|
1758
|
+
}
|
|
1759
|
+
if ("agent" in address && !("system" in address)) {
|
|
1760
|
+
const participantId = this.#findParticipantForAgent(address.agent);
|
|
1761
|
+
return participantId ? [participantId] : [];
|
|
1762
|
+
}
|
|
1763
|
+
if ("agents" in address) {
|
|
1764
|
+
return address.agents.map((agentId) => this.#findParticipantForAgent(agentId)).filter((id) => id !== void 0);
|
|
1765
|
+
}
|
|
1766
|
+
if ("scope" in address) {
|
|
1767
|
+
const scope = this.#scopes.get(address.scope);
|
|
1768
|
+
if (!scope) return [];
|
|
1769
|
+
return Array.from(scope.members).map((agentId) => this.#findParticipantForAgent(agentId)).filter((id) => id !== void 0);
|
|
1770
|
+
}
|
|
1771
|
+
if ("broadcast" in address) {
|
|
1772
|
+
return Array.from(this.#participants.keys());
|
|
1773
|
+
}
|
|
1774
|
+
if ("role" in address) {
|
|
1775
|
+
const agents = Array.from(this.#agents.values()).filter((a) => a.role === address.role);
|
|
1776
|
+
return agents.map((a) => this.#findParticipantForAgent(a.id)).filter((id) => id !== void 0);
|
|
1777
|
+
}
|
|
1778
|
+
if ("parent" in address || "children" in address || "siblings" in address || "ancestors" in address || "descendants" in address) {
|
|
1779
|
+
return this.#resolveHierarchicalAddress(address, senderAgentId);
|
|
1780
|
+
}
|
|
1781
|
+
return [];
|
|
1782
|
+
}
|
|
1783
|
+
/**
|
|
1784
|
+
* Resolve hierarchical address relative to sender agent
|
|
1785
|
+
*/
|
|
1786
|
+
#resolveHierarchicalAddress(address, senderAgentId) {
|
|
1787
|
+
if (!senderAgentId) return [];
|
|
1788
|
+
const senderAgent = this.#agents.get(senderAgentId);
|
|
1789
|
+
if (!senderAgent) return [];
|
|
1790
|
+
const targetAgentIds = [];
|
|
1791
|
+
const depth = address.depth;
|
|
1792
|
+
if (address.parent && senderAgent.parent) {
|
|
1793
|
+
targetAgentIds.push(senderAgent.parent);
|
|
1794
|
+
}
|
|
1795
|
+
if (address.children) {
|
|
1796
|
+
const children = Array.from(this.#agents.values()).filter((a) => a.parent === senderAgentId);
|
|
1797
|
+
targetAgentIds.push(...children.map((a) => a.id));
|
|
1798
|
+
}
|
|
1799
|
+
if (address.siblings && senderAgent.parent) {
|
|
1800
|
+
const siblings = Array.from(this.#agents.values()).filter((a) => a.parent === senderAgent.parent && a.id !== senderAgentId);
|
|
1801
|
+
targetAgentIds.push(...siblings.map((a) => a.id));
|
|
1802
|
+
}
|
|
1803
|
+
if (address.ancestors) {
|
|
1804
|
+
let currentAgent = senderAgent;
|
|
1805
|
+
let currentDepth = 0;
|
|
1806
|
+
while (currentAgent.parent && (depth === void 0 || currentDepth < depth)) {
|
|
1807
|
+
targetAgentIds.push(currentAgent.parent);
|
|
1808
|
+
currentAgent = this.#agents.get(currentAgent.parent);
|
|
1809
|
+
if (!currentAgent) break;
|
|
1810
|
+
currentDepth++;
|
|
1811
|
+
}
|
|
1812
|
+
}
|
|
1813
|
+
if (address.descendants) {
|
|
1814
|
+
const collectDescendants = (agentId, currentDepth) => {
|
|
1815
|
+
if (depth !== void 0 && currentDepth >= depth) return;
|
|
1816
|
+
const children = Array.from(this.#agents.values()).filter((a) => a.parent === agentId);
|
|
1817
|
+
for (const child of children) {
|
|
1818
|
+
targetAgentIds.push(child.id);
|
|
1819
|
+
collectDescendants(child.id, currentDepth + 1);
|
|
1820
|
+
}
|
|
1821
|
+
};
|
|
1822
|
+
collectDescendants(senderAgentId, 0);
|
|
1823
|
+
}
|
|
1824
|
+
return targetAgentIds.map((agentId) => this.#findParticipantForAgent(agentId)).filter((id) => id !== void 0);
|
|
1825
|
+
}
|
|
1826
|
+
/**
|
|
1827
|
+
* Find the participant ID that registered a given agent
|
|
1828
|
+
*/
|
|
1829
|
+
#findParticipantForAgent(agentId) {
|
|
1830
|
+
for (const [participantId, participant] of this.#participants) {
|
|
1831
|
+
if (participant.agentId === agentId) {
|
|
1832
|
+
return participantId;
|
|
1833
|
+
}
|
|
1834
|
+
}
|
|
1835
|
+
return void 0;
|
|
1836
|
+
}
|
|
1837
|
+
#matchesFilter(event, filter) {
|
|
1838
|
+
if (!filter) return true;
|
|
1839
|
+
if (filter.eventTypes?.length) {
|
|
1840
|
+
if (!filter.eventTypes.includes(event.type)) {
|
|
1841
|
+
return false;
|
|
1842
|
+
}
|
|
1843
|
+
}
|
|
1844
|
+
if (filter.agents?.length && event.source) {
|
|
1845
|
+
if (!filter.agents.includes(event.source)) {
|
|
1846
|
+
return false;
|
|
1847
|
+
}
|
|
1848
|
+
}
|
|
1849
|
+
if (filter.fromAgents?.length && event.source) {
|
|
1850
|
+
if (!filter.fromAgents.includes(event.source)) {
|
|
1851
|
+
return false;
|
|
1852
|
+
}
|
|
1853
|
+
}
|
|
1854
|
+
if (filter.scopes?.length) {
|
|
1855
|
+
const eventData = event.data;
|
|
1856
|
+
const scopeId = eventData?.scopeId;
|
|
1857
|
+
if (!scopeId || !filter.scopes.includes(scopeId)) {
|
|
1858
|
+
return false;
|
|
1859
|
+
}
|
|
1860
|
+
}
|
|
1861
|
+
if (filter.fromRoles?.length && event.source) {
|
|
1862
|
+
const agent = this.#agents.get(event.source);
|
|
1863
|
+
if (!agent?.role || !filter.fromRoles.includes(agent.role)) {
|
|
1864
|
+
return false;
|
|
1865
|
+
}
|
|
1866
|
+
}
|
|
1867
|
+
if (filter.correlationIds?.length) {
|
|
1868
|
+
const eventData = event.data;
|
|
1869
|
+
const correlationId = eventData?.correlationId;
|
|
1870
|
+
if (!correlationId || !filter.correlationIds.includes(correlationId)) {
|
|
1871
|
+
return false;
|
|
1872
|
+
}
|
|
1873
|
+
}
|
|
1874
|
+
if (filter.metadataMatch) {
|
|
1875
|
+
const eventData = event.data;
|
|
1876
|
+
const metadata = eventData?.metadata;
|
|
1877
|
+
if (!metadata) return false;
|
|
1878
|
+
for (const [key, value] of Object.entries(filter.metadataMatch)) {
|
|
1879
|
+
if (metadata[key] !== value) {
|
|
1880
|
+
return false;
|
|
1881
|
+
}
|
|
1882
|
+
}
|
|
1883
|
+
}
|
|
1884
|
+
return true;
|
|
1885
|
+
}
|
|
1886
|
+
// ===========================================================================
|
|
1887
|
+
// Federation Method Handlers
|
|
1888
|
+
// ===========================================================================
|
|
1889
|
+
#handleFederationRoute(params) {
|
|
1890
|
+
const { systemId, envelope, message } = params;
|
|
1891
|
+
let actualMessage;
|
|
1892
|
+
if (envelope && isValidEnvelope(envelope)) {
|
|
1893
|
+
if (this.#options.federationRouting) {
|
|
1894
|
+
const result = processFederationEnvelope(
|
|
1895
|
+
envelope,
|
|
1896
|
+
this.#options.federationRouting
|
|
1897
|
+
);
|
|
1898
|
+
if (!result.success) {
|
|
1899
|
+
throw new MAPRequestError(result.errorCode, result.errorMessage);
|
|
1900
|
+
}
|
|
1901
|
+
}
|
|
1902
|
+
actualMessage = unwrapEnvelope(envelope);
|
|
1903
|
+
} else if (message) {
|
|
1904
|
+
actualMessage = message;
|
|
1905
|
+
} else {
|
|
1906
|
+
throw MAPRequestError.invalidParams("Missing envelope or message");
|
|
1907
|
+
}
|
|
1908
|
+
this.#messages.push(actualMessage);
|
|
1909
|
+
this.emitEvent({
|
|
1910
|
+
type: EVENT_TYPES.MESSAGE_SENT,
|
|
1911
|
+
source: actualMessage.from,
|
|
1912
|
+
data: {
|
|
1913
|
+
messageId: actualMessage.id,
|
|
1914
|
+
from: actualMessage.from,
|
|
1915
|
+
to: actualMessage.to,
|
|
1916
|
+
federatedTo: systemId
|
|
1917
|
+
}
|
|
1918
|
+
});
|
|
1919
|
+
return { routed: true, messageId: actualMessage.id };
|
|
1920
|
+
}
|
|
1921
|
+
// ===========================================================================
|
|
1922
|
+
// Notification Handlers
|
|
1923
|
+
// ===========================================================================
|
|
1924
|
+
#handleSubscriptionAck(params) {
|
|
1925
|
+
const { subscriptionId, upToSequence } = params;
|
|
1926
|
+
const subscription = this.#subscriptions.get(subscriptionId);
|
|
1927
|
+
if (subscription) {
|
|
1928
|
+
subscription.lastAckedSequence = upToSequence;
|
|
1929
|
+
}
|
|
1930
|
+
}
|
|
1931
|
+
/**
|
|
1932
|
+
* Get ack state for a subscription (for testing assertions)
|
|
1933
|
+
*/
|
|
1934
|
+
getSubscriptionAckState(subscriptionId) {
|
|
1935
|
+
const subscription = this.#subscriptions.get(subscriptionId);
|
|
1936
|
+
if (!subscription) return void 0;
|
|
1937
|
+
return {
|
|
1938
|
+
sequenceNumber: subscription.sequenceNumber,
|
|
1939
|
+
lastAckedSequence: subscription.lastAckedSequence
|
|
1940
|
+
};
|
|
1941
|
+
}
|
|
1942
|
+
// ===========================================================================
|
|
1943
|
+
// Permission Handlers
|
|
1944
|
+
// ===========================================================================
|
|
1945
|
+
/**
|
|
1946
|
+
* Handle permissions update request.
|
|
1947
|
+
* Updates client capabilities and emits permission update event.
|
|
1948
|
+
*/
|
|
1949
|
+
#handlePermissionsUpdate(connection, params) {
|
|
1950
|
+
const { clientId, permissions } = params;
|
|
1951
|
+
const targetParticipant = this.#participants.get(clientId);
|
|
1952
|
+
if (!targetParticipant) {
|
|
1953
|
+
throw MAPRequestError.invalidParams(`Client ${clientId} not found`);
|
|
1954
|
+
}
|
|
1955
|
+
let requestingParticipantId;
|
|
1956
|
+
for (const [id, p] of this.#participants) {
|
|
1957
|
+
if (p.connection === connection) {
|
|
1958
|
+
requestingParticipantId = id;
|
|
1959
|
+
break;
|
|
1960
|
+
}
|
|
1961
|
+
}
|
|
1962
|
+
const existingCapabilities = targetParticipant.capabilities ?? {};
|
|
1963
|
+
const effectivePermissions = {};
|
|
1964
|
+
if (existingCapabilities.observation) {
|
|
1965
|
+
effectivePermissions.observation = { ...existingCapabilities.observation };
|
|
1966
|
+
}
|
|
1967
|
+
if (existingCapabilities.messaging) {
|
|
1968
|
+
effectivePermissions.messaging = { ...existingCapabilities.messaging };
|
|
1969
|
+
}
|
|
1970
|
+
if (existingCapabilities.lifecycle) {
|
|
1971
|
+
effectivePermissions.lifecycle = { ...existingCapabilities.lifecycle };
|
|
1972
|
+
}
|
|
1973
|
+
if (existingCapabilities.scopes) {
|
|
1974
|
+
effectivePermissions.scopes = { ...existingCapabilities.scopes };
|
|
1975
|
+
}
|
|
1976
|
+
if (existingCapabilities.federation) {
|
|
1977
|
+
effectivePermissions.federation = { ...existingCapabilities.federation };
|
|
1978
|
+
}
|
|
1979
|
+
if (existingCapabilities.streaming) {
|
|
1980
|
+
effectivePermissions.streaming = { ...existingCapabilities.streaming };
|
|
1981
|
+
}
|
|
1982
|
+
if (permissions.observation) {
|
|
1983
|
+
effectivePermissions.observation = {
|
|
1984
|
+
...effectivePermissions.observation,
|
|
1985
|
+
...permissions.observation
|
|
1986
|
+
};
|
|
1987
|
+
}
|
|
1988
|
+
if (permissions.messaging) {
|
|
1989
|
+
effectivePermissions.messaging = {
|
|
1990
|
+
...effectivePermissions.messaging,
|
|
1991
|
+
...permissions.messaging
|
|
1992
|
+
};
|
|
1993
|
+
}
|
|
1994
|
+
if (permissions.lifecycle) {
|
|
1995
|
+
effectivePermissions.lifecycle = {
|
|
1996
|
+
...effectivePermissions.lifecycle,
|
|
1997
|
+
...permissions.lifecycle
|
|
1998
|
+
};
|
|
1999
|
+
}
|
|
2000
|
+
if (permissions.scopes) {
|
|
2001
|
+
effectivePermissions.scopes = {
|
|
2002
|
+
...effectivePermissions.scopes,
|
|
2003
|
+
...permissions.scopes
|
|
2004
|
+
};
|
|
2005
|
+
}
|
|
2006
|
+
if (permissions.federation) {
|
|
2007
|
+
effectivePermissions.federation = {
|
|
2008
|
+
...effectivePermissions.federation,
|
|
2009
|
+
...permissions.federation
|
|
2010
|
+
};
|
|
2011
|
+
}
|
|
2012
|
+
if (permissions.streaming) {
|
|
2013
|
+
effectivePermissions.streaming = {
|
|
2014
|
+
...effectivePermissions.streaming,
|
|
2015
|
+
...permissions.streaming
|
|
2016
|
+
};
|
|
2017
|
+
}
|
|
2018
|
+
targetParticipant.capabilities = effectivePermissions;
|
|
2019
|
+
this.emitEvent({
|
|
2020
|
+
type: EVENT_TYPES.PERMISSIONS_CLIENT_UPDATED,
|
|
2021
|
+
data: {
|
|
2022
|
+
clientId,
|
|
2023
|
+
changes: permissions,
|
|
2024
|
+
effectivePermissions,
|
|
2025
|
+
changedBy: requestingParticipantId
|
|
2026
|
+
}
|
|
2027
|
+
});
|
|
2028
|
+
return {
|
|
2029
|
+
success: true,
|
|
2030
|
+
effectivePermissions
|
|
2031
|
+
};
|
|
2032
|
+
}
|
|
2033
|
+
};
|
|
2034
|
+
|
|
2035
|
+
// src/subscription/index.ts
|
|
2036
|
+
var Subscription = class {
|
|
2037
|
+
id;
|
|
2038
|
+
filter;
|
|
2039
|
+
#eventHandlers = /* @__PURE__ */ new Set();
|
|
2040
|
+
#overflowHandlers = /* @__PURE__ */ new Set();
|
|
2041
|
+
#eventQueue = [];
|
|
2042
|
+
#bufferSize;
|
|
2043
|
+
#unsubscribe;
|
|
2044
|
+
#sendAck;
|
|
2045
|
+
// Deduplication tracking
|
|
2046
|
+
#seenEventIds = /* @__PURE__ */ new Set();
|
|
2047
|
+
#seenEventIdOrder = [];
|
|
2048
|
+
// For LRU eviction
|
|
2049
|
+
#maxSeenEventIds;
|
|
2050
|
+
#eventResolver = null;
|
|
2051
|
+
#pauseResolver = null;
|
|
2052
|
+
#state = "active";
|
|
2053
|
+
#lastSequenceNumber = -1;
|
|
2054
|
+
#lastEventId;
|
|
2055
|
+
#lastTimestamp;
|
|
2056
|
+
// Overflow tracking
|
|
2057
|
+
#totalDropped = 0;
|
|
2058
|
+
#oldestDroppedId;
|
|
2059
|
+
#newestDroppedId;
|
|
2060
|
+
// Ack support
|
|
2061
|
+
#serverSupportsAck = false;
|
|
2062
|
+
constructor(id, unsubscribe, options = {}, sendAck) {
|
|
2063
|
+
this.id = id;
|
|
2064
|
+
this.filter = options.filter;
|
|
2065
|
+
this.#bufferSize = options.bufferSize ?? 1e3;
|
|
2066
|
+
this.#maxSeenEventIds = options.maxSeenEventIds ?? 1e4;
|
|
2067
|
+
this.#unsubscribe = unsubscribe;
|
|
2068
|
+
this.#sendAck = sendAck;
|
|
2069
|
+
}
|
|
2070
|
+
/**
|
|
2071
|
+
* Current subscription state
|
|
2072
|
+
*/
|
|
2073
|
+
get state() {
|
|
2074
|
+
return this.#state;
|
|
2075
|
+
}
|
|
2076
|
+
/**
|
|
2077
|
+
* Whether the subscription is closed
|
|
2078
|
+
*/
|
|
2079
|
+
get isClosed() {
|
|
2080
|
+
return this.#state === "closed";
|
|
2081
|
+
}
|
|
2082
|
+
/**
|
|
2083
|
+
* Whether the subscription is paused
|
|
2084
|
+
*/
|
|
2085
|
+
get isPaused() {
|
|
2086
|
+
return this.#state === "paused";
|
|
2087
|
+
}
|
|
2088
|
+
/**
|
|
2089
|
+
* Last received sequence number (for ordering verification)
|
|
2090
|
+
*/
|
|
2091
|
+
get lastSequenceNumber() {
|
|
2092
|
+
return this.#lastSequenceNumber;
|
|
2093
|
+
}
|
|
2094
|
+
/**
|
|
2095
|
+
* Last received eventId (for replay positioning)
|
|
2096
|
+
*/
|
|
2097
|
+
get lastEventId() {
|
|
2098
|
+
return this.#lastEventId;
|
|
2099
|
+
}
|
|
2100
|
+
/**
|
|
2101
|
+
* Last received server timestamp
|
|
2102
|
+
*/
|
|
2103
|
+
get lastTimestamp() {
|
|
2104
|
+
return this.#lastTimestamp;
|
|
2105
|
+
}
|
|
2106
|
+
/**
|
|
2107
|
+
* Number of events currently buffered
|
|
2108
|
+
*/
|
|
2109
|
+
get bufferedCount() {
|
|
2110
|
+
return this.#eventQueue.length;
|
|
2111
|
+
}
|
|
2112
|
+
/**
|
|
2113
|
+
* Number of eventIds being tracked for deduplication
|
|
2114
|
+
*/
|
|
2115
|
+
get trackedEventIdCount() {
|
|
2116
|
+
return this.#seenEventIds.size;
|
|
2117
|
+
}
|
|
2118
|
+
/**
|
|
2119
|
+
* Total number of events dropped due to buffer overflow
|
|
2120
|
+
*/
|
|
2121
|
+
get totalDropped() {
|
|
2122
|
+
return this.#totalDropped;
|
|
2123
|
+
}
|
|
2124
|
+
/**
|
|
2125
|
+
* Whether the server supports acknowledgments
|
|
2126
|
+
*/
|
|
2127
|
+
get supportsAck() {
|
|
2128
|
+
return this.#serverSupportsAck && !!this.#sendAck;
|
|
2129
|
+
}
|
|
2130
|
+
/**
|
|
2131
|
+
* Pause event delivery from the async iterator.
|
|
2132
|
+
* Events are still buffered but not yielded until resume() is called.
|
|
2133
|
+
* Event handlers (on('event', ...)) still receive events while paused.
|
|
2134
|
+
*/
|
|
2135
|
+
pause() {
|
|
2136
|
+
if (this.#state === "closed") return;
|
|
2137
|
+
this.#state = "paused";
|
|
2138
|
+
}
|
|
2139
|
+
/**
|
|
2140
|
+
* Resume event delivery from the async iterator.
|
|
2141
|
+
* Any events buffered during pause will be yielded.
|
|
2142
|
+
*/
|
|
2143
|
+
resume() {
|
|
2144
|
+
if (this.#state === "closed") return;
|
|
2145
|
+
this.#state = "active";
|
|
2146
|
+
if (this.#pauseResolver) {
|
|
2147
|
+
this.#pauseResolver();
|
|
2148
|
+
this.#pauseResolver = null;
|
|
2149
|
+
}
|
|
2150
|
+
if (this.#eventResolver && this.#eventQueue.length > 0) {
|
|
2151
|
+
const event = this.#eventQueue.shift();
|
|
2152
|
+
this.#eventResolver(event);
|
|
2153
|
+
this.#eventResolver = null;
|
|
2154
|
+
}
|
|
2155
|
+
}
|
|
2156
|
+
/**
|
|
2157
|
+
* Acknowledge events up to a sequence number.
|
|
2158
|
+
* No-op if server doesn't support acks.
|
|
2159
|
+
*
|
|
2160
|
+
* @param upToSequence - Acknowledge all events up to and including this sequence.
|
|
2161
|
+
* If omitted, acknowledges up to lastSequenceNumber.
|
|
2162
|
+
*/
|
|
2163
|
+
ack(upToSequence) {
|
|
2164
|
+
if (!this.supportsAck) return;
|
|
2165
|
+
const seq = upToSequence ?? this.#lastSequenceNumber;
|
|
2166
|
+
if (seq < 0) return;
|
|
2167
|
+
this.#sendAck({
|
|
2168
|
+
subscriptionId: this.id,
|
|
2169
|
+
upToSequence: seq
|
|
2170
|
+
});
|
|
2171
|
+
}
|
|
2172
|
+
on(type, handler) {
|
|
2173
|
+
if (type === "event") {
|
|
2174
|
+
this.#eventHandlers.add(handler);
|
|
2175
|
+
} else if (type === "overflow") {
|
|
2176
|
+
this.#overflowHandlers.add(handler);
|
|
2177
|
+
}
|
|
2178
|
+
return this;
|
|
2179
|
+
}
|
|
2180
|
+
off(type, handler) {
|
|
2181
|
+
if (type === "event") {
|
|
2182
|
+
this.#eventHandlers.delete(handler);
|
|
2183
|
+
} else if (type === "overflow") {
|
|
2184
|
+
this.#overflowHandlers.delete(handler);
|
|
2185
|
+
}
|
|
2186
|
+
return this;
|
|
2187
|
+
}
|
|
2188
|
+
/**
|
|
2189
|
+
* Register a one-time event handler
|
|
2190
|
+
*/
|
|
2191
|
+
once(type, handler) {
|
|
2192
|
+
if (type === "event") {
|
|
2193
|
+
const wrapper = (event) => {
|
|
2194
|
+
this.off("event", wrapper);
|
|
2195
|
+
handler(event);
|
|
2196
|
+
};
|
|
2197
|
+
this.on("event", wrapper);
|
|
2198
|
+
}
|
|
2199
|
+
return this;
|
|
2200
|
+
}
|
|
2201
|
+
/**
|
|
2202
|
+
* Unsubscribe and close the subscription
|
|
2203
|
+
*/
|
|
2204
|
+
async unsubscribe() {
|
|
2205
|
+
if (this.#state === "closed") return;
|
|
2206
|
+
this.#state = "closed";
|
|
2207
|
+
if (this.#eventResolver) {
|
|
2208
|
+
this.#eventResolver(null);
|
|
2209
|
+
this.#eventResolver = null;
|
|
2210
|
+
}
|
|
2211
|
+
if (this.#pauseResolver) {
|
|
2212
|
+
this.#pauseResolver();
|
|
2213
|
+
this.#pauseResolver = null;
|
|
2214
|
+
}
|
|
2215
|
+
this.#eventHandlers.clear();
|
|
2216
|
+
this.#overflowHandlers.clear();
|
|
2217
|
+
this.#seenEventIds.clear();
|
|
2218
|
+
this.#seenEventIdOrder.length = 0;
|
|
2219
|
+
await this.#unsubscribe();
|
|
2220
|
+
}
|
|
2221
|
+
/**
|
|
2222
|
+
* Set whether server supports acknowledgments.
|
|
2223
|
+
* Called by connection after capability negotiation.
|
|
2224
|
+
* @internal
|
|
2225
|
+
*/
|
|
2226
|
+
_setServerSupportsAck(supports) {
|
|
2227
|
+
this.#serverSupportsAck = supports;
|
|
2228
|
+
}
|
|
2229
|
+
/**
|
|
2230
|
+
* Push an event to the subscription (called by connection)
|
|
2231
|
+
* @internal
|
|
2232
|
+
*/
|
|
2233
|
+
_pushEvent(params) {
|
|
2234
|
+
if (this.#state === "closed") return;
|
|
2235
|
+
const { sequenceNumber, eventId, timestamp, event } = params;
|
|
2236
|
+
if (eventId) {
|
|
2237
|
+
if (this.#seenEventIds.has(eventId)) {
|
|
2238
|
+
return;
|
|
2239
|
+
}
|
|
2240
|
+
this.#seenEventIds.add(eventId);
|
|
2241
|
+
this.#seenEventIdOrder.push(eventId);
|
|
2242
|
+
while (this.#seenEventIds.size > this.#maxSeenEventIds) {
|
|
2243
|
+
const oldestId = this.#seenEventIdOrder.shift();
|
|
2244
|
+
if (oldestId) {
|
|
2245
|
+
this.#seenEventIds.delete(oldestId);
|
|
2246
|
+
}
|
|
2247
|
+
}
|
|
2248
|
+
this.#lastEventId = eventId;
|
|
2249
|
+
}
|
|
2250
|
+
if (timestamp !== void 0) {
|
|
2251
|
+
this.#lastTimestamp = timestamp;
|
|
2252
|
+
}
|
|
2253
|
+
if (this.#lastSequenceNumber >= 0 && sequenceNumber !== this.#lastSequenceNumber + 1) {
|
|
2254
|
+
console.warn(
|
|
2255
|
+
`MAP: Subscription ${this.id} sequence gap: expected ${this.#lastSequenceNumber + 1}, got ${sequenceNumber}`
|
|
2256
|
+
);
|
|
2257
|
+
}
|
|
2258
|
+
this.#lastSequenceNumber = sequenceNumber;
|
|
2259
|
+
for (const handler of this.#eventHandlers) {
|
|
2260
|
+
try {
|
|
2261
|
+
handler(event);
|
|
2262
|
+
} catch (error) {
|
|
2263
|
+
console.error("MAP: Event handler error:", error);
|
|
2264
|
+
}
|
|
2265
|
+
}
|
|
2266
|
+
if (this.#eventResolver && this.#state === "active") {
|
|
2267
|
+
this.#eventResolver(event);
|
|
2268
|
+
this.#eventResolver = null;
|
|
2269
|
+
return;
|
|
2270
|
+
}
|
|
2271
|
+
if (this.#eventQueue.length < this.#bufferSize) {
|
|
2272
|
+
this.#eventQueue.push(event);
|
|
2273
|
+
} else {
|
|
2274
|
+
this.#totalDropped++;
|
|
2275
|
+
if (eventId) {
|
|
2276
|
+
if (this.#oldestDroppedId === void 0) {
|
|
2277
|
+
this.#oldestDroppedId = eventId;
|
|
2278
|
+
}
|
|
2279
|
+
this.#newestDroppedId = eventId;
|
|
2280
|
+
}
|
|
2281
|
+
const info = {
|
|
2282
|
+
eventsDropped: 1,
|
|
2283
|
+
oldestDroppedId: this.#oldestDroppedId,
|
|
2284
|
+
newestDroppedId: this.#newestDroppedId,
|
|
2285
|
+
timestamp: Date.now(),
|
|
2286
|
+
totalDropped: this.#totalDropped
|
|
2287
|
+
};
|
|
2288
|
+
for (const handler of this.#overflowHandlers) {
|
|
2289
|
+
try {
|
|
2290
|
+
handler(info);
|
|
2291
|
+
} catch (error) {
|
|
2292
|
+
console.error("MAP: Overflow handler error:", error);
|
|
2293
|
+
}
|
|
2294
|
+
}
|
|
2295
|
+
console.warn(`MAP: Subscription ${this.id} buffer full, dropping event`);
|
|
2296
|
+
}
|
|
2297
|
+
}
|
|
2298
|
+
/**
|
|
2299
|
+
* Mark the subscription as closed (called by connection)
|
|
2300
|
+
* @internal
|
|
2301
|
+
*/
|
|
2302
|
+
_close() {
|
|
2303
|
+
this.#state = "closed";
|
|
2304
|
+
if (this.#eventResolver) {
|
|
2305
|
+
this.#eventResolver(null);
|
|
2306
|
+
this.#eventResolver = null;
|
|
2307
|
+
}
|
|
2308
|
+
if (this.#pauseResolver) {
|
|
2309
|
+
this.#pauseResolver();
|
|
2310
|
+
this.#pauseResolver = null;
|
|
2311
|
+
}
|
|
2312
|
+
}
|
|
2313
|
+
/**
|
|
2314
|
+
* Async iterator implementation
|
|
2315
|
+
*/
|
|
2316
|
+
async *[Symbol.asyncIterator]() {
|
|
2317
|
+
while (!this.isClosed) {
|
|
2318
|
+
while (this.isPaused) {
|
|
2319
|
+
await new Promise((resolve) => {
|
|
2320
|
+
this.#pauseResolver = resolve;
|
|
2321
|
+
});
|
|
2322
|
+
if (this.isClosed) {
|
|
2323
|
+
while (this.#eventQueue.length > 0) {
|
|
2324
|
+
yield this.#eventQueue.shift();
|
|
2325
|
+
}
|
|
2326
|
+
return;
|
|
2327
|
+
}
|
|
2328
|
+
}
|
|
2329
|
+
if (this.#eventQueue.length > 0) {
|
|
2330
|
+
yield this.#eventQueue.shift();
|
|
2331
|
+
continue;
|
|
2332
|
+
}
|
|
2333
|
+
const event = await new Promise((resolve) => {
|
|
2334
|
+
this.#eventResolver = resolve;
|
|
2335
|
+
});
|
|
2336
|
+
if (event === null) {
|
|
2337
|
+
break;
|
|
2338
|
+
}
|
|
2339
|
+
yield event;
|
|
2340
|
+
}
|
|
2341
|
+
while (this.#eventQueue.length > 0) {
|
|
2342
|
+
yield this.#eventQueue.shift();
|
|
2343
|
+
}
|
|
2344
|
+
}
|
|
2345
|
+
};
|
|
2346
|
+
function createSubscription(id, unsubscribe, options, sendAck) {
|
|
2347
|
+
return new Subscription(id, unsubscribe, options, sendAck);
|
|
2348
|
+
}
|
|
2349
|
+
|
|
2350
|
+
// src/connection/client.ts
|
|
2351
|
+
var ClientConnection = class {
|
|
2352
|
+
#connection;
|
|
2353
|
+
#subscriptions = /* @__PURE__ */ new Map();
|
|
2354
|
+
#subscriptionStates = /* @__PURE__ */ new Map();
|
|
2355
|
+
#reconnectionHandlers = /* @__PURE__ */ new Set();
|
|
2356
|
+
#options;
|
|
2357
|
+
#sessionId = null;
|
|
2358
|
+
#serverCapabilities = null;
|
|
2359
|
+
#connected = false;
|
|
2360
|
+
#lastConnectOptions;
|
|
2361
|
+
#isReconnecting = false;
|
|
2362
|
+
constructor(stream, options = {}) {
|
|
2363
|
+
this.#connection = new BaseConnection(stream, options);
|
|
2364
|
+
this.#options = options;
|
|
2365
|
+
this.#connection.setNotificationHandler(this.#handleNotification.bind(this));
|
|
2366
|
+
if (options.reconnection?.enabled && options.createStream) {
|
|
2367
|
+
this.#connection.onStateChange((newState) => {
|
|
2368
|
+
if (newState === "closed" && this.#connected && !this.#isReconnecting) {
|
|
2369
|
+
void this.#handleDisconnect();
|
|
2370
|
+
}
|
|
2371
|
+
});
|
|
2372
|
+
}
|
|
2373
|
+
}
|
|
2374
|
+
// ===========================================================================
|
|
2375
|
+
// Connection Lifecycle
|
|
2376
|
+
// ===========================================================================
|
|
2377
|
+
/**
|
|
2378
|
+
* Connect to the MAP system
|
|
2379
|
+
*/
|
|
2380
|
+
async connect(options) {
|
|
2381
|
+
const params = {
|
|
2382
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
2383
|
+
participantType: "client",
|
|
2384
|
+
name: this.#options.name,
|
|
2385
|
+
capabilities: this.#options.capabilities,
|
|
2386
|
+
sessionId: options?.sessionId,
|
|
2387
|
+
auth: options?.auth
|
|
2388
|
+
};
|
|
2389
|
+
const result = await this.#connection.sendRequest(CORE_METHODS.CONNECT, params);
|
|
2390
|
+
this.#sessionId = result.sessionId;
|
|
2391
|
+
this.#serverCapabilities = result.capabilities;
|
|
2392
|
+
this.#connected = true;
|
|
2393
|
+
this.#connection._transitionTo("connected");
|
|
2394
|
+
this.#lastConnectOptions = options;
|
|
2395
|
+
return result;
|
|
2396
|
+
}
|
|
2397
|
+
/**
|
|
2398
|
+
* Disconnect from the MAP system
|
|
2399
|
+
*/
|
|
2400
|
+
async disconnect(reason) {
|
|
2401
|
+
if (!this.#connected) return;
|
|
2402
|
+
try {
|
|
2403
|
+
await this.#connection.sendRequest(
|
|
2404
|
+
CORE_METHODS.DISCONNECT,
|
|
2405
|
+
reason ? { reason } : void 0
|
|
2406
|
+
);
|
|
2407
|
+
} finally {
|
|
2408
|
+
for (const subscription of this.#subscriptions.values()) {
|
|
2409
|
+
subscription._close();
|
|
2410
|
+
}
|
|
2411
|
+
this.#subscriptions.clear();
|
|
2412
|
+
await this.#connection.close();
|
|
2413
|
+
this.#connected = false;
|
|
2414
|
+
}
|
|
2415
|
+
}
|
|
2416
|
+
/**
|
|
2417
|
+
* Whether the client is connected
|
|
2418
|
+
*/
|
|
2419
|
+
get isConnected() {
|
|
2420
|
+
return this.#connected && !this.#connection.isClosed;
|
|
2421
|
+
}
|
|
2422
|
+
/**
|
|
2423
|
+
* Current session ID
|
|
2424
|
+
*/
|
|
2425
|
+
get sessionId() {
|
|
2426
|
+
return this.#sessionId;
|
|
2427
|
+
}
|
|
2428
|
+
/**
|
|
2429
|
+
* Server capabilities
|
|
2430
|
+
*/
|
|
2431
|
+
get serverCapabilities() {
|
|
2432
|
+
return this.#serverCapabilities;
|
|
2433
|
+
}
|
|
2434
|
+
/**
|
|
2435
|
+
* AbortSignal that triggers when the connection closes
|
|
2436
|
+
*/
|
|
2437
|
+
get signal() {
|
|
2438
|
+
return this.#connection.signal;
|
|
2439
|
+
}
|
|
2440
|
+
/**
|
|
2441
|
+
* Promise that resolves when the connection closes
|
|
2442
|
+
*/
|
|
2443
|
+
get closed() {
|
|
2444
|
+
return this.#connection.closed;
|
|
2445
|
+
}
|
|
2446
|
+
// ===========================================================================
|
|
2447
|
+
// Session Management
|
|
2448
|
+
// ===========================================================================
|
|
2449
|
+
/**
|
|
2450
|
+
* List available sessions
|
|
2451
|
+
*/
|
|
2452
|
+
async listSessions() {
|
|
2453
|
+
return this.#connection.sendRequest(SESSION_METHODS.SESSION_LIST);
|
|
2454
|
+
}
|
|
2455
|
+
/**
|
|
2456
|
+
* Load an existing session
|
|
2457
|
+
*/
|
|
2458
|
+
async loadSession(sessionId) {
|
|
2459
|
+
return this.#connection.sendRequest(SESSION_METHODS.SESSION_LOAD, { sessionId });
|
|
2460
|
+
}
|
|
2461
|
+
/**
|
|
2462
|
+
* Close the current session
|
|
2463
|
+
*/
|
|
2464
|
+
async closeSession(sessionId) {
|
|
2465
|
+
return this.#connection.sendRequest(SESSION_METHODS.SESSION_CLOSE, { sessionId });
|
|
2466
|
+
}
|
|
2467
|
+
// ===========================================================================
|
|
2468
|
+
// Agent Queries
|
|
2469
|
+
// ===========================================================================
|
|
2470
|
+
/**
|
|
2471
|
+
* List agents with optional filters
|
|
2472
|
+
*/
|
|
2473
|
+
async listAgents(options) {
|
|
2474
|
+
return this.#connection.sendRequest(OBSERVATION_METHODS.AGENTS_LIST, options);
|
|
2475
|
+
}
|
|
2476
|
+
/**
|
|
2477
|
+
* Get a single agent by ID
|
|
2478
|
+
*/
|
|
2479
|
+
async getAgent(agentId, options) {
|
|
2480
|
+
const params = { agentId, ...options };
|
|
2481
|
+
return this.#connection.sendRequest(
|
|
2482
|
+
OBSERVATION_METHODS.AGENTS_GET,
|
|
2483
|
+
params
|
|
2484
|
+
);
|
|
2485
|
+
}
|
|
2486
|
+
/**
|
|
2487
|
+
* Get the agent structure/hierarchy graph
|
|
2488
|
+
*/
|
|
2489
|
+
async getStructureGraph(options) {
|
|
2490
|
+
return this.#connection.sendRequest(OBSERVATION_METHODS.STRUCTURE_GRAPH, options);
|
|
2491
|
+
}
|
|
2492
|
+
// ===========================================================================
|
|
2493
|
+
// Scope Queries
|
|
2494
|
+
// ===========================================================================
|
|
2495
|
+
/**
|
|
2496
|
+
* List scopes
|
|
2497
|
+
*/
|
|
2498
|
+
async listScopes(options) {
|
|
2499
|
+
return this.#connection.sendRequest(OBSERVATION_METHODS.SCOPES_LIST, options);
|
|
2500
|
+
}
|
|
2501
|
+
/**
|
|
2502
|
+
* Get a single scope by ID
|
|
2503
|
+
*/
|
|
2504
|
+
async getScope(scopeId) {
|
|
2505
|
+
const result = await this.#connection.sendRequest(OBSERVATION_METHODS.SCOPES_GET, { scopeId });
|
|
2506
|
+
return result.scope;
|
|
2507
|
+
}
|
|
2508
|
+
/**
|
|
2509
|
+
* List members of a scope
|
|
2510
|
+
*/
|
|
2511
|
+
async getScopeMembers(scopeId, options) {
|
|
2512
|
+
return this.#connection.sendRequest(OBSERVATION_METHODS.SCOPES_MEMBERS, {
|
|
2513
|
+
scopeId,
|
|
2514
|
+
...options
|
|
2515
|
+
});
|
|
2516
|
+
}
|
|
2517
|
+
// ===========================================================================
|
|
2518
|
+
// Messaging
|
|
2519
|
+
// ===========================================================================
|
|
2520
|
+
/**
|
|
2521
|
+
* Send a message to an address
|
|
2522
|
+
*/
|
|
2523
|
+
async send(to, payload, meta) {
|
|
2524
|
+
const params = { to };
|
|
2525
|
+
if (payload !== void 0) params.payload = payload;
|
|
2526
|
+
if (meta) params.meta = meta;
|
|
2527
|
+
return this.#connection.sendRequest(CORE_METHODS.SEND, params);
|
|
2528
|
+
}
|
|
2529
|
+
/**
|
|
2530
|
+
* Send a message to a specific agent
|
|
2531
|
+
*/
|
|
2532
|
+
async sendToAgent(agentId, payload, meta) {
|
|
2533
|
+
return this.send({ agent: agentId }, payload, meta);
|
|
2534
|
+
}
|
|
2535
|
+
/**
|
|
2536
|
+
* Send a message to all agents in a scope
|
|
2537
|
+
*/
|
|
2538
|
+
async sendToScope(scopeId, payload, meta) {
|
|
2539
|
+
return this.send({ scope: scopeId }, payload, meta);
|
|
2540
|
+
}
|
|
2541
|
+
/**
|
|
2542
|
+
* Send a message to agents with a specific role
|
|
2543
|
+
*/
|
|
2544
|
+
async sendToRole(role, payload, meta, withinScope) {
|
|
2545
|
+
return this.send({ role, within: withinScope }, payload, meta);
|
|
2546
|
+
}
|
|
2547
|
+
/**
|
|
2548
|
+
* Broadcast a message to all agents
|
|
2549
|
+
*/
|
|
2550
|
+
async broadcast(payload, meta) {
|
|
2551
|
+
return this.send({ broadcast: true }, payload, meta);
|
|
2552
|
+
}
|
|
2553
|
+
/**
|
|
2554
|
+
* Send a request and wait for a correlated response
|
|
2555
|
+
*
|
|
2556
|
+
* This is a higher-level pattern for request/response messaging.
|
|
2557
|
+
* A correlationId is automatically generated.
|
|
2558
|
+
*/
|
|
2559
|
+
async request(to, payload, options) {
|
|
2560
|
+
const correlationId = `req-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
2561
|
+
const responseSub = await this.subscribe({
|
|
2562
|
+
// We'll filter in the handler since subscription filters don't support correlationId
|
|
2563
|
+
});
|
|
2564
|
+
try {
|
|
2565
|
+
await this.send(to, payload, {
|
|
2566
|
+
...options?.meta,
|
|
2567
|
+
expectsResponse: true,
|
|
2568
|
+
correlationId
|
|
2569
|
+
});
|
|
2570
|
+
const timeout = options?.timeout ?? 3e4;
|
|
2571
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
2572
|
+
setTimeout(() => reject(new Error(`Request timed out after ${timeout}ms`)), timeout);
|
|
2573
|
+
});
|
|
2574
|
+
const responsePromise = (async () => {
|
|
2575
|
+
for await (const event of responseSub) {
|
|
2576
|
+
if (event.type === "message_delivered" && event.data && event.data.correlationId === correlationId) {
|
|
2577
|
+
return event.data.message;
|
|
2578
|
+
}
|
|
2579
|
+
}
|
|
2580
|
+
throw new Error("Subscription closed before response received");
|
|
2581
|
+
})();
|
|
2582
|
+
return await Promise.race([responsePromise, timeoutPromise]);
|
|
2583
|
+
} finally {
|
|
2584
|
+
await responseSub.unsubscribe();
|
|
2585
|
+
}
|
|
2586
|
+
}
|
|
2587
|
+
// ===========================================================================
|
|
2588
|
+
// Subscriptions
|
|
2589
|
+
// ===========================================================================
|
|
2590
|
+
/**
|
|
2591
|
+
* Subscribe to events
|
|
2592
|
+
*/
|
|
2593
|
+
async subscribe(filter) {
|
|
2594
|
+
const params = {};
|
|
2595
|
+
if (filter) params.filter = filter;
|
|
2596
|
+
const result = await this.#connection.sendRequest(CORE_METHODS.SUBSCRIBE, params);
|
|
2597
|
+
const serverSupportsAck = this.#serverCapabilities?.streaming?.supportsAck === true;
|
|
2598
|
+
const sendAck = serverSupportsAck ? (ackParams) => {
|
|
2599
|
+
this.#connection.sendNotification(NOTIFICATION_METHODS.SUBSCRIBE_ACK, ackParams);
|
|
2600
|
+
} : void 0;
|
|
2601
|
+
const subscription = createSubscription(
|
|
2602
|
+
result.subscriptionId,
|
|
2603
|
+
() => this.unsubscribe(result.subscriptionId),
|
|
2604
|
+
{ filter },
|
|
2605
|
+
sendAck
|
|
2606
|
+
);
|
|
2607
|
+
if (serverSupportsAck) {
|
|
2608
|
+
subscription._setServerSupportsAck(true);
|
|
2609
|
+
}
|
|
2610
|
+
this.#subscriptions.set(result.subscriptionId, subscription);
|
|
2611
|
+
if (this.#options.reconnection?.restoreSubscriptions !== false) {
|
|
2612
|
+
this.#subscriptionStates.set(result.subscriptionId, {
|
|
2613
|
+
filter,
|
|
2614
|
+
handlers: /* @__PURE__ */ new Set()
|
|
2615
|
+
});
|
|
2616
|
+
const originalPushEvent = subscription._pushEvent.bind(subscription);
|
|
2617
|
+
subscription._pushEvent = (event) => {
|
|
2618
|
+
const state = this.#subscriptionStates.get(result.subscriptionId);
|
|
2619
|
+
if (state && event.eventId) {
|
|
2620
|
+
state.lastEventId = event.eventId;
|
|
2621
|
+
}
|
|
2622
|
+
originalPushEvent(event);
|
|
2623
|
+
};
|
|
2624
|
+
}
|
|
2625
|
+
return subscription;
|
|
2626
|
+
}
|
|
2627
|
+
/**
|
|
2628
|
+
* Unsubscribe from events
|
|
2629
|
+
*/
|
|
2630
|
+
async unsubscribe(subscriptionId) {
|
|
2631
|
+
const subscription = this.#subscriptions.get(subscriptionId);
|
|
2632
|
+
if (subscription) {
|
|
2633
|
+
subscription._close();
|
|
2634
|
+
this.#subscriptions.delete(subscriptionId);
|
|
2635
|
+
}
|
|
2636
|
+
this.#subscriptionStates.delete(subscriptionId);
|
|
2637
|
+
await this.#connection.sendRequest(CORE_METHODS.UNSUBSCRIBE, { subscriptionId });
|
|
2638
|
+
}
|
|
2639
|
+
// ===========================================================================
|
|
2640
|
+
// Event Replay
|
|
2641
|
+
// ===========================================================================
|
|
2642
|
+
/**
|
|
2643
|
+
* Replay historical events.
|
|
2644
|
+
*
|
|
2645
|
+
* Uses keyset pagination - pass the last eventId from the previous
|
|
2646
|
+
* response to get the next page.
|
|
2647
|
+
*
|
|
2648
|
+
* @example
|
|
2649
|
+
* ```typescript
|
|
2650
|
+
* // Replay all events from the last hour
|
|
2651
|
+
* const result = await client.replay({
|
|
2652
|
+
* fromTimestamp: Date.now() - 3600000,
|
|
2653
|
+
* filter: { eventTypes: ['agent.registered'] },
|
|
2654
|
+
* limit: 100
|
|
2655
|
+
* });
|
|
2656
|
+
*
|
|
2657
|
+
* // Paginate through results
|
|
2658
|
+
* let afterEventId: string | undefined;
|
|
2659
|
+
* do {
|
|
2660
|
+
* const page = await client.replay({ afterEventId, limit: 100 });
|
|
2661
|
+
* for (const item of page.events) {
|
|
2662
|
+
* console.log(item.eventId, item.event);
|
|
2663
|
+
* }
|
|
2664
|
+
* afterEventId = page.events.at(-1)?.eventId;
|
|
2665
|
+
* } while (page.hasMore);
|
|
2666
|
+
* ```
|
|
2667
|
+
*/
|
|
2668
|
+
async replay(params = {}) {
|
|
2669
|
+
const limit = Math.min(params.limit ?? 100, 1e3);
|
|
2670
|
+
return this.#connection.sendRequest(
|
|
2671
|
+
CORE_METHODS.REPLAY,
|
|
2672
|
+
{ ...params, limit }
|
|
2673
|
+
);
|
|
2674
|
+
}
|
|
2675
|
+
/**
|
|
2676
|
+
* Replay all events matching filter, handling pagination automatically.
|
|
2677
|
+
*
|
|
2678
|
+
* Returns an async generator for streaming through all results.
|
|
2679
|
+
*
|
|
2680
|
+
* @example
|
|
2681
|
+
* ```typescript
|
|
2682
|
+
* for await (const item of client.replayAll({
|
|
2683
|
+
* filter: { eventTypes: ['agent.registered'] }
|
|
2684
|
+
* })) {
|
|
2685
|
+
* console.log(item.eventId, item.event);
|
|
2686
|
+
* }
|
|
2687
|
+
* ```
|
|
2688
|
+
*/
|
|
2689
|
+
async *replayAll(params = {}) {
|
|
2690
|
+
let afterEventId;
|
|
2691
|
+
let hasMore = true;
|
|
2692
|
+
while (hasMore) {
|
|
2693
|
+
const result = await this.replay({ ...params, afterEventId });
|
|
2694
|
+
for (const item of result.events) {
|
|
2695
|
+
yield item;
|
|
2696
|
+
}
|
|
2697
|
+
hasMore = result.hasMore;
|
|
2698
|
+
afterEventId = result.events.at(-1)?.eventId;
|
|
2699
|
+
if (result.events.length === 0) {
|
|
2700
|
+
break;
|
|
2701
|
+
}
|
|
2702
|
+
}
|
|
2703
|
+
}
|
|
2704
|
+
// ===========================================================================
|
|
2705
|
+
// Steering (requires canSteer capability)
|
|
2706
|
+
// ===========================================================================
|
|
2707
|
+
/**
|
|
2708
|
+
* Inject context into a running agent
|
|
2709
|
+
*/
|
|
2710
|
+
async inject(agentId, content, delivery) {
|
|
2711
|
+
const params = { agentId, content };
|
|
2712
|
+
if (delivery) params.delivery = delivery;
|
|
2713
|
+
return this.#connection.sendRequest(STEERING_METHODS.INJECT, params);
|
|
2714
|
+
}
|
|
2715
|
+
// ===========================================================================
|
|
2716
|
+
// Lifecycle Control (requires canStop capability)
|
|
2717
|
+
// ===========================================================================
|
|
2718
|
+
/**
|
|
2719
|
+
* Request an agent to stop
|
|
2720
|
+
*/
|
|
2721
|
+
async stopAgent(agentId, options) {
|
|
2722
|
+
return this.#connection.sendRequest(STATE_METHODS.AGENTS_STOP, {
|
|
2723
|
+
agentId,
|
|
2724
|
+
...options
|
|
2725
|
+
});
|
|
2726
|
+
}
|
|
2727
|
+
/**
|
|
2728
|
+
* Suspend an agent
|
|
2729
|
+
*/
|
|
2730
|
+
async suspendAgent(agentId, reason) {
|
|
2731
|
+
return this.#connection.sendRequest(STATE_METHODS.AGENTS_SUSPEND, {
|
|
2732
|
+
agentId,
|
|
2733
|
+
reason
|
|
2734
|
+
});
|
|
2735
|
+
}
|
|
2736
|
+
/**
|
|
2737
|
+
* Resume a suspended agent
|
|
2738
|
+
*/
|
|
2739
|
+
async resumeAgent(agentId) {
|
|
2740
|
+
return this.#connection.sendRequest(STATE_METHODS.AGENTS_RESUME, { agentId });
|
|
2741
|
+
}
|
|
2742
|
+
// ===========================================================================
|
|
2743
|
+
// Reconnection
|
|
2744
|
+
// ===========================================================================
|
|
2745
|
+
/**
|
|
2746
|
+
* Current connection state
|
|
2747
|
+
*/
|
|
2748
|
+
get state() {
|
|
2749
|
+
return this.#connection.state;
|
|
2750
|
+
}
|
|
2751
|
+
/**
|
|
2752
|
+
* Whether the connection is currently reconnecting
|
|
2753
|
+
*/
|
|
2754
|
+
get isReconnecting() {
|
|
2755
|
+
return this.#isReconnecting;
|
|
2756
|
+
}
|
|
2757
|
+
/**
|
|
2758
|
+
* Register a handler for reconnection events.
|
|
2759
|
+
*
|
|
2760
|
+
* @param handler - Function called when reconnection events occur
|
|
2761
|
+
* @returns Unsubscribe function to remove the handler
|
|
2762
|
+
*
|
|
2763
|
+
* @example
|
|
2764
|
+
* ```typescript
|
|
2765
|
+
* const unsubscribe = client.onReconnection((event) => {
|
|
2766
|
+
* switch (event.type) {
|
|
2767
|
+
* case 'disconnected':
|
|
2768
|
+
* console.log('Connection lost');
|
|
2769
|
+
* break;
|
|
2770
|
+
* case 'reconnecting':
|
|
2771
|
+
* console.log(`Reconnecting, attempt ${event.attempt}`);
|
|
2772
|
+
* break;
|
|
2773
|
+
* case 'reconnected':
|
|
2774
|
+
* console.log('Reconnected successfully');
|
|
2775
|
+
* break;
|
|
2776
|
+
* case 'reconnectFailed':
|
|
2777
|
+
* console.log('Failed to reconnect:', event.error);
|
|
2778
|
+
* break;
|
|
2779
|
+
* }
|
|
2780
|
+
* });
|
|
2781
|
+
* ```
|
|
2782
|
+
*/
|
|
2783
|
+
onReconnection(handler) {
|
|
2784
|
+
this.#reconnectionHandlers.add(handler);
|
|
2785
|
+
return () => this.#reconnectionHandlers.delete(handler);
|
|
2786
|
+
}
|
|
2787
|
+
/**
|
|
2788
|
+
* Register a handler for connection state changes.
|
|
2789
|
+
*
|
|
2790
|
+
* @param handler - Function called when state changes
|
|
2791
|
+
* @returns Unsubscribe function to remove the handler
|
|
2792
|
+
*/
|
|
2793
|
+
onStateChange(handler) {
|
|
2794
|
+
return this.#connection.onStateChange(handler);
|
|
2795
|
+
}
|
|
2796
|
+
// ===========================================================================
|
|
2797
|
+
// Internal
|
|
2798
|
+
// ===========================================================================
|
|
2799
|
+
/**
|
|
2800
|
+
* Handle incoming notifications
|
|
2801
|
+
*/
|
|
2802
|
+
async #handleNotification(method, params) {
|
|
2803
|
+
switch (method) {
|
|
2804
|
+
case NOTIFICATION_METHODS.EVENT: {
|
|
2805
|
+
const eventParams = params;
|
|
2806
|
+
const subscription = this.#subscriptions.get(eventParams.subscriptionId);
|
|
2807
|
+
if (subscription) {
|
|
2808
|
+
subscription._pushEvent(eventParams);
|
|
2809
|
+
} else {
|
|
2810
|
+
console.warn("MAP: Event for unknown subscription:", eventParams.subscriptionId);
|
|
2811
|
+
}
|
|
2812
|
+
break;
|
|
2813
|
+
}
|
|
2814
|
+
case NOTIFICATION_METHODS.MESSAGE: {
|
|
2815
|
+
break;
|
|
2816
|
+
}
|
|
2817
|
+
default:
|
|
2818
|
+
console.warn("MAP: Unknown notification:", method);
|
|
2819
|
+
}
|
|
2820
|
+
}
|
|
2821
|
+
/**
|
|
2822
|
+
* Emit a reconnection event to all registered handlers
|
|
2823
|
+
*/
|
|
2824
|
+
#emitReconnectionEvent(event) {
|
|
2825
|
+
for (const handler of this.#reconnectionHandlers) {
|
|
2826
|
+
try {
|
|
2827
|
+
handler(event);
|
|
2828
|
+
} catch (error) {
|
|
2829
|
+
console.error("MAP: Reconnection event handler error:", error);
|
|
2830
|
+
}
|
|
2831
|
+
}
|
|
2832
|
+
}
|
|
2833
|
+
/**
|
|
2834
|
+
* Handle disconnect when auto-reconnect is enabled
|
|
2835
|
+
*/
|
|
2836
|
+
async #handleDisconnect() {
|
|
2837
|
+
this.#isReconnecting = true;
|
|
2838
|
+
this.#connected = false;
|
|
2839
|
+
this.#emitReconnectionEvent({ type: "disconnected" });
|
|
2840
|
+
try {
|
|
2841
|
+
await this.#attemptReconnect();
|
|
2842
|
+
} catch (error) {
|
|
2843
|
+
this.#isReconnecting = false;
|
|
2844
|
+
this.#emitReconnectionEvent({
|
|
2845
|
+
type: "reconnectFailed",
|
|
2846
|
+
error: error instanceof Error ? error : new Error(String(error))
|
|
2847
|
+
});
|
|
2848
|
+
}
|
|
2849
|
+
}
|
|
2850
|
+
/**
|
|
2851
|
+
* Attempt to reconnect with retry logic
|
|
2852
|
+
*/
|
|
2853
|
+
async #attemptReconnect() {
|
|
2854
|
+
const options = this.#options.reconnection;
|
|
2855
|
+
const createStream = this.#options.createStream;
|
|
2856
|
+
const retryPolicy = {
|
|
2857
|
+
maxRetries: options.maxRetries ?? DEFAULT_RETRY_POLICY.maxRetries,
|
|
2858
|
+
baseDelayMs: options.baseDelayMs ?? DEFAULT_RETRY_POLICY.baseDelayMs,
|
|
2859
|
+
maxDelayMs: options.maxDelayMs ?? DEFAULT_RETRY_POLICY.maxDelayMs,
|
|
2860
|
+
jitter: options.jitter ?? DEFAULT_RETRY_POLICY.jitter
|
|
2861
|
+
};
|
|
2862
|
+
await withRetry(
|
|
2863
|
+
async () => {
|
|
2864
|
+
const newStream = await createStream();
|
|
2865
|
+
await this.#connection.reconnect(newStream);
|
|
2866
|
+
const connectResult = await this.connect(this.#lastConnectOptions);
|
|
2867
|
+
this.#sessionId = connectResult.sessionId;
|
|
2868
|
+
this.#serverCapabilities = connectResult.capabilities;
|
|
2869
|
+
},
|
|
2870
|
+
retryPolicy,
|
|
2871
|
+
{
|
|
2872
|
+
onRetry: (state) => {
|
|
2873
|
+
this.#emitReconnectionEvent({
|
|
2874
|
+
type: "reconnecting",
|
|
2875
|
+
attempt: state.attempt,
|
|
2876
|
+
delay: state.nextDelayMs,
|
|
2877
|
+
error: state.lastError
|
|
2878
|
+
});
|
|
2879
|
+
}
|
|
2880
|
+
}
|
|
2881
|
+
);
|
|
2882
|
+
this.#isReconnecting = false;
|
|
2883
|
+
this.#emitReconnectionEvent({ type: "reconnected" });
|
|
2884
|
+
if (options.restoreSubscriptions !== false) {
|
|
2885
|
+
await this.#restoreSubscriptions();
|
|
2886
|
+
}
|
|
2887
|
+
}
|
|
2888
|
+
/**
|
|
2889
|
+
* Restore subscriptions after reconnection
|
|
2890
|
+
*/
|
|
2891
|
+
async #restoreSubscriptions() {
|
|
2892
|
+
const options = this.#options.reconnection;
|
|
2893
|
+
const subscriptionEntries = Array.from(this.#subscriptionStates.entries());
|
|
2894
|
+
this.#subscriptions.clear();
|
|
2895
|
+
this.#subscriptionStates.clear();
|
|
2896
|
+
for (const [oldId, state] of subscriptionEntries) {
|
|
2897
|
+
try {
|
|
2898
|
+
const newSubscription = await this.subscribe(state.filter);
|
|
2899
|
+
const newId = newSubscription.id;
|
|
2900
|
+
if (options.replayOnRestore !== false && state.lastEventId) {
|
|
2901
|
+
const maxEvents = options.maxReplayEventsPerSubscription ?? 1e3;
|
|
2902
|
+
try {
|
|
2903
|
+
let replayedCount = 0;
|
|
2904
|
+
let afterEventId = state.lastEventId;
|
|
2905
|
+
let hasMore = true;
|
|
2906
|
+
while (hasMore && replayedCount < maxEvents) {
|
|
2907
|
+
const result = await this.replay({
|
|
2908
|
+
afterEventId,
|
|
2909
|
+
filter: state.filter,
|
|
2910
|
+
limit: Math.min(100, maxEvents - replayedCount)
|
|
2911
|
+
});
|
|
2912
|
+
for (const replayedEvent of result.events) {
|
|
2913
|
+
if (replayedCount >= maxEvents) break;
|
|
2914
|
+
newSubscription._pushEvent({
|
|
2915
|
+
subscriptionId: newId,
|
|
2916
|
+
sequenceNumber: replayedCount + 1,
|
|
2917
|
+
eventId: replayedEvent.eventId,
|
|
2918
|
+
timestamp: replayedEvent.timestamp,
|
|
2919
|
+
event: replayedEvent.event
|
|
2920
|
+
});
|
|
2921
|
+
replayedCount++;
|
|
2922
|
+
}
|
|
2923
|
+
hasMore = result.hasMore;
|
|
2924
|
+
afterEventId = result.events.at(-1)?.eventId;
|
|
2925
|
+
if (result.events.length === 0) {
|
|
2926
|
+
break;
|
|
2927
|
+
}
|
|
2928
|
+
}
|
|
2929
|
+
} catch (replayError) {
|
|
2930
|
+
console.warn("MAP: Failed to replay events for subscription:", oldId, replayError);
|
|
2931
|
+
}
|
|
2932
|
+
}
|
|
2933
|
+
this.#emitReconnectionEvent({
|
|
2934
|
+
type: "subscriptionRestored",
|
|
2935
|
+
subscriptionId: oldId,
|
|
2936
|
+
newSubscriptionId: newId
|
|
2937
|
+
});
|
|
2938
|
+
} catch (error) {
|
|
2939
|
+
this.#emitReconnectionEvent({
|
|
2940
|
+
type: "subscriptionRestoreFailed",
|
|
2941
|
+
subscriptionId: oldId,
|
|
2942
|
+
error: error instanceof Error ? error : new Error(String(error))
|
|
2943
|
+
});
|
|
2944
|
+
}
|
|
2945
|
+
}
|
|
2946
|
+
}
|
|
2947
|
+
};
|
|
2948
|
+
|
|
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
|
+
// src/testing/client.ts
|
|
3043
|
+
var TestClient = class _TestClient {
|
|
3044
|
+
#connection;
|
|
3045
|
+
#subscriptions = /* @__PURE__ */ new Map();
|
|
3046
|
+
constructor(connection) {
|
|
3047
|
+
this.#connection = connection;
|
|
3048
|
+
}
|
|
3049
|
+
/**
|
|
3050
|
+
* Create and connect a test client
|
|
3051
|
+
*/
|
|
3052
|
+
static async create(server, options = {}) {
|
|
3053
|
+
const [clientStream, serverStream] = createStreamPair();
|
|
3054
|
+
const connection = new ClientConnection(clientStream, options);
|
|
3055
|
+
server.acceptConnection(serverStream);
|
|
3056
|
+
const client = new _TestClient(connection);
|
|
3057
|
+
if (options.autoConnect !== false) {
|
|
3058
|
+
await connection.connect();
|
|
3059
|
+
}
|
|
3060
|
+
return client;
|
|
3061
|
+
}
|
|
3062
|
+
/**
|
|
3063
|
+
* Get the underlying connection
|
|
3064
|
+
*/
|
|
3065
|
+
get connection() {
|
|
3066
|
+
return this.#connection;
|
|
3067
|
+
}
|
|
3068
|
+
/**
|
|
3069
|
+
* Whether the client is connected
|
|
3070
|
+
*/
|
|
3071
|
+
get isConnected() {
|
|
3072
|
+
return this.#connection.isConnected;
|
|
3073
|
+
}
|
|
3074
|
+
/**
|
|
3075
|
+
* Current session ID
|
|
3076
|
+
*/
|
|
3077
|
+
get sessionId() {
|
|
3078
|
+
return this.#connection.sessionId;
|
|
3079
|
+
}
|
|
3080
|
+
/**
|
|
3081
|
+
* Connect to the server
|
|
3082
|
+
*/
|
|
3083
|
+
async connect() {
|
|
3084
|
+
await this.#connection.connect();
|
|
3085
|
+
}
|
|
3086
|
+
/**
|
|
3087
|
+
* Disconnect from the server
|
|
3088
|
+
*/
|
|
3089
|
+
async disconnect() {
|
|
3090
|
+
for (const sub of this.#subscriptions.values()) {
|
|
3091
|
+
await sub.unsubscribe();
|
|
3092
|
+
}
|
|
3093
|
+
this.#subscriptions.clear();
|
|
3094
|
+
await this.#connection.disconnect();
|
|
3095
|
+
}
|
|
3096
|
+
// ===========================================================================
|
|
3097
|
+
// Agent Queries
|
|
3098
|
+
// ===========================================================================
|
|
3099
|
+
/**
|
|
3100
|
+
* List all agents
|
|
3101
|
+
*/
|
|
3102
|
+
async listAgents(options) {
|
|
3103
|
+
const result = await this.#connection.listAgents(options);
|
|
3104
|
+
return result.agents;
|
|
3105
|
+
}
|
|
3106
|
+
/**
|
|
3107
|
+
* Get agent by ID with optional hierarchy expansion
|
|
3108
|
+
*/
|
|
3109
|
+
async getAgent(agentId, options) {
|
|
3110
|
+
return this.#connection.getAgent(agentId, options);
|
|
3111
|
+
}
|
|
3112
|
+
/**
|
|
3113
|
+
* Find agent by name
|
|
3114
|
+
*/
|
|
3115
|
+
async findAgentByName(name) {
|
|
3116
|
+
const agents = await this.listAgents();
|
|
3117
|
+
return agents.find((a) => a.name === name);
|
|
3118
|
+
}
|
|
3119
|
+
/**
|
|
3120
|
+
* Find agents by role
|
|
3121
|
+
*/
|
|
3122
|
+
async findAgentsByRole(role) {
|
|
3123
|
+
const result = await this.#connection.listAgents({ filter: { roles: [role] } });
|
|
3124
|
+
return result.agents;
|
|
3125
|
+
}
|
|
3126
|
+
// ===========================================================================
|
|
3127
|
+
// Scope Queries
|
|
3128
|
+
// ===========================================================================
|
|
3129
|
+
/**
|
|
3130
|
+
* List all scopes
|
|
3131
|
+
*/
|
|
3132
|
+
async listScopes(options) {
|
|
3133
|
+
const result = await this.#connection.listScopes(options);
|
|
3134
|
+
return result.scopes;
|
|
3135
|
+
}
|
|
3136
|
+
/**
|
|
3137
|
+
* Get scope by ID
|
|
3138
|
+
*/
|
|
3139
|
+
async getScope(scopeId) {
|
|
3140
|
+
return this.#connection.getScope(scopeId);
|
|
3141
|
+
}
|
|
3142
|
+
// ===========================================================================
|
|
3143
|
+
// Messaging
|
|
3144
|
+
// ===========================================================================
|
|
3145
|
+
/**
|
|
3146
|
+
* Send message to agent
|
|
3147
|
+
*/
|
|
3148
|
+
async sendTo(agentId, payload) {
|
|
3149
|
+
const result = await this.#connection.sendToAgent(agentId, payload);
|
|
3150
|
+
return result.messageId;
|
|
3151
|
+
}
|
|
3152
|
+
/**
|
|
3153
|
+
* Broadcast to all agents
|
|
3154
|
+
*/
|
|
3155
|
+
async broadcast(payload) {
|
|
3156
|
+
const result = await this.#connection.broadcast(payload);
|
|
3157
|
+
return result.messageId;
|
|
3158
|
+
}
|
|
3159
|
+
/**
|
|
3160
|
+
* Send to all agents with a specific role
|
|
3161
|
+
*/
|
|
3162
|
+
async sendToRole(role, payload) {
|
|
3163
|
+
const result = await this.#connection.sendToRole(role, payload);
|
|
3164
|
+
return result.messageId;
|
|
3165
|
+
}
|
|
3166
|
+
/**
|
|
3167
|
+
* Send to all agents in a scope
|
|
3168
|
+
*/
|
|
3169
|
+
async sendToScope(scopeId, payload) {
|
|
3170
|
+
const result = await this.#connection.sendToScope(scopeId, payload);
|
|
3171
|
+
return result.messageId;
|
|
3172
|
+
}
|
|
3173
|
+
// ===========================================================================
|
|
3174
|
+
// Subscriptions
|
|
3175
|
+
// ===========================================================================
|
|
3176
|
+
/**
|
|
3177
|
+
* Subscribe to events with automatic tracking
|
|
3178
|
+
*/
|
|
3179
|
+
async subscribe(filter) {
|
|
3180
|
+
const subscription = await this.#connection.subscribe(filter);
|
|
3181
|
+
this.#subscriptions.set(subscription.id, subscription);
|
|
3182
|
+
return subscription;
|
|
3183
|
+
}
|
|
3184
|
+
/**
|
|
3185
|
+
* Collect events matching filter for a duration
|
|
3186
|
+
*/
|
|
3187
|
+
async collectEvents(filter, durationMs) {
|
|
3188
|
+
const events = [];
|
|
3189
|
+
const subscription = await this.subscribe(filter);
|
|
3190
|
+
subscription.on("event", (event) => events.push(event));
|
|
3191
|
+
await new Promise((resolve) => setTimeout(resolve, durationMs));
|
|
3192
|
+
await subscription.unsubscribe();
|
|
3193
|
+
this.#subscriptions.delete(subscription.id);
|
|
3194
|
+
return events;
|
|
3195
|
+
}
|
|
3196
|
+
/**
|
|
3197
|
+
* Wait for a specific event type
|
|
3198
|
+
*/
|
|
3199
|
+
async waitForEvent(eventType, timeoutMs = 5e3) {
|
|
3200
|
+
const subscription = await this.subscribe({ eventTypes: [eventType] });
|
|
3201
|
+
try {
|
|
3202
|
+
return await new Promise((resolve, reject) => {
|
|
3203
|
+
const timeout = setTimeout(() => {
|
|
3204
|
+
reject(new Error(`Timeout waiting for event: ${eventType}`));
|
|
3205
|
+
}, timeoutMs);
|
|
3206
|
+
subscription.on("event", (event) => {
|
|
3207
|
+
clearTimeout(timeout);
|
|
3208
|
+
resolve(event);
|
|
3209
|
+
});
|
|
3210
|
+
});
|
|
3211
|
+
} finally {
|
|
3212
|
+
await subscription.unsubscribe();
|
|
3213
|
+
this.#subscriptions.delete(subscription.id);
|
|
3214
|
+
}
|
|
3215
|
+
}
|
|
3216
|
+
// ===========================================================================
|
|
3217
|
+
// Control
|
|
3218
|
+
// ===========================================================================
|
|
3219
|
+
/**
|
|
3220
|
+
* Stop an agent
|
|
3221
|
+
*/
|
|
3222
|
+
async stopAgent(agentId, reason) {
|
|
3223
|
+
await this.#connection.stopAgent(agentId, { reason });
|
|
3224
|
+
}
|
|
3225
|
+
/**
|
|
3226
|
+
* Inject context into an agent
|
|
3227
|
+
*/
|
|
3228
|
+
async inject(agentId, content, delivery) {
|
|
3229
|
+
await this.#connection.inject(agentId, content, delivery);
|
|
3230
|
+
}
|
|
3231
|
+
};
|
|
3232
|
+
|
|
3233
|
+
// src/connection/agent.ts
|
|
3234
|
+
var AgentConnection = class {
|
|
3235
|
+
#connection;
|
|
3236
|
+
#subscriptions = /* @__PURE__ */ new Map();
|
|
3237
|
+
#options;
|
|
3238
|
+
#messageHandlers = /* @__PURE__ */ new Set();
|
|
3239
|
+
#reconnectionHandlers = /* @__PURE__ */ new Set();
|
|
3240
|
+
#scopeMemberships = /* @__PURE__ */ new Set();
|
|
3241
|
+
#agentId = null;
|
|
3242
|
+
#sessionId = null;
|
|
3243
|
+
#serverCapabilities = null;
|
|
3244
|
+
#currentState = "registered";
|
|
3245
|
+
#connected = false;
|
|
3246
|
+
#lastConnectOptions;
|
|
3247
|
+
#isReconnecting = false;
|
|
3248
|
+
constructor(stream, options = {}) {
|
|
3249
|
+
this.#connection = new BaseConnection(stream, options);
|
|
3250
|
+
this.#options = options;
|
|
3251
|
+
this.#connection.setNotificationHandler(this.#handleNotification.bind(this));
|
|
3252
|
+
if (options.reconnection?.enabled && options.createStream) {
|
|
3253
|
+
this.#connection.onStateChange((newState) => {
|
|
3254
|
+
if (newState === "closed" && this.#connected && !this.#isReconnecting) {
|
|
3255
|
+
void this.#handleDisconnect();
|
|
3256
|
+
}
|
|
3257
|
+
});
|
|
3258
|
+
}
|
|
3259
|
+
}
|
|
3260
|
+
// ===========================================================================
|
|
3261
|
+
// Connection Lifecycle
|
|
3262
|
+
// ===========================================================================
|
|
3263
|
+
/**
|
|
3264
|
+
* Connect and register with the MAP system
|
|
3265
|
+
*/
|
|
3266
|
+
async connect(options) {
|
|
3267
|
+
const connectParams = {
|
|
3268
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
3269
|
+
participantType: "agent",
|
|
3270
|
+
participantId: options?.agentId,
|
|
3271
|
+
name: this.#options.name,
|
|
3272
|
+
capabilities: this.#options.capabilities,
|
|
3273
|
+
auth: options?.auth
|
|
3274
|
+
};
|
|
3275
|
+
const connectResult = await this.#connection.sendRequest(CORE_METHODS.CONNECT, connectParams);
|
|
3276
|
+
this.#sessionId = connectResult.sessionId;
|
|
3277
|
+
this.#serverCapabilities = connectResult.capabilities;
|
|
3278
|
+
this.#connected = true;
|
|
3279
|
+
this.#lastConnectOptions = options;
|
|
3280
|
+
const registerParams = {
|
|
3281
|
+
agentId: options?.agentId,
|
|
3282
|
+
name: this.#options.name,
|
|
3283
|
+
role: this.#options.role,
|
|
3284
|
+
parent: this.#options.parent,
|
|
3285
|
+
scopes: this.#options.scopes,
|
|
3286
|
+
visibility: this.#options.visibility,
|
|
3287
|
+
capabilities: this.#options.capabilities
|
|
3288
|
+
};
|
|
3289
|
+
const registerResult = await this.#connection.sendRequest(LIFECYCLE_METHODS.AGENTS_REGISTER, registerParams);
|
|
3290
|
+
this.#agentId = registerResult.agent.id;
|
|
3291
|
+
this.#currentState = registerResult.agent.state;
|
|
3292
|
+
this.#connection._transitionTo("connected");
|
|
3293
|
+
return { connection: connectResult, agent: registerResult.agent };
|
|
3294
|
+
}
|
|
3295
|
+
/**
|
|
3296
|
+
* Disconnect from the MAP system
|
|
3297
|
+
*/
|
|
3298
|
+
async disconnect(reason) {
|
|
3299
|
+
if (!this.#connected) return;
|
|
3300
|
+
try {
|
|
3301
|
+
if (this.#agentId) {
|
|
3302
|
+
await this.#connection.sendRequest(LIFECYCLE_METHODS.AGENTS_UNREGISTER, {
|
|
3303
|
+
agentId: this.#agentId,
|
|
3304
|
+
reason
|
|
3305
|
+
});
|
|
3306
|
+
}
|
|
3307
|
+
await this.#connection.sendRequest(
|
|
3308
|
+
CORE_METHODS.DISCONNECT,
|
|
3309
|
+
reason ? { reason } : void 0
|
|
3310
|
+
);
|
|
3311
|
+
} finally {
|
|
3312
|
+
for (const subscription of this.#subscriptions.values()) {
|
|
3313
|
+
subscription._close();
|
|
3314
|
+
}
|
|
3315
|
+
this.#subscriptions.clear();
|
|
3316
|
+
await this.#connection.close();
|
|
3317
|
+
this.#connected = false;
|
|
3318
|
+
}
|
|
3319
|
+
}
|
|
3320
|
+
/**
|
|
3321
|
+
* Whether the agent is connected
|
|
3322
|
+
*/
|
|
3323
|
+
get isConnected() {
|
|
3324
|
+
return this.#connected && !this.#connection.isClosed;
|
|
3325
|
+
}
|
|
3326
|
+
/**
|
|
3327
|
+
* This agent's ID
|
|
3328
|
+
*/
|
|
3329
|
+
get agentId() {
|
|
3330
|
+
return this.#agentId;
|
|
3331
|
+
}
|
|
3332
|
+
/**
|
|
3333
|
+
* Current session ID
|
|
3334
|
+
*/
|
|
3335
|
+
get sessionId() {
|
|
3336
|
+
return this.#sessionId;
|
|
3337
|
+
}
|
|
3338
|
+
/**
|
|
3339
|
+
* Server capabilities
|
|
3340
|
+
*/
|
|
3341
|
+
get serverCapabilities() {
|
|
3342
|
+
return this.#serverCapabilities;
|
|
3343
|
+
}
|
|
3344
|
+
/**
|
|
3345
|
+
* Current agent state
|
|
3346
|
+
*/
|
|
3347
|
+
get state() {
|
|
3348
|
+
return this.#currentState;
|
|
3349
|
+
}
|
|
3350
|
+
/**
|
|
3351
|
+
* AbortSignal that triggers when the connection closes
|
|
3352
|
+
*/
|
|
3353
|
+
get signal() {
|
|
3354
|
+
return this.#connection.signal;
|
|
3355
|
+
}
|
|
3356
|
+
/**
|
|
3357
|
+
* Promise that resolves when the connection closes
|
|
3358
|
+
*/
|
|
3359
|
+
get closed() {
|
|
3360
|
+
return this.#connection.closed;
|
|
3361
|
+
}
|
|
3362
|
+
// ===========================================================================
|
|
3363
|
+
// Message Handling
|
|
3364
|
+
// ===========================================================================
|
|
3365
|
+
/**
|
|
3366
|
+
* Register a handler for incoming messages
|
|
3367
|
+
*/
|
|
3368
|
+
onMessage(handler) {
|
|
3369
|
+
this.#messageHandlers.add(handler);
|
|
3370
|
+
return this;
|
|
3371
|
+
}
|
|
3372
|
+
/**
|
|
3373
|
+
* Remove a message handler
|
|
3374
|
+
*/
|
|
3375
|
+
offMessage(handler) {
|
|
3376
|
+
this.#messageHandlers.delete(handler);
|
|
3377
|
+
return this;
|
|
3378
|
+
}
|
|
3379
|
+
// ===========================================================================
|
|
3380
|
+
// State Management
|
|
3381
|
+
// ===========================================================================
|
|
3382
|
+
/**
|
|
3383
|
+
* Update this agent's state
|
|
3384
|
+
*/
|
|
3385
|
+
async updateState(state) {
|
|
3386
|
+
if (!this.#agentId) {
|
|
3387
|
+
throw new Error("Agent not registered");
|
|
3388
|
+
}
|
|
3389
|
+
const result = await this.#connection.sendRequest(STATE_METHODS.AGENTS_UPDATE, {
|
|
3390
|
+
agentId: this.#agentId,
|
|
3391
|
+
state
|
|
3392
|
+
});
|
|
3393
|
+
this.#currentState = result.agent.state;
|
|
3394
|
+
return result.agent;
|
|
3395
|
+
}
|
|
3396
|
+
/**
|
|
3397
|
+
* Update this agent's metadata
|
|
3398
|
+
*/
|
|
3399
|
+
async updateMetadata(metadata) {
|
|
3400
|
+
if (!this.#agentId) {
|
|
3401
|
+
throw new Error("Agent not registered");
|
|
3402
|
+
}
|
|
3403
|
+
const result = await this.#connection.sendRequest(STATE_METHODS.AGENTS_UPDATE, {
|
|
3404
|
+
agentId: this.#agentId,
|
|
3405
|
+
metadata
|
|
3406
|
+
});
|
|
3407
|
+
return result.agent;
|
|
3408
|
+
}
|
|
3409
|
+
/**
|
|
3410
|
+
* Mark this agent as busy
|
|
3411
|
+
*/
|
|
3412
|
+
async busy() {
|
|
3413
|
+
return this.updateState("busy");
|
|
3414
|
+
}
|
|
3415
|
+
/**
|
|
3416
|
+
* Mark this agent as idle
|
|
3417
|
+
*/
|
|
3418
|
+
async idle() {
|
|
3419
|
+
return this.updateState("idle");
|
|
3420
|
+
}
|
|
3421
|
+
/**
|
|
3422
|
+
* Mark this agent as done/stopped
|
|
3423
|
+
*/
|
|
3424
|
+
async done(result) {
|
|
3425
|
+
if (!this.#agentId) {
|
|
3426
|
+
throw new Error("Agent not registered");
|
|
3427
|
+
}
|
|
3428
|
+
await this.updateState("stopped");
|
|
3429
|
+
if (result) {
|
|
3430
|
+
await this.updateMetadata({
|
|
3431
|
+
exitCode: result.exitCode,
|
|
3432
|
+
exitReason: result.exitReason
|
|
3433
|
+
});
|
|
3434
|
+
}
|
|
3435
|
+
}
|
|
3436
|
+
// ===========================================================================
|
|
3437
|
+
// Child Agent Management
|
|
3438
|
+
// ===========================================================================
|
|
3439
|
+
/**
|
|
3440
|
+
* Spawn a child agent
|
|
3441
|
+
*/
|
|
3442
|
+
async spawn(options) {
|
|
3443
|
+
if (!this.#agentId) {
|
|
3444
|
+
throw new Error("Agent not registered");
|
|
3445
|
+
}
|
|
3446
|
+
const params = {
|
|
3447
|
+
...options,
|
|
3448
|
+
parent: this.#agentId
|
|
3449
|
+
};
|
|
3450
|
+
return this.#connection.sendRequest(LIFECYCLE_METHODS.AGENTS_SPAWN, params);
|
|
3451
|
+
}
|
|
3452
|
+
// ===========================================================================
|
|
3453
|
+
// Messaging
|
|
3454
|
+
// ===========================================================================
|
|
3455
|
+
/**
|
|
3456
|
+
* Send a message to an address
|
|
3457
|
+
*/
|
|
3458
|
+
async send(to, payload, meta) {
|
|
3459
|
+
const params = { to };
|
|
3460
|
+
if (payload !== void 0) params.payload = payload;
|
|
3461
|
+
if (meta) params.meta = meta;
|
|
3462
|
+
return this.#connection.sendRequest(CORE_METHODS.SEND, params);
|
|
3463
|
+
}
|
|
3464
|
+
/**
|
|
3465
|
+
* Send a message to the parent agent
|
|
3466
|
+
*/
|
|
3467
|
+
async sendToParent(payload, meta) {
|
|
3468
|
+
return this.send({ parent: true }, payload, {
|
|
3469
|
+
...meta,
|
|
3470
|
+
relationship: "child-to-parent"
|
|
3471
|
+
});
|
|
3472
|
+
}
|
|
3473
|
+
/**
|
|
3474
|
+
* Send a message to child agents
|
|
3475
|
+
*/
|
|
3476
|
+
async sendToChildren(payload, meta) {
|
|
3477
|
+
return this.send({ children: true }, payload, {
|
|
3478
|
+
...meta,
|
|
3479
|
+
relationship: "parent-to-child"
|
|
3480
|
+
});
|
|
3481
|
+
}
|
|
3482
|
+
/**
|
|
3483
|
+
* Send a message to a specific agent
|
|
3484
|
+
*/
|
|
3485
|
+
async sendToAgent(agentId, payload, meta) {
|
|
3486
|
+
return this.send({ agent: agentId }, payload, meta);
|
|
3487
|
+
}
|
|
3488
|
+
/**
|
|
3489
|
+
* Send a message to all agents in a scope
|
|
3490
|
+
*/
|
|
3491
|
+
async sendToScope(scopeId, payload, meta) {
|
|
3492
|
+
return this.send({ scope: scopeId }, payload, meta);
|
|
3493
|
+
}
|
|
3494
|
+
/**
|
|
3495
|
+
* Send a message to sibling agents
|
|
3496
|
+
*/
|
|
3497
|
+
async sendToSiblings(payload, meta) {
|
|
3498
|
+
return this.send({ siblings: true }, payload, {
|
|
3499
|
+
...meta,
|
|
3500
|
+
relationship: "peer"
|
|
3501
|
+
});
|
|
3502
|
+
}
|
|
3503
|
+
/**
|
|
3504
|
+
* Reply to a message (uses correlationId from original)
|
|
3505
|
+
*/
|
|
3506
|
+
async reply(originalMessage, payload, meta) {
|
|
3507
|
+
return this.send({ agent: originalMessage.from }, payload, {
|
|
3508
|
+
...meta,
|
|
3509
|
+
correlationId: originalMessage.meta?.correlationId ?? originalMessage.id,
|
|
3510
|
+
isResult: true
|
|
3511
|
+
});
|
|
3512
|
+
}
|
|
3513
|
+
// ===========================================================================
|
|
3514
|
+
// Scope Management
|
|
3515
|
+
// ===========================================================================
|
|
3516
|
+
/**
|
|
3517
|
+
* Create a new scope
|
|
3518
|
+
*/
|
|
3519
|
+
async createScope(options) {
|
|
3520
|
+
const result = await this.#connection.sendRequest(SCOPE_METHODS.SCOPES_CREATE, options);
|
|
3521
|
+
return result.scope;
|
|
3522
|
+
}
|
|
3523
|
+
/**
|
|
3524
|
+
* Join a scope
|
|
3525
|
+
*/
|
|
3526
|
+
async joinScope(scopeId) {
|
|
3527
|
+
if (!this.#agentId) {
|
|
3528
|
+
throw new Error("Agent not registered");
|
|
3529
|
+
}
|
|
3530
|
+
const result = await this.#connection.sendRequest(SCOPE_METHODS.SCOPES_JOIN, {
|
|
3531
|
+
scopeId,
|
|
3532
|
+
agentId: this.#agentId
|
|
3533
|
+
});
|
|
3534
|
+
this.#scopeMemberships.add(scopeId);
|
|
3535
|
+
return result;
|
|
3536
|
+
}
|
|
3537
|
+
/**
|
|
3538
|
+
* Leave a scope
|
|
3539
|
+
*/
|
|
3540
|
+
async leaveScope(scopeId) {
|
|
3541
|
+
if (!this.#agentId) {
|
|
3542
|
+
throw new Error("Agent not registered");
|
|
3543
|
+
}
|
|
3544
|
+
const result = await this.#connection.sendRequest(SCOPE_METHODS.SCOPES_LEAVE, {
|
|
3545
|
+
scopeId,
|
|
3546
|
+
agentId: this.#agentId
|
|
3547
|
+
});
|
|
3548
|
+
this.#scopeMemberships.delete(scopeId);
|
|
3549
|
+
return result;
|
|
3550
|
+
}
|
|
3551
|
+
// ===========================================================================
|
|
3552
|
+
// Subscriptions
|
|
3553
|
+
// ===========================================================================
|
|
3554
|
+
/**
|
|
3555
|
+
* Subscribe to events
|
|
3556
|
+
*/
|
|
3557
|
+
async subscribe(filter) {
|
|
3558
|
+
const params = {};
|
|
3559
|
+
if (filter) params.filter = filter;
|
|
3560
|
+
const result = await this.#connection.sendRequest(CORE_METHODS.SUBSCRIBE, params);
|
|
3561
|
+
const subscription = createSubscription(
|
|
3562
|
+
result.subscriptionId,
|
|
3563
|
+
() => this.unsubscribe(result.subscriptionId),
|
|
3564
|
+
{ filter }
|
|
3565
|
+
);
|
|
3566
|
+
this.#subscriptions.set(result.subscriptionId, subscription);
|
|
3567
|
+
return subscription;
|
|
3568
|
+
}
|
|
3569
|
+
/**
|
|
3570
|
+
* Unsubscribe from events
|
|
3571
|
+
*/
|
|
3572
|
+
async unsubscribe(subscriptionId) {
|
|
3573
|
+
const subscription = this.#subscriptions.get(subscriptionId);
|
|
3574
|
+
if (subscription) {
|
|
3575
|
+
subscription._close();
|
|
3576
|
+
this.#subscriptions.delete(subscriptionId);
|
|
3577
|
+
}
|
|
3578
|
+
await this.#connection.sendRequest(CORE_METHODS.UNSUBSCRIBE, { subscriptionId });
|
|
3579
|
+
}
|
|
3580
|
+
// ===========================================================================
|
|
3581
|
+
// Reconnection
|
|
3582
|
+
// ===========================================================================
|
|
3583
|
+
/**
|
|
3584
|
+
* Current connection state
|
|
3585
|
+
*/
|
|
3586
|
+
get connectionState() {
|
|
3587
|
+
return this.#connection.state;
|
|
3588
|
+
}
|
|
3589
|
+
/**
|
|
3590
|
+
* Whether the connection is currently reconnecting
|
|
3591
|
+
*/
|
|
3592
|
+
get isReconnecting() {
|
|
3593
|
+
return this.#isReconnecting;
|
|
3594
|
+
}
|
|
3595
|
+
/**
|
|
3596
|
+
* Register a handler for reconnection events.
|
|
3597
|
+
*
|
|
3598
|
+
* @param handler - Function called when reconnection events occur
|
|
3599
|
+
* @returns Unsubscribe function to remove the handler
|
|
3600
|
+
*/
|
|
3601
|
+
onReconnection(handler) {
|
|
3602
|
+
this.#reconnectionHandlers.add(handler);
|
|
3603
|
+
return () => this.#reconnectionHandlers.delete(handler);
|
|
3604
|
+
}
|
|
3605
|
+
/**
|
|
3606
|
+
* Register a handler for connection state changes.
|
|
3607
|
+
*
|
|
3608
|
+
* @param handler - Function called when state changes
|
|
3609
|
+
* @returns Unsubscribe function to remove the handler
|
|
3610
|
+
*/
|
|
3611
|
+
onStateChange(handler) {
|
|
3612
|
+
return this.#connection.onStateChange(handler);
|
|
3613
|
+
}
|
|
3614
|
+
// ===========================================================================
|
|
3615
|
+
// Internal
|
|
3616
|
+
// ===========================================================================
|
|
3617
|
+
/**
|
|
3618
|
+
* Handle incoming notifications
|
|
3619
|
+
*/
|
|
3620
|
+
async #handleNotification(method, params) {
|
|
3621
|
+
switch (method) {
|
|
3622
|
+
case NOTIFICATION_METHODS.EVENT: {
|
|
3623
|
+
const eventParams = params;
|
|
3624
|
+
const subscription = this.#subscriptions.get(eventParams.subscriptionId);
|
|
3625
|
+
if (subscription) {
|
|
3626
|
+
subscription._pushEvent(eventParams);
|
|
3627
|
+
}
|
|
3628
|
+
break;
|
|
3629
|
+
}
|
|
3630
|
+
case NOTIFICATION_METHODS.MESSAGE: {
|
|
3631
|
+
const messageParams = params;
|
|
3632
|
+
for (const handler of this.#messageHandlers) {
|
|
3633
|
+
try {
|
|
3634
|
+
await handler(messageParams.message);
|
|
3635
|
+
} catch (error) {
|
|
3636
|
+
console.error("MAP: Message handler error:", error);
|
|
3637
|
+
}
|
|
3638
|
+
}
|
|
3639
|
+
break;
|
|
3640
|
+
}
|
|
3641
|
+
default:
|
|
3642
|
+
console.warn("MAP: Unknown notification:", method);
|
|
3643
|
+
}
|
|
3644
|
+
}
|
|
3645
|
+
/**
|
|
3646
|
+
* Emit a reconnection event to all registered handlers
|
|
3647
|
+
*/
|
|
3648
|
+
#emitReconnectionEvent(event) {
|
|
3649
|
+
for (const handler of this.#reconnectionHandlers) {
|
|
3650
|
+
try {
|
|
3651
|
+
handler(event);
|
|
3652
|
+
} catch (error) {
|
|
3653
|
+
console.error("MAP: Reconnection event handler error:", error);
|
|
3654
|
+
}
|
|
3655
|
+
}
|
|
3656
|
+
}
|
|
3657
|
+
/**
|
|
3658
|
+
* Handle disconnect when auto-reconnect is enabled
|
|
3659
|
+
*/
|
|
3660
|
+
async #handleDisconnect() {
|
|
3661
|
+
this.#isReconnecting = true;
|
|
3662
|
+
this.#connected = false;
|
|
3663
|
+
this.#emitReconnectionEvent({ type: "disconnected" });
|
|
3664
|
+
try {
|
|
3665
|
+
await this.#attemptReconnect();
|
|
3666
|
+
} catch (error) {
|
|
3667
|
+
this.#isReconnecting = false;
|
|
3668
|
+
this.#emitReconnectionEvent({
|
|
3669
|
+
type: "reconnectFailed",
|
|
3670
|
+
error: error instanceof Error ? error : new Error(String(error))
|
|
3671
|
+
});
|
|
3672
|
+
}
|
|
3673
|
+
}
|
|
3674
|
+
/**
|
|
3675
|
+
* Attempt to reconnect with retry logic
|
|
3676
|
+
*/
|
|
3677
|
+
async #attemptReconnect() {
|
|
3678
|
+
const options = this.#options.reconnection;
|
|
3679
|
+
const createStream = this.#options.createStream;
|
|
3680
|
+
const retryPolicy = {
|
|
3681
|
+
maxRetries: options.maxRetries ?? DEFAULT_RETRY_POLICY.maxRetries,
|
|
3682
|
+
baseDelayMs: options.baseDelayMs ?? DEFAULT_RETRY_POLICY.baseDelayMs,
|
|
3683
|
+
maxDelayMs: options.maxDelayMs ?? DEFAULT_RETRY_POLICY.maxDelayMs,
|
|
3684
|
+
jitter: options.jitter ?? DEFAULT_RETRY_POLICY.jitter
|
|
3685
|
+
};
|
|
3686
|
+
const scopesToRestore = Array.from(this.#scopeMemberships);
|
|
3687
|
+
await withRetry(
|
|
3688
|
+
async () => {
|
|
3689
|
+
const newStream = await createStream();
|
|
3690
|
+
await this.#connection.reconnect(newStream);
|
|
3691
|
+
const result = await this.connect({
|
|
3692
|
+
agentId: this.#agentId ?? this.#lastConnectOptions?.agentId,
|
|
3693
|
+
auth: this.#lastConnectOptions?.auth
|
|
3694
|
+
});
|
|
3695
|
+
this.#agentId = result.agent.id;
|
|
3696
|
+
this.#sessionId = result.connection.sessionId;
|
|
3697
|
+
this.#serverCapabilities = result.connection.capabilities;
|
|
3698
|
+
this.#currentState = result.agent.state;
|
|
3699
|
+
},
|
|
3700
|
+
retryPolicy,
|
|
3701
|
+
{
|
|
3702
|
+
onRetry: (state) => {
|
|
3703
|
+
this.#emitReconnectionEvent({
|
|
3704
|
+
type: "reconnecting",
|
|
3705
|
+
attempt: state.attempt,
|
|
3706
|
+
delay: state.nextDelayMs,
|
|
3707
|
+
error: state.lastError
|
|
3708
|
+
});
|
|
3709
|
+
}
|
|
3710
|
+
}
|
|
3711
|
+
);
|
|
3712
|
+
this.#isReconnecting = false;
|
|
3713
|
+
this.#emitReconnectionEvent({ type: "reconnected" });
|
|
3714
|
+
if (options.restoreScopeMemberships !== false) {
|
|
3715
|
+
await this.#restoreScopeMemberships(scopesToRestore);
|
|
3716
|
+
}
|
|
3717
|
+
}
|
|
3718
|
+
/**
|
|
3719
|
+
* Restore scope memberships after reconnection
|
|
3720
|
+
*/
|
|
3721
|
+
async #restoreScopeMemberships(scopes) {
|
|
3722
|
+
this.#scopeMemberships.clear();
|
|
3723
|
+
for (const scopeId of scopes) {
|
|
3724
|
+
try {
|
|
3725
|
+
await this.joinScope(scopeId);
|
|
3726
|
+
} catch (error) {
|
|
3727
|
+
console.warn("MAP: Failed to restore scope membership:", scopeId, error);
|
|
3728
|
+
}
|
|
3729
|
+
}
|
|
3730
|
+
}
|
|
3731
|
+
};
|
|
3732
|
+
|
|
3733
|
+
// src/testing/agent.ts
|
|
3734
|
+
var TestAgent = class _TestAgent {
|
|
3735
|
+
#connection;
|
|
3736
|
+
#receivedMessages = [];
|
|
3737
|
+
#subscriptions = /* @__PURE__ */ new Map();
|
|
3738
|
+
constructor(connection) {
|
|
3739
|
+
this.#connection = connection;
|
|
3740
|
+
connection.onMessage((msg) => {
|
|
3741
|
+
this.#receivedMessages.push(msg);
|
|
3742
|
+
});
|
|
3743
|
+
}
|
|
3744
|
+
/**
|
|
3745
|
+
* Create and connect a test agent
|
|
3746
|
+
*/
|
|
3747
|
+
static async create(server, options = {}) {
|
|
3748
|
+
const [clientStream, serverStream] = createStreamPair();
|
|
3749
|
+
const connection = new AgentConnection(clientStream, options);
|
|
3750
|
+
server.acceptConnection(serverStream);
|
|
3751
|
+
const agent = new _TestAgent(connection);
|
|
3752
|
+
if (options.autoConnect !== false) {
|
|
3753
|
+
await connection.connect({ agentId: options.agentId });
|
|
3754
|
+
}
|
|
3755
|
+
return agent;
|
|
3756
|
+
}
|
|
3757
|
+
/**
|
|
3758
|
+
* Get the underlying connection
|
|
3759
|
+
*/
|
|
3760
|
+
get connection() {
|
|
3761
|
+
return this.#connection;
|
|
3762
|
+
}
|
|
3763
|
+
/**
|
|
3764
|
+
* Whether the agent is connected
|
|
3765
|
+
*/
|
|
3766
|
+
get isConnected() {
|
|
3767
|
+
return this.#connection.isConnected;
|
|
3768
|
+
}
|
|
3769
|
+
/**
|
|
3770
|
+
* Agent ID
|
|
3771
|
+
*/
|
|
3772
|
+
get id() {
|
|
3773
|
+
return this.#connection.agentId;
|
|
3774
|
+
}
|
|
3775
|
+
/**
|
|
3776
|
+
* Current state
|
|
3777
|
+
*/
|
|
3778
|
+
get state() {
|
|
3779
|
+
return this.#connection.state;
|
|
3780
|
+
}
|
|
3781
|
+
/**
|
|
3782
|
+
* Session ID
|
|
3783
|
+
*/
|
|
3784
|
+
get sessionId() {
|
|
3785
|
+
return this.#connection.sessionId;
|
|
3786
|
+
}
|
|
3787
|
+
/**
|
|
3788
|
+
* All messages received by this agent
|
|
3789
|
+
*/
|
|
3790
|
+
get receivedMessages() {
|
|
3791
|
+
return this.#receivedMessages;
|
|
3792
|
+
}
|
|
3793
|
+
/**
|
|
3794
|
+
* Get the last received message
|
|
3795
|
+
*/
|
|
3796
|
+
get lastMessage() {
|
|
3797
|
+
return this.#receivedMessages[this.#receivedMessages.length - 1];
|
|
3798
|
+
}
|
|
3799
|
+
/**
|
|
3800
|
+
* Clear received messages
|
|
3801
|
+
*/
|
|
3802
|
+
clearMessages() {
|
|
3803
|
+
this.#receivedMessages.length = 0;
|
|
3804
|
+
}
|
|
3805
|
+
/**
|
|
3806
|
+
* Connect to the server
|
|
3807
|
+
*/
|
|
3808
|
+
async connect(agentId) {
|
|
3809
|
+
const result = await this.#connection.connect({ agentId });
|
|
3810
|
+
return result.agent;
|
|
3811
|
+
}
|
|
3812
|
+
/**
|
|
3813
|
+
* Disconnect from the server
|
|
3814
|
+
*/
|
|
3815
|
+
async disconnect(reason) {
|
|
3816
|
+
for (const sub of this.#subscriptions.values()) {
|
|
3817
|
+
await sub.unsubscribe();
|
|
3818
|
+
}
|
|
3819
|
+
this.#subscriptions.clear();
|
|
3820
|
+
await this.#connection.disconnect(reason);
|
|
3821
|
+
}
|
|
3822
|
+
// ===========================================================================
|
|
3823
|
+
// State Management
|
|
3824
|
+
// ===========================================================================
|
|
3825
|
+
/**
|
|
3826
|
+
* Update state
|
|
3827
|
+
*/
|
|
3828
|
+
async setState(state) {
|
|
3829
|
+
return this.#connection.updateState(state);
|
|
3830
|
+
}
|
|
3831
|
+
/**
|
|
3832
|
+
* Mark as busy
|
|
3833
|
+
*/
|
|
3834
|
+
async busy() {
|
|
3835
|
+
return this.#connection.busy();
|
|
3836
|
+
}
|
|
3837
|
+
/**
|
|
3838
|
+
* Mark as idle
|
|
3839
|
+
*/
|
|
3840
|
+
async idle() {
|
|
3841
|
+
return this.#connection.idle();
|
|
3842
|
+
}
|
|
3843
|
+
/**
|
|
3844
|
+
* Mark as done
|
|
3845
|
+
*/
|
|
3846
|
+
async done(exitCode, exitReason) {
|
|
3847
|
+
await this.#connection.done({ exitCode, exitReason });
|
|
3848
|
+
}
|
|
3849
|
+
/**
|
|
3850
|
+
* Update metadata
|
|
3851
|
+
*/
|
|
3852
|
+
async setMetadata(metadata) {
|
|
3853
|
+
return this.#connection.updateMetadata(metadata);
|
|
3854
|
+
}
|
|
3855
|
+
// ===========================================================================
|
|
3856
|
+
// Child Agent Management
|
|
3857
|
+
// ===========================================================================
|
|
3858
|
+
/**
|
|
3859
|
+
* Spawn a child agent
|
|
3860
|
+
*/
|
|
3861
|
+
async spawn(options = {}) {
|
|
3862
|
+
const result = await this.#connection.spawn(options);
|
|
3863
|
+
return result.agent;
|
|
3864
|
+
}
|
|
3865
|
+
/**
|
|
3866
|
+
* Spawn and return a connected TestAgent for the child
|
|
3867
|
+
*/
|
|
3868
|
+
async spawnTestAgent(server, options = {}) {
|
|
3869
|
+
await this.#connection.spawn({
|
|
3870
|
+
name: options.name,
|
|
3871
|
+
role: options.role,
|
|
3872
|
+
agentId: options.agentId
|
|
3873
|
+
});
|
|
3874
|
+
const child = await _TestAgent.create(server, {
|
|
3875
|
+
...options,
|
|
3876
|
+
parent: this.id,
|
|
3877
|
+
autoConnect: false
|
|
3878
|
+
});
|
|
3879
|
+
return child;
|
|
3880
|
+
}
|
|
3881
|
+
// ===========================================================================
|
|
3882
|
+
// Scope Management
|
|
3883
|
+
// ===========================================================================
|
|
3884
|
+
/**
|
|
3885
|
+
* Create a scope
|
|
3886
|
+
*/
|
|
3887
|
+
async createScope(name, options) {
|
|
3888
|
+
return this.#connection.createScope({ name, ...options });
|
|
3889
|
+
}
|
|
3890
|
+
/**
|
|
3891
|
+
* Join a scope
|
|
3892
|
+
*/
|
|
3893
|
+
async joinScope(scopeId) {
|
|
3894
|
+
return this.#connection.joinScope(scopeId);
|
|
3895
|
+
}
|
|
3896
|
+
/**
|
|
3897
|
+
* Leave a scope
|
|
3898
|
+
*/
|
|
3899
|
+
async leaveScope(scopeId) {
|
|
3900
|
+
return this.#connection.leaveScope(scopeId);
|
|
3901
|
+
}
|
|
3902
|
+
// ===========================================================================
|
|
3903
|
+
// Messaging
|
|
3904
|
+
// ===========================================================================
|
|
3905
|
+
/**
|
|
3906
|
+
* Send to another agent
|
|
3907
|
+
*/
|
|
3908
|
+
async sendTo(agentId, payload) {
|
|
3909
|
+
const result = await this.#connection.sendToAgent(agentId, payload);
|
|
3910
|
+
return result.messageId;
|
|
3911
|
+
}
|
|
3912
|
+
/**
|
|
3913
|
+
* Send to parent
|
|
3914
|
+
*/
|
|
3915
|
+
async sendToParent(payload) {
|
|
3916
|
+
const result = await this.#connection.sendToParent(payload);
|
|
3917
|
+
return result.messageId;
|
|
3918
|
+
}
|
|
3919
|
+
/**
|
|
3920
|
+
* Send to children
|
|
3921
|
+
*/
|
|
3922
|
+
async sendToChildren(payload) {
|
|
3923
|
+
const result = await this.#connection.sendToChildren(payload);
|
|
3924
|
+
return result.messageId;
|
|
3925
|
+
}
|
|
3926
|
+
/**
|
|
3927
|
+
* Send to siblings
|
|
3928
|
+
*/
|
|
3929
|
+
async sendToSiblings(payload) {
|
|
3930
|
+
const result = await this.#connection.sendToSiblings(payload);
|
|
3931
|
+
return result.messageId;
|
|
3932
|
+
}
|
|
3933
|
+
/**
|
|
3934
|
+
* Send to scope
|
|
3935
|
+
*/
|
|
3936
|
+
async sendToScope(scopeId, payload) {
|
|
3937
|
+
const result = await this.#connection.sendToScope(scopeId, payload);
|
|
3938
|
+
return result.messageId;
|
|
3939
|
+
}
|
|
3940
|
+
/**
|
|
3941
|
+
* Reply to a message
|
|
3942
|
+
*/
|
|
3943
|
+
async reply(message, payload) {
|
|
3944
|
+
const result = await this.#connection.reply(message, payload);
|
|
3945
|
+
return result.messageId;
|
|
3946
|
+
}
|
|
3947
|
+
/**
|
|
3948
|
+
* Wait for a message
|
|
3949
|
+
*/
|
|
3950
|
+
async waitForMessage(timeoutMs = 5e3) {
|
|
3951
|
+
const startLen = this.#receivedMessages.length;
|
|
3952
|
+
return new Promise((resolve, reject) => {
|
|
3953
|
+
const timeout = setTimeout(() => {
|
|
3954
|
+
reject(new Error("Timeout waiting for message"));
|
|
3955
|
+
}, timeoutMs);
|
|
3956
|
+
const checkInterval = setInterval(() => {
|
|
3957
|
+
if (this.#receivedMessages.length > startLen) {
|
|
3958
|
+
clearTimeout(timeout);
|
|
3959
|
+
clearInterval(checkInterval);
|
|
3960
|
+
resolve(this.#receivedMessages[this.#receivedMessages.length - 1]);
|
|
3961
|
+
}
|
|
3962
|
+
}, 10);
|
|
3963
|
+
});
|
|
3964
|
+
}
|
|
3965
|
+
// ===========================================================================
|
|
3966
|
+
// Subscriptions
|
|
3967
|
+
// ===========================================================================
|
|
3968
|
+
/**
|
|
3969
|
+
* Subscribe to events
|
|
3970
|
+
*/
|
|
3971
|
+
async subscribe(filter) {
|
|
3972
|
+
const subscription = await this.#connection.subscribe(filter);
|
|
3973
|
+
this.#subscriptions.set(subscription.id, subscription);
|
|
3974
|
+
return subscription;
|
|
3975
|
+
}
|
|
3976
|
+
/**
|
|
3977
|
+
* Wait for a specific event
|
|
3978
|
+
*/
|
|
3979
|
+
async waitForEvent(eventType, timeoutMs = 5e3) {
|
|
3980
|
+
const subscription = await this.subscribe({ eventTypes: [eventType] });
|
|
3981
|
+
try {
|
|
3982
|
+
return await new Promise((resolve, reject) => {
|
|
3983
|
+
const timeout = setTimeout(() => {
|
|
3984
|
+
reject(new Error(`Timeout waiting for event: ${eventType}`));
|
|
3985
|
+
}, timeoutMs);
|
|
3986
|
+
subscription.on("event", (event) => {
|
|
3987
|
+
clearTimeout(timeout);
|
|
3988
|
+
resolve(event);
|
|
3989
|
+
});
|
|
3990
|
+
});
|
|
3991
|
+
} finally {
|
|
3992
|
+
await subscription.unsubscribe();
|
|
3993
|
+
this.#subscriptions.delete(subscription.id);
|
|
3994
|
+
}
|
|
3995
|
+
}
|
|
3996
|
+
};
|
|
3997
|
+
|
|
3998
|
+
export { TestAgent, TestClient, TestServer };
|
|
3999
|
+
//# sourceMappingURL=testing.js.map
|
|
4000
|
+
//# sourceMappingURL=testing.js.map
|