@sentry/junior 0.18.1 → 0.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/app.js
CHANGED
|
@@ -5,9 +5,8 @@ import {
|
|
|
5
5
|
listCapabilityProviders,
|
|
6
6
|
loadSkillsByName,
|
|
7
7
|
logCapabilityCatalogLoadedOnce,
|
|
8
|
-
parseSkillInvocation
|
|
9
|
-
|
|
10
|
-
} from "./chunk-4XWTSMRF.js";
|
|
8
|
+
parseSkillInvocation
|
|
9
|
+
} from "./chunk-VJLT6LLV.js";
|
|
11
10
|
import {
|
|
12
11
|
SANDBOX_SKILLS_ROOT,
|
|
13
12
|
SANDBOX_WORKSPACE_ROOT,
|
|
@@ -25,8 +24,9 @@ import {
|
|
|
25
24
|
resolveRuntimeDependencySnapshot,
|
|
26
25
|
runNonInteractiveCommand,
|
|
27
26
|
sandboxSkillDir,
|
|
27
|
+
sandboxSkillFile,
|
|
28
28
|
toOptionalTrimmed
|
|
29
|
-
} from "./chunk-
|
|
29
|
+
} from "./chunk-LOTYK7IE.js";
|
|
30
30
|
import {
|
|
31
31
|
CredentialUnavailableError,
|
|
32
32
|
buildOAuthTokenRequest,
|
|
@@ -2999,6 +2999,8 @@ function buildSystemPrompt(params) {
|
|
|
2999
2999
|
"- If a loaded skill or `loadSkill` result declares `requires_capabilities`, run `jr-rpc issue-credential <capability> [--repo <owner/repo>]` as a bash command before authenticated bash/API work for that skill.",
|
|
3000
3000
|
"- Use the minimum declared capability needed for the current operation.",
|
|
3001
3001
|
"- If `jr-rpc issue-credential` returns `oauth_started`, relay its `message` to the user and stop. The runtime will resume after authorization.",
|
|
3002
|
+
"- For disconnect + reconnect requests, run `jr-rpc delete-token <provider>` first, then `jr-rpc issue-credential` \u2014 the system handles the reconnect without auto-resuming the reconnect message.",
|
|
3003
|
+
"- Use `jr-rpc oauth-start <provider>` only when the user explicitly asks to connect a provider and there is no task to resume after authorization.",
|
|
3002
3004
|
"- GitHub capabilities need repository context, which can come from `--repo` or a configured `github.repo` default.",
|
|
3003
3005
|
"- To persist or read conversation defaults (for example `github.repo`), run `jr-rpc config get|set|unset|list ...` as a bash command.",
|
|
3004
3006
|
"- Capabilities are provider-qualified (for example `github.issues.write`).",
|
|
@@ -3539,6 +3541,52 @@ async function handleIssueCredentialCommand(args, deps) {
|
|
|
3539
3541
|
});
|
|
3540
3542
|
} catch (error) {
|
|
3541
3543
|
if (error instanceof CredentialUnavailableError && getPluginOAuthConfig(error.provider) && deps.requesterId) {
|
|
3544
|
+
const authAction = deps.providerAuthActions?.get(error.provider);
|
|
3545
|
+
if (authAction?.kind === "oauth_started") {
|
|
3546
|
+
const providerLabel = formatProviderLabel(error.provider);
|
|
3547
|
+
return commandResult({
|
|
3548
|
+
stdout: {
|
|
3549
|
+
credential_unavailable: true,
|
|
3550
|
+
oauth_started: true,
|
|
3551
|
+
provider: error.provider,
|
|
3552
|
+
private_delivery_sent: authAction.delivered,
|
|
3553
|
+
message: authAction.delivered ? `I've already sent you a private authorization link to connect your ${providerLabel} account. Finish that flow, then return to Slack.` : `I still need to connect your ${providerLabel} account, but I wasn't able to send you a private authorization link. Please send me a direct message and try again.`
|
|
3554
|
+
},
|
|
3555
|
+
exitCode: 0
|
|
3556
|
+
});
|
|
3557
|
+
}
|
|
3558
|
+
if (authAction?.kind === "token_deleted") {
|
|
3559
|
+
const reconnectResult = await startOAuthFlow(error.provider, {
|
|
3560
|
+
requesterId: deps.requesterId,
|
|
3561
|
+
channelId: deps.channelId,
|
|
3562
|
+
threadTs: deps.threadTs,
|
|
3563
|
+
activeSkillName: deps.activeSkill?.name ?? void 0
|
|
3564
|
+
// Intentionally no userMessage — reconnect flows must not auto-resume.
|
|
3565
|
+
});
|
|
3566
|
+
if (!reconnectResult.ok) {
|
|
3567
|
+
return commandResult({
|
|
3568
|
+
stderr: `${reconnectResult.error}
|
|
3569
|
+
`,
|
|
3570
|
+
exitCode: 1
|
|
3571
|
+
});
|
|
3572
|
+
}
|
|
3573
|
+
const delivered = !!reconnectResult.delivery;
|
|
3574
|
+
deps.providerAuthActions?.set(error.provider, {
|
|
3575
|
+
kind: "oauth_started",
|
|
3576
|
+
delivered
|
|
3577
|
+
});
|
|
3578
|
+
const providerLabel = formatProviderLabel(error.provider);
|
|
3579
|
+
return commandResult({
|
|
3580
|
+
stdout: {
|
|
3581
|
+
credential_unavailable: true,
|
|
3582
|
+
oauth_started: true,
|
|
3583
|
+
provider: error.provider,
|
|
3584
|
+
private_delivery_sent: delivered,
|
|
3585
|
+
message: delivered ? `I need to connect your ${providerLabel} account first. I've sent you a private authorization link.` : `I need to connect your ${providerLabel} account first, but I wasn't able to send you a private authorization link. Please send me a direct message and try your command again.`
|
|
3586
|
+
},
|
|
3587
|
+
exitCode: 0
|
|
3588
|
+
});
|
|
3589
|
+
}
|
|
3542
3590
|
const oauthResult = await startOAuthFlow(error.provider, {
|
|
3543
3591
|
requesterId: deps.requesterId,
|
|
3544
3592
|
channelId: deps.channelId,
|
|
@@ -3808,6 +3856,10 @@ async function handleOAuthStartCommand(args, deps) {
|
|
|
3808
3856
|
return commandResult({ stderr: `${result.error}
|
|
3809
3857
|
`, exitCode: 1 });
|
|
3810
3858
|
}
|
|
3859
|
+
deps.providerAuthActions?.set(provider, {
|
|
3860
|
+
kind: "oauth_started",
|
|
3861
|
+
delivered: !!result.delivery
|
|
3862
|
+
});
|
|
3811
3863
|
if (!result.delivery) {
|
|
3812
3864
|
return commandResult({
|
|
3813
3865
|
stdout: {
|
|
@@ -3854,6 +3906,7 @@ async function handleDeleteTokenCommand(args, deps) {
|
|
|
3854
3906
|
});
|
|
3855
3907
|
}
|
|
3856
3908
|
await unlinkProvider(deps.requesterId, provider, deps.userTokenStore);
|
|
3909
|
+
deps.providerAuthActions?.set(provider, { kind: "token_deleted" });
|
|
3857
3910
|
logInfo(
|
|
3858
3911
|
"jr_rpc_delete_token",
|
|
3859
3912
|
{},
|
|
@@ -4968,7 +5021,7 @@ function toLoadedSkill(result, availableSkills) {
|
|
|
4968
5021
|
return {
|
|
4969
5022
|
name: result.skill_name,
|
|
4970
5023
|
description: result.description,
|
|
4971
|
-
skillPath: result.skill_dir,
|
|
5024
|
+
skillPath: metadata?.skillPath ?? result.skill_dir,
|
|
4972
5025
|
...metadata?.pluginProvider ? { pluginProvider: metadata.pluginProvider } : {},
|
|
4973
5026
|
...metadata?.allowedTools ? { allowedTools: metadata.allowedTools } : {},
|
|
4974
5027
|
...metadata?.requiresCapabilities ? { requiresCapabilities: metadata.requiresCapabilities } : {},
|
|
@@ -4976,7 +5029,7 @@ function toLoadedSkill(result, availableSkills) {
|
|
|
4976
5029
|
body: result.instructions
|
|
4977
5030
|
};
|
|
4978
5031
|
}
|
|
4979
|
-
async function
|
|
5032
|
+
async function loadSkillFromHost(availableSkills, skillName) {
|
|
4980
5033
|
const requested = skillName.trim().toLowerCase();
|
|
4981
5034
|
const skill = availableSkills.find(
|
|
4982
5035
|
(entry) => entry.name.toLowerCase() === requested
|
|
@@ -4989,10 +5042,10 @@ async function loadSkillFromSandbox(sandbox, availableSkills, skillName) {
|
|
|
4989
5042
|
};
|
|
4990
5043
|
}
|
|
4991
5044
|
const skillDir = sandboxSkillDir(skill.name);
|
|
4992
|
-
const skillFilePath =
|
|
4993
|
-
const
|
|
4994
|
-
if (!
|
|
4995
|
-
throw new Error(`failed to
|
|
5045
|
+
const skillFilePath = sandboxSkillFile(skill.name);
|
|
5046
|
+
const [loaded] = await loadSkillsByName([skill.name], availableSkills);
|
|
5047
|
+
if (!loaded) {
|
|
5048
|
+
throw new Error(`failed to load ${skill.name}`);
|
|
4996
5049
|
}
|
|
4997
5050
|
return {
|
|
4998
5051
|
ok: true,
|
|
@@ -5001,10 +5054,10 @@ async function loadSkillFromSandbox(sandbox, availableSkills, skillName) {
|
|
|
5001
5054
|
...skill.requiresCapabilities ? { requires_capabilities: skill.requiresCapabilities } : {},
|
|
5002
5055
|
skill_dir: skillDir,
|
|
5003
5056
|
location: skillFilePath,
|
|
5004
|
-
instructions:
|
|
5057
|
+
instructions: loaded.body
|
|
5005
5058
|
};
|
|
5006
5059
|
}
|
|
5007
|
-
function createLoadSkillTool(
|
|
5060
|
+
function createLoadSkillTool(availableSkills, options) {
|
|
5008
5061
|
return tool({
|
|
5009
5062
|
description: "Load a skill by name so its instructions are available for this turn. The result includes `requires_capabilities` when the skill declares authenticated provider access, and `available_tools` when the skill exposes MCP tools for this turn. Use when a request clearly matches a known skill. Do not use when no skill is relevant.",
|
|
5010
5063
|
inputSchema: Type4.Object({
|
|
@@ -5014,11 +5067,7 @@ function createLoadSkillTool(sandbox, availableSkills, options) {
|
|
|
5014
5067
|
})
|
|
5015
5068
|
}),
|
|
5016
5069
|
execute: async ({ skill_name }) => {
|
|
5017
|
-
const result = await
|
|
5018
|
-
sandbox,
|
|
5019
|
-
availableSkills,
|
|
5020
|
-
skill_name
|
|
5021
|
-
);
|
|
5070
|
+
const result = await loadSkillFromHost(availableSkills, skill_name);
|
|
5022
5071
|
const loadedSkill = toLoadedSkill(result, availableSkills);
|
|
5023
5072
|
if (loadedSkill) {
|
|
5024
5073
|
const metadata = await options?.onSkillLoaded?.(loadedSkill);
|
|
@@ -6754,103 +6803,50 @@ function createToolState(hooks, context) {
|
|
|
6754
6803
|
}
|
|
6755
6804
|
};
|
|
6756
6805
|
}
|
|
6757
|
-
function wrapToolExecution(toolName, toolDef, hooks) {
|
|
6758
|
-
const maybeExecutable = toolDef;
|
|
6759
|
-
if (!maybeExecutable.execute) {
|
|
6760
|
-
return toolDef;
|
|
6761
|
-
}
|
|
6762
|
-
const originalExecute = maybeExecutable.execute.bind(toolDef);
|
|
6763
|
-
maybeExecutable.execute = async (...args) => {
|
|
6764
|
-
const input = args[0];
|
|
6765
|
-
await hooks.onToolCallStart?.(toolName, input);
|
|
6766
|
-
return originalExecute(...args);
|
|
6767
|
-
};
|
|
6768
|
-
return toolDef;
|
|
6769
|
-
}
|
|
6770
6806
|
function createTools(availableSkills, hooks = {}, context) {
|
|
6771
6807
|
const state = createToolState(hooks, context);
|
|
6772
6808
|
const tools = {
|
|
6773
|
-
loadSkill:
|
|
6774
|
-
|
|
6775
|
-
|
|
6776
|
-
|
|
6777
|
-
|
|
6778
|
-
|
|
6779
|
-
),
|
|
6780
|
-
|
|
6781
|
-
|
|
6782
|
-
|
|
6783
|
-
|
|
6784
|
-
|
|
6785
|
-
hooks
|
|
6786
|
-
),
|
|
6787
|
-
readFile: wrapToolExecution("readFile", createReadFileTool(), hooks),
|
|
6788
|
-
writeFile: wrapToolExecution("writeFile", createWriteFileTool(), hooks),
|
|
6789
|
-
webSearch: wrapToolExecution("webSearch", createWebSearchTool(), hooks),
|
|
6790
|
-
webFetch: wrapToolExecution("webFetch", createWebFetchTool(hooks), hooks),
|
|
6791
|
-
imageGenerate: wrapToolExecution(
|
|
6792
|
-
"imageGenerate",
|
|
6793
|
-
createImageGenerateTool(hooks, hooks.toolOverrides?.imageGenerate),
|
|
6794
|
-
hooks
|
|
6795
|
-
),
|
|
6796
|
-
slackCanvasUpdate: wrapToolExecution(
|
|
6797
|
-
"slackCanvasUpdate",
|
|
6798
|
-
createSlackCanvasUpdateTool(state, context),
|
|
6799
|
-
hooks
|
|
6800
|
-
),
|
|
6801
|
-
slackListCreate: wrapToolExecution(
|
|
6802
|
-
"slackListCreate",
|
|
6803
|
-
createSlackListCreateTool(state),
|
|
6804
|
-
hooks
|
|
6805
|
-
),
|
|
6806
|
-
slackListAddItems: wrapToolExecution(
|
|
6807
|
-
"slackListAddItems",
|
|
6808
|
-
createSlackListAddItemsTool(state),
|
|
6809
|
-
hooks
|
|
6810
|
-
),
|
|
6811
|
-
slackListGetItems: wrapToolExecution(
|
|
6812
|
-
"slackListGetItems",
|
|
6813
|
-
createSlackListGetItemsTool(state),
|
|
6814
|
-
hooks
|
|
6809
|
+
loadSkill: createLoadSkillTool(availableSkills, {
|
|
6810
|
+
onSkillLoaded: hooks.onSkillLoaded
|
|
6811
|
+
}),
|
|
6812
|
+
systemTime: createSystemTimeTool(),
|
|
6813
|
+
bash: createBashTool(),
|
|
6814
|
+
attachFile: createAttachFileTool(context.sandbox, hooks),
|
|
6815
|
+
readFile: createReadFileTool(),
|
|
6816
|
+
writeFile: createWriteFileTool(),
|
|
6817
|
+
webSearch: createWebSearchTool(),
|
|
6818
|
+
webFetch: createWebFetchTool(hooks),
|
|
6819
|
+
imageGenerate: createImageGenerateTool(
|
|
6820
|
+
hooks,
|
|
6821
|
+
hooks.toolOverrides?.imageGenerate
|
|
6815
6822
|
),
|
|
6816
|
-
|
|
6817
|
-
|
|
6818
|
-
|
|
6819
|
-
|
|
6820
|
-
)
|
|
6823
|
+
slackCanvasUpdate: createSlackCanvasUpdateTool(state, context),
|
|
6824
|
+
slackListCreate: createSlackListCreateTool(state),
|
|
6825
|
+
slackListAddItems: createSlackListAddItemsTool(state),
|
|
6826
|
+
slackListGetItems: createSlackListGetItemsTool(state),
|
|
6827
|
+
slackListUpdateItem: createSlackListUpdateItemTool(state)
|
|
6821
6828
|
};
|
|
6822
6829
|
if (context.mcpToolManager && context.getActiveSkills) {
|
|
6823
|
-
tools.searchTools =
|
|
6824
|
-
|
|
6825
|
-
|
|
6826
|
-
hooks
|
|
6830
|
+
tools.searchTools = createSearchToolsTool(
|
|
6831
|
+
context.mcpToolManager,
|
|
6832
|
+
context.getActiveSkills
|
|
6827
6833
|
);
|
|
6828
6834
|
}
|
|
6829
6835
|
const { channelCapabilities } = context;
|
|
6830
6836
|
if (channelCapabilities.canCreateCanvas) {
|
|
6831
|
-
tools.slackCanvasCreate =
|
|
6832
|
-
"slackCanvasCreate",
|
|
6833
|
-
createSlackCanvasCreateTool(context, state),
|
|
6834
|
-
hooks
|
|
6835
|
-
);
|
|
6837
|
+
tools.slackCanvasCreate = createSlackCanvasCreateTool(context, state);
|
|
6836
6838
|
}
|
|
6837
6839
|
if (channelCapabilities.canPostToChannel) {
|
|
6838
|
-
tools.slackChannelPostMessage =
|
|
6839
|
-
|
|
6840
|
-
|
|
6841
|
-
hooks
|
|
6842
|
-
);
|
|
6843
|
-
tools.slackChannelListMessages = wrapToolExecution(
|
|
6844
|
-
"slackChannelListMessages",
|
|
6845
|
-
createSlackChannelListMessagesTool(context),
|
|
6846
|
-
hooks
|
|
6840
|
+
tools.slackChannelPostMessage = createSlackChannelPostMessageTool(
|
|
6841
|
+
context,
|
|
6842
|
+
state
|
|
6847
6843
|
);
|
|
6844
|
+
tools.slackChannelListMessages = createSlackChannelListMessagesTool(context);
|
|
6848
6845
|
}
|
|
6849
6846
|
if (channelCapabilities.canAddReactions) {
|
|
6850
|
-
tools.slackMessageAddReaction =
|
|
6851
|
-
|
|
6852
|
-
|
|
6853
|
-
hooks
|
|
6847
|
+
tools.slackMessageAddReaction = createSlackMessageAddReactionTool(
|
|
6848
|
+
context,
|
|
6849
|
+
state
|
|
6854
6850
|
);
|
|
6855
6851
|
}
|
|
6856
6852
|
return tools;
|
|
@@ -6866,10 +6862,7 @@ function resolveChannelCapabilities(channelId) {
|
|
|
6866
6862
|
}
|
|
6867
6863
|
|
|
6868
6864
|
// src/chat/sandbox/sandbox.ts
|
|
6869
|
-
import
|
|
6870
|
-
import path4 from "path";
|
|
6871
|
-
import { Sandbox } from "@vercel/sandbox";
|
|
6872
|
-
import { createBashTool as createBashTool2 } from "bash-tool";
|
|
6865
|
+
import fs4 from "fs/promises";
|
|
6873
6866
|
|
|
6874
6867
|
// src/chat/sandbox/http-error-details.ts
|
|
6875
6868
|
var DEFAULT_PREVIEW_LIMIT = 512;
|
|
@@ -6973,14 +6966,7 @@ function extractHttpErrorDetails(error, options = {}) {
|
|
|
6973
6966
|
};
|
|
6974
6967
|
}
|
|
6975
6968
|
|
|
6976
|
-
// src/chat/sandbox/
|
|
6977
|
-
var SANDBOX_TOOL_NAMES = /* @__PURE__ */ new Set(["bash", "readFile", "writeFile"]);
|
|
6978
|
-
var DEFAULT_MAX_OUTPUT_LENGTH = 3e4;
|
|
6979
|
-
var SANDBOX_RUNTIME = "node22";
|
|
6980
|
-
var SANDBOX_RUNTIME_BIN_DIR = `${SANDBOX_WORKSPACE_ROOT}/.junior/bin`;
|
|
6981
|
-
var EVAL_GH_STUB_PATH = `${SANDBOX_RUNTIME_BIN_DIR}/gh`;
|
|
6982
|
-
var SNAPSHOT_BOOT_RETRY_COUNT = 3;
|
|
6983
|
-
var SNAPSHOT_BOOT_RETRY_DELAY_MS = 1e3;
|
|
6969
|
+
// src/chat/sandbox/errors.ts
|
|
6984
6970
|
var SANDBOX_ERROR_FIELDS = [
|
|
6985
6971
|
{
|
|
6986
6972
|
sourceKey: "sandboxId",
|
|
@@ -6988,140 +6974,410 @@ var SANDBOX_ERROR_FIELDS = [
|
|
|
6988
6974
|
summaryKey: "sandboxId"
|
|
6989
6975
|
}
|
|
6990
6976
|
];
|
|
6991
|
-
function
|
|
6992
|
-
|
|
6993
|
-
|
|
6994
|
-
|
|
6995
|
-
|
|
6996
|
-
([domain, rules]) => [
|
|
6997
|
-
domain,
|
|
6998
|
-
Array.isArray(rules) ? [...rules] : []
|
|
6999
|
-
]
|
|
7000
|
-
)
|
|
7001
|
-
) : { "*": [] };
|
|
7002
|
-
for (const transform of headerTransforms) {
|
|
7003
|
-
const currentRules = existingAllow[transform.domain] ?? [];
|
|
7004
|
-
existingAllow[transform.domain] = [
|
|
7005
|
-
...currentRules,
|
|
7006
|
-
{ transform: [{ headers: transform.headers }] }
|
|
7007
|
-
];
|
|
7008
|
-
}
|
|
7009
|
-
return {
|
|
7010
|
-
...basePolicy,
|
|
7011
|
-
allow: existingAllow
|
|
7012
|
-
};
|
|
6977
|
+
function getSandboxErrorDetails(error) {
|
|
6978
|
+
return extractHttpErrorDetails(error, {
|
|
6979
|
+
attributePrefix: "app.sandbox.api_error",
|
|
6980
|
+
extraFields: [...SANDBOX_ERROR_FIELDS]
|
|
6981
|
+
});
|
|
7013
6982
|
}
|
|
7014
|
-
function
|
|
7015
|
-
|
|
7016
|
-
|
|
6983
|
+
function findInErrorChain(error, predicate) {
|
|
6984
|
+
const seen = /* @__PURE__ */ new Set();
|
|
6985
|
+
let current = error;
|
|
6986
|
+
while (current && !seen.has(current)) {
|
|
6987
|
+
if (predicate(current)) {
|
|
6988
|
+
return true;
|
|
6989
|
+
}
|
|
6990
|
+
seen.add(current);
|
|
6991
|
+
current = typeof current === "object" ? current.cause : void 0;
|
|
7017
6992
|
}
|
|
7018
|
-
|
|
7019
|
-
return {
|
|
7020
|
-
value: `${output.slice(0, maxLength)}
|
|
7021
|
-
|
|
7022
|
-
[output truncated: ${truncatedLength} characters removed]`,
|
|
7023
|
-
truncated: true
|
|
7024
|
-
};
|
|
7025
|
-
}
|
|
7026
|
-
function toPosixRelative(base, absolute) {
|
|
7027
|
-
return path4.relative(base, absolute).split(path4.sep).join("/");
|
|
6993
|
+
return false;
|
|
7028
6994
|
}
|
|
7029
|
-
|
|
7030
|
-
const
|
|
7031
|
-
|
|
7032
|
-
while (
|
|
7033
|
-
|
|
7034
|
-
|
|
7035
|
-
|
|
7036
|
-
|
|
7037
|
-
const absolute = path4.join(dir, entry.name);
|
|
7038
|
-
if (entry.isDirectory()) {
|
|
7039
|
-
queue.push(absolute);
|
|
7040
|
-
} else if (entry.isFile()) {
|
|
7041
|
-
files.push(absolute);
|
|
6995
|
+
function getFirstErrorMessage(error) {
|
|
6996
|
+
const seen = /* @__PURE__ */ new Set();
|
|
6997
|
+
let current = error;
|
|
6998
|
+
while (current && !seen.has(current)) {
|
|
6999
|
+
if (current instanceof Error) {
|
|
7000
|
+
const message = current.message.trim();
|
|
7001
|
+
if (message) {
|
|
7002
|
+
return message;
|
|
7042
7003
|
}
|
|
7043
7004
|
}
|
|
7005
|
+
seen.add(current);
|
|
7006
|
+
current = typeof current === "object" ? current.cause : void 0;
|
|
7044
7007
|
}
|
|
7045
|
-
return
|
|
7008
|
+
return void 0;
|
|
7046
7009
|
}
|
|
7047
|
-
|
|
7048
|
-
const
|
|
7049
|
-
|
|
7050
|
-
|
|
7051
|
-
|
|
7052
|
-
|
|
7053
|
-
const
|
|
7054
|
-
|
|
7055
|
-
|
|
7056
|
-
|
|
7057
|
-
|
|
7058
|
-
|
|
7059
|
-
|
|
7060
|
-
|
|
7061
|
-
|
|
7010
|
+
function isAlreadyExistsError(error) {
|
|
7011
|
+
const details = getSandboxErrorDetails(error);
|
|
7012
|
+
return details.searchableText.includes("already exists") || details.searchableText.includes("file exists") || details.searchableText.includes("eexist");
|
|
7013
|
+
}
|
|
7014
|
+
function isSandboxUnavailableError(error) {
|
|
7015
|
+
return findInErrorChain(error, (candidate) => {
|
|
7016
|
+
const details = getSandboxErrorDetails(candidate);
|
|
7017
|
+
const searchable = `${details.searchableText} ${details.summary}`.toLowerCase();
|
|
7018
|
+
return searchable.includes("sandbox_stopped") || searchable.includes("status=410") || searchable.includes("status code 410") || searchable.includes("no longer available");
|
|
7019
|
+
});
|
|
7020
|
+
}
|
|
7021
|
+
function isSnapshottingError(error) {
|
|
7022
|
+
return findInErrorChain(error, (candidate) => {
|
|
7023
|
+
const details = getSandboxErrorDetails(candidate);
|
|
7024
|
+
const searchable = `${details.searchableText} ${details.summary}`.toLowerCase();
|
|
7025
|
+
return searchable.includes("sandbox_snapshotting") || searchable.includes("creating a snapshot") || searchable.includes("stopped shortly");
|
|
7026
|
+
});
|
|
7027
|
+
}
|
|
7028
|
+
function wrapSandboxSetupError(error) {
|
|
7029
|
+
try {
|
|
7030
|
+
const details = getSandboxErrorDetails(error);
|
|
7031
|
+
if (details.summary) {
|
|
7032
|
+
return new Error(`sandbox setup failed (${details.summary})`, {
|
|
7033
|
+
cause: error
|
|
7062
7034
|
});
|
|
7063
7035
|
}
|
|
7064
|
-
|
|
7065
|
-
name: skill.name,
|
|
7066
|
-
description: skill.description,
|
|
7067
|
-
root: sandboxSkillDir(skill.name)
|
|
7068
|
-
});
|
|
7036
|
+
} catch {
|
|
7069
7037
|
}
|
|
7070
|
-
|
|
7071
|
-
|
|
7072
|
-
|
|
7073
|
-
})
|
|
7074
|
-
|
|
7075
|
-
filesToWrite.push({
|
|
7076
|
-
path: EVAL_GH_STUB_PATH,
|
|
7077
|
-
content: Buffer.from(buildEvalGitHubCliStub(), "utf8")
|
|
7078
|
-
});
|
|
7038
|
+
let causeMessage;
|
|
7039
|
+
try {
|
|
7040
|
+
causeMessage = getFirstErrorMessage(error);
|
|
7041
|
+
} catch (cause) {
|
|
7042
|
+
causeMessage = cause instanceof Error ? cause.message : void 0;
|
|
7079
7043
|
}
|
|
7080
|
-
|
|
7044
|
+
if (causeMessage && causeMessage.trim() && causeMessage !== "sandbox setup failed") {
|
|
7045
|
+
const oneLine = causeMessage.replace(/\s+/g, " ").trim();
|
|
7046
|
+
return new Error(`sandbox setup failed (${oneLine})`, { cause: error });
|
|
7047
|
+
}
|
|
7048
|
+
return new Error("sandbox setup failed", { cause: error });
|
|
7049
|
+
}
|
|
7050
|
+
function throwSandboxOperationError(action, error, includeMissingPath = false) {
|
|
7051
|
+
const details = getSandboxErrorDetails(error);
|
|
7052
|
+
setSpanAttributes({
|
|
7053
|
+
...details.attributes,
|
|
7054
|
+
...includeMissingPath ? {
|
|
7055
|
+
"app.sandbox.api_error.missing_path": details.searchableText.includes("no such file") || details.searchableText.includes("enoent")
|
|
7056
|
+
} : {},
|
|
7057
|
+
"app.sandbox.success": false
|
|
7058
|
+
});
|
|
7059
|
+
setSpanStatus("error");
|
|
7060
|
+
throw new Error(
|
|
7061
|
+
details.summary ? `${action} failed (${details.summary})` : `${action} failed`,
|
|
7062
|
+
{
|
|
7063
|
+
cause: error
|
|
7064
|
+
}
|
|
7065
|
+
);
|
|
7081
7066
|
}
|
|
7082
|
-
function buildEvalGitHubCliStub() {
|
|
7083
|
-
return `#!/usr/bin/env node
|
|
7084
|
-
const fs = require("node:fs");
|
|
7085
|
-
const path = require("node:path");
|
|
7086
|
-
const { spawnSync } = require("node:child_process");
|
|
7087
7067
|
|
|
7088
|
-
|
|
7089
|
-
|
|
7090
|
-
|
|
7091
|
-
const flagsWithValues = new Set([
|
|
7092
|
-
"--repo",
|
|
7093
|
-
"--title",
|
|
7094
|
-
"--body",
|
|
7095
|
-
"--body-file",
|
|
7096
|
-
"--json",
|
|
7097
|
-
"--search",
|
|
7098
|
-
"--state",
|
|
7099
|
-
"--limit",
|
|
7100
|
-
"--method",
|
|
7101
|
-
"--jq",
|
|
7102
|
-
"--template",
|
|
7103
|
-
"--hostname",
|
|
7104
|
-
]);
|
|
7068
|
+
// src/chat/sandbox/session.ts
|
|
7069
|
+
import { Sandbox } from "@vercel/sandbox";
|
|
7070
|
+
import { createBashTool as createBashTool2 } from "bash-tool";
|
|
7105
7071
|
|
|
7106
|
-
|
|
7107
|
-
|
|
7108
|
-
|
|
7109
|
-
|
|
7110
|
-
|
|
7111
|
-
}
|
|
7112
|
-
if (value.startsWith(name + "=")) {
|
|
7113
|
-
return value.slice(name.length + 1);
|
|
7114
|
-
}
|
|
7072
|
+
// src/chat/runtime/status-format.ts
|
|
7073
|
+
var SLACK_STATUS_MAX_LENGTH = 50;
|
|
7074
|
+
function truncateWithEllipsis(text, maxLength) {
|
|
7075
|
+
if (text.length <= maxLength) {
|
|
7076
|
+
return text;
|
|
7115
7077
|
}
|
|
7116
|
-
return
|
|
7078
|
+
return `${text.slice(0, Math.max(1, maxLength - 3)).trimEnd()}...`;
|
|
7117
7079
|
}
|
|
7118
|
-
|
|
7119
|
-
|
|
7120
|
-
|
|
7121
|
-
|
|
7122
|
-
|
|
7123
|
-
|
|
7124
|
-
|
|
7080
|
+
function truncateStatusText(text) {
|
|
7081
|
+
const trimmed = text.trim();
|
|
7082
|
+
if (!trimmed) {
|
|
7083
|
+
return "";
|
|
7084
|
+
}
|
|
7085
|
+
return truncateWithEllipsis(trimmed, SLACK_STATUS_MAX_LENGTH);
|
|
7086
|
+
}
|
|
7087
|
+
function compactStatusPath(value) {
|
|
7088
|
+
if (typeof value !== "string") {
|
|
7089
|
+
return void 0;
|
|
7090
|
+
}
|
|
7091
|
+
const trimmed = value.trim();
|
|
7092
|
+
if (!trimmed) {
|
|
7093
|
+
return void 0;
|
|
7094
|
+
}
|
|
7095
|
+
if (trimmed.length <= 80) {
|
|
7096
|
+
return trimmed;
|
|
7097
|
+
}
|
|
7098
|
+
return `...${trimmed.slice(-77)}`;
|
|
7099
|
+
}
|
|
7100
|
+
function compactStatusText(value, maxLength = 80) {
|
|
7101
|
+
if (typeof value !== "string") {
|
|
7102
|
+
return void 0;
|
|
7103
|
+
}
|
|
7104
|
+
const trimmed = value.trim();
|
|
7105
|
+
if (!trimmed) {
|
|
7106
|
+
return void 0;
|
|
7107
|
+
}
|
|
7108
|
+
return truncateWithEllipsis(trimmed, maxLength);
|
|
7109
|
+
}
|
|
7110
|
+
function readShellToken(command, startIndex) {
|
|
7111
|
+
let index = startIndex;
|
|
7112
|
+
while (index < command.length && /\s/.test(command[index] ?? "")) {
|
|
7113
|
+
index += 1;
|
|
7114
|
+
}
|
|
7115
|
+
if (index >= command.length) {
|
|
7116
|
+
return void 0;
|
|
7117
|
+
}
|
|
7118
|
+
let token = "";
|
|
7119
|
+
let quote;
|
|
7120
|
+
while (index < command.length) {
|
|
7121
|
+
const char = command[index];
|
|
7122
|
+
if (!char) {
|
|
7123
|
+
break;
|
|
7124
|
+
}
|
|
7125
|
+
if (quote) {
|
|
7126
|
+
if (char === quote) {
|
|
7127
|
+
quote = void 0;
|
|
7128
|
+
index += 1;
|
|
7129
|
+
continue;
|
|
7130
|
+
}
|
|
7131
|
+
if (char === "\\" && quote === '"' && index + 1 < command.length) {
|
|
7132
|
+
token += command[index + 1];
|
|
7133
|
+
index += 2;
|
|
7134
|
+
continue;
|
|
7135
|
+
}
|
|
7136
|
+
token += char;
|
|
7137
|
+
index += 1;
|
|
7138
|
+
continue;
|
|
7139
|
+
}
|
|
7140
|
+
if (/\s/.test(char)) {
|
|
7141
|
+
break;
|
|
7142
|
+
}
|
|
7143
|
+
if (char === '"' || char === "'") {
|
|
7144
|
+
quote = char;
|
|
7145
|
+
index += 1;
|
|
7146
|
+
continue;
|
|
7147
|
+
}
|
|
7148
|
+
if (char === "\\" && index + 1 < command.length) {
|
|
7149
|
+
token += command[index + 1];
|
|
7150
|
+
index += 2;
|
|
7151
|
+
continue;
|
|
7152
|
+
}
|
|
7153
|
+
token += char;
|
|
7154
|
+
index += 1;
|
|
7155
|
+
}
|
|
7156
|
+
return { token, nextIndex: index };
|
|
7157
|
+
}
|
|
7158
|
+
function compactStatusCommand(value) {
|
|
7159
|
+
if (typeof value !== "string") {
|
|
7160
|
+
return void 0;
|
|
7161
|
+
}
|
|
7162
|
+
const trimmed = value.trim();
|
|
7163
|
+
if (!trimmed) {
|
|
7164
|
+
return void 0;
|
|
7165
|
+
}
|
|
7166
|
+
let index = 0;
|
|
7167
|
+
while (index < trimmed.length) {
|
|
7168
|
+
const parsed = readShellToken(trimmed, index);
|
|
7169
|
+
if (!parsed) {
|
|
7170
|
+
return void 0;
|
|
7171
|
+
}
|
|
7172
|
+
index = parsed.nextIndex;
|
|
7173
|
+
if (!parsed.token) {
|
|
7174
|
+
continue;
|
|
7175
|
+
}
|
|
7176
|
+
if (/^[A-Za-z_][A-Za-z0-9_]*=/.test(parsed.token)) {
|
|
7177
|
+
continue;
|
|
7178
|
+
}
|
|
7179
|
+
const normalized = parsed.token.replace(/[\\/]+$/g, "");
|
|
7180
|
+
if (!normalized) {
|
|
7181
|
+
return void 0;
|
|
7182
|
+
}
|
|
7183
|
+
const parts = normalized.split(/[\\/]/).filter((part) => part.length > 0);
|
|
7184
|
+
const command = parts.length > 0 ? parts[parts.length - 1] : normalized;
|
|
7185
|
+
return compactStatusText(command, 40);
|
|
7186
|
+
}
|
|
7187
|
+
return void 0;
|
|
7188
|
+
}
|
|
7189
|
+
function compactStatusFilename(value) {
|
|
7190
|
+
if (typeof value !== "string") {
|
|
7191
|
+
return void 0;
|
|
7192
|
+
}
|
|
7193
|
+
const trimmed = value.trim().replace(/[\\/]+$/g, "");
|
|
7194
|
+
if (!trimmed) {
|
|
7195
|
+
return void 0;
|
|
7196
|
+
}
|
|
7197
|
+
const parts = trimmed.split(/[\\/]/).filter((part) => part.length > 0);
|
|
7198
|
+
const filename = parts.length > 0 ? parts[parts.length - 1] : trimmed;
|
|
7199
|
+
return compactStatusText(filename, 80);
|
|
7200
|
+
}
|
|
7201
|
+
function extractStatusUrlDomain(value) {
|
|
7202
|
+
if (typeof value !== "string") {
|
|
7203
|
+
return void 0;
|
|
7204
|
+
}
|
|
7205
|
+
const trimmed = value.trim();
|
|
7206
|
+
if (!trimmed) {
|
|
7207
|
+
return void 0;
|
|
7208
|
+
}
|
|
7209
|
+
try {
|
|
7210
|
+
const parsed = new URL(trimmed);
|
|
7211
|
+
return parsed.hostname || void 0;
|
|
7212
|
+
} catch {
|
|
7213
|
+
return void 0;
|
|
7214
|
+
}
|
|
7215
|
+
}
|
|
7216
|
+
|
|
7217
|
+
// src/chat/runtime/assistant-status.ts
|
|
7218
|
+
var STATUS_PATTERNS = {
|
|
7219
|
+
thinking: {
|
|
7220
|
+
defaultContext: "\u2026",
|
|
7221
|
+
variants: ["Thinking", "Reasoning", "Considering", "Working through"]
|
|
7222
|
+
},
|
|
7223
|
+
searching: {
|
|
7224
|
+
defaultContext: "sources",
|
|
7225
|
+
variants: ["Searching", "Scanning", "Probing", "Trawling"]
|
|
7226
|
+
},
|
|
7227
|
+
reading: {
|
|
7228
|
+
defaultContext: "task",
|
|
7229
|
+
variants: ["Reading", "Inspecting", "Parsing", "Skimming"]
|
|
7230
|
+
},
|
|
7231
|
+
reviewing: {
|
|
7232
|
+
defaultContext: "results",
|
|
7233
|
+
variants: ["Reviewing", "Checking", "Inspecting", "Auditing"]
|
|
7234
|
+
},
|
|
7235
|
+
loading: {
|
|
7236
|
+
defaultContext: "task",
|
|
7237
|
+
variants: ["Loading", "Priming", "Booting", "Spinning up"]
|
|
7238
|
+
},
|
|
7239
|
+
updating: {
|
|
7240
|
+
defaultContext: "state",
|
|
7241
|
+
variants: ["Updating", "Patching", "Refreshing", "Adjusting"]
|
|
7242
|
+
},
|
|
7243
|
+
fetching: {
|
|
7244
|
+
defaultContext: "sources",
|
|
7245
|
+
variants: ["Fetching", "Pulling", "Retrieving", "Loading"]
|
|
7246
|
+
},
|
|
7247
|
+
creating: {
|
|
7248
|
+
defaultContext: "draft",
|
|
7249
|
+
variants: ["Creating", "Building", "Assembling", "Generating"]
|
|
7250
|
+
},
|
|
7251
|
+
listing: {
|
|
7252
|
+
defaultContext: "items",
|
|
7253
|
+
variants: ["Listing", "Gathering", "Collecting", "Enumerating"]
|
|
7254
|
+
},
|
|
7255
|
+
posting: {
|
|
7256
|
+
defaultContext: "reply",
|
|
7257
|
+
variants: ["Posting", "Sending", "Delivering", "Dispatching"]
|
|
7258
|
+
},
|
|
7259
|
+
adding: {
|
|
7260
|
+
defaultContext: "details",
|
|
7261
|
+
variants: ["Adding", "Applying", "Attaching", "Dropping in"]
|
|
7262
|
+
},
|
|
7263
|
+
running: {
|
|
7264
|
+
defaultContext: "tasks",
|
|
7265
|
+
variants: ["Running", "Executing", "Launching", "Processing"]
|
|
7266
|
+
}
|
|
7267
|
+
};
|
|
7268
|
+
function makeAssistantStatus(kind, context) {
|
|
7269
|
+
return { kind, ...context ? { context } : {} };
|
|
7270
|
+
}
|
|
7271
|
+
function normalizeAssistantStatusText(text) {
|
|
7272
|
+
const trimmed = text.trim();
|
|
7273
|
+
if (!trimmed) {
|
|
7274
|
+
return "";
|
|
7275
|
+
}
|
|
7276
|
+
return truncateStatusText(trimmed.replace(/(?:\.\s*)+$/, "").trim());
|
|
7277
|
+
}
|
|
7278
|
+
function buildAssistantStatusPresentation(args) {
|
|
7279
|
+
const random = args.random ?? Math.random;
|
|
7280
|
+
const pattern = STATUS_PATTERNS[args.status.kind];
|
|
7281
|
+
const context = normalizeAssistantStatusText(args.status.context ?? "") || pattern.defaultContext;
|
|
7282
|
+
const index = Math.floor(random() * pattern.variants.length);
|
|
7283
|
+
const verb = pattern.variants[index] ?? pattern.variants[0];
|
|
7284
|
+
const visible = truncateStatusText(`${verb} ${context}`);
|
|
7285
|
+
const hint = truncateStatusText(`${pattern.variants[0]} ${context}`);
|
|
7286
|
+
return {
|
|
7287
|
+
key: `${args.status.kind}:${context}`,
|
|
7288
|
+
hint,
|
|
7289
|
+
visible,
|
|
7290
|
+
suggestions: Array.from(/* @__PURE__ */ new Set([visible, hint]))
|
|
7291
|
+
};
|
|
7292
|
+
}
|
|
7293
|
+
function createSlackAdapterAssistantStatusTransport(args) {
|
|
7294
|
+
return {
|
|
7295
|
+
async setStatus(channelId, threadTs, status, suggestions) {
|
|
7296
|
+
try {
|
|
7297
|
+
await args.getSlackAdapter().setAssistantStatus(channelId, threadTs, status, suggestions);
|
|
7298
|
+
} catch (error) {
|
|
7299
|
+
logAssistantStatusFailure(status, error);
|
|
7300
|
+
}
|
|
7301
|
+
}
|
|
7302
|
+
};
|
|
7303
|
+
}
|
|
7304
|
+
function createSlackWebApiAssistantStatusTransport(args) {
|
|
7305
|
+
const getClient2 = args?.getSlackClient ?? getSlackClient;
|
|
7306
|
+
return {
|
|
7307
|
+
async setStatus(channelId, threadTs, status, suggestions) {
|
|
7308
|
+
try {
|
|
7309
|
+
await getClient2().assistant.threads.setStatus({
|
|
7310
|
+
channel_id: channelId,
|
|
7311
|
+
thread_ts: threadTs,
|
|
7312
|
+
status,
|
|
7313
|
+
...suggestions ? { loading_messages: suggestions } : {}
|
|
7314
|
+
});
|
|
7315
|
+
} catch (error) {
|
|
7316
|
+
logAssistantStatusFailure(status, error);
|
|
7317
|
+
}
|
|
7318
|
+
}
|
|
7319
|
+
};
|
|
7320
|
+
}
|
|
7321
|
+
function logAssistantStatusFailure(status, error) {
|
|
7322
|
+
logWarn(
|
|
7323
|
+
"assistant_status_update_failed",
|
|
7324
|
+
{},
|
|
7325
|
+
{
|
|
7326
|
+
"app.slack.status_text": status || "(clear)",
|
|
7327
|
+
"error.message": error instanceof Error ? error.message : String(error)
|
|
7328
|
+
},
|
|
7329
|
+
"Failed to update assistant status"
|
|
7330
|
+
);
|
|
7331
|
+
}
|
|
7332
|
+
|
|
7333
|
+
// src/chat/sandbox/skill-sync.ts
|
|
7334
|
+
import fs3 from "fs/promises";
|
|
7335
|
+
import path4 from "path";
|
|
7336
|
+
|
|
7337
|
+
// src/chat/sandbox/eval-gh-stub.ts
|
|
7338
|
+
function buildEvalGitHubCliStub() {
|
|
7339
|
+
return `#!/usr/bin/env node
|
|
7340
|
+
const fs = require("node:fs");
|
|
7341
|
+
const path = require("node:path");
|
|
7342
|
+
const { spawnSync } = require("node:child_process");
|
|
7343
|
+
|
|
7344
|
+
const args = process.argv.slice(2);
|
|
7345
|
+
const statePath = "/vercel/sandbox/.junior/eval-gh-state.json";
|
|
7346
|
+
const fallbackBinaries = ["/usr/bin/gh", "/usr/local/bin/gh", "/bin/gh"];
|
|
7347
|
+
const flagsWithValues = new Set([
|
|
7348
|
+
"--repo",
|
|
7349
|
+
"--title",
|
|
7350
|
+
"--body",
|
|
7351
|
+
"--body-file",
|
|
7352
|
+
"--json",
|
|
7353
|
+
"--search",
|
|
7354
|
+
"--state",
|
|
7355
|
+
"--limit",
|
|
7356
|
+
"--method",
|
|
7357
|
+
"--jq",
|
|
7358
|
+
"--template",
|
|
7359
|
+
"--hostname",
|
|
7360
|
+
]);
|
|
7361
|
+
|
|
7362
|
+
function getFlag(name) {
|
|
7363
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
7364
|
+
const value = args[index];
|
|
7365
|
+
if (value === name) {
|
|
7366
|
+
return args[index + 1];
|
|
7367
|
+
}
|
|
7368
|
+
if (value.startsWith(name + "=")) {
|
|
7369
|
+
return value.slice(name.length + 1);
|
|
7370
|
+
}
|
|
7371
|
+
}
|
|
7372
|
+
return undefined;
|
|
7373
|
+
}
|
|
7374
|
+
|
|
7375
|
+
function getPositionals() {
|
|
7376
|
+
const values = [];
|
|
7377
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
7378
|
+
const value = args[index];
|
|
7379
|
+
if (flagsWithValues.has(value)) {
|
|
7380
|
+
index += 1;
|
|
7125
7381
|
continue;
|
|
7126
7382
|
}
|
|
7127
7383
|
if (value.startsWith("--") && value.includes("=")) {
|
|
@@ -7256,6 +7512,8 @@ if (args[0] === "api") {
|
|
|
7256
7512
|
outputJson({ items: [] });
|
|
7257
7513
|
process.exit(0);
|
|
7258
7514
|
}
|
|
7515
|
+
outputJson({});
|
|
7516
|
+
process.exit(0);
|
|
7259
7517
|
}
|
|
7260
7518
|
|
|
7261
7519
|
if (args[0] === "issue") {
|
|
@@ -7297,7 +7555,9 @@ if (args[0] === "issue") {
|
|
|
7297
7555
|
|
|
7298
7556
|
const number = Number.parseInt(positionals[2] || "", 10);
|
|
7299
7557
|
const key = repo + "#" + number;
|
|
7300
|
-
const record =
|
|
7558
|
+
const record =
|
|
7559
|
+
state.issues[key] ||
|
|
7560
|
+
defaultIssue(repo, Number.isFinite(number) ? number : 101);
|
|
7301
7561
|
|
|
7302
7562
|
if (subcommand === "view") {
|
|
7303
7563
|
const jsonFields = getFlag("--json");
|
|
@@ -7338,156 +7598,315 @@ if (args[0] === "issue") {
|
|
|
7338
7598
|
fallbackToRealGh();
|
|
7339
7599
|
`;
|
|
7340
7600
|
}
|
|
7341
|
-
|
|
7342
|
-
|
|
7343
|
-
|
|
7344
|
-
|
|
7345
|
-
|
|
7346
|
-
|
|
7347
|
-
|
|
7348
|
-
|
|
7349
|
-
|
|
7601
|
+
|
|
7602
|
+
// src/chat/sandbox/skill-sync.ts
|
|
7603
|
+
function toPosixRelative(base, absolute) {
|
|
7604
|
+
return path4.relative(base, absolute).split(path4.sep).join("/");
|
|
7605
|
+
}
|
|
7606
|
+
async function listFilesRecursive(root) {
|
|
7607
|
+
const queue = [root];
|
|
7608
|
+
const files = [];
|
|
7609
|
+
while (queue.length > 0) {
|
|
7610
|
+
const dir = queue.shift();
|
|
7611
|
+
const entries = await fs3.readdir(dir, { withFileTypes: true });
|
|
7612
|
+
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
7613
|
+
for (const entry of entries) {
|
|
7614
|
+
const absolute = path4.join(dir, entry.name);
|
|
7615
|
+
if (entry.isDirectory()) {
|
|
7616
|
+
queue.push(absolute);
|
|
7617
|
+
} else if (entry.isFile()) {
|
|
7618
|
+
files.push(absolute);
|
|
7619
|
+
}
|
|
7350
7620
|
}
|
|
7351
7621
|
}
|
|
7352
|
-
return
|
|
7353
|
-
(directory) => directory === SANDBOX_WORKSPACE_ROOT || directory.startsWith(`${SANDBOX_WORKSPACE_ROOT}/`)
|
|
7354
|
-
).sort((a, b) => a.length - b.length);
|
|
7355
|
-
}
|
|
7356
|
-
function getSandboxErrorDetails(error) {
|
|
7357
|
-
return extractHttpErrorDetails(error, {
|
|
7358
|
-
attributePrefix: "app.sandbox.api_error",
|
|
7359
|
-
extraFields: [...SANDBOX_ERROR_FIELDS]
|
|
7360
|
-
});
|
|
7622
|
+
return files;
|
|
7361
7623
|
}
|
|
7362
|
-
function
|
|
7363
|
-
|
|
7364
|
-
|
|
7624
|
+
async function buildSkillSyncFiles(availableSkills, runtimeBinDir) {
|
|
7625
|
+
const filesToWrite = [];
|
|
7626
|
+
const index = {
|
|
7627
|
+
skills: []
|
|
7628
|
+
};
|
|
7629
|
+
for (const skill of availableSkills) {
|
|
7630
|
+
const skillFiles = await listFilesRecursive(skill.skillPath);
|
|
7631
|
+
for (const absoluteFile of skillFiles) {
|
|
7632
|
+
const relative = toPosixRelative(skill.skillPath, absoluteFile);
|
|
7633
|
+
if (!relative || relative.startsWith("..")) {
|
|
7634
|
+
continue;
|
|
7635
|
+
}
|
|
7636
|
+
filesToWrite.push({
|
|
7637
|
+
path: `${sandboxSkillDir(skill.name)}/${relative}`,
|
|
7638
|
+
content: await fs3.readFile(absoluteFile)
|
|
7639
|
+
});
|
|
7640
|
+
}
|
|
7641
|
+
index.skills.push({
|
|
7642
|
+
name: skill.name,
|
|
7643
|
+
description: skill.description,
|
|
7644
|
+
root: sandboxSkillDir(skill.name)
|
|
7645
|
+
});
|
|
7646
|
+
}
|
|
7647
|
+
filesToWrite.push({
|
|
7648
|
+
path: `${SANDBOX_SKILLS_ROOT}/index.json`,
|
|
7649
|
+
content: Buffer.from(JSON.stringify(index), "utf8")
|
|
7365
7650
|
});
|
|
7651
|
+
if (process.env.EVAL_ENABLE_TEST_CREDENTIALS === "1") {
|
|
7652
|
+
filesToWrite.push({
|
|
7653
|
+
path: `${runtimeBinDir}/gh`,
|
|
7654
|
+
content: Buffer.from(buildEvalGitHubCliStub(), "utf8")
|
|
7655
|
+
});
|
|
7656
|
+
}
|
|
7657
|
+
return filesToWrite;
|
|
7366
7658
|
}
|
|
7367
|
-
function
|
|
7368
|
-
const
|
|
7369
|
-
|
|
7659
|
+
function collectDirectories(filesToWrite, workspaceRoot) {
|
|
7660
|
+
const directoriesToEnsure = /* @__PURE__ */ new Set();
|
|
7661
|
+
for (const file of filesToWrite) {
|
|
7662
|
+
const normalizedPath = path4.posix.normalize(file.path);
|
|
7663
|
+
const parts = normalizedPath.split("/").filter(Boolean);
|
|
7664
|
+
let current = "";
|
|
7665
|
+
for (let index = 0; index < parts.length - 1; index += 1) {
|
|
7666
|
+
current = `${current}/${parts[index]}`;
|
|
7667
|
+
directoriesToEnsure.add(current);
|
|
7668
|
+
}
|
|
7669
|
+
}
|
|
7670
|
+
return Array.from(directoriesToEnsure).filter(
|
|
7671
|
+
(directory) => directory === workspaceRoot || directory.startsWith(`${workspaceRoot}/`)
|
|
7672
|
+
).sort((a, b) => a.length - b.length);
|
|
7370
7673
|
}
|
|
7371
|
-
function
|
|
7372
|
-
const
|
|
7373
|
-
|
|
7374
|
-
|
|
7375
|
-
if (
|
|
7376
|
-
|
|
7674
|
+
function resolveHostSkillPath(availableSkills, sandboxPath) {
|
|
7675
|
+
const normalizedPath = path4.posix.normalize(sandboxPath.trim());
|
|
7676
|
+
for (const skill of availableSkills) {
|
|
7677
|
+
const virtualRoot = sandboxSkillDir(skill.name);
|
|
7678
|
+
if (normalizedPath !== virtualRoot && !normalizedPath.startsWith(`${virtualRoot}/`)) {
|
|
7679
|
+
continue;
|
|
7377
7680
|
}
|
|
7378
|
-
|
|
7379
|
-
if (
|
|
7380
|
-
|
|
7381
|
-
} else {
|
|
7382
|
-
current = void 0;
|
|
7681
|
+
const relativePath = path4.posix.relative(virtualRoot, normalizedPath);
|
|
7682
|
+
if (!relativePath || relativePath.startsWith("../")) {
|
|
7683
|
+
return null;
|
|
7383
7684
|
}
|
|
7685
|
+
const hostRoot = path4.resolve(skill.skillPath);
|
|
7686
|
+
const hostPath = path4.resolve(hostRoot, ...relativePath.split("/"));
|
|
7687
|
+
if (hostPath !== hostRoot && !hostPath.startsWith(`${hostRoot}${path4.sep}`)) {
|
|
7688
|
+
return null;
|
|
7689
|
+
}
|
|
7690
|
+
return hostPath;
|
|
7384
7691
|
}
|
|
7385
|
-
return
|
|
7386
|
-
}
|
|
7387
|
-
function isSandboxUnavailableError(error) {
|
|
7388
|
-
return findInErrorChain(error, (candidate) => {
|
|
7389
|
-
const details = getSandboxErrorDetails(candidate);
|
|
7390
|
-
const searchable = `${details.searchableText} ${details.summary}`.toLowerCase();
|
|
7391
|
-
return searchable.includes("sandbox_stopped") || searchable.includes("status=410") || searchable.includes("status code 410") || searchable.includes("no longer available");
|
|
7392
|
-
});
|
|
7692
|
+
return null;
|
|
7393
7693
|
}
|
|
7394
|
-
function
|
|
7395
|
-
return
|
|
7396
|
-
|
|
7397
|
-
|
|
7398
|
-
return searchable.includes("sandbox_snapshotting") || searchable.includes("creating a snapshot") || searchable.includes("stopped shortly");
|
|
7399
|
-
});
|
|
7694
|
+
function isHostFileMissingError(error) {
|
|
7695
|
+
return Boolean(
|
|
7696
|
+
error && typeof error === "object" && error.code === "ENOENT"
|
|
7697
|
+
);
|
|
7400
7698
|
}
|
|
7401
|
-
function
|
|
7402
|
-
const
|
|
7403
|
-
|
|
7404
|
-
|
|
7405
|
-
|
|
7406
|
-
|
|
7407
|
-
|
|
7408
|
-
|
|
7409
|
-
|
|
7699
|
+
async function syncSkillsToSandbox(params) {
|
|
7700
|
+
const workspaceRoot = params.workspaceRoot ?? SANDBOX_WORKSPACE_ROOT;
|
|
7701
|
+
await params.withSpan(
|
|
7702
|
+
"sandbox.sync_skills",
|
|
7703
|
+
"sandbox.sync",
|
|
7704
|
+
{
|
|
7705
|
+
"app.sandbox.skills_count": params.skills.length
|
|
7706
|
+
},
|
|
7707
|
+
async () => {
|
|
7708
|
+
const filesToWrite = await buildSkillSyncFiles(
|
|
7709
|
+
params.skills,
|
|
7710
|
+
params.runtimeBinDir
|
|
7711
|
+
);
|
|
7712
|
+
const bytesWritten = filesToWrite.reduce(
|
|
7713
|
+
(total, file) => total + file.content.length,
|
|
7714
|
+
0
|
|
7715
|
+
);
|
|
7716
|
+
const directories = collectDirectories(filesToWrite, workspaceRoot);
|
|
7717
|
+
await params.withSpan(
|
|
7718
|
+
"sandbox.sync_writeFiles",
|
|
7719
|
+
"sandbox.sync.write",
|
|
7720
|
+
{
|
|
7721
|
+
"app.sandbox.sync.files_written": filesToWrite.length,
|
|
7722
|
+
"app.sandbox.sync.bytes_written": bytesWritten,
|
|
7723
|
+
"app.sandbox.sync.directories_ensured": directories.length
|
|
7724
|
+
},
|
|
7725
|
+
async () => {
|
|
7726
|
+
try {
|
|
7727
|
+
for (const directory of directories) {
|
|
7728
|
+
try {
|
|
7729
|
+
await params.sandbox.mkDir(directory);
|
|
7730
|
+
} catch (error) {
|
|
7731
|
+
if (!isAlreadyExistsError(error)) {
|
|
7732
|
+
throw error;
|
|
7733
|
+
}
|
|
7734
|
+
}
|
|
7735
|
+
}
|
|
7736
|
+
await params.sandbox.writeFiles(filesToWrite);
|
|
7737
|
+
const executableFiles = filesToWrite.map((file) => file.path).filter(
|
|
7738
|
+
(filePath) => filePath.startsWith(`${params.runtimeBinDir}/`)
|
|
7739
|
+
);
|
|
7740
|
+
for (const filePath of executableFiles) {
|
|
7741
|
+
const chmod = await runNonInteractiveCommand(params.sandbox, {
|
|
7742
|
+
cmd: "chmod",
|
|
7743
|
+
args: ["0755", filePath],
|
|
7744
|
+
cwd: workspaceRoot
|
|
7745
|
+
});
|
|
7746
|
+
if (chmod.exitCode !== 0) {
|
|
7747
|
+
throw new Error(
|
|
7748
|
+
`sandbox chmod failed for ${filePath}: ${await chmod.stderr() || await chmod.stdout() || `exit ${chmod.exitCode}`}`
|
|
7749
|
+
);
|
|
7750
|
+
}
|
|
7751
|
+
}
|
|
7752
|
+
} catch (error) {
|
|
7753
|
+
throwSandboxOperationError("sandbox writeFiles", error, true);
|
|
7754
|
+
}
|
|
7755
|
+
}
|
|
7756
|
+
);
|
|
7410
7757
|
}
|
|
7411
|
-
|
|
7412
|
-
current = typeof current === "object" ? current.cause : void 0;
|
|
7413
|
-
}
|
|
7414
|
-
return void 0;
|
|
7758
|
+
);
|
|
7415
7759
|
}
|
|
7416
|
-
|
|
7417
|
-
|
|
7418
|
-
|
|
7419
|
-
|
|
7420
|
-
|
|
7421
|
-
|
|
7422
|
-
|
|
7423
|
-
|
|
7424
|
-
|
|
7425
|
-
}
|
|
7426
|
-
|
|
7427
|
-
|
|
7428
|
-
|
|
7429
|
-
|
|
7430
|
-
|
|
7760
|
+
|
|
7761
|
+
// src/chat/sandbox/session.ts
|
|
7762
|
+
var DEFAULT_MAX_OUTPUT_LENGTH = 3e4;
|
|
7763
|
+
var SANDBOX_RUNTIME = "node22";
|
|
7764
|
+
var SANDBOX_RUNTIME_BIN_DIR = `${SANDBOX_WORKSPACE_ROOT}/.junior/bin`;
|
|
7765
|
+
var SNAPSHOT_BOOT_RETRY_COUNT = 3;
|
|
7766
|
+
var SNAPSHOT_BOOT_RETRY_DELAY_MS = 1e3;
|
|
7767
|
+
var SNAPSHOT_PHASE_STATUS = {
|
|
7768
|
+
resolve_start: { kind: "loading", context: "sandbox snapshot cache" },
|
|
7769
|
+
waiting_for_lock: { kind: "loading", context: "sandbox snapshot build" },
|
|
7770
|
+
building_snapshot: { kind: "creating", context: "sandbox snapshot" },
|
|
7771
|
+
cache_hit: { kind: "loading", context: "sandbox snapshot" }
|
|
7772
|
+
};
|
|
7773
|
+
function mergeNetworkPolicyWithHeaderTransforms(networkPolicy, headerTransforms) {
|
|
7774
|
+
const basePolicy = networkPolicy && typeof networkPolicy === "object" && !Array.isArray(networkPolicy) ? { ...networkPolicy } : {};
|
|
7775
|
+
const existingAllowRaw = basePolicy.allow;
|
|
7776
|
+
const existingAllow = existingAllowRaw && typeof existingAllowRaw === "object" && !Array.isArray(existingAllowRaw) ? Object.fromEntries(
|
|
7777
|
+
Object.entries(existingAllowRaw).map(
|
|
7778
|
+
([domain, rules]) => [
|
|
7779
|
+
domain,
|
|
7780
|
+
Array.isArray(rules) ? [...rules] : []
|
|
7781
|
+
]
|
|
7782
|
+
)
|
|
7783
|
+
) : { "*": [] };
|
|
7784
|
+
for (const transform of headerTransforms) {
|
|
7785
|
+
const currentRules = existingAllow[transform.domain] ?? [];
|
|
7786
|
+
existingAllow[transform.domain] = [
|
|
7787
|
+
...currentRules,
|
|
7788
|
+
{ transform: [{ headers: transform.headers }] }
|
|
7789
|
+
];
|
|
7431
7790
|
}
|
|
7432
|
-
|
|
7433
|
-
|
|
7434
|
-
|
|
7791
|
+
return {
|
|
7792
|
+
...basePolicy,
|
|
7793
|
+
allow: existingAllow
|
|
7794
|
+
};
|
|
7795
|
+
}
|
|
7796
|
+
function truncateOutput(output, maxLength) {
|
|
7797
|
+
if (output.length <= maxLength) {
|
|
7798
|
+
return { value: output, truncated: false };
|
|
7435
7799
|
}
|
|
7436
|
-
|
|
7800
|
+
const truncatedLength = output.length - maxLength;
|
|
7801
|
+
return {
|
|
7802
|
+
value: `${output.slice(0, maxLength)}
|
|
7803
|
+
|
|
7804
|
+
[output truncated: ${truncatedLength} characters removed]`,
|
|
7805
|
+
truncated: true
|
|
7806
|
+
};
|
|
7437
7807
|
}
|
|
7438
|
-
function
|
|
7439
|
-
|
|
7440
|
-
|
|
7441
|
-
...details.attributes,
|
|
7442
|
-
...includeMissingPath ? {
|
|
7443
|
-
"app.sandbox.api_error.missing_path": details.searchableText.includes("no such file") || details.searchableText.includes("enoent")
|
|
7444
|
-
} : {},
|
|
7445
|
-
"app.sandbox.success": false
|
|
7808
|
+
function sleep2(ms) {
|
|
7809
|
+
return new Promise((resolve) => {
|
|
7810
|
+
setTimeout(resolve, ms);
|
|
7446
7811
|
});
|
|
7447
|
-
|
|
7448
|
-
|
|
7449
|
-
|
|
7450
|
-
|
|
7451
|
-
|
|
7812
|
+
}
|
|
7813
|
+
function createStatusEmitter(emitStatus) {
|
|
7814
|
+
let statusCount = 0;
|
|
7815
|
+
const sentStatuses = /* @__PURE__ */ new Set();
|
|
7816
|
+
const emit = async (status) => {
|
|
7817
|
+
const statusKey = `${status.kind}:${status.context ?? ""}`;
|
|
7818
|
+
if (!emitStatus || statusCount >= 4 || sentStatuses.has(statusKey)) {
|
|
7819
|
+
return;
|
|
7820
|
+
}
|
|
7821
|
+
sentStatuses.add(statusKey);
|
|
7822
|
+
statusCount += 1;
|
|
7823
|
+
await emitStatus(status);
|
|
7824
|
+
};
|
|
7825
|
+
const reportSnapshotPhase = async (phase) => {
|
|
7826
|
+
const status = SNAPSHOT_PHASE_STATUS[phase];
|
|
7827
|
+
if (status) {
|
|
7828
|
+
await emit(makeAssistantStatus(status.kind, status.context));
|
|
7452
7829
|
}
|
|
7830
|
+
};
|
|
7831
|
+
return { emit, reportSnapshotPhase };
|
|
7832
|
+
}
|
|
7833
|
+
function parseKeepAliveMs() {
|
|
7834
|
+
const parsed = Number.parseInt(
|
|
7835
|
+
process.env.VERCEL_SANDBOX_KEEPALIVE_MS ?? "0",
|
|
7836
|
+
10
|
|
7453
7837
|
);
|
|
7838
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : 0;
|
|
7454
7839
|
}
|
|
7455
|
-
function
|
|
7840
|
+
function createSandboxSessionManager(options) {
|
|
7456
7841
|
let sandbox = null;
|
|
7457
7842
|
let sandboxIdHint = options?.sandboxId;
|
|
7458
7843
|
let availableSkills = [];
|
|
7459
7844
|
let toolExecutors;
|
|
7460
7845
|
const timeoutMs = options?.timeoutMs ?? 1e3 * 60 * 30;
|
|
7461
7846
|
const traceContext = options?.traceContext ?? {};
|
|
7462
|
-
const emitStatus = options?.onStatus;
|
|
7463
7847
|
const dependencyProfileHash = getRuntimeDependencyProfileHash(SANDBOX_RUNTIME);
|
|
7464
7848
|
const withSandboxSpan = (name, op, attributes, callback) => withSpan(name, op, traceContext, callback, attributes);
|
|
7465
|
-
const
|
|
7466
|
-
|
|
7467
|
-
|
|
7468
|
-
|
|
7469
|
-
|
|
7470
|
-
|
|
7471
|
-
|
|
7472
|
-
|
|
7473
|
-
|
|
7474
|
-
|
|
7475
|
-
|
|
7476
|
-
|
|
7477
|
-
|
|
7478
|
-
|
|
7479
|
-
|
|
7849
|
+
const emitSandboxStatus = async (source, statusEmitter, status) => {
|
|
7850
|
+
logInfo(
|
|
7851
|
+
"sandbox_status_emitted",
|
|
7852
|
+
traceContext,
|
|
7853
|
+
{
|
|
7854
|
+
"app.sandbox.status.source": source,
|
|
7855
|
+
"app.sandbox.status.kind": status.kind,
|
|
7856
|
+
...status.context ? { "app.sandbox.status.context": status.context } : {}
|
|
7857
|
+
},
|
|
7858
|
+
"Sandbox status emitted"
|
|
7859
|
+
);
|
|
7860
|
+
if (typeof statusEmitter === "function") {
|
|
7861
|
+
await statusEmitter(status);
|
|
7862
|
+
return;
|
|
7863
|
+
}
|
|
7864
|
+
await statusEmitter.emit(status);
|
|
7865
|
+
};
|
|
7866
|
+
const clearSession = () => {
|
|
7867
|
+
sandbox = null;
|
|
7868
|
+
sandboxIdHint = void 0;
|
|
7869
|
+
toolExecutors = void 0;
|
|
7870
|
+
};
|
|
7871
|
+
const rememberSandbox = (nextSandbox) => {
|
|
7872
|
+
sandbox = nextSandbox;
|
|
7873
|
+
sandboxIdHint = nextSandbox.sandboxId;
|
|
7874
|
+
toolExecutors = void 0;
|
|
7875
|
+
return nextSandbox;
|
|
7876
|
+
};
|
|
7877
|
+
const failSetup = (error) => {
|
|
7878
|
+
throw wrapSandboxSetupError(error);
|
|
7879
|
+
};
|
|
7880
|
+
const syncSkills = async (targetSandbox) => {
|
|
7881
|
+
await syncSkillsToSandbox({
|
|
7882
|
+
sandbox: targetSandbox,
|
|
7883
|
+
skills: availableSkills,
|
|
7884
|
+
withSpan: withSandboxSpan,
|
|
7885
|
+
runtimeBinDir: SANDBOX_RUNTIME_BIN_DIR
|
|
7886
|
+
});
|
|
7887
|
+
};
|
|
7888
|
+
const ensureSandboxReachable = async (targetSandbox, source) => {
|
|
7889
|
+
await withSandboxSpan(
|
|
7890
|
+
"sandbox.reuse_probe",
|
|
7891
|
+
"sandbox.acquire.probe",
|
|
7892
|
+
{
|
|
7893
|
+
"app.sandbox.reused": true,
|
|
7894
|
+
"app.sandbox.source": source
|
|
7895
|
+
},
|
|
7896
|
+
async () => {
|
|
7897
|
+
try {
|
|
7898
|
+
await targetSandbox.mkDir(SANDBOX_WORKSPACE_ROOT);
|
|
7899
|
+
} catch (error) {
|
|
7900
|
+
if (!isAlreadyExistsError(error)) {
|
|
7901
|
+
throw error;
|
|
7902
|
+
}
|
|
7480
7903
|
}
|
|
7481
|
-
await sleep2(SNAPSHOT_BOOT_RETRY_DELAY_MS);
|
|
7482
7904
|
}
|
|
7483
|
-
|
|
7484
|
-
throw new Error(`Failed to boot sandbox from snapshot ${snapshotId}`);
|
|
7905
|
+
);
|
|
7485
7906
|
};
|
|
7486
7907
|
const invalidateSandboxInstance = async (targetSandbox, reason) => {
|
|
7487
7908
|
if (sandbox === targetSandbox) {
|
|
7488
|
-
|
|
7489
|
-
sandboxIdHint = void 0;
|
|
7490
|
-
toolExecutors = void 0;
|
|
7909
|
+
clearSession();
|
|
7491
7910
|
}
|
|
7492
7911
|
logWarn(
|
|
7493
7912
|
"sandbox_network_policy_restore_failed",
|
|
@@ -7502,277 +7921,304 @@ function createSandboxExecutor(options) {
|
|
|
7502
7921
|
} catch {
|
|
7503
7922
|
}
|
|
7504
7923
|
};
|
|
7505
|
-
const
|
|
7506
|
-
|
|
7507
|
-
"sandbox.
|
|
7508
|
-
"sandbox.
|
|
7509
|
-
|
|
7510
|
-
|
|
7511
|
-
|
|
7512
|
-
|
|
7513
|
-
|
|
7514
|
-
|
|
7515
|
-
|
|
7516
|
-
|
|
7517
|
-
|
|
7518
|
-
|
|
7519
|
-
|
|
7520
|
-
|
|
7521
|
-
|
|
7522
|
-
|
|
7523
|
-
|
|
7524
|
-
"
|
|
7525
|
-
|
|
7526
|
-
|
|
7527
|
-
|
|
7528
|
-
|
|
7529
|
-
|
|
7530
|
-
|
|
7531
|
-
|
|
7532
|
-
|
|
7533
|
-
|
|
7534
|
-
|
|
7535
|
-
|
|
7536
|
-
|
|
7537
|
-
|
|
7538
|
-
|
|
7539
|
-
|
|
7540
|
-
(filePath) => filePath.startsWith(`${SANDBOX_RUNTIME_BIN_DIR}/`)
|
|
7541
|
-
);
|
|
7542
|
-
for (const filePath of executableFiles) {
|
|
7543
|
-
const chmod = await runNonInteractiveCommand(targetSandbox, {
|
|
7544
|
-
cmd: "chmod",
|
|
7545
|
-
args: ["0755", filePath],
|
|
7546
|
-
cwd: SANDBOX_WORKSPACE_ROOT
|
|
7547
|
-
});
|
|
7548
|
-
if (chmod.exitCode !== 0) {
|
|
7549
|
-
throw new Error(
|
|
7550
|
-
`sandbox chmod failed for ${filePath}: ${await chmod.stderr() || await chmod.stdout() || `exit ${chmod.exitCode}`}`
|
|
7551
|
-
);
|
|
7552
|
-
}
|
|
7553
|
-
}
|
|
7554
|
-
} catch (error) {
|
|
7555
|
-
throwSandboxOperationError("sandbox writeFiles", error, true);
|
|
7556
|
-
}
|
|
7557
|
-
}
|
|
7558
|
-
);
|
|
7924
|
+
const recreateUnavailableSandbox = async (source) => {
|
|
7925
|
+
setSpanAttributes({
|
|
7926
|
+
"app.sandbox.recovery.attempted": true,
|
|
7927
|
+
"app.sandbox.recovery.source": source
|
|
7928
|
+
});
|
|
7929
|
+
clearSession();
|
|
7930
|
+
const replacement = await createFreshSandbox();
|
|
7931
|
+
setSpanAttributes({
|
|
7932
|
+
"app.sandbox.recovery.succeeded": true
|
|
7933
|
+
});
|
|
7934
|
+
return replacement;
|
|
7935
|
+
};
|
|
7936
|
+
const createSandboxFromSnapshot = async (snapshotId, sandboxCredentials, emitStatus) => {
|
|
7937
|
+
for (let attempt = 0; attempt < SNAPSHOT_BOOT_RETRY_COUNT; attempt += 1) {
|
|
7938
|
+
try {
|
|
7939
|
+
if (emitStatus) {
|
|
7940
|
+
await emitSandboxStatus(
|
|
7941
|
+
"snapshot_boot",
|
|
7942
|
+
emitStatus,
|
|
7943
|
+
makeAssistantStatus("loading", "sandbox")
|
|
7944
|
+
);
|
|
7945
|
+
}
|
|
7946
|
+
return await Sandbox.create({
|
|
7947
|
+
timeout: timeoutMs,
|
|
7948
|
+
source: {
|
|
7949
|
+
type: "snapshot",
|
|
7950
|
+
snapshotId
|
|
7951
|
+
},
|
|
7952
|
+
...sandboxCredentials ?? {}
|
|
7953
|
+
});
|
|
7954
|
+
} catch (error) {
|
|
7955
|
+
if (!isSnapshottingError(error) || attempt === SNAPSHOT_BOOT_RETRY_COUNT - 1) {
|
|
7956
|
+
throw error;
|
|
7957
|
+
}
|
|
7958
|
+
await sleep2(SNAPSHOT_BOOT_RETRY_DELAY_MS);
|
|
7559
7959
|
}
|
|
7560
|
-
|
|
7960
|
+
}
|
|
7961
|
+
throw new Error(`Failed to boot sandbox from snapshot ${snapshotId}`);
|
|
7962
|
+
};
|
|
7963
|
+
const setSnapshotAttributes = (snapshot) => {
|
|
7964
|
+
setSpanAttributes({
|
|
7965
|
+
"app.sandbox.source": snapshot.snapshotId ? "snapshot" : "created",
|
|
7966
|
+
"app.sandbox.snapshot.cache_hit": snapshot.cacheHit,
|
|
7967
|
+
"app.sandbox.snapshot.resolve_outcome": snapshot.resolveOutcome,
|
|
7968
|
+
...snapshot.profileHash ? {
|
|
7969
|
+
"app.sandbox.snapshot.profile_hash": snapshot.profileHash
|
|
7970
|
+
} : {},
|
|
7971
|
+
"app.sandbox.snapshot.dependency_count": snapshot.dependencyCount,
|
|
7972
|
+
...snapshot.rebuildReason ? {
|
|
7973
|
+
"app.sandbox.snapshot.rebuild_reason": snapshot.rebuildReason
|
|
7974
|
+
} : {}
|
|
7975
|
+
});
|
|
7976
|
+
};
|
|
7977
|
+
const createSandboxFromResolvedSnapshot = async (params) => {
|
|
7978
|
+
const { runtime, snapshot, sandboxCredentials, status } = params;
|
|
7979
|
+
if (!snapshot.snapshotId) {
|
|
7980
|
+
await emitSandboxStatus(
|
|
7981
|
+
"fresh_runtime_boot",
|
|
7982
|
+
status,
|
|
7983
|
+
makeAssistantStatus("loading", "sandbox")
|
|
7984
|
+
);
|
|
7985
|
+
return await Sandbox.create({
|
|
7986
|
+
timeout: timeoutMs,
|
|
7987
|
+
runtime,
|
|
7988
|
+
...sandboxCredentials ?? {}
|
|
7989
|
+
});
|
|
7990
|
+
}
|
|
7991
|
+
try {
|
|
7992
|
+
return await createSandboxFromSnapshot(
|
|
7993
|
+
snapshot.snapshotId,
|
|
7994
|
+
sandboxCredentials,
|
|
7995
|
+
status.emit
|
|
7996
|
+
);
|
|
7997
|
+
} catch (error) {
|
|
7998
|
+
if (!isSnapshotMissingError(error)) {
|
|
7999
|
+
throw error;
|
|
8000
|
+
}
|
|
8001
|
+
setSpanAttributes({
|
|
8002
|
+
"app.sandbox.snapshot.rebuild_after_missing": true
|
|
8003
|
+
});
|
|
8004
|
+
const rebuiltSnapshot = await resolveRuntimeDependencySnapshot({
|
|
8005
|
+
runtime,
|
|
8006
|
+
timeoutMs,
|
|
8007
|
+
forceRebuild: true,
|
|
8008
|
+
staleSnapshotId: snapshot.snapshotId,
|
|
8009
|
+
onProgress: status.reportSnapshotPhase
|
|
8010
|
+
});
|
|
8011
|
+
if (!rebuiltSnapshot.snapshotId) {
|
|
8012
|
+
throw error;
|
|
8013
|
+
}
|
|
8014
|
+
return await createSandboxFromSnapshot(
|
|
8015
|
+
rebuiltSnapshot.snapshotId,
|
|
8016
|
+
sandboxCredentials,
|
|
8017
|
+
status.emit
|
|
8018
|
+
);
|
|
8019
|
+
}
|
|
8020
|
+
};
|
|
8021
|
+
const createFreshSandbox = async () => {
|
|
8022
|
+
const runtime = SANDBOX_RUNTIME;
|
|
8023
|
+
const sandboxCredentials = getVercelSandboxCredentials();
|
|
8024
|
+
const status = createStatusEmitter(options?.onStatus);
|
|
8025
|
+
let createdSandbox;
|
|
8026
|
+
try {
|
|
8027
|
+
createdSandbox = await withSandboxSpan(
|
|
8028
|
+
"sandbox.create",
|
|
8029
|
+
"sandbox.create",
|
|
8030
|
+
{
|
|
8031
|
+
"app.sandbox.reused": false,
|
|
8032
|
+
"app.sandbox.timeout_ms": timeoutMs,
|
|
8033
|
+
"app.sandbox.runtime": runtime
|
|
8034
|
+
},
|
|
8035
|
+
async () => {
|
|
8036
|
+
await emitSandboxStatus(
|
|
8037
|
+
"runtime_dependency_resolve",
|
|
8038
|
+
status,
|
|
8039
|
+
makeAssistantStatus("loading", "sandbox runtime")
|
|
8040
|
+
);
|
|
8041
|
+
const snapshot = await resolveRuntimeDependencySnapshot({
|
|
8042
|
+
runtime,
|
|
8043
|
+
timeoutMs,
|
|
8044
|
+
onProgress: status.reportSnapshotPhase
|
|
8045
|
+
});
|
|
8046
|
+
setSnapshotAttributes(snapshot);
|
|
8047
|
+
return await createSandboxFromResolvedSnapshot({
|
|
8048
|
+
runtime,
|
|
8049
|
+
snapshot,
|
|
8050
|
+
sandboxCredentials,
|
|
8051
|
+
status
|
|
8052
|
+
});
|
|
8053
|
+
}
|
|
8054
|
+
);
|
|
8055
|
+
} catch (error) {
|
|
8056
|
+
return failSetup(error);
|
|
8057
|
+
}
|
|
8058
|
+
try {
|
|
8059
|
+
await syncSkills(createdSandbox);
|
|
8060
|
+
} catch (error) {
|
|
8061
|
+
return failSetup(error);
|
|
8062
|
+
}
|
|
8063
|
+
return rememberSandbox(createdSandbox);
|
|
8064
|
+
};
|
|
8065
|
+
const discardHintIfProfileChanged = () => {
|
|
8066
|
+
if (sandbox || !sandboxIdHint || dependencyProfileHash === options?.sandboxDependencyProfileHash) {
|
|
8067
|
+
return;
|
|
8068
|
+
}
|
|
8069
|
+
setSpanAttributes({
|
|
8070
|
+
"app.sandbox.reused": false,
|
|
8071
|
+
"app.sandbox.recreate.reason": "dependency_profile_mismatch",
|
|
8072
|
+
...options?.sandboxDependencyProfileHash ? {
|
|
8073
|
+
"app.sandbox.previous_profile_hash": options.sandboxDependencyProfileHash
|
|
8074
|
+
} : {},
|
|
8075
|
+
...dependencyProfileHash ? { "app.sandbox.current_profile_hash": dependencyProfileHash } : {}
|
|
8076
|
+
});
|
|
8077
|
+
sandboxIdHint = void 0;
|
|
8078
|
+
};
|
|
8079
|
+
const tryReuseCachedSandbox = async () => {
|
|
8080
|
+
const cachedSandbox = sandbox;
|
|
8081
|
+
if (!cachedSandbox) {
|
|
8082
|
+
return null;
|
|
8083
|
+
}
|
|
8084
|
+
try {
|
|
8085
|
+
await ensureSandboxReachable(cachedSandbox, "memory");
|
|
8086
|
+
return cachedSandbox;
|
|
8087
|
+
} catch (error) {
|
|
8088
|
+
if (isSandboxUnavailableError(error)) {
|
|
8089
|
+
return await recreateUnavailableSandbox("memory");
|
|
8090
|
+
}
|
|
8091
|
+
return failSetup(error);
|
|
8092
|
+
}
|
|
8093
|
+
};
|
|
8094
|
+
const tryRestoreHintedSandbox = async () => {
|
|
8095
|
+
if (!sandboxIdHint) {
|
|
8096
|
+
return null;
|
|
8097
|
+
}
|
|
8098
|
+
let hintedSandbox = null;
|
|
8099
|
+
try {
|
|
8100
|
+
const sandboxCredentials = getVercelSandboxCredentials();
|
|
8101
|
+
hintedSandbox = await withSandboxSpan(
|
|
8102
|
+
"sandbox.get",
|
|
8103
|
+
"sandbox.get",
|
|
8104
|
+
{
|
|
8105
|
+
"app.sandbox.reused": true,
|
|
8106
|
+
"app.sandbox.source": "id_hint"
|
|
8107
|
+
},
|
|
8108
|
+
async () => await Sandbox.get({
|
|
8109
|
+
sandboxId: sandboxIdHint,
|
|
8110
|
+
...sandboxCredentials ?? {}
|
|
8111
|
+
})
|
|
8112
|
+
);
|
|
8113
|
+
} catch {
|
|
8114
|
+
return null;
|
|
8115
|
+
}
|
|
8116
|
+
try {
|
|
8117
|
+
await syncSkills(hintedSandbox);
|
|
8118
|
+
return rememberSandbox(hintedSandbox);
|
|
8119
|
+
} catch (error) {
|
|
8120
|
+
if (isSandboxUnavailableError(error)) {
|
|
8121
|
+
return await recreateUnavailableSandbox("id_hint");
|
|
8122
|
+
}
|
|
8123
|
+
return failSetup(error);
|
|
8124
|
+
}
|
|
7561
8125
|
};
|
|
7562
8126
|
const acquireSandbox = async () => {
|
|
7563
|
-
return withSandboxSpan(
|
|
8127
|
+
return await withSandboxSpan(
|
|
7564
8128
|
"sandbox.acquire",
|
|
7565
8129
|
"sandbox.acquire",
|
|
7566
8130
|
{
|
|
7567
8131
|
"app.sandbox.id_hint_present": Boolean(sandboxIdHint),
|
|
7568
8132
|
"app.sandbox.timeout_ms": timeoutMs,
|
|
7569
|
-
"app.sandbox.runtime":
|
|
8133
|
+
"app.sandbox.runtime": SANDBOX_RUNTIME,
|
|
7570
8134
|
"app.sandbox.skills_count": availableSkills.length
|
|
7571
8135
|
},
|
|
7572
8136
|
async () => {
|
|
7573
|
-
|
|
7574
|
-
const
|
|
7575
|
-
|
|
7576
|
-
|
|
7577
|
-
toolExecutors = void 0;
|
|
7578
|
-
return nextSandbox;
|
|
7579
|
-
};
|
|
7580
|
-
const handleSetupFailure = (error) => {
|
|
7581
|
-
throw wrapSandboxSetupError(error);
|
|
7582
|
-
};
|
|
7583
|
-
const createFreshSandbox = async () => {
|
|
7584
|
-
const runtime = SANDBOX_RUNTIME;
|
|
7585
|
-
let statusCount = 0;
|
|
7586
|
-
const sentStatuses = /* @__PURE__ */ new Set();
|
|
7587
|
-
const emitSandboxStatus = async (status) => {
|
|
7588
|
-
if (!emitStatus || statusCount >= 4 || sentStatuses.has(status)) {
|
|
7589
|
-
return;
|
|
7590
|
-
}
|
|
7591
|
-
sentStatuses.add(status);
|
|
7592
|
-
statusCount += 1;
|
|
7593
|
-
await emitStatus(status);
|
|
7594
|
-
};
|
|
7595
|
-
const reportSnapshotPhase = async (phase) => {
|
|
7596
|
-
if (phase === "resolve_start") {
|
|
7597
|
-
await emitSandboxStatus("Checking sandbox snapshot cache...");
|
|
7598
|
-
return;
|
|
7599
|
-
}
|
|
7600
|
-
if (phase === "waiting_for_lock") {
|
|
7601
|
-
await emitSandboxStatus("Waiting for sandbox snapshot build...");
|
|
7602
|
-
return;
|
|
7603
|
-
}
|
|
7604
|
-
if (phase === "building_snapshot") {
|
|
7605
|
-
await emitSandboxStatus("Building sandbox snapshot...");
|
|
7606
|
-
return;
|
|
7607
|
-
}
|
|
7608
|
-
if (phase === "cache_hit") {
|
|
7609
|
-
await emitSandboxStatus("Using cached sandbox snapshot...");
|
|
7610
|
-
}
|
|
7611
|
-
};
|
|
7612
|
-
let createdSandbox;
|
|
7613
|
-
try {
|
|
7614
|
-
createdSandbox = await withSandboxSpan(
|
|
7615
|
-
"sandbox.create",
|
|
7616
|
-
"sandbox.create",
|
|
7617
|
-
{
|
|
7618
|
-
"app.sandbox.reused": false,
|
|
7619
|
-
"app.sandbox.timeout_ms": timeoutMs,
|
|
7620
|
-
"app.sandbox.runtime": runtime
|
|
7621
|
-
},
|
|
7622
|
-
async () => {
|
|
7623
|
-
await emitSandboxStatus("Preparing sandbox runtime...");
|
|
7624
|
-
const snapshot = await resolveRuntimeDependencySnapshot({
|
|
7625
|
-
runtime,
|
|
7626
|
-
timeoutMs,
|
|
7627
|
-
onProgress: reportSnapshotPhase
|
|
7628
|
-
});
|
|
7629
|
-
setSpanAttributes({
|
|
7630
|
-
"app.sandbox.source": snapshot.snapshotId ? "snapshot" : "created",
|
|
7631
|
-
"app.sandbox.snapshot.cache_hit": snapshot.cacheHit,
|
|
7632
|
-
"app.sandbox.snapshot.resolve_outcome": snapshot.resolveOutcome,
|
|
7633
|
-
...snapshot.profileHash ? {
|
|
7634
|
-
"app.sandbox.snapshot.profile_hash": snapshot.profileHash
|
|
7635
|
-
} : {},
|
|
7636
|
-
"app.sandbox.snapshot.dependency_count": snapshot.dependencyCount,
|
|
7637
|
-
...snapshot.rebuildReason ? {
|
|
7638
|
-
"app.sandbox.snapshot.rebuild_reason": snapshot.rebuildReason
|
|
7639
|
-
} : {}
|
|
7640
|
-
});
|
|
7641
|
-
if (!snapshot.snapshotId) {
|
|
7642
|
-
await emitSandboxStatus("Booting up...");
|
|
7643
|
-
return await Sandbox.create({
|
|
7644
|
-
timeout: timeoutMs,
|
|
7645
|
-
runtime,
|
|
7646
|
-
...sandboxCredentials ?? {}
|
|
7647
|
-
});
|
|
7648
|
-
}
|
|
7649
|
-
try {
|
|
7650
|
-
return await createSandboxFromSnapshot(
|
|
7651
|
-
snapshot.snapshotId,
|
|
7652
|
-
sandboxCredentials,
|
|
7653
|
-
emitSandboxStatus
|
|
7654
|
-
);
|
|
7655
|
-
} catch (error) {
|
|
7656
|
-
if (!isSnapshotMissingError(error)) {
|
|
7657
|
-
throw error;
|
|
7658
|
-
}
|
|
7659
|
-
setSpanAttributes({
|
|
7660
|
-
"app.sandbox.snapshot.rebuild_after_missing": true
|
|
7661
|
-
});
|
|
7662
|
-
const rebuiltSnapshot = await resolveRuntimeDependencySnapshot({
|
|
7663
|
-
runtime,
|
|
7664
|
-
timeoutMs,
|
|
7665
|
-
forceRebuild: true,
|
|
7666
|
-
staleSnapshotId: snapshot.snapshotId,
|
|
7667
|
-
onProgress: reportSnapshotPhase
|
|
7668
|
-
});
|
|
7669
|
-
if (!rebuiltSnapshot.snapshotId) {
|
|
7670
|
-
throw error;
|
|
7671
|
-
}
|
|
7672
|
-
return await createSandboxFromSnapshot(
|
|
7673
|
-
rebuiltSnapshot.snapshotId,
|
|
7674
|
-
sandboxCredentials,
|
|
7675
|
-
emitSandboxStatus
|
|
7676
|
-
);
|
|
7677
|
-
}
|
|
7678
|
-
}
|
|
7679
|
-
);
|
|
7680
|
-
} catch (error) {
|
|
7681
|
-
return handleSetupFailure(error);
|
|
7682
|
-
}
|
|
7683
|
-
try {
|
|
7684
|
-
await upsertSkillsToSandbox(createdSandbox);
|
|
7685
|
-
} catch (error) {
|
|
7686
|
-
return handleSetupFailure(error);
|
|
7687
|
-
}
|
|
7688
|
-
return assignSandbox(createdSandbox);
|
|
7689
|
-
};
|
|
7690
|
-
if (!sandbox && sandboxIdHint && dependencyProfileHash !== options?.sandboxDependencyProfileHash) {
|
|
7691
|
-
setSpanAttributes({
|
|
7692
|
-
"app.sandbox.reused": false,
|
|
7693
|
-
"app.sandbox.recreate.reason": "dependency_profile_mismatch",
|
|
7694
|
-
...options?.sandboxDependencyProfileHash ? {
|
|
7695
|
-
"app.sandbox.previous_profile_hash": options.sandboxDependencyProfileHash
|
|
7696
|
-
} : {},
|
|
7697
|
-
...dependencyProfileHash ? { "app.sandbox.current_profile_hash": dependencyProfileHash } : {}
|
|
7698
|
-
});
|
|
7699
|
-
sandboxIdHint = void 0;
|
|
8137
|
+
discardHintIfProfileChanged();
|
|
8138
|
+
const cachedSandbox = await tryReuseCachedSandbox();
|
|
8139
|
+
if (cachedSandbox) {
|
|
8140
|
+
return cachedSandbox;
|
|
7700
8141
|
}
|
|
7701
|
-
const
|
|
7702
|
-
|
|
7703
|
-
|
|
7704
|
-
"app.sandbox.recovery.source": source
|
|
7705
|
-
});
|
|
7706
|
-
sandbox = null;
|
|
7707
|
-
sandboxIdHint = void 0;
|
|
7708
|
-
toolExecutors = void 0;
|
|
7709
|
-
const replacement = await createFreshSandbox();
|
|
7710
|
-
setSpanAttributes({
|
|
7711
|
-
"app.sandbox.recovery.succeeded": true
|
|
7712
|
-
});
|
|
7713
|
-
return replacement;
|
|
7714
|
-
};
|
|
7715
|
-
if (sandbox) {
|
|
7716
|
-
const cachedSandbox = sandbox;
|
|
7717
|
-
try {
|
|
7718
|
-
await withSandboxSpan(
|
|
7719
|
-
"sandbox.reuse_cached",
|
|
7720
|
-
"sandbox.acquire.cached",
|
|
7721
|
-
{
|
|
7722
|
-
"app.sandbox.reused": true,
|
|
7723
|
-
"app.sandbox.source": "memory"
|
|
7724
|
-
},
|
|
7725
|
-
async () => {
|
|
7726
|
-
await upsertSkillsToSandbox(cachedSandbox);
|
|
7727
|
-
}
|
|
7728
|
-
);
|
|
7729
|
-
return cachedSandbox;
|
|
7730
|
-
} catch (error) {
|
|
7731
|
-
if (isSandboxUnavailableError(error)) {
|
|
7732
|
-
return recoverUnavailableSandbox("memory");
|
|
7733
|
-
}
|
|
7734
|
-
return handleSetupFailure(error);
|
|
7735
|
-
}
|
|
8142
|
+
const hintedSandbox = await tryRestoreHintedSandbox();
|
|
8143
|
+
if (hintedSandbox) {
|
|
8144
|
+
return hintedSandbox;
|
|
7736
8145
|
}
|
|
7737
|
-
|
|
7738
|
-
if (sandboxIdHint) {
|
|
7739
|
-
try {
|
|
7740
|
-
acquiredSandbox = await withSandboxSpan(
|
|
7741
|
-
"sandbox.get",
|
|
7742
|
-
"sandbox.get",
|
|
7743
|
-
{
|
|
7744
|
-
"app.sandbox.reused": true,
|
|
7745
|
-
"app.sandbox.source": "id_hint"
|
|
7746
|
-
},
|
|
7747
|
-
async () => Sandbox.get({
|
|
7748
|
-
sandboxId: sandboxIdHint,
|
|
7749
|
-
...sandboxCredentials ?? {}
|
|
7750
|
-
})
|
|
7751
|
-
);
|
|
7752
|
-
} catch {
|
|
7753
|
-
acquiredSandbox = null;
|
|
7754
|
-
}
|
|
7755
|
-
}
|
|
7756
|
-
if (acquiredSandbox) {
|
|
7757
|
-
try {
|
|
7758
|
-
await upsertSkillsToSandbox(acquiredSandbox);
|
|
7759
|
-
return assignSandbox(acquiredSandbox);
|
|
7760
|
-
} catch (error) {
|
|
7761
|
-
if (isSandboxUnavailableError(error)) {
|
|
7762
|
-
return recoverUnavailableSandbox("id_hint");
|
|
7763
|
-
}
|
|
7764
|
-
return handleSetupFailure(error);
|
|
7765
|
-
}
|
|
7766
|
-
}
|
|
7767
|
-
return createFreshSandbox();
|
|
8146
|
+
return await createFreshSandbox();
|
|
7768
8147
|
}
|
|
7769
8148
|
);
|
|
7770
8149
|
};
|
|
7771
|
-
const
|
|
7772
|
-
|
|
7773
|
-
|
|
8150
|
+
const getMaxOutputLength = () => {
|
|
8151
|
+
const maxOutputLength = Number.parseInt(
|
|
8152
|
+
process.env.SANDBOX_BASH_MAX_OUTPUT_CHARS ?? "",
|
|
8153
|
+
10
|
|
8154
|
+
);
|
|
8155
|
+
return Number.isFinite(maxOutputLength) && maxOutputLength > 0 ? maxOutputLength : DEFAULT_MAX_OUTPUT_LENGTH;
|
|
8156
|
+
};
|
|
8157
|
+
const readCommandOutput = async (commandResult2) => {
|
|
8158
|
+
const boundedOutputLength = getMaxOutputLength();
|
|
8159
|
+
const stdoutRaw = await commandResult2.stdout();
|
|
8160
|
+
const stderrRaw = await commandResult2.stderr();
|
|
8161
|
+
const stdout = truncateOutput(stdoutRaw, boundedOutputLength);
|
|
8162
|
+
const stderr = truncateOutput(stderrRaw, boundedOutputLength);
|
|
8163
|
+
return {
|
|
8164
|
+
stdout: stdout.value,
|
|
8165
|
+
stderr: stderr.value,
|
|
8166
|
+
exitCode: commandResult2.exitCode,
|
|
8167
|
+
stdoutTruncated: stdout.truncated,
|
|
8168
|
+
stderrTruncated: stderr.truncated
|
|
8169
|
+
};
|
|
8170
|
+
};
|
|
8171
|
+
const withTemporaryHeaderTransforms = async (sandboxInstance, headerTransforms, callback) => {
|
|
8172
|
+
if (!headerTransforms || headerTransforms.length === 0) {
|
|
8173
|
+
return await callback();
|
|
8174
|
+
}
|
|
8175
|
+
const restoreNetworkPolicy = sandboxInstance.networkPolicy ?? "allow-all";
|
|
8176
|
+
const policy = mergeNetworkPolicyWithHeaderTransforms(
|
|
8177
|
+
restoreNetworkPolicy,
|
|
8178
|
+
headerTransforms
|
|
8179
|
+
);
|
|
8180
|
+
await sandboxInstance.updateNetworkPolicy(policy);
|
|
8181
|
+
let callbackError;
|
|
8182
|
+
let restoreError;
|
|
8183
|
+
let result;
|
|
8184
|
+
try {
|
|
8185
|
+
result = await callback();
|
|
8186
|
+
} catch (error) {
|
|
8187
|
+
callbackError = error;
|
|
8188
|
+
throw error;
|
|
8189
|
+
} finally {
|
|
8190
|
+
try {
|
|
8191
|
+
await sandboxInstance.updateNetworkPolicy(restoreNetworkPolicy);
|
|
8192
|
+
} catch (error) {
|
|
8193
|
+
restoreError = error;
|
|
8194
|
+
await invalidateSandboxInstance(sandboxInstance, error);
|
|
8195
|
+
}
|
|
7774
8196
|
}
|
|
7775
|
-
|
|
8197
|
+
if (restoreError && !callbackError) {
|
|
8198
|
+
throw restoreError;
|
|
8199
|
+
}
|
|
8200
|
+
return result;
|
|
8201
|
+
};
|
|
8202
|
+
const extendKeepAlive = async (activeSandbox) => {
|
|
8203
|
+
const keepAliveMs = parseKeepAliveMs();
|
|
8204
|
+
if (keepAliveMs === 0) {
|
|
8205
|
+
return;
|
|
8206
|
+
}
|
|
8207
|
+
try {
|
|
8208
|
+
await withSandboxSpan(
|
|
8209
|
+
"sandbox.keepalive.extend",
|
|
8210
|
+
"sandbox.keepalive",
|
|
8211
|
+
{
|
|
8212
|
+
"app.sandbox.keepalive_ms": keepAliveMs
|
|
8213
|
+
},
|
|
8214
|
+
async () => {
|
|
8215
|
+
await activeSandbox.extendTimeout(keepAliveMs);
|
|
8216
|
+
}
|
|
8217
|
+
);
|
|
8218
|
+
} catch {
|
|
8219
|
+
}
|
|
8220
|
+
};
|
|
8221
|
+
const buildToolExecutors = async (sandboxInstance) => {
|
|
7776
8222
|
const toolkit = await withSandboxSpan(
|
|
7777
8223
|
"sandbox.bash_tool.init",
|
|
7778
8224
|
"sandbox.tool.init",
|
|
@@ -7780,8 +8226,8 @@ function createSandboxExecutor(options) {
|
|
|
7780
8226
|
"app.sandbox.tool_name": "bash",
|
|
7781
8227
|
"app.sandbox.destination": SANDBOX_WORKSPACE_ROOT
|
|
7782
8228
|
},
|
|
7783
|
-
async () => createBashTool2({
|
|
7784
|
-
sandbox:
|
|
8229
|
+
async () => await createBashTool2({
|
|
8230
|
+
sandbox: sandboxInstance,
|
|
7785
8231
|
destination: SANDBOX_WORKSPACE_ROOT
|
|
7786
8232
|
})
|
|
7787
8233
|
);
|
|
@@ -7790,63 +8236,24 @@ function createSandboxExecutor(options) {
|
|
|
7790
8236
|
if (!executeReadFile || !executeWriteFile) {
|
|
7791
8237
|
throw new Error("bash-tool did not return executable tool handlers");
|
|
7792
8238
|
}
|
|
7793
|
-
|
|
8239
|
+
return {
|
|
7794
8240
|
bash: async (input) => {
|
|
7795
|
-
const restoreNetworkPolicy = activeSandbox.networkPolicy ?? "allow-all";
|
|
7796
|
-
const headerTransforms = input.headerTransforms;
|
|
7797
|
-
if (headerTransforms && headerTransforms.length > 0) {
|
|
7798
|
-
const policy = mergeNetworkPolicyWithHeaderTransforms(
|
|
7799
|
-
restoreNetworkPolicy,
|
|
7800
|
-
headerTransforms
|
|
7801
|
-
);
|
|
7802
|
-
await activeSandbox.updateNetworkPolicy(policy);
|
|
7803
|
-
}
|
|
7804
8241
|
const script = buildNonInteractiveShellScript(input.command, {
|
|
7805
8242
|
env: input.env,
|
|
7806
8243
|
pathPrefix: `${SANDBOX_RUNTIME_BIN_DIR}:$PATH`
|
|
7807
8244
|
});
|
|
7808
|
-
|
|
7809
|
-
|
|
7810
|
-
|
|
7811
|
-
|
|
7812
|
-
|
|
7813
|
-
|
|
7814
|
-
|
|
7815
|
-
|
|
7816
|
-
|
|
7817
|
-
|
|
7818
|
-
process.env.SANDBOX_BASH_MAX_OUTPUT_CHARS ?? "",
|
|
7819
|
-
10
|
|
7820
|
-
);
|
|
7821
|
-
const boundedOutputLength = Number.isFinite(maxOutputLength) && maxOutputLength > 0 ? maxOutputLength : DEFAULT_MAX_OUTPUT_LENGTH;
|
|
7822
|
-
const stdoutRaw = await commandResult2.stdout();
|
|
7823
|
-
const stderrRaw = await commandResult2.stderr();
|
|
7824
|
-
const stdout = truncateOutput(stdoutRaw, boundedOutputLength);
|
|
7825
|
-
const stderr = truncateOutput(stderrRaw, boundedOutputLength);
|
|
7826
|
-
result = {
|
|
7827
|
-
stdout: stdout.value,
|
|
7828
|
-
stderr: stderr.value,
|
|
7829
|
-
exitCode: commandResult2.exitCode,
|
|
7830
|
-
stdoutTruncated: stdout.truncated,
|
|
7831
|
-
stderrTruncated: stderr.truncated
|
|
7832
|
-
};
|
|
7833
|
-
} catch (error) {
|
|
7834
|
-
commandError = error;
|
|
7835
|
-
throw error;
|
|
7836
|
-
} finally {
|
|
7837
|
-
if (headerTransforms && headerTransforms.length > 0) {
|
|
7838
|
-
try {
|
|
7839
|
-
await activeSandbox.updateNetworkPolicy(restoreNetworkPolicy);
|
|
7840
|
-
} catch (error) {
|
|
7841
|
-
restoreError = error;
|
|
7842
|
-
await invalidateSandboxInstance(activeSandbox, error);
|
|
7843
|
-
}
|
|
8245
|
+
return await withTemporaryHeaderTransforms(
|
|
8246
|
+
sandboxInstance,
|
|
8247
|
+
input.headerTransforms,
|
|
8248
|
+
async () => {
|
|
8249
|
+
const commandResult2 = await sandboxInstance.runCommand({
|
|
8250
|
+
cmd: "bash",
|
|
8251
|
+
args: ["-c", script],
|
|
8252
|
+
cwd: SANDBOX_WORKSPACE_ROOT
|
|
8253
|
+
});
|
|
8254
|
+
return await readCommandOutput(commandResult2);
|
|
7844
8255
|
}
|
|
7845
|
-
|
|
7846
|
-
if (restoreError && !commandError) {
|
|
7847
|
-
throw restoreError;
|
|
7848
|
-
}
|
|
7849
|
-
return result;
|
|
8256
|
+
);
|
|
7850
8257
|
},
|
|
7851
8258
|
readFile: async (input) => await executeReadFile(input, {
|
|
7852
8259
|
toolCallId: "sandbox-read-file",
|
|
@@ -7857,191 +8264,18 @@ function createSandboxExecutor(options) {
|
|
|
7857
8264
|
messages: []
|
|
7858
8265
|
})
|
|
7859
8266
|
};
|
|
7860
|
-
return toolExecutors;
|
|
7861
8267
|
};
|
|
7862
|
-
const
|
|
7863
|
-
const rawInput = params.input ?? {};
|
|
7864
|
-
const bashCommand = params.toolName === "bash" ? String(rawInput.command ?? "").trim() : void 0;
|
|
7865
|
-
if (params.toolName === "bash") {
|
|
7866
|
-
if (!bashCommand) {
|
|
7867
|
-
throw new Error("command is required");
|
|
7868
|
-
}
|
|
7869
|
-
if (options?.runBashCustomCommand) {
|
|
7870
|
-
const custom = await options.runBashCustomCommand(bashCommand);
|
|
7871
|
-
if (custom.handled) {
|
|
7872
|
-
return { result: custom.result };
|
|
7873
|
-
}
|
|
7874
|
-
}
|
|
7875
|
-
}
|
|
8268
|
+
const ensureReadySandbox = async () => {
|
|
7876
8269
|
const activeSandbox = await acquireSandbox();
|
|
7877
|
-
|
|
7878
|
-
|
|
7879
|
-
10
|
|
7880
|
-
);
|
|
7881
|
-
if (Number.isFinite(keepAliveMs) && keepAliveMs > 0) {
|
|
7882
|
-
try {
|
|
7883
|
-
await withSandboxSpan(
|
|
7884
|
-
"sandbox.keepalive.extend",
|
|
7885
|
-
"sandbox.keepalive",
|
|
7886
|
-
{
|
|
7887
|
-
"app.sandbox.keepalive_ms": keepAliveMs
|
|
7888
|
-
},
|
|
7889
|
-
async () => {
|
|
7890
|
-
await activeSandbox.extendTimeout(keepAliveMs);
|
|
7891
|
-
}
|
|
7892
|
-
);
|
|
7893
|
-
} catch {
|
|
7894
|
-
}
|
|
7895
|
-
}
|
|
7896
|
-
if (params.toolName === "bash") {
|
|
7897
|
-
const command = bashCommand;
|
|
7898
|
-
const headerTransformsInput = rawInput.headerTransforms;
|
|
7899
|
-
const headerTransforms = Array.isArray(headerTransformsInput) ? headerTransformsInput.filter(
|
|
7900
|
-
(value) => Boolean(value && typeof value === "object")
|
|
7901
|
-
).map((transform) => ({
|
|
7902
|
-
domain: String(transform.domain ?? "").trim(),
|
|
7903
|
-
headers: transform.headers && typeof transform.headers === "object" && !Array.isArray(transform.headers) ? Object.fromEntries(
|
|
7904
|
-
Object.entries(
|
|
7905
|
-
transform.headers
|
|
7906
|
-
).filter(([, value]) => typeof value === "string").map(([key, value]) => [key, value])
|
|
7907
|
-
) : {}
|
|
7908
|
-
})).filter(
|
|
7909
|
-
(transform) => transform.domain.length > 0 && Object.keys(transform.headers).length > 0
|
|
7910
|
-
) : void 0;
|
|
7911
|
-
const envInput = rawInput.env;
|
|
7912
|
-
const env = envInput && typeof envInput === "object" && !Array.isArray(envInput) ? Object.fromEntries(
|
|
7913
|
-
Object.entries(envInput).filter(([, value]) => typeof value === "string").map(([key, value]) => [key, value])
|
|
7914
|
-
) : void 0;
|
|
7915
|
-
const executeBash = (await getToolExecutors()).bash;
|
|
7916
|
-
const result = await withSandboxSpan(
|
|
7917
|
-
"bash",
|
|
7918
|
-
"process.exec",
|
|
7919
|
-
{
|
|
7920
|
-
"process.executable.name": "bash"
|
|
7921
|
-
},
|
|
7922
|
-
async () => {
|
|
7923
|
-
try {
|
|
7924
|
-
const response = await executeBash({
|
|
7925
|
-
command,
|
|
7926
|
-
...headerTransforms ? { headerTransforms } : {},
|
|
7927
|
-
...env ? { env } : {}
|
|
7928
|
-
});
|
|
7929
|
-
setSpanAttributes({
|
|
7930
|
-
"process.exit.code": response.exitCode,
|
|
7931
|
-
"app.sandbox.stdout_bytes": Buffer.byteLength(
|
|
7932
|
-
response.stdout ?? "",
|
|
7933
|
-
"utf8"
|
|
7934
|
-
),
|
|
7935
|
-
"app.sandbox.stderr_bytes": Buffer.byteLength(
|
|
7936
|
-
response.stderr ?? "",
|
|
7937
|
-
"utf8"
|
|
7938
|
-
),
|
|
7939
|
-
...response.exitCode !== 0 ? { "error.type": "nonzero_exit" } : {}
|
|
7940
|
-
});
|
|
7941
|
-
setSpanStatus(response.exitCode === 0 ? "ok" : "error");
|
|
7942
|
-
return response;
|
|
7943
|
-
} catch (error) {
|
|
7944
|
-
setSpanAttributes({
|
|
7945
|
-
"error.type": error instanceof Error ? error.name : "sandbox_execute_error"
|
|
7946
|
-
});
|
|
7947
|
-
setSpanStatus("error");
|
|
7948
|
-
throw error;
|
|
7949
|
-
}
|
|
7950
|
-
}
|
|
7951
|
-
);
|
|
7952
|
-
return {
|
|
7953
|
-
result: {
|
|
7954
|
-
ok: result.exitCode === 0,
|
|
7955
|
-
command,
|
|
7956
|
-
cwd: SANDBOX_WORKSPACE_ROOT,
|
|
7957
|
-
exit_code: result.exitCode,
|
|
7958
|
-
signal: null,
|
|
7959
|
-
timed_out: false,
|
|
7960
|
-
stdout: result.stdout,
|
|
7961
|
-
stderr: result.stderr,
|
|
7962
|
-
stdout_truncated: result.stdoutTruncated,
|
|
7963
|
-
stderr_truncated: result.stderrTruncated
|
|
7964
|
-
}
|
|
7965
|
-
};
|
|
7966
|
-
}
|
|
7967
|
-
if (params.toolName === "readFile") {
|
|
7968
|
-
const filePath = String(rawInput.path ?? "").trim();
|
|
7969
|
-
if (!filePath) {
|
|
7970
|
-
throw new Error("path is required");
|
|
7971
|
-
}
|
|
7972
|
-
const executeReadFile = (await getToolExecutors()).readFile;
|
|
7973
|
-
const result = await withSandboxSpan(
|
|
7974
|
-
"sandbox.readFile",
|
|
7975
|
-
"sandbox.fs.read",
|
|
7976
|
-
{
|
|
7977
|
-
"app.sandbox.path.length": filePath.length
|
|
7978
|
-
},
|
|
7979
|
-
async () => {
|
|
7980
|
-
const response = await executeReadFile({ path: filePath });
|
|
7981
|
-
const content = String(response.content ?? "");
|
|
7982
|
-
setSpanAttributes({
|
|
7983
|
-
"app.sandbox.read.bytes": Buffer.byteLength(content, "utf8"),
|
|
7984
|
-
"app.sandbox.read.chars": content.length
|
|
7985
|
-
});
|
|
7986
|
-
setSpanStatus("ok");
|
|
7987
|
-
return {
|
|
7988
|
-
content,
|
|
7989
|
-
path: filePath,
|
|
7990
|
-
success: true
|
|
7991
|
-
};
|
|
7992
|
-
}
|
|
7993
|
-
);
|
|
7994
|
-
return { result };
|
|
7995
|
-
}
|
|
7996
|
-
if (params.toolName === "writeFile") {
|
|
7997
|
-
const filePath = String(rawInput.path ?? "").trim();
|
|
7998
|
-
if (!filePath) {
|
|
7999
|
-
throw new Error("path is required");
|
|
8000
|
-
}
|
|
8001
|
-
const content = String(rawInput.content ?? "");
|
|
8002
|
-
const executeWriteFile = (await getToolExecutors()).writeFile;
|
|
8003
|
-
await withSandboxSpan(
|
|
8004
|
-
"sandbox.writeFile",
|
|
8005
|
-
"sandbox.fs.write",
|
|
8006
|
-
{
|
|
8007
|
-
"app.sandbox.path.length": filePath.length,
|
|
8008
|
-
"app.sandbox.write.bytes": Buffer.byteLength(content, "utf8")
|
|
8009
|
-
},
|
|
8010
|
-
async () => {
|
|
8011
|
-
try {
|
|
8012
|
-
await executeWriteFile({ path: filePath, content });
|
|
8013
|
-
setSpanStatus("ok");
|
|
8014
|
-
} catch (error) {
|
|
8015
|
-
throwSandboxOperationError("sandbox writeFile", error);
|
|
8016
|
-
}
|
|
8017
|
-
}
|
|
8018
|
-
);
|
|
8019
|
-
return {
|
|
8020
|
-
result: {
|
|
8021
|
-
ok: true,
|
|
8022
|
-
path: filePath,
|
|
8023
|
-
bytes_written: Buffer.byteLength(content, "utf8")
|
|
8024
|
-
}
|
|
8025
|
-
};
|
|
8026
|
-
}
|
|
8027
|
-
throw new Error(`unsupported sandbox tool: ${params.toolName}`);
|
|
8270
|
+
await extendKeepAlive(activeSandbox);
|
|
8271
|
+
return activeSandbox;
|
|
8028
8272
|
};
|
|
8029
|
-
const
|
|
8030
|
-
if (
|
|
8031
|
-
return;
|
|
8273
|
+
const loadToolExecutors = async (activeSandbox) => {
|
|
8274
|
+
if (toolExecutors) {
|
|
8275
|
+
return toolExecutors;
|
|
8032
8276
|
}
|
|
8033
|
-
await
|
|
8034
|
-
|
|
8035
|
-
"sandbox.stop",
|
|
8036
|
-
{
|
|
8037
|
-
"app.sandbox.stop.blocking": true
|
|
8038
|
-
},
|
|
8039
|
-
async () => {
|
|
8040
|
-
await sandbox.stop({ blocking: true });
|
|
8041
|
-
}
|
|
8042
|
-
);
|
|
8043
|
-
sandbox = null;
|
|
8044
|
-
toolExecutors = void 0;
|
|
8277
|
+
toolExecutors = await buildToolExecutors(activeSandbox);
|
|
8278
|
+
return toolExecutors;
|
|
8045
8279
|
};
|
|
8046
8280
|
return {
|
|
8047
8281
|
configureSkills(skills) {
|
|
@@ -8053,199 +8287,298 @@ function createSandboxExecutor(options) {
|
|
|
8053
8287
|
getDependencyProfileHash() {
|
|
8054
8288
|
return dependencyProfileHash;
|
|
8055
8289
|
},
|
|
8056
|
-
canExecute(toolName) {
|
|
8057
|
-
return SANDBOX_TOOL_NAMES.has(toolName);
|
|
8058
|
-
},
|
|
8059
8290
|
async createSandbox() {
|
|
8060
8291
|
return await acquireSandbox();
|
|
8061
8292
|
},
|
|
8062
|
-
|
|
8063
|
-
|
|
8293
|
+
async ensureToolExecutors() {
|
|
8294
|
+
return await loadToolExecutors(await ensureReadySandbox());
|
|
8295
|
+
},
|
|
8296
|
+
async dispose() {
|
|
8297
|
+
const activeSandbox = sandbox;
|
|
8298
|
+
if (!activeSandbox) {
|
|
8299
|
+
return;
|
|
8300
|
+
}
|
|
8301
|
+
await withSandboxSpan(
|
|
8302
|
+
"sandbox.stop",
|
|
8303
|
+
"sandbox.stop",
|
|
8304
|
+
{
|
|
8305
|
+
"app.sandbox.stop.blocking": true
|
|
8306
|
+
},
|
|
8307
|
+
async () => {
|
|
8308
|
+
await activeSandbox.stop({ blocking: true });
|
|
8309
|
+
}
|
|
8310
|
+
);
|
|
8311
|
+
sandbox = null;
|
|
8312
|
+
toolExecutors = void 0;
|
|
8313
|
+
}
|
|
8064
8314
|
};
|
|
8065
8315
|
}
|
|
8066
8316
|
|
|
8067
|
-
// src/chat/
|
|
8068
|
-
|
|
8069
|
-
|
|
8070
|
-
|
|
8071
|
-
|
|
8072
|
-
// src/chat/runtime/status-format.ts
|
|
8073
|
-
var SLACK_STATUS_MAX_LENGTH = 50;
|
|
8074
|
-
function truncateWithEllipsis(text, maxLength) {
|
|
8075
|
-
if (text.length <= maxLength) {
|
|
8076
|
-
return text;
|
|
8077
|
-
}
|
|
8078
|
-
return `${text.slice(0, Math.max(1, maxLength - 3)).trimEnd()}...`;
|
|
8079
|
-
}
|
|
8080
|
-
function truncateStatusText(text) {
|
|
8081
|
-
const trimmed = text.trim();
|
|
8082
|
-
if (!trimmed) {
|
|
8083
|
-
return "";
|
|
8084
|
-
}
|
|
8085
|
-
return truncateWithEllipsis(trimmed, SLACK_STATUS_MAX_LENGTH);
|
|
8086
|
-
}
|
|
8087
|
-
function compactStatusPath(value) {
|
|
8088
|
-
if (typeof value !== "string") {
|
|
8089
|
-
return void 0;
|
|
8090
|
-
}
|
|
8091
|
-
const trimmed = value.trim();
|
|
8092
|
-
if (!trimmed) {
|
|
8317
|
+
// src/chat/sandbox/sandbox.ts
|
|
8318
|
+
var SANDBOX_TOOL_NAMES = /* @__PURE__ */ new Set(["bash", "readFile", "writeFile"]);
|
|
8319
|
+
function parseHeaderTransforms(raw) {
|
|
8320
|
+
if (!Array.isArray(raw)) {
|
|
8093
8321
|
return void 0;
|
|
8094
8322
|
}
|
|
8095
|
-
|
|
8096
|
-
|
|
8097
|
-
|
|
8098
|
-
|
|
8323
|
+
return raw.filter(
|
|
8324
|
+
(value) => Boolean(value && typeof value === "object")
|
|
8325
|
+
).map((transform) => ({
|
|
8326
|
+
domain: String(transform.domain ?? "").trim(),
|
|
8327
|
+
headers: transform.headers && typeof transform.headers === "object" && !Array.isArray(transform.headers) ? Object.fromEntries(
|
|
8328
|
+
Object.entries(transform.headers).filter(([, value]) => typeof value === "string").map(([key, value]) => [key, value])
|
|
8329
|
+
) : {}
|
|
8330
|
+
})).filter(
|
|
8331
|
+
(transform) => transform.domain.length > 0 && Object.keys(transform.headers).length > 0
|
|
8332
|
+
);
|
|
8099
8333
|
}
|
|
8100
|
-
function
|
|
8101
|
-
if (typeof
|
|
8334
|
+
function parseEnv(raw) {
|
|
8335
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
8102
8336
|
return void 0;
|
|
8103
8337
|
}
|
|
8104
|
-
|
|
8105
|
-
|
|
8106
|
-
|
|
8107
|
-
}
|
|
8108
|
-
return truncateWithEllipsis(trimmed, maxLength);
|
|
8338
|
+
return Object.fromEntries(
|
|
8339
|
+
Object.entries(raw).filter(([, value]) => typeof value === "string").map(([key, value]) => [key, value])
|
|
8340
|
+
);
|
|
8109
8341
|
}
|
|
8110
|
-
function
|
|
8111
|
-
|
|
8112
|
-
|
|
8113
|
-
|
|
8114
|
-
|
|
8115
|
-
|
|
8116
|
-
|
|
8117
|
-
|
|
8118
|
-
let token = "";
|
|
8119
|
-
let quote;
|
|
8120
|
-
while (index < command.length) {
|
|
8121
|
-
const char = command[index];
|
|
8122
|
-
if (!char) {
|
|
8123
|
-
break;
|
|
8342
|
+
function createSandboxWorkspace(sandbox) {
|
|
8343
|
+
return {
|
|
8344
|
+
sandboxId: sandbox.sandboxId,
|
|
8345
|
+
readFileToBuffer(input) {
|
|
8346
|
+
return sandbox.readFileToBuffer(input);
|
|
8347
|
+
},
|
|
8348
|
+
runCommand(input) {
|
|
8349
|
+
return sandbox.runCommand(input);
|
|
8124
8350
|
}
|
|
8125
|
-
|
|
8126
|
-
|
|
8127
|
-
|
|
8128
|
-
|
|
8129
|
-
|
|
8351
|
+
};
|
|
8352
|
+
}
|
|
8353
|
+
function createSandboxExecutor(options) {
|
|
8354
|
+
let availableSkills = [];
|
|
8355
|
+
const traceContext = options?.traceContext ?? {};
|
|
8356
|
+
const sessionManager = createSandboxSessionManager({
|
|
8357
|
+
sandboxId: options?.sandboxId,
|
|
8358
|
+
sandboxDependencyProfileHash: options?.sandboxDependencyProfileHash,
|
|
8359
|
+
timeoutMs: options?.timeoutMs,
|
|
8360
|
+
traceContext,
|
|
8361
|
+
onStatus: options?.onStatus
|
|
8362
|
+
});
|
|
8363
|
+
const withSandboxSpan = (name, op, attributes, callback) => withSpan(name, op, traceContext, callback, attributes);
|
|
8364
|
+
const logSandboxBootRequest = (trigger, details = {}) => {
|
|
8365
|
+
if (sessionManager.getSandboxId()) {
|
|
8366
|
+
return;
|
|
8367
|
+
}
|
|
8368
|
+
logInfo(
|
|
8369
|
+
"sandbox_boot_requested",
|
|
8370
|
+
traceContext,
|
|
8371
|
+
{
|
|
8372
|
+
"app.sandbox.boot.trigger": trigger,
|
|
8373
|
+
...details
|
|
8374
|
+
},
|
|
8375
|
+
"Sandbox boot requested"
|
|
8376
|
+
);
|
|
8377
|
+
};
|
|
8378
|
+
const executeBashTool = async (rawInput, command) => {
|
|
8379
|
+
const headerTransforms = parseHeaderTransforms(rawInput.headerTransforms);
|
|
8380
|
+
const env = parseEnv(rawInput.env);
|
|
8381
|
+
logSandboxBootRequest("tool.bash", {
|
|
8382
|
+
"app.sandbox.command_length": command.length
|
|
8383
|
+
});
|
|
8384
|
+
const executeBash = (await sessionManager.ensureToolExecutors()).bash;
|
|
8385
|
+
const result = await withSandboxSpan(
|
|
8386
|
+
"bash",
|
|
8387
|
+
"process.exec",
|
|
8388
|
+
{
|
|
8389
|
+
"process.executable.name": "bash"
|
|
8390
|
+
},
|
|
8391
|
+
async () => {
|
|
8392
|
+
try {
|
|
8393
|
+
const response = await executeBash({
|
|
8394
|
+
command,
|
|
8395
|
+
...headerTransforms ? { headerTransforms } : {},
|
|
8396
|
+
...env ? { env } : {}
|
|
8397
|
+
});
|
|
8398
|
+
setSpanAttributes({
|
|
8399
|
+
"process.exit.code": response.exitCode,
|
|
8400
|
+
"app.sandbox.stdout_bytes": Buffer.byteLength(
|
|
8401
|
+
response.stdout ?? "",
|
|
8402
|
+
"utf8"
|
|
8403
|
+
),
|
|
8404
|
+
"app.sandbox.stderr_bytes": Buffer.byteLength(
|
|
8405
|
+
response.stderr ?? "",
|
|
8406
|
+
"utf8"
|
|
8407
|
+
),
|
|
8408
|
+
...response.exitCode !== 0 ? { "error.type": "nonzero_exit" } : {}
|
|
8409
|
+
});
|
|
8410
|
+
setSpanStatus(response.exitCode === 0 ? "ok" : "error");
|
|
8411
|
+
return response;
|
|
8412
|
+
} catch (error) {
|
|
8413
|
+
setSpanAttributes({
|
|
8414
|
+
"error.type": error instanceof Error ? error.name : "sandbox_execute_error"
|
|
8415
|
+
});
|
|
8416
|
+
setSpanStatus("error");
|
|
8417
|
+
throw error;
|
|
8418
|
+
}
|
|
8130
8419
|
}
|
|
8131
|
-
|
|
8132
|
-
|
|
8133
|
-
|
|
8134
|
-
|
|
8420
|
+
);
|
|
8421
|
+
return {
|
|
8422
|
+
result: {
|
|
8423
|
+
ok: result.exitCode === 0,
|
|
8424
|
+
command,
|
|
8425
|
+
cwd: SANDBOX_WORKSPACE_ROOT,
|
|
8426
|
+
exit_code: result.exitCode,
|
|
8427
|
+
signal: null,
|
|
8428
|
+
timed_out: false,
|
|
8429
|
+
stdout: result.stdout,
|
|
8430
|
+
stderr: result.stderr,
|
|
8431
|
+
stdout_truncated: result.stdoutTruncated,
|
|
8432
|
+
stderr_truncated: result.stderrTruncated
|
|
8433
|
+
}
|
|
8434
|
+
};
|
|
8435
|
+
};
|
|
8436
|
+
const executeReadFileTool = async (rawInput) => {
|
|
8437
|
+
const filePath = String(rawInput.path ?? "").trim();
|
|
8438
|
+
if (!filePath) {
|
|
8439
|
+
throw new Error("path is required");
|
|
8440
|
+
}
|
|
8441
|
+
if (!sessionManager.getSandboxId()) {
|
|
8442
|
+
const hostSkillPath = resolveHostSkillPath(availableSkills, filePath);
|
|
8443
|
+
if (hostSkillPath) {
|
|
8444
|
+
try {
|
|
8445
|
+
const content = await fs4.readFile(hostSkillPath, "utf8");
|
|
8446
|
+
setSpanAttributes({
|
|
8447
|
+
"app.sandbox.path.length": filePath.length,
|
|
8448
|
+
"app.sandbox.read.bytes": Buffer.byteLength(content, "utf8"),
|
|
8449
|
+
"app.sandbox.read.chars": content.length,
|
|
8450
|
+
"app.skill.virtual_read": true
|
|
8451
|
+
});
|
|
8452
|
+
setSpanStatus("ok");
|
|
8453
|
+
return {
|
|
8454
|
+
result: {
|
|
8455
|
+
content,
|
|
8456
|
+
path: filePath,
|
|
8457
|
+
success: true
|
|
8458
|
+
}
|
|
8459
|
+
};
|
|
8460
|
+
} catch (error) {
|
|
8461
|
+
if (!isHostFileMissingError(error)) {
|
|
8462
|
+
throw error;
|
|
8463
|
+
}
|
|
8464
|
+
}
|
|
8465
|
+
}
|
|
8466
|
+
}
|
|
8467
|
+
logSandboxBootRequest("tool.readFile", {
|
|
8468
|
+
"file.path": filePath
|
|
8469
|
+
});
|
|
8470
|
+
const executeReadFile = (await sessionManager.ensureToolExecutors()).readFile;
|
|
8471
|
+
const result = await withSandboxSpan(
|
|
8472
|
+
"sandbox.readFile",
|
|
8473
|
+
"sandbox.fs.read",
|
|
8474
|
+
{
|
|
8475
|
+
"app.sandbox.path.length": filePath.length
|
|
8476
|
+
},
|
|
8477
|
+
async () => {
|
|
8478
|
+
const response = await executeReadFile({ path: filePath });
|
|
8479
|
+
const content = String(response.content ?? "");
|
|
8480
|
+
setSpanAttributes({
|
|
8481
|
+
"app.sandbox.read.bytes": Buffer.byteLength(content, "utf8"),
|
|
8482
|
+
"app.sandbox.read.chars": content.length
|
|
8483
|
+
});
|
|
8484
|
+
setSpanStatus("ok");
|
|
8485
|
+
return {
|
|
8486
|
+
content,
|
|
8487
|
+
path: filePath,
|
|
8488
|
+
success: true
|
|
8489
|
+
};
|
|
8490
|
+
}
|
|
8491
|
+
);
|
|
8492
|
+
return { result };
|
|
8493
|
+
};
|
|
8494
|
+
const executeWriteFileTool = async (rawInput) => {
|
|
8495
|
+
const filePath = String(rawInput.path ?? "").trim();
|
|
8496
|
+
if (!filePath) {
|
|
8497
|
+
throw new Error("path is required");
|
|
8498
|
+
}
|
|
8499
|
+
const content = String(rawInput.content ?? "");
|
|
8500
|
+
logSandboxBootRequest("tool.writeFile", {
|
|
8501
|
+
"file.path": filePath
|
|
8502
|
+
});
|
|
8503
|
+
const executeWriteFile = (await sessionManager.ensureToolExecutors()).writeFile;
|
|
8504
|
+
await withSandboxSpan(
|
|
8505
|
+
"sandbox.writeFile",
|
|
8506
|
+
"sandbox.fs.write",
|
|
8507
|
+
{
|
|
8508
|
+
"app.sandbox.path.length": filePath.length,
|
|
8509
|
+
"app.sandbox.write.bytes": Buffer.byteLength(content, "utf8")
|
|
8510
|
+
},
|
|
8511
|
+
async () => {
|
|
8512
|
+
try {
|
|
8513
|
+
await executeWriteFile({ path: filePath, content });
|
|
8514
|
+
} catch (error) {
|
|
8515
|
+
throwSandboxOperationError("sandbox writeFile", error);
|
|
8516
|
+
}
|
|
8517
|
+
setSpanStatus("ok");
|
|
8518
|
+
}
|
|
8519
|
+
);
|
|
8520
|
+
return {
|
|
8521
|
+
result: {
|
|
8522
|
+
ok: true,
|
|
8523
|
+
path: filePath,
|
|
8524
|
+
bytes_written: Buffer.byteLength(content, "utf8")
|
|
8525
|
+
}
|
|
8526
|
+
};
|
|
8527
|
+
};
|
|
8528
|
+
const execute = async (params) => {
|
|
8529
|
+
const rawInput = params.input ?? {};
|
|
8530
|
+
const bashCommand = params.toolName === "bash" ? String(rawInput.command ?? "").trim() : void 0;
|
|
8531
|
+
if (params.toolName === "bash") {
|
|
8532
|
+
if (!bashCommand) {
|
|
8533
|
+
throw new Error("command is required");
|
|
8534
|
+
}
|
|
8535
|
+
if (options?.runBashCustomCommand) {
|
|
8536
|
+
const custom = await options.runBashCustomCommand(bashCommand);
|
|
8537
|
+
if (custom.handled) {
|
|
8538
|
+
return { result: custom.result };
|
|
8539
|
+
}
|
|
8135
8540
|
}
|
|
8136
|
-
|
|
8137
|
-
index += 1;
|
|
8138
|
-
continue;
|
|
8139
|
-
}
|
|
8140
|
-
if (/\s/.test(char)) {
|
|
8141
|
-
break;
|
|
8142
|
-
}
|
|
8143
|
-
if (char === '"' || char === "'") {
|
|
8144
|
-
quote = char;
|
|
8145
|
-
index += 1;
|
|
8146
|
-
continue;
|
|
8147
|
-
}
|
|
8148
|
-
if (char === "\\" && index + 1 < command.length) {
|
|
8149
|
-
token += command[index + 1];
|
|
8150
|
-
index += 2;
|
|
8151
|
-
continue;
|
|
8152
|
-
}
|
|
8153
|
-
token += char;
|
|
8154
|
-
index += 1;
|
|
8155
|
-
}
|
|
8156
|
-
return { token, nextIndex: index };
|
|
8157
|
-
}
|
|
8158
|
-
function compactStatusCommand(value) {
|
|
8159
|
-
if (typeof value !== "string") {
|
|
8160
|
-
return void 0;
|
|
8161
|
-
}
|
|
8162
|
-
const trimmed = value.trim();
|
|
8163
|
-
if (!trimmed) {
|
|
8164
|
-
return void 0;
|
|
8165
|
-
}
|
|
8166
|
-
let index = 0;
|
|
8167
|
-
while (index < trimmed.length) {
|
|
8168
|
-
const parsed = readShellToken(trimmed, index);
|
|
8169
|
-
if (!parsed) {
|
|
8170
|
-
return void 0;
|
|
8541
|
+
return await executeBashTool(rawInput, bashCommand);
|
|
8171
8542
|
}
|
|
8172
|
-
|
|
8173
|
-
|
|
8174
|
-
continue;
|
|
8543
|
+
if (params.toolName === "readFile") {
|
|
8544
|
+
return await executeReadFileTool(rawInput);
|
|
8175
8545
|
}
|
|
8176
|
-
if (
|
|
8177
|
-
|
|
8546
|
+
if (params.toolName === "writeFile") {
|
|
8547
|
+
return await executeWriteFileTool(rawInput);
|
|
8178
8548
|
}
|
|
8179
|
-
|
|
8180
|
-
|
|
8181
|
-
|
|
8549
|
+
throw new Error(`unsupported sandbox tool: ${params.toolName}`);
|
|
8550
|
+
};
|
|
8551
|
+
return {
|
|
8552
|
+
configureSkills(skills) {
|
|
8553
|
+
availableSkills = [...skills];
|
|
8554
|
+
sessionManager.configureSkills(skills);
|
|
8555
|
+
},
|
|
8556
|
+
getSandboxId() {
|
|
8557
|
+
return sessionManager.getSandboxId();
|
|
8558
|
+
},
|
|
8559
|
+
getDependencyProfileHash() {
|
|
8560
|
+
return sessionManager.getDependencyProfileHash();
|
|
8561
|
+
},
|
|
8562
|
+
canExecute(toolName) {
|
|
8563
|
+
return SANDBOX_TOOL_NAMES.has(toolName);
|
|
8564
|
+
},
|
|
8565
|
+
async createSandbox() {
|
|
8566
|
+
return createSandboxWorkspace(await sessionManager.createSandbox());
|
|
8567
|
+
},
|
|
8568
|
+
execute,
|
|
8569
|
+
async dispose() {
|
|
8570
|
+
await sessionManager.dispose();
|
|
8182
8571
|
}
|
|
8183
|
-
|
|
8184
|
-
const command = parts.length > 0 ? parts[parts.length - 1] : normalized;
|
|
8185
|
-
return compactStatusText(command, 40);
|
|
8186
|
-
}
|
|
8187
|
-
return void 0;
|
|
8188
|
-
}
|
|
8189
|
-
function compactStatusFilename(value) {
|
|
8190
|
-
if (typeof value !== "string") {
|
|
8191
|
-
return void 0;
|
|
8192
|
-
}
|
|
8193
|
-
const trimmed = value.trim().replace(/[\\/]+$/g, "");
|
|
8194
|
-
if (!trimmed) {
|
|
8195
|
-
return void 0;
|
|
8196
|
-
}
|
|
8197
|
-
const parts = trimmed.split(/[\\/]/).filter((part) => part.length > 0);
|
|
8198
|
-
const filename = parts.length > 0 ? parts[parts.length - 1] : trimmed;
|
|
8199
|
-
return compactStatusText(filename, 80);
|
|
8572
|
+
};
|
|
8200
8573
|
}
|
|
8201
|
-
|
|
8202
|
-
|
|
8203
|
-
|
|
8204
|
-
|
|
8205
|
-
const trimmed = value.trim();
|
|
8206
|
-
if (!trimmed) {
|
|
8207
|
-
return void 0;
|
|
8208
|
-
}
|
|
8209
|
-
try {
|
|
8210
|
-
const parsed = new URL(trimmed);
|
|
8211
|
-
return parsed.hostname || void 0;
|
|
8212
|
-
} catch {
|
|
8213
|
-
return void 0;
|
|
8214
|
-
}
|
|
8574
|
+
|
|
8575
|
+
// src/chat/runtime/dev-agent-trace.ts
|
|
8576
|
+
function shouldEmitDevAgentTrace() {
|
|
8577
|
+
return process.env.NODE_ENV === "development";
|
|
8215
8578
|
}
|
|
8216
8579
|
|
|
8217
8580
|
// src/chat/runtime/tool-status.ts
|
|
8218
|
-
function
|
|
8219
|
-
const known = {
|
|
8220
|
-
loadSkill: "Loading skill instructions",
|
|
8221
|
-
systemTime: "Reading current system time",
|
|
8222
|
-
bash: "Working in the shell",
|
|
8223
|
-
readFile: "Reading a file",
|
|
8224
|
-
writeFile: "Updating a file",
|
|
8225
|
-
webSearch: "Searching public sources",
|
|
8226
|
-
webFetch: "Reading source pages",
|
|
8227
|
-
slackChannelPostMessage: "Posting message to channel",
|
|
8228
|
-
slackMessageAddReaction: "Adding emoji reaction",
|
|
8229
|
-
slackChannelListMessages: "Listing channel messages",
|
|
8230
|
-
slackCanvasCreate: "Creating detailed brief",
|
|
8231
|
-
slackCanvasUpdate: "Updating detailed brief",
|
|
8232
|
-
slackListCreate: "Creating tracking list",
|
|
8233
|
-
slackListAddItems: "Updating tracking list",
|
|
8234
|
-
slackListUpdateItem: "Updating tracking list",
|
|
8235
|
-
imageGenerate: "Generating image",
|
|
8236
|
-
searchTools: "Searching active tools"
|
|
8237
|
-
};
|
|
8238
|
-
if (known[toolName]) {
|
|
8239
|
-
return known[toolName];
|
|
8240
|
-
}
|
|
8241
|
-
const mcpMatch = /^mcp__([^_]+)__(.+)$/.exec(toolName);
|
|
8242
|
-
if (mcpMatch) {
|
|
8243
|
-
return `Running ${mcpMatch[1]}/${mcpMatch[2]}`;
|
|
8244
|
-
}
|
|
8245
|
-
const readable = toolName.replaceAll("_", " ").trim();
|
|
8246
|
-
return readable.length > 0 ? `Running ${readable}` : "Running tool";
|
|
8247
|
-
}
|
|
8248
|
-
function formatToolStatusWithInput(toolName, input) {
|
|
8581
|
+
function buildToolStatus(toolName, input) {
|
|
8249
8582
|
const obj = input && typeof input === "object" ? input : void 0;
|
|
8250
8583
|
const command = obj ? compactStatusCommand(obj.command) : void 0;
|
|
8251
8584
|
const path6 = obj ? compactStatusPath(obj.path) : void 0;
|
|
@@ -8255,33 +8588,63 @@ function formatToolStatusWithInput(toolName, input) {
|
|
|
8255
8588
|
const skillName = obj ? compactStatusText(obj.skill_name ?? obj.skillName, 40) : void 0;
|
|
8256
8589
|
const provider = obj ? compactStatusText(obj.provider, 20) : void 0;
|
|
8257
8590
|
if (command && toolName === "bash") {
|
|
8258
|
-
return
|
|
8591
|
+
return makeAssistantStatus("running", command);
|
|
8259
8592
|
}
|
|
8260
8593
|
if (filename && toolName === "readFile") {
|
|
8261
|
-
return
|
|
8594
|
+
return makeAssistantStatus("reading", filename);
|
|
8262
8595
|
}
|
|
8263
8596
|
if (filename && toolName === "writeFile") {
|
|
8264
|
-
return
|
|
8597
|
+
return makeAssistantStatus("updating", filename);
|
|
8265
8598
|
}
|
|
8266
8599
|
if (path6 && toolName === "writeFile") {
|
|
8267
|
-
return
|
|
8600
|
+
return makeAssistantStatus("updating", path6);
|
|
8268
8601
|
}
|
|
8269
8602
|
if (skillName && toolName === "loadSkill") {
|
|
8270
|
-
return
|
|
8603
|
+
return makeAssistantStatus("loading", skillName);
|
|
8271
8604
|
}
|
|
8272
8605
|
if (query && toolName === "webSearch") {
|
|
8273
|
-
return `
|
|
8606
|
+
return makeAssistantStatus("searching", `"${query}"`);
|
|
8274
8607
|
}
|
|
8275
8608
|
if (query && provider && toolName === "searchTools") {
|
|
8276
|
-
return
|
|
8609
|
+
return makeAssistantStatus("searching", `${provider} "${query}"`);
|
|
8277
8610
|
}
|
|
8278
8611
|
if (query && toolName === "searchTools") {
|
|
8279
|
-
return `
|
|
8612
|
+
return makeAssistantStatus("searching", `"${query}"`);
|
|
8280
8613
|
}
|
|
8281
8614
|
if (domain && toolName === "webFetch") {
|
|
8282
|
-
return
|
|
8615
|
+
return makeAssistantStatus("fetching", domain);
|
|
8616
|
+
}
|
|
8617
|
+
const known = {
|
|
8618
|
+
loadSkill: makeAssistantStatus("loading", "skill instructions"),
|
|
8619
|
+
systemTime: makeAssistantStatus("reading", "system time"),
|
|
8620
|
+
bash: makeAssistantStatus("running", "shell"),
|
|
8621
|
+
readFile: makeAssistantStatus("reading", "file"),
|
|
8622
|
+
writeFile: makeAssistantStatus("updating", "file"),
|
|
8623
|
+
webSearch: makeAssistantStatus("searching", "sources"),
|
|
8624
|
+
webFetch: makeAssistantStatus("fetching", "pages"),
|
|
8625
|
+
slackChannelPostMessage: makeAssistantStatus("posting", "channel"),
|
|
8626
|
+
slackMessageAddReaction: makeAssistantStatus("adding", "reaction"),
|
|
8627
|
+
slackChannelListMessages: makeAssistantStatus("listing", "messages"),
|
|
8628
|
+
slackCanvasCreate: makeAssistantStatus("creating", "brief"),
|
|
8629
|
+
slackCanvasUpdate: makeAssistantStatus("updating", "brief"),
|
|
8630
|
+
slackListCreate: makeAssistantStatus("creating", "tracking list"),
|
|
8631
|
+
slackListAddItems: makeAssistantStatus("updating", "tracking list"),
|
|
8632
|
+
slackListUpdateItem: makeAssistantStatus("updating", "tracking list"),
|
|
8633
|
+
imageGenerate: makeAssistantStatus("creating", "image"),
|
|
8634
|
+
searchTools: makeAssistantStatus(
|
|
8635
|
+
"searching",
|
|
8636
|
+
provider ? `${provider} tools` : "tools"
|
|
8637
|
+
)
|
|
8638
|
+
};
|
|
8639
|
+
if (known[toolName]) {
|
|
8640
|
+
return known[toolName];
|
|
8641
|
+
}
|
|
8642
|
+
const mcpMatch = /^mcp__([^_]+)__(.+)$/.exec(toolName);
|
|
8643
|
+
if (mcpMatch) {
|
|
8644
|
+
return makeAssistantStatus("running", `${mcpMatch[1]}/${mcpMatch[2]}`);
|
|
8283
8645
|
}
|
|
8284
|
-
|
|
8646
|
+
const readable = toolName.replaceAll("_", " ").trim();
|
|
8647
|
+
return makeAssistantStatus("running", readable || "tool");
|
|
8285
8648
|
}
|
|
8286
8649
|
|
|
8287
8650
|
// src/chat/tools/execution/build-sandbox-input.ts
|
|
@@ -8432,7 +8795,7 @@ function createAgentTools(tools, sandbox, spanContext, onStatus, sandboxExecutor
|
|
|
8432
8795
|
turnId: spanContext.turnId,
|
|
8433
8796
|
agentId: spanContext.agentId
|
|
8434
8797
|
};
|
|
8435
|
-
await onStatus?.(
|
|
8798
|
+
await onStatus?.(buildToolStatus(toolName, params));
|
|
8436
8799
|
return withSpan(
|
|
8437
8800
|
`execute_tool ${toolName}`,
|
|
8438
8801
|
"gen_ai.execute_tool",
|
|
@@ -9027,6 +9390,14 @@ async function generateAssistantReply(messageText, context = {}) {
|
|
|
9027
9390
|
let lastKnownSandboxDependencyProfileHash = context.sandbox?.sandboxDependencyProfileHash;
|
|
9028
9391
|
let loadedSkillNamesForResume = [];
|
|
9029
9392
|
let mcpToolManager;
|
|
9393
|
+
let sandboxExecutor;
|
|
9394
|
+
const getSandboxMetadata = () => sandboxExecutor ? {
|
|
9395
|
+
sandboxId: sandboxExecutor.getSandboxId(),
|
|
9396
|
+
sandboxDependencyProfileHash: sandboxExecutor.getDependencyProfileHash()
|
|
9397
|
+
} : {
|
|
9398
|
+
sandboxId: lastKnownSandboxId,
|
|
9399
|
+
sandboxDependencyProfileHash: lastKnownSandboxDependencyProfileHash
|
|
9400
|
+
};
|
|
9030
9401
|
try {
|
|
9031
9402
|
const shouldTrace = shouldEmitDevAgentTrace();
|
|
9032
9403
|
const spanContext = {
|
|
@@ -9098,7 +9469,8 @@ async function generateAssistantReply(messageText, context = {}) {
|
|
|
9098
9469
|
requesterId: context.requester?.userId,
|
|
9099
9470
|
resolveConfiguration: async (key) => configurationValues[key]
|
|
9100
9471
|
});
|
|
9101
|
-
const
|
|
9472
|
+
const providerAuthActions = /* @__PURE__ */ new Map();
|
|
9473
|
+
sandboxExecutor = createSandboxExecutor({
|
|
9102
9474
|
sandboxId: context.sandbox?.sandboxId,
|
|
9103
9475
|
sandboxDependencyProfileHash: context.sandbox?.sandboxDependencyProfileHash,
|
|
9104
9476
|
traceContext: spanContext,
|
|
@@ -9113,6 +9485,7 @@ async function generateAssistantReply(messageText, context = {}) {
|
|
|
9113
9485
|
threadTs: context.correlation?.threadTs,
|
|
9114
9486
|
userMessage: userInput,
|
|
9115
9487
|
userTokenStore: createUserTokenStore(),
|
|
9488
|
+
providerAuthActions,
|
|
9116
9489
|
onConfigurationValueChanged: (key, value) => {
|
|
9117
9490
|
if (value === void 0) {
|
|
9118
9491
|
delete configurationValues[key];
|
|
@@ -9124,10 +9497,52 @@ async function generateAssistantReply(messageText, context = {}) {
|
|
|
9124
9497
|
return result.handled ? { handled: true, result: result.result } : { handled: false };
|
|
9125
9498
|
}
|
|
9126
9499
|
});
|
|
9127
|
-
|
|
9128
|
-
lastKnownSandboxDependencyProfileHash = sandboxExecutor.getDependencyProfileHash();
|
|
9500
|
+
const currentSandboxExecutor = sandboxExecutor;
|
|
9129
9501
|
sandboxExecutor.configureSkills(availableSkills);
|
|
9130
|
-
|
|
9502
|
+
let sandboxPromise;
|
|
9503
|
+
let sandboxPromiseId;
|
|
9504
|
+
const clearSandboxPromise = () => {
|
|
9505
|
+
sandboxPromise = void 0;
|
|
9506
|
+
sandboxPromiseId = void 0;
|
|
9507
|
+
};
|
|
9508
|
+
const getSandbox = (reason) => {
|
|
9509
|
+
const currentSandboxId = currentSandboxExecutor.getSandboxId();
|
|
9510
|
+
if (sandboxPromise && sandboxPromiseId && currentSandboxId !== sandboxPromiseId) {
|
|
9511
|
+
clearSandboxPromise();
|
|
9512
|
+
}
|
|
9513
|
+
if (!sandboxPromise) {
|
|
9514
|
+
logInfo(
|
|
9515
|
+
"sandbox_boot_requested",
|
|
9516
|
+
spanContext,
|
|
9517
|
+
{
|
|
9518
|
+
"app.sandbox.boot.trigger": reason.trigger,
|
|
9519
|
+
...reason.path ? { "file.path": reason.path } : {},
|
|
9520
|
+
...reason.cmd ? { "process.executable.name": reason.cmd } : {},
|
|
9521
|
+
...reason.cwd ? { "file.directory": reason.cwd } : {}
|
|
9522
|
+
},
|
|
9523
|
+
"Lazy sandbox boot requested"
|
|
9524
|
+
);
|
|
9525
|
+
sandboxPromise = currentSandboxExecutor.createSandbox().then((sandbox2) => {
|
|
9526
|
+
sandboxPromiseId = sandbox2.sandboxId;
|
|
9527
|
+
return sandbox2;
|
|
9528
|
+
}).catch((error) => {
|
|
9529
|
+
clearSandboxPromise();
|
|
9530
|
+
throw error;
|
|
9531
|
+
});
|
|
9532
|
+
}
|
|
9533
|
+
return sandboxPromise;
|
|
9534
|
+
};
|
|
9535
|
+
const sandbox = {
|
|
9536
|
+
readFileToBuffer: async (input) => (await getSandbox({
|
|
9537
|
+
trigger: "workspace.readFileToBuffer",
|
|
9538
|
+
path: input.path
|
|
9539
|
+
})).readFileToBuffer(input),
|
|
9540
|
+
runCommand: async (input) => (await getSandbox({
|
|
9541
|
+
trigger: "workspace.runCommand",
|
|
9542
|
+
cmd: input.cmd,
|
|
9543
|
+
cwd: input.cwd
|
|
9544
|
+
})).runCommand(input)
|
|
9545
|
+
};
|
|
9131
9546
|
for (const skillName of existingCheckpoint?.loadedSkillNames ?? []) {
|
|
9132
9547
|
const preloaded = await skillSandbox.loadSkill(skillName);
|
|
9133
9548
|
if (preloaded) {
|
|
@@ -9201,11 +9616,6 @@ async function generateAssistantReply(messageText, context = {}) {
|
|
|
9201
9616
|
onArtifactStatePatch: (patch) => {
|
|
9202
9617
|
Object.assign(artifactStatePatch, patch);
|
|
9203
9618
|
},
|
|
9204
|
-
onToolCallStart: async (toolName, input) => {
|
|
9205
|
-
await context.onStatus?.(
|
|
9206
|
-
`${formatToolStatusWithInput(toolName, input)}...`
|
|
9207
|
-
);
|
|
9208
|
-
},
|
|
9209
9619
|
toolOverrides: context.toolOverrides,
|
|
9210
9620
|
onSkillLoaded: async (loadedSkill) => {
|
|
9211
9621
|
const resolvedSkill = await skillSandbox.loadSkill(loadedSkill.name);
|
|
@@ -9271,16 +9681,32 @@ async function generateAssistantReply(messageText, context = {}) {
|
|
|
9271
9681
|
});
|
|
9272
9682
|
const userContentParts = [{ type: "text", text: userTurnText }];
|
|
9273
9683
|
for (const attachment of context.userAttachments ?? []) {
|
|
9274
|
-
if (attachment.
|
|
9684
|
+
if (attachment.promptText) {
|
|
9685
|
+
userContentParts.push({
|
|
9686
|
+
type: "text",
|
|
9687
|
+
text: attachment.promptText
|
|
9688
|
+
});
|
|
9689
|
+
} else if (attachment.mediaType.startsWith("image/")) {
|
|
9690
|
+
if (!attachment.data) {
|
|
9691
|
+
throw new Error("Image attachment is missing image data");
|
|
9692
|
+
}
|
|
9275
9693
|
userContentParts.push({
|
|
9276
9694
|
type: "image",
|
|
9277
9695
|
data: attachment.data.toString("base64"),
|
|
9278
9696
|
mimeType: attachment.mediaType
|
|
9279
9697
|
});
|
|
9280
9698
|
} else {
|
|
9699
|
+
if (!attachment.data) {
|
|
9700
|
+
throw new Error("Attachment is missing attachment data");
|
|
9701
|
+
}
|
|
9702
|
+
const promptAttachment = {
|
|
9703
|
+
data: attachment.data,
|
|
9704
|
+
mediaType: attachment.mediaType,
|
|
9705
|
+
filename: attachment.filename
|
|
9706
|
+
};
|
|
9281
9707
|
userContentParts.push({
|
|
9282
9708
|
type: "text",
|
|
9283
|
-
text: encodeNonImageAttachmentForPrompt(
|
|
9709
|
+
text: encodeNonImageAttachmentForPrompt(promptAttachment)
|
|
9284
9710
|
});
|
|
9285
9711
|
}
|
|
9286
9712
|
}
|
|
@@ -9477,8 +9903,8 @@ async function generateAssistantReply(messageText, context = {}) {
|
|
|
9477
9903
|
replyFiles,
|
|
9478
9904
|
artifactStatePatch,
|
|
9479
9905
|
toolCalls,
|
|
9480
|
-
sandboxId:
|
|
9481
|
-
sandboxDependencyProfileHash:
|
|
9906
|
+
sandboxId: currentSandboxExecutor.getSandboxId(),
|
|
9907
|
+
sandboxDependencyProfileHash: currentSandboxExecutor.getDependencyProfileHash(),
|
|
9482
9908
|
generatedFileCount: generatedFiles.length,
|
|
9483
9909
|
hasTextDeltaCallback: Boolean(context.onTextDelta),
|
|
9484
9910
|
shouldTrace,
|
|
@@ -9529,8 +9955,7 @@ async function generateAssistantReply(messageText, context = {}) {
|
|
|
9529
9955
|
const message = error instanceof Error ? error.message : String(error);
|
|
9530
9956
|
return {
|
|
9531
9957
|
text: `Error: ${message}`,
|
|
9532
|
-
|
|
9533
|
-
sandboxDependencyProfileHash: lastKnownSandboxDependencyProfileHash,
|
|
9958
|
+
...getSandboxMetadata(),
|
|
9534
9959
|
diagnostics: {
|
|
9535
9960
|
outcome: "provider_error",
|
|
9536
9961
|
modelId: botConfig.modelId,
|
|
@@ -9559,90 +9984,173 @@ async function generateAssistantReply(messageText, context = {}) {
|
|
|
9559
9984
|
}
|
|
9560
9985
|
}
|
|
9561
9986
|
|
|
9562
|
-
// src/
|
|
9563
|
-
|
|
9564
|
-
|
|
9565
|
-
|
|
9566
|
-
|
|
9567
|
-
const
|
|
9568
|
-
|
|
9569
|
-
|
|
9570
|
-
|
|
9571
|
-
|
|
9572
|
-
|
|
9573
|
-
|
|
9574
|
-
|
|
9575
|
-
|
|
9576
|
-
await getSlackClient().chat.postMessage({
|
|
9577
|
-
channel: channelId,
|
|
9578
|
-
thread_ts: threadTs,
|
|
9579
|
-
text
|
|
9580
|
-
});
|
|
9581
|
-
} catch {
|
|
9582
|
-
}
|
|
9583
|
-
}
|
|
9584
|
-
async function setAssistantStatus(channelId, threadTs, status) {
|
|
9585
|
-
try {
|
|
9586
|
-
await getSlackClient().assistant.threads.setStatus({
|
|
9587
|
-
channel_id: channelId,
|
|
9588
|
-
thread_ts: threadTs,
|
|
9589
|
-
status
|
|
9590
|
-
});
|
|
9591
|
-
} catch {
|
|
9592
|
-
}
|
|
9593
|
-
}
|
|
9594
|
-
var STATUS_DEBOUNCE_MS = 1e3;
|
|
9595
|
-
function createDebouncedStatusPoster(channelId, threadTs) {
|
|
9596
|
-
let lastPostAt = 0;
|
|
9597
|
-
let currentStatus = "";
|
|
9987
|
+
// src/chat/runtime/progress-reporter.ts
|
|
9988
|
+
var STATUS_UPDATE_DEBOUNCE_MS = 1e3;
|
|
9989
|
+
var STATUS_MIN_VISIBLE_MS = 1200;
|
|
9990
|
+
var STATUS_ROTATION_INTERVAL_MS = 3e4;
|
|
9991
|
+
function createProgressReporter(args) {
|
|
9992
|
+
const now = args.now ?? (() => Date.now());
|
|
9993
|
+
const setTimer = args.setTimer ?? ((callback, delayMs) => setTimeout(callback, delayMs));
|
|
9994
|
+
const clearTimer = args.clearTimer ?? ((timer) => clearTimeout(timer));
|
|
9995
|
+
const random = args.random ?? Math.random;
|
|
9996
|
+
let active = false;
|
|
9997
|
+
let currentKey = "";
|
|
9998
|
+
let currentStatus = makeAssistantStatus("thinking");
|
|
9999
|
+
let currentVisibleStatus = "";
|
|
10000
|
+
let lastStatusAt = 0;
|
|
9598
10001
|
let pendingStatus = null;
|
|
10002
|
+
let pendingKey = "";
|
|
9599
10003
|
let pendingTimer = null;
|
|
9600
|
-
let
|
|
9601
|
-
|
|
9602
|
-
|
|
9603
|
-
|
|
9604
|
-
|
|
9605
|
-
|
|
9606
|
-
|
|
9607
|
-
|
|
9608
|
-
|
|
9609
|
-
|
|
9610
|
-
|
|
9611
|
-
|
|
9612
|
-
|
|
9613
|
-
|
|
9614
|
-
const now = Date.now();
|
|
9615
|
-
const elapsed = now - lastPostAt;
|
|
9616
|
-
if (elapsed >= STATUS_DEBOUNCE_MS) {
|
|
9617
|
-
if (pendingTimer) {
|
|
9618
|
-
clearTimeout(pendingTimer);
|
|
9619
|
-
pendingTimer = null;
|
|
10004
|
+
let rotationTimer = null;
|
|
10005
|
+
let inflightStatusUpdate = Promise.resolve();
|
|
10006
|
+
const scheduleRotation = () => {
|
|
10007
|
+
if (rotationTimer) {
|
|
10008
|
+
clearTimer(rotationTimer);
|
|
10009
|
+
rotationTimer = null;
|
|
10010
|
+
}
|
|
10011
|
+
if (!active || !currentVisibleStatus) {
|
|
10012
|
+
return;
|
|
10013
|
+
}
|
|
10014
|
+
rotationTimer = setTimer(() => {
|
|
10015
|
+
rotationTimer = null;
|
|
10016
|
+
if (!active || !currentVisibleStatus) {
|
|
10017
|
+
return;
|
|
9620
10018
|
}
|
|
9621
|
-
|
|
9622
|
-
|
|
9623
|
-
|
|
9624
|
-
|
|
10019
|
+
void postRenderedStatus(currentStatus);
|
|
10020
|
+
}, STATUS_ROTATION_INTERVAL_MS);
|
|
10021
|
+
};
|
|
10022
|
+
const postStatus = async (text, suggestions) => {
|
|
10023
|
+
const channelId = args.channelId;
|
|
10024
|
+
const threadTs = args.threadTs;
|
|
10025
|
+
if (!channelId || !threadTs) {
|
|
9625
10026
|
return;
|
|
9626
10027
|
}
|
|
9627
|
-
|
|
9628
|
-
|
|
9629
|
-
pendingTimer = setTimeout(
|
|
9630
|
-
() => {
|
|
9631
|
-
void flush();
|
|
9632
|
-
},
|
|
9633
|
-
Math.max(1, STATUS_DEBOUNCE_MS - elapsed)
|
|
9634
|
-
);
|
|
10028
|
+
if (!text && !currentVisibleStatus) {
|
|
10029
|
+
return;
|
|
9635
10030
|
}
|
|
10031
|
+
currentVisibleStatus = text;
|
|
10032
|
+
lastStatusAt = now();
|
|
10033
|
+
scheduleRotation();
|
|
10034
|
+
const previous = inflightStatusUpdate;
|
|
10035
|
+
const request = (async () => {
|
|
10036
|
+
await previous;
|
|
10037
|
+
await args.transport.setStatus(channelId, threadTs, text, suggestions);
|
|
10038
|
+
})();
|
|
10039
|
+
inflightStatusUpdate = request;
|
|
10040
|
+
await request;
|
|
10041
|
+
};
|
|
10042
|
+
const postRenderedStatus = async (status) => {
|
|
10043
|
+
const presentation = buildAssistantStatusPresentation({
|
|
10044
|
+
status,
|
|
10045
|
+
random
|
|
10046
|
+
});
|
|
10047
|
+
currentStatus = status;
|
|
10048
|
+
currentKey = presentation.key;
|
|
10049
|
+
await postStatus(presentation.visible, presentation.suggestions);
|
|
9636
10050
|
};
|
|
9637
|
-
|
|
9638
|
-
stopped = true;
|
|
10051
|
+
const clearPending = () => {
|
|
9639
10052
|
if (pendingTimer) {
|
|
9640
|
-
|
|
10053
|
+
clearTimer(pendingTimer);
|
|
9641
10054
|
pendingTimer = null;
|
|
9642
10055
|
}
|
|
9643
10056
|
pendingStatus = null;
|
|
10057
|
+
pendingKey = "";
|
|
10058
|
+
};
|
|
10059
|
+
const flushPending = async () => {
|
|
10060
|
+
if (!active || !pendingStatus) {
|
|
10061
|
+
clearPending();
|
|
10062
|
+
return;
|
|
10063
|
+
}
|
|
10064
|
+
const next = pendingStatus;
|
|
10065
|
+
clearPending();
|
|
10066
|
+
const nextPresentation = buildAssistantStatusPresentation({
|
|
10067
|
+
status: next,
|
|
10068
|
+
random
|
|
10069
|
+
});
|
|
10070
|
+
if (nextPresentation.key !== currentKey) {
|
|
10071
|
+
await postRenderedStatus(next);
|
|
10072
|
+
}
|
|
10073
|
+
};
|
|
10074
|
+
return {
|
|
10075
|
+
async start() {
|
|
10076
|
+
active = true;
|
|
10077
|
+
clearPending();
|
|
10078
|
+
currentStatus = makeAssistantStatus("thinking");
|
|
10079
|
+
currentKey = "";
|
|
10080
|
+
void postRenderedStatus(currentStatus);
|
|
10081
|
+
},
|
|
10082
|
+
async stop() {
|
|
10083
|
+
active = false;
|
|
10084
|
+
clearPending();
|
|
10085
|
+
if (rotationTimer) {
|
|
10086
|
+
clearTimer(rotationTimer);
|
|
10087
|
+
rotationTimer = null;
|
|
10088
|
+
}
|
|
10089
|
+
currentKey = "";
|
|
10090
|
+
await postStatus("");
|
|
10091
|
+
},
|
|
10092
|
+
async setStatus(status) {
|
|
10093
|
+
if (!active) {
|
|
10094
|
+
return;
|
|
10095
|
+
}
|
|
10096
|
+
const presentation = buildAssistantStatusPresentation({
|
|
10097
|
+
status,
|
|
10098
|
+
random
|
|
10099
|
+
});
|
|
10100
|
+
if (!presentation.visible) {
|
|
10101
|
+
return;
|
|
10102
|
+
}
|
|
10103
|
+
if (presentation.key === currentKey || presentation.key === pendingKey) {
|
|
10104
|
+
return;
|
|
10105
|
+
}
|
|
10106
|
+
const elapsed = now() - lastStatusAt;
|
|
10107
|
+
const waitMs = Math.max(
|
|
10108
|
+
STATUS_UPDATE_DEBOUNCE_MS - elapsed,
|
|
10109
|
+
STATUS_MIN_VISIBLE_MS - elapsed,
|
|
10110
|
+
0
|
|
10111
|
+
);
|
|
10112
|
+
if (waitMs <= 0) {
|
|
10113
|
+
clearPending();
|
|
10114
|
+
void postRenderedStatus(status);
|
|
10115
|
+
return;
|
|
10116
|
+
}
|
|
10117
|
+
pendingStatus = status;
|
|
10118
|
+
pendingKey = presentation.key;
|
|
10119
|
+
if (pendingTimer) {
|
|
10120
|
+
return;
|
|
10121
|
+
}
|
|
10122
|
+
pendingTimer = setTimer(
|
|
10123
|
+
() => {
|
|
10124
|
+
pendingTimer = null;
|
|
10125
|
+
void flushPending();
|
|
10126
|
+
},
|
|
10127
|
+
Math.max(1, waitMs)
|
|
10128
|
+
);
|
|
10129
|
+
}
|
|
9644
10130
|
};
|
|
9645
|
-
|
|
10131
|
+
}
|
|
10132
|
+
|
|
10133
|
+
// src/handlers/oauth-resume.ts
|
|
10134
|
+
function resolveReplyTimeoutMs(explicitTimeoutMs) {
|
|
10135
|
+
if (typeof explicitTimeoutMs === "number" && explicitTimeoutMs > 0) {
|
|
10136
|
+
return explicitTimeoutMs;
|
|
10137
|
+
}
|
|
10138
|
+
const raw = process.env.EVAL_AGENT_REPLY_TIMEOUT_MS?.trim();
|
|
10139
|
+
if (!raw) {
|
|
10140
|
+
return void 0;
|
|
10141
|
+
}
|
|
10142
|
+
const parsed = Number.parseInt(raw, 10);
|
|
10143
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : void 0;
|
|
10144
|
+
}
|
|
10145
|
+
async function postSlackMessage(channelId, threadTs, text) {
|
|
10146
|
+
try {
|
|
10147
|
+
await getSlackClient().chat.postMessage({
|
|
10148
|
+
channel: channelId,
|
|
10149
|
+
thread_ts: threadTs,
|
|
10150
|
+
text
|
|
10151
|
+
});
|
|
10152
|
+
} catch {
|
|
10153
|
+
}
|
|
9646
10154
|
}
|
|
9647
10155
|
function createReadOnlyConfigService(values) {
|
|
9648
10156
|
const entries = Object.entries(values).map(([key, value]) => ({
|
|
@@ -9671,9 +10179,13 @@ function createReadOnlyConfigService(values) {
|
|
|
9671
10179
|
};
|
|
9672
10180
|
}
|
|
9673
10181
|
async function resumeAuthorizedRequest(args) {
|
|
9674
|
-
const
|
|
10182
|
+
const progress = createProgressReporter({
|
|
10183
|
+
channelId: args.channelId,
|
|
10184
|
+
threadTs: args.threadTs,
|
|
10185
|
+
transport: createSlackWebApiAssistantStatusTransport()
|
|
10186
|
+
});
|
|
9675
10187
|
await postSlackMessage(args.channelId, args.threadTs, args.connectedText);
|
|
9676
|
-
await
|
|
10188
|
+
await progress.start();
|
|
9677
10189
|
try {
|
|
9678
10190
|
const generateReply = args.generateReply ?? generateAssistantReply;
|
|
9679
10191
|
const replyPromise = generateReply(args.messageText, {
|
|
@@ -9691,7 +10203,7 @@ async function resumeAuthorizedRequest(args) {
|
|
|
9691
10203
|
artifactState: args.artifactState,
|
|
9692
10204
|
configuration: args.configuration,
|
|
9693
10205
|
channelConfiguration: args.configuration ? createReadOnlyConfigService(args.configuration) : void 0,
|
|
9694
|
-
onStatus:
|
|
10206
|
+
onStatus: (status) => progress.setStatus(status)
|
|
9695
10207
|
});
|
|
9696
10208
|
const replyTimeoutMs = resolveReplyTimeoutMs(args.replyTimeoutMs);
|
|
9697
10209
|
const reply = typeof replyTimeoutMs === "number" ? await Promise.race([
|
|
@@ -9707,8 +10219,7 @@ async function resumeAuthorizedRequest(args) {
|
|
|
9707
10219
|
)
|
|
9708
10220
|
)
|
|
9709
10221
|
]) : await replyPromise;
|
|
9710
|
-
|
|
9711
|
-
await setAssistantStatus(args.channelId, args.threadTs, "");
|
|
10222
|
+
await progress.stop();
|
|
9712
10223
|
if (args.onReply) {
|
|
9713
10224
|
await args.onReply(reply);
|
|
9714
10225
|
} else if (reply.text) {
|
|
@@ -9716,8 +10227,7 @@ async function resumeAuthorizedRequest(args) {
|
|
|
9716
10227
|
}
|
|
9717
10228
|
await args.onSuccess?.(reply);
|
|
9718
10229
|
} catch (error) {
|
|
9719
|
-
|
|
9720
|
-
await setAssistantStatus(args.channelId, args.threadTs, "");
|
|
10230
|
+
await progress.stop();
|
|
9721
10231
|
if (isRetryableTurnError(error, "mcp_auth_resume") && args.onAuthPause) {
|
|
9722
10232
|
await args.onAuthPause(error);
|
|
9723
10233
|
return;
|
|
@@ -10049,7 +10559,7 @@ async function GET4(request, provider, waitUntil) {
|
|
|
10049
10559
|
}
|
|
10050
10560
|
|
|
10051
10561
|
// src/chat/slack/app-home.ts
|
|
10052
|
-
import
|
|
10562
|
+
import fs5 from "fs";
|
|
10053
10563
|
import path5 from "path";
|
|
10054
10564
|
var DEFAULT_ABOUT_TEXT = "I help your team investigate, summarize, and act on work in Slack.";
|
|
10055
10565
|
var MAX_HOME_SKILLS = 6;
|
|
@@ -10064,7 +10574,7 @@ function clampSectionText(text) {
|
|
|
10064
10574
|
function loadAboutText() {
|
|
10065
10575
|
const aboutPath = path5.join(homeDir(), "ABOUT.md");
|
|
10066
10576
|
try {
|
|
10067
|
-
const raw =
|
|
10577
|
+
const raw = fs5.readFileSync(aboutPath, "utf8").trim();
|
|
10068
10578
|
if (raw.length > 0) {
|
|
10069
10579
|
return clampSectionText(raw);
|
|
10070
10580
|
}
|
|
@@ -10414,7 +10924,7 @@ var replyDecisionSchema = z.object({
|
|
|
10414
10924
|
var ROUTER_CONFIDENCE_THRESHOLD = 0.8;
|
|
10415
10925
|
var LEADING_SLACK_MENTION_RE = /^\s*<@([A-Z0-9]+)(?:\|([^>]+))?>[\s,:-]*/i;
|
|
10416
10926
|
var LEADING_NAMED_MENTION_RE = /^\s*@([a-z0-9._-]+)\b[\s,:-]*/i;
|
|
10417
|
-
var TRANSCRIPT_MESSAGE_LINE_RE = /^\[(assistant|system|user)\]\s+[^:]
|
|
10927
|
+
var TRANSCRIPT_MESSAGE_LINE_RE = /^\[(assistant|system|user)\]\s+([^:]+):\s+([\s\S]+)$/i;
|
|
10418
10928
|
var THREAD_OPTOUT_PATTERNS = [
|
|
10419
10929
|
/\bstop (?:watching|replying|participating)\b/i,
|
|
10420
10930
|
/\bstay out\b/i,
|
|
@@ -10422,6 +10932,11 @@ var THREAD_OPTOUT_PATTERNS = [
|
|
|
10422
10932
|
/\bunsubscribe\b/i,
|
|
10423
10933
|
/\bleave (?:this )?thread\b/i
|
|
10424
10934
|
];
|
|
10935
|
+
var ACKNOWLEDGMENT_ONLY_RE = /^(?:thanks(?: you)?|thank you|thx|ty|got it|sounds good|sgtm|lgtm|ok(?:ay)?|cool|nice|perfect|awesome|great|makes sense|understood|roger|yep|yup|kk|on it|will do)(?:[.!]+)?$/i;
|
|
10936
|
+
var DIRECTED_FOLLOW_UP_CUE_RE = /\b(?:you said|you just said|your last response|your last answer|what did you just say|what do you mean|what did you mean|explain(?: that| this| it| more)?|clarify(?: that| this| it)?|expand(?: on)?(?: that| this| it)?|elaborate(?: on)?(?: that| this| it)?|say more)\b/i;
|
|
10937
|
+
var TERSE_CLARIFICATION_RE = /^(?:which one|which ones|why|how so|what do you mean|what did you mean|say more|explain that|clarify that|expand on that|elaborate on that)\??$/i;
|
|
10938
|
+
var GENERIC_IMMEDIATE_SIDE_CONVERSATION_RE = /^(?:is that (?:the )?right (?:approach|call|move)|(?:can|could|would) you check on this)\??$/i;
|
|
10939
|
+
var RECENT_THREAD_WINDOW = 6;
|
|
10425
10940
|
function escapeRegExp(value) {
|
|
10426
10941
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
10427
10942
|
}
|
|
@@ -10458,36 +10973,113 @@ function isThreadOptOutInstruction(rawText, text) {
|
|
|
10458
10973
|
(pattern) => pattern.test(rawText) || pattern.test(text)
|
|
10459
10974
|
);
|
|
10460
10975
|
}
|
|
10461
|
-
function
|
|
10976
|
+
function isAcknowledgmentOnly(text) {
|
|
10977
|
+
return ACKNOWLEDGMENT_ONLY_RE.test(text.trim());
|
|
10978
|
+
}
|
|
10979
|
+
function hasDirectedFollowUpCue(text) {
|
|
10980
|
+
return DIRECTED_FOLLOW_UP_CUE_RE.test(text.trim());
|
|
10981
|
+
}
|
|
10982
|
+
function isTerseClarification(text) {
|
|
10983
|
+
return TERSE_CLARIFICATION_RE.test(text.trim());
|
|
10984
|
+
}
|
|
10985
|
+
function isGenericImmediateSideConversation(text) {
|
|
10986
|
+
const trimmed = text.trim();
|
|
10987
|
+
if (GENERIC_IMMEDIATE_SIDE_CONVERSATION_RE.test(trimmed)) {
|
|
10988
|
+
return true;
|
|
10989
|
+
}
|
|
10990
|
+
if (!trimmed.toLowerCase().startsWith("what about")) {
|
|
10991
|
+
return false;
|
|
10992
|
+
}
|
|
10993
|
+
const wordCount = trimmed.split(/\s+/).map((part) => part.trim()).filter(Boolean).length;
|
|
10994
|
+
return wordCount > 3;
|
|
10995
|
+
}
|
|
10996
|
+
function parseTranscriptMessages(conversationContext) {
|
|
10462
10997
|
if (!conversationContext) {
|
|
10463
|
-
return
|
|
10464
|
-
latestPriorMessageRole: "[none]",
|
|
10465
|
-
latestPriorAssistantMessage: "[none]"
|
|
10466
|
-
};
|
|
10998
|
+
return [];
|
|
10467
10999
|
}
|
|
11000
|
+
const messages = [];
|
|
10468
11001
|
const lines = conversationContext.split("\n").map((line) => line.trim()).filter((line) => line.length > 0);
|
|
10469
|
-
|
|
10470
|
-
|
|
10471
|
-
for (let index = lines.length - 1; index >= 0; index -= 1) {
|
|
10472
|
-
const match = lines[index]?.match(TRANSCRIPT_MESSAGE_LINE_RE);
|
|
11002
|
+
for (const line of lines) {
|
|
11003
|
+
const match = line.match(TRANSCRIPT_MESSAGE_LINE_RE);
|
|
10473
11004
|
if (!match) {
|
|
10474
11005
|
continue;
|
|
10475
11006
|
}
|
|
10476
|
-
|
|
10477
|
-
|
|
10478
|
-
|
|
10479
|
-
|
|
10480
|
-
|
|
11007
|
+
messages.push({
|
|
11008
|
+
role: match[1].toLowerCase(),
|
|
11009
|
+
author: match[2]?.trim() || "unknown",
|
|
11010
|
+
text: match[3]?.trim() || ""
|
|
11011
|
+
});
|
|
11012
|
+
}
|
|
11013
|
+
return messages;
|
|
11014
|
+
}
|
|
11015
|
+
function buildRouterSignals(input) {
|
|
11016
|
+
const transcriptMessages = parseTranscriptMessages(input.conversationContext);
|
|
11017
|
+
const recentMessages = transcriptMessages.filter((message) => message.role !== "system").slice(-RECENT_THREAD_WINDOW);
|
|
11018
|
+
const latestPriorMessage = [...transcriptMessages].reverse().find((message) => message.role !== "system");
|
|
11019
|
+
const latestPriorAssistantMessage = [...transcriptMessages].reverse().find((message) => message.role === "assistant");
|
|
11020
|
+
let humanMessagesSinceLastAssistant;
|
|
11021
|
+
let humanMessageCount = 0;
|
|
11022
|
+
for (let index = transcriptMessages.length - 1; index >= 0; index -= 1) {
|
|
11023
|
+
const message = transcriptMessages[index];
|
|
11024
|
+
if (!message || message.role === "system") {
|
|
11025
|
+
continue;
|
|
10481
11026
|
}
|
|
10482
|
-
if (
|
|
11027
|
+
if (message.role === "assistant") {
|
|
11028
|
+
humanMessagesSinceLastAssistant = humanMessageCount;
|
|
10483
11029
|
break;
|
|
10484
11030
|
}
|
|
11031
|
+
humanMessageCount += 1;
|
|
10485
11032
|
}
|
|
10486
11033
|
return {
|
|
10487
|
-
|
|
10488
|
-
|
|
11034
|
+
assistantWasLastSpeaker: latestPriorMessage?.role === "assistant",
|
|
11035
|
+
currentMessageHasDirectedFollowUpCue: hasDirectedFollowUpCue(input.text),
|
|
11036
|
+
currentMessageHasAttachments: Boolean(input.hasAttachments),
|
|
11037
|
+
currentMessageIsTerseClarification: isTerseClarification(input.text),
|
|
11038
|
+
humanMessagesSinceLastAssistant,
|
|
11039
|
+
latestPriorAssistantMessage: latestPriorAssistantMessage?.text || "[none]",
|
|
11040
|
+
latestPriorMessageRole: latestPriorMessage?.role || "[none]",
|
|
11041
|
+
recentMessages
|
|
10489
11042
|
};
|
|
10490
11043
|
}
|
|
11044
|
+
function buildRouterPrompt(rawText, signals) {
|
|
11045
|
+
const recentThread = signals.recentMessages.length > 0 ? signals.recentMessages.map(
|
|
11046
|
+
(message) => escapeXml(`[${message.role}] ${message.author}: ${message.text}`)
|
|
11047
|
+
).join("\n") : "[none]";
|
|
11048
|
+
return [
|
|
11049
|
+
`<latest-message>${escapeXml(rawText.trim() || "[attachment-only message]")}</latest-message>`,
|
|
11050
|
+
"<routing-signals>",
|
|
11051
|
+
`assistant_was_last_speaker=${signals.assistantWasLastSpeaker ? "true" : "false"}`,
|
|
11052
|
+
`human_messages_since_last_assistant=${signals.humanMessagesSinceLastAssistant ?? "none"}`,
|
|
11053
|
+
`latest_prior_message_role=${escapeXml(signals.latestPriorMessageRole)}`,
|
|
11054
|
+
`current_message_has_directed_follow_up_cue=${signals.currentMessageHasDirectedFollowUpCue ? "true" : "false"}`,
|
|
11055
|
+
`current_message_is_terse_clarification=${signals.currentMessageIsTerseClarification ? "true" : "false"}`,
|
|
11056
|
+
`current_message_has_attachments=${signals.currentMessageHasAttachments ? "true" : "false"}`,
|
|
11057
|
+
"</routing-signals>",
|
|
11058
|
+
`<latest-prior-assistant-message>${escapeXml(
|
|
11059
|
+
signals.latestPriorAssistantMessage
|
|
11060
|
+
)}</latest-prior-assistant-message>`,
|
|
11061
|
+
"<recent-thread>",
|
|
11062
|
+
recentThread,
|
|
11063
|
+
"</recent-thread>"
|
|
11064
|
+
].join("\n");
|
|
11065
|
+
}
|
|
11066
|
+
function getReplyConfidenceThreshold(signals) {
|
|
11067
|
+
let threshold = ROUTER_CONFIDENCE_THRESHOLD;
|
|
11068
|
+
if (signals.assistantWasLastSpeaker && signals.humanMessagesSinceLastAssistant === 0) {
|
|
11069
|
+
if (signals.currentMessageHasDirectedFollowUpCue || signals.currentMessageIsTerseClarification) {
|
|
11070
|
+
threshold = 0.65;
|
|
11071
|
+
} else {
|
|
11072
|
+
threshold = 0.9;
|
|
11073
|
+
}
|
|
11074
|
+
} else if (signals.humanMessagesSinceLastAssistant === 1) {
|
|
11075
|
+
threshold = signals.currentMessageHasDirectedFollowUpCue ? 0.8 : 0.9;
|
|
11076
|
+
} else if (signals.humanMessagesSinceLastAssistant === void 0) {
|
|
11077
|
+
threshold = 0.85;
|
|
11078
|
+
} else if (signals.humanMessagesSinceLastAssistant >= 2) {
|
|
11079
|
+
threshold = 0.9;
|
|
11080
|
+
}
|
|
11081
|
+
return Math.max(0.6, Math.min(0.9, threshold));
|
|
11082
|
+
}
|
|
10491
11083
|
function getSubscribedReplyPreflightDecision(args) {
|
|
10492
11084
|
const text = args.text.trim();
|
|
10493
11085
|
const rawText = args.rawText.trim();
|
|
@@ -10508,54 +11100,27 @@ function getSubscribedReplyPreflightDecision(args) {
|
|
|
10508
11100
|
reasonDetail: leadingOtherPartyAddress
|
|
10509
11101
|
};
|
|
10510
11102
|
}
|
|
10511
|
-
function buildRouterSystemPrompt(botUserName
|
|
10512
|
-
const { latestPriorMessageRole, latestPriorAssistantMessage } = getTranscriptMessageHints(conversationContext);
|
|
11103
|
+
function buildRouterSystemPrompt(botUserName) {
|
|
10513
11104
|
return [
|
|
10514
11105
|
"You are a message router for a Slack assistant named Junior in a subscribed Slack thread.",
|
|
10515
11106
|
"Decide whether Junior should reply to the latest message.",
|
|
10516
11107
|
"Subscribed threads are passive by default.",
|
|
10517
|
-
"
|
|
10518
|
-
"
|
|
10519
|
-
"",
|
|
10520
|
-
"
|
|
10521
|
-
"
|
|
10522
|
-
"",
|
|
10523
|
-
"
|
|
10524
|
-
"
|
|
10525
|
-
"
|
|
10526
|
-
"",
|
|
10527
|
-
"
|
|
10528
|
-
"- Questions between humans: 'Is that the right approach?', 'Can you check on this?', 'Did you deploy that?'",
|
|
10529
|
-
"- Acknowledgments: 'thanks', '+1', 'lgtm', 'ok cool', 'sounds good', 'nice'",
|
|
10530
|
-
"- Status updates: 'I just pushed a fix', 'Deploying now', 'Build is green'",
|
|
10531
|
-
"- General thread discussion: 'What about the billing issue?', 'I think we should revert'",
|
|
10532
|
-
"- Reactions to work: 'That looks wrong', 'Nice catch', 'Hmm interesting'",
|
|
10533
|
-
"",
|
|
10534
|
-
"Examples of messages Junior SHOULD reply to (should_reply=true):",
|
|
10535
|
-
"- Direct follow-ups to Junior's response: 'Can you explain that last point in more detail?'",
|
|
10536
|
-
"- Self-referential follow-ups after Junior just answered: 'What did you just say about the budget?', 'Can you explain your last response in more detail?'",
|
|
10537
|
-
"- Explicit requests for Junior's help: 'Junior, what's causing this error?'",
|
|
10538
|
-
"",
|
|
10539
|
-
"Treat a message as directed at Junior when it explicitly refers to Junior's immediately previous reply",
|
|
10540
|
-
"using language like 'you just said', 'your last response', 'your last answer', or similar self-reference.",
|
|
10541
|
-
"Do not confuse that with general topic continuation. A message like 'What about the billing worker timeline?'",
|
|
10542
|
-
"still should_reply=false unless it clearly asks Junior for help.",
|
|
10543
|
-
"",
|
|
10544
|
-
"When in doubt, should_reply=false. Most messages in a thread are human-to-human conversation.",
|
|
10545
|
-
"",
|
|
10546
|
-
"If the user is clearly telling Junior to stop watching, replying, or participating in the thread,",
|
|
10547
|
-
"set should_unsubscribe=true and should_reply=false.",
|
|
10548
|
-
"Use should_unsubscribe only for clear thread opt-out instructions, not for ordinary side conversation.",
|
|
10549
|
-
"If uncertain, set should_reply=false and use low confidence.",
|
|
11108
|
+
"Reply true only when the latest message is aimed at Junior.",
|
|
11109
|
+
"Use who currently has the conversation floor, not just topic overlap.",
|
|
11110
|
+
"If Junior was the last speaker, only a clear turn back to Junior should count as an implicit follow-up.",
|
|
11111
|
+
"Terse clarifications like 'which one?' or 'why?' right after Junior answers can be should_reply=true.",
|
|
11112
|
+
"Direct self-reference to Junior's prior answer like 'what did you just say?' or 'explain that more' can be should_reply=true.",
|
|
11113
|
+
"If one or more humans spoke after Junior, require a clear turn back to Junior. Shared domain vocabulary alone is not enough.",
|
|
11114
|
+
"Questions like 'what about auth?' or 'can you check on this?' are usually human-to-human unless the thread clearly turns back to Junior.",
|
|
11115
|
+
"A vague question like 'is that the right approach?' is still should_reply=false unless it clearly turns back to Junior.",
|
|
11116
|
+
"Acknowledgments, reactions, status chatter, and team coordination should be should_reply=false.",
|
|
11117
|
+
"If the latest message clearly tells Junior to stop watching, replying, or participating, set should_unsubscribe=true and should_reply=false.",
|
|
11118
|
+
"When uncertain, prefer should_reply=false with low confidence.",
|
|
10550
11119
|
"",
|
|
10551
11120
|
"Return JSON with should_reply, should_unsubscribe, confidence, and a short reason.",
|
|
10552
11121
|
"Do not return any extra keys.",
|
|
10553
11122
|
"",
|
|
10554
|
-
`<assistant-name>${escapeXml(botUserName)}</assistant-name
|
|
10555
|
-
`<explicit-mention>${isExplicitMention ? "true" : "false"}</explicit-mention>`,
|
|
10556
|
-
`<latest-prior-message-role>${escapeXml(latestPriorMessageRole)}</latest-prior-message-role>`,
|
|
10557
|
-
`<latest-prior-assistant-message>${escapeXml(latestPriorAssistantMessage)}</latest-prior-assistant-message>`,
|
|
10558
|
-
`<thread-context>${escapeXml(conversationContext?.trim() || "[none]")}</thread-context>`
|
|
11123
|
+
`<assistant-name>${escapeXml(botUserName)}</assistant-name>`
|
|
10559
11124
|
].join("\n");
|
|
10560
11125
|
}
|
|
10561
11126
|
async function decideSubscribedThreadReply(args) {
|
|
@@ -10570,11 +11135,16 @@ async function decideSubscribedThreadReply(args) {
|
|
|
10570
11135
|
if (preflightDecision) {
|
|
10571
11136
|
return preflightDecision;
|
|
10572
11137
|
}
|
|
11138
|
+
const signals = buildRouterSignals(args.input);
|
|
10573
11139
|
if (!text && !args.input.hasAttachments) {
|
|
10574
11140
|
return { shouldReply: false, reason: "empty_message" /* EmptyMessage */ };
|
|
10575
11141
|
}
|
|
10576
|
-
if (!
|
|
10577
|
-
return {
|
|
11142
|
+
if (!args.input.isExplicitMention && !args.input.hasAttachments && isAcknowledgmentOnly(text)) {
|
|
11143
|
+
return {
|
|
11144
|
+
shouldReply: false,
|
|
11145
|
+
reason: "side_conversation" /* SideConversation */,
|
|
11146
|
+
reasonDetail: "acknowledgment"
|
|
11147
|
+
};
|
|
10578
11148
|
}
|
|
10579
11149
|
if (args.input.isExplicitMention) {
|
|
10580
11150
|
if (isThreadOptOutInstruction(rawText, text)) {
|
|
@@ -10590,18 +11160,21 @@ async function decideSubscribedThreadReply(args) {
|
|
|
10590
11160
|
reason: "explicit_mention" /* ExplicitMention */
|
|
10591
11161
|
};
|
|
10592
11162
|
}
|
|
11163
|
+
if (signals.assistantWasLastSpeaker && signals.humanMessagesSinceLastAssistant === 0 && !signals.currentMessageHasAttachments && !signals.currentMessageHasDirectedFollowUpCue && !signals.currentMessageIsTerseClarification && isGenericImmediateSideConversation(text)) {
|
|
11164
|
+
return {
|
|
11165
|
+
shouldReply: false,
|
|
11166
|
+
reason: "side_conversation" /* SideConversation */,
|
|
11167
|
+
reasonDetail: "generic immediate side conversation"
|
|
11168
|
+
};
|
|
11169
|
+
}
|
|
10593
11170
|
try {
|
|
10594
11171
|
const result = await args.completeObject({
|
|
10595
11172
|
modelId: args.modelId,
|
|
10596
11173
|
schema: replyDecisionSchema,
|
|
10597
11174
|
maxTokens: 120,
|
|
10598
11175
|
temperature: 0,
|
|
10599
|
-
system: buildRouterSystemPrompt(
|
|
10600
|
-
|
|
10601
|
-
args.input.conversationContext,
|
|
10602
|
-
args.input.isExplicitMention
|
|
10603
|
-
),
|
|
10604
|
-
prompt: rawText,
|
|
11176
|
+
system: buildRouterSystemPrompt(args.botUserName),
|
|
11177
|
+
prompt: buildRouterPrompt(rawText, signals),
|
|
10605
11178
|
metadata: {
|
|
10606
11179
|
modelId: args.modelId,
|
|
10607
11180
|
threadId: args.input.context.threadId ?? "",
|
|
@@ -10612,6 +11185,7 @@ async function decideSubscribedThreadReply(args) {
|
|
|
10612
11185
|
});
|
|
10613
11186
|
const parsed = replyDecisionSchema.parse(result.object);
|
|
10614
11187
|
const reason = parsed.reason?.trim() || "classifier";
|
|
11188
|
+
const replyConfidenceThreshold = getReplyConfidenceThreshold(signals);
|
|
10615
11189
|
if (parsed.should_unsubscribe) {
|
|
10616
11190
|
if (parsed.confidence < ROUTER_CONFIDENCE_THRESHOLD) {
|
|
10617
11191
|
return {
|
|
@@ -10634,7 +11208,7 @@ async function decideSubscribedThreadReply(args) {
|
|
|
10634
11208
|
reasonDetail: reason
|
|
10635
11209
|
};
|
|
10636
11210
|
}
|
|
10637
|
-
if (parsed.confidence <
|
|
11211
|
+
if (parsed.confidence < replyConfidenceThreshold) {
|
|
10638
11212
|
return {
|
|
10639
11213
|
shouldReply: false,
|
|
10640
11214
|
reason: "low_confidence" /* LowConfidence */,
|
|
@@ -11229,16 +11803,166 @@ var MAX_USER_ATTACHMENTS = 3;
|
|
|
11229
11803
|
var MAX_USER_ATTACHMENT_BYTES = 5 * 1024 * 1024;
|
|
11230
11804
|
var MAX_MESSAGE_IMAGE_ATTACHMENTS = 3;
|
|
11231
11805
|
var MAX_VISION_SUMMARY_CHARS = 500;
|
|
11232
|
-
|
|
11806
|
+
function isVisionEnabled() {
|
|
11807
|
+
return Boolean(botConfig.visionModelId);
|
|
11808
|
+
}
|
|
11809
|
+
var ImageAttachmentProcessingError = class extends Error {
|
|
11810
|
+
constructor(message) {
|
|
11811
|
+
super(message);
|
|
11812
|
+
this.name = "ImageAttachmentProcessingError";
|
|
11813
|
+
}
|
|
11814
|
+
};
|
|
11815
|
+
function buildImageAttachmentPromptText(args) {
|
|
11816
|
+
return [
|
|
11817
|
+
"<image-attachment>",
|
|
11818
|
+
`filename: ${args.filename ?? "unnamed"}`,
|
|
11819
|
+
`media_type: ${args.mediaType}`,
|
|
11820
|
+
"<summary>",
|
|
11821
|
+
args.summary,
|
|
11822
|
+
"</summary>",
|
|
11823
|
+
"</image-attachment>"
|
|
11824
|
+
].join("\n");
|
|
11825
|
+
}
|
|
11826
|
+
async function summarizeImageWithVision(args) {
|
|
11827
|
+
const visionModelId = botConfig.visionModelId;
|
|
11828
|
+
if (!visionModelId) {
|
|
11829
|
+
return void 0;
|
|
11830
|
+
}
|
|
11831
|
+
const result = await args.completeText({
|
|
11832
|
+
modelId: visionModelId,
|
|
11833
|
+
temperature: 0,
|
|
11834
|
+
maxTokens: args.maxTokens,
|
|
11835
|
+
messages: [
|
|
11836
|
+
{
|
|
11837
|
+
role: "user",
|
|
11838
|
+
content: [
|
|
11839
|
+
{
|
|
11840
|
+
type: "text",
|
|
11841
|
+
text: args.prompt
|
|
11842
|
+
},
|
|
11843
|
+
{
|
|
11844
|
+
type: "image",
|
|
11845
|
+
data: args.imageData.toString("base64"),
|
|
11846
|
+
mimeType: args.mimeType
|
|
11847
|
+
}
|
|
11848
|
+
],
|
|
11849
|
+
timestamp: Date.now()
|
|
11850
|
+
}
|
|
11851
|
+
],
|
|
11852
|
+
metadata: {
|
|
11853
|
+
modelId: visionModelId,
|
|
11854
|
+
...args.metadata
|
|
11855
|
+
}
|
|
11856
|
+
});
|
|
11857
|
+
const summary = result.text.trim().replace(/\s+/g, " ");
|
|
11858
|
+
return summary || void 0;
|
|
11859
|
+
}
|
|
11860
|
+
function truncateVisionSummary(summary) {
|
|
11861
|
+
return summary.slice(0, MAX_VISION_SUMMARY_CHARS);
|
|
11862
|
+
}
|
|
11863
|
+
function getCachedImageSummaries(args) {
|
|
11864
|
+
if (!args.conversation || !args.messageTs) {
|
|
11865
|
+
return [];
|
|
11866
|
+
}
|
|
11867
|
+
const conversationMessage = args.conversation.messages.find(
|
|
11868
|
+
(message) => getConversationMessageSlackTs(message) === args.messageTs
|
|
11869
|
+
);
|
|
11870
|
+
if (!conversationMessage) {
|
|
11871
|
+
return [];
|
|
11872
|
+
}
|
|
11873
|
+
return (conversationMessage.meta?.imageFileIds ?? []).map(
|
|
11874
|
+
(fileId) => args.conversation?.vision.byFileId[fileId]?.summary?.trim()
|
|
11875
|
+
);
|
|
11876
|
+
}
|
|
11877
|
+
function createImageAttachmentProcessingError(attachment) {
|
|
11878
|
+
const label = attachment.filename ? `"${attachment.filename}"` : "this image";
|
|
11879
|
+
return new ImageAttachmentProcessingError(
|
|
11880
|
+
`Image attachment ${label} could not be analyzed`
|
|
11881
|
+
);
|
|
11882
|
+
}
|
|
11883
|
+
async function resolveUserAttachmentsWithDeps(attachments, context, deps) {
|
|
11233
11884
|
if (!attachments || attachments.length === 0) {
|
|
11234
11885
|
return [];
|
|
11235
11886
|
}
|
|
11236
11887
|
const results = [];
|
|
11888
|
+
const cachedImageSummaries = getCachedImageSummaries({
|
|
11889
|
+
conversation: context.conversation,
|
|
11890
|
+
messageTs: context.messageTs
|
|
11891
|
+
});
|
|
11892
|
+
let nextCachedImageSummaryIndex = 0;
|
|
11237
11893
|
for (const attachment of attachments) {
|
|
11238
11894
|
if (results.length >= MAX_USER_ATTACHMENTS) break;
|
|
11239
11895
|
if (attachment.type !== "image" && attachment.type !== "file") continue;
|
|
11240
11896
|
const mediaType = attachment.mimeType ?? "application/octet-stream";
|
|
11897
|
+
const isImageAttachment = attachment.type === "image" || mediaType.startsWith("image/");
|
|
11898
|
+
if (isImageAttachment && !isVisionEnabled()) {
|
|
11899
|
+
continue;
|
|
11900
|
+
}
|
|
11241
11901
|
try {
|
|
11902
|
+
const resolvedAttachment = {
|
|
11903
|
+
mediaType,
|
|
11904
|
+
filename: attachment.name
|
|
11905
|
+
};
|
|
11906
|
+
if (isImageAttachment) {
|
|
11907
|
+
const cachedSummary = cachedImageSummaries[nextCachedImageSummaryIndex];
|
|
11908
|
+
nextCachedImageSummaryIndex += 1;
|
|
11909
|
+
if (cachedSummary) {
|
|
11910
|
+
resolvedAttachment.promptText = buildImageAttachmentPromptText({
|
|
11911
|
+
filename: attachment.name,
|
|
11912
|
+
mediaType,
|
|
11913
|
+
summary: cachedSummary
|
|
11914
|
+
});
|
|
11915
|
+
results.push(resolvedAttachment);
|
|
11916
|
+
continue;
|
|
11917
|
+
}
|
|
11918
|
+
let imageData = null;
|
|
11919
|
+
if (attachment.fetchData) {
|
|
11920
|
+
imageData = await attachment.fetchData();
|
|
11921
|
+
} else if (attachment.data instanceof Buffer) {
|
|
11922
|
+
imageData = attachment.data;
|
|
11923
|
+
}
|
|
11924
|
+
if (!imageData) {
|
|
11925
|
+
throw createImageAttachmentProcessingError({
|
|
11926
|
+
filename: attachment.name
|
|
11927
|
+
});
|
|
11928
|
+
}
|
|
11929
|
+
if (imageData.byteLength > MAX_USER_ATTACHMENT_BYTES) {
|
|
11930
|
+
throw createImageAttachmentProcessingError({
|
|
11931
|
+
filename: attachment.name
|
|
11932
|
+
});
|
|
11933
|
+
}
|
|
11934
|
+
const summary = await summarizeImageWithVision({
|
|
11935
|
+
completeText: deps.completeText,
|
|
11936
|
+
imageData,
|
|
11937
|
+
mimeType: mediaType,
|
|
11938
|
+
maxTokens: 220,
|
|
11939
|
+
prompt: [
|
|
11940
|
+
"Extract concise, factual context from this user-provided image.",
|
|
11941
|
+
"Focus on visible text, UI state, charts, diagrams, errors, names, and other concrete details useful for answering the user's current request.",
|
|
11942
|
+
"Do not speculate.",
|
|
11943
|
+
"Return plain text only."
|
|
11944
|
+
].join(" "),
|
|
11945
|
+
metadata: {
|
|
11946
|
+
threadId: context.threadId ?? "",
|
|
11947
|
+
channelId: context.channelId ?? "",
|
|
11948
|
+
requesterId: context.requesterId ?? "",
|
|
11949
|
+
runId: context.runId ?? "",
|
|
11950
|
+
filename: attachment.name ?? ""
|
|
11951
|
+
}
|
|
11952
|
+
});
|
|
11953
|
+
if (!summary) {
|
|
11954
|
+
throw createImageAttachmentProcessingError({
|
|
11955
|
+
filename: attachment.name
|
|
11956
|
+
});
|
|
11957
|
+
}
|
|
11958
|
+
resolvedAttachment.promptText = buildImageAttachmentPromptText({
|
|
11959
|
+
filename: attachment.name,
|
|
11960
|
+
mediaType,
|
|
11961
|
+
summary: truncateVisionSummary(summary)
|
|
11962
|
+
});
|
|
11963
|
+
results.push(resolvedAttachment);
|
|
11964
|
+
continue;
|
|
11965
|
+
}
|
|
11242
11966
|
let data = null;
|
|
11243
11967
|
if (attachment.fetchData) {
|
|
11244
11968
|
data = await attachment.fetchData();
|
|
@@ -11265,12 +11989,32 @@ async function resolveUserAttachments(attachments, context) {
|
|
|
11265
11989
|
);
|
|
11266
11990
|
continue;
|
|
11267
11991
|
}
|
|
11268
|
-
|
|
11269
|
-
|
|
11270
|
-
mediaType,
|
|
11271
|
-
filename: attachment.name
|
|
11272
|
-
});
|
|
11992
|
+
resolvedAttachment.data = data;
|
|
11993
|
+
results.push(resolvedAttachment);
|
|
11273
11994
|
} catch (error) {
|
|
11995
|
+
if (isImageAttachment) {
|
|
11996
|
+
const attachmentError = error instanceof ImageAttachmentProcessingError ? error : createImageAttachmentProcessingError({
|
|
11997
|
+
filename: attachment.name
|
|
11998
|
+
});
|
|
11999
|
+
logWarn(
|
|
12000
|
+
"image_attachment_processing_failed",
|
|
12001
|
+
{
|
|
12002
|
+
slackThreadId: context.threadId,
|
|
12003
|
+
slackUserId: context.requesterId,
|
|
12004
|
+
slackChannelId: context.channelId,
|
|
12005
|
+
runId: context.runId,
|
|
12006
|
+
assistantUserName: botConfig.userName,
|
|
12007
|
+
modelId: botConfig.visionModelId ?? botConfig.modelId
|
|
12008
|
+
},
|
|
12009
|
+
{
|
|
12010
|
+
"error.message": error instanceof Error ? error.message : String(error),
|
|
12011
|
+
"file.mime_type": mediaType,
|
|
12012
|
+
...attachment.name ? { "file.name": attachment.name } : {}
|
|
12013
|
+
},
|
|
12014
|
+
"Image attachment processing failed"
|
|
12015
|
+
);
|
|
12016
|
+
throw attachmentError;
|
|
12017
|
+
}
|
|
11274
12018
|
logWarn(
|
|
11275
12019
|
"attachment_resolution_failed",
|
|
11276
12020
|
{
|
|
@@ -11292,35 +12036,23 @@ async function resolveUserAttachments(attachments, context) {
|
|
|
11292
12036
|
return results;
|
|
11293
12037
|
}
|
|
11294
12038
|
async function summarizeConversationImage(args, deps) {
|
|
12039
|
+
const visionModelId = botConfig.visionModelId;
|
|
12040
|
+
if (!visionModelId) {
|
|
12041
|
+
return void 0;
|
|
12042
|
+
}
|
|
11295
12043
|
try {
|
|
11296
|
-
const
|
|
11297
|
-
|
|
11298
|
-
|
|
12044
|
+
const summary = await summarizeImageWithVision({
|
|
12045
|
+
completeText: deps.completeText,
|
|
12046
|
+
imageData: args.imageData,
|
|
12047
|
+
mimeType: args.mimeType,
|
|
11299
12048
|
maxTokens: 220,
|
|
11300
|
-
|
|
11301
|
-
|
|
11302
|
-
|
|
11303
|
-
|
|
11304
|
-
|
|
11305
|
-
|
|
11306
|
-
text: [
|
|
11307
|
-
"Extract concise, factual context from this image for future thread turns.",
|
|
11308
|
-
"Focus on visible text, names, titles, companies, and candidate-identifying details.",
|
|
11309
|
-
"Do not speculate.",
|
|
11310
|
-
"Return plain text only."
|
|
11311
|
-
].join(" ")
|
|
11312
|
-
},
|
|
11313
|
-
{
|
|
11314
|
-
type: "image",
|
|
11315
|
-
data: args.imageData.toString("base64"),
|
|
11316
|
-
mimeType: args.mimeType
|
|
11317
|
-
}
|
|
11318
|
-
],
|
|
11319
|
-
timestamp: Date.now()
|
|
11320
|
-
}
|
|
11321
|
-
],
|
|
12049
|
+
prompt: [
|
|
12050
|
+
"Extract concise, factual context from this image for future thread turns.",
|
|
12051
|
+
"Focus on visible text, names, titles, companies, and candidate-identifying details.",
|
|
12052
|
+
"Do not speculate.",
|
|
12053
|
+
"Return plain text only."
|
|
12054
|
+
].join(" "),
|
|
11322
12055
|
metadata: {
|
|
11323
|
-
modelId: botConfig.modelId,
|
|
11324
12056
|
threadId: args.context.threadId ?? "",
|
|
11325
12057
|
channelId: args.context.channelId ?? "",
|
|
11326
12058
|
requesterId: args.context.requesterId ?? "",
|
|
@@ -11328,11 +12060,10 @@ async function summarizeConversationImage(args, deps) {
|
|
|
11328
12060
|
fileId: args.fileId
|
|
11329
12061
|
}
|
|
11330
12062
|
});
|
|
11331
|
-
const summary = result.text.trim().replace(/\s+/g, " ");
|
|
11332
12063
|
if (!summary) {
|
|
11333
12064
|
return void 0;
|
|
11334
12065
|
}
|
|
11335
|
-
return summary
|
|
12066
|
+
return truncateVisionSummary(summary);
|
|
11336
12067
|
} catch (error) {
|
|
11337
12068
|
logWarn(
|
|
11338
12069
|
"conversation_image_vision_failed",
|
|
@@ -11342,7 +12073,7 @@ async function summarizeConversationImage(args, deps) {
|
|
|
11342
12073
|
slackChannelId: args.context.channelId,
|
|
11343
12074
|
runId: args.context.runId,
|
|
11344
12075
|
assistantUserName: botConfig.userName,
|
|
11345
|
-
modelId:
|
|
12076
|
+
modelId: visionModelId
|
|
11346
12077
|
},
|
|
11347
12078
|
{
|
|
11348
12079
|
"error.message": error instanceof Error ? error.message : String(error),
|
|
@@ -11355,6 +12086,9 @@ async function summarizeConversationImage(args, deps) {
|
|
|
11355
12086
|
}
|
|
11356
12087
|
}
|
|
11357
12088
|
async function hydrateConversationVisionContextWithDeps(conversation, context, deps) {
|
|
12089
|
+
if (!isVisionEnabled()) {
|
|
12090
|
+
return;
|
|
12091
|
+
}
|
|
11358
12092
|
if (!context.channelId || !context.threadTs) {
|
|
11359
12093
|
return;
|
|
11360
12094
|
}
|
|
@@ -11555,6 +12289,7 @@ async function hydrateConversationVisionContextWithDeps(conversation, context, d
|
|
|
11555
12289
|
}
|
|
11556
12290
|
function createVisionContextService(deps) {
|
|
11557
12291
|
return {
|
|
12292
|
+
resolveUserAttachments: async (attachments, context) => await resolveUserAttachmentsWithDeps(attachments, context, deps),
|
|
11558
12293
|
hydrateConversationVisionContext: async (conversation, context) => await hydrateConversationVisionContextWithDeps(
|
|
11559
12294
|
conversation,
|
|
11560
12295
|
context,
|
|
@@ -11593,111 +12328,6 @@ function createJuniorRuntimeServices(overrides = {}) {
|
|
|
11593
12328
|
};
|
|
11594
12329
|
}
|
|
11595
12330
|
|
|
11596
|
-
// src/chat/runtime/progress-reporter.ts
|
|
11597
|
-
var STATUS_UPDATE_DEBOUNCE_MS = 1e3;
|
|
11598
|
-
var STATUS_MIN_VISIBLE_MS = 1200;
|
|
11599
|
-
function createProgressReporter(args) {
|
|
11600
|
-
const now = args.now ?? (() => Date.now());
|
|
11601
|
-
const setTimer = args.setTimer ?? ((callback, delayMs) => setTimeout(callback, delayMs));
|
|
11602
|
-
const clearTimer = args.clearTimer ?? ((timer) => clearTimeout(timer));
|
|
11603
|
-
let active = false;
|
|
11604
|
-
let currentStatus = "";
|
|
11605
|
-
let lastStatusAt = 0;
|
|
11606
|
-
let pendingStatus = null;
|
|
11607
|
-
let pendingTimer = null;
|
|
11608
|
-
let inflightStatusUpdate = Promise.resolve();
|
|
11609
|
-
const postStatus = async (text) => {
|
|
11610
|
-
const channelId = args.channelId;
|
|
11611
|
-
const threadTs = args.threadTs;
|
|
11612
|
-
if (!channelId || !threadTs) {
|
|
11613
|
-
return;
|
|
11614
|
-
}
|
|
11615
|
-
if (!text && !currentStatus) {
|
|
11616
|
-
return;
|
|
11617
|
-
}
|
|
11618
|
-
currentStatus = text;
|
|
11619
|
-
lastStatusAt = now();
|
|
11620
|
-
const suggestions = text ? [text] : void 0;
|
|
11621
|
-
const previous = inflightStatusUpdate;
|
|
11622
|
-
const request = (async () => {
|
|
11623
|
-
await previous;
|
|
11624
|
-
try {
|
|
11625
|
-
await args.setAssistantStatus(channelId, threadTs, text, suggestions);
|
|
11626
|
-
} catch (error) {
|
|
11627
|
-
logWarn(
|
|
11628
|
-
"assistant_status_update_failed",
|
|
11629
|
-
{},
|
|
11630
|
-
{
|
|
11631
|
-
"app.slack.status_text": text || "(clear)",
|
|
11632
|
-
"error.message": error instanceof Error ? error.message : String(error)
|
|
11633
|
-
},
|
|
11634
|
-
"Failed to update assistant status"
|
|
11635
|
-
);
|
|
11636
|
-
}
|
|
11637
|
-
})();
|
|
11638
|
-
inflightStatusUpdate = request;
|
|
11639
|
-
await request;
|
|
11640
|
-
};
|
|
11641
|
-
const clearPending = () => {
|
|
11642
|
-
if (pendingTimer) {
|
|
11643
|
-
clearTimer(pendingTimer);
|
|
11644
|
-
pendingTimer = null;
|
|
11645
|
-
}
|
|
11646
|
-
pendingStatus = null;
|
|
11647
|
-
};
|
|
11648
|
-
const flushPending = async () => {
|
|
11649
|
-
if (!active || !pendingStatus) {
|
|
11650
|
-
clearPending();
|
|
11651
|
-
return;
|
|
11652
|
-
}
|
|
11653
|
-
const next = pendingStatus;
|
|
11654
|
-
clearPending();
|
|
11655
|
-
if (next !== currentStatus) {
|
|
11656
|
-
await postStatus(next);
|
|
11657
|
-
}
|
|
11658
|
-
};
|
|
11659
|
-
return {
|
|
11660
|
-
async start() {
|
|
11661
|
-
active = true;
|
|
11662
|
-
clearPending();
|
|
11663
|
-
void postStatus("Thinking...");
|
|
11664
|
-
},
|
|
11665
|
-
async stop() {
|
|
11666
|
-
active = false;
|
|
11667
|
-
clearPending();
|
|
11668
|
-
await postStatus("");
|
|
11669
|
-
},
|
|
11670
|
-
async setStatus(text) {
|
|
11671
|
-
const truncated = truncateStatusText(text);
|
|
11672
|
-
if (!active || !truncated || truncated === currentStatus || truncated === pendingStatus) {
|
|
11673
|
-
return;
|
|
11674
|
-
}
|
|
11675
|
-
const elapsed = now() - lastStatusAt;
|
|
11676
|
-
const waitMs = Math.max(
|
|
11677
|
-
STATUS_UPDATE_DEBOUNCE_MS - elapsed,
|
|
11678
|
-
STATUS_MIN_VISIBLE_MS - elapsed,
|
|
11679
|
-
0
|
|
11680
|
-
);
|
|
11681
|
-
if (waitMs <= 0) {
|
|
11682
|
-
clearPending();
|
|
11683
|
-
void postStatus(truncated);
|
|
11684
|
-
return;
|
|
11685
|
-
}
|
|
11686
|
-
pendingStatus = truncated;
|
|
11687
|
-
if (pendingTimer) {
|
|
11688
|
-
return;
|
|
11689
|
-
}
|
|
11690
|
-
pendingTimer = setTimer(
|
|
11691
|
-
() => {
|
|
11692
|
-
pendingTimer = null;
|
|
11693
|
-
void flushPending();
|
|
11694
|
-
},
|
|
11695
|
-
Math.max(1, waitMs)
|
|
11696
|
-
);
|
|
11697
|
-
}
|
|
11698
|
-
};
|
|
11699
|
-
}
|
|
11700
|
-
|
|
11701
12331
|
// src/chat/runtime/streaming.ts
|
|
11702
12332
|
function createTextStreamBridge() {
|
|
11703
12333
|
const queue = [];
|
|
@@ -11738,36 +12368,6 @@ function createTextStreamBridge() {
|
|
|
11738
12368
|
}
|
|
11739
12369
|
};
|
|
11740
12370
|
}
|
|
11741
|
-
function createNormalizingStream(inner, normalize) {
|
|
11742
|
-
return {
|
|
11743
|
-
async *[Symbol.asyncIterator]() {
|
|
11744
|
-
let accumulated = "";
|
|
11745
|
-
let emitted = 0;
|
|
11746
|
-
for await (const chunk of inner) {
|
|
11747
|
-
accumulated += chunk;
|
|
11748
|
-
const lastNewline = accumulated.lastIndexOf("\n");
|
|
11749
|
-
if (lastNewline === -1) {
|
|
11750
|
-
const delta2 = accumulated.slice(emitted);
|
|
11751
|
-
if (delta2) {
|
|
11752
|
-
yield delta2;
|
|
11753
|
-
emitted = accumulated.length;
|
|
11754
|
-
}
|
|
11755
|
-
continue;
|
|
11756
|
-
}
|
|
11757
|
-
const stable = accumulated.slice(0, lastNewline + 1);
|
|
11758
|
-
const normalized = normalize(stable);
|
|
11759
|
-
const delta = normalized.slice(emitted);
|
|
11760
|
-
emitted = normalized.length;
|
|
11761
|
-
if (delta) yield delta;
|
|
11762
|
-
}
|
|
11763
|
-
if (accumulated) {
|
|
11764
|
-
const normalized = normalize(accumulated);
|
|
11765
|
-
const delta = normalized.slice(emitted);
|
|
11766
|
-
if (delta) yield delta;
|
|
11767
|
-
}
|
|
11768
|
-
}
|
|
11769
|
-
};
|
|
11770
|
-
}
|
|
11771
12371
|
|
|
11772
12372
|
// src/chat/runtime/reply-executor.ts
|
|
11773
12373
|
function getExecutionFailureReason(reply) {
|
|
@@ -11869,19 +12469,23 @@ function createReplyToThread(deps) {
|
|
|
11869
12469
|
if (resolvedUserName) {
|
|
11870
12470
|
setTags({ slackUserName: resolvedUserName });
|
|
11871
12471
|
}
|
|
11872
|
-
const userAttachments = await resolveUserAttachments(
|
|
12472
|
+
const userAttachments = await deps.resolveUserAttachments(
|
|
11873
12473
|
message.attachments,
|
|
11874
12474
|
{
|
|
11875
12475
|
threadId,
|
|
11876
12476
|
requesterId: message.author.userId,
|
|
11877
12477
|
channelId,
|
|
11878
|
-
runId
|
|
12478
|
+
runId,
|
|
12479
|
+
conversation: preparedState.conversation,
|
|
12480
|
+
messageTs: message.id
|
|
11879
12481
|
}
|
|
11880
12482
|
);
|
|
11881
12483
|
const progress = createProgressReporter({
|
|
11882
12484
|
channelId,
|
|
11883
12485
|
threadTs,
|
|
11884
|
-
|
|
12486
|
+
transport: createSlackAdapterAssistantStatusTransport({
|
|
12487
|
+
getSlackAdapter: deps.getSlackAdapter
|
|
12488
|
+
})
|
|
11885
12489
|
});
|
|
11886
12490
|
const textStream = createTextStreamBridge();
|
|
11887
12491
|
let streamedReplyPromise;
|
|
@@ -11898,10 +12502,7 @@ function createReplyToThread(deps) {
|
|
|
11898
12502
|
if (!streamedReplyPromise) {
|
|
11899
12503
|
const streamingReply = (async () => {
|
|
11900
12504
|
return await postThreadReply(
|
|
11901
|
-
|
|
11902
|
-
textStream.iterable,
|
|
11903
|
-
ensureBlockSpacing
|
|
11904
|
-
),
|
|
12505
|
+
textStream.iterable,
|
|
11905
12506
|
"streaming_initial_post"
|
|
11906
12507
|
);
|
|
11907
12508
|
})();
|
|
@@ -12286,7 +12887,7 @@ function createPrepareTurnState(deps) {
|
|
|
12286
12887
|
conversation,
|
|
12287
12888
|
incomingUserMessage
|
|
12288
12889
|
);
|
|
12289
|
-
if (
|
|
12890
|
+
if (isVisionEnabled() && (!conversation.vision.backfillCompletedAtMs || messageHasPotentialImageAttachment)) {
|
|
12290
12891
|
await deps.hydrateConversationVisionContext(conversation, {
|
|
12291
12892
|
threadId: args.context.threadId,
|
|
12292
12893
|
channelId: args.context.channelId,
|
|
@@ -12333,6 +12934,7 @@ function createSlackRuntime(options) {
|
|
|
12333
12934
|
const replyToThread = createReplyToThread({
|
|
12334
12935
|
getSlackAdapter: options.getSlackAdapter,
|
|
12335
12936
|
prepareTurnState,
|
|
12937
|
+
resolveUserAttachments: services.visionContext.resolveUserAttachments,
|
|
12336
12938
|
services: services.replyExecutor
|
|
12337
12939
|
});
|
|
12338
12940
|
return createSlackTurnRuntime({
|