@sentry/junior 0.3.0 → 0.4.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/README.md +5 -1
- package/dist/{bot-JSIREVQD.js → bot-Y6A47LEZ.js} +3 -3
- package/dist/{chunk-4RFOJSJL.js → chunk-DGKNXMK4.js} +2 -2
- package/dist/{chunk-DPTR2FNH.js → chunk-OZFXD5IG.js} +495 -130
- package/dist/{chunk-L745IWNK.js → chunk-RFUE5VBK.js} +854 -346
- package/dist/{chunk-RTQMRGZD.js → chunk-TEQ3UIS7.js} +1 -1
- package/dist/{chunk-SP6LV35L.js → chunk-Z5E25LRN.js} +173 -56
- package/dist/cli/snapshot-warmup.js +2 -2
- package/dist/handlers/queue-callback.js +4 -4
- package/dist/handlers/router.js +90 -40
- package/dist/handlers/webhooks.js +1 -1
- package/dist/next-config.js +33 -27
- package/package.json +1 -1
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
getPluginOAuthConfig,
|
|
11
11
|
getPluginProviders,
|
|
12
12
|
getPluginSkillRoots,
|
|
13
|
+
getRuntimeDependencyProfileHash,
|
|
13
14
|
getSlackBotToken,
|
|
14
15
|
getSlackClientId,
|
|
15
16
|
getSlackClientSecret,
|
|
@@ -25,7 +26,7 @@ import {
|
|
|
25
26
|
skillRoots,
|
|
26
27
|
soulPathCandidates,
|
|
27
28
|
upsertAgentTurnSessionCheckpoint
|
|
28
|
-
} from "./chunk-
|
|
29
|
+
} from "./chunk-OZFXD5IG.js";
|
|
29
30
|
import {
|
|
30
31
|
logError,
|
|
31
32
|
logException,
|
|
@@ -656,22 +657,34 @@ function normalizeForSlack(text) {
|
|
|
656
657
|
normalized = ensureBlockSpacing(normalized);
|
|
657
658
|
return normalized.replace(/\n{3,}/g, "\n\n").trim();
|
|
658
659
|
}
|
|
659
|
-
function buildSlackOutputMessage(text,
|
|
660
|
+
function buildSlackOutputMessage(text, files) {
|
|
660
661
|
const normalized = normalizeForSlack(text);
|
|
662
|
+
const fileCount = files?.length ?? 0;
|
|
661
663
|
if (!normalized) {
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
664
|
+
if (fileCount > 0) {
|
|
665
|
+
return {
|
|
666
|
+
raw: "",
|
|
667
|
+
files
|
|
668
|
+
};
|
|
669
|
+
}
|
|
670
|
+
logWarn(
|
|
671
|
+
"slack_output_normalized_empty",
|
|
672
|
+
{},
|
|
673
|
+
{
|
|
674
|
+
"app.output.original_length": text.length,
|
|
675
|
+
"app.output.parsed_length": normalized.length,
|
|
676
|
+
"app.output.file_count": fileCount
|
|
677
|
+
},
|
|
678
|
+
"Slack output normalized to empty content"
|
|
679
|
+
);
|
|
667
680
|
return {
|
|
668
681
|
markdown: "I couldn't produce a response.",
|
|
669
|
-
files
|
|
682
|
+
files
|
|
670
683
|
};
|
|
671
684
|
}
|
|
672
685
|
return {
|
|
673
686
|
markdown: normalized,
|
|
674
|
-
files
|
|
687
|
+
files
|
|
675
688
|
};
|
|
676
689
|
}
|
|
677
690
|
var slackOutputPolicy = {
|
|
@@ -759,10 +772,14 @@ function formatAvailableSkillsForPrompt(skills) {
|
|
|
759
772
|
const skillLocation = `${workspaceSkillDir(skill.name)}/SKILL.md`;
|
|
760
773
|
lines.push(" <skill>");
|
|
761
774
|
lines.push(` <name>${escapeXml(skill.name)}</name>`);
|
|
762
|
-
lines.push(
|
|
775
|
+
lines.push(
|
|
776
|
+
` <description>${escapeXml(skill.description)}</description>`
|
|
777
|
+
);
|
|
763
778
|
lines.push(` <location>${escapeXml(skillLocation)}</location>`);
|
|
764
779
|
if (skill.usesConfig && skill.usesConfig.length > 0) {
|
|
765
|
-
lines.push(
|
|
780
|
+
lines.push(
|
|
781
|
+
` <uses_config>${escapeXml(skill.usesConfig.join(" "))}</uses_config>`
|
|
782
|
+
);
|
|
766
783
|
}
|
|
767
784
|
lines.push(" </skill>");
|
|
768
785
|
}
|
|
@@ -776,10 +793,14 @@ function formatLoadedSkillsForPrompt(skills) {
|
|
|
776
793
|
const lines = ["<loaded_skills>"];
|
|
777
794
|
for (const skill of skills) {
|
|
778
795
|
const skillDir = workspaceSkillDir(skill.name);
|
|
779
|
-
lines.push(
|
|
796
|
+
lines.push(
|
|
797
|
+
` <skill name="${escapeXml(skill.name)}" location="${escapeXml(`${skillDir}/SKILL.md`)}">`
|
|
798
|
+
);
|
|
780
799
|
lines.push(`References are relative to ${escapeXml(skillDir)}.`);
|
|
781
800
|
if (skill.usesConfig && skill.usesConfig.length > 0) {
|
|
782
|
-
lines.push(
|
|
801
|
+
lines.push(
|
|
802
|
+
`Uses config keys: ${escapeXml(skill.usesConfig.join(", "))}.`
|
|
803
|
+
);
|
|
783
804
|
}
|
|
784
805
|
lines.push("");
|
|
785
806
|
lines.push(skill.body);
|
|
@@ -859,12 +880,20 @@ function buildSystemPrompt(params) {
|
|
|
859
880
|
"Loaded skills for this turn:",
|
|
860
881
|
formatLoadedSkillsForPrompt(activeSkills)
|
|
861
882
|
].join("\n");
|
|
862
|
-
const configurationKeys = Object.keys(configuration ?? {}).sort(
|
|
883
|
+
const configurationKeys = Object.keys(configuration ?? {}).sort(
|
|
884
|
+
(a, b) => a.localeCompare(b)
|
|
885
|
+
);
|
|
863
886
|
const relevantConfigSet = new Set(
|
|
864
|
-
(relevantConfigurationKeys ?? []).filter(
|
|
887
|
+
(relevantConfigurationKeys ?? []).filter(
|
|
888
|
+
(key) => Object.prototype.hasOwnProperty.call(configuration ?? {}, key)
|
|
889
|
+
)
|
|
890
|
+
);
|
|
891
|
+
const relevantConfigLines = configurationKeys.filter((key) => relevantConfigSet.has(key)).map(
|
|
892
|
+
(key) => ` - ${escapeXml(key)}: ${formatConfigurationValue(configuration?.[key])}`
|
|
893
|
+
);
|
|
894
|
+
const otherConfigLines = configurationKeys.filter((key) => !relevantConfigSet.has(key)).map(
|
|
895
|
+
(key) => ` - ${escapeXml(key)}: ${formatConfigurationValue(configuration?.[key])}`
|
|
865
896
|
);
|
|
866
|
-
const relevantConfigLines = configurationKeys.filter((key) => relevantConfigSet.has(key)).map((key) => ` - ${escapeXml(key)}: ${formatConfigurationValue(configuration?.[key])}`);
|
|
867
|
-
const otherConfigLines = configurationKeys.filter((key) => !relevantConfigSet.has(key)).map((key) => ` - ${escapeXml(key)}: ${formatConfigurationValue(configuration?.[key])}`);
|
|
868
897
|
const configurationSection = [
|
|
869
898
|
"Use these conversation-scoped defaults when the user has not provided explicit values in this turn.",
|
|
870
899
|
"If explicit user input conflicts with configuration, follow explicit user input.",
|
|
@@ -936,6 +965,10 @@ function buildSystemPrompt(params) {
|
|
|
936
965
|
"- For factual or external questions, run tools/skills first, then answer from evidence.",
|
|
937
966
|
"- Use tool descriptions as the source of truth for when each tool should or should not be called.",
|
|
938
967
|
"- Use `bash` to inspect skill files from `skill_dir` and run shell commands inside the sandbox workspace.",
|
|
968
|
+
"- Use `attachFile` to attach files from the sandbox (for example screenshots, PDFs, logs) to the Slack reply.",
|
|
969
|
+
"- If the user asks to see/share/show a screenshot or file, attach the file with `attachFile` instead of only reporting its path.",
|
|
970
|
+
"- Never claim a screenshot/file is attached unless `attachFile` succeeded in this turn.",
|
|
971
|
+
"- If `attachFile` fails, explain the failure and do not say the file was shared.",
|
|
939
972
|
"- Use `imageGenerate` when the user asks for image creation.",
|
|
940
973
|
"- Use `slackCanvasCreate` for long-form docs/specs and `slackCanvasUpdate` for doc follow-ups.",
|
|
941
974
|
"- `slackCanvasUpdate` targets the active artifact-context canvas automatically; do not ask the user for `canvas_id`.",
|
|
@@ -946,7 +979,7 @@ function buildSystemPrompt(params) {
|
|
|
946
979
|
"- Use `slackMessageAddReaction` for rare lightweight acknowledgements. It reacts to the current inbound message via runtime context; never pick a target message yourself.",
|
|
947
980
|
"- If the user explicitly asks for an emoji reaction instead of text, use `slackMessageAddReaction` with a Slack emoji alias name (for example `thumbsup`, not unicode emoji), and avoid redundant acknowledgment text.",
|
|
948
981
|
"- Suggested acknowledgement reactions include \u{1F44B}, \u2705, \u{1F44D}, and \u{1F440}, but choose what best fits the request.",
|
|
949
|
-
"- To enable provider credentials for this turn, run `jr-rpc issue-credential <capability> [--repo <owner/repo>]` as a bash command before commands that need authenticated API calls.",
|
|
982
|
+
"- To enable provider credentials for this turn, run `jr-rpc issue-credential <capability> [--repo <owner/repo>]` as a bash command before commands that need authenticated API calls. GitHub capabilities need repository context, which can come from `--repo` or a configured `github.repo` default.",
|
|
950
983
|
"- To persist or read conversation defaults (for example `github.repo`), run `jr-rpc config get|set|unset|list ...` as a bash command.",
|
|
951
984
|
"- Capabilities are provider-qualified (for example `github.issues.write`).",
|
|
952
985
|
"- When your work is complete, provide the exact user-facing markdown response.",
|
|
@@ -958,10 +991,10 @@ function buildSystemPrompt(params) {
|
|
|
958
991
|
renderTag(
|
|
959
992
|
"skills",
|
|
960
993
|
[
|
|
961
|
-
"-
|
|
962
|
-
"- If
|
|
963
|
-
"- Otherwise, for
|
|
964
|
-
"- For
|
|
994
|
+
"- Explicit skill triggers may appear as `/skillname` or `!skillname`.",
|
|
995
|
+
"- If explicitly invoked skill instructions are already present in <loaded_skills>, apply them immediately.",
|
|
996
|
+
"- Otherwise, for an explicitly invoked skill, call `loadSkill` for that exact skill before applying skill-specific behavior.",
|
|
997
|
+
"- For requests without an explicit trigger where a skill clearly matches, call `loadSkill` before applying skill-specific behavior.",
|
|
965
998
|
"- Do not claim to have used a skill unless it is present in <loaded_skills> or `loadSkill` succeeded in this turn.",
|
|
966
999
|
"- Never apply skill-specific behavior unless the skill is present in <loaded_skills> or `loadSkill` succeeded in this turn.",
|
|
967
1000
|
"- Load only the best matching skill first; do not load multiple skills upfront.",
|
|
@@ -987,7 +1020,7 @@ function buildSystemPrompt(params) {
|
|
|
987
1020
|
activeSkillsSection,
|
|
988
1021
|
renderTag(
|
|
989
1022
|
"invocation-context",
|
|
990
|
-
invocation ? `
|
|
1023
|
+
invocation ? invocation.source === "hard_bang" ? `Explicit skill trigger detected: !${invocation.skillName}` : `Legacy slash hint detected: /${invocation.skillName} (non-authoritative)` : "No explicit skill trigger detected."
|
|
991
1024
|
)
|
|
992
1025
|
];
|
|
993
1026
|
return sections.join("\n\n");
|
|
@@ -1278,6 +1311,7 @@ var SkillCapabilityRuntime = class {
|
|
|
1278
1311
|
// src/chat/credentials/state-adapter-token-store.ts
|
|
1279
1312
|
var KEY_PREFIX = "oauth-token";
|
|
1280
1313
|
var BUFFER_MS = 24 * 60 * 60 * 1e3;
|
|
1314
|
+
var LONG_LIVED_TTL_MS = 365 * 24 * 60 * 60 * 1e3;
|
|
1281
1315
|
function tokenKey(userId, provider) {
|
|
1282
1316
|
return `${KEY_PREFIX}:${userId}:${provider}`;
|
|
1283
1317
|
}
|
|
@@ -1287,11 +1321,13 @@ var StateAdapterTokenStore = class {
|
|
|
1287
1321
|
this.state = stateAdapter;
|
|
1288
1322
|
}
|
|
1289
1323
|
async get(userId, provider) {
|
|
1290
|
-
const stored = await this.state.get(
|
|
1324
|
+
const stored = await this.state.get(
|
|
1325
|
+
tokenKey(userId, provider)
|
|
1326
|
+
);
|
|
1291
1327
|
return stored ?? void 0;
|
|
1292
1328
|
}
|
|
1293
1329
|
async set(userId, provider, tokens) {
|
|
1294
|
-
const ttlMs = Math.max(tokens.expiresAt - Date.now() + BUFFER_MS, BUFFER_MS);
|
|
1330
|
+
const ttlMs = tokens.expiresAt ? Math.max(tokens.expiresAt - Date.now() + BUFFER_MS, BUFFER_MS) : LONG_LIVED_TTL_MS;
|
|
1295
1331
|
await this.state.set(tokenKey(userId, provider), tokens, ttlMs);
|
|
1296
1332
|
}
|
|
1297
1333
|
async delete(userId, provider) {
|
|
@@ -1319,6 +1355,7 @@ var TestCredentialBroker = class {
|
|
|
1319
1355
|
headerTransforms: this.config.domains.map((domain) => ({
|
|
1320
1356
|
domain,
|
|
1321
1357
|
headers: {
|
|
1358
|
+
...this.config.apiHeaders ?? {},
|
|
1322
1359
|
Authorization: `Bearer ${token}`
|
|
1323
1360
|
}
|
|
1324
1361
|
})),
|
|
@@ -1350,7 +1387,13 @@ function createSkillCapabilityRuntime(options = {}) {
|
|
|
1350
1387
|
continue;
|
|
1351
1388
|
}
|
|
1352
1389
|
const placeholder = resolveAuthTokenPlaceholder(credentials);
|
|
1353
|
-
brokersByProvider[name] = useTestBroker ? new TestCredentialBroker({
|
|
1390
|
+
brokersByProvider[name] = useTestBroker ? new TestCredentialBroker({
|
|
1391
|
+
provider: name,
|
|
1392
|
+
domains: credentials.apiDomains,
|
|
1393
|
+
apiHeaders: credentials.apiHeaders,
|
|
1394
|
+
envKey: credentials.authTokenEnv,
|
|
1395
|
+
placeholder
|
|
1396
|
+
}) : createPluginBroker(name, { userTokenStore });
|
|
1354
1397
|
}
|
|
1355
1398
|
const router = new ProviderCredentialRouter({ brokersByProvider });
|
|
1356
1399
|
return new SkillCapabilityRuntime({
|
|
@@ -1362,9 +1405,11 @@ function createSkillCapabilityRuntime(options = {}) {
|
|
|
1362
1405
|
}
|
|
1363
1406
|
|
|
1364
1407
|
// src/chat/capabilities/jr-rpc-command.ts
|
|
1365
|
-
import { randomBytes } from "crypto";
|
|
1366
1408
|
import { Bash, defineCommand } from "just-bash";
|
|
1367
1409
|
|
|
1410
|
+
// src/chat/oauth-flow.ts
|
|
1411
|
+
import { randomBytes } from "crypto";
|
|
1412
|
+
|
|
1368
1413
|
// src/chat/slack-actions/client.ts
|
|
1369
1414
|
import { WebClient } from "@slack/web-api";
|
|
1370
1415
|
var SlackActionError = class extends Error {
|
|
@@ -1620,19 +1665,36 @@ async function downloadPrivateSlackFile(url) {
|
|
|
1620
1665
|
return Buffer.from(await response.arrayBuffer());
|
|
1621
1666
|
}
|
|
1622
1667
|
|
|
1623
|
-
// src/chat/
|
|
1668
|
+
// src/chat/oauth-flow.ts
|
|
1669
|
+
var OAUTH_STATE_TTL_MS = 10 * 60 * 1e3;
|
|
1670
|
+
function formatProviderLabel(provider) {
|
|
1671
|
+
return provider.charAt(0).toUpperCase() + provider.slice(1);
|
|
1672
|
+
}
|
|
1673
|
+
function resolveBaseUrl() {
|
|
1674
|
+
const explicit = process.env.JUNIOR_BASE_URL?.trim();
|
|
1675
|
+
if (explicit) return explicit;
|
|
1676
|
+
const vercelProd = process.env.VERCEL_PROJECT_PRODUCTION_URL?.trim();
|
|
1677
|
+
if (vercelProd) return `https://${vercelProd}`;
|
|
1678
|
+
const vercelUrl = process.env.VERCEL_URL?.trim();
|
|
1679
|
+
if (vercelUrl) return `https://${vercelUrl}`;
|
|
1680
|
+
return void 0;
|
|
1681
|
+
}
|
|
1624
1682
|
async function deliverPrivateMessage(input) {
|
|
1625
1683
|
let client2;
|
|
1626
1684
|
try {
|
|
1627
1685
|
client2 = getSlackClient();
|
|
1628
1686
|
} catch {
|
|
1629
|
-
logWarn(
|
|
1687
|
+
logWarn(
|
|
1688
|
+
"oauth_private_delivery_skip",
|
|
1689
|
+
{},
|
|
1690
|
+
{ "app.reason": "missing_bot_token" },
|
|
1691
|
+
"Skipped private message delivery \u2014 no SLACK_BOT_TOKEN"
|
|
1692
|
+
);
|
|
1630
1693
|
return false;
|
|
1631
1694
|
}
|
|
1632
1695
|
if (input.channelId) {
|
|
1633
|
-
const isDm = isDmChannel(input.channelId);
|
|
1634
1696
|
try {
|
|
1635
|
-
if (
|
|
1697
|
+
if (isDmChannel(input.channelId)) {
|
|
1636
1698
|
await client2.chat.postMessage({
|
|
1637
1699
|
channel: input.channelId,
|
|
1638
1700
|
text: input.text,
|
|
@@ -1648,35 +1710,113 @@ async function deliverPrivateMessage(input) {
|
|
|
1648
1710
|
}
|
|
1649
1711
|
return "in_context";
|
|
1650
1712
|
} catch (error) {
|
|
1651
|
-
const slackError = error instanceof Error ? error.message : String(error);
|
|
1652
1713
|
logWarn(
|
|
1653
1714
|
"oauth_private_delivery_failed",
|
|
1654
1715
|
{},
|
|
1655
|
-
{
|
|
1656
|
-
|
|
1716
|
+
{
|
|
1717
|
+
"app.slack.error": error instanceof Error ? error.message : String(error),
|
|
1718
|
+
"app.slack.channel": input.channelId
|
|
1719
|
+
},
|
|
1720
|
+
"Private message delivery failed, falling back to DM"
|
|
1657
1721
|
);
|
|
1658
1722
|
}
|
|
1659
1723
|
}
|
|
1660
1724
|
try {
|
|
1661
|
-
const
|
|
1662
|
-
const dmChannelId = openResult.channel?.id;
|
|
1725
|
+
const dmChannelId = (await client2.conversations.open({ users: input.userId })).channel?.id;
|
|
1663
1726
|
if (!dmChannelId) {
|
|
1664
|
-
logWarn(
|
|
1727
|
+
logWarn(
|
|
1728
|
+
"oauth_dm_fallback_failed",
|
|
1729
|
+
{},
|
|
1730
|
+
{ "app.reason": "no_dm_channel_id" },
|
|
1731
|
+
"conversations.open returned no channel ID"
|
|
1732
|
+
);
|
|
1665
1733
|
return false;
|
|
1666
1734
|
}
|
|
1667
1735
|
await client2.chat.postMessage({ channel: dmChannelId, text: input.text });
|
|
1668
1736
|
return "fallback_dm";
|
|
1669
1737
|
} catch (error) {
|
|
1670
|
-
const slackError = error instanceof Error ? error.message : String(error);
|
|
1671
1738
|
logWarn(
|
|
1672
1739
|
"oauth_dm_fallback_failed",
|
|
1673
1740
|
{},
|
|
1674
|
-
{
|
|
1741
|
+
{
|
|
1742
|
+
"app.slack.error": error instanceof Error ? error.message : String(error)
|
|
1743
|
+
},
|
|
1675
1744
|
"DM fallback delivery failed"
|
|
1676
1745
|
);
|
|
1677
1746
|
return false;
|
|
1678
1747
|
}
|
|
1679
1748
|
}
|
|
1749
|
+
async function startOAuthFlow(provider, input) {
|
|
1750
|
+
const providerConfig = getPluginOAuthConfig(provider);
|
|
1751
|
+
if (!providerConfig) {
|
|
1752
|
+
return {
|
|
1753
|
+
ok: false,
|
|
1754
|
+
error: `Provider "${provider}" does not support OAuth authorization`
|
|
1755
|
+
};
|
|
1756
|
+
}
|
|
1757
|
+
const clientId = process.env[providerConfig.clientIdEnv]?.trim();
|
|
1758
|
+
if (!clientId) {
|
|
1759
|
+
return {
|
|
1760
|
+
ok: false,
|
|
1761
|
+
error: `Missing ${providerConfig.clientIdEnv} environment variable`
|
|
1762
|
+
};
|
|
1763
|
+
}
|
|
1764
|
+
const baseUrl = resolveBaseUrl();
|
|
1765
|
+
if (!baseUrl) {
|
|
1766
|
+
return {
|
|
1767
|
+
ok: false,
|
|
1768
|
+
error: "Cannot determine base URL (set JUNIOR_BASE_URL or deploy to Vercel)"
|
|
1769
|
+
};
|
|
1770
|
+
}
|
|
1771
|
+
const configuration = input.userMessage && input.channelConfiguration ? await input.channelConfiguration.resolveValues() : void 0;
|
|
1772
|
+
const state = randomBytes(32).toString("hex");
|
|
1773
|
+
await getStateAdapter().set(
|
|
1774
|
+
`oauth-state:${state}`,
|
|
1775
|
+
{
|
|
1776
|
+
userId: input.requesterId,
|
|
1777
|
+
provider,
|
|
1778
|
+
...input.channelId ? { channelId: input.channelId } : {},
|
|
1779
|
+
...input.threadTs ? { threadTs: input.threadTs } : {},
|
|
1780
|
+
...input.userMessage ? { pendingMessage: input.userMessage } : {},
|
|
1781
|
+
...configuration && Object.keys(configuration).length > 0 ? { configuration } : {}
|
|
1782
|
+
},
|
|
1783
|
+
OAUTH_STATE_TTL_MS
|
|
1784
|
+
);
|
|
1785
|
+
const authorizeParams = new URLSearchParams({
|
|
1786
|
+
client_id: clientId,
|
|
1787
|
+
state,
|
|
1788
|
+
redirect_uri: `${baseUrl}${providerConfig.callbackPath}`,
|
|
1789
|
+
response_type: "code"
|
|
1790
|
+
});
|
|
1791
|
+
if (providerConfig.scope) {
|
|
1792
|
+
authorizeParams.set("scope", providerConfig.scope);
|
|
1793
|
+
}
|
|
1794
|
+
for (const [key, value] of Object.entries(
|
|
1795
|
+
providerConfig.authorizeParams ?? {}
|
|
1796
|
+
)) {
|
|
1797
|
+
authorizeParams.set(key, value);
|
|
1798
|
+
}
|
|
1799
|
+
logInfo(
|
|
1800
|
+
"jr_rpc_oauth_start",
|
|
1801
|
+
{},
|
|
1802
|
+
{
|
|
1803
|
+
"app.credential.provider": provider,
|
|
1804
|
+
...input.activeSkillName ? { "app.skill.name": input.activeSkillName } : {}
|
|
1805
|
+
},
|
|
1806
|
+
"Initiated OAuth authorization code flow"
|
|
1807
|
+
);
|
|
1808
|
+
return {
|
|
1809
|
+
ok: true,
|
|
1810
|
+
delivery: await deliverPrivateMessage({
|
|
1811
|
+
channelId: input.channelId,
|
|
1812
|
+
threadTs: input.threadTs,
|
|
1813
|
+
userId: input.requesterId,
|
|
1814
|
+
text: `<${providerConfig.authorizeEndpoint}?${authorizeParams.toString()}|Click here to link your ${formatProviderLabel(provider)} account>. Once you've authorized, you'll see a confirmation in Slack.`
|
|
1815
|
+
})
|
|
1816
|
+
};
|
|
1817
|
+
}
|
|
1818
|
+
|
|
1819
|
+
// src/chat/capabilities/jr-rpc-command.ts
|
|
1680
1820
|
function commandResult(input) {
|
|
1681
1821
|
let stdout = "";
|
|
1682
1822
|
if (typeof input.stdout === "string") {
|
|
@@ -1759,7 +1899,7 @@ async function handleIssueCredentialCommand(args, deps) {
|
|
|
1759
1899
|
reason: `skill:${deps.activeSkill?.name ?? "unknown"}:jr-rpc:issue-credential`
|
|
1760
1900
|
});
|
|
1761
1901
|
} catch (error) {
|
|
1762
|
-
if (error instanceof CredentialUnavailableError &&
|
|
1902
|
+
if (error instanceof CredentialUnavailableError && getPluginOAuthConfig(error.provider) && deps.requesterId) {
|
|
1763
1903
|
const oauthResult = await startOAuthFlow(error.provider, {
|
|
1764
1904
|
requesterId: deps.requesterId,
|
|
1765
1905
|
channelId: deps.channelId,
|
|
@@ -1769,14 +1909,14 @@ async function handleIssueCredentialCommand(args, deps) {
|
|
|
1769
1909
|
activeSkillName: deps.activeSkill?.name ?? void 0
|
|
1770
1910
|
});
|
|
1771
1911
|
if (oauthResult.ok) {
|
|
1772
|
-
const
|
|
1912
|
+
const providerLabel = formatProviderLabel(error.provider);
|
|
1773
1913
|
return commandResult({
|
|
1774
1914
|
stdout: {
|
|
1775
1915
|
credential_unavailable: true,
|
|
1776
1916
|
oauth_started: true,
|
|
1777
1917
|
provider: error.provider,
|
|
1778
1918
|
private_delivery_sent: !!oauthResult.delivery,
|
|
1779
|
-
message: oauthResult.delivery ? `I need to connect your ${
|
|
1919
|
+
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.`
|
|
1780
1920
|
},
|
|
1781
1921
|
exitCode: 1
|
|
1782
1922
|
});
|
|
@@ -1986,75 +2126,6 @@ ${usage}
|
|
|
1986
2126
|
function isKnownProvider(provider) {
|
|
1987
2127
|
return listCapabilityProviders().some((p) => p.provider === provider);
|
|
1988
2128
|
}
|
|
1989
|
-
function getOAuthProviderConfig(provider) {
|
|
1990
|
-
return getPluginOAuthConfig(provider);
|
|
1991
|
-
}
|
|
1992
|
-
var OAUTH_STATE_TTL_MS = 10 * 60 * 1e3;
|
|
1993
|
-
function resolveBaseUrl() {
|
|
1994
|
-
const explicit = process.env.JUNIOR_BASE_URL?.trim();
|
|
1995
|
-
if (explicit) return explicit;
|
|
1996
|
-
const vercelProd = process.env.VERCEL_PROJECT_PRODUCTION_URL?.trim();
|
|
1997
|
-
if (vercelProd) return `https://${vercelProd}`;
|
|
1998
|
-
const vercelUrl = process.env.VERCEL_URL?.trim();
|
|
1999
|
-
if (vercelUrl) return `https://${vercelUrl}`;
|
|
2000
|
-
return void 0;
|
|
2001
|
-
}
|
|
2002
|
-
async function startOAuthFlow(provider, input) {
|
|
2003
|
-
const providerConfig = getOAuthProviderConfig(provider);
|
|
2004
|
-
if (!providerConfig) {
|
|
2005
|
-
return { ok: false, error: `Provider "${provider}" does not support OAuth authorization` };
|
|
2006
|
-
}
|
|
2007
|
-
const clientId = process.env[providerConfig.clientIdEnv]?.trim();
|
|
2008
|
-
if (!clientId) {
|
|
2009
|
-
return { ok: false, error: `Missing ${providerConfig.clientIdEnv} environment variable` };
|
|
2010
|
-
}
|
|
2011
|
-
const baseUrl = resolveBaseUrl();
|
|
2012
|
-
if (!baseUrl) {
|
|
2013
|
-
return { ok: false, error: "Cannot determine base URL (set JUNIOR_BASE_URL or deploy to Vercel)" };
|
|
2014
|
-
}
|
|
2015
|
-
let configuration;
|
|
2016
|
-
if (input.userMessage && input.channelConfiguration) {
|
|
2017
|
-
configuration = await input.channelConfiguration.resolveValues();
|
|
2018
|
-
}
|
|
2019
|
-
const state = randomBytes(32).toString("hex");
|
|
2020
|
-
const stateKey = `oauth-state:${state}`;
|
|
2021
|
-
const stateAdapter = getStateAdapter();
|
|
2022
|
-
const statePayload = {
|
|
2023
|
-
userId: input.requesterId,
|
|
2024
|
-
provider,
|
|
2025
|
-
...input.channelId ? { channelId: input.channelId } : {},
|
|
2026
|
-
...input.threadTs ? { threadTs: input.threadTs } : {},
|
|
2027
|
-
...input.userMessage ? { pendingMessage: input.userMessage } : {},
|
|
2028
|
-
...configuration && Object.keys(configuration).length > 0 ? { configuration } : {}
|
|
2029
|
-
};
|
|
2030
|
-
await stateAdapter.set(stateKey, statePayload, OAUTH_STATE_TTL_MS);
|
|
2031
|
-
const redirectUri = `${baseUrl}${providerConfig.callbackPath}`;
|
|
2032
|
-
const params = new URLSearchParams({
|
|
2033
|
-
client_id: clientId,
|
|
2034
|
-
scope: providerConfig.scope,
|
|
2035
|
-
state,
|
|
2036
|
-
redirect_uri: redirectUri,
|
|
2037
|
-
response_type: "code"
|
|
2038
|
-
});
|
|
2039
|
-
const authorizeUrl = `${providerConfig.authorizeEndpoint}?${params.toString()}`;
|
|
2040
|
-
logInfo(
|
|
2041
|
-
"jr_rpc_oauth_start",
|
|
2042
|
-
{},
|
|
2043
|
-
{
|
|
2044
|
-
"app.credential.provider": provider,
|
|
2045
|
-
...input.activeSkillName ? { "app.skill.name": input.activeSkillName } : {}
|
|
2046
|
-
},
|
|
2047
|
-
"Initiated OAuth authorization code flow"
|
|
2048
|
-
);
|
|
2049
|
-
const providerLabel2 = provider.charAt(0).toUpperCase() + provider.slice(1);
|
|
2050
|
-
const delivery = await deliverPrivateMessage({
|
|
2051
|
-
channelId: input.channelId,
|
|
2052
|
-
threadTs: input.threadTs,
|
|
2053
|
-
userId: input.requesterId,
|
|
2054
|
-
text: `<${authorizeUrl}|Click here to link your ${providerLabel2} account>. Once you've authorized, you'll see a confirmation in Slack.`
|
|
2055
|
-
});
|
|
2056
|
-
return { ok: true, delivery };
|
|
2057
|
-
}
|
|
2058
2129
|
async function handleOAuthStartCommand(args, deps) {
|
|
2059
2130
|
const provider = (args[0] ?? "").trim();
|
|
2060
2131
|
if (!provider) {
|
|
@@ -2071,14 +2142,14 @@ async function handleOAuthStartCommand(args, deps) {
|
|
|
2071
2142
|
}
|
|
2072
2143
|
if (deps.requesterId && deps.userTokenStore) {
|
|
2073
2144
|
const stored = await deps.userTokenStore.get(deps.requesterId, provider);
|
|
2074
|
-
if (stored && stored.expiresAt > Date.now()) {
|
|
2075
|
-
const
|
|
2145
|
+
if (stored && (stored.expiresAt === void 0 || stored.expiresAt > Date.now())) {
|
|
2146
|
+
const providerLabel = formatProviderLabel(provider);
|
|
2076
2147
|
return commandResult({
|
|
2077
2148
|
stdout: {
|
|
2078
2149
|
ok: true,
|
|
2079
2150
|
already_connected: true,
|
|
2080
2151
|
provider,
|
|
2081
|
-
message: `Your ${
|
|
2152
|
+
message: `Your ${providerLabel} account is already connected.`
|
|
2082
2153
|
},
|
|
2083
2154
|
exitCode: 0
|
|
2084
2155
|
});
|
|
@@ -2233,6 +2304,38 @@ function isExplicitChannelPostIntent(text) {
|
|
|
2233
2304
|
}
|
|
2234
2305
|
|
|
2235
2306
|
// src/chat/delivery/plan.ts
|
|
2307
|
+
var REACTION_ONLY_ACK_RE = /^(?::[a-z0-9_+-]+:|[\p{Extended_Pictographic}\uFE0F\u200D]+)$/u;
|
|
2308
|
+
var REDUNDANT_REACTION_ACK_TEXT = ["done", "got it", "ok", "okay"];
|
|
2309
|
+
var REACTION_ALIAS_PREFIX_RE = /^:[a-z0-9_+-]*$/i;
|
|
2310
|
+
function normalizeReactionAckText(text) {
|
|
2311
|
+
return text.trim().toLowerCase().replace(/[!.]+$/g, "");
|
|
2312
|
+
}
|
|
2313
|
+
function isRedundantReactionAckText(text) {
|
|
2314
|
+
const trimmed = text.trim();
|
|
2315
|
+
if (!trimmed) {
|
|
2316
|
+
return false;
|
|
2317
|
+
}
|
|
2318
|
+
if (REACTION_ONLY_ACK_RE.test(trimmed)) {
|
|
2319
|
+
return true;
|
|
2320
|
+
}
|
|
2321
|
+
const normalized = normalizeReactionAckText(text);
|
|
2322
|
+
return REDUNDANT_REACTION_ACK_TEXT.includes(
|
|
2323
|
+
normalized
|
|
2324
|
+
);
|
|
2325
|
+
}
|
|
2326
|
+
function isPotentialRedundantReactionAckText(text) {
|
|
2327
|
+
const trimmed = text.trim();
|
|
2328
|
+
if (!trimmed) {
|
|
2329
|
+
return true;
|
|
2330
|
+
}
|
|
2331
|
+
if (REACTION_ONLY_ACK_RE.test(trimmed) || REACTION_ALIAS_PREFIX_RE.test(trimmed)) {
|
|
2332
|
+
return true;
|
|
2333
|
+
}
|
|
2334
|
+
const normalized = normalizeReactionAckText(text);
|
|
2335
|
+
return REDUNDANT_REACTION_ACK_TEXT.some(
|
|
2336
|
+
(candidate) => candidate.startsWith(normalized)
|
|
2337
|
+
);
|
|
2338
|
+
}
|
|
2236
2339
|
function buildReplyDeliveryPlan(args) {
|
|
2237
2340
|
const mode = args.explicitChannelPostIntent && args.channelPostPerformed ? "channel_only" : "thread";
|
|
2238
2341
|
let attachFiles = "none";
|
|
@@ -2501,19 +2604,31 @@ async function discoverSkills(options) {
|
|
|
2501
2604
|
}
|
|
2502
2605
|
function parseSkillInvocation(messageText, availableSkills) {
|
|
2503
2606
|
const trimmed = messageText.trim();
|
|
2504
|
-
const
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2607
|
+
const toInvocation = (match, source) => {
|
|
2608
|
+
if (!match) {
|
|
2609
|
+
return null;
|
|
2610
|
+
}
|
|
2611
|
+
const skillName = match[1].toLowerCase();
|
|
2612
|
+
if (!availableSkills.some((skill) => skill.name === skillName)) {
|
|
2613
|
+
return null;
|
|
2614
|
+
}
|
|
2615
|
+
return {
|
|
2616
|
+
skillName,
|
|
2617
|
+
args: (match[2] ?? "").trim(),
|
|
2618
|
+
source
|
|
2619
|
+
};
|
|
2516
2620
|
};
|
|
2621
|
+
const hardBangInvocation = toInvocation(
|
|
2622
|
+
/(?:^|\s)!([a-z0-9]+(?:-[a-z0-9]+)*)(?:\s+([\s\S]*))?/i.exec(trimmed),
|
|
2623
|
+
"hard_bang"
|
|
2624
|
+
);
|
|
2625
|
+
if (hardBangInvocation) {
|
|
2626
|
+
return hardBangInvocation;
|
|
2627
|
+
}
|
|
2628
|
+
return toInvocation(
|
|
2629
|
+
/(?:^|\s)\/([a-z0-9]+(?:-[a-z0-9]+)*)(?:\s+([\s\S]*))?/i.exec(trimmed),
|
|
2630
|
+
"legacy_slash"
|
|
2631
|
+
);
|
|
2517
2632
|
}
|
|
2518
2633
|
function findSkillByName(skillName, available) {
|
|
2519
2634
|
return available.find((skill) => skill.name === skillName) ?? null;
|
|
@@ -2736,8 +2851,135 @@ function createBashTool() {
|
|
|
2736
2851
|
});
|
|
2737
2852
|
}
|
|
2738
2853
|
|
|
2739
|
-
// src/chat/tools/
|
|
2854
|
+
// src/chat/tools/attach-file.ts
|
|
2855
|
+
import path3 from "path";
|
|
2740
2856
|
import { Type as Type2 } from "@sinclair/typebox";
|
|
2857
|
+
var MAX_ATTACH_FILE_BYTES = 10 * 1024 * 1024;
|
|
2858
|
+
var MIME_BY_EXTENSION = {
|
|
2859
|
+
".png": "image/png",
|
|
2860
|
+
".jpg": "image/jpeg",
|
|
2861
|
+
".jpeg": "image/jpeg",
|
|
2862
|
+
".gif": "image/gif",
|
|
2863
|
+
".webp": "image/webp",
|
|
2864
|
+
".svg": "image/svg+xml",
|
|
2865
|
+
".pdf": "application/pdf",
|
|
2866
|
+
".txt": "text/plain",
|
|
2867
|
+
".md": "text/markdown",
|
|
2868
|
+
".json": "application/json",
|
|
2869
|
+
".csv": "text/csv",
|
|
2870
|
+
".log": "text/plain"
|
|
2871
|
+
};
|
|
2872
|
+
function normalizeSandboxPath(inputPath) {
|
|
2873
|
+
const trimmed = inputPath.trim();
|
|
2874
|
+
if (!trimmed) {
|
|
2875
|
+
throw new Error("path is required");
|
|
2876
|
+
}
|
|
2877
|
+
if (path3.posix.isAbsolute(trimmed)) {
|
|
2878
|
+
return trimmed;
|
|
2879
|
+
}
|
|
2880
|
+
return path3.posix.join(SANDBOX_WORKSPACE_ROOT, trimmed);
|
|
2881
|
+
}
|
|
2882
|
+
function sanitizeFilename(value, fallbackPath) {
|
|
2883
|
+
const candidate = (value ?? "").trim();
|
|
2884
|
+
if (candidate) {
|
|
2885
|
+
const base = path3.posix.basename(candidate);
|
|
2886
|
+
if (base && base !== "." && base !== "..") {
|
|
2887
|
+
return base;
|
|
2888
|
+
}
|
|
2889
|
+
}
|
|
2890
|
+
const derived = path3.posix.basename(fallbackPath);
|
|
2891
|
+
if (derived && derived !== "." && derived !== "..") {
|
|
2892
|
+
return derived;
|
|
2893
|
+
}
|
|
2894
|
+
return "attachment.bin";
|
|
2895
|
+
}
|
|
2896
|
+
function inferMimeType(filename, explicitMimeType) {
|
|
2897
|
+
const explicit = explicitMimeType?.trim();
|
|
2898
|
+
if (explicit) {
|
|
2899
|
+
return explicit;
|
|
2900
|
+
}
|
|
2901
|
+
const ext = path3.extname(filename).toLowerCase();
|
|
2902
|
+
return MIME_BY_EXTENSION[ext] ?? "application/octet-stream";
|
|
2903
|
+
}
|
|
2904
|
+
async function detectMimeType(sandbox, targetPath) {
|
|
2905
|
+
try {
|
|
2906
|
+
const result = await sandbox.runCommand({
|
|
2907
|
+
cmd: "file",
|
|
2908
|
+
args: ["--mime-type", "-b", targetPath]
|
|
2909
|
+
});
|
|
2910
|
+
if (result.exitCode !== 0) {
|
|
2911
|
+
return void 0;
|
|
2912
|
+
}
|
|
2913
|
+
const value = (await result.stdout()).trim();
|
|
2914
|
+
return value || void 0;
|
|
2915
|
+
} catch {
|
|
2916
|
+
return void 0;
|
|
2917
|
+
}
|
|
2918
|
+
}
|
|
2919
|
+
function createAttachFileTool(sandbox, hooks = {}) {
|
|
2920
|
+
return tool({
|
|
2921
|
+
description: "Attach a file from the sandbox to the Slack reply. Use this immediately after creating screenshots/reports when the user asks to see/share the actual file, not just its path.",
|
|
2922
|
+
inputSchema: Type2.Object(
|
|
2923
|
+
{
|
|
2924
|
+
path: Type2.String({
|
|
2925
|
+
minLength: 1,
|
|
2926
|
+
description: "Absolute path (for example /tmp/screenshot.png) or workspace-relative path."
|
|
2927
|
+
}),
|
|
2928
|
+
filename: Type2.Optional(
|
|
2929
|
+
Type2.String({
|
|
2930
|
+
minLength: 1,
|
|
2931
|
+
description: "Optional filename override shown in Slack."
|
|
2932
|
+
})
|
|
2933
|
+
),
|
|
2934
|
+
mimeType: Type2.Optional(
|
|
2935
|
+
Type2.String({
|
|
2936
|
+
minLength: 1,
|
|
2937
|
+
description: "Optional MIME type override (for example image/png)."
|
|
2938
|
+
})
|
|
2939
|
+
)
|
|
2940
|
+
},
|
|
2941
|
+
{ additionalProperties: false }
|
|
2942
|
+
),
|
|
2943
|
+
execute: async ({ path: requestedPath, filename, mimeType }) => {
|
|
2944
|
+
const targetPath = normalizeSandboxPath(requestedPath);
|
|
2945
|
+
const fileBuffer = await sandbox.readFileToBuffer({ path: targetPath });
|
|
2946
|
+
if (!fileBuffer) {
|
|
2947
|
+
throw new Error(`failed to read file: ${targetPath}`);
|
|
2948
|
+
}
|
|
2949
|
+
if (fileBuffer.byteLength === 0) {
|
|
2950
|
+
throw new Error(`file is empty: ${targetPath}`);
|
|
2951
|
+
}
|
|
2952
|
+
if (fileBuffer.byteLength > MAX_ATTACH_FILE_BYTES) {
|
|
2953
|
+
throw new Error(
|
|
2954
|
+
`file exceeds ${MAX_ATTACH_FILE_BYTES} bytes: ${targetPath} (${fileBuffer.byteLength} bytes)`
|
|
2955
|
+
);
|
|
2956
|
+
}
|
|
2957
|
+
const resolvedFilename = sanitizeFilename(filename, targetPath);
|
|
2958
|
+
const detectedMimeType = await detectMimeType(sandbox, targetPath);
|
|
2959
|
+
const resolvedMimeType = inferMimeType(
|
|
2960
|
+
resolvedFilename,
|
|
2961
|
+
mimeType ?? detectedMimeType
|
|
2962
|
+
);
|
|
2963
|
+
const upload = {
|
|
2964
|
+
data: fileBuffer,
|
|
2965
|
+
filename: resolvedFilename,
|
|
2966
|
+
mimeType: resolvedMimeType
|
|
2967
|
+
};
|
|
2968
|
+
hooks.onGeneratedFiles?.([upload]);
|
|
2969
|
+
return {
|
|
2970
|
+
ok: true,
|
|
2971
|
+
attached: true,
|
|
2972
|
+
path: targetPath,
|
|
2973
|
+
filename: resolvedFilename,
|
|
2974
|
+
mime_type: resolvedMimeType,
|
|
2975
|
+
bytes: fileBuffer.byteLength
|
|
2976
|
+
};
|
|
2977
|
+
}
|
|
2978
|
+
});
|
|
2979
|
+
}
|
|
2980
|
+
|
|
2981
|
+
// src/chat/tools/image-generate.ts
|
|
2982
|
+
import { Type as Type3 } from "@sinclair/typebox";
|
|
2741
2983
|
|
|
2742
2984
|
// src/chat/pi/client.ts
|
|
2743
2985
|
import { completeSimple, getEnvApiKey, getModels } from "@mariozechner/pi-ai";
|
|
@@ -3018,8 +3260,8 @@ function parseImageGenerationError(status, body, model) {
|
|
|
3018
3260
|
function createImageGenerateTool(hooks) {
|
|
3019
3261
|
return tool({
|
|
3020
3262
|
description: "Generate images from a prompt. Use when the user wants to visually show or represent something \u2014 feelings, concepts, art, humor, or any visual idea. Also use for explicit image creation requests.",
|
|
3021
|
-
inputSchema:
|
|
3022
|
-
prompt:
|
|
3263
|
+
inputSchema: Type3.Object({
|
|
3264
|
+
prompt: Type3.String({
|
|
3023
3265
|
minLength: 1,
|
|
3024
3266
|
maxLength: 4e3,
|
|
3025
3267
|
description: "Image generation prompt."
|
|
@@ -3095,7 +3337,7 @@ function createImageGenerateTool(hooks) {
|
|
|
3095
3337
|
}
|
|
3096
3338
|
|
|
3097
3339
|
// src/chat/tools/load-skill.ts
|
|
3098
|
-
import { Type as
|
|
3340
|
+
import { Type as Type4 } from "@sinclair/typebox";
|
|
3099
3341
|
function toLoadedSkill(result) {
|
|
3100
3342
|
if (result.ok !== true || typeof result.skill_name !== "string" || typeof result.description !== "string" || typeof result.skill_dir !== "string" || typeof result.instructions !== "string") {
|
|
3101
3343
|
return null;
|
|
@@ -3144,9 +3386,9 @@ async function loadSkillFromSandbox(sandbox, availableSkills, skillName) {
|
|
|
3144
3386
|
}
|
|
3145
3387
|
function createLoadSkillTool(sandbox, availableSkills, options) {
|
|
3146
3388
|
return tool({
|
|
3147
|
-
description: "Load a skill by name so its instructions are available for this turn. Use when a request clearly matches a known skill
|
|
3148
|
-
inputSchema:
|
|
3149
|
-
skill_name:
|
|
3389
|
+
description: "Load a skill by name so its instructions are available for this turn. Use when a request clearly matches a known skill. Do not use when no skill is relevant.",
|
|
3390
|
+
inputSchema: Type4.Object({
|
|
3391
|
+
skill_name: Type4.String({
|
|
3150
3392
|
minLength: 1,
|
|
3151
3393
|
description: "Skill name to load, without the leading slash."
|
|
3152
3394
|
})
|
|
@@ -3163,12 +3405,12 @@ function createLoadSkillTool(sandbox, availableSkills, options) {
|
|
|
3163
3405
|
}
|
|
3164
3406
|
|
|
3165
3407
|
// src/chat/tools/read-file.ts
|
|
3166
|
-
import { Type as
|
|
3408
|
+
import { Type as Type5 } from "@sinclair/typebox";
|
|
3167
3409
|
function createReadFileTool() {
|
|
3168
3410
|
return tool({
|
|
3169
3411
|
description: "Read a file from the sandbox workspace. Use when you need exact file contents to verify facts or make edits safely. Do not use for broad discovery when search tools are better.",
|
|
3170
|
-
inputSchema:
|
|
3171
|
-
path:
|
|
3412
|
+
inputSchema: Type5.Object({
|
|
3413
|
+
path: Type5.String({
|
|
3172
3414
|
minLength: 1,
|
|
3173
3415
|
description: "Path to the file in the sandbox workspace."
|
|
3174
3416
|
})
|
|
@@ -3180,7 +3422,7 @@ function createReadFileTool() {
|
|
|
3180
3422
|
}
|
|
3181
3423
|
|
|
3182
3424
|
// src/chat/tools/slack-channel-list-messages.ts
|
|
3183
|
-
import { Type as
|
|
3425
|
+
import { Type as Type6 } from "@sinclair/typebox";
|
|
3184
3426
|
|
|
3185
3427
|
// src/chat/slack-actions/channel.ts
|
|
3186
3428
|
async function postMessageToChannel(input) {
|
|
@@ -3353,39 +3595,39 @@ async function listThreadReplies(input) {
|
|
|
3353
3595
|
function createSlackChannelListMessagesTool(context) {
|
|
3354
3596
|
return tool({
|
|
3355
3597
|
description: "List channel messages from Slack history in the active channel context. Use when the user asks for recent or historical channel context outside this thread. Do not use for live monitoring or when current thread context already answers the question.",
|
|
3356
|
-
inputSchema:
|
|
3357
|
-
limit:
|
|
3358
|
-
|
|
3598
|
+
inputSchema: Type6.Object({
|
|
3599
|
+
limit: Type6.Optional(
|
|
3600
|
+
Type6.Integer({
|
|
3359
3601
|
minimum: 1,
|
|
3360
3602
|
maximum: 1e3,
|
|
3361
3603
|
description: "Maximum number of messages to return across pages."
|
|
3362
3604
|
})
|
|
3363
3605
|
),
|
|
3364
|
-
cursor:
|
|
3365
|
-
|
|
3606
|
+
cursor: Type6.Optional(
|
|
3607
|
+
Type6.String({
|
|
3366
3608
|
minLength: 1,
|
|
3367
3609
|
description: "Optional cursor to continue from a prior call."
|
|
3368
3610
|
})
|
|
3369
3611
|
),
|
|
3370
|
-
oldest:
|
|
3371
|
-
|
|
3612
|
+
oldest: Type6.Optional(
|
|
3613
|
+
Type6.String({
|
|
3372
3614
|
minLength: 1,
|
|
3373
3615
|
description: "Optional oldest message timestamp (Slack ts) for range filtering."
|
|
3374
3616
|
})
|
|
3375
3617
|
),
|
|
3376
|
-
latest:
|
|
3377
|
-
|
|
3618
|
+
latest: Type6.Optional(
|
|
3619
|
+
Type6.String({
|
|
3378
3620
|
minLength: 1,
|
|
3379
3621
|
description: "Optional latest message timestamp (Slack ts) for range filtering."
|
|
3380
3622
|
})
|
|
3381
3623
|
),
|
|
3382
|
-
inclusive:
|
|
3383
|
-
|
|
3624
|
+
inclusive: Type6.Optional(
|
|
3625
|
+
Type6.Boolean({
|
|
3384
3626
|
description: "Whether oldest/latest bounds should be inclusive."
|
|
3385
3627
|
})
|
|
3386
3628
|
),
|
|
3387
|
-
max_pages:
|
|
3388
|
-
|
|
3629
|
+
max_pages: Type6.Optional(
|
|
3630
|
+
Type6.Integer({
|
|
3389
3631
|
minimum: 1,
|
|
3390
3632
|
maximum: 10,
|
|
3391
3633
|
description: "Maximum number of API pages to traverse in a single call."
|
|
@@ -3418,7 +3660,7 @@ function createSlackChannelListMessagesTool(context) {
|
|
|
3418
3660
|
}
|
|
3419
3661
|
|
|
3420
3662
|
// src/chat/tools/slack-channel-post-message.ts
|
|
3421
|
-
import { Type as
|
|
3663
|
+
import { Type as Type7 } from "@sinclair/typebox";
|
|
3422
3664
|
|
|
3423
3665
|
// src/chat/tools/idempotency.ts
|
|
3424
3666
|
function stableSerialize(value) {
|
|
@@ -3440,8 +3682,8 @@ function createOperationKey(toolName, input) {
|
|
|
3440
3682
|
function createSlackChannelPostMessageTool(context, state) {
|
|
3441
3683
|
return tool({
|
|
3442
3684
|
description: "Post a message in the active Slack channel context (outside the thread). Use this when the user explicitly asks to post/send/share/say something in the channel. Do not use for normal thread replies or speculative broadcasts. Do not claim a channel message was posted unless this tool succeeds in this turn.",
|
|
3443
|
-
inputSchema:
|
|
3444
|
-
text:
|
|
3685
|
+
inputSchema: Type7.Object({
|
|
3686
|
+
text: Type7.String({
|
|
3445
3687
|
minLength: 1,
|
|
3446
3688
|
maxLength: 4e4,
|
|
3447
3689
|
description: "Slack mrkdwn text to post."
|
|
@@ -3480,13 +3722,13 @@ function createSlackChannelPostMessageTool(context, state) {
|
|
|
3480
3722
|
}
|
|
3481
3723
|
|
|
3482
3724
|
// src/chat/tools/slack-message-add-reaction.ts
|
|
3483
|
-
import { Type as
|
|
3725
|
+
import { Type as Type8 } from "@sinclair/typebox";
|
|
3484
3726
|
var SLACK_EMOJI_NAME_RE = /^[a-z0-9_+-]+$/;
|
|
3485
3727
|
function createSlackMessageAddReactionTool(context, state) {
|
|
3486
3728
|
return tool({
|
|
3487
3729
|
description: "Add an emoji reaction to the current inbound Slack message. Use sparingly for lightweight acknowledgements. Provide a Slack emoji alias name (for example `thumbsup` or `white_check_mark`), not a unicode emoji glyph. The target message is injected by runtime context; do not use this for arbitrary historical messages.",
|
|
3488
|
-
inputSchema:
|
|
3489
|
-
emoji:
|
|
3730
|
+
inputSchema: Type8.Object({
|
|
3731
|
+
emoji: Type8.String({
|
|
3490
3732
|
minLength: 1,
|
|
3491
3733
|
maxLength: 64,
|
|
3492
3734
|
description: "Slack emoji alias name to react with (for example `thumbsup` or `white_check_mark`). Optional surrounding colons are allowed."
|
|
@@ -3541,7 +3783,7 @@ function createSlackMessageAddReactionTool(context, state) {
|
|
|
3541
3783
|
}
|
|
3542
3784
|
|
|
3543
3785
|
// src/chat/tools/slack-canvas-create.ts
|
|
3544
|
-
import { Type as
|
|
3786
|
+
import { Type as Type9 } from "@sinclair/typebox";
|
|
3545
3787
|
|
|
3546
3788
|
// src/chat/slack-actions/canvases.ts
|
|
3547
3789
|
function normalizeCanvasMarkdown(markdown) {
|
|
@@ -3669,13 +3911,13 @@ function mergeRecentCanvases(existing, created) {
|
|
|
3669
3911
|
function createSlackCanvasCreateTool(context, state) {
|
|
3670
3912
|
return tool({
|
|
3671
3913
|
description: "Create a Slack canvas for long-form output in the active assistant context channel. Use when content is too long for a thread reply or needs a persistent document. Do not use for short answers that fit in-thread.",
|
|
3672
|
-
inputSchema:
|
|
3673
|
-
title:
|
|
3914
|
+
inputSchema: Type9.Object({
|
|
3915
|
+
title: Type9.String({
|
|
3674
3916
|
minLength: 1,
|
|
3675
3917
|
maxLength: 160,
|
|
3676
3918
|
description: "Canvas title."
|
|
3677
3919
|
}),
|
|
3678
|
-
markdown:
|
|
3920
|
+
markdown: Type9.String({
|
|
3679
3921
|
minLength: 1,
|
|
3680
3922
|
description: "Canvas markdown body content."
|
|
3681
3923
|
})
|
|
@@ -3737,29 +3979,29 @@ function createSlackCanvasCreateTool(context, state) {
|
|
|
3737
3979
|
}
|
|
3738
3980
|
|
|
3739
3981
|
// src/chat/tools/slack-canvas-update.ts
|
|
3740
|
-
import { Type as
|
|
3982
|
+
import { Type as Type10 } from "@sinclair/typebox";
|
|
3741
3983
|
function createSlackCanvasUpdateTool(state, _context) {
|
|
3742
3984
|
return tool({
|
|
3743
3985
|
description: "Update the active Slack canvas tracked in artifact context. Use when continuing or correcting a document already tracked in this thread. Do not use to create a brand-new long-form artifact.",
|
|
3744
|
-
inputSchema:
|
|
3745
|
-
markdown:
|
|
3986
|
+
inputSchema: Type10.Object({
|
|
3987
|
+
markdown: Type10.String({
|
|
3746
3988
|
minLength: 1,
|
|
3747
3989
|
description: "Markdown content to insert or use as replacement text."
|
|
3748
3990
|
}),
|
|
3749
|
-
operation:
|
|
3750
|
-
|
|
3751
|
-
[
|
|
3991
|
+
operation: Type10.Optional(
|
|
3992
|
+
Type10.Union(
|
|
3993
|
+
[Type10.Literal("insert_at_end"), Type10.Literal("insert_at_start"), Type10.Literal("replace")],
|
|
3752
3994
|
{ description: "Canvas update mode." }
|
|
3753
3995
|
)
|
|
3754
3996
|
),
|
|
3755
|
-
section_id:
|
|
3756
|
-
|
|
3997
|
+
section_id: Type10.Optional(
|
|
3998
|
+
Type10.String({
|
|
3757
3999
|
minLength: 1,
|
|
3758
4000
|
description: "Optional section ID required for targeted replace operations."
|
|
3759
4001
|
})
|
|
3760
4002
|
),
|
|
3761
|
-
section_contains_text:
|
|
3762
|
-
|
|
4003
|
+
section_contains_text: Type10.Optional(
|
|
4004
|
+
Type10.String({
|
|
3763
4005
|
minLength: 1,
|
|
3764
4006
|
description: "Optional helper text used to find the target section when section_id is not provided."
|
|
3765
4007
|
})
|
|
@@ -3819,7 +4061,7 @@ function createSlackCanvasUpdateTool(state, _context) {
|
|
|
3819
4061
|
}
|
|
3820
4062
|
|
|
3821
4063
|
// src/chat/tools/slack-list-add-items.ts
|
|
3822
|
-
import { Type as
|
|
4064
|
+
import { Type as Type11 } from "@sinclair/typebox";
|
|
3823
4065
|
|
|
3824
4066
|
// src/chat/slack-actions/lists.ts
|
|
3825
4067
|
function normalizeKey(value) {
|
|
@@ -3984,20 +4226,20 @@ async function updateListItem(input) {
|
|
|
3984
4226
|
function createSlackListAddItemsTool(state) {
|
|
3985
4227
|
return tool({
|
|
3986
4228
|
description: "Add tasks to the active Slack list tracked in artifact context. Use when the user wants actionable items recorded in the current thread list. Do not use when no list exists and list creation was not requested.",
|
|
3987
|
-
inputSchema:
|
|
3988
|
-
items:
|
|
4229
|
+
inputSchema: Type11.Object({
|
|
4230
|
+
items: Type11.Array(Type11.String({ minLength: 1 }), {
|
|
3989
4231
|
minItems: 1,
|
|
3990
4232
|
maxItems: 25,
|
|
3991
4233
|
description: "List item titles to create."
|
|
3992
4234
|
}),
|
|
3993
|
-
assignee_user_id:
|
|
3994
|
-
|
|
4235
|
+
assignee_user_id: Type11.Optional(
|
|
4236
|
+
Type11.String({
|
|
3995
4237
|
minLength: 1,
|
|
3996
4238
|
description: "Optional Slack user ID assigned to all created items."
|
|
3997
4239
|
})
|
|
3998
4240
|
),
|
|
3999
|
-
due_date:
|
|
4000
|
-
|
|
4241
|
+
due_date: Type11.Optional(
|
|
4242
|
+
Type11.String({
|
|
4001
4243
|
pattern: "^\\d{4}-\\d{2}-\\d{2}$",
|
|
4002
4244
|
description: "Optional due date in YYYY-MM-DD format."
|
|
4003
4245
|
})
|
|
@@ -4045,12 +4287,12 @@ function createSlackListAddItemsTool(state) {
|
|
|
4045
4287
|
}
|
|
4046
4288
|
|
|
4047
4289
|
// src/chat/tools/slack-list-create.ts
|
|
4048
|
-
import { Type as
|
|
4290
|
+
import { Type as Type12 } from "@sinclair/typebox";
|
|
4049
4291
|
function createSlackListCreateTool(state) {
|
|
4050
4292
|
return tool({
|
|
4051
4293
|
description: "Create a Slack todo list for action tracking. Use when the user needs structured tasks with ownership/completion tracking. Do not use for one-off notes without task management needs.",
|
|
4052
|
-
inputSchema:
|
|
4053
|
-
name:
|
|
4294
|
+
inputSchema: Type12.Object({
|
|
4295
|
+
name: Type12.String({
|
|
4054
4296
|
minLength: 1,
|
|
4055
4297
|
maxLength: 160,
|
|
4056
4298
|
description: "Name for the new Slack list."
|
|
@@ -4084,13 +4326,13 @@ function createSlackListCreateTool(state) {
|
|
|
4084
4326
|
}
|
|
4085
4327
|
|
|
4086
4328
|
// src/chat/tools/slack-list-get-items.ts
|
|
4087
|
-
import { Type as
|
|
4329
|
+
import { Type as Type13 } from "@sinclair/typebox";
|
|
4088
4330
|
function createSlackListGetItemsTool(state) {
|
|
4089
4331
|
return tool({
|
|
4090
4332
|
description: "Read items from the active Slack list tracked in artifact context. Use when the user asks for task status, open items, or list contents. Do not use when list state is already known from the immediately prior result.",
|
|
4091
|
-
inputSchema:
|
|
4092
|
-
limit:
|
|
4093
|
-
|
|
4333
|
+
inputSchema: Type13.Object({
|
|
4334
|
+
limit: Type13.Optional(
|
|
4335
|
+
Type13.Integer({
|
|
4094
4336
|
minimum: 1,
|
|
4095
4337
|
maximum: 200,
|
|
4096
4338
|
description: "Maximum number of list items to return."
|
|
@@ -4114,23 +4356,23 @@ function createSlackListGetItemsTool(state) {
|
|
|
4114
4356
|
}
|
|
4115
4357
|
|
|
4116
4358
|
// src/chat/tools/slack-list-update-item.ts
|
|
4117
|
-
import { Type as
|
|
4359
|
+
import { Type as Type14 } from "@sinclair/typebox";
|
|
4118
4360
|
function createSlackListUpdateItemTool(state) {
|
|
4119
4361
|
return tool({
|
|
4120
4362
|
description: "Update an item in the active Slack list tracked in artifact context (title/completion). Use when the user asks to mark progress or rename a tracked task. Do not use to add new tasks.",
|
|
4121
|
-
inputSchema:
|
|
4363
|
+
inputSchema: Type14.Object(
|
|
4122
4364
|
{
|
|
4123
|
-
item_id:
|
|
4365
|
+
item_id: Type14.String({
|
|
4124
4366
|
minLength: 1,
|
|
4125
4367
|
description: "ID of the Slack list item to update."
|
|
4126
4368
|
}),
|
|
4127
|
-
completed:
|
|
4128
|
-
|
|
4369
|
+
completed: Type14.Optional(
|
|
4370
|
+
Type14.Boolean({
|
|
4129
4371
|
description: "Optional completion status update."
|
|
4130
4372
|
})
|
|
4131
4373
|
),
|
|
4132
|
-
title:
|
|
4133
|
-
|
|
4374
|
+
title: Type14.Optional(
|
|
4375
|
+
Type14.String({
|
|
4134
4376
|
minLength: 1,
|
|
4135
4377
|
description: "Optional new item title."
|
|
4136
4378
|
})
|
|
@@ -4180,11 +4422,11 @@ function createSlackListUpdateItemTool(state) {
|
|
|
4180
4422
|
}
|
|
4181
4423
|
|
|
4182
4424
|
// src/chat/tools/system-time.ts
|
|
4183
|
-
import { Type as
|
|
4425
|
+
import { Type as Type15 } from "@sinclair/typebox";
|
|
4184
4426
|
function createSystemTimeTool() {
|
|
4185
4427
|
return tool({
|
|
4186
4428
|
description: "Return current system time in UTC and local ISO formats. Use when the user asks for current time/date context. Do not use as a substitute for historical or timezone-conversion research.",
|
|
4187
|
-
inputSchema:
|
|
4429
|
+
inputSchema: Type15.Object({}),
|
|
4188
4430
|
execute: async () => {
|
|
4189
4431
|
const now = /* @__PURE__ */ new Date();
|
|
4190
4432
|
return {
|
|
@@ -4199,7 +4441,7 @@ function createSystemTimeTool() {
|
|
|
4199
4441
|
}
|
|
4200
4442
|
|
|
4201
4443
|
// src/chat/tools/web-fetch.ts
|
|
4202
|
-
import { Type as
|
|
4444
|
+
import { Type as Type16 } from "@sinclair/typebox";
|
|
4203
4445
|
|
|
4204
4446
|
// src/chat/tools/constants.ts
|
|
4205
4447
|
var USER_AGENT = "junior-bot/0.1";
|
|
@@ -4528,13 +4770,13 @@ function extractHttpStatusFromMessage(message) {
|
|
|
4528
4770
|
function createWebFetchTool(hooks) {
|
|
4529
4771
|
return tool({
|
|
4530
4772
|
description: "Fetch and extract readable content from a specific URL. Use when you need details from a known page or document. Do not use for discovery when search is the first step.",
|
|
4531
|
-
inputSchema:
|
|
4532
|
-
url:
|
|
4773
|
+
inputSchema: Type16.Object({
|
|
4774
|
+
url: Type16.String({
|
|
4533
4775
|
minLength: 1,
|
|
4534
4776
|
description: "HTTP(S) URL to fetch."
|
|
4535
4777
|
}),
|
|
4536
|
-
max_chars:
|
|
4537
|
-
|
|
4778
|
+
max_chars: Type16.Optional(
|
|
4779
|
+
Type16.Integer({
|
|
4538
4780
|
minimum: 500,
|
|
4539
4781
|
maximum: MAX_FETCH_CHARS,
|
|
4540
4782
|
description: "Optional maximum number of extracted characters to return."
|
|
@@ -4587,7 +4829,7 @@ function createWebFetchTool(hooks) {
|
|
|
4587
4829
|
// src/chat/tools/web-search.ts
|
|
4588
4830
|
import { generateText } from "ai";
|
|
4589
4831
|
import { createGatewayProvider } from "@ai-sdk/gateway";
|
|
4590
|
-
import { Type as
|
|
4832
|
+
import { Type as Type17 } from "@sinclair/typebox";
|
|
4591
4833
|
var SEARCH_TIMEOUT_MS = 1e4;
|
|
4592
4834
|
var MAX_RESULTS = 5;
|
|
4593
4835
|
var DEFAULT_SEARCH_MODEL = "xai/grok-4-fast-reasoning";
|
|
@@ -4638,14 +4880,14 @@ function isTimeoutSearchFailure(message) {
|
|
|
4638
4880
|
function createWebSearchTool() {
|
|
4639
4881
|
return tool({
|
|
4640
4882
|
description: "Search public web sources and return top snippets/URLs. Use when you need discovery or source candidates. Do not use when the user already provided a specific URL to inspect.",
|
|
4641
|
-
inputSchema:
|
|
4642
|
-
query:
|
|
4883
|
+
inputSchema: Type17.Object({
|
|
4884
|
+
query: Type17.String({
|
|
4643
4885
|
minLength: 1,
|
|
4644
4886
|
maxLength: 500,
|
|
4645
4887
|
description: "Search query."
|
|
4646
4888
|
}),
|
|
4647
|
-
max_results:
|
|
4648
|
-
|
|
4889
|
+
max_results: Type17.Optional(
|
|
4890
|
+
Type17.Integer({
|
|
4649
4891
|
minimum: 1,
|
|
4650
4892
|
maximum: MAX_RESULTS,
|
|
4651
4893
|
description: "Max results to return."
|
|
@@ -4710,16 +4952,16 @@ function createWebSearchTool() {
|
|
|
4710
4952
|
}
|
|
4711
4953
|
|
|
4712
4954
|
// src/chat/tools/write-file.ts
|
|
4713
|
-
import { Type as
|
|
4955
|
+
import { Type as Type18 } from "@sinclair/typebox";
|
|
4714
4956
|
function createWriteFileTool() {
|
|
4715
4957
|
return tool({
|
|
4716
4958
|
description: "Write UTF-8 content to a file in the sandbox workspace. Use for intentional file creation or replacement after validation. Do not use for exploratory analysis-only turns.",
|
|
4717
|
-
inputSchema:
|
|
4718
|
-
path:
|
|
4959
|
+
inputSchema: Type18.Object({
|
|
4960
|
+
path: Type18.String({
|
|
4719
4961
|
minLength: 1,
|
|
4720
4962
|
description: "Path to write in the sandbox workspace."
|
|
4721
4963
|
}),
|
|
4722
|
-
content:
|
|
4964
|
+
content: Type18.String({
|
|
4723
4965
|
description: "UTF-8 file content to write."
|
|
4724
4966
|
})
|
|
4725
4967
|
}, { additionalProperties: false }),
|
|
@@ -4793,15 +5035,40 @@ function createTools(availableSkills, hooks = {}, context) {
|
|
|
4793
5035
|
),
|
|
4794
5036
|
systemTime: wrapToolExecution("systemTime", createSystemTimeTool(), hooks),
|
|
4795
5037
|
bash: wrapToolExecution("bash", createBashTool(), hooks),
|
|
5038
|
+
attachFile: wrapToolExecution(
|
|
5039
|
+
"attachFile",
|
|
5040
|
+
createAttachFileTool(context.sandbox, hooks),
|
|
5041
|
+
hooks
|
|
5042
|
+
),
|
|
4796
5043
|
readFile: wrapToolExecution("readFile", createReadFileTool(), hooks),
|
|
4797
5044
|
writeFile: wrapToolExecution("writeFile", createWriteFileTool(), hooks),
|
|
4798
5045
|
webSearch: wrapToolExecution("webSearch", createWebSearchTool(), hooks),
|
|
4799
5046
|
webFetch: wrapToolExecution("webFetch", createWebFetchTool(hooks), hooks),
|
|
4800
|
-
imageGenerate: wrapToolExecution(
|
|
4801
|
-
|
|
4802
|
-
|
|
4803
|
-
|
|
4804
|
-
|
|
5047
|
+
imageGenerate: wrapToolExecution(
|
|
5048
|
+
"imageGenerate",
|
|
5049
|
+
createImageGenerateTool(hooks),
|
|
5050
|
+
hooks
|
|
5051
|
+
),
|
|
5052
|
+
slackCanvasUpdate: wrapToolExecution(
|
|
5053
|
+
"slackCanvasUpdate",
|
|
5054
|
+
createSlackCanvasUpdateTool(state, context),
|
|
5055
|
+
hooks
|
|
5056
|
+
),
|
|
5057
|
+
slackListCreate: wrapToolExecution(
|
|
5058
|
+
"slackListCreate",
|
|
5059
|
+
createSlackListCreateTool(state),
|
|
5060
|
+
hooks
|
|
5061
|
+
),
|
|
5062
|
+
slackListAddItems: wrapToolExecution(
|
|
5063
|
+
"slackListAddItems",
|
|
5064
|
+
createSlackListAddItemsTool(state),
|
|
5065
|
+
hooks
|
|
5066
|
+
),
|
|
5067
|
+
slackListGetItems: wrapToolExecution(
|
|
5068
|
+
"slackListGetItems",
|
|
5069
|
+
createSlackListGetItemsTool(state),
|
|
5070
|
+
hooks
|
|
5071
|
+
),
|
|
4805
5072
|
slackListUpdateItem: wrapToolExecution(
|
|
4806
5073
|
"slackListUpdateItem",
|
|
4807
5074
|
createSlackListUpdateItemTool(state),
|
|
@@ -4839,7 +5106,7 @@ function createTools(availableSkills, hooks = {}, context) {
|
|
|
4839
5106
|
|
|
4840
5107
|
// src/chat/sandbox/sandbox.ts
|
|
4841
5108
|
import fs4 from "fs/promises";
|
|
4842
|
-
import
|
|
5109
|
+
import path4 from "path";
|
|
4843
5110
|
import { Sandbox } from "@vercel/sandbox";
|
|
4844
5111
|
import { createBashTool as createBashTool2 } from "bash-tool";
|
|
4845
5112
|
|
|
@@ -4939,20 +5206,32 @@ function extractHttpErrorDetails(error, options = {}) {
|
|
|
4939
5206
|
// src/chat/sandbox/sandbox.ts
|
|
4940
5207
|
var SANDBOX_TOOL_NAMES = /* @__PURE__ */ new Set(["bash", "readFile", "writeFile"]);
|
|
4941
5208
|
var DEFAULT_MAX_OUTPUT_LENGTH = 3e4;
|
|
5209
|
+
var SANDBOX_RUNTIME = "node22";
|
|
4942
5210
|
var SANDBOX_RUNTIME_BIN_DIR = `${SANDBOX_WORKSPACE_ROOT}/.junior/bin`;
|
|
4943
|
-
var SANDBOX_ERROR_FIELDS = [
|
|
5211
|
+
var SANDBOX_ERROR_FIELDS = [
|
|
5212
|
+
{
|
|
5213
|
+
sourceKey: "sandboxId",
|
|
5214
|
+
attributeKey: "sandbox_id",
|
|
5215
|
+
summaryKey: "sandboxId"
|
|
5216
|
+
}
|
|
5217
|
+
];
|
|
4944
5218
|
function mergeNetworkPolicyWithHeaderTransforms(networkPolicy, headerTransforms) {
|
|
4945
5219
|
const basePolicy = networkPolicy && typeof networkPolicy === "object" && !Array.isArray(networkPolicy) ? { ...networkPolicy } : {};
|
|
4946
5220
|
const existingAllowRaw = basePolicy.allow;
|
|
4947
5221
|
const existingAllow = existingAllowRaw && typeof existingAllowRaw === "object" && !Array.isArray(existingAllowRaw) ? Object.fromEntries(
|
|
4948
|
-
Object.entries(existingAllowRaw).map(
|
|
4949
|
-
domain,
|
|
4950
|
-
|
|
4951
|
-
|
|
5222
|
+
Object.entries(existingAllowRaw).map(
|
|
5223
|
+
([domain, rules]) => [
|
|
5224
|
+
domain,
|
|
5225
|
+
Array.isArray(rules) ? [...rules] : []
|
|
5226
|
+
]
|
|
5227
|
+
)
|
|
4952
5228
|
) : { "*": [] };
|
|
4953
5229
|
for (const transform of headerTransforms) {
|
|
4954
5230
|
const currentRules = existingAllow[transform.domain] ?? [];
|
|
4955
|
-
existingAllow[transform.domain] = [
|
|
5231
|
+
existingAllow[transform.domain] = [
|
|
5232
|
+
...currentRules,
|
|
5233
|
+
{ transform: [{ headers: transform.headers }] }
|
|
5234
|
+
];
|
|
4956
5235
|
}
|
|
4957
5236
|
return {
|
|
4958
5237
|
...basePolicy,
|
|
@@ -4972,7 +5251,7 @@ function truncateOutput(output, maxLength) {
|
|
|
4972
5251
|
};
|
|
4973
5252
|
}
|
|
4974
5253
|
function toPosixRelative(base, absolute) {
|
|
4975
|
-
return
|
|
5254
|
+
return path4.relative(base, absolute).split(path4.sep).join("/");
|
|
4976
5255
|
}
|
|
4977
5256
|
async function listFilesRecursive(root) {
|
|
4978
5257
|
const queue = [root];
|
|
@@ -4982,7 +5261,7 @@ async function listFilesRecursive(root) {
|
|
|
4982
5261
|
const entries = await fs4.readdir(dir, { withFileTypes: true });
|
|
4983
5262
|
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
4984
5263
|
for (const entry of entries) {
|
|
4985
|
-
const absolute =
|
|
5264
|
+
const absolute = path4.join(dir, entry.name);
|
|
4986
5265
|
if (entry.isDirectory()) {
|
|
4987
5266
|
queue.push(absolute);
|
|
4988
5267
|
} else if (entry.isFile()) {
|
|
@@ -5024,7 +5303,7 @@ async function buildSkillSyncFiles(availableSkills) {
|
|
|
5024
5303
|
function collectDirectories(filesToWrite) {
|
|
5025
5304
|
const directoriesToEnsure = /* @__PURE__ */ new Set();
|
|
5026
5305
|
for (const file of filesToWrite) {
|
|
5027
|
-
const normalizedPath =
|
|
5306
|
+
const normalizedPath = path4.posix.normalize(file.path);
|
|
5028
5307
|
const parts = normalizedPath.split("/").filter(Boolean);
|
|
5029
5308
|
let current = "";
|
|
5030
5309
|
for (let index = 0; index < parts.length - 1; index += 1) {
|
|
@@ -5032,7 +5311,9 @@ function collectDirectories(filesToWrite) {
|
|
|
5032
5311
|
directoriesToEnsure.add(current);
|
|
5033
5312
|
}
|
|
5034
5313
|
}
|
|
5035
|
-
return Array.from(directoriesToEnsure).filter(
|
|
5314
|
+
return Array.from(directoriesToEnsure).filter(
|
|
5315
|
+
(directory) => directory === SANDBOX_WORKSPACE_ROOT || directory.startsWith(`${SANDBOX_WORKSPACE_ROOT}/`)
|
|
5316
|
+
).sort((a, b) => a.length - b.length);
|
|
5036
5317
|
}
|
|
5037
5318
|
function getSandboxErrorDetails(error) {
|
|
5038
5319
|
return extractHttpErrorDetails(error, {
|
|
@@ -5086,7 +5367,9 @@ function wrapSandboxSetupError(error) {
|
|
|
5086
5367
|
try {
|
|
5087
5368
|
const details = getSandboxErrorDetails(error);
|
|
5088
5369
|
if (details.summary) {
|
|
5089
|
-
return new Error(`sandbox setup failed (${details.summary})`, {
|
|
5370
|
+
return new Error(`sandbox setup failed (${details.summary})`, {
|
|
5371
|
+
cause: error
|
|
5372
|
+
});
|
|
5090
5373
|
}
|
|
5091
5374
|
} catch {
|
|
5092
5375
|
}
|
|
@@ -5112,9 +5395,12 @@ function throwSandboxOperationError(action, error, includeMissingPath = false) {
|
|
|
5112
5395
|
"app.sandbox.success": false
|
|
5113
5396
|
});
|
|
5114
5397
|
setSpanStatus("error");
|
|
5115
|
-
throw new Error(
|
|
5116
|
-
|
|
5117
|
-
|
|
5398
|
+
throw new Error(
|
|
5399
|
+
details.summary ? `${action} failed (${details.summary})` : `${action} failed`,
|
|
5400
|
+
{
|
|
5401
|
+
cause: error
|
|
5402
|
+
}
|
|
5403
|
+
);
|
|
5118
5404
|
}
|
|
5119
5405
|
function createSandboxExecutor(options) {
|
|
5120
5406
|
let sandbox = null;
|
|
@@ -5124,6 +5410,7 @@ function createSandboxExecutor(options) {
|
|
|
5124
5410
|
const timeoutMs = options?.timeoutMs ?? 1e3 * 60 * 30;
|
|
5125
5411
|
const traceContext = options?.traceContext ?? {};
|
|
5126
5412
|
const emitStatus = options?.onStatus;
|
|
5413
|
+
const dependencyProfileHash = getRuntimeDependencyProfileHash(SANDBOX_RUNTIME);
|
|
5127
5414
|
const withSandboxSpan = (name, op, attributes, callback) => withSpan(name, op, traceContext, callback, attributes);
|
|
5128
5415
|
const invalidateSandboxInstance = async (targetSandbox, reason) => {
|
|
5129
5416
|
if (sandbox === targetSandbox) {
|
|
@@ -5153,7 +5440,10 @@ function createSandboxExecutor(options) {
|
|
|
5153
5440
|
},
|
|
5154
5441
|
async () => {
|
|
5155
5442
|
const filesToWrite = await buildSkillSyncFiles(availableSkills);
|
|
5156
|
-
const bytesWritten = filesToWrite.reduce(
|
|
5443
|
+
const bytesWritten = filesToWrite.reduce(
|
|
5444
|
+
(total, file) => total + file.content.length,
|
|
5445
|
+
0
|
|
5446
|
+
);
|
|
5157
5447
|
const directories = collectDirectories(filesToWrite);
|
|
5158
5448
|
await withSandboxSpan(
|
|
5159
5449
|
"sandbox.sync_writeFiles",
|
|
@@ -5204,7 +5494,7 @@ function createSandboxExecutor(options) {
|
|
|
5204
5494
|
throw wrapSandboxSetupError(error);
|
|
5205
5495
|
};
|
|
5206
5496
|
const createFreshSandbox = async () => {
|
|
5207
|
-
const runtime =
|
|
5497
|
+
const runtime = SANDBOX_RUNTIME;
|
|
5208
5498
|
let statusCount = 0;
|
|
5209
5499
|
const sentStatuses = /* @__PURE__ */ new Set();
|
|
5210
5500
|
const emitSandboxStatus = async (status) => {
|
|
@@ -5253,9 +5543,13 @@ function createSandboxExecutor(options) {
|
|
|
5253
5543
|
"app.sandbox.source": snapshot.snapshotId ? "snapshot" : "created",
|
|
5254
5544
|
"app.sandbox.snapshot.cache_hit": snapshot.cacheHit,
|
|
5255
5545
|
"app.sandbox.snapshot.resolve_outcome": snapshot.resolveOutcome,
|
|
5256
|
-
...snapshot.profileHash ? {
|
|
5546
|
+
...snapshot.profileHash ? {
|
|
5547
|
+
"app.sandbox.snapshot.profile_hash": snapshot.profileHash
|
|
5548
|
+
} : {},
|
|
5257
5549
|
"app.sandbox.snapshot.dependency_count": snapshot.dependencyCount,
|
|
5258
|
-
...snapshot.rebuildReason ? {
|
|
5550
|
+
...snapshot.rebuildReason ? {
|
|
5551
|
+
"app.sandbox.snapshot.rebuild_reason": snapshot.rebuildReason
|
|
5552
|
+
} : {}
|
|
5259
5553
|
});
|
|
5260
5554
|
if (!snapshot.snapshotId) {
|
|
5261
5555
|
await emitSandboxStatus("Starting sandbox...");
|
|
@@ -5290,7 +5584,9 @@ function createSandboxExecutor(options) {
|
|
|
5290
5584
|
if (!rebuiltSnapshot.snapshotId) {
|
|
5291
5585
|
throw error;
|
|
5292
5586
|
}
|
|
5293
|
-
await emitSandboxStatus(
|
|
5587
|
+
await emitSandboxStatus(
|
|
5588
|
+
"Retrying sandbox startup with a fresh snapshot..."
|
|
5589
|
+
);
|
|
5294
5590
|
return await Sandbox.create({
|
|
5295
5591
|
timeout: timeoutMs,
|
|
5296
5592
|
source: {
|
|
@@ -5311,6 +5607,17 @@ function createSandboxExecutor(options) {
|
|
|
5311
5607
|
}
|
|
5312
5608
|
return assignSandbox(createdSandbox);
|
|
5313
5609
|
};
|
|
5610
|
+
if (!sandbox && sandboxIdHint && dependencyProfileHash !== options?.sandboxDependencyProfileHash) {
|
|
5611
|
+
setSpanAttributes({
|
|
5612
|
+
"app.sandbox.reused": false,
|
|
5613
|
+
"app.sandbox.recreate.reason": "dependency_profile_mismatch",
|
|
5614
|
+
...options?.sandboxDependencyProfileHash ? {
|
|
5615
|
+
"app.sandbox.previous_profile_hash": options.sandboxDependencyProfileHash
|
|
5616
|
+
} : {},
|
|
5617
|
+
...dependencyProfileHash ? { "app.sandbox.current_profile_hash": dependencyProfileHash } : {}
|
|
5618
|
+
});
|
|
5619
|
+
sandboxIdHint = void 0;
|
|
5620
|
+
}
|
|
5314
5621
|
const recoverUnavailableSandbox = async (source) => {
|
|
5315
5622
|
setSpanAttributes({
|
|
5316
5623
|
"app.sandbox.recovery.attempted": true,
|
|
@@ -5405,11 +5712,16 @@ function createSandboxExecutor(options) {
|
|
|
5405
5712
|
const restoreNetworkPolicy = activeSandbox.networkPolicy ?? "allow-all";
|
|
5406
5713
|
const headerTransforms = input.headerTransforms;
|
|
5407
5714
|
if (headerTransforms && headerTransforms.length > 0) {
|
|
5408
|
-
const policy = mergeNetworkPolicyWithHeaderTransforms(
|
|
5715
|
+
const policy = mergeNetworkPolicyWithHeaderTransforms(
|
|
5716
|
+
restoreNetworkPolicy,
|
|
5717
|
+
headerTransforms
|
|
5718
|
+
);
|
|
5409
5719
|
await activeSandbox.updateNetworkPolicy(policy);
|
|
5410
5720
|
}
|
|
5411
5721
|
const pathPrefix = `${SANDBOX_RUNTIME_BIN_DIR}:$PATH`;
|
|
5412
|
-
const envExports = input.env ? Object.entries(input.env).map(
|
|
5722
|
+
const envExports = input.env ? Object.entries(input.env).map(
|
|
5723
|
+
([key, value]) => `export ${key}='${value.replace(/'/g, "'\\''")}'`
|
|
5724
|
+
).join(" && ") : "";
|
|
5413
5725
|
const preamble = envExports ? `export PATH="${pathPrefix}" && ${envExports}` : `export PATH="${pathPrefix}"`;
|
|
5414
5726
|
let commandError;
|
|
5415
5727
|
try {
|
|
@@ -5418,7 +5730,10 @@ function createSandboxExecutor(options) {
|
|
|
5418
5730
|
args: ["-c", `${preamble} && ${input.command}`],
|
|
5419
5731
|
cwd: SANDBOX_WORKSPACE_ROOT
|
|
5420
5732
|
});
|
|
5421
|
-
const maxOutputLength = Number.parseInt(
|
|
5733
|
+
const maxOutputLength = Number.parseInt(
|
|
5734
|
+
process.env.SANDBOX_BASH_MAX_OUTPUT_CHARS ?? "",
|
|
5735
|
+
10
|
|
5736
|
+
);
|
|
5422
5737
|
const boundedOutputLength = Number.isFinite(maxOutputLength) && maxOutputLength > 0 ? maxOutputLength : DEFAULT_MAX_OUTPUT_LENGTH;
|
|
5423
5738
|
const stdoutRaw = await commandResult2.stdout();
|
|
5424
5739
|
const stderrRaw = await commandResult2.stderr();
|
|
@@ -5473,7 +5788,10 @@ function createSandboxExecutor(options) {
|
|
|
5473
5788
|
}
|
|
5474
5789
|
}
|
|
5475
5790
|
const activeSandbox = await createSandbox();
|
|
5476
|
-
const keepAliveMs = Number.parseInt(
|
|
5791
|
+
const keepAliveMs = Number.parseInt(
|
|
5792
|
+
process.env.VERCEL_SANDBOX_KEEPALIVE_MS ?? "0",
|
|
5793
|
+
10
|
|
5794
|
+
);
|
|
5477
5795
|
if (Number.isFinite(keepAliveMs) && keepAliveMs > 0) {
|
|
5478
5796
|
try {
|
|
5479
5797
|
await withSandboxSpan(
|
|
@@ -5492,12 +5810,18 @@ function createSandboxExecutor(options) {
|
|
|
5492
5810
|
if (params.toolName === "bash") {
|
|
5493
5811
|
const command = bashCommand;
|
|
5494
5812
|
const headerTransformsInput = rawInput.headerTransforms;
|
|
5495
|
-
const headerTransforms = Array.isArray(headerTransformsInput) ? headerTransformsInput.filter(
|
|
5813
|
+
const headerTransforms = Array.isArray(headerTransformsInput) ? headerTransformsInput.filter(
|
|
5814
|
+
(value) => Boolean(value && typeof value === "object")
|
|
5815
|
+
).map((transform) => ({
|
|
5496
5816
|
domain: String(transform.domain ?? "").trim(),
|
|
5497
5817
|
headers: transform.headers && typeof transform.headers === "object" && !Array.isArray(transform.headers) ? Object.fromEntries(
|
|
5498
|
-
Object.entries(
|
|
5818
|
+
Object.entries(
|
|
5819
|
+
transform.headers
|
|
5820
|
+
).filter(([, value]) => typeof value === "string").map(([key, value]) => [key, value])
|
|
5499
5821
|
) : {}
|
|
5500
|
-
})).filter(
|
|
5822
|
+
})).filter(
|
|
5823
|
+
(transform) => transform.domain.length > 0 && Object.keys(transform.headers).length > 0
|
|
5824
|
+
) : void 0;
|
|
5501
5825
|
const envInput = rawInput.env;
|
|
5502
5826
|
const env = envInput && typeof envInput === "object" && !Array.isArray(envInput) ? Object.fromEntries(
|
|
5503
5827
|
Object.entries(envInput).filter(([, value]) => typeof value === "string").map(([key, value]) => [key, value])
|
|
@@ -5511,11 +5835,21 @@ function createSandboxExecutor(options) {
|
|
|
5511
5835
|
},
|
|
5512
5836
|
async () => {
|
|
5513
5837
|
try {
|
|
5514
|
-
const response = await executeBash({
|
|
5838
|
+
const response = await executeBash({
|
|
5839
|
+
command,
|
|
5840
|
+
...headerTransforms ? { headerTransforms } : {},
|
|
5841
|
+
...env ? { env } : {}
|
|
5842
|
+
});
|
|
5515
5843
|
setSpanAttributes({
|
|
5516
5844
|
"process.exit.code": response.exitCode,
|
|
5517
|
-
"app.sandbox.stdout_bytes": Buffer.byteLength(
|
|
5518
|
-
|
|
5845
|
+
"app.sandbox.stdout_bytes": Buffer.byteLength(
|
|
5846
|
+
response.stdout ?? "",
|
|
5847
|
+
"utf8"
|
|
5848
|
+
),
|
|
5849
|
+
"app.sandbox.stderr_bytes": Buffer.byteLength(
|
|
5850
|
+
response.stderr ?? "",
|
|
5851
|
+
"utf8"
|
|
5852
|
+
),
|
|
5519
5853
|
...response.exitCode !== 0 ? { "error.type": "nonzero_exit" } : {}
|
|
5520
5854
|
});
|
|
5521
5855
|
setSpanStatus(response.exitCode === 0 ? "ok" : "error");
|
|
@@ -5630,6 +5964,9 @@ function createSandboxExecutor(options) {
|
|
|
5630
5964
|
getSandboxId() {
|
|
5631
5965
|
return sandbox?.sandboxId ?? sandboxIdHint;
|
|
5632
5966
|
},
|
|
5967
|
+
getDependencyProfileHash() {
|
|
5968
|
+
return dependencyProfileHash;
|
|
5969
|
+
},
|
|
5633
5970
|
canExecute(toolName) {
|
|
5634
5971
|
return SANDBOX_TOOL_NAMES.has(toolName);
|
|
5635
5972
|
},
|
|
@@ -5742,6 +6079,35 @@ function isRetryableTurnError(error, reason) {
|
|
|
5742
6079
|
return error.reason === reason;
|
|
5743
6080
|
}
|
|
5744
6081
|
|
|
6082
|
+
// src/chat/attachment-claims.ts
|
|
6083
|
+
function splitSentences(text) {
|
|
6084
|
+
return text.split(/\n+/).flatMap((line) => line.split(/(?<=[.!?])\s+/)).map((part) => part.trim()).filter((part) => part.length > 0);
|
|
6085
|
+
}
|
|
6086
|
+
function sentenceClaimsAttachment(sentence) {
|
|
6087
|
+
const hasAttachmentNoun = /\b(screenshot|image|file|attachment)\b/i.test(
|
|
6088
|
+
sentence
|
|
6089
|
+
);
|
|
6090
|
+
if (!hasAttachmentNoun) {
|
|
6091
|
+
return false;
|
|
6092
|
+
}
|
|
6093
|
+
const hasPositiveAttachmentVerb = /\b(attached|shared|uploaded|included)\b/i.test(sentence);
|
|
6094
|
+
const hasDeicticSharePhrase = /\bhere(?:'s| is)\b/i.test(sentence);
|
|
6095
|
+
return hasPositiveAttachmentVerb || hasDeicticSharePhrase;
|
|
6096
|
+
}
|
|
6097
|
+
function claimsAttachment(text) {
|
|
6098
|
+
return splitSentences(text).some(
|
|
6099
|
+
(sentence) => sentenceClaimsAttachment(sentence)
|
|
6100
|
+
);
|
|
6101
|
+
}
|
|
6102
|
+
function enforceAttachmentClaimTruth(text, hasAttachedFiles) {
|
|
6103
|
+
if (hasAttachedFiles || !claimsAttachment(text)) {
|
|
6104
|
+
return text;
|
|
6105
|
+
}
|
|
6106
|
+
return `${text}
|
|
6107
|
+
|
|
6108
|
+
Note: No file was attached in this turn. I need to attach the file before claiming it is shared.`;
|
|
6109
|
+
}
|
|
6110
|
+
|
|
5745
6111
|
// src/chat/respond.ts
|
|
5746
6112
|
var MAX_INLINE_ATTACHMENT_BASE64_CHARS = 12e4;
|
|
5747
6113
|
var startupDiscoveryLogged = false;
|
|
@@ -5809,7 +6175,8 @@ function isToolPayloadShape(payload) {
|
|
|
5809
6175
|
const record = payload;
|
|
5810
6176
|
const type = typeof record.type === "string" ? record.type.toLowerCase() : "";
|
|
5811
6177
|
if (type.startsWith("tool-")) return true;
|
|
5812
|
-
if (type === "tool_use" || type === "tool_call" || type === "tool_result" || type === "tool_error")
|
|
6178
|
+
if (type === "tool_use" || type === "tool_call" || type === "tool_result" || type === "tool_error")
|
|
6179
|
+
return true;
|
|
5813
6180
|
const hasToolName = typeof record.toolName === "string" || typeof record.name === "string";
|
|
5814
6181
|
const hasToolInput = Object.prototype.hasOwnProperty.call(record, "input") || Object.prototype.hasOwnProperty.call(record, "args");
|
|
5815
6182
|
if (hasToolName && hasToolInput) return true;
|
|
@@ -5853,7 +6220,7 @@ function formatToolStatus(toolName) {
|
|
|
5853
6220
|
}
|
|
5854
6221
|
function formatToolStatusWithInput(toolName, input) {
|
|
5855
6222
|
const obj = input && typeof input === "object" ? input : void 0;
|
|
5856
|
-
const
|
|
6223
|
+
const path6 = obj ? compactStatusPath(obj.path) : void 0;
|
|
5857
6224
|
const filename = obj ? compactStatusFilename(obj.path) : void 0;
|
|
5858
6225
|
const query = obj ? compactStatusText(obj.query, 70) : void 0;
|
|
5859
6226
|
const domain = obj ? extractStatusUrlDomain(obj.url) : void 0;
|
|
@@ -5861,8 +6228,8 @@ function formatToolStatusWithInput(toolName, input) {
|
|
|
5861
6228
|
if (filename && toolName === "readFile") {
|
|
5862
6229
|
return `Reading file ${filename}`;
|
|
5863
6230
|
}
|
|
5864
|
-
if (
|
|
5865
|
-
return `Writing file ${
|
|
6231
|
+
if (path6 && toolName === "writeFile") {
|
|
6232
|
+
return `Writing file ${path6}`;
|
|
5866
6233
|
}
|
|
5867
6234
|
if (skillName && toolName === "loadSkill") {
|
|
5868
6235
|
return `Loading skill ${skillName}`;
|
|
@@ -5902,7 +6269,7 @@ function formatToolResultStatus(toolName) {
|
|
|
5902
6269
|
}
|
|
5903
6270
|
function formatToolResultStatusWithInput(toolName, input) {
|
|
5904
6271
|
const obj = input && typeof input === "object" ? input : void 0;
|
|
5905
|
-
const
|
|
6272
|
+
const path6 = obj ? compactStatusPath(obj.path) : void 0;
|
|
5906
6273
|
const filename = obj ? compactStatusFilename(obj.path) : void 0;
|
|
5907
6274
|
const query = obj ? compactStatusText(obj.query, 70) : void 0;
|
|
5908
6275
|
const domain = obj ? extractStatusUrlDomain(obj.url) : void 0;
|
|
@@ -5910,8 +6277,8 @@ function formatToolResultStatusWithInput(toolName, input) {
|
|
|
5910
6277
|
if (filename && toolName === "readFile") {
|
|
5911
6278
|
return `Reviewed file ${filename}`;
|
|
5912
6279
|
}
|
|
5913
|
-
if (
|
|
5914
|
-
return `Saved file ${
|
|
6280
|
+
if (path6 && toolName === "writeFile") {
|
|
6281
|
+
return `Saved file ${path6}`;
|
|
5915
6282
|
}
|
|
5916
6283
|
if (skillName && toolName === "loadSkill") {
|
|
5917
6284
|
return `Loaded skill ${skillName}`;
|
|
@@ -6013,11 +6380,16 @@ function isAssistantMessage(value) {
|
|
|
6013
6380
|
}
|
|
6014
6381
|
function extractAssistantText(message) {
|
|
6015
6382
|
const content = message.content ?? [];
|
|
6016
|
-
return content.filter(
|
|
6383
|
+
return content.filter(
|
|
6384
|
+
(part) => part.type === "text" && typeof part.text === "string"
|
|
6385
|
+
).map((part) => part.text).join("\n");
|
|
6017
6386
|
}
|
|
6018
6387
|
function collectRelevantConfigurationKeys(activeSkills, explicitSkill) {
|
|
6019
6388
|
const keys = /* @__PURE__ */ new Set();
|
|
6020
|
-
for (const skill of [
|
|
6389
|
+
for (const skill of [
|
|
6390
|
+
...activeSkills,
|
|
6391
|
+
...explicitSkill ? [explicitSkill] : []
|
|
6392
|
+
]) {
|
|
6021
6393
|
for (const key of skill.usesConfig ?? []) {
|
|
6022
6394
|
keys.add(key);
|
|
6023
6395
|
}
|
|
@@ -6122,7 +6494,9 @@ function createAgentTools(tools, sandbox, spanContext, onStatus, sandboxExecutor
|
|
|
6122
6494
|
...toolResultAttribute2 ? { "gen_ai.tool.call.result": toolResultAttribute2 } : {}
|
|
6123
6495
|
});
|
|
6124
6496
|
setSpanStatus("ok");
|
|
6125
|
-
await onStatus?.(
|
|
6497
|
+
await onStatus?.(
|
|
6498
|
+
`${formatToolResultStatusWithInput(toolName, parsed)}...`
|
|
6499
|
+
);
|
|
6126
6500
|
if (shouldTrace) {
|
|
6127
6501
|
logInfo(
|
|
6128
6502
|
"agent_tool_call_completed",
|
|
@@ -6150,7 +6524,9 @@ function createAgentTools(tools, sandbox, spanContext, onStatus, sandboxExecutor
|
|
|
6150
6524
|
const isCustomBashCommand = toolName === "bash" && /^jr-rpc(?:\s|$)/.test(bashCommand);
|
|
6151
6525
|
const shouldLogCredentialInjection = toolName === "bash" && !isCustomBashCommand && Boolean(injectedHeaders && injectedHeaders.length > 0);
|
|
6152
6526
|
if (shouldLogCredentialInjection) {
|
|
6153
|
-
const headerDomains = (injectedHeaders ?? []).map(
|
|
6527
|
+
const headerDomains = (injectedHeaders ?? []).map(
|
|
6528
|
+
(transform) => transform.domain
|
|
6529
|
+
);
|
|
6154
6530
|
logInfo(
|
|
6155
6531
|
"credential_inject_start",
|
|
6156
6532
|
{},
|
|
@@ -6163,7 +6539,10 @@ function createAgentTools(tools, sandbox, spanContext, onStatus, sandboxExecutor
|
|
|
6163
6539
|
);
|
|
6164
6540
|
}
|
|
6165
6541
|
const hasBashCredentials = injectedHeaders || injectedEnv;
|
|
6166
|
-
const sandboxInput = toolName === "bash" ? { command: String(parsed.command ?? "") } : toolName === "readFile" ? { path: String(parsed.path ?? "") } : toolName === "writeFile" ? {
|
|
6542
|
+
const sandboxInput = toolName === "bash" ? { command: String(parsed.command ?? "") } : toolName === "readFile" ? { path: String(parsed.path ?? "") } : toolName === "writeFile" ? {
|
|
6543
|
+
path: String(parsed.path ?? ""),
|
|
6544
|
+
content: String(parsed.content ?? "")
|
|
6545
|
+
} : parsed;
|
|
6167
6546
|
const result = sandboxExecutor?.canExecute(toolName) ? await sandboxExecutor.execute({
|
|
6168
6547
|
toolName,
|
|
6169
6548
|
input: toolName === "bash" && hasBashCredentials ? {
|
|
@@ -6193,7 +6572,9 @@ function createAgentTools(tools, sandbox, spanContext, onStatus, sandboxExecutor
|
|
|
6193
6572
|
...toolResultAttribute ? { "gen_ai.tool.call.result": toolResultAttribute } : {}
|
|
6194
6573
|
});
|
|
6195
6574
|
setSpanStatus("ok");
|
|
6196
|
-
await onStatus?.(
|
|
6575
|
+
await onStatus?.(
|
|
6576
|
+
`${formatToolResultStatusWithInput(toolName, parsed)}...`
|
|
6577
|
+
);
|
|
6197
6578
|
if (shouldTrace) {
|
|
6198
6579
|
logInfo(
|
|
6199
6580
|
"agent_tool_call_completed",
|
|
@@ -6211,7 +6592,9 @@ function createAgentTools(tools, sandbox, spanContext, onStatus, sandboxExecutor
|
|
|
6211
6592
|
);
|
|
6212
6593
|
}
|
|
6213
6594
|
return {
|
|
6214
|
-
content: [
|
|
6595
|
+
content: [
|
|
6596
|
+
{ type: "text", text: toToolContentText(resultDetails) }
|
|
6597
|
+
],
|
|
6215
6598
|
details: resultDetails
|
|
6216
6599
|
};
|
|
6217
6600
|
} catch (error) {
|
|
@@ -6272,6 +6655,8 @@ async function generateAssistantReply(messageText, context = {}) {
|
|
|
6272
6655
|
let timeoutResumeSessionId;
|
|
6273
6656
|
let timeoutResumeSliceId = 1;
|
|
6274
6657
|
let timeoutResumeMessages = [];
|
|
6658
|
+
let lastKnownSandboxId = context.sandbox?.sandboxId;
|
|
6659
|
+
let lastKnownSandboxDependencyProfileHash = context.sandbox?.sandboxDependencyProfileHash;
|
|
6275
6660
|
try {
|
|
6276
6661
|
const shouldTrace = shouldEmitDevAgentTrace();
|
|
6277
6662
|
const spanContext = {
|
|
@@ -6285,11 +6670,15 @@ async function generateAssistantReply(messageText, context = {}) {
|
|
|
6285
6670
|
assistantUserName: context.assistant?.userName,
|
|
6286
6671
|
modelId: botConfig.modelId
|
|
6287
6672
|
};
|
|
6288
|
-
const availableSkills = await discoverSkills({
|
|
6673
|
+
const availableSkills = await discoverSkills({
|
|
6674
|
+
additionalRoots: context.skillDirs
|
|
6675
|
+
});
|
|
6289
6676
|
if (!startupDiscoveryLogged) {
|
|
6290
6677
|
startupDiscoveryLogged = true;
|
|
6291
6678
|
const plugins = getPluginProviders();
|
|
6292
|
-
const roots = [
|
|
6679
|
+
const roots = [
|
|
6680
|
+
...new Set(availableSkills.map((skill) => skill.skillPath))
|
|
6681
|
+
].sort();
|
|
6293
6682
|
logInfo(
|
|
6294
6683
|
"startup_discovery_summary",
|
|
6295
6684
|
spanContext,
|
|
@@ -6321,17 +6710,18 @@ async function generateAssistantReply(messageText, context = {}) {
|
|
|
6321
6710
|
"Agent message received"
|
|
6322
6711
|
);
|
|
6323
6712
|
}
|
|
6324
|
-
const
|
|
6325
|
-
const
|
|
6713
|
+
const skillInvocation = parseSkillInvocation(userInput, availableSkills);
|
|
6714
|
+
const invokedSkill = skillInvocation ? findSkillByName(skillInvocation.skillName, availableSkills) : null;
|
|
6326
6715
|
const activeSkills = [];
|
|
6327
6716
|
const skillSandbox = new SkillSandbox(availableSkills, activeSkills);
|
|
6328
6717
|
const capabilityRuntime = createSkillCapabilityRuntime({
|
|
6329
|
-
invocationArgs:
|
|
6718
|
+
invocationArgs: skillInvocation?.args,
|
|
6330
6719
|
requesterId: context.requester?.userId,
|
|
6331
6720
|
resolveConfiguration: async (key) => configurationValues[key]
|
|
6332
6721
|
});
|
|
6333
6722
|
const sandboxExecutor = createSandboxExecutor({
|
|
6334
6723
|
sandboxId: context.sandbox?.sandboxId,
|
|
6724
|
+
sandboxDependencyProfileHash: context.sandbox?.sandboxDependencyProfileHash,
|
|
6335
6725
|
traceContext: spanContext,
|
|
6336
6726
|
onStatus: context.onStatus,
|
|
6337
6727
|
runBashCustomCommand: async (command) => {
|
|
@@ -6355,20 +6745,26 @@ async function generateAssistantReply(messageText, context = {}) {
|
|
|
6355
6745
|
return result.handled ? { handled: true, result: result.result } : { handled: false };
|
|
6356
6746
|
}
|
|
6357
6747
|
});
|
|
6748
|
+
lastKnownSandboxId = sandboxExecutor.getSandboxId();
|
|
6749
|
+
lastKnownSandboxDependencyProfileHash = sandboxExecutor.getDependencyProfileHash();
|
|
6358
6750
|
sandboxExecutor.configureSkills(availableSkills);
|
|
6359
6751
|
const sandbox = await sandboxExecutor.createSandbox();
|
|
6360
|
-
if (
|
|
6361
|
-
const preloaded = await skillSandbox.loadSkill(
|
|
6752
|
+
if (invokedSkill && skillInvocation?.source === "hard_bang") {
|
|
6753
|
+
const preloaded = await skillSandbox.loadSkill(invokedSkill.name);
|
|
6362
6754
|
if (preloaded) {
|
|
6363
6755
|
activeSkills.push(preloaded);
|
|
6364
6756
|
}
|
|
6365
6757
|
}
|
|
6366
|
-
const userTurnText = buildUserTurnText(
|
|
6758
|
+
const userTurnText = buildUserTurnText(
|
|
6759
|
+
userInput,
|
|
6760
|
+
context.conversationContext
|
|
6761
|
+
);
|
|
6367
6762
|
if (!getGatewayApiKey()) {
|
|
6368
6763
|
const providerError = "Missing AI gateway credentials (AI_GATEWAY_API_KEY or VERCEL_OIDC_TOKEN)";
|
|
6369
6764
|
return {
|
|
6370
6765
|
text: `Error: ${providerError}`,
|
|
6371
6766
|
sandboxId: sandboxExecutor.getSandboxId(),
|
|
6767
|
+
sandboxDependencyProfileHash: sandboxExecutor.getDependencyProfileHash(),
|
|
6372
6768
|
diagnostics: {
|
|
6373
6769
|
outcome: "provider_error",
|
|
6374
6770
|
modelId: botConfig.modelId,
|
|
@@ -6405,15 +6801,21 @@ async function generateAssistantReply(messageText, context = {}) {
|
|
|
6405
6801
|
Object.assign(artifactStatePatch, patch);
|
|
6406
6802
|
},
|
|
6407
6803
|
onToolCallStart: async (toolName, input) => {
|
|
6408
|
-
await context.onStatus?.(
|
|
6804
|
+
await context.onStatus?.(
|
|
6805
|
+
`${formatToolStatusWithInput(toolName, input)}...`
|
|
6806
|
+
);
|
|
6409
6807
|
},
|
|
6410
6808
|
onToolCallEnd: async (toolName, input) => {
|
|
6411
|
-
await context.onStatus?.(
|
|
6809
|
+
await context.onStatus?.(
|
|
6810
|
+
`${formatToolResultStatusWithInput(toolName, input)}...`
|
|
6811
|
+
);
|
|
6412
6812
|
},
|
|
6413
6813
|
onSkillLoaded: async (loadedSkill) => {
|
|
6414
6814
|
const resolvedSkill = await skillSandbox.loadSkill(loadedSkill.name);
|
|
6415
6815
|
const effective = resolvedSkill ?? loadedSkill;
|
|
6416
|
-
const existing = activeSkills.find(
|
|
6816
|
+
const existing = activeSkills.find(
|
|
6817
|
+
(skill) => skill.name === effective.name
|
|
6818
|
+
);
|
|
6417
6819
|
if (existing) {
|
|
6418
6820
|
existing.body = effective.body;
|
|
6419
6821
|
existing.description = effective.description;
|
|
@@ -6439,17 +6841,18 @@ async function generateAssistantReply(messageText, context = {}) {
|
|
|
6439
6841
|
const baseInstructions = buildSystemPrompt({
|
|
6440
6842
|
availableSkills,
|
|
6441
6843
|
activeSkills,
|
|
6442
|
-
invocation:
|
|
6844
|
+
invocation: skillInvocation,
|
|
6443
6845
|
assistant: context.assistant,
|
|
6444
6846
|
requester: context.requester,
|
|
6445
6847
|
artifactState: context.artifactState,
|
|
6446
6848
|
configuration: configurationValues,
|
|
6447
|
-
relevantConfigurationKeys: collectRelevantConfigurationKeys(
|
|
6849
|
+
relevantConfigurationKeys: collectRelevantConfigurationKeys(
|
|
6850
|
+
activeSkills,
|
|
6851
|
+
invokedSkill
|
|
6852
|
+
),
|
|
6448
6853
|
runtimeMetadata: getRuntimeMetadata()
|
|
6449
6854
|
});
|
|
6450
|
-
const userContentParts = [
|
|
6451
|
-
{ type: "text", text: userTurnText }
|
|
6452
|
-
];
|
|
6855
|
+
const userContentParts = [{ type: "text", text: userTurnText }];
|
|
6453
6856
|
for (const attachment of context.userAttachments ?? []) {
|
|
6454
6857
|
if (attachment.mediaType.startsWith("image/")) {
|
|
6455
6858
|
userContentParts.push({
|
|
@@ -6532,7 +6935,9 @@ async function generateAssistantReply(messageText, context = {}) {
|
|
|
6532
6935
|
logWarn(
|
|
6533
6936
|
"streaming_text_delta_error",
|
|
6534
6937
|
{},
|
|
6535
|
-
{
|
|
6938
|
+
{
|
|
6939
|
+
"error.message": error instanceof Error ? error.message : String(error)
|
|
6940
|
+
},
|
|
6536
6941
|
"Failed to deliver text delta to stream"
|
|
6537
6942
|
);
|
|
6538
6943
|
});
|
|
@@ -6541,9 +6946,14 @@ async function generateAssistantReply(messageText, context = {}) {
|
|
|
6541
6946
|
let newMessages = [];
|
|
6542
6947
|
try {
|
|
6543
6948
|
if (resumedFromCheckpoint) {
|
|
6544
|
-
const didReplace = await maybeReplaceAgentMessages(
|
|
6949
|
+
const didReplace = await maybeReplaceAgentMessages(
|
|
6950
|
+
agent,
|
|
6951
|
+
existingTurnCheckpoint.piMessages
|
|
6952
|
+
);
|
|
6545
6953
|
if (!didReplace) {
|
|
6546
|
-
throw new Error(
|
|
6954
|
+
throw new Error(
|
|
6955
|
+
"Agent session resume requested but replaceMessages is unavailable"
|
|
6956
|
+
);
|
|
6547
6957
|
}
|
|
6548
6958
|
}
|
|
6549
6959
|
beforeMessageCount = agent.state.messages.length;
|
|
@@ -6592,10 +7002,16 @@ async function generateAssistantReply(messageText, context = {}) {
|
|
|
6592
7002
|
clearTimeout(timeoutId);
|
|
6593
7003
|
}
|
|
6594
7004
|
}
|
|
6595
|
-
newMessages = agent.state.messages.slice(
|
|
7005
|
+
newMessages = agent.state.messages.slice(
|
|
7006
|
+
beforeMessageCount
|
|
7007
|
+
);
|
|
6596
7008
|
const outputMessages = newMessages.filter(isAssistantMessage);
|
|
6597
7009
|
const outputMessagesAttribute = serializeGenAiAttribute(outputMessages);
|
|
6598
|
-
const usageAttributes = extractGenAiUsageAttributes(
|
|
7010
|
+
const usageAttributes = extractGenAiUsageAttributes(
|
|
7011
|
+
promptResult,
|
|
7012
|
+
agent.state,
|
|
7013
|
+
...outputMessages
|
|
7014
|
+
);
|
|
6599
7015
|
setSpanAttributes({
|
|
6600
7016
|
...outputMessagesAttribute ? { "gen_ai.output.messages": outputMessagesAttribute } : {},
|
|
6601
7017
|
...usageAttributes
|
|
@@ -6623,13 +7039,19 @@ async function generateAssistantReply(messageText, context = {}) {
|
|
|
6623
7039
|
const toolResults = newMessages.filter(isToolResultMessage);
|
|
6624
7040
|
const assistantMessages = newMessages.filter(isAssistantMessage);
|
|
6625
7041
|
const primaryText = assistantMessages.map((message) => extractAssistantText(message)).join("\n\n").trim();
|
|
6626
|
-
const toolErrorCount = toolResults.filter(
|
|
7042
|
+
const toolErrorCount = toolResults.filter(
|
|
7043
|
+
(result) => result.isError
|
|
7044
|
+
).length;
|
|
6627
7045
|
const explicitChannelPostIntent = isExplicitChannelPostIntent(userInput);
|
|
6628
7046
|
const successfulToolNames = new Set(
|
|
6629
7047
|
toolResults.filter((result) => !isToolResultError(result)).map((result) => normalizeToolNameFromResult(result)).filter((value) => Boolean(value))
|
|
6630
7048
|
);
|
|
6631
|
-
const channelPostPerformed = successfulToolNames.has(
|
|
6632
|
-
|
|
7049
|
+
const channelPostPerformed = successfulToolNames.has(
|
|
7050
|
+
"slackChannelPostMessage"
|
|
7051
|
+
);
|
|
7052
|
+
const reactionPerformed = successfulToolNames.has(
|
|
7053
|
+
"slackMessageAddReaction"
|
|
7054
|
+
);
|
|
6633
7055
|
const deliveryPlan = buildReplyDeliveryPlan({
|
|
6634
7056
|
explicitChannelPostIntent,
|
|
6635
7057
|
channelPostPerformed,
|
|
@@ -6663,7 +7085,10 @@ async function generateAssistantReply(messageText, context = {}) {
|
|
|
6663
7085
|
const errorMessage = typeof lastAssistant?.errorMessage === "string" ? lastAssistant.errorMessage : void 0;
|
|
6664
7086
|
const usedPrimaryText = Boolean(primaryText);
|
|
6665
7087
|
const outcome = primaryText ? stopReason === "error" ? "provider_error" : "success" : "execution_failure";
|
|
6666
|
-
const
|
|
7088
|
+
const candidateText = primaryText || buildExecutionFailureMessage(toolErrorCount);
|
|
7089
|
+
const escapedOrRawPayload = isExecutionEscapeResponse(candidateText) || isRawToolPayloadResponse(candidateText);
|
|
7090
|
+
const resolvedText = escapedOrRawPayload ? buildExecutionFailureMessage(toolErrorCount) : enforceAttachmentClaimTruth(candidateText, generatedFiles.length > 0);
|
|
7091
|
+
const resolvedOutcome = escapedOrRawPayload ? "execution_failure" : outcome;
|
|
6667
7092
|
if (shouldTrace) {
|
|
6668
7093
|
logInfo(
|
|
6669
7094
|
"agent_message_out",
|
|
@@ -6672,22 +7097,23 @@ async function generateAssistantReply(messageText, context = {}) {
|
|
|
6672
7097
|
"app.message.kind": "assistant_outbound",
|
|
6673
7098
|
"app.message.length": resolvedText.length,
|
|
6674
7099
|
"app.message.output": summarizeMessageText(resolvedText),
|
|
6675
|
-
"app.ai.outcome":
|
|
7100
|
+
"app.ai.outcome": resolvedOutcome,
|
|
6676
7101
|
"app.ai.assistant_messages": assistantMessages.length,
|
|
6677
7102
|
...stopReason ? { "app.ai.stop_reason": stopReason } : {}
|
|
6678
7103
|
},
|
|
6679
7104
|
"Agent message sent"
|
|
6680
7105
|
);
|
|
6681
7106
|
}
|
|
6682
|
-
if (
|
|
7107
|
+
if (escapedOrRawPayload) {
|
|
6683
7108
|
return {
|
|
6684
|
-
text:
|
|
7109
|
+
text: resolvedText,
|
|
6685
7110
|
files: generatedFiles.length > 0 ? generatedFiles : void 0,
|
|
6686
7111
|
artifactStatePatch: Object.keys(artifactStatePatch).length > 0 ? artifactStatePatch : void 0,
|
|
6687
7112
|
deliveryPlan,
|
|
6688
7113
|
deliveryMode,
|
|
6689
7114
|
ackStrategy,
|
|
6690
7115
|
sandboxId: sandboxExecutor.getSandboxId(),
|
|
7116
|
+
sandboxDependencyProfileHash: sandboxExecutor.getDependencyProfileHash(),
|
|
6691
7117
|
diagnostics: {
|
|
6692
7118
|
outcome: "execution_failure",
|
|
6693
7119
|
modelId: botConfig.modelId,
|
|
@@ -6710,6 +7136,7 @@ async function generateAssistantReply(messageText, context = {}) {
|
|
|
6710
7136
|
deliveryMode,
|
|
6711
7137
|
ackStrategy,
|
|
6712
7138
|
sandboxId: sandboxExecutor.getSandboxId(),
|
|
7139
|
+
sandboxDependencyProfileHash: sandboxExecutor.getDependencyProfileHash(),
|
|
6713
7140
|
diagnostics: {
|
|
6714
7141
|
outcome,
|
|
6715
7142
|
modelId: botConfig.modelId,
|
|
@@ -6747,7 +7174,10 @@ async function generateAssistantReply(messageText, context = {}) {
|
|
|
6747
7174
|
"Agent turn timed out and will be resumed"
|
|
6748
7175
|
);
|
|
6749
7176
|
try {
|
|
6750
|
-
const latestCheckpoint = await getAgentTurnSessionCheckpoint(
|
|
7177
|
+
const latestCheckpoint = await getAgentTurnSessionCheckpoint(
|
|
7178
|
+
timeoutResumeConversationId,
|
|
7179
|
+
timeoutResumeSessionId
|
|
7180
|
+
);
|
|
6751
7181
|
const piMessages = timeoutResumeMessages.length > 0 ? timeoutResumeMessages : latestCheckpoint?.piMessages ?? [];
|
|
6752
7182
|
await upsertAgentTurnSessionCheckpoint({
|
|
6753
7183
|
conversationId: timeoutResumeConversationId,
|
|
@@ -6787,18 +7217,25 @@ async function generateAssistantReply(messageText, context = {}) {
|
|
|
6787
7217
|
if (isRetryableTurnError(error)) {
|
|
6788
7218
|
throw error;
|
|
6789
7219
|
}
|
|
6790
|
-
logException(
|
|
6791
|
-
|
|
6792
|
-
|
|
6793
|
-
|
|
6794
|
-
|
|
6795
|
-
|
|
6796
|
-
|
|
6797
|
-
|
|
7220
|
+
logException(
|
|
7221
|
+
error,
|
|
7222
|
+
"assistant_reply_generation_failed",
|
|
7223
|
+
{
|
|
7224
|
+
slackThreadId: context.correlation?.threadId,
|
|
7225
|
+
slackUserId: context.correlation?.requesterId,
|
|
7226
|
+
slackChannelId: context.correlation?.channelId,
|
|
7227
|
+
runId: context.correlation?.runId,
|
|
7228
|
+
assistantUserName: context.assistant?.userName,
|
|
7229
|
+
modelId: botConfig.modelId
|
|
7230
|
+
},
|
|
7231
|
+
{},
|
|
7232
|
+
"generateAssistantReply failed"
|
|
7233
|
+
);
|
|
6798
7234
|
const message = error instanceof Error ? error.message : String(error);
|
|
6799
7235
|
return {
|
|
6800
7236
|
text: `Error: ${message}`,
|
|
6801
|
-
sandboxId:
|
|
7237
|
+
sandboxId: lastKnownSandboxId,
|
|
7238
|
+
sandboxDependencyProfileHash: lastKnownSandboxDependencyProfileHash,
|
|
6802
7239
|
diagnostics: {
|
|
6803
7240
|
outcome: "provider_error",
|
|
6804
7241
|
modelId: botConfig.modelId,
|
|
@@ -7812,10 +8249,11 @@ function createAppSlackRuntime(deps) {
|
|
|
7812
8249
|
|
|
7813
8250
|
// src/chat/app-home.ts
|
|
7814
8251
|
import fs5 from "fs";
|
|
7815
|
-
import
|
|
8252
|
+
import path5 from "path";
|
|
7816
8253
|
var DEFAULT_ABOUT_TEXT = "I help your team investigate, summarize, and act on work in Slack.";
|
|
7817
8254
|
var MAX_HOME_SKILLS = 6;
|
|
7818
8255
|
var MAX_SECTION_TEXT_CHARS = 3e3;
|
|
8256
|
+
var HIDDEN_HOME_SKILLS = /* @__PURE__ */ new Set(["jr-rpc"]);
|
|
7819
8257
|
function clampSectionText(text) {
|
|
7820
8258
|
if (text.length <= MAX_SECTION_TEXT_CHARS) {
|
|
7821
8259
|
return text;
|
|
@@ -7823,7 +8261,7 @@ function clampSectionText(text) {
|
|
|
7823
8261
|
return `${text.slice(0, MAX_SECTION_TEXT_CHARS - 1)}\u2026`;
|
|
7824
8262
|
}
|
|
7825
8263
|
function loadAboutText() {
|
|
7826
|
-
const aboutPath =
|
|
8264
|
+
const aboutPath = path5.join(homeDir(), "ABOUT.md");
|
|
7827
8265
|
try {
|
|
7828
8266
|
const raw = fs5.readFileSync(aboutPath, "utf8").trim();
|
|
7829
8267
|
if (raw.length > 0) {
|
|
@@ -7834,12 +8272,12 @@ function loadAboutText() {
|
|
|
7834
8272
|
return DEFAULT_ABOUT_TEXT;
|
|
7835
8273
|
}
|
|
7836
8274
|
async function buildSkillsSummaryText() {
|
|
7837
|
-
const skills = await discoverSkills();
|
|
8275
|
+
const skills = (await discoverSkills()).filter((skill) => !HIDDEN_HOME_SKILLS.has(skill.name));
|
|
7838
8276
|
if (skills.length === 0) {
|
|
7839
8277
|
return "No skills installed.";
|
|
7840
8278
|
}
|
|
7841
8279
|
const visible = skills.slice(0, MAX_HOME_SKILLS);
|
|
7842
|
-
const lines = visible.map((skill) => `\u2022
|
|
8280
|
+
const lines = visible.map((skill) => `\u2022 \`!${skill.name}\` \u2014 ${skill.description}`);
|
|
7843
8281
|
if (skills.length > visible.length) {
|
|
7844
8282
|
lines.push(`\u2022 \u2026and ${skills.length - visible.length} more`);
|
|
7845
8283
|
}
|
|
@@ -7939,9 +8377,6 @@ async function publishAppHomeView(slackClient, userId, userTokenStore) {
|
|
|
7939
8377
|
}
|
|
7940
8378
|
|
|
7941
8379
|
// src/chat/slash-command.ts
|
|
7942
|
-
function providerLabel(provider) {
|
|
7943
|
-
return provider.charAt(0).toUpperCase() + provider.slice(1);
|
|
7944
|
-
}
|
|
7945
8380
|
async function postEphemeral(event, text) {
|
|
7946
8381
|
await event.channel.postEphemeral(event.user, text, { fallbackToDM: false });
|
|
7947
8382
|
}
|
|
@@ -7950,10 +8385,10 @@ async function handleLink(event, provider) {
|
|
|
7950
8385
|
await postEphemeral(event, `Unknown provider: \`${provider}\``);
|
|
7951
8386
|
return;
|
|
7952
8387
|
}
|
|
7953
|
-
if (!
|
|
8388
|
+
if (!getPluginOAuthConfig(provider)) {
|
|
7954
8389
|
await postEphemeral(
|
|
7955
8390
|
event,
|
|
7956
|
-
`${
|
|
8391
|
+
`${formatProviderLabel(provider)} doesn't support account linking.`
|
|
7957
8392
|
);
|
|
7958
8393
|
return;
|
|
7959
8394
|
}
|
|
@@ -7967,7 +8402,10 @@ async function handleLink(event, provider) {
|
|
|
7967
8402
|
return;
|
|
7968
8403
|
}
|
|
7969
8404
|
if (result.delivery === "fallback_dm") {
|
|
7970
|
-
await postEphemeral(
|
|
8405
|
+
await postEphemeral(
|
|
8406
|
+
event,
|
|
8407
|
+
`Check your DMs for a ${formatProviderLabel(provider)} authorization link.`
|
|
8408
|
+
);
|
|
7971
8409
|
} else if (result.delivery === false) {
|
|
7972
8410
|
await postEphemeral(
|
|
7973
8411
|
event,
|
|
@@ -7980,10 +8418,10 @@ async function handleUnlink(event, provider) {
|
|
|
7980
8418
|
await postEphemeral(event, `Unknown provider: \`${provider}\``);
|
|
7981
8419
|
return;
|
|
7982
8420
|
}
|
|
7983
|
-
if (!
|
|
8421
|
+
if (!getPluginOAuthConfig(provider)) {
|
|
7984
8422
|
await postEphemeral(
|
|
7985
8423
|
event,
|
|
7986
|
-
`${
|
|
8424
|
+
`${formatProviderLabel(provider)} doesn't support account unlinking.`
|
|
7987
8425
|
);
|
|
7988
8426
|
return;
|
|
7989
8427
|
}
|
|
@@ -7993,14 +8431,20 @@ async function handleUnlink(event, provider) {
|
|
|
7993
8431
|
"slash_command_unlink",
|
|
7994
8432
|
{ slackUserId: event.user.userId },
|
|
7995
8433
|
{ "app.credential.provider": provider },
|
|
7996
|
-
`Unlinked ${
|
|
8434
|
+
`Unlinked ${formatProviderLabel(provider)} account via /jr slash command`
|
|
8435
|
+
);
|
|
8436
|
+
await postEphemeral(
|
|
8437
|
+
event,
|
|
8438
|
+
`Your ${formatProviderLabel(provider)} account has been unlinked.`
|
|
7997
8439
|
);
|
|
7998
|
-
await postEphemeral(event, `Your ${providerLabel(provider)} account has been unlinked.`);
|
|
7999
8440
|
}
|
|
8000
8441
|
async function handleSlashCommand(event) {
|
|
8001
8442
|
const [subcommand, provider, ...rest] = event.text.trim().split(/\s+/);
|
|
8002
8443
|
if (!subcommand || !["link", "unlink"].includes(subcommand)) {
|
|
8003
|
-
await postEphemeral(
|
|
8444
|
+
await postEphemeral(
|
|
8445
|
+
event,
|
|
8446
|
+
"Usage: `/jr link <provider>` or `/jr unlink <provider>`"
|
|
8447
|
+
);
|
|
8004
8448
|
return;
|
|
8005
8449
|
}
|
|
8006
8450
|
if (!provider || rest.length > 0) {
|
|
@@ -8359,6 +8803,9 @@ async function persistThreadState(thread, patch) {
|
|
|
8359
8803
|
if (patch.sandboxId) {
|
|
8360
8804
|
payload.app_sandbox_id = patch.sandboxId;
|
|
8361
8805
|
}
|
|
8806
|
+
if (patch.sandboxDependencyProfileHash) {
|
|
8807
|
+
payload.app_sandbox_dependency_profile_hash = patch.sandboxDependencyProfileHash;
|
|
8808
|
+
}
|
|
8362
8809
|
if (Object.keys(payload).length === 0) {
|
|
8363
8810
|
return;
|
|
8364
8811
|
}
|
|
@@ -9203,7 +9650,9 @@ async function hydrateConversationVisionContext(conversation, context) {
|
|
|
9203
9650
|
|
|
9204
9651
|
// src/chat/turn/execute.ts
|
|
9205
9652
|
function resolveReplyDelivery(args) {
|
|
9206
|
-
const replyHasFiles = Boolean(
|
|
9653
|
+
const replyHasFiles = Boolean(
|
|
9654
|
+
args.reply.files && args.reply.files.length > 0
|
|
9655
|
+
);
|
|
9207
9656
|
const deliveryPlan = args.reply.deliveryPlan ?? {
|
|
9208
9657
|
mode: args.reply.deliveryMode ?? "thread",
|
|
9209
9658
|
ack: args.reply.ackStrategy ?? "none",
|
|
@@ -9214,8 +9663,9 @@ function resolveReplyDelivery(args) {
|
|
|
9214
9663
|
if (attachFiles === "followup" && !args.hasStreamedThreadReply) {
|
|
9215
9664
|
attachFiles = "inline";
|
|
9216
9665
|
}
|
|
9666
|
+
const suppressRedundantReactionReply = deliveryPlan.ack === "reaction" && !replyHasFiles && isRedundantReactionAckText(args.reply.text);
|
|
9217
9667
|
return {
|
|
9218
|
-
shouldPostThreadReply: deliveryPlan.postThreadText,
|
|
9668
|
+
shouldPostThreadReply: deliveryPlan.postThreadText && !suppressRedundantReactionReply,
|
|
9219
9669
|
attachFiles
|
|
9220
9670
|
};
|
|
9221
9671
|
}
|
|
@@ -9279,7 +9729,9 @@ function createReplyToThread(deps) {
|
|
|
9279
9729
|
thread,
|
|
9280
9730
|
message,
|
|
9281
9731
|
userText,
|
|
9282
|
-
explicitMention: Boolean(
|
|
9732
|
+
explicitMention: Boolean(
|
|
9733
|
+
options.explicitMention || message.isMention
|
|
9734
|
+
),
|
|
9283
9735
|
context: {
|
|
9284
9736
|
threadId,
|
|
9285
9737
|
requesterId: message.author.userId,
|
|
@@ -9324,17 +9776,22 @@ function createReplyToThread(deps) {
|
|
|
9324
9776
|
await persistThreadState(thread, {
|
|
9325
9777
|
conversation: preparedState.conversation
|
|
9326
9778
|
});
|
|
9327
|
-
const fallbackIdentity = await getBotDeps().lookupSlackUser(
|
|
9779
|
+
const fallbackIdentity = await getBotDeps().lookupSlackUser(
|
|
9780
|
+
message.author.userId
|
|
9781
|
+
);
|
|
9328
9782
|
const resolvedUserName = message.author.userName ?? fallbackIdentity?.userName;
|
|
9329
9783
|
if (resolvedUserName) {
|
|
9330
9784
|
setTags({ slackUserName: resolvedUserName });
|
|
9331
9785
|
}
|
|
9332
|
-
const userAttachments = await resolveUserAttachments(
|
|
9333
|
-
|
|
9334
|
-
|
|
9335
|
-
|
|
9336
|
-
|
|
9337
|
-
|
|
9786
|
+
const userAttachments = await resolveUserAttachments(
|
|
9787
|
+
message.attachments,
|
|
9788
|
+
{
|
|
9789
|
+
threadId,
|
|
9790
|
+
requesterId: message.author.userId,
|
|
9791
|
+
channelId,
|
|
9792
|
+
runId
|
|
9793
|
+
}
|
|
9794
|
+
);
|
|
9338
9795
|
const progress = createProgressReporter({
|
|
9339
9796
|
channelId,
|
|
9340
9797
|
threadTs,
|
|
@@ -9342,6 +9799,7 @@ function createReplyToThread(deps) {
|
|
|
9342
9799
|
});
|
|
9343
9800
|
const textStream = createTextStreamBridge();
|
|
9344
9801
|
let streamedReplyPromise;
|
|
9802
|
+
let pendingStreamText = "";
|
|
9345
9803
|
let beforeFirstResponsePostCalled = false;
|
|
9346
9804
|
const beforeFirstResponsePost = async () => {
|
|
9347
9805
|
if (beforeFirstResponsePostCalled) {
|
|
@@ -9354,13 +9812,24 @@ function createReplyToThread(deps) {
|
|
|
9354
9812
|
if (!streamedReplyPromise) {
|
|
9355
9813
|
const streamingReply = (async () => {
|
|
9356
9814
|
return await postThreadReply(
|
|
9357
|
-
createNormalizingStream(
|
|
9815
|
+
createNormalizingStream(
|
|
9816
|
+
textStream.iterable,
|
|
9817
|
+
ensureBlockSpacing
|
|
9818
|
+
),
|
|
9358
9819
|
"streaming_initial_post"
|
|
9359
9820
|
);
|
|
9360
9821
|
})();
|
|
9361
9822
|
streamedReplyPromise = streamingReply;
|
|
9362
9823
|
}
|
|
9363
9824
|
};
|
|
9825
|
+
const flushPendingStreamText = () => {
|
|
9826
|
+
if (!pendingStreamText) {
|
|
9827
|
+
return;
|
|
9828
|
+
}
|
|
9829
|
+
startStreamingReply();
|
|
9830
|
+
textStream.push(pendingStreamText);
|
|
9831
|
+
pendingStreamText = "";
|
|
9832
|
+
};
|
|
9364
9833
|
const postThreadReply = async (payload, stage) => {
|
|
9365
9834
|
await beforeFirstResponsePost();
|
|
9366
9835
|
try {
|
|
@@ -9411,17 +9880,28 @@ function createReplyToThread(deps) {
|
|
|
9411
9880
|
},
|
|
9412
9881
|
toolChannelId,
|
|
9413
9882
|
sandbox: {
|
|
9414
|
-
sandboxId: preparedState.sandboxId
|
|
9883
|
+
sandboxId: preparedState.sandboxId,
|
|
9884
|
+
sandboxDependencyProfileHash: preparedState.sandboxDependencyProfileHash
|
|
9415
9885
|
},
|
|
9416
9886
|
onStatus: (status) => progress.setStatus(status),
|
|
9417
9887
|
onTextDelta: (deltaText) => {
|
|
9418
9888
|
if (explicitChannelPostIntent) {
|
|
9419
9889
|
return;
|
|
9420
9890
|
}
|
|
9421
|
-
|
|
9422
|
-
|
|
9891
|
+
if (streamedReplyPromise) {
|
|
9892
|
+
textStream.push(deltaText);
|
|
9893
|
+
return;
|
|
9894
|
+
}
|
|
9895
|
+
pendingStreamText += deltaText;
|
|
9896
|
+
if (isPotentialRedundantReactionAckText(pendingStreamText)) {
|
|
9897
|
+
return;
|
|
9898
|
+
}
|
|
9899
|
+
flushPendingStreamText();
|
|
9423
9900
|
}
|
|
9424
9901
|
});
|
|
9902
|
+
if (streamedReplyPromise) {
|
|
9903
|
+
flushPendingStreamText();
|
|
9904
|
+
}
|
|
9425
9905
|
textStream.end();
|
|
9426
9906
|
const diagnosticsContext = {
|
|
9427
9907
|
slackThreadId: threadId,
|
|
@@ -9445,7 +9925,9 @@ function createReplyToThread(deps) {
|
|
|
9445
9925
|
};
|
|
9446
9926
|
setSpanAttributes(diagnosticsAttributes);
|
|
9447
9927
|
if (reply.diagnostics.outcome === "provider_error") {
|
|
9448
|
-
const providerError = reply.diagnostics.providerError ?? new Error(
|
|
9928
|
+
const providerError = reply.diagnostics.providerError ?? new Error(
|
|
9929
|
+
reply.diagnostics.errorMessage ?? "Provider error without explicit message"
|
|
9930
|
+
);
|
|
9449
9931
|
logException(
|
|
9450
9932
|
providerError,
|
|
9451
9933
|
"agent_turn_provider_error",
|
|
@@ -9461,10 +9943,14 @@ function createReplyToThread(deps) {
|
|
|
9461
9943
|
"Agent turn completed with execution failure"
|
|
9462
9944
|
);
|
|
9463
9945
|
}
|
|
9464
|
-
markConversationMessage(
|
|
9465
|
-
|
|
9466
|
-
|
|
9467
|
-
|
|
9946
|
+
markConversationMessage(
|
|
9947
|
+
preparedState.conversation,
|
|
9948
|
+
preparedState.userMessageId,
|
|
9949
|
+
{
|
|
9950
|
+
replied: true,
|
|
9951
|
+
skippedReason: void 0
|
|
9952
|
+
}
|
|
9953
|
+
);
|
|
9468
9954
|
upsertConversationMessage(preparedState.conversation, {
|
|
9469
9955
|
id: generateConversationId("assistant"),
|
|
9470
9956
|
role: "assistant",
|
|
@@ -9487,15 +9973,19 @@ function createReplyToThread(deps) {
|
|
|
9487
9973
|
if (shouldPostThreadReply) {
|
|
9488
9974
|
if (!streamedReplyPromise) {
|
|
9489
9975
|
await postThreadReply(
|
|
9490
|
-
buildSlackOutputMessage(
|
|
9491
|
-
|
|
9492
|
-
|
|
9976
|
+
buildSlackOutputMessage(
|
|
9977
|
+
reply.text,
|
|
9978
|
+
resolvedAttachFiles === "inline" ? replyFiles : void 0
|
|
9979
|
+
),
|
|
9493
9980
|
"thread_reply"
|
|
9494
9981
|
);
|
|
9495
9982
|
} else {
|
|
9496
9983
|
await streamedReplyPromise;
|
|
9497
9984
|
if (reply.diagnostics.outcome !== "success" && reply.text.trim().length > 0) {
|
|
9498
|
-
await postThreadReply(
|
|
9985
|
+
await postThreadReply(
|
|
9986
|
+
buildSlackOutputMessage(reply.text),
|
|
9987
|
+
"thread_reply_after_stream_failure"
|
|
9988
|
+
);
|
|
9499
9989
|
}
|
|
9500
9990
|
}
|
|
9501
9991
|
}
|
|
@@ -9509,7 +9999,8 @@ function createReplyToThread(deps) {
|
|
|
9509
9999
|
await persistThreadState(thread, {
|
|
9510
10000
|
artifacts: nextArtifacts,
|
|
9511
10001
|
conversation: preparedState.conversation,
|
|
9512
|
-
sandboxId: reply.sandboxId
|
|
10002
|
+
sandboxId: reply.sandboxId,
|
|
10003
|
+
sandboxDependencyProfileHash: reply.sandboxDependencyProfileHash
|
|
9513
10004
|
});
|
|
9514
10005
|
persistedAtLeastOnce = true;
|
|
9515
10006
|
if (shouldEmitDevAgentTrace()) {
|
|
@@ -9525,9 +10016,13 @@ function createReplyToThread(deps) {
|
|
|
9525
10016
|
"Agent turn completed"
|
|
9526
10017
|
);
|
|
9527
10018
|
}
|
|
9528
|
-
const isFirstAssistantReply = preparedState.conversation.stats.compactedMessageCount === 0 && preparedState.conversation.messages.filter(
|
|
10019
|
+
const isFirstAssistantReply = preparedState.conversation.stats.compactedMessageCount === 0 && preparedState.conversation.messages.filter(
|
|
10020
|
+
(m) => m.role === "assistant"
|
|
10021
|
+
).length === 1;
|
|
9529
10022
|
if (isFirstAssistantReply && channelId && isDmChannel(channelId) && threadTs) {
|
|
9530
|
-
void generateThreadTitle(userText, reply.text).then(
|
|
10023
|
+
void generateThreadTitle(userText, reply.text).then(
|
|
10024
|
+
(title) => deps.getSlackAdapter().setAssistantTitle(channelId, threadTs, title)
|
|
10025
|
+
).catch((error) => {
|
|
9531
10026
|
const slackErrorCode = getSlackApiErrorCode(error);
|
|
9532
10027
|
const assistantTitleErrorAttributes = {
|
|
9533
10028
|
"app.slack.assistant_title.outcome": "permission_denied",
|
|
@@ -9560,14 +10055,16 @@ function createReplyToThread(deps) {
|
|
|
9560
10055
|
assistantUserName: botConfig.userName,
|
|
9561
10056
|
modelId: botConfig.fastModelId
|
|
9562
10057
|
},
|
|
9563
|
-
{
|
|
10058
|
+
{
|
|
10059
|
+
"error.message": error instanceof Error ? error.message : String(error)
|
|
10060
|
+
},
|
|
9564
10061
|
"Thread title generation failed"
|
|
9565
10062
|
);
|
|
9566
10063
|
});
|
|
9567
10064
|
}
|
|
9568
10065
|
if (shouldPostThreadReply && resolvedAttachFiles === "followup" && replyFiles) {
|
|
9569
10066
|
await postThreadReply(
|
|
9570
|
-
|
|
10067
|
+
buildSlackOutputMessage("", replyFiles),
|
|
9571
10068
|
"thread_reply_files_followup"
|
|
9572
10069
|
);
|
|
9573
10070
|
}
|
|
@@ -9610,7 +10107,12 @@ function createReplyToThread(deps) {
|
|
|
9610
10107
|
// src/chat/runtime/turn-preparation.ts
|
|
9611
10108
|
async function prepareTurnState(args) {
|
|
9612
10109
|
const existingState = await args.thread.state;
|
|
9613
|
-
const existingSandboxId = existingState ? toOptionalString(
|
|
10110
|
+
const existingSandboxId = existingState ? toOptionalString(
|
|
10111
|
+
existingState.app_sandbox_id
|
|
10112
|
+
) : void 0;
|
|
10113
|
+
const existingSandboxDependencyProfileHash = existingState ? toOptionalString(
|
|
10114
|
+
existingState.app_sandbox_dependency_profile_hash
|
|
10115
|
+
) : void 0;
|
|
9614
10116
|
const artifacts = coerceThreadArtifactsState(existingState);
|
|
9615
10117
|
const conversation = coerceThreadConversationState(existingState);
|
|
9616
10118
|
const channelConfiguration = getChannelConfigurationService(args.thread);
|
|
@@ -9619,13 +10121,15 @@ async function prepareTurnState(args) {
|
|
|
9619
10121
|
messageId: args.message.id,
|
|
9620
10122
|
messageCreatedAtMs: args.message.metadata.dateSent.getTime()
|
|
9621
10123
|
});
|
|
9622
|
-
const messageHasPotentialImageAttachment = args.message.attachments.some(
|
|
9623
|
-
|
|
9624
|
-
|
|
10124
|
+
const messageHasPotentialImageAttachment = args.message.attachments.some(
|
|
10125
|
+
(attachment) => {
|
|
10126
|
+
if (attachment.type === "image") {
|
|
10127
|
+
return true;
|
|
10128
|
+
}
|
|
10129
|
+
const mimeType = attachment.mimeType ?? "";
|
|
10130
|
+
return attachment.type === "file" && mimeType.startsWith("image/");
|
|
9625
10131
|
}
|
|
9626
|
-
|
|
9627
|
-
return attachment.type === "file" && mimeType.startsWith("image/");
|
|
9628
|
-
});
|
|
10132
|
+
);
|
|
9629
10133
|
const normalizedUserText = normalizeConversationText(args.userText) || "[non-text message]";
|
|
9630
10134
|
const incomingUserMessage = {
|
|
9631
10135
|
id: args.message.id,
|
|
@@ -9644,7 +10148,10 @@ async function prepareTurnState(args) {
|
|
|
9644
10148
|
imagesHydrated: !messageHasPotentialImageAttachment
|
|
9645
10149
|
}
|
|
9646
10150
|
};
|
|
9647
|
-
const userMessageId = upsertConversationMessage(
|
|
10151
|
+
const userMessageId = upsertConversationMessage(
|
|
10152
|
+
conversation,
|
|
10153
|
+
incomingUserMessage
|
|
10154
|
+
);
|
|
9648
10155
|
if (messageHasPotentialImageAttachment || !conversation.vision.backfillCompletedAtMs) {
|
|
9649
10156
|
await hydrateConversationVisionContext(conversation, {
|
|
9650
10157
|
threadId: args.context.threadId,
|
|
@@ -9674,6 +10181,7 @@ async function prepareTurnState(args) {
|
|
|
9674
10181
|
channelConfiguration,
|
|
9675
10182
|
conversation,
|
|
9676
10183
|
sandboxId: existingSandboxId,
|
|
10184
|
+
sandboxDependencyProfileHash: existingSandboxDependencyProfileHash,
|
|
9677
10185
|
conversationContext,
|
|
9678
10186
|
routingContext,
|
|
9679
10187
|
userMessageId
|
|
@@ -9766,7 +10274,7 @@ export {
|
|
|
9766
10274
|
getUserTokenStore,
|
|
9767
10275
|
getSlackClient,
|
|
9768
10276
|
downloadPrivateSlackFile,
|
|
9769
|
-
|
|
10277
|
+
formatProviderLabel,
|
|
9770
10278
|
resolveBaseUrl,
|
|
9771
10279
|
escapeXml,
|
|
9772
10280
|
removeReactionFromMessage,
|