@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.
Files changed (43) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +32 -0
  3. package/dist/adapters/chat-sdk-slack.d.ts +18 -0
  4. package/dist/adapters/chat-sdk-slack.d.ts.map +1 -0
  5. package/dist/adapters/chat-sdk-slack.js +201 -0
  6. package/dist/adapters/chat-sdk-slack.js.map +1 -0
  7. package/dist/adapters/github.d.ts +3 -0
  8. package/dist/adapters/github.d.ts.map +1 -0
  9. package/dist/adapters/github.js +114 -0
  10. package/dist/adapters/github.js.map +1 -0
  11. package/dist/adapters/linear.d.ts +3 -0
  12. package/dist/adapters/linear.d.ts.map +1 -0
  13. package/dist/adapters/linear.js +47 -0
  14. package/dist/adapters/linear.js.map +1 -0
  15. package/dist/adapters/whatsapp.d.ts +25 -0
  16. package/dist/adapters/whatsapp.d.ts.map +1 -0
  17. package/dist/adapters/whatsapp.js +96 -0
  18. package/dist/adapters/whatsapp.js.map +1 -0
  19. package/dist/channels.d.ts +80 -0
  20. package/dist/channels.d.ts.map +1 -0
  21. package/dist/channels.js +499 -0
  22. package/dist/channels.js.map +1 -0
  23. package/dist/circuit-breaker.d.ts +79 -0
  24. package/dist/circuit-breaker.d.ts.map +1 -0
  25. package/dist/circuit-breaker.js +288 -0
  26. package/dist/circuit-breaker.js.map +1 -0
  27. package/dist/conversation.d.ts +29 -0
  28. package/dist/conversation.d.ts.map +1 -0
  29. package/dist/conversation.js +86 -0
  30. package/dist/conversation.js.map +1 -0
  31. package/dist/core.d.ts +254 -0
  32. package/dist/core.d.ts.map +1 -0
  33. package/dist/core.js +1775 -0
  34. package/dist/core.js.map +1 -0
  35. package/package.json +40 -0
  36. package/src/adapters/chat-sdk-slack.ts +209 -0
  37. package/src/adapters/github.ts +111 -0
  38. package/src/adapters/linear.ts +53 -0
  39. package/src/adapters/whatsapp.ts +120 -0
  40. package/src/channels.ts +600 -0
  41. package/src/circuit-breaker.ts +337 -0
  42. package/src/conversation.ts +96 -0
  43. package/src/core.ts +2413 -0
