@moda-labs/bobi-events-core 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 +201 -0
- package/README.md +32 -0
- package/dist/adapters/chat-sdk-slack.d.ts +18 -0
- package/dist/adapters/chat-sdk-slack.d.ts.map +1 -0
- package/dist/adapters/chat-sdk-slack.js +201 -0
- package/dist/adapters/chat-sdk-slack.js.map +1 -0
- package/dist/adapters/github.d.ts +3 -0
- package/dist/adapters/github.d.ts.map +1 -0
- package/dist/adapters/github.js +114 -0
- package/dist/adapters/github.js.map +1 -0
- package/dist/adapters/linear.d.ts +3 -0
- package/dist/adapters/linear.d.ts.map +1 -0
- package/dist/adapters/linear.js +47 -0
- package/dist/adapters/linear.js.map +1 -0
- package/dist/adapters/whatsapp.d.ts +25 -0
- package/dist/adapters/whatsapp.d.ts.map +1 -0
- package/dist/adapters/whatsapp.js +96 -0
- package/dist/adapters/whatsapp.js.map +1 -0
- package/dist/channels.d.ts +80 -0
- package/dist/channels.d.ts.map +1 -0
- package/dist/channels.js +499 -0
- package/dist/channels.js.map +1 -0
- package/dist/circuit-breaker.d.ts +79 -0
- package/dist/circuit-breaker.d.ts.map +1 -0
- package/dist/circuit-breaker.js +288 -0
- package/dist/circuit-breaker.js.map +1 -0
- package/dist/conversation.d.ts +29 -0
- package/dist/conversation.d.ts.map +1 -0
- package/dist/conversation.js +86 -0
- package/dist/conversation.js.map +1 -0
- package/dist/core.d.ts +254 -0
- package/dist/core.d.ts.map +1 -0
- package/dist/core.js +1775 -0
- package/dist/core.js.map +1 -0
- package/package.json +40 -0
- package/src/adapters/chat-sdk-slack.ts +209 -0
- package/src/adapters/github.ts +111 -0
- package/src/adapters/linear.ts +53 -0
- package/src/adapters/whatsapp.ts +120 -0
- package/src/channels.ts +600 -0
- package/src/circuit-breaker.ts +337 -0
- package/src/conversation.ts +96 -0
- package/src/core.ts +2413 -0
package/dist/channels.js
ADDED
|
@@ -0,0 +1,499 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Channel adapters - outbound contract for the channel gateway (#190 Phase 2).
|
|
3
|
+
*
|
|
4
|
+
* Each chat channel is one adapter implementing this contract, registered in
|
|
5
|
+
* CHANNEL_ADAPTERS. The generic /channels/* handlers in core.ts parse a
|
|
6
|
+
* conversation reference, resolve the sending credential, and delegate here;
|
|
7
|
+
* they never learn platform semantics. Capability degradation is the
|
|
8
|
+
* gateway's job: an `update` on a channel without edit support becomes a
|
|
9
|
+
* follow-up post, `typing` on a channel without indicators is a silent no-op.
|
|
10
|
+
*
|
|
11
|
+
* The Slack adapter is built on the Chat SDK's api module
|
|
12
|
+
* (@chat-adapter/slack/api) - the same package whose webhook parser serves
|
|
13
|
+
* inbound (adapters/chat-sdk-slack.ts). Outbound text arrives as markdown and
|
|
14
|
+
* is delivered natively via Slack's `markdown_text`, so callers never convert
|
|
15
|
+
* client-side.
|
|
16
|
+
*/
|
|
17
|
+
import { postSlackMessage, updateSlackMessage, uploadSlackFiles, fetchSlackThreadReplies, callSlackApi, SlackApiError, } from "@chat-adapter/slack/api";
|
|
18
|
+
// Slack Web API base. Overridable so integration tests can stub the Slack
|
|
19
|
+
// API with a local server (local.ts wires BOBI_ES_SLACK_API_URL); production
|
|
20
|
+
// always uses the default. Must end with "/" - the SDK resolves method names
|
|
21
|
+
// relative to it.
|
|
22
|
+
const DEFAULT_SLACK_API_URL = "https://slack.com/api/";
|
|
23
|
+
let slackApiUrlOverride;
|
|
24
|
+
export function setSlackApiUrl(url) {
|
|
25
|
+
slackApiUrlOverride = url ? (url.endsWith("/") ? url : `${url}/`) : undefined;
|
|
26
|
+
}
|
|
27
|
+
export function slackApiUrl() {
|
|
28
|
+
return slackApiUrlOverride ?? DEFAULT_SLACK_API_URL;
|
|
29
|
+
}
|
|
30
|
+
// Meta Graph API base for WhatsApp Cloud API calls. Overridable so tests can
|
|
31
|
+
// stub the Graph API with a local server (local.ts wires
|
|
32
|
+
// BOBI_ES_WHATSAPP_API_URL); production always uses the default.
|
|
33
|
+
const DEFAULT_WHATSAPP_API_URL = "https://graph.facebook.com/v23.0/";
|
|
34
|
+
let whatsappApiUrlOverride;
|
|
35
|
+
export function setWhatsAppApiUrl(url) {
|
|
36
|
+
whatsappApiUrlOverride = url ? (url.endsWith("/") ? url : `${url}/`) : undefined;
|
|
37
|
+
}
|
|
38
|
+
export function whatsappApiUrl() {
|
|
39
|
+
return whatsappApiUrlOverride ?? DEFAULT_WHATSAPP_API_URL;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Outbound budget enforcement (#651). Terminal sends (post / final) go out
|
|
43
|
+
* whole as natural-boundary chunks via chunkForChannel; streaming updates
|
|
44
|
+
* rewrite one message in place, so they stay on truncateForChannel - chunking
|
|
45
|
+
* a rewrite would post a new message on every tick.
|
|
46
|
+
*/
|
|
47
|
+
const TRUNCATION_MARKER = "\n_(truncated)_";
|
|
48
|
+
/** Runaway guard: past this many chunks the tail is truncated with the marker. */
|
|
49
|
+
const MAX_CHUNKS = 8;
|
|
50
|
+
/** Appended to a chunk that splits inside a code fence, so it renders closed. */
|
|
51
|
+
const FENCE_CLOSE = "\n```";
|
|
52
|
+
/** Length of `s` in the channel's declared unit. */
|
|
53
|
+
function unitLength(s, caps) {
|
|
54
|
+
if (caps.lengthUnit === "utf16")
|
|
55
|
+
return s.length;
|
|
56
|
+
let n = 0;
|
|
57
|
+
for (const _ch of s)
|
|
58
|
+
n++;
|
|
59
|
+
return n;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* UTF-16 index covering at most `units` of `s` in the channel's unit, never
|
|
63
|
+
* splitting a surrogate pair.
|
|
64
|
+
*/
|
|
65
|
+
function unitIndex(s, units, caps) {
|
|
66
|
+
if (units <= 0)
|
|
67
|
+
return 0;
|
|
68
|
+
let idx;
|
|
69
|
+
if (caps.lengthUnit === "utf16") {
|
|
70
|
+
idx = Math.min(units, s.length);
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
idx = 0;
|
|
74
|
+
let counted = 0;
|
|
75
|
+
for (const ch of s) {
|
|
76
|
+
if (counted >= units)
|
|
77
|
+
break;
|
|
78
|
+
idx += ch.length;
|
|
79
|
+
counted++;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
// Never split a surrogate pair: if the boundary lands between a high and
|
|
83
|
+
// low surrogate, step back one unit.
|
|
84
|
+
if (idx > 0 && idx < s.length) {
|
|
85
|
+
const cc = s.charCodeAt(idx - 1);
|
|
86
|
+
if (cc >= 0xd800 && cc <= 0xdbff)
|
|
87
|
+
idx--;
|
|
88
|
+
}
|
|
89
|
+
return idx;
|
|
90
|
+
}
|
|
91
|
+
export function truncateForChannel(text, caps) {
|
|
92
|
+
if (unitLength(text, caps) <= caps.maxLength)
|
|
93
|
+
return text;
|
|
94
|
+
// The marker must fit INSIDE the budget - maxLength is the channel's hard
|
|
95
|
+
// limit, and overshooting it fails the whole send (msg_too_long).
|
|
96
|
+
const keep = unitIndex(text, caps.maxLength - TRUNCATION_MARKER.length, caps);
|
|
97
|
+
return text.slice(0, keep) + TRUNCATION_MARKER;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Fence state after scanning `s`. `open` is "" outside a fence, else the
|
|
101
|
+
* fence header to reopen with (e.g. "```python"). Fence lines toggle; an
|
|
102
|
+
* opening line's info string is preserved for reopening. The info string may
|
|
103
|
+
* not contain backticks (per CommonMark), which keeps a line-leading inline
|
|
104
|
+
* span like ```cmd``` from reading as an opener; an implausibly long "info
|
|
105
|
+
* string" (a one-line blob that happens to start with backticks) reopens as
|
|
106
|
+
* a bare fence so the carried prefix can never blow the chunk budget.
|
|
107
|
+
*/
|
|
108
|
+
const MAX_FENCE_HEADER = 24;
|
|
109
|
+
function fenceStateAfter(s, open) {
|
|
110
|
+
for (const line of s.split("\n")) {
|
|
111
|
+
const m = line.match(/^\s*(`{3,})([^`]*)$/);
|
|
112
|
+
if (!m)
|
|
113
|
+
continue;
|
|
114
|
+
if (open) {
|
|
115
|
+
open = "";
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
const info = m[2].trim();
|
|
119
|
+
open = info.length > MAX_FENCE_HEADER ? m[1] : m[1] + info;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return open;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Split over-budget text into channel-sized chunks at natural boundaries:
|
|
126
|
+
* paragraph break, then line break, then a hard (surrogate-safe) cut. A split
|
|
127
|
+
* inside a code fence closes the fence at the chunk end and reopens it (with
|
|
128
|
+
* its info string) at the start of the next chunk, so every chunk renders.
|
|
129
|
+
* Within-budget text comes back as a single chunk, byte-identical.
|
|
130
|
+
*/
|
|
131
|
+
export function chunkForChannel(text, caps) {
|
|
132
|
+
if (unitLength(text, caps) <= caps.maxLength)
|
|
133
|
+
return [text];
|
|
134
|
+
const chunks = [];
|
|
135
|
+
let remaining = text;
|
|
136
|
+
let reopen = ""; // fence header carried into the next chunk
|
|
137
|
+
while (chunks.length < MAX_CHUNKS - 1) {
|
|
138
|
+
const prefix = reopen ? reopen + "\n" : "";
|
|
139
|
+
if (unitLength(prefix, caps) + unitLength(remaining, caps) <= caps.maxLength) {
|
|
140
|
+
chunks.push(prefix + remaining);
|
|
141
|
+
return chunks;
|
|
142
|
+
}
|
|
143
|
+
// Reserve room for the prefix and a possible fence close so the chunk
|
|
144
|
+
// never overshoots the hard limit (over-reserving costs a few chars).
|
|
145
|
+
const avail = Math.max(1, caps.maxLength - unitLength(prefix, caps) - unitLength(FENCE_CLOSE, caps));
|
|
146
|
+
const windowEnd = unitIndex(remaining, avail, caps);
|
|
147
|
+
const window = remaining.slice(0, windowEnd);
|
|
148
|
+
// Natural boundaries, but never a blank chunk: a cut that leaves only
|
|
149
|
+
// whitespace (e.g. leading newlines) would fail the whole send.
|
|
150
|
+
let cut = window.lastIndexOf("\n\n");
|
|
151
|
+
if (cut <= 0 || !window.slice(0, cut).trim())
|
|
152
|
+
cut = window.lastIndexOf("\n");
|
|
153
|
+
if (cut <= 0 || !window.slice(0, cut).trim())
|
|
154
|
+
cut = windowEnd; // hard split, surrogate-safe
|
|
155
|
+
const head = remaining.slice(0, cut);
|
|
156
|
+
remaining = remaining.slice(cut).replace(/^\n+/, "");
|
|
157
|
+
reopen = fenceStateAfter(head, reopen);
|
|
158
|
+
let chunk = prefix + head;
|
|
159
|
+
if (reopen)
|
|
160
|
+
chunk += FENCE_CLOSE;
|
|
161
|
+
chunks.push(chunk);
|
|
162
|
+
}
|
|
163
|
+
// Chunk-count guard hit: truncate the tail instead of posting unbounded
|
|
164
|
+
// follow-ups for pathological input, keeping any open fence closed so the
|
|
165
|
+
// final chunk still renders.
|
|
166
|
+
const prefix = reopen ? reopen + "\n" : "";
|
|
167
|
+
let tail = truncateForChannel(prefix + remaining, caps);
|
|
168
|
+
if (fenceStateAfter(tail, "")) {
|
|
169
|
+
tail = truncateForChannel(prefix + remaining, { ...caps, maxLength: caps.maxLength - unitLength(FENCE_CLOSE, caps) }) + FENCE_CLOSE;
|
|
170
|
+
}
|
|
171
|
+
chunks.push(tail);
|
|
172
|
+
return chunks;
|
|
173
|
+
}
|
|
174
|
+
function slackError(err) {
|
|
175
|
+
if (err instanceof SlackApiError) {
|
|
176
|
+
return err.response?.error || err.message;
|
|
177
|
+
}
|
|
178
|
+
return String(err);
|
|
179
|
+
}
|
|
180
|
+
function toConversationMessage(raw) {
|
|
181
|
+
const entry = {
|
|
182
|
+
user: raw.user || "",
|
|
183
|
+
text: raw.text || "",
|
|
184
|
+
ts: raw.ts || "",
|
|
185
|
+
};
|
|
186
|
+
const rawFiles = raw.files;
|
|
187
|
+
if (Array.isArray(rawFiles) && rawFiles.length > 0) {
|
|
188
|
+
entry.files = rawFiles.map((f) => ({
|
|
189
|
+
id: String(f.id ?? ""),
|
|
190
|
+
name: String(f.name ?? ""),
|
|
191
|
+
mimetype: String(f.mimetype ?? ""),
|
|
192
|
+
url_private: String(f.url_private ?? ""),
|
|
193
|
+
}));
|
|
194
|
+
}
|
|
195
|
+
return entry;
|
|
196
|
+
}
|
|
197
|
+
// Message body for a Slack post/update. Raw markdown goes out as Slack's
|
|
198
|
+
// native `markdown_text` (AST-rendered by Slack, 12k budget).
|
|
199
|
+
function slackContent(text) {
|
|
200
|
+
return { markdownText: text };
|
|
201
|
+
}
|
|
202
|
+
const slackAdapter = {
|
|
203
|
+
descriptor: {
|
|
204
|
+
name: "slack",
|
|
205
|
+
topicShape: "slack:<team_id>[:app:<app_id>][:<channel>]",
|
|
206
|
+
transport: "webhook",
|
|
207
|
+
capabilities: {
|
|
208
|
+
edit: true,
|
|
209
|
+
typing: true,
|
|
210
|
+
streaming: "native",
|
|
211
|
+
threads: true,
|
|
212
|
+
files: true,
|
|
213
|
+
// Slack's markdown_text limit; plain text allows ~40k but the
|
|
214
|
+
// gateway sends markdown, so the stricter budget applies. The
|
|
215
|
+
// budget has always been enforced in UTF-16 units (JS .length);
|
|
216
|
+
// declare that unit so chunking keeps the proven behavior.
|
|
217
|
+
maxLength: 12000,
|
|
218
|
+
lengthUnit: "utf16",
|
|
219
|
+
},
|
|
220
|
+
credentials: [
|
|
221
|
+
{ env: "SLACK_BOT_TOKEN", secret: true, label: "Bot User OAuth Token" },
|
|
222
|
+
{ env: "SLACK_SIGNING_SECRET", secret: true, label: "App Signing Secret" },
|
|
223
|
+
],
|
|
224
|
+
promptHint: "Slack renders standard markdown. Keep replies concise; use "
|
|
225
|
+
+ "triple-backtick blocks for multi-line code.",
|
|
226
|
+
setupSkill: "skills/slack-setup.md",
|
|
227
|
+
},
|
|
228
|
+
async send(token, conv, text) {
|
|
229
|
+
try {
|
|
230
|
+
const posted = await postSlackMessage({
|
|
231
|
+
apiUrl: slackApiUrl(),
|
|
232
|
+
token,
|
|
233
|
+
channel: conv.chatId,
|
|
234
|
+
...(conv.threadId ? { threadTs: conv.threadId } : {}),
|
|
235
|
+
...slackContent(text),
|
|
236
|
+
});
|
|
237
|
+
return { ok: true, ts: posted.id };
|
|
238
|
+
}
|
|
239
|
+
catch (err) {
|
|
240
|
+
return { ok: false, error: slackError(err) };
|
|
241
|
+
}
|
|
242
|
+
},
|
|
243
|
+
async update(token, conv, messageId, text) {
|
|
244
|
+
try {
|
|
245
|
+
const posted = await updateSlackMessage({
|
|
246
|
+
apiUrl: slackApiUrl(),
|
|
247
|
+
token,
|
|
248
|
+
channel: conv.chatId,
|
|
249
|
+
ts: messageId,
|
|
250
|
+
...slackContent(text),
|
|
251
|
+
});
|
|
252
|
+
return { ok: true, ts: posted.id };
|
|
253
|
+
}
|
|
254
|
+
catch (err) {
|
|
255
|
+
return { ok: false, error: slackError(err) };
|
|
256
|
+
}
|
|
257
|
+
},
|
|
258
|
+
// Slack's "typing" is the assistant thread status ("is thinking..."),
|
|
259
|
+
// which only exists in thread context and expires after ~2 minutes -
|
|
260
|
+
// callers refresh while work runs. Best-effort: only DM threads support
|
|
261
|
+
// it, so failures are swallowed.
|
|
262
|
+
async typing(token, conv, on) {
|
|
263
|
+
if (!conv.threadId)
|
|
264
|
+
return;
|
|
265
|
+
try {
|
|
266
|
+
await callSlackApi("assistant.threads.setStatus", {
|
|
267
|
+
channel_id: conv.chatId,
|
|
268
|
+
thread_ts: conv.threadId,
|
|
269
|
+
status: on ? "is thinking…" : "",
|
|
270
|
+
}, { apiUrl: slackApiUrl(), token });
|
|
271
|
+
}
|
|
272
|
+
catch {
|
|
273
|
+
// non-fatal
|
|
274
|
+
}
|
|
275
|
+
},
|
|
276
|
+
async fetchConversation(token, conv, limit = 100) {
|
|
277
|
+
// A ref without a thread anchor reads the channel/DM history instead
|
|
278
|
+
// of thread replies (e.g. a proactive conversation with no thread).
|
|
279
|
+
if (!conv.threadId) {
|
|
280
|
+
const raw = await callSlackApi("conversations.history", {
|
|
281
|
+
channel: conv.chatId,
|
|
282
|
+
limit: Math.min(limit, 200),
|
|
283
|
+
}, { apiUrl: slackApiUrl(), token });
|
|
284
|
+
const rows = raw.messages ?? [];
|
|
285
|
+
// conversations.history returns newest-first; deliver oldest-first
|
|
286
|
+
// to match the thread-replies ordering.
|
|
287
|
+
return rows.map(toConversationMessage).reverse().slice(0, limit);
|
|
288
|
+
}
|
|
289
|
+
const messages = [];
|
|
290
|
+
let cursor;
|
|
291
|
+
while (messages.length < limit) {
|
|
292
|
+
const page = await fetchSlackThreadReplies({
|
|
293
|
+
apiUrl: slackApiUrl(),
|
|
294
|
+
token,
|
|
295
|
+
channel: conv.chatId,
|
|
296
|
+
ts: conv.threadId,
|
|
297
|
+
limit: Math.min(limit - messages.length, 200),
|
|
298
|
+
...(cursor ? { cursor } : {}),
|
|
299
|
+
});
|
|
300
|
+
for (const raw of page.messages) {
|
|
301
|
+
messages.push(toConversationMessage(raw));
|
|
302
|
+
}
|
|
303
|
+
cursor = page.nextCursor;
|
|
304
|
+
if (!cursor)
|
|
305
|
+
break;
|
|
306
|
+
}
|
|
307
|
+
return messages.slice(0, limit);
|
|
308
|
+
},
|
|
309
|
+
async uploadFiles(token, conv, files, comment) {
|
|
310
|
+
try {
|
|
311
|
+
const result = await uploadSlackFiles(files.map((f) => ({
|
|
312
|
+
data: f.data,
|
|
313
|
+
filename: f.name,
|
|
314
|
+
...(f.title ? { title: f.title } : {}),
|
|
315
|
+
})), {
|
|
316
|
+
apiUrl: slackApiUrl(),
|
|
317
|
+
token,
|
|
318
|
+
channelId: conv.chatId,
|
|
319
|
+
...(conv.threadId ? { threadTs: conv.threadId } : {}),
|
|
320
|
+
...(comment ? { initialComment: comment } : {}),
|
|
321
|
+
});
|
|
322
|
+
return { ok: true, ts: result.fileIds[0] };
|
|
323
|
+
}
|
|
324
|
+
catch (err) {
|
|
325
|
+
return { ok: false, error: slackError(err) };
|
|
326
|
+
}
|
|
327
|
+
},
|
|
328
|
+
};
|
|
329
|
+
// --- WhatsApp (Meta Cloud API) - #656, epic #190 Phase 3 ---
|
|
330
|
+
// One Graph API call. Returns the parsed JSON body; throws on transport
|
|
331
|
+
// errors; a Graph error body ({error: {message}}) is surfaced by callers.
|
|
332
|
+
export async function whatsappApi(token, path, init) {
|
|
333
|
+
const headers = { Authorization: `Bearer ${token}` };
|
|
334
|
+
// Structural (not BodyInit): only these two shapes are ever built, and
|
|
335
|
+
// the name resolves under every lib variant this file compiles against
|
|
336
|
+
// (dom for the published dist, @types/node for the in-repo build).
|
|
337
|
+
let body;
|
|
338
|
+
if (init.json !== undefined) {
|
|
339
|
+
headers["Content-Type"] = "application/json";
|
|
340
|
+
body = JSON.stringify(init.json);
|
|
341
|
+
}
|
|
342
|
+
else if (init.form) {
|
|
343
|
+
body = init.form; // fetch sets the multipart boundary itself
|
|
344
|
+
}
|
|
345
|
+
const resp = await fetch(`${whatsappApiUrl()}${path}`, {
|
|
346
|
+
method: init.method ?? "POST",
|
|
347
|
+
headers,
|
|
348
|
+
...(body !== undefined ? { body } : {}),
|
|
349
|
+
});
|
|
350
|
+
return (await resp.json());
|
|
351
|
+
}
|
|
352
|
+
// Meta's cap for media/document captions, distinct from the 4096 text limit.
|
|
353
|
+
const WHATSAPP_CAPTION_LIMIT = 1024;
|
|
354
|
+
function whatsappError(data) {
|
|
355
|
+
const err = data.error;
|
|
356
|
+
return err?.message || "whatsapp api error";
|
|
357
|
+
}
|
|
358
|
+
const whatsappAdapter = {
|
|
359
|
+
descriptor: {
|
|
360
|
+
name: "whatsapp",
|
|
361
|
+
topicShape: "whatsapp:<phone_number_id>",
|
|
362
|
+
transport: "webhook",
|
|
363
|
+
capabilities: {
|
|
364
|
+
// No message editing on WhatsApp - the gateway degrades placeholder
|
|
365
|
+
// edits to follow-up posts. No typing indicator, no threads, no
|
|
366
|
+
// native streaming: reactive-only replies (epic #190 Phase 3).
|
|
367
|
+
edit: false,
|
|
368
|
+
typing: false,
|
|
369
|
+
streaming: "none",
|
|
370
|
+
threads: false,
|
|
371
|
+
files: true,
|
|
372
|
+
// Cloud API text body limit, counted in characters (code points) -
|
|
373
|
+
// the unit-aware budget #651 wired in.
|
|
374
|
+
maxLength: 4096,
|
|
375
|
+
lengthUnit: "chars",
|
|
376
|
+
// Meta's customer-service window: replies are free-form for 24h
|
|
377
|
+
// after the user's last inbound message; outside it only template
|
|
378
|
+
// messages send (template support is a follow-up - the gateway
|
|
379
|
+
// returns a typed error so the agent can report the situation).
|
|
380
|
+
messageWindow: { hours: 24, outsideWindow: "template" },
|
|
381
|
+
},
|
|
382
|
+
credentials: [
|
|
383
|
+
{ env: "WHATSAPP_ACCESS_TOKEN", secret: true, label: "Cloud API access token" },
|
|
384
|
+
{ env: "WHATSAPP_PHONE_NUMBER_ID", secret: false, label: "Phone number ID" },
|
|
385
|
+
{ env: "WHATSAPP_APP_SECRET", secret: true, label: "Meta app secret (webhook signatures)" },
|
|
386
|
+
{ env: "WHATSAPP_VERIFY_TOKEN", secret: true, label: "Webhook verify token (your choice)" },
|
|
387
|
+
],
|
|
388
|
+
promptHint: "WhatsApp renders its own light formatting (*bold*, _italic_, "
|
|
389
|
+
+ "```monospace```), not full markdown. Keep replies short and "
|
|
390
|
+
+ "conversational; avoid headers and tables.",
|
|
391
|
+
setupSkill: "skills/whatsapp-setup.md",
|
|
392
|
+
},
|
|
393
|
+
async send(token, conv, text) {
|
|
394
|
+
try {
|
|
395
|
+
const data = await whatsappApi(token, `${conv.scope}/messages`, {
|
|
396
|
+
json: {
|
|
397
|
+
messaging_product: "whatsapp",
|
|
398
|
+
to: conv.chatId,
|
|
399
|
+
type: "text",
|
|
400
|
+
text: { body: text },
|
|
401
|
+
},
|
|
402
|
+
});
|
|
403
|
+
const id = data.messages?.[0]?.id;
|
|
404
|
+
if (!id)
|
|
405
|
+
return { ok: false, error: whatsappError(data) };
|
|
406
|
+
return { ok: true, ts: id };
|
|
407
|
+
}
|
|
408
|
+
catch (err) {
|
|
409
|
+
return { ok: false, error: String(err) };
|
|
410
|
+
}
|
|
411
|
+
},
|
|
412
|
+
// Two-step Cloud API upload: POST the bytes to /{pnid}/media, then send a
|
|
413
|
+
// document message referencing the media id. Sent as `document` for any
|
|
414
|
+
// mime type - it preserves the filename and works for all content. The
|
|
415
|
+
// media endpoint validates the declared MIME, so it is inferred from the
|
|
416
|
+
// filename (octet-stream is not on Meta's supported list).
|
|
417
|
+
async uploadFiles(token, conv, files, comment) {
|
|
418
|
+
let delivered = 0;
|
|
419
|
+
try {
|
|
420
|
+
let lastId = "";
|
|
421
|
+
for (const [i, f] of files.entries()) {
|
|
422
|
+
const mime = mimeForFilename(f.name);
|
|
423
|
+
const form = new FormData();
|
|
424
|
+
form.append("messaging_product", "whatsapp");
|
|
425
|
+
form.append("type", mime);
|
|
426
|
+
form.append("file", new Blob([f.data], { type: mime }), f.name);
|
|
427
|
+
const uploaded = await whatsappApi(token, `${conv.scope}/media`, { form });
|
|
428
|
+
const mediaId = uploaded.id;
|
|
429
|
+
if (!mediaId)
|
|
430
|
+
return { ok: false, error: partialUploadError(whatsappError(uploaded), i, files.length) };
|
|
431
|
+
// Media captions have their own Cloud API limit (1024 chars),
|
|
432
|
+
// well below the 4096 text-body cap the gateway budget uses.
|
|
433
|
+
const caption = (i === 0 ? comment || f.title || "" : f.title || "")
|
|
434
|
+
.slice(0, WHATSAPP_CAPTION_LIMIT);
|
|
435
|
+
const sent = await whatsappApi(token, `${conv.scope}/messages`, {
|
|
436
|
+
json: {
|
|
437
|
+
messaging_product: "whatsapp",
|
|
438
|
+
to: conv.chatId,
|
|
439
|
+
type: "document",
|
|
440
|
+
document: {
|
|
441
|
+
id: mediaId,
|
|
442
|
+
filename: f.name,
|
|
443
|
+
...(caption ? { caption } : {}),
|
|
444
|
+
},
|
|
445
|
+
},
|
|
446
|
+
});
|
|
447
|
+
const id = sent.messages?.[0]?.id;
|
|
448
|
+
if (!id)
|
|
449
|
+
return { ok: false, error: partialUploadError(whatsappError(sent), i, files.length) };
|
|
450
|
+
lastId = id;
|
|
451
|
+
delivered = i + 1;
|
|
452
|
+
}
|
|
453
|
+
return { ok: true, ts: lastId };
|
|
454
|
+
}
|
|
455
|
+
catch (err) {
|
|
456
|
+
return { ok: false, error: partialUploadError(String(err), delivered, files.length) };
|
|
457
|
+
}
|
|
458
|
+
},
|
|
459
|
+
};
|
|
460
|
+
// Mirror the chunk path's partial-delivery contract: once any file reached
|
|
461
|
+
// the user, the error must tell the caller not to blind-retry the batch.
|
|
462
|
+
function partialUploadError(error, delivered, total) {
|
|
463
|
+
if (delivered === 0)
|
|
464
|
+
return error;
|
|
465
|
+
return `file ${delivered + 1}/${total} failed after partial delivery `
|
|
466
|
+
+ `(do not resend; ${delivered} file(s) are already visible): ${error}`;
|
|
467
|
+
}
|
|
468
|
+
// Minimal extension map for the document types agents actually send. Meta's
|
|
469
|
+
// media endpoint rejects MIME types outside its supported list
|
|
470
|
+
// (application/octet-stream is not on it), so unknowns go out as text/plain.
|
|
471
|
+
const MIME_BY_EXT = {
|
|
472
|
+
pdf: "application/pdf",
|
|
473
|
+
txt: "text/plain",
|
|
474
|
+
md: "text/plain",
|
|
475
|
+
csv: "text/csv",
|
|
476
|
+
png: "image/png",
|
|
477
|
+
jpg: "image/jpeg",
|
|
478
|
+
jpeg: "image/jpeg",
|
|
479
|
+
webp: "image/webp",
|
|
480
|
+
mp4: "video/mp4",
|
|
481
|
+
doc: "application/msword",
|
|
482
|
+
docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
483
|
+
xls: "application/vnd.ms-excel",
|
|
484
|
+
xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
485
|
+
pptx: "application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
|
486
|
+
zip: "application/zip",
|
|
487
|
+
};
|
|
488
|
+
function mimeForFilename(name) {
|
|
489
|
+
const ext = name.toLowerCase().split(".").pop() || "";
|
|
490
|
+
return MIME_BY_EXT[ext] || "text/plain";
|
|
491
|
+
}
|
|
492
|
+
const CHANNEL_ADAPTERS = {
|
|
493
|
+
slack: slackAdapter,
|
|
494
|
+
whatsapp: whatsappAdapter,
|
|
495
|
+
};
|
|
496
|
+
export function getChannelAdapter(source) {
|
|
497
|
+
return CHANNEL_ADAPTERS[source];
|
|
498
|
+
}
|
|
499
|
+
//# sourceMappingURL=channels.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"channels.js","sourceRoot":"","sources":["../src/channels.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Delivery-path circuit breaker (#299, split from #215 loop-safety).
|
|
3
|
+
*
|
|
4
|
+
* Keying decision: the breaker keys on `(deployment_id, conversation)` where
|
|
5
|
+
* conversation is derived from external channel identity:
|
|
6
|
+
* - Slack: thread_ts (or ts if no thread)
|
|
7
|
+
* - GitHub: repo + issue/PR number
|
|
8
|
+
* - Other: event type (coarse — acceptable because custom events rarely loop)
|
|
9
|
+
*
|
|
10
|
+
* EXEMPTIONS: `inbox/*` and `reply/*` topics are legitimate internal
|
|
11
|
+
* agent↔agent comms and are NEVER counted toward the breaker. They flow
|
|
12
|
+
* through a separate routing namespace (`agent/*` topics) that does not
|
|
13
|
+
* represent external conversation depth.
|
|
14
|
+
*
|
|
15
|
+
* Trip condition: ≥ THRESHOLD non-human-authored deliveries in one key within
|
|
16
|
+
* WINDOW_MS with zero human events → pause delivery for that key (buffered,
|
|
17
|
+
* not dropped). Emit `system.loop_detected`. Auto-resume after COOLDOWN_MS or
|
|
18
|
+
* on the next human-authored event in the same key.
|
|
19
|
+
*/
|
|
20
|
+
import type { NormalizedEvent } from "./core.js";
|
|
21
|
+
export declare const BREAKER_THRESHOLD = 5;
|
|
22
|
+
export declare const BREAKER_WINDOW_MS = 60000;
|
|
23
|
+
export declare const BREAKER_COOLDOWN_MS = 300000;
|
|
24
|
+
export interface BreakerState {
|
|
25
|
+
/** Timestamps of non-human deliveries within the current window */
|
|
26
|
+
timestamps: number[];
|
|
27
|
+
/** Whether the breaker is currently tripped */
|
|
28
|
+
tripped: boolean;
|
|
29
|
+
/** When the breaker was tripped (epoch ms), undefined if not tripped */
|
|
30
|
+
trippedAt?: number;
|
|
31
|
+
/** Events paused while tripped — delivered on resume */
|
|
32
|
+
paused: NormalizedEvent[];
|
|
33
|
+
}
|
|
34
|
+
export interface BreakerVerdict {
|
|
35
|
+
/** Whether delivery should proceed */
|
|
36
|
+
allow: boolean;
|
|
37
|
+
/** Whether this delivery tripped the breaker (first trip) */
|
|
38
|
+
justTripped: boolean;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Returns true if this event's type is exempt from breaker counting.
|
|
42
|
+
* inbox/* and reply/* are internal agent↔agent comms, not external
|
|
43
|
+
* conversation depth.
|
|
44
|
+
*/
|
|
45
|
+
export declare function isExemptFromBreaker(event: NormalizedEvent): boolean;
|
|
46
|
+
/**
|
|
47
|
+
* Extract the conversation key from an event. Returns null if no meaningful
|
|
48
|
+
* conversation can be identified (breaker won't apply).
|
|
49
|
+
*/
|
|
50
|
+
export declare function conversationKey(event: NormalizedEvent): string | null;
|
|
51
|
+
/**
|
|
52
|
+
* Determine whether an event is authored by a bot (non-human).
|
|
53
|
+
* Human-authored events reset the breaker window.
|
|
54
|
+
*/
|
|
55
|
+
export declare function isBotAuthored(event: NormalizedEvent): boolean;
|
|
56
|
+
/**
|
|
57
|
+
* Record a delivery attempt and return a verdict.
|
|
58
|
+
*
|
|
59
|
+
* Call this BEFORE actually delivering the event. If `allow` is false,
|
|
60
|
+
* the caller must buffer the event (it is already added to state.paused).
|
|
61
|
+
*/
|
|
62
|
+
export declare function recordDelivery(deploymentId: string, event: NormalizedEvent): BreakerVerdict;
|
|
63
|
+
/**
|
|
64
|
+
* Drain paused events for a deployment+conversation. Returns the events
|
|
65
|
+
* that were buffered and clears the pause buffer. The caller is responsible
|
|
66
|
+
* for delivering them.
|
|
67
|
+
*/
|
|
68
|
+
export declare function drainPaused(deploymentId: string, event: NormalizedEvent): NormalizedEvent[];
|
|
69
|
+
/**
|
|
70
|
+
* Check whether a breaker is currently tripped for a deployment+conversation.
|
|
71
|
+
* Used after cooldown to decide whether to flush paused events.
|
|
72
|
+
*/
|
|
73
|
+
export declare function isBreakerTripped(deploymentId: string, convKey: string): boolean;
|
|
74
|
+
/**
|
|
75
|
+
* Build the system.loop_detected event to be published.
|
|
76
|
+
*/
|
|
77
|
+
export declare function buildLoopDetectedEvent(deploymentId: string, convKey: string, triggerEvent: NormalizedEvent): NormalizedEvent;
|
|
78
|
+
export declare function resetAllBreakers(): void;
|
|
79
|
+
//# sourceMappingURL=circuit-breaker.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"circuit-breaker.d.ts","sourceRoot":"","sources":["../src/circuit-breaker.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AAMjD,eAAO,MAAM,iBAAiB,IAAI,CAAC;AACnC,eAAO,MAAM,iBAAiB,QAAS,CAAC;AACxC,eAAO,MAAM,mBAAmB,SAAU,CAAC;AAM3C,MAAM,WAAW,YAAY;IAC5B,mEAAmE;IACnE,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,+CAA+C;IAC/C,OAAO,EAAE,OAAO,CAAC;IACjB,wEAAwE;IACxE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,wDAAwD;IACxD,MAAM,EAAE,eAAe,EAAE,CAAC;CAC1B;AAED,MAAM,WAAW,cAAc;IAC9B,sCAAsC;IACtC,KAAK,EAAE,OAAO,CAAC;IACf,6DAA6D;IAC7D,WAAW,EAAE,OAAO,CAAC;CACrB;AAaD;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,eAAe,GAAG,OAAO,CAanE;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,eAAe,GAAG,MAAM,GAAG,IAAI,CAmDrE;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,eAAe,GAAG,OAAO,CA4B7D;AAkCD;;;;;GAKG;AACH,wBAAgB,cAAc,CAC7B,YAAY,EAAE,MAAM,EACpB,KAAK,EAAE,eAAe,GACpB,cAAc,CAsChB;AAaD;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,YAAY,EAAE,MAAM,EAAE,KAAK,EAAE,eAAe,GAAG,eAAe,EAAE,CAQ3F;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,YAAY,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAS/E;AAED;;GAEG;AACH,wBAAgB,sBAAsB,CACrC,YAAY,EAAE,MAAM,EACpB,OAAO,EAAE,MAAM,EACf,YAAY,EAAE,eAAe,GAC3B,eAAe,CAyBjB;AAMD,wBAAgB,gBAAgB,IAAI,IAAI,CAEvC"}
|