@cuylabs/channel-slack 0.10.0 → 0.12.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 (42) hide show
  1. package/README.md +36 -3
  2. package/dist/adapter/index.d.ts +53 -0
  3. package/dist/adapter/index.js +13 -0
  4. package/dist/app-surface.d.ts +86 -0
  5. package/dist/app-surface.js +15 -0
  6. package/dist/app.d.ts +58 -0
  7. package/dist/app.js +86 -0
  8. package/dist/artifacts/index.d.ts +57 -3
  9. package/dist/artifacts/index.js +88 -0
  10. package/dist/assistant/index.d.ts +18 -53
  11. package/dist/assistant/index.js +15 -184
  12. package/dist/bolt-app-BM0tiL7c.d.ts +49 -0
  13. package/dist/{chunk-TWJGVDA2.js → chunk-37RN2YUI.js} +88 -1
  14. package/dist/chunk-LFQCINHI.js +187 -0
  15. package/dist/chunk-Q6YX7HHK.js +1062 -0
  16. package/dist/chunk-RHOIVQLD.js +127 -0
  17. package/dist/chunk-RTDLIYEE.js +446 -0
  18. package/dist/core.d.ts +5 -201
  19. package/dist/core.js +10 -12
  20. package/dist/feedback/index.d.ts +2 -2
  21. package/dist/feedback/index.js +5 -120
  22. package/dist/formatting-C-kwQseI.d.ts +25 -0
  23. package/dist/index.d.ts +6 -3
  24. package/dist/index.js +10 -12
  25. package/dist/interactive/index.d.ts +68 -4
  26. package/dist/interactive/index.js +432 -0
  27. package/dist/options-B0xQCaez.d.ts +221 -0
  28. package/dist/options-DQacQDmD.d.ts +368 -0
  29. package/dist/runtime/index.d.ts +6 -220
  30. package/dist/socket.d.ts +142 -0
  31. package/dist/socket.js +77 -0
  32. package/dist/transports/index.d.ts +2 -1
  33. package/dist/transports/socket/index.d.ts +4 -49
  34. package/dist/turn-BGAXddH_.d.ts +178 -0
  35. package/dist/types-wLZzyI9r.d.ts +375 -0
  36. package/docs/README.md +1 -0
  37. package/docs/concepts/interactive-requests.md +85 -0
  38. package/docs/reference/channel-slack-boundary.md +6 -3
  39. package/docs/reference/exports.md +6 -2
  40. package/docs/reference/source-layout.md +1 -0
  41. package/package.json +23 -3
  42. package/dist/chunk-ISOMBQXE.js +0 -89
