stoops 0.1.0 → 0.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.
@@ -0,0 +1,968 @@
1
+ import {
2
+ EventCategory
3
+ } from "./chunk-5ADJGMXQ.js";
4
+ import {
5
+ StoopsEngagement,
6
+ buildCatchUpLines,
7
+ formatEvent
8
+ } from "./chunk-BLGV3QN4.js";
9
+ import {
10
+ createEvent
11
+ } from "./chunk-HQS7HBZR.js";
12
+
13
+ // src/agent/room-data-source.ts
14
+ var LocalRoomDataSource = class {
15
+ constructor(_room, _channel) {
16
+ this._room = _room;
17
+ this._channel = _channel;
18
+ }
19
+ get roomId() {
20
+ return this._room.roomId;
21
+ }
22
+ /** Direct access to the underlying Room (for backward compat / internal use). */
23
+ get room() {
24
+ return this._room;
25
+ }
26
+ /** Direct access to the underlying Channel (for backward compat / internal use). */
27
+ get channel() {
28
+ return this._channel;
29
+ }
30
+ listParticipants() {
31
+ return this._room.listParticipants();
32
+ }
33
+ async getMessage(id) {
34
+ return this._room.getMessage(id);
35
+ }
36
+ async searchMessages(query, limit = 10, cursor = null) {
37
+ return this._room.searchMessages(query, limit, cursor);
38
+ }
39
+ async getMessages(limit = 30, cursor = null) {
40
+ return this._room.listMessages(limit, cursor);
41
+ }
42
+ async getEvents(category = null, limit = 50, cursor = null) {
43
+ return this._room.listEvents(category, limit, cursor);
44
+ }
45
+ async sendMessage(content, replyToId, image) {
46
+ return this._channel.sendMessage(content, replyToId, image ?? void 0);
47
+ }
48
+ async emitEvent(event) {
49
+ await this._channel.emit(event);
50
+ }
51
+ };
52
+
53
+ // src/agent/multiplexer.ts
54
+ var EventMultiplexer = class {
55
+ _queue = [];
56
+ _waiters = [];
57
+ _channels = /* @__PURE__ */ new Map();
58
+ _closed = false;
59
+ _closeResolve = null;
60
+ addChannel(roomId, roomName, channel) {
61
+ if (this._channels.has(roomId) || this._closed) return;
62
+ const abortController = new AbortController();
63
+ const loopPromise = this._listenLoop(roomId, roomName, channel, abortController.signal);
64
+ this._channels.set(roomId, { channel, roomName, abortController, loopPromise });
65
+ }
66
+ removeChannel(roomId) {
67
+ const entry = this._channels.get(roomId);
68
+ if (!entry) return;
69
+ entry.abortController.abort();
70
+ this._channels.delete(roomId);
71
+ }
72
+ close() {
73
+ this._closed = true;
74
+ for (const [, entry] of this._channels) {
75
+ entry.abortController.abort();
76
+ }
77
+ this._channels.clear();
78
+ if (this._closeResolve) {
79
+ this._closeResolve();
80
+ this._closeResolve = null;
81
+ }
82
+ for (const waiter of this._waiters) {
83
+ waiter.resolve(null);
84
+ }
85
+ this._waiters = [];
86
+ }
87
+ async _listenLoop(roomId, roomName, channel, signal) {
88
+ try {
89
+ for await (const event of channel) {
90
+ if (signal.aborted) break;
91
+ this._push({ roomId, roomName, event });
92
+ }
93
+ } catch {
94
+ }
95
+ }
96
+ _push(labeled) {
97
+ if (this._closed) return;
98
+ if (this._waiters.length > 0) {
99
+ const waiter = this._waiters.shift();
100
+ waiter.resolve(labeled);
101
+ } else {
102
+ this._queue.push(labeled);
103
+ }
104
+ }
105
+ [Symbol.asyncIterator]() {
106
+ return {
107
+ next: () => {
108
+ if (this._queue.length > 0) {
109
+ return Promise.resolve({ value: this._queue.shift(), done: false });
110
+ }
111
+ if (this._closed) {
112
+ return Promise.resolve({ value: void 0, done: true });
113
+ }
114
+ return new Promise((resolve) => {
115
+ this._waiters.push({
116
+ resolve: (value) => {
117
+ if (this._closed || value === null) {
118
+ resolve({ value: void 0, done: true });
119
+ } else {
120
+ resolve({ value, done: false });
121
+ }
122
+ }
123
+ });
124
+ });
125
+ }
126
+ };
127
+ }
128
+ };
129
+
130
+ // src/agent/ref-map.ts
131
+ var RefMap = class {
132
+ _counter = Math.floor(Math.random() * 1e4);
133
+ _refToId = /* @__PURE__ */ new Map();
134
+ _idToRef = /* @__PURE__ */ new Map();
135
+ /** Assign a short ref to a message ID. Returns existing ref if already assigned. */
136
+ assign(messageId) {
137
+ const existing = this._idToRef.get(messageId);
138
+ if (existing) return existing;
139
+ const ref = String(this._counter * 6337 % 1e4).padStart(4, "0");
140
+ this._counter++;
141
+ if (this._refToId.has(ref)) {
142
+ const hex = messageId.replace(/-/g, "");
143
+ let fallback = null;
144
+ for (let i = 0; i <= hex.length - 4; i++) {
145
+ const candidate = hex.slice(i, i + 4);
146
+ if (!this._refToId.has(candidate)) {
147
+ fallback = candidate;
148
+ break;
149
+ }
150
+ }
151
+ if (!fallback) fallback = messageId.slice(0, 8);
152
+ this._refToId.set(fallback, messageId);
153
+ this._idToRef.set(messageId, fallback);
154
+ return fallback;
155
+ }
156
+ this._refToId.set(ref, messageId);
157
+ this._idToRef.set(messageId, ref);
158
+ return ref;
159
+ }
160
+ /** Resolve a ref back to the full message UUID. Returns undefined if unknown. */
161
+ resolve(ref) {
162
+ return this._refToId.get(ref);
163
+ }
164
+ /** Clear all mappings and reset the counter. Called on context compaction. */
165
+ clear() {
166
+ this._refToId.clear();
167
+ this._idToRef.clear();
168
+ this._counter = Math.floor(Math.random() * 1e4);
169
+ }
170
+ };
171
+
172
+ // src/agent/connection-registry.ts
173
+ var ConnectionRegistry = class {
174
+ _connections = /* @__PURE__ */ new Map();
175
+ _nameToId = /* @__PURE__ */ new Map();
176
+ _identifierToId = /* @__PURE__ */ new Map();
177
+ _lastMessages = /* @__PURE__ */ new Map();
178
+ add(roomId, conn) {
179
+ this._connections.set(roomId, conn);
180
+ this._nameToId.set(conn.name, roomId);
181
+ if (conn.identifier) this._identifierToId.set(conn.identifier, roomId);
182
+ }
183
+ remove(roomId) {
184
+ const conn = this._connections.get(roomId);
185
+ if (!conn) return void 0;
186
+ this._nameToId.delete(conn.name);
187
+ if (conn.identifier) this._identifierToId.delete(conn.identifier);
188
+ this._connections.delete(roomId);
189
+ this._lastMessages.delete(roomId);
190
+ return conn;
191
+ }
192
+ get(roomId) {
193
+ return this._connections.get(roomId);
194
+ }
195
+ has(roomId) {
196
+ return this._connections.has(roomId);
197
+ }
198
+ resolve(roomName) {
199
+ const roomId = this._nameToId.get(roomName);
200
+ if (roomId) return this._connections.get(roomId) ?? null;
201
+ const idFromIdentifier = this._identifierToId.get(roomName);
202
+ if (idFromIdentifier) return this._connections.get(idFromIdentifier) ?? null;
203
+ return this._connections.get(roomName) ?? null;
204
+ }
205
+ listAll(getModeForRoom) {
206
+ return [...this._connections.entries()].map(([roomId, conn]) => ({
207
+ name: conn.name,
208
+ roomId,
209
+ ...conn.identifier ? { identifier: conn.identifier } : {},
210
+ mode: getModeForRoom(roomId),
211
+ participantCount: conn.dataSource.listParticipants().length,
212
+ ...this._lastMessages.has(roomId) ? { lastMessage: this._lastMessages.get(roomId) } : {}
213
+ }));
214
+ }
215
+ setLastMessage(roomId, text) {
216
+ this._lastMessages.set(roomId, text);
217
+ }
218
+ entries() {
219
+ return this._connections.entries();
220
+ }
221
+ get size() {
222
+ return this._connections.size;
223
+ }
224
+ clear() {
225
+ this._connections.clear();
226
+ this._nameToId.clear();
227
+ this._identifierToId.clear();
228
+ this._lastMessages.clear();
229
+ }
230
+ values() {
231
+ return this._connections.values();
232
+ }
233
+ };
234
+
235
+ // src/agent/content-buffer.ts
236
+ var ContentBuffer = class {
237
+ _buffer = /* @__PURE__ */ new Map();
238
+ push(roomId, item) {
239
+ const buf = this._buffer.get(roomId) ?? [];
240
+ buf.push(item);
241
+ this._buffer.set(roomId, buf);
242
+ }
243
+ flush(roomId) {
244
+ const items = this._buffer.get(roomId) ?? [];
245
+ this._buffer.delete(roomId);
246
+ return items;
247
+ }
248
+ delete(roomId) {
249
+ this._buffer.delete(roomId);
250
+ }
251
+ clear() {
252
+ this._buffer.clear();
253
+ }
254
+ };
255
+
256
+ // src/agent/event-tracker.ts
257
+ var EventTracker = class {
258
+ _processedIds = /* @__PURE__ */ new Set();
259
+ _deliveredIds = /* @__PURE__ */ new Set();
260
+ /** Returns true if this event was already processed (and adds it if not). */
261
+ isDuplicate(id) {
262
+ if (this._processedIds.has(id)) return true;
263
+ this._processedIds.add(id);
264
+ if (this._processedIds.size > 500) {
265
+ const arr = [...this._processedIds];
266
+ this._processedIds = new Set(arr.slice(arr.length >> 1));
267
+ }
268
+ return false;
269
+ }
270
+ isDelivered(id) {
271
+ return this._deliveredIds.has(id);
272
+ }
273
+ markDelivered(id) {
274
+ this._deliveredIds.add(id);
275
+ }
276
+ markManyDelivered(ids) {
277
+ for (const id of ids) {
278
+ this._deliveredIds.add(id);
279
+ this._processedIds.add(id);
280
+ }
281
+ }
282
+ clearDelivered() {
283
+ this._deliveredIds.clear();
284
+ }
285
+ clearAll() {
286
+ this._processedIds.clear();
287
+ this._deliveredIds.clear();
288
+ }
289
+ };
290
+
291
+ // src/agent/event-processor.ts
292
+ function mergeParts(arrays) {
293
+ const result = [];
294
+ for (let i = 0; i < arrays.length; i++) {
295
+ if (i > 0) result.push({ type: "text", text: "\n" });
296
+ result.push(...arrays[i]);
297
+ }
298
+ return result;
299
+ }
300
+ var EventProcessor = class {
301
+ _participantId;
302
+ _participantName;
303
+ _options;
304
+ _engagement;
305
+ _deliver = null;
306
+ _multiplexer = new EventMultiplexer();
307
+ _registry = new ConnectionRegistry();
308
+ _buffer = new ContentBuffer();
309
+ _tracker = new EventTracker();
310
+ _processing = false;
311
+ _eventQueue = [];
312
+ _stopped = false;
313
+ _currentContextRoomId = null;
314
+ _log = [];
315
+ _refMap = new RefMap();
316
+ _injectBuffer = [];
317
+ /** Per-room participant IDs (for multi-server CLI agents where each server assigns a different ID). */
318
+ _roomSelfIds = /* @__PURE__ */ new Map();
319
+ constructor(participantId, participantName, options = {}) {
320
+ this._participantId = participantId;
321
+ this._participantName = participantName;
322
+ this._options = options;
323
+ this._engagement = options.engagement ?? new StoopsEngagement(options.defaultMode ?? "everyone", options.personParticipantId);
324
+ }
325
+ // ── Public accessors ────────────────────────────────────────────────────────
326
+ get participantId() {
327
+ return this._participantId;
328
+ }
329
+ set participantId(id) {
330
+ this._participantId = id;
331
+ }
332
+ get participantName() {
333
+ return this._participantName;
334
+ }
335
+ get currentContextRoomId() {
336
+ return this._currentContextRoomId;
337
+ }
338
+ /** Set a room-specific participant ID (for multi-server CLI agents). */
339
+ setRoomParticipantId(roomId, participantId) {
340
+ this._roomSelfIds.set(roomId, participantId);
341
+ }
342
+ /** Get the effective selfId for a room — room-specific if set, otherwise the global one. */
343
+ getSelfIdForRoom(roomId) {
344
+ return this._roomSelfIds.get(roomId) ?? this._participantId;
345
+ }
346
+ // ── Ref map (consumer calls these for MCP tools) ────────────────────────────
347
+ assignRef(messageId) {
348
+ return this._refMap.assign(messageId);
349
+ }
350
+ resolveRef(ref) {
351
+ return this._refMap.resolve(ref);
352
+ }
353
+ // ── Seen-event cache (consumer calls these for catch_up MCP tool) ───────────
354
+ isEventSeen(eventId) {
355
+ return this._tracker.isDelivered(eventId);
356
+ }
357
+ markEventsSeen(eventIds) {
358
+ this._tracker.markManyDelivered(eventIds);
359
+ }
360
+ // ── Inject buffer (LangGraph mid-loop event injection) ──────────────────────
361
+ drainInjectBuffer() {
362
+ if (this._injectBuffer.length === 0) return null;
363
+ const drained = this._injectBuffer;
364
+ this._injectBuffer = [];
365
+ return drained;
366
+ }
367
+ // ── Consumer hooks (called by consumer during/after delivery) ────────────────
368
+ /**
369
+ * Called by the consumer when context was compacted.
370
+ * Clears seen-event cache and ref map so catch_up returns full history.
371
+ */
372
+ onContextCompacted() {
373
+ this._tracker.clearDelivered();
374
+ this._refMap.clear();
375
+ }
376
+ /**
377
+ * Called by the consumer when a tool call starts or completes.
378
+ * Routes ToolUseEvent to the room that triggered the current evaluation.
379
+ */
380
+ emitToolUse(toolName, status) {
381
+ if (this._currentContextRoomId) {
382
+ const conn = this._registry.get(this._currentContextRoomId);
383
+ if (conn) {
384
+ const event = createEvent({
385
+ type: "ToolUse",
386
+ category: "ACTIVITY",
387
+ room_id: this._currentContextRoomId,
388
+ participant_id: this._participantId,
389
+ tool_name: toolName,
390
+ status
391
+ });
392
+ const emitter = conn.dataSource.emitEvent ? (e) => conn.dataSource.emitEvent(e) : conn.channel ? (e) => conn.channel.emit(e) : null;
393
+ if (!emitter) return;
394
+ emitter(event).catch(() => {
395
+ });
396
+ }
397
+ }
398
+ }
399
+ // ── RoomResolver implementation ─────────────────────────────────────────────
400
+ resolve(roomName) {
401
+ return this._registry.resolve(roomName);
402
+ }
403
+ listAll() {
404
+ return this._registry.listAll((roomId) => this.getModeForRoom(roomId));
405
+ }
406
+ // ── Room connection management ──────────────────────────────────────────────
407
+ async connectRoom(room, roomName, mode, identifier) {
408
+ if (this._registry.has(room.roomId)) return;
409
+ const channel = await room.connect(this._participantId, this._participantName, {
410
+ type: "agent",
411
+ identifier: this._options.selfIdentifier,
412
+ subscribe: /* @__PURE__ */ new Set([
413
+ EventCategory.MESSAGE,
414
+ EventCategory.PRESENCE,
415
+ EventCategory.ACTIVITY,
416
+ EventCategory.MENTION
417
+ ]),
418
+ silent: true
419
+ });
420
+ const dataSource = new LocalRoomDataSource(room, channel);
421
+ const conn = { dataSource, room, channel, name: roomName, identifier };
422
+ this._registry.add(room.roomId, conn);
423
+ if (mode) this._engagement.setMode?.(room.roomId, mode);
424
+ const initialMode = this.getModeForRoom(room.roomId);
425
+ channel.emit(createEvent({
426
+ type: "Activity",
427
+ category: "ACTIVITY",
428
+ room_id: room.roomId,
429
+ participant_id: this._participantId,
430
+ action: "mode_changed",
431
+ detail: { mode: initialMode }
432
+ })).catch(() => {
433
+ });
434
+ this._multiplexer.addChannel(room.roomId, roomName, channel);
435
+ }
436
+ /**
437
+ * Connect a remote room via a RoomDataSource (no local Room/Channel).
438
+ *
439
+ * Used by the client-side agent runtime to register rooms that are
440
+ * accessed over HTTP. Events come from an external source (SSE multiplexer)
441
+ * passed to run(), not from the internal EventMultiplexer.
442
+ */
443
+ connectRemoteRoom(dataSource, roomName, mode, identifier) {
444
+ if (this._registry.has(dataSource.roomId)) return;
445
+ const conn = {
446
+ dataSource,
447
+ name: roomName,
448
+ identifier
449
+ };
450
+ this._registry.add(dataSource.roomId, conn);
451
+ if (mode) this._engagement.setMode?.(dataSource.roomId, mode);
452
+ }
453
+ /** Disconnect a remote room (by room ID). */
454
+ disconnectRemoteRoom(roomId) {
455
+ if (!this._registry.has(roomId)) return;
456
+ this._registry.remove(roomId);
457
+ this._engagement.onRoomDisconnected?.(roomId);
458
+ this._buffer.delete(roomId);
459
+ this._roomSelfIds.delete(roomId);
460
+ }
461
+ async disconnectRoom(roomId) {
462
+ const conn = this._registry.get(roomId);
463
+ if (!conn) return;
464
+ this._multiplexer.removeChannel(roomId);
465
+ await conn.channel?.disconnect(true);
466
+ this._registry.remove(roomId);
467
+ this._engagement.onRoomDisconnected?.(roomId);
468
+ this._buffer.delete(roomId);
469
+ this._roomSelfIds.delete(roomId);
470
+ }
471
+ // ── Mode management ─────────────────────────────────────────────────────────
472
+ getModeForRoom(roomId) {
473
+ return this._engagement.getMode?.(roomId) ?? "everyone";
474
+ }
475
+ setModeForRoom(roomId, mode, notifyAgent = true) {
476
+ this._engagement.setMode?.(roomId, mode);
477
+ const conn = this._registry.get(roomId);
478
+ if (conn) {
479
+ conn.channel?.emit(createEvent({
480
+ type: "Activity",
481
+ category: "ACTIVITY",
482
+ room_id: roomId,
483
+ participant_id: this._participantId,
484
+ action: "mode_changed",
485
+ detail: { mode }
486
+ }))?.catch(() => {
487
+ });
488
+ this._options.onModeChange?.(roomId, conn.name, mode);
489
+ }
490
+ }
491
+ // ── Log ─────────────────────────────────────────────────────────────────────
492
+ getLog() {
493
+ return this._log;
494
+ }
495
+ // ── Main event loop ─────────────────────────────────────────────────────────
496
+ /**
497
+ * Start the event loop.
498
+ *
499
+ * @param deliver — callback that receives formatted content and delivers
500
+ * it to the agent. This is the consumer's responsibility. The function
501
+ * should block until delivery is complete (e.g., LLM evaluation finished).
502
+ * @param eventSource — optional external event source (e.g. SseMultiplexer).
503
+ * If provided, events are consumed from this instead of the internal
504
+ * EventMultiplexer. Used by the client-side agent runtime.
505
+ * @param initialParts — optional content to deliver before entering the
506
+ * event loop. Used by the runtime to deliver auto-join confirmation.
507
+ */
508
+ async run(deliver, eventSource, initialParts) {
509
+ this._deliver = deliver;
510
+ if (initialParts && initialParts.length > 0) {
511
+ await this._processRaw(
512
+ initialParts,
513
+ this._registry.values().next().value?.dataSource.roomId ?? null
514
+ );
515
+ }
516
+ const source = eventSource ?? this._multiplexer;
517
+ for await (const labeled of source) {
518
+ if (this._stopped) break;
519
+ await this._handleLabeledEvent(labeled);
520
+ }
521
+ }
522
+ async stop() {
523
+ this._stopped = true;
524
+ this._multiplexer.close();
525
+ await Promise.allSettled(
526
+ [...this._registry.values()].filter((conn) => conn.channel).map((conn) => conn.channel.disconnect(true))
527
+ );
528
+ this._registry.clear();
529
+ this._buffer.clear();
530
+ this._tracker.clearAll();
531
+ this._refMap.clear();
532
+ this._roomSelfIds.clear();
533
+ this._deliver = null;
534
+ }
535
+ // ── Full catch-up (kept for app-path consumers) ────────────────────────────
536
+ async buildFullCatchUp() {
537
+ return this._buildFullCatchUp();
538
+ }
539
+ async _buildFullCatchUp() {
540
+ const sections = ["[Session context \u2014 loaded automatically]"];
541
+ const nameCounts = /* @__PURE__ */ new Map();
542
+ for (const [, c] of this._registry.entries()) nameCounts.set(c.name, (nameCounts.get(c.name) ?? 0) + 1);
543
+ for (const [roomId, conn] of this._registry.entries()) {
544
+ const mode = this.getModeForRoom(roomId);
545
+ const isDuplicate = (nameCounts.get(conn.name) ?? 0) > 1;
546
+ const ref = isDuplicate ? roomId : conn.identifier ?? roomId;
547
+ sections.push(`
548
+ ${conn.name} [${ref}] \u2014 ${mode}`);
549
+ if (mode.startsWith("standby-")) {
550
+ sections.push(" (standby \u2014 @mentions only)");
551
+ } else {
552
+ const participants = conn.dataSource.listParticipants().filter((p) => p.id !== this._participantId);
553
+ if (participants.length > 0) {
554
+ const pList = participants.map((p) => `${p.type} ${p.name}`).join(", ");
555
+ sections.push(`Participants: ${pList}`);
556
+ }
557
+ const lines = await buildCatchUpLines(conn, {
558
+ isEventSeen: (id) => this._tracker.isDelivered(id),
559
+ markEventsSeen: (ids) => {
560
+ this._tracker.markManyDelivered(ids);
561
+ },
562
+ assignRef: (id) => this.assignRef(id)
563
+ });
564
+ if (lines.length > 0) {
565
+ for (const line of lines) sections.push(` ${line}`);
566
+ } else {
567
+ sections.push(" (nothing new)");
568
+ }
569
+ }
570
+ }
571
+ sections.push(
572
+ "\n\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500",
573
+ "Continue immediately if you see fit, or explore further in any active room."
574
+ );
575
+ return [{ type: "text", text: sections.join("\n") }];
576
+ }
577
+ // ── Event handling ──────────────────────────────────────────────────────────
578
+ async _handleLabeledEvent(labeled) {
579
+ if (this._tracker.isDuplicate(labeled.event.id)) return;
580
+ const { roomId, event } = labeled;
581
+ const conn = this._registry.get(roomId);
582
+ const senderLookupId = event.type === "Mentioned" ? event.message.sender_id : event.participant_id;
583
+ const sender = conn?.dataSource.listParticipants().find((p) => p.id === senderLookupId);
584
+ const senderType = sender?.type ?? "human";
585
+ const selfId = this.getSelfIdForRoom(roomId);
586
+ const disposition = this._engagement.classify(event, roomId, selfId, senderType, senderLookupId);
587
+ if (disposition === "drop") return;
588
+ this._tracker.markDelivered(event.id);
589
+ if (disposition === "content") {
590
+ this._buffer.push(roomId, { event, roomId, roomName: labeled.roomName });
591
+ return;
592
+ }
593
+ this._log.push(event);
594
+ if (this._processing) {
595
+ this._eventQueue.push(labeled);
596
+ this._formatForLLM(event, roomId, labeled.roomName).then((parts) => {
597
+ if (parts) this._injectBuffer.push(parts);
598
+ }).catch((err) => {
599
+ console.error(`[${this._participantName}] inject buffer format error:`, err);
600
+ });
601
+ return;
602
+ }
603
+ await this._processTrigger(labeled);
604
+ while (this._eventQueue.length > 0) {
605
+ const queued = this._eventQueue;
606
+ this._eventQueue = [];
607
+ const formatted = [];
608
+ let batchContextRoom = null;
609
+ const roomsProcessed = /* @__PURE__ */ new Set();
610
+ for (const qe of queued) {
611
+ const qConn = this._registry.get(qe.roomId);
612
+ const qSenderLookupId = qe.event.type === "Mentioned" ? qe.event.message.sender_id : qe.event.participant_id;
613
+ const qSender = qConn?.dataSource.listParticipants().find((p) => p.id === qSenderLookupId);
614
+ const qSenderType = qSender?.type ?? "human";
615
+ const qSelfId = this.getSelfIdForRoom(qe.roomId);
616
+ const qDisposition = this._engagement.classify(qe.event, qe.roomId, qSelfId, qSenderType, qSenderLookupId);
617
+ if (qDisposition === "drop") continue;
618
+ if (!roomsProcessed.has(qe.roomId)) {
619
+ roomsProcessed.add(qe.roomId);
620
+ const buffered = this._buffer.flush(qe.roomId);
621
+ for (const item of buffered) {
622
+ const parts2 = await this._formatForLLM(item.event, item.roomId, item.roomName);
623
+ if (parts2) {
624
+ formatted.push(parts2);
625
+ if (!batchContextRoom) batchContextRoom = qe.roomId;
626
+ }
627
+ }
628
+ }
629
+ this._log.push(qe.event);
630
+ const parts = await this._formatForLLM(qe.event, qe.roomId, qe.roomName);
631
+ if (parts) {
632
+ formatted.push(parts);
633
+ if (!batchContextRoom) batchContextRoom = qe.roomId;
634
+ }
635
+ }
636
+ if (formatted.length > 0) {
637
+ await this._processRaw(mergeParts(formatted), batchContextRoom);
638
+ }
639
+ }
640
+ }
641
+ async _processTrigger(labeled) {
642
+ const { roomId, roomName, event } = labeled;
643
+ const buffered = this._buffer.flush(roomId);
644
+ const contentPartArrays = [];
645
+ for (const item of buffered) {
646
+ const parts = await this._formatForLLM(item.event, item.roomId, item.roomName);
647
+ if (parts) contentPartArrays.push(parts);
648
+ }
649
+ if (event.type === "MessageSent") {
650
+ const conn = this._registry.get(roomId);
651
+ const senderLabel = conn?.dataSource.listParticipants().find((p) => p.id === event.message.sender_id)?.name ?? event.message.sender_name;
652
+ const contentPreview = event.message.content.length > 60 ? event.message.content.slice(0, 57) + "..." : event.message.content;
653
+ const preview = event.message.image_url && !event.message.content.trim() ? "sent an image" : contentPreview;
654
+ this._registry.setLastMessage(roomId, `${senderLabel}: ${preview}`);
655
+ }
656
+ const triggerParts = await this._formatForLLM(event, roomId, roomName);
657
+ if (!triggerParts && contentPartArrays.length === 0) return;
658
+ const mergedParts = mergeParts([...contentPartArrays, ...triggerParts ? [triggerParts] : []]);
659
+ await this._processRaw(mergedParts, roomId);
660
+ }
661
+ // ── Formatting ──────────────────────────────────────────────────────────────
662
+ _resolveParticipantForRoom(roomId) {
663
+ return (id) => {
664
+ const conn = this._registry.get(roomId);
665
+ if (!conn) return null;
666
+ return conn.dataSource.listParticipants().find((p) => p.id === id) ?? null;
667
+ };
668
+ }
669
+ async _resolveReplyContext(event, roomId) {
670
+ if (event.type !== "MessageSent" && event.type !== "Mentioned") return null;
671
+ const msg = event.message;
672
+ if (!msg.reply_to_id) return null;
673
+ const conn = this._registry.get(roomId);
674
+ if (!conn) return null;
675
+ const repliedTo = await conn.dataSource.getMessage(msg.reply_to_id);
676
+ return repliedTo ? { senderName: repliedTo.sender_name, content: repliedTo.content } : null;
677
+ }
678
+ async _resolveReactionTarget(event, roomId) {
679
+ if (event.type !== "ReactionAdded") return null;
680
+ const conn = this._registry.get(roomId);
681
+ if (!conn) return null;
682
+ const target = await conn.dataSource.getMessage(event.message_id);
683
+ if (!target) return null;
684
+ return {
685
+ senderName: target.sender_name,
686
+ content: target.content,
687
+ isSelf: target.sender_id === this._participantId
688
+ };
689
+ }
690
+ async _formatForLLM(event, roomId, roomName) {
691
+ const mode = this.getModeForRoom(roomId);
692
+ const label = mode !== "everyone" ? `${roomName} \u2014 ${mode}` : roomName;
693
+ const replyContext = await this._resolveReplyContext(event, roomId);
694
+ const reactionTarget = await this._resolveReactionTarget(event, roomId);
695
+ return formatEvent(
696
+ event,
697
+ this._resolveParticipantForRoom(roomId),
698
+ replyContext,
699
+ label,
700
+ reactionTarget,
701
+ (id) => this.assignRef(id)
702
+ );
703
+ }
704
+ // ── Delivery ────────────────────────────────────────────────────────────────
705
+ async _processRaw(parts, contextRoomId) {
706
+ if (this._options.preQuery && !await this._options.preQuery()) {
707
+ return;
708
+ }
709
+ this._processing = true;
710
+ this._currentContextRoomId = contextRoomId;
711
+ try {
712
+ if (!this._deliver) return;
713
+ await this._deliver(parts);
714
+ } catch (err) {
715
+ console.error(`[${this._participantName}] error:`, err);
716
+ } finally {
717
+ this._currentContextRoomId = null;
718
+ this._processing = false;
719
+ }
720
+ }
721
+ };
722
+
723
+ // src/agent/remote-room-data-source.ts
724
+ var RemoteRoomDataSource = class {
725
+ constructor(_serverUrl, _sessionToken, _roomId) {
726
+ this._serverUrl = _serverUrl;
727
+ this._sessionToken = _sessionToken;
728
+ this._roomId = _roomId;
729
+ }
730
+ _participants = [];
731
+ _selfId = "";
732
+ _selfName = "";
733
+ /** Set own identity for populating outgoing message stubs. */
734
+ setSelf(id, name) {
735
+ this._selfId = id;
736
+ this._selfName = name;
737
+ }
738
+ get roomId() {
739
+ return this._roomId;
740
+ }
741
+ get serverUrl() {
742
+ return this._serverUrl;
743
+ }
744
+ get sessionToken() {
745
+ return this._sessionToken;
746
+ }
747
+ // ── Participant cache ─────────────────────────────────────────────────────
748
+ /** Set the initial participant list (from join response). */
749
+ setParticipants(participants) {
750
+ this._participants = [...participants];
751
+ }
752
+ /** Add a participant (on ParticipantJoined event). */
753
+ addParticipant(participant) {
754
+ this._participants = this._participants.filter((p) => p.id !== participant.id);
755
+ this._participants.push(participant);
756
+ }
757
+ /** Remove a participant (on ParticipantLeft event). */
758
+ removeParticipant(participantId) {
759
+ this._participants = this._participants.filter((p) => p.id !== participantId);
760
+ }
761
+ listParticipants() {
762
+ return [...this._participants];
763
+ }
764
+ // ── HTTP-backed data access ───────────────────────────────────────────────
765
+ async getMessage(id) {
766
+ try {
767
+ const res = await fetch(
768
+ `${this._serverUrl}/message/${encodeURIComponent(id)}?token=${this._sessionToken}`
769
+ );
770
+ if (!res.ok) return null;
771
+ const data = await res.json();
772
+ return data.message;
773
+ } catch {
774
+ return null;
775
+ }
776
+ }
777
+ async searchMessages(query, limit = 10, cursor = null) {
778
+ const params = new URLSearchParams({ token: this._sessionToken, query, count: String(limit) });
779
+ if (cursor) params.set("cursor", cursor);
780
+ try {
781
+ const res = await fetch(`${this._serverUrl}/search?${params}`);
782
+ if (!res.ok) return { items: [], has_more: false, next_cursor: null };
783
+ return await res.json();
784
+ } catch {
785
+ return { items: [], has_more: false, next_cursor: null };
786
+ }
787
+ }
788
+ async getMessages(limit = 30, cursor = null) {
789
+ const params = new URLSearchParams({ token: this._sessionToken, count: String(limit) });
790
+ if (cursor) params.set("cursor", cursor);
791
+ try {
792
+ const res = await fetch(`${this._serverUrl}/messages?${params}`);
793
+ if (!res.ok) return { items: [], has_more: false, next_cursor: null };
794
+ return await res.json();
795
+ } catch {
796
+ return { items: [], has_more: false, next_cursor: null };
797
+ }
798
+ }
799
+ async getEvents(category = null, limit = 50, cursor = null) {
800
+ const params = new URLSearchParams({ token: this._sessionToken, count: String(limit) });
801
+ if (category) params.set("category", category);
802
+ if (cursor) params.set("cursor", cursor);
803
+ try {
804
+ const res = await fetch(`${this._serverUrl}/events/history?${params}`);
805
+ if (!res.ok) return { items: [], has_more: false, next_cursor: null };
806
+ return await res.json();
807
+ } catch {
808
+ return { items: [], has_more: false, next_cursor: null };
809
+ }
810
+ }
811
+ async sendMessage(content, replyToId, image) {
812
+ const body = { token: this._sessionToken, content };
813
+ if (replyToId) body.replyTo = replyToId;
814
+ if (image) body.image = image;
815
+ const res = await fetch(`${this._serverUrl}/message`, {
816
+ method: "POST",
817
+ headers: { "Content-Type": "application/json" },
818
+ body: JSON.stringify(body)
819
+ });
820
+ if (!res.ok) {
821
+ const err = await res.text();
822
+ throw new Error(`Failed to send message: ${err}`);
823
+ }
824
+ const data = await res.json();
825
+ return {
826
+ id: data.messageId,
827
+ room_id: this._roomId,
828
+ sender_id: this._selfId,
829
+ sender_name: this._selfName,
830
+ content,
831
+ timestamp: /* @__PURE__ */ new Date()
832
+ };
833
+ }
834
+ async emitEvent(event) {
835
+ const res = await fetch(`${this._serverUrl}/event`, {
836
+ method: "POST",
837
+ headers: { "Content-Type": "application/json" },
838
+ body: JSON.stringify({ token: this._sessionToken, event })
839
+ });
840
+ if (!res.ok) {
841
+ }
842
+ }
843
+ };
844
+
845
+ // src/agent/sse-multiplexer.ts
846
+ var SseMultiplexer = class {
847
+ _queue = [];
848
+ _waiters = [];
849
+ _connections = /* @__PURE__ */ new Map();
850
+ _closed = false;
851
+ /**
852
+ * Add an SSE connection to a stoop server.
853
+ * Starts streaming events immediately.
854
+ */
855
+ addConnection(serverUrl, sessionToken, roomName, roomId) {
856
+ if (this._connections.has(roomId) || this._closed) return;
857
+ const abortController = new AbortController();
858
+ const loopPromise = this._sseLoop(serverUrl, sessionToken, roomName, roomId, abortController.signal);
859
+ this._connections.set(roomId, { serverUrl, sessionToken, roomName, roomId, abortController, loopPromise });
860
+ }
861
+ /** Remove a connection by room ID. */
862
+ removeConnection(roomId) {
863
+ const entry = this._connections.get(roomId);
864
+ if (!entry) return;
865
+ entry.abortController.abort();
866
+ this._connections.delete(roomId);
867
+ }
868
+ /** Close all connections and signal the iterator to finish. */
869
+ close() {
870
+ this._closed = true;
871
+ for (const [, entry] of this._connections) {
872
+ entry.abortController.abort();
873
+ }
874
+ this._connections.clear();
875
+ for (const waiter of this._waiters) {
876
+ waiter.resolve(null);
877
+ }
878
+ this._waiters = [];
879
+ }
880
+ async _sseLoop(serverUrl, sessionToken, roomName, roomId, signal) {
881
+ const INITIAL_BACKOFF = 1e3;
882
+ const MAX_BACKOFF = 3e4;
883
+ let backoff = INITIAL_BACKOFF;
884
+ while (!signal.aborted && !this._closed) {
885
+ try {
886
+ const res = await fetch(`${serverUrl}/events`, {
887
+ method: "POST",
888
+ headers: {
889
+ Accept: "text/event-stream",
890
+ Authorization: `Bearer ${sessionToken}`
891
+ },
892
+ signal
893
+ });
894
+ if (!res.ok || !res.body) {
895
+ throw new Error(`SSE connect failed: ${res.status}`);
896
+ }
897
+ backoff = INITIAL_BACKOFF;
898
+ const reader = res.body.getReader();
899
+ const decoder = new TextDecoder();
900
+ let buffer = "";
901
+ while (!signal.aborted) {
902
+ const { done, value } = await reader.read();
903
+ if (done) break;
904
+ buffer += decoder.decode(value, { stream: true });
905
+ const parts = buffer.split("\n\n");
906
+ buffer = parts.pop();
907
+ for (const part of parts) {
908
+ const dataLine = part.split("\n").find((l) => l.startsWith("data: "));
909
+ if (!dataLine) continue;
910
+ try {
911
+ const event = JSON.parse(dataLine.slice(6));
912
+ this._push({ roomId, roomName, event });
913
+ } catch {
914
+ }
915
+ }
916
+ }
917
+ } catch (err) {
918
+ if (signal.aborted) break;
919
+ }
920
+ if (!signal.aborted && !this._closed) {
921
+ await new Promise((r) => setTimeout(r, backoff));
922
+ backoff = Math.min(backoff * 2, MAX_BACKOFF);
923
+ }
924
+ }
925
+ }
926
+ _push(labeled) {
927
+ if (this._closed) return;
928
+ if (this._waiters.length > 0) {
929
+ const waiter = this._waiters.shift();
930
+ waiter.resolve(labeled);
931
+ } else {
932
+ this._queue.push(labeled);
933
+ }
934
+ }
935
+ [Symbol.asyncIterator]() {
936
+ return {
937
+ next: () => {
938
+ if (this._queue.length > 0) {
939
+ return Promise.resolve({ value: this._queue.shift(), done: false });
940
+ }
941
+ if (this._closed) {
942
+ return Promise.resolve({ value: void 0, done: true });
943
+ }
944
+ return new Promise((resolve) => {
945
+ this._waiters.push({
946
+ resolve: (value) => {
947
+ if (this._closed || value === null) {
948
+ resolve({ value: void 0, done: true });
949
+ } else {
950
+ resolve({ value, done: false });
951
+ }
952
+ }
953
+ });
954
+ });
955
+ }
956
+ };
957
+ }
958
+ };
959
+
960
+ export {
961
+ LocalRoomDataSource,
962
+ EventMultiplexer,
963
+ RefMap,
964
+ EventProcessor,
965
+ RemoteRoomDataSource,
966
+ SseMultiplexer
967
+ };
968
+ //# sourceMappingURL=chunk-SS5NGUJM.js.map