@copilotkit/channels-slack 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 (141) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +353 -0
  3. package/dist/__tests__/assistant.test.d.ts +2 -0
  4. package/dist/__tests__/assistant.test.d.ts.map +1 -0
  5. package/dist/__tests__/assistant.test.js +153 -0
  6. package/dist/__tests__/auto-close-streaming.test.d.ts +2 -0
  7. package/dist/__tests__/auto-close-streaming.test.d.ts.map +1 -0
  8. package/dist/__tests__/auto-close-streaming.test.js +139 -0
  9. package/dist/__tests__/built-ins.test.d.ts +2 -0
  10. package/dist/__tests__/built-ins.test.d.ts.map +1 -0
  11. package/dist/__tests__/built-ins.test.js +67 -0
  12. package/dist/__tests__/chunked-message-stream.test.d.ts +2 -0
  13. package/dist/__tests__/chunked-message-stream.test.d.ts.map +1 -0
  14. package/dist/__tests__/chunked-message-stream.test.js +226 -0
  15. package/dist/__tests__/conversation-store.test.d.ts +2 -0
  16. package/dist/__tests__/conversation-store.test.d.ts.map +1 -0
  17. package/dist/__tests__/conversation-store.test.js +209 -0
  18. package/dist/__tests__/download-files.test.d.ts +2 -0
  19. package/dist/__tests__/download-files.test.d.ts.map +1 -0
  20. package/dist/__tests__/download-files.test.js +142 -0
  21. package/dist/__tests__/ephemeral.test.d.ts +2 -0
  22. package/dist/__tests__/ephemeral.test.d.ts.map +1 -0
  23. package/dist/__tests__/ephemeral.test.js +85 -0
  24. package/dist/__tests__/event-renderer.test.d.ts +2 -0
  25. package/dist/__tests__/event-renderer.test.d.ts.map +1 -0
  26. package/dist/__tests__/event-renderer.test.js +521 -0
  27. package/dist/__tests__/markdown-to-mrkdwn.test.d.ts +2 -0
  28. package/dist/__tests__/markdown-to-mrkdwn.test.d.ts.map +1 -0
  29. package/dist/__tests__/markdown-to-mrkdwn.test.js +80 -0
  30. package/dist/__tests__/message-stream.test.d.ts +2 -0
  31. package/dist/__tests__/message-stream.test.d.ts.map +1 -0
  32. package/dist/__tests__/message-stream.test.js +116 -0
  33. package/dist/__tests__/native-renderer.test.d.ts +2 -0
  34. package/dist/__tests__/native-renderer.test.d.ts.map +1 -0
  35. package/dist/__tests__/native-renderer.test.js +236 -0
  36. package/dist/__tests__/native-stream.test.d.ts +2 -0
  37. package/dist/__tests__/native-stream.test.d.ts.map +1 -0
  38. package/dist/__tests__/native-stream.test.js +235 -0
  39. package/dist/__tests__/reactions.test.d.ts +2 -0
  40. package/dist/__tests__/reactions.test.d.ts.map +1 -0
  41. package/dist/__tests__/reactions.test.js +193 -0
  42. package/dist/__tests__/slack-listener.test.d.ts +2 -0
  43. package/dist/__tests__/slack-listener.test.d.ts.map +1 -0
  44. package/dist/__tests__/slack-listener.test.js +559 -0
  45. package/dist/__tests__/views.test.d.ts +2 -0
  46. package/dist/__tests__/views.test.d.ts.map +1 -0
  47. package/dist/__tests__/views.test.js +143 -0
  48. package/dist/adapter.d.ts +188 -0
  49. package/dist/adapter.d.ts.map +1 -0
  50. package/dist/adapter.js +859 -0
  51. package/dist/adapter.test.d.ts +2 -0
  52. package/dist/adapter.test.d.ts.map +1 -0
  53. package/dist/adapter.test.js +368 -0
  54. package/dist/assistant.d.ts +19 -0
  55. package/dist/assistant.d.ts.map +1 -0
  56. package/dist/assistant.js +124 -0
  57. package/dist/auto-close-streaming.d.ts +62 -0
  58. package/dist/auto-close-streaming.d.ts.map +1 -0
  59. package/dist/auto-close-streaming.js +273 -0
  60. package/dist/built-in-context.d.ts +22 -0
  61. package/dist/built-in-context.d.ts.map +1 -0
  62. package/dist/built-in-context.js +65 -0
  63. package/dist/built-in-tools.d.ts +23 -0
  64. package/dist/built-in-tools.d.ts.map +1 -0
  65. package/dist/built-in-tools.js +45 -0
  66. package/dist/chunked-message-stream.d.ts +67 -0
  67. package/dist/chunked-message-stream.d.ts.map +1 -0
  68. package/dist/chunked-message-stream.js +134 -0
  69. package/dist/codec.d.ts +18 -0
  70. package/dist/codec.d.ts.map +1 -0
  71. package/dist/codec.js +18 -0
  72. package/dist/codec.test.d.ts +2 -0
  73. package/dist/codec.test.d.ts.map +1 -0
  74. package/dist/codec.test.js +19 -0
  75. package/dist/conversation-store.d.ts +92 -0
  76. package/dist/conversation-store.d.ts.map +1 -0
  77. package/dist/conversation-store.js +196 -0
  78. package/dist/download-files.d.ts +59 -0
  79. package/dist/download-files.d.ts.map +1 -0
  80. package/dist/download-files.js +116 -0
  81. package/dist/event-renderer.d.ts +98 -0
  82. package/dist/event-renderer.d.ts.map +1 -0
  83. package/dist/event-renderer.js +493 -0
  84. package/dist/index.d.ts +34 -0
  85. package/dist/index.d.ts.map +1 -0
  86. package/dist/index.js +23 -0
  87. package/dist/ingress-normalize.d.ts +75 -0
  88. package/dist/ingress-normalize.d.ts.map +1 -0
  89. package/dist/ingress-normalize.js +153 -0
  90. package/dist/ingress-normalize.test.d.ts +2 -0
  91. package/dist/ingress-normalize.test.d.ts.map +1 -0
  92. package/dist/ingress-normalize.test.js +187 -0
  93. package/dist/interaction.d.ts +30 -0
  94. package/dist/interaction.d.ts.map +1 -0
  95. package/dist/interaction.js +219 -0
  96. package/dist/interaction.test.d.ts +2 -0
  97. package/dist/interaction.test.d.ts.map +1 -0
  98. package/dist/interaction.test.js +137 -0
  99. package/dist/markdown-to-mrkdwn.d.ts +21 -0
  100. package/dist/markdown-to-mrkdwn.d.ts.map +1 -0
  101. package/dist/markdown-to-mrkdwn.js +99 -0
  102. package/dist/message-stream.d.ts +44 -0
  103. package/dist/message-stream.d.ts.map +1 -0
  104. package/dist/message-stream.js +70 -0
  105. package/dist/native-stream.d.ts +131 -0
  106. package/dist/native-stream.d.ts.map +1 -0
  107. package/dist/native-stream.js +208 -0
  108. package/dist/render/block-kit.d.ts +35 -0
  109. package/dist/render/block-kit.d.ts.map +1 -0
  110. package/dist/render/block-kit.js +399 -0
  111. package/dist/render/block-kit.test.d.ts +2 -0
  112. package/dist/render/block-kit.test.d.ts.map +1 -0
  113. package/dist/render/block-kit.test.js +289 -0
  114. package/dist/render/budget.d.ts +24 -0
  115. package/dist/render/budget.d.ts.map +1 -0
  116. package/dist/render/budget.js +30 -0
  117. package/dist/render/budget.test.d.ts +2 -0
  118. package/dist/render/budget.test.d.ts.map +1 -0
  119. package/dist/render/budget.test.js +49 -0
  120. package/dist/render/modal.d.ts +13 -0
  121. package/dist/render/modal.d.ts.map +1 -0
  122. package/dist/render/modal.js +108 -0
  123. package/dist/render/transport.d.ts +42 -0
  124. package/dist/render/transport.d.ts.map +1 -0
  125. package/dist/render/transport.js +1 -0
  126. package/dist/render.d.ts +23 -0
  127. package/dist/render.d.ts.map +1 -0
  128. package/dist/render.js +20 -0
  129. package/dist/sanitizing-http-agent.d.ts +28 -0
  130. package/dist/sanitizing-http-agent.d.ts.map +1 -0
  131. package/dist/sanitizing-http-agent.js +51 -0
  132. package/dist/slack-listener.d.ts +73 -0
  133. package/dist/slack-listener.d.ts.map +1 -0
  134. package/dist/slack-listener.js +126 -0
  135. package/dist/slack-listener.test.d.ts +2 -0
  136. package/dist/slack-listener.test.d.ts.map +1 -0
  137. package/dist/slack-listener.test.js +115 -0
  138. package/dist/types.d.ts +153 -0
  139. package/dist/types.d.ts.map +1 -0
  140. package/dist/types.js +20 -0
  141. package/package.json +73 -0