@@ -0,0 +1,1062 @@
1
+ import {
2
+ SLACK_FEEDBACK_ACTION_ID,
3
+ createSlackFeedbackBlock,
4
+ registerSlackFeedbackAction
5
+ } from "./chunk-RHOIVQLD.js";
6
+ import {
7
+ UnsupportedSlackInteractiveRequestError,
8
+ bridgeSlackTurnEventsToSlack,
9
+ resolveSlackEventBridgeOptions
10
+ } from "./chunk-IAQXQESO.js";
11
+ import {
12
+ extractSlackActionToken,
13
+ extractSlackAuthContext,
14
+ runWithSlackTurnContext
15
+ } from "./chunk-37RN2YUI.js";
16
+ import {
17
+ createSlackTurnCancelMessage,
18
+ registerSlackTurnCancelAction,
19
+ resolveSlackTurnCancelActionId
20
+ } from "./chunk-3KP3CBCC.js";
21
+ import {
22
+ resolveSlackChannelType,
23
+ stripLeadingMentions
24
+ } from "./chunk-FPCE5V5Y.js";
25
+ import {
26
+ resolveSlackMessageFormatter
27
+ } from "./chunk-6WHFQUYQ.js";
28
+
29
+ // src/assistant/thread-context-store.ts
30
+ var ASSISTANT_THREAD_CONTEXT_EVENT_TYPE = "assistant_thread_context";
31
+ function createSlackAssistantThreadContextStore() {
32
+ const cache = /* @__PURE__ */ new Map();
33
+ return {
34
+ async get(args) {
35
+ args.logger?.debug?.("[channel-slack] thread context store get called");
36
+ const { channelId, threadTs } = extractThreadInfo(args.payload);
37
+ const cacheKey = contextCacheKey(channelId, threadTs);
38
+ const cached = cache.get(cacheKey);
39
+ if (cached) return cached;
40
+ const messages = await readThreadMessages(args, channelId, threadTs);
41
+ const botUserId = args.context?.botUserId;
42
+ const metadataMessage = findContextMessage(messages, botUserId);
43
+ const context = toThreadContext(metadataMessage?.metadata?.event_payload);
44
+ cache.set(cacheKey, context);
45
+ return context;
46
+ },
47
+ async save(args) {
48
+ args.logger?.debug?.("[channel-slack] thread context store save called");
49
+ const { channelId, threadTs, context } = extractThreadInfo(args.payload);
50
+ const cacheKey = contextCacheKey(channelId, threadTs);
51
+ cache.set(cacheKey, context);
52
+ const messages = await readThreadMessages(args, channelId, threadTs);
53
+ const initialMessage = findInitialBotMessage(
54
+ messages,
55
+ args.context?.botUserId
56
+ );
57
+ if (!initialMessage?.ts) return;
58
+ await args.client?.chat?.update?.({
59
+ channel: channelId,
60
+ ts: initialMessage.ts,
61
+ text: initialMessage.text ?? "",
62
+ blocks: Array.isArray(initialMessage.blocks) ? initialMessage.blocks : [],
63
+ metadata: {
64
+ event_type: ASSISTANT_THREAD_CONTEXT_EVENT_TYPE,
65
+ event_payload: context
66
+ }
67
+ });
68
+ }
69
+ };
70
+ }
71
+ async function readThreadMessages(args, channelId, threadTs) {
72
+ const replies = args.client?.conversations?.replies;
73
+ if (typeof replies !== "function") return [];
74
+ const result = await replies({
75
+ channel: channelId,
76
+ ts: threadTs,
77
+ oldest: threadTs,
78
+ include_all_metadata: true,
79
+ limit: 4
80
+ });
81
+ return Array.isArray(result.messages) ? result.messages.filter(isSlackMessageWithMetadata) : [];
82
+ }
83
+ function findContextMessage(messages, botUserId) {
84
+ return messages.find(
85
+ (message) => isBotAuthoredMessage(message, botUserId) && message.metadata?.event_type === ASSISTANT_THREAD_CONTEXT_EVENT_TYPE
86
+ ) ?? messages.find(
87
+ (message) => isBotAuthoredMessage(message, botUserId) && message.metadata?.event_payload
88
+ );
89
+ }
90
+ function findInitialBotMessage(messages, botUserId) {
91
+ return messages.find((message) => isBotAuthoredMessage(message, botUserId));
92
+ }
93
+ function isBotAuthoredMessage(message, botUserId) {
94
+ if (message.subtype) return false;
95
+ return botUserId ? message.user === botUserId : Boolean(message.user);
96
+ }
97
+ function extractThreadInfo(payload) {
98
+ const channelId = extractChannelId(payload);
99
+ const threadTs = extractThreadTs(payload);
100
+ if (!channelId || !threadTs) {
101
+ const missing = [];
102
+ if (!channelId) missing.push("channel_id");
103
+ if (!threadTs) missing.push("thread_ts");
104
+ throw new Error(
105
+ `Assistant payload is missing required properties: ${missing.join(", ")}`
106
+ );
107
+ }
108
+ return {
109
+ channelId,
110
+ threadTs,
111
+ context: extractThreadContext(payload)
112
+ };
113
+ }
114
+ function extractThreadContext(payload) {
115
+ if (!isRecord(payload)) return {};
116
+ const assistantThread = payload.assistant_thread;
117
+ if (!isRecord(assistantThread) || !isRecord(assistantThread.context)) {
118
+ return {};
119
+ }
120
+ return toThreadContext(assistantThread.context);
121
+ }
122
+ function extractThreadTs(payload) {
123
+ if (!isRecord(payload)) return void 0;
124
+ if (isNonEmptyString(payload.thread_ts)) return payload.thread_ts;
125
+ if (isRecord(payload.assistant_thread) && isNonEmptyString(payload.assistant_thread.thread_ts)) {
126
+ return payload.assistant_thread.thread_ts;
127
+ }
128
+ if (isRecord(payload.message) && isNonEmptyString(payload.message.thread_ts)) {
129
+ return payload.message.thread_ts;
130
+ }
131
+ if (isRecord(payload.previous_message) && isNonEmptyString(payload.previous_message.thread_ts)) {
132
+ return payload.previous_message.thread_ts;
133
+ }
134
+ return void 0;
135
+ }
136
+ function extractChannelId(payload) {
137
+ if (!isRecord(payload)) return void 0;
138
+ if (isNonEmptyString(payload.channel)) return payload.channel;
139
+ if (isRecord(payload.channel) && isNonEmptyString(payload.channel.id)) {
140
+ return payload.channel.id;
141
+ }
142
+ if (isNonEmptyString(payload.channel_id)) return payload.channel_id;
143
+ if (isRecord(payload.item) && isNonEmptyString(payload.item.channel)) {
144
+ return payload.item.channel;
145
+ }
146
+ if (isRecord(payload.assistant_thread) && isNonEmptyString(payload.assistant_thread.channel_id)) {
147
+ return payload.assistant_thread.channel_id;
148
+ }
149
+ return void 0;
150
+ }
151
+ function toThreadContext(value) {
152
+ if (!isRecord(value)) return {};
153
+ return {
154
+ ...isNonEmptyString(value.channel_id) ? { channel_id: value.channel_id } : {},
155
+ ...isNonEmptyString(value.team_id) ? { team_id: value.team_id } : {},
156
+ ..."enterprise_id" in value ? {
157
+ enterprise_id: typeof value.enterprise_id === "string" ? value.enterprise_id : null
158
+ } : {}
159
+ };
160
+ }
161
+ function isSlackMessageWithMetadata(value) {
162
+ return isRecord(value);
163
+ }
164
+ function contextCacheKey(channelId, threadTs) {
165
+ return `${channelId}:${threadTs}`;
166
+ }
167
+ function isNonEmptyString(value) {
168
+ return typeof value === "string" && value.length > 0;
169
+ }
170
+ function isRecord(value) {
171
+ return typeof value === "object" && value !== null;
172
+ }
173
+
174
+ // src/assistant/message-parser.ts
175
+ function parseSlackMessageActivityFromMessageEvent(message) {
176
+ if (!message || typeof message !== "object") return void 0;
177
+ const m = message;
178
+ if (m.subtype && m.subtype.length > 0) return void 0;
179
+ if (typeof m.text !== "string" || m.text.length === 0) return void 0;
180
+ if (typeof m.thread_ts !== "string" || m.thread_ts.length === 0) {
181
+ return void 0;
182
+ }
183
+ const channel = typeof m.channel === "string" ? m.channel : void 0;
184
+ if (!channel) return void 0;
185
+ const threadTs = m.thread_ts;
186
+ const messageTs = m.ts;
187
+ const isThread = threadTs !== messageTs;
188
+ const channelType = resolveSlackChannelType(channel, threadTs, isThread);
189
+ const rawText = m.text;
190
+ const text = channelType === "dm" ? rawText.trim() : stripLeadingMentions(rawText);
191
+ return {
192
+ channel,
193
+ threadTs,
194
+ channelId: channel,
195
+ channelType,
196
+ userId: m.user ?? "unknown",
197
+ teamId: m.team ?? void 0,
198
+ parentUserId: m.parent_user_id,
199
+ actionToken: extractSlackActionToken(m),
200
+ text,
201
+ isMention: false,
202
+ ...messageTs ? { messageTs } : {}
203
+ };
204
+ }
205
+
206
+ // src/assistant/bridge.ts
207
+ import { Assistant } from "@slack/bolt";
208
+
209
+ // src/assistant/options.ts
210
+ var DEFAULT_INITIAL_REPLY = "Hi, how can I help?";
211
+ var DEFAULT_INITIAL_STATUS = {
212
+ status: "Thinking..."
213
+ };
214
+ var DEFAULT_STREAM_ERROR_MESSAGE = "I ran into an error while preparing this answer. Please try again.";
215
+
216
+ // src/assistant/helpers.ts
217
+ function toError(value) {
218
+ return value instanceof Error ? value : new Error(String(value));
219
+ }
220
+ function formatErrorForLog(error) {
221
+ if (error instanceof Error) {
222
+ return error.stack ?? error.message;
223
+ }
224
+ return String(error);
225
+ }
226
+ async function safeGetThreadContext(getThreadContext, logger) {
227
+ try {
228
+ const value = await getThreadContext();
229
+ if (value && typeof value === "object") {
230
+ return value;
231
+ }
232
+ } catch (error) {
233
+ logger.warn?.("getThreadContext failed", {
234
+ error: formatErrorForLog(error)
235
+ });
236
+ }
237
+ return {};
238
+ }
239
+ async function resolveInitialReply(resolver, context) {
240
+ if (resolver === void 0) {
241
+ return DEFAULT_INITIAL_REPLY;
242
+ }
243
+ const value = await resolver(context);
244
+ if (value === false) {
245
+ return void 0;
246
+ }
247
+ return value ?? void 0;
248
+ }
249
+ async function resolveInitialStatus(resolver, request) {
250
+ if (resolver === void 0) {
251
+ return DEFAULT_INITIAL_STATUS;
252
+ }
253
+ if (typeof resolver === "function") {
254
+ return await resolver(request) ?? void 0;
255
+ }
256
+ return resolver;
257
+ }
258
+ async function resolveStatusUpdate(resolver, label, event, rawArgs) {
259
+ if (!resolver) {
260
+ return label ? { status: label } : { status: "" };
261
+ }
262
+ if (!event) {
263
+ return void 0;
264
+ }
265
+ return await resolver({ event, rawArgs }) ?? void 0;
266
+ }
267
+ function resolveFeedbackSessionId(context, strategy) {
268
+ const threadTs = context.threadTs ?? context.messageTs;
269
+ switch (strategy) {
270
+ case "channel-id":
271
+ return context.channelId;
272
+ case "user-per-thread":
273
+ return `${context.channelId}:${threadTs}:${context.userId}`;
274
+ default:
275
+ return `${context.channelId}:${threadTs}`;
276
+ }
277
+ }
278
+
279
+ // src/assistant/lifecycle/thread-started.ts
280
+ function createThreadStartedHandler(options) {
281
+ return async (args) => {
282
+ const {
283
+ event,
284
+ context,
285
+ client,
286
+ logger,
287
+ say,
288
+ setSuggestedPrompts,
289
+ saveThreadContext
290
+ } = args;
291
+ const threadCtx = {
292
+ event,
293
+ context,
294
+ client,
295
+ logger
296
+ };
297
+ let savedThreadContext = false;
298
+ const saveContext = async () => {
299
+ if (savedThreadContext) return;
300
+ try {
301
+ await saveThreadContext();
302
+ savedThreadContext = true;
303
+ } catch (error) {
304
+ logger.error(
305
+ `[channel-slack] saveThreadContext failed: ${error instanceof Error ? error.message : String(error)}`
306
+ );
307
+ }
308
+ };
309
+ try {
310
+ const initialReply = await resolveInitialReply(
311
+ options.getInitialReply,
312
+ threadCtx
313
+ );
314
+ if (initialReply) {
315
+ await say(initialReply);
316
+ }
317
+ } catch (error) {
318
+ logger.error(
319
+ `[channel-slack] initial assistant reply failed: ${error instanceof Error ? error.message : String(error)}`
320
+ );
321
+ }
322
+ await saveContext();
323
+ try {
324
+ const prompts = await options.getSuggestedPrompts?.(threadCtx);
325
+ if (prompts && prompts.prompts.length > 0) {
326
+ await setSuggestedPrompts({
327
+ ...prompts.title ? { title: prompts.title } : {},
328
+ prompts: prompts.prompts
329
+ });
330
+ }
331
+ } catch (error) {
332
+ logger.error(
333
+ `[channel-slack] setSuggestedPrompts failed: ${error instanceof Error ? error.message : String(error)}`
334
+ );
335
+ }
336
+ };
337
+ }
338
+
339
+ // src/assistant/lifecycle/thread-context-changed.ts
340
+ function createThreadContextChangedHandler() {
341
+ return async ({ event, saveThreadContext, logger }) => {
342
+ try {
343
+ await saveThreadContext();
344
+ logger.info("[channel-slack] thread context changed saved", {
345
+ channelId: event.assistant_thread.channel_id,
346
+ threadTs: event.assistant_thread.thread_ts,
347
+ originChannelId: event.assistant_thread.context.channel_id,
348
+ teamId: event.assistant_thread.context.team_id
349
+ });
350
+ } catch (error) {
351
+ logger.error(
352
+ `[channel-slack] threadContextChanged failed: ${error instanceof Error ? error.message : String(error)}`
353
+ );
354
+ }
355
+ };
356
+ }
357
+
358
+ // src/assistant/session.ts
359
+ function resolveAssistantSessionId(message, strategy) {
360
+ switch (strategy) {
361
+ case "channel-id":
362
+ return message.channel;
363
+ case "user-per-thread":
364
+ return `${message.channel}:${message.threadTs}:${message.userId}`;
365
+ default:
366
+ return `${message.channel}:${message.threadTs}`;
367
+ }
368
+ }
369
+
370
+ // src/assistant/sink.ts
371
+ function createAssistantSink(params) {
372
+ let stream;
373
+ let stopped = false;
374
+ function ensureStream(bufferSize) {
375
+ if (stream) return stream;
376
+ const streamArgs = {
377
+ ...params.chatStreamStartArgs ?? {},
378
+ task_display_mode: params.taskDisplayMode,
379
+ buffer_size: bufferSize
380
+ };
381
+ let created;
382
+ if (typeof params.sayStream === "function") {
383
+ created = params.sayStream(streamArgs);
384
+ } else {
385
+ const clientWithStream = params.client;
386
+ if (typeof clientWithStream.chatStream !== "function") {
387
+ throw new Error(
388
+ "Slack client does not expose chatStream; ensure @slack/web-api >=7.16.0 is installed."
389
+ );
390
+ }
391
+ created = clientWithStream.chatStream({
392
+ channel: params.channel,
393
+ thread_ts: params.threadTs,
394
+ ...params.teamId ? { recipient_team_id: params.teamId } : {},
395
+ ...params.userId ? { recipient_user_id: params.userId } : {},
396
+ ...streamArgs
397
+ });
398
+ }
399
+ stream = {
400
+ append: (args) => created.append(args),
401
+ stop: async (args) => {
402
+ if (stopped) return void 0;
403
+ stopped = true;
404
+ return created.stop(args);
405
+ }
406
+ };
407
+ return stream;
408
+ }
409
+ return {
410
+ artifactClient: params.client,
411
+ artifactTarget: {
412
+ channelId: params.channel,
413
+ threadTs: params.threadTs
414
+ },
415
+ async postMessage() {
416
+ throw new Error(
417
+ "Assistant bridge uses chat-stream mode and does not support postMessage."
418
+ );
419
+ },
420
+ async updateMessage() {
421
+ },
422
+ createChatStream: ({ bufferSize }) => ensureStream(bufferSize),
423
+ async appendError(text) {
424
+ if (stopped) return;
425
+ const target = ensureStream(256);
426
+ await target.append({ markdown_text: `
427
+
428
+ ${text}` });
429
+ },
430
+ async finalize() {
431
+ if (stopped || !stream) return;
432
+ await stream.stop();
433
+ }
434
+ };
435
+ }
436
+ function createAssistantInteractiveResponder(params) {
437
+ return {
438
+ async postMessage(message) {
439
+ const result = await params.client.chat.postMessage({
440
+ channel: params.channel,
441
+ thread_ts: params.threadTs,
442
+ text: message.text,
443
+ ...message.blocks ? { blocks: message.blocks } : {}
444
+ });
445
+ if (!result.channel || !result.ts) {
446
+ throw new Error(
447
+ "Slack chat.postMessage response did not include channel and ts."
448
+ );
449
+ }
450
+ return { channel: result.channel, ts: result.ts };
451
+ },
452
+ async updateMessage(ref, message) {
453
+ await params.client.chat.update({
454
+ channel: ref.channel,
455
+ ts: ref.ts,
456
+ text: message.text,
457
+ ...message.blocks ? { blocks: message.blocks } : {}
458
+ });
459
+ }
460
+ };
461
+ }
462
+
463
+ // src/assistant/lifecycle/user-message.ts
464
+ function createUserMessageHandler(deps) {
465
+ const { options, feedbackBlock, sessionStrategy, turnControlController } = deps;
466
+ const taskDisplayMode = options.taskDisplayMode ?? "timeline";
467
+ const messageFormatter = resolveSlackMessageFormatter(
468
+ options.formatChatMarkdown ?? {}
469
+ );
470
+ const formatStreamError = options.formatStreamError ?? (() => DEFAULT_STREAM_ERROR_MESSAGE);
471
+ const showReasoning = options.showReasoning ?? false;
472
+ const showToolUsage = options.showToolUsage ?? true;
473
+ const showSubagentToolUsage = options.showSubagentToolUsage ?? false;
474
+ const showSubagentResultInTask = options.showSubagentResultInTask ?? false;
475
+ const timeoutMs = options.timeoutMs ?? 12e4;
476
+ return async (args) => {
477
+ const {
478
+ message,
479
+ client,
480
+ context,
481
+ logger,
482
+ sayStream,
483
+ setStatus,
484
+ setSuggestedPrompts,
485
+ setTitle,
486
+ getThreadContext,
487
+ saveThreadContext
488
+ } = args;
489
+ const parsed = parseSlackMessageActivityFromMessageEvent(message);
490
+ if (!parsed) {
491
+ return;
492
+ }
493
+ const { channel, threadTs, text, userId } = parsed;
494
+ const teamId = context.teamId ?? context.enterpriseId;
495
+ if (!channel || !threadTs || !text) {
496
+ return;
497
+ }
498
+ const diagnosticsLogger = options.logger ?? logger;
499
+ const auth = extractSlackAuthContext(context, userId);
500
+ const threadContext = await safeGetThreadContext(
501
+ getThreadContext,
502
+ diagnosticsLogger
503
+ );
504
+ const assistantUtilities = {
505
+ setStatus: async (update) => {
506
+ await setStatus(update);
507
+ },
508
+ setSuggestedPrompts: async (prompts) => {
509
+ await setSuggestedPrompts({
510
+ ...prompts.title ? { title: prompts.title } : {},
511
+ prompts: prompts.prompts
512
+ });
513
+ },
514
+ setTitle: async (title) => {
515
+ await setTitle(title);
516
+ },
517
+ getThreadContext: () => getThreadContext(),
518
+ saveThreadContext: () => saveThreadContext()
519
+ };
520
+ const initialSessionId = resolveAssistantSessionId(parsed, sessionStrategy);
521
+ const baseRequest = {
522
+ message: parsed,
523
+ sessionId: initialSessionId,
524
+ client,
525
+ rawArgs: args,
526
+ auth,
527
+ threadContext,
528
+ assistant: assistantUtilities
529
+ };
530
+ const sink = createAssistantSink({
531
+ client,
532
+ sayStream,
533
+ channel,
534
+ threadTs,
535
+ teamId,
536
+ userId,
537
+ taskDisplayMode,
538
+ ...options.chatStreamStartArgs ? { chatStreamStartArgs: options.chatStreamStartArgs } : {}
539
+ });
540
+ let translatedError;
541
+ let unsupportedInteractive;
542
+ let timeoutId;
543
+ try {
544
+ const initialStatus = await resolveInitialStatus(
545
+ options.initialStatus,
546
+ baseRequest
547
+ );
548
+ if (initialStatus) {
549
+ try {
550
+ await setStatus(initialStatus);
551
+ } catch (error) {
552
+ diagnosticsLogger.warn?.("setStatus failed", {
553
+ error: formatErrorForLog(error)
554
+ });
555
+ }
556
+ }
557
+ let sessionId = initialSessionId;
558
+ if (options.resolveSession) {
559
+ const resolved = await options.resolveSession(baseRequest);
560
+ if (resolved && resolved.length > 0) {
561
+ sessionId = resolved;
562
+ }
563
+ }
564
+ const turnPrep = await options.prepareTurn?.({ ...baseRequest, sessionId }) ?? {};
565
+ if (turnPrep.sessionId && turnPrep.sessionId.length > 0) {
566
+ sessionId = turnPrep.sessionId;
567
+ }
568
+ const turnMessage = turnPrep.message ?? text;
569
+ const userMessageRequest = {
570
+ ...baseRequest,
571
+ sessionId,
572
+ message: { ...parsed, text: turnMessage }
573
+ };
574
+ if (options.formatThreadTitle) {
575
+ try {
576
+ const title = await options.formatThreadTitle(userMessageRequest);
577
+ if (title) {
578
+ await setTitle(title);
579
+ }
580
+ } catch (error) {
581
+ diagnosticsLogger.warn?.("setTitle failed", {
582
+ error: formatErrorForLog(error)
583
+ });
584
+ }
585
+ }
586
+ const configuredFinalBlocks = Array.isArray(
587
+ options.chatStreamFinalArgs?.blocks
588
+ ) ? options.chatStreamFinalArgs.blocks : [];
589
+ const finalBlocks = feedbackBlock ? [...configuredFinalBlocks, feedbackBlock] : configuredFinalBlocks;
590
+ const chatStreamFinalArgs = options.chatStreamFinalArgs || finalBlocks.length > 0 ? {
591
+ ...options.chatStreamFinalArgs ?? {},
592
+ ...finalBlocks.length > 0 ? { blocks: finalBlocks } : {}
593
+ } : void 0;
594
+ const bridgeOptions = resolveSlackEventBridgeOptions({
595
+ streamingMode: "chat-stream",
596
+ showReasoning,
597
+ showToolUsage,
598
+ showSubagentToolUsage,
599
+ showSubagentResultInTask,
600
+ ...options.formatToolTitle ? { formatToolTitle: options.formatToolTitle } : {},
601
+ ...options.formatToolUpdate ? { formatToolUpdate: options.formatToolUpdate } : {},
602
+ ...options.formatToolDetails ? { formatToolDetails: options.formatToolDetails } : {},
603
+ ...options.formatToolResultOutput ? { formatToolResultOutput: options.formatToolResultOutput } : {},
604
+ ...options.formatToolError ? { formatToolError: options.formatToolError } : {},
605
+ ...options.formatReasoningUpdate ? { formatReasoningUpdate: options.formatReasoningUpdate } : {},
606
+ ...options.chatStreamBufferSize !== void 0 ? { chatStreamBufferSize: options.chatStreamBufferSize } : {},
607
+ ...options.maxTaskUpdates !== void 0 ? { maxTaskUpdates: options.maxTaskUpdates } : {},
608
+ ...options.maxTaskUpdateTextChars !== void 0 ? { maxTaskUpdateTextChars: options.maxTaskUpdateTextChars } : {},
609
+ ...options.maxTaskUpdateFieldChars !== void 0 ? { maxTaskUpdateFieldChars: options.maxTaskUpdateFieldChars } : {},
610
+ ...chatStreamFinalArgs ? { chatStreamFinalArgs } : {},
611
+ ...options.publishFinalResponseArtifact ? {
612
+ publishFinalResponseArtifact: options.publishFinalResponseArtifact
613
+ } : {},
614
+ ...options.finalResponseArtifactMode ? { finalResponseArtifactMode: options.finalResponseArtifactMode } : {},
615
+ ...options.finalResponseArtifactStreamThreshold !== void 0 ? {
616
+ finalResponseArtifactStreamThreshold: options.finalResponseArtifactStreamThreshold
617
+ } : {},
618
+ ...options.formatFinalResponseArtifactContinuationNotice ? {
619
+ formatFinalResponseArtifactContinuationNotice: options.formatFinalResponseArtifactContinuationNotice
620
+ } : {},
621
+ ...options.formatFinalResponseArtifactMessage ? {
622
+ formatFinalResponseArtifactMessage: options.formatFinalResponseArtifactMessage
623
+ } : {},
624
+ ...options.onFinalResponseArtifactError ? {
625
+ onFinalResponseArtifactError: options.onFinalResponseArtifactError
626
+ } : {},
627
+ formatMessageText: messageFormatter,
628
+ onStatusChange: async (label, event) => {
629
+ const update = await resolveStatusUpdate(
630
+ options.formatStatus,
631
+ label,
632
+ event,
633
+ args
634
+ );
635
+ if (update) {
636
+ try {
637
+ await setStatus(update);
638
+ } catch (error) {
639
+ diagnosticsLogger.warn?.("setStatus failed", {
640
+ error: formatErrorForLog(error)
641
+ });
642
+ }
643
+ }
644
+ },
645
+ ...options.handleInteractiveRequest ? {
646
+ handleInteractiveRequest: (interactive) => options.handleInteractiveRequest({
647
+ ...interactive,
648
+ slackActivity: parsed,
649
+ user: {
650
+ userId: userId ?? "unknown",
651
+ channelId: channel,
652
+ ...teamId ? { teamId } : {},
653
+ threadTs,
654
+ ...parsed.messageTs ? { messageTs: parsed.messageTs } : {}
655
+ },
656
+ sessionId,
657
+ message: turnMessage,
658
+ responder: createAssistantInteractiveResponder({
659
+ client,
660
+ channel,
661
+ threadTs
662
+ })
663
+ })
664
+ } : {}
665
+ });
666
+ const abortController = new AbortController();
667
+ timeoutId = timeoutMs > 0 ? setTimeout(() => abortController.abort(), timeoutMs) : void 0;
668
+ const cancelControl = turnControlController?.createCancelControl({
669
+ abortController,
670
+ channel,
671
+ client,
672
+ logger: diagnosticsLogger,
673
+ sessionId,
674
+ ...teamId ? { teamId } : {},
675
+ threadTs,
676
+ userId: userId ?? "unknown"
677
+ });
678
+ cancelControl?.show("turn-start");
679
+ await runWithSlackTurnContext(
680
+ {
681
+ slackActivity: parsed,
682
+ user: {
683
+ userId: userId ?? "unknown",
684
+ channelId: channel,
685
+ ...teamId ? { teamId } : {},
686
+ threadTs
687
+ },
688
+ sessionId,
689
+ message: turnMessage,
690
+ auth,
691
+ threadContext,
692
+ assistant: assistantUtilities,
693
+ ...turnPrep.context ? { context: turnPrep.context } : {}
694
+ },
695
+ async () => {
696
+ const events = options.source.chat(sessionId, turnMessage, {
697
+ abort: abortController.signal,
698
+ ...turnPrep.system ? { system: turnPrep.system } : {}
699
+ });
700
+ const translated = (async function* () {
701
+ try {
702
+ for await (const event of events) {
703
+ cancelControl?.sync(event);
704
+ if (event.type === "error") {
705
+ translatedError = toError(
706
+ event.error
707
+ );
708
+ yield {
709
+ type: "text-delta",
710
+ text: `
711
+
712
+ ${formatStreamError(translatedError)}`
713
+ };
714
+ yield { type: "complete" };
715
+ return;
716
+ }
717
+ yield event;
718
+ }
719
+ } catch (error) {
720
+ if (error instanceof UnsupportedSlackInteractiveRequestError) {
721
+ unsupportedInteractive = error;
722
+ throw error;
723
+ }
724
+ translatedError = toError(error);
725
+ yield {
726
+ type: "text-delta",
727
+ text: `
728
+
729
+ ${formatStreamError(translatedError)}`
730
+ };
731
+ yield { type: "complete" };
732
+ } finally {
733
+ cancelControl?.clear("turn-finished");
734
+ }
735
+ })();
736
+ const finalText = await bridgeSlackTurnEventsToSlack(
737
+ translated,
738
+ sink,
739
+ bridgeOptions
740
+ );
741
+ if (!translatedError && options.getFollowUpPrompts) {
742
+ try {
743
+ const followUps = await options.getFollowUpPrompts({
744
+ ...userMessageRequest,
745
+ finalText
746
+ });
747
+ if (followUps && followUps.prompts.length > 0) {
748
+ await assistantUtilities.setSuggestedPrompts(followUps);
749
+ }
750
+ } catch (error) {
751
+ diagnosticsLogger.warn?.("getFollowUpPrompts failed", {
752
+ error: formatErrorForLog(error)
753
+ });
754
+ }
755
+ }
756
+ }
757
+ );
758
+ } catch (error) {
759
+ if (error instanceof UnsupportedSlackInteractiveRequestError && !unsupportedInteractive) {
760
+ unsupportedInteractive = error;
761
+ }
762
+ if (!unsupportedInteractive) {
763
+ try {
764
+ await sink.appendError(formatStreamError(toError(error)));
765
+ } catch {
766
+ }
767
+ }
768
+ try {
769
+ await sink.finalize();
770
+ } catch {
771
+ }
772
+ } finally {
773
+ if (timeoutId) {
774
+ clearTimeout(timeoutId);
775
+ }
776
+ try {
777
+ await setStatus({ status: "" });
778
+ } catch {
779
+ }
780
+ if (translatedError) {
781
+ logger.error(
782
+ `[channel-slack] userMessage error surfaced to user: ${translatedError.message}`
783
+ );
784
+ }
785
+ }
786
+ };
787
+ }
788
+
789
+ // src/assistant/turn-controls.ts
790
+ import { randomUUID } from "crypto";
791
+ function createSlackAssistantTurnControlController(options) {
792
+ const resolvedCancelOptions = resolveCancelOptions(options);
793
+ if (!resolvedCancelOptions) {
794
+ return void 0;
795
+ }
796
+ const cancelOptions = resolvedCancelOptions;
797
+ const actionId = resolveSlackTurnCancelActionId(cancelOptions.actionId);
798
+ const activeControls = /* @__PURE__ */ new Map();
799
+ function install(app) {
800
+ registerSlackTurnCancelAction(app, {
801
+ actionId,
802
+ onCancel: (context) => handleCancelAction(context)
803
+ });
804
+ }
805
+ function createCancelControl(controlOptions) {
806
+ const control = {
807
+ abortController: controlOptions.abortController,
808
+ channel: controlOptions.channel,
809
+ client: controlOptions.client,
810
+ controlId: randomUUID(),
811
+ logger: controlOptions.logger,
812
+ pending: Promise.resolve(),
813
+ sessionId: controlOptions.sessionId,
814
+ ...controlOptions.teamId ? { teamId: controlOptions.teamId } : {},
815
+ threadTs: controlOptions.threadTs,
816
+ userId: controlOptions.userId,
817
+ visible: false
818
+ };
819
+ activeControls.set(control.controlId, control);
820
+ return {
821
+ show(reason) {
822
+ showCancelControl(control, reason);
823
+ },
824
+ clear(reason) {
825
+ clearCancelControl(control, reason);
826
+ },
827
+ sync(event) {
828
+ syncCancelControlForEvent(control, event);
829
+ }
830
+ };
831
+ }
832
+ async function handleCancelAction(context) {
833
+ const control = activeControls.get(context.controlId);
834
+ if (!control) {
835
+ await context.deleteMessage().catch(() => void 0);
836
+ await acknowledge(context, cancelOptions.alreadyCompletedAck);
837
+ return;
838
+ }
839
+ if (context.userId !== control.userId) {
840
+ await acknowledge(context, cancelOptions.unauthorizedAck);
841
+ return;
842
+ }
843
+ clearCancelControl(control, "button-cancel");
844
+ activeControls.delete(control.controlId);
845
+ if (!control.abortController.signal.aborted) {
846
+ control.abortController.abort(createSlackTurnCancelledError());
847
+ }
848
+ await cancelOptions.onCancel?.({
849
+ controlId: control.controlId,
850
+ sessionId: control.sessionId,
851
+ channelId: control.channel,
852
+ threadTs: control.threadTs,
853
+ userId: control.userId,
854
+ ...control.teamId ? { teamId: control.teamId } : {}
855
+ });
856
+ await acknowledge(context, cancelOptions.canceledAck);
857
+ }
858
+ function showCancelControl(control, reason) {
859
+ if (control.visible || control.abortController.signal.aborted) {
860
+ return;
861
+ }
862
+ control.visible = true;
863
+ enqueueControlUpdate(control, "show", reason, async () => {
864
+ const message = createSlackTurnCancelMessage({
865
+ actionId,
866
+ controlId: control.controlId,
867
+ messageText: cancelOptions.messageText,
868
+ buttonText: cancelOptions.buttonText,
869
+ sessionId: control.sessionId
870
+ });
871
+ if (control.target) {
872
+ await control.client.chat.update({
873
+ channel: control.target.channel,
874
+ ts: control.target.ts,
875
+ text: message.text,
876
+ blocks: message.blocks
877
+ });
878
+ return;
879
+ }
880
+ const response = await control.client.chat.postMessage({
881
+ channel: control.channel,
882
+ thread_ts: control.threadTs,
883
+ text: message.text,
884
+ blocks: message.blocks
885
+ });
886
+ if (response.channel && response.ts) {
887
+ control.target = { channel: response.channel, ts: response.ts };
888
+ }
889
+ });
890
+ }
891
+ function clearCancelControl(control, reason) {
892
+ const terminal = isTerminalClearReason(reason);
893
+ if (terminal) {
894
+ activeControls.delete(control.controlId);
895
+ }
896
+ if (!control.visible) {
897
+ return;
898
+ }
899
+ control.visible = false;
900
+ enqueueControlUpdate(control, "clear", reason, async () => {
901
+ if (!control.target) {
902
+ return;
903
+ }
904
+ const target = control.target;
905
+ control.target = void 0;
906
+ await control.client.chat.delete({
907
+ channel: target.channel,
908
+ ts: target.ts
909
+ });
910
+ });
911
+ }
912
+ function syncCancelControlForEvent(control, event) {
913
+ switch (event.type) {
914
+ case "text-start":
915
+ case "text-delta":
916
+ case "text-end":
917
+ if (cancelOptions.visibleWhen === "before-output") {
918
+ clearCancelControl(control, "streaming-started");
919
+ }
920
+ break;
921
+ case "reasoning-start":
922
+ case "reasoning-end":
923
+ case "tool-start":
924
+ case "approval-request":
925
+ case "human-input-request":
926
+ showCancelControl(control, event.type);
927
+ break;
928
+ case "status":
929
+ if (isWaitingForSlackStatus(event.status)) {
930
+ showCancelControl(control, `status:${event.status}`);
931
+ } else if (event.status === "idle" || event.status === "error") {
932
+ clearCancelControl(control, `status:${event.status}`);
933
+ }
934
+ break;
935
+ case "complete":
936
+ clearCancelControl(control, "turn-finished");
937
+ break;
938
+ case "error":
939
+ clearCancelControl(control, "turn-error");
940
+ break;
941
+ default:
942
+ break;
943
+ }
944
+ }
945
+ function enqueueControlUpdate(control, action, reason, update) {
946
+ control.pending = control.pending.then(update);
947
+ control.pending = control.pending.catch((error) => {
948
+ control.logger.warn?.("Slack turn cancel control update failed", {
949
+ action,
950
+ controlId: control.controlId,
951
+ error: formatErrorForLog(error),
952
+ reason,
953
+ sessionId: control.sessionId
954
+ });
955
+ });
956
+ }
957
+ async function acknowledge(context, text) {
958
+ if (text === false) {
959
+ return;
960
+ }
961
+ await context.acknowledgeEphemeral(text ?? "Canceled the running request.");
962
+ }
963
+ return { install, createCancelControl };
964
+ }
965
+ function resolveCancelOptions(options) {
966
+ if (!options?.cancel) {
967
+ return void 0;
968
+ }
969
+ const input = options.cancel === true ? {} : options.cancel;
970
+ return {
971
+ ...input,
972
+ visibleWhen: input.visibleWhen ?? "before-output",
973
+ canceledAck: input.canceledAck ?? "Canceled the running request.",
974
+ alreadyCompletedAck: input.alreadyCompletedAck ?? "That request is no longer running.",
975
+ unauthorizedAck: input.unauthorizedAck ?? "Only the user who started this request can cancel it."
976
+ };
977
+ }
978
+ function isWaitingForSlackStatus(status) {
979
+ return status === "processing" || status === "thinking" || status === "reasoning" || status === "calling-tool" || status === "waiting-approval" || status === "waiting-input";
980
+ }
981
+ function isTerminalClearReason(reason) {
982
+ return reason === "streaming-started" || reason === "turn-finished" || reason === "turn-error" || reason === "button-cancel";
983
+ }
984
+ function createSlackTurnCancelledError() {
985
+ return Object.assign(new Error("Slack turn was cancelled by the user."), {
986
+ category: "cancelled"
987
+ });
988
+ }
989
+
990
+ // src/assistant/bridge.ts
991
+ function createSlackAssistantBridge(options) {
992
+ if (!options.source || typeof options.source.chat !== "function") {
993
+ throw new Error(
994
+ "createSlackAssistantBridge: options.source must implement SlackTurnSource"
995
+ );
996
+ }
997
+ if (options.timeoutMs !== void 0 && (!Number.isFinite(options.timeoutMs) || options.timeoutMs < 0)) {
998
+ throw new Error(
999
+ "createSlackAssistantBridge: timeoutMs must be a finite non-negative number"
1000
+ );
1001
+ }
1002
+ const sessionStrategy = options.sessionStrategy ?? "thread-aware";
1003
+ const feedbackConfig = options.feedback === false ? void 0 : options.feedback ?? {};
1004
+ const feedbackBlock = feedbackConfig ? createSlackFeedbackBlock(feedbackConfig) : void 0;
1005
+ const feedbackActionId = feedbackConfig?.actionId ?? SLACK_FEEDBACK_ACTION_ID;
1006
+ const turnControlController = createSlackAssistantTurnControlController(
1007
+ options.turnControls
1008
+ );
1009
+ const turnCancelActionId = turnControlController ? resolveSlackTurnCancelActionId(
1010
+ resolveTurnCancelActionIdOption(options.turnControls)
1011
+ ) : void 0;
1012
+ const threadStarted = createThreadStartedHandler(options);
1013
+ const threadContextChanged = createThreadContextChangedHandler();
1014
+ const threadContextStore = options.threadContextStore ?? createSlackAssistantThreadContextStore();
1015
+ const userMessage = createUserMessageHandler({
1016
+ options,
1017
+ feedbackBlock,
1018
+ sessionStrategy,
1019
+ ...turnControlController ? { turnControlController } : {}
1020
+ });
1021
+ const config = {
1022
+ threadStarted,
1023
+ threadContextChanged,
1024
+ userMessage,
1025
+ // The store shape must match Bolt's `AssistantThreadContextStore` exactly.
1026
+ // We accept the structural alias above and cast here so callers don't
1027
+ // need to import Bolt's internal types in their own code.
1028
+ threadContextStore
1029
+ };
1030
+ const assistant = new Assistant(config);
1031
+ function install(app) {
1032
+ app.assistant(assistant);
1033
+ if (feedbackConfig) {
1034
+ registerSlackFeedbackAction(app, {
1035
+ ...feedbackConfig,
1036
+ resolveSessionId: (context) => resolveFeedbackSessionId(context, sessionStrategy),
1037
+ onFeedback: feedbackConfig.onFeedback ?? (async (_ctx) => {
1038
+ })
1039
+ });
1040
+ }
1041
+ turnControlController?.install(app);
1042
+ }
1043
+ return {
1044
+ assistant,
1045
+ install,
1046
+ ...feedbackConfig ? { feedbackActionId } : {},
1047
+ ...turnCancelActionId ? { turnCancelActionId } : {}
1048
+ };
1049
+ }
1050
+ function resolveTurnCancelActionIdOption(options) {
1051
+ if (!options || !options.cancel) {
1052
+ return void 0;
1053
+ }
1054
+ return typeof options.cancel === "object" ? options.cancel.actionId : void 0;
1055
+ }
1056
+
1057
+ export {
1058
+ createSlackAssistantThreadContextStore,
1059
+ parseSlackMessageActivityFromMessageEvent,
1060
+ resolveAssistantSessionId,
1061
+ createSlackAssistantBridge
1062
+ };