@gorlitzer-labs/apiary 1.13.3
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/LICENSE +21 -0
- package/README.md +328 -0
- package/dist/agent/index.d.ts +689 -0
- package/dist/agent/index.js +34 -0
- package/dist/agent/index.js.map +1 -0
- package/dist/chunk-5V7QLCGB.js +184 -0
- package/dist/chunk-5V7QLCGB.js.map +1 -0
- package/dist/chunk-AYQJIAR5.js +152 -0
- package/dist/chunk-AYQJIAR5.js.map +1 -0
- package/dist/chunk-BZ4VDWC2.js +76 -0
- package/dist/chunk-BZ4VDWC2.js.map +1 -0
- package/dist/chunk-CFY6Z6Q6.js +95 -0
- package/dist/chunk-CFY6Z6Q6.js.map +1 -0
- package/dist/chunk-CZHO4DBN.js +1719 -0
- package/dist/chunk-CZHO4DBN.js.map +1 -0
- package/dist/chunk-D7DXZQWT.js +208 -0
- package/dist/chunk-D7DXZQWT.js.map +1 -0
- package/dist/chunk-ED6ROH6B.js +2021 -0
- package/dist/chunk-ED6ROH6B.js.map +1 -0
- package/dist/chunk-PEB6HEFT.js +606 -0
- package/dist/chunk-PEB6HEFT.js.map +1 -0
- package/dist/chunk-QL66XSDB.js +157 -0
- package/dist/chunk-QL66XSDB.js.map +1 -0
- package/dist/chunk-TPNFOQA7.js +288 -0
- package/dist/chunk-TPNFOQA7.js.map +1 -0
- package/dist/chunk-ZEFRNAX6.js +1771 -0
- package/dist/chunk-ZEFRNAX6.js.map +1 -0
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.js +2432 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/index-Dp3QSGog.d.ts +837 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +22 -0
- package/dist/index.js.map +1 -0
- package/dist/invites-OFXIQLLN.js +23 -0
- package/dist/invites-OFXIQLLN.js.map +1 -0
- package/dist/join-CQXEF65B.js +15 -0
- package/dist/join-CQXEF65B.js.map +1 -0
- package/dist/room-Z7KDRVYD.js +1099 -0
- package/dist/room-Z7KDRVYD.js.map +1 -0
- package/dist/run-DOADB4T7.js +90 -0
- package/dist/run-DOADB4T7.js.map +1 -0
- package/dist/update-JGE3WCQB.js +11 -0
- package/dist/update-JGE3WCQB.js.map +1 -0
- package/package.json +72 -0
|
@@ -0,0 +1,1771 @@
|
|
|
1
|
+
import {
|
|
2
|
+
MODE_DESCRIPTIONS,
|
|
3
|
+
can,
|
|
4
|
+
formatEvent,
|
|
5
|
+
formatTimestamp,
|
|
6
|
+
messageRef
|
|
7
|
+
} from "./chunk-QL66XSDB.js";
|
|
8
|
+
import {
|
|
9
|
+
EVENT_ROLE,
|
|
10
|
+
EventCategory,
|
|
11
|
+
createEvent
|
|
12
|
+
} from "./chunk-BZ4VDWC2.js";
|
|
13
|
+
|
|
14
|
+
// src/agent/room-data-source.ts
|
|
15
|
+
var LocalRoomDataSource = class {
|
|
16
|
+
constructor(_room, _channel) {
|
|
17
|
+
this._room = _room;
|
|
18
|
+
this._channel = _channel;
|
|
19
|
+
}
|
|
20
|
+
_room;
|
|
21
|
+
_channel;
|
|
22
|
+
get roomId() {
|
|
23
|
+
return this._room.roomId;
|
|
24
|
+
}
|
|
25
|
+
get selfId() {
|
|
26
|
+
return this._channel.participantId;
|
|
27
|
+
}
|
|
28
|
+
/** Direct access to the underlying Room (for backward compat / internal use). */
|
|
29
|
+
get room() {
|
|
30
|
+
return this._room;
|
|
31
|
+
}
|
|
32
|
+
/** Direct access to the underlying Channel (for backward compat / internal use). */
|
|
33
|
+
get channel() {
|
|
34
|
+
return this._channel;
|
|
35
|
+
}
|
|
36
|
+
listParticipants() {
|
|
37
|
+
return this._room.listParticipants();
|
|
38
|
+
}
|
|
39
|
+
async getMessage(id) {
|
|
40
|
+
return this._room.getMessage(id);
|
|
41
|
+
}
|
|
42
|
+
async searchMessages(query, limit = 10, cursor = null) {
|
|
43
|
+
return this._room.searchMessages(query, limit, cursor);
|
|
44
|
+
}
|
|
45
|
+
async getMessages(limit = 30, cursor = null) {
|
|
46
|
+
return this._room.listMessages(limit, cursor);
|
|
47
|
+
}
|
|
48
|
+
async getEvents(category = null, limit = 50, cursor = null) {
|
|
49
|
+
return this._room.listEvents(category, limit, cursor);
|
|
50
|
+
}
|
|
51
|
+
async sendMessage(content, replyToId, image, attachments, recipients) {
|
|
52
|
+
return this._channel.sendMessage(content, replyToId, image ?? void 0, attachments, recipients);
|
|
53
|
+
}
|
|
54
|
+
async emitEvent(event) {
|
|
55
|
+
await this._channel.emit(event);
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
// src/agent/engagement.ts
|
|
60
|
+
var ENGAGEMENT_MODE_NAMES = [
|
|
61
|
+
"everyone",
|
|
62
|
+
"people",
|
|
63
|
+
"agents",
|
|
64
|
+
"fight",
|
|
65
|
+
"standby-everyone",
|
|
66
|
+
"standby-people",
|
|
67
|
+
"standby-agents"
|
|
68
|
+
];
|
|
69
|
+
function parseEngagementMode(value) {
|
|
70
|
+
if (!value) return void 0;
|
|
71
|
+
const lc = value.trim().toLowerCase();
|
|
72
|
+
if (lc === "standby") return "standby-everyone";
|
|
73
|
+
return ENGAGEMENT_MODE_NAMES.includes(lc) ? lc : void 0;
|
|
74
|
+
}
|
|
75
|
+
function senderMatches(filter, senderType, senderId) {
|
|
76
|
+
switch (filter) {
|
|
77
|
+
case "everyone":
|
|
78
|
+
return true;
|
|
79
|
+
case "fight":
|
|
80
|
+
return true;
|
|
81
|
+
case "people":
|
|
82
|
+
return senderType === "human";
|
|
83
|
+
case "agents":
|
|
84
|
+
return senderType === "agent";
|
|
85
|
+
default:
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
function isWhisperTo(event, selfId) {
|
|
90
|
+
if (event.type !== "MessageSent") return false;
|
|
91
|
+
const recipients = event.message.recipients;
|
|
92
|
+
if (!recipients || recipients.length === 0) return false;
|
|
93
|
+
return recipients.includes(selfId);
|
|
94
|
+
}
|
|
95
|
+
function classify(event, mode, selfId, senderType, senderId) {
|
|
96
|
+
const role = EVENT_ROLE[event.type];
|
|
97
|
+
if (role === "internal") return "drop";
|
|
98
|
+
if (role !== "mention" && role !== "ping" && event.participant_id === selfId) return "drop";
|
|
99
|
+
const isStandby = mode.startsWith("standby-");
|
|
100
|
+
const filter = isStandby ? mode.slice(8) : mode;
|
|
101
|
+
if (isStandby) {
|
|
102
|
+
if ((role === "mention" || role === "ping") && event.participant_id === selfId && senderMatches(filter, senderType, senderId)) return "trigger";
|
|
103
|
+
if (isWhisperTo(event, selfId) && senderMatches(filter, senderType, senderId)) return "trigger";
|
|
104
|
+
return "drop";
|
|
105
|
+
}
|
|
106
|
+
if (role === "ping") return "trigger";
|
|
107
|
+
if (role === "mention") return "drop";
|
|
108
|
+
if (role === "message") {
|
|
109
|
+
return senderMatches(filter, senderType, senderId) ? "trigger" : "content";
|
|
110
|
+
}
|
|
111
|
+
if (role === "ambient") return "content";
|
|
112
|
+
return "drop";
|
|
113
|
+
}
|
|
114
|
+
var StoopsEngagement = class {
|
|
115
|
+
_modes = /* @__PURE__ */ new Map();
|
|
116
|
+
_defaultMode;
|
|
117
|
+
constructor(defaultMode) {
|
|
118
|
+
this._defaultMode = defaultMode;
|
|
119
|
+
}
|
|
120
|
+
/** Get the engagement mode for a room. Falls back to the default mode. */
|
|
121
|
+
getMode(roomId) {
|
|
122
|
+
return this._modes.get(roomId) ?? this._defaultMode;
|
|
123
|
+
}
|
|
124
|
+
/** Set the engagement mode for a room. */
|
|
125
|
+
setMode(roomId, mode) {
|
|
126
|
+
this._modes.set(roomId, mode);
|
|
127
|
+
}
|
|
128
|
+
/** Called when a room is disconnected. Removes the room's mode so it doesn't linger. */
|
|
129
|
+
onRoomDisconnected(roomId) {
|
|
130
|
+
this._modes.delete(roomId);
|
|
131
|
+
}
|
|
132
|
+
classify(event, roomId, selfId, senderType, senderId) {
|
|
133
|
+
const mode = this._modes.get(roomId) ?? this._defaultMode;
|
|
134
|
+
return classify(event, mode, selfId, senderType, senderId);
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
function classifyEvent(event, mode, selfId, senderType, senderId) {
|
|
138
|
+
return classify(event, mode, selfId, senderType, senderId);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// src/agent/remote-room-data-source.ts
|
|
142
|
+
var RemoteRoomDataSource = class {
|
|
143
|
+
constructor(_serverUrl, _sessionToken, _roomId) {
|
|
144
|
+
this._serverUrl = _serverUrl;
|
|
145
|
+
this._sessionToken = _sessionToken;
|
|
146
|
+
this._roomId = _roomId;
|
|
147
|
+
}
|
|
148
|
+
_serverUrl;
|
|
149
|
+
_sessionToken;
|
|
150
|
+
_roomId;
|
|
151
|
+
_participants = [];
|
|
152
|
+
_selfId = "";
|
|
153
|
+
_selfName = "";
|
|
154
|
+
/** Set own identity for populating outgoing message stubs. */
|
|
155
|
+
setSelf(id, name) {
|
|
156
|
+
this._selfId = id;
|
|
157
|
+
this._selfName = name;
|
|
158
|
+
}
|
|
159
|
+
get roomId() {
|
|
160
|
+
return this._roomId;
|
|
161
|
+
}
|
|
162
|
+
get selfId() {
|
|
163
|
+
return this._selfId;
|
|
164
|
+
}
|
|
165
|
+
get serverUrl() {
|
|
166
|
+
return this._serverUrl;
|
|
167
|
+
}
|
|
168
|
+
get sessionToken() {
|
|
169
|
+
return this._sessionToken;
|
|
170
|
+
}
|
|
171
|
+
// ── Participant cache ─────────────────────────────────────────────────────
|
|
172
|
+
/** Set the initial participant list (from join response). */
|
|
173
|
+
setParticipants(participants) {
|
|
174
|
+
this._participants = [...participants];
|
|
175
|
+
}
|
|
176
|
+
/** Add a participant (on ParticipantJoined event). */
|
|
177
|
+
addParticipant(participant) {
|
|
178
|
+
this._participants = this._participants.filter((p) => p.id !== participant.id);
|
|
179
|
+
this._participants.push(participant);
|
|
180
|
+
}
|
|
181
|
+
/** Remove a participant (on ParticipantLeft event). */
|
|
182
|
+
removeParticipant(participantId) {
|
|
183
|
+
this._participants = this._participants.filter((p) => p.id !== participantId);
|
|
184
|
+
}
|
|
185
|
+
listParticipants() {
|
|
186
|
+
return [...this._participants];
|
|
187
|
+
}
|
|
188
|
+
// ── HTTP-backed data access ───────────────────────────────────────────────
|
|
189
|
+
async getMessage(id) {
|
|
190
|
+
try {
|
|
191
|
+
const res = await fetch(
|
|
192
|
+
`${this._serverUrl}/message/${encodeURIComponent(id)}`,
|
|
193
|
+
{ headers: { Authorization: `Bearer ${this._sessionToken}` } }
|
|
194
|
+
);
|
|
195
|
+
if (!res.ok) return null;
|
|
196
|
+
const data = await res.json();
|
|
197
|
+
return data.message;
|
|
198
|
+
} catch {
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
async searchMessages(query, limit = 10, cursor = null) {
|
|
203
|
+
const params = new URLSearchParams({ query, count: String(limit) });
|
|
204
|
+
if (cursor) params.set("cursor", cursor);
|
|
205
|
+
try {
|
|
206
|
+
const res = await fetch(`${this._serverUrl}/search?${params}`, {
|
|
207
|
+
headers: { Authorization: `Bearer ${this._sessionToken}` }
|
|
208
|
+
});
|
|
209
|
+
if (!res.ok) return { items: [], has_more: false, next_cursor: null };
|
|
210
|
+
return await res.json();
|
|
211
|
+
} catch {
|
|
212
|
+
return { items: [], has_more: false, next_cursor: null };
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
async getMessages(limit = 30, cursor = null) {
|
|
216
|
+
const params = new URLSearchParams({ count: String(limit) });
|
|
217
|
+
if (cursor) params.set("cursor", cursor);
|
|
218
|
+
try {
|
|
219
|
+
const res = await fetch(`${this._serverUrl}/messages?${params}`, {
|
|
220
|
+
headers: { Authorization: `Bearer ${this._sessionToken}` }
|
|
221
|
+
});
|
|
222
|
+
if (!res.ok) return { items: [], has_more: false, next_cursor: null };
|
|
223
|
+
return await res.json();
|
|
224
|
+
} catch {
|
|
225
|
+
return { items: [], has_more: false, next_cursor: null };
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
async getEvents(category = null, limit = 50, cursor = null) {
|
|
229
|
+
const params = new URLSearchParams({ count: String(limit) });
|
|
230
|
+
if (category) params.set("category", category);
|
|
231
|
+
if (cursor) params.set("cursor", cursor);
|
|
232
|
+
try {
|
|
233
|
+
const res = await fetch(`${this._serverUrl}/events/history?${params}`, {
|
|
234
|
+
headers: { Authorization: `Bearer ${this._sessionToken}` }
|
|
235
|
+
});
|
|
236
|
+
if (!res.ok) return { items: [], has_more: false, next_cursor: null };
|
|
237
|
+
return await res.json();
|
|
238
|
+
} catch {
|
|
239
|
+
return { items: [], has_more: false, next_cursor: null };
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
async sendMessage(content, replyToId, image, attachments, recipients) {
|
|
243
|
+
const body = { content };
|
|
244
|
+
if (replyToId) body.replyTo = replyToId;
|
|
245
|
+
if (image) body.image = image;
|
|
246
|
+
if (attachments?.length) body.attachments = attachments;
|
|
247
|
+
if (recipients?.length) body.recipients = recipients;
|
|
248
|
+
const res = await fetch(`${this._serverUrl}/message`, {
|
|
249
|
+
method: "POST",
|
|
250
|
+
headers: { "Content-Type": "application/json", Authorization: `Bearer ${this._sessionToken}` },
|
|
251
|
+
body: JSON.stringify(body)
|
|
252
|
+
});
|
|
253
|
+
if (!res.ok) {
|
|
254
|
+
const err = await res.text();
|
|
255
|
+
throw new Error(`Failed to send message: ${err}`);
|
|
256
|
+
}
|
|
257
|
+
const data = await res.json();
|
|
258
|
+
return {
|
|
259
|
+
id: data.messageId,
|
|
260
|
+
room_id: this._roomId,
|
|
261
|
+
sender_id: this._selfId,
|
|
262
|
+
sender_name: this._selfName,
|
|
263
|
+
content,
|
|
264
|
+
timestamp: /* @__PURE__ */ new Date()
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
async emitEvent(event) {
|
|
268
|
+
const res = await fetch(`${this._serverUrl}/event`, {
|
|
269
|
+
method: "POST",
|
|
270
|
+
headers: { "Content-Type": "application/json", Authorization: `Bearer ${this._sessionToken}` },
|
|
271
|
+
body: JSON.stringify({ event })
|
|
272
|
+
});
|
|
273
|
+
if (!res.ok) {
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
// src/agent/multiplexer.ts
|
|
279
|
+
var EventMultiplexer = class {
|
|
280
|
+
_queue = [];
|
|
281
|
+
_waiters = [];
|
|
282
|
+
_channels = /* @__PURE__ */ new Map();
|
|
283
|
+
_closed = false;
|
|
284
|
+
_closeResolve = null;
|
|
285
|
+
addChannel(roomId, roomName, channel) {
|
|
286
|
+
if (this._channels.has(roomId) || this._closed) return;
|
|
287
|
+
const abortController = new AbortController();
|
|
288
|
+
const loopPromise = this._listenLoop(roomId, roomName, channel, abortController.signal);
|
|
289
|
+
this._channels.set(roomId, { channel, roomName, abortController, loopPromise });
|
|
290
|
+
}
|
|
291
|
+
removeChannel(roomId) {
|
|
292
|
+
const entry = this._channels.get(roomId);
|
|
293
|
+
if (!entry) return;
|
|
294
|
+
entry.abortController.abort();
|
|
295
|
+
this._channels.delete(roomId);
|
|
296
|
+
}
|
|
297
|
+
close() {
|
|
298
|
+
this._closed = true;
|
|
299
|
+
for (const [, entry] of this._channels) {
|
|
300
|
+
entry.abortController.abort();
|
|
301
|
+
}
|
|
302
|
+
this._channels.clear();
|
|
303
|
+
if (this._closeResolve) {
|
|
304
|
+
this._closeResolve();
|
|
305
|
+
this._closeResolve = null;
|
|
306
|
+
}
|
|
307
|
+
for (const waiter of this._waiters) {
|
|
308
|
+
waiter.resolve(null);
|
|
309
|
+
}
|
|
310
|
+
this._waiters = [];
|
|
311
|
+
}
|
|
312
|
+
async _listenLoop(roomId, roomName, channel, signal) {
|
|
313
|
+
try {
|
|
314
|
+
for await (const event of channel) {
|
|
315
|
+
if (signal.aborted) break;
|
|
316
|
+
this._push({ roomId, roomName, event });
|
|
317
|
+
}
|
|
318
|
+
} catch {
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
_push(labeled) {
|
|
322
|
+
if (this._closed) return;
|
|
323
|
+
if (this._waiters.length > 0) {
|
|
324
|
+
const waiter = this._waiters.shift();
|
|
325
|
+
waiter.resolve(labeled);
|
|
326
|
+
} else {
|
|
327
|
+
this._queue.push(labeled);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
[Symbol.asyncIterator]() {
|
|
331
|
+
return {
|
|
332
|
+
next: () => {
|
|
333
|
+
if (this._queue.length > 0) {
|
|
334
|
+
return Promise.resolve({ value: this._queue.shift(), done: false });
|
|
335
|
+
}
|
|
336
|
+
if (this._closed) {
|
|
337
|
+
return Promise.resolve({ value: void 0, done: true });
|
|
338
|
+
}
|
|
339
|
+
return new Promise((resolve) => {
|
|
340
|
+
this._waiters.push({
|
|
341
|
+
resolve: (value) => {
|
|
342
|
+
if (this._closed || value === null) {
|
|
343
|
+
resolve({ value: void 0, done: true });
|
|
344
|
+
} else {
|
|
345
|
+
resolve({ value, done: false });
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
});
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
// src/agent/ref-map.ts
|
|
356
|
+
var RefMap = class {
|
|
357
|
+
_counter = Math.floor(Math.random() * 1e4);
|
|
358
|
+
_refToId = /* @__PURE__ */ new Map();
|
|
359
|
+
_idToRef = /* @__PURE__ */ new Map();
|
|
360
|
+
/** Assign a short ref to a message ID. Returns existing ref if already assigned. */
|
|
361
|
+
assign(messageId) {
|
|
362
|
+
const existing = this._idToRef.get(messageId);
|
|
363
|
+
if (existing) return existing;
|
|
364
|
+
const ref = String(this._counter * 6337 % 1e4).padStart(4, "0");
|
|
365
|
+
this._counter++;
|
|
366
|
+
if (this._refToId.has(ref)) {
|
|
367
|
+
const hex = messageId.replace(/-/g, "");
|
|
368
|
+
let fallback = null;
|
|
369
|
+
for (let i = 0; i <= hex.length - 4; i++) {
|
|
370
|
+
const candidate = hex.slice(i, i + 4);
|
|
371
|
+
if (!this._refToId.has(candidate)) {
|
|
372
|
+
fallback = candidate;
|
|
373
|
+
break;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
if (!fallback) fallback = messageId.slice(0, 8);
|
|
377
|
+
this._refToId.set(fallback, messageId);
|
|
378
|
+
this._idToRef.set(messageId, fallback);
|
|
379
|
+
return fallback;
|
|
380
|
+
}
|
|
381
|
+
this._refToId.set(ref, messageId);
|
|
382
|
+
this._idToRef.set(messageId, ref);
|
|
383
|
+
return ref;
|
|
384
|
+
}
|
|
385
|
+
/** Resolve a ref back to the full message UUID. Returns undefined if unknown. */
|
|
386
|
+
resolve(ref) {
|
|
387
|
+
return this._refToId.get(ref);
|
|
388
|
+
}
|
|
389
|
+
/** Clear all mappings and reset the counter. Called on context compaction. */
|
|
390
|
+
clear() {
|
|
391
|
+
this._refToId.clear();
|
|
392
|
+
this._idToRef.clear();
|
|
393
|
+
this._counter = Math.floor(Math.random() * 1e4);
|
|
394
|
+
}
|
|
395
|
+
};
|
|
396
|
+
|
|
397
|
+
// src/agent/tool-handlers.ts
|
|
398
|
+
import { readFile } from "fs/promises";
|
|
399
|
+
function resolveOrError(resolver, roomName) {
|
|
400
|
+
const conn = resolver.resolve(roomName);
|
|
401
|
+
if (!conn) {
|
|
402
|
+
return {
|
|
403
|
+
error: true,
|
|
404
|
+
result: { content: [{ type: "text", text: `Unknown room "${roomName}".` }] }
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
return { error: false, conn };
|
|
408
|
+
}
|
|
409
|
+
function textResult(text) {
|
|
410
|
+
return { content: [{ type: "text", text }] };
|
|
411
|
+
}
|
|
412
|
+
async function formatMsgLine(msg, conn, mkRef) {
|
|
413
|
+
const ts = formatTimestamp(new Date(msg.timestamp));
|
|
414
|
+
const ref = mkRef(msg.id);
|
|
415
|
+
const imageNote = msg.image_url ? ` [[img:${msg.image_url}]]` : "";
|
|
416
|
+
const attachmentNotes = (msg.attachments ?? []).map((a) => {
|
|
417
|
+
if (a.type === "path") return ` [attachment: ${a.name} (${a.mime_type}, path: ${a.path} \u2014 same-machine only)]`;
|
|
418
|
+
return ` [attachment: ${a.name} (${a.mime_type}, ${a.size}B, url: ${a.url})]`;
|
|
419
|
+
}).join("");
|
|
420
|
+
const whisperNote = (msg.recipients?.length ?? 0) > 0 ? ` [whisper \u2192 ${conn.dataSource.listParticipants().filter((p) => msg.recipients.includes(p.id)).map((p) => p.name).join(", ")}]` : "";
|
|
421
|
+
let line = `[${ts}] ${ref} ${msg.sender_name}: ${msg.content}${whisperNote}${imageNote}${attachmentNotes}`;
|
|
422
|
+
if (msg.reply_to_id) {
|
|
423
|
+
const target = await conn.dataSource.getMessage(msg.reply_to_id);
|
|
424
|
+
if (target) {
|
|
425
|
+
const targetRef = mkRef(target.id);
|
|
426
|
+
line = `[${ts}] ${ref} ${msg.sender_name} (\u2192 ${targetRef} ${target.sender_name}): ${msg.content}${imageNote}${attachmentNotes}`;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
return line;
|
|
430
|
+
}
|
|
431
|
+
async function tryReadImageAttachment(att) {
|
|
432
|
+
if (!att.mime_type.startsWith("image/")) return null;
|
|
433
|
+
try {
|
|
434
|
+
const bytes = await readFile(att.path);
|
|
435
|
+
return {
|
|
436
|
+
type: "image",
|
|
437
|
+
source: { type: "base64", media_type: att.mime_type, data: bytes.toString("base64") }
|
|
438
|
+
};
|
|
439
|
+
} catch {
|
|
440
|
+
return null;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
async function buildCatchUpLines(conn, options) {
|
|
444
|
+
const result = await conn.dataSource.getEvents(null, 50, null);
|
|
445
|
+
const chronological = [...result.items].reverse();
|
|
446
|
+
let startIdx = chronological.length;
|
|
447
|
+
for (let i = 0; i < chronological.length; i++) {
|
|
448
|
+
if (!options.isEventSeen?.(chronological[i].id)) {
|
|
449
|
+
startIdx = i;
|
|
450
|
+
break;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
const unseen = chronological.slice(startIdx);
|
|
454
|
+
const lines = [];
|
|
455
|
+
const imageBlocks = [];
|
|
456
|
+
const seenIds = [];
|
|
457
|
+
const mkRef = (id) => `#${options.assignRef?.(id) ?? messageRef(id)}`;
|
|
458
|
+
const selfId = conn.dataSource.selfId || conn.channel?.participantId;
|
|
459
|
+
for (const event of unseen) {
|
|
460
|
+
seenIds.push(event.id);
|
|
461
|
+
const ts = formatTimestamp(new Date(event.timestamp));
|
|
462
|
+
if (event.type === "MessageSent") {
|
|
463
|
+
const msg = event.message;
|
|
464
|
+
if ((msg.recipients?.length ?? 0) > 0 && msg.sender_id !== selfId && (!selfId || !msg.recipients.includes(selfId))) {
|
|
465
|
+
continue;
|
|
466
|
+
}
|
|
467
|
+
lines.push(await formatMsgLine(event.message, conn, (id) => mkRef(id)));
|
|
468
|
+
for (const att of event.message.attachments ?? []) {
|
|
469
|
+
if (att.type === "path") {
|
|
470
|
+
const block = await tryReadImageAttachment(att);
|
|
471
|
+
if (block) imageBlocks.push(block);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
} else if (event.type === "ParticipantJoined") {
|
|
475
|
+
const participant = conn.dataSource.listParticipants().find((p) => p.id === event.participant_id);
|
|
476
|
+
const name = participant?.name ?? event.participant_id;
|
|
477
|
+
lines.push(`[${ts}] + ${name} joined`);
|
|
478
|
+
} else if (event.type === "ParticipantLeft") {
|
|
479
|
+
const name = event.participant?.name ?? event.participant_id;
|
|
480
|
+
lines.push(`[${ts}] - ${name} left`);
|
|
481
|
+
} else if (event.type === "ReactionAdded") {
|
|
482
|
+
const participant = conn.dataSource.listParticipants().find((p) => p.id === event.participant_id);
|
|
483
|
+
const name = participant?.name ?? event.participant_id;
|
|
484
|
+
const target = await conn.dataSource.getMessage(event.message_id);
|
|
485
|
+
const targetRef = target ? ` to ${mkRef(target.id)}` : "";
|
|
486
|
+
lines.push(`[${ts}] ${name} reacted ${event.emoji}${targetRef}`);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
if (seenIds.length > 0) options.markEventsSeen?.(seenIds);
|
|
490
|
+
return { lines, imageBlocks };
|
|
491
|
+
}
|
|
492
|
+
async function handleCatchUp(resolver, args, options) {
|
|
493
|
+
const r = resolveOrError(resolver, args.room);
|
|
494
|
+
if (r.error) return r.result;
|
|
495
|
+
const { lines, imageBlocks } = await buildCatchUpLines(r.conn, options);
|
|
496
|
+
const out = [`Catching up on [${args.room}]:`];
|
|
497
|
+
if (lines.length > 0) {
|
|
498
|
+
out.push("", ...lines);
|
|
499
|
+
} else {
|
|
500
|
+
out.push("", "(nothing new)");
|
|
501
|
+
}
|
|
502
|
+
const content = [{ type: "text", text: out.join("\n") }];
|
|
503
|
+
content.push(...imageBlocks);
|
|
504
|
+
return { content };
|
|
505
|
+
}
|
|
506
|
+
async function handleSearchByText(resolver, args, options) {
|
|
507
|
+
const r = resolveOrError(resolver, args.room);
|
|
508
|
+
if (r.error) return r.result;
|
|
509
|
+
const { conn } = r;
|
|
510
|
+
const count = args.count ?? 3;
|
|
511
|
+
const mkRef = (id) => `#${options.assignRef?.(id) ?? messageRef(id)}`;
|
|
512
|
+
const searchResult = await conn.dataSource.searchMessages(args.query, 50, args.cursor ?? null);
|
|
513
|
+
const totalVisible = searchResult.items.length;
|
|
514
|
+
if (totalVisible === 0) {
|
|
515
|
+
return textResult(`No messages found in [${args.room}] matching "${args.query}".`);
|
|
516
|
+
}
|
|
517
|
+
const toShow = searchResult.items.slice(0, count);
|
|
518
|
+
const recentResult = await conn.dataSource.getMessages(100, null);
|
|
519
|
+
const recentChron = [...recentResult.items].reverse();
|
|
520
|
+
const msgIdxMap = /* @__PURE__ */ new Map();
|
|
521
|
+
recentChron.forEach((m, i) => msgIdxMap.set(m.id, i));
|
|
522
|
+
const clusters = [];
|
|
523
|
+
let newerCount = null;
|
|
524
|
+
for (const match of [...toShow].reverse()) {
|
|
525
|
+
const cluster = [];
|
|
526
|
+
const matchIdx = msgIdxMap.get(match.id);
|
|
527
|
+
if (matchIdx !== void 0) {
|
|
528
|
+
if (matchIdx > 0) {
|
|
529
|
+
cluster.push(await formatMsgLine(recentChron[matchIdx - 1], conn, mkRef));
|
|
530
|
+
}
|
|
531
|
+
cluster.push(await formatMsgLine(match, conn, mkRef) + " \u2190");
|
|
532
|
+
if (matchIdx < recentChron.length - 1) {
|
|
533
|
+
cluster.push(await formatMsgLine(recentChron[matchIdx + 1], conn, mkRef));
|
|
534
|
+
}
|
|
535
|
+
if (newerCount === null && match.id === toShow[0].id) {
|
|
536
|
+
newerCount = recentChron.length - matchIdx - 1;
|
|
537
|
+
}
|
|
538
|
+
} else {
|
|
539
|
+
cluster.push(await formatMsgLine(match, conn, mkRef) + " \u2190");
|
|
540
|
+
}
|
|
541
|
+
clusters.push(cluster);
|
|
542
|
+
}
|
|
543
|
+
const shownCount = toShow.length;
|
|
544
|
+
const totalNote = searchResult.has_more ? `showing ${shownCount} of 50+` : `${shownCount} of ${totalVisible}`;
|
|
545
|
+
const out = [`Search results in [${args.room}] for "${args.query}" (${totalNote} matches):`, ""];
|
|
546
|
+
for (let i = 0; i < clusters.length; i++) {
|
|
547
|
+
for (const line of clusters[i]) {
|
|
548
|
+
out.push(` ${line}`);
|
|
549
|
+
}
|
|
550
|
+
if (i < clusters.length - 1) out.push("");
|
|
551
|
+
}
|
|
552
|
+
if (newerCount !== null && newerCount > 0) {
|
|
553
|
+
out.push("", `${newerCount} newer message${newerCount === 1 ? "" : "s"} in this room.`);
|
|
554
|
+
}
|
|
555
|
+
if (searchResult.has_more && searchResult.next_cursor) {
|
|
556
|
+
out.push(`(cursor: "${searchResult.next_cursor}" for next ${count} matches)`);
|
|
557
|
+
}
|
|
558
|
+
return textResult(out.join("\n"));
|
|
559
|
+
}
|
|
560
|
+
async function handleSearchByMessage(resolver, args, options) {
|
|
561
|
+
const r = resolveOrError(resolver, args.room);
|
|
562
|
+
if (r.error) return r.result;
|
|
563
|
+
const { conn } = r;
|
|
564
|
+
const direction = args.direction ?? "before";
|
|
565
|
+
const count = args.count ?? 10;
|
|
566
|
+
const mkRef = (id) => `#${options.assignRef?.(id) ?? messageRef(id)}`;
|
|
567
|
+
const rawRef = args.ref.startsWith("#") ? args.ref.slice(1) : args.ref;
|
|
568
|
+
const anchorId = options.resolveRef?.(rawRef) ?? rawRef;
|
|
569
|
+
const anchor = await conn.dataSource.getMessage(anchorId);
|
|
570
|
+
if (!anchor) return textResult(`Message ${args.ref} not found.`);
|
|
571
|
+
const recentResult = await conn.dataSource.getMessages(100, null);
|
|
572
|
+
const recentChron = [...recentResult.items].reverse();
|
|
573
|
+
const anchorIdx = recentChron.findIndex((m) => m.id === anchor.id);
|
|
574
|
+
let displayMessages;
|
|
575
|
+
let newerCount;
|
|
576
|
+
if (direction === "before") {
|
|
577
|
+
const beforeResult = await conn.dataSource.getMessages(count, anchor.id);
|
|
578
|
+
const beforeMessages = [...beforeResult.items].reverse();
|
|
579
|
+
displayMessages = [...beforeMessages, anchor];
|
|
580
|
+
newerCount = anchorIdx >= 0 ? recentChron.length - anchorIdx - 1 : 100;
|
|
581
|
+
} else {
|
|
582
|
+
if (anchorIdx >= 0) {
|
|
583
|
+
const afterMessages = recentChron.slice(anchorIdx + 1, anchorIdx + 1 + count);
|
|
584
|
+
displayMessages = [anchor, ...afterMessages];
|
|
585
|
+
newerCount = recentChron.length - anchorIdx - 1 - afterMessages.length;
|
|
586
|
+
} else {
|
|
587
|
+
displayMessages = [anchor];
|
|
588
|
+
newerCount = 100;
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
const anchorRef = mkRef(anchor.id);
|
|
592
|
+
const lines = [`Context in [${args.room}] around ${anchorRef}:`, ""];
|
|
593
|
+
for (const msg of displayMessages) {
|
|
594
|
+
const line = await formatMsgLine(msg, conn, mkRef);
|
|
595
|
+
const isAnchor = msg.id === anchor.id;
|
|
596
|
+
lines.push(` ${line}${isAnchor ? " \u2190" : ""}`);
|
|
597
|
+
}
|
|
598
|
+
if (newerCount > 0) {
|
|
599
|
+
const countLabel = newerCount >= 100 ? "100+" : String(newerCount);
|
|
600
|
+
lines.push("", `${countLabel} newer message${newerCount === 1 ? "" : "s"} in this room.`);
|
|
601
|
+
}
|
|
602
|
+
return textResult(lines.join("\n"));
|
|
603
|
+
}
|
|
604
|
+
async function handleSendMessage(resolver, args, options) {
|
|
605
|
+
const r = resolveOrError(resolver, args.room);
|
|
606
|
+
if (r.error) return r.result;
|
|
607
|
+
const image = args.image_url ? {
|
|
608
|
+
url: args.image_url,
|
|
609
|
+
mimeType: args.image_mime_type ?? "image/*",
|
|
610
|
+
sizeBytes: args.image_size_bytes ?? 0
|
|
611
|
+
} : null;
|
|
612
|
+
let replyToId = args.reply_to_id;
|
|
613
|
+
if (replyToId) {
|
|
614
|
+
const rawRef = replyToId.startsWith("#") ? replyToId.slice(1) : replyToId;
|
|
615
|
+
replyToId = options.resolveRef?.(rawRef) ?? replyToId;
|
|
616
|
+
}
|
|
617
|
+
let recipientIds;
|
|
618
|
+
if (args.to && args.to.length > 0) {
|
|
619
|
+
const participants = r.conn.dataSource.listParticipants();
|
|
620
|
+
recipientIds = [];
|
|
621
|
+
for (const name of args.to) {
|
|
622
|
+
const matches = participants.filter((p) => p.name === name);
|
|
623
|
+
if (matches.length === 0) return textResult(`Unknown participant "${name}". Known: ${participants.map((p) => p.name).join(", ")}.`);
|
|
624
|
+
if (matches.length > 1) return textResult(`Ambiguous name "${name}" \u2014 multiple participants match. Use a more specific name.`);
|
|
625
|
+
recipientIds.push(matches[0].id);
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
const message = await r.conn.dataSource.sendMessage(args.content, replyToId, image, args.attachments, recipientIds);
|
|
629
|
+
const ref = options.assignRef?.(message.id) ?? messageRef(message.id);
|
|
630
|
+
return textResult(`Message sent #${ref}.`);
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
// src/agent/connection-registry.ts
|
|
634
|
+
var ConnectionRegistry = class {
|
|
635
|
+
_connections = /* @__PURE__ */ new Map();
|
|
636
|
+
_nameToId = /* @__PURE__ */ new Map();
|
|
637
|
+
_identifierToId = /* @__PURE__ */ new Map();
|
|
638
|
+
_lastMessages = /* @__PURE__ */ new Map();
|
|
639
|
+
add(roomId, conn) {
|
|
640
|
+
this._connections.set(roomId, conn);
|
|
641
|
+
this._nameToId.set(conn.name, roomId);
|
|
642
|
+
if (conn.identifier) this._identifierToId.set(conn.identifier, roomId);
|
|
643
|
+
}
|
|
644
|
+
remove(roomId) {
|
|
645
|
+
const conn = this._connections.get(roomId);
|
|
646
|
+
if (!conn) return void 0;
|
|
647
|
+
this._nameToId.delete(conn.name);
|
|
648
|
+
if (conn.identifier) this._identifierToId.delete(conn.identifier);
|
|
649
|
+
this._connections.delete(roomId);
|
|
650
|
+
this._lastMessages.delete(roomId);
|
|
651
|
+
return conn;
|
|
652
|
+
}
|
|
653
|
+
get(roomId) {
|
|
654
|
+
return this._connections.get(roomId);
|
|
655
|
+
}
|
|
656
|
+
has(roomId) {
|
|
657
|
+
return this._connections.has(roomId);
|
|
658
|
+
}
|
|
659
|
+
resolve(roomName) {
|
|
660
|
+
const roomId = this._nameToId.get(roomName);
|
|
661
|
+
if (roomId) return this._connections.get(roomId) ?? null;
|
|
662
|
+
const idFromIdentifier = this._identifierToId.get(roomName);
|
|
663
|
+
if (idFromIdentifier) return this._connections.get(idFromIdentifier) ?? null;
|
|
664
|
+
return this._connections.get(roomName) ?? null;
|
|
665
|
+
}
|
|
666
|
+
listAll(getModeForRoom) {
|
|
667
|
+
return [...this._connections.entries()].map(([roomId, conn]) => ({
|
|
668
|
+
name: conn.name,
|
|
669
|
+
roomId,
|
|
670
|
+
...conn.identifier ? { identifier: conn.identifier } : {},
|
|
671
|
+
mode: getModeForRoom(roomId),
|
|
672
|
+
participantCount: conn.dataSource.listParticipants().length,
|
|
673
|
+
...this._lastMessages.has(roomId) ? { lastMessage: this._lastMessages.get(roomId) } : {}
|
|
674
|
+
}));
|
|
675
|
+
}
|
|
676
|
+
setLastMessage(roomId, text) {
|
|
677
|
+
this._lastMessages.set(roomId, text);
|
|
678
|
+
}
|
|
679
|
+
entries() {
|
|
680
|
+
return this._connections.entries();
|
|
681
|
+
}
|
|
682
|
+
get size() {
|
|
683
|
+
return this._connections.size;
|
|
684
|
+
}
|
|
685
|
+
clear() {
|
|
686
|
+
this._connections.clear();
|
|
687
|
+
this._nameToId.clear();
|
|
688
|
+
this._identifierToId.clear();
|
|
689
|
+
this._lastMessages.clear();
|
|
690
|
+
}
|
|
691
|
+
values() {
|
|
692
|
+
return this._connections.values();
|
|
693
|
+
}
|
|
694
|
+
};
|
|
695
|
+
|
|
696
|
+
// src/agent/content-buffer.ts
|
|
697
|
+
var ContentBuffer = class {
|
|
698
|
+
_buffer = /* @__PURE__ */ new Map();
|
|
699
|
+
push(roomId, item) {
|
|
700
|
+
const buf = this._buffer.get(roomId) ?? [];
|
|
701
|
+
buf.push(item);
|
|
702
|
+
this._buffer.set(roomId, buf);
|
|
703
|
+
}
|
|
704
|
+
flush(roomId) {
|
|
705
|
+
const items = this._buffer.get(roomId) ?? [];
|
|
706
|
+
this._buffer.delete(roomId);
|
|
707
|
+
return items;
|
|
708
|
+
}
|
|
709
|
+
delete(roomId) {
|
|
710
|
+
this._buffer.delete(roomId);
|
|
711
|
+
}
|
|
712
|
+
clear() {
|
|
713
|
+
this._buffer.clear();
|
|
714
|
+
}
|
|
715
|
+
};
|
|
716
|
+
|
|
717
|
+
// src/agent/event-tracker.ts
|
|
718
|
+
var PROCESSED_CAP = 500;
|
|
719
|
+
var DELIVERED_CAP = 5e3;
|
|
720
|
+
var EventTracker = class {
|
|
721
|
+
_processedIds = /* @__PURE__ */ new Set();
|
|
722
|
+
_deliveredIds = /* @__PURE__ */ new Set();
|
|
723
|
+
/** Returns true if this event was already processed (and adds it if not). */
|
|
724
|
+
isDuplicate(id) {
|
|
725
|
+
if (this._processedIds.has(id)) return true;
|
|
726
|
+
this._processedIds.add(id);
|
|
727
|
+
if (this._processedIds.size > PROCESSED_CAP) {
|
|
728
|
+
this._processedIds = halve(this._processedIds);
|
|
729
|
+
}
|
|
730
|
+
return false;
|
|
731
|
+
}
|
|
732
|
+
isDelivered(id) {
|
|
733
|
+
return this._deliveredIds.has(id);
|
|
734
|
+
}
|
|
735
|
+
markDelivered(id) {
|
|
736
|
+
this._deliveredIds.add(id);
|
|
737
|
+
if (this._deliveredIds.size > DELIVERED_CAP) {
|
|
738
|
+
this._deliveredIds = halve(this._deliveredIds);
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
markManyDelivered(ids) {
|
|
742
|
+
for (const id of ids) {
|
|
743
|
+
this._deliveredIds.add(id);
|
|
744
|
+
this._processedIds.add(id);
|
|
745
|
+
}
|
|
746
|
+
if (this._deliveredIds.size > DELIVERED_CAP) this._deliveredIds = halve(this._deliveredIds);
|
|
747
|
+
if (this._processedIds.size > PROCESSED_CAP) this._processedIds = halve(this._processedIds);
|
|
748
|
+
}
|
|
749
|
+
clearDelivered() {
|
|
750
|
+
this._deliveredIds.clear();
|
|
751
|
+
}
|
|
752
|
+
clearAll() {
|
|
753
|
+
this._processedIds.clear();
|
|
754
|
+
this._deliveredIds.clear();
|
|
755
|
+
}
|
|
756
|
+
};
|
|
757
|
+
function halve(set) {
|
|
758
|
+
const arr = [...set];
|
|
759
|
+
return new Set(arr.slice(arr.length >> 1));
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
// src/agent/event-processor.ts
|
|
763
|
+
function mergeParts(arrays) {
|
|
764
|
+
const result = [];
|
|
765
|
+
for (let i = 0; i < arrays.length; i++) {
|
|
766
|
+
if (i > 0) result.push({ type: "text", text: "\n" });
|
|
767
|
+
result.push(...arrays[i]);
|
|
768
|
+
}
|
|
769
|
+
return result;
|
|
770
|
+
}
|
|
771
|
+
var ACTIVITY_REPORT_MIN_MS = 1e4;
|
|
772
|
+
function shouldReportActivity(opts) {
|
|
773
|
+
if (opts.label !== opts.lastLabel || opts.state !== opts.lastState) return true;
|
|
774
|
+
return opts.now - opts.lastReportAt > (opts.minIntervalMs ?? ACTIVITY_REPORT_MIN_MS);
|
|
775
|
+
}
|
|
776
|
+
var EventProcessor = class {
|
|
777
|
+
_participantId;
|
|
778
|
+
_participantName;
|
|
779
|
+
_options;
|
|
780
|
+
_engagement;
|
|
781
|
+
_deliver = null;
|
|
782
|
+
_multiplexer = new EventMultiplexer();
|
|
783
|
+
_registry = new ConnectionRegistry();
|
|
784
|
+
_buffer = new ContentBuffer();
|
|
785
|
+
_tracker = new EventTracker();
|
|
786
|
+
_processing = false;
|
|
787
|
+
_eventQueue = [];
|
|
788
|
+
_stopped = false;
|
|
789
|
+
_currentContextRoomId = null;
|
|
790
|
+
_log = [];
|
|
791
|
+
_refMap = new RefMap();
|
|
792
|
+
_injectBuffer = [];
|
|
793
|
+
/** Per-room participant IDs (for multi-server CLI agents where each server assigns a different ID). */
|
|
794
|
+
_roomSelfIds = /* @__PURE__ */ new Map();
|
|
795
|
+
constructor(participantId, participantName, options = {}) {
|
|
796
|
+
this._participantId = participantId;
|
|
797
|
+
this._participantName = participantName;
|
|
798
|
+
this._options = options;
|
|
799
|
+
this._engagement = options.engagement ?? new StoopsEngagement(options.defaultMode ?? "everyone");
|
|
800
|
+
}
|
|
801
|
+
// ── Public accessors ────────────────────────────────────────────────────────
|
|
802
|
+
get participantId() {
|
|
803
|
+
return this._participantId;
|
|
804
|
+
}
|
|
805
|
+
set participantId(id) {
|
|
806
|
+
this._participantId = id;
|
|
807
|
+
}
|
|
808
|
+
get participantName() {
|
|
809
|
+
return this._participantName;
|
|
810
|
+
}
|
|
811
|
+
get currentContextRoomId() {
|
|
812
|
+
return this._currentContextRoomId;
|
|
813
|
+
}
|
|
814
|
+
/** Set a room-specific participant ID (for multi-server CLI agents). */
|
|
815
|
+
setRoomParticipantId(roomId, participantId) {
|
|
816
|
+
this._roomSelfIds.set(roomId, participantId);
|
|
817
|
+
}
|
|
818
|
+
/** Get the effective selfId for a room — room-specific if set, otherwise the global one. */
|
|
819
|
+
getSelfIdForRoom(roomId) {
|
|
820
|
+
return this._roomSelfIds.get(roomId) ?? this._participantId;
|
|
821
|
+
}
|
|
822
|
+
// ── Ref map (consumer calls these for MCP tools) ────────────────────────────
|
|
823
|
+
assignRef(messageId) {
|
|
824
|
+
return this._refMap.assign(messageId);
|
|
825
|
+
}
|
|
826
|
+
resolveRef(ref) {
|
|
827
|
+
return this._refMap.resolve(ref);
|
|
828
|
+
}
|
|
829
|
+
// ── Seen-event cache (consumer calls these for catch_up MCP tool) ───────────
|
|
830
|
+
isEventSeen(eventId) {
|
|
831
|
+
return this._tracker.isDelivered(eventId);
|
|
832
|
+
}
|
|
833
|
+
markEventsSeen(eventIds) {
|
|
834
|
+
this._tracker.markManyDelivered(eventIds);
|
|
835
|
+
}
|
|
836
|
+
// ── Inject buffer (LangGraph mid-loop event injection) ──────────────────────
|
|
837
|
+
drainInjectBuffer() {
|
|
838
|
+
if (this._injectBuffer.length === 0) return null;
|
|
839
|
+
const drained = this._injectBuffer;
|
|
840
|
+
this._injectBuffer = [];
|
|
841
|
+
return drained;
|
|
842
|
+
}
|
|
843
|
+
// ── Consumer hooks (called by consumer during/after delivery) ────────────────
|
|
844
|
+
/**
|
|
845
|
+
* Called by the consumer when context was compacted.
|
|
846
|
+
* Clears seen-event cache and ref map so catch_up returns full history.
|
|
847
|
+
*/
|
|
848
|
+
onContextCompacted() {
|
|
849
|
+
this._tracker.clearDelivered();
|
|
850
|
+
this._refMap.clear();
|
|
851
|
+
}
|
|
852
|
+
/**
|
|
853
|
+
* Called by the consumer when a tool call starts or completes.
|
|
854
|
+
* Routes ToolUseEvent to the room that triggered the current evaluation.
|
|
855
|
+
*/
|
|
856
|
+
emitToolUse(toolName, status) {
|
|
857
|
+
if (this._currentContextRoomId) {
|
|
858
|
+
const conn = this._registry.get(this._currentContextRoomId);
|
|
859
|
+
if (conn) {
|
|
860
|
+
const event = createEvent({
|
|
861
|
+
type: "ToolUse",
|
|
862
|
+
category: "ACTIVITY",
|
|
863
|
+
room_id: this._currentContextRoomId,
|
|
864
|
+
participant_id: this._participantId,
|
|
865
|
+
tool_name: toolName,
|
|
866
|
+
status
|
|
867
|
+
});
|
|
868
|
+
const emitter = conn.dataSource.emitEvent ? (e) => conn.dataSource.emitEvent(e) : conn.channel ? (e) => conn.channel.emit(e) : null;
|
|
869
|
+
if (!emitter) return;
|
|
870
|
+
emitter(event).catch(() => {
|
|
871
|
+
});
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
// ── RoomResolver implementation ─────────────────────────────────────────────
|
|
876
|
+
resolve(roomName) {
|
|
877
|
+
return this._registry.resolve(roomName);
|
|
878
|
+
}
|
|
879
|
+
listAll() {
|
|
880
|
+
return this._registry.listAll((roomId) => this.getModeForRoom(roomId));
|
|
881
|
+
}
|
|
882
|
+
/**
|
|
883
|
+
* Push a live status label ("Sautéed for 12s", "Compacting…", null=idle)
|
|
884
|
+
* to every connected REMOTE room (i.e. CLI-path agents talking to an
|
|
885
|
+
* apiary HTTP server). Local app-path connections are skipped — they don't
|
|
886
|
+
* have a /activity HTTP endpoint to hit.
|
|
887
|
+
*
|
|
888
|
+
* Called from the CLI runtime's tmux poll loop. Best-effort: failures are
|
|
889
|
+
* swallowed so a flaky network never crashes the agent.
|
|
890
|
+
*/
|
|
891
|
+
async broadcastActivity(label, state) {
|
|
892
|
+
for (const conn of this._registry.values()) {
|
|
893
|
+
const ds = conn.dataSource;
|
|
894
|
+
if (!(ds instanceof RemoteRoomDataSource)) continue;
|
|
895
|
+
try {
|
|
896
|
+
await fetch(`${ds.serverUrl}/activity`, {
|
|
897
|
+
method: "POST",
|
|
898
|
+
headers: {
|
|
899
|
+
"Content-Type": "application/json",
|
|
900
|
+
Authorization: `Bearer ${ds.sessionToken}`
|
|
901
|
+
},
|
|
902
|
+
body: JSON.stringify(state === void 0 ? { label } : { label, state })
|
|
903
|
+
});
|
|
904
|
+
} catch {
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
/**
|
|
909
|
+
* Push token usage + last-turn context size to every connected REMOTE
|
|
910
|
+
* room so the room TUI can show per-agent context size and roll up a
|
|
911
|
+
* room total. Mirrors broadcastActivity — same best-effort semantics.
|
|
912
|
+
*/
|
|
913
|
+
async broadcastMetrics(metrics) {
|
|
914
|
+
for (const conn of this._registry.values()) {
|
|
915
|
+
const ds = conn.dataSource;
|
|
916
|
+
if (!(ds instanceof RemoteRoomDataSource)) continue;
|
|
917
|
+
try {
|
|
918
|
+
await fetch(`${ds.serverUrl}/metrics`, {
|
|
919
|
+
method: "POST",
|
|
920
|
+
headers: {
|
|
921
|
+
"Content-Type": "application/json",
|
|
922
|
+
Authorization: `Bearer ${ds.sessionToken}`
|
|
923
|
+
},
|
|
924
|
+
body: JSON.stringify(metrics)
|
|
925
|
+
});
|
|
926
|
+
} catch {
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
// ── Room connection management ──────────────────────────────────────────────
|
|
931
|
+
async connectRoom(room, roomName, mode, identifier) {
|
|
932
|
+
if (this._registry.has(room.roomId)) return;
|
|
933
|
+
const channel = await room.connect(this._participantId, this._participantName, {
|
|
934
|
+
type: "agent",
|
|
935
|
+
identifier: this._options.selfIdentifier,
|
|
936
|
+
subscribe: /* @__PURE__ */ new Set([
|
|
937
|
+
EventCategory.MESSAGE,
|
|
938
|
+
EventCategory.PRESENCE,
|
|
939
|
+
EventCategory.ACTIVITY,
|
|
940
|
+
EventCategory.MENTION
|
|
941
|
+
]),
|
|
942
|
+
silent: true
|
|
943
|
+
});
|
|
944
|
+
const dataSource = new LocalRoomDataSource(room, channel);
|
|
945
|
+
const conn = { dataSource, room, channel, name: roomName, identifier };
|
|
946
|
+
this._registry.add(room.roomId, conn);
|
|
947
|
+
if (mode) this._engagement.setMode?.(room.roomId, mode);
|
|
948
|
+
const initialMode = this.getModeForRoom(room.roomId);
|
|
949
|
+
channel.emit(createEvent({
|
|
950
|
+
type: "Activity",
|
|
951
|
+
category: "ACTIVITY",
|
|
952
|
+
room_id: room.roomId,
|
|
953
|
+
participant_id: this._participantId,
|
|
954
|
+
action: "mode_changed",
|
|
955
|
+
detail: { mode: initialMode }
|
|
956
|
+
})).catch(() => {
|
|
957
|
+
});
|
|
958
|
+
this._multiplexer.addChannel(room.roomId, roomName, channel);
|
|
959
|
+
}
|
|
960
|
+
/**
|
|
961
|
+
* Connect a remote room via a RoomDataSource (no local Room/Channel).
|
|
962
|
+
*
|
|
963
|
+
* Used by the client-side agent runtime to register rooms that are
|
|
964
|
+
* accessed over HTTP. Events come from an external source (SSE multiplexer)
|
|
965
|
+
* passed to run(), not from the internal EventMultiplexer.
|
|
966
|
+
*/
|
|
967
|
+
connectRemoteRoom(dataSource, roomName, mode, identifier) {
|
|
968
|
+
if (this._registry.has(dataSource.roomId)) return;
|
|
969
|
+
const conn = {
|
|
970
|
+
dataSource,
|
|
971
|
+
name: roomName,
|
|
972
|
+
identifier
|
|
973
|
+
};
|
|
974
|
+
this._registry.add(dataSource.roomId, conn);
|
|
975
|
+
if (mode) this._engagement.setMode?.(dataSource.roomId, mode);
|
|
976
|
+
}
|
|
977
|
+
/** Disconnect a remote room (by room ID). */
|
|
978
|
+
disconnectRemoteRoom(roomId) {
|
|
979
|
+
if (!this._registry.has(roomId)) return;
|
|
980
|
+
this._registry.remove(roomId);
|
|
981
|
+
this._engagement.onRoomDisconnected?.(roomId);
|
|
982
|
+
this._buffer.delete(roomId);
|
|
983
|
+
this._roomSelfIds.delete(roomId);
|
|
984
|
+
}
|
|
985
|
+
async disconnectRoom(roomId) {
|
|
986
|
+
const conn = this._registry.get(roomId);
|
|
987
|
+
if (!conn) return;
|
|
988
|
+
this._multiplexer.removeChannel(roomId);
|
|
989
|
+
await conn.channel?.disconnect(true);
|
|
990
|
+
this._registry.remove(roomId);
|
|
991
|
+
this._engagement.onRoomDisconnected?.(roomId);
|
|
992
|
+
this._buffer.delete(roomId);
|
|
993
|
+
this._roomSelfIds.delete(roomId);
|
|
994
|
+
}
|
|
995
|
+
// ── Mode management ─────────────────────────────────────────────────────────
|
|
996
|
+
getModeForRoom(roomId) {
|
|
997
|
+
return this._engagement.getMode?.(roomId) ?? "everyone";
|
|
998
|
+
}
|
|
999
|
+
setModeForRoom(roomId, mode, notifyAgent = true) {
|
|
1000
|
+
this._engagement.setMode?.(roomId, mode);
|
|
1001
|
+
const conn = this._registry.get(roomId);
|
|
1002
|
+
if (conn) {
|
|
1003
|
+
conn.channel?.emit(createEvent({
|
|
1004
|
+
type: "Activity",
|
|
1005
|
+
category: "ACTIVITY",
|
|
1006
|
+
room_id: roomId,
|
|
1007
|
+
participant_id: this._participantId,
|
|
1008
|
+
action: "mode_changed",
|
|
1009
|
+
detail: { mode }
|
|
1010
|
+
}))?.catch(() => {
|
|
1011
|
+
});
|
|
1012
|
+
this._options.onModeChange?.(roomId, conn.name, mode);
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
// ── Log ─────────────────────────────────────────────────────────────────────
|
|
1016
|
+
getLog() {
|
|
1017
|
+
return this._log;
|
|
1018
|
+
}
|
|
1019
|
+
// ── Main event loop ─────────────────────────────────────────────────────────
|
|
1020
|
+
/**
|
|
1021
|
+
* Start the event loop.
|
|
1022
|
+
*
|
|
1023
|
+
* @param deliver — callback that receives formatted content and delivers
|
|
1024
|
+
* it to the agent. This is the consumer's responsibility. The function
|
|
1025
|
+
* should block until delivery is complete (e.g., LLM evaluation finished).
|
|
1026
|
+
* @param eventSource — optional external event source (e.g. SseMultiplexer).
|
|
1027
|
+
* If provided, events are consumed from this instead of the internal
|
|
1028
|
+
* EventMultiplexer. Used by the client-side agent runtime.
|
|
1029
|
+
* @param initialParts — optional content to deliver before entering the
|
|
1030
|
+
* event loop. Used by the runtime to deliver auto-join confirmation.
|
|
1031
|
+
*/
|
|
1032
|
+
async run(deliver, eventSource, initialParts) {
|
|
1033
|
+
this._deliver = deliver;
|
|
1034
|
+
if (initialParts && initialParts.length > 0) {
|
|
1035
|
+
await this._processRaw(
|
|
1036
|
+
initialParts,
|
|
1037
|
+
this._registry.values().next().value?.dataSource.roomId ?? null
|
|
1038
|
+
);
|
|
1039
|
+
}
|
|
1040
|
+
const source = eventSource ?? this._multiplexer;
|
|
1041
|
+
for await (const labeled of source) {
|
|
1042
|
+
if (this._stopped) break;
|
|
1043
|
+
await this._handleLabeledEvent(labeled);
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
async stop() {
|
|
1047
|
+
this._stopped = true;
|
|
1048
|
+
this._multiplexer.close();
|
|
1049
|
+
await Promise.allSettled(
|
|
1050
|
+
[...this._registry.values()].filter((conn) => conn.channel).map((conn) => conn.channel.disconnect(true))
|
|
1051
|
+
);
|
|
1052
|
+
this._registry.clear();
|
|
1053
|
+
this._buffer.clear();
|
|
1054
|
+
this._tracker.clearAll();
|
|
1055
|
+
this._refMap.clear();
|
|
1056
|
+
this._roomSelfIds.clear();
|
|
1057
|
+
this._deliver = null;
|
|
1058
|
+
}
|
|
1059
|
+
// ── Full catch-up (kept for app-path consumers) ────────────────────────────
|
|
1060
|
+
async buildFullCatchUp() {
|
|
1061
|
+
return this._buildFullCatchUp();
|
|
1062
|
+
}
|
|
1063
|
+
async _buildFullCatchUp() {
|
|
1064
|
+
const sections = ["[Session context \u2014 loaded automatically]"];
|
|
1065
|
+
const nameCounts = /* @__PURE__ */ new Map();
|
|
1066
|
+
for (const [, c] of this._registry.entries()) nameCounts.set(c.name, (nameCounts.get(c.name) ?? 0) + 1);
|
|
1067
|
+
for (const [roomId, conn] of this._registry.entries()) {
|
|
1068
|
+
const mode = this.getModeForRoom(roomId);
|
|
1069
|
+
const isDuplicate = (nameCounts.get(conn.name) ?? 0) > 1;
|
|
1070
|
+
const ref = isDuplicate ? roomId : conn.identifier ?? roomId;
|
|
1071
|
+
sections.push(`
|
|
1072
|
+
${conn.name} [${ref}] \u2014 ${mode}`);
|
|
1073
|
+
if (mode.startsWith("standby-")) {
|
|
1074
|
+
sections.push(" (standby \u2014 @mentions only)");
|
|
1075
|
+
} else {
|
|
1076
|
+
const participants = conn.dataSource.listParticipants().filter((p) => p.id !== this._participantId);
|
|
1077
|
+
if (participants.length > 0) {
|
|
1078
|
+
const pList = participants.map((p) => `${p.type} ${p.name}`).join(", ");
|
|
1079
|
+
sections.push(`Participants: ${pList}`);
|
|
1080
|
+
}
|
|
1081
|
+
const { lines } = await buildCatchUpLines(conn, {
|
|
1082
|
+
isEventSeen: (id) => this._tracker.isDelivered(id),
|
|
1083
|
+
markEventsSeen: (ids) => {
|
|
1084
|
+
this._tracker.markManyDelivered(ids);
|
|
1085
|
+
},
|
|
1086
|
+
assignRef: (id) => this.assignRef(id)
|
|
1087
|
+
});
|
|
1088
|
+
if (lines.length > 0) {
|
|
1089
|
+
for (const line of lines) sections.push(` ${line}`);
|
|
1090
|
+
} else {
|
|
1091
|
+
sections.push(" (nothing new)");
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
sections.push(
|
|
1096
|
+
"\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",
|
|
1097
|
+
"Continue immediately if you see fit, or explore further in any active room."
|
|
1098
|
+
);
|
|
1099
|
+
return [{ type: "text", text: sections.join("\n") }];
|
|
1100
|
+
}
|
|
1101
|
+
// ── Event handling ──────────────────────────────────────────────────────────
|
|
1102
|
+
async _handleLabeledEvent(labeled) {
|
|
1103
|
+
if (this._tracker.isDuplicate(labeled.event.id)) return;
|
|
1104
|
+
const { roomId, event } = labeled;
|
|
1105
|
+
const conn = this._registry.get(roomId);
|
|
1106
|
+
const senderLookupId = event.type === "Mentioned" ? event.message.sender_id : event.participant_id;
|
|
1107
|
+
const sender = conn?.dataSource.listParticipants().find((p) => p.id === senderLookupId);
|
|
1108
|
+
const senderType = sender?.type ?? "human";
|
|
1109
|
+
const selfId = this.getSelfIdForRoom(roomId);
|
|
1110
|
+
const disposition = this._engagement.classify(event, roomId, selfId, senderType, senderLookupId);
|
|
1111
|
+
if (disposition === "drop") return;
|
|
1112
|
+
this._tracker.markDelivered(event.id);
|
|
1113
|
+
if (disposition === "content") {
|
|
1114
|
+
this._buffer.push(roomId, { event, roomId, roomName: labeled.roomName });
|
|
1115
|
+
return;
|
|
1116
|
+
}
|
|
1117
|
+
this._log.push(event);
|
|
1118
|
+
if (this._processing) {
|
|
1119
|
+
this._eventQueue.push(labeled);
|
|
1120
|
+
this._formatForLLM(event, roomId, labeled.roomName).then((parts) => {
|
|
1121
|
+
if (parts) this._injectBuffer.push(parts);
|
|
1122
|
+
}).catch((err) => {
|
|
1123
|
+
console.error(`[${this._participantName}] inject buffer format error:`, err);
|
|
1124
|
+
});
|
|
1125
|
+
return;
|
|
1126
|
+
}
|
|
1127
|
+
await this._processTrigger(labeled);
|
|
1128
|
+
while (this._eventQueue.length > 0) {
|
|
1129
|
+
const queued = this._eventQueue;
|
|
1130
|
+
this._eventQueue = [];
|
|
1131
|
+
const formatted = [];
|
|
1132
|
+
let batchContextRoom = null;
|
|
1133
|
+
const roomsProcessed = /* @__PURE__ */ new Set();
|
|
1134
|
+
for (const qe of queued) {
|
|
1135
|
+
const qConn = this._registry.get(qe.roomId);
|
|
1136
|
+
const qSenderLookupId = qe.event.type === "Mentioned" ? qe.event.message.sender_id : qe.event.participant_id;
|
|
1137
|
+
const qSender = qConn?.dataSource.listParticipants().find((p) => p.id === qSenderLookupId);
|
|
1138
|
+
const qSenderType = qSender?.type ?? "human";
|
|
1139
|
+
const qSelfId = this.getSelfIdForRoom(qe.roomId);
|
|
1140
|
+
const qDisposition = this._engagement.classify(qe.event, qe.roomId, qSelfId, qSenderType, qSenderLookupId);
|
|
1141
|
+
if (qDisposition === "drop") continue;
|
|
1142
|
+
if (!roomsProcessed.has(qe.roomId)) {
|
|
1143
|
+
roomsProcessed.add(qe.roomId);
|
|
1144
|
+
const buffered = this._buffer.flush(qe.roomId);
|
|
1145
|
+
for (const item of buffered) {
|
|
1146
|
+
const parts2 = await this._formatForLLM(item.event, item.roomId, item.roomName);
|
|
1147
|
+
if (parts2) {
|
|
1148
|
+
formatted.push(parts2);
|
|
1149
|
+
if (!batchContextRoom) batchContextRoom = qe.roomId;
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
this._log.push(qe.event);
|
|
1154
|
+
const parts = await this._formatForLLM(qe.event, qe.roomId, qe.roomName);
|
|
1155
|
+
if (parts) {
|
|
1156
|
+
formatted.push(parts);
|
|
1157
|
+
if (!batchContextRoom) batchContextRoom = qe.roomId;
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
if (formatted.length > 0) {
|
|
1161
|
+
await this._processRaw(mergeParts(formatted), batchContextRoom);
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
async _processTrigger(labeled) {
|
|
1166
|
+
const { roomId, roomName, event } = labeled;
|
|
1167
|
+
const buffered = this._buffer.flush(roomId);
|
|
1168
|
+
const contentPartArrays = [];
|
|
1169
|
+
for (const item of buffered) {
|
|
1170
|
+
const parts = await this._formatForLLM(item.event, item.roomId, item.roomName);
|
|
1171
|
+
if (parts) contentPartArrays.push(parts);
|
|
1172
|
+
}
|
|
1173
|
+
if (event.type === "MessageSent") {
|
|
1174
|
+
const conn = this._registry.get(roomId);
|
|
1175
|
+
const senderLabel = conn?.dataSource.listParticipants().find((p) => p.id === event.message.sender_id)?.name ?? event.message.sender_name;
|
|
1176
|
+
const contentPreview = event.message.content.length > 60 ? event.message.content.slice(0, 57) + "..." : event.message.content;
|
|
1177
|
+
const preview = event.message.image_url && !event.message.content.trim() ? "sent an image" : contentPreview;
|
|
1178
|
+
this._registry.setLastMessage(roomId, `${senderLabel}: ${preview}`);
|
|
1179
|
+
}
|
|
1180
|
+
const triggerParts = await this._formatForLLM(event, roomId, roomName);
|
|
1181
|
+
if (!triggerParts && contentPartArrays.length === 0) return;
|
|
1182
|
+
const mergedParts = mergeParts([...contentPartArrays, ...triggerParts ? [triggerParts] : []]);
|
|
1183
|
+
await this._processRaw(mergedParts, roomId);
|
|
1184
|
+
}
|
|
1185
|
+
// ── Formatting ──────────────────────────────────────────────────────────────
|
|
1186
|
+
_resolveParticipantForRoom(roomId) {
|
|
1187
|
+
return (id) => {
|
|
1188
|
+
const conn = this._registry.get(roomId);
|
|
1189
|
+
if (!conn) return null;
|
|
1190
|
+
return conn.dataSource.listParticipants().find((p) => p.id === id) ?? null;
|
|
1191
|
+
};
|
|
1192
|
+
}
|
|
1193
|
+
async _resolveReplyContext(event, roomId) {
|
|
1194
|
+
if (event.type !== "MessageSent" && event.type !== "Mentioned") return null;
|
|
1195
|
+
const msg = event.message;
|
|
1196
|
+
if (!msg.reply_to_id) return null;
|
|
1197
|
+
const conn = this._registry.get(roomId);
|
|
1198
|
+
if (!conn) return null;
|
|
1199
|
+
const repliedTo = await conn.dataSource.getMessage(msg.reply_to_id);
|
|
1200
|
+
return repliedTo ? { senderName: repliedTo.sender_name, content: repliedTo.content } : null;
|
|
1201
|
+
}
|
|
1202
|
+
async _resolveReactionTarget(event, roomId) {
|
|
1203
|
+
if (event.type !== "ReactionAdded") return null;
|
|
1204
|
+
const conn = this._registry.get(roomId);
|
|
1205
|
+
if (!conn) return null;
|
|
1206
|
+
const target = await conn.dataSource.getMessage(event.message_id);
|
|
1207
|
+
if (!target) return null;
|
|
1208
|
+
return {
|
|
1209
|
+
senderName: target.sender_name,
|
|
1210
|
+
content: target.content,
|
|
1211
|
+
isSelf: target.sender_id === this._participantId
|
|
1212
|
+
};
|
|
1213
|
+
}
|
|
1214
|
+
async _formatForLLM(event, roomId, roomName) {
|
|
1215
|
+
const mode = this.getModeForRoom(roomId);
|
|
1216
|
+
const label = mode !== "everyone" ? `${roomName} \u2014 ${mode}` : roomName;
|
|
1217
|
+
const replyContext = await this._resolveReplyContext(event, roomId);
|
|
1218
|
+
const reactionTarget = await this._resolveReactionTarget(event, roomId);
|
|
1219
|
+
return formatEvent(
|
|
1220
|
+
event,
|
|
1221
|
+
this._resolveParticipantForRoom(roomId),
|
|
1222
|
+
replyContext,
|
|
1223
|
+
label,
|
|
1224
|
+
reactionTarget,
|
|
1225
|
+
(id) => this.assignRef(id)
|
|
1226
|
+
);
|
|
1227
|
+
}
|
|
1228
|
+
// ── Delivery ────────────────────────────────────────────────────────────────
|
|
1229
|
+
async _processRaw(parts, contextRoomId) {
|
|
1230
|
+
if (this._options.preQuery && !await this._options.preQuery()) {
|
|
1231
|
+
return;
|
|
1232
|
+
}
|
|
1233
|
+
this._processing = true;
|
|
1234
|
+
this._currentContextRoomId = contextRoomId;
|
|
1235
|
+
try {
|
|
1236
|
+
if (!this._deliver) return;
|
|
1237
|
+
await this._deliver(parts);
|
|
1238
|
+
} catch (err) {
|
|
1239
|
+
console.error(`[${this._participantName}] error:`, err);
|
|
1240
|
+
} finally {
|
|
1241
|
+
this._currentContextRoomId = null;
|
|
1242
|
+
this._processing = false;
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
};
|
|
1246
|
+
|
|
1247
|
+
// src/agent/mcp/runtime.ts
|
|
1248
|
+
import { createServer } from "http";
|
|
1249
|
+
import { z } from "zod";
|
|
1250
|
+
var RUNTIME_MODES = /* @__PURE__ */ new Set([
|
|
1251
|
+
"everyone",
|
|
1252
|
+
"people",
|
|
1253
|
+
"agents",
|
|
1254
|
+
"standby-everyone",
|
|
1255
|
+
"standby-people",
|
|
1256
|
+
"standby-agents"
|
|
1257
|
+
]);
|
|
1258
|
+
var RUNTIME_MODES_LIST = "everyone, people, agents, standby-everyone, standby-people, standby-agents";
|
|
1259
|
+
function isValidRuntimeMode(mode) {
|
|
1260
|
+
return RUNTIME_MODES.has(mode);
|
|
1261
|
+
}
|
|
1262
|
+
function formatJoinResponse(result) {
|
|
1263
|
+
const lines = [];
|
|
1264
|
+
lines.push(`Joined ${result.roomName} as "${result.agentName}" (${result.authority})`);
|
|
1265
|
+
lines.push("");
|
|
1266
|
+
lines.push("Nobody reads your terminal \u2014 only this room:");
|
|
1267
|
+
lines.push(" - Ask questions and report blockers here with send_message, or they are invisible.");
|
|
1268
|
+
lines.push(" - Don't end a turn waiting on an answer you never posted.");
|
|
1269
|
+
lines.push("");
|
|
1270
|
+
if (result.mode) {
|
|
1271
|
+
lines.push(`Mode: ${result.mode}`);
|
|
1272
|
+
const desc = MODE_DESCRIPTIONS[result.mode];
|
|
1273
|
+
if (desc) lines.push(` ${desc}`);
|
|
1274
|
+
lines.push(` Change with set_mode.`);
|
|
1275
|
+
lines.push("");
|
|
1276
|
+
}
|
|
1277
|
+
if (result.personName) {
|
|
1278
|
+
lines.push(`Person: ${result.personName}`);
|
|
1279
|
+
lines.push(` Your person's messages always reach you regardless of mode.`);
|
|
1280
|
+
lines.push("");
|
|
1281
|
+
}
|
|
1282
|
+
if (result.rules && result.rules.length > 0) {
|
|
1283
|
+
lines.push("Room rules \u2014 follow these:");
|
|
1284
|
+
result.rules.forEach((r, i) => lines.push(` ${i + 1}. ${r}`));
|
|
1285
|
+
lines.push("");
|
|
1286
|
+
}
|
|
1287
|
+
if (result.participants && result.participants.length > 0) {
|
|
1288
|
+
lines.push("Participants:");
|
|
1289
|
+
for (const p of result.participants) {
|
|
1290
|
+
lines.push(` ${p.name} (${p.authority})`);
|
|
1291
|
+
}
|
|
1292
|
+
lines.push("");
|
|
1293
|
+
}
|
|
1294
|
+
if (result.recentLines && result.recentLines.length > 0) {
|
|
1295
|
+
lines.push("Recent:");
|
|
1296
|
+
for (const line of result.recentLines) {
|
|
1297
|
+
lines.push(` ${line}`);
|
|
1298
|
+
}
|
|
1299
|
+
lines.push("");
|
|
1300
|
+
lines.push(`${result.recentLines.length} message${result.recentLines.length === 1 ? "" : "s"} shown. Use catch_up("${result.roomName}") for more.`);
|
|
1301
|
+
}
|
|
1302
|
+
return lines.join("\n");
|
|
1303
|
+
}
|
|
1304
|
+
var RUNTIME_TOOL_NAMES = [
|
|
1305
|
+
"apiary__catch_up",
|
|
1306
|
+
"apiary__search_by_text",
|
|
1307
|
+
"apiary__search_by_message",
|
|
1308
|
+
"apiary__send_message",
|
|
1309
|
+
"apiary__set_mode",
|
|
1310
|
+
"apiary__join_room",
|
|
1311
|
+
"apiary__leave_room",
|
|
1312
|
+
"apiary__ping",
|
|
1313
|
+
"apiary__wait_for_agent",
|
|
1314
|
+
"apiary__admin__set_mode_for",
|
|
1315
|
+
"apiary__admin__mute",
|
|
1316
|
+
"apiary__admin__unmute",
|
|
1317
|
+
"apiary__admin__kick",
|
|
1318
|
+
"apiary__admin__promote",
|
|
1319
|
+
"apiary__admin__demote"
|
|
1320
|
+
];
|
|
1321
|
+
function registerTools(server, opts) {
|
|
1322
|
+
const { resolver, toolOptions } = opts;
|
|
1323
|
+
server.tool(
|
|
1324
|
+
"apiary__catch_up",
|
|
1325
|
+
"List your rooms and status. Call with no arguments to see connected rooms. With a room name, returns recent activity you haven't seen.",
|
|
1326
|
+
{
|
|
1327
|
+
room: z.string().optional().describe("Room name. Omit to list all connected rooms.")
|
|
1328
|
+
},
|
|
1329
|
+
{ readOnlyHint: true },
|
|
1330
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1331
|
+
async ({ room }) => {
|
|
1332
|
+
if (!room) {
|
|
1333
|
+
const rooms = resolver.listAll();
|
|
1334
|
+
if (rooms.length === 0) {
|
|
1335
|
+
return textResult("Not connected to any rooms.");
|
|
1336
|
+
}
|
|
1337
|
+
const lines = ["Connected rooms:", ""];
|
|
1338
|
+
for (const r of rooms) {
|
|
1339
|
+
const idPart = r.identifier ? ` [${r.identifier}]` : "";
|
|
1340
|
+
lines.push(` ${r.name}${idPart} \u2014 ${r.mode} (${r.participantCount} participants)`);
|
|
1341
|
+
if (r.lastMessage) lines.push(` Last: ${r.lastMessage}`);
|
|
1342
|
+
}
|
|
1343
|
+
return textResult(lines.join("\n"));
|
|
1344
|
+
}
|
|
1345
|
+
return handleCatchUp(resolver, { room }, toolOptions);
|
|
1346
|
+
}
|
|
1347
|
+
);
|
|
1348
|
+
server.tool(
|
|
1349
|
+
"apiary__search_by_text",
|
|
1350
|
+
"Search chat history by keyword.",
|
|
1351
|
+
{
|
|
1352
|
+
room: z.string().describe("Room name"),
|
|
1353
|
+
query: z.string().describe("Keyword or phrase to search for"),
|
|
1354
|
+
count: z.number().int().min(1).max(10).default(3).optional().describe("Number of matches (default 3)"),
|
|
1355
|
+
cursor: z.string().optional().describe("Pagination cursor")
|
|
1356
|
+
},
|
|
1357
|
+
{ readOnlyHint: true },
|
|
1358
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1359
|
+
async (args) => handleSearchByText(resolver, args, toolOptions)
|
|
1360
|
+
);
|
|
1361
|
+
server.tool(
|
|
1362
|
+
"apiary__search_by_message",
|
|
1363
|
+
"Show messages around a known message ref.",
|
|
1364
|
+
{
|
|
1365
|
+
room: z.string().describe("Room name"),
|
|
1366
|
+
ref: z.string().describe("Message ref (e.g. #3847)"),
|
|
1367
|
+
direction: z.enum(["before", "after"]).default("before").optional().describe("'before' to scroll back, 'after' to scroll forward"),
|
|
1368
|
+
count: z.number().int().min(1).max(50).default(10).optional().describe("Number of messages (default 10)")
|
|
1369
|
+
},
|
|
1370
|
+
{ readOnlyHint: true },
|
|
1371
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1372
|
+
async (args) => handleSearchByMessage(resolver, args, toolOptions)
|
|
1373
|
+
);
|
|
1374
|
+
server.tool(
|
|
1375
|
+
"apiary__send_message",
|
|
1376
|
+
"Send a message to a room.",
|
|
1377
|
+
{
|
|
1378
|
+
room: z.string().describe("Room name"),
|
|
1379
|
+
content: z.string().describe("Message content. @name will notify that participant \u2014 use sparingly."),
|
|
1380
|
+
reply_to_id: z.string().optional().describe("Message ref to reply to (e.g. #3847)."),
|
|
1381
|
+
attachments: z.array(z.discriminatedUnion("type", [
|
|
1382
|
+
z.object({
|
|
1383
|
+
type: z.literal("path"),
|
|
1384
|
+
id: z.string().describe("Stable ID (use crypto.randomUUID())"),
|
|
1385
|
+
name: z.string().describe("Display filename"),
|
|
1386
|
+
mime_type: z.string().describe("MIME type, e.g. image/png"),
|
|
1387
|
+
size: z.number().optional().describe("File size in bytes"),
|
|
1388
|
+
path: z.string().describe("Absolute local filesystem path \u2014 readable by agents on the same machine")
|
|
1389
|
+
}),
|
|
1390
|
+
z.object({
|
|
1391
|
+
type: z.literal("upload"),
|
|
1392
|
+
id: z.string().describe("Attachment ID returned by POST /attachment"),
|
|
1393
|
+
name: z.string().describe("Display filename"),
|
|
1394
|
+
mime_type: z.string().describe("MIME type, e.g. image/png"),
|
|
1395
|
+
size: z.number().describe("File size in bytes"),
|
|
1396
|
+
url: z.string().describe("URL returned by POST /attachment")
|
|
1397
|
+
})
|
|
1398
|
+
])).optional().describe("Typed file/image attachments"),
|
|
1399
|
+
to: z.array(z.string()).optional().describe("Participant display names to whisper to. Others see [sender \u2192 recipient] but not the content. Omit for a public message.")
|
|
1400
|
+
},
|
|
1401
|
+
{ readOnlyHint: false, destructiveHint: false },
|
|
1402
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1403
|
+
async (args) => handleSendMessage(resolver, args, toolOptions)
|
|
1404
|
+
);
|
|
1405
|
+
server.tool(
|
|
1406
|
+
"apiary__set_mode",
|
|
1407
|
+
"Change your engagement mode. Controls which messages are pushed to you: everyone \u2014 all messages, people \u2014 human messages only, agents \u2014 agent messages only. Prefix with standby- for @mentions only.",
|
|
1408
|
+
{
|
|
1409
|
+
room: z.string().describe("Room name"),
|
|
1410
|
+
mode: z.string().describe("Engagement mode")
|
|
1411
|
+
},
|
|
1412
|
+
{ readOnlyHint: false, destructiveHint: false, idempotentHint: true },
|
|
1413
|
+
async ({ room, mode }) => {
|
|
1414
|
+
if (!opts.onSetMode) return textResult("Mode changes not supported.");
|
|
1415
|
+
if (!isValidRuntimeMode(mode)) {
|
|
1416
|
+
return textResult(`Invalid mode "${mode}". Valid modes: ${RUNTIME_MODES_LIST}.`);
|
|
1417
|
+
}
|
|
1418
|
+
const result = await opts.onSetMode(room, mode);
|
|
1419
|
+
return result.success ? textResult(`Mode set to ${mode} for [${room}].`) : textResult(result.error ?? "Failed to set mode.");
|
|
1420
|
+
}
|
|
1421
|
+
);
|
|
1422
|
+
server.tool(
|
|
1423
|
+
"apiary__join_room",
|
|
1424
|
+
"Join a room. Returns your identity, participants, mode, and recent activity.",
|
|
1425
|
+
{
|
|
1426
|
+
url: z.string().describe("Share URL to join"),
|
|
1427
|
+
alias: z.string().optional().describe("Local alias for the room (if name collides)"),
|
|
1428
|
+
name: z.string().optional().describe("Display name to use in this room (overrides default)")
|
|
1429
|
+
},
|
|
1430
|
+
{ readOnlyHint: false, destructiveHint: false },
|
|
1431
|
+
async ({ url, alias, name }) => {
|
|
1432
|
+
if (!opts.onJoinRoom) return textResult("Joining rooms not supported.");
|
|
1433
|
+
const result = await opts.onJoinRoom(url, alias, name);
|
|
1434
|
+
if (!result.success) return textResult(result.error ?? "Failed to join room.");
|
|
1435
|
+
if (result.roomName && result.agentName) {
|
|
1436
|
+
return textResult(formatJoinResponse(result));
|
|
1437
|
+
}
|
|
1438
|
+
return textResult(`Joined room successfully.`);
|
|
1439
|
+
}
|
|
1440
|
+
);
|
|
1441
|
+
server.tool(
|
|
1442
|
+
"apiary__leave_room",
|
|
1443
|
+
"Leave a room. Events stop flowing from it.",
|
|
1444
|
+
{
|
|
1445
|
+
room: z.string().describe("Room name to leave")
|
|
1446
|
+
},
|
|
1447
|
+
{ readOnlyHint: false, destructiveHint: false, idempotentHint: true },
|
|
1448
|
+
async ({ room }) => {
|
|
1449
|
+
if (!opts.onLeaveRoom) return textResult("Leaving rooms not supported.");
|
|
1450
|
+
const result = await opts.onLeaveRoom(room);
|
|
1451
|
+
return result.success ? textResult(`Left [${room}].`) : textResult(result.error ?? "Failed to leave room.");
|
|
1452
|
+
}
|
|
1453
|
+
);
|
|
1454
|
+
server.tool(
|
|
1455
|
+
"apiary__ping",
|
|
1456
|
+
"Ping a participant for a status check. Non-blocking \u2014 they'll see it as context, not an interrupt.",
|
|
1457
|
+
{
|
|
1458
|
+
room: z.string().describe("Room name"),
|
|
1459
|
+
participant: z.string().describe("Participant name to ping")
|
|
1460
|
+
},
|
|
1461
|
+
{ readOnlyHint: false, destructiveHint: false },
|
|
1462
|
+
async ({ room, participant }) => {
|
|
1463
|
+
if (!opts.onPing) return textResult("Ping not supported.");
|
|
1464
|
+
const result = await opts.onPing(room, participant);
|
|
1465
|
+
return result.success ? textResult(`Pinged ${participant} in [${room}].`) : textResult(result.error ?? "Failed to ping participant.");
|
|
1466
|
+
}
|
|
1467
|
+
);
|
|
1468
|
+
server.tool(
|
|
1469
|
+
"apiary__wait_for_agent",
|
|
1470
|
+
"Wait until another agent is idle (finished), blocked (needs a human), or working. Use this instead of re-reading the room in a loop to check on someone \u2014 polling costs a full turn each time, this costs one call. Returns as soon as they reach the state, or after the timeout with whatever state they're in; call again to keep waiting.",
|
|
1471
|
+
{
|
|
1472
|
+
room: z.string().describe("Room name"),
|
|
1473
|
+
participant: z.string().describe("Agent to wait for"),
|
|
1474
|
+
until: z.enum(["idle", "blocked", "working", "change"]).optional().describe("What to wait for. idle = they finished (default). blocked = they need a human. change = any state change."),
|
|
1475
|
+
timeout_sec: z.number().optional().describe("How long to wait, in seconds. Default 60, max 120.")
|
|
1476
|
+
},
|
|
1477
|
+
{ readOnlyHint: true, destructiveHint: false, idempotentHint: true },
|
|
1478
|
+
async ({ room, participant, until, timeout_sec }) => {
|
|
1479
|
+
if (!opts.onWaitForAgent) return textResult("Waiting not supported.");
|
|
1480
|
+
const result = await opts.onWaitForAgent(room, participant, until, timeout_sec);
|
|
1481
|
+
return result.success ? textResult(result.message ?? "Done.") : textResult(result.error ?? "Failed to wait.");
|
|
1482
|
+
}
|
|
1483
|
+
);
|
|
1484
|
+
if (can(opts.authority, "set_mode_for")) {
|
|
1485
|
+
server.tool(
|
|
1486
|
+
"apiary__admin__set_mode_for",
|
|
1487
|
+
"Admin: set engagement mode for another participant.",
|
|
1488
|
+
{
|
|
1489
|
+
room: z.string().describe("Room name"),
|
|
1490
|
+
participant: z.string().describe("Participant name"),
|
|
1491
|
+
mode: z.string().describe("Engagement mode to set")
|
|
1492
|
+
},
|
|
1493
|
+
{ readOnlyHint: false, destructiveHint: false, idempotentHint: true },
|
|
1494
|
+
async ({ room, participant, mode }) => {
|
|
1495
|
+
if (!opts.onAdminSetModeFor) return textResult("Admin mode changes not supported.");
|
|
1496
|
+
if (!isValidRuntimeMode(mode)) {
|
|
1497
|
+
return textResult(`Invalid mode "${mode}". Valid modes: ${RUNTIME_MODES_LIST}.`);
|
|
1498
|
+
}
|
|
1499
|
+
const result = await opts.onAdminSetModeFor(room, participant, mode);
|
|
1500
|
+
return result.success ? textResult(`Set ${participant}'s mode to ${mode} in [${room}].`) : textResult(result.error ?? "Failed to set mode.");
|
|
1501
|
+
}
|
|
1502
|
+
);
|
|
1503
|
+
server.tool(
|
|
1504
|
+
"apiary__admin__mute",
|
|
1505
|
+
"Admin: make a participant read-only (demote to guest).",
|
|
1506
|
+
{
|
|
1507
|
+
room: z.string().describe("Room name"),
|
|
1508
|
+
participant: z.string().describe("Participant name to mute")
|
|
1509
|
+
},
|
|
1510
|
+
{ readOnlyHint: false, destructiveHint: false, idempotentHint: true },
|
|
1511
|
+
async ({ room, participant }) => {
|
|
1512
|
+
if (!opts.onAdminMute) return textResult("Admin mute not supported.");
|
|
1513
|
+
const result = await opts.onAdminMute(room, participant);
|
|
1514
|
+
return result.success ? textResult(`Muted ${participant} in [${room}] (guest).`) : textResult(result.error ?? "Failed to mute participant.");
|
|
1515
|
+
}
|
|
1516
|
+
);
|
|
1517
|
+
server.tool(
|
|
1518
|
+
"apiary__admin__unmute",
|
|
1519
|
+
"Admin: restore a muted participant (promote to member).",
|
|
1520
|
+
{
|
|
1521
|
+
room: z.string().describe("Room name"),
|
|
1522
|
+
participant: z.string().describe("Participant name to unmute")
|
|
1523
|
+
},
|
|
1524
|
+
{ readOnlyHint: false, destructiveHint: false, idempotentHint: true },
|
|
1525
|
+
async ({ room, participant }) => {
|
|
1526
|
+
if (!opts.onAdminUnmute) return textResult("Admin unmute not supported.");
|
|
1527
|
+
const result = await opts.onAdminUnmute(room, participant);
|
|
1528
|
+
return result.success ? textResult(`Unmuted ${participant} in [${room}] (member).`) : textResult(result.error ?? "Failed to unmute participant.");
|
|
1529
|
+
}
|
|
1530
|
+
);
|
|
1531
|
+
}
|
|
1532
|
+
if (can(opts.authority, "kick")) {
|
|
1533
|
+
server.tool(
|
|
1534
|
+
"apiary__admin__kick",
|
|
1535
|
+
"Admin: kick a participant from a room.",
|
|
1536
|
+
{
|
|
1537
|
+
room: z.string().describe("Room name"),
|
|
1538
|
+
participant: z.string().describe("Participant name to kick")
|
|
1539
|
+
},
|
|
1540
|
+
{ readOnlyHint: false, destructiveHint: true },
|
|
1541
|
+
async ({ room, participant }) => {
|
|
1542
|
+
if (!opts.onAdminKick) return textResult("Admin kick not supported.");
|
|
1543
|
+
const result = await opts.onAdminKick(room, participant);
|
|
1544
|
+
return result.success ? textResult(`Kicked ${participant} from [${room}].`) : textResult(result.error ?? "Failed to kick participant.");
|
|
1545
|
+
}
|
|
1546
|
+
);
|
|
1547
|
+
server.tool(
|
|
1548
|
+
"apiary__admin__promote",
|
|
1549
|
+
"Admin: promote a participant to product owner (can mute/unmute members, set modes).",
|
|
1550
|
+
{
|
|
1551
|
+
room: z.string().describe("Room name"),
|
|
1552
|
+
participant: z.string().describe("Participant name to promote")
|
|
1553
|
+
},
|
|
1554
|
+
{ readOnlyHint: false, destructiveHint: false, idempotentHint: true },
|
|
1555
|
+
async ({ room, participant }) => {
|
|
1556
|
+
if (!opts.onAdminPromote) return textResult("Admin promote not supported.");
|
|
1557
|
+
const result = await opts.onAdminPromote(room, participant);
|
|
1558
|
+
return result.success ? textResult(`Promoted ${participant} to product owner in [${room}].`) : textResult(result.error ?? "Failed to promote participant.");
|
|
1559
|
+
}
|
|
1560
|
+
);
|
|
1561
|
+
server.tool(
|
|
1562
|
+
"apiary__admin__demote",
|
|
1563
|
+
"Admin: demote a product owner back to member.",
|
|
1564
|
+
{
|
|
1565
|
+
room: z.string().describe("Room name"),
|
|
1566
|
+
participant: z.string().describe("Participant name to demote")
|
|
1567
|
+
},
|
|
1568
|
+
{ readOnlyHint: false, destructiveHint: false, idempotentHint: true },
|
|
1569
|
+
async ({ room, participant }) => {
|
|
1570
|
+
if (!opts.onAdminDemote) return textResult("Admin demote not supported.");
|
|
1571
|
+
const result = await opts.onAdminDemote(room, participant);
|
|
1572
|
+
return result.success ? textResult(`Demoted ${participant} to member in [${room}].`) : textResult(result.error ?? "Failed to demote participant.");
|
|
1573
|
+
}
|
|
1574
|
+
);
|
|
1575
|
+
}
|
|
1576
|
+
}
|
|
1577
|
+
async function createRuntimeMcpServer(opts) {
|
|
1578
|
+
const { McpServer } = await import("@modelcontextprotocol/sdk/server/mcp.js");
|
|
1579
|
+
const { StreamableHTTPServerTransport } = await import("@modelcontextprotocol/sdk/server/streamableHttp.js");
|
|
1580
|
+
const httpServer = createServer(async (req, res) => {
|
|
1581
|
+
if (req.url !== "/mcp") {
|
|
1582
|
+
res.writeHead(404).end();
|
|
1583
|
+
return;
|
|
1584
|
+
}
|
|
1585
|
+
const reqServer = new McpServer({ name: "apiary_runtime", version: "1.0.0" });
|
|
1586
|
+
registerTools(reqServer, opts);
|
|
1587
|
+
const transport = new StreamableHTTPServerTransport({
|
|
1588
|
+
sessionIdGenerator: void 0
|
|
1589
|
+
});
|
|
1590
|
+
await reqServer.connect(transport);
|
|
1591
|
+
let body;
|
|
1592
|
+
if (req.method === "POST") {
|
|
1593
|
+
const chunks = [];
|
|
1594
|
+
for await (const chunk of req) chunks.push(chunk);
|
|
1595
|
+
try {
|
|
1596
|
+
body = JSON.parse(Buffer.concat(chunks).toString());
|
|
1597
|
+
} catch {
|
|
1598
|
+
body = void 0;
|
|
1599
|
+
}
|
|
1600
|
+
}
|
|
1601
|
+
await transport.handleRequest(req, res, body);
|
|
1602
|
+
});
|
|
1603
|
+
const port = await new Promise((resolve, reject) => {
|
|
1604
|
+
httpServer.listen(0, "127.0.0.1", () => {
|
|
1605
|
+
const addr = httpServer.address();
|
|
1606
|
+
if (addr && typeof addr === "object") resolve(addr.port);
|
|
1607
|
+
else reject(new Error("Could not determine server port"));
|
|
1608
|
+
});
|
|
1609
|
+
httpServer.once("error", reject);
|
|
1610
|
+
});
|
|
1611
|
+
const url = `http://127.0.0.1:${port}/mcp`;
|
|
1612
|
+
let stopPromise = null;
|
|
1613
|
+
const stop = () => {
|
|
1614
|
+
if (!stopPromise) {
|
|
1615
|
+
stopPromise = new Promise(
|
|
1616
|
+
(resolve, reject) => httpServer.close((err) => err ? reject(err) : resolve())
|
|
1617
|
+
);
|
|
1618
|
+
}
|
|
1619
|
+
return stopPromise;
|
|
1620
|
+
};
|
|
1621
|
+
return { url, stop };
|
|
1622
|
+
}
|
|
1623
|
+
async function createStdioRuntimeMcpServer(opts) {
|
|
1624
|
+
const { McpServer } = await import("@modelcontextprotocol/sdk/server/mcp.js");
|
|
1625
|
+
const { StdioServerTransport } = await import("@modelcontextprotocol/sdk/server/stdio.js");
|
|
1626
|
+
const mcpServer = new McpServer({ name: "apiary_runtime", version: "1.0.0" });
|
|
1627
|
+
registerTools(mcpServer, opts);
|
|
1628
|
+
const transport = new StdioServerTransport();
|
|
1629
|
+
const closed = new Promise((resolve) => {
|
|
1630
|
+
transport.onclose = () => resolve();
|
|
1631
|
+
});
|
|
1632
|
+
await mcpServer.connect(transport);
|
|
1633
|
+
const stop = async () => {
|
|
1634
|
+
await transport.close();
|
|
1635
|
+
await mcpServer.close();
|
|
1636
|
+
};
|
|
1637
|
+
return { stop, closed };
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1640
|
+
// src/agent/sse-multiplexer.ts
|
|
1641
|
+
var SseMultiplexer = class {
|
|
1642
|
+
_queue = [];
|
|
1643
|
+
_waiters = [];
|
|
1644
|
+
_connections = /* @__PURE__ */ new Map();
|
|
1645
|
+
_closed = false;
|
|
1646
|
+
/**
|
|
1647
|
+
* Add an SSE connection to a stoop server.
|
|
1648
|
+
* Starts streaming events immediately.
|
|
1649
|
+
*/
|
|
1650
|
+
addConnection(serverUrl, sessionToken, roomName, roomId) {
|
|
1651
|
+
if (this._connections.has(roomId) || this._closed) return;
|
|
1652
|
+
const abortController = new AbortController();
|
|
1653
|
+
const loopPromise = this._sseLoop(serverUrl, sessionToken, roomName, roomId, abortController.signal);
|
|
1654
|
+
this._connections.set(roomId, { serverUrl, sessionToken, roomName, roomId, abortController, loopPromise });
|
|
1655
|
+
}
|
|
1656
|
+
/** Remove a connection by room ID. */
|
|
1657
|
+
removeConnection(roomId) {
|
|
1658
|
+
const entry = this._connections.get(roomId);
|
|
1659
|
+
if (!entry) return;
|
|
1660
|
+
entry.abortController.abort();
|
|
1661
|
+
this._connections.delete(roomId);
|
|
1662
|
+
}
|
|
1663
|
+
/** Close all connections and signal the iterator to finish. */
|
|
1664
|
+
close() {
|
|
1665
|
+
this._closed = true;
|
|
1666
|
+
for (const [, entry] of this._connections) {
|
|
1667
|
+
entry.abortController.abort();
|
|
1668
|
+
}
|
|
1669
|
+
this._connections.clear();
|
|
1670
|
+
for (const waiter of this._waiters) {
|
|
1671
|
+
waiter.resolve(null);
|
|
1672
|
+
}
|
|
1673
|
+
this._waiters = [];
|
|
1674
|
+
}
|
|
1675
|
+
async _sseLoop(serverUrl, sessionToken, roomName, roomId, signal) {
|
|
1676
|
+
const INITIAL_BACKOFF = 1e3;
|
|
1677
|
+
const MAX_BACKOFF = 3e4;
|
|
1678
|
+
let backoff = INITIAL_BACKOFF;
|
|
1679
|
+
while (!signal.aborted && !this._closed) {
|
|
1680
|
+
try {
|
|
1681
|
+
const res = await fetch(`${serverUrl}/events`, {
|
|
1682
|
+
method: "POST",
|
|
1683
|
+
headers: {
|
|
1684
|
+
Accept: "text/event-stream",
|
|
1685
|
+
Authorization: `Bearer ${sessionToken}`
|
|
1686
|
+
},
|
|
1687
|
+
signal
|
|
1688
|
+
});
|
|
1689
|
+
if (!res.ok || !res.body) {
|
|
1690
|
+
throw new Error(`SSE connect failed: ${res.status}`);
|
|
1691
|
+
}
|
|
1692
|
+
backoff = INITIAL_BACKOFF;
|
|
1693
|
+
const reader = res.body.getReader();
|
|
1694
|
+
const decoder = new TextDecoder();
|
|
1695
|
+
let buffer = "";
|
|
1696
|
+
while (!signal.aborted) {
|
|
1697
|
+
const { done, value } = await reader.read();
|
|
1698
|
+
if (done) break;
|
|
1699
|
+
buffer += decoder.decode(value, { stream: true });
|
|
1700
|
+
const parts = buffer.split("\n\n");
|
|
1701
|
+
buffer = parts.pop();
|
|
1702
|
+
for (const part of parts) {
|
|
1703
|
+
const dataLine = part.split("\n").find((l) => l.startsWith("data: "));
|
|
1704
|
+
if (!dataLine) continue;
|
|
1705
|
+
try {
|
|
1706
|
+
const event = JSON.parse(dataLine.slice(6));
|
|
1707
|
+
this._push({ roomId, roomName, event });
|
|
1708
|
+
} catch {
|
|
1709
|
+
}
|
|
1710
|
+
}
|
|
1711
|
+
}
|
|
1712
|
+
} catch (err) {
|
|
1713
|
+
if (signal.aborted) break;
|
|
1714
|
+
}
|
|
1715
|
+
if (!signal.aborted && !this._closed) {
|
|
1716
|
+
await new Promise((r) => setTimeout(r, backoff));
|
|
1717
|
+
backoff = Math.min(backoff * 2, MAX_BACKOFF);
|
|
1718
|
+
}
|
|
1719
|
+
}
|
|
1720
|
+
}
|
|
1721
|
+
_push(labeled) {
|
|
1722
|
+
if (this._closed) return;
|
|
1723
|
+
if (this._waiters.length > 0) {
|
|
1724
|
+
const waiter = this._waiters.shift();
|
|
1725
|
+
waiter.resolve(labeled);
|
|
1726
|
+
} else {
|
|
1727
|
+
this._queue.push(labeled);
|
|
1728
|
+
}
|
|
1729
|
+
}
|
|
1730
|
+
[Symbol.asyncIterator]() {
|
|
1731
|
+
return {
|
|
1732
|
+
next: () => {
|
|
1733
|
+
if (this._queue.length > 0) {
|
|
1734
|
+
return Promise.resolve({ value: this._queue.shift(), done: false });
|
|
1735
|
+
}
|
|
1736
|
+
if (this._closed) {
|
|
1737
|
+
return Promise.resolve({ value: void 0, done: true });
|
|
1738
|
+
}
|
|
1739
|
+
return new Promise((resolve) => {
|
|
1740
|
+
this._waiters.push({
|
|
1741
|
+
resolve: (value) => {
|
|
1742
|
+
if (this._closed || value === null) {
|
|
1743
|
+
resolve({ value: void 0, done: true });
|
|
1744
|
+
} else {
|
|
1745
|
+
resolve({ value, done: false });
|
|
1746
|
+
}
|
|
1747
|
+
}
|
|
1748
|
+
});
|
|
1749
|
+
});
|
|
1750
|
+
}
|
|
1751
|
+
};
|
|
1752
|
+
}
|
|
1753
|
+
};
|
|
1754
|
+
|
|
1755
|
+
export {
|
|
1756
|
+
LocalRoomDataSource,
|
|
1757
|
+
parseEngagementMode,
|
|
1758
|
+
StoopsEngagement,
|
|
1759
|
+
classifyEvent,
|
|
1760
|
+
RemoteRoomDataSource,
|
|
1761
|
+
buildCatchUpLines,
|
|
1762
|
+
EventMultiplexer,
|
|
1763
|
+
RefMap,
|
|
1764
|
+
shouldReportActivity,
|
|
1765
|
+
EventProcessor,
|
|
1766
|
+
RUNTIME_TOOL_NAMES,
|
|
1767
|
+
createRuntimeMcpServer,
|
|
1768
|
+
createStdioRuntimeMcpServer,
|
|
1769
|
+
SseMultiplexer
|
|
1770
|
+
};
|
|
1771
|
+
//# sourceMappingURL=chunk-ZEFRNAX6.js.map
|