@copilotkit/channels 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (156) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +204 -0
  3. package/dist/action-registry.d.ts +59 -0
  4. package/dist/action-registry.d.ts.map +1 -0
  5. package/dist/action-registry.js +217 -0
  6. package/dist/action-registry.test.d.ts +2 -0
  7. package/dist/action-registry.test.d.ts.map +1 -0
  8. package/dist/action-registry.test.js +115 -0
  9. package/dist/action-store.d.ts +21 -0
  10. package/dist/action-store.d.ts.map +1 -0
  11. package/dist/action-store.js +23 -0
  12. package/dist/action-store.test.d.ts +2 -0
  13. package/dist/action-store.test.d.ts.map +1 -0
  14. package/dist/action-store.test.js +27 -0
  15. package/dist/codec.d.ts +25 -0
  16. package/dist/codec.d.ts.map +1 -0
  17. package/dist/codec.js +1 -0
  18. package/dist/commands.d.ts +73 -0
  19. package/dist/commands.d.ts.map +1 -0
  20. package/dist/commands.js +38 -0
  21. package/dist/commands.test.d.ts +2 -0
  22. package/dist/commands.test.d.ts.map +1 -0
  23. package/dist/commands.test.js +23 -0
  24. package/dist/create-bot.d.ts +171 -0
  25. package/dist/create-bot.d.ts.map +1 -0
  26. package/dist/create-bot.foundations.test.d.ts +2 -0
  27. package/dist/create-bot.foundations.test.d.ts.map +1 -0
  28. package/dist/create-bot.foundations.test.js +147 -0
  29. package/dist/create-bot.js +576 -0
  30. package/dist/create-bot.test.d.ts +2 -0
  31. package/dist/create-bot.test.d.ts.map +1 -0
  32. package/dist/create-bot.test.js +936 -0
  33. package/dist/index.d.ts +26 -0
  34. package/dist/index.d.ts.map +1 -0
  35. package/dist/index.js +32 -0
  36. package/dist/mint-id.d.ts +3 -0
  37. package/dist/mint-id.d.ts.map +1 -0
  38. package/dist/mint-id.js +20 -0
  39. package/dist/mint-id.test.d.ts +2 -0
  40. package/dist/mint-id.test.d.ts.map +1 -0
  41. package/dist/mint-id.test.js +16 -0
  42. package/dist/modal-submit.test.d.ts +2 -0
  43. package/dist/modal-submit.test.d.ts.map +1 -0
  44. package/dist/modal-submit.test.js +68 -0
  45. package/dist/platform-adapter.d.ts +312 -0
  46. package/dist/platform-adapter.d.ts.map +1 -0
  47. package/dist/platform-adapter.js +1 -0
  48. package/dist/platform-adapter.test.d.ts +2 -0
  49. package/dist/platform-adapter.test.d.ts.map +1 -0
  50. package/dist/platform-adapter.test.js +40 -0
  51. package/dist/reactions.test.d.ts +2 -0
  52. package/dist/reactions.test.d.ts.map +1 -0
  53. package/dist/reactions.test.js +197 -0
  54. package/dist/run-loop.d.ts +41 -0
  55. package/dist/run-loop.d.ts.map +1 -0
  56. package/dist/run-loop.js +102 -0
  57. package/dist/run-loop.test.d.ts +2 -0
  58. package/dist/run-loop.test.d.ts.map +1 -0
  59. package/dist/run-loop.test.js +125 -0
  60. package/dist/standard-schema.d.ts +52 -0
  61. package/dist/standard-schema.d.ts.map +1 -0
  62. package/dist/standard-schema.js +66 -0
  63. package/dist/state/duration.d.ts +3 -0
  64. package/dist/state/duration.d.ts.map +1 -0
  65. package/dist/state/duration.js +16 -0
  66. package/dist/state/duration.test.d.ts +2 -0
  67. package/dist/state/duration.test.d.ts.map +1 -0
  68. package/dist/state/duration.test.js +13 -0
  69. package/dist/state/kv-action-store.d.ts +7 -0
  70. package/dist/state/kv-action-store.d.ts.map +1 -0
  71. package/dist/state/kv-action-store.js +9 -0
  72. package/dist/state/kv-action-store.test.d.ts +2 -0
  73. package/dist/state/kv-action-store.test.d.ts.map +1 -0
  74. package/dist/state/kv-action-store.test.js +21 -0
  75. package/dist/state/memory-store.d.ts +41 -0
  76. package/dist/state/memory-store.d.ts.map +1 -0
  77. package/dist/state/memory-store.js +110 -0
  78. package/dist/state/memory-store.test.d.ts +2 -0
  79. package/dist/state/memory-store.test.d.ts.map +1 -0
  80. package/dist/state/memory-store.test.js +22 -0
  81. package/dist/state/state-conversation-store.d.ts +14 -0
  82. package/dist/state/state-conversation-store.d.ts.map +1 -0
  83. package/dist/state/state-conversation-store.js +22 -0
  84. package/dist/state/state-conversation-store.test.d.ts +2 -0
  85. package/dist/state/state-conversation-store.test.d.ts.map +1 -0
  86. package/dist/state/state-conversation-store.test.js +58 -0
  87. package/dist/state/state-store.d.ts +63 -0
  88. package/dist/state/state-store.d.ts.map +1 -0
  89. package/dist/state/state-store.js +1 -0
  90. package/dist/telemetry/bot-telemetry.d.ts +31 -0
  91. package/dist/telemetry/bot-telemetry.d.ts.map +1 -0
  92. package/dist/telemetry/bot-telemetry.js +70 -0
  93. package/dist/telemetry/bot-telemetry.test.d.ts +2 -0
  94. package/dist/telemetry/bot-telemetry.test.d.ts.map +1 -0
  95. package/dist/telemetry/bot-telemetry.test.js +56 -0
  96. package/dist/telemetry/create-bot-telemetry.test.d.ts +2 -0
  97. package/dist/telemetry/create-bot-telemetry.test.d.ts.map +1 -0
  98. package/dist/telemetry/create-bot-telemetry.test.js +71 -0
  99. package/dist/telemetry/e2e-telemetry.test.d.ts +2 -0
  100. package/dist/telemetry/e2e-telemetry.test.d.ts.map +1 -0
  101. package/dist/telemetry/e2e-telemetry.test.js +56 -0
  102. package/dist/telemetry/events-catalog.test.d.ts +2 -0
  103. package/dist/telemetry/events-catalog.test.d.ts.map +1 -0
  104. package/dist/telemetry/events-catalog.test.js +8 -0
  105. package/dist/telemetry/install-id.d.ts +8 -0
  106. package/dist/telemetry/install-id.d.ts.map +1 -0
  107. package/dist/telemetry/install-id.js +43 -0
  108. package/dist/telemetry/install-id.test.d.ts +2 -0
  109. package/dist/telemetry/install-id.test.d.ts.map +1 -0
  110. package/dist/telemetry/install-id.test.js +43 -0
  111. package/dist/telemetry/platform-allowlist-coverage.test.d.ts +2 -0
  112. package/dist/telemetry/platform-allowlist-coverage.test.d.ts.map +1 -0
  113. package/dist/telemetry/platform-allowlist-coverage.test.js +45 -0
  114. package/dist/telemetry/sanitize-error.d.ts +3 -0
  115. package/dist/telemetry/sanitize-error.d.ts.map +1 -0
  116. package/dist/telemetry/sanitize-error.js +31 -0
  117. package/dist/telemetry/sanitize-error.test.d.ts +2 -0
  118. package/dist/telemetry/sanitize-error.test.d.ts.map +1 -0
  119. package/dist/telemetry/sanitize-error.test.js +33 -0
  120. package/dist/testing/fake-adapter.d.ts +137 -0
  121. package/dist/testing/fake-adapter.d.ts.map +1 -0
  122. package/dist/testing/fake-adapter.js +273 -0
  123. package/dist/testing/fake-agent.d.ts +28 -0
  124. package/dist/testing/fake-agent.d.ts.map +1 -0
  125. package/dist/testing/fake-agent.js +36 -0
  126. package/dist/testing/state-store-conformance.d.ts +4 -0
  127. package/dist/testing/state-store-conformance.d.ts.map +1 -0
  128. package/dist/testing/state-store-conformance.js +117 -0
  129. package/dist/thread-capabilities.test.d.ts +2 -0
  130. package/dist/thread-capabilities.test.d.ts.map +1 -0
  131. package/dist/thread-capabilities.test.js +77 -0
  132. package/dist/thread-ephemeral.test.d.ts +2 -0
  133. package/dist/thread-ephemeral.test.d.ts.map +1 -0
  134. package/dist/thread-ephemeral.test.js +45 -0
  135. package/dist/thread-reactions.test.d.ts +2 -0
  136. package/dist/thread-reactions.test.d.ts.map +1 -0
  137. package/dist/thread-reactions.test.js +38 -0
  138. package/dist/thread.d.ts +152 -0
  139. package/dist/thread.d.ts.map +1 -0
  140. package/dist/thread.js +329 -0
  141. package/dist/thread.test.d.ts +2 -0
  142. package/dist/thread.test.d.ts.map +1 -0
  143. package/dist/thread.test.js +83 -0
  144. package/dist/tools.d.ts +67 -0
  145. package/dist/tools.d.ts.map +1 -0
  146. package/dist/tools.js +38 -0
  147. package/dist/tools.test.d.ts +2 -0
  148. package/dist/tools.test.d.ts.map +1 -0
  149. package/dist/tools.test.js +31 -0
  150. package/dist/transcripts.d.ts +52 -0
  151. package/dist/transcripts.d.ts.map +1 -0
  152. package/dist/transcripts.js +67 -0
  153. package/dist/transcripts.test.d.ts +2 -0
  154. package/dist/transcripts.test.d.ts.map +1 -0
  155. package/dist/transcripts.test.js +108 -0
  156. package/package.json +72 -0
