@cuylabs/channel-slack 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +168 -0
- package/dist/activity-ByrD9Ftr.d.ts +66 -0
- package/dist/assistant.d.ts +58 -0
- package/dist/assistant.js +188 -0
- package/dist/bolt.d.ts +344 -0
- package/dist/bolt.js +705 -0
- package/dist/chunk-BODPT4I6.js +322 -0
- package/dist/chunk-FPCE5V5Y.js +292 -0
- package/dist/chunk-FX2JOVX5.js +405 -0
- package/dist/chunk-JZG4IETE.js +141 -0
- package/dist/chunk-NE57BLLU.js +0 -0
- package/dist/chunk-TWJGVDA2.js +108 -0
- package/dist/core.d.ts +425 -0
- package/dist/core.js +42 -0
- package/dist/diagnostics.d.ts +105 -0
- package/dist/diagnostics.js +8 -0
- package/dist/feedback.d.ts +137 -0
- package/dist/feedback.js +128 -0
- package/dist/history.d.ts +266 -0
- package/dist/history.js +747 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +57 -0
- package/dist/logging-Bl3HfcC8.d.ts +8 -0
- package/dist/policy.d.ts +130 -0
- package/dist/policy.js +16 -0
- package/dist/setup.d.ts +165 -0
- package/dist/setup.js +453 -0
- package/dist/shared.d.ts +2 -0
- package/dist/shared.js +43 -0
- package/dist/targets.d.ts +113 -0
- package/dist/targets.js +484 -0
- package/dist/users.d.ts +109 -0
- package/dist/users.js +240 -0
- package/docs/concepts/activity.md +33 -0
- package/docs/concepts/bolt-runtime.md +30 -0
- package/docs/concepts/message-policy.md +49 -0
- package/docs/concepts/setup-requirements.md +44 -0
- package/docs/concepts/supplemental-history.md +55 -0
- package/docs/recipes/app-mention-handler.md +34 -0
- package/docs/recipes/assistant-thread-handler.md +28 -0
- package/docs/recipes/generate-slack-manifest.md +28 -0
- package/docs/recipes/history-visibility.md +36 -0
- package/docs/recipes/socket-mode-app.md +29 -0
- package/docs/reference/channel-slack-boundary.md +50 -0
- package/docs/reference/exports.md +32 -0
- package/package.json +130 -0
package/dist/history.js
ADDED
|
@@ -0,0 +1,747 @@
|
|
|
1
|
+
import {
|
|
2
|
+
extractSlackMessageText
|
|
3
|
+
} from "./chunk-FPCE5V5Y.js";
|
|
4
|
+
|
|
5
|
+
// src/history/context/assistant-root.ts
|
|
6
|
+
function normalizeAssistantSurfaceThreadRootMessages(messages, options) {
|
|
7
|
+
const rootUser = options.assistantSurfaceThreadRootUser;
|
|
8
|
+
const threadTs = options.activity.threadTs;
|
|
9
|
+
if (!rootUser?.userId || !threadTs || messages.length === 0) {
|
|
10
|
+
return [...messages];
|
|
11
|
+
}
|
|
12
|
+
const firstMessage = messages[0];
|
|
13
|
+
if (!firstMessage || firstMessage.role !== "assistant") {
|
|
14
|
+
return [...messages];
|
|
15
|
+
}
|
|
16
|
+
const text = firstMessage.text.trim();
|
|
17
|
+
if (!text || isLikelyAssistantGreeting(text)) {
|
|
18
|
+
return [...messages];
|
|
19
|
+
}
|
|
20
|
+
const duplicateIndex = messages.findIndex(
|
|
21
|
+
(message, index) => index > 0 && message.userId === rootUser.userId && normalizeHistoryText(message.text) === normalizeHistoryText(text)
|
|
22
|
+
);
|
|
23
|
+
if (duplicateIndex >= 0) {
|
|
24
|
+
options.onAssistantSurfaceThreadRootNormalized?.({
|
|
25
|
+
action: "removed-duplicate",
|
|
26
|
+
channelId: options.activity.channelId,
|
|
27
|
+
...options.teamId ?? options.activity.teamId ? { teamId: options.teamId ?? options.activity.teamId } : {},
|
|
28
|
+
threadTs,
|
|
29
|
+
userId: rootUser.userId
|
|
30
|
+
});
|
|
31
|
+
return messages.slice(1);
|
|
32
|
+
}
|
|
33
|
+
options.onAssistantSurfaceThreadRootNormalized?.({
|
|
34
|
+
action: "corrected-author",
|
|
35
|
+
channelId: options.activity.channelId,
|
|
36
|
+
...options.teamId ?? options.activity.teamId ? { teamId: options.teamId ?? options.activity.teamId } : {},
|
|
37
|
+
threadTs,
|
|
38
|
+
userId: rootUser.userId
|
|
39
|
+
});
|
|
40
|
+
return [
|
|
41
|
+
{
|
|
42
|
+
...firstMessage,
|
|
43
|
+
userId: rootUser.userId,
|
|
44
|
+
botId: void 0,
|
|
45
|
+
appId: void 0,
|
|
46
|
+
role: "user",
|
|
47
|
+
isBot: false
|
|
48
|
+
},
|
|
49
|
+
...messages.slice(1)
|
|
50
|
+
];
|
|
51
|
+
}
|
|
52
|
+
function normalizeHistoryText(value) {
|
|
53
|
+
return value.trim().replace(/\s+/g, " ");
|
|
54
|
+
}
|
|
55
|
+
function isLikelyAssistantGreeting(text) {
|
|
56
|
+
const normalized = text.trim().toLowerCase();
|
|
57
|
+
return normalized === "how can i help?" || normalized === "how can i help" || normalized === "hi, how can i help?" || normalized === "hi, how can i help";
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// src/history/reader.ts
|
|
61
|
+
var DEFAULT_HISTORY_LIMIT = 15;
|
|
62
|
+
var DEFAULT_HEADER = "Slack conversation history:";
|
|
63
|
+
var DEFAULT_MAX_CHARACTERS = 12e3;
|
|
64
|
+
var DEFAULT_ROLE_LABELS = {
|
|
65
|
+
assistant: "Assistant",
|
|
66
|
+
bot: "Bot",
|
|
67
|
+
system: "System",
|
|
68
|
+
unknown: "Message",
|
|
69
|
+
user: "User"
|
|
70
|
+
};
|
|
71
|
+
async function readSlackThreadHistory(options) {
|
|
72
|
+
const client = resolveConversationsClient(options.client);
|
|
73
|
+
const response = await client.replies({
|
|
74
|
+
channel: options.channelId,
|
|
75
|
+
ts: options.threadTs,
|
|
76
|
+
limit: options.limit ?? DEFAULT_HISTORY_LIMIT,
|
|
77
|
+
...options.cursor ? { cursor: options.cursor } : {},
|
|
78
|
+
...options.token ? { token: options.token } : {},
|
|
79
|
+
...options.includeAllMetadata !== void 0 ? { include_all_metadata: options.includeAllMetadata } : {},
|
|
80
|
+
...options.oldest ? { oldest: options.oldest } : {},
|
|
81
|
+
...options.latest ? { latest: options.latest } : {},
|
|
82
|
+
...options.inclusive !== void 0 ? { inclusive: options.inclusive } : {}
|
|
83
|
+
});
|
|
84
|
+
assertSlackHistoryResponseOk(response, "conversations.replies");
|
|
85
|
+
return toHistoryPage(response, options.channelId, options.botUserId);
|
|
86
|
+
}
|
|
87
|
+
async function readSlackChannelHistory(options) {
|
|
88
|
+
const client = resolveConversationsClient(options.client);
|
|
89
|
+
const response = await client.history({
|
|
90
|
+
channel: options.channelId,
|
|
91
|
+
limit: options.limit ?? DEFAULT_HISTORY_LIMIT,
|
|
92
|
+
...options.cursor ? { cursor: options.cursor } : {},
|
|
93
|
+
...options.token ? { token: options.token } : {},
|
|
94
|
+
...options.includeAllMetadata !== void 0 ? { include_all_metadata: options.includeAllMetadata } : {},
|
|
95
|
+
...options.oldest ? { oldest: options.oldest } : {},
|
|
96
|
+
...options.latest ? { latest: options.latest } : {},
|
|
97
|
+
...options.inclusive !== void 0 ? { inclusive: options.inclusive } : {}
|
|
98
|
+
});
|
|
99
|
+
assertSlackHistoryResponseOk(response, "conversations.history");
|
|
100
|
+
const page = toHistoryPage(response, options.channelId, options.botUserId);
|
|
101
|
+
return options.oldestFirst === false ? page : { ...page, messages: [...page.messages].reverse() };
|
|
102
|
+
}
|
|
103
|
+
function formatSlackHistoryForPrompt(messages, options = {}) {
|
|
104
|
+
const selected = options.maxMessages && options.maxMessages > 0 ? messages.slice(-options.maxMessages) : messages;
|
|
105
|
+
const labels = { ...DEFAULT_ROLE_LABELS, ...options.roleLabels ?? {} };
|
|
106
|
+
const lines = selected.filter((message) => message.text.trim().length > 0).map((message) => {
|
|
107
|
+
const label = normalizeAuthorLabel(options.formatAuthor?.(message)) ?? labels[message.role];
|
|
108
|
+
const timestamp = options.includeTimestamps && message.ts ? ` [${message.ts}]` : "";
|
|
109
|
+
return `${label}${timestamp}: ${message.text.trim()}`;
|
|
110
|
+
});
|
|
111
|
+
const header = options.header === false ? void 0 : options.header ?? DEFAULT_HEADER;
|
|
112
|
+
const text = header ? [header, ...lines].join("\n") : lines.join("\n");
|
|
113
|
+
const maxCharacters = options.maxCharacters ?? DEFAULT_MAX_CHARACTERS;
|
|
114
|
+
if (maxCharacters <= 0 || text.length <= maxCharacters) {
|
|
115
|
+
return text;
|
|
116
|
+
}
|
|
117
|
+
return text.slice(0, maxCharacters).trimEnd();
|
|
118
|
+
}
|
|
119
|
+
function toHistoryPage(response, channelId, botUserId) {
|
|
120
|
+
const messages = (response.messages ?? []).map(
|
|
121
|
+
(message) => normalizeSlackHistoryMessage(message, channelId, botUserId)
|
|
122
|
+
).filter((message) => message !== void 0);
|
|
123
|
+
const nextCursor = response.response_metadata?.next_cursor;
|
|
124
|
+
return {
|
|
125
|
+
messages,
|
|
126
|
+
...nextCursor ? { nextCursor } : {},
|
|
127
|
+
hasMore: response.has_more === true || Boolean(nextCursor)
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
function normalizeSlackHistoryMessage(message, channelId, botUserId) {
|
|
131
|
+
if (!message.ts) {
|
|
132
|
+
return void 0;
|
|
133
|
+
}
|
|
134
|
+
const userId = readString(message.user);
|
|
135
|
+
const botId = readString(message.bot_id);
|
|
136
|
+
const appId = readString(message.app_id);
|
|
137
|
+
const subtype = readString(readMessageField(message, "subtype"));
|
|
138
|
+
const text = extractSlackMessageText(message);
|
|
139
|
+
const isBot = Boolean(botId) || Boolean(message.bot_profile) || Boolean(botUserId) && userId === botUserId;
|
|
140
|
+
return {
|
|
141
|
+
channelId,
|
|
142
|
+
ts: message.ts,
|
|
143
|
+
...message.thread_ts ? { threadTs: message.thread_ts } : {},
|
|
144
|
+
...userId ? { userId } : {},
|
|
145
|
+
...botId ? { botId } : {},
|
|
146
|
+
...appId ? { appId } : {},
|
|
147
|
+
...subtype ? { subtype } : {},
|
|
148
|
+
text,
|
|
149
|
+
role: resolveHistoryRole({ userId, botId, subtype, isBot, botUserId }),
|
|
150
|
+
isBot,
|
|
151
|
+
raw: message
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
function resolveHistoryRole(context) {
|
|
155
|
+
if (context.botUserId && context.userId === context.botUserId) {
|
|
156
|
+
return "assistant";
|
|
157
|
+
}
|
|
158
|
+
if (context.botId || context.isBot) {
|
|
159
|
+
return "bot";
|
|
160
|
+
}
|
|
161
|
+
if (context.userId) {
|
|
162
|
+
return "user";
|
|
163
|
+
}
|
|
164
|
+
if (context.subtype) {
|
|
165
|
+
return "system";
|
|
166
|
+
}
|
|
167
|
+
return "unknown";
|
|
168
|
+
}
|
|
169
|
+
function assertSlackHistoryResponseOk(response, method) {
|
|
170
|
+
if (response.ok === false) {
|
|
171
|
+
throw new Error(
|
|
172
|
+
`${method} failed${response.error ? `: ${response.error}` : ""}`
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
function readString(value) {
|
|
177
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
178
|
+
}
|
|
179
|
+
function normalizeAuthorLabel(value) {
|
|
180
|
+
const trimmed = value?.trim();
|
|
181
|
+
return trimmed || void 0;
|
|
182
|
+
}
|
|
183
|
+
function resolveConversationsClient(client) {
|
|
184
|
+
if ("conversations" in client) {
|
|
185
|
+
return client.conversations;
|
|
186
|
+
}
|
|
187
|
+
return client;
|
|
188
|
+
}
|
|
189
|
+
function readMessageField(message, field) {
|
|
190
|
+
return message[field];
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// src/history/context/errors.ts
|
|
194
|
+
var EXPECTED_SLACK_HISTORY_ERRORS = {
|
|
195
|
+
access_denied: "Slack denied access to the conversation history.",
|
|
196
|
+
channel_not_found: "The bot could not find this Slack conversation or is not a member.",
|
|
197
|
+
missing_scope: "The Slack token is missing a history scope for this conversation type.",
|
|
198
|
+
no_permission: "The bot does not have permission to read this conversation.",
|
|
199
|
+
not_in_channel: "The bot is not a member of this channel and cannot read its history."
|
|
200
|
+
};
|
|
201
|
+
function toUnavailable(context) {
|
|
202
|
+
const info = inspectSlackHistoryError(context.error);
|
|
203
|
+
return {
|
|
204
|
+
source: context.source,
|
|
205
|
+
method: context.method,
|
|
206
|
+
channelId: context.channelId,
|
|
207
|
+
...info
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
function inspectSlackHistoryError(error) {
|
|
211
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
212
|
+
const errorCode = readSlackErrorCode(message);
|
|
213
|
+
const hint = errorCode ? EXPECTED_SLACK_HISTORY_ERRORS[errorCode] : void 0;
|
|
214
|
+
return {
|
|
215
|
+
message,
|
|
216
|
+
...errorCode ? { errorCode } : {},
|
|
217
|
+
expected: Boolean(hint),
|
|
218
|
+
...hint ? { hint } : {}
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
function readSlackErrorCode(message) {
|
|
222
|
+
const normalized = message.trim();
|
|
223
|
+
const match = /failed:\s*([a-z_]+)/i.exec(normalized);
|
|
224
|
+
if (match?.[1]) {
|
|
225
|
+
return match[1];
|
|
226
|
+
}
|
|
227
|
+
return EXPECTED_SLACK_HISTORY_ERRORS[normalized] ? normalized : void 0;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// src/history/context/fetch.ts
|
|
231
|
+
function shouldReadTopLevelChannelHistory(options, threadMessages) {
|
|
232
|
+
if (options.includeTopLevelChannelFallback === false) {
|
|
233
|
+
return false;
|
|
234
|
+
}
|
|
235
|
+
return !options.activity.threadTs || threadMessages.length === 0;
|
|
236
|
+
}
|
|
237
|
+
function shouldReadOriginChannelHistory(options, topLevelChannelMessages) {
|
|
238
|
+
return Boolean(
|
|
239
|
+
options.originChannelId && options.originChannelId !== options.activity.channelId && topLevelChannelMessages.length === 0
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
async function readThreadMessages(options, unavailable) {
|
|
243
|
+
const threadTs = options.activity.threadTs;
|
|
244
|
+
if (!threadTs) {
|
|
245
|
+
return [];
|
|
246
|
+
}
|
|
247
|
+
try {
|
|
248
|
+
const page = await readSlackThreadHistory({
|
|
249
|
+
client: options.client,
|
|
250
|
+
channelId: options.activity.channelId,
|
|
251
|
+
threadTs,
|
|
252
|
+
botUserId: options.botUserId,
|
|
253
|
+
token: options.token,
|
|
254
|
+
limit: options.limit
|
|
255
|
+
});
|
|
256
|
+
return filterCurrentActivity(page.messages, options.activity);
|
|
257
|
+
} catch (error) {
|
|
258
|
+
unavailable.push(
|
|
259
|
+
toUnavailable({
|
|
260
|
+
source: "thread",
|
|
261
|
+
method: "conversations.replies",
|
|
262
|
+
channelId: options.activity.channelId,
|
|
263
|
+
error
|
|
264
|
+
})
|
|
265
|
+
);
|
|
266
|
+
return [];
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
async function readTopLevelChannelMessages(options, unavailable) {
|
|
270
|
+
try {
|
|
271
|
+
const page = await readSlackChannelHistory({
|
|
272
|
+
client: options.client,
|
|
273
|
+
channelId: options.activity.channelId,
|
|
274
|
+
botUserId: options.botUserId,
|
|
275
|
+
token: options.token,
|
|
276
|
+
limit: options.limit,
|
|
277
|
+
latest: options.activity.messageTs,
|
|
278
|
+
inclusive: false
|
|
279
|
+
});
|
|
280
|
+
return filterCurrentActivity(page.messages, options.activity);
|
|
281
|
+
} catch (error) {
|
|
282
|
+
unavailable.push(
|
|
283
|
+
toUnavailable({
|
|
284
|
+
source: "channel",
|
|
285
|
+
method: "conversations.history",
|
|
286
|
+
channelId: options.activity.channelId,
|
|
287
|
+
error
|
|
288
|
+
})
|
|
289
|
+
);
|
|
290
|
+
return [];
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
async function readOriginChannelMessages(options, unavailable) {
|
|
294
|
+
const channelId = options.originChannelId;
|
|
295
|
+
if (!channelId) {
|
|
296
|
+
return [];
|
|
297
|
+
}
|
|
298
|
+
try {
|
|
299
|
+
const page = await readSlackChannelHistory({
|
|
300
|
+
client: options.client,
|
|
301
|
+
channelId,
|
|
302
|
+
botUserId: options.botUserId,
|
|
303
|
+
token: options.token,
|
|
304
|
+
limit: options.limit,
|
|
305
|
+
latest: options.activity.messageTs,
|
|
306
|
+
inclusive: false
|
|
307
|
+
});
|
|
308
|
+
return page.messages;
|
|
309
|
+
} catch (error) {
|
|
310
|
+
unavailable.push(
|
|
311
|
+
toUnavailable({
|
|
312
|
+
source: "origin-channel",
|
|
313
|
+
method: "conversations.history",
|
|
314
|
+
channelId,
|
|
315
|
+
error
|
|
316
|
+
})
|
|
317
|
+
);
|
|
318
|
+
return [];
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
function filterCurrentActivity(messages, activity) {
|
|
322
|
+
return messages.filter((message) => message.ts !== activity.messageTs);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// src/history/context/format.ts
|
|
326
|
+
async function formatHistorySection(messages, options) {
|
|
327
|
+
if (messages.length === 0 || !messages.some((message) => message.text.trim().length > 0)) {
|
|
328
|
+
return void 0;
|
|
329
|
+
}
|
|
330
|
+
const authorContext = toAuthorResolverContext(options);
|
|
331
|
+
const formatAuthor = await createResolvedAuthorFormatter(
|
|
332
|
+
messages,
|
|
333
|
+
options.resolveAuthorLabel,
|
|
334
|
+
authorContext
|
|
335
|
+
);
|
|
336
|
+
return formatSlackHistoryForPrompt(messages, {
|
|
337
|
+
header: options.header,
|
|
338
|
+
maxCharacters: options.maxCharacters,
|
|
339
|
+
...formatAuthor ? { formatAuthor } : {}
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
function boundText(text, maxCharacters) {
|
|
343
|
+
const trimmed = text.trim();
|
|
344
|
+
if (!trimmed || maxCharacters === void 0 || maxCharacters <= 0 || trimmed.length <= maxCharacters) {
|
|
345
|
+
return trimmed;
|
|
346
|
+
}
|
|
347
|
+
return trimmed.slice(0, maxCharacters).trimEnd();
|
|
348
|
+
}
|
|
349
|
+
function createCachedAuthorLabelResolver(resolveAuthor, onResolveAuthorError) {
|
|
350
|
+
if (!resolveAuthor) {
|
|
351
|
+
return void 0;
|
|
352
|
+
}
|
|
353
|
+
const authorByKey = /* @__PURE__ */ new Map();
|
|
354
|
+
return async (message, context) => {
|
|
355
|
+
const key = authorCacheKey(message, context);
|
|
356
|
+
if (!key) {
|
|
357
|
+
return resolveAuthorSafely(
|
|
358
|
+
message,
|
|
359
|
+
context,
|
|
360
|
+
resolveAuthor,
|
|
361
|
+
onResolveAuthorError
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
let cached = authorByKey.get(key);
|
|
365
|
+
if (!cached) {
|
|
366
|
+
cached = resolveAuthorSafely(
|
|
367
|
+
message,
|
|
368
|
+
context,
|
|
369
|
+
resolveAuthor,
|
|
370
|
+
onResolveAuthorError
|
|
371
|
+
);
|
|
372
|
+
authorByKey.set(key, cached);
|
|
373
|
+
}
|
|
374
|
+
return cached;
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
function toAuthorResolverContext(context) {
|
|
378
|
+
return {
|
|
379
|
+
source: context.source,
|
|
380
|
+
channelId: context.channelId,
|
|
381
|
+
...context.teamId ? { teamId: context.teamId } : {}
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
async function resolveAuthorSafely(message, context, resolveAuthor, onResolveAuthorError) {
|
|
385
|
+
try {
|
|
386
|
+
return normalizeAuthorLabel2(await resolveAuthor(message, context));
|
|
387
|
+
} catch (error) {
|
|
388
|
+
onResolveAuthorError?.({
|
|
389
|
+
...context,
|
|
390
|
+
...message.userId ? { userId: message.userId } : {},
|
|
391
|
+
error
|
|
392
|
+
});
|
|
393
|
+
return void 0;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
async function createResolvedAuthorFormatter(messages, resolveAuthorLabel, context) {
|
|
397
|
+
if (!resolveAuthorLabel) {
|
|
398
|
+
return void 0;
|
|
399
|
+
}
|
|
400
|
+
const authorByMessage = /* @__PURE__ */ new Map();
|
|
401
|
+
await Promise.all(
|
|
402
|
+
messages.map(async (message) => {
|
|
403
|
+
const author = await resolveAuthorLabel(message, context);
|
|
404
|
+
if (author) {
|
|
405
|
+
authorByMessage.set(message, author);
|
|
406
|
+
}
|
|
407
|
+
})
|
|
408
|
+
);
|
|
409
|
+
if (authorByMessage.size === 0) {
|
|
410
|
+
return void 0;
|
|
411
|
+
}
|
|
412
|
+
return (message) => authorByMessage.get(message);
|
|
413
|
+
}
|
|
414
|
+
function normalizeAuthorLabel2(value) {
|
|
415
|
+
const trimmed = value?.trim();
|
|
416
|
+
return trimmed || void 0;
|
|
417
|
+
}
|
|
418
|
+
function authorCacheKey(message, context) {
|
|
419
|
+
if (message.userId) {
|
|
420
|
+
return `${context.teamId ?? "unknown-team"}:${message.role}:user:${message.userId}`;
|
|
421
|
+
}
|
|
422
|
+
if (message.botId) {
|
|
423
|
+
return `${context.teamId ?? "unknown-team"}:${message.role}:bot:${message.botId}`;
|
|
424
|
+
}
|
|
425
|
+
if (message.appId) {
|
|
426
|
+
return `${context.teamId ?? "unknown-team"}:${message.role}:app:${message.appId}`;
|
|
427
|
+
}
|
|
428
|
+
return void 0;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// src/history/context/visibility.ts
|
|
432
|
+
async function applyHistoryVisibilityPolicy({
|
|
433
|
+
options,
|
|
434
|
+
originChannelMessages,
|
|
435
|
+
threadMessages,
|
|
436
|
+
topLevelChannelMessages
|
|
437
|
+
}) {
|
|
438
|
+
const teamId = options.teamId ?? options.activity.teamId;
|
|
439
|
+
const thread = await applyHistoryVisibilityPolicyForSource({
|
|
440
|
+
options,
|
|
441
|
+
messages: threadMessages,
|
|
442
|
+
source: "thread",
|
|
443
|
+
channelId: options.activity.channelId,
|
|
444
|
+
...teamId ? { teamId } : {}
|
|
445
|
+
});
|
|
446
|
+
const topLevelChannel = await applyHistoryVisibilityPolicyForSource({
|
|
447
|
+
options,
|
|
448
|
+
messages: topLevelChannelMessages,
|
|
449
|
+
source: "channel",
|
|
450
|
+
channelId: options.activity.channelId,
|
|
451
|
+
...teamId ? { teamId } : {}
|
|
452
|
+
});
|
|
453
|
+
const originChannel = await applyHistoryVisibilityPolicyForSource({
|
|
454
|
+
options,
|
|
455
|
+
messages: originChannelMessages,
|
|
456
|
+
source: "origin-channel",
|
|
457
|
+
channelId: options.originChannelId ?? options.activity.channelId,
|
|
458
|
+
...teamId ? { teamId } : {}
|
|
459
|
+
});
|
|
460
|
+
const decisions = [thread, topLevelChannel, originChannel];
|
|
461
|
+
return {
|
|
462
|
+
decisions,
|
|
463
|
+
omittedTotal: decisions.reduce(
|
|
464
|
+
(total, decision) => total + decision.omittedCount,
|
|
465
|
+
0
|
|
466
|
+
),
|
|
467
|
+
originChannel,
|
|
468
|
+
thread,
|
|
469
|
+
topLevelChannel
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
async function applyHistoryVisibilityPolicyForSource({
|
|
473
|
+
channelId,
|
|
474
|
+
messages,
|
|
475
|
+
options,
|
|
476
|
+
source,
|
|
477
|
+
teamId
|
|
478
|
+
}) {
|
|
479
|
+
const base = {
|
|
480
|
+
channelId,
|
|
481
|
+
source,
|
|
482
|
+
...teamId ? { teamId } : {}
|
|
483
|
+
};
|
|
484
|
+
if (!options.visibilityPolicy) {
|
|
485
|
+
return {
|
|
486
|
+
messages: [...messages],
|
|
487
|
+
mode: "all",
|
|
488
|
+
omittedCount: 0,
|
|
489
|
+
source
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
try {
|
|
493
|
+
return await options.visibilityPolicy.filter(messages, {
|
|
494
|
+
activity: options.activity,
|
|
495
|
+
botUserId: options.botUserId,
|
|
496
|
+
...base
|
|
497
|
+
});
|
|
498
|
+
} catch (error) {
|
|
499
|
+
options.onVisibilityPolicyError?.({
|
|
500
|
+
...base,
|
|
501
|
+
error
|
|
502
|
+
});
|
|
503
|
+
return {
|
|
504
|
+
messages: [],
|
|
505
|
+
mode: "error",
|
|
506
|
+
omittedCount: messages.length,
|
|
507
|
+
source
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// src/history/context/load.ts
|
|
513
|
+
var DEFAULT_HISTORY_CONTEXT_MAX_CHARACTERS = 12e3;
|
|
514
|
+
async function loadSlackTurnHistoryContext(options) {
|
|
515
|
+
const maxCharacters = options.maxCharacters ?? DEFAULT_HISTORY_CONTEXT_MAX_CHARACTERS;
|
|
516
|
+
const unavailable = [];
|
|
517
|
+
const threadMessages = normalizeAssistantSurfaceThreadRootMessages(
|
|
518
|
+
await readThreadMessages(options, unavailable),
|
|
519
|
+
options
|
|
520
|
+
);
|
|
521
|
+
const topLevelChannelMessages = shouldReadTopLevelChannelHistory(
|
|
522
|
+
options,
|
|
523
|
+
threadMessages
|
|
524
|
+
) ? await readTopLevelChannelMessages(options, unavailable) : [];
|
|
525
|
+
const originChannelMessages = shouldReadOriginChannelHistory(
|
|
526
|
+
options,
|
|
527
|
+
topLevelChannelMessages
|
|
528
|
+
) ? await readOriginChannelMessages(options, unavailable) : [];
|
|
529
|
+
const visibility = await applyHistoryVisibilityPolicy({
|
|
530
|
+
options,
|
|
531
|
+
threadMessages,
|
|
532
|
+
topLevelChannelMessages,
|
|
533
|
+
originChannelMessages
|
|
534
|
+
});
|
|
535
|
+
const visibleThreadMessages = visibility.thread.messages;
|
|
536
|
+
const visibleTopLevelChannelMessages = visibility.topLevelChannel.messages;
|
|
537
|
+
const visibleOriginChannelMessages = visibility.originChannel.messages;
|
|
538
|
+
const resolveAuthorLabel = createCachedAuthorLabelResolver(
|
|
539
|
+
options.resolveAuthor,
|
|
540
|
+
options.onResolveAuthorError
|
|
541
|
+
);
|
|
542
|
+
const teamId = options.teamId ?? options.activity.teamId;
|
|
543
|
+
const sections = (await Promise.all([
|
|
544
|
+
formatHistorySection(visibleThreadMessages, {
|
|
545
|
+
source: "thread",
|
|
546
|
+
channelId: options.activity.channelId,
|
|
547
|
+
...teamId ? { teamId } : {},
|
|
548
|
+
header: options.headers?.thread ?? "Slack thread history:",
|
|
549
|
+
maxCharacters,
|
|
550
|
+
resolveAuthorLabel
|
|
551
|
+
}),
|
|
552
|
+
formatHistorySection(visibleTopLevelChannelMessages, {
|
|
553
|
+
source: "channel",
|
|
554
|
+
channelId: options.activity.channelId,
|
|
555
|
+
...teamId ? { teamId } : {},
|
|
556
|
+
header: options.headers?.channel ?? "Recent Slack channel history:",
|
|
557
|
+
maxCharacters,
|
|
558
|
+
resolveAuthorLabel
|
|
559
|
+
}),
|
|
560
|
+
formatHistorySection(visibleOriginChannelMessages, {
|
|
561
|
+
source: "origin-channel",
|
|
562
|
+
channelId: options.originChannelId ?? options.activity.channelId,
|
|
563
|
+
...teamId ? { teamId } : {},
|
|
564
|
+
header: options.headers?.originChannel ?? "Recent Slack origin channel history:",
|
|
565
|
+
maxCharacters,
|
|
566
|
+
resolveAuthorLabel
|
|
567
|
+
})
|
|
568
|
+
])).filter((section) => Boolean(section));
|
|
569
|
+
const prompt = boundText(sections.join("\n\n"), maxCharacters);
|
|
570
|
+
return {
|
|
571
|
+
...prompt ? {
|
|
572
|
+
prompt,
|
|
573
|
+
fragment: {
|
|
574
|
+
content: prompt,
|
|
575
|
+
title: "Slack Conversation History",
|
|
576
|
+
source: "slack",
|
|
577
|
+
kind: "slack-context",
|
|
578
|
+
role: "user",
|
|
579
|
+
placement: "after-latest-user",
|
|
580
|
+
lifetime: "turn",
|
|
581
|
+
budgetBehavior: "droppable",
|
|
582
|
+
maxChars: maxCharacters,
|
|
583
|
+
metadata: {
|
|
584
|
+
threadMessageCount: visibleThreadMessages.length,
|
|
585
|
+
topLevelChannelMessageCount: visibleTopLevelChannelMessages.length,
|
|
586
|
+
originChannelMessageCount: visibleOriginChannelMessages.length,
|
|
587
|
+
visibilityOmittedMessageCount: visibility.omittedTotal,
|
|
588
|
+
threadVisibilityOmittedMessageCount: visibility.thread.omittedCount,
|
|
589
|
+
topLevelChannelVisibilityOmittedMessageCount: visibility.topLevelChannel.omittedCount,
|
|
590
|
+
originChannelVisibilityOmittedMessageCount: visibility.originChannel.omittedCount,
|
|
591
|
+
unavailableCount: unavailable.length
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
} : {},
|
|
595
|
+
threadMessageCount: visibleThreadMessages.length,
|
|
596
|
+
topLevelChannelMessageCount: visibleTopLevelChannelMessages.length,
|
|
597
|
+
originChannelMessageCount: visibleOriginChannelMessages.length,
|
|
598
|
+
threadVisibilityOmittedMessageCount: visibility.thread.omittedCount,
|
|
599
|
+
topLevelChannelVisibilityOmittedMessageCount: visibility.topLevelChannel.omittedCount,
|
|
600
|
+
originChannelVisibilityOmittedMessageCount: visibility.originChannel.omittedCount,
|
|
601
|
+
visibilityOmittedMessageCount: visibility.omittedTotal,
|
|
602
|
+
visibility: visibility.decisions,
|
|
603
|
+
unavailable
|
|
604
|
+
};
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
// src/history/inclusion-policy.ts
|
|
608
|
+
function createSlackSupplementalHistoryPolicy({
|
|
609
|
+
logger,
|
|
610
|
+
sessionTranscript
|
|
611
|
+
} = {}) {
|
|
612
|
+
const seenDirectSessionIds = /* @__PURE__ */ new Set();
|
|
613
|
+
const transcriptCoveredDirectSessionIds = /* @__PURE__ */ new Set();
|
|
614
|
+
async function hasPersistedConversationMessages(request, sessionId) {
|
|
615
|
+
if (transcriptCoveredDirectSessionIds.has(sessionId)) {
|
|
616
|
+
return true;
|
|
617
|
+
}
|
|
618
|
+
if (!sessionTranscript) {
|
|
619
|
+
return void 0;
|
|
620
|
+
}
|
|
621
|
+
try {
|
|
622
|
+
const hasMessages = await sessionTranscript.hasConversationMessages(sessionId);
|
|
623
|
+
if (hasMessages) {
|
|
624
|
+
transcriptCoveredDirectSessionIds.add(sessionId);
|
|
625
|
+
}
|
|
626
|
+
return hasMessages;
|
|
627
|
+
} catch (error) {
|
|
628
|
+
logger?.debug?.("Slack session transcript check failed", {
|
|
629
|
+
channelId: request.slackActivity.channelId,
|
|
630
|
+
channelType: request.slackActivity.channelType,
|
|
631
|
+
error: formatError(error),
|
|
632
|
+
sessionId,
|
|
633
|
+
teamId: request.slackActivity.teamId,
|
|
634
|
+
threadTs: request.slackActivity.threadTs
|
|
635
|
+
});
|
|
636
|
+
return void 0;
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
return {
|
|
640
|
+
async decide(request) {
|
|
641
|
+
if (request.slackActivity.channelType !== "dm") {
|
|
642
|
+
return {
|
|
643
|
+
include: true,
|
|
644
|
+
reason: "shared-slack-surface",
|
|
645
|
+
source: "shared-surface"
|
|
646
|
+
};
|
|
647
|
+
}
|
|
648
|
+
const sessionId = request.sessionId;
|
|
649
|
+
if (sessionId) {
|
|
650
|
+
const transcriptCoversTurn = await hasPersistedConversationMessages(
|
|
651
|
+
request,
|
|
652
|
+
sessionId
|
|
653
|
+
);
|
|
654
|
+
if (transcriptCoversTurn === true) {
|
|
655
|
+
return {
|
|
656
|
+
include: false,
|
|
657
|
+
reason: "session-transcript-covers-direct-turn",
|
|
658
|
+
source: "session-transcript"
|
|
659
|
+
};
|
|
660
|
+
}
|
|
661
|
+
if (transcriptCoversTurn === false) {
|
|
662
|
+
return {
|
|
663
|
+
include: true,
|
|
664
|
+
reason: "first-direct-session-turn",
|
|
665
|
+
source: "session-transcript"
|
|
666
|
+
};
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
if (!sessionId || !seenDirectSessionIds.has(sessionId)) {
|
|
670
|
+
if (sessionId) {
|
|
671
|
+
seenDirectSessionIds.add(sessionId);
|
|
672
|
+
}
|
|
673
|
+
return {
|
|
674
|
+
include: true,
|
|
675
|
+
reason: "first-direct-session-turn",
|
|
676
|
+
source: "process-memory"
|
|
677
|
+
};
|
|
678
|
+
}
|
|
679
|
+
return {
|
|
680
|
+
include: false,
|
|
681
|
+
reason: "session-transcript-covers-direct-turn",
|
|
682
|
+
source: "process-memory"
|
|
683
|
+
};
|
|
684
|
+
}
|
|
685
|
+
};
|
|
686
|
+
}
|
|
687
|
+
function formatError(error) {
|
|
688
|
+
return error instanceof Error ? error.message : String(error);
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
// src/history/visibility-policy.ts
|
|
692
|
+
function createSlackSupplementalHistoryVisibilityPolicy({
|
|
693
|
+
allowedUserIds = [],
|
|
694
|
+
mode = "all"
|
|
695
|
+
} = {}) {
|
|
696
|
+
const allowedUsers = new Set(
|
|
697
|
+
Array.from(allowedUserIds, (userId) => userId.trim()).filter(Boolean)
|
|
698
|
+
);
|
|
699
|
+
return {
|
|
700
|
+
async filter(messages, context) {
|
|
701
|
+
if (mode === "all") {
|
|
702
|
+
return {
|
|
703
|
+
messages: [...messages],
|
|
704
|
+
mode,
|
|
705
|
+
omittedCount: 0,
|
|
706
|
+
source: context.source
|
|
707
|
+
};
|
|
708
|
+
}
|
|
709
|
+
const filtered = messages.filter(
|
|
710
|
+
(message) => isVisibleSlackHistoryMessage(message, context, mode, allowedUsers)
|
|
711
|
+
);
|
|
712
|
+
return {
|
|
713
|
+
messages: filtered,
|
|
714
|
+
mode,
|
|
715
|
+
omittedCount: messages.length - filtered.length,
|
|
716
|
+
source: context.source
|
|
717
|
+
};
|
|
718
|
+
}
|
|
719
|
+
};
|
|
720
|
+
}
|
|
721
|
+
function isVisibleSlackHistoryMessage(message, context, mode, allowedUsers) {
|
|
722
|
+
if (isAssistantHistoryMessage(message, context.botUserId)) {
|
|
723
|
+
return true;
|
|
724
|
+
}
|
|
725
|
+
if (!message.userId) {
|
|
726
|
+
return false;
|
|
727
|
+
}
|
|
728
|
+
if (mode === "allowed-users-plus-assistant") {
|
|
729
|
+
return allowedUsers.has(message.userId);
|
|
730
|
+
}
|
|
731
|
+
if (mode === "current-user-plus-assistant") {
|
|
732
|
+
return message.userId === context.activity.userId;
|
|
733
|
+
}
|
|
734
|
+
const originalUserId = context.activity.parentUserId ?? context.activity.userId;
|
|
735
|
+
return message.userId === originalUserId;
|
|
736
|
+
}
|
|
737
|
+
function isAssistantHistoryMessage(message, botUserId) {
|
|
738
|
+
return message.role === "assistant" || Boolean(botUserId && message.userId === botUserId);
|
|
739
|
+
}
|
|
740
|
+
export {
|
|
741
|
+
createSlackSupplementalHistoryPolicy,
|
|
742
|
+
createSlackSupplementalHistoryVisibilityPolicy,
|
|
743
|
+
formatSlackHistoryForPrompt,
|
|
744
|
+
loadSlackTurnHistoryContext,
|
|
745
|
+
readSlackChannelHistory,
|
|
746
|
+
readSlackThreadHistory
|
|
747
|
+
};
|