@sentry/junior 0.24.0 → 0.25.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/app.js +1100 -871
- package/dist/{chunk-B5O2EJUV.js → chunk-A75TWGF2.js} +1 -1
- package/dist/{chunk-DGN3WLA4.js → chunk-ICIRAL6Y.js} +9 -52
- package/dist/{chunk-J7JEFMVD.js → chunk-RZJDO55D.js} +138 -158
- package/dist/cli/check.js +2 -2
- package/dist/cli/snapshot-warmup.js +2 -2
- package/package.json +1 -1
package/dist/app.js
CHANGED
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
2
|
discoverSkills,
|
|
3
3
|
findSkillByName,
|
|
4
|
-
getCapabilityProvider,
|
|
5
|
-
listCapabilityProviders,
|
|
6
4
|
loadSkillsByName,
|
|
7
5
|
logCapabilityCatalogLoadedOnce,
|
|
8
6
|
parseSkillInvocation
|
|
9
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-ICIRAL6Y.js";
|
|
10
8
|
import {
|
|
11
9
|
SANDBOX_DATA_ROOT,
|
|
12
10
|
SANDBOX_SKILLS_ROOT,
|
|
@@ -27,7 +25,7 @@ import {
|
|
|
27
25
|
sandboxSkillDir,
|
|
28
26
|
sandboxSkillFile,
|
|
29
27
|
toOptionalTrimmed
|
|
30
|
-
} from "./chunk-
|
|
28
|
+
} from "./chunk-A75TWGF2.js";
|
|
31
29
|
import {
|
|
32
30
|
CredentialUnavailableError,
|
|
33
31
|
buildOAuthTokenRequest,
|
|
@@ -35,6 +33,7 @@ import {
|
|
|
35
33
|
createPluginBroker,
|
|
36
34
|
createRequestContext,
|
|
37
35
|
extractGenAiUsageAttributes,
|
|
36
|
+
extractGenAiUsageSummary,
|
|
38
37
|
getActiveTraceId,
|
|
39
38
|
getPluginDefinition,
|
|
40
39
|
getPluginMcpProviders,
|
|
@@ -58,7 +57,7 @@ import {
|
|
|
58
57
|
toOptionalString,
|
|
59
58
|
withContext,
|
|
60
59
|
withSpan
|
|
61
|
-
} from "./chunk-
|
|
60
|
+
} from "./chunk-RZJDO55D.js";
|
|
62
61
|
import "./chunk-Z3YD6NHK.js";
|
|
63
62
|
import {
|
|
64
63
|
discoverInstalledPluginPackageContent,
|
|
@@ -1082,6 +1081,9 @@ async function postSlackMessage(input) {
|
|
|
1082
1081
|
channel: channelId,
|
|
1083
1082
|
text,
|
|
1084
1083
|
mrkdwn: true,
|
|
1084
|
+
...input.blocks?.length ? {
|
|
1085
|
+
blocks: input.blocks
|
|
1086
|
+
} : {},
|
|
1085
1087
|
...threadTs ? { thread_ts: threadTs } : {}
|
|
1086
1088
|
}),
|
|
1087
1089
|
3,
|
|
@@ -1100,6 +1102,24 @@ async function postSlackMessage(input) {
|
|
|
1100
1102
|
} : {}
|
|
1101
1103
|
};
|
|
1102
1104
|
}
|
|
1105
|
+
async function deleteSlackMessage(input) {
|
|
1106
|
+
const channelId = requireSlackConversationId(
|
|
1107
|
+
input.channelId,
|
|
1108
|
+
"Slack message deletion"
|
|
1109
|
+
);
|
|
1110
|
+
const timestamp = requireSlackMessageTimestamp(
|
|
1111
|
+
input.timestamp,
|
|
1112
|
+
"Slack message deletion"
|
|
1113
|
+
);
|
|
1114
|
+
await withSlackRetries(
|
|
1115
|
+
() => getSlackClient().chat.delete({
|
|
1116
|
+
channel: channelId,
|
|
1117
|
+
ts: timestamp
|
|
1118
|
+
}),
|
|
1119
|
+
3,
|
|
1120
|
+
{ action: "chat.delete" }
|
|
1121
|
+
);
|
|
1122
|
+
}
|
|
1103
1123
|
async function postSlackEphemeralMessage(input) {
|
|
1104
1124
|
const channelId = requireSlackConversationId(
|
|
1105
1125
|
input.channelId,
|
|
@@ -1197,234 +1217,6 @@ async function addReactionToMessage(input) {
|
|
|
1197
1217
|
return { ok: true };
|
|
1198
1218
|
}
|
|
1199
1219
|
|
|
1200
|
-
// src/chat/respond-helpers.ts
|
|
1201
|
-
var MAX_INLINE_ATTACHMENT_BASE64_CHARS = 12e4;
|
|
1202
|
-
function getSessionIdentifiers(context) {
|
|
1203
|
-
return {
|
|
1204
|
-
conversationId: context.correlation?.conversationId ?? context.correlation?.threadId ?? context.correlation?.runId,
|
|
1205
|
-
sessionId: context.correlation?.turnId
|
|
1206
|
-
};
|
|
1207
|
-
}
|
|
1208
|
-
function isExecutionDeferralResponse(text) {
|
|
1209
|
-
return /\b(want me to proceed|do you want me to proceed|shall i proceed|can i proceed|should i proceed|let me do that now|give me a moment|tag me again|fresh invocation)\b/i.test(
|
|
1210
|
-
text
|
|
1211
|
-
);
|
|
1212
|
-
}
|
|
1213
|
-
function isToolAccessDisclaimerResponse(text) {
|
|
1214
|
-
return /\b(i (don't|do not) have access to (active )?tool|tool results came back empty|prior results .* empty|cannot access .*tool|need to (run|load) .*tool .* first)\b/i.test(
|
|
1215
|
-
text
|
|
1216
|
-
);
|
|
1217
|
-
}
|
|
1218
|
-
function isExecutionEscapeResponse(text) {
|
|
1219
|
-
const trimmed = text.trim();
|
|
1220
|
-
if (!trimmed) return false;
|
|
1221
|
-
return isExecutionDeferralResponse(trimmed) || isToolAccessDisclaimerResponse(trimmed);
|
|
1222
|
-
}
|
|
1223
|
-
function parseJsonCandidate(text) {
|
|
1224
|
-
const trimmed = text.trim();
|
|
1225
|
-
if (!trimmed) return void 0;
|
|
1226
|
-
try {
|
|
1227
|
-
return JSON.parse(trimmed);
|
|
1228
|
-
} catch {
|
|
1229
|
-
const fenced = trimmed.match(/^```(?:json)?\s*([\s\S]*?)\s*```$/i);
|
|
1230
|
-
if (!fenced) return void 0;
|
|
1231
|
-
try {
|
|
1232
|
-
return JSON.parse(fenced[1]);
|
|
1233
|
-
} catch {
|
|
1234
|
-
return void 0;
|
|
1235
|
-
}
|
|
1236
|
-
}
|
|
1237
|
-
}
|
|
1238
|
-
function isToolPayloadShape(payload) {
|
|
1239
|
-
if (!payload || typeof payload !== "object") return false;
|
|
1240
|
-
const record = payload;
|
|
1241
|
-
const type = typeof record.type === "string" ? record.type.toLowerCase() : "";
|
|
1242
|
-
if (type.startsWith("tool-")) return true;
|
|
1243
|
-
if (type === "tool_use" || type === "tool_call" || type === "tool_result" || type === "tool_error")
|
|
1244
|
-
return true;
|
|
1245
|
-
const hasToolName = typeof record.toolName === "string" || typeof record.name === "string";
|
|
1246
|
-
const hasToolInput = Object.prototype.hasOwnProperty.call(record, "input") || Object.prototype.hasOwnProperty.call(record, "args");
|
|
1247
|
-
if (hasToolName && hasToolInput) return true;
|
|
1248
|
-
return false;
|
|
1249
|
-
}
|
|
1250
|
-
function isRawToolPayloadResponse(text) {
|
|
1251
|
-
const parsed = parseJsonCandidate(text);
|
|
1252
|
-
if (Array.isArray(parsed)) {
|
|
1253
|
-
return parsed.some((entry) => isToolPayloadShape(entry));
|
|
1254
|
-
}
|
|
1255
|
-
if (isToolPayloadShape(parsed)) {
|
|
1256
|
-
return true;
|
|
1257
|
-
}
|
|
1258
|
-
const compact = text.replace(/\s+/g, " ");
|
|
1259
|
-
return /"type"\s*:\s*"tool[-_](use|call|result|error)"/i.test(compact);
|
|
1260
|
-
}
|
|
1261
|
-
function toObservablePromptPart(part) {
|
|
1262
|
-
if (part.type === "text") {
|
|
1263
|
-
return {
|
|
1264
|
-
type: "text",
|
|
1265
|
-
text: part.text
|
|
1266
|
-
};
|
|
1267
|
-
}
|
|
1268
|
-
return {
|
|
1269
|
-
type: "image",
|
|
1270
|
-
mimeType: part.mimeType,
|
|
1271
|
-
data: `[omitted:${part.data.length}]`
|
|
1272
|
-
};
|
|
1273
|
-
}
|
|
1274
|
-
function summarizeMessageText(text) {
|
|
1275
|
-
const normalized = text.trim().replace(/\s+/g, " ");
|
|
1276
|
-
if (!normalized) {
|
|
1277
|
-
return "[empty]";
|
|
1278
|
-
}
|
|
1279
|
-
return normalized.length > 1200 ? `${normalized.slice(0, 1200)}...` : normalized;
|
|
1280
|
-
}
|
|
1281
|
-
function buildUserTurnText(userInput, conversationContext, metadata) {
|
|
1282
|
-
const trimmedContext = conversationContext?.trim();
|
|
1283
|
-
const hasSessionContext = Boolean(metadata?.sessionContext?.conversationId);
|
|
1284
|
-
const hasTurnContext = Boolean(metadata?.turnContext?.traceId);
|
|
1285
|
-
if (!trimmedContext && !hasSessionContext && !hasTurnContext) {
|
|
1286
|
-
return userInput;
|
|
1287
|
-
}
|
|
1288
|
-
const sections = [
|
|
1289
|
-
"<current-message>",
|
|
1290
|
-
userInput,
|
|
1291
|
-
"</current-message>"
|
|
1292
|
-
];
|
|
1293
|
-
if (trimmedContext) {
|
|
1294
|
-
sections.push(
|
|
1295
|
-
"",
|
|
1296
|
-
"<thread-conversation-context>",
|
|
1297
|
-
"Use this context for continuity across prior thread turns.",
|
|
1298
|
-
trimmedContext,
|
|
1299
|
-
"</thread-conversation-context>"
|
|
1300
|
-
);
|
|
1301
|
-
}
|
|
1302
|
-
if (metadata?.sessionContext?.conversationId) {
|
|
1303
|
-
sections.push(
|
|
1304
|
-
"",
|
|
1305
|
-
"<session-context>",
|
|
1306
|
-
`- gen_ai.conversation.id: ${metadata.sessionContext.conversationId}`,
|
|
1307
|
-
"</session-context>"
|
|
1308
|
-
);
|
|
1309
|
-
}
|
|
1310
|
-
if (metadata?.turnContext?.traceId) {
|
|
1311
|
-
sections.push(
|
|
1312
|
-
"",
|
|
1313
|
-
"<turn-context>",
|
|
1314
|
-
`- trace_id: ${metadata.turnContext.traceId}`,
|
|
1315
|
-
"</turn-context>"
|
|
1316
|
-
);
|
|
1317
|
-
}
|
|
1318
|
-
return sections.join("\n");
|
|
1319
|
-
}
|
|
1320
|
-
function encodeNonImageAttachmentForPrompt(attachment) {
|
|
1321
|
-
const base64 = attachment.data.toString("base64");
|
|
1322
|
-
const wasTruncated = base64.length > MAX_INLINE_ATTACHMENT_BASE64_CHARS;
|
|
1323
|
-
const encodedPayload = wasTruncated ? `${base64.slice(0, MAX_INLINE_ATTACHMENT_BASE64_CHARS)}...` : base64;
|
|
1324
|
-
return [
|
|
1325
|
-
"<attachment>",
|
|
1326
|
-
`filename: ${attachment.filename ?? "unnamed"}`,
|
|
1327
|
-
`media_type: ${attachment.mediaType}`,
|
|
1328
|
-
"encoding: base64",
|
|
1329
|
-
`truncated: ${wasTruncated ? "true" : "false"}`,
|
|
1330
|
-
"<data_base64>",
|
|
1331
|
-
encodedPayload,
|
|
1332
|
-
"</data_base64>",
|
|
1333
|
-
"</attachment>"
|
|
1334
|
-
].join("\n");
|
|
1335
|
-
}
|
|
1336
|
-
function buildExecutionFailureMessage(toolErrorCount) {
|
|
1337
|
-
if (toolErrorCount > 0) {
|
|
1338
|
-
return "I couldn't complete this because one or more required tools failed in this turn. I've logged the failure details.";
|
|
1339
|
-
}
|
|
1340
|
-
return "I couldn't complete this request in this turn due to an execution failure. I've logged the details for debugging.";
|
|
1341
|
-
}
|
|
1342
|
-
function isToolResultMessage(value) {
|
|
1343
|
-
return typeof value === "object" && value !== null && value.role === "toolResult";
|
|
1344
|
-
}
|
|
1345
|
-
function normalizeToolNameFromResult(result) {
|
|
1346
|
-
if (!result || typeof result !== "object") return void 0;
|
|
1347
|
-
const record = result;
|
|
1348
|
-
if (typeof record.toolName === "string" && record.toolName.length > 0) {
|
|
1349
|
-
return record.toolName;
|
|
1350
|
-
}
|
|
1351
|
-
if (typeof record.name === "string" && record.name.length > 0) {
|
|
1352
|
-
return record.name;
|
|
1353
|
-
}
|
|
1354
|
-
return void 0;
|
|
1355
|
-
}
|
|
1356
|
-
function isToolResultError(result) {
|
|
1357
|
-
if (!result || typeof result !== "object") return false;
|
|
1358
|
-
return Boolean(result.isError);
|
|
1359
|
-
}
|
|
1360
|
-
function isAssistantMessage(value) {
|
|
1361
|
-
return typeof value === "object" && value !== null && value.role === "assistant";
|
|
1362
|
-
}
|
|
1363
|
-
function getPiMessageRole(value) {
|
|
1364
|
-
if (!value || typeof value !== "object") {
|
|
1365
|
-
return void 0;
|
|
1366
|
-
}
|
|
1367
|
-
const role = value.role;
|
|
1368
|
-
return typeof role === "string" ? role : void 0;
|
|
1369
|
-
}
|
|
1370
|
-
function extractAssistantText(message) {
|
|
1371
|
-
const content = message.content ?? [];
|
|
1372
|
-
return content.filter(
|
|
1373
|
-
(part) => part.type === "text" && typeof part.text === "string"
|
|
1374
|
-
).map((part) => part.text).join("\n");
|
|
1375
|
-
}
|
|
1376
|
-
function getTerminalAssistantMessages(messages) {
|
|
1377
|
-
let lastToolResultIndex = -1;
|
|
1378
|
-
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
|
1379
|
-
if (isToolResultMessage(messages[index])) {
|
|
1380
|
-
lastToolResultIndex = index;
|
|
1381
|
-
break;
|
|
1382
|
-
}
|
|
1383
|
-
}
|
|
1384
|
-
return messages.slice(lastToolResultIndex + 1).filter(isAssistantMessage);
|
|
1385
|
-
}
|
|
1386
|
-
function hasCompletedAssistantTurn(messages) {
|
|
1387
|
-
const message = getTerminalAssistantMessages(messages).at(-1);
|
|
1388
|
-
if (!message) {
|
|
1389
|
-
return false;
|
|
1390
|
-
}
|
|
1391
|
-
const stopReason = message.stopReason;
|
|
1392
|
-
return typeof stopReason === "string" && stopReason !== "error" && extractAssistantText(message).trim().length > 0;
|
|
1393
|
-
}
|
|
1394
|
-
function upsertActiveSkill(activeSkills, next) {
|
|
1395
|
-
const existing = activeSkills.find((skill) => skill.name === next.name);
|
|
1396
|
-
if (existing) {
|
|
1397
|
-
existing.body = next.body;
|
|
1398
|
-
existing.description = next.description;
|
|
1399
|
-
existing.skillPath = next.skillPath;
|
|
1400
|
-
existing.allowedTools = next.allowedTools;
|
|
1401
|
-
existing.requiresCapabilities = next.requiresCapabilities;
|
|
1402
|
-
existing.usesConfig = next.usesConfig;
|
|
1403
|
-
existing.pluginProvider = next.pluginProvider;
|
|
1404
|
-
return;
|
|
1405
|
-
}
|
|
1406
|
-
activeSkills.push(next);
|
|
1407
|
-
}
|
|
1408
|
-
function collectRelevantConfigurationKeys(activeSkills, explicitSkill) {
|
|
1409
|
-
const keys = /* @__PURE__ */ new Set();
|
|
1410
|
-
for (const skill of [
|
|
1411
|
-
...activeSkills,
|
|
1412
|
-
...explicitSkill ? [explicitSkill] : []
|
|
1413
|
-
]) {
|
|
1414
|
-
for (const key of skill.usesConfig ?? []) {
|
|
1415
|
-
keys.add(key);
|
|
1416
|
-
}
|
|
1417
|
-
}
|
|
1418
|
-
return [...keys].sort((a, b) => a.localeCompare(b));
|
|
1419
|
-
}
|
|
1420
|
-
function trimTrailingAssistantMessages(messages) {
|
|
1421
|
-
let end = messages.length;
|
|
1422
|
-
while (end > 0 && getPiMessageRole(messages[end - 1]) === "assistant") {
|
|
1423
|
-
end -= 1;
|
|
1424
|
-
}
|
|
1425
|
-
return end === messages.length ? [...messages] : messages.slice(0, end);
|
|
1426
|
-
}
|
|
1427
|
-
|
|
1428
1220
|
// src/chat/oauth-flow.ts
|
|
1429
1221
|
var OAUTH_STATE_TTL_MS = 10 * 60 * 1e3;
|
|
1430
1222
|
function formatProviderLabel(provider) {
|
|
@@ -1538,7 +1330,9 @@ async function startOAuthFlow(provider, input) {
|
|
|
1538
1330
|
...input.channelId ? { channelId: input.channelId } : {},
|
|
1539
1331
|
...input.threadTs ? { threadTs: input.threadTs } : {},
|
|
1540
1332
|
...input.userMessage ? { pendingMessage: input.userMessage } : {},
|
|
1541
|
-
...configuration && Object.keys(configuration).length > 0 ? { configuration } : {}
|
|
1333
|
+
...configuration && Object.keys(configuration).length > 0 ? { configuration } : {},
|
|
1334
|
+
...input.resumeConversationId ? { resumeConversationId: input.resumeConversationId } : {},
|
|
1335
|
+
...input.resumeSessionId ? { resumeSessionId: input.resumeSessionId } : {}
|
|
1542
1336
|
},
|
|
1543
1337
|
OAUTH_STATE_TTL_MS
|
|
1544
1338
|
);
|
|
@@ -1575,61 +1369,6 @@ async function startOAuthFlow(provider, input) {
|
|
|
1575
1369
|
})
|
|
1576
1370
|
};
|
|
1577
1371
|
}
|
|
1578
|
-
function extractOAuthStartedPayload(value) {
|
|
1579
|
-
if (typeof value === "string") {
|
|
1580
|
-
const parsed = parseJsonCandidate(value);
|
|
1581
|
-
return parsed === void 0 ? void 0 : extractOAuthStartedPayload(parsed);
|
|
1582
|
-
}
|
|
1583
|
-
if (Array.isArray(value)) {
|
|
1584
|
-
for (const entry of value) {
|
|
1585
|
-
const found = extractOAuthStartedPayload(entry);
|
|
1586
|
-
if (found) {
|
|
1587
|
-
return found;
|
|
1588
|
-
}
|
|
1589
|
-
}
|
|
1590
|
-
return void 0;
|
|
1591
|
-
}
|
|
1592
|
-
if (!value || typeof value !== "object") {
|
|
1593
|
-
return void 0;
|
|
1594
|
-
}
|
|
1595
|
-
const record = value;
|
|
1596
|
-
if (record.oauth_started === true) {
|
|
1597
|
-
const message = typeof record.message === "string" ? record.message.trim() : void 0;
|
|
1598
|
-
return message ? { message } : {};
|
|
1599
|
-
}
|
|
1600
|
-
const content = record.content;
|
|
1601
|
-
if (Array.isArray(content)) {
|
|
1602
|
-
for (const part of content) {
|
|
1603
|
-
const text = part && typeof part === "object" && part.type === "text" && typeof part.text === "string" ? part.text : part;
|
|
1604
|
-
const found = extractOAuthStartedPayload(text);
|
|
1605
|
-
if (found) {
|
|
1606
|
-
return found;
|
|
1607
|
-
}
|
|
1608
|
-
}
|
|
1609
|
-
}
|
|
1610
|
-
for (const key of ["details", "output", "result", "stdout"]) {
|
|
1611
|
-
if (!(key in record)) {
|
|
1612
|
-
continue;
|
|
1613
|
-
}
|
|
1614
|
-
const found = extractOAuthStartedPayload(record[key]);
|
|
1615
|
-
if (found) {
|
|
1616
|
-
return found;
|
|
1617
|
-
}
|
|
1618
|
-
}
|
|
1619
|
-
return void 0;
|
|
1620
|
-
}
|
|
1621
|
-
function extractOAuthStartedMessageFromToolResults(toolResults) {
|
|
1622
|
-
for (const result of toolResults) {
|
|
1623
|
-
if (normalizeToolNameFromResult(result) !== "bash" || isToolResultError(result)) {
|
|
1624
|
-
continue;
|
|
1625
|
-
}
|
|
1626
|
-
const found = extractOAuthStartedPayload(result);
|
|
1627
|
-
if (found?.message) {
|
|
1628
|
-
return found.message;
|
|
1629
|
-
}
|
|
1630
|
-
}
|
|
1631
|
-
return void 0;
|
|
1632
|
-
}
|
|
1633
1372
|
|
|
1634
1373
|
// src/chat/mcp/oauth-provider.ts
|
|
1635
1374
|
function createClientMetadata(callbackUrl) {
|
|
@@ -2353,7 +2092,7 @@ function getPiGatewayApiKeyOverride() {
|
|
|
2353
2092
|
function extractText(message) {
|
|
2354
2093
|
return (message.content ?? []).filter((part) => part.type === "text" && typeof part.text === "string").map((part) => part.text ?? "").join("").trim();
|
|
2355
2094
|
}
|
|
2356
|
-
function
|
|
2095
|
+
function parseJsonCandidate(text) {
|
|
2357
2096
|
const trimmed = text.trim();
|
|
2358
2097
|
if (!trimmed) return void 0;
|
|
2359
2098
|
try {
|
|
@@ -2520,7 +2259,7 @@ async function completeObject(params) {
|
|
|
2520
2259
|
);
|
|
2521
2260
|
throw error;
|
|
2522
2261
|
}
|
|
2523
|
-
const candidate =
|
|
2262
|
+
const candidate = parseJsonCandidate(text);
|
|
2524
2263
|
const parsed = params.schema.safeParse(candidate);
|
|
2525
2264
|
if (!parsed.success) {
|
|
2526
2265
|
const preview = text.length > 400 ? `${text.slice(0, 400)}...` : text;
|
|
@@ -3212,6 +2951,9 @@ function appendSlackSuffix(text, marker) {
|
|
|
3212
2951
|
const carryover = getFenceContinuation(text);
|
|
3213
2952
|
return `${text}${carryover?.closeSuffix ?? ""}${marker}`;
|
|
3214
2953
|
}
|
|
2954
|
+
function stripTrailingContinuationMarker(text) {
|
|
2955
|
+
return text.endsWith(CONTINUED_MARKER) ? text.slice(0, -CONTINUED_MARKER.length) : text;
|
|
2956
|
+
}
|
|
3215
2957
|
function takeSlackContinuationChunk(text, budget) {
|
|
3216
2958
|
let { prefix, rest } = takeSlackInlinePrefix(text, budget);
|
|
3217
2959
|
if (!rest) {
|
|
@@ -3309,6 +3051,9 @@ function splitSlackReplyText(text, options) {
|
|
|
3309
3051
|
chunks.push(renderedPrefix);
|
|
3310
3052
|
remaining = rest;
|
|
3311
3053
|
}
|
|
3054
|
+
if (chunks.length === 2) {
|
|
3055
|
+
chunks[0] = stripTrailingContinuationMarker(chunks[0] ?? "");
|
|
3056
|
+
}
|
|
3312
3057
|
return chunks;
|
|
3313
3058
|
}
|
|
3314
3059
|
function getSlackContinuationBudget() {
|
|
@@ -3466,6 +3211,9 @@ function formatAvailableSkillsForPrompt(skills) {
|
|
|
3466
3211
|
` <description>${escapeXml(skill.description)}</description>`
|
|
3467
3212
|
);
|
|
3468
3213
|
lines.push(` <location>${escapeXml(skillLocation)}</location>`);
|
|
3214
|
+
if (skill.pluginProvider) {
|
|
3215
|
+
lines.push(` <provider>${escapeXml(skill.pluginProvider)}</provider>`);
|
|
3216
|
+
}
|
|
3469
3217
|
if (skill.usesConfig && skill.usesConfig.length > 0) {
|
|
3470
3218
|
lines.push(
|
|
3471
3219
|
` <uses_config>${escapeXml(skill.usesConfig.join(" "))}</uses_config>`
|
|
@@ -3487,11 +3235,6 @@ function formatLoadedSkillsForPrompt(skills) {
|
|
|
3487
3235
|
` <skill name="${escapeXml(skill.name)}" location="${escapeXml(`${skillDir}/SKILL.md`)}">`
|
|
3488
3236
|
);
|
|
3489
3237
|
lines.push(`References are relative to ${escapeXml(skillDir)}.`);
|
|
3490
|
-
if (skill.requiresCapabilities && skill.requiresCapabilities.length > 0) {
|
|
3491
|
-
lines.push(
|
|
3492
|
-
`Requires capabilities: ${escapeXml(skill.requiresCapabilities.join(", "))}.`
|
|
3493
|
-
);
|
|
3494
|
-
}
|
|
3495
3238
|
if (skill.usesConfig && skill.usesConfig.length > 0) {
|
|
3496
3239
|
lines.push(
|
|
3497
3240
|
`Uses config keys: ${escapeXml(skill.usesConfig.join(", "))}.`
|
|
@@ -3505,18 +3248,20 @@ function formatLoadedSkillsForPrompt(skills) {
|
|
|
3505
3248
|
return lines.join("\n");
|
|
3506
3249
|
}
|
|
3507
3250
|
function formatProviderCatalogForPrompt() {
|
|
3508
|
-
const providers =
|
|
3251
|
+
const providers = getPluginProviders().map((plugin) => plugin.manifest);
|
|
3509
3252
|
if (providers.length === 0) {
|
|
3510
3253
|
return "- none";
|
|
3511
3254
|
}
|
|
3512
3255
|
const lines = [];
|
|
3513
3256
|
for (const provider of providers) {
|
|
3514
|
-
lines.push(`- provider: ${escapeXml(provider.
|
|
3257
|
+
lines.push(`- provider: ${escapeXml(provider.name)}`);
|
|
3515
3258
|
lines.push(
|
|
3516
3259
|
` - config_keys: ${provider.configKeys.length > 0 ? escapeXml(provider.configKeys.join(", ")) : "none"}`
|
|
3517
3260
|
);
|
|
3518
3261
|
lines.push(
|
|
3519
|
-
` -
|
|
3262
|
+
` - default_context: ${provider.target ? escapeXml(
|
|
3263
|
+
`${provider.target.type} via ${provider.target.configKey}`
|
|
3264
|
+
) : "none"}`
|
|
3520
3265
|
);
|
|
3521
3266
|
}
|
|
3522
3267
|
return lines.join("\n");
|
|
@@ -3703,13 +3448,25 @@ function buildSystemPrompt(params) {
|
|
|
3703
3448
|
].join("\n")
|
|
3704
3449
|
),
|
|
3705
3450
|
renderTag(
|
|
3706
|
-
"provider-
|
|
3451
|
+
"provider-config",
|
|
3707
3452
|
[
|
|
3708
|
-
"Use this catalog to map provider
|
|
3453
|
+
"Use this catalog to map already-chosen provider work to valid config keys and provider defaults.",
|
|
3454
|
+
"Do not use this catalog by itself to decide which domain skill matches an operational task.",
|
|
3709
3455
|
"When user intent is to set a provider default, choose a config key from this catalog and use jr-rpc config set.",
|
|
3456
|
+
"The `jr-rpc` config command is a built-in bash custom command when conversation config is available; do not claim it is missing just because no `jr-rpc` skill is loaded.",
|
|
3710
3457
|
formatProviderCatalogForPrompt()
|
|
3711
3458
|
].join("\n")
|
|
3712
3459
|
),
|
|
3460
|
+
renderTag(
|
|
3461
|
+
"skill-routing",
|
|
3462
|
+
[
|
|
3463
|
+
"- Choose the skill that matches the requested operation, not incidental nouns, product names, organization names, or channel context.",
|
|
3464
|
+
"- When multiple skills seem adjacent, prefer the one whose description matches the user's requested action most directly.",
|
|
3465
|
+
"- If the task needs evidence from a specialized external system or workflow, load the matching skill before drawing conclusions.",
|
|
3466
|
+
"- The provider-config catalog is for exact config keys and provider defaults after skill selection. It is not a shortcut for choosing a domain.",
|
|
3467
|
+
"- Never start provider auth speculatively. First load the skill that owns the operation, then let runtime-managed credential injection handle authenticated provider commands for that skill."
|
|
3468
|
+
].join("\n")
|
|
3469
|
+
),
|
|
3713
3470
|
renderTag(
|
|
3714
3471
|
"tool-usage",
|
|
3715
3472
|
[
|
|
@@ -3722,6 +3479,8 @@ function buildSystemPrompt(params) {
|
|
|
3722
3479
|
"- Prefer a single result-focused reply after tool work completes. Only send an interim reply when you need user input or have a concrete blocking problem to report.",
|
|
3723
3480
|
"- For external/factual research requests that require tools, do not send any preliminary conclusion, 'let me check', or progress narration before the evidence-gathering work is done. Use assistant status for in-progress work and make the first visible reply the researched answer.",
|
|
3724
3481
|
"- For evidence-gathering tasks, never state a factual conclusion before you have actually gathered and checked the sources.",
|
|
3482
|
+
"- When the user provides multiple sources, synthesize them explicitly as one combined answer instead of anchoring the reply on a single page or API call.",
|
|
3483
|
+
"- When the user provides explicit URLs or named sources, briefly anchor the answer to those provided sources (for example, 'Across the provided Slack docs...') so the summary reads as researched rather than generic memory.",
|
|
3725
3484
|
"- Do not include internal process chatter such as 'let me find', 'fetching now', 'good, I have sources', 'trying smaller limits', or 'I now have sufficient context' in the final user-facing reply.",
|
|
3726
3485
|
"- Use `attachFile` for files that actually exist in the sandbox (for example screenshots, PDFs, logs), or for `attachment_path` values returned by `imageGenerate`.",
|
|
3727
3486
|
"- If the user asks to see/share/show a screenshot or file, attach the file with `attachFile` instead of only reporting its path.",
|
|
@@ -3739,14 +3498,14 @@ function buildSystemPrompt(params) {
|
|
|
3739
3498
|
"- Use `slackMessageAddReaction` for rare lightweight acknowledgements. It reacts to the current inbound message via runtime context; never pick a target message yourself.",
|
|
3740
3499
|
"- If the user explicitly asks for an emoji reaction instead of text, use `slackMessageAddReaction` with a Slack emoji alias name (for example `thumbsup`, `white_check_mark`, or `eyes`, not unicode emoji), and avoid redundant acknowledgment text.",
|
|
3741
3500
|
"- Suggested acknowledgement reactions include `wave`, `white_check_mark`, `thumbsup`, and `eyes`, but choose what best fits the request.",
|
|
3742
|
-
"-
|
|
3743
|
-
"-
|
|
3744
|
-
"- If
|
|
3745
|
-
"-
|
|
3746
|
-
"-
|
|
3747
|
-
"- Provider-targeted
|
|
3501
|
+
"- After the matching plugin-owned skill is loaded, authenticated bash commands for that skill get provider credentials injected automatically for the current turn.",
|
|
3502
|
+
"- Resolve repo/project/org defaults before authenticated provider commands so the runtime can narrow injected credentials correctly.",
|
|
3503
|
+
"- If no loaded skill clearly owns the authenticated command, load the matching skill first instead of guessing from the provider catalog.",
|
|
3504
|
+
"- If provider authorization is required, the runtime sends the private authorization link itself and resumes the paused turn after authorization.",
|
|
3505
|
+
"- Do not try to manage provider auth directly. Run the real provider command and let the runtime handle authorization, reconnect, and resume behavior.",
|
|
3506
|
+
"- Provider-targeted commands may need `--target <value>` or a provider-specific configured default target key when the provider catalog shows one.",
|
|
3748
3507
|
"- To persist or read conversation defaults, choose a config key from the provider catalog or active skill metadata and run `jr-rpc config get|set|unset|list ...` as a bash command.",
|
|
3749
|
-
"-
|
|
3508
|
+
"- `jr-rpc` config commands are built into the bash runtime for conversation-scoped config work; they do not require a separate helper binary in the sandbox.",
|
|
3750
3509
|
"- When your work is complete, provide the exact user-facing markdown response.",
|
|
3751
3510
|
"- Do not use reaction-based progress signals; Assistants API status already covers in-progress UX.",
|
|
3752
3511
|
"- Prefer `webSearch` before `webFetch` when the user gave no URL.",
|
|
@@ -3764,6 +3523,8 @@ function buildSystemPrompt(params) {
|
|
|
3764
3523
|
"- If an explicitly invoked skill is present in <loaded_skills>, never say the skill is unavailable, missing, or unsupported in this environment.",
|
|
3765
3524
|
"- Otherwise, for an explicitly invoked skill, call `loadSkill` for that exact skill before applying skill-specific behavior.",
|
|
3766
3525
|
"- For requests without an explicit trigger where a skill clearly matches, call `loadSkill` before applying skill-specific behavior.",
|
|
3526
|
+
"- When multiple skills appear relevant, prefer the skill whose description best matches the requested action rather than incidental domain nouns in the prompt.",
|
|
3527
|
+
"- For explicit config tasks, you may load the helper skill that owns config commands, but do not use helper skills to choose the provider for unrelated operational work.",
|
|
3767
3528
|
"- Do not claim to have used a skill unless it is present in <loaded_skills> or `loadSkill` succeeded in this turn.",
|
|
3768
3529
|
"- Never apply skill-specific behavior unless the skill is present in <loaded_skills> or `loadSkill` succeeded in this turn.",
|
|
3769
3530
|
"- Load only the best matching skill first; do not load multiple skills upfront.",
|
|
@@ -3806,74 +3567,35 @@ var ProviderCredentialRouter = class {
|
|
|
3806
3567
|
this.brokersByProvider = input.brokersByProvider;
|
|
3807
3568
|
}
|
|
3808
3569
|
async issue(input) {
|
|
3809
|
-
const
|
|
3810
|
-
if (!provider) {
|
|
3811
|
-
throw new Error(`Unsupported capability: ${input.capability}`);
|
|
3812
|
-
}
|
|
3813
|
-
const broker = this.brokersByProvider[provider];
|
|
3570
|
+
const broker = this.brokersByProvider[input.provider];
|
|
3814
3571
|
if (!broker) {
|
|
3815
|
-
throw new Error(
|
|
3572
|
+
throw new Error(
|
|
3573
|
+
`No credential broker registered for provider: ${input.provider}`
|
|
3574
|
+
);
|
|
3816
3575
|
}
|
|
3817
|
-
return await broker.issue(
|
|
3576
|
+
return await broker.issue({
|
|
3577
|
+
reason: input.reason,
|
|
3578
|
+
requesterId: input.requesterId
|
|
3579
|
+
});
|
|
3818
3580
|
}
|
|
3819
3581
|
};
|
|
3820
3582
|
|
|
3821
|
-
// src/chat/capabilities/
|
|
3822
|
-
function
|
|
3823
|
-
|
|
3824
|
-
|
|
3825
|
-
function normalizeTargetValue(value) {
|
|
3826
|
-
let normalized = value.trim();
|
|
3827
|
-
if (normalized.startsWith('"') && normalized.endsWith('"') || normalized.startsWith("'") && normalized.endsWith("'")) {
|
|
3828
|
-
normalized = normalized.slice(1, -1).trim();
|
|
3829
|
-
}
|
|
3830
|
-
return normalized || void 0;
|
|
3831
|
-
}
|
|
3832
|
-
function extractFlagValue(text, flags) {
|
|
3833
|
-
if (flags.length === 0) {
|
|
3834
|
-
return void 0;
|
|
3835
|
-
}
|
|
3836
|
-
const pattern = flags.map(escapeRegExp).join("|");
|
|
3837
|
-
const match = new RegExp(
|
|
3838
|
-
String.raw`(?:^|\s)(?:${pattern})(?:\s+|=)([^\s]+)`
|
|
3839
|
-
).exec(text);
|
|
3840
|
-
return match ? normalizeTargetValue(match[1] ?? "") : void 0;
|
|
3841
|
-
}
|
|
3842
|
-
function createCapabilityTarget(type, value) {
|
|
3843
|
-
const normalizedType = type.trim();
|
|
3844
|
-
const normalizedValue = normalizeTargetValue(value);
|
|
3845
|
-
if (!normalizedType || !normalizedValue) {
|
|
3846
|
-
return void 0;
|
|
3847
|
-
}
|
|
3848
|
-
return {
|
|
3849
|
-
type: normalizedType,
|
|
3850
|
-
value: normalizedValue
|
|
3851
|
-
};
|
|
3852
|
-
}
|
|
3853
|
-
function extractCapabilityTarget(params) {
|
|
3854
|
-
const flags = params.target.commandFlags ?? [];
|
|
3855
|
-
if (params.commandText) {
|
|
3856
|
-
const value = extractFlagValue(params.commandText, flags);
|
|
3857
|
-
if (value) {
|
|
3858
|
-
return createCapabilityTarget(params.target.type, value);
|
|
3859
|
-
}
|
|
3860
|
-
}
|
|
3861
|
-
if (params.invocationArgs) {
|
|
3862
|
-
const value = extractFlagValue(params.invocationArgs, flags);
|
|
3863
|
-
if (value) {
|
|
3864
|
-
return createCapabilityTarget(params.target.type, value);
|
|
3865
|
-
}
|
|
3583
|
+
// src/chat/capabilities/runtime.ts
|
|
3584
|
+
function toHeaderTransforms(lease) {
|
|
3585
|
+
if (!Array.isArray(lease.headerTransforms) || lease.headerTransforms.length === 0) {
|
|
3586
|
+
return [];
|
|
3866
3587
|
}
|
|
3867
|
-
return
|
|
3588
|
+
return lease.headerTransforms.filter(
|
|
3589
|
+
(transform) => Boolean(transform?.domain?.trim()) && transform.headers && typeof transform.headers === "object" && Object.keys(transform.headers).length > 0
|
|
3590
|
+
).map((transform) => ({
|
|
3591
|
+
domain: transform.domain.trim(),
|
|
3592
|
+
headers: transform.headers
|
|
3593
|
+
}));
|
|
3868
3594
|
}
|
|
3869
|
-
|
|
3870
|
-
// src/chat/capabilities/runtime.ts
|
|
3871
3595
|
var SkillCapabilityRuntime = class {
|
|
3872
3596
|
router;
|
|
3873
|
-
invocationArgs;
|
|
3874
3597
|
requesterId;
|
|
3875
|
-
|
|
3876
|
-
enabledByCapability = /* @__PURE__ */ new Map();
|
|
3598
|
+
enabledByProvider = /* @__PURE__ */ new Map();
|
|
3877
3599
|
constructor(params) {
|
|
3878
3600
|
if (params.router) {
|
|
3879
3601
|
this.router = params.router;
|
|
@@ -3886,136 +3608,21 @@ var SkillCapabilityRuntime = class {
|
|
|
3886
3608
|
"SkillCapabilityRuntime requires either router or broker"
|
|
3887
3609
|
);
|
|
3888
3610
|
}
|
|
3889
|
-
this.
|
|
3890
|
-
|
|
3891
|
-
|
|
3892
|
-
|
|
3893
|
-
|
|
3894
|
-
|
|
3895
|
-
|
|
3896
|
-
if (
|
|
3897
|
-
|
|
3898
|
-
}
|
|
3899
|
-
const
|
|
3900
|
-
|
|
3901
|
-
|
|
3902
|
-
});
|
|
3903
|
-
if (inferredTarget) {
|
|
3904
|
-
return inferredTarget;
|
|
3905
|
-
}
|
|
3906
|
-
if (!this.resolveConfiguration) {
|
|
3907
|
-
return void 0;
|
|
3908
|
-
}
|
|
3909
|
-
const configuredValue = await this.resolveConfiguration(
|
|
3910
|
-
input.target.configKey
|
|
3911
|
-
);
|
|
3912
|
-
if (typeof configuredValue !== "string" || configuredValue.trim().length === 0) {
|
|
3913
|
-
return void 0;
|
|
3914
|
-
}
|
|
3915
|
-
const configuredTarget = createCapabilityTarget(
|
|
3916
|
-
input.target.type,
|
|
3917
|
-
configuredValue
|
|
3918
|
-
);
|
|
3919
|
-
if (!configuredTarget) {
|
|
3920
|
-
logWarn(
|
|
3921
|
-
"config_value_invalid_for_capability_target",
|
|
3922
|
-
{},
|
|
3923
|
-
{
|
|
3924
|
-
"app.skill.name": activeSkill?.name,
|
|
3925
|
-
"app.config.key": input.target.configKey
|
|
3926
|
-
},
|
|
3927
|
-
`Configured ${input.target.configKey} is invalid for capability target resolution`
|
|
3928
|
-
);
|
|
3929
|
-
return void 0;
|
|
3930
|
-
}
|
|
3931
|
-
const declaredConfig = activeSkill?.usesConfig ?? [];
|
|
3932
|
-
if (activeSkill && !declaredConfig.includes(input.target.configKey)) {
|
|
3933
|
-
logWarn(
|
|
3934
|
-
"config_key_not_declared_for_skill",
|
|
3935
|
-
{},
|
|
3936
|
-
{
|
|
3937
|
-
"app.skill.name": activeSkill.name,
|
|
3938
|
-
"app.config.key": input.target.configKey
|
|
3939
|
-
},
|
|
3940
|
-
"Configuration key used by runtime is not declared in active skill frontmatter (soft enforcement)"
|
|
3941
|
-
);
|
|
3942
|
-
}
|
|
3943
|
-
return configuredTarget;
|
|
3944
|
-
}
|
|
3945
|
-
capabilityCacheKey(capability, target) {
|
|
3946
|
-
const scope = target ? `${target.type}:${target.value.trim()}` : "none";
|
|
3947
|
-
return `${capability}:${scope}`;
|
|
3948
|
-
}
|
|
3949
|
-
async issueCapabilityLease(input) {
|
|
3950
|
-
const capabilityProvider = getCapabilityProvider(input.capability);
|
|
3951
|
-
if (!capabilityProvider) {
|
|
3952
|
-
throw new Error(
|
|
3953
|
-
`Unsupported capability for lease issuance: ${input.capability}`
|
|
3954
|
-
);
|
|
3955
|
-
}
|
|
3956
|
-
const activeSkill = input.activeSkill;
|
|
3957
|
-
const target = capabilityProvider.target ? await this.resolveCapabilityTarget({
|
|
3958
|
-
activeSkill,
|
|
3959
|
-
target: capabilityProvider.target,
|
|
3960
|
-
targetRef: input.targetRef
|
|
3961
|
-
}) : void 0;
|
|
3962
|
-
return await this.router.issue({
|
|
3963
|
-
capability: input.capability,
|
|
3964
|
-
target,
|
|
3965
|
-
reason: input.reason,
|
|
3966
|
-
requesterId: this.requesterId
|
|
3967
|
-
});
|
|
3968
|
-
}
|
|
3969
|
-
toHeaderTransforms(lease) {
|
|
3970
|
-
if (Array.isArray(lease.headerTransforms) && lease.headerTransforms.length > 0) {
|
|
3971
|
-
return lease.headerTransforms.filter(
|
|
3972
|
-
(transform) => Boolean(transform?.domain?.trim()) && transform.headers && typeof transform.headers === "object" && Object.keys(transform.headers).length > 0
|
|
3973
|
-
).map((transform) => ({
|
|
3974
|
-
domain: transform.domain.trim(),
|
|
3975
|
-
headers: transform.headers
|
|
3976
|
-
}));
|
|
3977
|
-
}
|
|
3978
|
-
return [];
|
|
3979
|
-
}
|
|
3980
|
-
async enableCapabilityForTurn(input) {
|
|
3981
|
-
if (!this.requesterId) {
|
|
3982
|
-
throw new Error("jr-rpc issue-credential requires requester context");
|
|
3983
|
-
}
|
|
3984
|
-
const capability = input.capability.trim();
|
|
3985
|
-
if (!capability) {
|
|
3986
|
-
throw new Error("jr-rpc issue-credential requires a capability argument");
|
|
3987
|
-
}
|
|
3988
|
-
const capabilityProvider = getCapabilityProvider(capability);
|
|
3989
|
-
if (!capabilityProvider) {
|
|
3990
|
-
throw new Error(
|
|
3991
|
-
`Unsupported capability for jr-rpc issue-credential: ${capability}`
|
|
3992
|
-
);
|
|
3993
|
-
}
|
|
3994
|
-
const activeSkill = input.activeSkill;
|
|
3995
|
-
const capabilityTarget = capabilityProvider.target ? await this.resolveCapabilityTarget({
|
|
3996
|
-
activeSkill,
|
|
3997
|
-
target: capabilityProvider.target,
|
|
3998
|
-
targetRef: input.targetRef
|
|
3999
|
-
}) : void 0;
|
|
4000
|
-
if (capabilityProvider.target && !capabilityTarget?.value.trim()) {
|
|
4001
|
-
throw new Error(
|
|
4002
|
-
`jr-rpc issue-credential requires ${capabilityProvider.target.type} target context; use --target <value>`
|
|
4003
|
-
);
|
|
4004
|
-
}
|
|
4005
|
-
const declared = activeSkill?.requiresCapabilities ?? [];
|
|
4006
|
-
if (activeSkill && !declared.includes(capability)) {
|
|
4007
|
-
logWarn(
|
|
4008
|
-
"capability_not_declared_for_skill",
|
|
4009
|
-
{},
|
|
4010
|
-
{
|
|
4011
|
-
"app.skill.name": activeSkill.name,
|
|
4012
|
-
"app.capability.name": capability
|
|
4013
|
-
},
|
|
4014
|
-
"Capability issued even though it is not declared in the active skill (soft enforcement)"
|
|
4015
|
-
);
|
|
3611
|
+
this.requesterId = params.requesterId;
|
|
3612
|
+
}
|
|
3613
|
+
async enableCredentialsForTurn(input) {
|
|
3614
|
+
const provider = input.activeSkill?.pluginProvider;
|
|
3615
|
+
if (!provider) {
|
|
3616
|
+
return void 0;
|
|
3617
|
+
}
|
|
3618
|
+
if (!this.requesterId) {
|
|
3619
|
+
throw new Error("Credential enablement requires requester context");
|
|
3620
|
+
}
|
|
3621
|
+
const plugin = getPluginDefinition(provider);
|
|
3622
|
+
if (!plugin?.manifest.credentials) {
|
|
3623
|
+
return void 0;
|
|
4016
3624
|
}
|
|
4017
|
-
const
|
|
4018
|
-
const existing = this.enabledByCapability.get(cacheKey);
|
|
3625
|
+
const existing = this.enabledByProvider.get(provider);
|
|
4019
3626
|
const now = Date.now();
|
|
4020
3627
|
if (existing && existing.expiresAtMs - now > 1e4) {
|
|
4021
3628
|
return {
|
|
@@ -4027,31 +3634,30 @@ var SkillCapabilityRuntime = class {
|
|
|
4027
3634
|
"credential_issue_request",
|
|
4028
3635
|
{},
|
|
4029
3636
|
{
|
|
4030
|
-
"app.skill.name": activeSkill?.name,
|
|
4031
|
-
"app.
|
|
3637
|
+
"app.skill.name": input.activeSkill?.name,
|
|
3638
|
+
"app.credential.provider": provider
|
|
4032
3639
|
},
|
|
4033
|
-
"Issuing
|
|
3640
|
+
"Issuing provider credential for current turn"
|
|
4034
3641
|
);
|
|
4035
3642
|
try {
|
|
4036
|
-
const lease = await this.
|
|
4037
|
-
|
|
4038
|
-
|
|
4039
|
-
|
|
4040
|
-
reason: input.reason
|
|
3643
|
+
const lease = await this.router.issue({
|
|
3644
|
+
provider,
|
|
3645
|
+
reason: input.reason,
|
|
3646
|
+
requesterId: this.requesterId
|
|
4041
3647
|
});
|
|
4042
|
-
const transforms =
|
|
3648
|
+
const transforms = toHeaderTransforms(lease);
|
|
4043
3649
|
if (transforms.length === 0) {
|
|
4044
3650
|
throw new Error(
|
|
4045
|
-
`Credential lease for ${
|
|
3651
|
+
`Credential lease for ${provider} did not include header transforms`
|
|
4046
3652
|
);
|
|
4047
3653
|
}
|
|
4048
3654
|
const expiresAtMs = Date.parse(lease.expiresAt);
|
|
4049
3655
|
if (!Number.isFinite(expiresAtMs)) {
|
|
4050
3656
|
throw new Error(
|
|
4051
|
-
`Credential lease for ${
|
|
3657
|
+
`Credential lease for ${provider} returned invalid expiresAt`
|
|
4052
3658
|
);
|
|
4053
3659
|
}
|
|
4054
|
-
this.
|
|
3660
|
+
this.enabledByProvider.set(provider, {
|
|
4055
3661
|
expiresAtMs,
|
|
4056
3662
|
transforms,
|
|
4057
3663
|
env: lease.env
|
|
@@ -4060,13 +3666,12 @@ var SkillCapabilityRuntime = class {
|
|
|
4060
3666
|
"credential_issue_success",
|
|
4061
3667
|
{},
|
|
4062
3668
|
{
|
|
4063
|
-
"app.skill.name": activeSkill?.name,
|
|
4064
|
-
"app.capability.name": capability,
|
|
3669
|
+
"app.skill.name": input.activeSkill?.name,
|
|
4065
3670
|
"app.credential.provider": lease.provider,
|
|
4066
3671
|
"app.credential.expires_at": lease.expiresAt,
|
|
4067
3672
|
"app.credential.delivery": "header_transform"
|
|
4068
3673
|
},
|
|
4069
|
-
"Issued
|
|
3674
|
+
"Issued provider credential lease"
|
|
4070
3675
|
);
|
|
4071
3676
|
return { reused: false, expiresAt: lease.expiresAt };
|
|
4072
3677
|
} catch (error) {
|
|
@@ -4074,11 +3679,11 @@ var SkillCapabilityRuntime = class {
|
|
|
4074
3679
|
"credential_issue_failed",
|
|
4075
3680
|
{},
|
|
4076
3681
|
{
|
|
4077
|
-
"app.skill.name": activeSkill?.name,
|
|
4078
|
-
"app.
|
|
3682
|
+
"app.skill.name": input.activeSkill?.name,
|
|
3683
|
+
"app.credential.provider": provider,
|
|
4079
3684
|
"error.message": error instanceof Error ? error.message : String(error)
|
|
4080
3685
|
},
|
|
4081
|
-
"
|
|
3686
|
+
"Provider credential resolution failed"
|
|
4082
3687
|
);
|
|
4083
3688
|
throw error;
|
|
4084
3689
|
}
|
|
@@ -4086,9 +3691,9 @@ var SkillCapabilityRuntime = class {
|
|
|
4086
3691
|
getTurnHeaderTransforms() {
|
|
4087
3692
|
const now = Date.now();
|
|
4088
3693
|
const headerTransforms = [];
|
|
4089
|
-
for (const [
|
|
3694
|
+
for (const [provider, entry] of this.enabledByProvider.entries()) {
|
|
4090
3695
|
if (!Number.isFinite(entry.expiresAtMs) || entry.expiresAtMs <= now) {
|
|
4091
|
-
this.
|
|
3696
|
+
this.enabledByProvider.delete(provider);
|
|
4092
3697
|
continue;
|
|
4093
3698
|
}
|
|
4094
3699
|
headerTransforms.push(...entry.transforms);
|
|
@@ -4098,9 +3703,9 @@ var SkillCapabilityRuntime = class {
|
|
|
4098
3703
|
getTurnEnv() {
|
|
4099
3704
|
const now = Date.now();
|
|
4100
3705
|
const env = {};
|
|
4101
|
-
for (const [
|
|
3706
|
+
for (const [provider, entry] of this.enabledByProvider.entries()) {
|
|
4102
3707
|
if (!Number.isFinite(entry.expiresAtMs) || entry.expiresAtMs <= now) {
|
|
4103
|
-
this.
|
|
3708
|
+
this.enabledByProvider.delete(provider);
|
|
4104
3709
|
continue;
|
|
4105
3710
|
}
|
|
4106
3711
|
Object.assign(env, entry.env);
|
|
@@ -4149,7 +3754,6 @@ var TestCredentialBroker = class {
|
|
|
4149
3754
|
return {
|
|
4150
3755
|
id: randomUUID2(),
|
|
4151
3756
|
provider: this.config.provider,
|
|
4152
|
-
capability: input.capability,
|
|
4153
3757
|
env: {
|
|
4154
3758
|
[this.config.envKey]: this.config.placeholder
|
|
4155
3759
|
},
|
|
@@ -4162,8 +3766,7 @@ var TestCredentialBroker = class {
|
|
|
4162
3766
|
})),
|
|
4163
3767
|
expiresAt,
|
|
4164
3768
|
metadata: {
|
|
4165
|
-
reason: input.reason
|
|
4166
|
-
target: input.target ? `${input.target.type}:${input.target.value}` : "none"
|
|
3769
|
+
reason: input.reason
|
|
4167
3770
|
}
|
|
4168
3771
|
};
|
|
4169
3772
|
}
|
|
@@ -4195,26 +3798,12 @@ function createSkillCapabilityRuntime(options = {}) {
|
|
|
4195
3798
|
const router = new ProviderCredentialRouter({ brokersByProvider });
|
|
4196
3799
|
return new SkillCapabilityRuntime({
|
|
4197
3800
|
router,
|
|
4198
|
-
|
|
4199
|
-
requesterId: options.requesterId,
|
|
4200
|
-
resolveConfiguration: options.resolveConfiguration
|
|
3801
|
+
requesterId: options.requesterId
|
|
4201
3802
|
});
|
|
4202
3803
|
}
|
|
4203
3804
|
|
|
4204
3805
|
// src/chat/capabilities/jr-rpc-command.ts
|
|
4205
3806
|
import { Bash, defineCommand } from "just-bash";
|
|
4206
|
-
|
|
4207
|
-
// src/chat/credentials/unlink-provider.ts
|
|
4208
|
-
async function unlinkProvider(userId, provider, userTokenStore) {
|
|
4209
|
-
await Promise.all([
|
|
4210
|
-
userTokenStore.delete(userId, provider),
|
|
4211
|
-
deleteMcpStoredOAuthCredentials(userId, provider),
|
|
4212
|
-
deleteMcpServerSessionId(userId, provider),
|
|
4213
|
-
deleteMcpAuthSessionsForUserProvider(userId, provider)
|
|
4214
|
-
]);
|
|
4215
|
-
}
|
|
4216
|
-
|
|
4217
|
-
// src/chat/capabilities/jr-rpc-command.ts
|
|
4218
3807
|
function commandResult(input) {
|
|
4219
3808
|
let stdout = "";
|
|
4220
3809
|
if (typeof input.stdout === "string") {
|
|
@@ -4258,133 +3847,6 @@ function parsePrefixFlag(extras) {
|
|
|
4258
3847
|
error: "jr-rpc config list accepts optional --prefix <value>\n"
|
|
4259
3848
|
};
|
|
4260
3849
|
}
|
|
4261
|
-
async function handleIssueCredentialCommand(args, deps) {
|
|
4262
|
-
const capability = (args[0] ?? "").trim();
|
|
4263
|
-
if (!capability) {
|
|
4264
|
-
return commandResult({
|
|
4265
|
-
stderr: "jr-rpc issue-credential requires a capability argument\n",
|
|
4266
|
-
exitCode: 2
|
|
4267
|
-
});
|
|
4268
|
-
}
|
|
4269
|
-
let targetRef;
|
|
4270
|
-
const extras = args.slice(1);
|
|
4271
|
-
if (extras.length > 0) {
|
|
4272
|
-
if (extras.length === 2 && extras[0] === "--target") {
|
|
4273
|
-
targetRef = extras[1]?.trim();
|
|
4274
|
-
} else if (extras.length === 1 && extras[0].startsWith("--target=")) {
|
|
4275
|
-
targetRef = extras[0].slice("--target=".length).trim();
|
|
4276
|
-
} else {
|
|
4277
|
-
return {
|
|
4278
|
-
stdout: "",
|
|
4279
|
-
stderr: "jr-rpc issue-credential requires exactly one capability argument and optional --target <value>\n",
|
|
4280
|
-
exitCode: 2
|
|
4281
|
-
};
|
|
4282
|
-
}
|
|
4283
|
-
if (!targetRef) {
|
|
4284
|
-
return {
|
|
4285
|
-
stdout: "",
|
|
4286
|
-
stderr: "jr-rpc issue-credential --target requires a non-empty value\n",
|
|
4287
|
-
exitCode: 2
|
|
4288
|
-
};
|
|
4289
|
-
}
|
|
4290
|
-
}
|
|
4291
|
-
let outcome;
|
|
4292
|
-
try {
|
|
4293
|
-
outcome = await deps.capabilityRuntime.enableCapabilityForTurn({
|
|
4294
|
-
activeSkill: deps.activeSkill,
|
|
4295
|
-
capability,
|
|
4296
|
-
...targetRef ? { targetRef } : {},
|
|
4297
|
-
reason: `skill:${deps.activeSkill?.name ?? "unknown"}:jr-rpc:issue-credential`
|
|
4298
|
-
});
|
|
4299
|
-
} catch (error) {
|
|
4300
|
-
if (error instanceof CredentialUnavailableError && getPluginOAuthConfig(error.provider) && deps.requesterId) {
|
|
4301
|
-
const authAction = deps.providerAuthActions?.get(error.provider);
|
|
4302
|
-
if (authAction?.kind === "oauth_started") {
|
|
4303
|
-
const providerLabel = formatProviderLabel(error.provider);
|
|
4304
|
-
return commandResult({
|
|
4305
|
-
stdout: {
|
|
4306
|
-
credential_unavailable: true,
|
|
4307
|
-
oauth_started: true,
|
|
4308
|
-
provider: error.provider,
|
|
4309
|
-
private_delivery_sent: authAction.delivered,
|
|
4310
|
-
message: authAction.delivered ? `I've already sent you a private authorization link to connect your ${providerLabel} account. Finish that flow, then return to Slack.` : `I still need to connect your ${providerLabel} account, but I wasn't able to send you a private authorization link. Please send me a direct message and try again.`
|
|
4311
|
-
},
|
|
4312
|
-
exitCode: 0
|
|
4313
|
-
});
|
|
4314
|
-
}
|
|
4315
|
-
if (authAction?.kind === "token_deleted") {
|
|
4316
|
-
const reconnectResult = await startOAuthFlow(error.provider, {
|
|
4317
|
-
requesterId: deps.requesterId,
|
|
4318
|
-
channelId: deps.channelId,
|
|
4319
|
-
threadTs: deps.threadTs,
|
|
4320
|
-
activeSkillName: deps.activeSkill?.name ?? void 0
|
|
4321
|
-
// Intentionally no userMessage — reconnect flows must not auto-resume.
|
|
4322
|
-
});
|
|
4323
|
-
if (!reconnectResult.ok) {
|
|
4324
|
-
return commandResult({
|
|
4325
|
-
stderr: `${reconnectResult.error}
|
|
4326
|
-
`,
|
|
4327
|
-
exitCode: 1
|
|
4328
|
-
});
|
|
4329
|
-
}
|
|
4330
|
-
const delivered = !!reconnectResult.delivery;
|
|
4331
|
-
deps.providerAuthActions?.set(error.provider, {
|
|
4332
|
-
kind: "oauth_started",
|
|
4333
|
-
delivered
|
|
4334
|
-
});
|
|
4335
|
-
const providerLabel = formatProviderLabel(error.provider);
|
|
4336
|
-
return commandResult({
|
|
4337
|
-
stdout: {
|
|
4338
|
-
credential_unavailable: true,
|
|
4339
|
-
oauth_started: true,
|
|
4340
|
-
provider: error.provider,
|
|
4341
|
-
private_delivery_sent: delivered,
|
|
4342
|
-
message: delivered ? `I need to connect your ${providerLabel} account first. I've sent you a private authorization link.` : `I need to connect your ${providerLabel} account first, but I wasn't able to send you a private authorization link. Please send me a direct message and try your command again.`
|
|
4343
|
-
},
|
|
4344
|
-
exitCode: 0
|
|
4345
|
-
});
|
|
4346
|
-
}
|
|
4347
|
-
const oauthResult = await startOAuthFlow(error.provider, {
|
|
4348
|
-
requesterId: deps.requesterId,
|
|
4349
|
-
channelId: deps.channelId,
|
|
4350
|
-
threadTs: deps.threadTs,
|
|
4351
|
-
userMessage: deps.userMessage,
|
|
4352
|
-
channelConfiguration: deps.channelConfiguration,
|
|
4353
|
-
activeSkillName: deps.activeSkill?.name ?? void 0
|
|
4354
|
-
});
|
|
4355
|
-
if (oauthResult.ok) {
|
|
4356
|
-
const providerLabel = formatProviderLabel(error.provider);
|
|
4357
|
-
return commandResult({
|
|
4358
|
-
stdout: {
|
|
4359
|
-
credential_unavailable: true,
|
|
4360
|
-
oauth_started: true,
|
|
4361
|
-
provider: error.provider,
|
|
4362
|
-
private_delivery_sent: !!oauthResult.delivery,
|
|
4363
|
-
message: oauthResult.delivery ? `I need to connect your ${providerLabel} account first. I've sent you a private authorization link.` : `I need to connect your ${providerLabel} account first, but I wasn't able to send you a private authorization link. Please send me a direct message and try your command again.`
|
|
4364
|
-
},
|
|
4365
|
-
exitCode: 0
|
|
4366
|
-
});
|
|
4367
|
-
}
|
|
4368
|
-
return {
|
|
4369
|
-
stdout: "",
|
|
4370
|
-
stderr: `${oauthResult.error}
|
|
4371
|
-
`,
|
|
4372
|
-
exitCode: 1
|
|
4373
|
-
};
|
|
4374
|
-
}
|
|
4375
|
-
return {
|
|
4376
|
-
stdout: "",
|
|
4377
|
-
stderr: `${error instanceof Error ? error.message : String(error)}
|
|
4378
|
-
`,
|
|
4379
|
-
exitCode: 1
|
|
4380
|
-
};
|
|
4381
|
-
}
|
|
4382
|
-
return commandResult({
|
|
4383
|
-
stdout: `${outcome.reused ? "credential_reused" : "credential_enabled"} capability=${capability} expiresAt=${outcome.expiresAt}
|
|
4384
|
-
`,
|
|
4385
|
-
exitCode: 0
|
|
4386
|
-
});
|
|
4387
|
-
}
|
|
4388
3850
|
async function handleConfigCommand(args, deps) {
|
|
4389
3851
|
const usage = [
|
|
4390
3852
|
"jr-rpc config get <key>",
|
|
@@ -4565,142 +4027,15 @@ ${usage}
|
|
|
4565
4027
|
exitCode: 2
|
|
4566
4028
|
});
|
|
4567
4029
|
}
|
|
4568
|
-
function isKnownProvider(provider) {
|
|
4569
|
-
return listCapabilityProviders().some((p) => p.provider === provider) || isPluginProvider(provider);
|
|
4570
|
-
}
|
|
4571
|
-
async function handleOAuthStartCommand(args, deps) {
|
|
4572
|
-
const provider = (args[0] ?? "").trim();
|
|
4573
|
-
if (!provider) {
|
|
4574
|
-
return commandResult({
|
|
4575
|
-
stderr: "jr-rpc oauth-start requires: <provider>\n",
|
|
4576
|
-
exitCode: 2
|
|
4577
|
-
});
|
|
4578
|
-
}
|
|
4579
|
-
if (args.length > 1) {
|
|
4580
|
-
return commandResult({
|
|
4581
|
-
stderr: "jr-rpc oauth-start accepts only a provider argument\n",
|
|
4582
|
-
exitCode: 2
|
|
4583
|
-
});
|
|
4584
|
-
}
|
|
4585
|
-
if (deps.requesterId && deps.userTokenStore) {
|
|
4586
|
-
const stored = await deps.userTokenStore.get(deps.requesterId, provider);
|
|
4587
|
-
const providerConfig = getPluginOAuthConfig(provider);
|
|
4588
|
-
if (stored && (stored.expiresAt === void 0 || stored.expiresAt > Date.now()) && hasRequiredOAuthScope(stored.scope, providerConfig?.scope)) {
|
|
4589
|
-
const providerLabel = formatProviderLabel(provider);
|
|
4590
|
-
return commandResult({
|
|
4591
|
-
stdout: {
|
|
4592
|
-
ok: true,
|
|
4593
|
-
already_connected: true,
|
|
4594
|
-
provider,
|
|
4595
|
-
message: `Your ${providerLabel} account is already connected.`
|
|
4596
|
-
},
|
|
4597
|
-
exitCode: 0
|
|
4598
|
-
});
|
|
4599
|
-
}
|
|
4600
|
-
}
|
|
4601
|
-
if (!deps.requesterId) {
|
|
4602
|
-
return commandResult({
|
|
4603
|
-
stderr: "jr-rpc oauth-start requires requester context (requesterId)\n",
|
|
4604
|
-
exitCode: 1
|
|
4605
|
-
});
|
|
4606
|
-
}
|
|
4607
|
-
const result = await startOAuthFlow(provider, {
|
|
4608
|
-
requesterId: deps.requesterId,
|
|
4609
|
-
channelId: deps.channelId,
|
|
4610
|
-
threadTs: deps.threadTs,
|
|
4611
|
-
activeSkillName: deps.activeSkill?.name ?? void 0
|
|
4612
|
-
});
|
|
4613
|
-
if (!result.ok) {
|
|
4614
|
-
return commandResult({ stderr: `${result.error}
|
|
4615
|
-
`, exitCode: 1 });
|
|
4616
|
-
}
|
|
4617
|
-
deps.providerAuthActions?.set(provider, {
|
|
4618
|
-
kind: "oauth_started",
|
|
4619
|
-
delivered: !!result.delivery
|
|
4620
|
-
});
|
|
4621
|
-
if (!result.delivery) {
|
|
4622
|
-
return commandResult({
|
|
4623
|
-
stdout: {
|
|
4624
|
-
ok: true,
|
|
4625
|
-
private_delivery_sent: false,
|
|
4626
|
-
message: "I wasn't able to send you a private authorization link. Please send me a direct message and try again."
|
|
4627
|
-
},
|
|
4628
|
-
exitCode: 0
|
|
4629
|
-
});
|
|
4630
|
-
}
|
|
4631
|
-
return commandResult({
|
|
4632
|
-
stdout: {
|
|
4633
|
-
ok: true,
|
|
4634
|
-
private_delivery_sent: true
|
|
4635
|
-
},
|
|
4636
|
-
exitCode: 0
|
|
4637
|
-
});
|
|
4638
|
-
}
|
|
4639
|
-
async function handleDeleteTokenCommand(args, deps) {
|
|
4640
|
-
const provider = (args[0] ?? "").trim();
|
|
4641
|
-
if (!provider) {
|
|
4642
|
-
return commandResult({
|
|
4643
|
-
stderr: "jr-rpc delete-token requires: <provider>\n",
|
|
4644
|
-
exitCode: 2
|
|
4645
|
-
});
|
|
4646
|
-
}
|
|
4647
|
-
if (!isKnownProvider(provider)) {
|
|
4648
|
-
return commandResult({
|
|
4649
|
-
stderr: `Unknown provider: ${provider}
|
|
4650
|
-
`,
|
|
4651
|
-
exitCode: 2
|
|
4652
|
-
});
|
|
4653
|
-
}
|
|
4654
|
-
if (!deps.requesterId) {
|
|
4655
|
-
return commandResult({
|
|
4656
|
-
stderr: "jr-rpc delete-token requires requester context (requesterId)\n",
|
|
4657
|
-
exitCode: 1
|
|
4658
|
-
});
|
|
4659
|
-
}
|
|
4660
|
-
if (!deps.userTokenStore) {
|
|
4661
|
-
return commandResult({
|
|
4662
|
-
stderr: "Token storage is not available\n",
|
|
4663
|
-
exitCode: 1
|
|
4664
|
-
});
|
|
4665
|
-
}
|
|
4666
|
-
await unlinkProvider(deps.requesterId, provider, deps.userTokenStore);
|
|
4667
|
-
deps.providerAuthActions?.set(provider, { kind: "token_deleted" });
|
|
4668
|
-
logInfo(
|
|
4669
|
-
"jr_rpc_delete_token",
|
|
4670
|
-
{},
|
|
4671
|
-
{
|
|
4672
|
-
"app.credential.provider": provider,
|
|
4673
|
-
...deps.activeSkill?.name ? { "app.skill.name": deps.activeSkill.name } : {}
|
|
4674
|
-
},
|
|
4675
|
-
"Deleted user token via jr-rpc"
|
|
4676
|
-
);
|
|
4677
|
-
return commandResult({
|
|
4678
|
-
stdout: `token_deleted provider=${provider}
|
|
4679
|
-
`,
|
|
4680
|
-
exitCode: 0
|
|
4681
|
-
});
|
|
4682
|
-
}
|
|
4683
4030
|
function createJrRpcCommand(deps) {
|
|
4684
4031
|
return defineCommand("jr-rpc", async (args) => {
|
|
4685
4032
|
const usage = [
|
|
4686
|
-
"jr-rpc issue-credential <capability> [--target <value>]",
|
|
4687
|
-
"jr-rpc oauth-start <provider>",
|
|
4688
|
-
"jr-rpc delete-token <provider>",
|
|
4689
4033
|
"jr-rpc config get <key>",
|
|
4690
4034
|
"jr-rpc config set <key> <value> [--json]",
|
|
4691
4035
|
"jr-rpc config unset <key>",
|
|
4692
4036
|
"jr-rpc config list [--prefix <value>]"
|
|
4693
4037
|
].join("\n");
|
|
4694
4038
|
const verb = (args[0] ?? "").trim();
|
|
4695
|
-
if (verb === "issue-credential") {
|
|
4696
|
-
return handleIssueCredentialCommand(args.slice(1), deps);
|
|
4697
|
-
}
|
|
4698
|
-
if (verb === "oauth-start") {
|
|
4699
|
-
return handleOAuthStartCommand(args.slice(1), deps);
|
|
4700
|
-
}
|
|
4701
|
-
if (verb === "delete-token") {
|
|
4702
|
-
return handleDeleteTokenCommand(args.slice(1), deps);
|
|
4703
|
-
}
|
|
4704
4039
|
if (verb === "config") {
|
|
4705
4040
|
return handleConfigCommand(args.slice(1), deps);
|
|
4706
4041
|
}
|
|
@@ -5782,7 +5117,6 @@ function toLoadedSkill(result, availableSkills) {
|
|
|
5782
5117
|
skillPath: metadata?.skillPath ?? result.skill_dir,
|
|
5783
5118
|
...metadata?.pluginProvider ? { pluginProvider: metadata.pluginProvider } : {},
|
|
5784
5119
|
...metadata?.allowedTools ? { allowedTools: metadata.allowedTools } : {},
|
|
5785
|
-
...metadata?.requiresCapabilities ? { requiresCapabilities: metadata.requiresCapabilities } : {},
|
|
5786
5120
|
...metadata?.usesConfig ? { usesConfig: metadata.usesConfig } : {},
|
|
5787
5121
|
body: result.instructions
|
|
5788
5122
|
};
|
|
@@ -5809,7 +5143,6 @@ async function loadSkillFromHost(availableSkills, skillName) {
|
|
|
5809
5143
|
ok: true,
|
|
5810
5144
|
skill_name: skill.name,
|
|
5811
5145
|
description: skill.description,
|
|
5812
|
-
...skill.requiresCapabilities ? { requires_capabilities: skill.requiresCapabilities } : {},
|
|
5813
5146
|
skill_dir: skillDir,
|
|
5814
5147
|
location: skillFilePath,
|
|
5815
5148
|
instructions: loaded.body
|
|
@@ -5817,7 +5150,7 @@ async function loadSkillFromHost(availableSkills, skillName) {
|
|
|
5817
5150
|
}
|
|
5818
5151
|
function createLoadSkillTool(availableSkills, options) {
|
|
5819
5152
|
return tool({
|
|
5820
|
-
description: "Load a skill by name so its instructions are available for this turn. The result includes `
|
|
5153
|
+
description: "Load a skill by name so its instructions are available for this turn. The result includes `available_tools` when the skill exposes MCP tools for this turn. Use when a request clearly matches a known skill. Do not use when no skill is relevant.",
|
|
5821
5154
|
inputSchema: Type4.Object({
|
|
5822
5155
|
skill_name: Type4.String({
|
|
5823
5156
|
minLength: 1,
|
|
@@ -8364,6 +7697,92 @@ fallbackToRealGh();
|
|
|
8364
7697
|
`;
|
|
8365
7698
|
}
|
|
8366
7699
|
|
|
7700
|
+
// src/chat/sandbox/eval-oauth-stub.ts
|
|
7701
|
+
function buildEvalOauthCliStub() {
|
|
7702
|
+
return `#!/usr/bin/env node
|
|
7703
|
+
const fs = require("node:fs");
|
|
7704
|
+
|
|
7705
|
+
const args = process.argv.slice(2);
|
|
7706
|
+
|
|
7707
|
+
function outputText(value) {
|
|
7708
|
+
fs.writeFileSync(process.stdout.fd, value);
|
|
7709
|
+
}
|
|
7710
|
+
|
|
7711
|
+
if (args.length === 0 || args[0] === "--version" || args[0] === "version") {
|
|
7712
|
+
outputText("eval-oauth 1.0.0 (junior-eval)\\n");
|
|
7713
|
+
process.exit(0);
|
|
7714
|
+
}
|
|
7715
|
+
|
|
7716
|
+
if (args[0] === "whoami") {
|
|
7717
|
+
outputText("eval-oauth-user\\n");
|
|
7718
|
+
process.exit(0);
|
|
7719
|
+
}
|
|
7720
|
+
|
|
7721
|
+
process.stderr.write("eval-oauth stub: unsupported command\\n");
|
|
7722
|
+
process.exit(1);
|
|
7723
|
+
`;
|
|
7724
|
+
}
|
|
7725
|
+
|
|
7726
|
+
// src/chat/sandbox/eval-sentry-stub.ts
|
|
7727
|
+
function buildEvalSentryCliStub() {
|
|
7728
|
+
return `#!/usr/bin/env node
|
|
7729
|
+
const fs = require("node:fs");
|
|
7730
|
+
const { spawnSync } = require("node:child_process");
|
|
7731
|
+
|
|
7732
|
+
const args = process.argv.slice(2);
|
|
7733
|
+
const fallbackBinaries = ["/usr/bin/sentry", "/usr/local/bin/sentry", "/bin/sentry"];
|
|
7734
|
+
|
|
7735
|
+
function hasFlag(name) {
|
|
7736
|
+
return args.includes(name) || args.some((value) => value.startsWith(name + "="));
|
|
7737
|
+
}
|
|
7738
|
+
|
|
7739
|
+
function outputJson(value) {
|
|
7740
|
+
fs.writeFileSync(process.stdout.fd, JSON.stringify(value, null, 2) + "\\n");
|
|
7741
|
+
}
|
|
7742
|
+
|
|
7743
|
+
function outputText(value) {
|
|
7744
|
+
fs.writeFileSync(process.stdout.fd, value);
|
|
7745
|
+
}
|
|
7746
|
+
|
|
7747
|
+
function fallbackToRealSentry() {
|
|
7748
|
+
for (const binary of fallbackBinaries) {
|
|
7749
|
+
if (!fs.existsSync(binary)) {
|
|
7750
|
+
continue;
|
|
7751
|
+
}
|
|
7752
|
+
const result = spawnSync(binary, args, { stdio: "inherit" });
|
|
7753
|
+
process.exit(result.status ?? 1);
|
|
7754
|
+
}
|
|
7755
|
+
process.stderr.write("sentry stub: unsupported command\\n");
|
|
7756
|
+
process.exit(1);
|
|
7757
|
+
}
|
|
7758
|
+
|
|
7759
|
+
if (args.length === 0 || args[0] === "--version" || args[0] === "version") {
|
|
7760
|
+
outputText("sentry-cli 2.0.0 (junior-eval)\\n");
|
|
7761
|
+
process.exit(0);
|
|
7762
|
+
}
|
|
7763
|
+
|
|
7764
|
+
if (args[0] === "issues" && args[1] === "list") {
|
|
7765
|
+
if (hasFlag("--json")) {
|
|
7766
|
+
outputJson([]);
|
|
7767
|
+
} else {
|
|
7768
|
+
outputText("No issues found.\\n");
|
|
7769
|
+
}
|
|
7770
|
+
process.exit(0);
|
|
7771
|
+
}
|
|
7772
|
+
|
|
7773
|
+
if (args[0] === "organizations" && args[1] === "list") {
|
|
7774
|
+
if (hasFlag("--json")) {
|
|
7775
|
+
outputJson([{ slug: "getsentry", name: "Sentry" }]);
|
|
7776
|
+
} else {
|
|
7777
|
+
outputText("getsentry\\n");
|
|
7778
|
+
}
|
|
7779
|
+
process.exit(0);
|
|
7780
|
+
}
|
|
7781
|
+
|
|
7782
|
+
fallbackToRealSentry();
|
|
7783
|
+
`;
|
|
7784
|
+
}
|
|
7785
|
+
|
|
8367
7786
|
// src/chat/sandbox/skill-sync.ts
|
|
8368
7787
|
function toPosixRelative(base, absolute) {
|
|
8369
7788
|
return path5.relative(base, absolute).split(path5.sep).join("/");
|
|
@@ -8427,6 +7846,16 @@ async function buildSkillSyncFiles(availableSkills, runtimeBinDir, referenceFile
|
|
|
8427
7846
|
path: `${runtimeBinDir}/gh`,
|
|
8428
7847
|
content: Buffer.from(buildEvalGitHubCliStub(), "utf8")
|
|
8429
7848
|
});
|
|
7849
|
+
filesToWrite.push({
|
|
7850
|
+
path: `${runtimeBinDir}/sentry`,
|
|
7851
|
+
content: Buffer.from(buildEvalSentryCliStub(), "utf8")
|
|
7852
|
+
});
|
|
7853
|
+
}
|
|
7854
|
+
if (availableSkills.some((skill) => skill.name === "eval-oauth")) {
|
|
7855
|
+
filesToWrite.push({
|
|
7856
|
+
path: `${runtimeBinDir}/eval-oauth`,
|
|
7857
|
+
content: Buffer.from(buildEvalOauthCliStub(), "utf8")
|
|
7858
|
+
});
|
|
8430
7859
|
}
|
|
8431
7860
|
return filesToWrite;
|
|
8432
7861
|
}
|
|
@@ -9376,11 +8805,140 @@ function createSandboxExecutor(options) {
|
|
|
9376
8805
|
await sessionManager.dispose();
|
|
9377
8806
|
}
|
|
9378
8807
|
};
|
|
9379
|
-
}
|
|
9380
|
-
|
|
9381
|
-
// src/chat/runtime/dev-agent-trace.ts
|
|
9382
|
-
function shouldEmitDevAgentTrace() {
|
|
9383
|
-
return process.env.NODE_ENV === "development";
|
|
8808
|
+
}
|
|
8809
|
+
|
|
8810
|
+
// src/chat/runtime/dev-agent-trace.ts
|
|
8811
|
+
function shouldEmitDevAgentTrace() {
|
|
8812
|
+
return process.env.NODE_ENV === "development";
|
|
8813
|
+
}
|
|
8814
|
+
|
|
8815
|
+
// src/chat/credentials/unlink-provider.ts
|
|
8816
|
+
async function unlinkProvider(userId, provider, userTokenStore) {
|
|
8817
|
+
await Promise.all([
|
|
8818
|
+
userTokenStore.delete(userId, provider),
|
|
8819
|
+
deleteMcpStoredOAuthCredentials(userId, provider),
|
|
8820
|
+
deleteMcpServerSessionId(userId, provider),
|
|
8821
|
+
deleteMcpAuthSessionsForUserProvider(userId, provider)
|
|
8822
|
+
]);
|
|
8823
|
+
}
|
|
8824
|
+
|
|
8825
|
+
// src/chat/services/plugin-auth-orchestration.ts
|
|
8826
|
+
var PluginAuthorizationPauseError = class extends Error {
|
|
8827
|
+
provider;
|
|
8828
|
+
constructor(provider) {
|
|
8829
|
+
super(`Plugin authorization started for ${provider}`);
|
|
8830
|
+
this.name = "PluginAuthorizationPauseError";
|
|
8831
|
+
this.provider = provider;
|
|
8832
|
+
}
|
|
8833
|
+
};
|
|
8834
|
+
function isCommandAuthFailure(details) {
|
|
8835
|
+
if (!details || typeof details !== "object") {
|
|
8836
|
+
return false;
|
|
8837
|
+
}
|
|
8838
|
+
const result = details;
|
|
8839
|
+
if (typeof result.exit_code !== "number" || result.exit_code === 0) {
|
|
8840
|
+
return false;
|
|
8841
|
+
}
|
|
8842
|
+
const text = `${typeof result.stdout === "string" ? result.stdout : ""}
|
|
8843
|
+
${typeof result.stderr === "string" ? result.stderr : ""}`.toLowerCase();
|
|
8844
|
+
if (!text.trim()) {
|
|
8845
|
+
return false;
|
|
8846
|
+
}
|
|
8847
|
+
return [
|
|
8848
|
+
/\b401\b/,
|
|
8849
|
+
/\bunauthorized\b/,
|
|
8850
|
+
/\bbad credentials\b/,
|
|
8851
|
+
/\binvalid token\b/,
|
|
8852
|
+
/\btoken (?:expired|revoked)\b/,
|
|
8853
|
+
/\bexpired token\b/,
|
|
8854
|
+
/\bmissing scopes?\b/,
|
|
8855
|
+
/\binsufficient scope\b/,
|
|
8856
|
+
/\binvalid grant\b/,
|
|
8857
|
+
/\breauthoriz/
|
|
8858
|
+
].some((pattern) => pattern.test(text));
|
|
8859
|
+
}
|
|
8860
|
+
function commandTargetsProvider(provider, command, details) {
|
|
8861
|
+
const normalizedCommand = command.trim().toLowerCase();
|
|
8862
|
+
if (!normalizedCommand) {
|
|
8863
|
+
return false;
|
|
8864
|
+
}
|
|
8865
|
+
if (provider === "github" && /^(gh|git)\b/.test(normalizedCommand)) {
|
|
8866
|
+
return true;
|
|
8867
|
+
}
|
|
8868
|
+
const plugin = getPluginDefinition(provider);
|
|
8869
|
+
const candidates = /* @__PURE__ */ new Set([provider.toLowerCase()]);
|
|
8870
|
+
const credentials = plugin?.manifest.credentials;
|
|
8871
|
+
if (credentials) {
|
|
8872
|
+
candidates.add(credentials.authTokenEnv.toLowerCase());
|
|
8873
|
+
for (const domain of credentials.apiDomains) {
|
|
8874
|
+
candidates.add(domain.toLowerCase());
|
|
8875
|
+
}
|
|
8876
|
+
}
|
|
8877
|
+
const combinedText = `${normalizedCommand}
|
|
8878
|
+
${details.stdout?.toLowerCase() ?? ""}
|
|
8879
|
+
${details.stderr?.toLowerCase() ?? ""}`;
|
|
8880
|
+
return [...candidates].some((candidate) => combinedText.includes(candidate));
|
|
8881
|
+
}
|
|
8882
|
+
function createPluginAuthOrchestration(deps, abortAgent) {
|
|
8883
|
+
let pendingPause;
|
|
8884
|
+
const startAuthorizationPause = async (provider, activeSkill, options) => {
|
|
8885
|
+
if (pendingPause) {
|
|
8886
|
+
throw pendingPause;
|
|
8887
|
+
}
|
|
8888
|
+
if (!deps.requesterId || !getPluginOAuthConfig(provider)) {
|
|
8889
|
+
throw new Error(`Cannot start plugin authorization for ${provider}`);
|
|
8890
|
+
}
|
|
8891
|
+
const providerLabel = formatProviderLabel(provider);
|
|
8892
|
+
const oauthResult = await startOAuthFlow(provider, {
|
|
8893
|
+
requesterId: deps.requesterId,
|
|
8894
|
+
channelId: deps.channelId,
|
|
8895
|
+
threadTs: deps.threadTs,
|
|
8896
|
+
userMessage: deps.userMessage,
|
|
8897
|
+
channelConfiguration: deps.channelConfiguration,
|
|
8898
|
+
activeSkillName: activeSkill?.name ?? void 0,
|
|
8899
|
+
resumeConversationId: deps.conversationId,
|
|
8900
|
+
resumeSessionId: deps.sessionId
|
|
8901
|
+
});
|
|
8902
|
+
if (!oauthResult.ok) {
|
|
8903
|
+
throw new Error(oauthResult.error);
|
|
8904
|
+
}
|
|
8905
|
+
if (!oauthResult.delivery) {
|
|
8906
|
+
throw new Error(
|
|
8907
|
+
`I need to connect your ${providerLabel} account first, but I wasn't able to send you a private authorization link. Please send me a direct message and try again.`
|
|
8908
|
+
);
|
|
8909
|
+
}
|
|
8910
|
+
if (options?.unlinkExistingProvider && deps.requesterId && deps.userTokenStore) {
|
|
8911
|
+
await unlinkProvider(deps.requesterId, provider, deps.userTokenStore);
|
|
8912
|
+
}
|
|
8913
|
+
pendingPause = new PluginAuthorizationPauseError(provider);
|
|
8914
|
+
abortAgent();
|
|
8915
|
+
throw pendingPause;
|
|
8916
|
+
};
|
|
8917
|
+
const handleCredentialUnavailable = async (input) => {
|
|
8918
|
+
if (pendingPause) {
|
|
8919
|
+
throw pendingPause;
|
|
8920
|
+
}
|
|
8921
|
+
if (!deps.requesterId || !getPluginOAuthConfig(input.error.provider)) {
|
|
8922
|
+
throw input.error;
|
|
8923
|
+
}
|
|
8924
|
+
return await startAuthorizationPause(
|
|
8925
|
+
input.error.provider,
|
|
8926
|
+
input.activeSkill
|
|
8927
|
+
);
|
|
8928
|
+
};
|
|
8929
|
+
return {
|
|
8930
|
+
handleCredentialUnavailable,
|
|
8931
|
+
handleCommandFailure: async (input) => {
|
|
8932
|
+
const provider = input.activeSkill?.pluginProvider;
|
|
8933
|
+
if (!provider || !deps.requesterId || !deps.userTokenStore || !getPluginOAuthConfig(provider) || !isCommandAuthFailure(input.details) || !commandTargetsProvider(provider, input.command, input.details)) {
|
|
8934
|
+
return;
|
|
8935
|
+
}
|
|
8936
|
+
await startAuthorizationPause(provider, input.activeSkill, {
|
|
8937
|
+
unlinkExistingProvider: true
|
|
8938
|
+
});
|
|
8939
|
+
},
|
|
8940
|
+
getPendingPause: () => pendingPause
|
|
8941
|
+
};
|
|
9384
8942
|
}
|
|
9385
8943
|
|
|
9386
8944
|
// src/chat/runtime/tool-status.ts
|
|
@@ -9584,7 +9142,7 @@ function handleToolExecutionError(error, toolName, toolCallId, shouldTrace, trac
|
|
|
9584
9142
|
}
|
|
9585
9143
|
|
|
9586
9144
|
// src/chat/tools/agent-tools.ts
|
|
9587
|
-
function createAgentTools(tools, sandbox, spanContext, onStatus, sandboxExecutor, capabilityRuntime, hooks) {
|
|
9145
|
+
function createAgentTools(tools, sandbox, spanContext, onStatus, sandboxExecutor, capabilityRuntime, pluginAuthOrchestration, hooks) {
|
|
9588
9146
|
const shouldTrace = shouldEmitDevAgentTrace();
|
|
9589
9147
|
return Object.entries(tools).map(([toolName, toolDef]) => ({
|
|
9590
9148
|
name: toolName,
|
|
@@ -9642,6 +9200,13 @@ function createAgentTools(tools, sandbox, spanContext, onStatus, sandboxExecutor
|
|
|
9642
9200
|
experimental_context: sandbox
|
|
9643
9201
|
});
|
|
9644
9202
|
const normalized = normalizeToolResult(result, isSandbox);
|
|
9203
|
+
if (bashCommand && pluginAuthOrchestration) {
|
|
9204
|
+
await pluginAuthOrchestration.handleCommandFailure({
|
|
9205
|
+
activeSkill: sandbox.getActiveSkill(),
|
|
9206
|
+
command: bashCommand,
|
|
9207
|
+
details: normalized.details
|
|
9208
|
+
});
|
|
9209
|
+
}
|
|
9645
9210
|
const toolResultAttribute = serializeGenAiAttribute(
|
|
9646
9211
|
normalized.details
|
|
9647
9212
|
);
|
|
@@ -9652,6 +9217,9 @@ function createAgentTools(tools, sandbox, spanContext, onStatus, sandboxExecutor
|
|
|
9652
9217
|
}
|
|
9653
9218
|
return normalized;
|
|
9654
9219
|
} catch (error) {
|
|
9220
|
+
if (error instanceof PluginAuthorizationPauseError) {
|
|
9221
|
+
throw error;
|
|
9222
|
+
}
|
|
9655
9223
|
handleToolExecutionError(
|
|
9656
9224
|
error,
|
|
9657
9225
|
toolName,
|
|
@@ -9674,6 +9242,233 @@ function createAgentTools(tools, sandbox, spanContext, onStatus, sandboxExecutor
|
|
|
9674
9242
|
}));
|
|
9675
9243
|
}
|
|
9676
9244
|
|
|
9245
|
+
// src/chat/respond-helpers.ts
|
|
9246
|
+
var MAX_INLINE_ATTACHMENT_BASE64_CHARS = 12e4;
|
|
9247
|
+
function getSessionIdentifiers(context) {
|
|
9248
|
+
return {
|
|
9249
|
+
conversationId: context.correlation?.conversationId ?? context.correlation?.threadId ?? context.correlation?.runId,
|
|
9250
|
+
sessionId: context.correlation?.turnId
|
|
9251
|
+
};
|
|
9252
|
+
}
|
|
9253
|
+
function isExecutionDeferralResponse(text) {
|
|
9254
|
+
return /\b(want me to proceed|do you want me to proceed|shall i proceed|can i proceed|should i proceed|let me do that now|give me a moment|tag me again|fresh invocation)\b/i.test(
|
|
9255
|
+
text
|
|
9256
|
+
);
|
|
9257
|
+
}
|
|
9258
|
+
function isToolAccessDisclaimerResponse(text) {
|
|
9259
|
+
return /\b(i (don't|do not) have access to (active )?tool|tool results came back empty|prior results .* empty|cannot access .*tool|need to (run|load) .*tool .* first)\b/i.test(
|
|
9260
|
+
text
|
|
9261
|
+
);
|
|
9262
|
+
}
|
|
9263
|
+
function isExecutionEscapeResponse(text) {
|
|
9264
|
+
const trimmed = text.trim();
|
|
9265
|
+
if (!trimmed) return false;
|
|
9266
|
+
return isExecutionDeferralResponse(trimmed) || isToolAccessDisclaimerResponse(trimmed);
|
|
9267
|
+
}
|
|
9268
|
+
function parseJsonCandidate2(text) {
|
|
9269
|
+
const trimmed = text.trim();
|
|
9270
|
+
if (!trimmed) return void 0;
|
|
9271
|
+
try {
|
|
9272
|
+
return JSON.parse(trimmed);
|
|
9273
|
+
} catch {
|
|
9274
|
+
const fenced = trimmed.match(/^```(?:json)?\s*([\s\S]*?)\s*```$/i);
|
|
9275
|
+
if (!fenced) return void 0;
|
|
9276
|
+
try {
|
|
9277
|
+
return JSON.parse(fenced[1]);
|
|
9278
|
+
} catch {
|
|
9279
|
+
return void 0;
|
|
9280
|
+
}
|
|
9281
|
+
}
|
|
9282
|
+
}
|
|
9283
|
+
function isToolPayloadShape(payload) {
|
|
9284
|
+
if (!payload || typeof payload !== "object") return false;
|
|
9285
|
+
const record = payload;
|
|
9286
|
+
const type = typeof record.type === "string" ? record.type.toLowerCase() : "";
|
|
9287
|
+
if (type.startsWith("tool-")) return true;
|
|
9288
|
+
if (type === "tool_use" || type === "tool_call" || type === "tool_result" || type === "tool_error")
|
|
9289
|
+
return true;
|
|
9290
|
+
const hasToolName = typeof record.toolName === "string" || typeof record.name === "string";
|
|
9291
|
+
const hasToolInput = Object.prototype.hasOwnProperty.call(record, "input") || Object.prototype.hasOwnProperty.call(record, "args");
|
|
9292
|
+
if (hasToolName && hasToolInput) return true;
|
|
9293
|
+
return false;
|
|
9294
|
+
}
|
|
9295
|
+
function isRawToolPayloadResponse(text) {
|
|
9296
|
+
const parsed = parseJsonCandidate2(text);
|
|
9297
|
+
if (Array.isArray(parsed)) {
|
|
9298
|
+
return parsed.some((entry) => isToolPayloadShape(entry));
|
|
9299
|
+
}
|
|
9300
|
+
if (isToolPayloadShape(parsed)) {
|
|
9301
|
+
return true;
|
|
9302
|
+
}
|
|
9303
|
+
const compact = text.replace(/\s+/g, " ");
|
|
9304
|
+
return /"type"\s*:\s*"tool[-_](use|call|result|error)"/i.test(compact);
|
|
9305
|
+
}
|
|
9306
|
+
function toObservablePromptPart(part) {
|
|
9307
|
+
if (part.type === "text") {
|
|
9308
|
+
return {
|
|
9309
|
+
type: "text",
|
|
9310
|
+
text: part.text
|
|
9311
|
+
};
|
|
9312
|
+
}
|
|
9313
|
+
return {
|
|
9314
|
+
type: "image",
|
|
9315
|
+
mimeType: part.mimeType,
|
|
9316
|
+
data: `[omitted:${part.data.length}]`
|
|
9317
|
+
};
|
|
9318
|
+
}
|
|
9319
|
+
function summarizeMessageText(text) {
|
|
9320
|
+
const normalized = text.trim().replace(/\s+/g, " ");
|
|
9321
|
+
if (!normalized) {
|
|
9322
|
+
return "[empty]";
|
|
9323
|
+
}
|
|
9324
|
+
return normalized.length > 1200 ? `${normalized.slice(0, 1200)}...` : normalized;
|
|
9325
|
+
}
|
|
9326
|
+
function buildUserTurnText(userInput, conversationContext, metadata) {
|
|
9327
|
+
const trimmedContext = conversationContext?.trim();
|
|
9328
|
+
const hasSessionContext = Boolean(metadata?.sessionContext?.conversationId);
|
|
9329
|
+
const hasTurnContext = Boolean(metadata?.turnContext?.traceId);
|
|
9330
|
+
if (!trimmedContext && !hasSessionContext && !hasTurnContext) {
|
|
9331
|
+
return userInput;
|
|
9332
|
+
}
|
|
9333
|
+
const sections = [
|
|
9334
|
+
"<current-message>",
|
|
9335
|
+
userInput,
|
|
9336
|
+
"</current-message>"
|
|
9337
|
+
];
|
|
9338
|
+
if (trimmedContext) {
|
|
9339
|
+
sections.push(
|
|
9340
|
+
"",
|
|
9341
|
+
"<thread-conversation-context>",
|
|
9342
|
+
"Use this context for continuity across prior thread turns.",
|
|
9343
|
+
trimmedContext,
|
|
9344
|
+
"</thread-conversation-context>"
|
|
9345
|
+
);
|
|
9346
|
+
}
|
|
9347
|
+
if (metadata?.sessionContext?.conversationId) {
|
|
9348
|
+
sections.push(
|
|
9349
|
+
"",
|
|
9350
|
+
"<session-context>",
|
|
9351
|
+
`- gen_ai.conversation.id: ${metadata.sessionContext.conversationId}`,
|
|
9352
|
+
"</session-context>"
|
|
9353
|
+
);
|
|
9354
|
+
}
|
|
9355
|
+
if (metadata?.turnContext?.traceId) {
|
|
9356
|
+
sections.push(
|
|
9357
|
+
"",
|
|
9358
|
+
"<turn-context>",
|
|
9359
|
+
`- trace_id: ${metadata.turnContext.traceId}`,
|
|
9360
|
+
"</turn-context>"
|
|
9361
|
+
);
|
|
9362
|
+
}
|
|
9363
|
+
return sections.join("\n");
|
|
9364
|
+
}
|
|
9365
|
+
function encodeNonImageAttachmentForPrompt(attachment) {
|
|
9366
|
+
const base64 = attachment.data.toString("base64");
|
|
9367
|
+
const wasTruncated = base64.length > MAX_INLINE_ATTACHMENT_BASE64_CHARS;
|
|
9368
|
+
const encodedPayload = wasTruncated ? `${base64.slice(0, MAX_INLINE_ATTACHMENT_BASE64_CHARS)}...` : base64;
|
|
9369
|
+
return [
|
|
9370
|
+
"<attachment>",
|
|
9371
|
+
`filename: ${attachment.filename ?? "unnamed"}`,
|
|
9372
|
+
`media_type: ${attachment.mediaType}`,
|
|
9373
|
+
"encoding: base64",
|
|
9374
|
+
`truncated: ${wasTruncated ? "true" : "false"}`,
|
|
9375
|
+
"<data_base64>",
|
|
9376
|
+
encodedPayload,
|
|
9377
|
+
"</data_base64>",
|
|
9378
|
+
"</attachment>"
|
|
9379
|
+
].join("\n");
|
|
9380
|
+
}
|
|
9381
|
+
function buildExecutionFailureMessage(toolErrorCount) {
|
|
9382
|
+
if (toolErrorCount > 0) {
|
|
9383
|
+
return "I couldn't complete this because one or more required tools failed in this turn. I've logged the failure details.";
|
|
9384
|
+
}
|
|
9385
|
+
return "I couldn't complete this request in this turn due to an execution failure. I've logged the details for debugging.";
|
|
9386
|
+
}
|
|
9387
|
+
function isToolResultMessage(value) {
|
|
9388
|
+
return typeof value === "object" && value !== null && value.role === "toolResult";
|
|
9389
|
+
}
|
|
9390
|
+
function normalizeToolNameFromResult(result) {
|
|
9391
|
+
if (!result || typeof result !== "object") return void 0;
|
|
9392
|
+
const record = result;
|
|
9393
|
+
if (typeof record.toolName === "string" && record.toolName.length > 0) {
|
|
9394
|
+
return record.toolName;
|
|
9395
|
+
}
|
|
9396
|
+
if (typeof record.name === "string" && record.name.length > 0) {
|
|
9397
|
+
return record.name;
|
|
9398
|
+
}
|
|
9399
|
+
return void 0;
|
|
9400
|
+
}
|
|
9401
|
+
function isToolResultError(result) {
|
|
9402
|
+
if (!result || typeof result !== "object") return false;
|
|
9403
|
+
return Boolean(result.isError);
|
|
9404
|
+
}
|
|
9405
|
+
function isAssistantMessage(value) {
|
|
9406
|
+
return typeof value === "object" && value !== null && value.role === "assistant";
|
|
9407
|
+
}
|
|
9408
|
+
function getPiMessageRole(value) {
|
|
9409
|
+
if (!value || typeof value !== "object") {
|
|
9410
|
+
return void 0;
|
|
9411
|
+
}
|
|
9412
|
+
const role = value.role;
|
|
9413
|
+
return typeof role === "string" ? role : void 0;
|
|
9414
|
+
}
|
|
9415
|
+
function extractAssistantText(message) {
|
|
9416
|
+
const content = message.content ?? [];
|
|
9417
|
+
return content.filter(
|
|
9418
|
+
(part) => part.type === "text" && typeof part.text === "string"
|
|
9419
|
+
).map((part) => part.text).join("\n");
|
|
9420
|
+
}
|
|
9421
|
+
function getTerminalAssistantMessages(messages) {
|
|
9422
|
+
let lastToolResultIndex = -1;
|
|
9423
|
+
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
|
9424
|
+
if (isToolResultMessage(messages[index])) {
|
|
9425
|
+
lastToolResultIndex = index;
|
|
9426
|
+
break;
|
|
9427
|
+
}
|
|
9428
|
+
}
|
|
9429
|
+
return messages.slice(lastToolResultIndex + 1).filter(isAssistantMessage);
|
|
9430
|
+
}
|
|
9431
|
+
function hasCompletedAssistantTurn(messages) {
|
|
9432
|
+
const message = getTerminalAssistantMessages(messages).at(-1);
|
|
9433
|
+
if (!message) {
|
|
9434
|
+
return false;
|
|
9435
|
+
}
|
|
9436
|
+
const stopReason = message.stopReason;
|
|
9437
|
+
return typeof stopReason === "string" && stopReason !== "error" && extractAssistantText(message).trim().length > 0;
|
|
9438
|
+
}
|
|
9439
|
+
function upsertActiveSkill(activeSkills, next) {
|
|
9440
|
+
const existing = activeSkills.find((skill) => skill.name === next.name);
|
|
9441
|
+
if (existing) {
|
|
9442
|
+
existing.body = next.body;
|
|
9443
|
+
existing.description = next.description;
|
|
9444
|
+
existing.skillPath = next.skillPath;
|
|
9445
|
+
existing.allowedTools = next.allowedTools;
|
|
9446
|
+
existing.usesConfig = next.usesConfig;
|
|
9447
|
+
existing.pluginProvider = next.pluginProvider;
|
|
9448
|
+
return;
|
|
9449
|
+
}
|
|
9450
|
+
activeSkills.push(next);
|
|
9451
|
+
}
|
|
9452
|
+
function collectRelevantConfigurationKeys(activeSkills, explicitSkill) {
|
|
9453
|
+
const keys = /* @__PURE__ */ new Set();
|
|
9454
|
+
for (const skill of [
|
|
9455
|
+
...activeSkills,
|
|
9456
|
+
...explicitSkill ? [explicitSkill] : []
|
|
9457
|
+
]) {
|
|
9458
|
+
for (const key of skill.usesConfig ?? []) {
|
|
9459
|
+
keys.add(key);
|
|
9460
|
+
}
|
|
9461
|
+
}
|
|
9462
|
+
return [...keys].sort((a, b) => a.localeCompare(b));
|
|
9463
|
+
}
|
|
9464
|
+
function trimTrailingAssistantMessages(messages) {
|
|
9465
|
+
let end = messages.length;
|
|
9466
|
+
while (end > 0 && getPiMessageRole(messages[end - 1]) === "assistant") {
|
|
9467
|
+
end -= 1;
|
|
9468
|
+
}
|
|
9469
|
+
return end === messages.length ? [...messages] : messages.slice(0, end);
|
|
9470
|
+
}
|
|
9471
|
+
|
|
9677
9472
|
// src/chat/services/reply-delivery-plan.ts
|
|
9678
9473
|
var REACTION_ONLY_ACK_RE = /^(?::[a-z0-9_+-]+:|[\p{Extended_Pictographic}\uFE0F\u200D]+)$/u;
|
|
9679
9474
|
var REDUNDANT_REACTION_ACK_TEXT = ["done", "got it", "ok", "okay"];
|
|
@@ -9762,8 +9557,10 @@ function buildTurnResult(input) {
|
|
|
9762
9557
|
toolCalls,
|
|
9763
9558
|
sandboxId,
|
|
9764
9559
|
sandboxDependencyProfileHash,
|
|
9560
|
+
durationMs,
|
|
9765
9561
|
shouldTrace,
|
|
9766
9562
|
spanContext,
|
|
9563
|
+
usage,
|
|
9767
9564
|
correlation,
|
|
9768
9565
|
assistantUserName
|
|
9769
9566
|
} = input;
|
|
@@ -9771,7 +9568,6 @@ function buildTurnResult(input) {
|
|
|
9771
9568
|
const assistantMessages = newMessages.filter(isAssistantMessage);
|
|
9772
9569
|
const terminalAssistantMessages = getTerminalAssistantMessages(newMessages);
|
|
9773
9570
|
const primaryText = terminalAssistantMessages.map((message) => extractAssistantText(message)).join("\n\n").trim();
|
|
9774
|
-
const oauthStartedMessage = extractOAuthStartedMessageFromToolResults(toolResults);
|
|
9775
9571
|
const toolErrorCount = toolResults.filter((result) => result.isError).length;
|
|
9776
9572
|
const explicitChannelPostIntent = isExplicitChannelPostIntent(userInput);
|
|
9777
9573
|
const successfulToolNames = new Set(
|
|
@@ -9780,13 +9576,14 @@ function buildTurnResult(input) {
|
|
|
9780
9576
|
const channelPostPerformed = successfulToolNames.has(
|
|
9781
9577
|
"slackChannelPostMessage"
|
|
9782
9578
|
);
|
|
9783
|
-
const
|
|
9579
|
+
const reactionPerformed = successfulToolNames.has("slackMessageAddReaction");
|
|
9580
|
+
const baseDeliveryPlan = buildReplyDeliveryPlan({
|
|
9784
9581
|
explicitChannelPostIntent,
|
|
9785
9582
|
channelPostPerformed,
|
|
9786
9583
|
hasFiles: replyFiles.length > 0
|
|
9787
9584
|
});
|
|
9788
|
-
const
|
|
9789
|
-
if (!primaryText && !
|
|
9585
|
+
const sideEffectOnlySuccess = !primaryText && toolErrorCount === 0 && (reactionPerformed || channelPostPerformed || replyFiles.length > 0);
|
|
9586
|
+
if (!primaryText && !sideEffectOnlySuccess) {
|
|
9790
9587
|
logWarn(
|
|
9791
9588
|
"ai_model_response_empty",
|
|
9792
9589
|
{
|
|
@@ -9809,12 +9606,17 @@ function buildTurnResult(input) {
|
|
|
9809
9606
|
const stopReason = typeof lastAssistant?.stopReason === "string" ? lastAssistant.stopReason : void 0;
|
|
9810
9607
|
const errorMessage = typeof lastAssistant?.errorMessage === "string" ? lastAssistant.errorMessage : void 0;
|
|
9811
9608
|
const usedPrimaryText = Boolean(primaryText);
|
|
9812
|
-
const outcome = primaryText
|
|
9813
|
-
const fallbackText =
|
|
9814
|
-
const responseText = primaryText || fallbackText;
|
|
9609
|
+
const outcome = primaryText ? stopReason === "error" ? "provider_error" : "success" : sideEffectOnlySuccess ? "success" : "execution_failure";
|
|
9610
|
+
const fallbackText = buildExecutionFailureMessage(toolErrorCount);
|
|
9611
|
+
const responseText = primaryText || (sideEffectOnlySuccess ? "" : fallbackText);
|
|
9815
9612
|
const escapedOrRawPayload = Boolean(primaryText) && (isExecutionEscapeResponse(primaryText) || isRawToolPayloadResponse(primaryText));
|
|
9816
9613
|
const resolvedText = escapedOrRawPayload ? fallbackText : enforceAttachmentClaimTruth(responseText, replyFiles.length > 0);
|
|
9817
|
-
const
|
|
9614
|
+
const deliveryPlan = reactionPerformed && !resolvedText && replyFiles.length === 0 && !channelPostPerformed ? {
|
|
9615
|
+
...baseDeliveryPlan,
|
|
9616
|
+
postThreadText: false
|
|
9617
|
+
} : baseDeliveryPlan;
|
|
9618
|
+
const deliveryMode = deliveryPlan.mode;
|
|
9619
|
+
const resolvedOutcome = escapedOrRawPayload ? "execution_failure" : outcome;
|
|
9818
9620
|
if (shouldTrace) {
|
|
9819
9621
|
logInfo(
|
|
9820
9622
|
"agent_message_out",
|
|
@@ -9838,6 +9640,8 @@ function buildTurnResult(input) {
|
|
|
9838
9640
|
toolResultCount: toolResults.length,
|
|
9839
9641
|
toolErrorCount,
|
|
9840
9642
|
usedPrimaryText,
|
|
9643
|
+
durationMs,
|
|
9644
|
+
usage,
|
|
9841
9645
|
stopReason,
|
|
9842
9646
|
errorMessage,
|
|
9843
9647
|
providerError: void 0
|
|
@@ -10156,6 +9960,7 @@ function mcpToolsToDefinitions(mcpTools) {
|
|
|
10156
9960
|
return defs;
|
|
10157
9961
|
}
|
|
10158
9962
|
async function generateAssistantReply(messageText, context = {}) {
|
|
9963
|
+
const replyStartedAtMs = Date.now();
|
|
10159
9964
|
let timeoutResumeConversationId;
|
|
10160
9965
|
let timeoutResumeSessionId;
|
|
10161
9966
|
let timeoutResumeSliceId = 1;
|
|
@@ -10166,6 +9971,7 @@ async function generateAssistantReply(messageText, context = {}) {
|
|
|
10166
9971
|
let mcpToolManager;
|
|
10167
9972
|
let sandboxExecutor;
|
|
10168
9973
|
let timedOut = false;
|
|
9974
|
+
let turnUsage;
|
|
10169
9975
|
const getSandboxMetadata = () => sandboxExecutor ? {
|
|
10170
9976
|
sandboxId: sandboxExecutor.getSandboxId(),
|
|
10171
9977
|
sandboxDependencyProfileHash: sandboxExecutor.getDependencyProfileHash()
|
|
@@ -10249,11 +10055,9 @@ async function generateAssistantReply(messageText, context = {}) {
|
|
|
10249
10055
|
...persistedConfigurationValues
|
|
10250
10056
|
};
|
|
10251
10057
|
const capabilityRuntime = createSkillCapabilityRuntime({
|
|
10252
|
-
|
|
10253
|
-
requesterId: context.requester?.userId,
|
|
10254
|
-
resolveConfiguration: async (key) => configurationValues[key]
|
|
10058
|
+
requesterId: context.requester?.userId
|
|
10255
10059
|
});
|
|
10256
|
-
const
|
|
10060
|
+
const userTokenStore = createUserTokenStore();
|
|
10257
10061
|
sandboxExecutor = createSandboxExecutor({
|
|
10258
10062
|
sandboxId: context.sandbox?.sandboxId,
|
|
10259
10063
|
sandboxDependencyProfileHash: context.sandbox?.sandboxDependencyProfileHash,
|
|
@@ -10266,15 +10070,9 @@ async function generateAssistantReply(messageText, context = {}) {
|
|
|
10266
10070
|
},
|
|
10267
10071
|
runBashCustomCommand: async (command) => {
|
|
10268
10072
|
const result = await maybeExecuteJrRpcCustomCommand(command, {
|
|
10269
|
-
capabilityRuntime,
|
|
10270
10073
|
activeSkill: skillSandbox.getActiveSkill(),
|
|
10271
10074
|
channelConfiguration: context.channelConfiguration,
|
|
10272
10075
|
requesterId: context.requester?.userId,
|
|
10273
|
-
channelId: context.correlation?.channelId,
|
|
10274
|
-
threadTs: context.correlation?.threadTs,
|
|
10275
|
-
userMessage: userInput,
|
|
10276
|
-
userTokenStore: createUserTokenStore(),
|
|
10277
|
-
providerAuthActions,
|
|
10278
10076
|
onConfigurationValueChanged: (key, value) => {
|
|
10279
10077
|
if (value === void 0) {
|
|
10280
10078
|
delete configurationValues[key];
|
|
@@ -10374,14 +10172,47 @@ async function generateAssistantReply(messageText, context = {}) {
|
|
|
10374
10172
|
},
|
|
10375
10173
|
() => agent?.abort()
|
|
10376
10174
|
);
|
|
10175
|
+
const pluginAuth = createPluginAuthOrchestration(
|
|
10176
|
+
{
|
|
10177
|
+
conversationId: sessionConversationId,
|
|
10178
|
+
sessionId,
|
|
10179
|
+
requesterId: context.requester?.userId,
|
|
10180
|
+
channelId: context.correlation?.channelId,
|
|
10181
|
+
threadTs: context.correlation?.threadTs,
|
|
10182
|
+
userMessage: userInput,
|
|
10183
|
+
channelConfiguration: context.channelConfiguration,
|
|
10184
|
+
userTokenStore
|
|
10185
|
+
},
|
|
10186
|
+
() => agent?.abort()
|
|
10187
|
+
);
|
|
10377
10188
|
mcpToolManager = new McpToolManager(getPluginMcpProviders(), {
|
|
10378
10189
|
authProviderFactory: mcpAuth.authProviderFactory,
|
|
10379
10190
|
onAuthorizationRequired: mcpAuth.onAuthorizationRequired
|
|
10380
10191
|
});
|
|
10381
10192
|
const turnMcpToolManager = mcpToolManager;
|
|
10193
|
+
const getPendingAuthPause = () => pluginAuth.getPendingPause() ?? mcpAuth.getPendingPause();
|
|
10382
10194
|
const syncResumeState = () => {
|
|
10383
10195
|
loadedSkillNamesForResume = activeSkills.map((skill) => skill.name);
|
|
10384
10196
|
};
|
|
10197
|
+
const enableSkillCredentials = async (skill, reason) => {
|
|
10198
|
+
if (!skill?.pluginProvider) {
|
|
10199
|
+
return;
|
|
10200
|
+
}
|
|
10201
|
+
try {
|
|
10202
|
+
await capabilityRuntime.enableCredentialsForTurn({
|
|
10203
|
+
activeSkill: skill,
|
|
10204
|
+
reason
|
|
10205
|
+
});
|
|
10206
|
+
} catch (error) {
|
|
10207
|
+
if (error instanceof CredentialUnavailableError && context.requester?.userId) {
|
|
10208
|
+
await pluginAuth.handleCredentialUnavailable({
|
|
10209
|
+
activeSkill: skill,
|
|
10210
|
+
error
|
|
10211
|
+
});
|
|
10212
|
+
}
|
|
10213
|
+
throw error;
|
|
10214
|
+
}
|
|
10215
|
+
};
|
|
10385
10216
|
setTags({
|
|
10386
10217
|
conversationId: spanContext.conversationId,
|
|
10387
10218
|
turnId: spanContext.turnId,
|
|
@@ -10423,6 +10254,10 @@ async function generateAssistantReply(messageText, context = {}) {
|
|
|
10423
10254
|
if (mcpAuth.getPendingPause()) {
|
|
10424
10255
|
return void 0;
|
|
10425
10256
|
}
|
|
10257
|
+
await enableSkillCredentials(
|
|
10258
|
+
effective,
|
|
10259
|
+
`skill:${effective.name}:turn:load`
|
|
10260
|
+
);
|
|
10426
10261
|
if (!effective.pluginProvider) {
|
|
10427
10262
|
return void 0;
|
|
10428
10263
|
}
|
|
@@ -10457,6 +10292,7 @@ async function generateAssistantReply(messageText, context = {}) {
|
|
|
10457
10292
|
timeoutResumeMessages = existingCheckpoint?.piMessages ?? [];
|
|
10458
10293
|
throw mcpAuth.getPendingPause();
|
|
10459
10294
|
}
|
|
10295
|
+
await enableSkillCredentials(skill, `skill:${skill.name}:turn:resume`);
|
|
10460
10296
|
}
|
|
10461
10297
|
syncResumeState();
|
|
10462
10298
|
const activeToolSummaries = turnMcpToolManager.getActiveToolCatalog(activeSkills).map(toExposedToolSummary);
|
|
@@ -10547,6 +10383,7 @@ async function generateAssistantReply(messageText, context = {}) {
|
|
|
10547
10383
|
context.onStatus,
|
|
10548
10384
|
sandboxExecutor,
|
|
10549
10385
|
capabilityRuntime,
|
|
10386
|
+
pluginAuth,
|
|
10550
10387
|
agentToolHooks
|
|
10551
10388
|
);
|
|
10552
10389
|
const agentTools = [...baseAgentTools];
|
|
@@ -10560,6 +10397,7 @@ async function generateAssistantReply(messageText, context = {}) {
|
|
|
10560
10397
|
context.onStatus,
|
|
10561
10398
|
sandboxExecutor,
|
|
10562
10399
|
capabilityRuntime,
|
|
10400
|
+
pluginAuth,
|
|
10563
10401
|
agentToolHooks
|
|
10564
10402
|
);
|
|
10565
10403
|
agentTools.length = 0;
|
|
@@ -10666,9 +10504,9 @@ async function generateAssistantReply(messageText, context = {}) {
|
|
|
10666
10504
|
});
|
|
10667
10505
|
timeoutResumeMessages = [...agent.state.messages];
|
|
10668
10506
|
}
|
|
10669
|
-
if (
|
|
10507
|
+
if (getPendingAuthPause()) {
|
|
10670
10508
|
timeoutResumeMessages = [...agent.state.messages];
|
|
10671
|
-
throw
|
|
10509
|
+
throw getPendingAuthPause();
|
|
10672
10510
|
}
|
|
10673
10511
|
throw error;
|
|
10674
10512
|
} finally {
|
|
@@ -10678,20 +10516,24 @@ async function generateAssistantReply(messageText, context = {}) {
|
|
|
10678
10516
|
}
|
|
10679
10517
|
newMessages = agent.state.messages.slice(beforeMessageCount);
|
|
10680
10518
|
completedAssistantTurn = hasCompletedAssistantTurn(newMessages);
|
|
10681
|
-
if (
|
|
10519
|
+
if (getPendingAuthPause() && !completedAssistantTurn) {
|
|
10682
10520
|
timeoutResumeMessages = [...agent.state.messages];
|
|
10683
|
-
throw
|
|
10521
|
+
throw getPendingAuthPause();
|
|
10684
10522
|
}
|
|
10685
10523
|
const outputMessages = newMessages.filter(isAssistantMessage);
|
|
10686
10524
|
const outputMessagesAttribute = serializeGenAiAttribute(outputMessages);
|
|
10687
|
-
const
|
|
10525
|
+
const usageSummary = extractGenAiUsageSummary(
|
|
10688
10526
|
promptResult,
|
|
10689
10527
|
agent.state,
|
|
10690
10528
|
...outputMessages
|
|
10691
10529
|
);
|
|
10530
|
+
turnUsage = Object.values(usageSummary).some(
|
|
10531
|
+
(value) => value !== void 0
|
|
10532
|
+
) ? usageSummary : void 0;
|
|
10692
10533
|
setSpanAttributes({
|
|
10693
10534
|
...outputMessagesAttribute ? { "gen_ai.output.messages": outputMessagesAttribute } : {},
|
|
10694
|
-
...
|
|
10535
|
+
...usageSummary.inputTokens !== void 0 ? { "gen_ai.usage.input_tokens": usageSummary.inputTokens } : {},
|
|
10536
|
+
...usageSummary.outputTokens !== void 0 ? { "gen_ai.usage.output_tokens": usageSummary.outputTokens } : {}
|
|
10695
10537
|
});
|
|
10696
10538
|
},
|
|
10697
10539
|
{
|
|
@@ -10704,8 +10546,8 @@ async function generateAssistantReply(messageText, context = {}) {
|
|
|
10704
10546
|
} finally {
|
|
10705
10547
|
unsubscribe();
|
|
10706
10548
|
}
|
|
10707
|
-
if (
|
|
10708
|
-
throw
|
|
10549
|
+
if (getPendingAuthPause() && !completedAssistantTurn) {
|
|
10550
|
+
throw getPendingAuthPause();
|
|
10709
10551
|
}
|
|
10710
10552
|
if (checkpointState.canUseTurnSession && sessionConversationId && sessionId) {
|
|
10711
10553
|
await persistCompletedCheckpoint({
|
|
@@ -10724,9 +10566,11 @@ async function generateAssistantReply(messageText, context = {}) {
|
|
|
10724
10566
|
toolCalls,
|
|
10725
10567
|
sandboxId: currentSandboxExecutor.getSandboxId(),
|
|
10726
10568
|
sandboxDependencyProfileHash: currentSandboxExecutor.getDependencyProfileHash(),
|
|
10569
|
+
durationMs: Date.now() - replyStartedAtMs,
|
|
10727
10570
|
generatedFileCount: generatedFiles.length,
|
|
10728
10571
|
shouldTrace,
|
|
10729
10572
|
spanContext,
|
|
10573
|
+
usage: turnUsage,
|
|
10730
10574
|
correlation: context.correlation,
|
|
10731
10575
|
assistantUserName: context.assistant?.userName
|
|
10732
10576
|
});
|
|
@@ -10761,7 +10605,7 @@ async function generateAssistantReply(messageText, context = {}) {
|
|
|
10761
10605
|
);
|
|
10762
10606
|
}
|
|
10763
10607
|
}
|
|
10764
|
-
if (error instanceof McpAuthorizationPauseError && timeoutResumeConversationId && timeoutResumeSessionId) {
|
|
10608
|
+
if ((error instanceof McpAuthorizationPauseError || error instanceof PluginAuthorizationPauseError) && timeoutResumeConversationId && timeoutResumeSessionId) {
|
|
10765
10609
|
const nextSliceId = await persistAuthPauseCheckpoint({
|
|
10766
10610
|
conversationId: timeoutResumeConversationId,
|
|
10767
10611
|
sessionId: timeoutResumeSessionId,
|
|
@@ -10779,7 +10623,7 @@ async function generateAssistantReply(messageText, context = {}) {
|
|
|
10779
10623
|
}
|
|
10780
10624
|
});
|
|
10781
10625
|
throw new RetryableTurnError(
|
|
10782
|
-
"mcp_auth_resume",
|
|
10626
|
+
error instanceof PluginAuthorizationPauseError ? "plugin_auth_resume" : "mcp_auth_resume",
|
|
10783
10627
|
`conversation=${timeoutResumeConversationId} session=${timeoutResumeSessionId} slice=${nextSliceId}`,
|
|
10784
10628
|
{
|
|
10785
10629
|
conversationId: timeoutResumeConversationId,
|
|
@@ -10817,6 +10661,7 @@ async function generateAssistantReply(messageText, context = {}) {
|
|
|
10817
10661
|
toolResultCount: 0,
|
|
10818
10662
|
toolErrorCount: 0,
|
|
10819
10663
|
usedPrimaryText: false,
|
|
10664
|
+
durationMs: Date.now() - replyStartedAtMs,
|
|
10820
10665
|
errorMessage: message,
|
|
10821
10666
|
providerError: error
|
|
10822
10667
|
}
|
|
@@ -10837,6 +10682,92 @@ async function generateAssistantReply(messageText, context = {}) {
|
|
|
10837
10682
|
}
|
|
10838
10683
|
}
|
|
10839
10684
|
|
|
10685
|
+
// src/chat/slack/footer.ts
|
|
10686
|
+
function escapeSlackMrkdwn(text) {
|
|
10687
|
+
return text.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">");
|
|
10688
|
+
}
|
|
10689
|
+
function formatSlackTokenCount(value) {
|
|
10690
|
+
return new Intl.NumberFormat("en-US").format(value);
|
|
10691
|
+
}
|
|
10692
|
+
function formatSlackDuration(durationMs) {
|
|
10693
|
+
if (durationMs < 1e3) {
|
|
10694
|
+
return `${durationMs}ms`;
|
|
10695
|
+
}
|
|
10696
|
+
const durationSeconds = durationMs / 1e3;
|
|
10697
|
+
if (durationSeconds < 10) {
|
|
10698
|
+
return `${durationSeconds.toFixed(1).replace(/\.0$/, "")}s`;
|
|
10699
|
+
}
|
|
10700
|
+
return `${Math.round(durationSeconds)}s`;
|
|
10701
|
+
}
|
|
10702
|
+
function resolveTotalTokens(usage) {
|
|
10703
|
+
if (!usage) {
|
|
10704
|
+
return void 0;
|
|
10705
|
+
}
|
|
10706
|
+
const components = [
|
|
10707
|
+
usage.inputTokens,
|
|
10708
|
+
usage.outputTokens,
|
|
10709
|
+
usage.cachedInputTokens,
|
|
10710
|
+
usage.cacheCreationTokens
|
|
10711
|
+
].filter((value) => value !== void 0);
|
|
10712
|
+
if (components.length > 0) {
|
|
10713
|
+
return components.reduce((sum, value) => sum + value, 0);
|
|
10714
|
+
}
|
|
10715
|
+
return usage.totalTokens;
|
|
10716
|
+
}
|
|
10717
|
+
function buildSlackReplyFooter(args) {
|
|
10718
|
+
const items = [];
|
|
10719
|
+
const conversationId = args.conversationId?.trim();
|
|
10720
|
+
if (conversationId) {
|
|
10721
|
+
items.push({
|
|
10722
|
+
label: "ID",
|
|
10723
|
+
value: conversationId
|
|
10724
|
+
});
|
|
10725
|
+
}
|
|
10726
|
+
const totalTokens = resolveTotalTokens(args.usage);
|
|
10727
|
+
if (totalTokens !== void 0) {
|
|
10728
|
+
items.push({
|
|
10729
|
+
label: "Tokens",
|
|
10730
|
+
value: formatSlackTokenCount(totalTokens)
|
|
10731
|
+
});
|
|
10732
|
+
}
|
|
10733
|
+
if (typeof args.durationMs === "number" && Number.isFinite(args.durationMs)) {
|
|
10734
|
+
const durationMs = Math.max(0, Math.floor(args.durationMs));
|
|
10735
|
+
items.push({
|
|
10736
|
+
label: "Time",
|
|
10737
|
+
value: formatSlackDuration(durationMs)
|
|
10738
|
+
});
|
|
10739
|
+
}
|
|
10740
|
+
const traceId = args.traceId?.trim();
|
|
10741
|
+
if (traceId) {
|
|
10742
|
+
items.push({
|
|
10743
|
+
label: "Trace",
|
|
10744
|
+
value: traceId
|
|
10745
|
+
});
|
|
10746
|
+
}
|
|
10747
|
+
return items.length > 0 ? { items } : void 0;
|
|
10748
|
+
}
|
|
10749
|
+
function buildSlackReplyBlocks(text, footer) {
|
|
10750
|
+
if (!text.trim() || !footer?.items.length) {
|
|
10751
|
+
return void 0;
|
|
10752
|
+
}
|
|
10753
|
+
return [
|
|
10754
|
+
{
|
|
10755
|
+
type: "section",
|
|
10756
|
+
text: {
|
|
10757
|
+
type: "mrkdwn",
|
|
10758
|
+
text
|
|
10759
|
+
}
|
|
10760
|
+
},
|
|
10761
|
+
{
|
|
10762
|
+
type: "context",
|
|
10763
|
+
elements: footer.items.map((item) => ({
|
|
10764
|
+
type: "mrkdwn",
|
|
10765
|
+
text: `*${escapeSlackMrkdwn(item.label)}:* ${escapeSlackMrkdwn(item.value)}`
|
|
10766
|
+
}))
|
|
10767
|
+
}
|
|
10768
|
+
];
|
|
10769
|
+
}
|
|
10770
|
+
|
|
10840
10771
|
// src/chat/slack/reply.ts
|
|
10841
10772
|
import { Buffer as Buffer2 } from "buffer";
|
|
10842
10773
|
function isInterruptedVisibleReply(reply) {
|
|
@@ -10892,14 +10823,25 @@ async function normalizeFileUploads(files) {
|
|
|
10892
10823
|
})
|
|
10893
10824
|
);
|
|
10894
10825
|
}
|
|
10895
|
-
|
|
10826
|
+
function findLastTextPostIndex(posts) {
|
|
10827
|
+
for (let index = posts.length - 1; index >= 0; index -= 1) {
|
|
10828
|
+
if (posts[index]?.text.trim().length) {
|
|
10829
|
+
return index;
|
|
10830
|
+
}
|
|
10831
|
+
}
|
|
10832
|
+
return -1;
|
|
10833
|
+
}
|
|
10834
|
+
async function uploadReplyFiles(args) {
|
|
10896
10835
|
try {
|
|
10897
10836
|
await uploadFilesToThread({
|
|
10898
10837
|
channelId: args.channelId,
|
|
10899
10838
|
threadTs: args.threadTs,
|
|
10900
10839
|
files: await normalizeFileUploads(args.files)
|
|
10901
10840
|
});
|
|
10902
|
-
} catch {
|
|
10841
|
+
} catch (error) {
|
|
10842
|
+
if (args.failureMode === "strict") {
|
|
10843
|
+
throw error;
|
|
10844
|
+
}
|
|
10903
10845
|
}
|
|
10904
10846
|
}
|
|
10905
10847
|
function planSlackReplyPosts(args) {
|
|
@@ -10937,19 +10879,44 @@ function planSlackReplyPosts(args) {
|
|
|
10937
10879
|
return posts;
|
|
10938
10880
|
}
|
|
10939
10881
|
async function postSlackApiReplyPosts(args) {
|
|
10940
|
-
|
|
10941
|
-
|
|
10942
|
-
|
|
10943
|
-
|
|
10944
|
-
if (
|
|
10945
|
-
|
|
10882
|
+
const lastTextPostIndex = findLastTextPostIndex(args.posts);
|
|
10883
|
+
let lastPostedMessageTs;
|
|
10884
|
+
for (const [index, post] of args.posts.entries()) {
|
|
10885
|
+
const hasVisibleDelivery = post.text.trim().length > 0 || post.files?.length;
|
|
10886
|
+
if (hasVisibleDelivery) {
|
|
10887
|
+
await args.beforePost?.();
|
|
10888
|
+
}
|
|
10889
|
+
let messageTs;
|
|
10890
|
+
try {
|
|
10891
|
+
if (post.text.trim().length > 0) {
|
|
10892
|
+
const response = await postSlackMessage({
|
|
10893
|
+
channelId: args.channelId,
|
|
10894
|
+
threadTs: args.threadTs,
|
|
10895
|
+
text: post.text,
|
|
10896
|
+
...index === lastTextPostIndex && args.footer ? { blocks: buildSlackReplyBlocks(post.text, args.footer) } : {}
|
|
10897
|
+
});
|
|
10898
|
+
messageTs = response.ts;
|
|
10899
|
+
lastPostedMessageTs = response.ts;
|
|
10900
|
+
}
|
|
10901
|
+
if (!post.files?.length) {
|
|
10902
|
+
continue;
|
|
10903
|
+
}
|
|
10904
|
+
await uploadReplyFiles({
|
|
10905
|
+
channelId: args.channelId,
|
|
10906
|
+
failureMode: args.fileUploadFailureMode ?? "best_effort",
|
|
10907
|
+
threadTs: args.threadTs,
|
|
10908
|
+
files: post.files
|
|
10909
|
+
});
|
|
10910
|
+
} catch (error) {
|
|
10911
|
+
await args.onPostError?.({
|
|
10912
|
+
error,
|
|
10913
|
+
messageTs,
|
|
10914
|
+
stage: post.stage
|
|
10915
|
+
});
|
|
10916
|
+
throw error;
|
|
10946
10917
|
}
|
|
10947
|
-
await uploadReplyFilesBestEffort({
|
|
10948
|
-
channelId: args.channelId,
|
|
10949
|
-
threadTs: args.threadTs,
|
|
10950
|
-
files: post.files
|
|
10951
|
-
});
|
|
10952
10918
|
}
|
|
10919
|
+
return lastPostedMessageTs;
|
|
10953
10920
|
}
|
|
10954
10921
|
|
|
10955
10922
|
// src/chat/slack/resume.ts
|
|
@@ -10964,16 +10931,13 @@ function resolveReplyTimeoutMs(explicitTimeoutMs) {
|
|
|
10964
10931
|
const parsed = Number.parseInt(raw, 10);
|
|
10965
10932
|
return Number.isFinite(parsed) && parsed > 0 ? parsed : void 0;
|
|
10966
10933
|
}
|
|
10967
|
-
async function postSlackMessage2(channelId, threadTs, text) {
|
|
10968
|
-
await postSlackMessage({
|
|
10969
|
-
channelId,
|
|
10970
|
-
threadTs,
|
|
10971
|
-
text
|
|
10972
|
-
});
|
|
10973
|
-
}
|
|
10974
10934
|
async function postSlackMessageBestEffort(channelId, threadTs, text) {
|
|
10975
10935
|
try {
|
|
10976
|
-
await
|
|
10936
|
+
await postSlackMessage({
|
|
10937
|
+
channelId,
|
|
10938
|
+
threadTs,
|
|
10939
|
+
text
|
|
10940
|
+
});
|
|
10977
10941
|
} catch {
|
|
10978
10942
|
}
|
|
10979
10943
|
}
|
|
@@ -11097,16 +11061,23 @@ async function resumeSlackTurn(args) {
|
|
|
11097
11061
|
)
|
|
11098
11062
|
]) : await replyPromise;
|
|
11099
11063
|
await status.stop();
|
|
11064
|
+
const footer = buildSlackReplyFooter({
|
|
11065
|
+
conversationId: args.replyContext?.correlation?.conversationId ?? lockKey,
|
|
11066
|
+
durationMs: reply.diagnostics.durationMs,
|
|
11067
|
+
traceId: getActiveTraceId(),
|
|
11068
|
+
usage: reply.diagnostics.usage
|
|
11069
|
+
});
|
|
11100
11070
|
await postSlackApiReplyPosts({
|
|
11101
11071
|
channelId: args.channelId,
|
|
11102
11072
|
threadTs: args.threadTs,
|
|
11103
11073
|
posts: planSlackReplyPosts({ reply }),
|
|
11104
|
-
|
|
11074
|
+
fileUploadFailureMode: "best_effort",
|
|
11075
|
+
footer
|
|
11105
11076
|
});
|
|
11106
11077
|
await args.onSuccess?.(reply);
|
|
11107
11078
|
} catch (error) {
|
|
11108
11079
|
await status.stop();
|
|
11109
|
-
if (isRetryableTurnError(error, "mcp_auth_resume") && args.onAuthPause) {
|
|
11080
|
+
if ((isRetryableTurnError(error, "mcp_auth_resume") || isRetryableTurnError(error, "plugin_auth_resume")) && args.onAuthPause) {
|
|
11110
11081
|
deferredPauseHandler = async () => {
|
|
11111
11082
|
await args.onAuthPause?.(error);
|
|
11112
11083
|
};
|
|
@@ -11721,6 +11692,186 @@ async function buildResumeConversationContext2(channelId, threadTs) {
|
|
|
11721
11692
|
excludeMessageId: latestUserMessageId
|
|
11722
11693
|
});
|
|
11723
11694
|
}
|
|
11695
|
+
async function buildCheckpointConversationContext(conversationId, sessionId) {
|
|
11696
|
+
const conversation = coerceThreadConversationState(
|
|
11697
|
+
await getPersistedThreadState(conversationId)
|
|
11698
|
+
);
|
|
11699
|
+
const userMessage = getTurnUserMessage(conversation, sessionId);
|
|
11700
|
+
return buildConversationContext(conversation, {
|
|
11701
|
+
excludeMessageId: userMessage?.id
|
|
11702
|
+
});
|
|
11703
|
+
}
|
|
11704
|
+
async function persistCompletedOAuthReplyState(args) {
|
|
11705
|
+
const currentState = await getPersistedThreadState(args.conversationId);
|
|
11706
|
+
const conversation = coerceThreadConversationState(currentState);
|
|
11707
|
+
const artifacts = coerceThreadArtifactsState(currentState);
|
|
11708
|
+
const nextArtifacts = args.reply.artifactStatePatch ? mergeArtifactsState(artifacts, args.reply.artifactStatePatch) : void 0;
|
|
11709
|
+
const userMessage = getTurnUserMessage(conversation, args.sessionId);
|
|
11710
|
+
markConversationMessage(conversation, userMessage?.id, {
|
|
11711
|
+
replied: true,
|
|
11712
|
+
skippedReason: void 0
|
|
11713
|
+
});
|
|
11714
|
+
upsertConversationMessage(conversation, {
|
|
11715
|
+
id: generateConversationId("assistant"),
|
|
11716
|
+
role: "assistant",
|
|
11717
|
+
text: normalizeConversationText(args.reply.text) || "[empty response]",
|
|
11718
|
+
createdAtMs: Date.now(),
|
|
11719
|
+
author: {
|
|
11720
|
+
userName: botConfig.userName,
|
|
11721
|
+
isBot: true
|
|
11722
|
+
},
|
|
11723
|
+
meta: {
|
|
11724
|
+
replied: true
|
|
11725
|
+
}
|
|
11726
|
+
});
|
|
11727
|
+
markTurnCompleted({
|
|
11728
|
+
conversation,
|
|
11729
|
+
nowMs: Date.now(),
|
|
11730
|
+
updateConversationStats
|
|
11731
|
+
});
|
|
11732
|
+
await persistThreadStateById(args.conversationId, {
|
|
11733
|
+
artifacts: nextArtifacts,
|
|
11734
|
+
conversation,
|
|
11735
|
+
sandboxId: args.reply.sandboxId,
|
|
11736
|
+
sandboxDependencyProfileHash: args.reply.sandboxDependencyProfileHash
|
|
11737
|
+
});
|
|
11738
|
+
}
|
|
11739
|
+
async function persistFailedOAuthReplyState(args) {
|
|
11740
|
+
const currentState = await getPersistedThreadState(args.conversationId);
|
|
11741
|
+
const conversation = coerceThreadConversationState(currentState);
|
|
11742
|
+
markTurnFailed({
|
|
11743
|
+
conversation,
|
|
11744
|
+
nowMs: Date.now(),
|
|
11745
|
+
userMessageId: getTurnUserMessage(conversation, args.sessionId)?.id,
|
|
11746
|
+
markConversationMessage,
|
|
11747
|
+
updateConversationStats
|
|
11748
|
+
});
|
|
11749
|
+
await persistThreadStateById(args.conversationId, {
|
|
11750
|
+
conversation
|
|
11751
|
+
});
|
|
11752
|
+
}
|
|
11753
|
+
async function resumeCheckpointedOAuthTurn(stored) {
|
|
11754
|
+
if (!stored.resumeConversationId || !stored.resumeSessionId || !stored.channelId || !stored.threadTs) {
|
|
11755
|
+
return false;
|
|
11756
|
+
}
|
|
11757
|
+
const checkpoint = await getAgentTurnSessionCheckpoint(
|
|
11758
|
+
stored.resumeConversationId,
|
|
11759
|
+
stored.resumeSessionId
|
|
11760
|
+
);
|
|
11761
|
+
if (!checkpoint || checkpoint.state !== "awaiting_resume" || checkpoint.resumeReason !== "auth") {
|
|
11762
|
+
return false;
|
|
11763
|
+
}
|
|
11764
|
+
const currentState = await getPersistedThreadState(
|
|
11765
|
+
stored.resumeConversationId
|
|
11766
|
+
);
|
|
11767
|
+
const conversation = coerceThreadConversationState(currentState);
|
|
11768
|
+
const artifacts = coerceThreadArtifactsState(currentState);
|
|
11769
|
+
const userMessage = getTurnUserMessage(conversation, stored.resumeSessionId);
|
|
11770
|
+
if (!userMessage?.author?.userId) {
|
|
11771
|
+
return false;
|
|
11772
|
+
}
|
|
11773
|
+
if (conversation.processing.activeTurnId !== stored.resumeSessionId) {
|
|
11774
|
+
return true;
|
|
11775
|
+
}
|
|
11776
|
+
const conversationContext = await buildCheckpointConversationContext(
|
|
11777
|
+
stored.resumeConversationId,
|
|
11778
|
+
stored.resumeSessionId
|
|
11779
|
+
);
|
|
11780
|
+
const channelConfiguration = getChannelConfigurationServiceById(
|
|
11781
|
+
stored.channelId
|
|
11782
|
+
);
|
|
11783
|
+
const providerLabel = formatProviderLabel(stored.provider);
|
|
11784
|
+
await resumeSlackTurn({
|
|
11785
|
+
messageText: stored.pendingMessage ?? userMessage.text,
|
|
11786
|
+
channelId: stored.channelId,
|
|
11787
|
+
threadTs: stored.threadTs,
|
|
11788
|
+
lockKey: stored.resumeConversationId,
|
|
11789
|
+
initialText: `Your ${providerLabel} account is now connected. Processing your request...`,
|
|
11790
|
+
failureText: "I connected your account but hit an error processing your request. Please try the command again.",
|
|
11791
|
+
replyContext: {
|
|
11792
|
+
assistant: { userName: botConfig.userName },
|
|
11793
|
+
requester: {
|
|
11794
|
+
userId: userMessage.author.userId,
|
|
11795
|
+
userName: userMessage.author.userName,
|
|
11796
|
+
fullName: userMessage.author.fullName
|
|
11797
|
+
},
|
|
11798
|
+
correlation: {
|
|
11799
|
+
channelId: stored.channelId,
|
|
11800
|
+
threadTs: stored.threadTs,
|
|
11801
|
+
requesterId: userMessage.author.userId
|
|
11802
|
+
},
|
|
11803
|
+
toolChannelId: artifacts.assistantContextChannelId ?? stored.channelId,
|
|
11804
|
+
artifactState: artifacts,
|
|
11805
|
+
conversationContext,
|
|
11806
|
+
channelConfiguration,
|
|
11807
|
+
sandbox: getPersistedSandboxState(currentState),
|
|
11808
|
+
threadParticipants: buildThreadParticipants(conversation.messages),
|
|
11809
|
+
...getTurnUserReplyAttachmentContext(userMessage)
|
|
11810
|
+
},
|
|
11811
|
+
onSuccess: async (reply) => {
|
|
11812
|
+
logInfo(
|
|
11813
|
+
"oauth_callback_resume_complete",
|
|
11814
|
+
{},
|
|
11815
|
+
{
|
|
11816
|
+
"app.credential.provider": stored.provider,
|
|
11817
|
+
"app.ai.outcome": reply.diagnostics.outcome,
|
|
11818
|
+
"app.ai.tool_calls": reply.diagnostics.toolCalls.length
|
|
11819
|
+
},
|
|
11820
|
+
"Auto-resumed checkpointed turn after OAuth callback"
|
|
11821
|
+
);
|
|
11822
|
+
await persistCompletedOAuthReplyState({
|
|
11823
|
+
conversationId: stored.resumeConversationId,
|
|
11824
|
+
sessionId: stored.resumeSessionId,
|
|
11825
|
+
reply
|
|
11826
|
+
});
|
|
11827
|
+
},
|
|
11828
|
+
onFailure: async (error) => {
|
|
11829
|
+
logException(
|
|
11830
|
+
error,
|
|
11831
|
+
"oauth_callback_resume_failed",
|
|
11832
|
+
{},
|
|
11833
|
+
{ "app.credential.provider": stored.provider },
|
|
11834
|
+
"Failed to auto-resume checkpointed turn after OAuth callback"
|
|
11835
|
+
);
|
|
11836
|
+
await persistFailedOAuthReplyState({
|
|
11837
|
+
conversationId: stored.resumeConversationId,
|
|
11838
|
+
sessionId: stored.resumeSessionId
|
|
11839
|
+
});
|
|
11840
|
+
},
|
|
11841
|
+
onAuthPause: async (error) => {
|
|
11842
|
+
logException(
|
|
11843
|
+
error,
|
|
11844
|
+
"oauth_callback_resume_reparked_for_auth",
|
|
11845
|
+
{},
|
|
11846
|
+
{ "app.credential.provider": stored.provider },
|
|
11847
|
+
"Resumed OAuth turn requested another authorization flow"
|
|
11848
|
+
);
|
|
11849
|
+
},
|
|
11850
|
+
onTimeoutPause: async (error) => {
|
|
11851
|
+
if (!isRetryableTurnError(error, "turn_timeout_resume")) {
|
|
11852
|
+
throw error;
|
|
11853
|
+
}
|
|
11854
|
+
const checkpointVersion = error.metadata?.checkpointVersion;
|
|
11855
|
+
const nextSliceId = error.metadata?.sliceId;
|
|
11856
|
+
if (typeof checkpointVersion !== "number") {
|
|
11857
|
+
throw new Error(
|
|
11858
|
+
"Timed-out OAuth resume did not include a checkpoint version"
|
|
11859
|
+
);
|
|
11860
|
+
}
|
|
11861
|
+
if (!canScheduleTurnTimeoutResume(nextSliceId)) {
|
|
11862
|
+
throw new Error(
|
|
11863
|
+
"Timed-out turn exceeded the automatic resume slice limit"
|
|
11864
|
+
);
|
|
11865
|
+
}
|
|
11866
|
+
await scheduleTurnTimeoutResume({
|
|
11867
|
+
conversationId: stored.resumeConversationId,
|
|
11868
|
+
sessionId: stored.resumeSessionId,
|
|
11869
|
+
expectedCheckpointVersion: checkpointVersion
|
|
11870
|
+
});
|
|
11871
|
+
}
|
|
11872
|
+
});
|
|
11873
|
+
return true;
|
|
11874
|
+
}
|
|
11724
11875
|
async function resumePendingOAuthMessage(stored) {
|
|
11725
11876
|
if (!stored.pendingMessage || !stored.channelId || !stored.threadTs) return;
|
|
11726
11877
|
const providerLabel = formatProviderLabel(stored.provider);
|
|
@@ -11899,15 +12050,27 @@ async function GET5(request, provider, waitUntil) {
|
|
|
11899
12050
|
}
|
|
11900
12051
|
});
|
|
11901
12052
|
if (stored.pendingMessage && stored.channelId && stored.threadTs) {
|
|
11902
|
-
waitUntil(() =>
|
|
12053
|
+
waitUntil(async () => {
|
|
12054
|
+
try {
|
|
12055
|
+
const resumed = await resumeCheckpointedOAuthTurn(stored);
|
|
12056
|
+
if (!resumed) {
|
|
12057
|
+
await resumePendingOAuthMessage(stored);
|
|
12058
|
+
}
|
|
12059
|
+
} catch (error) {
|
|
12060
|
+
if (error instanceof ResumeTurnBusyError) {
|
|
12061
|
+
return;
|
|
12062
|
+
}
|
|
12063
|
+
throw error;
|
|
12064
|
+
}
|
|
12065
|
+
});
|
|
11903
12066
|
} else if (stored.channelId && stored.threadTs) {
|
|
11904
12067
|
const { channelId, threadTs } = stored;
|
|
11905
12068
|
waitUntil(
|
|
11906
|
-
() =>
|
|
12069
|
+
() => postSlackMessage({
|
|
11907
12070
|
channelId,
|
|
11908
12071
|
threadTs,
|
|
11909
|
-
`Your ${providerLabel} account is now connected. You can start using ${providerLabel} commands.`
|
|
11910
|
-
)
|
|
12072
|
+
text: `Your ${providerLabel} account is now connected. You can start using ${providerLabel} commands.`
|
|
12073
|
+
})
|
|
11911
12074
|
);
|
|
11912
12075
|
}
|
|
11913
12076
|
const statusMessage = stored.pendingMessage ? "Your request is being processed in Slack." : "You can close this tab and return to Slack.";
|
|
@@ -12223,11 +12386,11 @@ var DIRECTED_FOLLOW_UP_CUE_RE = /\b(?:you said|you just said|your last response|
|
|
|
12223
12386
|
var TERSE_CLARIFICATION_RE = /^(?:which one|which ones|why|how so|what do you mean|what did you mean|say more|explain that|clarify that|expand on that|elaborate on that)\??$/i;
|
|
12224
12387
|
var GENERIC_IMMEDIATE_SIDE_CONVERSATION_RE = /^(?:is that (?:the )?right (?:approach|call|move)|(?:can|could|would) you check on this)\??$/i;
|
|
12225
12388
|
var RECENT_THREAD_WINDOW = 6;
|
|
12226
|
-
function
|
|
12389
|
+
function escapeRegExp(value) {
|
|
12227
12390
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
12228
12391
|
}
|
|
12229
12392
|
function containsAssistantInvocation(text, botUserName) {
|
|
12230
|
-
const escapedUserName =
|
|
12393
|
+
const escapedUserName = escapeRegExp(botUserName);
|
|
12231
12394
|
const plainNameMentionRe = new RegExp(`(^|\\s)@${escapedUserName}\\b`, "i");
|
|
12232
12395
|
const labeledEntityMentionRe = new RegExp(
|
|
12233
12396
|
`<@[^>|]+\\|${escapedUserName}>`,
|
|
@@ -12446,6 +12609,13 @@ async function decideSubscribedThreadReply(args) {
|
|
|
12446
12609
|
reason: "explicit_mention" /* ExplicitMention */
|
|
12447
12610
|
};
|
|
12448
12611
|
}
|
|
12612
|
+
if (signals.assistantWasLastSpeaker && signals.humanMessagesSinceLastAssistant === 0 && !signals.currentMessageHasAttachments && (signals.currentMessageHasDirectedFollowUpCue || signals.currentMessageIsTerseClarification)) {
|
|
12613
|
+
return {
|
|
12614
|
+
shouldReply: true,
|
|
12615
|
+
reason: "directed_follow_up" /* DirectedFollowUp */,
|
|
12616
|
+
reasonDetail: signals.currentMessageIsTerseClarification ? "immediate terse clarification" : "immediate directed follow-up cue"
|
|
12617
|
+
};
|
|
12618
|
+
}
|
|
12449
12619
|
if (signals.assistantWasLastSpeaker && signals.humanMessagesSinceLastAssistant === 0 && !signals.currentMessageHasAttachments && !signals.currentMessageHasDirectedFollowUpCue && !signals.currentMessageIsTerseClarification && isGenericImmediateSideConversation(text)) {
|
|
12450
12620
|
return {
|
|
12451
12621
|
shouldReply: false,
|
|
@@ -12516,7 +12686,7 @@ async function decideSubscribedThreadReply(args) {
|
|
|
12516
12686
|
}
|
|
12517
12687
|
|
|
12518
12688
|
// src/chat/runtime/thread-context.ts
|
|
12519
|
-
function
|
|
12689
|
+
function escapeRegExp2(value) {
|
|
12520
12690
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
12521
12691
|
}
|
|
12522
12692
|
function stripLeadingBotMention(text, options = {}) {
|
|
@@ -12526,12 +12696,12 @@ function stripLeadingBotMention(text, options = {}) {
|
|
|
12526
12696
|
next = next.replace(/^\s*<@[^>]+>[\s,:-]*/, "").trim();
|
|
12527
12697
|
}
|
|
12528
12698
|
const mentionByNameRe = new RegExp(
|
|
12529
|
-
`^\\s*@${
|
|
12699
|
+
`^\\s*@${escapeRegExp2(botConfig.userName)}\\b[\\s,:-]*`,
|
|
12530
12700
|
"i"
|
|
12531
12701
|
);
|
|
12532
12702
|
next = next.replace(mentionByNameRe, "").trim();
|
|
12533
12703
|
const mentionByLabeledEntityRe = new RegExp(
|
|
12534
|
-
`^\\s*<@[^>|]+\\|${
|
|
12704
|
+
`^\\s*<@[^>|]+\\|${escapeRegExp2(botConfig.userName)}>[\\s,:-]*`,
|
|
12535
12705
|
"i"
|
|
12536
12706
|
);
|
|
12537
12707
|
next = next.replace(mentionByLabeledEntityRe, "").trim();
|
|
@@ -12553,11 +12723,20 @@ function getAssistantThreadContext(message) {
|
|
|
12553
12723
|
const raw = message.raw;
|
|
12554
12724
|
const rawRecord = raw && typeof raw === "object" ? raw : void 0;
|
|
12555
12725
|
const channelId = toOptionalString(rawRecord?.channel);
|
|
12556
|
-
|
|
12557
|
-
|
|
12726
|
+
if (channelId) {
|
|
12727
|
+
const rawThreadTs = toOptionalString(rawRecord?.thread_ts);
|
|
12728
|
+
const threadTs = isDmChannel(channelId) ? rawThreadTs : rawThreadTs ?? toOptionalString(rawRecord?.ts);
|
|
12729
|
+
if (threadTs) {
|
|
12730
|
+
return { channelId, threadTs };
|
|
12731
|
+
}
|
|
12732
|
+
}
|
|
12733
|
+
const parsedThreadId = parseSlackThreadId(
|
|
12734
|
+
toOptionalString(message.threadId)
|
|
12735
|
+
);
|
|
12736
|
+
if (!parsedThreadId || isDmChannel(parsedThreadId.channelId)) {
|
|
12558
12737
|
return void 0;
|
|
12559
12738
|
}
|
|
12560
|
-
return
|
|
12739
|
+
return parsedThreadId;
|
|
12561
12740
|
}
|
|
12562
12741
|
function getMessageTs(message) {
|
|
12563
12742
|
const directTs = toOptionalString(
|
|
@@ -12724,13 +12903,13 @@ function createSlackTurnRuntime(deps) {
|
|
|
12724
12903
|
channelId: deps.getChannelId(thread, message),
|
|
12725
12904
|
runId: deps.getRunId(thread, message)
|
|
12726
12905
|
});
|
|
12727
|
-
if (isRetryableTurnError(error, "mcp_auth_resume")) {
|
|
12906
|
+
if (isRetryableTurnError(error, "mcp_auth_resume") || isRetryableTurnError(error, "plugin_auth_resume")) {
|
|
12728
12907
|
deps.logException(
|
|
12729
12908
|
error,
|
|
12730
12909
|
"mention_handler_auth_pause",
|
|
12731
12910
|
errorContext,
|
|
12732
12911
|
{ "app.turn.retryable_reason": error.reason },
|
|
12733
|
-
"onNewMention parked turn for
|
|
12912
|
+
"onNewMention parked turn for auth resume"
|
|
12734
12913
|
);
|
|
12735
12914
|
return;
|
|
12736
12915
|
}
|
|
@@ -12856,13 +13035,13 @@ function createSlackTurnRuntime(deps) {
|
|
|
12856
13035
|
channelId: deps.getChannelId(thread, message),
|
|
12857
13036
|
runId: deps.getRunId(thread, message)
|
|
12858
13037
|
});
|
|
12859
|
-
if (isRetryableTurnError(error, "mcp_auth_resume")) {
|
|
13038
|
+
if (isRetryableTurnError(error, "mcp_auth_resume") || isRetryableTurnError(error, "plugin_auth_resume")) {
|
|
12860
13039
|
deps.logException(
|
|
12861
13040
|
error,
|
|
12862
13041
|
"subscribed_message_handler_auth_pause",
|
|
12863
13042
|
errorContext,
|
|
12864
13043
|
{ "app.turn.retryable_reason": error.reason },
|
|
12865
|
-
"onSubscribedMessage parked turn for
|
|
13044
|
+
"onSubscribedMessage parked turn for auth resume"
|
|
12866
13045
|
);
|
|
12867
13046
|
return;
|
|
12868
13047
|
}
|
|
@@ -13948,13 +14127,63 @@ function createReplyToThread(deps) {
|
|
|
13948
14127
|
"slackMessageAddReaction"
|
|
13949
14128
|
);
|
|
13950
14129
|
const plannedPosts = planSlackReplyPosts({ reply });
|
|
14130
|
+
const replyFooter = buildSlackReplyFooter({
|
|
14131
|
+
conversationId,
|
|
14132
|
+
durationMs: reply.diagnostics.durationMs,
|
|
14133
|
+
traceId: getActiveTraceId(),
|
|
14134
|
+
usage: reply.diagnostics.usage
|
|
14135
|
+
});
|
|
14136
|
+
const shouldUseSlackFooter = Boolean(replyFooter) && Boolean(channelId && threadTs) && thread.adapter?.name === "slack";
|
|
13951
14137
|
if (plannedPosts.length > 0) {
|
|
13952
14138
|
let sent;
|
|
13953
|
-
|
|
13954
|
-
|
|
13955
|
-
|
|
13956
|
-
|
|
13957
|
-
|
|
14139
|
+
if (shouldUseSlackFooter) {
|
|
14140
|
+
const slackChannelId = channelId;
|
|
14141
|
+
const slackThreadTs = threadTs;
|
|
14142
|
+
if (!slackChannelId || !slackThreadTs) {
|
|
14143
|
+
throw new Error(
|
|
14144
|
+
"Slack footer delivery requires a concrete channel and thread timestamp"
|
|
14145
|
+
);
|
|
14146
|
+
}
|
|
14147
|
+
const sentMessageTs = await postSlackApiReplyPosts({
|
|
14148
|
+
beforePost: beforeFirstResponsePost,
|
|
14149
|
+
channelId: slackChannelId,
|
|
14150
|
+
threadTs: slackThreadTs,
|
|
14151
|
+
posts: plannedPosts,
|
|
14152
|
+
fileUploadFailureMode: "strict",
|
|
14153
|
+
footer: replyFooter,
|
|
14154
|
+
onPostError: ({ error, messageTs: messageTs2, stage }) => {
|
|
14155
|
+
logException(
|
|
14156
|
+
error,
|
|
14157
|
+
"slack_thread_post_failed",
|
|
14158
|
+
turnTraceContext,
|
|
14159
|
+
{
|
|
14160
|
+
"app.slack.reply_stage": stage,
|
|
14161
|
+
...messageTs2 ? { "messaging.message.id": messageTs2 } : {},
|
|
14162
|
+
...getSlackErrorObservabilityAttributes(error)
|
|
14163
|
+
},
|
|
14164
|
+
"Failed to post Slack thread reply"
|
|
14165
|
+
);
|
|
14166
|
+
}
|
|
14167
|
+
});
|
|
14168
|
+
if (sentMessageTs) {
|
|
14169
|
+
sent = {
|
|
14170
|
+
id: sentMessageTs,
|
|
14171
|
+
text: reply.text,
|
|
14172
|
+
delete: async () => {
|
|
14173
|
+
await deleteSlackMessage({
|
|
14174
|
+
channelId: slackChannelId,
|
|
14175
|
+
timestamp: sentMessageTs
|
|
14176
|
+
});
|
|
14177
|
+
}
|
|
14178
|
+
};
|
|
14179
|
+
}
|
|
14180
|
+
} else {
|
|
14181
|
+
for (const post of plannedPosts) {
|
|
14182
|
+
sent = await postThreadReply(
|
|
14183
|
+
buildSlackOutputMessage(post.text, post.files),
|
|
14184
|
+
post.stage
|
|
14185
|
+
);
|
|
14186
|
+
}
|
|
13958
14187
|
}
|
|
13959
14188
|
const firstPlannedMessageHasFiles = (plannedPosts[0]?.files?.length ?? 0) > 0;
|
|
13960
14189
|
if (sent && reactionPerformed && plannedPosts.length === 1 && !firstPlannedMessageHasFiles && isRedundantReactionAckText(reply.text)) {
|
|
@@ -13993,7 +14222,7 @@ function createReplyToThread(deps) {
|
|
|
13993
14222
|
);
|
|
13994
14223
|
}
|
|
13995
14224
|
} catch (error) {
|
|
13996
|
-
if (isRetryableTurnError(error, "mcp_auth_resume")) {
|
|
14225
|
+
if (isRetryableTurnError(error, "mcp_auth_resume") || isRetryableTurnError(error, "plugin_auth_resume")) {
|
|
13997
14226
|
shouldPersistFailureState = false;
|
|
13998
14227
|
throw error;
|
|
13999
14228
|
}
|