@nolag/agents 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs ADDED
@@ -0,0 +1,1121 @@
1
+ 'use strict';
2
+
3
+ var jsSdk = require('@nolag/js-sdk');
4
+
5
+ /**
6
+ * Tiny typed event emitter — framework-agnostic base for NoLagAgents and AgentRoom.
7
+ *
8
+ * EventMap is a record of event name -> tuple of handler arguments.
9
+ * e.g. { task: [TaskEnvelope]; result: [ResultEnvelope] }
10
+ */
11
+ class EventEmitter {
12
+ constructor() {
13
+ this._handlers = new Map();
14
+ }
15
+ on(event, handler) {
16
+ if (!this._handlers.has(event)) {
17
+ this._handlers.set(event, new Set());
18
+ }
19
+ this._handlers.get(event).add(handler);
20
+ return this;
21
+ }
22
+ off(event, handler) {
23
+ if (handler) {
24
+ this._handlers.get(event)?.delete(handler);
25
+ }
26
+ else {
27
+ this._handlers.delete(event);
28
+ }
29
+ return this;
30
+ }
31
+ removeAllListeners() {
32
+ this._handlers.clear();
33
+ return this;
34
+ }
35
+ emit(event, ...args) {
36
+ const handlers = this._handlers.get(event);
37
+ if (!handlers)
38
+ return;
39
+ for (const handler of handlers) {
40
+ try {
41
+ handler(...args);
42
+ }
43
+ catch (e) {
44
+ console.error(`Error in ${String(event)} handler:`, e);
45
+ }
46
+ }
47
+ }
48
+ listenerCount(event) {
49
+ return this._handlers.get(event)?.size ?? 0;
50
+ }
51
+ }
52
+
53
+ /** Default app name for agent coordination */
54
+ const DEFAULT_APP_NAME = "agents";
55
+ /** Topic name for task dispatch (Handoff pattern) */
56
+ const TOPIC_TASKS = "tasks";
57
+ /** Topic name for task results */
58
+ const TOPIC_RESULTS = "results";
59
+ /** Topic name for shared state (Blackboard pattern) */
60
+ const TOPIC_STATE = "state";
61
+ /** Topic name for observability events (Observe pattern) */
62
+ const TOPIC_EVENTS = "events";
63
+ /** Topic name for per-agent inboxes (Inbox pattern) */
64
+ const TOPIC_INBOX = "inbox";
65
+ /** Topic name for tool invocations (Tools pattern) */
66
+ const TOPIC_TOOLS = "tools";
67
+ /** Topic name for human-in-the-loop approval (Approve pattern) */
68
+ const TOPIC_APPROVAL = "approval";
69
+ /** Default room for agent coordination */
70
+ const DEFAULT_ROOM = "default-workflow";
71
+
72
+ /**
73
+ * AgentRoom — wraps a RoomContext from @nolag/js-sdk.
74
+ *
75
+ * Provides typed pub/sub for agent coordination topics,
76
+ * presence-based service discovery, and capability routing.
77
+ *
78
+ * @example
79
+ * ```typescript
80
+ * const room = agents.room('default-workflow');
81
+ *
82
+ * // Service discovery - see who's connected
83
+ * const agents = room.getConnectedAgents();
84
+ * const summarizers = room.findAgents('summarize');
85
+ *
86
+ * // Capability-filtered task handler
87
+ * room.on('task', (envelope) => console.log('New task:', envelope));
88
+ * ```
89
+ */
90
+ class AgentRoom extends EventEmitter {
91
+ constructor(name, roomContext, client, log, agentId, presence) {
92
+ super();
93
+ /** Registry of connected agents discovered via presence */
94
+ this._agents = new Map();
95
+ this.name = name;
96
+ this.agentId = agentId;
97
+ this._roomContext = roomContext;
98
+ this._client = client;
99
+ this._log = log;
100
+ this._presence = presence;
101
+ this._wireTopicListeners();
102
+ this._wirePresenceListeners();
103
+ // Set presence if provided
104
+ if (presence) {
105
+ this._log(`setting presence in room ${name}:`, presence);
106
+ this._roomContext.setPresence(presence);
107
+ }
108
+ // Fetch initial presence snapshot
109
+ this._fetchInitialPresence();
110
+ }
111
+ // ============================================================
112
+ // SERVICE DISCOVERY
113
+ // ============================================================
114
+ /** Get all currently connected agents */
115
+ getConnectedAgents() {
116
+ return Array.from(this._agents.values());
117
+ }
118
+ /** Find agents that have a specific capability */
119
+ findAgents(capability) {
120
+ return Array.from(this._agents.values()).filter((a) => a.capabilities.includes(capability));
121
+ }
122
+ /** Check if any connected agent can handle a capability */
123
+ hasCapability(capability) {
124
+ return this.findAgents(capability).length > 0;
125
+ }
126
+ /** Get all capabilities available across connected agents */
127
+ getAvailableCapabilities() {
128
+ const caps = new Set();
129
+ for (const agent of this._agents.values()) {
130
+ for (const cap of agent.capabilities) {
131
+ caps.add(cap);
132
+ }
133
+ }
134
+ return Array.from(caps);
135
+ }
136
+ // ============================================================
137
+ // PRESENCE
138
+ // ============================================================
139
+ /** Update this agent's presence data */
140
+ setPresence(data) {
141
+ this._presence = data;
142
+ this._log(`updating presence in room ${this.name}`);
143
+ this._roomContext.setPresence(data);
144
+ }
145
+ /** Fetch current presence snapshot for this room */
146
+ async fetchPresence() {
147
+ try {
148
+ const actors = await this._roomContext.fetchPresence();
149
+ return (actors || []).map((a) => this._toConnectedAgent(a));
150
+ }
151
+ catch {
152
+ return [];
153
+ }
154
+ }
155
+ /**
156
+ * @internal Emit a presence event (used by NoLagAgents for lobby forwarding)
157
+ */
158
+ _emitPresence(event, actorId, data) {
159
+ if (event === 'presenceLeave') {
160
+ this.emit('presenceLeave', actorId);
161
+ }
162
+ else {
163
+ this.emit(event, actorId, data || {});
164
+ }
165
+ }
166
+ // ============================================================
167
+ // PUBLISH (with automatic agentId injection)
168
+ // ============================================================
169
+ /** Get the underlying RoomContext for advanced usage */
170
+ get context() {
171
+ return this._roomContext;
172
+ }
173
+ /** Publish to the tasks topic */
174
+ publishTask(envelope) {
175
+ // Auto-set createdBy if not set
176
+ if (!envelope.createdBy) {
177
+ envelope.createdBy = this.agentId;
178
+ }
179
+ this._publish(TOPIC_TASKS, envelope);
180
+ }
181
+ /** Publish to the results topic */
182
+ publishResult(envelope) {
183
+ // Auto-set completedBy if not set
184
+ if (!envelope.completedBy) {
185
+ envelope.completedBy = this.agentId;
186
+ }
187
+ this._publish(TOPIC_RESULTS, envelope);
188
+ }
189
+ /** Publish to the state topic (retained) */
190
+ publishState(data) {
191
+ // Auto-set updatedBy if not set
192
+ if (!data.updatedBy) {
193
+ data.updatedBy = this.agentId;
194
+ }
195
+ this._publish(TOPIC_STATE, data, { retain: true });
196
+ }
197
+ /** Publish to the events topic */
198
+ publishEvent(data) {
199
+ // Auto-set emittedBy if not set
200
+ if (!data.emittedBy) {
201
+ data.emittedBy = this.agentId;
202
+ }
203
+ this._publish(TOPIC_EVENTS, data);
204
+ }
205
+ /** Publish to the inbox topic */
206
+ publishInbox(data) {
207
+ this._publish(TOPIC_INBOX, data);
208
+ }
209
+ /** Publish to the tools topic */
210
+ publishTools(data) {
211
+ this._publish(TOPIC_TOOLS, data);
212
+ }
213
+ /** Publish to the approval topic (retained) */
214
+ publishApproval(data) {
215
+ this._publish(TOPIC_APPROVAL, data, { retain: true });
216
+ }
217
+ // ============================================================
218
+ // INTERNALS
219
+ // ============================================================
220
+ _publish(topic, data, options) {
221
+ this._log(`publish to ${topic} in room ${this.name}`);
222
+ if (options) {
223
+ this._roomContext.emit(topic, data, options);
224
+ }
225
+ else {
226
+ this._roomContext.emit(topic, data);
227
+ }
228
+ }
229
+ _toConnectedAgent(actor) {
230
+ const presence = actor.presence || actor.data || {};
231
+ return {
232
+ actorId: actor.actorTokenId || actor.actorId || '',
233
+ name: presence.name || actor.actorTokenId || '',
234
+ role: presence.role || 'agent',
235
+ capabilities: presence.capabilities || [],
236
+ metadata: presence.metadata,
237
+ connectedAt: actor.joinedAt || Date.now(),
238
+ };
239
+ }
240
+ async _fetchInitialPresence() {
241
+ try {
242
+ const actors = await this._roomContext.fetchPresence();
243
+ if (Array.isArray(actors)) {
244
+ for (const actor of actors) {
245
+ const connected = this._toConnectedAgent(actor);
246
+ if (connected.actorId) {
247
+ this._agents.set(connected.actorId, connected);
248
+ }
249
+ }
250
+ this._log(`discovered ${this._agents.size} agents in room ${this.name}`);
251
+ }
252
+ }
253
+ catch {
254
+ // fetchPresence may not be available yet
255
+ }
256
+ }
257
+ _wirePresenceListeners() {
258
+ if (!this._client)
259
+ return;
260
+ const client = this._client;
261
+ client.on?.('presence:join', (evt) => {
262
+ if (evt?.roomId === this.name || !evt?.roomId) {
263
+ const id = evt?.actorId || evt?.actorTokenId;
264
+ const data = evt?.data || evt?.presence || {};
265
+ if (id) {
266
+ const agent = {
267
+ actorId: id,
268
+ name: data.name || id,
269
+ role: data.role || 'agent',
270
+ capabilities: data.capabilities || [],
271
+ metadata: data.metadata,
272
+ connectedAt: Date.now(),
273
+ };
274
+ this._agents.set(id, agent);
275
+ this._log(`agent joined room ${this.name}:`, agent.name, agent.capabilities);
276
+ this.emit('presenceJoin', id, data);
277
+ }
278
+ }
279
+ });
280
+ client.on?.('presence:leave', (evt) => {
281
+ if (evt?.roomId === this.name || !evt?.roomId) {
282
+ const id = evt?.actorId || evt?.actorTokenId;
283
+ if (id) {
284
+ const agent = this._agents.get(id);
285
+ this._agents.delete(id);
286
+ this._log(`agent left room ${this.name}:`, agent?.name || id);
287
+ this.emit('presenceLeave', id);
288
+ }
289
+ }
290
+ });
291
+ client.on?.('presence:update', (evt) => {
292
+ if (evt?.roomId === this.name || !evt?.roomId) {
293
+ const id = evt?.actorId || evt?.actorTokenId;
294
+ const data = evt?.data || evt?.presence || {};
295
+ if (id) {
296
+ const existing = this._agents.get(id);
297
+ const agent = {
298
+ actorId: id,
299
+ name: data.name || existing?.name || id,
300
+ role: data.role || existing?.role || 'agent',
301
+ capabilities: data.capabilities || existing?.capabilities || [],
302
+ metadata: data.metadata || existing?.metadata,
303
+ connectedAt: existing?.connectedAt || Date.now(),
304
+ };
305
+ this._agents.set(id, agent);
306
+ this.emit('presenceUpdate', id, data);
307
+ }
308
+ }
309
+ });
310
+ }
311
+ _wireTopicListeners() {
312
+ // All topics that need broker subscriptions
313
+ const allTopics = [
314
+ TOPIC_TASKS,
315
+ TOPIC_RESULTS,
316
+ TOPIC_STATE,
317
+ TOPIC_EVENTS,
318
+ TOPIC_INBOX,
319
+ TOPIC_TOOLS,
320
+ TOPIC_APPROVAL,
321
+ ];
322
+ for (const topic of allTopics) {
323
+ this._roomContext.subscribe(topic);
324
+ }
325
+ // Simple 1:1 mappings
326
+ const simpleMap = [
327
+ { topic: TOPIC_TASKS, event: "task" },
328
+ { topic: TOPIC_RESULTS, event: "result" },
329
+ { topic: TOPIC_STATE, event: "stateChange" },
330
+ { topic: TOPIC_EVENTS, event: "event" },
331
+ { topic: TOPIC_INBOX, event: "inbox" },
332
+ ];
333
+ for (const { topic, event } of simpleMap) {
334
+ this._roomContext.on(topic, (data) => {
335
+ this._log(`received ${topic} in room ${this.name}`);
336
+ this.emit(event, data);
337
+ });
338
+ }
339
+ // Multiplexed: approval topic carries requests + responses
340
+ this._roomContext.on(TOPIC_APPROVAL, (data) => {
341
+ this._log(`received ${TOPIC_APPROVAL} in room ${this.name}`);
342
+ if (data?.type === "approval_response") {
343
+ this.emit("approvalResponse", data);
344
+ }
345
+ else {
346
+ this.emit("approvalRequest", data);
347
+ }
348
+ });
349
+ // Multiplexed: tools topic carries requests + responses
350
+ this._roomContext.on(TOPIC_TOOLS, (data) => {
351
+ this._log(`received ${TOPIC_TOOLS} in room ${this.name}`);
352
+ if (data?.type === "tool_response") {
353
+ this.emit("toolResponse", data);
354
+ }
355
+ else {
356
+ this.emit("toolRequest", data);
357
+ }
358
+ });
359
+ }
360
+ }
361
+
362
+ /**
363
+ * Generate a unique ID.
364
+ * Uses crypto.randomUUID when available, falls back to a simple random string.
365
+ */
366
+ function generateId() {
367
+ if (typeof crypto !== "undefined" &&
368
+ typeof crypto.randomUUID === "function") {
369
+ return crypto.randomUUID();
370
+ }
371
+ return "xxxx-xxxx-xxxx-xxxx".replace(/x/g, () => Math.floor(Math.random() * 16).toString(16));
372
+ }
373
+ /**
374
+ * Create a debug logger that only logs when enabled.
375
+ */
376
+ function createLogger(prefix, enabled) {
377
+ if (!enabled) {
378
+ return (..._args) => { };
379
+ }
380
+ return (...args) => {
381
+ console.log(`[${prefix}]`, ...args);
382
+ };
383
+ }
384
+ /**
385
+ * Create a Unix millisecond timestamp.
386
+ */
387
+ function createTimestamp() {
388
+ return Date.now();
389
+ }
390
+
391
+ /**
392
+ * NoLagAgents — high-level agent coordination SDK built on @nolag/js-sdk.
393
+ *
394
+ * Provides typed rooms for multi-agent patterns: Handoff, Blackboard,
395
+ * Inbox, Tools, Approval, and Observe.
396
+ *
397
+ * @example
398
+ * ```typescript
399
+ * import { NoLagAgents } from '@nolag/agents';
400
+ *
401
+ * const agents = new NoLagAgents(token, {
402
+ * appName: 'my-workflow',
403
+ * agentId: 'worker-1',
404
+ * presence: { name: 'worker-1', role: 'agent', capabilities: ['summarize'] },
405
+ * });
406
+ * await agents.connect();
407
+ *
408
+ * const room = agents.room('default-workflow');
409
+ * room.handoff.onTask(['summarize'], async (task, respond) => {
410
+ * const result = await summarize(task.payload);
411
+ * respond('success', { result });
412
+ * });
413
+ * ```
414
+ */
415
+ class NoLagAgents extends EventEmitter {
416
+ constructor(token, options = {}) {
417
+ super();
418
+ this._client = null;
419
+ this._appContext = null;
420
+ this._rooms = new Map();
421
+ this._connected = false;
422
+ this._token = token;
423
+ this._options = {
424
+ appName: options.appName ?? DEFAULT_APP_NAME,
425
+ agentId: options.agentId ?? generateId(),
426
+ debug: options.debug ?? false,
427
+ rooms: options.rooms ?? [DEFAULT_ROOM],
428
+ lobby: options.lobby,
429
+ presence: options.presence,
430
+ clientOptions: options.clientOptions,
431
+ };
432
+ this._log = createLogger("NoLagAgents", this._options.debug);
433
+ }
434
+ /** The agent's unique ID */
435
+ get agentId() {
436
+ return this._options.agentId;
437
+ }
438
+ /** Whether the client is connected */
439
+ get connected() {
440
+ return this._connected;
441
+ }
442
+ /** Map of joined rooms */
443
+ get rooms() {
444
+ return this._rooms;
445
+ }
446
+ /** Connect to NoLag and join configured rooms */
447
+ async connect() {
448
+ this._log("connecting...");
449
+ this._client = jsSdk.NoLag(this._token, {
450
+ ...this._options.clientOptions,
451
+ });
452
+ this._appContext = this._client.setApp(this._options.appName);
453
+ this._client.on("connected", () => {
454
+ this._connected = true;
455
+ this._log("connected");
456
+ this.emit("connected");
457
+ });
458
+ this._client.on("disconnected", (reason) => {
459
+ this._connected = false;
460
+ this._log("disconnected:", reason);
461
+ this.emit("disconnected", reason);
462
+ });
463
+ this._client.on("reconnected", () => {
464
+ this._connected = true;
465
+ this._log("reconnected");
466
+ this.emit("reconnected");
467
+ });
468
+ this._client.on("error", (err) => {
469
+ this._log("error:", err.message);
470
+ this.emit("error", err);
471
+ });
472
+ await this._client.connect();
473
+ // Auto-join configured rooms
474
+ for (const roomName of this._options.rooms) {
475
+ this.room(roomName);
476
+ }
477
+ // Auto-subscribe to lobby if configured (for cross-room presence observation)
478
+ if (this._options.lobby) {
479
+ await this.subscribeLobby(this._options.lobby);
480
+ }
481
+ }
482
+ /**
483
+ * Subscribe to a lobby for cross-room presence observation.
484
+ * Lobby presence events are forwarded to all AgentRooms.
485
+ *
486
+ * Returns the initial presence snapshot.
487
+ */
488
+ async subscribeLobby(lobbySlug) {
489
+ if (!this._appContext) {
490
+ throw new Error("Not connected. Call connect() before subscribing to lobbies.");
491
+ }
492
+ this._log(`subscribing to lobby: ${lobbySlug}`);
493
+ const lobby = this._appContext.setLobby(lobbySlug);
494
+ // Listen for lobby presence events on the client
495
+ // (lobby.on() uses lobby UUID internally which may not match)
496
+ this._client.on('lobbyPresence:join', (evt) => {
497
+ const id = evt?.actorId;
498
+ const data = evt?.data || {};
499
+ if (id) {
500
+ this._log(`lobby presence:join — ${data.name || id}`);
501
+ for (const room of this._rooms.values()) {
502
+ const agents = room._agents;
503
+ if (!agents.has(id)) {
504
+ agents.set(id, {
505
+ actorId: id,
506
+ name: data.name || id,
507
+ role: data.role || 'agent',
508
+ capabilities: data.capabilities || [],
509
+ metadata: data.metadata,
510
+ connectedAt: Date.now(),
511
+ });
512
+ }
513
+ room._emitPresence('presenceJoin', id, data);
514
+ }
515
+ }
516
+ });
517
+ this._client.on('lobbyPresence:leave', (evt) => {
518
+ const id = evt?.actorId;
519
+ if (id) {
520
+ this._log(`lobby presence:leave — ${id}`);
521
+ for (const room of this._rooms.values()) {
522
+ const agents = room._agents;
523
+ agents.delete(id);
524
+ room._emitPresence('presenceLeave', id);
525
+ }
526
+ }
527
+ });
528
+ this._client.on('lobbyPresence:update', (evt) => {
529
+ const id = evt?.actorId;
530
+ const data = evt?.data || {};
531
+ if (id) {
532
+ for (const room of this._rooms.values()) {
533
+ const agents = room._agents;
534
+ const existing = agents.get(id);
535
+ if (existing) {
536
+ if (data.name)
537
+ existing.name = data.name;
538
+ if (data.role)
539
+ existing.role = data.role;
540
+ if (data.capabilities)
541
+ existing.capabilities = data.capabilities;
542
+ if (data.metadata)
543
+ existing.metadata = data.metadata;
544
+ }
545
+ room._emitPresence('presenceUpdate', id, data);
546
+ }
547
+ }
548
+ });
549
+ try {
550
+ const initialState = await lobby.subscribe();
551
+ this._log(`lobby subscribed, initial state:`, Object.keys(initialState || {}));
552
+ return initialState || {};
553
+ }
554
+ catch (err) {
555
+ this._log(`lobby subscription failed:`, err);
556
+ return {};
557
+ }
558
+ }
559
+ /** Disconnect from NoLag */
560
+ disconnect() {
561
+ this._log("disconnecting...");
562
+ this._client?.disconnect();
563
+ this._rooms.clear();
564
+ this._client = null;
565
+ this._appContext = null;
566
+ this._connected = false;
567
+ }
568
+ /**
569
+ * Get or create an AgentRoom wrapper.
570
+ * If the room hasn't been joined yet, it will be joined automatically.
571
+ */
572
+ room(name) {
573
+ let agentRoom = this._rooms.get(name);
574
+ if (agentRoom)
575
+ return agentRoom;
576
+ if (!this._appContext) {
577
+ throw new Error("Not connected. Call connect() before accessing rooms.");
578
+ }
579
+ this._log(`joining room: ${name}`);
580
+ const roomContext = this._appContext.setRoom(name);
581
+ agentRoom = new AgentRoom(name, roomContext, this._client, this._log, this._options.agentId, this._options.presence);
582
+ this._rooms.set(name, agentRoom);
583
+ return agentRoom;
584
+ }
585
+ }
586
+
587
+ /**
588
+ * CorrelationManager — maps correlationIds to pending promises with timeout.
589
+ * Used by Handoff and Tools patterns for request/response correlation.
590
+ */
591
+ class CorrelationManager {
592
+ constructor() {
593
+ this._pending = new Map();
594
+ }
595
+ /**
596
+ * Register a pending correlation. Returns a promise that resolves
597
+ * when `resolve()` is called with the matching correlationId.
598
+ */
599
+ register(correlationId, timeoutMs) {
600
+ return new Promise((resolve, reject) => {
601
+ let timer = null;
602
+ if (timeoutMs && timeoutMs > 0) {
603
+ timer = setTimeout(() => {
604
+ this._pending.delete(correlationId);
605
+ reject(new Error(`Correlation ${correlationId} timed out after ${timeoutMs}ms`));
606
+ }, timeoutMs);
607
+ }
608
+ this._pending.set(correlationId, { resolve, reject, timer });
609
+ });
610
+ }
611
+ /**
612
+ * Resolve a pending correlation with a value.
613
+ * Returns true if the correlationId was found and resolved.
614
+ */
615
+ resolve(correlationId, value) {
616
+ const entry = this._pending.get(correlationId);
617
+ if (!entry)
618
+ return false;
619
+ if (entry.timer)
620
+ clearTimeout(entry.timer);
621
+ this._pending.delete(correlationId);
622
+ entry.resolve(value);
623
+ return true;
624
+ }
625
+ /**
626
+ * Reject a pending correlation with an error.
627
+ */
628
+ reject(correlationId, error) {
629
+ const entry = this._pending.get(correlationId);
630
+ if (!entry)
631
+ return false;
632
+ if (entry.timer)
633
+ clearTimeout(entry.timer);
634
+ this._pending.delete(correlationId);
635
+ entry.reject(error);
636
+ return true;
637
+ }
638
+ /**
639
+ * Check if a correlationId is pending.
640
+ */
641
+ has(correlationId) {
642
+ return this._pending.has(correlationId);
643
+ }
644
+ /**
645
+ * Cancel all pending correlations.
646
+ */
647
+ clear() {
648
+ for (const [id, entry] of this._pending) {
649
+ if (entry.timer)
650
+ clearTimeout(entry.timer);
651
+ entry.reject(new Error(`Correlation ${id} cancelled`));
652
+ }
653
+ this._pending.clear();
654
+ }
655
+ get size() {
656
+ return this._pending.size;
657
+ }
658
+ }
659
+
660
+ function createTaskEnvelope(capability, payload, options) {
661
+ return {
662
+ type: "task",
663
+ taskId: generateId(),
664
+ correlationId: generateId(),
665
+ replyTo: options?.replyTo,
666
+ capability,
667
+ payload,
668
+ tags: options?.tags,
669
+ priority: options?.priority ?? "medium",
670
+ metadata: options?.metadata,
671
+ createdAt: createTimestamp(),
672
+ createdBy: options?.createdBy,
673
+ timeout: options?.timeout,
674
+ };
675
+ }
676
+ function createResultEnvelope(taskId, correlationId, status, payload, error, completedBy) {
677
+ return {
678
+ type: "result",
679
+ correlationId,
680
+ taskId,
681
+ status,
682
+ payload,
683
+ error,
684
+ completedAt: createTimestamp(),
685
+ completedBy,
686
+ };
687
+ }
688
+ function createStateEnvelope(key, value, version, updatedBy) {
689
+ return {
690
+ type: "state",
691
+ key,
692
+ value,
693
+ version,
694
+ updatedBy,
695
+ updatedAt: createTimestamp(),
696
+ };
697
+ }
698
+ function createEventEnvelope(category, emittedBy, payload, severity = "info") {
699
+ return {
700
+ type: "event",
701
+ eventId: generateId(),
702
+ severity,
703
+ category,
704
+ emittedBy,
705
+ payload,
706
+ timestamp: createTimestamp(),
707
+ };
708
+ }
709
+ function createApprovalRequest(action, context, requestedBy, options) {
710
+ return {
711
+ type: "approval_request",
712
+ requestId: generateId(),
713
+ correlationId: generateId(),
714
+ action,
715
+ context,
716
+ urgency: options?.urgency ?? "medium",
717
+ requestedBy,
718
+ requestedAt: createTimestamp(),
719
+ expiresAt: options?.expiresAt,
720
+ };
721
+ }
722
+ function createApprovalResponse(requestId, correlationId, decision, respondedBy, reason) {
723
+ return {
724
+ type: "approval_response",
725
+ requestId,
726
+ correlationId,
727
+ decision,
728
+ respondedBy,
729
+ reason,
730
+ respondedAt: createTimestamp(),
731
+ };
732
+ }
733
+ function createToolRequest(toolName, args, requestedBy, options) {
734
+ return {
735
+ type: "tool_request",
736
+ requestId: generateId(),
737
+ correlationId: generateId(),
738
+ replyTo: options?.replyTo,
739
+ toolName,
740
+ arguments: args,
741
+ requestedBy,
742
+ requestedAt: createTimestamp(),
743
+ };
744
+ }
745
+ function createToolResponse(requestId, correlationId, status, result, error, respondedBy) {
746
+ return {
747
+ type: "tool_response",
748
+ requestId,
749
+ correlationId,
750
+ status,
751
+ result,
752
+ error,
753
+ respondedBy,
754
+ respondedAt: createTimestamp(),
755
+ };
756
+ }
757
+
758
+ /**
759
+ * Handoff pattern — dispatch tasks to agents and receive results.
760
+ *
761
+ * Orchestrators use `dispatch()` to send work. The SDK checks if any
762
+ * connected agent has the requested capability (via presence-based
763
+ * service discovery) before dispatching.
764
+ *
765
+ * Workers use `onTask()` with a capabilities filter — they only receive
766
+ * tasks matching their registered capabilities.
767
+ *
768
+ * @example
769
+ * ```typescript
770
+ * // Orchestrator
771
+ * const handoff = new Handoff(room);
772
+ * const result = await handoff.dispatch('summarize', { text }, { waitForResult: true });
773
+ *
774
+ * // Worker
775
+ * const handoff = new Handoff(room);
776
+ * handoff.onTask(['summarize', 'translate'], async (task, respond) => {
777
+ * const output = await processTask(task);
778
+ * respond('success', { output });
779
+ * });
780
+ * ```
781
+ */
782
+ class Handoff {
783
+ constructor(room) {
784
+ this._correlations = new CorrelationManager();
785
+ this._room = room;
786
+ // Wire result correlation
787
+ this._room.on("result", (envelope) => {
788
+ this._correlations.resolve(envelope.correlationId, envelope);
789
+ });
790
+ }
791
+ /**
792
+ * Dispatch a task to agents with the given capability.
793
+ *
794
+ * Uses presence-based service discovery to verify at least one agent
795
+ * can handle the capability before dispatching. Throws if no capable
796
+ * agent is connected (unless `allowNoWorkers` is set).
797
+ */
798
+ async dispatch(capability, payload, options) {
799
+ // Service discovery: check if any agent can handle this capability
800
+ if (!options?.allowNoWorkers) {
801
+ const capable = this._room.findAgents(capability);
802
+ if (capable.length === 0) {
803
+ throw new Error(`No agent with capability "${capability}" is connected. ` +
804
+ `Available capabilities: [${this._room.getAvailableCapabilities().join(', ')}]. ` +
805
+ `Connected agents: ${this._room.getConnectedAgents().length}. ` +
806
+ `Use { allowNoWorkers: true } to dispatch anyway.`);
807
+ }
808
+ }
809
+ const envelope = createTaskEnvelope(capability, payload, {
810
+ ...options,
811
+ createdBy: this._room.agentId,
812
+ });
813
+ this._room.publishTask(envelope);
814
+ if (options?.waitForResult) {
815
+ return this._correlations.register(envelope.correlationId, options.timeout);
816
+ }
817
+ }
818
+ /**
819
+ * Register a handler for incoming tasks, filtered by capabilities.
820
+ *
821
+ * Only tasks whose `capability` field matches one of the provided
822
+ * capabilities will be delivered to the handler. Non-matching tasks
823
+ * are silently ignored.
824
+ *
825
+ * @param capabilities - Array of capabilities this worker handles.
826
+ * Pass `'*'` to receive all tasks.
827
+ * @param handler - Async handler called with the task and a respond function.
828
+ */
829
+ onTask(capabilities, handler) {
830
+ this._room.on("task", (task) => {
831
+ // Filter by capability unless wildcard
832
+ if (capabilities !== '*' && !capabilities.includes(task.capability)) {
833
+ return;
834
+ }
835
+ const respond = (status, payload, error) => {
836
+ const result = createResultEnvelope(task.taskId, task.correlationId, status, payload, error, this._room.agentId);
837
+ this._room.publishResult(result);
838
+ };
839
+ handler(task, respond);
840
+ });
841
+ }
842
+ /**
843
+ * Get agents capable of handling a specific task type.
844
+ * Delegates to the room's presence-based service discovery.
845
+ */
846
+ getCapableAgents(capability) {
847
+ return this._room.findAgents(capability);
848
+ }
849
+ /** Cancel all pending correlations */
850
+ dispose() {
851
+ this._correlations.clear();
852
+ }
853
+ }
854
+
855
+ /**
856
+ * Inbox pattern — per-agent durable message queues.
857
+ *
858
+ * Agents send direct messages to other agents via their inbox.
859
+ * Messages are persisted and replayed on reconnect (requires persistent sessions).
860
+ */
861
+ class Inbox {
862
+ constructor(room, agentId) {
863
+ this._room = room;
864
+ this._agentId = agentId;
865
+ }
866
+ /**
867
+ * Send a message to another agent's inbox.
868
+ */
869
+ send(to, payload) {
870
+ const message = {
871
+ messageId: generateId(),
872
+ from: this._agentId,
873
+ to,
874
+ payload,
875
+ createdAt: createTimestamp(),
876
+ };
877
+ this._room.publishInbox(message);
878
+ }
879
+ /**
880
+ * Register a handler for incoming inbox messages.
881
+ */
882
+ onMessage(handler) {
883
+ this._room.on("inbox", (envelope) => {
884
+ const msg = envelope;
885
+ if (msg.to === this._agentId) {
886
+ handler(msg);
887
+ }
888
+ });
889
+ }
890
+ }
891
+
892
+ /**
893
+ * Blackboard pattern — shared state across agents.
894
+ *
895
+ * Agents read and write key-value pairs visible to all room participants.
896
+ * Uses retained messages so state is available on join.
897
+ */
898
+ class Blackboard {
899
+ constructor(room, agentId) {
900
+ this._state = new Map();
901
+ this._room = room;
902
+ this._agentId = agentId;
903
+ this._room.on("stateChange", (envelope) => {
904
+ this._state.set(envelope.key, envelope);
905
+ });
906
+ }
907
+ /**
908
+ * Set a shared state value.
909
+ */
910
+ set(key, value) {
911
+ const existing = this._state.get(key);
912
+ const version = existing ? existing.version + 1 : 1;
913
+ const envelope = createStateEnvelope(key, value, version, this._agentId);
914
+ this._state.set(key, envelope);
915
+ this._room.publishState(envelope);
916
+ }
917
+ /**
918
+ * Get a shared state value.
919
+ */
920
+ get(key) {
921
+ return this._state.get(key)?.value;
922
+ }
923
+ /**
924
+ * Get the full state envelope for a key.
925
+ */
926
+ getEnvelope(key) {
927
+ return this._state.get(key);
928
+ }
929
+ /**
930
+ * Get all state entries.
931
+ */
932
+ getAll() {
933
+ return this._state;
934
+ }
935
+ /**
936
+ * Register a handler for state changes on a specific key.
937
+ */
938
+ onChange(key, handler) {
939
+ this._room.on("stateChange", (envelope) => {
940
+ if (envelope.key === key) {
941
+ handler(envelope);
942
+ }
943
+ });
944
+ }
945
+ }
946
+
947
+ /**
948
+ * Observe pattern — emit and listen to observability events.
949
+ *
950
+ * Agents emit structured events; observers/dashboards subscribe to the stream.
951
+ * Events have severity, category, and emittedBy for filtering.
952
+ */
953
+ class Observe {
954
+ constructor(room, emittedBy) {
955
+ this._room = room;
956
+ this._emittedBy = emittedBy;
957
+ }
958
+ /**
959
+ * Emit an observability event.
960
+ */
961
+ emit(category, payload, severity = "info") {
962
+ const envelope = createEventEnvelope(category, this._emittedBy, payload, severity);
963
+ this._room.publishEvent(envelope);
964
+ }
965
+ /**
966
+ * Listen for events, optionally filtered by category or severity.
967
+ */
968
+ on(handler, filter) {
969
+ this._room.on("event", (envelope) => {
970
+ if (filter?.category && envelope.category !== filter.category)
971
+ return;
972
+ if (filter?.severity && envelope.severity !== filter.severity)
973
+ return;
974
+ handler(envelope);
975
+ });
976
+ }
977
+ }
978
+
979
+ /**
980
+ * Approve pattern — human-in-the-loop approval gates.
981
+ *
982
+ * Agents request approval before taking actions; humans (or other agents)
983
+ * approve or reject via the approval topic.
984
+ */
985
+ class Approve {
986
+ constructor(room, agentId) {
987
+ this._correlations = new CorrelationManager();
988
+ this._room = room;
989
+ this._agentId = agentId;
990
+ // Wire approval response correlation
991
+ this._room.on("approvalResponse", (envelope) => {
992
+ this._correlations.resolve(envelope.correlationId, envelope);
993
+ });
994
+ }
995
+ /**
996
+ * Request approval for an action. Returns the approval response.
997
+ */
998
+ async request(action, context, options) {
999
+ const envelope = createApprovalRequest(action, context, this._agentId, {
1000
+ urgency: options?.urgency,
1001
+ expiresAt: options?.expiresAt,
1002
+ });
1003
+ this._room.publishApproval(envelope);
1004
+ return this._correlations.register(envelope.correlationId, options?.timeout);
1005
+ }
1006
+ /**
1007
+ * Register a handler for incoming approval requests.
1008
+ * The handler receives the request and a respond function.
1009
+ */
1010
+ onRequest(handler) {
1011
+ this._room.on("approvalRequest", (request) => {
1012
+ const respond = (decision, reason) => {
1013
+ const response = createApprovalResponse(request.requestId, request.correlationId, decision, this._agentId, reason);
1014
+ this._room.publishApproval(response);
1015
+ };
1016
+ handler(request, respond);
1017
+ });
1018
+ }
1019
+ /** Cancel all pending correlations */
1020
+ dispose() {
1021
+ this._correlations.clear();
1022
+ }
1023
+ }
1024
+
1025
+ /**
1026
+ * Tools pattern — typed RPC over pub/sub for tool invocations.
1027
+ *
1028
+ * Agents register tool handlers; callers invoke tools and receive
1029
+ * correlated responses.
1030
+ */
1031
+ class Tools {
1032
+ constructor(room, agentId) {
1033
+ this._correlations = new CorrelationManager();
1034
+ this._handlers = new Map();
1035
+ this._room = room;
1036
+ this._agentId = agentId;
1037
+ // Wire response correlation
1038
+ this._room.on("toolResponse", (envelope) => {
1039
+ this._correlations.resolve(envelope.correlationId, envelope);
1040
+ });
1041
+ // Wire request handling
1042
+ this._room.on("toolRequest", async (envelope) => {
1043
+ const handler = this._handlers.get(envelope.toolName);
1044
+ if (!handler)
1045
+ return;
1046
+ try {
1047
+ const result = await handler(envelope.arguments);
1048
+ const response = createToolResponse(envelope.requestId, envelope.correlationId, "success", result, undefined, this._agentId);
1049
+ this._room.publishTools(response);
1050
+ }
1051
+ catch (err) {
1052
+ const response = createToolResponse(envelope.requestId, envelope.correlationId, "error", null, {
1053
+ code: "TOOL_ERROR",
1054
+ message: err instanceof Error ? err.message : String(err),
1055
+ }, this._agentId);
1056
+ this._room.publishTools(response);
1057
+ }
1058
+ });
1059
+ }
1060
+ /**
1061
+ * Register a tool handler.
1062
+ */
1063
+ register(toolName, handler) {
1064
+ this._handlers.set(toolName, handler);
1065
+ }
1066
+ /**
1067
+ * Invoke a remote tool and wait for the response.
1068
+ */
1069
+ async invoke(toolName, args, options) {
1070
+ const envelope = createToolRequest(toolName, args, this._agentId);
1071
+ this._room.publishTools(envelope);
1072
+ return this._correlations.register(envelope.correlationId, options?.timeout);
1073
+ }
1074
+ /** Cancel all pending correlations */
1075
+ dispose() {
1076
+ this._correlations.clear();
1077
+ this._handlers.clear();
1078
+ }
1079
+ }
1080
+
1081
+ /** Standard tag prefixes for agent coordination */
1082
+ const TAG_PREFIX = {
1083
+ CAPABILITY: "capability",
1084
+ PRIORITY: "priority",
1085
+ ROLE: "role",
1086
+ SEVERITY: "severity",
1087
+ URGENCY: "urgency",
1088
+ TENANT: "tenant",
1089
+ };
1090
+ /** Boolean flags (used as standalone tags, no prefix) */
1091
+ const TAG_FLAGS = {
1092
+ REQUIRES_HUMAN: "requires_human",
1093
+ REQUIRES_AUDIT: "requires_audit",
1094
+ };
1095
+ /** Helper to create prefixed tags */
1096
+ function tag(prefix, value) {
1097
+ return `${prefix}:${value}`;
1098
+ }
1099
+
1100
+ exports.AgentRoom = AgentRoom;
1101
+ exports.Approve = Approve;
1102
+ exports.Blackboard = Blackboard;
1103
+ exports.CorrelationManager = CorrelationManager;
1104
+ exports.EventEmitter = EventEmitter;
1105
+ exports.Handoff = Handoff;
1106
+ exports.Inbox = Inbox;
1107
+ exports.NoLagAgents = NoLagAgents;
1108
+ exports.Observe = Observe;
1109
+ exports.TAG_FLAGS = TAG_FLAGS;
1110
+ exports.TAG_PREFIX = TAG_PREFIX;
1111
+ exports.Tools = Tools;
1112
+ exports.createApprovalRequest = createApprovalRequest;
1113
+ exports.createApprovalResponse = createApprovalResponse;
1114
+ exports.createEventEnvelope = createEventEnvelope;
1115
+ exports.createResultEnvelope = createResultEnvelope;
1116
+ exports.createStateEnvelope = createStateEnvelope;
1117
+ exports.createTaskEnvelope = createTaskEnvelope;
1118
+ exports.createToolRequest = createToolRequest;
1119
+ exports.createToolResponse = createToolResponse;
1120
+ exports.tag = tag;
1121
+ //# sourceMappingURL=index.cjs.map