@sentry/junior 0.22.0 → 0.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/app.js +829 -296
- package/package.json +1 -1
package/dist/app.js
CHANGED
|
@@ -311,9 +311,6 @@ async function GET3() {
|
|
|
311
311
|
});
|
|
312
312
|
}
|
|
313
313
|
|
|
314
|
-
// src/handlers/mcp-oauth-callback.ts
|
|
315
|
-
import { Buffer as Buffer2 } from "buffer";
|
|
316
|
-
|
|
317
314
|
// src/chat/state/conversation.ts
|
|
318
315
|
function coerceRole(value) {
|
|
319
316
|
return value === "assistant" || value === "system" || value === "user" ? value : "user";
|
|
@@ -729,81 +726,6 @@ async function deleteMcpServerSessionId(userId, provider) {
|
|
|
729
726
|
await stateAdapter.delete(serverSessionKey(userId, provider));
|
|
730
727
|
}
|
|
731
728
|
|
|
732
|
-
// src/chat/slack/output.ts
|
|
733
|
-
var MAX_INLINE_CHARS = 2200;
|
|
734
|
-
var MAX_INLINE_LINES = 45;
|
|
735
|
-
function ensureBlockSpacing(text) {
|
|
736
|
-
const codeBlockPattern = /^```/;
|
|
737
|
-
const listItemPattern = /^[-*•]\s|^\d+\.\s/;
|
|
738
|
-
const lines = text.split("\n");
|
|
739
|
-
const result = [];
|
|
740
|
-
let inCodeBlock = false;
|
|
741
|
-
for (let i = 0; i < lines.length; i++) {
|
|
742
|
-
const line = lines[i];
|
|
743
|
-
const isCodeFence = codeBlockPattern.test(line.trimStart());
|
|
744
|
-
if (isCodeFence) {
|
|
745
|
-
if (!inCodeBlock) {
|
|
746
|
-
const prev2 = result.length > 0 ? result[result.length - 1] : void 0;
|
|
747
|
-
if (prev2 !== void 0 && prev2.trim() !== "") {
|
|
748
|
-
result.push("");
|
|
749
|
-
}
|
|
750
|
-
}
|
|
751
|
-
inCodeBlock = !inCodeBlock;
|
|
752
|
-
result.push(line);
|
|
753
|
-
continue;
|
|
754
|
-
}
|
|
755
|
-
if (inCodeBlock) {
|
|
756
|
-
result.push(line);
|
|
757
|
-
continue;
|
|
758
|
-
}
|
|
759
|
-
const prev = result.length > 0 ? result[result.length - 1] : void 0;
|
|
760
|
-
if (prev !== void 0 && prev.trim() !== "" && line.trim() !== "" && !(listItemPattern.test(prev.trimStart()) && listItemPattern.test(line.trimStart()))) {
|
|
761
|
-
result.push("");
|
|
762
|
-
}
|
|
763
|
-
result.push(line);
|
|
764
|
-
}
|
|
765
|
-
return result.join("\n");
|
|
766
|
-
}
|
|
767
|
-
function normalizeForSlack(text) {
|
|
768
|
-
let normalized = text.replace(/\r\n?/g, "\n").replace(/[ \t]+$/gm, "");
|
|
769
|
-
normalized = ensureBlockSpacing(normalized);
|
|
770
|
-
return normalized.replace(/\n{3,}/g, "\n\n").trim();
|
|
771
|
-
}
|
|
772
|
-
function buildSlackOutputMessage(text, files) {
|
|
773
|
-
const normalized = normalizeForSlack(text);
|
|
774
|
-
const fileCount = files?.length ?? 0;
|
|
775
|
-
if (!normalized) {
|
|
776
|
-
if (fileCount > 0) {
|
|
777
|
-
return {
|
|
778
|
-
raw: "",
|
|
779
|
-
files
|
|
780
|
-
};
|
|
781
|
-
}
|
|
782
|
-
logWarn(
|
|
783
|
-
"slack_output_normalized_empty",
|
|
784
|
-
{},
|
|
785
|
-
{
|
|
786
|
-
"app.output.original_length": text.length,
|
|
787
|
-
"app.output.parsed_length": normalized.length,
|
|
788
|
-
"app.output.file_count": fileCount
|
|
789
|
-
},
|
|
790
|
-
"Slack output normalized to empty content"
|
|
791
|
-
);
|
|
792
|
-
return {
|
|
793
|
-
markdown: "I couldn't produce a response.",
|
|
794
|
-
files
|
|
795
|
-
};
|
|
796
|
-
}
|
|
797
|
-
return {
|
|
798
|
-
markdown: normalized,
|
|
799
|
-
files
|
|
800
|
-
};
|
|
801
|
-
}
|
|
802
|
-
var slackOutputPolicy = {
|
|
803
|
-
maxInlineChars: MAX_INLINE_CHARS,
|
|
804
|
-
maxInlineLines: MAX_INLINE_LINES
|
|
805
|
-
};
|
|
806
|
-
|
|
807
729
|
// src/chat/mcp/oauth.ts
|
|
808
730
|
import { randomUUID } from "crypto";
|
|
809
731
|
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
@@ -2176,24 +2098,6 @@ function markTurnFailed(args) {
|
|
|
2176
2098
|
});
|
|
2177
2099
|
args.updateConversationStats(args.conversation);
|
|
2178
2100
|
}
|
|
2179
|
-
function resolveReplyDelivery(args) {
|
|
2180
|
-
const replyHasFiles = Boolean(
|
|
2181
|
-
args.reply.files && args.reply.files.length > 0
|
|
2182
|
-
);
|
|
2183
|
-
const deliveryPlan = args.reply.deliveryPlan ?? {
|
|
2184
|
-
mode: args.reply.deliveryMode ?? "thread",
|
|
2185
|
-
postThreadText: (args.reply.deliveryMode ?? "thread") !== "channel_only",
|
|
2186
|
-
attachFiles: replyHasFiles ? args.hasStreamedThreadReply ? "followup" : "inline" : "none"
|
|
2187
|
-
};
|
|
2188
|
-
let attachFiles = replyHasFiles ? deliveryPlan.attachFiles : "none";
|
|
2189
|
-
if (attachFiles === "followup" && !args.hasStreamedThreadReply) {
|
|
2190
|
-
attachFiles = "inline";
|
|
2191
|
-
}
|
|
2192
|
-
return {
|
|
2193
|
-
shouldPostThreadReply: deliveryPlan.postThreadText,
|
|
2194
|
-
attachFiles
|
|
2195
|
-
};
|
|
2196
|
-
}
|
|
2197
2101
|
|
|
2198
2102
|
// src/chat/runtime/turn-user-message.ts
|
|
2199
2103
|
function getTurnUserMessage(conversation, sessionId) {
|
|
@@ -2799,6 +2703,276 @@ import { Agent } from "@mariozechner/pi-agent-core";
|
|
|
2799
2703
|
// src/chat/prompt.ts
|
|
2800
2704
|
import fs from "fs";
|
|
2801
2705
|
import path2 from "path";
|
|
2706
|
+
|
|
2707
|
+
// src/chat/slack/output.ts
|
|
2708
|
+
var MAX_INLINE_CHARS = 2200;
|
|
2709
|
+
var MAX_INLINE_LINES = 45;
|
|
2710
|
+
var CONTINUED_MARKER = "\n\n[Continued below]";
|
|
2711
|
+
var INTERRUPTED_MARKER = "\n\n[Response interrupted before completion]";
|
|
2712
|
+
var STREAMING_FENCE_CLOSE_GUARD = "\n```";
|
|
2713
|
+
function ensureBlockSpacing(text) {
|
|
2714
|
+
const codeBlockPattern = /^```/;
|
|
2715
|
+
const listItemPattern = /^[-*•]\s|^\d+\.\s/;
|
|
2716
|
+
const lines = text.split("\n");
|
|
2717
|
+
const result = [];
|
|
2718
|
+
let inCodeBlock = false;
|
|
2719
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2720
|
+
const line = lines[i];
|
|
2721
|
+
const isCodeFence = codeBlockPattern.test(line.trimStart());
|
|
2722
|
+
if (isCodeFence) {
|
|
2723
|
+
if (!inCodeBlock) {
|
|
2724
|
+
const prev2 = result.length > 0 ? result[result.length - 1] : void 0;
|
|
2725
|
+
if (prev2 !== void 0 && prev2.trim() !== "") {
|
|
2726
|
+
result.push("");
|
|
2727
|
+
}
|
|
2728
|
+
}
|
|
2729
|
+
inCodeBlock = !inCodeBlock;
|
|
2730
|
+
result.push(line);
|
|
2731
|
+
continue;
|
|
2732
|
+
}
|
|
2733
|
+
if (inCodeBlock) {
|
|
2734
|
+
result.push(line);
|
|
2735
|
+
continue;
|
|
2736
|
+
}
|
|
2737
|
+
const prev = result.length > 0 ? result[result.length - 1] : void 0;
|
|
2738
|
+
if (prev !== void 0 && prev.trim() !== "" && line.trim() !== "" && !(listItemPattern.test(prev.trimStart()) && listItemPattern.test(line.trimStart()))) {
|
|
2739
|
+
result.push("");
|
|
2740
|
+
}
|
|
2741
|
+
result.push(line);
|
|
2742
|
+
}
|
|
2743
|
+
return result.join("\n");
|
|
2744
|
+
}
|
|
2745
|
+
function normalizeForSlack(text) {
|
|
2746
|
+
let normalized = text.replace(/\r\n?/g, "\n").replace(/[ \t]+$/gm, "");
|
|
2747
|
+
normalized = ensureBlockSpacing(normalized);
|
|
2748
|
+
return normalized.replace(/\n{3,}/g, "\n\n").trim();
|
|
2749
|
+
}
|
|
2750
|
+
function countSlackLines(text) {
|
|
2751
|
+
if (!text) {
|
|
2752
|
+
return 0;
|
|
2753
|
+
}
|
|
2754
|
+
return text.split("\n").length;
|
|
2755
|
+
}
|
|
2756
|
+
function fitsInlineBudget(text, maxChars = MAX_INLINE_CHARS, maxLines = MAX_INLINE_LINES) {
|
|
2757
|
+
return text.length <= maxChars && countSlackLines(text) <= maxLines;
|
|
2758
|
+
}
|
|
2759
|
+
function findSplitIndex(text, maxChars) {
|
|
2760
|
+
if (text.length <= maxChars) {
|
|
2761
|
+
return text.length;
|
|
2762
|
+
}
|
|
2763
|
+
const bounded = text.slice(0, maxChars);
|
|
2764
|
+
const newlineIndex = bounded.lastIndexOf("\n");
|
|
2765
|
+
if (newlineIndex > 0) {
|
|
2766
|
+
return newlineIndex;
|
|
2767
|
+
}
|
|
2768
|
+
const spaceIndex = bounded.lastIndexOf(" ");
|
|
2769
|
+
if (spaceIndex > 0) {
|
|
2770
|
+
return spaceIndex;
|
|
2771
|
+
}
|
|
2772
|
+
return maxChars;
|
|
2773
|
+
}
|
|
2774
|
+
function splitByLineBudget(text, maxLines) {
|
|
2775
|
+
if (maxLines <= 0) {
|
|
2776
|
+
return "";
|
|
2777
|
+
}
|
|
2778
|
+
const lines = text.split("\n");
|
|
2779
|
+
if (lines.length <= maxLines) {
|
|
2780
|
+
return text;
|
|
2781
|
+
}
|
|
2782
|
+
return lines.slice(0, maxLines).join("\n");
|
|
2783
|
+
}
|
|
2784
|
+
function reserveInlineBudgetForSuffix(suffix, maxChars = MAX_INLINE_CHARS, maxLines = MAX_INLINE_LINES) {
|
|
2785
|
+
return {
|
|
2786
|
+
maxChars: Math.max(1, maxChars - suffix.length),
|
|
2787
|
+
maxLines: Math.max(1, maxLines - Math.max(0, countSlackLines(suffix) - 1))
|
|
2788
|
+
};
|
|
2789
|
+
}
|
|
2790
|
+
function forceSplitBudget(text, budget) {
|
|
2791
|
+
const lineCount = countSlackLines(text);
|
|
2792
|
+
return {
|
|
2793
|
+
maxChars: text.length <= budget.maxChars ? Math.max(1, text.length - 1) : budget.maxChars,
|
|
2794
|
+
maxLines: lineCount <= budget.maxLines ? Math.max(1, lineCount - 1) : budget.maxLines
|
|
2795
|
+
};
|
|
2796
|
+
}
|
|
2797
|
+
function getFenceContinuation(text) {
|
|
2798
|
+
let open;
|
|
2799
|
+
for (const line of text.split("\n")) {
|
|
2800
|
+
const trimmed = line.trimStart();
|
|
2801
|
+
const openerMatch = trimmed.match(/^(`{3,}|~{3,})(.*)$/);
|
|
2802
|
+
if (!openerMatch) {
|
|
2803
|
+
continue;
|
|
2804
|
+
}
|
|
2805
|
+
if (!open) {
|
|
2806
|
+
open = {
|
|
2807
|
+
fence: openerMatch[1],
|
|
2808
|
+
openerLine: trimmed
|
|
2809
|
+
};
|
|
2810
|
+
continue;
|
|
2811
|
+
}
|
|
2812
|
+
if (new RegExp(`^${open.fence}\\s*$`).test(trimmed)) {
|
|
2813
|
+
open = void 0;
|
|
2814
|
+
}
|
|
2815
|
+
}
|
|
2816
|
+
if (!open) {
|
|
2817
|
+
return null;
|
|
2818
|
+
}
|
|
2819
|
+
return {
|
|
2820
|
+
closeSuffix: text.endsWith("\n") ? open.fence : `
|
|
2821
|
+
${open.fence}`,
|
|
2822
|
+
reopenPrefix: `${open.openerLine}
|
|
2823
|
+
`
|
|
2824
|
+
};
|
|
2825
|
+
}
|
|
2826
|
+
function appendSlackSuffix(text, marker) {
|
|
2827
|
+
const carryover = getFenceContinuation(text);
|
|
2828
|
+
return `${text}${carryover?.closeSuffix ?? ""}${marker}`;
|
|
2829
|
+
}
|
|
2830
|
+
function takeSlackContinuationChunk(text, budget) {
|
|
2831
|
+
let { prefix, rest } = takeSlackInlinePrefix(text, budget);
|
|
2832
|
+
if (!rest) {
|
|
2833
|
+
({ prefix, rest } = takeSlackInlinePrefix(
|
|
2834
|
+
text,
|
|
2835
|
+
forceSplitBudget(text, budget)
|
|
2836
|
+
));
|
|
2837
|
+
}
|
|
2838
|
+
let carryover = rest ? getFenceContinuation(prefix) : null;
|
|
2839
|
+
if (!carryover) {
|
|
2840
|
+
return { prefix, rest };
|
|
2841
|
+
}
|
|
2842
|
+
const carryoverBudget = reserveInlineBudgetForSuffix(
|
|
2843
|
+
`${carryover.closeSuffix}${CONTINUED_MARKER}`
|
|
2844
|
+
);
|
|
2845
|
+
({ prefix, rest } = takeSlackInlinePrefix(text, carryoverBudget));
|
|
2846
|
+
if (!rest) {
|
|
2847
|
+
({ prefix, rest } = takeSlackInlinePrefix(
|
|
2848
|
+
text,
|
|
2849
|
+
forceSplitBudget(text, carryoverBudget)
|
|
2850
|
+
));
|
|
2851
|
+
}
|
|
2852
|
+
carryover = rest ? getFenceContinuation(prefix) : null;
|
|
2853
|
+
if (!carryover) {
|
|
2854
|
+
return { prefix, rest };
|
|
2855
|
+
}
|
|
2856
|
+
return {
|
|
2857
|
+
prefix,
|
|
2858
|
+
rest: `${carryover.reopenPrefix}${rest}`
|
|
2859
|
+
};
|
|
2860
|
+
}
|
|
2861
|
+
function takeSlackContinuationPrefix(text, options) {
|
|
2862
|
+
const budget = {
|
|
2863
|
+
maxChars: options?.maxChars ?? getSlackContinuationBudget().maxChars,
|
|
2864
|
+
maxLines: options?.maxLines ?? getSlackContinuationBudget().maxLines
|
|
2865
|
+
};
|
|
2866
|
+
const { prefix, rest } = (() => {
|
|
2867
|
+
if (options?.forceSplit) {
|
|
2868
|
+
return takeSlackContinuationChunk(text, budget);
|
|
2869
|
+
}
|
|
2870
|
+
const initial = takeSlackInlinePrefix(text, budget);
|
|
2871
|
+
return initial.rest ? takeSlackContinuationChunk(text, budget) : initial;
|
|
2872
|
+
})();
|
|
2873
|
+
return {
|
|
2874
|
+
prefix,
|
|
2875
|
+
renderedPrefix: rest ? appendSlackSuffix(prefix, CONTINUED_MARKER) : prefix,
|
|
2876
|
+
rest
|
|
2877
|
+
};
|
|
2878
|
+
}
|
|
2879
|
+
function takeSlackInlinePrefix(text, options) {
|
|
2880
|
+
const maxChars = options?.maxChars ?? MAX_INLINE_CHARS;
|
|
2881
|
+
const maxLines = options?.maxLines ?? MAX_INLINE_LINES;
|
|
2882
|
+
const normalized = text.replace(/\r\n?/g, "\n");
|
|
2883
|
+
if (!normalized) {
|
|
2884
|
+
return { prefix: "", rest: "" };
|
|
2885
|
+
}
|
|
2886
|
+
if (fitsInlineBudget(normalized, maxChars, maxLines)) {
|
|
2887
|
+
return { prefix: normalized, rest: "" };
|
|
2888
|
+
}
|
|
2889
|
+
const lineBounded = splitByLineBudget(normalized, maxLines);
|
|
2890
|
+
const cutIndex = findSplitIndex(lineBounded, maxChars);
|
|
2891
|
+
const prefix = lineBounded.slice(0, cutIndex).trimEnd();
|
|
2892
|
+
if (prefix) {
|
|
2893
|
+
return {
|
|
2894
|
+
prefix,
|
|
2895
|
+
rest: normalized.slice(prefix.length).trimStart()
|
|
2896
|
+
};
|
|
2897
|
+
}
|
|
2898
|
+
const hardPrefix = normalized.slice(0, Math.max(1, maxChars)).trimEnd();
|
|
2899
|
+
return {
|
|
2900
|
+
prefix: hardPrefix || normalized.slice(0, Math.max(1, maxChars)),
|
|
2901
|
+
rest: normalized.slice(hardPrefix.length || Math.max(1, maxChars)).trimStart()
|
|
2902
|
+
};
|
|
2903
|
+
}
|
|
2904
|
+
function splitSlackReplyText(text, options) {
|
|
2905
|
+
const normalized = normalizeForSlack(text);
|
|
2906
|
+
if (!normalized) {
|
|
2907
|
+
return [];
|
|
2908
|
+
}
|
|
2909
|
+
const chunks = [];
|
|
2910
|
+
const continuationBudget = reserveInlineBudgetForSuffix(CONTINUED_MARKER);
|
|
2911
|
+
let remaining = normalized;
|
|
2912
|
+
while (remaining) {
|
|
2913
|
+
const fitsFinalChunk = options?.interrupted ? fitsInlineBudget(appendSlackSuffix(remaining, INTERRUPTED_MARKER)) : fitsInlineBudget(remaining);
|
|
2914
|
+
if (fitsFinalChunk) {
|
|
2915
|
+
chunks.push(
|
|
2916
|
+
options?.interrupted ? appendSlackSuffix(remaining, INTERRUPTED_MARKER) : remaining
|
|
2917
|
+
);
|
|
2918
|
+
break;
|
|
2919
|
+
}
|
|
2920
|
+
const { renderedPrefix, rest } = takeSlackContinuationPrefix(remaining, {
|
|
2921
|
+
...continuationBudget,
|
|
2922
|
+
forceSplit: true
|
|
2923
|
+
});
|
|
2924
|
+
chunks.push(renderedPrefix);
|
|
2925
|
+
remaining = rest;
|
|
2926
|
+
}
|
|
2927
|
+
return chunks;
|
|
2928
|
+
}
|
|
2929
|
+
function getSlackInterruptionMarker() {
|
|
2930
|
+
return INTERRUPTED_MARKER;
|
|
2931
|
+
}
|
|
2932
|
+
function getSlackContinuationBudget() {
|
|
2933
|
+
return reserveInlineBudgetForSuffix(CONTINUED_MARKER);
|
|
2934
|
+
}
|
|
2935
|
+
function getSlackStreamingContinuationBudget() {
|
|
2936
|
+
return reserveInlineBudgetForSuffix(
|
|
2937
|
+
`${STREAMING_FENCE_CLOSE_GUARD}${CONTINUED_MARKER}`
|
|
2938
|
+
);
|
|
2939
|
+
}
|
|
2940
|
+
function buildSlackOutputMessage(text, files) {
|
|
2941
|
+
const normalized = normalizeForSlack(text);
|
|
2942
|
+
const fileCount = files?.length ?? 0;
|
|
2943
|
+
if (!normalized) {
|
|
2944
|
+
if (fileCount > 0) {
|
|
2945
|
+
return {
|
|
2946
|
+
raw: "",
|
|
2947
|
+
files
|
|
2948
|
+
};
|
|
2949
|
+
}
|
|
2950
|
+
logWarn(
|
|
2951
|
+
"slack_output_normalized_empty",
|
|
2952
|
+
{},
|
|
2953
|
+
{
|
|
2954
|
+
"app.output.original_length": text.length,
|
|
2955
|
+
"app.output.parsed_length": normalized.length,
|
|
2956
|
+
"app.output.file_count": fileCount
|
|
2957
|
+
},
|
|
2958
|
+
"Slack output normalized to empty content"
|
|
2959
|
+
);
|
|
2960
|
+
return {
|
|
2961
|
+
markdown: "I couldn't produce a response.",
|
|
2962
|
+
files
|
|
2963
|
+
};
|
|
2964
|
+
}
|
|
2965
|
+
return {
|
|
2966
|
+
markdown: normalized,
|
|
2967
|
+
files
|
|
2968
|
+
};
|
|
2969
|
+
}
|
|
2970
|
+
var slackOutputPolicy = {
|
|
2971
|
+
maxInlineChars: MAX_INLINE_CHARS,
|
|
2972
|
+
maxInlineLines: MAX_INLINE_LINES
|
|
2973
|
+
};
|
|
2974
|
+
|
|
2975
|
+
// src/chat/prompt.ts
|
|
2802
2976
|
var DEFAULT_SOUL = "You are Junior, a practical and concise assistant.";
|
|
2803
2977
|
function getLoggedMarkdownFiles() {
|
|
2804
2978
|
const globalState = globalThis;
|
|
@@ -3169,6 +3343,9 @@ function buildSystemPrompt(params) {
|
|
|
3169
3343
|
"- Keep routine setup and research steps silent in user-facing replies. Do not narrate duplicate checks, credential issuance, file writes, or similar internal progress unless the result is user-relevant.",
|
|
3170
3344
|
"- If a routine prerequisite check finds nothing notable, omit it entirely from the final reply and report only the user-relevant outcome.",
|
|
3171
3345
|
"- Prefer a single result-focused reply after tool work completes. Only send an interim reply when you need user input or have a concrete blocking problem to report.",
|
|
3346
|
+
"- For external/factual research requests that require tools, do not send any preliminary conclusion, 'let me check', or progress narration before the evidence-gathering work is done. Use assistant status for in-progress work and make the first visible reply the researched answer.",
|
|
3347
|
+
"- For evidence-gathering tasks, never state a factual conclusion before you have actually gathered and checked the sources.",
|
|
3348
|
+
"- Do not include internal process chatter such as 'let me find', 'fetching now', 'good, I have sources', 'trying smaller limits', or 'I now have sufficient context' in the final user-facing reply.",
|
|
3172
3349
|
"- Use `attachFile` for files that actually exist in the sandbox (for example screenshots, PDFs, logs), or for `attachment_path` values returned by `imageGenerate`.",
|
|
3173
3350
|
"- If the user asks to see/share/show a screenshot or file, attach the file with `attachFile` instead of only reporting its path.",
|
|
3174
3351
|
"- Never claim a screenshot/file is attached unless `attachFile` succeeded in this turn.",
|
|
@@ -3227,7 +3404,8 @@ function buildSystemPrompt(params) {
|
|
|
3227
3404
|
"- Use plain Slack-safe markdown (headings, bullets, short code blocks).",
|
|
3228
3405
|
"- Keep normal responses brief and scannable.",
|
|
3229
3406
|
"- If depth is needed, start with a concise summary and then provide fuller detail.",
|
|
3230
|
-
"-
|
|
3407
|
+
"- For tool-heavy research, discovery, or source-checking requests, do not send an initial acknowledgment. Start the visible reply only once you can present the actual answer.",
|
|
3408
|
+
"- Do not narrate tool execution or repeated status updates in the visible reply.",
|
|
3231
3409
|
"- Avoid tables unless explicitly requested.",
|
|
3232
3410
|
"- End every turn with a final user-facing markdown response.",
|
|
3233
3411
|
"</output>"
|
|
@@ -8847,13 +9025,13 @@ function buildToolStatus(toolName, input) {
|
|
|
8847
9025
|
return makeAssistantStatus("loading", skillName);
|
|
8848
9026
|
}
|
|
8849
9027
|
if (query && toolName === "webSearch") {
|
|
8850
|
-
return makeAssistantStatus("searching",
|
|
8851
|
-
}
|
|
8852
|
-
if (query && provider && toolName === "searchTools") {
|
|
8853
|
-
return makeAssistantStatus("searching", `${provider} "${query}"`);
|
|
9028
|
+
return makeAssistantStatus("searching", "sources");
|
|
8854
9029
|
}
|
|
8855
9030
|
if (query && toolName === "searchTools") {
|
|
8856
|
-
return makeAssistantStatus(
|
|
9031
|
+
return makeAssistantStatus(
|
|
9032
|
+
"searching",
|
|
9033
|
+
provider ? `${provider} tools` : "tools"
|
|
9034
|
+
);
|
|
8857
9035
|
}
|
|
8858
9036
|
if (domain && toolName === "webFetch") {
|
|
8859
9037
|
return makeAssistantStatus("fetching", domain);
|
|
@@ -9157,13 +9335,34 @@ function buildReplyDeliveryPlan(args) {
|
|
|
9157
9335
|
attachFiles
|
|
9158
9336
|
};
|
|
9159
9337
|
}
|
|
9160
|
-
|
|
9161
|
-
|
|
9162
|
-
|
|
9163
|
-
|
|
9164
|
-
|
|
9165
|
-
|
|
9166
|
-
|
|
9338
|
+
function resolveReplyDelivery(args) {
|
|
9339
|
+
const replyHasFiles = Boolean(
|
|
9340
|
+
args.reply.files && args.reply.files.length > 0
|
|
9341
|
+
);
|
|
9342
|
+
const deliveryPlan = args.reply.deliveryPlan ?? {
|
|
9343
|
+
mode: args.reply.deliveryMode ?? "thread",
|
|
9344
|
+
postThreadText: (args.reply.deliveryMode ?? "thread") !== "channel_only",
|
|
9345
|
+
attachFiles: replyHasFiles ? args.hasStreamedThreadReply ? "followup" : "inline" : "none"
|
|
9346
|
+
};
|
|
9347
|
+
let attachFiles = replyHasFiles ? deliveryPlan.attachFiles : "none";
|
|
9348
|
+
if (attachFiles === "followup" && !args.hasStreamedThreadReply) {
|
|
9349
|
+
attachFiles = "inline";
|
|
9350
|
+
}
|
|
9351
|
+
if (attachFiles === "inline" && args.hasStreamedThreadReply) {
|
|
9352
|
+
attachFiles = "followup";
|
|
9353
|
+
}
|
|
9354
|
+
return {
|
|
9355
|
+
shouldPostThreadReply: deliveryPlan.postThreadText,
|
|
9356
|
+
attachFiles
|
|
9357
|
+
};
|
|
9358
|
+
}
|
|
9359
|
+
|
|
9360
|
+
// src/chat/services/channel-intent.ts
|
|
9361
|
+
function isExplicitChannelPostIntent(text) {
|
|
9362
|
+
if (!/\bchannel\b/i.test(text)) {
|
|
9363
|
+
return false;
|
|
9364
|
+
}
|
|
9365
|
+
const directChannelVerb = /\b(show|post|send|share|say|announce|broadcast)\b[\s\S]{0,80}\b(?:the\s+)?channel\b/i;
|
|
9167
9366
|
if (directChannelVerb.test(text)) {
|
|
9168
9367
|
return true;
|
|
9169
9368
|
}
|
|
@@ -9954,6 +10153,17 @@ async function generateAssistantReply(messageText, context = {}) {
|
|
|
9954
10153
|
const agentToolHooks = {
|
|
9955
10154
|
onToolCall: (toolName) => {
|
|
9956
10155
|
toolCalls.push(toolName);
|
|
10156
|
+
Promise.resolve(context.onToolCall?.(toolName)).catch((error) => {
|
|
10157
|
+
logWarn(
|
|
10158
|
+
"streaming_tool_call_error",
|
|
10159
|
+
{},
|
|
10160
|
+
{
|
|
10161
|
+
"error.message": error instanceof Error ? error.message : String(error),
|
|
10162
|
+
"gen_ai.tool.name": toolName
|
|
10163
|
+
},
|
|
10164
|
+
"Failed to deliver tool call event to stream coordinator"
|
|
10165
|
+
);
|
|
10166
|
+
});
|
|
9957
10167
|
}
|
|
9958
10168
|
};
|
|
9959
10169
|
const baseAgentTools = createAgentTools(
|
|
@@ -9994,6 +10204,16 @@ async function generateAssistantReply(messageText, context = {}) {
|
|
|
9994
10204
|
let needsSeparator = false;
|
|
9995
10205
|
const unsubscribe = agent.subscribe((event) => {
|
|
9996
10206
|
if (event.type === "message_start") {
|
|
10207
|
+
Promise.resolve(context.onAssistantMessageStart?.()).catch((error) => {
|
|
10208
|
+
logWarn(
|
|
10209
|
+
"streaming_message_start_error",
|
|
10210
|
+
{},
|
|
10211
|
+
{
|
|
10212
|
+
"error.message": error instanceof Error ? error.message : String(error)
|
|
10213
|
+
},
|
|
10214
|
+
"Failed to deliver assistant message start to stream coordinator"
|
|
10215
|
+
);
|
|
10216
|
+
});
|
|
9997
10217
|
if (hasEmittedText) {
|
|
9998
10218
|
needsSeparator = true;
|
|
9999
10219
|
}
|
|
@@ -10385,7 +10605,194 @@ function createProgressReporter(args) {
|
|
|
10385
10605
|
};
|
|
10386
10606
|
}
|
|
10387
10607
|
|
|
10388
|
-
// src/
|
|
10608
|
+
// src/chat/slack/reply.ts
|
|
10609
|
+
import { Buffer as Buffer2 } from "buffer";
|
|
10610
|
+
function isInterruptedVisibleReply(reply) {
|
|
10611
|
+
return reply.diagnostics.outcome === "provider_error";
|
|
10612
|
+
}
|
|
10613
|
+
function buildChunkMessage(chunk, files) {
|
|
10614
|
+
return {
|
|
10615
|
+
markdown: chunk,
|
|
10616
|
+
...files ? { files } : {}
|
|
10617
|
+
};
|
|
10618
|
+
}
|
|
10619
|
+
function buildTextPosts(args) {
|
|
10620
|
+
const chunks = splitSlackReplyText(args.text, {
|
|
10621
|
+
interrupted: args.interrupted
|
|
10622
|
+
});
|
|
10623
|
+
return chunks.map((chunk, index) => ({
|
|
10624
|
+
message: buildChunkMessage(
|
|
10625
|
+
chunk,
|
|
10626
|
+
index === 0 ? args.firstFiles : void 0
|
|
10627
|
+
),
|
|
10628
|
+
stage: index === 0 ? args.firstStage ?? "thread_reply" : "thread_reply_continuation"
|
|
10629
|
+
}));
|
|
10630
|
+
}
|
|
10631
|
+
async function normalizeFileUploads(files) {
|
|
10632
|
+
return await Promise.all(
|
|
10633
|
+
files.map(async (file) => {
|
|
10634
|
+
let data;
|
|
10635
|
+
if (Buffer2.isBuffer(file.data)) {
|
|
10636
|
+
data = file.data;
|
|
10637
|
+
} else if (file.data instanceof ArrayBuffer) {
|
|
10638
|
+
data = Buffer2.from(file.data);
|
|
10639
|
+
} else {
|
|
10640
|
+
data = Buffer2.from(await file.data.arrayBuffer());
|
|
10641
|
+
}
|
|
10642
|
+
return {
|
|
10643
|
+
data,
|
|
10644
|
+
filename: file.filename
|
|
10645
|
+
};
|
|
10646
|
+
})
|
|
10647
|
+
);
|
|
10648
|
+
}
|
|
10649
|
+
async function uploadReplyFilesBestEffort(args) {
|
|
10650
|
+
try {
|
|
10651
|
+
await uploadFilesToThread({
|
|
10652
|
+
channelId: args.channelId,
|
|
10653
|
+
threadTs: args.threadTs,
|
|
10654
|
+
files: await normalizeFileUploads(args.files)
|
|
10655
|
+
});
|
|
10656
|
+
} catch {
|
|
10657
|
+
}
|
|
10658
|
+
}
|
|
10659
|
+
function getReplyMessageText(message) {
|
|
10660
|
+
if (typeof message !== "object" || message === null) {
|
|
10661
|
+
return void 0;
|
|
10662
|
+
}
|
|
10663
|
+
if ("markdown" in message && typeof message.markdown === "string") {
|
|
10664
|
+
return message.markdown;
|
|
10665
|
+
}
|
|
10666
|
+
if ("raw" in message && typeof message.raw === "string") {
|
|
10667
|
+
return message.raw;
|
|
10668
|
+
}
|
|
10669
|
+
return void 0;
|
|
10670
|
+
}
|
|
10671
|
+
function getReplyMessageFiles(message) {
|
|
10672
|
+
if (typeof message === "object" && message !== null && "files" in message && Array.isArray(message.files)) {
|
|
10673
|
+
return message.files;
|
|
10674
|
+
}
|
|
10675
|
+
return void 0;
|
|
10676
|
+
}
|
|
10677
|
+
function createSlackStreamAccumulator() {
|
|
10678
|
+
let pendingCarriageReturn = false;
|
|
10679
|
+
let streamedVisibleText = "";
|
|
10680
|
+
let streamedRenderedText = "";
|
|
10681
|
+
let overflowText = "";
|
|
10682
|
+
let streamOverflowed = false;
|
|
10683
|
+
const continuationBudget = getSlackStreamingContinuationBudget();
|
|
10684
|
+
const normalizeDelta = (deltaText) => {
|
|
10685
|
+
let text = deltaText;
|
|
10686
|
+
if (pendingCarriageReturn) {
|
|
10687
|
+
text = `\r${text}`;
|
|
10688
|
+
pendingCarriageReturn = false;
|
|
10689
|
+
}
|
|
10690
|
+
if (text.endsWith("\r")) {
|
|
10691
|
+
text = text.slice(0, -1);
|
|
10692
|
+
pendingCarriageReturn = true;
|
|
10693
|
+
}
|
|
10694
|
+
return text.replace(/\r\n?/g, "\n");
|
|
10695
|
+
};
|
|
10696
|
+
return {
|
|
10697
|
+
append(deltaText) {
|
|
10698
|
+
const normalizedDeltaText = normalizeDelta(deltaText);
|
|
10699
|
+
if (!normalizedDeltaText) {
|
|
10700
|
+
return "";
|
|
10701
|
+
}
|
|
10702
|
+
if (streamOverflowed) {
|
|
10703
|
+
overflowText += normalizedDeltaText;
|
|
10704
|
+
return "";
|
|
10705
|
+
}
|
|
10706
|
+
const candidate = `${streamedVisibleText}${normalizedDeltaText}`;
|
|
10707
|
+
const { prefix, renderedPrefix, rest } = takeSlackContinuationPrefix(
|
|
10708
|
+
candidate,
|
|
10709
|
+
continuationBudget
|
|
10710
|
+
);
|
|
10711
|
+
const additional = renderedPrefix.length > streamedRenderedText.length ? renderedPrefix.slice(streamedRenderedText.length) : "";
|
|
10712
|
+
streamedVisibleText = prefix;
|
|
10713
|
+
streamedRenderedText = renderedPrefix;
|
|
10714
|
+
if (rest) {
|
|
10715
|
+
overflowText += rest;
|
|
10716
|
+
streamOverflowed = true;
|
|
10717
|
+
}
|
|
10718
|
+
return additional;
|
|
10719
|
+
},
|
|
10720
|
+
getOverflowText() {
|
|
10721
|
+
return overflowText;
|
|
10722
|
+
}
|
|
10723
|
+
};
|
|
10724
|
+
}
|
|
10725
|
+
function planSlackReplyPosts(args) {
|
|
10726
|
+
const replyFiles = args.reply.files && args.reply.files.length > 0 ? args.reply.files : void 0;
|
|
10727
|
+
const { shouldPostThreadReply, attachFiles } = resolveReplyDelivery({
|
|
10728
|
+
reply: args.reply,
|
|
10729
|
+
hasStreamedThreadReply: args.hasStreamedThreadReply
|
|
10730
|
+
});
|
|
10731
|
+
const interrupted = isInterruptedVisibleReply(args.reply);
|
|
10732
|
+
const posts = [];
|
|
10733
|
+
if (args.hasStreamedThreadReply) {
|
|
10734
|
+
if (shouldPostThreadReply && args.streamedOverflowText) {
|
|
10735
|
+
posts.push(
|
|
10736
|
+
...buildTextPosts({
|
|
10737
|
+
text: args.streamedOverflowText,
|
|
10738
|
+
interrupted,
|
|
10739
|
+
firstStage: "thread_reply_continuation"
|
|
10740
|
+
})
|
|
10741
|
+
);
|
|
10742
|
+
} else if (shouldPostThreadReply && interrupted) {
|
|
10743
|
+
posts.push({
|
|
10744
|
+
message: buildSlackOutputMessage(
|
|
10745
|
+
getSlackInterruptionMarker().trimStart()
|
|
10746
|
+
),
|
|
10747
|
+
stage: "thread_reply_continuation"
|
|
10748
|
+
});
|
|
10749
|
+
}
|
|
10750
|
+
} else {
|
|
10751
|
+
const textPosts = shouldPostThreadReply ? buildTextPosts({
|
|
10752
|
+
text: args.reply.text,
|
|
10753
|
+
interrupted,
|
|
10754
|
+
firstFiles: attachFiles === "inline" ? replyFiles : void 0
|
|
10755
|
+
}) : [];
|
|
10756
|
+
posts.push(...textPosts);
|
|
10757
|
+
if (attachFiles === "inline" && replyFiles && textPosts.length === 0) {
|
|
10758
|
+
posts.push({
|
|
10759
|
+
message: buildSlackOutputMessage("", replyFiles),
|
|
10760
|
+
stage: "thread_reply"
|
|
10761
|
+
});
|
|
10762
|
+
} else if (shouldPostThreadReply && textPosts.length === 0) {
|
|
10763
|
+
posts.push({
|
|
10764
|
+
message: buildSlackOutputMessage(args.reply.text),
|
|
10765
|
+
stage: "thread_reply"
|
|
10766
|
+
});
|
|
10767
|
+
}
|
|
10768
|
+
}
|
|
10769
|
+
if (attachFiles === "followup" && replyFiles) {
|
|
10770
|
+
posts.push({
|
|
10771
|
+
message: buildSlackOutputMessage("", replyFiles),
|
|
10772
|
+
stage: "thread_reply_files_followup"
|
|
10773
|
+
});
|
|
10774
|
+
}
|
|
10775
|
+
return posts;
|
|
10776
|
+
}
|
|
10777
|
+
async function postSlackApiReplyPosts(args) {
|
|
10778
|
+
for (const post of args.posts) {
|
|
10779
|
+
const text = getReplyMessageText(post.message);
|
|
10780
|
+
if (text && text.trim().length > 0) {
|
|
10781
|
+
await args.postMessage(args.channelId, args.threadTs, text);
|
|
10782
|
+
}
|
|
10783
|
+
const files = getReplyMessageFiles(post.message);
|
|
10784
|
+
if (!files?.length) {
|
|
10785
|
+
continue;
|
|
10786
|
+
}
|
|
10787
|
+
await uploadReplyFilesBestEffort({
|
|
10788
|
+
channelId: args.channelId,
|
|
10789
|
+
threadTs: args.threadTs,
|
|
10790
|
+
files
|
|
10791
|
+
});
|
|
10792
|
+
}
|
|
10793
|
+
}
|
|
10794
|
+
|
|
10795
|
+
// src/chat/slack/resume.ts
|
|
10389
10796
|
function resolveReplyTimeoutMs(explicitTimeoutMs) {
|
|
10390
10797
|
if (typeof explicitTimeoutMs === "number" && explicitTimeoutMs > 0) {
|
|
10391
10798
|
return explicitTimeoutMs;
|
|
@@ -10531,11 +10938,15 @@ async function resumeSlackTurn(args) {
|
|
|
10531
10938
|
)
|
|
10532
10939
|
]) : await replyPromise;
|
|
10533
10940
|
await progress.stop();
|
|
10534
|
-
|
|
10535
|
-
|
|
10536
|
-
|
|
10537
|
-
|
|
10538
|
-
|
|
10941
|
+
await postSlackApiReplyPosts({
|
|
10942
|
+
channelId: args.channelId,
|
|
10943
|
+
threadTs: args.threadTs,
|
|
10944
|
+
posts: planSlackReplyPosts({
|
|
10945
|
+
reply,
|
|
10946
|
+
hasStreamedThreadReply: false
|
|
10947
|
+
}),
|
|
10948
|
+
postMessage: postSlackMessage
|
|
10949
|
+
});
|
|
10539
10950
|
await args.onSuccess?.(reply);
|
|
10540
10951
|
} catch (error) {
|
|
10541
10952
|
await progress.stop();
|
|
@@ -10592,7 +11003,6 @@ async function resumeAuthorizedRequest(args) {
|
|
|
10592
11003
|
initialText: args.connectedText,
|
|
10593
11004
|
failureText: args.failureText,
|
|
10594
11005
|
generateReply: args.generateReply,
|
|
10595
|
-
onReply: args.onReply,
|
|
10596
11006
|
onSuccess: args.onSuccess,
|
|
10597
11007
|
onFailure: args.onFailure,
|
|
10598
11008
|
onAuthPause: args.onAuthPause,
|
|
@@ -10756,65 +11166,6 @@ function htmlResponse(kind) {
|
|
|
10756
11166
|
const page = CALLBACK_PAGES[kind];
|
|
10757
11167
|
return htmlCallbackResponse(page.title, page.message, page.status);
|
|
10758
11168
|
}
|
|
10759
|
-
function extractSlackText(text, files) {
|
|
10760
|
-
const message = buildSlackOutputMessage(text, files);
|
|
10761
|
-
if (typeof message === "object" && message !== null && "markdown" in message && typeof message.markdown === "string") {
|
|
10762
|
-
return message.markdown;
|
|
10763
|
-
}
|
|
10764
|
-
if (typeof message === "object" && message !== null && "raw" in message && typeof message.raw === "string") {
|
|
10765
|
-
return message.raw;
|
|
10766
|
-
}
|
|
10767
|
-
return text;
|
|
10768
|
-
}
|
|
10769
|
-
async function normalizeFileUploads(files) {
|
|
10770
|
-
const normalized = [];
|
|
10771
|
-
for (const file of files) {
|
|
10772
|
-
let data;
|
|
10773
|
-
if (Buffer2.isBuffer(file.data)) {
|
|
10774
|
-
data = file.data;
|
|
10775
|
-
} else if (file.data instanceof ArrayBuffer) {
|
|
10776
|
-
data = Buffer2.from(file.data);
|
|
10777
|
-
} else {
|
|
10778
|
-
data = Buffer2.from(await file.data.arrayBuffer());
|
|
10779
|
-
}
|
|
10780
|
-
normalized.push({
|
|
10781
|
-
data,
|
|
10782
|
-
filename: file.filename
|
|
10783
|
-
});
|
|
10784
|
-
}
|
|
10785
|
-
return normalized;
|
|
10786
|
-
}
|
|
10787
|
-
async function deliverReplyToThread(channelId, threadTs, reply) {
|
|
10788
|
-
const replyFiles = reply.files && reply.files.length > 0 ? reply.files : void 0;
|
|
10789
|
-
const { shouldPostThreadReply, attachFiles } = resolveReplyDelivery({
|
|
10790
|
-
reply,
|
|
10791
|
-
hasStreamedThreadReply: false
|
|
10792
|
-
});
|
|
10793
|
-
if (shouldPostThreadReply) {
|
|
10794
|
-
const text = extractSlackText(
|
|
10795
|
-
reply.text,
|
|
10796
|
-
attachFiles === "inline" ? replyFiles : void 0
|
|
10797
|
-
);
|
|
10798
|
-
if (text.trim().length > 0) {
|
|
10799
|
-
await postSlackMessage(channelId, threadTs, text);
|
|
10800
|
-
}
|
|
10801
|
-
}
|
|
10802
|
-
if (!replyFiles || attachFiles === "none") {
|
|
10803
|
-
return;
|
|
10804
|
-
}
|
|
10805
|
-
const files = await normalizeFileUploads(replyFiles);
|
|
10806
|
-
if (files.length === 0) {
|
|
10807
|
-
return;
|
|
10808
|
-
}
|
|
10809
|
-
try {
|
|
10810
|
-
await uploadFilesToThread({
|
|
10811
|
-
channelId,
|
|
10812
|
-
threadTs,
|
|
10813
|
-
files
|
|
10814
|
-
});
|
|
10815
|
-
} catch {
|
|
10816
|
-
}
|
|
10817
|
-
}
|
|
10818
11169
|
async function buildResumeConversationContext(channelId, threadTs, sessionId) {
|
|
10819
11170
|
const threadId = `slack:${channelId}:${threadTs}`;
|
|
10820
11171
|
const conversation = coerceThreadConversationState(
|
|
@@ -10899,7 +11250,6 @@ async function resumeAuthorizedMcpTurn(args) {
|
|
|
10899
11250
|
);
|
|
10900
11251
|
await resumeAuthorizedRequest({
|
|
10901
11252
|
messageText: authSession.userMessage,
|
|
10902
|
-
provider,
|
|
10903
11253
|
channelId: authSession.channelId,
|
|
10904
11254
|
threadTs: authSession.threadTs,
|
|
10905
11255
|
lockKey: authSession.conversationId,
|
|
@@ -10927,13 +11277,6 @@ async function resumeAuthorizedMcpTurn(args) {
|
|
|
10927
11277
|
sandbox: getPersistedSandboxState(currentState),
|
|
10928
11278
|
threadParticipants: buildThreadParticipants(conversation.messages)
|
|
10929
11279
|
},
|
|
10930
|
-
onReply: async (reply) => {
|
|
10931
|
-
await deliverReplyToThread(
|
|
10932
|
-
authSession.channelId,
|
|
10933
|
-
authSession.threadTs,
|
|
10934
|
-
reply
|
|
10935
|
-
);
|
|
10936
|
-
},
|
|
10937
11280
|
onSuccess: async (reply) => {
|
|
10938
11281
|
try {
|
|
10939
11282
|
await persistCompletedReplyState(
|
|
@@ -11230,7 +11573,6 @@ async function resumePendingOAuthMessage(stored) {
|
|
|
11230
11573
|
);
|
|
11231
11574
|
await resumeAuthorizedRequest({
|
|
11232
11575
|
messageText: stored.pendingMessage,
|
|
11233
|
-
provider: stored.provider,
|
|
11234
11576
|
channelId: stored.channelId,
|
|
11235
11577
|
threadTs: stored.threadTs,
|
|
11236
11578
|
connectedText: `Your ${providerLabel} account is now connected. Processing your request...`,
|
|
@@ -11428,9 +11770,6 @@ async function GET5(request, provider, waitUntil) {
|
|
|
11428
11770
|
});
|
|
11429
11771
|
}
|
|
11430
11772
|
|
|
11431
|
-
// src/handlers/turn-resume.ts
|
|
11432
|
-
import { Buffer as Buffer3 } from "buffer";
|
|
11433
|
-
|
|
11434
11773
|
// src/chat/slack/context.ts
|
|
11435
11774
|
function toTrimmedSlackString(value) {
|
|
11436
11775
|
const normalized = toOptionalString(value);
|
|
@@ -11478,65 +11817,6 @@ function resolveSlackChannelIdFromMessage(message) {
|
|
|
11478
11817
|
}
|
|
11479
11818
|
|
|
11480
11819
|
// src/handlers/turn-resume.ts
|
|
11481
|
-
function extractSlackText2(text, files) {
|
|
11482
|
-
const message = buildSlackOutputMessage(text, files);
|
|
11483
|
-
if (typeof message === "object" && message !== null && "markdown" in message && typeof message.markdown === "string") {
|
|
11484
|
-
return message.markdown;
|
|
11485
|
-
}
|
|
11486
|
-
if (typeof message === "object" && message !== null && "raw" in message && typeof message.raw === "string") {
|
|
11487
|
-
return message.raw;
|
|
11488
|
-
}
|
|
11489
|
-
return text;
|
|
11490
|
-
}
|
|
11491
|
-
async function normalizeFileUploads2(files) {
|
|
11492
|
-
const normalized = [];
|
|
11493
|
-
for (const file of files) {
|
|
11494
|
-
let data;
|
|
11495
|
-
if (Buffer3.isBuffer(file.data)) {
|
|
11496
|
-
data = file.data;
|
|
11497
|
-
} else if (file.data instanceof ArrayBuffer) {
|
|
11498
|
-
data = Buffer3.from(file.data);
|
|
11499
|
-
} else {
|
|
11500
|
-
data = Buffer3.from(await file.data.arrayBuffer());
|
|
11501
|
-
}
|
|
11502
|
-
normalized.push({
|
|
11503
|
-
data,
|
|
11504
|
-
filename: file.filename
|
|
11505
|
-
});
|
|
11506
|
-
}
|
|
11507
|
-
return normalized;
|
|
11508
|
-
}
|
|
11509
|
-
async function deliverReplyToThread2(args) {
|
|
11510
|
-
const replyFiles = args.reply.files && args.reply.files.length > 0 ? args.reply.files : void 0;
|
|
11511
|
-
const { shouldPostThreadReply, attachFiles } = resolveReplyDelivery({
|
|
11512
|
-
reply: args.reply,
|
|
11513
|
-
hasStreamedThreadReply: false
|
|
11514
|
-
});
|
|
11515
|
-
if (shouldPostThreadReply) {
|
|
11516
|
-
const text = extractSlackText2(
|
|
11517
|
-
args.reply.text,
|
|
11518
|
-
attachFiles === "inline" ? replyFiles : void 0
|
|
11519
|
-
);
|
|
11520
|
-
if (text.trim().length > 0) {
|
|
11521
|
-
await postSlackMessage(args.channelId, args.threadTs, text);
|
|
11522
|
-
}
|
|
11523
|
-
}
|
|
11524
|
-
if (!replyFiles || attachFiles === "none") {
|
|
11525
|
-
return;
|
|
11526
|
-
}
|
|
11527
|
-
const files = await normalizeFileUploads2(replyFiles);
|
|
11528
|
-
if (files.length === 0) {
|
|
11529
|
-
return;
|
|
11530
|
-
}
|
|
11531
|
-
try {
|
|
11532
|
-
await uploadFilesToThread({
|
|
11533
|
-
channelId: args.channelId,
|
|
11534
|
-
threadTs: args.threadTs,
|
|
11535
|
-
files
|
|
11536
|
-
});
|
|
11537
|
-
} catch {
|
|
11538
|
-
}
|
|
11539
|
-
}
|
|
11540
11820
|
async function persistCompletedReplyState2(args) {
|
|
11541
11821
|
const currentState = await getPersistedThreadState(
|
|
11542
11822
|
args.checkpoint.conversationId
|
|
@@ -11651,13 +11931,6 @@ async function resumeTimedOutTurn(payload) {
|
|
|
11651
11931
|
sandbox,
|
|
11652
11932
|
threadParticipants: buildThreadParticipants(conversation.messages)
|
|
11653
11933
|
},
|
|
11654
|
-
onReply: async (reply) => {
|
|
11655
|
-
await deliverReplyToThread2({
|
|
11656
|
-
channelId: thread.channelId,
|
|
11657
|
-
threadTs: thread.threadTs,
|
|
11658
|
-
reply
|
|
11659
|
-
});
|
|
11660
|
-
},
|
|
11661
11934
|
onSuccess: async (reply) => {
|
|
11662
11935
|
try {
|
|
11663
11936
|
await persistCompletedReplyState2({ checkpoint, reply });
|
|
@@ -11766,9 +12039,6 @@ async function POST(request, waitUntil) {
|
|
|
11766
12039
|
return new Response("Accepted", { status: 202 });
|
|
11767
12040
|
}
|
|
11768
12041
|
|
|
11769
|
-
// src/chat/app/production.ts
|
|
11770
|
-
import { createSlackAdapter } from "@chat-adapter/slack";
|
|
11771
|
-
|
|
11772
12042
|
// src/chat/services/subscribed-decision.ts
|
|
11773
12043
|
import { z } from "zod";
|
|
11774
12044
|
var replyDecisionSchema = z.object({
|
|
@@ -12615,6 +12885,15 @@ var MAX_USER_ATTACHMENTS = 3;
|
|
|
12615
12885
|
var MAX_USER_ATTACHMENT_BYTES = 5 * 1024 * 1024;
|
|
12616
12886
|
var MAX_MESSAGE_IMAGE_ATTACHMENTS = 3;
|
|
12617
12887
|
var MAX_VISION_SUMMARY_CHARS = 500;
|
|
12888
|
+
function hasPotentialImageAttachment(attachments) {
|
|
12889
|
+
return attachments?.some((attachment) => {
|
|
12890
|
+
if (attachment.type === "image") {
|
|
12891
|
+
return true;
|
|
12892
|
+
}
|
|
12893
|
+
const mimeType = attachment.mimeType ?? "";
|
|
12894
|
+
return attachment.type === "file" && mimeType.startsWith("image/");
|
|
12895
|
+
}) ?? false;
|
|
12896
|
+
}
|
|
12618
12897
|
function isVisionEnabled() {
|
|
12619
12898
|
return Boolean(botConfig.visionModelId);
|
|
12620
12899
|
}
|
|
@@ -12957,6 +13236,7 @@ async function hydrateConversationVisionContextWithDeps(conversation, context, d
|
|
|
12957
13236
|
continue;
|
|
12958
13237
|
}
|
|
12959
13238
|
hydratedMessageIds.add(conversationMessage.id);
|
|
13239
|
+
const existingMeta = conversationMessage.meta ?? {};
|
|
12960
13240
|
const imageFiles = (reply.files ?? []).filter((file) => {
|
|
12961
13241
|
const mimeType = toOptionalString(file.mimetype);
|
|
12962
13242
|
return Boolean(
|
|
@@ -12964,10 +13244,15 @@ async function hydrateConversationVisionContextWithDeps(conversation, context, d
|
|
|
12964
13244
|
);
|
|
12965
13245
|
}).slice(0, MAX_MESSAGE_IMAGE_ATTACHMENTS);
|
|
12966
13246
|
if (imageFiles.length === 0) {
|
|
13247
|
+
conversationMessage.meta = {
|
|
13248
|
+
...existingMeta,
|
|
13249
|
+
slackTs: existingMeta.slackTs ?? ts,
|
|
13250
|
+
imagesHydrated: true
|
|
13251
|
+
};
|
|
13252
|
+
mutated = true;
|
|
12967
13253
|
continue;
|
|
12968
13254
|
}
|
|
12969
13255
|
const imageFileIds = imageFiles.map((file) => toOptionalString(file.id)).filter((fileId) => Boolean(fileId));
|
|
12970
|
-
const existingMeta = conversationMessage.meta ?? {};
|
|
12971
13256
|
conversationMessage.meta = {
|
|
12972
13257
|
...existingMeta,
|
|
12973
13258
|
slackTs: existingMeta.slackTs ?? ts,
|
|
@@ -13196,6 +13481,14 @@ function getExecutionFailureReason(reply) {
|
|
|
13196
13481
|
}
|
|
13197
13482
|
return "empty assistant turn";
|
|
13198
13483
|
}
|
|
13484
|
+
function shouldAutoStartStreaming(args) {
|
|
13485
|
+
const { text, deltaCount } = args;
|
|
13486
|
+
const trimmed = text.trim();
|
|
13487
|
+
if (!trimmed || isPotentialRedundantReactionAckText(trimmed)) {
|
|
13488
|
+
return false;
|
|
13489
|
+
}
|
|
13490
|
+
return deltaCount >= 2;
|
|
13491
|
+
}
|
|
13199
13492
|
function createReplyToThread(deps) {
|
|
13200
13493
|
return async function replyToThread(thread, message, options = {}) {
|
|
13201
13494
|
if (message.author.isMe) {
|
|
@@ -13304,7 +13597,10 @@ function createReplyToThread(deps) {
|
|
|
13304
13597
|
const textStream = createTextStreamBridge();
|
|
13305
13598
|
let streamedReplyPromise;
|
|
13306
13599
|
let pendingStreamText = "";
|
|
13600
|
+
let pendingStreamDeltaCount = 0;
|
|
13601
|
+
let awaitingPostToolAssistantMessage = false;
|
|
13307
13602
|
let beforeFirstResponsePostCalled = false;
|
|
13603
|
+
let streamedReplyState = createSlackStreamAccumulator();
|
|
13308
13604
|
const beforeFirstResponsePost = async () => {
|
|
13309
13605
|
if (beforeFirstResponsePostCalled) {
|
|
13310
13606
|
return;
|
|
@@ -13330,6 +13626,51 @@ function createReplyToThread(deps) {
|
|
|
13330
13626
|
startStreamingReply();
|
|
13331
13627
|
textStream.push(pendingStreamText);
|
|
13332
13628
|
pendingStreamText = "";
|
|
13629
|
+
pendingStreamDeltaCount = 0;
|
|
13630
|
+
};
|
|
13631
|
+
const clearPendingStreamText = () => {
|
|
13632
|
+
pendingStreamText = "";
|
|
13633
|
+
pendingStreamDeltaCount = 0;
|
|
13634
|
+
};
|
|
13635
|
+
const discardPendingStreamPreview = () => {
|
|
13636
|
+
clearPendingStreamText();
|
|
13637
|
+
streamedReplyState = createSlackStreamAccumulator();
|
|
13638
|
+
};
|
|
13639
|
+
const finalizePendingStreamText = () => {
|
|
13640
|
+
if (!pendingStreamText || streamedReplyPromise || isPotentialRedundantReactionAckText(pendingStreamText)) {
|
|
13641
|
+
return;
|
|
13642
|
+
}
|
|
13643
|
+
flushPendingStreamText();
|
|
13644
|
+
};
|
|
13645
|
+
const queueVisibleStreamText = (text) => {
|
|
13646
|
+
if (!text) {
|
|
13647
|
+
return;
|
|
13648
|
+
}
|
|
13649
|
+
if (awaitingPostToolAssistantMessage) {
|
|
13650
|
+
return;
|
|
13651
|
+
}
|
|
13652
|
+
if (streamedReplyPromise) {
|
|
13653
|
+
textStream.push(text);
|
|
13654
|
+
return;
|
|
13655
|
+
}
|
|
13656
|
+
pendingStreamText += text;
|
|
13657
|
+
pendingStreamDeltaCount += 1;
|
|
13658
|
+
if (isPotentialRedundantReactionAckText(pendingStreamText)) {
|
|
13659
|
+
return;
|
|
13660
|
+
}
|
|
13661
|
+
if (!shouldAutoStartStreaming({
|
|
13662
|
+
text: pendingStreamText,
|
|
13663
|
+
deltaCount: pendingStreamDeltaCount
|
|
13664
|
+
})) {
|
|
13665
|
+
return;
|
|
13666
|
+
}
|
|
13667
|
+
flushPendingStreamText();
|
|
13668
|
+
};
|
|
13669
|
+
const appendVisibleStreamDelta = (deltaText) => {
|
|
13670
|
+
if (awaitingPostToolAssistantMessage && !streamedReplyPromise) {
|
|
13671
|
+
return;
|
|
13672
|
+
}
|
|
13673
|
+
queueVisibleStreamText(streamedReplyState.append(deltaText));
|
|
13333
13674
|
};
|
|
13334
13675
|
const postThreadReply = async (payload, stage) => {
|
|
13335
13676
|
await beforeFirstResponsePost();
|
|
@@ -13402,19 +13743,26 @@ function createReplyToThread(deps) {
|
|
|
13402
13743
|
if (explicitChannelPostIntent) {
|
|
13403
13744
|
return;
|
|
13404
13745
|
}
|
|
13405
|
-
|
|
13406
|
-
|
|
13746
|
+
appendVisibleStreamDelta(deltaText);
|
|
13747
|
+
},
|
|
13748
|
+
onAssistantMessageStart: () => {
|
|
13749
|
+
if (!awaitingPostToolAssistantMessage) {
|
|
13407
13750
|
return;
|
|
13408
13751
|
}
|
|
13409
|
-
|
|
13410
|
-
|
|
13411
|
-
|
|
13752
|
+
awaitingPostToolAssistantMessage = false;
|
|
13753
|
+
discardPendingStreamPreview();
|
|
13754
|
+
},
|
|
13755
|
+
onToolCall: () => {
|
|
13756
|
+
if (!streamedReplyPromise) {
|
|
13757
|
+
awaitingPostToolAssistantMessage = true;
|
|
13758
|
+
discardPendingStreamPreview();
|
|
13412
13759
|
}
|
|
13413
|
-
flushPendingStreamText();
|
|
13414
13760
|
}
|
|
13415
13761
|
});
|
|
13416
13762
|
if (streamedReplyPromise) {
|
|
13417
13763
|
flushPendingStreamText();
|
|
13764
|
+
} else {
|
|
13765
|
+
finalizePendingStreamText();
|
|
13418
13766
|
}
|
|
13419
13767
|
textStream.end();
|
|
13420
13768
|
const diagnosticsContext = {
|
|
@@ -13488,28 +13836,32 @@ function createReplyToThread(deps) {
|
|
|
13488
13836
|
}
|
|
13489
13837
|
});
|
|
13490
13838
|
const artifactStatePatch = reply.artifactStatePatch ? { ...reply.artifactStatePatch } : {};
|
|
13491
|
-
const replyFiles = reply.files && reply.files.length > 0 ? reply.files : void 0;
|
|
13492
|
-
const { shouldPostThreadReply, attachFiles: resolvedAttachFiles } = resolveReplyDelivery({
|
|
13493
|
-
reply,
|
|
13494
|
-
hasStreamedThreadReply: Boolean(streamedReplyPromise)
|
|
13495
|
-
});
|
|
13496
13839
|
const reactionPerformed = reply.diagnostics.toolCalls.includes(
|
|
13497
13840
|
"slackMessageAddReaction"
|
|
13498
13841
|
);
|
|
13499
|
-
|
|
13842
|
+
const plannedPosts = planSlackReplyPosts({
|
|
13843
|
+
reply,
|
|
13844
|
+
hasStreamedThreadReply: Boolean(streamedReplyPromise),
|
|
13845
|
+
streamedOverflowText: streamedReplyState.getOverflowText()
|
|
13846
|
+
});
|
|
13847
|
+
if (streamedReplyPromise) {
|
|
13848
|
+
await streamedReplyPromise;
|
|
13849
|
+
}
|
|
13850
|
+
if (plannedPosts.length > 0) {
|
|
13500
13851
|
if (!streamedReplyPromise) {
|
|
13501
|
-
|
|
13502
|
-
|
|
13503
|
-
|
|
13504
|
-
|
|
13505
|
-
|
|
13506
|
-
|
|
13507
|
-
)
|
|
13508
|
-
if (reactionPerformed && isRedundantReactionAckText(reply.text)) {
|
|
13852
|
+
let sent;
|
|
13853
|
+
for (const post of plannedPosts) {
|
|
13854
|
+
sent = await postThreadReply(post.message, post.stage);
|
|
13855
|
+
}
|
|
13856
|
+
const firstPlannedMessage = plannedPosts[0]?.message;
|
|
13857
|
+
const firstPlannedMessageHasFiles = typeof firstPlannedMessage === "object" && firstPlannedMessage !== null && "files" in firstPlannedMessage && Array.isArray(firstPlannedMessage.files) && firstPlannedMessage.files.length > 0;
|
|
13858
|
+
if (sent && reactionPerformed && plannedPosts.length === 1 && !firstPlannedMessageHasFiles && isRedundantReactionAckText(reply.text)) {
|
|
13509
13859
|
await sent.delete();
|
|
13510
13860
|
}
|
|
13511
13861
|
} else {
|
|
13512
|
-
|
|
13862
|
+
for (const post of plannedPosts) {
|
|
13863
|
+
await postThreadReply(post.message, post.stage);
|
|
13864
|
+
}
|
|
13513
13865
|
}
|
|
13514
13866
|
}
|
|
13515
13867
|
const shouldPersistArtifacts = Object.keys(artifactStatePatch).length > 0;
|
|
@@ -13585,12 +13937,6 @@ function createReplyToThread(deps) {
|
|
|
13585
13937
|
);
|
|
13586
13938
|
});
|
|
13587
13939
|
}
|
|
13588
|
-
if (shouldPostThreadReply && resolvedAttachFiles === "followup" && replyFiles) {
|
|
13589
|
-
await postThreadReply(
|
|
13590
|
-
buildSlackOutputMessage("", replyFiles),
|
|
13591
|
-
"thread_reply_files_followup"
|
|
13592
|
-
);
|
|
13593
|
-
}
|
|
13594
13940
|
} catch (error) {
|
|
13595
13941
|
if (isRetryableTurnError(error, "mcp_auth_resume")) {
|
|
13596
13942
|
shouldPersistFailureState = false;
|
|
@@ -13722,6 +14068,11 @@ async function initializeAssistantThread(event) {
|
|
|
13722
14068
|
}
|
|
13723
14069
|
|
|
13724
14070
|
// src/chat/runtime/turn-preparation.ts
|
|
14071
|
+
function hasPendingImageHydration(conversation) {
|
|
14072
|
+
return conversation.messages.some(
|
|
14073
|
+
(message) => isHumanConversationMessage(message) && !message.meta?.imagesHydrated
|
|
14074
|
+
);
|
|
14075
|
+
}
|
|
13725
14076
|
function createPrepareTurnState(deps) {
|
|
13726
14077
|
return async function prepareTurnState(args) {
|
|
13727
14078
|
const existingState = await args.thread.state;
|
|
@@ -13739,14 +14090,8 @@ function createPrepareTurnState(deps) {
|
|
|
13739
14090
|
messageId: args.message.id,
|
|
13740
14091
|
messageCreatedAtMs: args.message.metadata.dateSent.getTime()
|
|
13741
14092
|
});
|
|
13742
|
-
const messageHasPotentialImageAttachment =
|
|
13743
|
-
|
|
13744
|
-
if (attachment.type === "image") {
|
|
13745
|
-
return true;
|
|
13746
|
-
}
|
|
13747
|
-
const mimeType = attachment.mimeType ?? "";
|
|
13748
|
-
return attachment.type === "file" && mimeType.startsWith("image/");
|
|
13749
|
-
}
|
|
14093
|
+
const messageHasPotentialImageAttachment = hasPotentialImageAttachment(
|
|
14094
|
+
args.message.attachments
|
|
13750
14095
|
);
|
|
13751
14096
|
const normalizedUserText = normalizeConversationText(args.userText) || "[non-text message]";
|
|
13752
14097
|
const slackTs = getSlackMessageTs(args.message);
|
|
@@ -13771,7 +14116,8 @@ function createPrepareTurnState(deps) {
|
|
|
13771
14116
|
conversation,
|
|
13772
14117
|
incomingUserMessage
|
|
13773
14118
|
);
|
|
13774
|
-
|
|
14119
|
+
const shouldHydrateVisionContext = !conversation.vision.backfillCompletedAtMs || messageHasPotentialImageAttachment || hasPendingImageHydration(conversation);
|
|
14120
|
+
if (isVisionEnabled() && shouldHydrateVisionContext) {
|
|
13775
14121
|
await deps.hydrateConversationVisionContext(conversation, {
|
|
13776
14122
|
threadId: args.context.threadId,
|
|
13777
14123
|
channelId: args.context.channelId,
|
|
@@ -13867,7 +14213,7 @@ function createSlackRuntime(options) {
|
|
|
13867
14213
|
slackTs,
|
|
13868
14214
|
replied: false,
|
|
13869
14215
|
skippedReason: decision.reason,
|
|
13870
|
-
imagesHydrated:
|
|
14216
|
+
imagesHydrated: !hasPotentialImageAttachment(message.attachments)
|
|
13871
14217
|
}
|
|
13872
14218
|
});
|
|
13873
14219
|
conversation.processing.activeTurnId = void 0;
|
|
@@ -14151,6 +14497,166 @@ var JuniorChat = class extends Chat {
|
|
|
14151
14497
|
}
|
|
14152
14498
|
};
|
|
14153
14499
|
|
|
14500
|
+
// src/chat/slack/adapter.ts
|
|
14501
|
+
import {
|
|
14502
|
+
createSlackAdapter
|
|
14503
|
+
} from "@chat-adapter/slack";
|
|
14504
|
+
import {
|
|
14505
|
+
StreamingMarkdownRenderer
|
|
14506
|
+
} from "chat";
|
|
14507
|
+
var STREAM_BUFFER_SIZE = 64;
|
|
14508
|
+
var CLIENT_STREAM_PATCHED = /* @__PURE__ */ Symbol("junior.slack.client_stream_patched");
|
|
14509
|
+
var ADAPTER_STREAM_PATCHED = /* @__PURE__ */ Symbol("junior.slack.adapter_stream_patched");
|
|
14510
|
+
function assertSlackAdapterInternals(internals) {
|
|
14511
|
+
if (!internals.client || typeof internals.client.chatStream !== "function") {
|
|
14512
|
+
throw new Error("Slack adapter client does not expose chatStream()");
|
|
14513
|
+
}
|
|
14514
|
+
if (typeof internals.stream !== "function") {
|
|
14515
|
+
throw new Error("Slack adapter does not expose stream()");
|
|
14516
|
+
}
|
|
14517
|
+
if (typeof internals.decodeThreadId !== "function") {
|
|
14518
|
+
throw new Error("Slack adapter does not expose decodeThreadId()");
|
|
14519
|
+
}
|
|
14520
|
+
if (typeof internals.getToken !== "function") {
|
|
14521
|
+
throw new Error("Slack adapter does not expose getToken()");
|
|
14522
|
+
}
|
|
14523
|
+
if (!internals.logger || typeof internals.logger.debug !== "function" || typeof internals.logger.warn !== "function") {
|
|
14524
|
+
throw new Error("Slack adapter does not expose logger debug/warn methods");
|
|
14525
|
+
}
|
|
14526
|
+
}
|
|
14527
|
+
function shouldEagerFlushPlainText(text) {
|
|
14528
|
+
return text.length > 0 && !text.includes("\n") && !/[`*~[\]|]/.test(text);
|
|
14529
|
+
}
|
|
14530
|
+
function getNextRenderableDelta(renderer, lastAppended) {
|
|
14531
|
+
const committable = renderer.getCommittableText();
|
|
14532
|
+
if (committable.startsWith(lastAppended)) {
|
|
14533
|
+
const delta = committable.slice(lastAppended.length);
|
|
14534
|
+
if (delta) {
|
|
14535
|
+
return { delta, nextAppended: committable };
|
|
14536
|
+
}
|
|
14537
|
+
}
|
|
14538
|
+
const rawText = renderer.getText();
|
|
14539
|
+
if (shouldEagerFlushPlainText(rawText) && rawText.startsWith(lastAppended) && rawText.length > lastAppended.length) {
|
|
14540
|
+
return {
|
|
14541
|
+
delta: rawText.slice(lastAppended.length),
|
|
14542
|
+
nextAppended: rawText
|
|
14543
|
+
};
|
|
14544
|
+
}
|
|
14545
|
+
return { delta: "", nextAppended: lastAppended };
|
|
14546
|
+
}
|
|
14547
|
+
function patchSlackClientStream(adapter) {
|
|
14548
|
+
const internals = adapter;
|
|
14549
|
+
const { client: client2 } = internals;
|
|
14550
|
+
if (client2[CLIENT_STREAM_PATCHED]) {
|
|
14551
|
+
return;
|
|
14552
|
+
}
|
|
14553
|
+
const originalChatStream = client2.chatStream.bind(client2);
|
|
14554
|
+
client2.chatStream = (params) => originalChatStream({
|
|
14555
|
+
...params,
|
|
14556
|
+
buffer_size: STREAM_BUFFER_SIZE
|
|
14557
|
+
});
|
|
14558
|
+
client2[CLIENT_STREAM_PATCHED] = true;
|
|
14559
|
+
}
|
|
14560
|
+
function patchSlackAdapterStream(adapter) {
|
|
14561
|
+
const internals = adapter;
|
|
14562
|
+
if (internals[ADAPTER_STREAM_PATCHED]) {
|
|
14563
|
+
return;
|
|
14564
|
+
}
|
|
14565
|
+
const originalStream = internals.stream.bind(adapter);
|
|
14566
|
+
internals.stream = async function(threadId, textStream, options) {
|
|
14567
|
+
if (!(options?.recipientUserId && options?.recipientTeamId)) {
|
|
14568
|
+
return originalStream(threadId, textStream, options);
|
|
14569
|
+
}
|
|
14570
|
+
const { channel, threadTs } = internals.decodeThreadId(threadId);
|
|
14571
|
+
internals.logger.debug("Slack: starting stream", { channel, threadTs });
|
|
14572
|
+
const token = internals.getToken();
|
|
14573
|
+
const streamer = internals.client.chatStream({
|
|
14574
|
+
channel,
|
|
14575
|
+
thread_ts: threadTs,
|
|
14576
|
+
recipient_user_id: options.recipientUserId,
|
|
14577
|
+
recipient_team_id: options.recipientTeamId,
|
|
14578
|
+
...options.taskDisplayMode ? { task_display_mode: options.taskDisplayMode } : {}
|
|
14579
|
+
});
|
|
14580
|
+
let first = true;
|
|
14581
|
+
let lastAppended = "";
|
|
14582
|
+
let structuredChunksSupported = true;
|
|
14583
|
+
const renderer = new StreamingMarkdownRenderer();
|
|
14584
|
+
const flushMarkdownDelta = async (delta) => {
|
|
14585
|
+
if (delta.length === 0) {
|
|
14586
|
+
return;
|
|
14587
|
+
}
|
|
14588
|
+
if (first) {
|
|
14589
|
+
await streamer.append({ markdown_text: delta, token, chunks: [] });
|
|
14590
|
+
first = false;
|
|
14591
|
+
return;
|
|
14592
|
+
}
|
|
14593
|
+
await streamer.append({ markdown_text: delta });
|
|
14594
|
+
};
|
|
14595
|
+
const flushText = async () => {
|
|
14596
|
+
const { delta, nextAppended } = getNextRenderableDelta(
|
|
14597
|
+
renderer,
|
|
14598
|
+
lastAppended
|
|
14599
|
+
);
|
|
14600
|
+
await flushMarkdownDelta(delta);
|
|
14601
|
+
lastAppended = nextAppended;
|
|
14602
|
+
};
|
|
14603
|
+
const sendStructuredChunk = async (chunk) => {
|
|
14604
|
+
if (!structuredChunksSupported) {
|
|
14605
|
+
return;
|
|
14606
|
+
}
|
|
14607
|
+
await flushText();
|
|
14608
|
+
try {
|
|
14609
|
+
if (first) {
|
|
14610
|
+
await streamer.append({ chunks: [chunk], token });
|
|
14611
|
+
first = false;
|
|
14612
|
+
return;
|
|
14613
|
+
}
|
|
14614
|
+
await streamer.append({ chunks: [chunk] });
|
|
14615
|
+
} catch (error) {
|
|
14616
|
+
structuredChunksSupported = false;
|
|
14617
|
+
internals.logger.warn(
|
|
14618
|
+
"Structured streaming chunk failed, falling back to text-only streaming. Ensure your Slack app manifest includes assistant_view, assistant:write scope, and @slack/web-api >= 7.14.0",
|
|
14619
|
+
{ chunkType: chunk.type, error }
|
|
14620
|
+
);
|
|
14621
|
+
}
|
|
14622
|
+
};
|
|
14623
|
+
const pushTextAndFlush = async (text) => {
|
|
14624
|
+
renderer.push(text);
|
|
14625
|
+
await flushText();
|
|
14626
|
+
};
|
|
14627
|
+
for await (const chunk of textStream) {
|
|
14628
|
+
if (typeof chunk === "string") {
|
|
14629
|
+
await pushTextAndFlush(chunk);
|
|
14630
|
+
} else if (chunk.type === "markdown_text") {
|
|
14631
|
+
await pushTextAndFlush(chunk.text);
|
|
14632
|
+
} else {
|
|
14633
|
+
await sendStructuredChunk(chunk);
|
|
14634
|
+
}
|
|
14635
|
+
}
|
|
14636
|
+
renderer.finish();
|
|
14637
|
+
await flushText();
|
|
14638
|
+
const result = await streamer.stop(
|
|
14639
|
+
options?.stopBlocks ? { blocks: options.stopBlocks } : void 0
|
|
14640
|
+
);
|
|
14641
|
+
const messageTs = result.message?.ts ?? result.ts;
|
|
14642
|
+
internals.logger.debug("Slack: stream complete", { messageId: messageTs });
|
|
14643
|
+
return {
|
|
14644
|
+
id: messageTs,
|
|
14645
|
+
threadId,
|
|
14646
|
+
raw: result
|
|
14647
|
+
};
|
|
14648
|
+
};
|
|
14649
|
+
internals[ADAPTER_STREAM_PATCHED] = true;
|
|
14650
|
+
}
|
|
14651
|
+
function createJuniorSlackAdapter(config) {
|
|
14652
|
+
const adapter = createSlackAdapter(config);
|
|
14653
|
+
const internals = adapter;
|
|
14654
|
+
assertSlackAdapterInternals(internals);
|
|
14655
|
+
patchSlackClientStream(adapter);
|
|
14656
|
+
patchSlackAdapterStream(adapter);
|
|
14657
|
+
return adapter;
|
|
14658
|
+
}
|
|
14659
|
+
|
|
14154
14660
|
// src/chat/queue/thread-message-dispatcher.ts
|
|
14155
14661
|
function rehydrateAttachmentFetchers(message, downloadPrivateSlackFile2 = downloadPrivateSlackFile) {
|
|
14156
14662
|
for (const attachment of message.attachments) {
|
|
@@ -14269,7 +14775,7 @@ function createProductionBot() {
|
|
|
14269
14775
|
if (!signingSecret) {
|
|
14270
14776
|
throw new Error("SLACK_SIGNING_SECRET is required");
|
|
14271
14777
|
}
|
|
14272
|
-
return
|
|
14778
|
+
return createJuniorSlackAdapter({
|
|
14273
14779
|
logger: logger.child("slack"),
|
|
14274
14780
|
signingSecret,
|
|
14275
14781
|
...botToken ? { botToken } : {},
|
|
@@ -14405,6 +14911,32 @@ function isMessageChangedEnvelope(value) {
|
|
|
14405
14911
|
function textMentionsBot(text, botUserId) {
|
|
14406
14912
|
return text.includes(`<@${botUserId}>`);
|
|
14407
14913
|
}
|
|
14914
|
+
function getAttachmentType(mimeType) {
|
|
14915
|
+
if (mimeType?.startsWith("image/")) {
|
|
14916
|
+
return "image";
|
|
14917
|
+
}
|
|
14918
|
+
if (mimeType?.startsWith("video/")) {
|
|
14919
|
+
return "video";
|
|
14920
|
+
}
|
|
14921
|
+
if (mimeType?.startsWith("audio/")) {
|
|
14922
|
+
return "audio";
|
|
14923
|
+
}
|
|
14924
|
+
return "file";
|
|
14925
|
+
}
|
|
14926
|
+
function extractEditedMessageAttachments(files) {
|
|
14927
|
+
if (!files || files.length === 0) {
|
|
14928
|
+
return [];
|
|
14929
|
+
}
|
|
14930
|
+
return files.map((file) => ({
|
|
14931
|
+
type: getAttachmentType(file.mimetype),
|
|
14932
|
+
url: file.url_private_download ?? file.url_private,
|
|
14933
|
+
name: file.name,
|
|
14934
|
+
mimeType: file.mimetype,
|
|
14935
|
+
size: file.size,
|
|
14936
|
+
width: file.original_w,
|
|
14937
|
+
height: file.original_h
|
|
14938
|
+
}));
|
|
14939
|
+
}
|
|
14408
14940
|
function extractMessageChangedMention(body, botUserId, adapter) {
|
|
14409
14941
|
if (!isMessageChangedEnvelope(body)) return null;
|
|
14410
14942
|
const { event } = body;
|
|
@@ -14430,7 +14962,7 @@ function extractMessageChangedMention(body, botUserId, adapter) {
|
|
|
14430
14962
|
threadId,
|
|
14431
14963
|
text: newText,
|
|
14432
14964
|
isMention: true,
|
|
14433
|
-
attachments:
|
|
14965
|
+
attachments: extractEditedMessageAttachments(event.message.files),
|
|
14434
14966
|
metadata: { dateSent: new Date(Number(messageTs) * 1e3), edited: true },
|
|
14435
14967
|
formatted: { type: "root", children: [] },
|
|
14436
14968
|
raw,
|
|
@@ -14483,6 +15015,7 @@ async function handleAuthenticatedSlackMessageChangedMention(args) {
|
|
|
14483
15015
|
if (!result) {
|
|
14484
15016
|
return false;
|
|
14485
15017
|
}
|
|
15018
|
+
rehydrateAttachmentFetchers(result.message);
|
|
14486
15019
|
args.bot.processMessage(
|
|
14487
15020
|
slackAdapter,
|
|
14488
15021
|
result.threadId,
|