@@ -0,0 +1,576 @@
1
+ import { ActionRegistry, ActionExpiredError } from "./action-registry.js";
2
+ import { MemoryStore } from "./state/memory-store.js";
3
+ import { kvActionStore } from "./state/kv-action-store.js";
4
+ import { toAgentToolDescriptors, parseToolArgs } from "./tools.js";
5
+ import { normalizeCommandName, toCommandSpec } from "./commands.js";
6
+ import { Thread } from "./thread.js";
7
+ import { normalizeEmoji, toCanonicalEmoji, renderToIR, } from "@copilotkit/channels-ui";
8
+ import { Transcripts } from "./transcripts.js";
9
+ import { BotTelemetry } from "./telemetry/bot-telemetry.js";
10
+ import { errorClass, normalizePlatform } from "./telemetry/sanitize-error.js";
11
+ import { createRequire } from "node:module";
12
+ const pkg = createRequire(import.meta.url)("../package.json");
13
+ function storeKind(s) {
14
+ const n = s.constructor?.name;
15
+ if (n === "MemoryStore")
16
+ return "memory";
17
+ if (n === "PostgresStore")
18
+ return "postgres";
19
+ if (n === "RedisStore")
20
+ return "redis";
21
+ return "custom";
22
+ }
23
+ /** Platforms whose tokens the emoji table can normalize. */
24
+ const EMOJI_PLATFORMS = new Set([
25
+ "slack",
26
+ "discord",
27
+ "telegram",
28
+ ]);
29
+ function isEmojiPlatform(platform) {
30
+ return EMOJI_PLATFORMS.has(platform);
31
+ }
32
+ /** Build the IncomingMessage object from an IncomingTurn (shared by lock-conflict callback and handler path). */
33
+ function msgFromTurn(turn) {
34
+ return {
35
+ text: turn.userText,
36
+ contentParts: turn.contentParts,
37
+ user: turn.user ?? { id: "" },
38
+ ref: { id: "" },
39
+ platform: turn.platform,
40
+ eventId: turn.eventId,
41
+ turnId: turn.turnId,
42
+ deliveryId: turn.deliveryId,
43
+ };
44
+ }
45
+ /**
46
+ * Enforce V1 managed exclusivity: a managed adapter (`intelligenceAdapter`)
47
+ * must be the only adapter on a bot. Managed and direct delivery are
48
+ * alternative modes per platform — Intelligence holds the platform creds, or
49
+ * the runtime does, never both.
50
+ */
51
+ function assertExclusive(adapters) {
52
+ if (adapters.some((a) => a.__managed) && adapters.length > 1) {
53
+ throw new Error("intelligenceAdapter() must be the only adapter on a bot — managed and " +
54
+ "direct delivery are alternative modes. Use intelligenceAdapter() OR " +
55
+ "direct adapters (slack/discord/...), not both.");
56
+ }
57
+ }
58
+ /**
59
+ * Resolve the persistence backend at start(): an explicit `store.adapter` wins
60
+ * (silently); otherwise an adapter-provided `stateStore` is used (warning when
61
+ * more than one adapter provides one); otherwise an in-memory store.
62
+ */
63
+ function resolveBackend(explicit, adapters) {
64
+ if (explicit)
65
+ return explicit;
66
+ const providers = adapters.filter((a) => a.stateStore);
67
+ if (providers.length > 1) {
68
+ console.warn(`[bot] multiple adapters provide a state store (${providers
69
+ .map((a) => a.platform)
70
+ .join(", ")}); using "${providers[0].platform}". Pass store.adapter to choose explicitly.`);
71
+ }
72
+ return providers[0]?.stateStore ?? new MemoryStore();
73
+ }
74
+ export function createBot(opts) {
75
+ const cfg = opts.store ?? {};
76
+ if ((cfg.identity && !cfg.transcripts) ||
77
+ (!cfg.identity && cfg.transcripts)) {
78
+ throw new Error("createBot: `identity` and `transcripts` must be configured together.");
79
+ }
80
+ // Adapters can be supplied up front or added later via `bot.addAdapter`
81
+ // (before `start()`). The runtime uses the latter to attach managed delivery.
82
+ const adapters = [...(opts.adapters ?? [])];
83
+ assertExclusive(adapters);
84
+ let started = false;
85
+ // Backend, transcripts, telemetry, the action registry, and component
86
+ // registration are resolved in `start()` — not at construction — so an
87
+ // adapter added via `addAdapter` after `createBot` can still supply the
88
+ // persistence backend (see `resolveBackend`). Nothing reads these before the
89
+ // first event, which can only arrive after `start()`.
90
+ let backend;
91
+ let transcripts;
92
+ let registry;
93
+ let telemetry;
94
+ const agentFactory = (() => {
95
+ const a = opts.agent;
96
+ if (typeof a === "function")
97
+ return a;
98
+ if (a)
99
+ return () => a;
100
+ return () => {
101
+ throw new Error("createBot: no agent configured (pass `agent` to use runAgent)");
102
+ };
103
+ })();
104
+ const toolMap = new Map();
105
+ for (const t of opts.tools ?? [])
106
+ toolMap.set(t.name, t);
107
+ const context = opts.context ?? [];
108
+ const mentionHandlers = [];
109
+ const messageHandlers = [];
110
+ const threadStartedHandlers = [];
111
+ const interactionHandlers = new Map();
112
+ const interruptHandlers = new Map();
113
+ const commandHandlers = new Map();
114
+ for (const c of opts.commands ?? [])
115
+ commandHandlers.set(normalizeCommandName(c.name), c);
116
+ const reactionHandlers = [];
117
+ const modalSubmitHandlers = new Map();
118
+ const modalCloseHandlers = new Map();
119
+ const waiters = new Map();
120
+ // Recomputed on start() so tools added via bot.tool() before start are picked up.
121
+ let toolDescriptors = toAgentToolDescriptors([...toolMap.values()]);
122
+ function makeThread(adapter, replyTarget, conversationKey, extras) {
123
+ if (!backend || !registry || !telemetry) {
124
+ throw new Error("bot not started: call bot.start() before handling events");
125
+ }
126
+ const deps = {
127
+ adapter,
128
+ replyTarget,
129
+ conversationKey,
130
+ registry,
131
+ agentFactory,
132
+ tools: toolMap,
133
+ toolDescriptors,
134
+ context,
135
+ registerWaiter: (k, r) => waiters.set(k, r),
136
+ interruptHandlers,
137
+ state: backend,
138
+ stateSchema: cfg.state,
139
+ transcripts,
140
+ userKey: extras?.userKey,
141
+ message: extras?.message,
142
+ telemetry,
143
+ };
144
+ return new Thread(deps);
145
+ }
146
+ /**
147
+ * Build the context-level `openModal` closure, or `undefined` when the surface
148
+ * can't open one. Requires both `adapter.openModal` and a platform `triggerId`;
149
+ * otherwise the context omits `openModal` (callers guard `ctx.openModal?.(view)`).
150
+ */
151
+ function makeOpenModal(adapter, replyTarget, triggerId) {
152
+ if (!adapter.openModal || !triggerId)
153
+ return undefined;
154
+ return (view) => adapter.openModal(replyTarget, triggerId, renderToIR(view));
155
+ }
156
+ function makeSink(adapter) {
157
+ // backend/registry are resolved in start() before any adapter.start() runs,
158
+ // so they are always set by the time the sink receives an event.
159
+ const store = backend;
160
+ return {
161
+ async onTurn(turn) {
162
+ const lockKey = `turn:${turn.conversationKey}`;
163
+ const acquired = await store.lock.acquire(lockKey, {
164
+ ttlMs: cfg.lockTtl ?? 60_000,
165
+ });
166
+ if (!acquired) {
167
+ const decision = typeof cfg.onLockConflict === "function"
168
+ ? await cfg.onLockConflict(turn.conversationKey, msgFromTurn(turn))
169
+ : (cfg.onLockConflict ?? "drop");
170
+ if (decision === "drop")
171
+ return; // discard overlapping turn
172
+ // "force": proceed WITHOUT a lock token. Does NOT cancel the
173
+ // in-flight handler — cooperative cancellation is a future extension.
174
+ }
175
+ try {
176
+ // Dedup AFTER acquiring the lock: a turn dropped on lock-conflict must NOT burn its
177
+ // eventId, so Slack's retry can still be processed once the lock frees. (A handler
178
+ // that throws still leaves its event marked seen — dedup drops duplicate DELIVERIES,
179
+ // it is not retry-of-failed-turns.)
180
+ if (turn.eventId && !adapter.skipIngressDedup) {
181
+ const dupKey = `evt:${adapter.platform}:${turn.eventId}`;
182
+ try {
183
+ if (await store.dedup.seen(dupKey, cfg.dedupTtl ?? 300_000))
184
+ return;
185
+ }
186
+ catch (err) {
187
+ console.warn(`[bot] dedup check failed for ${adapter.platform}; processing without dedup`, err);
188
+ }
189
+ }
190
+ // Resolve cross-platform identity key (if configured) and stamp it on
191
+ // the message so handlers and transcript storage can use it. Done
192
+ // BEFORE makeThread so the thread carries the userKey + message for
193
+ // the transcript auto-bridge (runAgent({ transcript: true })).
194
+ let userKey;
195
+ if (cfg.identity) {
196
+ try {
197
+ const resolved = await cfg.identity({
198
+ adapter: adapter.platform,
199
+ author: turn.user ?? { id: "" },
200
+ message: msgFromTurn(turn),
201
+ });
202
+ userKey = resolved ?? undefined;
203
+ }
204
+ catch (err) {
205
+ console.warn(`[bot] identity resolution failed for ${adapter.platform}; continuing without userKey`, err);
206
+ }
207
+ }
208
+ const message = { ...msgFromTurn(turn), userKey };
209
+ const thread = makeThread(adapter, turn.replyTarget, turn.conversationKey, { userKey, message });
210
+ // v1 routing: there is no turn `kind`, so prefer mention handlers; if
211
+ // none are registered, fall back to message handlers. (The reference
212
+ // example registers identical handlers on both, so this avoids
213
+ // double-firing while still invoking whatever is registered.)
214
+ const handlers = mentionHandlers.length > 0 ? mentionHandlers : messageHandlers;
215
+ for (const h of handlers)
216
+ await h({ thread, message });
217
+ }
218
+ finally {
219
+ // acquired is null on "force" — naturally skips release.
220
+ if (acquired)
221
+ await store.lock.release(lockKey, acquired.token);
222
+ }
223
+ },
224
+ async onInteraction(evt) {
225
+ // Dedup guard: drop duplicate deliveries of the same event within the TTL window.
226
+ if (evt.eventId && !adapter.skipIngressDedup) {
227
+ const dupKey = `evt:${adapter.platform}:${evt.eventId}`;
228
+ try {
229
+ if (await store.dedup.seen(dupKey, cfg.dedupTtl ?? 300_000))
230
+ return;
231
+ }
232
+ catch (err) {
233
+ console.warn(`[bot] dedup check failed for ${adapter.platform}; processing without dedup`, err);
234
+ }
235
+ }
236
+ const thread = makeThread(adapter, evt.replyTarget, evt.conversationKey);
237
+ const user = evt.user ?? { id: "" };
238
+ const ctx = {
239
+ thread,
240
+ message: {
241
+ text: "",
242
+ user,
243
+ ref: evt.messageRef ?? { id: "" },
244
+ platform: adapter.platform,
245
+ },
246
+ action: { id: evt.id, value: evt.value },
247
+ values: {},
248
+ user,
249
+ platform: adapter.platform,
250
+ };
251
+ const openModal = makeOpenModal(adapter, evt.replyTarget, evt.triggerId);
252
+ if (openModal)
253
+ ctx.openModal = openModal;
254
+ // The clicked element's `value`, recovered by the registry when it
255
+ // re-renders to find the handler. Used to resolve a HITL waiter on
256
+ // platforms whose callback payload can't carry the value (Telegram),
257
+ // where `evt.value` is undefined.
258
+ let dispatchedValue;
259
+ try {
260
+ const explicit = interactionHandlers.get(evt.id);
261
+ if (explicit) {
262
+ await explicit(ctx);
263
+ }
264
+ else {
265
+ dispatchedValue = await registry.dispatch(evt.id, ctx);
266
+ }
267
+ }
268
+ catch (err) {
269
+ // v1: swallow expired-action dispatches; surface anything else.
270
+ if (!(err instanceof ActionExpiredError))
271
+ throw err;
272
+ }
273
+ // Resolve any HITL waiter awaiting a choice in this conversation. Prefer
274
+ // the value carried in the event (Slack), falling back to the value the
275
+ // registry recovered from the rendered element (Telegram).
276
+ const w = waiters.get(evt.conversationKey);
277
+ if (w) {
278
+ waiters.delete(evt.conversationKey);
279
+ w(evt.value !== undefined ? evt.value : dispatchedValue);
280
+ }
281
+ },
282
+ async onCommand(cmd) {
283
+ // Dedup guard: drop duplicate deliveries of the same event within the TTL window.
284
+ if (cmd.eventId && !adapter.skipIngressDedup) {
285
+ const dupKey = `evt:${adapter.platform}:${cmd.eventId}`;
286
+ try {
287
+ if (await store.dedup.seen(dupKey, cfg.dedupTtl ?? 300_000))
288
+ return;
289
+ }
290
+ catch (err) {
291
+ console.warn(`[bot] dedup check failed for ${adapter.platform}; processing without dedup`, err);
292
+ }
293
+ }
294
+ const command = commandHandlers.get(normalizeCommandName(cmd.command));
295
+ if (!command)
296
+ return; // unregistered command → skip
297
+ const thread = makeThread(adapter, cmd.replyTarget, cmd.conversationKey);
298
+ // Resolve typed options from any structured args the surface supplied
299
+ // (e.g. Discord); text-only surfaces (Slack) leave `options` empty and
300
+ // the handler reads `text`.
301
+ let options = {};
302
+ if (command.options && cmd.rawOptions) {
303
+ const parsed = await parseToolArgs(command.options, cmd.rawOptions);
304
+ if (parsed.ok)
305
+ options = parsed.value;
306
+ }
307
+ const ctx = {
308
+ thread,
309
+ command: normalizeCommandName(cmd.command),
310
+ text: cmd.text,
311
+ options,
312
+ user: cmd.user,
313
+ platform: cmd.platform,
314
+ };
315
+ const openModal = makeOpenModal(adapter, cmd.replyTarget, cmd.triggerId);
316
+ if (openModal)
317
+ ctx.openModal = openModal;
318
+ await command.handler(ctx);
319
+ },
320
+ async onThreadStarted(evt) {
321
+ // The adapter has already applied its static defaults (greeting /
322
+ // prompts) before emitting this, so handlers layer on top and never
323
+ // race. Zero handlers → no-op.
324
+ const thread = makeThread(adapter, evt.replyTarget, evt.conversationKey);
325
+ for (const h of threadStartedHandlers)
326
+ await h({ thread, user: evt.user });
327
+ },
328
+ async onReaction(evt) {
329
+ // Only normalize when the adapter's platform is one the emoji table
330
+ // knows; otherwise the raw token passes through unchanged.
331
+ const normalized = isEmojiPlatform(adapter.platform)
332
+ ? normalizeEmoji(evt.rawEmoji, adapter.platform)
333
+ : undefined;
334
+ const value = normalized ?? evt.rawEmoji;
335
+ const thread = makeThread(adapter, evt.replyTarget, evt.conversationKey);
336
+ // Prefer the adapter's update-capable ref; fall back to the bare id.
337
+ const messageRef = evt.messageRef ?? { id: evt.messageId };
338
+ const reactionEvt = {
339
+ emoji: value,
340
+ rawEmoji: evt.rawEmoji,
341
+ added: evt.added,
342
+ user: evt.user,
343
+ messageId: evt.messageId,
344
+ messageRef,
345
+ threadId: evt.threadId,
346
+ thread,
347
+ adapter,
348
+ raw: evt.raw,
349
+ };
350
+ for (const reg of reactionHandlers) {
351
+ if (!reg.emojis || reg.emojis.has(value))
352
+ await reg.handler(reactionEvt);
353
+ }
354
+ // Per-message handler set via `<Message onReaction>` on the posted
355
+ // message — hot cache, falling back to the durable snapshot after a restart.
356
+ const perMessage = await registry.resolveMessageReaction(evt.messageId);
357
+ if (perMessage) {
358
+ await perMessage(value, {
359
+ emoji: value,
360
+ rawEmoji: evt.rawEmoji,
361
+ added: evt.added,
362
+ user: evt.user,
363
+ messageId: evt.messageId,
364
+ thread,
365
+ messageRef,
366
+ });
367
+ }
368
+ },
369
+ async onModalSubmit(evt) {
370
+ const handler = modalSubmitHandlers.get(evt.callbackId);
371
+ if (!handler)
372
+ return; // unregistered → closes
373
+ const thread = evt.conversationKey !== undefined && evt.replyTarget !== undefined
374
+ ? makeThread(adapter, evt.replyTarget, evt.conversationKey)
375
+ : undefined;
376
+ const result = await handler({
377
+ callbackId: evt.callbackId,
378
+ values: evt.values,
379
+ user: evt.user,
380
+ thread,
381
+ privateMetadata: evt.privateMetadata,
382
+ raw: evt.raw,
383
+ });
384
+ return result ?? undefined;
385
+ },
386
+ async onModalClose(evt) {
387
+ const handler = modalCloseHandlers.get(evt.callbackId);
388
+ if (!handler)
389
+ return;
390
+ await handler({
391
+ callbackId: evt.callbackId,
392
+ user: evt.user,
393
+ privateMetadata: evt.privateMetadata,
394
+ raw: evt.raw,
395
+ });
396
+ },
397
+ };
398
+ }
399
+ const bot = {
400
+ name: opts.name,
401
+ get commandNames() {
402
+ return [...commandHandlers.keys()];
403
+ },
404
+ get transcripts() {
405
+ if (!transcripts) {
406
+ throw new Error("bot.transcripts is available after bot.start()");
407
+ }
408
+ return transcripts;
409
+ },
410
+ addAdapter(adapter) {
411
+ if (started) {
412
+ throw new Error("bot.addAdapter must be called before bot.start()");
413
+ }
414
+ assertExclusive([...adapters, adapter]);
415
+ adapters.push(adapter);
416
+ },
417
+ onMention(h) {
418
+ // The public surface narrows `thread` to StatefulThread<TState>; the
419
+ // internal arrays hold the loose `BotHandler` shape. A real Thread is
420
+ // assignable to StatefulThread<TState> (its generic setState/state
421
+ // satisfy the narrowed signatures), so the cast is sound.
422
+ mentionHandlers.push(h);
423
+ },
424
+ onMessage(h) {
425
+ messageHandlers.push(h);
426
+ },
427
+ onThreadStarted(h) {
428
+ threadStartedHandlers.push(h);
429
+ },
430
+ onInteraction(id, h) {
431
+ interactionHandlers.set(id, h);
432
+ },
433
+ onInterrupt(eventName, h) {
434
+ interruptHandlers.set(eventName, h);
435
+ },
436
+ onCommand(commandOrName, handler) {
437
+ const command = typeof commandOrName === "string"
438
+ ? { name: commandOrName, handler: handler }
439
+ : commandOrName;
440
+ commandHandlers.set(normalizeCommandName(command.name), command);
441
+ },
442
+ onReaction(emojiOrHandler, maybeHandler) {
443
+ if (typeof emojiOrHandler === "function") {
444
+ reactionHandlers.push({ handler: emojiOrHandler });
445
+ return;
446
+ }
447
+ const list = Array.isArray(emojiOrHandler)
448
+ ? emojiOrHandler
449
+ : [emojiOrHandler];
450
+ // Ingress normalizes inbound reactions to their canonical name, so
451
+ // normalize the caller's filter tokens too — otherwise a raw unicode
452
+ // ("👍") or Slack alias ("thumbsup") filter would never match the
453
+ // canonical "thumbs_up" the engine compares against. Unknown/custom
454
+ // tokens pass through unchanged.
455
+ const emojis = new Set(list.map((e) => toCanonicalEmoji(e)));
456
+ reactionHandlers.push({ emojis, handler: maybeHandler });
457
+ },
458
+ onModalSubmit(callbackId, handler) {
459
+ modalSubmitHandlers.set(callbackId, handler);
460
+ },
461
+ onModalClose(callbackId, handler) {
462
+ modalCloseHandlers.set(callbackId, handler);
463
+ },
464
+ tool(t) {
465
+ toolMap.set(t.name, t);
466
+ },
467
+ async start() {
468
+ // Idempotent: a second start() must not re-resolve the backend and
469
+ // rebuild Transcripts/Telemetry/ActionRegistry or re-call adapter.start()
470
+ // — with a MemoryStore that wipes all lock/dedup/transcript/action state,
471
+ // and real adapters would connect/port-bind twice.
472
+ if (started)
473
+ return;
474
+ started = true;
475
+ assertExclusive(adapters);
476
+ // Resolve persistence now that all adapters (including any attached via
477
+ // addAdapter) are known, then build the transcript store, action
478
+ // registry, and register components against it.
479
+ backend = resolveBackend(cfg.adapter, adapters);
480
+ transcripts = new Transcripts(backend, cfg.transcripts ?? {});
481
+ const tel = new BotTelemetry({
482
+ backend,
483
+ packageName: pkg.name,
484
+ packageVersion: pkg.version,
485
+ });
486
+ telemetry = tel;
487
+ const registryInstance = new ActionRegistry({
488
+ store: opts.actionStore ?? kvActionStore(backend),
489
+ });
490
+ registry = registryInstance;
491
+ for (const c of opts.components ?? []) {
492
+ if (!c.name) {
493
+ console.warn("[bot] createBot: skipping anonymous component — give it a name to enable durable actions after restart.");
494
+ continue;
495
+ }
496
+ registryInstance.registerComponent(c.name, c);
497
+ }
498
+ toolDescriptors = toAgentToolDescriptors([...toolMap.values()]);
499
+ tel.capture("oss.bot.configured", {
500
+ platforms: adapters.map((a) => normalizePlatform(a.platform)),
501
+ adapterCount: adapters.length,
502
+ store: storeKind(backend),
503
+ hasComponents: (opts.components?.length ?? 0) > 0,
504
+ componentsCount: opts.components?.length ?? 0,
505
+ toolsCount: toolMap.size,
506
+ commandsCount: commandHandlers.size,
507
+ contextCount: context.length,
508
+ transcripts: !!cfg.transcripts,
509
+ identity: !!cfg.identity,
510
+ });
511
+ // Isolate per-adapter startup failures: one adapter rejecting (e.g.
512
+ // Telegram's setMyCommands rejecting a hyphenated command name, a revoked
513
+ // token, a port already in use) must NOT crash the bot or prevent the
514
+ // other adapters from starting. Log + degrade, never throw.
515
+ const startResults = await Promise.allSettled(adapters.map((a) => a.start(makeSink(a), { botName: opts.name })));
516
+ const startedPlatforms = [];
517
+ const failedPlatforms = [];
518
+ startResults.forEach((r, i) => {
519
+ const rawPlatform = adapters[i].platform;
520
+ // Raw label for the human-facing log; normalized label for telemetry.
521
+ const platform = normalizePlatform(rawPlatform);
522
+ if (r.status === "rejected") {
523
+ failedPlatforms.push(platform);
524
+ console.error(`[bot] adapter "${rawPlatform}" failed to start:`, r.reason);
525
+ tel.capture("oss.bot.start_failed", {
526
+ platform,
527
+ errorClass: errorClass(r.reason),
528
+ });
529
+ }
530
+ else {
531
+ startedPlatforms.push(platform);
532
+ }
533
+ });
534
+ if (startedPlatforms.length > 0) {
535
+ tel.capture("oss.bot.started", {
536
+ platforms: startedPlatforms,
537
+ startedCount: startedPlatforms.length,
538
+ failedCount: failedPlatforms.length,
539
+ hasMentionHandler: mentionHandlers.length > 0,
540
+ hasMessageHandler: messageHandlers.length > 0,
541
+ interruptHandlers: interruptHandlers.size,
542
+ commandsCount: commandHandlers.size,
543
+ toolsCount: toolMap.size,
544
+ });
545
+ }
546
+ // Hand declared commands to adapters that register them up front (e.g.
547
+ // Discord); adapters without `registerCommands` are skipped. Per-adapter
548
+ // failures are isolated the same way as start().
549
+ const commandSpecs = [...commandHandlers.values()].map(toCommandSpec);
550
+ if (commandSpecs.length > 0) {
551
+ const registerResults = await Promise.allSettled(adapters.map((a) => a.registerCommands?.(commandSpecs)));
552
+ registerResults.forEach((r, i) => {
553
+ if (r.status === "rejected") {
554
+ console.error(`[bot] adapter "${adapters[i].platform}" failed to register commands:`, r.reason);
555
+ }
556
+ });
557
+ }
558
+ },
559
+ async stop() {
560
+ // Clear the started flag so a later start() is a real restart (re-resolve
561
+ // backend, rebuild components, reconnect adapters) rather than a silent
562
+ // no-op. The idempotency guard in start() only exists to block a DOUBLE
563
+ // start() while running — not a legitimate start→stop→start cycle.
564
+ started = false;
565
+ // Isolate per-adapter shutdown failures: one adapter's stop() rejecting
566
+ // must not prevent the others from being stopped.
567
+ const stopResults = await Promise.allSettled(adapters.map((a) => a.stop()));
568
+ stopResults.forEach((r, i) => {
569
+ if (r.status === "rejected") {
570
+ console.error(`[bot] adapter "${adapters[i].platform}" failed to stop:`, r.reason);
571
+ }
572
+ });
573
+ },
574
+ };
575
+ return bot;
576
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=create-bot.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"create-bot.test.d.ts","sourceRoot":"","sources":["../src/create-bot.test.ts"],"names":[],"mappings":""}