@sentry/junior 0.23.0 → 0.24.1
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 +1322 -1178
- package/dist/{chunk-MCJJKEB3.js → chunk-I3WA75AD.js} +29 -1
- package/dist/{chunk-JWBWBJYJ.js → chunk-O5N42P7K.js} +1 -1
- package/dist/{chunk-THPM7NSG.js → chunk-RMVXZMXQ.js} +1 -1
- package/dist/cli/check.js +2 -2
- package/dist/cli/snapshot-warmup.js +2 -2
- package/package.json +19 -19
package/dist/app.js
CHANGED
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
loadSkillsByName,
|
|
7
7
|
logCapabilityCatalogLoadedOnce,
|
|
8
8
|
parseSkillInvocation
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-O5N42P7K.js";
|
|
10
10
|
import {
|
|
11
11
|
SANDBOX_DATA_ROOT,
|
|
12
12
|
SANDBOX_SKILLS_ROOT,
|
|
@@ -27,7 +27,7 @@ import {
|
|
|
27
27
|
sandboxSkillDir,
|
|
28
28
|
sandboxSkillFile,
|
|
29
29
|
toOptionalTrimmed
|
|
30
|
-
} from "./chunk-
|
|
30
|
+
} from "./chunk-RMVXZMXQ.js";
|
|
31
31
|
import {
|
|
32
32
|
CredentialUnavailableError,
|
|
33
33
|
buildOAuthTokenRequest,
|
|
@@ -35,6 +35,7 @@ import {
|
|
|
35
35
|
createPluginBroker,
|
|
36
36
|
createRequestContext,
|
|
37
37
|
extractGenAiUsageAttributes,
|
|
38
|
+
extractGenAiUsageSummary,
|
|
38
39
|
getActiveTraceId,
|
|
39
40
|
getPluginDefinition,
|
|
40
41
|
getPluginMcpProviders,
|
|
@@ -58,7 +59,7 @@ import {
|
|
|
58
59
|
toOptionalString,
|
|
59
60
|
withContext,
|
|
60
61
|
withSpan
|
|
61
|
-
} from "./chunk-
|
|
62
|
+
} from "./chunk-I3WA75AD.js";
|
|
62
63
|
import "./chunk-Z3YD6NHK.js";
|
|
63
64
|
import {
|
|
64
65
|
discoverInstalledPluginPackageContent,
|
|
@@ -333,9 +334,17 @@ function coerceAuthor(value) {
|
|
|
333
334
|
function coerceMessageMeta(value) {
|
|
334
335
|
if (!isRecord(value)) return void 0;
|
|
335
336
|
const meta = {};
|
|
337
|
+
const attachmentCount = toOptionalNumber(value.attachmentCount);
|
|
338
|
+
if (typeof attachmentCount === "number" && attachmentCount > 0) {
|
|
339
|
+
meta.attachmentCount = attachmentCount;
|
|
340
|
+
}
|
|
336
341
|
if (typeof value.explicitMention === "boolean") {
|
|
337
342
|
meta.explicitMention = value.explicitMention;
|
|
338
343
|
}
|
|
344
|
+
const imageAttachmentCount = toOptionalNumber(value.imageAttachmentCount);
|
|
345
|
+
if (typeof imageAttachmentCount === "number" && imageAttachmentCount > 0) {
|
|
346
|
+
meta.imageAttachmentCount = imageAttachmentCount;
|
|
347
|
+
}
|
|
339
348
|
if (typeof value.replied === "boolean") {
|
|
340
349
|
meta.replied = value.replied;
|
|
341
350
|
}
|
|
@@ -356,7 +365,7 @@ function coerceMessageMeta(value) {
|
|
|
356
365
|
if (typeof value.imagesHydrated === "boolean") {
|
|
357
366
|
meta.imagesHydrated = value.imagesHydrated;
|
|
358
367
|
}
|
|
359
|
-
if (meta.explicitMention === void 0 && meta.replied === void 0 && meta.skippedReason === void 0 && meta.slackTs === void 0 && meta.imageFileIds === void 0 && meta.imagesHydrated === void 0) {
|
|
368
|
+
if (meta.attachmentCount === void 0 && meta.explicitMention === void 0 && meta.imageAttachmentCount === void 0 && meta.replied === void 0 && meta.skippedReason === void 0 && meta.slackTs === void 0 && meta.imageFileIds === void 0 && meta.imagesHydrated === void 0) {
|
|
360
369
|
return void 0;
|
|
361
370
|
}
|
|
362
371
|
return meta;
|
|
@@ -868,13 +877,19 @@ function mapSlackError(error) {
|
|
|
868
877
|
if (apiError === "not_in_channel") {
|
|
869
878
|
return new SlackActionError(message, "not_in_channel", baseOptions);
|
|
870
879
|
}
|
|
880
|
+
if (apiError === "already_reacted") {
|
|
881
|
+
return new SlackActionError(message, "already_reacted", baseOptions);
|
|
882
|
+
}
|
|
883
|
+
if (apiError === "no_reaction") {
|
|
884
|
+
return new SlackActionError(message, "no_reaction", baseOptions);
|
|
885
|
+
}
|
|
871
886
|
if (apiError === "invalid_arguments") {
|
|
872
887
|
return new SlackActionError(message, "invalid_arguments", baseOptions);
|
|
873
888
|
}
|
|
874
889
|
if (apiError === "invalid_name") {
|
|
875
890
|
return new SlackActionError(message, "invalid_arguments", baseOptions);
|
|
876
891
|
}
|
|
877
|
-
if (apiError === "not_found") {
|
|
892
|
+
if (apiError === "not_found" || apiError === "channel_not_found" || apiError === "message_not_found") {
|
|
878
893
|
return new SlackActionError(message, "not_found", baseOptions);
|
|
879
894
|
}
|
|
880
895
|
if (apiError === "feature_not_enabled" || apiError === "not_allowed_token_type") {
|
|
@@ -974,19 +989,6 @@ async function getFilePermalink(fileId) {
|
|
|
974
989
|
);
|
|
975
990
|
return response.file?.permalink;
|
|
976
991
|
}
|
|
977
|
-
async function uploadFilesToThread(args) {
|
|
978
|
-
const client2 = getClient();
|
|
979
|
-
await withSlackRetries(
|
|
980
|
-
() => client2.filesUploadV2({
|
|
981
|
-
channel_id: args.channelId,
|
|
982
|
-
thread_ts: args.threadTs,
|
|
983
|
-
file_uploads: args.files.map((f) => ({
|
|
984
|
-
file: f.data,
|
|
985
|
-
filename: f.filename
|
|
986
|
-
}))
|
|
987
|
-
})
|
|
988
|
-
);
|
|
989
|
-
}
|
|
990
992
|
async function downloadPrivateSlackFile(url) {
|
|
991
993
|
const token = getSlackBotToken();
|
|
992
994
|
if (!token) {
|
|
@@ -1006,6 +1008,217 @@ async function downloadPrivateSlackFile(url) {
|
|
|
1006
1008
|
return Buffer.from(await response.arrayBuffer());
|
|
1007
1009
|
}
|
|
1008
1010
|
|
|
1011
|
+
// src/chat/slack/emoji.ts
|
|
1012
|
+
var SLACK_EMOJI_NAME_RE = /^(?:[a-z0-9_+-]+)(?:::(?:skin-tone-[2-6]))?$/;
|
|
1013
|
+
function normalizeSlackEmojiName(value) {
|
|
1014
|
+
const trimmed = value.trim().toLowerCase();
|
|
1015
|
+
if (!trimmed) {
|
|
1016
|
+
return null;
|
|
1017
|
+
}
|
|
1018
|
+
const normalized = trimmed.startsWith(":") && trimmed.endsWith(":") ? trimmed.slice(1, -1) : trimmed;
|
|
1019
|
+
return SLACK_EMOJI_NAME_RE.test(normalized) ? normalized : null;
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
// src/chat/slack/outbound.ts
|
|
1023
|
+
var MAX_SLACK_MESSAGE_TEXT_CHARS = 4e4;
|
|
1024
|
+
function requireSlackConversationId(channelId, action) {
|
|
1025
|
+
const normalized = normalizeSlackConversationId(channelId);
|
|
1026
|
+
if (!normalized) {
|
|
1027
|
+
throw new Error(`${action} requires a valid channel ID`);
|
|
1028
|
+
}
|
|
1029
|
+
return normalized;
|
|
1030
|
+
}
|
|
1031
|
+
function requireSlackThreadTimestamp(threadTs, action) {
|
|
1032
|
+
const normalized = threadTs.trim();
|
|
1033
|
+
if (!normalized) {
|
|
1034
|
+
throw new Error(`${action} requires a thread timestamp`);
|
|
1035
|
+
}
|
|
1036
|
+
return normalized;
|
|
1037
|
+
}
|
|
1038
|
+
function requireSlackMessageTimestamp(timestamp, action) {
|
|
1039
|
+
const normalized = timestamp.trim();
|
|
1040
|
+
if (!normalized) {
|
|
1041
|
+
throw new Error(`${action} requires a target message timestamp`);
|
|
1042
|
+
}
|
|
1043
|
+
return normalized;
|
|
1044
|
+
}
|
|
1045
|
+
function requireSlackMessageText(text, action) {
|
|
1046
|
+
if (text.trim().length === 0) {
|
|
1047
|
+
throw new Error(`${action} requires non-empty text`);
|
|
1048
|
+
}
|
|
1049
|
+
if (text.length > MAX_SLACK_MESSAGE_TEXT_CHARS) {
|
|
1050
|
+
throw new Error(
|
|
1051
|
+
`${action} text exceeds Slack's 40000 character truncation limit`
|
|
1052
|
+
);
|
|
1053
|
+
}
|
|
1054
|
+
return text;
|
|
1055
|
+
}
|
|
1056
|
+
async function getPermalinkBestEffort(args) {
|
|
1057
|
+
try {
|
|
1058
|
+
const response = await withSlackRetries(
|
|
1059
|
+
() => getSlackClient().chat.getPermalink({
|
|
1060
|
+
channel: args.channelId,
|
|
1061
|
+
message_ts: args.messageTs
|
|
1062
|
+
}),
|
|
1063
|
+
3,
|
|
1064
|
+
{ action: "chat.getPermalink" }
|
|
1065
|
+
);
|
|
1066
|
+
return response.permalink;
|
|
1067
|
+
} catch {
|
|
1068
|
+
return void 0;
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
async function postSlackMessage(input) {
|
|
1072
|
+
const channelId = requireSlackConversationId(
|
|
1073
|
+
input.channelId,
|
|
1074
|
+
"Slack message posting"
|
|
1075
|
+
);
|
|
1076
|
+
const text = requireSlackMessageText(input.text, "Slack message posting");
|
|
1077
|
+
const threadTs = input.threadTs ? requireSlackThreadTimestamp(
|
|
1078
|
+
input.threadTs,
|
|
1079
|
+
"Slack thread message posting"
|
|
1080
|
+
) : void 0;
|
|
1081
|
+
const response = await withSlackRetries(
|
|
1082
|
+
() => getSlackClient().chat.postMessage({
|
|
1083
|
+
channel: channelId,
|
|
1084
|
+
text,
|
|
1085
|
+
mrkdwn: true,
|
|
1086
|
+
...input.blocks?.length ? {
|
|
1087
|
+
blocks: input.blocks
|
|
1088
|
+
} : {},
|
|
1089
|
+
...threadTs ? { thread_ts: threadTs } : {}
|
|
1090
|
+
}),
|
|
1091
|
+
3,
|
|
1092
|
+
{ action: "chat.postMessage" }
|
|
1093
|
+
);
|
|
1094
|
+
if (!response.ts) {
|
|
1095
|
+
throw new Error("Slack message posted without ts");
|
|
1096
|
+
}
|
|
1097
|
+
return {
|
|
1098
|
+
ts: response.ts,
|
|
1099
|
+
...input.includePermalink ? {
|
|
1100
|
+
permalink: await getPermalinkBestEffort({
|
|
1101
|
+
channelId,
|
|
1102
|
+
messageTs: response.ts
|
|
1103
|
+
})
|
|
1104
|
+
} : {}
|
|
1105
|
+
};
|
|
1106
|
+
}
|
|
1107
|
+
async function deleteSlackMessage(input) {
|
|
1108
|
+
const channelId = requireSlackConversationId(
|
|
1109
|
+
input.channelId,
|
|
1110
|
+
"Slack message deletion"
|
|
1111
|
+
);
|
|
1112
|
+
const timestamp = requireSlackMessageTimestamp(
|
|
1113
|
+
input.timestamp,
|
|
1114
|
+
"Slack message deletion"
|
|
1115
|
+
);
|
|
1116
|
+
await withSlackRetries(
|
|
1117
|
+
() => getSlackClient().chat.delete({
|
|
1118
|
+
channel: channelId,
|
|
1119
|
+
ts: timestamp
|
|
1120
|
+
}),
|
|
1121
|
+
3,
|
|
1122
|
+
{ action: "chat.delete" }
|
|
1123
|
+
);
|
|
1124
|
+
}
|
|
1125
|
+
async function postSlackEphemeralMessage(input) {
|
|
1126
|
+
const channelId = requireSlackConversationId(
|
|
1127
|
+
input.channelId,
|
|
1128
|
+
"Slack ephemeral message posting"
|
|
1129
|
+
);
|
|
1130
|
+
const userId = input.userId.trim();
|
|
1131
|
+
if (!userId) {
|
|
1132
|
+
throw new Error("Slack ephemeral message posting requires a user ID");
|
|
1133
|
+
}
|
|
1134
|
+
const text = requireSlackMessageText(
|
|
1135
|
+
input.text,
|
|
1136
|
+
"Slack ephemeral message posting"
|
|
1137
|
+
);
|
|
1138
|
+
const threadTs = input.threadTs ? requireSlackThreadTimestamp(
|
|
1139
|
+
input.threadTs,
|
|
1140
|
+
"Slack ephemeral thread message posting"
|
|
1141
|
+
) : void 0;
|
|
1142
|
+
const response = await withSlackRetries(
|
|
1143
|
+
() => getSlackClient().chat.postEphemeral({
|
|
1144
|
+
channel: channelId,
|
|
1145
|
+
user: userId,
|
|
1146
|
+
text,
|
|
1147
|
+
...threadTs ? { thread_ts: threadTs } : {}
|
|
1148
|
+
}),
|
|
1149
|
+
3,
|
|
1150
|
+
{ action: "chat.postEphemeral" }
|
|
1151
|
+
);
|
|
1152
|
+
return {
|
|
1153
|
+
messageTs: response.message_ts
|
|
1154
|
+
};
|
|
1155
|
+
}
|
|
1156
|
+
async function uploadFilesToThread(input) {
|
|
1157
|
+
const channelId = requireSlackConversationId(
|
|
1158
|
+
input.channelId,
|
|
1159
|
+
"Slack file upload"
|
|
1160
|
+
);
|
|
1161
|
+
const threadTs = requireSlackThreadTimestamp(
|
|
1162
|
+
input.threadTs,
|
|
1163
|
+
"Slack file upload"
|
|
1164
|
+
);
|
|
1165
|
+
if (input.files.length === 0) {
|
|
1166
|
+
throw new Error("Slack file upload requires at least one file");
|
|
1167
|
+
}
|
|
1168
|
+
const fileUploads = input.files.map((file) => {
|
|
1169
|
+
const filename = file.filename.trim();
|
|
1170
|
+
if (!filename) {
|
|
1171
|
+
throw new Error(
|
|
1172
|
+
"Slack file upload requires every file to have a filename"
|
|
1173
|
+
);
|
|
1174
|
+
}
|
|
1175
|
+
return {
|
|
1176
|
+
file: file.data,
|
|
1177
|
+
filename
|
|
1178
|
+
};
|
|
1179
|
+
});
|
|
1180
|
+
await withSlackRetries(
|
|
1181
|
+
() => getSlackClient().filesUploadV2({
|
|
1182
|
+
channel_id: channelId,
|
|
1183
|
+
thread_ts: threadTs,
|
|
1184
|
+
file_uploads: fileUploads
|
|
1185
|
+
}),
|
|
1186
|
+
3,
|
|
1187
|
+
{ action: "filesUploadV2" }
|
|
1188
|
+
);
|
|
1189
|
+
}
|
|
1190
|
+
async function addReactionToMessage(input) {
|
|
1191
|
+
const channelId = requireSlackConversationId(
|
|
1192
|
+
input.channelId,
|
|
1193
|
+
"Slack reaction"
|
|
1194
|
+
);
|
|
1195
|
+
const timestamp = requireSlackMessageTimestamp(
|
|
1196
|
+
input.timestamp,
|
|
1197
|
+
"Slack reaction"
|
|
1198
|
+
);
|
|
1199
|
+
const emoji = normalizeSlackEmojiName(input.emoji);
|
|
1200
|
+
if (!emoji) {
|
|
1201
|
+
throw new Error("Slack reaction requires a valid emoji alias name");
|
|
1202
|
+
}
|
|
1203
|
+
try {
|
|
1204
|
+
await withSlackRetries(
|
|
1205
|
+
() => getSlackClient().reactions.add({
|
|
1206
|
+
channel: channelId,
|
|
1207
|
+
timestamp,
|
|
1208
|
+
name: emoji
|
|
1209
|
+
}),
|
|
1210
|
+
3,
|
|
1211
|
+
{ action: "reactions.add" }
|
|
1212
|
+
);
|
|
1213
|
+
} catch (error) {
|
|
1214
|
+
if (error instanceof SlackActionError && error.code === "already_reacted") {
|
|
1215
|
+
return { ok: true };
|
|
1216
|
+
}
|
|
1217
|
+
throw error;
|
|
1218
|
+
}
|
|
1219
|
+
return { ok: true };
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1009
1222
|
// src/chat/respond-helpers.ts
|
|
1010
1223
|
var MAX_INLINE_ATTACHMENT_BASE64_CHARS = 12e4;
|
|
1011
1224
|
function getSessionIdentifiers(context) {
|
|
@@ -1182,16 +1395,23 @@ function extractAssistantText(message) {
|
|
|
1182
1395
|
(part) => part.type === "text" && typeof part.text === "string"
|
|
1183
1396
|
).map((part) => part.text).join("\n");
|
|
1184
1397
|
}
|
|
1185
|
-
function
|
|
1398
|
+
function getTerminalAssistantMessages(messages) {
|
|
1399
|
+
let lastToolResultIndex = -1;
|
|
1186
1400
|
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1401
|
+
if (isToolResultMessage(messages[index])) {
|
|
1402
|
+
lastToolResultIndex = index;
|
|
1403
|
+
break;
|
|
1190
1404
|
}
|
|
1191
|
-
const stopReason = message.stopReason;
|
|
1192
|
-
return typeof stopReason === "string" && stopReason !== "error" && extractAssistantText(message).trim().length > 0;
|
|
1193
1405
|
}
|
|
1194
|
-
return
|
|
1406
|
+
return messages.slice(lastToolResultIndex + 1).filter(isAssistantMessage);
|
|
1407
|
+
}
|
|
1408
|
+
function hasCompletedAssistantTurn(messages) {
|
|
1409
|
+
const message = getTerminalAssistantMessages(messages).at(-1);
|
|
1410
|
+
if (!message) {
|
|
1411
|
+
return false;
|
|
1412
|
+
}
|
|
1413
|
+
const stopReason = message.stopReason;
|
|
1414
|
+
return typeof stopReason === "string" && stopReason !== "error" && extractAssistantText(message).trim().length > 0;
|
|
1195
1415
|
}
|
|
1196
1416
|
function upsertActiveSkill(activeSkills, next) {
|
|
1197
1417
|
const existing = activeSkills.find((skill) => skill.name === next.name);
|
|
@@ -1257,17 +1477,17 @@ async function deliverPrivateMessage(input) {
|
|
|
1257
1477
|
if (input.channelId) {
|
|
1258
1478
|
try {
|
|
1259
1479
|
if (isDmChannel(input.channelId)) {
|
|
1260
|
-
await
|
|
1261
|
-
|
|
1480
|
+
await postSlackMessage({
|
|
1481
|
+
channelId: input.channelId,
|
|
1262
1482
|
text: input.text,
|
|
1263
|
-
|
|
1483
|
+
threadTs: input.threadTs
|
|
1264
1484
|
});
|
|
1265
1485
|
} else {
|
|
1266
|
-
await
|
|
1267
|
-
|
|
1268
|
-
|
|
1486
|
+
await postSlackEphemeralMessage({
|
|
1487
|
+
channelId: input.channelId,
|
|
1488
|
+
userId: input.userId,
|
|
1269
1489
|
text: input.text,
|
|
1270
|
-
|
|
1490
|
+
threadTs: input.threadTs
|
|
1271
1491
|
});
|
|
1272
1492
|
}
|
|
1273
1493
|
return "in_context";
|
|
@@ -1294,7 +1514,7 @@ async function deliverPrivateMessage(input) {
|
|
|
1294
1514
|
);
|
|
1295
1515
|
return false;
|
|
1296
1516
|
}
|
|
1297
|
-
await
|
|
1517
|
+
await postSlackMessage({ channelId: dmChannelId, text: input.text });
|
|
1298
1518
|
return "fallback_dm";
|
|
1299
1519
|
} catch (error) {
|
|
1300
1520
|
logWarn(
|
|
@@ -1914,6 +2134,7 @@ function coerceThreadArtifactsState(value) {
|
|
|
1914
2134
|
}
|
|
1915
2135
|
return {
|
|
1916
2136
|
assistantContextChannelId: typeof artifacts.assistantContextChannelId === "string" ? artifacts.assistantContextChannelId : void 0,
|
|
2137
|
+
assistantTitleSourceMessageId: typeof artifacts.assistantTitleSourceMessageId === "string" ? artifacts.assistantTitleSourceMessageId : void 0,
|
|
1917
2138
|
lastCanvasId: typeof artifacts.lastCanvasId === "string" ? artifacts.lastCanvasId : void 0,
|
|
1918
2139
|
lastCanvasUrl: typeof artifacts.lastCanvasUrl === "string" ? artifacts.lastCanvasUrl : void 0,
|
|
1919
2140
|
recentCanvases,
|
|
@@ -2115,6 +2336,15 @@ function getTurnUserMessage(conversation, sessionId) {
|
|
|
2115
2336
|
function getTurnUserMessageId(conversation, sessionId) {
|
|
2116
2337
|
return getTurnUserMessage(conversation, sessionId)?.id;
|
|
2117
2338
|
}
|
|
2339
|
+
function getTurnUserReplyAttachmentContext(message) {
|
|
2340
|
+
const inboundAttachmentCount = message?.meta?.attachmentCount ?? 0;
|
|
2341
|
+
const imageAttachmentCount = message?.meta?.imageAttachmentCount ?? 0;
|
|
2342
|
+
const imagesHydrated = message?.meta?.imagesHydrated === true;
|
|
2343
|
+
return {
|
|
2344
|
+
...inboundAttachmentCount > 0 ? { inboundAttachmentCount } : {},
|
|
2345
|
+
...!imagesHydrated && imageAttachmentCount > 0 ? { omittedImageAttachmentCount: imageAttachmentCount } : {}
|
|
2346
|
+
};
|
|
2347
|
+
}
|
|
2118
2348
|
|
|
2119
2349
|
// src/chat/pi/client.ts
|
|
2120
2350
|
import {
|
|
@@ -2535,7 +2765,7 @@ async function summarizeConversationChunk(messages, conversation, context, deps)
|
|
|
2535
2765
|
}
|
|
2536
2766
|
return transcript.slice(0, 2800);
|
|
2537
2767
|
}
|
|
2538
|
-
async function generateThreadTitleWithDeps(
|
|
2768
|
+
async function generateThreadTitleWithDeps(sourceText, deps) {
|
|
2539
2769
|
const result = await deps.completeText({
|
|
2540
2770
|
modelId: botConfig.fastModelId,
|
|
2541
2771
|
temperature: 0,
|
|
@@ -2543,17 +2773,41 @@ async function generateThreadTitleWithDeps(userText, assistantText, deps) {
|
|
|
2543
2773
|
{
|
|
2544
2774
|
role: "user",
|
|
2545
2775
|
content: [
|
|
2546
|
-
"Generate a concise 5-8 word
|
|
2776
|
+
"Generate a concise 5-8 word Slack conversation title from the first user message below.",
|
|
2777
|
+
"Capture the user's main request.",
|
|
2778
|
+
"Reply with ONLY the title, with no quotes or trailing punctuation.",
|
|
2547
2779
|
"",
|
|
2548
|
-
`
|
|
2549
|
-
`Assistant: ${assistantText.slice(0, 500)}`
|
|
2780
|
+
`First user message: ${sourceText.slice(0, 500)}`
|
|
2550
2781
|
].join("\n"),
|
|
2551
2782
|
timestamp: Date.now()
|
|
2552
2783
|
}
|
|
2553
|
-
]
|
|
2784
|
+
],
|
|
2785
|
+
metadata: {
|
|
2786
|
+
modelId: botConfig.fastModelId
|
|
2787
|
+
}
|
|
2554
2788
|
});
|
|
2555
2789
|
return result.text.trim().slice(0, 60);
|
|
2556
2790
|
}
|
|
2791
|
+
function getThreadTitleSourceMessage(conversation) {
|
|
2792
|
+
let firstMessage;
|
|
2793
|
+
for (const message of conversation.messages) {
|
|
2794
|
+
if (!isHumanConversationMessage(message)) {
|
|
2795
|
+
continue;
|
|
2796
|
+
}
|
|
2797
|
+
if (!firstMessage) {
|
|
2798
|
+
firstMessage = message;
|
|
2799
|
+
continue;
|
|
2800
|
+
}
|
|
2801
|
+
if (message.createdAtMs < firstMessage.createdAtMs) {
|
|
2802
|
+
firstMessage = message;
|
|
2803
|
+
continue;
|
|
2804
|
+
}
|
|
2805
|
+
if (message.createdAtMs === firstMessage.createdAtMs && message.id < firstMessage.id) {
|
|
2806
|
+
firstMessage = message;
|
|
2807
|
+
}
|
|
2808
|
+
}
|
|
2809
|
+
return firstMessage;
|
|
2810
|
+
}
|
|
2557
2811
|
async function compactConversationIfNeededWithDeps(conversation, context, deps) {
|
|
2558
2812
|
updateConversationStats(conversation);
|
|
2559
2813
|
let estimatedTokens = conversation.stats.estimatedContextTokens;
|
|
@@ -2598,7 +2852,7 @@ async function compactConversationIfNeededWithDeps(conversation, context, deps)
|
|
|
2598
2852
|
function createConversationMemoryService(deps) {
|
|
2599
2853
|
return {
|
|
2600
2854
|
compactConversationIfNeeded: async (conversation, context) => await compactConversationIfNeededWithDeps(conversation, context, deps),
|
|
2601
|
-
generateThreadTitle: async (
|
|
2855
|
+
generateThreadTitle: async (sourceText) => await generateThreadTitleWithDeps(sourceText, deps)
|
|
2602
2856
|
};
|
|
2603
2857
|
}
|
|
2604
2858
|
var defaultConversationMemoryService = createConversationMemoryService({
|
|
@@ -2704,104 +2958,257 @@ import { Agent } from "@mariozechner/pi-agent-core";
|
|
|
2704
2958
|
import fs from "fs";
|
|
2705
2959
|
import path2 from "path";
|
|
2706
2960
|
|
|
2707
|
-
// src/chat/
|
|
2708
|
-
var
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
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);
|
|
2961
|
+
// src/chat/runtime/status-format.ts
|
|
2962
|
+
var SLACK_STATUS_MAX_LENGTH = 50;
|
|
2963
|
+
function truncateWithEllipsis(text, maxLength) {
|
|
2964
|
+
if (text.length <= maxLength) {
|
|
2965
|
+
return text;
|
|
2742
2966
|
}
|
|
2743
|
-
return
|
|
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();
|
|
2967
|
+
return `${text.slice(0, Math.max(1, maxLength - 3)).trimEnd()}...`;
|
|
2749
2968
|
}
|
|
2750
|
-
function
|
|
2751
|
-
|
|
2752
|
-
|
|
2969
|
+
function truncateStatusText(text) {
|
|
2970
|
+
const trimmed = text.trim();
|
|
2971
|
+
if (!trimmed) {
|
|
2972
|
+
return "";
|
|
2753
2973
|
}
|
|
2754
|
-
return
|
|
2755
|
-
}
|
|
2756
|
-
function fitsInlineBudget(text, maxChars = MAX_INLINE_CHARS, maxLines = MAX_INLINE_LINES) {
|
|
2757
|
-
return text.length <= maxChars && countSlackLines(text) <= maxLines;
|
|
2974
|
+
return truncateWithEllipsis(trimmed, SLACK_STATUS_MAX_LENGTH);
|
|
2758
2975
|
}
|
|
2759
|
-
function
|
|
2760
|
-
if (
|
|
2761
|
-
return
|
|
2976
|
+
function compactStatusPath(value) {
|
|
2977
|
+
if (typeof value !== "string") {
|
|
2978
|
+
return void 0;
|
|
2762
2979
|
}
|
|
2763
|
-
const
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
return newlineIndex;
|
|
2980
|
+
const trimmed = value.trim();
|
|
2981
|
+
if (!trimmed) {
|
|
2982
|
+
return void 0;
|
|
2767
2983
|
}
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
return spaceIndex;
|
|
2984
|
+
if (trimmed.length <= 80) {
|
|
2985
|
+
return trimmed;
|
|
2771
2986
|
}
|
|
2772
|
-
return
|
|
2987
|
+
return `...${trimmed.slice(-77)}`;
|
|
2773
2988
|
}
|
|
2774
|
-
function
|
|
2775
|
-
if (
|
|
2776
|
-
return
|
|
2989
|
+
function compactStatusText(value, maxLength = 80) {
|
|
2990
|
+
if (typeof value !== "string") {
|
|
2991
|
+
return void 0;
|
|
2777
2992
|
}
|
|
2778
|
-
const
|
|
2779
|
-
if (
|
|
2780
|
-
return
|
|
2993
|
+
const trimmed = value.trim();
|
|
2994
|
+
if (!trimmed) {
|
|
2995
|
+
return void 0;
|
|
2781
2996
|
}
|
|
2782
|
-
return
|
|
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
|
-
};
|
|
2997
|
+
return truncateWithEllipsis(trimmed, maxLength);
|
|
2796
2998
|
}
|
|
2797
|
-
function
|
|
2798
|
-
let
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2999
|
+
function readShellToken(command, startIndex) {
|
|
3000
|
+
let index = startIndex;
|
|
3001
|
+
while (index < command.length && /\s/.test(command[index] ?? "")) {
|
|
3002
|
+
index += 1;
|
|
3003
|
+
}
|
|
3004
|
+
if (index >= command.length) {
|
|
3005
|
+
return void 0;
|
|
3006
|
+
}
|
|
3007
|
+
let token = "";
|
|
3008
|
+
let quote;
|
|
3009
|
+
while (index < command.length) {
|
|
3010
|
+
const char = command[index];
|
|
3011
|
+
if (!char) {
|
|
3012
|
+
break;
|
|
3013
|
+
}
|
|
3014
|
+
if (quote) {
|
|
3015
|
+
if (char === quote) {
|
|
3016
|
+
quote = void 0;
|
|
3017
|
+
index += 1;
|
|
3018
|
+
continue;
|
|
3019
|
+
}
|
|
3020
|
+
if (char === "\\" && quote === '"' && index + 1 < command.length) {
|
|
3021
|
+
token += command[index + 1];
|
|
3022
|
+
index += 2;
|
|
3023
|
+
continue;
|
|
3024
|
+
}
|
|
3025
|
+
token += char;
|
|
3026
|
+
index += 1;
|
|
3027
|
+
continue;
|
|
3028
|
+
}
|
|
3029
|
+
if (/\s/.test(char)) {
|
|
3030
|
+
break;
|
|
3031
|
+
}
|
|
3032
|
+
if (char === '"' || char === "'") {
|
|
3033
|
+
quote = char;
|
|
3034
|
+
index += 1;
|
|
3035
|
+
continue;
|
|
3036
|
+
}
|
|
3037
|
+
if (char === "\\" && index + 1 < command.length) {
|
|
3038
|
+
token += command[index + 1];
|
|
3039
|
+
index += 2;
|
|
3040
|
+
continue;
|
|
3041
|
+
}
|
|
3042
|
+
token += char;
|
|
3043
|
+
index += 1;
|
|
3044
|
+
}
|
|
3045
|
+
return { token, nextIndex: index };
|
|
3046
|
+
}
|
|
3047
|
+
function compactStatusCommand(value) {
|
|
3048
|
+
if (typeof value !== "string") {
|
|
3049
|
+
return void 0;
|
|
3050
|
+
}
|
|
3051
|
+
const trimmed = value.trim();
|
|
3052
|
+
if (!trimmed) {
|
|
3053
|
+
return void 0;
|
|
3054
|
+
}
|
|
3055
|
+
let index = 0;
|
|
3056
|
+
while (index < trimmed.length) {
|
|
3057
|
+
const parsed = readShellToken(trimmed, index);
|
|
3058
|
+
if (!parsed) {
|
|
3059
|
+
return void 0;
|
|
3060
|
+
}
|
|
3061
|
+
index = parsed.nextIndex;
|
|
3062
|
+
if (!parsed.token) {
|
|
3063
|
+
continue;
|
|
3064
|
+
}
|
|
3065
|
+
if (/^[A-Za-z_][A-Za-z0-9_]*=/.test(parsed.token)) {
|
|
3066
|
+
continue;
|
|
3067
|
+
}
|
|
3068
|
+
const normalized = parsed.token.replace(/[\\/]+$/g, "");
|
|
3069
|
+
if (!normalized) {
|
|
3070
|
+
return void 0;
|
|
3071
|
+
}
|
|
3072
|
+
const parts = normalized.split(/[\\/]/).filter((part) => part.length > 0);
|
|
3073
|
+
const command = parts.length > 0 ? parts[parts.length - 1] : normalized;
|
|
3074
|
+
return compactStatusText(command, 40);
|
|
3075
|
+
}
|
|
3076
|
+
return void 0;
|
|
3077
|
+
}
|
|
3078
|
+
function compactStatusFilename(value) {
|
|
3079
|
+
if (typeof value !== "string") {
|
|
3080
|
+
return void 0;
|
|
3081
|
+
}
|
|
3082
|
+
const trimmed = value.trim().replace(/[\\/]+$/g, "");
|
|
3083
|
+
if (!trimmed) {
|
|
3084
|
+
return void 0;
|
|
3085
|
+
}
|
|
3086
|
+
const parts = trimmed.split(/[\\/]/).filter((part) => part.length > 0);
|
|
3087
|
+
const filename = parts.length > 0 ? parts[parts.length - 1] : trimmed;
|
|
3088
|
+
return compactStatusText(filename, 80);
|
|
3089
|
+
}
|
|
3090
|
+
function extractStatusUrlDomain(value) {
|
|
3091
|
+
if (typeof value !== "string") {
|
|
3092
|
+
return void 0;
|
|
3093
|
+
}
|
|
3094
|
+
const trimmed = value.trim();
|
|
3095
|
+
if (!trimmed) {
|
|
3096
|
+
return void 0;
|
|
3097
|
+
}
|
|
3098
|
+
try {
|
|
3099
|
+
const parsed = new URL(trimmed);
|
|
3100
|
+
return parsed.hostname || void 0;
|
|
3101
|
+
} catch {
|
|
3102
|
+
return void 0;
|
|
3103
|
+
}
|
|
3104
|
+
}
|
|
3105
|
+
|
|
3106
|
+
// src/chat/slack/mrkdwn.ts
|
|
3107
|
+
function ensureBlockSpacing(text) {
|
|
3108
|
+
const codeBlockPattern = /^```/;
|
|
3109
|
+
const listItemPattern = /^[-*•]\s|^\d+\.\s/;
|
|
3110
|
+
const lines = text.split("\n");
|
|
3111
|
+
const result = [];
|
|
3112
|
+
let inCodeBlock = false;
|
|
3113
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3114
|
+
const line = lines[i];
|
|
3115
|
+
const isCodeFence = codeBlockPattern.test(line.trimStart());
|
|
3116
|
+
if (isCodeFence) {
|
|
3117
|
+
if (!inCodeBlock) {
|
|
3118
|
+
const prev2 = result.length > 0 ? result[result.length - 1] : void 0;
|
|
3119
|
+
if (prev2 !== void 0 && prev2.trim() !== "") {
|
|
3120
|
+
result.push("");
|
|
3121
|
+
}
|
|
3122
|
+
}
|
|
3123
|
+
inCodeBlock = !inCodeBlock;
|
|
3124
|
+
result.push(line);
|
|
3125
|
+
continue;
|
|
3126
|
+
}
|
|
3127
|
+
if (inCodeBlock) {
|
|
3128
|
+
result.push(line);
|
|
3129
|
+
continue;
|
|
3130
|
+
}
|
|
3131
|
+
const prev = result.length > 0 ? result[result.length - 1] : void 0;
|
|
3132
|
+
if (prev !== void 0 && prev.trim() !== "" && line.trim() !== "" && !(listItemPattern.test(prev.trimStart()) && listItemPattern.test(line.trimStart()))) {
|
|
3133
|
+
result.push("");
|
|
3134
|
+
}
|
|
3135
|
+
result.push(line);
|
|
3136
|
+
}
|
|
3137
|
+
return result.join("\n");
|
|
3138
|
+
}
|
|
3139
|
+
function renderSlackMrkdwn(text) {
|
|
3140
|
+
let normalized = text.replace(/\r\n?/g, "\n").replace(/[ \t]+$/gm, "");
|
|
3141
|
+
normalized = ensureBlockSpacing(normalized);
|
|
3142
|
+
return normalized.replace(/\n{3,}/g, "\n\n").trim();
|
|
3143
|
+
}
|
|
3144
|
+
function normalizeSlackStatusText(text) {
|
|
3145
|
+
const trimmed = text.trim();
|
|
3146
|
+
if (!trimmed) {
|
|
3147
|
+
return "";
|
|
3148
|
+
}
|
|
3149
|
+
return truncateStatusText(trimmed.replace(/(?:\.\s*)+$/, "").trim());
|
|
3150
|
+
}
|
|
3151
|
+
|
|
3152
|
+
// src/chat/slack/output.ts
|
|
3153
|
+
var MAX_INLINE_CHARS = 2200;
|
|
3154
|
+
var MAX_INLINE_LINES = 45;
|
|
3155
|
+
var CONTINUED_MARKER = "\n\n[Continued below]";
|
|
3156
|
+
var INTERRUPTED_MARKER = "\n\n[Response interrupted before completion]";
|
|
3157
|
+
function countSlackLines(text) {
|
|
3158
|
+
if (!text) {
|
|
3159
|
+
return 0;
|
|
3160
|
+
}
|
|
3161
|
+
return text.split("\n").length;
|
|
3162
|
+
}
|
|
3163
|
+
function fitsInlineBudget(text, maxChars = MAX_INLINE_CHARS, maxLines = MAX_INLINE_LINES) {
|
|
3164
|
+
return text.length <= maxChars && countSlackLines(text) <= maxLines;
|
|
3165
|
+
}
|
|
3166
|
+
function findSplitIndex(text, maxChars) {
|
|
3167
|
+
if (text.length <= maxChars) {
|
|
3168
|
+
return text.length;
|
|
3169
|
+
}
|
|
3170
|
+
const bounded = text.slice(0, maxChars);
|
|
3171
|
+
const newlineIndex = bounded.lastIndexOf("\n");
|
|
3172
|
+
if (newlineIndex > 0) {
|
|
3173
|
+
return newlineIndex;
|
|
3174
|
+
}
|
|
3175
|
+
const spaceIndex = bounded.lastIndexOf(" ");
|
|
3176
|
+
if (spaceIndex > 0) {
|
|
3177
|
+
return spaceIndex;
|
|
3178
|
+
}
|
|
3179
|
+
return maxChars;
|
|
3180
|
+
}
|
|
3181
|
+
function splitByLineBudget(text, maxLines) {
|
|
3182
|
+
if (maxLines <= 0) {
|
|
3183
|
+
return "";
|
|
3184
|
+
}
|
|
3185
|
+
const lines = text.split("\n");
|
|
3186
|
+
if (lines.length <= maxLines) {
|
|
3187
|
+
return text;
|
|
3188
|
+
}
|
|
3189
|
+
return lines.slice(0, maxLines).join("\n");
|
|
3190
|
+
}
|
|
3191
|
+
function reserveInlineBudgetForSuffix(suffix, maxChars = MAX_INLINE_CHARS, maxLines = MAX_INLINE_LINES) {
|
|
3192
|
+
return {
|
|
3193
|
+
maxChars: Math.max(1, maxChars - suffix.length),
|
|
3194
|
+
maxLines: Math.max(1, maxLines - Math.max(0, countSlackLines(suffix) - 1))
|
|
3195
|
+
};
|
|
3196
|
+
}
|
|
3197
|
+
function forceSplitBudget(text, budget) {
|
|
3198
|
+
const lineCount = countSlackLines(text);
|
|
3199
|
+
return {
|
|
3200
|
+
maxChars: text.length <= budget.maxChars ? Math.max(1, text.length - 1) : budget.maxChars,
|
|
3201
|
+
maxLines: lineCount <= budget.maxLines ? Math.max(1, lineCount - 1) : budget.maxLines
|
|
3202
|
+
};
|
|
3203
|
+
}
|
|
3204
|
+
function getFenceContinuation(text) {
|
|
3205
|
+
let open;
|
|
3206
|
+
for (const line of text.split("\n")) {
|
|
3207
|
+
const trimmed = line.trimStart();
|
|
3208
|
+
const openerMatch = trimmed.match(/^(`{3,}|~{3,})(.*)$/);
|
|
3209
|
+
if (!openerMatch) {
|
|
3210
|
+
continue;
|
|
3211
|
+
}
|
|
2805
3212
|
if (!open) {
|
|
2806
3213
|
open = {
|
|
2807
3214
|
fence: openerMatch[1],
|
|
@@ -2827,6 +3234,9 @@ function appendSlackSuffix(text, marker) {
|
|
|
2827
3234
|
const carryover = getFenceContinuation(text);
|
|
2828
3235
|
return `${text}${carryover?.closeSuffix ?? ""}${marker}`;
|
|
2829
3236
|
}
|
|
3237
|
+
function stripTrailingContinuationMarker(text) {
|
|
3238
|
+
return text.endsWith(CONTINUED_MARKER) ? text.slice(0, -CONTINUED_MARKER.length) : text;
|
|
3239
|
+
}
|
|
2830
3240
|
function takeSlackContinuationChunk(text, budget) {
|
|
2831
3241
|
let { prefix, rest } = takeSlackInlinePrefix(text, budget);
|
|
2832
3242
|
if (!rest) {
|
|
@@ -2902,7 +3312,7 @@ function takeSlackInlinePrefix(text, options) {
|
|
|
2902
3312
|
};
|
|
2903
3313
|
}
|
|
2904
3314
|
function splitSlackReplyText(text, options) {
|
|
2905
|
-
const normalized =
|
|
3315
|
+
const normalized = renderSlackMrkdwn(text);
|
|
2906
3316
|
if (!normalized) {
|
|
2907
3317
|
return [];
|
|
2908
3318
|
}
|
|
@@ -2924,21 +3334,16 @@ function splitSlackReplyText(text, options) {
|
|
|
2924
3334
|
chunks.push(renderedPrefix);
|
|
2925
3335
|
remaining = rest;
|
|
2926
3336
|
}
|
|
3337
|
+
if (chunks.length === 2) {
|
|
3338
|
+
chunks[0] = stripTrailingContinuationMarker(chunks[0] ?? "");
|
|
3339
|
+
}
|
|
2927
3340
|
return chunks;
|
|
2928
3341
|
}
|
|
2929
|
-
function getSlackInterruptionMarker() {
|
|
2930
|
-
return INTERRUPTED_MARKER;
|
|
2931
|
-
}
|
|
2932
3342
|
function getSlackContinuationBudget() {
|
|
2933
3343
|
return reserveInlineBudgetForSuffix(CONTINUED_MARKER);
|
|
2934
3344
|
}
|
|
2935
|
-
function getSlackStreamingContinuationBudget() {
|
|
2936
|
-
return reserveInlineBudgetForSuffix(
|
|
2937
|
-
`${STREAMING_FENCE_CLOSE_GUARD}${CONTINUED_MARKER}`
|
|
2938
|
-
);
|
|
2939
|
-
}
|
|
2940
3345
|
function buildSlackOutputMessage(text, files) {
|
|
2941
|
-
const normalized =
|
|
3346
|
+
const normalized = renderSlackMrkdwn(text);
|
|
2942
3347
|
const fileCount = files?.length ?? 0;
|
|
2943
3348
|
if (!normalized) {
|
|
2944
3349
|
if (fileCount > 0) {
|
|
@@ -3401,12 +3806,12 @@ function buildSystemPrompt(params) {
|
|
|
3401
3806
|
[
|
|
3402
3807
|
"Always produce output that follows this contract:",
|
|
3403
3808
|
`<output format="slack-mrkdwn" max_inline_chars="${slackOutputPolicy.maxInlineChars}" max_inline_lines="${slackOutputPolicy.maxInlineLines}">`,
|
|
3404
|
-
"- Use
|
|
3809
|
+
"- Use Slack-friendly markdown, not full CommonMark. Prefer bold section labels over markdown headings, and use bullets and short code blocks when helpful.",
|
|
3405
3810
|
"- Keep normal responses brief and scannable.",
|
|
3406
3811
|
"- If depth is needed, start with a concise summary and then provide fuller detail.",
|
|
3407
3812
|
"- 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
3813
|
"- Do not narrate tool execution or repeated status updates in the visible reply.",
|
|
3409
|
-
"- Avoid tables unless explicitly requested.",
|
|
3814
|
+
"- Avoid tables and markdown links like `[label](url)` unless explicitly requested. Prefer plain URLs or Slack-native entities when exact rendering matters.",
|
|
3410
3815
|
"- End every turn with a final user-facing markdown response.",
|
|
3411
3816
|
"</output>"
|
|
3412
3817
|
].join("\n")
|
|
@@ -5557,103 +5962,29 @@ function createSearchToolsTool(mcpToolManager, getActiveSkills) {
|
|
|
5557
5962
|
// src/chat/tools/slack/channel-list-messages.ts
|
|
5558
5963
|
import { Type as Type7 } from "@sinclair/typebox";
|
|
5559
5964
|
|
|
5560
|
-
// src/chat/slack/emoji.ts
|
|
5561
|
-
var SLACK_EMOJI_NAME_RE = /^(?:[a-z0-9_+-]+)(?:::(?:skin-tone-[2-6]))?$/;
|
|
5562
|
-
function normalizeSlackEmojiName(value) {
|
|
5563
|
-
const trimmed = value.trim().toLowerCase();
|
|
5564
|
-
if (!trimmed) {
|
|
5565
|
-
return null;
|
|
5566
|
-
}
|
|
5567
|
-
const normalized = trimmed.startsWith(":") && trimmed.endsWith(":") ? trimmed.slice(1, -1) : trimmed;
|
|
5568
|
-
return SLACK_EMOJI_NAME_RE.test(normalized) ? normalized : null;
|
|
5569
|
-
}
|
|
5570
|
-
|
|
5571
5965
|
// src/chat/slack/channel.ts
|
|
5572
|
-
async function
|
|
5966
|
+
async function listChannelMessages(input) {
|
|
5573
5967
|
const client2 = getSlackClient();
|
|
5574
5968
|
const channelId = normalizeSlackConversationId(input.channelId);
|
|
5575
5969
|
if (!channelId) {
|
|
5576
|
-
throw new Error(
|
|
5577
|
-
"Slack channel message posting requires a valid channel ID"
|
|
5578
|
-
);
|
|
5579
|
-
}
|
|
5580
|
-
const response = await withSlackRetries(
|
|
5581
|
-
() => client2.chat.postMessage({
|
|
5582
|
-
channel: channelId,
|
|
5583
|
-
text: input.text,
|
|
5584
|
-
mrkdwn: true
|
|
5585
|
-
}),
|
|
5586
|
-
3,
|
|
5587
|
-
{ action: "chat.postMessage" }
|
|
5588
|
-
);
|
|
5589
|
-
if (!response.ts) {
|
|
5590
|
-
throw new Error("Slack channel message posted without ts");
|
|
5970
|
+
throw new Error("Slack channel history lookup requires a valid channel ID");
|
|
5591
5971
|
}
|
|
5592
|
-
|
|
5593
|
-
|
|
5594
|
-
|
|
5595
|
-
|
|
5972
|
+
const targetLimit = Math.max(1, Math.min(input.limit, 1e3));
|
|
5973
|
+
const maxPages = Math.max(1, Math.min(input.maxPages ?? 5, 10));
|
|
5974
|
+
const messages = [];
|
|
5975
|
+
let cursor = input.cursor;
|
|
5976
|
+
let pages = 0;
|
|
5977
|
+
while (messages.length < targetLimit && pages < maxPages) {
|
|
5978
|
+
pages += 1;
|
|
5979
|
+
const pageLimit = Math.max(1, Math.min(200, targetLimit - messages.length));
|
|
5980
|
+
const response = await withSlackRetries(
|
|
5981
|
+
() => client2.conversations.history({
|
|
5596
5982
|
channel: channelId,
|
|
5597
|
-
|
|
5598
|
-
|
|
5599
|
-
|
|
5600
|
-
|
|
5601
|
-
|
|
5602
|
-
permalink = permalinkResponse.permalink;
|
|
5603
|
-
} catch {
|
|
5604
|
-
}
|
|
5605
|
-
return {
|
|
5606
|
-
ts: response.ts,
|
|
5607
|
-
permalink
|
|
5608
|
-
};
|
|
5609
|
-
}
|
|
5610
|
-
async function addReactionToMessage(input) {
|
|
5611
|
-
const client2 = getSlackClient();
|
|
5612
|
-
const channelId = normalizeSlackConversationId(input.channelId);
|
|
5613
|
-
if (!channelId) {
|
|
5614
|
-
throw new Error("Slack reaction requires a valid channel ID");
|
|
5615
|
-
}
|
|
5616
|
-
const timestamp = input.timestamp.trim();
|
|
5617
|
-
if (!timestamp) {
|
|
5618
|
-
throw new Error("Slack reaction requires a target message timestamp");
|
|
5619
|
-
}
|
|
5620
|
-
const emoji = normalizeSlackEmojiName(input.emoji);
|
|
5621
|
-
if (!emoji) {
|
|
5622
|
-
throw new Error("Slack reaction requires a valid emoji alias name");
|
|
5623
|
-
}
|
|
5624
|
-
await withSlackRetries(
|
|
5625
|
-
() => client2.reactions.add({
|
|
5626
|
-
channel: channelId,
|
|
5627
|
-
timestamp,
|
|
5628
|
-
name: emoji
|
|
5629
|
-
}),
|
|
5630
|
-
3,
|
|
5631
|
-
{ action: "reactions.add" }
|
|
5632
|
-
);
|
|
5633
|
-
return { ok: true };
|
|
5634
|
-
}
|
|
5635
|
-
async function listChannelMessages(input) {
|
|
5636
|
-
const client2 = getSlackClient();
|
|
5637
|
-
const channelId = normalizeSlackConversationId(input.channelId);
|
|
5638
|
-
if (!channelId) {
|
|
5639
|
-
throw new Error("Slack channel history lookup requires a valid channel ID");
|
|
5640
|
-
}
|
|
5641
|
-
const targetLimit = Math.max(1, Math.min(input.limit, 1e3));
|
|
5642
|
-
const maxPages = Math.max(1, Math.min(input.maxPages ?? 5, 10));
|
|
5643
|
-
const messages = [];
|
|
5644
|
-
let cursor = input.cursor;
|
|
5645
|
-
let pages = 0;
|
|
5646
|
-
while (messages.length < targetLimit && pages < maxPages) {
|
|
5647
|
-
pages += 1;
|
|
5648
|
-
const pageLimit = Math.max(1, Math.min(200, targetLimit - messages.length));
|
|
5649
|
-
const response = await withSlackRetries(
|
|
5650
|
-
() => client2.conversations.history({
|
|
5651
|
-
channel: channelId,
|
|
5652
|
-
limit: pageLimit,
|
|
5653
|
-
cursor,
|
|
5654
|
-
oldest: input.oldest,
|
|
5655
|
-
latest: input.latest,
|
|
5656
|
-
inclusive: input.inclusive
|
|
5983
|
+
limit: pageLimit,
|
|
5984
|
+
cursor,
|
|
5985
|
+
oldest: input.oldest,
|
|
5986
|
+
latest: input.latest,
|
|
5987
|
+
inclusive: input.inclusive
|
|
5657
5988
|
}),
|
|
5658
5989
|
3,
|
|
5659
5990
|
{ action: "conversations.history" }
|
|
@@ -5841,9 +6172,10 @@ function createSlackChannelPostMessageTool(context, state) {
|
|
|
5841
6172
|
deduplicated: true
|
|
5842
6173
|
};
|
|
5843
6174
|
}
|
|
5844
|
-
const posted = await
|
|
6175
|
+
const posted = await postSlackMessage({
|
|
5845
6176
|
channelId: targetChannelId,
|
|
5846
|
-
text
|
|
6177
|
+
text,
|
|
6178
|
+
includePermalink: true
|
|
5847
6179
|
});
|
|
5848
6180
|
const response = {
|
|
5849
6181
|
ok: true,
|
|
@@ -7450,152 +7782,7 @@ function throwSandboxOperationError(action, error, includeMissingPath = false) {
|
|
|
7450
7782
|
import { Sandbox } from "@vercel/sandbox";
|
|
7451
7783
|
import { createBashTool as createBashTool2 } from "bash-tool";
|
|
7452
7784
|
|
|
7453
|
-
// src/chat/
|
|
7454
|
-
var SLACK_STATUS_MAX_LENGTH = 50;
|
|
7455
|
-
function truncateWithEllipsis(text, maxLength) {
|
|
7456
|
-
if (text.length <= maxLength) {
|
|
7457
|
-
return text;
|
|
7458
|
-
}
|
|
7459
|
-
return `${text.slice(0, Math.max(1, maxLength - 3)).trimEnd()}...`;
|
|
7460
|
-
}
|
|
7461
|
-
function truncateStatusText(text) {
|
|
7462
|
-
const trimmed = text.trim();
|
|
7463
|
-
if (!trimmed) {
|
|
7464
|
-
return "";
|
|
7465
|
-
}
|
|
7466
|
-
return truncateWithEllipsis(trimmed, SLACK_STATUS_MAX_LENGTH);
|
|
7467
|
-
}
|
|
7468
|
-
function compactStatusPath(value) {
|
|
7469
|
-
if (typeof value !== "string") {
|
|
7470
|
-
return void 0;
|
|
7471
|
-
}
|
|
7472
|
-
const trimmed = value.trim();
|
|
7473
|
-
if (!trimmed) {
|
|
7474
|
-
return void 0;
|
|
7475
|
-
}
|
|
7476
|
-
if (trimmed.length <= 80) {
|
|
7477
|
-
return trimmed;
|
|
7478
|
-
}
|
|
7479
|
-
return `...${trimmed.slice(-77)}`;
|
|
7480
|
-
}
|
|
7481
|
-
function compactStatusText(value, maxLength = 80) {
|
|
7482
|
-
if (typeof value !== "string") {
|
|
7483
|
-
return void 0;
|
|
7484
|
-
}
|
|
7485
|
-
const trimmed = value.trim();
|
|
7486
|
-
if (!trimmed) {
|
|
7487
|
-
return void 0;
|
|
7488
|
-
}
|
|
7489
|
-
return truncateWithEllipsis(trimmed, maxLength);
|
|
7490
|
-
}
|
|
7491
|
-
function readShellToken(command, startIndex) {
|
|
7492
|
-
let index = startIndex;
|
|
7493
|
-
while (index < command.length && /\s/.test(command[index] ?? "")) {
|
|
7494
|
-
index += 1;
|
|
7495
|
-
}
|
|
7496
|
-
if (index >= command.length) {
|
|
7497
|
-
return void 0;
|
|
7498
|
-
}
|
|
7499
|
-
let token = "";
|
|
7500
|
-
let quote;
|
|
7501
|
-
while (index < command.length) {
|
|
7502
|
-
const char = command[index];
|
|
7503
|
-
if (!char) {
|
|
7504
|
-
break;
|
|
7505
|
-
}
|
|
7506
|
-
if (quote) {
|
|
7507
|
-
if (char === quote) {
|
|
7508
|
-
quote = void 0;
|
|
7509
|
-
index += 1;
|
|
7510
|
-
continue;
|
|
7511
|
-
}
|
|
7512
|
-
if (char === "\\" && quote === '"' && index + 1 < command.length) {
|
|
7513
|
-
token += command[index + 1];
|
|
7514
|
-
index += 2;
|
|
7515
|
-
continue;
|
|
7516
|
-
}
|
|
7517
|
-
token += char;
|
|
7518
|
-
index += 1;
|
|
7519
|
-
continue;
|
|
7520
|
-
}
|
|
7521
|
-
if (/\s/.test(char)) {
|
|
7522
|
-
break;
|
|
7523
|
-
}
|
|
7524
|
-
if (char === '"' || char === "'") {
|
|
7525
|
-
quote = char;
|
|
7526
|
-
index += 1;
|
|
7527
|
-
continue;
|
|
7528
|
-
}
|
|
7529
|
-
if (char === "\\" && index + 1 < command.length) {
|
|
7530
|
-
token += command[index + 1];
|
|
7531
|
-
index += 2;
|
|
7532
|
-
continue;
|
|
7533
|
-
}
|
|
7534
|
-
token += char;
|
|
7535
|
-
index += 1;
|
|
7536
|
-
}
|
|
7537
|
-
return { token, nextIndex: index };
|
|
7538
|
-
}
|
|
7539
|
-
function compactStatusCommand(value) {
|
|
7540
|
-
if (typeof value !== "string") {
|
|
7541
|
-
return void 0;
|
|
7542
|
-
}
|
|
7543
|
-
const trimmed = value.trim();
|
|
7544
|
-
if (!trimmed) {
|
|
7545
|
-
return void 0;
|
|
7546
|
-
}
|
|
7547
|
-
let index = 0;
|
|
7548
|
-
while (index < trimmed.length) {
|
|
7549
|
-
const parsed = readShellToken(trimmed, index);
|
|
7550
|
-
if (!parsed) {
|
|
7551
|
-
return void 0;
|
|
7552
|
-
}
|
|
7553
|
-
index = parsed.nextIndex;
|
|
7554
|
-
if (!parsed.token) {
|
|
7555
|
-
continue;
|
|
7556
|
-
}
|
|
7557
|
-
if (/^[A-Za-z_][A-Za-z0-9_]*=/.test(parsed.token)) {
|
|
7558
|
-
continue;
|
|
7559
|
-
}
|
|
7560
|
-
const normalized = parsed.token.replace(/[\\/]+$/g, "");
|
|
7561
|
-
if (!normalized) {
|
|
7562
|
-
return void 0;
|
|
7563
|
-
}
|
|
7564
|
-
const parts = normalized.split(/[\\/]/).filter((part) => part.length > 0);
|
|
7565
|
-
const command = parts.length > 0 ? parts[parts.length - 1] : normalized;
|
|
7566
|
-
return compactStatusText(command, 40);
|
|
7567
|
-
}
|
|
7568
|
-
return void 0;
|
|
7569
|
-
}
|
|
7570
|
-
function compactStatusFilename(value) {
|
|
7571
|
-
if (typeof value !== "string") {
|
|
7572
|
-
return void 0;
|
|
7573
|
-
}
|
|
7574
|
-
const trimmed = value.trim().replace(/[\\/]+$/g, "");
|
|
7575
|
-
if (!trimmed) {
|
|
7576
|
-
return void 0;
|
|
7577
|
-
}
|
|
7578
|
-
const parts = trimmed.split(/[\\/]/).filter((part) => part.length > 0);
|
|
7579
|
-
const filename = parts.length > 0 ? parts[parts.length - 1] : trimmed;
|
|
7580
|
-
return compactStatusText(filename, 80);
|
|
7581
|
-
}
|
|
7582
|
-
function extractStatusUrlDomain(value) {
|
|
7583
|
-
if (typeof value !== "string") {
|
|
7584
|
-
return void 0;
|
|
7585
|
-
}
|
|
7586
|
-
const trimmed = value.trim();
|
|
7587
|
-
if (!trimmed) {
|
|
7588
|
-
return void 0;
|
|
7589
|
-
}
|
|
7590
|
-
try {
|
|
7591
|
-
const parsed = new URL(trimmed);
|
|
7592
|
-
return parsed.hostname || void 0;
|
|
7593
|
-
} catch {
|
|
7594
|
-
return void 0;
|
|
7595
|
-
}
|
|
7596
|
-
}
|
|
7597
|
-
|
|
7598
|
-
// src/chat/runtime/assistant-status.ts
|
|
7785
|
+
// src/chat/slack/assistant-thread/status-render.ts
|
|
7599
7786
|
var STATUS_PATTERNS = {
|
|
7600
7787
|
thinking: {
|
|
7601
7788
|
defaultContext: "\u2026",
|
|
@@ -7649,17 +7836,10 @@ var STATUS_PATTERNS = {
|
|
|
7649
7836
|
function makeAssistantStatus(kind, context) {
|
|
7650
7837
|
return { kind, ...context ? { context } : {} };
|
|
7651
7838
|
}
|
|
7652
|
-
function
|
|
7653
|
-
const trimmed = text.trim();
|
|
7654
|
-
if (!trimmed) {
|
|
7655
|
-
return "";
|
|
7656
|
-
}
|
|
7657
|
-
return truncateStatusText(trimmed.replace(/(?:\.\s*)+$/, "").trim());
|
|
7658
|
-
}
|
|
7659
|
-
function buildAssistantStatusPresentation(args) {
|
|
7839
|
+
function renderAssistantStatus(args) {
|
|
7660
7840
|
const random = args.random ?? Math.random;
|
|
7661
7841
|
const pattern = STATUS_PATTERNS[args.status.kind];
|
|
7662
|
-
const context =
|
|
7842
|
+
const context = normalizeSlackStatusText(args.status.context ?? "") || pattern.defaultContext;
|
|
7663
7843
|
const index = Math.floor(random() * pattern.variants.length);
|
|
7664
7844
|
const verb = pattern.variants[index] ?? pattern.variants[0];
|
|
7665
7845
|
const visible = truncateStatusText(`${verb} ${context}`);
|
|
@@ -7671,46 +7851,274 @@ function buildAssistantStatusPresentation(args) {
|
|
|
7671
7851
|
suggestions: Array.from(/* @__PURE__ */ new Set([visible, hint]))
|
|
7672
7852
|
};
|
|
7673
7853
|
}
|
|
7674
|
-
|
|
7854
|
+
|
|
7855
|
+
// src/chat/slack/assistant-thread/status-scheduler.ts
|
|
7856
|
+
var STATUS_UPDATE_DEBOUNCE_MS = 1e3;
|
|
7857
|
+
var STATUS_MIN_VISIBLE_MS = 1200;
|
|
7858
|
+
var STATUS_ROTATION_INTERVAL_MS = 3e4;
|
|
7859
|
+
function createAssistantStatusScheduler(args) {
|
|
7860
|
+
const now = args.now ?? (() => Date.now());
|
|
7861
|
+
const setTimer = args.setTimer ?? ((callback, delayMs) => setTimeout(callback, delayMs));
|
|
7862
|
+
const clearTimer = args.clearTimer ?? ((timer) => clearTimeout(timer));
|
|
7863
|
+
const random = args.random ?? Math.random;
|
|
7864
|
+
let active = false;
|
|
7865
|
+
let currentKey = "";
|
|
7866
|
+
let currentStatus = makeAssistantStatus("thinking");
|
|
7867
|
+
let currentVisibleStatus = "";
|
|
7868
|
+
let lastStatusAt = 0;
|
|
7869
|
+
let pendingStatus = null;
|
|
7870
|
+
let pendingKey = "";
|
|
7871
|
+
let pendingTimer = null;
|
|
7872
|
+
let rotationTimer = null;
|
|
7873
|
+
let inflightStatusUpdate = Promise.resolve();
|
|
7874
|
+
const enqueueStatusUpdate = (task) => {
|
|
7875
|
+
const request = inflightStatusUpdate.catch(() => void 0).then(async () => {
|
|
7876
|
+
await task();
|
|
7877
|
+
});
|
|
7878
|
+
inflightStatusUpdate = request.catch(() => void 0);
|
|
7879
|
+
return request;
|
|
7880
|
+
};
|
|
7881
|
+
const scheduleRotation = () => {
|
|
7882
|
+
if (rotationTimer) {
|
|
7883
|
+
clearTimer(rotationTimer);
|
|
7884
|
+
rotationTimer = null;
|
|
7885
|
+
}
|
|
7886
|
+
if (!active || !currentVisibleStatus) {
|
|
7887
|
+
return;
|
|
7888
|
+
}
|
|
7889
|
+
rotationTimer = setTimer(() => {
|
|
7890
|
+
rotationTimer = null;
|
|
7891
|
+
if (!active || !currentVisibleStatus) {
|
|
7892
|
+
return;
|
|
7893
|
+
}
|
|
7894
|
+
void postRenderedStatus(currentStatus);
|
|
7895
|
+
}, STATUS_ROTATION_INTERVAL_MS);
|
|
7896
|
+
};
|
|
7897
|
+
const postStatus = async (text, suggestions) => {
|
|
7898
|
+
if (!text && !currentVisibleStatus) {
|
|
7899
|
+
return;
|
|
7900
|
+
}
|
|
7901
|
+
currentVisibleStatus = text;
|
|
7902
|
+
lastStatusAt = now();
|
|
7903
|
+
scheduleRotation();
|
|
7904
|
+
await enqueueStatusUpdate(async () => {
|
|
7905
|
+
await args.sendStatus(text, suggestions);
|
|
7906
|
+
});
|
|
7907
|
+
};
|
|
7908
|
+
const postRenderedStatus = async (status) => {
|
|
7909
|
+
const presentation = renderAssistantStatus({
|
|
7910
|
+
status,
|
|
7911
|
+
random
|
|
7912
|
+
});
|
|
7913
|
+
currentStatus = status;
|
|
7914
|
+
currentKey = presentation.key;
|
|
7915
|
+
await postStatus(presentation.visible, presentation.suggestions);
|
|
7916
|
+
};
|
|
7917
|
+
const clearPending = () => {
|
|
7918
|
+
if (pendingTimer) {
|
|
7919
|
+
clearTimer(pendingTimer);
|
|
7920
|
+
pendingTimer = null;
|
|
7921
|
+
}
|
|
7922
|
+
pendingStatus = null;
|
|
7923
|
+
pendingKey = "";
|
|
7924
|
+
};
|
|
7925
|
+
const flushPending = async () => {
|
|
7926
|
+
if (!active || !pendingStatus) {
|
|
7927
|
+
clearPending();
|
|
7928
|
+
return;
|
|
7929
|
+
}
|
|
7930
|
+
const next = pendingStatus;
|
|
7931
|
+
clearPending();
|
|
7932
|
+
const nextPresentation = renderAssistantStatus({
|
|
7933
|
+
status: next,
|
|
7934
|
+
random
|
|
7935
|
+
});
|
|
7936
|
+
if (nextPresentation.key !== currentKey) {
|
|
7937
|
+
await postRenderedStatus(next);
|
|
7938
|
+
}
|
|
7939
|
+
};
|
|
7675
7940
|
return {
|
|
7676
|
-
|
|
7677
|
-
|
|
7678
|
-
|
|
7679
|
-
|
|
7680
|
-
|
|
7941
|
+
start() {
|
|
7942
|
+
active = true;
|
|
7943
|
+
clearPending();
|
|
7944
|
+
currentStatus = makeAssistantStatus("thinking");
|
|
7945
|
+
currentKey = "";
|
|
7946
|
+
void postRenderedStatus(currentStatus);
|
|
7947
|
+
},
|
|
7948
|
+
async stop() {
|
|
7949
|
+
active = false;
|
|
7950
|
+
clearPending();
|
|
7951
|
+
if (rotationTimer) {
|
|
7952
|
+
clearTimer(rotationTimer);
|
|
7953
|
+
rotationTimer = null;
|
|
7954
|
+
}
|
|
7955
|
+
currentKey = "";
|
|
7956
|
+
await postStatus("");
|
|
7957
|
+
},
|
|
7958
|
+
update(status) {
|
|
7959
|
+
if (!active) {
|
|
7960
|
+
return;
|
|
7961
|
+
}
|
|
7962
|
+
const presentation = renderAssistantStatus({
|
|
7963
|
+
status,
|
|
7964
|
+
random
|
|
7965
|
+
});
|
|
7966
|
+
if (!presentation.visible) {
|
|
7967
|
+
return;
|
|
7968
|
+
}
|
|
7969
|
+
if (presentation.key === currentKey || presentation.key === pendingKey) {
|
|
7970
|
+
return;
|
|
7971
|
+
}
|
|
7972
|
+
const elapsed = now() - lastStatusAt;
|
|
7973
|
+
const waitMs = Math.max(
|
|
7974
|
+
STATUS_UPDATE_DEBOUNCE_MS - elapsed,
|
|
7975
|
+
STATUS_MIN_VISIBLE_MS - elapsed,
|
|
7976
|
+
0
|
|
7977
|
+
);
|
|
7978
|
+
if (waitMs <= 0) {
|
|
7979
|
+
clearPending();
|
|
7980
|
+
void postRenderedStatus(status);
|
|
7981
|
+
return;
|
|
7982
|
+
}
|
|
7983
|
+
pendingStatus = status;
|
|
7984
|
+
pendingKey = presentation.key;
|
|
7985
|
+
if (pendingTimer) {
|
|
7986
|
+
return;
|
|
7681
7987
|
}
|
|
7988
|
+
pendingTimer = setTimer(
|
|
7989
|
+
() => {
|
|
7990
|
+
pendingTimer = null;
|
|
7991
|
+
void flushPending();
|
|
7992
|
+
},
|
|
7993
|
+
Math.max(1, waitMs)
|
|
7994
|
+
);
|
|
7995
|
+
}
|
|
7996
|
+
};
|
|
7997
|
+
}
|
|
7998
|
+
|
|
7999
|
+
// src/chat/slack/assistant-thread/status-send.ts
|
|
8000
|
+
function createSlackAdapterStatusSender(args) {
|
|
8001
|
+
const adapter = args.getSlackAdapter();
|
|
8002
|
+
const boundToken = getSlackAdapterRequestToken(adapter);
|
|
8003
|
+
return async (text, suggestions) => {
|
|
8004
|
+
const channelId = args.channelId;
|
|
8005
|
+
const threadTs = args.threadTs;
|
|
8006
|
+
if (!channelId || !threadTs) {
|
|
8007
|
+
return;
|
|
8008
|
+
}
|
|
8009
|
+
const normalizedChannelId = normalizeSlackConversationId(channelId);
|
|
8010
|
+
if (!normalizedChannelId) {
|
|
8011
|
+
return;
|
|
8012
|
+
}
|
|
8013
|
+
try {
|
|
8014
|
+
await runWithBoundSlackToken(
|
|
8015
|
+
adapter,
|
|
8016
|
+
boundToken,
|
|
8017
|
+
() => adapter.setAssistantStatus(
|
|
8018
|
+
normalizedChannelId,
|
|
8019
|
+
threadTs,
|
|
8020
|
+
text,
|
|
8021
|
+
suggestions
|
|
8022
|
+
)
|
|
8023
|
+
);
|
|
8024
|
+
} catch (error) {
|
|
8025
|
+
logAssistantStatusFailure({
|
|
8026
|
+
status: text,
|
|
8027
|
+
error,
|
|
8028
|
+
channelId,
|
|
8029
|
+
normalizedChannelId,
|
|
8030
|
+
threadTs
|
|
8031
|
+
});
|
|
8032
|
+
}
|
|
8033
|
+
};
|
|
8034
|
+
}
|
|
8035
|
+
function createSlackWebApiStatusSender(args) {
|
|
8036
|
+
const getClient2 = args.getSlackClient ?? getSlackClient;
|
|
8037
|
+
return async (text, suggestions) => {
|
|
8038
|
+
const channelId = args.channelId;
|
|
8039
|
+
const threadTs = args.threadTs;
|
|
8040
|
+
if (!channelId || !threadTs) {
|
|
8041
|
+
return;
|
|
7682
8042
|
}
|
|
7683
|
-
|
|
7684
|
-
|
|
7685
|
-
|
|
7686
|
-
|
|
7687
|
-
|
|
7688
|
-
|
|
7689
|
-
|
|
7690
|
-
|
|
7691
|
-
|
|
7692
|
-
|
|
7693
|
-
|
|
7694
|
-
|
|
7695
|
-
|
|
7696
|
-
|
|
7697
|
-
|
|
7698
|
-
|
|
8043
|
+
const normalizedChannelId = normalizeSlackConversationId(channelId);
|
|
8044
|
+
if (!normalizedChannelId) {
|
|
8045
|
+
return;
|
|
8046
|
+
}
|
|
8047
|
+
try {
|
|
8048
|
+
await getClient2().assistant.threads.setStatus({
|
|
8049
|
+
channel_id: normalizedChannelId,
|
|
8050
|
+
thread_ts: threadTs,
|
|
8051
|
+
status: text,
|
|
8052
|
+
...suggestions ? { loading_messages: suggestions } : {}
|
|
8053
|
+
});
|
|
8054
|
+
} catch (error) {
|
|
8055
|
+
logAssistantStatusFailure({
|
|
8056
|
+
status: text,
|
|
8057
|
+
error,
|
|
8058
|
+
channelId,
|
|
8059
|
+
normalizedChannelId,
|
|
8060
|
+
threadTs
|
|
8061
|
+
});
|
|
7699
8062
|
}
|
|
7700
8063
|
};
|
|
7701
8064
|
}
|
|
7702
|
-
function
|
|
8065
|
+
function getSlackAdapterRequestToken(adapter) {
|
|
8066
|
+
const token = adapter.requestContext?.getStore()?.token;
|
|
8067
|
+
if (typeof token !== "string") {
|
|
8068
|
+
return void 0;
|
|
8069
|
+
}
|
|
8070
|
+
const trimmed = token.trim();
|
|
8071
|
+
return trimmed || void 0;
|
|
8072
|
+
}
|
|
8073
|
+
async function runWithBoundSlackToken(adapter, token, task) {
|
|
8074
|
+
if (!token) {
|
|
8075
|
+
return await task();
|
|
8076
|
+
}
|
|
8077
|
+
return await adapter.withBotToken(token, task);
|
|
8078
|
+
}
|
|
8079
|
+
function logAssistantStatusFailure(args) {
|
|
7703
8080
|
logWarn(
|
|
7704
8081
|
"assistant_status_update_failed",
|
|
7705
8082
|
{},
|
|
7706
8083
|
{
|
|
7707
|
-
"app.slack.status_text": status || "(clear)",
|
|
7708
|
-
"
|
|
8084
|
+
"app.slack.status_text": args.status || "(clear)",
|
|
8085
|
+
"app.slack.channel_id_raw": args.channelId,
|
|
8086
|
+
"app.slack.channel_id": args.normalizedChannelId,
|
|
8087
|
+
"app.slack.thread_ts": args.threadTs,
|
|
8088
|
+
"error.message": args.error instanceof Error ? args.error.message : String(args.error)
|
|
7709
8089
|
},
|
|
7710
|
-
|
|
8090
|
+
`Failed to update assistant status channel=${args.normalizedChannelId} raw=${args.channelId} thread=${args.threadTs}`
|
|
7711
8091
|
);
|
|
7712
8092
|
}
|
|
7713
8093
|
|
|
8094
|
+
// src/chat/slack/assistant-thread/status.ts
|
|
8095
|
+
function createSlackAdapterAssistantStatusSession(args) {
|
|
8096
|
+
return createAssistantStatusScheduler({
|
|
8097
|
+
sendStatus: createSlackAdapterStatusSender({
|
|
8098
|
+
channelId: args.channelId,
|
|
8099
|
+
threadTs: args.threadTs,
|
|
8100
|
+
getSlackAdapter: args.getSlackAdapter
|
|
8101
|
+
}),
|
|
8102
|
+
now: args.now,
|
|
8103
|
+
setTimer: args.setTimer,
|
|
8104
|
+
clearTimer: args.clearTimer,
|
|
8105
|
+
random: args.random
|
|
8106
|
+
});
|
|
8107
|
+
}
|
|
8108
|
+
function createSlackWebApiAssistantStatusSession(args) {
|
|
8109
|
+
return createAssistantStatusScheduler({
|
|
8110
|
+
sendStatus: createSlackWebApiStatusSender({
|
|
8111
|
+
channelId: args.channelId,
|
|
8112
|
+
threadTs: args.threadTs,
|
|
8113
|
+
getSlackClient: args.getSlackClient
|
|
8114
|
+
}),
|
|
8115
|
+
now: args.now,
|
|
8116
|
+
setTimer: args.setTimer,
|
|
8117
|
+
clearTimer: args.clearTimer,
|
|
8118
|
+
random: args.random
|
|
8119
|
+
});
|
|
8120
|
+
}
|
|
8121
|
+
|
|
7714
8122
|
// src/chat/sandbox/skill-sync.ts
|
|
7715
8123
|
import fs3 from "fs/promises";
|
|
7716
8124
|
import path5 from "path";
|
|
@@ -7832,7 +8240,11 @@ function pickFields(record, csv) {
|
|
|
7832
8240
|
}
|
|
7833
8241
|
|
|
7834
8242
|
function outputJson(value) {
|
|
7835
|
-
process.stdout.
|
|
8243
|
+
fs.writeFileSync(process.stdout.fd, JSON.stringify(value, null, 2) + "\\n");
|
|
8244
|
+
}
|
|
8245
|
+
|
|
8246
|
+
function outputText(value) {
|
|
8247
|
+
fs.writeFileSync(process.stdout.fd, value);
|
|
7836
8248
|
}
|
|
7837
8249
|
|
|
7838
8250
|
function fallbackToRealGh() {
|
|
@@ -7848,12 +8260,12 @@ function fallbackToRealGh() {
|
|
|
7848
8260
|
}
|
|
7849
8261
|
|
|
7850
8262
|
if (args.length === 0 || args[0] === "--version" || args[0] === "version") {
|
|
7851
|
-
|
|
8263
|
+
outputText("gh version 2.0.0 (junior-eval)\\n");
|
|
7852
8264
|
process.exit(0);
|
|
7853
8265
|
}
|
|
7854
8266
|
|
|
7855
8267
|
if (args[0] === "auth" && args[1] === "status") {
|
|
7856
|
-
|
|
8268
|
+
outputText("github.com\\n \u2713 Logged in to github.com as junior-eval\\n");
|
|
7857
8269
|
process.exit(0);
|
|
7858
8270
|
}
|
|
7859
8271
|
|
|
@@ -7877,7 +8289,7 @@ if (args[0] === "repo" && args[1] === "view") {
|
|
|
7877
8289
|
if (jsonFields) {
|
|
7878
8290
|
outputJson(pickFields(record, jsonFields));
|
|
7879
8291
|
} else {
|
|
7880
|
-
|
|
8292
|
+
outputText(record.url + "\\n");
|
|
7881
8293
|
}
|
|
7882
8294
|
process.exit(0);
|
|
7883
8295
|
}
|
|
@@ -7929,7 +8341,7 @@ if (args[0] === "issue") {
|
|
|
7929
8341
|
if (jsonFields) {
|
|
7930
8342
|
outputJson(pickFields(record, jsonFields));
|
|
7931
8343
|
} else {
|
|
7932
|
-
|
|
8344
|
+
outputText(record.url + "\\n");
|
|
7933
8345
|
}
|
|
7934
8346
|
process.exit(0);
|
|
7935
8347
|
}
|
|
@@ -7945,7 +8357,7 @@ if (args[0] === "issue") {
|
|
|
7945
8357
|
if (jsonFields) {
|
|
7946
8358
|
outputJson(pickFields(record, jsonFields));
|
|
7947
8359
|
} else {
|
|
7948
|
-
|
|
8360
|
+
outputText(record.url + "\\n");
|
|
7949
8361
|
}
|
|
7950
8362
|
process.exit(0);
|
|
7951
8363
|
}
|
|
@@ -7962,7 +8374,7 @@ if (args[0] === "issue") {
|
|
|
7962
8374
|
}
|
|
7963
8375
|
|
|
7964
8376
|
if (subcommand === "comment") {
|
|
7965
|
-
|
|
8377
|
+
outputText(record.url + "#issuecomment-1\\n");
|
|
7966
8378
|
process.exit(0);
|
|
7967
8379
|
}
|
|
7968
8380
|
|
|
@@ -9293,7 +9705,6 @@ function createAgentTools(tools, sandbox, spanContext, onStatus, sandboxExecutor
|
|
|
9293
9705
|
// src/chat/services/reply-delivery-plan.ts
|
|
9294
9706
|
var REACTION_ONLY_ACK_RE = /^(?::[a-z0-9_+-]+:|[\p{Extended_Pictographic}\uFE0F\u200D]+)$/u;
|
|
9295
9707
|
var REDUNDANT_REACTION_ACK_TEXT = ["done", "got it", "ok", "okay"];
|
|
9296
|
-
var REACTION_ALIAS_PREFIX_RE = /^:[a-z0-9_+-]*$/i;
|
|
9297
9708
|
function normalizeReactionAckText(text) {
|
|
9298
9709
|
return text.trim().toLowerCase().replace(/[!.]+$/g, "");
|
|
9299
9710
|
}
|
|
@@ -9310,24 +9721,11 @@ function isRedundantReactionAckText(text) {
|
|
|
9310
9721
|
normalized
|
|
9311
9722
|
);
|
|
9312
9723
|
}
|
|
9313
|
-
function isPotentialRedundantReactionAckText(text) {
|
|
9314
|
-
const trimmed = text.trim();
|
|
9315
|
-
if (!trimmed) {
|
|
9316
|
-
return true;
|
|
9317
|
-
}
|
|
9318
|
-
if (REACTION_ONLY_ACK_RE.test(trimmed) || REACTION_ALIAS_PREFIX_RE.test(trimmed)) {
|
|
9319
|
-
return true;
|
|
9320
|
-
}
|
|
9321
|
-
const normalized = normalizeReactionAckText(text);
|
|
9322
|
-
return REDUNDANT_REACTION_ACK_TEXT.some(
|
|
9323
|
-
(candidate) => candidate.startsWith(normalized)
|
|
9324
|
-
);
|
|
9325
|
-
}
|
|
9326
9724
|
function buildReplyDeliveryPlan(args) {
|
|
9327
9725
|
const mode = args.explicitChannelPostIntent && args.channelPostPerformed ? "channel_only" : "thread";
|
|
9328
9726
|
let attachFiles = "none";
|
|
9329
9727
|
if (args.hasFiles && mode === "thread") {
|
|
9330
|
-
attachFiles =
|
|
9728
|
+
attachFiles = "inline";
|
|
9331
9729
|
}
|
|
9332
9730
|
return {
|
|
9333
9731
|
mode,
|
|
@@ -9335,27 +9733,6 @@ function buildReplyDeliveryPlan(args) {
|
|
|
9335
9733
|
attachFiles
|
|
9336
9734
|
};
|
|
9337
9735
|
}
|
|
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
9736
|
|
|
9360
9737
|
// src/chat/services/channel-intent.ts
|
|
9361
9738
|
function isExplicitChannelPostIntent(text) {
|
|
@@ -9381,6 +9758,10 @@ function sentenceClaimsAttachment(sentence) {
|
|
|
9381
9758
|
if (!hasAttachmentNoun) {
|
|
9382
9759
|
return false;
|
|
9383
9760
|
}
|
|
9761
|
+
const hasNegativeAttachmentPhrase = /\bno (?:screenshot|image|file|attachment)\b/i.test(sentence) || /\b(?:isn['’]t|is not|wasn['’]t|was not)\s+attached\b/i.test(sentence) || /\bwithout (?:an? )?(?:screenshot|image|file|attachment)\b/i.test(sentence);
|
|
9762
|
+
if (hasNegativeAttachmentPhrase) {
|
|
9763
|
+
return false;
|
|
9764
|
+
}
|
|
9384
9765
|
const hasPositiveAttachmentVerb = /\b(attached|shared|uploaded|included)\b/i.test(sentence);
|
|
9385
9766
|
const hasDeicticSharePhrase = /\bhere(?:'s| is)\b/i.test(sentence);
|
|
9386
9767
|
return hasPositiveAttachmentVerb || hasDeicticSharePhrase;
|
|
@@ -9409,15 +9790,17 @@ function buildTurnResult(input) {
|
|
|
9409
9790
|
toolCalls,
|
|
9410
9791
|
sandboxId,
|
|
9411
9792
|
sandboxDependencyProfileHash,
|
|
9412
|
-
|
|
9793
|
+
durationMs,
|
|
9413
9794
|
shouldTrace,
|
|
9414
9795
|
spanContext,
|
|
9796
|
+
usage,
|
|
9415
9797
|
correlation,
|
|
9416
9798
|
assistantUserName
|
|
9417
9799
|
} = input;
|
|
9418
9800
|
const toolResults = newMessages.filter(isToolResultMessage);
|
|
9419
9801
|
const assistantMessages = newMessages.filter(isAssistantMessage);
|
|
9420
|
-
const
|
|
9802
|
+
const terminalAssistantMessages = getTerminalAssistantMessages(newMessages);
|
|
9803
|
+
const primaryText = terminalAssistantMessages.map((message) => extractAssistantText(message)).join("\n\n").trim();
|
|
9421
9804
|
const oauthStartedMessage = extractOAuthStartedMessageFromToolResults(toolResults);
|
|
9422
9805
|
const toolErrorCount = toolResults.filter((result) => result.isError).length;
|
|
9423
9806
|
const explicitChannelPostIntent = isExplicitChannelPostIntent(userInput);
|
|
@@ -9430,8 +9813,7 @@ function buildTurnResult(input) {
|
|
|
9430
9813
|
const deliveryPlan = buildReplyDeliveryPlan({
|
|
9431
9814
|
explicitChannelPostIntent,
|
|
9432
9815
|
channelPostPerformed,
|
|
9433
|
-
hasFiles: replyFiles.length > 0
|
|
9434
|
-
streamingThreadReply: hasTextDeltaCallback
|
|
9816
|
+
hasFiles: replyFiles.length > 0
|
|
9435
9817
|
});
|
|
9436
9818
|
const deliveryMode = deliveryPlan.mode;
|
|
9437
9819
|
if (!primaryText && !oauthStartedMessage) {
|
|
@@ -9453,7 +9835,7 @@ function buildTurnResult(input) {
|
|
|
9453
9835
|
"Model returned empty text response"
|
|
9454
9836
|
);
|
|
9455
9837
|
}
|
|
9456
|
-
const lastAssistant =
|
|
9838
|
+
const lastAssistant = terminalAssistantMessages.at(-1);
|
|
9457
9839
|
const stopReason = typeof lastAssistant?.stopReason === "string" ? lastAssistant.stopReason : void 0;
|
|
9458
9840
|
const errorMessage = typeof lastAssistant?.errorMessage === "string" ? lastAssistant.errorMessage : void 0;
|
|
9459
9841
|
const usedPrimaryText = Boolean(primaryText);
|
|
@@ -9486,6 +9868,8 @@ function buildTurnResult(input) {
|
|
|
9486
9868
|
toolResultCount: toolResults.length,
|
|
9487
9869
|
toolErrorCount,
|
|
9488
9870
|
usedPrimaryText,
|
|
9871
|
+
durationMs,
|
|
9872
|
+
usage,
|
|
9489
9873
|
stopReason,
|
|
9490
9874
|
errorMessage,
|
|
9491
9875
|
providerError: void 0
|
|
@@ -9779,6 +10163,16 @@ function createMcpAuthOrchestration(deps, abortAgent) {
|
|
|
9779
10163
|
|
|
9780
10164
|
// src/chat/respond.ts
|
|
9781
10165
|
var startupDiscoveryLogged = false;
|
|
10166
|
+
function buildOmittedImageAttachmentNotice(count) {
|
|
10167
|
+
return [
|
|
10168
|
+
"<omitted-image-attachments>",
|
|
10169
|
+
`count: ${count}`,
|
|
10170
|
+
"Slack included image attachments with this turn, but this runtime cannot analyze images because no vision model is configured.",
|
|
10171
|
+
"Do not claim that no image was attached.",
|
|
10172
|
+
"If the user asks about image contents, explain that image analysis is unavailable in this runtime and continue with any text or non-image files that are still available.",
|
|
10173
|
+
"</omitted-image-attachments>"
|
|
10174
|
+
].join("\n");
|
|
10175
|
+
}
|
|
9782
10176
|
function mcpToolsToDefinitions(mcpTools) {
|
|
9783
10177
|
const defs = {};
|
|
9784
10178
|
for (const tool2 of mcpTools) {
|
|
@@ -9794,6 +10188,7 @@ function mcpToolsToDefinitions(mcpTools) {
|
|
|
9794
10188
|
return defs;
|
|
9795
10189
|
}
|
|
9796
10190
|
async function generateAssistantReply(messageText, context = {}) {
|
|
10191
|
+
const replyStartedAtMs = Date.now();
|
|
9797
10192
|
let timeoutResumeConversationId;
|
|
9798
10193
|
let timeoutResumeSessionId;
|
|
9799
10194
|
let timeoutResumeSliceId = 1;
|
|
@@ -9804,6 +10199,7 @@ async function generateAssistantReply(messageText, context = {}) {
|
|
|
9804
10199
|
let mcpToolManager;
|
|
9805
10200
|
let sandboxExecutor;
|
|
9806
10201
|
let timedOut = false;
|
|
10202
|
+
let turnUsage;
|
|
9807
10203
|
const getSandboxMetadata = () => sandboxExecutor ? {
|
|
9808
10204
|
sandboxId: sandboxExecutor.getSandboxId(),
|
|
9809
10205
|
sandboxDependencyProfileHash: sandboxExecutor.getDependencyProfileHash()
|
|
@@ -9850,6 +10246,8 @@ async function generateAssistantReply(messageText, context = {}) {
|
|
|
9850
10246
|
let configurationValues;
|
|
9851
10247
|
const userInput = messageText;
|
|
9852
10248
|
if (shouldTrace) {
|
|
10249
|
+
const inboundAttachmentCount = context.inboundAttachmentCount ?? 0;
|
|
10250
|
+
const promptAttachmentCount = context.userAttachments?.length ?? 0;
|
|
9853
10251
|
logInfo(
|
|
9854
10252
|
"agent_message_in",
|
|
9855
10253
|
spanContext,
|
|
@@ -9857,7 +10255,10 @@ async function generateAssistantReply(messageText, context = {}) {
|
|
|
9857
10255
|
"app.message.kind": "user_inbound",
|
|
9858
10256
|
"app.message.length": userInput.length,
|
|
9859
10257
|
"app.message.input": summarizeMessageText(userInput),
|
|
9860
|
-
|
|
10258
|
+
// Log both counts so image uploads filtered by vision/config do not
|
|
10259
|
+
// look indistinguishable from Slack ingress dropping attachments.
|
|
10260
|
+
"app.message.attachment_count": inboundAttachmentCount,
|
|
10261
|
+
"app.message.prompt_attachment_count": promptAttachmentCount,
|
|
9861
10262
|
"messaging.message.id": context.correlation?.messageTs ?? ""
|
|
9862
10263
|
},
|
|
9863
10264
|
"Agent message received"
|
|
@@ -10110,6 +10511,13 @@ async function generateAssistantReply(messageText, context = {}) {
|
|
|
10110
10511
|
threadParticipants: context.threadParticipants
|
|
10111
10512
|
});
|
|
10112
10513
|
const userContentParts = [{ type: "text", text: userTurnText }];
|
|
10514
|
+
const omittedImageAttachmentCount = context.omittedImageAttachmentCount ?? 0;
|
|
10515
|
+
if (omittedImageAttachmentCount > 0) {
|
|
10516
|
+
userContentParts.push({
|
|
10517
|
+
type: "text",
|
|
10518
|
+
text: buildOmittedImageAttachmentNotice(omittedImageAttachmentCount)
|
|
10519
|
+
});
|
|
10520
|
+
}
|
|
10113
10521
|
for (const attachment of context.userAttachments ?? []) {
|
|
10114
10522
|
if (attachment.promptText) {
|
|
10115
10523
|
userContentParts.push({
|
|
@@ -10251,7 +10659,12 @@ async function generateAssistantReply(messageText, context = {}) {
|
|
|
10251
10659
|
spanContext,
|
|
10252
10660
|
async () => {
|
|
10253
10661
|
let promptResult;
|
|
10254
|
-
const promptPromise = resumedFromCheckpoint ?
|
|
10662
|
+
const promptPromise = resumedFromCheckpoint ? (
|
|
10663
|
+
// Checkpoint resumes continue from the persisted Pi message
|
|
10664
|
+
// state. Any reconstructed replyContext only matters when the
|
|
10665
|
+
// turn parked before the initial user prompt was recorded.
|
|
10666
|
+
agent.continue()
|
|
10667
|
+
) : agent.prompt({
|
|
10255
10668
|
role: "user",
|
|
10256
10669
|
content: userContentParts,
|
|
10257
10670
|
timestamp: Date.now()
|
|
@@ -10305,14 +10718,16 @@ async function generateAssistantReply(messageText, context = {}) {
|
|
|
10305
10718
|
}
|
|
10306
10719
|
const outputMessages = newMessages.filter(isAssistantMessage);
|
|
10307
10720
|
const outputMessagesAttribute = serializeGenAiAttribute(outputMessages);
|
|
10308
|
-
const
|
|
10721
|
+
const usageSummary = extractGenAiUsageSummary(
|
|
10309
10722
|
promptResult,
|
|
10310
10723
|
agent.state,
|
|
10311
10724
|
...outputMessages
|
|
10312
10725
|
);
|
|
10726
|
+
turnUsage = usageSummary.inputTokens !== void 0 || usageSummary.outputTokens !== void 0 || usageSummary.totalTokens !== void 0 ? usageSummary : void 0;
|
|
10313
10727
|
setSpanAttributes({
|
|
10314
10728
|
...outputMessagesAttribute ? { "gen_ai.output.messages": outputMessagesAttribute } : {},
|
|
10315
|
-
...
|
|
10729
|
+
...usageSummary.inputTokens !== void 0 ? { "gen_ai.usage.input_tokens": usageSummary.inputTokens } : {},
|
|
10730
|
+
...usageSummary.outputTokens !== void 0 ? { "gen_ai.usage.output_tokens": usageSummary.outputTokens } : {}
|
|
10316
10731
|
});
|
|
10317
10732
|
},
|
|
10318
10733
|
{
|
|
@@ -10345,10 +10760,11 @@ async function generateAssistantReply(messageText, context = {}) {
|
|
|
10345
10760
|
toolCalls,
|
|
10346
10761
|
sandboxId: currentSandboxExecutor.getSandboxId(),
|
|
10347
10762
|
sandboxDependencyProfileHash: currentSandboxExecutor.getDependencyProfileHash(),
|
|
10763
|
+
durationMs: Date.now() - replyStartedAtMs,
|
|
10348
10764
|
generatedFileCount: generatedFiles.length,
|
|
10349
|
-
hasTextDeltaCallback: Boolean(context.onTextDelta),
|
|
10350
10765
|
shouldTrace,
|
|
10351
10766
|
spanContext,
|
|
10767
|
+
usage: turnUsage,
|
|
10352
10768
|
correlation: context.correlation,
|
|
10353
10769
|
assistantUserName: context.assistant?.userName
|
|
10354
10770
|
});
|
|
@@ -10439,6 +10855,7 @@ async function generateAssistantReply(messageText, context = {}) {
|
|
|
10439
10855
|
toolResultCount: 0,
|
|
10440
10856
|
toolErrorCount: 0,
|
|
10441
10857
|
usedPrimaryText: false,
|
|
10858
|
+
durationMs: Date.now() - replyStartedAtMs,
|
|
10442
10859
|
errorMessage: message,
|
|
10443
10860
|
providerError: error
|
|
10444
10861
|
}
|
|
@@ -10459,150 +10876,84 @@ async function generateAssistantReply(messageText, context = {}) {
|
|
|
10459
10876
|
}
|
|
10460
10877
|
}
|
|
10461
10878
|
|
|
10462
|
-
// src/chat/
|
|
10463
|
-
|
|
10464
|
-
|
|
10465
|
-
|
|
10466
|
-
function
|
|
10467
|
-
|
|
10468
|
-
|
|
10469
|
-
|
|
10470
|
-
|
|
10471
|
-
|
|
10472
|
-
|
|
10473
|
-
|
|
10474
|
-
|
|
10475
|
-
|
|
10476
|
-
|
|
10477
|
-
|
|
10478
|
-
|
|
10479
|
-
|
|
10480
|
-
|
|
10481
|
-
|
|
10482
|
-
|
|
10483
|
-
|
|
10484
|
-
|
|
10485
|
-
|
|
10486
|
-
|
|
10487
|
-
|
|
10488
|
-
|
|
10489
|
-
|
|
10490
|
-
|
|
10491
|
-
|
|
10492
|
-
|
|
10493
|
-
|
|
10494
|
-
|
|
10495
|
-
}, STATUS_ROTATION_INTERVAL_MS);
|
|
10496
|
-
};
|
|
10497
|
-
const postStatus = async (text, suggestions) => {
|
|
10498
|
-
const channelId = args.channelId;
|
|
10499
|
-
const threadTs = args.threadTs;
|
|
10500
|
-
if (!channelId || !threadTs) {
|
|
10501
|
-
return;
|
|
10502
|
-
}
|
|
10503
|
-
if (!text && !currentVisibleStatus) {
|
|
10504
|
-
return;
|
|
10505
|
-
}
|
|
10506
|
-
currentVisibleStatus = text;
|
|
10507
|
-
lastStatusAt = now();
|
|
10508
|
-
scheduleRotation();
|
|
10509
|
-
const previous = inflightStatusUpdate;
|
|
10510
|
-
const request = (async () => {
|
|
10511
|
-
await previous;
|
|
10512
|
-
await args.transport.setStatus(channelId, threadTs, text, suggestions);
|
|
10513
|
-
})();
|
|
10514
|
-
inflightStatusUpdate = request;
|
|
10515
|
-
await request;
|
|
10516
|
-
};
|
|
10517
|
-
const postRenderedStatus = async (status) => {
|
|
10518
|
-
const presentation = buildAssistantStatusPresentation({
|
|
10519
|
-
status,
|
|
10520
|
-
random
|
|
10879
|
+
// src/chat/slack/footer.ts
|
|
10880
|
+
function escapeSlackMrkdwn(text) {
|
|
10881
|
+
return text.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">");
|
|
10882
|
+
}
|
|
10883
|
+
function formatSlackTokenCount(value) {
|
|
10884
|
+
return new Intl.NumberFormat("en-US").format(value);
|
|
10885
|
+
}
|
|
10886
|
+
function formatSlackDuration(durationMs) {
|
|
10887
|
+
if (durationMs < 1e3) {
|
|
10888
|
+
return `${durationMs}ms`;
|
|
10889
|
+
}
|
|
10890
|
+
const durationSeconds = durationMs / 1e3;
|
|
10891
|
+
if (durationSeconds < 10) {
|
|
10892
|
+
return `${durationSeconds.toFixed(1).replace(/\.0$/, "")}s`;
|
|
10893
|
+
}
|
|
10894
|
+
return `${Math.round(durationSeconds)}s`;
|
|
10895
|
+
}
|
|
10896
|
+
function resolveTotalTokens(usage) {
|
|
10897
|
+
if (usage?.totalTokens !== void 0) {
|
|
10898
|
+
return usage.totalTokens;
|
|
10899
|
+
}
|
|
10900
|
+
if (usage?.inputTokens !== void 0 && usage.outputTokens !== void 0) {
|
|
10901
|
+
return usage.inputTokens + usage.outputTokens;
|
|
10902
|
+
}
|
|
10903
|
+
return void 0;
|
|
10904
|
+
}
|
|
10905
|
+
function buildSlackReplyFooter(args) {
|
|
10906
|
+
const items = [];
|
|
10907
|
+
const conversationId = args.conversationId?.trim();
|
|
10908
|
+
if (conversationId) {
|
|
10909
|
+
items.push({
|
|
10910
|
+
label: "ID",
|
|
10911
|
+
value: conversationId
|
|
10521
10912
|
});
|
|
10522
|
-
|
|
10523
|
-
|
|
10524
|
-
|
|
10525
|
-
|
|
10526
|
-
|
|
10527
|
-
|
|
10528
|
-
clearTimer(pendingTimer);
|
|
10529
|
-
pendingTimer = null;
|
|
10530
|
-
}
|
|
10531
|
-
pendingStatus = null;
|
|
10532
|
-
pendingKey = "";
|
|
10533
|
-
};
|
|
10534
|
-
const flushPending = async () => {
|
|
10535
|
-
if (!active || !pendingStatus) {
|
|
10536
|
-
clearPending();
|
|
10537
|
-
return;
|
|
10538
|
-
}
|
|
10539
|
-
const next = pendingStatus;
|
|
10540
|
-
clearPending();
|
|
10541
|
-
const nextPresentation = buildAssistantStatusPresentation({
|
|
10542
|
-
status: next,
|
|
10543
|
-
random
|
|
10913
|
+
}
|
|
10914
|
+
const totalTokens = resolveTotalTokens(args.usage);
|
|
10915
|
+
if (totalTokens !== void 0) {
|
|
10916
|
+
items.push({
|
|
10917
|
+
label: "Tokens",
|
|
10918
|
+
value: formatSlackTokenCount(totalTokens)
|
|
10544
10919
|
});
|
|
10545
|
-
|
|
10546
|
-
|
|
10547
|
-
|
|
10548
|
-
|
|
10549
|
-
|
|
10550
|
-
|
|
10551
|
-
|
|
10552
|
-
|
|
10553
|
-
|
|
10554
|
-
|
|
10555
|
-
|
|
10556
|
-
|
|
10557
|
-
|
|
10558
|
-
|
|
10559
|
-
|
|
10560
|
-
|
|
10561
|
-
|
|
10562
|
-
|
|
10920
|
+
}
|
|
10921
|
+
if (typeof args.durationMs === "number" && Number.isFinite(args.durationMs)) {
|
|
10922
|
+
const durationMs = Math.max(0, Math.floor(args.durationMs));
|
|
10923
|
+
items.push({
|
|
10924
|
+
label: "Time",
|
|
10925
|
+
value: formatSlackDuration(durationMs)
|
|
10926
|
+
});
|
|
10927
|
+
}
|
|
10928
|
+
const traceId = args.traceId?.trim();
|
|
10929
|
+
if (traceId) {
|
|
10930
|
+
items.push({
|
|
10931
|
+
label: "Trace",
|
|
10932
|
+
value: traceId
|
|
10933
|
+
});
|
|
10934
|
+
}
|
|
10935
|
+
return items.length > 0 ? { items } : void 0;
|
|
10936
|
+
}
|
|
10937
|
+
function buildSlackReplyBlocks(text, footer) {
|
|
10938
|
+
if (!text.trim() || !footer?.items.length) {
|
|
10939
|
+
return void 0;
|
|
10940
|
+
}
|
|
10941
|
+
return [
|
|
10942
|
+
{
|
|
10943
|
+
type: "section",
|
|
10944
|
+
text: {
|
|
10945
|
+
type: "mrkdwn",
|
|
10946
|
+
text
|
|
10563
10947
|
}
|
|
10564
|
-
currentKey = "";
|
|
10565
|
-
await postStatus("");
|
|
10566
10948
|
},
|
|
10567
|
-
|
|
10568
|
-
|
|
10569
|
-
|
|
10570
|
-
|
|
10571
|
-
|
|
10572
|
-
|
|
10573
|
-
random
|
|
10574
|
-
});
|
|
10575
|
-
if (!presentation.visible) {
|
|
10576
|
-
return;
|
|
10577
|
-
}
|
|
10578
|
-
if (presentation.key === currentKey || presentation.key === pendingKey) {
|
|
10579
|
-
return;
|
|
10580
|
-
}
|
|
10581
|
-
const elapsed = now() - lastStatusAt;
|
|
10582
|
-
const waitMs = Math.max(
|
|
10583
|
-
STATUS_UPDATE_DEBOUNCE_MS - elapsed,
|
|
10584
|
-
STATUS_MIN_VISIBLE_MS - elapsed,
|
|
10585
|
-
0
|
|
10586
|
-
);
|
|
10587
|
-
if (waitMs <= 0) {
|
|
10588
|
-
clearPending();
|
|
10589
|
-
void postRenderedStatus(status);
|
|
10590
|
-
return;
|
|
10591
|
-
}
|
|
10592
|
-
pendingStatus = status;
|
|
10593
|
-
pendingKey = presentation.key;
|
|
10594
|
-
if (pendingTimer) {
|
|
10595
|
-
return;
|
|
10596
|
-
}
|
|
10597
|
-
pendingTimer = setTimer(
|
|
10598
|
-
() => {
|
|
10599
|
-
pendingTimer = null;
|
|
10600
|
-
void flushPending();
|
|
10601
|
-
},
|
|
10602
|
-
Math.max(1, waitMs)
|
|
10603
|
-
);
|
|
10949
|
+
{
|
|
10950
|
+
type: "context",
|
|
10951
|
+
elements: footer.items.map((item) => ({
|
|
10952
|
+
type: "mrkdwn",
|
|
10953
|
+
text: `*${escapeSlackMrkdwn(item.label)}:* ${escapeSlackMrkdwn(item.value)}`
|
|
10954
|
+
}))
|
|
10604
10955
|
}
|
|
10605
|
-
|
|
10956
|
+
];
|
|
10606
10957
|
}
|
|
10607
10958
|
|
|
10608
10959
|
// src/chat/slack/reply.ts
|
|
@@ -10610,21 +10961,35 @@ import { Buffer as Buffer2 } from "buffer";
|
|
|
10610
10961
|
function isInterruptedVisibleReply(reply) {
|
|
10611
10962
|
return reply.diagnostics.outcome === "provider_error";
|
|
10612
10963
|
}
|
|
10613
|
-
function
|
|
10964
|
+
function resolveReplyDelivery(reply) {
|
|
10965
|
+
const replyHasFiles = Boolean(reply.files && reply.files.length > 0);
|
|
10966
|
+
const deliveryPlan = reply.deliveryPlan ?? {
|
|
10967
|
+
mode: reply.deliveryMode ?? "thread",
|
|
10968
|
+
postThreadText: (reply.deliveryMode ?? "thread") !== "channel_only",
|
|
10969
|
+
attachFiles: replyHasFiles ? "inline" : "none"
|
|
10970
|
+
};
|
|
10614
10971
|
return {
|
|
10615
|
-
|
|
10616
|
-
|
|
10972
|
+
shouldPostThreadReply: deliveryPlan.postThreadText,
|
|
10973
|
+
attachFiles: replyHasFiles && deliveryPlan.attachFiles !== "none" ? "inline" : "none"
|
|
10617
10974
|
};
|
|
10618
10975
|
}
|
|
10976
|
+
function buildReplyText(text) {
|
|
10977
|
+
const message = buildSlackOutputMessage(text);
|
|
10978
|
+
if (typeof message === "object" && message !== null && "markdown" in message && typeof message.markdown === "string") {
|
|
10979
|
+
return message.markdown;
|
|
10980
|
+
}
|
|
10981
|
+
if (typeof message === "object" && message !== null && "raw" in message && typeof message.raw === "string") {
|
|
10982
|
+
return message.raw;
|
|
10983
|
+
}
|
|
10984
|
+
return "";
|
|
10985
|
+
}
|
|
10619
10986
|
function buildTextPosts(args) {
|
|
10620
10987
|
const chunks = splitSlackReplyText(args.text, {
|
|
10621
10988
|
interrupted: args.interrupted
|
|
10622
10989
|
});
|
|
10623
10990
|
return chunks.map((chunk, index) => ({
|
|
10624
|
-
|
|
10625
|
-
|
|
10626
|
-
index === 0 ? args.firstFiles : void 0
|
|
10627
|
-
),
|
|
10991
|
+
text: chunk,
|
|
10992
|
+
...index === 0 && args.firstFiles ? { files: args.firstFiles } : {},
|
|
10628
10993
|
stage: index === 0 ? args.firstStage ?? "thread_reply" : "thread_reply_continuation"
|
|
10629
10994
|
}));
|
|
10630
10995
|
}
|
|
@@ -10646,150 +11011,100 @@ async function normalizeFileUploads(files) {
|
|
|
10646
11011
|
})
|
|
10647
11012
|
);
|
|
10648
11013
|
}
|
|
10649
|
-
|
|
11014
|
+
function findLastTextPostIndex(posts) {
|
|
11015
|
+
for (let index = posts.length - 1; index >= 0; index -= 1) {
|
|
11016
|
+
if (posts[index]?.text.trim().length) {
|
|
11017
|
+
return index;
|
|
11018
|
+
}
|
|
11019
|
+
}
|
|
11020
|
+
return -1;
|
|
11021
|
+
}
|
|
11022
|
+
async function uploadReplyFiles(args) {
|
|
10650
11023
|
try {
|
|
10651
11024
|
await uploadFilesToThread({
|
|
10652
11025
|
channelId: args.channelId,
|
|
10653
11026
|
threadTs: args.threadTs,
|
|
10654
11027
|
files: await normalizeFileUploads(args.files)
|
|
10655
11028
|
});
|
|
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;
|
|
11029
|
+
} catch (error) {
|
|
11030
|
+
if (args.failureMode === "strict") {
|
|
11031
|
+
throw error;
|
|
10722
11032
|
}
|
|
10723
|
-
}
|
|
11033
|
+
}
|
|
10724
11034
|
}
|
|
10725
11035
|
function planSlackReplyPosts(args) {
|
|
10726
11036
|
const replyFiles = args.reply.files && args.reply.files.length > 0 ? args.reply.files : void 0;
|
|
10727
|
-
const { shouldPostThreadReply, attachFiles } = resolveReplyDelivery(
|
|
10728
|
-
|
|
10729
|
-
|
|
10730
|
-
});
|
|
11037
|
+
const { shouldPostThreadReply, attachFiles } = resolveReplyDelivery(
|
|
11038
|
+
args.reply
|
|
11039
|
+
);
|
|
10731
11040
|
const interrupted = isInterruptedVisibleReply(args.reply);
|
|
10732
11041
|
const posts = [];
|
|
10733
|
-
|
|
10734
|
-
|
|
10735
|
-
|
|
10736
|
-
|
|
10737
|
-
|
|
10738
|
-
|
|
10739
|
-
|
|
10740
|
-
|
|
10741
|
-
|
|
10742
|
-
|
|
10743
|
-
|
|
10744
|
-
|
|
10745
|
-
|
|
10746
|
-
|
|
10747
|
-
|
|
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
|
-
}
|
|
11042
|
+
const textPosts = shouldPostThreadReply ? buildTextPosts({
|
|
11043
|
+
text: args.reply.text,
|
|
11044
|
+
interrupted,
|
|
11045
|
+
firstFiles: attachFiles === "inline" ? replyFiles : void 0
|
|
11046
|
+
}) : [];
|
|
11047
|
+
posts.push(...textPosts);
|
|
11048
|
+
if (attachFiles === "inline" && replyFiles && textPosts.length === 0) {
|
|
11049
|
+
posts.push({
|
|
11050
|
+
files: replyFiles,
|
|
11051
|
+
stage: "thread_reply",
|
|
11052
|
+
text: ""
|
|
11053
|
+
});
|
|
11054
|
+
} else if (shouldPostThreadReply && textPosts.length === 0) {
|
|
11055
|
+
posts.push({
|
|
11056
|
+
text: buildReplyText(args.reply.text),
|
|
11057
|
+
stage: "thread_reply"
|
|
11058
|
+
});
|
|
10768
11059
|
}
|
|
10769
11060
|
if (attachFiles === "followup" && replyFiles) {
|
|
10770
11061
|
posts.push({
|
|
10771
|
-
|
|
10772
|
-
stage: "thread_reply_files_followup"
|
|
11062
|
+
files: replyFiles,
|
|
11063
|
+
stage: "thread_reply_files_followup",
|
|
11064
|
+
text: ""
|
|
10773
11065
|
});
|
|
10774
11066
|
}
|
|
10775
11067
|
return posts;
|
|
10776
11068
|
}
|
|
10777
11069
|
async function postSlackApiReplyPosts(args) {
|
|
10778
|
-
|
|
10779
|
-
|
|
10780
|
-
|
|
10781
|
-
|
|
10782
|
-
|
|
10783
|
-
|
|
10784
|
-
|
|
10785
|
-
|
|
11070
|
+
const lastTextPostIndex = findLastTextPostIndex(args.posts);
|
|
11071
|
+
let lastPostedMessageTs;
|
|
11072
|
+
for (const [index, post] of args.posts.entries()) {
|
|
11073
|
+
const hasVisibleDelivery = post.text.trim().length > 0 || post.files?.length;
|
|
11074
|
+
if (hasVisibleDelivery) {
|
|
11075
|
+
await args.beforePost?.();
|
|
11076
|
+
}
|
|
11077
|
+
let messageTs;
|
|
11078
|
+
try {
|
|
11079
|
+
if (post.text.trim().length > 0) {
|
|
11080
|
+
const response = await postSlackMessage({
|
|
11081
|
+
channelId: args.channelId,
|
|
11082
|
+
threadTs: args.threadTs,
|
|
11083
|
+
text: post.text,
|
|
11084
|
+
...index === lastTextPostIndex && args.footer ? { blocks: buildSlackReplyBlocks(post.text, args.footer) } : {}
|
|
11085
|
+
});
|
|
11086
|
+
messageTs = response.ts;
|
|
11087
|
+
lastPostedMessageTs = response.ts;
|
|
11088
|
+
}
|
|
11089
|
+
if (!post.files?.length) {
|
|
11090
|
+
continue;
|
|
11091
|
+
}
|
|
11092
|
+
await uploadReplyFiles({
|
|
11093
|
+
channelId: args.channelId,
|
|
11094
|
+
failureMode: args.fileUploadFailureMode ?? "best_effort",
|
|
11095
|
+
threadTs: args.threadTs,
|
|
11096
|
+
files: post.files
|
|
11097
|
+
});
|
|
11098
|
+
} catch (error) {
|
|
11099
|
+
await args.onPostError?.({
|
|
11100
|
+
error,
|
|
11101
|
+
messageTs,
|
|
11102
|
+
stage: post.stage
|
|
11103
|
+
});
|
|
11104
|
+
throw error;
|
|
10786
11105
|
}
|
|
10787
|
-
await uploadReplyFilesBestEffort({
|
|
10788
|
-
channelId: args.channelId,
|
|
10789
|
-
threadTs: args.threadTs,
|
|
10790
|
-
files
|
|
10791
|
-
});
|
|
10792
11106
|
}
|
|
11107
|
+
return lastPostedMessageTs;
|
|
10793
11108
|
}
|
|
10794
11109
|
|
|
10795
11110
|
// src/chat/slack/resume.ts
|
|
@@ -10804,16 +11119,13 @@ function resolveReplyTimeoutMs(explicitTimeoutMs) {
|
|
|
10804
11119
|
const parsed = Number.parseInt(raw, 10);
|
|
10805
11120
|
return Number.isFinite(parsed) && parsed > 0 ? parsed : void 0;
|
|
10806
11121
|
}
|
|
10807
|
-
async function postSlackMessage(channelId, threadTs, text) {
|
|
10808
|
-
await getSlackClient().chat.postMessage({
|
|
10809
|
-
channel: channelId,
|
|
10810
|
-
thread_ts: threadTs,
|
|
10811
|
-
text
|
|
10812
|
-
});
|
|
10813
|
-
}
|
|
10814
11122
|
async function postSlackMessageBestEffort(channelId, threadTs, text) {
|
|
10815
11123
|
try {
|
|
10816
|
-
await postSlackMessage(
|
|
11124
|
+
await postSlackMessage({
|
|
11125
|
+
channelId,
|
|
11126
|
+
threadTs,
|
|
11127
|
+
text
|
|
11128
|
+
});
|
|
10817
11129
|
} catch {
|
|
10818
11130
|
}
|
|
10819
11131
|
}
|
|
@@ -10852,7 +11164,7 @@ var ResumeTurnBusyError = class extends Error {
|
|
|
10852
11164
|
function getDefaultLockKey(channelId, threadTs) {
|
|
10853
11165
|
return `slack:${channelId}:${threadTs}`;
|
|
10854
11166
|
}
|
|
10855
|
-
function createResumeReplyContext(args,
|
|
11167
|
+
function createResumeReplyContext(args, statusSession) {
|
|
10856
11168
|
const replyContext = args.replyContext ?? {};
|
|
10857
11169
|
const threadId = args.lockKey ?? getDefaultLockKey(args.channelId, args.threadTs);
|
|
10858
11170
|
const persistedChannelConfiguration = replyContext.channelConfiguration ?? (replyContext.configuration ? createReadOnlyConfigService(replyContext.configuration) : void 0);
|
|
@@ -10881,9 +11193,9 @@ function createResumeReplyContext(args, progress) {
|
|
|
10881
11193
|
await persistThreadStateById(threadId, { artifacts });
|
|
10882
11194
|
await replyContext.onArtifactStateUpdated?.(artifacts);
|
|
10883
11195
|
},
|
|
10884
|
-
onStatus: async (
|
|
10885
|
-
|
|
10886
|
-
await replyContext.onStatus?.(
|
|
11196
|
+
onStatus: async (nextStatus) => {
|
|
11197
|
+
statusSession.update(nextStatus);
|
|
11198
|
+
await replyContext.onStatus?.(nextStatus);
|
|
10887
11199
|
}
|
|
10888
11200
|
};
|
|
10889
11201
|
}
|
|
@@ -10902,10 +11214,9 @@ async function resumeSlackTurn(args) {
|
|
|
10902
11214
|
if (!lock) {
|
|
10903
11215
|
throw new ResumeTurnBusyError(lockKey);
|
|
10904
11216
|
}
|
|
10905
|
-
const
|
|
11217
|
+
const status = createSlackWebApiAssistantStatusSession({
|
|
10906
11218
|
channelId: args.channelId,
|
|
10907
|
-
threadTs: args.threadTs
|
|
10908
|
-
transport: createSlackWebApiAssistantStatusTransport()
|
|
11219
|
+
threadTs: args.threadTs
|
|
10909
11220
|
});
|
|
10910
11221
|
let deferredPauseHandler;
|
|
10911
11222
|
let deferredFailureHandler;
|
|
@@ -10917,9 +11228,9 @@ async function resumeSlackTurn(args) {
|
|
|
10917
11228
|
args.initialText
|
|
10918
11229
|
);
|
|
10919
11230
|
}
|
|
10920
|
-
|
|
11231
|
+
status.start();
|
|
10921
11232
|
const generateReply = args.generateReply ?? generateAssistantReply;
|
|
10922
|
-
const replyContext = createResumeReplyContext(args,
|
|
11233
|
+
const replyContext = createResumeReplyContext(args, status);
|
|
10923
11234
|
const replyPromise = generateReply(args.messageText, {
|
|
10924
11235
|
...replyContext
|
|
10925
11236
|
});
|
|
@@ -10937,19 +11248,23 @@ async function resumeSlackTurn(args) {
|
|
|
10937
11248
|
)
|
|
10938
11249
|
)
|
|
10939
11250
|
]) : await replyPromise;
|
|
10940
|
-
await
|
|
11251
|
+
await status.stop();
|
|
11252
|
+
const footer = buildSlackReplyFooter({
|
|
11253
|
+
conversationId: args.replyContext?.correlation?.conversationId ?? lockKey,
|
|
11254
|
+
durationMs: reply.diagnostics.durationMs,
|
|
11255
|
+
traceId: getActiveTraceId(),
|
|
11256
|
+
usage: reply.diagnostics.usage
|
|
11257
|
+
});
|
|
10941
11258
|
await postSlackApiReplyPosts({
|
|
10942
11259
|
channelId: args.channelId,
|
|
10943
11260
|
threadTs: args.threadTs,
|
|
10944
|
-
posts: planSlackReplyPosts({
|
|
10945
|
-
|
|
10946
|
-
|
|
10947
|
-
}),
|
|
10948
|
-
postMessage: postSlackMessage
|
|
11261
|
+
posts: planSlackReplyPosts({ reply }),
|
|
11262
|
+
fileUploadFailureMode: "best_effort",
|
|
11263
|
+
footer
|
|
10949
11264
|
});
|
|
10950
11265
|
await args.onSuccess?.(reply);
|
|
10951
11266
|
} catch (error) {
|
|
10952
|
-
await
|
|
11267
|
+
await status.stop();
|
|
10953
11268
|
if (isRetryableTurnError(error, "mcp_auth_resume") && args.onAuthPause) {
|
|
10954
11269
|
deferredPauseHandler = async () => {
|
|
10955
11270
|
await args.onAuthPause?.(error);
|
|
@@ -11275,7 +11590,8 @@ async function resumeAuthorizedMcpTurn(args) {
|
|
|
11275
11590
|
configuration: authSession.configuration,
|
|
11276
11591
|
channelConfiguration,
|
|
11277
11592
|
sandbox: getPersistedSandboxState(currentState),
|
|
11278
|
-
threadParticipants: buildThreadParticipants(conversation.messages)
|
|
11593
|
+
threadParticipants: buildThreadParticipants(conversation.messages),
|
|
11594
|
+
...getTurnUserReplyAttachmentContext(userMessage)
|
|
11279
11595
|
},
|
|
11280
11596
|
onSuccess: async (reply) => {
|
|
11281
11597
|
try {
|
|
@@ -11746,11 +12062,11 @@ async function GET5(request, provider, waitUntil) {
|
|
|
11746
12062
|
} else if (stored.channelId && stored.threadTs) {
|
|
11747
12063
|
const { channelId, threadTs } = stored;
|
|
11748
12064
|
waitUntil(
|
|
11749
|
-
() => postSlackMessage(
|
|
12065
|
+
() => postSlackMessage({
|
|
11750
12066
|
channelId,
|
|
11751
12067
|
threadTs,
|
|
11752
|
-
`Your ${providerLabel} account is now connected. You can start using ${providerLabel} commands.`
|
|
11753
|
-
)
|
|
12068
|
+
text: `Your ${providerLabel} account is now connected. You can start using ${providerLabel} commands.`
|
|
12069
|
+
})
|
|
11754
12070
|
);
|
|
11755
12071
|
}
|
|
11756
12072
|
const statusMessage = stored.pendingMessage ? "Your request is being processed in Slack." : "You can close this tab and return to Slack.";
|
|
@@ -11929,7 +12245,8 @@ async function resumeTimedOutTurn(payload) {
|
|
|
11929
12245
|
conversationContext,
|
|
11930
12246
|
channelConfiguration,
|
|
11931
12247
|
sandbox,
|
|
11932
|
-
threadParticipants: buildThreadParticipants(conversation.messages)
|
|
12248
|
+
threadParticipants: buildThreadParticipants(conversation.messages),
|
|
12249
|
+
...getTurnUserReplyAttachmentContext(userMessage)
|
|
11933
12250
|
},
|
|
11934
12251
|
onSuccess: async (reply) => {
|
|
11935
12252
|
try {
|
|
@@ -12386,11 +12703,30 @@ function getRunId(thread, message) {
|
|
|
12386
12703
|
return toOptionalString(thread.runId) ?? toOptionalString(message.runId);
|
|
12387
12704
|
}
|
|
12388
12705
|
function getChannelId(thread, message) {
|
|
12389
|
-
return thread.channelId ?? resolveSlackChannelIdFromMessage(message);
|
|
12706
|
+
return resolveSlackChannelIdFromThreadId(toOptionalString(thread.id)) ?? normalizeSlackConversationId(toOptionalString(thread.channelId)) ?? resolveSlackChannelIdFromMessage(message);
|
|
12390
12707
|
}
|
|
12391
12708
|
function getThreadTs(threadId) {
|
|
12392
12709
|
return parseSlackThreadId(threadId)?.threadTs;
|
|
12393
12710
|
}
|
|
12711
|
+
function getAssistantThreadContext(message) {
|
|
12712
|
+
const raw = message.raw;
|
|
12713
|
+
const rawRecord = raw && typeof raw === "object" ? raw : void 0;
|
|
12714
|
+
const channelId = toOptionalString(rawRecord?.channel);
|
|
12715
|
+
if (channelId) {
|
|
12716
|
+
const rawThreadTs = toOptionalString(rawRecord?.thread_ts);
|
|
12717
|
+
const threadTs = isDmChannel(channelId) ? rawThreadTs : rawThreadTs ?? toOptionalString(rawRecord?.ts);
|
|
12718
|
+
if (threadTs) {
|
|
12719
|
+
return { channelId, threadTs };
|
|
12720
|
+
}
|
|
12721
|
+
}
|
|
12722
|
+
const parsedThreadId = parseSlackThreadId(
|
|
12723
|
+
toOptionalString(message.threadId)
|
|
12724
|
+
);
|
|
12725
|
+
if (!parsedThreadId || isDmChannel(parsedThreadId.channelId)) {
|
|
12726
|
+
return void 0;
|
|
12727
|
+
}
|
|
12728
|
+
return parsedThreadId;
|
|
12729
|
+
}
|
|
12394
12730
|
function getMessageTs(message) {
|
|
12395
12731
|
const directTs = toOptionalString(
|
|
12396
12732
|
message.ts
|
|
@@ -12743,7 +13079,7 @@ function createSlackTurnRuntime(deps) {
|
|
|
12743
13079
|
},
|
|
12744
13080
|
async handleAssistantContextChanged(event) {
|
|
12745
13081
|
try {
|
|
12746
|
-
await deps.
|
|
13082
|
+
await deps.refreshAssistantThreadContext({
|
|
12747
13083
|
threadId: event.threadId,
|
|
12748
13084
|
channelId: event.channelId,
|
|
12749
13085
|
threadTs: event.threadTs,
|
|
@@ -12886,13 +13222,16 @@ var MAX_USER_ATTACHMENT_BYTES = 5 * 1024 * 1024;
|
|
|
12886
13222
|
var MAX_MESSAGE_IMAGE_ATTACHMENTS = 3;
|
|
12887
13223
|
var MAX_VISION_SUMMARY_CHARS = 500;
|
|
12888
13224
|
function hasPotentialImageAttachment(attachments) {
|
|
12889
|
-
return attachments
|
|
13225
|
+
return countPotentialImageAttachments(attachments) > 0;
|
|
13226
|
+
}
|
|
13227
|
+
function countPotentialImageAttachments(attachments) {
|
|
13228
|
+
return attachments?.filter((attachment) => {
|
|
12890
13229
|
if (attachment.type === "image") {
|
|
12891
13230
|
return true;
|
|
12892
13231
|
}
|
|
12893
13232
|
const mimeType = attachment.mimeType ?? "";
|
|
12894
13233
|
return attachment.type === "file" && mimeType.startsWith("image/");
|
|
12895
|
-
}) ??
|
|
13234
|
+
}).length ?? 0;
|
|
12896
13235
|
}
|
|
12897
13236
|
function isVisionEnabled() {
|
|
12898
13237
|
return Boolean(botConfig.visionModelId);
|
|
@@ -13426,45 +13765,71 @@ function createJuniorRuntimeServices(overrides = {}) {
|
|
|
13426
13765
|
};
|
|
13427
13766
|
}
|
|
13428
13767
|
|
|
13429
|
-
// src/chat/
|
|
13430
|
-
function
|
|
13431
|
-
const
|
|
13432
|
-
|
|
13433
|
-
|
|
13434
|
-
|
|
13435
|
-
|
|
13436
|
-
|
|
13437
|
-
|
|
13438
|
-
|
|
13439
|
-
|
|
13440
|
-
|
|
13441
|
-
|
|
13442
|
-
|
|
13443
|
-
|
|
13444
|
-
|
|
13445
|
-
|
|
13446
|
-
|
|
13447
|
-
|
|
13448
|
-
|
|
13449
|
-
|
|
13450
|
-
|
|
13451
|
-
|
|
13452
|
-
|
|
13453
|
-
|
|
13454
|
-
|
|
13455
|
-
|
|
13456
|
-
|
|
13457
|
-
|
|
13458
|
-
|
|
13459
|
-
if (
|
|
13460
|
-
|
|
13768
|
+
// src/chat/slack/assistant-thread/title.ts
|
|
13769
|
+
function maybeUpdateAssistantTitle(args) {
|
|
13770
|
+
const assistantThreadContext = args.assistantThreadContext;
|
|
13771
|
+
if (!assistantThreadContext?.channelId || !assistantThreadContext.threadTs || !isDmChannel(assistantThreadContext.channelId)) {
|
|
13772
|
+
return Promise.resolve(void 0);
|
|
13773
|
+
}
|
|
13774
|
+
const titleSourceMessage = getThreadTitleSourceMessage(args.conversation);
|
|
13775
|
+
if (!titleSourceMessage) {
|
|
13776
|
+
return Promise.resolve(void 0);
|
|
13777
|
+
}
|
|
13778
|
+
if (args.artifacts.assistantTitleSourceMessageId === titleSourceMessage.id) {
|
|
13779
|
+
return Promise.resolve(void 0);
|
|
13780
|
+
}
|
|
13781
|
+
return (async () => {
|
|
13782
|
+
try {
|
|
13783
|
+
const title = await args.generateThreadTitle(titleSourceMessage.text);
|
|
13784
|
+
await args.getSlackAdapter().setAssistantTitle(
|
|
13785
|
+
assistantThreadContext.channelId,
|
|
13786
|
+
assistantThreadContext.threadTs,
|
|
13787
|
+
title
|
|
13788
|
+
);
|
|
13789
|
+
return titleSourceMessage.id;
|
|
13790
|
+
} catch (error) {
|
|
13791
|
+
const slackErrorCode = getSlackApiErrorCode(error);
|
|
13792
|
+
const assistantTitleErrorAttributes = {
|
|
13793
|
+
"app.slack.assistant_title.outcome": "permission_denied",
|
|
13794
|
+
...slackErrorCode ? {
|
|
13795
|
+
"app.slack.assistant_title.error_code": slackErrorCode
|
|
13796
|
+
} : {}
|
|
13797
|
+
};
|
|
13798
|
+
if (isSlackTitlePermissionError(error)) {
|
|
13799
|
+
setSpanAttributes(assistantTitleErrorAttributes);
|
|
13800
|
+
logError(
|
|
13801
|
+
"thread_title_generation_permission_denied",
|
|
13802
|
+
{
|
|
13803
|
+
slackThreadId: args.threadId,
|
|
13804
|
+
slackUserId: args.requesterId,
|
|
13805
|
+
slackChannelId: args.channelId,
|
|
13806
|
+
runId: args.runId,
|
|
13807
|
+
assistantUserName: args.assistantUserName,
|
|
13808
|
+
modelId: args.modelId
|
|
13809
|
+
},
|
|
13810
|
+
assistantTitleErrorAttributes,
|
|
13811
|
+
"Skipping thread title update due to Slack permission error"
|
|
13812
|
+
);
|
|
13813
|
+
return titleSourceMessage.id;
|
|
13461
13814
|
}
|
|
13462
|
-
|
|
13463
|
-
|
|
13464
|
-
|
|
13465
|
-
|
|
13815
|
+
logWarn(
|
|
13816
|
+
"thread_title_generation_failed",
|
|
13817
|
+
{
|
|
13818
|
+
slackThreadId: args.threadId,
|
|
13819
|
+
slackUserId: args.requesterId,
|
|
13820
|
+
slackChannelId: args.channelId,
|
|
13821
|
+
runId: args.runId,
|
|
13822
|
+
assistantUserName: args.assistantUserName,
|
|
13823
|
+
modelId: args.modelId
|
|
13824
|
+
},
|
|
13825
|
+
{
|
|
13826
|
+
"error.message": error instanceof Error ? error.message : String(error)
|
|
13827
|
+
},
|
|
13828
|
+
"Thread title generation failed"
|
|
13829
|
+
);
|
|
13830
|
+
return void 0;
|
|
13466
13831
|
}
|
|
13467
|
-
};
|
|
13832
|
+
})();
|
|
13468
13833
|
}
|
|
13469
13834
|
|
|
13470
13835
|
// src/chat/runtime/reply-executor.ts
|
|
@@ -13481,14 +13846,6 @@ function getExecutionFailureReason(reply) {
|
|
|
13481
13846
|
}
|
|
13482
13847
|
return "empty assistant turn";
|
|
13483
13848
|
}
|
|
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
|
-
}
|
|
13492
13849
|
function createReplyToThread(deps) {
|
|
13493
13850
|
return async function replyToThread(thread, message, options = {}) {
|
|
13494
13851
|
if (message.author.isMe) {
|
|
@@ -13497,6 +13854,7 @@ function createReplyToThread(deps) {
|
|
|
13497
13854
|
const threadId = getThreadId(thread, message);
|
|
13498
13855
|
const channelId = getChannelId(thread, message);
|
|
13499
13856
|
const threadTs = getThreadTs(threadId);
|
|
13857
|
+
const assistantThreadContext = getAssistantThreadContext(message);
|
|
13500
13858
|
const messageTs = getMessageTs(message);
|
|
13501
13859
|
const runId = getRunId(thread, message);
|
|
13502
13860
|
const conversationId = threadId ?? runId;
|
|
@@ -13516,7 +13874,6 @@ function createReplyToThread(deps) {
|
|
|
13516
13874
|
const userText = stripLeadingBotMention(message.text, {
|
|
13517
13875
|
stripLeadingSlackMentionToken: options.explicitMention || Boolean(message.isMention)
|
|
13518
13876
|
});
|
|
13519
|
-
const explicitChannelPostIntent = isExplicitChannelPostIntent(userText);
|
|
13520
13877
|
const preparedState = options.preparedState ?? await deps.prepareTurnState({
|
|
13521
13878
|
thread,
|
|
13522
13879
|
message,
|
|
@@ -13587,20 +13944,13 @@ function createReplyToThread(deps) {
|
|
|
13587
13944
|
messageTs: slackMessageTs
|
|
13588
13945
|
}
|
|
13589
13946
|
);
|
|
13590
|
-
const
|
|
13591
|
-
|
|
13592
|
-
|
|
13593
|
-
|
|
13594
|
-
|
|
13595
|
-
})
|
|
13947
|
+
const omittedImageAttachmentCount = !isVisionEnabled() && hasPotentialImageAttachment(message.attachments) ? countPotentialImageAttachments(message.attachments) : 0;
|
|
13948
|
+
const status = createSlackAdapterAssistantStatusSession({
|
|
13949
|
+
channelId: assistantThreadContext?.channelId,
|
|
13950
|
+
threadTs: assistantThreadContext?.threadTs,
|
|
13951
|
+
getSlackAdapter: deps.getSlackAdapter
|
|
13596
13952
|
});
|
|
13597
|
-
const textStream = createTextStreamBridge();
|
|
13598
|
-
let streamedReplyPromise;
|
|
13599
|
-
let pendingStreamText = "";
|
|
13600
|
-
let pendingStreamDeltaCount = 0;
|
|
13601
|
-
let awaitingPostToolAssistantMessage = false;
|
|
13602
13953
|
let beforeFirstResponsePostCalled = false;
|
|
13603
|
-
let streamedReplyState = createSlackStreamAccumulator();
|
|
13604
13954
|
const beforeFirstResponsePost = async () => {
|
|
13605
13955
|
if (beforeFirstResponsePostCalled) {
|
|
13606
13956
|
return;
|
|
@@ -13608,70 +13958,6 @@ function createReplyToThread(deps) {
|
|
|
13608
13958
|
beforeFirstResponsePostCalled = true;
|
|
13609
13959
|
await options.beforeFirstResponsePost?.();
|
|
13610
13960
|
};
|
|
13611
|
-
const startStreamingReply = () => {
|
|
13612
|
-
if (!streamedReplyPromise) {
|
|
13613
|
-
const streamingReply = (async () => {
|
|
13614
|
-
return await postThreadReply(
|
|
13615
|
-
textStream.iterable,
|
|
13616
|
-
"streaming_initial_post"
|
|
13617
|
-
);
|
|
13618
|
-
})();
|
|
13619
|
-
streamedReplyPromise = streamingReply;
|
|
13620
|
-
}
|
|
13621
|
-
};
|
|
13622
|
-
const flushPendingStreamText = () => {
|
|
13623
|
-
if (!pendingStreamText) {
|
|
13624
|
-
return;
|
|
13625
|
-
}
|
|
13626
|
-
startStreamingReply();
|
|
13627
|
-
textStream.push(pendingStreamText);
|
|
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));
|
|
13674
|
-
};
|
|
13675
13961
|
const postThreadReply = async (payload, stage) => {
|
|
13676
13962
|
await beforeFirstResponsePost();
|
|
13677
13963
|
try {
|
|
@@ -13691,7 +13977,20 @@ function createReplyToThread(deps) {
|
|
|
13691
13977
|
throw error;
|
|
13692
13978
|
}
|
|
13693
13979
|
};
|
|
13694
|
-
|
|
13980
|
+
status.start();
|
|
13981
|
+
const assistantTitleTask = maybeUpdateAssistantTitle({
|
|
13982
|
+
assistantThreadContext,
|
|
13983
|
+
assistantUserName: botConfig.userName,
|
|
13984
|
+
artifacts: preparedState.artifacts,
|
|
13985
|
+
channelId,
|
|
13986
|
+
conversation: preparedState.conversation,
|
|
13987
|
+
generateThreadTitle: deps.services.generateThreadTitle,
|
|
13988
|
+
getSlackAdapter: deps.getSlackAdapter,
|
|
13989
|
+
modelId: botConfig.fastModelId,
|
|
13990
|
+
requesterId: message.author.userId,
|
|
13991
|
+
runId,
|
|
13992
|
+
threadId
|
|
13993
|
+
});
|
|
13695
13994
|
let persistedAtLeastOnce = false;
|
|
13696
13995
|
let shouldPersistFailureState = true;
|
|
13697
13996
|
try {
|
|
@@ -13712,6 +14011,8 @@ function createReplyToThread(deps) {
|
|
|
13712
14011
|
artifactState: preparedState.artifacts,
|
|
13713
14012
|
configuration: preparedState.configuration,
|
|
13714
14013
|
channelConfiguration: preparedState.channelConfiguration,
|
|
14014
|
+
inboundAttachmentCount: message.attachments.length,
|
|
14015
|
+
omittedImageAttachmentCount,
|
|
13715
14016
|
userAttachments,
|
|
13716
14017
|
correlation: {
|
|
13717
14018
|
conversationId,
|
|
@@ -13738,33 +14039,8 @@ function createReplyToThread(deps) {
|
|
|
13738
14039
|
await persistThreadState(thread, { artifacts });
|
|
13739
14040
|
},
|
|
13740
14041
|
threadParticipants,
|
|
13741
|
-
onStatus: (
|
|
13742
|
-
onTextDelta: (deltaText) => {
|
|
13743
|
-
if (explicitChannelPostIntent) {
|
|
13744
|
-
return;
|
|
13745
|
-
}
|
|
13746
|
-
appendVisibleStreamDelta(deltaText);
|
|
13747
|
-
},
|
|
13748
|
-
onAssistantMessageStart: () => {
|
|
13749
|
-
if (!awaitingPostToolAssistantMessage) {
|
|
13750
|
-
return;
|
|
13751
|
-
}
|
|
13752
|
-
awaitingPostToolAssistantMessage = false;
|
|
13753
|
-
discardPendingStreamPreview();
|
|
13754
|
-
},
|
|
13755
|
-
onToolCall: () => {
|
|
13756
|
-
if (!streamedReplyPromise) {
|
|
13757
|
-
awaitingPostToolAssistantMessage = true;
|
|
13758
|
-
discardPendingStreamPreview();
|
|
13759
|
-
}
|
|
13760
|
-
}
|
|
14042
|
+
onStatus: (nextStatus) => status.update(nextStatus)
|
|
13761
14043
|
});
|
|
13762
|
-
if (streamedReplyPromise) {
|
|
13763
|
-
flushPendingStreamText();
|
|
13764
|
-
} else {
|
|
13765
|
-
finalizePendingStreamText();
|
|
13766
|
-
}
|
|
13767
|
-
textStream.end();
|
|
13768
14044
|
const diagnosticsContext = {
|
|
13769
14045
|
slackThreadId: threadId,
|
|
13770
14046
|
slackUserId: message.author.userId,
|
|
@@ -13839,30 +14115,73 @@ function createReplyToThread(deps) {
|
|
|
13839
14115
|
const reactionPerformed = reply.diagnostics.toolCalls.includes(
|
|
13840
14116
|
"slackMessageAddReaction"
|
|
13841
14117
|
);
|
|
13842
|
-
const plannedPosts = planSlackReplyPosts({
|
|
13843
|
-
|
|
13844
|
-
|
|
13845
|
-
|
|
14118
|
+
const plannedPosts = planSlackReplyPosts({ reply });
|
|
14119
|
+
const replyFooter = buildSlackReplyFooter({
|
|
14120
|
+
conversationId,
|
|
14121
|
+
durationMs: reply.diagnostics.durationMs,
|
|
14122
|
+
traceId: getActiveTraceId(),
|
|
14123
|
+
usage: reply.diagnostics.usage
|
|
13846
14124
|
});
|
|
13847
|
-
|
|
13848
|
-
await streamedReplyPromise;
|
|
13849
|
-
}
|
|
14125
|
+
const shouldUseSlackFooter = Boolean(replyFooter) && Boolean(channelId && threadTs) && thread.adapter?.name === "slack";
|
|
13850
14126
|
if (plannedPosts.length > 0) {
|
|
13851
|
-
|
|
13852
|
-
|
|
13853
|
-
|
|
13854
|
-
|
|
14127
|
+
let sent;
|
|
14128
|
+
if (shouldUseSlackFooter) {
|
|
14129
|
+
const slackChannelId = channelId;
|
|
14130
|
+
const slackThreadTs = threadTs;
|
|
14131
|
+
if (!slackChannelId || !slackThreadTs) {
|
|
14132
|
+
throw new Error(
|
|
14133
|
+
"Slack footer delivery requires a concrete channel and thread timestamp"
|
|
14134
|
+
);
|
|
13855
14135
|
}
|
|
13856
|
-
const
|
|
13857
|
-
|
|
13858
|
-
|
|
13859
|
-
|
|
14136
|
+
const sentMessageTs = await postSlackApiReplyPosts({
|
|
14137
|
+
beforePost: beforeFirstResponsePost,
|
|
14138
|
+
channelId: slackChannelId,
|
|
14139
|
+
threadTs: slackThreadTs,
|
|
14140
|
+
posts: plannedPosts,
|
|
14141
|
+
fileUploadFailureMode: "strict",
|
|
14142
|
+
footer: replyFooter,
|
|
14143
|
+
onPostError: ({ error, messageTs: messageTs2, stage }) => {
|
|
14144
|
+
logException(
|
|
14145
|
+
error,
|
|
14146
|
+
"slack_thread_post_failed",
|
|
14147
|
+
turnTraceContext,
|
|
14148
|
+
{
|
|
14149
|
+
"app.slack.reply_stage": stage,
|
|
14150
|
+
...messageTs2 ? { "messaging.message.id": messageTs2 } : {},
|
|
14151
|
+
...getSlackErrorObservabilityAttributes(error)
|
|
14152
|
+
},
|
|
14153
|
+
"Failed to post Slack thread reply"
|
|
14154
|
+
);
|
|
14155
|
+
}
|
|
14156
|
+
});
|
|
14157
|
+
if (sentMessageTs) {
|
|
14158
|
+
sent = {
|
|
14159
|
+
id: sentMessageTs,
|
|
14160
|
+
text: reply.text,
|
|
14161
|
+
delete: async () => {
|
|
14162
|
+
await deleteSlackMessage({
|
|
14163
|
+
channelId: slackChannelId,
|
|
14164
|
+
timestamp: sentMessageTs
|
|
14165
|
+
});
|
|
14166
|
+
}
|
|
14167
|
+
};
|
|
13860
14168
|
}
|
|
13861
14169
|
} else {
|
|
13862
14170
|
for (const post of plannedPosts) {
|
|
13863
|
-
await postThreadReply(
|
|
14171
|
+
sent = await postThreadReply(
|
|
14172
|
+
buildSlackOutputMessage(post.text, post.files),
|
|
14173
|
+
post.stage
|
|
14174
|
+
);
|
|
13864
14175
|
}
|
|
13865
14176
|
}
|
|
14177
|
+
const firstPlannedMessageHasFiles = (plannedPosts[0]?.files?.length ?? 0) > 0;
|
|
14178
|
+
if (sent && reactionPerformed && plannedPosts.length === 1 && !firstPlannedMessageHasFiles && isRedundantReactionAckText(reply.text)) {
|
|
14179
|
+
await sent.delete();
|
|
14180
|
+
}
|
|
14181
|
+
}
|
|
14182
|
+
const titleUpdateResult = await assistantTitleTask;
|
|
14183
|
+
if (titleUpdateResult) {
|
|
14184
|
+
artifactStatePatch.assistantTitleSourceMessageId = titleUpdateResult;
|
|
13866
14185
|
}
|
|
13867
14186
|
const shouldPersistArtifacts = Object.keys(artifactStatePatch).length > 0;
|
|
13868
14187
|
const nextArtifacts = shouldPersistArtifacts ? mergeArtifactsState(preparedState.artifacts, artifactStatePatch) : void 0;
|
|
@@ -13891,73 +14210,17 @@ function createReplyToThread(deps) {
|
|
|
13891
14210
|
"Agent turn completed"
|
|
13892
14211
|
);
|
|
13893
14212
|
}
|
|
13894
|
-
const isFirstAssistantReply = preparedState.conversation.stats.compactedMessageCount === 0 && preparedState.conversation.messages.filter(
|
|
13895
|
-
(m) => m.role === "assistant"
|
|
13896
|
-
).length === 1;
|
|
13897
|
-
if (isFirstAssistantReply && channelId && isDmChannel(channelId) && threadTs) {
|
|
13898
|
-
void deps.services.generateThreadTitle(userText, reply.text).then(
|
|
13899
|
-
(title) => deps.getSlackAdapter().setAssistantTitle(channelId, threadTs, title)
|
|
13900
|
-
).catch((error) => {
|
|
13901
|
-
const slackErrorCode = getSlackApiErrorCode(error);
|
|
13902
|
-
const assistantTitleErrorAttributes = {
|
|
13903
|
-
"app.slack.assistant_title.outcome": "permission_denied",
|
|
13904
|
-
...slackErrorCode ? { "app.slack.assistant_title.error_code": slackErrorCode } : {}
|
|
13905
|
-
};
|
|
13906
|
-
if (isSlackTitlePermissionError(error)) {
|
|
13907
|
-
setSpanAttributes(assistantTitleErrorAttributes);
|
|
13908
|
-
logError(
|
|
13909
|
-
"thread_title_generation_permission_denied",
|
|
13910
|
-
{
|
|
13911
|
-
slackThreadId: threadId,
|
|
13912
|
-
slackUserId: message.author.userId,
|
|
13913
|
-
slackChannelId: channelId,
|
|
13914
|
-
runId,
|
|
13915
|
-
assistantUserName: botConfig.userName,
|
|
13916
|
-
modelId: botConfig.fastModelId
|
|
13917
|
-
},
|
|
13918
|
-
assistantTitleErrorAttributes,
|
|
13919
|
-
"Skipping thread title update due to Slack permission error"
|
|
13920
|
-
);
|
|
13921
|
-
return;
|
|
13922
|
-
}
|
|
13923
|
-
logWarn(
|
|
13924
|
-
"thread_title_generation_failed",
|
|
13925
|
-
{
|
|
13926
|
-
slackThreadId: threadId,
|
|
13927
|
-
slackUserId: message.author.userId,
|
|
13928
|
-
slackChannelId: channelId,
|
|
13929
|
-
runId,
|
|
13930
|
-
assistantUserName: botConfig.userName,
|
|
13931
|
-
modelId: botConfig.fastModelId
|
|
13932
|
-
},
|
|
13933
|
-
{
|
|
13934
|
-
"error.message": error instanceof Error ? error.message : String(error)
|
|
13935
|
-
},
|
|
13936
|
-
"Thread title generation failed"
|
|
13937
|
-
);
|
|
13938
|
-
});
|
|
13939
|
-
}
|
|
13940
14213
|
} catch (error) {
|
|
13941
14214
|
if (isRetryableTurnError(error, "mcp_auth_resume")) {
|
|
13942
14215
|
shouldPersistFailureState = false;
|
|
13943
14216
|
throw error;
|
|
13944
14217
|
}
|
|
13945
14218
|
if (isRetryableTurnError(error, "turn_timeout_resume")) {
|
|
13946
|
-
textStream.end();
|
|
13947
|
-
const hasVisibleAssistantOutput = Boolean(streamedReplyPromise);
|
|
13948
|
-
if (hasVisibleAssistantOutput) {
|
|
13949
|
-
logWarn(
|
|
13950
|
-
"agent_turn_timeout_resume_skipped_after_visible_output",
|
|
13951
|
-
turnTraceContext,
|
|
13952
|
-
messageTs ? { "messaging.message.id": messageTs } : {},
|
|
13953
|
-
"Skipped automatic timeout resume because assistant text had already started streaming"
|
|
13954
|
-
);
|
|
13955
|
-
}
|
|
13956
14219
|
const conversationIdForResume = error.metadata?.conversationId;
|
|
13957
14220
|
const sessionIdForResume = error.metadata?.sessionId;
|
|
13958
14221
|
const checkpointVersion = error.metadata?.checkpointVersion;
|
|
13959
14222
|
const nextSliceId = error.metadata?.sliceId;
|
|
13960
|
-
if (
|
|
14223
|
+
if (conversationIdForResume && sessionIdForResume && typeof checkpointVersion === "number" && canScheduleTurnTimeoutResume(nextSliceId)) {
|
|
13961
14224
|
try {
|
|
13962
14225
|
await deps.services.scheduleTurnTimeoutResume({
|
|
13963
14226
|
conversationId: conversationIdForResume,
|
|
@@ -13978,7 +14241,7 @@ function createReplyToThread(deps) {
|
|
|
13978
14241
|
"Failed to schedule timeout resume callback"
|
|
13979
14242
|
);
|
|
13980
14243
|
}
|
|
13981
|
-
} else if (
|
|
14244
|
+
} else if (conversationIdForResume && sessionIdForResume && typeof checkpointVersion === "number") {
|
|
13982
14245
|
logWarn(
|
|
13983
14246
|
"agent_turn_timeout_resume_slice_limit_reached",
|
|
13984
14247
|
turnTraceContext,
|
|
@@ -14000,7 +14263,6 @@ function createReplyToThread(deps) {
|
|
|
14000
14263
|
shouldPersistFailureState = true;
|
|
14001
14264
|
throw error;
|
|
14002
14265
|
} finally {
|
|
14003
|
-
textStream.end();
|
|
14004
14266
|
if (!persistedAtLeastOnce && shouldPersistFailureState) {
|
|
14005
14267
|
markTurnFailed({
|
|
14006
14268
|
conversation: preparedState.conversation,
|
|
@@ -14025,19 +14287,26 @@ function createReplyToThread(deps) {
|
|
|
14025
14287
|
);
|
|
14026
14288
|
}
|
|
14027
14289
|
}
|
|
14028
|
-
await
|
|
14290
|
+
await status.stop();
|
|
14029
14291
|
}
|
|
14030
14292
|
}
|
|
14031
14293
|
);
|
|
14032
14294
|
};
|
|
14033
14295
|
}
|
|
14034
14296
|
|
|
14035
|
-
// src/chat/
|
|
14297
|
+
// src/chat/slack/assistant-thread/lifecycle.ts
|
|
14036
14298
|
import { ThreadImpl } from "chat";
|
|
14037
|
-
async function
|
|
14299
|
+
async function syncAssistantThreadContext(event, options) {
|
|
14300
|
+
const channelId = normalizeSlackConversationId(event.channelId);
|
|
14301
|
+
if (!channelId) {
|
|
14302
|
+
throw new Error("Assistant thread initialization requires a channel ID");
|
|
14303
|
+
}
|
|
14304
|
+
const sourceChannelId = event.sourceChannelId ? normalizeSlackConversationId(event.sourceChannelId) : void 0;
|
|
14038
14305
|
const slack = event.getSlackAdapter();
|
|
14039
|
-
|
|
14040
|
-
|
|
14306
|
+
if (options.setInitialTitle) {
|
|
14307
|
+
await slack.setAssistantTitle(channelId, event.threadTs, "Junior");
|
|
14308
|
+
}
|
|
14309
|
+
await slack.setSuggestedPrompts(channelId, event.threadTs, [
|
|
14041
14310
|
{
|
|
14042
14311
|
title: "Summarize thread",
|
|
14043
14312
|
message: "Summarize the latest discussion in this thread."
|
|
@@ -14048,24 +14317,30 @@ async function initializeAssistantThread(event) {
|
|
|
14048
14317
|
message: "Generate an image based on this conversation."
|
|
14049
14318
|
}
|
|
14050
14319
|
]);
|
|
14051
|
-
if (!
|
|
14320
|
+
if (!sourceChannelId) {
|
|
14052
14321
|
return;
|
|
14053
14322
|
}
|
|
14054
14323
|
const thread = ThreadImpl.fromJSON({
|
|
14055
14324
|
_type: "chat:Thread",
|
|
14056
14325
|
adapterName: "slack",
|
|
14057
|
-
channelId
|
|
14326
|
+
channelId,
|
|
14058
14327
|
id: event.threadId,
|
|
14059
|
-
isDM:
|
|
14328
|
+
isDM: channelId.startsWith("D")
|
|
14060
14329
|
});
|
|
14061
14330
|
const currentArtifacts = coerceThreadArtifactsState(await thread.state);
|
|
14062
14331
|
const nextArtifacts = mergeArtifactsState(currentArtifacts, {
|
|
14063
|
-
assistantContextChannelId:
|
|
14332
|
+
assistantContextChannelId: sourceChannelId
|
|
14064
14333
|
});
|
|
14065
14334
|
await persistThreadState(thread, {
|
|
14066
14335
|
artifacts: nextArtifacts
|
|
14067
14336
|
});
|
|
14068
14337
|
}
|
|
14338
|
+
async function initializeAssistantThread(event) {
|
|
14339
|
+
await syncAssistantThreadContext(event, { setInitialTitle: true });
|
|
14340
|
+
}
|
|
14341
|
+
async function refreshAssistantThreadContext(event) {
|
|
14342
|
+
await syncAssistantThreadContext(event, { setInitialTitle: false });
|
|
14343
|
+
}
|
|
14069
14344
|
|
|
14070
14345
|
// src/chat/runtime/turn-preparation.ts
|
|
14071
14346
|
function hasPendingImageHydration(conversation) {
|
|
@@ -14093,6 +14368,7 @@ function createPrepareTurnState(deps) {
|
|
|
14093
14368
|
const messageHasPotentialImageAttachment = hasPotentialImageAttachment(
|
|
14094
14369
|
args.message.attachments
|
|
14095
14370
|
);
|
|
14371
|
+
const imageAttachmentCount = messageHasPotentialImageAttachment ? countPotentialImageAttachments(args.message.attachments) : 0;
|
|
14096
14372
|
const normalizedUserText = normalizeConversationText(args.userText) || "[non-text message]";
|
|
14097
14373
|
const slackTs = getSlackMessageTs(args.message);
|
|
14098
14374
|
const incomingUserMessage = {
|
|
@@ -14107,7 +14383,9 @@ function createPrepareTurnState(deps) {
|
|
|
14107
14383
|
isBot: typeof args.message.author.isBot === "boolean" ? args.message.author.isBot : void 0
|
|
14108
14384
|
},
|
|
14109
14385
|
meta: {
|
|
14386
|
+
attachmentCount: args.message.attachments.length,
|
|
14110
14387
|
explicitMention: args.explicitMention,
|
|
14388
|
+
imageAttachmentCount: imageAttachmentCount > 0 ? imageAttachmentCount : void 0,
|
|
14111
14389
|
slackTs,
|
|
14112
14390
|
imagesHydrated: !messageHasPotentialImageAttachment
|
|
14113
14391
|
}
|
|
@@ -14261,6 +14539,20 @@ function createSlackRuntime(options) {
|
|
|
14261
14539
|
sourceChannelId,
|
|
14262
14540
|
getSlackAdapter: options.getSlackAdapter
|
|
14263
14541
|
});
|
|
14542
|
+
},
|
|
14543
|
+
refreshAssistantThreadContext: async ({
|
|
14544
|
+
threadId,
|
|
14545
|
+
channelId,
|
|
14546
|
+
threadTs,
|
|
14547
|
+
sourceChannelId
|
|
14548
|
+
}) => {
|
|
14549
|
+
await refreshAssistantThreadContext({
|
|
14550
|
+
threadId,
|
|
14551
|
+
channelId,
|
|
14552
|
+
threadTs,
|
|
14553
|
+
sourceChannelId,
|
|
14554
|
+
getSlackAdapter: options.getSlackAdapter
|
|
14555
|
+
});
|
|
14264
14556
|
}
|
|
14265
14557
|
});
|
|
14266
14558
|
}
|
|
@@ -14333,14 +14625,14 @@ var JuniorChat = class extends Chat {
|
|
|
14333
14625
|
(async () => {
|
|
14334
14626
|
try {
|
|
14335
14627
|
const message = await messageOrFactory();
|
|
14336
|
-
const
|
|
14628
|
+
const normalized = normalizeIncomingSlackThreadId(
|
|
14337
14629
|
threadId,
|
|
14338
14630
|
message
|
|
14339
14631
|
);
|
|
14340
|
-
if (
|
|
14341
|
-
message.threadId =
|
|
14632
|
+
if (normalized !== threadId && "threadId" in message) {
|
|
14633
|
+
message.threadId = normalized;
|
|
14342
14634
|
}
|
|
14343
|
-
super.processMessage(adapter,
|
|
14635
|
+
super.processMessage(adapter, normalized, message, options);
|
|
14344
14636
|
} catch (error) {
|
|
14345
14637
|
runtime.logger?.error?.("Message factory resolution error", {
|
|
14346
14638
|
error,
|
|
@@ -14351,14 +14643,19 @@ var JuniorChat = class extends Chat {
|
|
|
14351
14643
|
);
|
|
14352
14644
|
return;
|
|
14353
14645
|
}
|
|
14354
|
-
|
|
14355
|
-
|
|
14356
|
-
|
|
14646
|
+
enqueueBackgroundTask(
|
|
14647
|
+
options,
|
|
14648
|
+
(async () => {
|
|
14649
|
+
const normalized = normalizeIncomingSlackThreadId(
|
|
14650
|
+
threadId,
|
|
14651
|
+
messageOrFactory
|
|
14652
|
+
);
|
|
14653
|
+
if (normalized !== threadId && "threadId" in messageOrFactory) {
|
|
14654
|
+
messageOrFactory.threadId = normalized;
|
|
14655
|
+
}
|
|
14656
|
+
super.processMessage(adapter, normalized, messageOrFactory, options);
|
|
14657
|
+
})()
|
|
14357
14658
|
);
|
|
14358
|
-
if (normalized !== threadId && "threadId" in messageOrFactory) {
|
|
14359
|
-
messageOrFactory.threadId = normalized;
|
|
14360
|
-
}
|
|
14361
|
-
super.processMessage(adapter, normalized, messageOrFactory, options);
|
|
14362
14659
|
}
|
|
14363
14660
|
processReaction(event, options) {
|
|
14364
14661
|
const runtime = this;
|
|
@@ -14379,20 +14676,19 @@ var JuniorChat = class extends Chat {
|
|
|
14379
14676
|
}
|
|
14380
14677
|
processAction(event, options) {
|
|
14381
14678
|
const runtime = this;
|
|
14382
|
-
|
|
14383
|
-
|
|
14384
|
-
|
|
14385
|
-
|
|
14386
|
-
|
|
14387
|
-
|
|
14388
|
-
|
|
14389
|
-
|
|
14390
|
-
|
|
14391
|
-
|
|
14392
|
-
|
|
14393
|
-
|
|
14394
|
-
|
|
14395
|
-
);
|
|
14679
|
+
const task = (async () => {
|
|
14680
|
+
try {
|
|
14681
|
+
await runtime.handleActionEvent(event, options);
|
|
14682
|
+
} catch (error) {
|
|
14683
|
+
runtime.logger?.error?.("Action processing error", {
|
|
14684
|
+
error,
|
|
14685
|
+
actionId: event.actionId,
|
|
14686
|
+
messageId: event.messageId
|
|
14687
|
+
});
|
|
14688
|
+
}
|
|
14689
|
+
})();
|
|
14690
|
+
enqueueBackgroundTask(options, task);
|
|
14691
|
+
return task;
|
|
14396
14692
|
}
|
|
14397
14693
|
processModalClose(event, contextId, options) {
|
|
14398
14694
|
const runtime = this;
|
|
@@ -14430,7 +14726,7 @@ var JuniorChat = class extends Chat {
|
|
|
14430
14726
|
options,
|
|
14431
14727
|
(async () => {
|
|
14432
14728
|
try {
|
|
14433
|
-
await runtime.handleSlashCommandEvent(event);
|
|
14729
|
+
await runtime.handleSlashCommandEvent(event, options);
|
|
14434
14730
|
} catch (error) {
|
|
14435
14731
|
runtime.logger?.error?.("Slash command processing error", {
|
|
14436
14732
|
error,
|
|
@@ -14501,160 +14797,8 @@ var JuniorChat = class extends Chat {
|
|
|
14501
14797
|
import {
|
|
14502
14798
|
createSlackAdapter
|
|
14503
14799
|
} 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
14800
|
function createJuniorSlackAdapter(config) {
|
|
14652
|
-
|
|
14653
|
-
const internals = adapter;
|
|
14654
|
-
assertSlackAdapterInternals(internals);
|
|
14655
|
-
patchSlackClientStream(adapter);
|
|
14656
|
-
patchSlackAdapterStream(adapter);
|
|
14657
|
-
return adapter;
|
|
14801
|
+
return createSlackAdapter(config);
|
|
14658
14802
|
}
|
|
14659
14803
|
|
|
14660
14804
|
// src/chat/queue/thread-message-dispatcher.ts
|