@@ -0,0 +1,288 @@
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
+ // ---------------------------------------------------------------------------
21
+ // Tunables
22
+ // ---------------------------------------------------------------------------
23
+ export const BREAKER_THRESHOLD = 5;
24
+ export const BREAKER_WINDOW_MS = 60_000; // 60s
25
+ export const BREAKER_COOLDOWN_MS = 300_000; // 5min
26
+ const AGENT_LIFECYCLE_EVENTS = new Set([
27
+ "agent/session.completed",
28
+ "agent/session.failed",
29
+ "session.completed",
30
+ "session.failed",
31
+ ]);
32
+ // ---------------------------------------------------------------------------
33
+ // Conversation key extraction
34
+ // ---------------------------------------------------------------------------
35
+ /**
36
+ * Returns true if this event's type is exempt from breaker counting.
37
+ * inbox/* and reply/* are internal agent↔agent comms, not external
38
+ * conversation depth.
39
+ */
40
+ export function isExemptFromBreaker(event) {
41
+ const t = event.type;
42
+ if (t.startsWith("inbox/") || t.startsWith("reply/"))
43
+ return true;
44
+ if (event.source === "agent" && AGENT_LIFECYCLE_EVENTS.has(t))
45
+ return true;
46
+ // Also check topics — events published on inbox/* or reply/* topics
47
+ // (via createTopicEvent) carry those as their routing keys.
48
+ if (event.topics?.some((topic) => topic.startsWith("inbox/") || topic.startsWith("reply/"))) {
49
+ return true;
50
+ }
51
+ if (event.source === "agent" && event.topics?.some((topic) => AGENT_LIFECYCLE_EVENTS.has(topic))) {
52
+ return true;
53
+ }
54
+ return false;
55
+ }
56
+ /**
57
+ * Extract the conversation key from an event. Returns null if no meaningful
58
+ * conversation can be identified (breaker won't apply).
59
+ */
60
+ export function conversationKey(event) {
61
+ const payload = event.payload;
62
+ const fields = event.fields;
63
+ const payloadString = (key) => {
64
+ const value = payload?.[key];
65
+ return typeof value === "string" ? value.trim() : "";
66
+ };
67
+ if (event.source === "slack") {
68
+ // Slack: thread_ts identifies a conversation (thread). If no thread,
69
+ // use ts (the message itself becomes the "thread" root).
70
+ const threadTs = payload?.thread_ts || fields?.thread_ts || "";
71
+ const ts = payload?.ts || fields?.ts || "";
72
+ const channel = payload?.channel || fields?.channel || "";
73
+ const key = threadTs || ts;
74
+ if (key && channel)
75
+ return `slack:${channel}:${key}`;
76
+ if (key)
77
+ return `slack:${key}`;
78
+ return null;
79
+ }
80
+ if (event.source === "github") {
81
+ // GitHub: repo + issue/PR number identifies the conversation.
82
+ const repo = payload?.repository?.full_name;
83
+ const issue = payload?.issue;
84
+ const pr = payload?.pull_request;
85
+ const num = (issue?.number ?? pr?.number);
86
+ if (repo && num !== undefined)
87
+ return `github:${repo}#${num}`;
88
+ // Fall back to repo-level for events without an issue/PR (e.g. push)
89
+ if (repo)
90
+ return `github:${repo}`;
91
+ return null;
92
+ }
93
+ if (event.source === "monitor") {
94
+ // Monitor findings are often batched by one scheduled run but emitted as
95
+ // separate actionable findings. Key by the monitor plus the scheduler's
96
+ // stable finding identity so one audit pass with several distinct
97
+ // findings does not look like a self-reinforcing event loop.
98
+ const monitor = payloadString("monitor");
99
+ const findingKey = payloadString("finding_key") || payloadString("key");
100
+ if (monitor && findingKey) {
101
+ return `monitor:${encodeURIComponent(monitor)}:${encodeURIComponent(findingKey)}`;
102
+ }
103
+ if (monitor)
104
+ return `monitor:${encodeURIComponent(monitor)}`;
105
+ return `monitor:${encodeURIComponent(event.type)}`;
106
+ }
107
+ // Fallback for custom/linear events: use the event type as a coarse key.
108
+ // This means N different events of the same type within one deployment
109
+ // share a single bucket — acceptable because custom loops are rare and
110
+ // the breaker trips only on sustained depth.
111
+ return `other:${event.type}`;
112
+ }
113
+ /**
114
+ * Determine whether an event is authored by a bot (non-human).
115
+ * Human-authored events reset the breaker window.
116
+ */
117
+ export function isBotAuthored(event) {
118
+ const payload = event.payload;
119
+ if (event.source === "slack") {
120
+ // Slack: bot_id present on the inner event means a bot posted it.
121
+ const innerEvent = payload?.event;
122
+ const botId = innerEvent?.bot_id || payload?.bot_id || "";
123
+ return !!botId;
124
+ }
125
+ if (event.source === "github") {
126
+ // GitHub: sender.type === "Bot" or well-known agent logins.
127
+ const sender = payload?.sender;
128
+ if (sender?.type === "Bot")
129
+ return true;
130
+ const login = sender?.login || "";
131
+ // Common bot suffixes: [bot], -bot, _bot
132
+ if (login.endsWith("[bot]") || login.endsWith("-bot") || login.endsWith("_bot"))
133
+ return true;
134
+ return false;
135
+ }
136
+ // For custom/linear sources: conservatively treat as bot-authored if the
137
+ // source field suggests automation. This avoids false negatives on agent-
138
+ // published events while still allowing human-triggered custom events to
139
+ // reset the breaker.
140
+ const source = event.source || "";
141
+ if (source === "agent" || source === "monitor" || source === "system")
142
+ return true;
143
+ return false;
144
+ }
145
+ // ---------------------------------------------------------------------------
146
+ // Breaker engine (in-memory, per-process)
147
+ // ---------------------------------------------------------------------------
148
+ /** Map of `deploymentId:conversationKey` → BreakerState */
149
+ const states = new Map();
150
+ function getState(compositeKey) {
151
+ let s = states.get(compositeKey);
152
+ if (!s) {
153
+ s = { timestamps: [], tripped: false, paused: [] };
154
+ states.set(compositeKey, s);
155
+ }
156
+ return s;
157
+ }
158
+ /**
159
+ * Prune timestamps outside the current window.
160
+ */
161
+ function pruneWindow(state, now) {
162
+ const cutoff = now - BREAKER_WINDOW_MS;
163
+ state.timestamps = state.timestamps.filter((t) => t > cutoff);
164
+ }
165
+ /**
166
+ * Check whether a tripped breaker has cooled down and should auto-resume.
167
+ */
168
+ function checkCooldown(state, now) {
169
+ if (!state.tripped || !state.trippedAt)
170
+ return false;
171
+ return now - state.trippedAt >= BREAKER_COOLDOWN_MS;
172
+ }
173
+ /**
174
+ * Record a delivery attempt and return a verdict.
175
+ *
176
+ * Call this BEFORE actually delivering the event. If `allow` is false,
177
+ * the caller must buffer the event (it is already added to state.paused).
178
+ */
179
+ export function recordDelivery(deploymentId, event) {
180
+ const convKey = conversationKey(event);
181
+ if (!convKey)
182
+ return { allow: true, justTripped: false };
183
+ const compositeKey = `${deploymentId}:${convKey}`;
184
+ const state = getState(compositeKey);
185
+ const now = Date.now();
186
+ // Auto-resume on cooldown expiry
187
+ if (state.tripped && checkCooldown(state, now)) {
188
+ resumeState(state);
189
+ }
190
+ // Human event resets the breaker
191
+ if (!isBotAuthored(event)) {
192
+ resumeState(state);
193
+ return { allow: true, justTripped: false };
194
+ }
195
+ // If already tripped, buffer the event
196
+ if (state.tripped) {
197
+ state.paused.push(event);
198
+ return { allow: false, justTripped: false };
199
+ }
200
+ // Record this bot delivery
201
+ pruneWindow(state, now);
202
+ state.timestamps.push(now);
203
+ // Check threshold
204
+ if (state.timestamps.length >= BREAKER_THRESHOLD) {
205
+ state.tripped = true;
206
+ state.trippedAt = now;
207
+ state.paused.push(event);
208
+ return { allow: false, justTripped: true };
209
+ }
210
+ return { allow: true, justTripped: false };
211
+ }
212
+ /**
213
+ * Resume a tripped breaker — returns any paused events that should now
214
+ * be delivered.
215
+ */
216
+ function resumeState(state) {
217
+ state.tripped = false;
218
+ state.trippedAt = undefined;
219
+ state.timestamps = [];
220
+ // Note: paused events are drained by the caller via drainPaused()
221
+ }
222
+ /**
223
+ * Drain paused events for a deployment+conversation. Returns the events
224
+ * that were buffered and clears the pause buffer. The caller is responsible
225
+ * for delivering them.
226
+ */
227
+ export function drainPaused(deploymentId, event) {
228
+ const convKey = conversationKey(event);
229
+ if (!convKey)
230
+ return [];
231
+ const compositeKey = `${deploymentId}:${convKey}`;
232
+ const state = states.get(compositeKey);
233
+ if (!state || state.paused.length === 0)
234
+ return [];
235
+ const drained = state.paused.splice(0);
236
+ return drained;
237
+ }
238
+ /**
239
+ * Check whether a breaker is currently tripped for a deployment+conversation.
240
+ * Used after cooldown to decide whether to flush paused events.
241
+ */
242
+ export function isBreakerTripped(deploymentId, convKey) {
243
+ const compositeKey = `${deploymentId}:${convKey}`;
244
+ const state = states.get(compositeKey);
245
+ if (!state)
246
+ return false;
247
+ if (state.tripped && state.trippedAt && checkCooldown(state, Date.now())) {
248
+ resumeState(state);
249
+ return false;
250
+ }
251
+ return state.tripped;
252
+ }
253
+ /**
254
+ * Build the system.loop_detected event to be published.
255
+ */
256
+ export function buildLoopDetectedEvent(deploymentId, convKey, triggerEvent) {
257
+ return {
258
+ v: 2,
259
+ id: crypto.randomUUID(),
260
+ source: "system",
261
+ type: "system.loop_detected",
262
+ topics: ["system.loop_detected"],
263
+ delivery: "bulk",
264
+ text: `Circuit breaker tripped: deployment=${deploymentId} conversation=${convKey} trigger=${triggerEvent.type}`,
265
+ fields: {
266
+ deployment_id: deploymentId,
267
+ conversation_key: convKey,
268
+ trigger_event_type: triggerEvent.type,
269
+ trigger_event_id: triggerEvent.id,
270
+ threshold: BREAKER_THRESHOLD,
271
+ window_ms: BREAKER_WINDOW_MS,
272
+ cooldown_ms: BREAKER_COOLDOWN_MS,
273
+ },
274
+ timestamp: new Date().toISOString(),
275
+ payload: {
276
+ deployment_id: deploymentId,
277
+ conversation_key: convKey,
278
+ trigger_event: triggerEvent,
279
+ },
280
+ };
281
+ }
282
+ // ---------------------------------------------------------------------------
283
+ // Reset (for testing)
284
+ // ---------------------------------------------------------------------------
285
+ export function resetAllBreakers() {
286
+ states.clear();
287
+ }
288
+ //# sourceMappingURL=circuit-breaker.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"circuit-breaker.js","sourceRoot":"","sources":["../src/circuit-breaker.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Conversation references - channel-agnostic addressing for chat replies (#618).
3
+ *
4
+ * A conversation reference is an opaque string the agent echoes back verbatim
5
+ * to reply into the thread/DM an inbound event came from. Only adapters build
6
+ * refs and only the gateway parses them; the agent never learns platform
7
+ * addressing semantics.
8
+ *
9
+ * Grammar:
10
+ * <source>:<scope>:<chat_type>:<chat_id>[:thread:<thread_id>]
11
+ *
12
+ * where <scope> is the platform's tenancy unit (Slack team id, WhatsApp phone
13
+ * number id). Segment values must not contain ":" - buildConversation enforces
14
+ * this, so a platform whose ids carry ":" (Matrix, Teams) needs a grammar
15
+ * extension (escaping or a new trailer), not a silent mis-parse.
16
+ */
17
+ export type ChatType = "dm" | "group" | "channel";
18
+ export interface Conversation {
19
+ source: string;
20
+ scope: string;
21
+ chatType: ChatType;
22
+ chatId: string;
23
+ threadId?: string;
24
+ }
25
+ export declare function buildConversation(c: Conversation): string;
26
+ export declare function parseConversation(ref: string): Conversation | null;
27
+ export declare function slackChatType(channelType: string): ChatType;
28
+ export declare function slackConversation(teamId: string, channel: string, channelType: string, ts: string, threadTs?: string): string | undefined;
29
+ //# sourceMappingURL=conversation.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"conversation.d.ts","sourceRoot":"","sources":["../src/conversation.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,MAAM,MAAM,QAAQ,GAAG,IAAI,GAAG,OAAO,GAAG,SAAS,CAAC;AAElD,MAAM,WAAW,YAAY;IAC5B,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,QAAQ,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;CAClB;AAID,wBAAgB,iBAAiB,CAAC,CAAC,EAAE,YAAY,GAAG,MAAM,CAUzD;AAED,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,YAAY,GAAG,IAAI,CAkBlE;AAID,wBAAgB,aAAa,CAAC,WAAW,EAAE,MAAM,GAAG,QAAQ,CAI3D;AAMD,wBAAgB,iBAAiB,CAChC,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,MAAM,EACf,WAAW,EAAE,MAAM,EACnB,EAAE,EAAE,MAAM,EACV,QAAQ,CAAC,EAAE,MAAM,GACf,MAAM,GAAG,SAAS,CAgBpB"}
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Conversation references - channel-agnostic addressing for chat replies (#618).
3
+ *
4
+ * A conversation reference is an opaque string the agent echoes back verbatim
5
+ * to reply into the thread/DM an inbound event came from. Only adapters build
6
+ * refs and only the gateway parses them; the agent never learns platform
7
+ * addressing semantics.
8
+ *
9
+ * Grammar:
10
+ * <source>:<scope>:<chat_type>:<chat_id>[:thread:<thread_id>]
11
+ *
12
+ * where <scope> is the platform's tenancy unit (Slack team id, WhatsApp phone
13
+ * number id). Segment values must not contain ":" - buildConversation enforces
14
+ * this, so a platform whose ids carry ":" (Matrix, Teams) needs a grammar
15
+ * extension (escaping or a new trailer), not a silent mis-parse.
16
+ */
17
+ const CHAT_TYPES = new Set(["dm", "group", "channel"]);
18
+ export function buildConversation(c) {
19
+ const segments = [c.source, c.scope, c.chatType, c.chatId];
20
+ if (c.threadId !== undefined)
21
+ segments.push(c.threadId);
22
+ for (const s of segments) {
23
+ if (!s || s.includes(":")) {
24
+ throw new Error(`invalid conversation segment: ${JSON.stringify(s)}`);
25
+ }
26
+ }
27
+ const base = `${c.source}:${c.scope}:${c.chatType}:${c.chatId}`;
28
+ return c.threadId ? `${base}:thread:${c.threadId}` : base;
29
+ }
30
+ export function parseConversation(ref) {
31
+ if (typeof ref !== "string")
32
+ return null;
33
+ const parts = ref.split(":");
34
+ if (parts.length !== 4 && parts.length !== 6)
35
+ return null;
36
+ if (parts.some((p) => p === ""))
37
+ return null;
38
+ const [source, scope, chatType, chatId] = parts;
39
+ if (!CHAT_TYPES.has(chatType))
40
+ return null;
41
+ const conv = {
42
+ source,
43
+ scope,
44
+ chatType: chatType,
45
+ chatId,
46
+ };
47
+ if (parts.length === 6) {
48
+ if (parts[4] !== "thread")
49
+ return null;
50
+ conv.threadId = parts[5];
51
+ }
52
+ return conv;
53
+ }
54
+ // Slack channel_type -> chat_type. "im" is a 1:1 DM, "mpim" a group DM;
55
+ // public ("channel") and private ("group") channels both address as channel.
56
+ export function slackChatType(channelType) {
57
+ if (channelType === "im")
58
+ return "dm";
59
+ if (channelType === "mpim")
60
+ return "group";
61
+ return "channel";
62
+ }
63
+ // Build the reply address for an inbound Slack message, or undefined when the
64
+ // payload lacks usable ids. Owns the reply-anchoring policy: replies land in
65
+ // the originating thread, and a top-level message anchors its own thread (ts),
66
+ // matching where the placeholder handler posts.
67
+ export function slackConversation(teamId, channel, channelType, ts, threadTs) {
68
+ if (!teamId || !channel)
69
+ return undefined;
70
+ const anchor = threadTs || ts;
71
+ try {
72
+ return buildConversation({
73
+ source: "slack",
74
+ scope: teamId,
75
+ chatType: slackChatType(channelType),
76
+ chatId: channel,
77
+ ...(anchor ? { threadId: anchor } : {}),
78
+ });
79
+ }
80
+ catch {
81
+ // A colon-bearing id in the webhook payload must not kill normalization;
82
+ // the event just ships without a reply address.
83
+ return undefined;
84
+ }
85
+ }
86
+ //# sourceMappingURL=conversation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"conversation.js","sourceRoot":"","sources":["../src/conversation.ts"],"names":[],"mappings":"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"}