@@ -0,0 +1,493 @@
1
+ import { ChunkedMessageStream } from "./chunked-message-stream.js";
2
+ import { markdownToMrkdwn } from "./markdown-to-mrkdwn.js";
3
+ import { autoCloseOpenMarkdown } from "./auto-close-streaming.js";
4
+ import { NativeMessageStream } from "./native-stream.js";
5
+ const DEFAULT_THINKING_STATUS = "is thinking…";
6
+ /**
7
+ * The display-transform applied to every streaming chunk before it hits
8
+ * `chat.update` (the LEGACY transport only — the native path streams raw
9
+ * markdown). Composed of two pure functions:
10
+ *
11
+ * 1. `autoCloseOpenMarkdown` — closes dangling fenced code, inline
12
+ * backticks, bold/italic/strike so a mid-stream buffer renders as
13
+ * valid mrkdwn (instead of leaking code-styling through the rest of
14
+ * the Slack message). When the agent eventually emits the real
15
+ * close marker the buffer becomes balanced and this step adds
16
+ * nothing, so the committed Slack message is never double-closed.
17
+ *
18
+ * 2. `markdownToMrkdwn` — translates the (now-balanced) markdown into
19
+ * Slack mrkdwn.
20
+ */
21
+ const displayTransform = (text) => markdownToMrkdwn(autoCloseOpenMarkdown(text));
22
+ const INTERRUPTED_SUFFIX = "\n_(interrupted)_";
23
+ /**
24
+ * Construct a {@link RunRenderer} for a single agent run in Slack.
25
+ *
26
+ * The `subscriber` is passed to `runAgent`. After the run resolves, the
27
+ * run-loop reads `getCapturedToolCalls()` (filtering by which tools are
28
+ * registered) and `getPendingInterrupt()`, then calls `finish()` once at
29
+ * turn end. `markInterrupted()` is called when a new turn arrives for the
30
+ * same conversation while this run is still streaming — it appends a
31
+ * `_(interrupted)_` marker to any partial reply so the user sees a clear
32
+ * visual cue that the bot stopped.
33
+ *
34
+ * ## Native vs legacy text streaming
35
+ *
36
+ * When `nativeStreaming` is set, the run uses a SINGLE turn-scoped
37
+ * `chat.startStream` message for the whole turn: text from every AG-UI
38
+ * message accumulates into it (separated by blank lines), and tool calls
39
+ * surface as native `task_update` chunks INSIDE that message. The message is
40
+ * finalized once, at `finish()`, optionally carrying a feedback row. The
41
+ * legacy path keeps the prior behavior — one `chat.update` message per AG-UI
42
+ * text message, plus separate `:wrench:` tool-status rows.
43
+ */
44
+ export function createRunRenderer(args) {
45
+ const { transport, target } = args;
46
+ const interruptEventNames = args.interruptEventNames ?? new Set(["on_interrupt"]);
47
+ const showToolStatus = args.showToolStatus ?? true;
48
+ const nativeMode = args.nativeStreaming !== undefined;
49
+ // ── Native status mode ──────────────────────────────────────────────
50
+ // Whenever the reply is anchored to a thread, the run lifecycle drives
51
+ // Slack's native `assistant.threads.setStatus` ("is thinking…") instead of a
52
+ // placeholder message. `isPane` only selects how *tool* progress is shown.
53
+ const status = args.status;
54
+ const statusMode = status !== undefined;
55
+ const isPane = status?.isPane ?? false;
56
+ const paneToolStatus = status?.config?.toolStatus ?? true;
57
+ const setStatus = async (text) => {
58
+ if (!status)
59
+ return;
60
+ try {
61
+ await transport.setStatus({
62
+ channel_id: target.channel,
63
+ thread_ts: status.threadTs,
64
+ status: text,
65
+ ...(text && status.config?.loadingMessages
66
+ ? { loading_messages: [...status.config.loadingMessages] }
67
+ : {}),
68
+ });
69
+ }
70
+ catch (err) {
71
+ console.error("[slack-renderer] setStatus failed:", err);
72
+ }
73
+ };
74
+ /** Clear the native status (best-effort). */
75
+ const clearStatus = async () => {
76
+ if (!statusMode)
77
+ return;
78
+ await setStatus("");
79
+ };
80
+ /** Whether this run has posted any visible reply yet (drives status clear). */
81
+ let postedReply = false;
82
+ const onFirstReply = async () => {
83
+ if (postedReply)
84
+ return;
85
+ postedReply = true;
86
+ // Slack auto-clears status when the app replies; clear explicitly too so
87
+ // there's no lingering indicator on slow/edge paths.
88
+ await clearStatus();
89
+ };
90
+ // ── Legacy per-message streaming state ──────────────────────────────
91
+ /** Per-AG-UI-message accumulated text (legacy path; native uses one buffer). */
92
+ const buffers = new Map();
93
+ /** Per-AG-UI-message text stream (legacy). Lazily created on first content. */
94
+ const streams = new Map();
95
+ /** Per-tool-call Slack message ts so we can edit it on END (legacy/degraded). */
96
+ const toolStatusTs = new Map();
97
+ /**
98
+ * Once a stream has been "finalised" (either via TEXT_MESSAGE_END or via
99
+ * markInterrupted), we drop it. Late-arriving events for the same
100
+ * messageId are ignored — they'd otherwise reopen a closed stream and
101
+ * post a fresh placeholder *after* the user already saw the message
102
+ * was over.
103
+ */
104
+ const finalised = new Set();
105
+ // ── Native turn-scoped streaming state ──────────────────────────────
106
+ /** The single native stream for the whole turn (lazily created). */
107
+ let turnStream;
108
+ /** All text streamed this turn (every AG-UI message concatenated). */
109
+ let turnText = "";
110
+ /** Set after a TEXT_MESSAGE_END so the next message inserts a blank-line gap. */
111
+ let pendingSeparator = false;
112
+ /** True once the turn stream has been finalized (interrupt / error / finish). */
113
+ let turnFinalised = false;
114
+ /**
115
+ * Whether native structured chunks (`task_update`) are usable. Flipped off
116
+ * the first time a chunk append fails (old workspace / missing scope), after
117
+ * which tool progress degrades to `:wrench:` rows.
118
+ */
119
+ let taskChunksOk = nativeMode && (args.nativeStreaming?.taskChunks ?? true);
120
+ /**
121
+ * Set when the caller intentionally aborted the run (i.e. a new turn
122
+ * arrived for the same conversation). Suppresses the RUN_ERROR /
123
+ * AbortError warning and stops accepting further AG-UI events — the
124
+ * `_(interrupted)_` marker conveys the state visually.
125
+ */
126
+ let aborted = false;
127
+ /** Tool calls observed in this run, in event order. */
128
+ const capturedToolCalls = [];
129
+ /**
130
+ * Interrupt observed via a matching `onCustomEvent`. The React
131
+ * `useInterrupt` pattern buffers locally and commits on run-finalize,
132
+ * which we mirror via this single-slot variable — the run-loop reads it
133
+ * after `runAgent` resolves.
134
+ */
135
+ let pendingInterrupt;
136
+ // The shipped chat.update streamer (mrkdwn-translated). Also the automatic
137
+ // fallback for the native transport.
138
+ const makeLegacyStream = () => new ChunkedMessageStream({
139
+ postPlaceholder: async (text) => {
140
+ const posted = await transport.postMessage({
141
+ channel: target.channel,
142
+ thread_ts: target.threadTs,
143
+ text,
144
+ });
145
+ if (!posted.ts)
146
+ throw new Error("postMessage returned no ts");
147
+ await onFirstReply();
148
+ return posted.ts;
149
+ },
150
+ updateAt: async (ts, text) => {
151
+ await transport.updateMessage({ channel: target.channel, ts, text });
152
+ },
153
+ transform: displayTransform,
154
+ });
155
+ // The single native turn stream — raw markdown, no mrkdwn translation, with
156
+ // interleaved `task_update` chunks.
157
+ const ensureTurnStream = () => {
158
+ if (turnStream)
159
+ return turnStream;
160
+ const ns = args.nativeStreaming;
161
+ turnStream = new NativeMessageStream({
162
+ transport: {
163
+ startStream: async () => {
164
+ const ts = await ns.transport.startStream();
165
+ // The native message owns the bubble now; clear the native status
166
+ // (the reply is visible).
167
+ await onFirstReply();
168
+ return ts;
169
+ },
170
+ appendText: (ts, md) => ns.transport.appendText(ts, md),
171
+ appendChunks: (ts, chunks) => ns.transport.appendChunks(ts, chunks),
172
+ stopStream: (ts, blocks) => ns.transport.stopStream(ts, blocks),
173
+ },
174
+ fallback: makeLegacyStream,
175
+ onStartFailure: ns.onStartFailure,
176
+ onChunkFailure: () => {
177
+ // Structured chunks unsupported on this workspace — degrade tool
178
+ // progress to `:wrench:` rows for the rest of the run, and let the
179
+ // adapter persist it so later turns skip chunks entirely.
180
+ taskChunksOk = false;
181
+ ns.onChunkFailure?.();
182
+ },
183
+ });
184
+ return turnStream;
185
+ };
186
+ const ensureLegacyStream = (messageId) => {
187
+ if (finalised.has(messageId))
188
+ return undefined;
189
+ let s = streams.get(messageId);
190
+ if (!s) {
191
+ s = makeLegacyStream();
192
+ streams.set(messageId, s);
193
+ }
194
+ return s;
195
+ };
196
+ const captureToolCall = (toolCallId, toolCallName, toolCallArgs) => {
197
+ const existing = capturedToolCalls.find((c) => c.toolCallId === toolCallId);
198
+ if (existing) {
199
+ existing.toolCallName = toolCallName;
200
+ existing.toolCallArgs = toolCallArgs;
201
+ }
202
+ else {
203
+ capturedToolCalls.push({ toolCallId, toolCallName, toolCallArgs });
204
+ }
205
+ };
206
+ /** Post a `:wrench:` tool-start row (legacy path / native degradation). */
207
+ const postToolStartRow = async (toolCallId, toolCallName) => {
208
+ if (!showToolStatus)
209
+ return;
210
+ if (toolStatusTs.has(toolCallId))
211
+ return; // dedup
212
+ try {
213
+ const posted = await transport.postMessage({
214
+ channel: target.channel,
215
+ thread_ts: target.threadTs,
216
+ text: `:wrench: Calling \`${toolCallName}\`…`,
217
+ });
218
+ if (posted.ts)
219
+ toolStatusTs.set(toolCallId, posted.ts);
220
+ }
221
+ catch (err) {
222
+ console.error("[slack-renderer] tool-start post failed:", err);
223
+ }
224
+ };
225
+ /** Edit the `:wrench:` row to a checkmark on END (legacy path / native degradation). */
226
+ const finishToolStatusRow = async (toolCallId, toolCallName) => {
227
+ if (!showToolStatus)
228
+ return;
229
+ const ts = toolStatusTs.get(toolCallId);
230
+ if (!ts)
231
+ return;
232
+ try {
233
+ await transport.updateMessage({
234
+ channel: target.channel,
235
+ ts,
236
+ text: `:white_check_mark: \`${toolCallName}\``,
237
+ });
238
+ }
239
+ catch (err) {
240
+ console.error("[slack-renderer] tool-end update failed:", err);
241
+ }
242
+ toolStatusTs.delete(toolCallId);
243
+ };
244
+ const subscriber = {
245
+ // ── 0. Run lifecycle: thinking indicator ───────────────────────────
246
+ async onRunStartedEvent() {
247
+ if (aborted)
248
+ return;
249
+ // Native "is thinking…" status on the thread. `runAgent` fires once per
250
+ // tool-loop iteration, so we (re)set it on each RUN_STARTED — Slack also
251
+ // resets its 2-minute status timeout on each call.
252
+ if (statusMode) {
253
+ await setStatus(status?.config?.thinking || DEFAULT_THINKING_STATUS);
254
+ }
255
+ },
256
+ async onRunFinishedEvent() {
257
+ // The NATIVE turn stream is NOT finalized here: the run-loop may
258
+ // re-invoke for tool results, so it stays open and is finalized in
259
+ // `finish()`. For the LEGACY per-message path, defensively finalize any
260
+ // stream still open at the end of THIS run so its text is fully posted
261
+ // before the run-loop executes tool handlers that post out-of-band
262
+ // content (images/cards). In native mode `streams` is empty, so this
263
+ // loop is a no-op.
264
+ const drains = [];
265
+ for (const [id, stream] of Array.from(streams.entries())) {
266
+ drains.push(stream.finish());
267
+ streams.delete(id);
268
+ finalised.add(id);
269
+ buffers.delete(id);
270
+ }
271
+ if (drains.length > 0)
272
+ await Promise.all(drains);
273
+ // A posted reply auto-clears Slack's status; clear it explicitly when the
274
+ // run produced no visible reply yet (tool/component/HITL output, or none).
275
+ if (statusMode && !postedReply)
276
+ await clearStatus();
277
+ },
278
+ // ── 1. Text streaming ──────────────────────────────────────────────
279
+ onTextMessageStartEvent({ event }) {
280
+ if (aborted)
281
+ return;
282
+ if (nativeMode) {
283
+ // A new message after prior text → blank-line gap in the one bubble.
284
+ if (turnText.length > 0)
285
+ pendingSeparator = true;
286
+ return;
287
+ }
288
+ buffers.set(event.messageId, "");
289
+ },
290
+ onTextMessageContentEvent({ event }) {
291
+ if (aborted)
292
+ return;
293
+ const delta = event.delta ?? "";
294
+ if (nativeMode) {
295
+ if (turnFinalised)
296
+ return;
297
+ if (pendingSeparator && turnText.length > 0) {
298
+ turnText += "\n\n";
299
+ pendingSeparator = false;
300
+ }
301
+ turnText += delta;
302
+ ensureTurnStream().append(turnText);
303
+ return;
304
+ }
305
+ const next = (buffers.get(event.messageId) ?? "") + delta;
306
+ buffers.set(event.messageId, next);
307
+ ensureLegacyStream(event.messageId)?.append(next);
308
+ },
309
+ async onTextMessageEndEvent({ event }) {
310
+ if (aborted)
311
+ return;
312
+ if (nativeMode) {
313
+ // Do NOT finish the turn stream — more messages / tools may follow.
314
+ // The blank-line separator is applied on the next message's first
315
+ // content. Finalization happens in `finish()`.
316
+ return;
317
+ }
318
+ const stream = streams.get(event.messageId);
319
+ if (stream) {
320
+ await stream.finish();
321
+ streams.delete(event.messageId);
322
+ }
323
+ buffers.delete(event.messageId);
324
+ finalised.add(event.messageId);
325
+ },
326
+ // ── 2. Tool-call surfacing + capture ──────────────────────────────
327
+ async onToolCallStartEvent({ event }) {
328
+ if (aborted)
329
+ return;
330
+ // Composer "is using `tool`…" status on ANY thread anchor (not just
331
+ // panes) — the native status API works on every surface. Each setStatus
332
+ // also resets Slack's status timeout. The pane's `toolStatus` config knob
333
+ // still gates panes; every other surface shows it when tool status is on.
334
+ if (statusMode && showToolStatus && (isPane ? paneToolStatus : true)) {
335
+ await setStatus(`is using \`${event.toolCallName}\`…`);
336
+ }
337
+ // Panes surface tool activity ONLY as composer status — no in-thread rows.
338
+ if (isPane)
339
+ return;
340
+ // Native path: also surface the call as an in-message `task_update` chunk.
341
+ if (nativeMode && taskChunksOk) {
342
+ if (showToolStatus) {
343
+ ensureTurnStream().appendChunk({
344
+ type: "task_update",
345
+ id: event.toolCallId,
346
+ title: `Using \`${event.toolCallName}\``,
347
+ status: "in_progress",
348
+ });
349
+ }
350
+ return;
351
+ }
352
+ // Legacy path (or native degraded): a `:wrench:` status row.
353
+ await postToolStartRow(event.toolCallId, event.toolCallName);
354
+ },
355
+ onToolCallArgsEvent({ event, toolCallName, partialToolCallArgs }) {
356
+ if (aborted)
357
+ return;
358
+ // Accumulate partial args so the captured entry tracks the latest
359
+ // parsed shape as args stream in. We keep a single entry per
360
+ // toolCallId, overwriting `toolCallArgs`.
361
+ captureToolCall(event.toolCallId, toolCallName, (partialToolCallArgs ?? {}));
362
+ },
363
+ async onToolCallEndEvent({ event, toolCallName, toolCallArgs }) {
364
+ if (aborted)
365
+ return;
366
+ // Capture EVERY tool call — the run-loop filters by which tools are
367
+ // registered. A tool with no `args` events still lands here.
368
+ captureToolCall(event.toolCallId, toolCallName, (toolCallArgs ?? {}));
369
+ // Pane threads use live status (set on START); no per-call rows to edit.
370
+ if (isPane)
371
+ return;
372
+ // Native path: complete the in-message `task_update`.
373
+ if (nativeMode && taskChunksOk) {
374
+ if (showToolStatus) {
375
+ ensureTurnStream().appendChunk({
376
+ type: "task_update",
377
+ id: event.toolCallId,
378
+ title: `Used \`${toolCallName}\``,
379
+ status: "complete",
380
+ });
381
+ }
382
+ return;
383
+ }
384
+ // Legacy path (or native degraded): edit the `:wrench:` row to a check.
385
+ await finishToolStatusRow(event.toolCallId, toolCallName);
386
+ },
387
+ // ── 3. Interrupts (LangGraph `interrupt()` → AG-UI custom event) ─
388
+ onCustomEvent({ event }) {
389
+ if (aborted)
390
+ return;
391
+ const e = event;
392
+ if (!e.name || !interruptEventNames.has(e.name))
393
+ return;
394
+ // LangGraph's AG-UI adapter ships the interrupt value as a JSON
395
+ // string in some shapes (and as an object in others). Normalize
396
+ // here so downstream schema validation always sees the parsed shape.
397
+ let value = e.value;
398
+ if (typeof value === "string") {
399
+ try {
400
+ value = JSON.parse(value);
401
+ }
402
+ catch {
403
+ // Leave it as a string — the handler's schema will reject it
404
+ // explicitly with a clearer error.
405
+ }
406
+ }
407
+ pendingInterrupt = { eventName: e.name, value };
408
+ },
409
+ // ── 4. Errors ─────────────────────────────────────────────────────
410
+ async onRunErrorEvent({ event }) {
411
+ // Don't post a warning if we're the ones aborting the run; the
412
+ // `_(interrupted)_` marker on the partial reply is the user-visible
413
+ // signal in that case.
414
+ if (statusMode)
415
+ await clearStatus();
416
+ // Close any open native turn stream so the partial reply is committed.
417
+ await finalizeTurnStream();
418
+ if (aborted)
419
+ return;
420
+ try {
421
+ await transport.postMessage({
422
+ channel: target.channel,
423
+ thread_ts: target.threadTs,
424
+ text: `:warning: Agent error: ${event.message ?? "unknown error"}`,
425
+ });
426
+ }
427
+ catch (err) {
428
+ console.error("[slack-renderer] error notice failed:", err);
429
+ }
430
+ },
431
+ };
432
+ /** Finalize the native turn stream once (idempotent), attaching feedback if any. */
433
+ const finalizeTurnStream = async () => {
434
+ if (turnFinalised)
435
+ return;
436
+ turnFinalised = true;
437
+ if (turnStream) {
438
+ // Attach the feedback row only to a COMPLETE reply that streamed text —
439
+ // never to an interrupted/aborted partial (no point rating a half answer).
440
+ const blocks = !aborted && turnText.length > 0 ? args.feedbackBlocks : undefined;
441
+ await turnStream.finish(blocks);
442
+ }
443
+ };
444
+ return {
445
+ subscriber,
446
+ getCapturedToolCalls: () => capturedToolCalls,
447
+ getPendingInterrupt: () => pendingInterrupt,
448
+ clearPendingInterrupt: () => {
449
+ pendingInterrupt = undefined;
450
+ },
451
+ async finish() {
452
+ // Turn-end hook (called by the engine after the run-loop resolves).
453
+ // No-op if the run was interrupted (markInterrupted already drained).
454
+ if (aborted)
455
+ return;
456
+ await finalizeTurnStream();
457
+ },
458
+ async markInterrupted() {
459
+ // Idempotent. Mark BEFORE any await so subsequent subscriber
460
+ // callbacks (including the RUN_ERROR that AG-UI fires when we
461
+ // abort) see the flag and bail.
462
+ if (aborted)
463
+ return;
464
+ aborted = true;
465
+ // Clear the native status; the interrupted marker (or the next turn) is
466
+ // the user-visible signal now.
467
+ if (statusMode)
468
+ await clearStatus();
469
+ // Native turn stream: append the interrupted marker to the partial reply
470
+ // and finalize it.
471
+ if (nativeMode) {
472
+ if (turnStream && turnText.length > 0 && !turnFinalised) {
473
+ turnStream.append(turnText + INTERRUPTED_SUFFIX);
474
+ }
475
+ await finalizeTurnStream();
476
+ }
477
+ // Legacy per-message streams: append the marker and drain. Streams with
478
+ // no content yet are silently dropped (the bot never posted anything).
479
+ const tasks = [];
480
+ for (const [id, stream] of Array.from(streams.entries())) {
481
+ const buf = buffers.get(id) ?? "";
482
+ if (buf.length > 0) {
483
+ stream.append(buf + INTERRUPTED_SUFFIX);
484
+ tasks.push(stream.finish());
485
+ }
486
+ streams.delete(id);
487
+ finalised.add(id);
488
+ }
489
+ buffers.clear();
490
+ await Promise.all(tasks);
491
+ },
492
+ };
493
+ }
@@ -0,0 +1,34 @@
1
+ export { SlackConversationStore } from "./conversation-store.js";
2
+ export type { AgentSession } from "./conversation-store.js";
3
+ export { slackCodec } from "./codec.js";
4
+ export { normalizeSlackEvent, stripMentions, deriveEventId, isPlainUserMessage, } from "./ingress-normalize.js";
5
+ export type { SlackNeutralEvent, PlainUserMessage, } from "./ingress-normalize.js";
6
+ export type { ConversationKey, IncomingTurn, ResolvedSlackRespondToOptions, ReplyTarget, SlackAssistantOptions, SlackAppMentionOptions, SlackFeedback, SlackFeedbackOptions, SlackMentionReplyMode, SlackRespondToOptions, SlackThreadReplyMode, } from "./types.js";
7
+ export { DEFAULT_SLACK_RESPOND_TO_OPTIONS, DM_SCOPE, resolveSlackRespondToOptions, } from "./types.js";
8
+ export { MessageStream } from "./message-stream.js";
9
+ export type { MessageStreamConfig } from "./message-stream.js";
10
+ export { ChunkedMessageStream } from "./chunked-message-stream.js";
11
+ export type { ChunkedMessageStreamConfig } from "./chunked-message-stream.js";
12
+ export { NativeMessageStream } from "./native-stream.js";
13
+ export type { NativeMessageStreamConfig, NativeStreamTransport, TextStream, } from "./native-stream.js";
14
+ export { attachAssistant } from "./assistant.js";
15
+ export type { AttachAssistantConfig, AssistantHandle } from "./assistant.js";
16
+ export { autoCloseOpenMarkdown, detectOpenContext, renderContextOpener, } from "./auto-close-streaming.js";
17
+ export type { OpenMarkdownContext } from "./auto-close-streaming.js";
18
+ export { markdownToMrkdwn } from "./markdown-to-mrkdwn.js";
19
+ export { attachSlackListener } from "./slack-listener.js";
20
+ export type { ListenerConfig, TurnHandler } from "./slack-listener.js";
21
+ export { slackTaggingContext, slackFormattingContext, slackConversationModelContext, defaultSlackContext, } from "./built-in-context.js";
22
+ export { SanitizingHttpAgent } from "./sanitizing-http-agent.js";
23
+ export { buildFileContentParts } from "./download-files.js";
24
+ export type { SlackFileRef, AgentContentPart, FileDeliveryConfig, } from "./download-files.js";
25
+ export { lookupSlackUserTool, defaultSlackTools } from "./built-in-tools.js";
26
+ export { slack, SlackAdapter } from "./adapter.js";
27
+ export type { SlackAdapterOptions } from "./adapter.js";
28
+ export { createRunRenderer } from "./event-renderer.js";
29
+ export type { SlackRenderTransport } from "./render/transport.js";
30
+ export { decodeInteraction, conversationKeyOf } from "./interaction.js";
31
+ export { renderBlockKit, renderSlackMessage, buildFeedbackBlocks, FEEDBACK_ACTION_ID, } from "./render/block-kit.js";
32
+ export { renderSlackModal } from "./render/modal.js";
33
+ export { SLACK_LIMITS } from "./render/budget.js";
34
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,sBAAsB,EAAE,MAAM,yBAAyB,CAAC;AACjE,YAAY,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAI5D,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AACxC,OAAO,EACL,mBAAmB,EACnB,aAAa,EACb,aAAa,EACb,kBAAkB,GACnB,MAAM,wBAAwB,CAAC;AAChC,YAAY,EACV,iBAAiB,EACjB,gBAAgB,GACjB,MAAM,wBAAwB,CAAC;AAEhC,YAAY,EACV,eAAe,EACf,YAAY,EACZ,6BAA6B,EAC7B,WAAW,EACX,qBAAqB,EACrB,sBAAsB,EACtB,aAAa,EACb,oBAAoB,EACpB,qBAAqB,EACrB,qBAAqB,EACrB,oBAAoB,GACrB,MAAM,YAAY,CAAC;AACpB,OAAO,EACL,gCAAgC,EAChC,QAAQ,EACR,4BAA4B,GAC7B,MAAM,YAAY,CAAC;AAEpB,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACpD,YAAY,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AAE/D,OAAO,EAAE,oBAAoB,EAAE,MAAM,6BAA6B,CAAC;AACnE,YAAY,EAAE,0BAA0B,EAAE,MAAM,6BAA6B,CAAC;AAE9E,OAAO,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AACzD,YAAY,EACV,yBAAyB,EACzB,qBAAqB,EACrB,UAAU,GACX,MAAM,oBAAoB,CAAC;AAE5B,OAAO,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AACjD,YAAY,EAAE,qBAAqB,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAE7E,OAAO,EACL,qBAAqB,EACrB,iBAAiB,EACjB,mBAAmB,GACpB,MAAM,2BAA2B,CAAC;AACnC,YAAY,EAAE,mBAAmB,EAAE,MAAM,2BAA2B,CAAC;AAErE,OAAO,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAE3D,OAAO,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AAC1D,YAAY,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAEvE,OAAO,EACL,mBAAmB,EACnB,sBAAsB,EACtB,6BAA6B,EAC7B,mBAAmB,GACpB,MAAM,uBAAuB,CAAC;AAE/B,OAAO,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AAEjE,OAAO,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AAC5D,YAAY,EACV,YAAY,EACZ,gBAAgB,EAChB,kBAAkB,GACnB,MAAM,qBAAqB,CAAC;AAE7B,OAAO,EAAE,mBAAmB,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AAE7E,OAAO,EAAE,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AACnD,YAAY,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AAExD,OAAO,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AACxD,YAAY,EAAE,oBAAoB,EAAE,MAAM,uBAAuB,CAAC;AAElE,OAAO,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAExE,OAAO,EACL,cAAc,EACd,kBAAkB,EAClB,mBAAmB,EACnB,kBAAkB,GACnB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AACrD,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,23 @@
1
+ export { SlackConversationStore } from "./conversation-store.js";
2
+ // Pure Slack codec (render + normalize) shared with the managed/Connector-Outbox
3
+ // and webhook-ingress paths.
4
+ export { slackCodec } from "./codec.js";
5
+ export { normalizeSlackEvent, stripMentions, deriveEventId, isPlainUserMessage, } from "./ingress-normalize.js";
6
+ export { DEFAULT_SLACK_RESPOND_TO_OPTIONS, DM_SCOPE, resolveSlackRespondToOptions, } from "./types.js";
7
+ export { MessageStream } from "./message-stream.js";
8
+ export { ChunkedMessageStream } from "./chunked-message-stream.js";
9
+ export { NativeMessageStream } from "./native-stream.js";
10
+ export { attachAssistant } from "./assistant.js";
11
+ export { autoCloseOpenMarkdown, detectOpenContext, renderContextOpener, } from "./auto-close-streaming.js";
12
+ export { markdownToMrkdwn } from "./markdown-to-mrkdwn.js";
13
+ export { attachSlackListener } from "./slack-listener.js";
14
+ export { slackTaggingContext, slackFormattingContext, slackConversationModelContext, defaultSlackContext, } from "./built-in-context.js";
15
+ export { SanitizingHttpAgent } from "./sanitizing-http-agent.js";
16
+ export { buildFileContentParts } from "./download-files.js";
17
+ export { lookupSlackUserTool, defaultSlackTools } from "./built-in-tools.js";
18
+ export { slack, SlackAdapter } from "./adapter.js";
19
+ export { createRunRenderer } from "./event-renderer.js";
20
+ export { decodeInteraction, conversationKeyOf } from "./interaction.js";
21
+ export { renderBlockKit, renderSlackMessage, buildFeedbackBlocks, FEEDBACK_ACTION_ID, } from "./render/block-kit.js";
22
+ export { renderSlackModal } from "./render/modal.js";
23
+ export { SLACK_LIMITS } from "./render/budget.js";
@@ -0,0 +1,75 @@
1
+ /** Strip `<@U…>` / `<@U…|handle>` mention tokens and collapse whitespace. */
2
+ export declare const stripMentions: (text: string) => string;
3
+ /**
4
+ * Derive a stable per-delivery id for inbound idempotency. Prefer the Events
5
+ * API envelope `event_id` (stable across Slack's automatic retries), then the
6
+ * message's `client_msg_id`, then a synthesized `${channel}:${ts}`. Returns
7
+ * undefined only when none is available — never fabricate a random id (that
8
+ * would defeat dedup).
9
+ */
10
+ export declare function deriveEventId(body: unknown, event: {
11
+ client_msg_id?: string;
12
+ ts?: string;
13
+ channel?: string;
14
+ }, channel: string): string | undefined;
15
+ /** Minimal shape of a plain Slack message we care about, after filtering. */
16
+ export interface PlainUserMessage {
17
+ channel: string;
18
+ text: string;
19
+ user?: string;
20
+ thread_ts?: string;
21
+ channel_type?: string;
22
+ files?: unknown[];
23
+ /** Slack's client-generated message id; a per-delivery dedup fallback. */
24
+ client_msg_id?: string;
25
+ /** Message ts; last-resort dedup key as `${channel}:${ts}`. */
26
+ ts?: string;
27
+ }
28
+ /**
29
+ * Narrow to plain, user-authored, non-bot, non-subtyped messages with text.
30
+ * Rejects subtypes (edits/joins/…) except `file_share`, bot posts, and
31
+ * app-only messages. Anything that fails this is uninteresting to a bot.
32
+ */
33
+ export declare function isPlainUserMessage(message: unknown, botUserId: string | undefined): message is PlainUserMessage;
34
+ /** Platform-neutral ingress event the closed Intelligence side maps to its envelope. */
35
+ export type SlackNeutralEvent = {
36
+ kind: "turn";
37
+ /** Which Slack trigger produced this turn. */
38
+ source: "app_mention" | "direct_message" | "thread_reply";
39
+ channel: string;
40
+ /** Thread anchor: present for mentions and thread replies. */
41
+ threadTs?: string;
42
+ /** Inbound message ts (lets a renderer anchor a "thinking…" status on DMs). */
43
+ ts?: string;
44
+ userText: string;
45
+ senderUserId?: string;
46
+ eventId?: string;
47
+ hasFiles: boolean;
48
+ } | {
49
+ kind: "command";
50
+ command: string;
51
+ text: string;
52
+ channel: string;
53
+ senderUserId?: string;
54
+ triggerId?: string;
55
+ eventId?: string;
56
+ };
57
+ interface RawSlackEventBody {
58
+ command?: string;
59
+ text?: string;
60
+ channel_id?: string;
61
+ user_id?: string;
62
+ trigger_id?: string;
63
+ event_id?: string;
64
+ event?: Record<string, unknown>;
65
+ }
66
+ /**
67
+ * Map a raw Slack payload (Events API envelope or slash-command body) to the
68
+ * platform-neutral ingress shape, or `undefined` when it isn't a turn/command
69
+ * worth delivering (bot echo, subtype, empty). The caller applies its own
70
+ * policy/entitlement gating and builds the reply route. First slice covers
71
+ * app_mention, direct/thread messages, and slash commands.
72
+ */
73
+ export declare function normalizeSlackEvent(body: RawSlackEventBody, botUserId?: string): SlackNeutralEvent | undefined;
74
+ export {};
75
+ //# sourceMappingURL=ingress-normalize.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ingress-normalize.d.ts","sourceRoot":"","sources":["../src/ingress-normalize.ts"],"names":[],"mappings":"AAeA,6EAA6E;AAC7E,eAAO,MAAM,aAAa,GAAI,MAAM,MAAM,KAAG,MACa,CAAC;AAE3D;;;;;;GAMG;AACH,wBAAgB,aAAa,CAC3B,IAAI,EAAE,OAAO,EACb,KAAK,EAAE;IAAE,aAAa,CAAC,EAAE,MAAM,CAAC;IAAC,EAAE,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,EAChE,OAAO,EAAE,MAAM,GACd,MAAM,GAAG,SAAS,CAMpB;AAED,6EAA6E;AAC7E,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,KAAK,CAAC,EAAE,OAAO,EAAE,CAAC;IAClB,0EAA0E;IAC1E,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,+DAA+D;IAC/D,EAAE,CAAC,EAAE,MAAM,CAAC;CACb;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAChC,OAAO,EAAE,OAAO,EAChB,SAAS,EAAE,MAAM,GAAG,SAAS,GAC5B,OAAO,IAAI,gBAAgB,CAe7B;AAED,wFAAwF;AACxF,MAAM,MAAM,iBAAiB,GACzB;IACE,IAAI,EAAE,MAAM,CAAC;IACb,8CAA8C;IAC9C,MAAM,EAAE,aAAa,GAAG,gBAAgB,GAAG,cAAc,CAAC;IAC1D,OAAO,EAAE,MAAM,CAAC;IAChB,8DAA8D;IAC9D,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,+EAA+E;IAC/E,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,OAAO,CAAC;CACnB,GACD;IACE,IAAI,EAAE,SAAS,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB,CAAC;AAEN,UAAU,iBAAiB;IACzB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACjC;AAMD;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CACjC,IAAI,EAAE,iBAAiB,EACvB,SAAS,CAAC,EAAE,MAAM,GACjB,iBAAiB,GAAG,SAAS,CAyF/B"}