@cuylabs/channel-slack 0.9.0 → 0.10.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.
@@ -0,0 +1,1008 @@
1
+ // src/runtime/interactive.ts
2
+ var UnsupportedSlackInteractiveRequestError = class extends Error {
3
+ kind;
4
+ requestId;
5
+ constructor(kind, requestId, message) {
6
+ super(message);
7
+ this.name = "UnsupportedSlackInteractiveRequestError";
8
+ this.kind = kind;
9
+ this.requestId = requestId;
10
+ }
11
+ };
12
+
13
+ // src/runtime/bridge.ts
14
+ async function bridgeSlackTurnEventsToSlack(events, sink, options) {
15
+ let fullResponse = "";
16
+ let lastUpdateLength = 0;
17
+ let postedTs;
18
+ let postedChannel;
19
+ let chatStream;
20
+ let chatStreamStopped = false;
21
+ let chatStreamAppendDisabled = false;
22
+ let chatStreamOverflowed = false;
23
+ let msgTooLongFallbackPosted = false;
24
+ let finalResponseStreamTextSuppressed = false;
25
+ let chatStreamContinuationNoticeAppended = false;
26
+ let progressiveContinuationNoticeShown = false;
27
+ let finalResponseArtifactContinuationNotice;
28
+ let statusLabel = "";
29
+ let lastNotifiedStatus = "";
30
+ let lastProgressiveUpdateAtMs;
31
+ const subAgentTitles = /* @__PURE__ */ new Map();
32
+ const openSubAgentTaskTitles = /* @__PURE__ */ new Map();
33
+ const taskUpdateFingerprints = /* @__PURE__ */ new Set();
34
+ let taskUpdateCount = 0;
35
+ let taskUpdateTextChars = 0;
36
+ let taskUpdatesOmitted = false;
37
+ async function notifyStatus(label, event) {
38
+ if (!options.onStatusChange) return;
39
+ const trimmed = label.trim();
40
+ if (trimmed === lastNotifiedStatus) return;
41
+ lastNotifiedStatus = trimmed;
42
+ try {
43
+ await options.onStatusChange(trimmed, event);
44
+ } catch {
45
+ }
46
+ }
47
+ async function ensurePlaceholder() {
48
+ if (postedTs) return;
49
+ const placeholder = statusLabel.trim() || "...";
50
+ const result = await sink.postMessage(placeholder);
51
+ postedTs = result.ts;
52
+ postedChannel = result.channel;
53
+ lastUpdateLength = 0;
54
+ }
55
+ async function updatePlaceholder(text) {
56
+ if (!postedTs || !postedChannel) return;
57
+ await sink.updateMessage(postedChannel, postedTs, text);
58
+ }
59
+ async function updateIfNeeded(force = false) {
60
+ if (!postedTs || !postedChannel || !fullResponse) return;
61
+ const replacingWithArtifact = shouldReplaceFinalResponseWithArtifact();
62
+ const replacementTextSuppressed = replacingWithArtifact && hasCrossedFinalResponseArtifactStreamThreshold();
63
+ if (replacementTextSuppressed && progressiveContinuationNoticeShown && !force) {
64
+ return;
65
+ }
66
+ const shouldUpdate = force || !replacementTextSuppressed && fullResponse.length - lastUpdateLength >= options.progressiveUpdateThreshold && shouldIssueProgressiveUpdate();
67
+ if (shouldUpdate) {
68
+ const text = formatProgressiveResponseText();
69
+ await sink.updateMessage(postedChannel, postedTs, text);
70
+ lastUpdateLength = fullResponse.length;
71
+ lastProgressiveUpdateAtMs = Date.now();
72
+ if (replacementTextSuppressed) {
73
+ progressiveContinuationNoticeShown = true;
74
+ finalResponseStreamTextSuppressed = true;
75
+ }
76
+ }
77
+ }
78
+ function shouldIssueProgressiveUpdate() {
79
+ if (options.progressiveUpdateIntervalMs <= 0) {
80
+ return true;
81
+ }
82
+ if (lastProgressiveUpdateAtMs === void 0) {
83
+ return true;
84
+ }
85
+ return Date.now() - lastProgressiveUpdateAtMs >= options.progressiveUpdateIntervalMs;
86
+ }
87
+ function shouldReplaceFinalResponseWithArtifact() {
88
+ return Boolean(
89
+ options.publishFinalResponseArtifact && options.finalResponseArtifactMode === "replace"
90
+ );
91
+ }
92
+ function finalResponseArtifactStreamThreshold() {
93
+ return Math.max(1, options.finalResponseArtifactStreamThreshold ?? 4e3);
94
+ }
95
+ function hasCrossedFinalResponseArtifactStreamThreshold() {
96
+ return fullResponse.length > finalResponseArtifactStreamThreshold();
97
+ }
98
+ function getFinalResponseArtifactContinuationNotice() {
99
+ if (finalResponseArtifactContinuationNotice !== void 0) {
100
+ return finalResponseArtifactContinuationNotice;
101
+ }
102
+ const notice = options.formatFinalResponseArtifactContinuationNotice?.({
103
+ text: fullResponse,
104
+ formattedText: options.formatMessageText(fullResponse)
105
+ });
106
+ finalResponseArtifactContinuationNotice = notice?.trim() ? notice : "";
107
+ return finalResponseArtifactContinuationNotice || void 0;
108
+ }
109
+ function formatTruncatedFinalResponseWithContinuationNotice() {
110
+ const threshold = finalResponseArtifactStreamThreshold();
111
+ const visibleText = fullResponse.slice(0, threshold).trimEnd();
112
+ const formattedText = options.formatMessageText(visibleText);
113
+ const notice = getFinalResponseArtifactContinuationNotice();
114
+ return notice ? `${formattedText}${notice}` : formattedText;
115
+ }
116
+ function formatProgressiveResponseText() {
117
+ if (shouldReplaceFinalResponseWithArtifact() && hasCrossedFinalResponseArtifactStreamThreshold()) {
118
+ return formatTruncatedFinalResponseWithContinuationNotice();
119
+ }
120
+ return options.formatMessageText(fullResponse);
121
+ }
122
+ function ensureChatStream() {
123
+ if (chatStream) {
124
+ return chatStream;
125
+ }
126
+ if (!sink.createChatStream) {
127
+ throw new Error(
128
+ "SlackResponseSink.createChatStream is required for chat-stream mode."
129
+ );
130
+ }
131
+ chatStream = sink.createChatStream({
132
+ bufferSize: options.chatStreamBufferSize
133
+ });
134
+ return chatStream;
135
+ }
136
+ async function postMsgTooLongFallback(text) {
137
+ if (msgTooLongFallbackPosted) {
138
+ return;
139
+ }
140
+ msgTooLongFallbackPosted = true;
141
+ const fallback = formatMsgTooLongFallback(text);
142
+ await sink.postMessage(fallback).catch(() => void 0);
143
+ }
144
+ function formatToolTitle(toolName) {
145
+ return options.formatToolTitle?.(toolName) ?? toolName;
146
+ }
147
+ function formatToolResultOutput(event) {
148
+ const formatted = options.formatToolResultOutput?.(event);
149
+ if (formatted === null) {
150
+ return void 0;
151
+ }
152
+ if (formatted !== void 0) {
153
+ return normalizeStreamChunkOutput(formatted, { truncate: false });
154
+ }
155
+ return formatStreamChunkOutput(event.result);
156
+ }
157
+ function subAgentTaskId(dispatchId) {
158
+ return `subagent:${dispatchId}`;
159
+ }
160
+ function subAgentToolTaskId(dispatchId, toolCallId) {
161
+ return `subagent:${dispatchId}:tool:${toolCallId}`;
162
+ }
163
+ function shouldRenderTaskUpdates() {
164
+ return options.showToolUsage && options.streamingMode === "chat-stream";
165
+ }
166
+ function formatSubAgentRole(role) {
167
+ const words = role.replace(/[_-]+/g, " ").trim();
168
+ return words ? words.charAt(0).toUpperCase() + words.slice(1) : "Subagent";
169
+ }
170
+ function inferSubAgentDepth(event) {
171
+ if (typeof event.depth === "number" && event.depth > 0) {
172
+ return event.depth;
173
+ }
174
+ if (event.agentPath) {
175
+ return Math.max(0, event.agentPath.split("/").filter(Boolean).length - 1);
176
+ }
177
+ return event.parentDispatchId ? 1 : 0;
178
+ }
179
+ function fallbackNestedPrefix(depth) {
180
+ return depth > 0 ? `Nested ${depth}` : "Nested";
181
+ }
182
+ function formatSubAgentTaskTitle(event) {
183
+ const roleTitle = formatSubAgentRole(event.role);
184
+ if (!event.parentDispatchId) {
185
+ return roleTitle;
186
+ }
187
+ const parentTitle = subAgentTitles.get(event.parentDispatchId);
188
+ const title = parentTitle ? `${parentTitle} / ${roleTitle}` : `${fallbackNestedPrefix(inferSubAgentDepth(event))} / ${roleTitle}`;
189
+ subAgentTitles.set(event.dispatchId, title);
190
+ return title;
191
+ }
192
+ function formatSubAgentStartDetails(event) {
193
+ return event.title && event.title !== event.role ? event.title : "Started";
194
+ }
195
+ function formatSubAgentStatusDetails(status) {
196
+ switch (status) {
197
+ case "thinking":
198
+ case "reasoning":
199
+ return options.formatReasoningUpdate();
200
+ case "calling-tool":
201
+ return "Using tools...";
202
+ case "waiting-approval":
203
+ return "Waiting for approval...";
204
+ case "waiting-input":
205
+ return "Waiting for input...";
206
+ case "processing":
207
+ return "Working...";
208
+ case "error":
209
+ return "Error";
210
+ default:
211
+ return void 0;
212
+ }
213
+ }
214
+ async function appendChatStreamText(text) {
215
+ if (chatStreamAppendDisabled) {
216
+ return;
217
+ }
218
+ const formatted = options.formatMessageText(text);
219
+ if (!formatted) {
220
+ return;
221
+ }
222
+ await appendToChatStream({ markdown_text: formatted });
223
+ }
224
+ async function appendFinalResponseTextToChatStream(text, previousLength) {
225
+ if (!shouldReplaceFinalResponseWithArtifact()) {
226
+ await appendChatStreamText(text);
227
+ return;
228
+ }
229
+ const threshold = finalResponseArtifactStreamThreshold();
230
+ if (previousLength >= threshold) {
231
+ finalResponseStreamTextSuppressed = true;
232
+ await appendFinalResponseArtifactContinuationNotice();
233
+ return;
234
+ }
235
+ const remainingVisibleCharacters = threshold - previousLength;
236
+ const visibleText = text.slice(0, remainingVisibleCharacters);
237
+ if (visibleText) {
238
+ await appendChatStreamText(visibleText);
239
+ }
240
+ if (text.length > remainingVisibleCharacters) {
241
+ finalResponseStreamTextSuppressed = true;
242
+ await appendFinalResponseArtifactContinuationNotice();
243
+ }
244
+ }
245
+ async function appendFinalResponseArtifactContinuationNotice() {
246
+ if (chatStreamContinuationNoticeAppended) {
247
+ return;
248
+ }
249
+ const notice = getFinalResponseArtifactContinuationNotice();
250
+ if (!notice) {
251
+ return;
252
+ }
253
+ chatStreamContinuationNoticeAppended = true;
254
+ await appendToChatStream({ markdown_text: notice });
255
+ }
256
+ async function appendChatStreamChunk(chunk) {
257
+ if (chatStreamAppendDisabled) {
258
+ return;
259
+ }
260
+ await appendToChatStream({ chunks: [chunk] });
261
+ }
262
+ async function appendToChatStream(args) {
263
+ try {
264
+ await ensureChatStream().append(args);
265
+ } catch (error) {
266
+ if (!isSlackMsgTooLongError(error)) {
267
+ throw error;
268
+ }
269
+ chatStreamAppendDisabled = true;
270
+ chatStreamOverflowed = true;
271
+ }
272
+ }
273
+ function truncateTaskField(value) {
274
+ if (value === void 0) {
275
+ return void 0;
276
+ }
277
+ const maxLength = Math.max(0, options.maxTaskUpdateFieldChars);
278
+ if (value.length <= maxLength) {
279
+ return value;
280
+ }
281
+ if (maxLength <= 3) {
282
+ return value.slice(0, maxLength);
283
+ }
284
+ return `${value.slice(0, maxLength - 3).trimEnd()}...`;
285
+ }
286
+ function normalizeTaskUpdate(chunk) {
287
+ const normalized = {
288
+ type: "task_update",
289
+ id: chunk.id,
290
+ title: chunk.title,
291
+ status: chunk.status
292
+ };
293
+ const details = truncateTaskField(chunk.details);
294
+ const output = truncateTaskField(chunk.output);
295
+ if (details !== void 0) {
296
+ normalized.details = details;
297
+ }
298
+ if (output !== void 0) {
299
+ normalized.output = output;
300
+ }
301
+ return normalized;
302
+ }
303
+ function taskUpdateTextLength(chunk) {
304
+ return chunk.title.length + (chunk.details?.length ?? 0) + (chunk.output?.length ?? 0);
305
+ }
306
+ function taskUpdateFingerprint(chunk) {
307
+ return JSON.stringify([
308
+ chunk.id,
309
+ chunk.title,
310
+ chunk.status,
311
+ chunk.details ?? "",
312
+ chunk.output ?? ""
313
+ ]);
314
+ }
315
+ async function appendTaskUpdatesOmittedNotice() {
316
+ if (taskUpdatesOmitted || taskUpdateCount >= options.maxTaskUpdates || options.maxTaskUpdateTextChars <= 0) {
317
+ return;
318
+ }
319
+ taskUpdatesOmitted = true;
320
+ const notice = {
321
+ type: "task_update",
322
+ id: "slack-task-updates-omitted",
323
+ title: "Additional task updates omitted",
324
+ status: "complete",
325
+ details: "Slack task timeline limit reached; see the final answer for remaining context."
326
+ };
327
+ taskUpdateCount += 1;
328
+ taskUpdateTextChars += taskUpdateTextLength(notice);
329
+ taskUpdateFingerprints.add(taskUpdateFingerprint(notice));
330
+ await appendChatStreamChunk(notice);
331
+ }
332
+ async function appendTaskUpdate(chunk, appendOptions = {}) {
333
+ const normalized = normalizeTaskUpdate(chunk);
334
+ const fingerprint = taskUpdateFingerprint(normalized);
335
+ if (taskUpdateFingerprints.has(fingerprint)) {
336
+ return false;
337
+ }
338
+ const textLength = taskUpdateTextLength(normalized);
339
+ const maxRealTaskUpdates = Math.max(0, options.maxTaskUpdates - 1);
340
+ if (!appendOptions.force && (taskUpdateCount >= maxRealTaskUpdates || taskUpdateTextChars + textLength > options.maxTaskUpdateTextChars)) {
341
+ await appendTaskUpdatesOmittedNotice();
342
+ return false;
343
+ }
344
+ taskUpdateFingerprints.add(fingerprint);
345
+ taskUpdateCount += 1;
346
+ taskUpdateTextChars += textLength;
347
+ await appendChatStreamChunk(normalized);
348
+ return !chatStreamAppendDisabled;
349
+ }
350
+ async function finalizeOpenSubAgentTasks() {
351
+ if (!shouldRenderTaskUpdates() || openSubAgentTaskTitles.size === 0) {
352
+ return;
353
+ }
354
+ for (const [dispatchId, title] of [...openSubAgentTaskTitles]) {
355
+ await appendTaskUpdate(
356
+ {
357
+ type: "task_update",
358
+ id: subAgentTaskId(dispatchId),
359
+ title,
360
+ status: "complete",
361
+ output: "Still running in the background."
362
+ },
363
+ { force: true }
364
+ );
365
+ openSubAgentTaskTitles.delete(dispatchId);
366
+ }
367
+ }
368
+ async function stopChatStream(markdownText, fallbackText) {
369
+ if (chatStreamStopped) {
370
+ return;
371
+ }
372
+ const stream = ensureChatStream();
373
+ if (chatStreamOverflowed) {
374
+ try {
375
+ await stream.stop();
376
+ } catch (error) {
377
+ if (!isSlackMsgTooLongError(error)) {
378
+ throw error;
379
+ }
380
+ }
381
+ chatStreamStopped = true;
382
+ await postMsgTooLongFallback(fallbackText ?? markdownText);
383
+ return;
384
+ }
385
+ const finalArgs = {
386
+ ...options.chatStreamFinalArgs ?? {}
387
+ };
388
+ if (markdownText) {
389
+ finalArgs.markdown_text = markdownText;
390
+ }
391
+ try {
392
+ await stream.stop(
393
+ Object.keys(finalArgs).length > 0 ? finalArgs : void 0
394
+ );
395
+ chatStreamStopped = true;
396
+ } catch (error) {
397
+ if (!isSlackMsgTooLongError(error)) {
398
+ throw error;
399
+ }
400
+ chatStreamStopped = true;
401
+ await postMsgTooLongFallback(fallbackText ?? markdownText);
402
+ }
403
+ }
404
+ async function publishFinalResponseArtifact(text, formattedText) {
405
+ if (!options.publishFinalResponseArtifact) {
406
+ return void 0;
407
+ }
408
+ const client = sink.artifactClient;
409
+ const channelId = sink.artifactTarget?.channelId;
410
+ if (!text || !client || !channelId) {
411
+ return void 0;
412
+ }
413
+ const context = {
414
+ text,
415
+ formattedText,
416
+ client,
417
+ channelId,
418
+ ...sink.artifactTarget?.threadTs ? { threadTs: sink.artifactTarget.threadTs } : {}
419
+ };
420
+ try {
421
+ const result = await options.publishFinalResponseArtifact(context);
422
+ return result ? { result, context } : void 0;
423
+ } catch (error) {
424
+ try {
425
+ await options.onFinalResponseArtifactError?.(error, context);
426
+ } catch {
427
+ }
428
+ return void 0;
429
+ }
430
+ }
431
+ function formatFinalResponseArtifactMessage(result, context) {
432
+ const message = options.formatFinalResponseArtifactMessage?.(result, context)?.trim();
433
+ if (message) {
434
+ return message;
435
+ }
436
+ const title = result.publication.artifact.title.trim() || "Agent response";
437
+ if (result.publication.method === "canvas") {
438
+ return `I created a Slack Canvas for the full response: ${title}`;
439
+ }
440
+ if (result.publication.method === "file") {
441
+ return `I uploaded the full response as a file: ${title}`;
442
+ }
443
+ return `I published the full response as a Slack artifact: ${title}`;
444
+ }
445
+ try {
446
+ for await (const event of events) {
447
+ switch (event.type) {
448
+ // ── Text streaming ──────────────────────────────────────────────
449
+ case "text-start":
450
+ if (options.streamingMode === "progressive") {
451
+ await ensurePlaceholder();
452
+ }
453
+ break;
454
+ case "text-delta": {
455
+ if (statusLabel) {
456
+ statusLabel = "";
457
+ await notifyStatus("", event);
458
+ }
459
+ const previousResponseLength = fullResponse.length;
460
+ fullResponse += event.text;
461
+ if (options.streamingMode === "chat-stream") {
462
+ await appendFinalResponseTextToChatStream(
463
+ event.text,
464
+ previousResponseLength
465
+ );
466
+ } else if (options.streamingMode === "progressive") {
467
+ await ensurePlaceholder();
468
+ const crossedReplacementThreshold = shouldReplaceFinalResponseWithArtifact() && previousResponseLength <= finalResponseArtifactStreamThreshold() && hasCrossedFinalResponseArtifactStreamThreshold();
469
+ await updateIfNeeded(crossedReplacementThreshold);
470
+ }
471
+ break;
472
+ }
473
+ case "text-end":
474
+ break;
475
+ // ── Reasoning ───────────────────────────────────────────────────
476
+ case "reasoning-start":
477
+ if (options.showReasoning) {
478
+ statusLabel = options.formatReasoningUpdate();
479
+ await notifyStatus(statusLabel, event);
480
+ if (options.streamingMode === "progressive") {
481
+ if (postedTs) {
482
+ await updatePlaceholder(statusLabel);
483
+ } else {
484
+ await ensurePlaceholder();
485
+ }
486
+ }
487
+ }
488
+ break;
489
+ case "reasoning-end":
490
+ statusLabel = "";
491
+ break;
492
+ // ── Tools ───────────────────────────────────────────────────────
493
+ case "tool-start":
494
+ if (options.showToolUsage) {
495
+ const toolTitle = formatToolTitle(event.toolName);
496
+ statusLabel = options.formatToolUpdate(event.toolName);
497
+ const toolDetails = options.formatToolDetails?.(event) ?? statusLabel;
498
+ await notifyStatus(statusLabel, event);
499
+ if (options.streamingMode === "chat-stream") {
500
+ await appendTaskUpdate({
501
+ type: "task_update",
502
+ id: event.toolCallId,
503
+ title: toolTitle,
504
+ status: "in_progress",
505
+ details: toolDetails
506
+ });
507
+ } else if (options.streamingMode === "progressive") {
508
+ if (!postedTs) {
509
+ const result = await sink.postMessage(statusLabel);
510
+ postedTs = result.ts;
511
+ postedChannel = result.channel;
512
+ } else if (!fullResponse) {
513
+ await updatePlaceholder(statusLabel);
514
+ }
515
+ }
516
+ }
517
+ break;
518
+ case "tool-result":
519
+ statusLabel = "";
520
+ if (options.showToolUsage && options.streamingMode === "chat-stream") {
521
+ const toolTitle = formatToolTitle(event.toolName);
522
+ await appendTaskUpdate({
523
+ type: "task_update",
524
+ id: event.toolCallId,
525
+ title: toolTitle,
526
+ status: "complete",
527
+ output: formatToolResultOutput(event)
528
+ });
529
+ }
530
+ break;
531
+ case "tool-error":
532
+ if (options.showToolUsage) {
533
+ const toolTitle = formatToolTitle(event.toolName);
534
+ statusLabel = options.formatToolError(event.toolName, event.error);
535
+ await notifyStatus(statusLabel, event);
536
+ if (options.streamingMode === "chat-stream") {
537
+ await appendTaskUpdate({
538
+ type: "task_update",
539
+ id: event.toolCallId,
540
+ title: toolTitle,
541
+ status: "error",
542
+ details: statusLabel
543
+ });
544
+ } else if (options.streamingMode === "progressive" && postedTs && !fullResponse) {
545
+ await updatePlaceholder(statusLabel);
546
+ }
547
+ }
548
+ break;
549
+ // ── Subagents ───────────────────────────────────────────────────
550
+ case "subagent-start": {
551
+ const roleTitle = formatSubAgentTaskTitle(event);
552
+ subAgentTitles.set(event.dispatchId, roleTitle);
553
+ const details = formatSubAgentStartDetails(event);
554
+ statusLabel = `${roleTitle}: ${details}`;
555
+ await notifyStatus(statusLabel, event);
556
+ if (shouldRenderTaskUpdates()) {
557
+ const appended = await appendTaskUpdate({
558
+ type: "task_update",
559
+ id: subAgentTaskId(event.dispatchId),
560
+ title: roleTitle,
561
+ status: "in_progress",
562
+ details
563
+ });
564
+ if (appended) {
565
+ openSubAgentTaskTitles.set(event.dispatchId, roleTitle);
566
+ }
567
+ }
568
+ break;
569
+ }
570
+ case "subagent-event": {
571
+ const roleTitle = formatSubAgentTaskTitle(event);
572
+ const childEvent = event.event;
573
+ if (childEvent.type === "approval-request") {
574
+ const msg = `${roleTitle}: ${options.formatApprovalRequired(
575
+ childEvent.request
576
+ )}`;
577
+ const handled = await options.handleInteractiveRequest?.({
578
+ kind: "approval",
579
+ request: childEvent.request,
580
+ fallbackText: msg
581
+ });
582
+ if (handled) {
583
+ statusLabel = "";
584
+ await notifyStatus("", event);
585
+ break;
586
+ }
587
+ if (options.interactiveMode === "message-and-error") {
588
+ if (options.streamingMode === "chat-stream") {
589
+ await stopChatStream(msg, msg);
590
+ } else if (postedTs && postedChannel) {
591
+ await sink.updateMessage(postedChannel, postedTs, msg);
592
+ } else {
593
+ await sink.postMessage(msg);
594
+ }
595
+ throw new UnsupportedSlackInteractiveRequestError(
596
+ "approval",
597
+ childEvent.request.id ?? "unknown",
598
+ msg
599
+ );
600
+ }
601
+ break;
602
+ }
603
+ if (childEvent.type === "human-input-request") {
604
+ const msg = `${roleTitle}: ${options.formatHumanInputRequired(
605
+ childEvent.request
606
+ )}`;
607
+ const handled = await options.handleInteractiveRequest?.({
608
+ kind: "human-input",
609
+ request: childEvent.request,
610
+ fallbackText: msg
611
+ });
612
+ if (handled) {
613
+ statusLabel = "";
614
+ await notifyStatus("", event);
615
+ break;
616
+ }
617
+ if (options.interactiveMode === "message-and-error") {
618
+ if (options.streamingMode === "chat-stream") {
619
+ await stopChatStream(msg, msg);
620
+ } else if (postedTs && postedChannel) {
621
+ await sink.updateMessage(postedChannel, postedTs, msg);
622
+ } else {
623
+ await sink.postMessage(msg);
624
+ }
625
+ throw new UnsupportedSlackInteractiveRequestError(
626
+ "human-input",
627
+ childEvent.request.id ?? "unknown",
628
+ msg
629
+ );
630
+ }
631
+ break;
632
+ }
633
+ if (childEvent.type === "approval-resolved" || childEvent.type === "human-input-resolved") {
634
+ statusLabel = "";
635
+ await notifyStatus("", event);
636
+ break;
637
+ }
638
+ if (childEvent.type === "status") {
639
+ const details = formatSubAgentStatusDetails(childEvent.status);
640
+ if (details) {
641
+ statusLabel = `${roleTitle}: ${details}`;
642
+ await notifyStatus(statusLabel, event);
643
+ }
644
+ break;
645
+ }
646
+ if (!options.showToolUsage || !options.showSubagentToolUsage || options.streamingMode !== "chat-stream") {
647
+ break;
648
+ }
649
+ if (childEvent.type === "tool-start") {
650
+ const toolTitle = formatToolTitle(childEvent.toolName);
651
+ const toolDetails = options.formatToolDetails?.(childEvent) ?? options.formatToolUpdate(childEvent.toolName);
652
+ await appendTaskUpdate({
653
+ type: "task_update",
654
+ id: subAgentToolTaskId(event.dispatchId, childEvent.toolCallId),
655
+ title: `${roleTitle}: ${toolTitle}`,
656
+ status: "in_progress",
657
+ details: toolDetails
658
+ });
659
+ } else if (childEvent.type === "tool-result") {
660
+ const toolTitle = formatToolTitle(childEvent.toolName);
661
+ await appendTaskUpdate({
662
+ type: "task_update",
663
+ id: subAgentToolTaskId(event.dispatchId, childEvent.toolCallId),
664
+ title: `${roleTitle}: ${toolTitle}`,
665
+ status: "complete",
666
+ output: formatToolResultOutput(childEvent)
667
+ });
668
+ } else if (childEvent.type === "tool-error") {
669
+ const toolTitle = formatToolTitle(childEvent.toolName);
670
+ await appendTaskUpdate({
671
+ type: "task_update",
672
+ id: subAgentToolTaskId(event.dispatchId, childEvent.toolCallId),
673
+ title: `${roleTitle}: ${toolTitle}`,
674
+ status: "error",
675
+ details: options.formatToolError(
676
+ childEvent.toolName,
677
+ childEvent.error
678
+ )
679
+ });
680
+ }
681
+ break;
682
+ }
683
+ case "subagent-complete": {
684
+ const roleTitle = formatSubAgentTaskTitle(event);
685
+ statusLabel = "";
686
+ await notifyStatus("", event);
687
+ if (shouldRenderTaskUpdates()) {
688
+ const wasOpen = openSubAgentTaskTitles.has(event.dispatchId);
689
+ const taskUpdate = {
690
+ type: "task_update",
691
+ id: subAgentTaskId(event.dispatchId),
692
+ title: roleTitle,
693
+ status: "complete"
694
+ };
695
+ if (options.showSubagentResultInTask) {
696
+ taskUpdate.output = formatStreamChunkOutput(event.output);
697
+ }
698
+ await appendTaskUpdate(taskUpdate, { force: wasOpen });
699
+ openSubAgentTaskTitles.delete(event.dispatchId);
700
+ }
701
+ break;
702
+ }
703
+ case "subagent-error": {
704
+ const roleTitle = formatSubAgentTaskTitle(event);
705
+ statusLabel = `${roleTitle}: ${event.error}`;
706
+ await notifyStatus(statusLabel, event);
707
+ if (shouldRenderTaskUpdates()) {
708
+ const wasOpen = openSubAgentTaskTitles.has(event.dispatchId);
709
+ await appendTaskUpdate(
710
+ {
711
+ type: "task_update",
712
+ id: subAgentTaskId(event.dispatchId),
713
+ title: roleTitle,
714
+ status: "error",
715
+ details: event.error
716
+ },
717
+ { force: wasOpen }
718
+ );
719
+ openSubAgentTaskTitles.delete(event.dispatchId);
720
+ }
721
+ break;
722
+ }
723
+ // ── Status ──────────────────────────────────────────────────────
724
+ case "status":
725
+ if ((event.status === "thinking" || event.status === "reasoning") && options.showReasoning) {
726
+ statusLabel = options.formatReasoningUpdate();
727
+ await notifyStatus(statusLabel, event);
728
+ }
729
+ break;
730
+ // ── Completion ──────────────────────────────────────────────────
731
+ case "complete": {
732
+ if (!fullResponse && event.output) {
733
+ const previousResponseLength = fullResponse.length;
734
+ fullResponse = event.output;
735
+ if (options.streamingMode === "chat-stream") {
736
+ await appendFinalResponseTextToChatStream(
737
+ event.output,
738
+ previousResponseLength
739
+ );
740
+ }
741
+ }
742
+ await finalizeOpenSubAgentTasks();
743
+ break;
744
+ }
745
+ // ── Interactive requests (unsupported in Slack transport) ────────
746
+ case "approval-request": {
747
+ const msg = options.formatApprovalRequired(event.request);
748
+ const handled = await options.handleInteractiveRequest?.({
749
+ kind: "approval",
750
+ request: event.request,
751
+ fallbackText: msg
752
+ });
753
+ if (handled) {
754
+ statusLabel = "";
755
+ await notifyStatus("", event);
756
+ break;
757
+ }
758
+ if (options.interactiveMode === "message-and-error") {
759
+ if (options.streamingMode === "chat-stream") {
760
+ await stopChatStream(msg, msg);
761
+ } else if (postedTs && postedChannel) {
762
+ await sink.updateMessage(postedChannel, postedTs, msg);
763
+ } else {
764
+ await sink.postMessage(msg);
765
+ }
766
+ throw new UnsupportedSlackInteractiveRequestError(
767
+ "approval",
768
+ event.request.id ?? "unknown",
769
+ msg
770
+ );
771
+ }
772
+ break;
773
+ }
774
+ case "human-input-request": {
775
+ const msg = options.formatHumanInputRequired(event.request);
776
+ const handled = await options.handleInteractiveRequest?.({
777
+ kind: "human-input",
778
+ request: event.request,
779
+ fallbackText: msg
780
+ });
781
+ if (handled) {
782
+ statusLabel = "";
783
+ await notifyStatus("", event);
784
+ break;
785
+ }
786
+ if (options.interactiveMode === "message-and-error") {
787
+ if (options.streamingMode === "chat-stream") {
788
+ await stopChatStream(msg, msg);
789
+ } else if (postedTs && postedChannel) {
790
+ await sink.updateMessage(postedChannel, postedTs, msg);
791
+ } else {
792
+ await sink.postMessage(msg);
793
+ }
794
+ throw new UnsupportedSlackInteractiveRequestError(
795
+ "human-input",
796
+ event.request.id ?? "unknown",
797
+ msg
798
+ );
799
+ }
800
+ break;
801
+ }
802
+ default:
803
+ break;
804
+ }
805
+ }
806
+ const rawFinalText = fullResponse.trim();
807
+ let finalText = rawFinalText ? options.formatMessageText(rawFinalText) : "_No response_";
808
+ let publishedArtifact;
809
+ if (shouldReplaceFinalResponseWithArtifact()) {
810
+ publishedArtifact = await publishFinalResponseArtifact(
811
+ rawFinalText,
812
+ finalText
813
+ );
814
+ if (publishedArtifact) {
815
+ finalText = formatFinalResponseArtifactMessage(
816
+ publishedArtifact.result,
817
+ publishedArtifact.context
818
+ );
819
+ }
820
+ }
821
+ if (options.streamingMode === "chat-stream") {
822
+ const chatStreamFinalText = publishedArtifact || !rawFinalText || finalResponseStreamTextSuppressed ? finalText : void 0;
823
+ await stopChatStream(chatStreamFinalText, finalText);
824
+ } else if (postedTs && postedChannel) {
825
+ await sink.updateMessage(postedChannel, postedTs, finalText);
826
+ } else {
827
+ await sink.postMessage(finalText);
828
+ }
829
+ if (!shouldReplaceFinalResponseWithArtifact()) {
830
+ await publishFinalResponseArtifact(rawFinalText, finalText);
831
+ }
832
+ return fullResponse;
833
+ } catch (error) {
834
+ if (error instanceof UnsupportedSlackInteractiveRequestError) {
835
+ throw error;
836
+ }
837
+ if (options.streamingMode === "chat-stream") {
838
+ if (chatStream && !chatStreamStopped) {
839
+ await stopChatStream(
840
+ void 0,
841
+ fullResponse.trim() ? options.formatMessageText(fullResponse.trim()) : void 0
842
+ ).catch(() => void 0);
843
+ }
844
+ } else if (fullResponse && postedTs && postedChannel) {
845
+ await sink.updateMessage(
846
+ postedChannel,
847
+ postedTs,
848
+ options.formatMessageText(fullResponse)
849
+ ).catch(() => void 0);
850
+ }
851
+ throw error;
852
+ }
853
+ }
854
+ function formatStreamChunkOutput(value) {
855
+ if (value === void 0 || value === null) {
856
+ return void 0;
857
+ }
858
+ const text = typeof value === "string" ? value : safeJsonStringify(value) ?? String(value);
859
+ return normalizeStreamChunkOutput(text, { truncate: true });
860
+ }
861
+ function normalizeStreamChunkOutput(text, options) {
862
+ const normalized = text.trim();
863
+ if (!normalized) {
864
+ return void 0;
865
+ }
866
+ return options.truncate && normalized.length > 500 ? `${normalized.slice(0, 497).trimEnd()}...` : normalized;
867
+ }
868
+ function isSlackMsgTooLongError(error) {
869
+ if (typeof error === "object" && error !== null) {
870
+ const record = error;
871
+ if (record.code === "msg_too_long" || record.error === "msg_too_long") {
872
+ return true;
873
+ }
874
+ const data = record.data;
875
+ if (typeof data === "object" && data !== null) {
876
+ const dataRecord = data;
877
+ if (dataRecord.error === "msg_too_long" || dataRecord.code === "msg_too_long") {
878
+ return true;
879
+ }
880
+ }
881
+ }
882
+ return error instanceof Error && error.message.includes("msg_too_long");
883
+ }
884
+ function formatMsgTooLongFallback(text) {
885
+ const intro = "Slack could not finalize the streamed task timeline because it was too large. Task details were truncated.";
886
+ const normalized = text?.trim();
887
+ if (!normalized) {
888
+ return intro;
889
+ }
890
+ const maxLength = 8e3;
891
+ const separator = "\n\n";
892
+ const available = Math.max(0, maxLength - intro.length - separator.length);
893
+ const body = normalized.length > available ? `${normalized.slice(0, Math.max(0, available - 3)).trimEnd()}...` : normalized;
894
+ return `${intro}${separator}${body}`;
895
+ }
896
+ function safeJsonStringify(value) {
897
+ try {
898
+ return JSON.stringify(value);
899
+ } catch {
900
+ return void 0;
901
+ }
902
+ }
903
+
904
+ // src/runtime/options.ts
905
+ var DEFAULT_OPTIONS = {
906
+ showReasoning: false,
907
+ showToolUsage: true,
908
+ showSubagentToolUsage: false,
909
+ showSubagentResultInTask: false,
910
+ formatToolTitle: (toolName) => toolName,
911
+ formatToolUpdate: (toolName) => `\u{1F50D} Using tool: ${toolName}...`,
912
+ formatToolError: (toolName, _error) => `\u26A0\uFE0F Tool ${toolName} encountered an error`,
913
+ formatReasoningUpdate: () => "Thinking...",
914
+ formatMessageText: (text) => text,
915
+ streamingMode: "progressive",
916
+ progressiveUpdateThreshold: 150,
917
+ progressiveUpdateIntervalMs: 3e3,
918
+ chatStreamBufferSize: 256,
919
+ maxTaskUpdates: 200,
920
+ maxTaskUpdateTextChars: 2e4,
921
+ maxTaskUpdateFieldChars: 500,
922
+ interactiveMode: "message-and-error",
923
+ formatApprovalRequired: (request) => `This turn requires approval for "${request.tool}", but in-channel approval is not configured for this Slack host.`,
924
+ formatHumanInputRequired: (request) => `This turn is waiting for input: ${request.title}. In-channel human input is not configured for this Slack host.`,
925
+ finalResponseArtifactMode: "supplemental",
926
+ finalResponseArtifactStreamThreshold: 4e3,
927
+ formatFinalResponseArtifactContinuationNotice: () => "\n\nFull response is long. I will publish the complete answer as a Slack artifact when it finishes.",
928
+ formatFinalResponseArtifactMessage: (result) => {
929
+ const title = result.publication.artifact.title.trim() || "Agent response";
930
+ if (result.publication.method === "canvas") {
931
+ return `I created a Slack Canvas for the full response: ${title}`;
932
+ }
933
+ if (result.publication.method === "file") {
934
+ return `I uploaded the full response as a file: ${title}`;
935
+ }
936
+ return `I published the full response as a Slack artifact: ${title}`;
937
+ }
938
+ };
939
+ function resolveSlackEventBridgeOptions(partial) {
940
+ return {
941
+ showReasoning: partial.showReasoning ?? DEFAULT_OPTIONS.showReasoning,
942
+ showToolUsage: partial.showToolUsage ?? DEFAULT_OPTIONS.showToolUsage,
943
+ showSubagentToolUsage: partial.showSubagentToolUsage ?? DEFAULT_OPTIONS.showSubagentToolUsage,
944
+ showSubagentResultInTask: partial.showSubagentResultInTask ?? DEFAULT_OPTIONS.showSubagentResultInTask,
945
+ formatToolTitle: partial.formatToolTitle ?? DEFAULT_OPTIONS.formatToolTitle,
946
+ formatToolUpdate: partial.formatToolUpdate ?? DEFAULT_OPTIONS.formatToolUpdate,
947
+ ...partial.formatToolDetails ? { formatToolDetails: partial.formatToolDetails } : {},
948
+ ...partial.formatToolResultOutput ? { formatToolResultOutput: partial.formatToolResultOutput } : {},
949
+ formatToolError: partial.formatToolError ?? DEFAULT_OPTIONS.formatToolError,
950
+ formatReasoningUpdate: partial.formatReasoningUpdate ?? DEFAULT_OPTIONS.formatReasoningUpdate,
951
+ formatMessageText: partial.formatMessageText ?? DEFAULT_OPTIONS.formatMessageText,
952
+ streamingMode: partial.streamingMode ?? DEFAULT_OPTIONS.streamingMode,
953
+ progressiveUpdateThreshold: partial.progressiveUpdateThreshold ?? DEFAULT_OPTIONS.progressiveUpdateThreshold,
954
+ progressiveUpdateIntervalMs: partial.progressiveUpdateIntervalMs ?? DEFAULT_OPTIONS.progressiveUpdateIntervalMs,
955
+ chatStreamBufferSize: partial.chatStreamBufferSize ?? DEFAULT_OPTIONS.chatStreamBufferSize,
956
+ maxTaskUpdates: resolveNonNegativeInteger(
957
+ partial.maxTaskUpdates,
958
+ DEFAULT_OPTIONS.maxTaskUpdates
959
+ ),
960
+ maxTaskUpdateTextChars: resolveNonNegativeInteger(
961
+ partial.maxTaskUpdateTextChars,
962
+ DEFAULT_OPTIONS.maxTaskUpdateTextChars
963
+ ),
964
+ maxTaskUpdateFieldChars: resolveNonNegativeInteger(
965
+ partial.maxTaskUpdateFieldChars,
966
+ DEFAULT_OPTIONS.maxTaskUpdateFieldChars
967
+ ),
968
+ interactiveMode: partial.interactiveMode ?? DEFAULT_OPTIONS.interactiveMode,
969
+ ...partial.handleInteractiveRequest ? { handleInteractiveRequest: partial.handleInteractiveRequest } : {},
970
+ formatApprovalRequired: partial.formatApprovalRequired ?? DEFAULT_OPTIONS.formatApprovalRequired,
971
+ formatHumanInputRequired: partial.formatHumanInputRequired ?? DEFAULT_OPTIONS.formatHumanInputRequired,
972
+ ...partial.onStatusChange ? { onStatusChange: partial.onStatusChange } : {},
973
+ ...partial.chatStreamFinalArgs ? { chatStreamFinalArgs: partial.chatStreamFinalArgs } : {},
974
+ ...partial.publishFinalResponseArtifact ? { publishFinalResponseArtifact: partial.publishFinalResponseArtifact } : {},
975
+ finalResponseArtifactMode: partial.finalResponseArtifactMode ?? DEFAULT_OPTIONS.finalResponseArtifactMode,
976
+ finalResponseArtifactStreamThreshold: resolvePositiveInteger(
977
+ partial.finalResponseArtifactStreamThreshold,
978
+ DEFAULT_OPTIONS.finalResponseArtifactStreamThreshold ?? 4e3
979
+ ),
980
+ formatFinalResponseArtifactContinuationNotice: partial.formatFinalResponseArtifactContinuationNotice ?? DEFAULT_OPTIONS.formatFinalResponseArtifactContinuationNotice,
981
+ formatFinalResponseArtifactMessage: partial.formatFinalResponseArtifactMessage ?? DEFAULT_OPTIONS.formatFinalResponseArtifactMessage,
982
+ ...partial.onFinalResponseArtifactError ? { onFinalResponseArtifactError: partial.onFinalResponseArtifactError } : {}
983
+ };
984
+ }
985
+ function resolveNonNegativeInteger(value, fallback) {
986
+ if (value === void 0) {
987
+ return fallback;
988
+ }
989
+ if (!Number.isFinite(value) || value < 0) {
990
+ return fallback;
991
+ }
992
+ return Math.floor(value);
993
+ }
994
+ function resolvePositiveInteger(value, fallback) {
995
+ if (value === void 0) {
996
+ return fallback;
997
+ }
998
+ if (!Number.isFinite(value) || value < 1) {
999
+ return fallback;
1000
+ }
1001
+ return Math.floor(value);
1002
+ }
1003
+
1004
+ export {
1005
+ UnsupportedSlackInteractiveRequestError,
1006
+ bridgeSlackTurnEventsToSlack,
1007
+ resolveSlackEventBridgeOptions
1008
+ };