@polpo-ai/channels 0.15.63

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,806 @@
1
+ import { MemoryStateAdapter } from "@chat-adapter/state-memory";
2
+ import { Chat, } from "chat";
3
+ import { createOfficialChannelAdapter } from "./providers.js";
4
+ import { segmentChannelText } from "./response.js";
5
+ const DEFAULT_CONCURRENCY = {
6
+ debounceMs: 1_000,
7
+ maxQueueSize: 20,
8
+ onQueueFull: "drop-oldest",
9
+ queueEntryTtlMs: 120_000,
10
+ strategy: "burst",
11
+ };
12
+ const DEFAULT_DEDUPE_TTL_MS = 10 * 60_000;
13
+ export class ChannelRuntime {
14
+ entries = new Map();
15
+ pendingInstallations = new Map();
16
+ pendingTurns = new Map();
17
+ options;
18
+ constructor(options) {
19
+ if (!options.handleEvent && !options.handleTurn) {
20
+ throw new Error("ChannelRuntime requires handleEvent or handleTurn");
21
+ }
22
+ this.options = {
23
+ ...options,
24
+ idleTtlMs: options.idleTtlMs ?? 15 * 60_000,
25
+ maxInstances: options.maxInstances ?? 100,
26
+ };
27
+ }
28
+ async handleWebhook(installation, request, options) {
29
+ await this.emit({
30
+ installationId: installation.id,
31
+ name: "webhook.received",
32
+ provider: installation.provider,
33
+ });
34
+ const entry = await this.getOrCreate(installation);
35
+ entry.lastUsedAt = Date.now();
36
+ const webhook = entry.chat.webhooks[installation.provider];
37
+ if (!webhook) {
38
+ throw new Error(`Missing Chat SDK webhook for ${installation.provider}`);
39
+ }
40
+ return webhook(request, options);
41
+ }
42
+ async post(installation, threadId, result) {
43
+ const entry = await this.getOrCreate(installation);
44
+ entry.lastUsedAt = Date.now();
45
+ const thread = entry.chat.thread(threadId);
46
+ await this.deliver(installation, {
47
+ channelId: thread.channelId,
48
+ postable: thread,
49
+ threadId: thread.id,
50
+ }, typeof result === "string" ? { text: result } : result);
51
+ }
52
+ async invalidate(installationId) {
53
+ const matching = [...this.entries.values()].filter((entry) => entry.installation.id === installationId);
54
+ await Promise.all(matching.map((entry) => this.evict(entry)));
55
+ }
56
+ async shutdown() {
57
+ const entries = [...this.entries.values()];
58
+ await Promise.all(entries.map((entry) => this.evict(entry)));
59
+ }
60
+ get size() {
61
+ return this.entries.size;
62
+ }
63
+ async getOrCreate(installation) {
64
+ await this.prune();
65
+ const key = runtimeKey(installation, this.options.concurrency);
66
+ const cached = this.entries.get(key);
67
+ if (cached)
68
+ return cached;
69
+ const pending = this.pendingInstallations.get(installation.id);
70
+ if (pending) {
71
+ await pending;
72
+ return this.getOrCreate(installation);
73
+ }
74
+ const creating = this.createEntry(installation);
75
+ this.pendingInstallations.set(installation.id, creating);
76
+ try {
77
+ return await creating;
78
+ }
79
+ finally {
80
+ if (this.pendingInstallations.get(installation.id) === creating) {
81
+ this.pendingInstallations.delete(installation.id);
82
+ }
83
+ }
84
+ }
85
+ async createEntry(installation) {
86
+ const key = runtimeKey(installation, this.options.concurrency);
87
+ await this.invalidate(installation.id);
88
+ const adapter = (this.options.adapterFactory ?? createOfficialChannelAdapter)(installation);
89
+ const state = await (this.options.stateFactory?.(installation)
90
+ ?? Promise.resolve(new MemoryStateAdapter()));
91
+ const chat = new Chat({
92
+ adapters: { [installation.provider]: adapter },
93
+ concurrency: installation.concurrency
94
+ ?? this.options.concurrency
95
+ ?? DEFAULT_CONCURRENCY,
96
+ dedupeTtlMs: this.options.dedupeTtlMs,
97
+ fallbackStreamingPlaceholderText: this.options.fallbackStreamingPlaceholderText ?? null,
98
+ state,
99
+ streamingUpdateIntervalMs: this.options.streamingUpdateIntervalMs,
100
+ threadHistory: { maxMessages: 30, ttlMs: 24 * 60 * 60_000 },
101
+ userName: installation.userName ?? "polpo",
102
+ });
103
+ const handler = (thread, message, context) => this.handleMessage(installation, thread, message, context);
104
+ chat.onDirectMessage((thread, message, _channel, context) => this.handleMessage(installation, thread, message, context));
105
+ chat.onNewMention(handler);
106
+ chat.onNewMessage(/[\s\S]*/, handler);
107
+ chat.onSubscribedMessage(handler);
108
+ chat.onSlashCommand((event) => this.handleSlashCommand(installation, state, event));
109
+ if (this.options.handleEvent) {
110
+ chat.onAction((event) => this.handleAction(installation, state, event));
111
+ chat.onReaction((event) => this.handleReaction(installation, state, event));
112
+ chat.onModalSubmit((event) => this.handleModalSubmit(installation, state, event));
113
+ chat.onModalClose((event) => this.handleModalClose(installation, state, event));
114
+ chat.onOptionsLoad((event) => this.handleOptionsLoad(installation, event));
115
+ }
116
+ const entry = {
117
+ chat: chat,
118
+ installation,
119
+ key,
120
+ lastUsedAt: Date.now(),
121
+ };
122
+ this.entries.set(key, entry);
123
+ await this.emit({
124
+ installationId: installation.id,
125
+ name: "runtime.created",
126
+ provider: installation.provider,
127
+ });
128
+ return entry;
129
+ }
130
+ async handleMessage(installation, thread, message, context) {
131
+ await thread.subscribe();
132
+ const messages = await Promise.all([...(context?.skipped ?? []), message].map(mapMessage));
133
+ const messageIds = messages.map((item) => item.id);
134
+ const concurrency = installation.concurrency
135
+ ?? this.options.concurrency
136
+ ?? DEFAULT_CONCURRENCY;
137
+ const turn = {
138
+ channelId: thread.channelId,
139
+ coordination: {
140
+ grouped: messages.length > 1,
141
+ messageCount: messages.length,
142
+ messageIds,
143
+ primaryMessageId: message.id,
144
+ strategy: concurrency.strategy,
145
+ },
146
+ credentialRevision: installation.credentialRevision,
147
+ installationId: installation.id,
148
+ isDirectMessage: thread.isDM,
149
+ messages,
150
+ provider: installation.provider,
151
+ providerEventId: message.id,
152
+ threadId: thread.id,
153
+ };
154
+ await this.executeTurn(installation, {
155
+ channelId: thread.channelId,
156
+ postable: thread,
157
+ threadId: thread.id,
158
+ }, turn);
159
+ }
160
+ async handleSlashCommand(installation, state, event) {
161
+ const providerEventId = providerEventIdFor(event.raw, [
162
+ event.command,
163
+ event.text,
164
+ event.channel.id,
165
+ event.user.userId,
166
+ ]);
167
+ const accepted = await state.setIfNotExists(`polpo:slash-command:${installation.provider}:${installation.id}:${providerEventId}`, true, this.options.dedupeTtlMs ?? DEFAULT_DEDUPE_TTL_MS);
168
+ if (!accepted)
169
+ return;
170
+ if (this.options.handleEvent) {
171
+ const normalized = {
172
+ channelId: event.channel.id,
173
+ command: event.command,
174
+ credentialRevision: installation.credentialRevision,
175
+ installationId: installation.id,
176
+ openModal: event.openModal,
177
+ provider: installation.provider,
178
+ providerEventId,
179
+ raw: event.raw,
180
+ text: event.text,
181
+ threadId: event.channel.id,
182
+ type: "slash_command",
183
+ user: mapAuthor(event.user),
184
+ };
185
+ await this.executeEvent(installation, {
186
+ channelId: event.channel.id,
187
+ postable: event.channel,
188
+ threadId: event.channel.id,
189
+ }, normalized);
190
+ return;
191
+ }
192
+ const text = [event.command, event.text].filter(Boolean).join(" ").trim();
193
+ const concurrency = installation.concurrency
194
+ ?? this.options.concurrency
195
+ ?? DEFAULT_CONCURRENCY;
196
+ const turn = {
197
+ channelId: event.channel.id,
198
+ coordination: {
199
+ grouped: false,
200
+ messageCount: 1,
201
+ messageIds: [providerEventId],
202
+ primaryMessageId: providerEventId,
203
+ strategy: concurrency.strategy,
204
+ },
205
+ credentialRevision: installation.credentialRevision,
206
+ installationId: installation.id,
207
+ isDirectMessage: event.channel.isDM,
208
+ messages: [{
209
+ attachments: [],
210
+ author: mapAuthor(event.user),
211
+ edited: false,
212
+ formatted: markdownAst(text),
213
+ id: providerEventId,
214
+ isMention: true,
215
+ links: [],
216
+ raw: event.raw,
217
+ text,
218
+ timestamp: providerTimestamp(event.raw),
219
+ }],
220
+ provider: installation.provider,
221
+ providerEventId,
222
+ threadId: event.channel.id,
223
+ };
224
+ await this.executeTurn(installation, {
225
+ channelId: event.channel.id,
226
+ postable: event.channel,
227
+ threadId: event.channel.id,
228
+ }, turn);
229
+ }
230
+ async handleAction(installation, state, event) {
231
+ const providerEventId = providerEventIdFor(event.raw, [
232
+ event.actionId,
233
+ event.messageId,
234
+ event.threadId,
235
+ event.user.userId,
236
+ event.value ?? "",
237
+ ]);
238
+ if (!await this.acceptEvent(state, installation, "action", providerEventId))
239
+ return;
240
+ await this.executeEvent(installation, event.thread
241
+ ? { channelId: event.thread.channelId, postable: event.thread, threadId: event.thread.id }
242
+ : undefined, {
243
+ actionId: event.actionId,
244
+ channelId: event.thread?.channelId,
245
+ credentialRevision: installation.credentialRevision,
246
+ installationId: installation.id,
247
+ messageId: event.messageId,
248
+ openModal: event.openModal,
249
+ provider: installation.provider,
250
+ providerEventId,
251
+ raw: event.raw,
252
+ threadId: event.threadId,
253
+ triggerId: event.triggerId,
254
+ type: "action",
255
+ user: mapAuthor(event.user),
256
+ value: event.value,
257
+ });
258
+ }
259
+ async handleReaction(installation, state, event) {
260
+ const providerEventId = providerEventIdFor(event.raw, [
261
+ event.messageId,
262
+ event.threadId,
263
+ event.user.userId,
264
+ event.emoji.name,
265
+ String(event.added),
266
+ ]);
267
+ if (!await this.acceptEvent(state, installation, "reaction", providerEventId))
268
+ return;
269
+ await this.executeEvent(installation, {
270
+ channelId: event.thread.channelId,
271
+ postable: event.thread,
272
+ threadId: event.thread.id,
273
+ }, {
274
+ added: event.added,
275
+ channelId: event.thread.channelId,
276
+ credentialRevision: installation.credentialRevision,
277
+ emoji: event.emoji.name,
278
+ installationId: installation.id,
279
+ messageId: event.messageId,
280
+ provider: installation.provider,
281
+ providerEventId,
282
+ raw: event.raw,
283
+ rawEmoji: event.rawEmoji,
284
+ threadId: event.threadId,
285
+ type: "reaction",
286
+ user: mapAuthor(event.user),
287
+ });
288
+ }
289
+ async handleModalSubmit(installation, state, event) {
290
+ const providerEventId = providerEventIdFor(event.raw, [
291
+ event.callbackId,
292
+ event.viewId,
293
+ event.user.userId,
294
+ JSON.stringify(event.values),
295
+ ]);
296
+ if (!await this.acceptEvent(state, installation, "modal.submit", providerEventId)) {
297
+ return undefined;
298
+ }
299
+ const result = await this.executeEvent(installation, eventTarget(event.relatedThread, event.relatedChannel), {
300
+ callbackId: event.callbackId,
301
+ channelId: event.relatedThread?.channelId ?? event.relatedChannel?.id,
302
+ credentialRevision: installation.credentialRevision,
303
+ installationId: installation.id,
304
+ messageId: event.relatedMessage?.id,
305
+ privateMetadata: event.privateMetadata,
306
+ provider: installation.provider,
307
+ providerEventId,
308
+ raw: event.raw,
309
+ threadId: event.relatedThread?.id,
310
+ type: "modal.submit",
311
+ user: mapAuthor(event.user),
312
+ values: event.values,
313
+ viewId: event.viewId,
314
+ }, false);
315
+ return result?.modalResponse;
316
+ }
317
+ async handleModalClose(installation, state, event) {
318
+ const providerEventId = providerEventIdFor(event.raw, [
319
+ event.callbackId,
320
+ event.viewId,
321
+ event.user.userId,
322
+ ]);
323
+ if (!await this.acceptEvent(state, installation, "modal.close", providerEventId))
324
+ return;
325
+ await this.executeEvent(installation, eventTarget(event.relatedThread, event.relatedChannel), {
326
+ callbackId: event.callbackId,
327
+ channelId: event.relatedThread?.channelId ?? event.relatedChannel?.id,
328
+ credentialRevision: installation.credentialRevision,
329
+ installationId: installation.id,
330
+ messageId: event.relatedMessage?.id,
331
+ privateMetadata: event.privateMetadata,
332
+ provider: installation.provider,
333
+ providerEventId,
334
+ raw: event.raw,
335
+ threadId: event.relatedThread?.id,
336
+ type: "modal.close",
337
+ user: mapAuthor(event.user),
338
+ viewId: event.viewId,
339
+ }, false);
340
+ }
341
+ async handleOptionsLoad(installation, event) {
342
+ const result = await this.options.handleEvent?.({
343
+ actionId: event.actionId,
344
+ credentialRevision: installation.credentialRevision,
345
+ installationId: installation.id,
346
+ provider: installation.provider,
347
+ providerEventId: providerEventIdFor(event.raw, [
348
+ event.actionId,
349
+ event.user.userId,
350
+ event.query,
351
+ ]),
352
+ query: event.query,
353
+ raw: event.raw,
354
+ type: "options.load",
355
+ user: mapAuthor(event.user),
356
+ });
357
+ return result?.options;
358
+ }
359
+ async acceptEvent(state, installation, type, providerEventId) {
360
+ return state.setIfNotExists(`polpo:event:${installation.provider}:${installation.id}:${type}:${providerEventId}`, true, this.options.dedupeTtlMs ?? DEFAULT_DEDUPE_TTL_MS);
361
+ }
362
+ async executeTurn(installation, target, turn) {
363
+ const messageId = turn.providerEventId;
364
+ const event = { ...turn, type: "message" };
365
+ const execute = async () => {
366
+ await this.emit({
367
+ channelId: target.channelId,
368
+ installationId: installation.id,
369
+ messageId,
370
+ name: "turn.started",
371
+ provider: installation.provider,
372
+ threadId: target.threadId,
373
+ });
374
+ try {
375
+ if (installation.typingEnabled !== false
376
+ && await (this.options.shouldStartTyping?.(turn) ?? true)) {
377
+ try {
378
+ await target.postable.startTyping();
379
+ }
380
+ catch (error) {
381
+ await this.emit({
382
+ channelId: target.channelId,
383
+ error: errorMessage(error),
384
+ installationId: installation.id,
385
+ messageId,
386
+ name: "typing.failed",
387
+ provider: installation.provider,
388
+ threadId: target.threadId,
389
+ });
390
+ }
391
+ }
392
+ const result = this.options.handleEvent
393
+ ? await this.options.handleEvent(event)
394
+ : await this.options.handleTurn(turn);
395
+ if (result)
396
+ await this.deliver(installation, target, result, messageId);
397
+ await this.emit({
398
+ channelId: target.channelId,
399
+ installationId: installation.id,
400
+ messageId,
401
+ name: "turn.completed",
402
+ provider: installation.provider,
403
+ threadId: target.threadId,
404
+ });
405
+ }
406
+ catch (error) {
407
+ await this.emit({
408
+ channelId: target.channelId,
409
+ error: errorMessage(error),
410
+ installationId: installation.id,
411
+ messageId,
412
+ name: "turn.failed",
413
+ provider: installation.provider,
414
+ threadId: target.threadId,
415
+ });
416
+ throw error;
417
+ }
418
+ };
419
+ if (this.options.handleEvent && this.options.coordinateEvent) {
420
+ const disposition = await coordinateWithDisposition(this.options.coordinateEvent, event, execute);
421
+ if (disposition && disposition !== "executed") {
422
+ await this.emit({
423
+ channelId: turn.channelId,
424
+ installationId: installation.id,
425
+ messageId,
426
+ name: `event.${disposition}`,
427
+ provider: installation.provider,
428
+ threadId: turn.threadId,
429
+ });
430
+ }
431
+ return;
432
+ }
433
+ if (this.options.coordinateTurn) {
434
+ await this.options.coordinateTurn(turn, execute);
435
+ return;
436
+ }
437
+ await this.coordinateLocally(turn, execute);
438
+ }
439
+ async executeEvent(installation, target, event, coordinate = true) {
440
+ if (!this.options.handleEvent) {
441
+ await this.emit({
442
+ channelId: event.channelId,
443
+ installationId: installation.id,
444
+ messageId: event.providerEventId,
445
+ name: "event.unhandled",
446
+ provider: installation.provider,
447
+ threadId: event.threadId,
448
+ });
449
+ return;
450
+ }
451
+ let result = undefined;
452
+ const execute = async () => {
453
+ await this.emit({
454
+ channelId: event.channelId,
455
+ installationId: installation.id,
456
+ messageId: event.providerEventId,
457
+ name: "turn.started",
458
+ provider: installation.provider,
459
+ threadId: event.threadId,
460
+ });
461
+ try {
462
+ result = await this.options.handleEvent(event);
463
+ if (result && target && hasDeliverableOutput(result)) {
464
+ await this.deliver(installation, target, result, event.providerEventId);
465
+ }
466
+ await this.emit({
467
+ channelId: event.channelId,
468
+ installationId: installation.id,
469
+ messageId: event.providerEventId,
470
+ name: "turn.completed",
471
+ provider: installation.provider,
472
+ threadId: event.threadId,
473
+ });
474
+ }
475
+ catch (error) {
476
+ await this.emit({
477
+ channelId: event.channelId,
478
+ error: errorMessage(error),
479
+ installationId: installation.id,
480
+ messageId: event.providerEventId,
481
+ name: "turn.failed",
482
+ provider: installation.provider,
483
+ threadId: event.threadId,
484
+ });
485
+ throw error;
486
+ }
487
+ };
488
+ if (coordinate && this.options.coordinateEvent) {
489
+ const disposition = await coordinateWithDisposition(this.options.coordinateEvent, event, execute);
490
+ if (disposition && disposition !== "executed") {
491
+ await this.emit({
492
+ channelId: event.channelId,
493
+ installationId: installation.id,
494
+ messageId: event.providerEventId,
495
+ name: `event.${disposition}`,
496
+ provider: installation.provider,
497
+ threadId: event.threadId,
498
+ });
499
+ }
500
+ }
501
+ else if (coordinate) {
502
+ await this.coordinateEventLocally(event, execute);
503
+ }
504
+ else {
505
+ await execute();
506
+ }
507
+ return result;
508
+ }
509
+ async coordinateEventLocally(event, execute) {
510
+ const key = `${event.provider}:${event.installationId}:${event.threadId ?? event.providerEventId}`;
511
+ const previous = this.pendingTurns.get(key) ?? Promise.resolve();
512
+ const current = previous.catch(() => { }).then(execute);
513
+ this.pendingTurns.set(key, current);
514
+ try {
515
+ await current;
516
+ }
517
+ finally {
518
+ if (this.pendingTurns.get(key) === current)
519
+ this.pendingTurns.delete(key);
520
+ }
521
+ }
522
+ async coordinateLocally(turn, execute) {
523
+ const key = `${turn.provider}:${turn.installationId}:${turn.threadId}`;
524
+ const previous = this.pendingTurns.get(key) ?? Promise.resolve();
525
+ const current = previous.catch(() => { }).then(execute);
526
+ this.pendingTurns.set(key, current);
527
+ try {
528
+ await current;
529
+ }
530
+ finally {
531
+ if (this.pendingTurns.get(key) === current) {
532
+ this.pendingTurns.delete(key);
533
+ }
534
+ }
535
+ }
536
+ async deliver(installation, target, result, sourceMessageId) {
537
+ try {
538
+ validateTurnResult(result);
539
+ if (result.posts?.length) {
540
+ for (const post of result.posts)
541
+ await target.postable.post(post);
542
+ }
543
+ else if (result.stream && !result.files?.length && !result.text) {
544
+ await target.postable.post(result.stream);
545
+ }
546
+ else {
547
+ const streamedText = result.stream
548
+ ? await collectText(result.stream)
549
+ : "";
550
+ const segments = segmentChannelText(installation.provider, [result.text, streamedText].filter(Boolean).join(""), installation.responseDelivery);
551
+ const files = result.files?.map((file) => ({
552
+ data: file.data,
553
+ filename: file.filename,
554
+ mimeType: file.mimeType,
555
+ }));
556
+ const attachments = result.files
557
+ ?.filter((file) => file.type)
558
+ .map((file) => ({
559
+ data: file.data instanceof ArrayBuffer
560
+ ? new Blob([file.data])
561
+ : file.data,
562
+ mimeType: file.mimeType,
563
+ name: file.filename,
564
+ type: file.type,
565
+ }));
566
+ const genericFiles = result.files?.some((file) => file.type)
567
+ ? files?.filter((_file, index) => !result.files?.[index]?.type)
568
+ : files;
569
+ if (segments.length === 0 && files?.length) {
570
+ await target.postable.post({
571
+ attachments,
572
+ files: genericFiles,
573
+ markdown: "",
574
+ });
575
+ }
576
+ else {
577
+ for (const [index, text] of segments.entries()) {
578
+ if (index === 0 && files?.length) {
579
+ await target.postable.post({
580
+ attachments,
581
+ files: genericFiles,
582
+ markdown: text,
583
+ });
584
+ }
585
+ else {
586
+ await target.postable.post(text);
587
+ }
588
+ }
589
+ }
590
+ }
591
+ await this.emit({
592
+ channelId: target.channelId,
593
+ installationId: installation.id,
594
+ messageId: sourceMessageId,
595
+ name: "delivery.completed",
596
+ provider: installation.provider,
597
+ threadId: target.threadId,
598
+ });
599
+ }
600
+ catch (error) {
601
+ await this.emit({
602
+ channelId: target.channelId,
603
+ error: errorMessage(error),
604
+ installationId: installation.id,
605
+ messageId: sourceMessageId,
606
+ name: "delivery.failed",
607
+ provider: installation.provider,
608
+ threadId: target.threadId,
609
+ });
610
+ throw error;
611
+ }
612
+ }
613
+ async prune() {
614
+ const now = Date.now();
615
+ const expired = [...this.entries.values()].filter((entry) => now - entry.lastUsedAt >= this.options.idleTtlMs);
616
+ await Promise.all(expired.map((entry) => this.evict(entry)));
617
+ const overflow = this.entries.size - this.options.maxInstances + 1;
618
+ if (overflow > 0) {
619
+ const oldest = [...this.entries.values()]
620
+ .sort((a, b) => a.lastUsedAt - b.lastUsedAt)
621
+ .slice(0, overflow);
622
+ await Promise.all(oldest.map((entry) => this.evict(entry)));
623
+ }
624
+ }
625
+ async evict(entry) {
626
+ if (!this.entries.delete(entry.key))
627
+ return;
628
+ await entry.chat.shutdown();
629
+ await this.emit({
630
+ installationId: entry.installation.id,
631
+ name: "runtime.evicted",
632
+ provider: entry.installation.provider,
633
+ });
634
+ }
635
+ async emit(event) {
636
+ await this.options.onEvent?.(event);
637
+ }
638
+ }
639
+ function runtimeKey(installation, defaultConcurrency) {
640
+ const typing = installation.typingEnabled === false ? "silent" : "typing";
641
+ const concurrency = JSON.stringify(installation.concurrency ?? defaultConcurrency ?? DEFAULT_CONCURRENCY);
642
+ const responseDelivery = JSON.stringify(installation.responseDelivery ?? null);
643
+ return `${installation.provider}:${installation.id}:${installation.credentialRevision}:${typing}:${concurrency}:${responseDelivery}`;
644
+ }
645
+ async function coordinateWithDisposition(coordinator, event, execute) {
646
+ let executions = 0;
647
+ const executeOnce = async () => {
648
+ executions += 1;
649
+ if (executions > 1) {
650
+ throw new Error("Channel event coordinator attempted to execute an event more than once");
651
+ }
652
+ await execute();
653
+ };
654
+ const disposition = await coordinator(event, executeOnce);
655
+ if (disposition === "executed" && executions !== 1) {
656
+ throw new Error('Channel event coordinator returned "executed" without executing the event');
657
+ }
658
+ if (disposition
659
+ && disposition !== "executed"
660
+ && executions !== 0) {
661
+ throw new Error(`Channel event coordinator returned "${disposition}" after executing the event`);
662
+ }
663
+ if (!disposition && executions === 0) {
664
+ throw new Error("Channel event coordinator returned no disposition and did not execute the event");
665
+ }
666
+ return disposition;
667
+ }
668
+ async function mapMessage(message) {
669
+ const subject = await message.subject.catch(() => null);
670
+ return {
671
+ attachments: message.attachments.map((attachment) => ({
672
+ data: attachment.data,
673
+ fetchData: attachment.fetchData,
674
+ fetchMetadata: attachment.fetchMetadata,
675
+ height: attachment.height,
676
+ mimeType: attachment.mimeType,
677
+ name: attachment.name,
678
+ size: attachment.size,
679
+ type: attachment.type,
680
+ url: attachment.url,
681
+ width: attachment.width,
682
+ })),
683
+ author: mapAuthor(message.author),
684
+ edited: message.metadata.edited,
685
+ editedAt: message.metadata.editedAt,
686
+ formatted: message.formatted,
687
+ id: message.id,
688
+ isMention: message.isMention ?? false,
689
+ links: message.links,
690
+ raw: message.raw,
691
+ subject: subject ?? undefined,
692
+ text: message.text,
693
+ timestamp: message.metadata.dateSent,
694
+ };
695
+ }
696
+ function providerEventIdFor(raw, fallback) {
697
+ const record = asRecord(raw);
698
+ for (const key of ["id", "event_id", "trigger_id"]) {
699
+ const value = record?.[key];
700
+ if (typeof value === "string" && value.trim())
701
+ return value;
702
+ if (typeof value === "number" && Number.isFinite(value))
703
+ return String(value);
704
+ }
705
+ return `derived-${stableHash(`${stableSerialize(raw)}|${fallback.join("|")}`)}`;
706
+ }
707
+ function providerTimestamp(raw) {
708
+ const record = asRecord(raw);
709
+ for (const key of ["timestamp", "event_ts", "event_time"]) {
710
+ const value = record?.[key];
711
+ if (typeof value === "number" && Number.isFinite(value)) {
712
+ const milliseconds = value < 10_000_000_000 ? value * 1_000 : value;
713
+ const date = new Date(milliseconds);
714
+ if (!Number.isNaN(date.getTime()))
715
+ return date;
716
+ }
717
+ if (typeof value === "string" && value.trim()) {
718
+ const numeric = Number(value);
719
+ const date = Number.isFinite(numeric)
720
+ ? new Date(numeric < 10_000_000_000 ? numeric * 1_000 : numeric)
721
+ : new Date(value);
722
+ if (!Number.isNaN(date.getTime()))
723
+ return date;
724
+ }
725
+ }
726
+ return new Date();
727
+ }
728
+ function asRecord(value) {
729
+ return value !== null && typeof value === "object"
730
+ ? value
731
+ : null;
732
+ }
733
+ function errorMessage(error) {
734
+ return error instanceof Error ? error.message : String(error);
735
+ }
736
+ async function collectText(stream) {
737
+ let text = "";
738
+ for await (const chunk of stream) {
739
+ if (typeof chunk === "string") {
740
+ text += chunk;
741
+ }
742
+ else if (chunk.type === "text-delta" && "textDelta" in chunk) {
743
+ text += chunk.textDelta;
744
+ }
745
+ else if (chunk.type === "markdown_text" && "text" in chunk) {
746
+ text += chunk.text;
747
+ }
748
+ else if (chunk.type === "finish-step") {
749
+ text += "\n\n";
750
+ }
751
+ }
752
+ return text;
753
+ }
754
+ function mapAuthor(author) {
755
+ return {
756
+ email: author.email,
757
+ fullName: author.fullName,
758
+ isBot: author.isBot,
759
+ userId: author.userId,
760
+ userName: author.userName,
761
+ };
762
+ }
763
+ function markdownAst(text) {
764
+ return {
765
+ type: "root",
766
+ children: [{
767
+ type: "paragraph",
768
+ children: [{ type: "text", value: text }],
769
+ }],
770
+ };
771
+ }
772
+ function eventTarget(thread, channel) {
773
+ const postable = thread ?? channel;
774
+ if (!postable)
775
+ return undefined;
776
+ return {
777
+ channelId: thread?.channelId ?? postable.id,
778
+ postable,
779
+ threadId: thread?.id ?? postable.id,
780
+ };
781
+ }
782
+ function hasDeliverableOutput(result) {
783
+ return Boolean(result.posts?.length || result.stream || result.files?.length || result.text);
784
+ }
785
+ function validateTurnResult(result) {
786
+ if (result.posts?.length && (result.text || result.stream || result.files?.length)) {
787
+ throw new Error("Native posts cannot be combined with text, stream, or files");
788
+ }
789
+ }
790
+ function stableSerialize(value) {
791
+ if (value === null || typeof value !== "object")
792
+ return JSON.stringify(value);
793
+ if (Array.isArray(value))
794
+ return `[${value.map(stableSerialize).join(",")}]`;
795
+ const record = value;
796
+ return `{${Object.keys(record).sort().map((key) => `${JSON.stringify(key)}:${stableSerialize(record[key])}`).join(",")}}`;
797
+ }
798
+ function stableHash(value) {
799
+ let hash = 0x811c9dc5;
800
+ for (let index = 0; index < value.length; index += 1) {
801
+ hash ^= value.charCodeAt(index);
802
+ hash = Math.imul(hash, 0x01000193);
803
+ }
804
+ return (hash >>> 0).toString(36);
805
+ }
806
+ //# sourceMappingURL=runtime.js.map