@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.
- package/LICENSE +21 -0
- package/README.md +204 -0
- package/dist/action-registry.d.ts +59 -0
- package/dist/action-registry.d.ts.map +1 -0
- package/dist/action-registry.js +217 -0
- package/dist/action-registry.test.d.ts +2 -0
- package/dist/action-registry.test.d.ts.map +1 -0
- package/dist/action-registry.test.js +115 -0
- package/dist/action-store.d.ts +21 -0
- package/dist/action-store.d.ts.map +1 -0
- package/dist/action-store.js +23 -0
- package/dist/action-store.test.d.ts +2 -0
- package/dist/action-store.test.d.ts.map +1 -0
- package/dist/action-store.test.js +27 -0
- package/dist/codec.d.ts +25 -0
- package/dist/codec.d.ts.map +1 -0
- package/dist/codec.js +1 -0
- package/dist/commands.d.ts +73 -0
- package/dist/commands.d.ts.map +1 -0
- package/dist/commands.js +38 -0
- package/dist/commands.test.d.ts +2 -0
- package/dist/commands.test.d.ts.map +1 -0
- package/dist/commands.test.js +23 -0
- package/dist/create-bot.d.ts +171 -0
- package/dist/create-bot.d.ts.map +1 -0
- package/dist/create-bot.foundations.test.d.ts +2 -0
- package/dist/create-bot.foundations.test.d.ts.map +1 -0
- package/dist/create-bot.foundations.test.js +147 -0
- package/dist/create-bot.js +576 -0
- package/dist/create-bot.test.d.ts +2 -0
- package/dist/create-bot.test.d.ts.map +1 -0
- package/dist/create-bot.test.js +936 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +32 -0
- package/dist/mint-id.d.ts +3 -0
- package/dist/mint-id.d.ts.map +1 -0
- package/dist/mint-id.js +20 -0
- package/dist/mint-id.test.d.ts +2 -0
- package/dist/mint-id.test.d.ts.map +1 -0
- package/dist/mint-id.test.js +16 -0
- package/dist/modal-submit.test.d.ts +2 -0
- package/dist/modal-submit.test.d.ts.map +1 -0
- package/dist/modal-submit.test.js +68 -0
- package/dist/platform-adapter.d.ts +312 -0
- package/dist/platform-adapter.d.ts.map +1 -0
- package/dist/platform-adapter.js +1 -0
- package/dist/platform-adapter.test.d.ts +2 -0
- package/dist/platform-adapter.test.d.ts.map +1 -0
- package/dist/platform-adapter.test.js +40 -0
- package/dist/reactions.test.d.ts +2 -0
- package/dist/reactions.test.d.ts.map +1 -0
- package/dist/reactions.test.js +197 -0
- package/dist/run-loop.d.ts +41 -0
- package/dist/run-loop.d.ts.map +1 -0
- package/dist/run-loop.js +102 -0
- package/dist/run-loop.test.d.ts +2 -0
- package/dist/run-loop.test.d.ts.map +1 -0
- package/dist/run-loop.test.js +125 -0
- package/dist/standard-schema.d.ts +52 -0
- package/dist/standard-schema.d.ts.map +1 -0
- package/dist/standard-schema.js +66 -0
- package/dist/state/duration.d.ts +3 -0
- package/dist/state/duration.d.ts.map +1 -0
- package/dist/state/duration.js +16 -0
- package/dist/state/duration.test.d.ts +2 -0
- package/dist/state/duration.test.d.ts.map +1 -0
- package/dist/state/duration.test.js +13 -0
- package/dist/state/kv-action-store.d.ts +7 -0
- package/dist/state/kv-action-store.d.ts.map +1 -0
- package/dist/state/kv-action-store.js +9 -0
- package/dist/state/kv-action-store.test.d.ts +2 -0
- package/dist/state/kv-action-store.test.d.ts.map +1 -0
- package/dist/state/kv-action-store.test.js +21 -0
- package/dist/state/memory-store.d.ts +41 -0
- package/dist/state/memory-store.d.ts.map +1 -0
- package/dist/state/memory-store.js +110 -0
- package/dist/state/memory-store.test.d.ts +2 -0
- package/dist/state/memory-store.test.d.ts.map +1 -0
- package/dist/state/memory-store.test.js +22 -0
- package/dist/state/state-conversation-store.d.ts +14 -0
- package/dist/state/state-conversation-store.d.ts.map +1 -0
- package/dist/state/state-conversation-store.js +22 -0
- package/dist/state/state-conversation-store.test.d.ts +2 -0
- package/dist/state/state-conversation-store.test.d.ts.map +1 -0
- package/dist/state/state-conversation-store.test.js +58 -0
- package/dist/state/state-store.d.ts +63 -0
- package/dist/state/state-store.d.ts.map +1 -0
- package/dist/state/state-store.js +1 -0
- package/dist/telemetry/bot-telemetry.d.ts +31 -0
- package/dist/telemetry/bot-telemetry.d.ts.map +1 -0
- package/dist/telemetry/bot-telemetry.js +70 -0
- package/dist/telemetry/bot-telemetry.test.d.ts +2 -0
- package/dist/telemetry/bot-telemetry.test.d.ts.map +1 -0
- package/dist/telemetry/bot-telemetry.test.js +56 -0
- package/dist/telemetry/create-bot-telemetry.test.d.ts +2 -0
- package/dist/telemetry/create-bot-telemetry.test.d.ts.map +1 -0
- package/dist/telemetry/create-bot-telemetry.test.js +71 -0
- package/dist/telemetry/e2e-telemetry.test.d.ts +2 -0
- package/dist/telemetry/e2e-telemetry.test.d.ts.map +1 -0
- package/dist/telemetry/e2e-telemetry.test.js +56 -0
- package/dist/telemetry/events-catalog.test.d.ts +2 -0
- package/dist/telemetry/events-catalog.test.d.ts.map +1 -0
- package/dist/telemetry/events-catalog.test.js +8 -0
- package/dist/telemetry/install-id.d.ts +8 -0
- package/dist/telemetry/install-id.d.ts.map +1 -0
- package/dist/telemetry/install-id.js +43 -0
- package/dist/telemetry/install-id.test.d.ts +2 -0
- package/dist/telemetry/install-id.test.d.ts.map +1 -0
- package/dist/telemetry/install-id.test.js +43 -0
- package/dist/telemetry/platform-allowlist-coverage.test.d.ts +2 -0
- package/dist/telemetry/platform-allowlist-coverage.test.d.ts.map +1 -0
- package/dist/telemetry/platform-allowlist-coverage.test.js +45 -0
- package/dist/telemetry/sanitize-error.d.ts +3 -0
- package/dist/telemetry/sanitize-error.d.ts.map +1 -0
- package/dist/telemetry/sanitize-error.js +31 -0
- package/dist/telemetry/sanitize-error.test.d.ts +2 -0
- package/dist/telemetry/sanitize-error.test.d.ts.map +1 -0
- package/dist/telemetry/sanitize-error.test.js +33 -0
- package/dist/testing/fake-adapter.d.ts +137 -0
- package/dist/testing/fake-adapter.d.ts.map +1 -0
- package/dist/testing/fake-adapter.js +273 -0
- package/dist/testing/fake-agent.d.ts +28 -0
- package/dist/testing/fake-agent.d.ts.map +1 -0
- package/dist/testing/fake-agent.js +36 -0
- package/dist/testing/state-store-conformance.d.ts +4 -0
- package/dist/testing/state-store-conformance.d.ts.map +1 -0
- package/dist/testing/state-store-conformance.js +117 -0
- package/dist/thread-capabilities.test.d.ts +2 -0
- package/dist/thread-capabilities.test.d.ts.map +1 -0
- package/dist/thread-capabilities.test.js +77 -0
- package/dist/thread-ephemeral.test.d.ts +2 -0
- package/dist/thread-ephemeral.test.d.ts.map +1 -0
- package/dist/thread-ephemeral.test.js +45 -0
- package/dist/thread-reactions.test.d.ts +2 -0
- package/dist/thread-reactions.test.d.ts.map +1 -0
- package/dist/thread-reactions.test.js +38 -0
- package/dist/thread.d.ts +152 -0
- package/dist/thread.d.ts.map +1 -0
- package/dist/thread.js +329 -0
- package/dist/thread.test.d.ts +2 -0
- package/dist/thread.test.d.ts.map +1 -0
- package/dist/thread.test.js +83 -0
- package/dist/tools.d.ts +67 -0
- package/dist/tools.d.ts.map +1 -0
- package/dist/tools.js +38 -0
- package/dist/tools.test.d.ts +2 -0
- package/dist/tools.test.d.ts.map +1 -0
- package/dist/tools.test.js +31 -0
- package/dist/transcripts.d.ts +52 -0
- package/dist/transcripts.d.ts.map +1 -0
- package/dist/transcripts.js +67 -0
- package/dist/transcripts.test.d.ts +2 -0
- package/dist/transcripts.test.d.ts.map +1 -0
- package/dist/transcripts.test.js +108 -0
- 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 @@
|
|
|
1
|
+
{"version":3,"file":"create-bot.test.d.ts","sourceRoot":"","sources":["../src/create-bot.test.ts"],"names":[],"mappings":""}
|