@nolag/agents 1.1.1 → 1.2.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/AgentRoom.d.ts +49 -8
- package/dist/NoLagAgents.d.ts +2 -2
- package/dist/browser.js +1 -1
- package/dist/browser.js.map +1 -1
- package/dist/index.cjs +251 -78
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.mjs +251 -78
- package/dist/index.mjs.map +1 -1
- package/dist/patterns/observe.d.ts +26 -2
- package/dist/react-native.js +251 -78
- package/dist/react-native.js.map +1 -1
- package/dist/types.d.ts +50 -0
- package/dist/utils.d.ts +49 -0
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -58,6 +58,97 @@ class EventEmitter {
|
|
|
58
58
|
}
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
/**
|
|
62
|
+
* Generate a unique ID.
|
|
63
|
+
* Uses crypto.randomUUID when available, falls back to a simple random string.
|
|
64
|
+
*/
|
|
65
|
+
function generateId() {
|
|
66
|
+
if (typeof crypto !== "undefined" &&
|
|
67
|
+
typeof crypto.randomUUID === "function") {
|
|
68
|
+
return crypto.randomUUID();
|
|
69
|
+
}
|
|
70
|
+
return "xxxx-xxxx-xxxx-xxxx".replace(/x/g, () => Math.floor(Math.random() * 16).toString(16));
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Create a debug logger that only logs when enabled.
|
|
74
|
+
*/
|
|
75
|
+
function createLogger(prefix, enabled) {
|
|
76
|
+
if (!enabled) {
|
|
77
|
+
return (..._args) => { };
|
|
78
|
+
}
|
|
79
|
+
return (...args) => {
|
|
80
|
+
console.log(`[${prefix}]`, ...args);
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Create a Unix millisecond timestamp.
|
|
85
|
+
*/
|
|
86
|
+
function createTimestamp() {
|
|
87
|
+
return Date.now();
|
|
88
|
+
}
|
|
89
|
+
// ============ Filters ============
|
|
90
|
+
/**
|
|
91
|
+
* Build the filter fragment of an emit options object.
|
|
92
|
+
*
|
|
93
|
+
* `filter` wins over `filters`: a publish is routed to exactly one topic, so
|
|
94
|
+
* honouring both would silently drop one of them.
|
|
95
|
+
*/
|
|
96
|
+
function filterEmitOptions(opts) {
|
|
97
|
+
if (opts?.filter)
|
|
98
|
+
return { filter: opts.filter };
|
|
99
|
+
if (opts?.filters && opts.filters.length > 0)
|
|
100
|
+
return { filters: opts.filters };
|
|
101
|
+
return {};
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Merge OR terms into an existing filter set. AND groups (nested arrays) are
|
|
105
|
+
* preserved as-is — only plain string terms are deduplicated.
|
|
106
|
+
*/
|
|
107
|
+
function mergeFilters(existing, add) {
|
|
108
|
+
const simple = new Set();
|
|
109
|
+
const groups = [];
|
|
110
|
+
for (const f of existing) {
|
|
111
|
+
if (typeof f === 'string')
|
|
112
|
+
simple.add(f);
|
|
113
|
+
else
|
|
114
|
+
groups.push(f);
|
|
115
|
+
}
|
|
116
|
+
for (const v of add)
|
|
117
|
+
simple.add(v);
|
|
118
|
+
return [...simple, ...groups];
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Drop OR terms from a filter set. AND groups are left untouched — remove
|
|
122
|
+
* those by calling `setFilters` with the set you want.
|
|
123
|
+
*/
|
|
124
|
+
function withoutFilters(existing, remove) {
|
|
125
|
+
const drop = new Set(remove);
|
|
126
|
+
return existing.filter((f) => typeof f !== 'string' || !drop.has(f));
|
|
127
|
+
}
|
|
128
|
+
// ============ Wrapper registry ============
|
|
129
|
+
// One wrapper instance per (client, appName): two wrappers sharing an app on
|
|
130
|
+
// one connection would collide on topics, presence and the lobby.
|
|
131
|
+
// Warn (not throw): HMR and tests legitimately construct before disposing.
|
|
132
|
+
const wrapperRegistry = new WeakMap();
|
|
133
|
+
/** Register a wrapper against a client + appName; warns on collision. */
|
|
134
|
+
function registerWrapper(client, appName, wrapperName) {
|
|
135
|
+
let apps = wrapperRegistry.get(client);
|
|
136
|
+
if (!apps) {
|
|
137
|
+
apps = new Map();
|
|
138
|
+
wrapperRegistry.set(client, apps);
|
|
139
|
+
}
|
|
140
|
+
const existing = apps.get(appName);
|
|
141
|
+
if (existing) {
|
|
142
|
+
console.warn(`[${wrapperName}] Another wrapper (${existing}) is already attached to this client for app "${appName}". ` +
|
|
143
|
+
`Use one wrapper per (client, app) — detach the other instance first.`);
|
|
144
|
+
}
|
|
145
|
+
apps.set(appName, wrapperName);
|
|
146
|
+
}
|
|
147
|
+
/** Release a wrapper's (client, appName) registration on detach. */
|
|
148
|
+
function releaseWrapper(client, appName) {
|
|
149
|
+
wrapperRegistry.get(client)?.delete(appName);
|
|
150
|
+
}
|
|
151
|
+
|
|
61
152
|
/** Default app name for agent coordination */
|
|
62
153
|
const DEFAULT_APP_NAME = "agents";
|
|
63
154
|
/** Topic name for task dispatch (Handoff pattern) */
|
|
@@ -83,6 +174,19 @@ const LOBBY_REFRESH_DELAY_MS = 2000;
|
|
|
83
174
|
* broadcast replies (pre-0.2.0 SDKs). */
|
|
84
175
|
const AGENTS_PROTOCOL_VERSION = 2;
|
|
85
176
|
|
|
177
|
+
/**
|
|
178
|
+
* Maps the public topic names onto the wire topics. `results` is absent by
|
|
179
|
+
* design — it is reserved for directed replies keyed to this agent's id.
|
|
180
|
+
*/
|
|
181
|
+
const FILTER_TOPICS = {
|
|
182
|
+
tasks: TOPIC_TASKS,
|
|
183
|
+
tools: TOPIC_TOOLS,
|
|
184
|
+
state: TOPIC_STATE,
|
|
185
|
+
events: TOPIC_EVENTS,
|
|
186
|
+
inbox: TOPIC_INBOX,
|
|
187
|
+
approval: TOPIC_APPROVAL,
|
|
188
|
+
};
|
|
189
|
+
const ALL_FILTER_TOPICS = Object.keys(FILTER_TOPICS);
|
|
86
190
|
/**
|
|
87
191
|
* AgentRoom — a single agent-coordination room (scoped unit).
|
|
88
192
|
*
|
|
@@ -109,13 +213,17 @@ const AGENTS_PROTOCOL_VERSION = 2;
|
|
|
109
213
|
*/
|
|
110
214
|
class AgentRoom extends EventEmitter {
|
|
111
215
|
/** @internal */
|
|
112
|
-
constructor(name, roomContext, log, agentId, appName, isConnected, presence) {
|
|
216
|
+
constructor(name, roomContext, log, agentId, appName, isConnected, presence, filters) {
|
|
113
217
|
super();
|
|
114
218
|
/** Registry of connected agents discovered via presence */
|
|
115
219
|
this._agents = new Map();
|
|
116
220
|
// Stored topic handler refs — cleanup removes exactly these, never all
|
|
117
221
|
// handlers for a topic (the client may be shared with other consumers).
|
|
118
222
|
this._topicHandlers = [];
|
|
223
|
+
/** Filter values applied per topic. `results` is never included. */
|
|
224
|
+
this._filters = {
|
|
225
|
+
tasks: [], tools: [], state: [], events: [], inbox: [], approval: [],
|
|
226
|
+
};
|
|
119
227
|
this.name = name;
|
|
120
228
|
this.agentId = agentId;
|
|
121
229
|
this._roomContext = roomContext;
|
|
@@ -123,6 +231,10 @@ class AgentRoom extends EventEmitter {
|
|
|
123
231
|
this._appName = appName;
|
|
124
232
|
this._isConnected = isConnected;
|
|
125
233
|
this._presence = presence;
|
|
234
|
+
if (filters && filters.length > 0) {
|
|
235
|
+
for (const topic of ALL_FILTER_TOPICS)
|
|
236
|
+
this._filters[topic] = [...filters];
|
|
237
|
+
}
|
|
126
238
|
this._wireTopicListeners();
|
|
127
239
|
// Set presence if provided (with the SDK's protocol version advertised
|
|
128
240
|
// so counterparts can detect incompatible reply semantics, and a __scope
|
|
@@ -187,12 +299,15 @@ class AgentRoom extends EventEmitter {
|
|
|
187
299
|
// PUBLISH (with automatic agentId injection)
|
|
188
300
|
// ============================================================
|
|
189
301
|
/** Publish to the tasks topic */
|
|
190
|
-
publishTask(envelope) {
|
|
302
|
+
publishTask(envelope, opts) {
|
|
191
303
|
// Auto-set createdBy if not set
|
|
192
304
|
if (!envelope.createdBy) {
|
|
193
305
|
envelope.createdBy = this.agentId;
|
|
194
306
|
}
|
|
195
|
-
|
|
307
|
+
// Routing by capability (`{ filter: envelope.capability }`) is opt-in:
|
|
308
|
+
// it only reaches workers that filter on it, and mixing filtered and
|
|
309
|
+
// wildcard workers in one load-balance pool double-delivers.
|
|
310
|
+
this._publish(TOPIC_TASKS, envelope, filterEmitOptions(opts));
|
|
196
311
|
}
|
|
197
312
|
/** Publish to the results topic — directed to the dispatcher via filter when replyTo is set */
|
|
198
313
|
publishResult(envelope) {
|
|
@@ -210,24 +325,24 @@ class AgentRoom extends EventEmitter {
|
|
|
210
325
|
}
|
|
211
326
|
}
|
|
212
327
|
/** Publish to the state topic (retained) */
|
|
213
|
-
publishState(data) {
|
|
328
|
+
publishState(data, opts) {
|
|
214
329
|
// Auto-set updatedBy if not set
|
|
215
330
|
if (!data.updatedBy) {
|
|
216
331
|
data.updatedBy = this.agentId;
|
|
217
332
|
}
|
|
218
|
-
this._publish(TOPIC_STATE, data, { retain: true });
|
|
333
|
+
this._publish(TOPIC_STATE, data, { retain: true, ...filterEmitOptions(opts) });
|
|
219
334
|
}
|
|
220
335
|
/** Publish to the events topic */
|
|
221
|
-
publishEvent(data) {
|
|
336
|
+
publishEvent(data, opts) {
|
|
222
337
|
// Auto-set emittedBy if not set
|
|
223
338
|
if (!data.emittedBy) {
|
|
224
339
|
data.emittedBy = this.agentId;
|
|
225
340
|
}
|
|
226
|
-
this._publish(TOPIC_EVENTS, data);
|
|
341
|
+
this._publish(TOPIC_EVENTS, data, filterEmitOptions(opts));
|
|
227
342
|
}
|
|
228
343
|
/** Publish to the inbox topic */
|
|
229
|
-
publishInbox(data) {
|
|
230
|
-
this._publish(TOPIC_INBOX, data);
|
|
344
|
+
publishInbox(data, opts) {
|
|
345
|
+
this._publish(TOPIC_INBOX, data, filterEmitOptions(opts));
|
|
231
346
|
}
|
|
232
347
|
/**
|
|
233
348
|
* Publish a tool message.
|
|
@@ -235,16 +350,84 @@ class AgentRoom extends EventEmitter {
|
|
|
235
350
|
* replicas). Responses are directed to the requester on the results topic
|
|
236
351
|
* via filter — never load-balanced, never broadcast.
|
|
237
352
|
*/
|
|
238
|
-
publishTools(data) {
|
|
353
|
+
publishTools(data, opts) {
|
|
239
354
|
if (data?.type === "tool_response" && typeof data.replyTo === "string" && data.replyTo) {
|
|
355
|
+
// Responses stay keyed to the requester; a caller filter must not
|
|
356
|
+
// redirect them away from the agent waiting on the correlation.
|
|
240
357
|
this._publish(TOPIC_RESULTS, data, { filter: data.replyTo });
|
|
241
358
|
return;
|
|
242
359
|
}
|
|
243
|
-
this._publish(TOPIC_TOOLS, data);
|
|
360
|
+
this._publish(TOPIC_TOOLS, data, filterEmitOptions(opts));
|
|
244
361
|
}
|
|
245
362
|
/** Publish to the approval topic (retained) */
|
|
246
|
-
publishApproval(data) {
|
|
247
|
-
this._publish(TOPIC_APPROVAL, data, { retain: true });
|
|
363
|
+
publishApproval(data, opts) {
|
|
364
|
+
this._publish(TOPIC_APPROVAL, data, { retain: true, ...filterEmitOptions(opts) });
|
|
365
|
+
}
|
|
366
|
+
// ============================================================
|
|
367
|
+
// FILTERS
|
|
368
|
+
// ============================================================
|
|
369
|
+
/** The filter values currently applied to this room, by topic. */
|
|
370
|
+
get filters() {
|
|
371
|
+
return {
|
|
372
|
+
tasks: [...this._filters.tasks],
|
|
373
|
+
tools: [...this._filters.tools],
|
|
374
|
+
state: [...this._filters.state],
|
|
375
|
+
events: [...this._filters.events],
|
|
376
|
+
inbox: [...this._filters.inbox],
|
|
377
|
+
approval: [...this._filters.approval],
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
/**
|
|
381
|
+
* Replace this room's filters — only messages published with one of these
|
|
382
|
+
* values are delivered. Applies to every filterable topic unless scoped with
|
|
383
|
+
* `{ topic }`.
|
|
384
|
+
*
|
|
385
|
+
* The usual case is a worker declaring its capabilities on `tasks`, so the
|
|
386
|
+
* broker routes only work it can do instead of every worker receiving every
|
|
387
|
+
* task and discarding the rest. Pair it with
|
|
388
|
+
* `publishTask(envelope, { filter: capability })` on the dispatcher.
|
|
389
|
+
*
|
|
390
|
+
* `results` is never filtered here: it carries directed replies keyed to
|
|
391
|
+
* this agent's id, and repointing it would strand pending results.
|
|
392
|
+
*
|
|
393
|
+
* With load balancing on, keep a worker pool uniform — the broker treats a
|
|
394
|
+
* wildcard and a filtered subscription as separate share groups, so a mixed
|
|
395
|
+
* pool delivers each task twice.
|
|
396
|
+
*
|
|
397
|
+
* Passing an empty array clears filtering and restores the wildcard
|
|
398
|
+
* subscription, which receives everything.
|
|
399
|
+
*
|
|
400
|
+
* @example
|
|
401
|
+
* ```ts
|
|
402
|
+
* room.setFilters(['ocr', 'translate'], { topic: 'tasks' });
|
|
403
|
+
* room.setFilters([]); // everything
|
|
404
|
+
* ```
|
|
405
|
+
*/
|
|
406
|
+
setFilters(values, opts) {
|
|
407
|
+
for (const topic of this._targetTopics(opts)) {
|
|
408
|
+
this._filters[topic] = [...values];
|
|
409
|
+
// The core types filters as `string[]`, but both its implementation and
|
|
410
|
+
// the wire protocol accept AND groups (nested arrays).
|
|
411
|
+
this._roomContext.setFilters(FILTER_TOPICS[topic], values);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
/** Add filter values to the existing set. Existing AND groups are kept. */
|
|
415
|
+
addFilters(values, opts) {
|
|
416
|
+
for (const topic of this._targetTopics(opts)) {
|
|
417
|
+
this.setFilters(mergeFilters(this._filters[topic], values), { topic });
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
/**
|
|
421
|
+
* Remove filter values from the existing set. Removing the last value
|
|
422
|
+
* restores the wildcard subscription.
|
|
423
|
+
*/
|
|
424
|
+
removeFilters(values, opts) {
|
|
425
|
+
for (const topic of this._targetTopics(opts)) {
|
|
426
|
+
this.setFilters(withoutFilters(this._filters[topic], values), { topic });
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
_targetTopics(opts) {
|
|
430
|
+
return opts?.topic ? [opts.topic] : ALL_FILTER_TOPICS;
|
|
248
431
|
}
|
|
249
432
|
// ============================================================
|
|
250
433
|
// INTERNAL (called by NoLagAgents)
|
|
@@ -325,13 +508,24 @@ class AgentRoom extends EventEmitter {
|
|
|
325
508
|
}
|
|
326
509
|
_publish(topic, data, options) {
|
|
327
510
|
this._log(`publish to ${topic} in room ${this.name}`);
|
|
328
|
-
|
|
511
|
+
// An empty options object is dropped rather than forwarded: publishing
|
|
512
|
+
// without filters should look exactly as it did before filters existed.
|
|
513
|
+
if (options && Object.keys(options).length > 0) {
|
|
329
514
|
this._roomContext.emit(topic, data, options);
|
|
330
515
|
}
|
|
331
516
|
else {
|
|
332
517
|
this._roomContext.emit(topic, data);
|
|
333
518
|
}
|
|
334
519
|
}
|
|
520
|
+
/** @internal Subscribe honouring the topic's filter set. */
|
|
521
|
+
_subscribeFiltered(topic, values) {
|
|
522
|
+
if (values.length > 0) {
|
|
523
|
+
this._roomContext.subscribe(topic, { filters: values });
|
|
524
|
+
}
|
|
525
|
+
else {
|
|
526
|
+
this._roomContext.subscribe(topic);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
335
529
|
_toConnectedAgent(actor) {
|
|
336
530
|
const presence = (actor.presence || actor.data || {});
|
|
337
531
|
return {
|
|
@@ -367,8 +561,8 @@ class AgentRoom extends EventEmitter {
|
|
|
367
561
|
// setting, so a pool shares each message one-of-N (no double handling):
|
|
368
562
|
// - tasks: each task goes to exactly one worker in the group
|
|
369
563
|
// - tools: each tool REQUEST goes to exactly one tool-server replica
|
|
370
|
-
this.
|
|
371
|
-
this.
|
|
564
|
+
this._subscribeFiltered(TOPIC_TASKS, this._filters.tasks);
|
|
565
|
+
this._subscribeFiltered(TOPIC_TOOLS, this._filters.tools);
|
|
372
566
|
// Replies are DIRECTED, not broadcast: the results topic carries task
|
|
373
567
|
// results and tool responses published with `filter: <recipient agentId>`,
|
|
374
568
|
// and each agent subscribes only to its own filter sub-topic. The broker
|
|
@@ -383,9 +577,13 @@ class AgentRoom extends EventEmitter {
|
|
|
383
577
|
// Broadcast topics must always fan out, even when the connection enables
|
|
384
578
|
// loadBalance for work distribution: state/events are broadcasts by
|
|
385
579
|
// nature; inbox and approval messages are claimed client-side.
|
|
386
|
-
const broadcastTopics = [
|
|
387
|
-
for (const
|
|
388
|
-
|
|
580
|
+
const broadcastTopics = ["state", "events", "inbox", "approval"];
|
|
581
|
+
for (const key of broadcastTopics) {
|
|
582
|
+
const values = this._filters[key];
|
|
583
|
+
this._roomContext.subscribe(FILTER_TOPICS[key], {
|
|
584
|
+
loadBalance: false,
|
|
585
|
+
...(values.length > 0 ? { filters: values } : {}),
|
|
586
|
+
});
|
|
389
587
|
}
|
|
390
588
|
// Simple 1:1 mappings
|
|
391
589
|
const simpleMap = [
|
|
@@ -436,58 +634,6 @@ class AgentRoom extends EventEmitter {
|
|
|
436
634
|
}
|
|
437
635
|
}
|
|
438
636
|
|
|
439
|
-
/**
|
|
440
|
-
* Generate a unique ID.
|
|
441
|
-
* Uses crypto.randomUUID when available, falls back to a simple random string.
|
|
442
|
-
*/
|
|
443
|
-
function generateId() {
|
|
444
|
-
if (typeof crypto !== "undefined" &&
|
|
445
|
-
typeof crypto.randomUUID === "function") {
|
|
446
|
-
return crypto.randomUUID();
|
|
447
|
-
}
|
|
448
|
-
return "xxxx-xxxx-xxxx-xxxx".replace(/x/g, () => Math.floor(Math.random() * 16).toString(16));
|
|
449
|
-
}
|
|
450
|
-
/**
|
|
451
|
-
* Create a debug logger that only logs when enabled.
|
|
452
|
-
*/
|
|
453
|
-
function createLogger(prefix, enabled) {
|
|
454
|
-
if (!enabled) {
|
|
455
|
-
return (..._args) => { };
|
|
456
|
-
}
|
|
457
|
-
return (...args) => {
|
|
458
|
-
console.log(`[${prefix}]`, ...args);
|
|
459
|
-
};
|
|
460
|
-
}
|
|
461
|
-
/**
|
|
462
|
-
* Create a Unix millisecond timestamp.
|
|
463
|
-
*/
|
|
464
|
-
function createTimestamp() {
|
|
465
|
-
return Date.now();
|
|
466
|
-
}
|
|
467
|
-
// ============ Wrapper registry ============
|
|
468
|
-
// One wrapper instance per (client, appName): two wrappers sharing an app on
|
|
469
|
-
// one connection would collide on topics, presence and the lobby.
|
|
470
|
-
// Warn (not throw): HMR and tests legitimately construct before disposing.
|
|
471
|
-
const wrapperRegistry = new WeakMap();
|
|
472
|
-
/** Register a wrapper against a client + appName; warns on collision. */
|
|
473
|
-
function registerWrapper(client, appName, wrapperName) {
|
|
474
|
-
let apps = wrapperRegistry.get(client);
|
|
475
|
-
if (!apps) {
|
|
476
|
-
apps = new Map();
|
|
477
|
-
wrapperRegistry.set(client, apps);
|
|
478
|
-
}
|
|
479
|
-
const existing = apps.get(appName);
|
|
480
|
-
if (existing) {
|
|
481
|
-
console.warn(`[${wrapperName}] Another wrapper (${existing}) is already attached to this client for app "${appName}". ` +
|
|
482
|
-
`Use one wrapper per (client, app) — detach the other instance first.`);
|
|
483
|
-
}
|
|
484
|
-
apps.set(appName, wrapperName);
|
|
485
|
-
}
|
|
486
|
-
/** Release a wrapper's (client, appName) registration on detach. */
|
|
487
|
-
function releaseWrapper(client, appName) {
|
|
488
|
-
wrapperRegistry.get(client)?.delete(appName);
|
|
489
|
-
}
|
|
490
|
-
|
|
491
637
|
/**
|
|
492
638
|
* NoLagAgents — high-level agent coordination SDK built on @nolag/js-sdk.
|
|
493
639
|
*
|
|
@@ -759,12 +905,16 @@ class NoLagAgents extends EventEmitter {
|
|
|
759
905
|
* Get or create an AgentRoom wrapper.
|
|
760
906
|
* If the room hasn't been joined yet, it will be joined automatically.
|
|
761
907
|
*/
|
|
762
|
-
room(name) {
|
|
908
|
+
room(name, opts) {
|
|
763
909
|
this._assertUsable();
|
|
764
910
|
const existing = this._rooms.get(name);
|
|
765
|
-
if (existing)
|
|
911
|
+
if (existing) {
|
|
912
|
+
// Already joined — re-point its filters rather than ignoring them.
|
|
913
|
+
if (opts?.filters)
|
|
914
|
+
existing.setFilters(opts.filters);
|
|
766
915
|
return existing;
|
|
767
|
-
|
|
916
|
+
}
|
|
917
|
+
return this._joinRoomInternal(name, opts?.filters);
|
|
768
918
|
}
|
|
769
919
|
// ============ Lobby (cross-room presence observation) ============
|
|
770
920
|
/**
|
|
@@ -802,10 +952,10 @@ class NoLagAgents extends EventEmitter {
|
|
|
802
952
|
}
|
|
803
953
|
}
|
|
804
954
|
// ============ Private: Room Setup ============
|
|
805
|
-
_joinRoomInternal(name) {
|
|
955
|
+
_joinRoomInternal(name, filters) {
|
|
806
956
|
this._log(`joining room: ${name}`);
|
|
807
957
|
const roomContext = this._client.setApp(this._options.appName).setRoom(name);
|
|
808
|
-
const room = new AgentRoom(name, roomContext, createLogger(`AgentRoom:${name}`, this._options.debug), this._options.agentId, this._options.appName, () => this._client.connected, this._options.presence);
|
|
958
|
+
const room = new AgentRoom(name, roomContext, createLogger(`AgentRoom:${name}`, this._options.debug), this._options.agentId, this._options.appName, () => this._client.connected, this._options.presence, filters);
|
|
809
959
|
this._rooms.set(name, room);
|
|
810
960
|
return room;
|
|
811
961
|
}
|
|
@@ -1347,6 +1497,11 @@ class Blackboard {
|
|
|
1347
1497
|
*
|
|
1348
1498
|
* Agents emit structured events; observers/dashboards subscribe to the stream.
|
|
1349
1499
|
* Events have severity, category, and emittedBy for filtering.
|
|
1500
|
+
*
|
|
1501
|
+
* `on(handler, filter)` discards non-matching events after they arrive, which
|
|
1502
|
+
* is fine for a quiet room and wasteful for a loud one. `setFilters` moves the
|
|
1503
|
+
* same selection to the broker, so an observer is only sent the categories it
|
|
1504
|
+
* asked for. Emit with a matching `filter` for that to work — see `emit`.
|
|
1350
1505
|
*/
|
|
1351
1506
|
class Observe {
|
|
1352
1507
|
constructor(room, emittedBy) {
|
|
@@ -1355,10 +1510,28 @@ class Observe {
|
|
|
1355
1510
|
}
|
|
1356
1511
|
/**
|
|
1357
1512
|
* Emit an observability event.
|
|
1513
|
+
*
|
|
1514
|
+
* Pass `{ filter: category }` to route it server-side, so observers that
|
|
1515
|
+
* called `setFilters` receive only the categories they subscribed to.
|
|
1516
|
+
* Observers with no filters still receive it either way, so tagging is safe
|
|
1517
|
+
* to adopt without coordinating with them.
|
|
1358
1518
|
*/
|
|
1359
|
-
emit(category, payload, severity = "info") {
|
|
1519
|
+
emit(category, payload, severity = "info", opts) {
|
|
1360
1520
|
const envelope = createEventEnvelope(category, this._emittedBy, payload, severity);
|
|
1361
|
-
this._room.publishEvent(envelope);
|
|
1521
|
+
this._room.publishEvent(envelope, opts);
|
|
1522
|
+
}
|
|
1523
|
+
/**
|
|
1524
|
+
* Replace the observer's server-side event filters.
|
|
1525
|
+
*
|
|
1526
|
+
* Scoped to the events topic, so it never disturbs the room's other
|
|
1527
|
+
* subscriptions — notably `inbox`, whose messages are published unfiltered
|
|
1528
|
+
* and would stop arriving if this were applied room-wide.
|
|
1529
|
+
*
|
|
1530
|
+
* An empty array restores the wildcard subscription, which receives every
|
|
1531
|
+
* event on the room.
|
|
1532
|
+
*/
|
|
1533
|
+
setFilters(values) {
|
|
1534
|
+
this._room.setFilters(values, { topic: "events" });
|
|
1362
1535
|
}
|
|
1363
1536
|
/**
|
|
1364
1537
|
* Listen for events, optionally filtered by category or severity.
|