@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.
- package/LICENSE +13 -0
- package/README.md +205 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/dist/providers.d.ts +5 -0
- package/dist/providers.d.ts.map +1 -0
- package/dist/providers.js +109 -0
- package/dist/providers.js.map +1 -0
- package/dist/response.d.ts +5 -0
- package/dist/response.d.ts.map +1 -0
- package/dist/response.js +115 -0
- package/dist/response.js.map +1 -0
- package/dist/runtime.d.ts +32 -0
- package/dist/runtime.d.ts.map +1 -0
- package/dist/runtime.js +806 -0
- package/dist/runtime.js.map +1 -0
- package/dist/types.d.ts +240 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +7 -0
- package/dist/types.js.map +1 -0
- package/dist/webhook.d.ts +17 -0
- package/dist/webhook.d.ts.map +1 -0
- package/dist/webhook.js +28 -0
- package/dist/webhook.js.map +1 -0
- package/package.json +64 -0
package/dist/runtime.js
ADDED
|
@@ -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
|