@sentry/junior 0.19.0 → 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,
|
|
@@ -5021,7 +5021,7 @@ function toLoadedSkill(result, availableSkills) {
|
|
|
5021
5021
|
return {
|
|
5022
5022
|
name: result.skill_name,
|
|
5023
5023
|
description: result.description,
|
|
5024
|
-
skillPath: result.skill_dir,
|
|
5024
|
+
skillPath: metadata?.skillPath ?? result.skill_dir,
|
|
5025
5025
|
...metadata?.pluginProvider ? { pluginProvider: metadata.pluginProvider } : {},
|
|
5026
5026
|
...metadata?.allowedTools ? { allowedTools: metadata.allowedTools } : {},
|
|
5027
5027
|
...metadata?.requiresCapabilities ? { requiresCapabilities: metadata.requiresCapabilities } : {},
|
|
@@ -5029,7 +5029,7 @@ function toLoadedSkill(result, availableSkills) {
|
|
|
5029
5029
|
body: result.instructions
|
|
5030
5030
|
};
|
|
5031
5031
|
}
|
|
5032
|
-
async function
|
|
5032
|
+
async function loadSkillFromHost(availableSkills, skillName) {
|
|
5033
5033
|
const requested = skillName.trim().toLowerCase();
|
|
5034
5034
|
const skill = availableSkills.find(
|
|
5035
5035
|
(entry) => entry.name.toLowerCase() === requested
|
|
@@ -5042,10 +5042,10 @@ async function loadSkillFromSandbox(sandbox, availableSkills, skillName) {
|
|
|
5042
5042
|
};
|
|
5043
5043
|
}
|
|
5044
5044
|
const skillDir = sandboxSkillDir(skill.name);
|
|
5045
|
-
const skillFilePath =
|
|
5046
|
-
const
|
|
5047
|
-
if (!
|
|
5048
|
-
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}`);
|
|
5049
5049
|
}
|
|
5050
5050
|
return {
|
|
5051
5051
|
ok: true,
|
|
@@ -5054,10 +5054,10 @@ async function loadSkillFromSandbox(sandbox, availableSkills, skillName) {
|
|
|
5054
5054
|
...skill.requiresCapabilities ? { requires_capabilities: skill.requiresCapabilities } : {},
|
|
5055
5055
|
skill_dir: skillDir,
|
|
5056
5056
|
location: skillFilePath,
|
|
5057
|
-
instructions:
|
|
5057
|
+
instructions: loaded.body
|
|
5058
5058
|
};
|
|
5059
5059
|
}
|
|
5060
|
-
function createLoadSkillTool(
|
|
5060
|
+
function createLoadSkillTool(availableSkills, options) {
|
|
5061
5061
|
return tool({
|
|
5062
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.",
|
|
5063
5063
|
inputSchema: Type4.Object({
|
|
@@ -5067,11 +5067,7 @@ function createLoadSkillTool(sandbox, availableSkills, options) {
|
|
|
5067
5067
|
})
|
|
5068
5068
|
}),
|
|
5069
5069
|
execute: async ({ skill_name }) => {
|
|
5070
|
-
const result = await
|
|
5071
|
-
sandbox,
|
|
5072
|
-
availableSkills,
|
|
5073
|
-
skill_name
|
|
5074
|
-
);
|
|
5070
|
+
const result = await loadSkillFromHost(availableSkills, skill_name);
|
|
5075
5071
|
const loadedSkill = toLoadedSkill(result, availableSkills);
|
|
5076
5072
|
if (loadedSkill) {
|
|
5077
5073
|
const metadata = await options?.onSkillLoaded?.(loadedSkill);
|
|
@@ -6810,7 +6806,7 @@ function createToolState(hooks, context) {
|
|
|
6810
6806
|
function createTools(availableSkills, hooks = {}, context) {
|
|
6811
6807
|
const state = createToolState(hooks, context);
|
|
6812
6808
|
const tools = {
|
|
6813
|
-
loadSkill: createLoadSkillTool(
|
|
6809
|
+
loadSkill: createLoadSkillTool(availableSkills, {
|
|
6814
6810
|
onSkillLoaded: hooks.onSkillLoaded
|
|
6815
6811
|
}),
|
|
6816
6812
|
systemTime: createSystemTimeTool(),
|
|
@@ -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,6 +6966,109 @@ function extractHttpErrorDetails(error, options = {}) {
|
|
|
6973
6966
|
};
|
|
6974
6967
|
}
|
|
6975
6968
|
|
|
6969
|
+
// src/chat/sandbox/errors.ts
|
|
6970
|
+
var SANDBOX_ERROR_FIELDS = [
|
|
6971
|
+
{
|
|
6972
|
+
sourceKey: "sandboxId",
|
|
6973
|
+
attributeKey: "sandbox_id",
|
|
6974
|
+
summaryKey: "sandboxId"
|
|
6975
|
+
}
|
|
6976
|
+
];
|
|
6977
|
+
function getSandboxErrorDetails(error) {
|
|
6978
|
+
return extractHttpErrorDetails(error, {
|
|
6979
|
+
attributePrefix: "app.sandbox.api_error",
|
|
6980
|
+
extraFields: [...SANDBOX_ERROR_FIELDS]
|
|
6981
|
+
});
|
|
6982
|
+
}
|
|
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;
|
|
6992
|
+
}
|
|
6993
|
+
return false;
|
|
6994
|
+
}
|
|
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;
|
|
7003
|
+
}
|
|
7004
|
+
}
|
|
7005
|
+
seen.add(current);
|
|
7006
|
+
current = typeof current === "object" ? current.cause : void 0;
|
|
7007
|
+
}
|
|
7008
|
+
return void 0;
|
|
7009
|
+
}
|
|
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
|
|
7034
|
+
});
|
|
7035
|
+
}
|
|
7036
|
+
} catch {
|
|
7037
|
+
}
|
|
7038
|
+
let causeMessage;
|
|
7039
|
+
try {
|
|
7040
|
+
causeMessage = getFirstErrorMessage(error);
|
|
7041
|
+
} catch (cause) {
|
|
7042
|
+
causeMessage = cause instanceof Error ? cause.message : void 0;
|
|
7043
|
+
}
|
|
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
|
+
);
|
|
7066
|
+
}
|
|
7067
|
+
|
|
7068
|
+
// src/chat/sandbox/session.ts
|
|
7069
|
+
import { Sandbox } from "@vercel/sandbox";
|
|
7070
|
+
import { createBashTool as createBashTool2 } from "bash-tool";
|
|
7071
|
+
|
|
6976
7072
|
// src/chat/runtime/status-format.ts
|
|
6977
7073
|
var SLACK_STATUS_MAX_LENGTH = 50;
|
|
6978
7074
|
function truncateWithEllipsis(text, maxLength) {
|
|
@@ -7234,112 +7330,11 @@ function logAssistantStatusFailure(status, error) {
|
|
|
7234
7330
|
);
|
|
7235
7331
|
}
|
|
7236
7332
|
|
|
7237
|
-
// src/chat/sandbox/
|
|
7238
|
-
|
|
7239
|
-
|
|
7240
|
-
var SANDBOX_RUNTIME = "node22";
|
|
7241
|
-
var SANDBOX_RUNTIME_BIN_DIR = `${SANDBOX_WORKSPACE_ROOT}/.junior/bin`;
|
|
7242
|
-
var EVAL_GH_STUB_PATH = `${SANDBOX_RUNTIME_BIN_DIR}/gh`;
|
|
7243
|
-
var SNAPSHOT_BOOT_RETRY_COUNT = 3;
|
|
7244
|
-
var SNAPSHOT_BOOT_RETRY_DELAY_MS = 1e3;
|
|
7245
|
-
var SANDBOX_ERROR_FIELDS = [
|
|
7246
|
-
{
|
|
7247
|
-
sourceKey: "sandboxId",
|
|
7248
|
-
attributeKey: "sandbox_id",
|
|
7249
|
-
summaryKey: "sandboxId"
|
|
7250
|
-
}
|
|
7251
|
-
];
|
|
7252
|
-
function mergeNetworkPolicyWithHeaderTransforms(networkPolicy, headerTransforms) {
|
|
7253
|
-
const basePolicy = networkPolicy && typeof networkPolicy === "object" && !Array.isArray(networkPolicy) ? { ...networkPolicy } : {};
|
|
7254
|
-
const existingAllowRaw = basePolicy.allow;
|
|
7255
|
-
const existingAllow = existingAllowRaw && typeof existingAllowRaw === "object" && !Array.isArray(existingAllowRaw) ? Object.fromEntries(
|
|
7256
|
-
Object.entries(existingAllowRaw).map(
|
|
7257
|
-
([domain, rules]) => [
|
|
7258
|
-
domain,
|
|
7259
|
-
Array.isArray(rules) ? [...rules] : []
|
|
7260
|
-
]
|
|
7261
|
-
)
|
|
7262
|
-
) : { "*": [] };
|
|
7263
|
-
for (const transform of headerTransforms) {
|
|
7264
|
-
const currentRules = existingAllow[transform.domain] ?? [];
|
|
7265
|
-
existingAllow[transform.domain] = [
|
|
7266
|
-
...currentRules,
|
|
7267
|
-
{ transform: [{ headers: transform.headers }] }
|
|
7268
|
-
];
|
|
7269
|
-
}
|
|
7270
|
-
return {
|
|
7271
|
-
...basePolicy,
|
|
7272
|
-
allow: existingAllow
|
|
7273
|
-
};
|
|
7274
|
-
}
|
|
7275
|
-
function truncateOutput(output, maxLength) {
|
|
7276
|
-
if (output.length <= maxLength) {
|
|
7277
|
-
return { value: output, truncated: false };
|
|
7278
|
-
}
|
|
7279
|
-
const truncatedLength = output.length - maxLength;
|
|
7280
|
-
return {
|
|
7281
|
-
value: `${output.slice(0, maxLength)}
|
|
7333
|
+
// src/chat/sandbox/skill-sync.ts
|
|
7334
|
+
import fs3 from "fs/promises";
|
|
7335
|
+
import path4 from "path";
|
|
7282
7336
|
|
|
7283
|
-
|
|
7284
|
-
truncated: true
|
|
7285
|
-
};
|
|
7286
|
-
}
|
|
7287
|
-
function toPosixRelative(base, absolute) {
|
|
7288
|
-
return path4.relative(base, absolute).split(path4.sep).join("/");
|
|
7289
|
-
}
|
|
7290
|
-
async function listFilesRecursive(root) {
|
|
7291
|
-
const queue = [root];
|
|
7292
|
-
const files = [];
|
|
7293
|
-
while (queue.length > 0) {
|
|
7294
|
-
const dir = queue.shift();
|
|
7295
|
-
const entries = await fs3.readdir(dir, { withFileTypes: true });
|
|
7296
|
-
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
7297
|
-
for (const entry of entries) {
|
|
7298
|
-
const absolute = path4.join(dir, entry.name);
|
|
7299
|
-
if (entry.isDirectory()) {
|
|
7300
|
-
queue.push(absolute);
|
|
7301
|
-
} else if (entry.isFile()) {
|
|
7302
|
-
files.push(absolute);
|
|
7303
|
-
}
|
|
7304
|
-
}
|
|
7305
|
-
}
|
|
7306
|
-
return files;
|
|
7307
|
-
}
|
|
7308
|
-
async function buildSkillSyncFiles(availableSkills) {
|
|
7309
|
-
const filesToWrite = [];
|
|
7310
|
-
const index = {
|
|
7311
|
-
skills: []
|
|
7312
|
-
};
|
|
7313
|
-
for (const skill of availableSkills) {
|
|
7314
|
-
const skillFiles = await listFilesRecursive(skill.skillPath);
|
|
7315
|
-
for (const absoluteFile of skillFiles) {
|
|
7316
|
-
const relative = toPosixRelative(skill.skillPath, absoluteFile);
|
|
7317
|
-
if (!relative || relative.startsWith("..")) {
|
|
7318
|
-
continue;
|
|
7319
|
-
}
|
|
7320
|
-
filesToWrite.push({
|
|
7321
|
-
path: `${sandboxSkillDir(skill.name)}/${relative}`,
|
|
7322
|
-
content: await fs3.readFile(absoluteFile)
|
|
7323
|
-
});
|
|
7324
|
-
}
|
|
7325
|
-
index.skills.push({
|
|
7326
|
-
name: skill.name,
|
|
7327
|
-
description: skill.description,
|
|
7328
|
-
root: sandboxSkillDir(skill.name)
|
|
7329
|
-
});
|
|
7330
|
-
}
|
|
7331
|
-
filesToWrite.push({
|
|
7332
|
-
path: `${SANDBOX_SKILLS_ROOT}/index.json`,
|
|
7333
|
-
content: Buffer.from(JSON.stringify(index), "utf8")
|
|
7334
|
-
});
|
|
7335
|
-
if (process.env.EVAL_ENABLE_TEST_CREDENTIALS === "1") {
|
|
7336
|
-
filesToWrite.push({
|
|
7337
|
-
path: EVAL_GH_STUB_PATH,
|
|
7338
|
-
content: Buffer.from(buildEvalGitHubCliStub(), "utf8")
|
|
7339
|
-
});
|
|
7340
|
-
}
|
|
7341
|
-
return filesToWrite;
|
|
7342
|
-
}
|
|
7337
|
+
// src/chat/sandbox/eval-gh-stub.ts
|
|
7343
7338
|
function buildEvalGitHubCliStub() {
|
|
7344
7339
|
return `#!/usr/bin/env node
|
|
7345
7340
|
const fs = require("node:fs");
|
|
@@ -7517,6 +7512,8 @@ if (args[0] === "api") {
|
|
|
7517
7512
|
outputJson({ items: [] });
|
|
7518
7513
|
process.exit(0);
|
|
7519
7514
|
}
|
|
7515
|
+
outputJson({});
|
|
7516
|
+
process.exit(0);
|
|
7520
7517
|
}
|
|
7521
7518
|
|
|
7522
7519
|
if (args[0] === "issue") {
|
|
@@ -7558,7 +7555,9 @@ if (args[0] === "issue") {
|
|
|
7558
7555
|
|
|
7559
7556
|
const number = Number.parseInt(positionals[2] || "", 10);
|
|
7560
7557
|
const key = repo + "#" + number;
|
|
7561
|
-
const record =
|
|
7558
|
+
const record =
|
|
7559
|
+
state.issues[key] ||
|
|
7560
|
+
defaultIssue(repo, Number.isFinite(number) ? number : 101);
|
|
7562
7561
|
|
|
7563
7562
|
if (subcommand === "view") {
|
|
7564
7563
|
const jsonFields = getFlag("--json");
|
|
@@ -7599,156 +7598,315 @@ if (args[0] === "issue") {
|
|
|
7599
7598
|
fallbackToRealGh();
|
|
7600
7599
|
`;
|
|
7601
7600
|
}
|
|
7602
|
-
|
|
7603
|
-
|
|
7604
|
-
|
|
7605
|
-
|
|
7606
|
-
|
|
7607
|
-
|
|
7608
|
-
|
|
7609
|
-
|
|
7610
|
-
|
|
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
|
+
}
|
|
7611
7620
|
}
|
|
7612
7621
|
}
|
|
7613
|
-
return
|
|
7614
|
-
(directory) => directory === SANDBOX_WORKSPACE_ROOT || directory.startsWith(`${SANDBOX_WORKSPACE_ROOT}/`)
|
|
7615
|
-
).sort((a, b) => a.length - b.length);
|
|
7616
|
-
}
|
|
7617
|
-
function getSandboxErrorDetails(error) {
|
|
7618
|
-
return extractHttpErrorDetails(error, {
|
|
7619
|
-
attributePrefix: "app.sandbox.api_error",
|
|
7620
|
-
extraFields: [...SANDBOX_ERROR_FIELDS]
|
|
7621
|
-
});
|
|
7622
|
-
}
|
|
7623
|
-
function sleep2(ms) {
|
|
7624
|
-
return new Promise((resolve) => {
|
|
7625
|
-
setTimeout(resolve, ms);
|
|
7626
|
-
});
|
|
7627
|
-
}
|
|
7628
|
-
function isAlreadyExistsError(error) {
|
|
7629
|
-
const details = getSandboxErrorDetails(error);
|
|
7630
|
-
return details.searchableText.includes("already exists") || details.searchableText.includes("file exists") || details.searchableText.includes("eexist");
|
|
7622
|
+
return files;
|
|
7631
7623
|
}
|
|
7632
|
-
function
|
|
7633
|
-
const
|
|
7634
|
-
|
|
7635
|
-
|
|
7636
|
-
|
|
7637
|
-
|
|
7638
|
-
|
|
7639
|
-
|
|
7640
|
-
|
|
7641
|
-
|
|
7642
|
-
|
|
7643
|
-
|
|
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
|
+
});
|
|
7644
7640
|
}
|
|
7641
|
+
index.skills.push({
|
|
7642
|
+
name: skill.name,
|
|
7643
|
+
description: skill.description,
|
|
7644
|
+
root: sandboxSkillDir(skill.name)
|
|
7645
|
+
});
|
|
7645
7646
|
}
|
|
7646
|
-
|
|
7647
|
-
}
|
|
7648
|
-
|
|
7649
|
-
return findInErrorChain(error, (candidate) => {
|
|
7650
|
-
const details = getSandboxErrorDetails(candidate);
|
|
7651
|
-
const searchable = `${details.searchableText} ${details.summary}`.toLowerCase();
|
|
7652
|
-
return searchable.includes("sandbox_stopped") || searchable.includes("status=410") || searchable.includes("status code 410") || searchable.includes("no longer available");
|
|
7653
|
-
});
|
|
7654
|
-
}
|
|
7655
|
-
function isSnapshottingError(error) {
|
|
7656
|
-
return findInErrorChain(error, (candidate) => {
|
|
7657
|
-
const details = getSandboxErrorDetails(candidate);
|
|
7658
|
-
const searchable = `${details.searchableText} ${details.summary}`.toLowerCase();
|
|
7659
|
-
return searchable.includes("sandbox_snapshotting") || searchable.includes("creating a snapshot") || searchable.includes("stopped shortly");
|
|
7647
|
+
filesToWrite.push({
|
|
7648
|
+
path: `${SANDBOX_SKILLS_ROOT}/index.json`,
|
|
7649
|
+
content: Buffer.from(JSON.stringify(index), "utf8")
|
|
7660
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;
|
|
7661
7658
|
}
|
|
7662
|
-
function
|
|
7663
|
-
const
|
|
7664
|
-
|
|
7665
|
-
|
|
7666
|
-
|
|
7667
|
-
|
|
7668
|
-
|
|
7669
|
-
|
|
7670
|
-
|
|
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);
|
|
7671
7668
|
}
|
|
7672
|
-
seen.add(current);
|
|
7673
|
-
current = typeof current === "object" ? current.cause : void 0;
|
|
7674
7669
|
}
|
|
7675
|
-
return
|
|
7670
|
+
return Array.from(directoriesToEnsure).filter(
|
|
7671
|
+
(directory) => directory === workspaceRoot || directory.startsWith(`${workspaceRoot}/`)
|
|
7672
|
+
).sort((a, b) => a.length - b.length);
|
|
7676
7673
|
}
|
|
7677
|
-
function
|
|
7678
|
-
|
|
7679
|
-
|
|
7680
|
-
|
|
7681
|
-
|
|
7682
|
-
|
|
7683
|
-
});
|
|
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;
|
|
7684
7680
|
}
|
|
7685
|
-
|
|
7681
|
+
const relativePath = path4.posix.relative(virtualRoot, normalizedPath);
|
|
7682
|
+
if (!relativePath || relativePath.startsWith("../")) {
|
|
7683
|
+
return null;
|
|
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;
|
|
7686
7691
|
}
|
|
7687
|
-
|
|
7688
|
-
|
|
7689
|
-
|
|
7690
|
-
|
|
7691
|
-
|
|
7692
|
+
return null;
|
|
7693
|
+
}
|
|
7694
|
+
function isHostFileMissingError(error) {
|
|
7695
|
+
return Boolean(
|
|
7696
|
+
error && typeof error === "object" && error.code === "ENOENT"
|
|
7697
|
+
);
|
|
7698
|
+
}
|
|
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
|
+
);
|
|
7757
|
+
}
|
|
7758
|
+
);
|
|
7759
|
+
}
|
|
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
|
+
];
|
|
7692
7790
|
}
|
|
7693
|
-
|
|
7694
|
-
|
|
7695
|
-
|
|
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 };
|
|
7696
7799
|
}
|
|
7697
|
-
|
|
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
|
+
};
|
|
7698
7807
|
}
|
|
7699
|
-
function
|
|
7700
|
-
|
|
7701
|
-
|
|
7702
|
-
...details.attributes,
|
|
7703
|
-
...includeMissingPath ? {
|
|
7704
|
-
"app.sandbox.api_error.missing_path": details.searchableText.includes("no such file") || details.searchableText.includes("enoent")
|
|
7705
|
-
} : {},
|
|
7706
|
-
"app.sandbox.success": false
|
|
7808
|
+
function sleep2(ms) {
|
|
7809
|
+
return new Promise((resolve) => {
|
|
7810
|
+
setTimeout(resolve, ms);
|
|
7707
7811
|
});
|
|
7708
|
-
|
|
7709
|
-
|
|
7710
|
-
|
|
7711
|
-
|
|
7712
|
-
|
|
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));
|
|
7713
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
|
|
7714
7837
|
);
|
|
7838
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : 0;
|
|
7715
7839
|
}
|
|
7716
|
-
function
|
|
7840
|
+
function createSandboxSessionManager(options) {
|
|
7717
7841
|
let sandbox = null;
|
|
7718
7842
|
let sandboxIdHint = options?.sandboxId;
|
|
7719
7843
|
let availableSkills = [];
|
|
7720
7844
|
let toolExecutors;
|
|
7721
7845
|
const timeoutMs = options?.timeoutMs ?? 1e3 * 60 * 30;
|
|
7722
7846
|
const traceContext = options?.traceContext ?? {};
|
|
7723
|
-
const emitStatus = options?.onStatus;
|
|
7724
7847
|
const dependencyProfileHash = getRuntimeDependencyProfileHash(SANDBOX_RUNTIME);
|
|
7725
7848
|
const withSandboxSpan = (name, op, attributes, callback) => withSpan(name, op, traceContext, callback, attributes);
|
|
7726
|
-
const
|
|
7727
|
-
|
|
7728
|
-
|
|
7729
|
-
|
|
7730
|
-
|
|
7731
|
-
|
|
7732
|
-
|
|
7733
|
-
|
|
7734
|
-
|
|
7735
|
-
|
|
7736
|
-
|
|
7737
|
-
|
|
7738
|
-
|
|
7739
|
-
|
|
7740
|
-
|
|
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
|
+
}
|
|
7741
7903
|
}
|
|
7742
|
-
await sleep2(SNAPSHOT_BOOT_RETRY_DELAY_MS);
|
|
7743
7904
|
}
|
|
7744
|
-
|
|
7745
|
-
throw new Error(`Failed to boot sandbox from snapshot ${snapshotId}`);
|
|
7905
|
+
);
|
|
7746
7906
|
};
|
|
7747
7907
|
const invalidateSandboxInstance = async (targetSandbox, reason) => {
|
|
7748
7908
|
if (sandbox === targetSandbox) {
|
|
7749
|
-
|
|
7750
|
-
sandboxIdHint = void 0;
|
|
7751
|
-
toolExecutors = void 0;
|
|
7909
|
+
clearSession();
|
|
7752
7910
|
}
|
|
7753
7911
|
logWarn(
|
|
7754
7912
|
"sandbox_network_policy_restore_failed",
|
|
@@ -7763,290 +7921,304 @@ function createSandboxExecutor(options) {
|
|
|
7763
7921
|
} catch {
|
|
7764
7922
|
}
|
|
7765
7923
|
};
|
|
7766
|
-
const
|
|
7767
|
-
|
|
7768
|
-
"sandbox.
|
|
7769
|
-
"sandbox.
|
|
7770
|
-
|
|
7771
|
-
|
|
7772
|
-
|
|
7773
|
-
|
|
7774
|
-
|
|
7775
|
-
|
|
7776
|
-
|
|
7777
|
-
|
|
7778
|
-
|
|
7779
|
-
|
|
7780
|
-
|
|
7781
|
-
|
|
7782
|
-
|
|
7783
|
-
|
|
7784
|
-
|
|
7785
|
-
"
|
|
7786
|
-
|
|
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
|
|
7787
7951
|
},
|
|
7788
|
-
|
|
7789
|
-
|
|
7790
|
-
|
|
7791
|
-
|
|
7792
|
-
|
|
7793
|
-
|
|
7794
|
-
|
|
7795
|
-
throw error;
|
|
7796
|
-
}
|
|
7797
|
-
}
|
|
7798
|
-
}
|
|
7799
|
-
await targetSandbox.writeFiles(filesToWrite);
|
|
7800
|
-
const executableFiles = filesToWrite.map((file) => file.path).filter(
|
|
7801
|
-
(filePath) => filePath.startsWith(`${SANDBOX_RUNTIME_BIN_DIR}/`)
|
|
7802
|
-
);
|
|
7803
|
-
for (const filePath of executableFiles) {
|
|
7804
|
-
const chmod = await runNonInteractiveCommand(targetSandbox, {
|
|
7805
|
-
cmd: "chmod",
|
|
7806
|
-
args: ["0755", filePath],
|
|
7807
|
-
cwd: SANDBOX_WORKSPACE_ROOT
|
|
7808
|
-
});
|
|
7809
|
-
if (chmod.exitCode !== 0) {
|
|
7810
|
-
throw new Error(
|
|
7811
|
-
`sandbox chmod failed for ${filePath}: ${await chmod.stderr() || await chmod.stdout() || `exit ${chmod.exitCode}`}`
|
|
7812
|
-
);
|
|
7813
|
-
}
|
|
7814
|
-
}
|
|
7815
|
-
} catch (error) {
|
|
7816
|
-
throwSandboxOperationError("sandbox writeFiles", error, true);
|
|
7817
|
-
}
|
|
7818
|
-
}
|
|
7819
|
-
);
|
|
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);
|
|
7820
7959
|
}
|
|
7821
|
-
|
|
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
|
+
}
|
|
7822
8125
|
};
|
|
7823
8126
|
const acquireSandbox = async () => {
|
|
7824
|
-
return withSandboxSpan(
|
|
8127
|
+
return await withSandboxSpan(
|
|
7825
8128
|
"sandbox.acquire",
|
|
7826
8129
|
"sandbox.acquire",
|
|
7827
8130
|
{
|
|
7828
8131
|
"app.sandbox.id_hint_present": Boolean(sandboxIdHint),
|
|
7829
8132
|
"app.sandbox.timeout_ms": timeoutMs,
|
|
7830
|
-
"app.sandbox.runtime":
|
|
8133
|
+
"app.sandbox.runtime": SANDBOX_RUNTIME,
|
|
7831
8134
|
"app.sandbox.skills_count": availableSkills.length
|
|
7832
8135
|
},
|
|
7833
8136
|
async () => {
|
|
7834
|
-
|
|
7835
|
-
const
|
|
7836
|
-
|
|
7837
|
-
|
|
7838
|
-
toolExecutors = void 0;
|
|
7839
|
-
return nextSandbox;
|
|
7840
|
-
};
|
|
7841
|
-
const handleSetupFailure = (error) => {
|
|
7842
|
-
throw wrapSandboxSetupError(error);
|
|
7843
|
-
};
|
|
7844
|
-
const createFreshSandbox = async () => {
|
|
7845
|
-
const runtime = SANDBOX_RUNTIME;
|
|
7846
|
-
let statusCount = 0;
|
|
7847
|
-
const sentStatuses = /* @__PURE__ */ new Set();
|
|
7848
|
-
const emitSandboxStatus = async (status) => {
|
|
7849
|
-
const statusKey = `${status.kind}:${status.context ?? ""}`;
|
|
7850
|
-
if (!emitStatus || statusCount >= 4 || sentStatuses.has(statusKey)) {
|
|
7851
|
-
return;
|
|
7852
|
-
}
|
|
7853
|
-
sentStatuses.add(statusKey);
|
|
7854
|
-
statusCount += 1;
|
|
7855
|
-
await emitStatus(status);
|
|
7856
|
-
};
|
|
7857
|
-
const reportSnapshotPhase = async (phase) => {
|
|
7858
|
-
if (phase === "resolve_start") {
|
|
7859
|
-
await emitSandboxStatus(
|
|
7860
|
-
makeAssistantStatus("loading", "sandbox snapshot cache")
|
|
7861
|
-
);
|
|
7862
|
-
return;
|
|
7863
|
-
}
|
|
7864
|
-
if (phase === "waiting_for_lock") {
|
|
7865
|
-
await emitSandboxStatus(
|
|
7866
|
-
makeAssistantStatus("loading", "sandbox snapshot build")
|
|
7867
|
-
);
|
|
7868
|
-
return;
|
|
7869
|
-
}
|
|
7870
|
-
if (phase === "building_snapshot") {
|
|
7871
|
-
await emitSandboxStatus(
|
|
7872
|
-
makeAssistantStatus("creating", "sandbox snapshot")
|
|
7873
|
-
);
|
|
7874
|
-
return;
|
|
7875
|
-
}
|
|
7876
|
-
if (phase === "cache_hit") {
|
|
7877
|
-
await emitSandboxStatus(
|
|
7878
|
-
makeAssistantStatus("loading", "sandbox snapshot")
|
|
7879
|
-
);
|
|
7880
|
-
}
|
|
7881
|
-
};
|
|
7882
|
-
let createdSandbox;
|
|
7883
|
-
try {
|
|
7884
|
-
createdSandbox = await withSandboxSpan(
|
|
7885
|
-
"sandbox.create",
|
|
7886
|
-
"sandbox.create",
|
|
7887
|
-
{
|
|
7888
|
-
"app.sandbox.reused": false,
|
|
7889
|
-
"app.sandbox.timeout_ms": timeoutMs,
|
|
7890
|
-
"app.sandbox.runtime": runtime
|
|
7891
|
-
},
|
|
7892
|
-
async () => {
|
|
7893
|
-
await emitSandboxStatus(
|
|
7894
|
-
makeAssistantStatus("loading", "sandbox runtime")
|
|
7895
|
-
);
|
|
7896
|
-
const snapshot = await resolveRuntimeDependencySnapshot({
|
|
7897
|
-
runtime,
|
|
7898
|
-
timeoutMs,
|
|
7899
|
-
onProgress: reportSnapshotPhase
|
|
7900
|
-
});
|
|
7901
|
-
setSpanAttributes({
|
|
7902
|
-
"app.sandbox.source": snapshot.snapshotId ? "snapshot" : "created",
|
|
7903
|
-
"app.sandbox.snapshot.cache_hit": snapshot.cacheHit,
|
|
7904
|
-
"app.sandbox.snapshot.resolve_outcome": snapshot.resolveOutcome,
|
|
7905
|
-
...snapshot.profileHash ? {
|
|
7906
|
-
"app.sandbox.snapshot.profile_hash": snapshot.profileHash
|
|
7907
|
-
} : {},
|
|
7908
|
-
"app.sandbox.snapshot.dependency_count": snapshot.dependencyCount,
|
|
7909
|
-
...snapshot.rebuildReason ? {
|
|
7910
|
-
"app.sandbox.snapshot.rebuild_reason": snapshot.rebuildReason
|
|
7911
|
-
} : {}
|
|
7912
|
-
});
|
|
7913
|
-
if (!snapshot.snapshotId) {
|
|
7914
|
-
await emitSandboxStatus(
|
|
7915
|
-
makeAssistantStatus("loading", "sandbox")
|
|
7916
|
-
);
|
|
7917
|
-
return await Sandbox.create({
|
|
7918
|
-
timeout: timeoutMs,
|
|
7919
|
-
runtime,
|
|
7920
|
-
...sandboxCredentials ?? {}
|
|
7921
|
-
});
|
|
7922
|
-
}
|
|
7923
|
-
try {
|
|
7924
|
-
return await createSandboxFromSnapshot(
|
|
7925
|
-
snapshot.snapshotId,
|
|
7926
|
-
sandboxCredentials,
|
|
7927
|
-
emitSandboxStatus
|
|
7928
|
-
);
|
|
7929
|
-
} catch (error) {
|
|
7930
|
-
if (!isSnapshotMissingError(error)) {
|
|
7931
|
-
throw error;
|
|
7932
|
-
}
|
|
7933
|
-
setSpanAttributes({
|
|
7934
|
-
"app.sandbox.snapshot.rebuild_after_missing": true
|
|
7935
|
-
});
|
|
7936
|
-
const rebuiltSnapshot = await resolveRuntimeDependencySnapshot({
|
|
7937
|
-
runtime,
|
|
7938
|
-
timeoutMs,
|
|
7939
|
-
forceRebuild: true,
|
|
7940
|
-
staleSnapshotId: snapshot.snapshotId,
|
|
7941
|
-
onProgress: reportSnapshotPhase
|
|
7942
|
-
});
|
|
7943
|
-
if (!rebuiltSnapshot.snapshotId) {
|
|
7944
|
-
throw error;
|
|
7945
|
-
}
|
|
7946
|
-
return await createSandboxFromSnapshot(
|
|
7947
|
-
rebuiltSnapshot.snapshotId,
|
|
7948
|
-
sandboxCredentials,
|
|
7949
|
-
emitSandboxStatus
|
|
7950
|
-
);
|
|
7951
|
-
}
|
|
7952
|
-
}
|
|
7953
|
-
);
|
|
7954
|
-
} catch (error) {
|
|
7955
|
-
return handleSetupFailure(error);
|
|
7956
|
-
}
|
|
7957
|
-
try {
|
|
7958
|
-
await upsertSkillsToSandbox(createdSandbox);
|
|
7959
|
-
} catch (error) {
|
|
7960
|
-
return handleSetupFailure(error);
|
|
7961
|
-
}
|
|
7962
|
-
return assignSandbox(createdSandbox);
|
|
7963
|
-
};
|
|
7964
|
-
if (!sandbox && sandboxIdHint && dependencyProfileHash !== options?.sandboxDependencyProfileHash) {
|
|
7965
|
-
setSpanAttributes({
|
|
7966
|
-
"app.sandbox.reused": false,
|
|
7967
|
-
"app.sandbox.recreate.reason": "dependency_profile_mismatch",
|
|
7968
|
-
...options?.sandboxDependencyProfileHash ? {
|
|
7969
|
-
"app.sandbox.previous_profile_hash": options.sandboxDependencyProfileHash
|
|
7970
|
-
} : {},
|
|
7971
|
-
...dependencyProfileHash ? { "app.sandbox.current_profile_hash": dependencyProfileHash } : {}
|
|
7972
|
-
});
|
|
7973
|
-
sandboxIdHint = void 0;
|
|
7974
|
-
}
|
|
7975
|
-
const recoverUnavailableSandbox = async (source) => {
|
|
7976
|
-
setSpanAttributes({
|
|
7977
|
-
"app.sandbox.recovery.attempted": true,
|
|
7978
|
-
"app.sandbox.recovery.source": source
|
|
7979
|
-
});
|
|
7980
|
-
sandbox = null;
|
|
7981
|
-
sandboxIdHint = void 0;
|
|
7982
|
-
toolExecutors = void 0;
|
|
7983
|
-
const replacement = await createFreshSandbox();
|
|
7984
|
-
setSpanAttributes({
|
|
7985
|
-
"app.sandbox.recovery.succeeded": true
|
|
7986
|
-
});
|
|
7987
|
-
return replacement;
|
|
7988
|
-
};
|
|
7989
|
-
if (sandbox) {
|
|
7990
|
-
const cachedSandbox = sandbox;
|
|
7991
|
-
try {
|
|
7992
|
-
await withSandboxSpan(
|
|
7993
|
-
"sandbox.reuse_cached",
|
|
7994
|
-
"sandbox.acquire.cached",
|
|
7995
|
-
{
|
|
7996
|
-
"app.sandbox.reused": true,
|
|
7997
|
-
"app.sandbox.source": "memory"
|
|
7998
|
-
},
|
|
7999
|
-
async () => {
|
|
8000
|
-
await upsertSkillsToSandbox(cachedSandbox);
|
|
8001
|
-
}
|
|
8002
|
-
);
|
|
8003
|
-
return cachedSandbox;
|
|
8004
|
-
} catch (error) {
|
|
8005
|
-
if (isSandboxUnavailableError(error)) {
|
|
8006
|
-
return recoverUnavailableSandbox("memory");
|
|
8007
|
-
}
|
|
8008
|
-
return handleSetupFailure(error);
|
|
8009
|
-
}
|
|
8137
|
+
discardHintIfProfileChanged();
|
|
8138
|
+
const cachedSandbox = await tryReuseCachedSandbox();
|
|
8139
|
+
if (cachedSandbox) {
|
|
8140
|
+
return cachedSandbox;
|
|
8010
8141
|
}
|
|
8011
|
-
|
|
8012
|
-
if (
|
|
8013
|
-
|
|
8014
|
-
acquiredSandbox = await withSandboxSpan(
|
|
8015
|
-
"sandbox.get",
|
|
8016
|
-
"sandbox.get",
|
|
8017
|
-
{
|
|
8018
|
-
"app.sandbox.reused": true,
|
|
8019
|
-
"app.sandbox.source": "id_hint"
|
|
8020
|
-
},
|
|
8021
|
-
async () => Sandbox.get({
|
|
8022
|
-
sandboxId: sandboxIdHint,
|
|
8023
|
-
...sandboxCredentials ?? {}
|
|
8024
|
-
})
|
|
8025
|
-
);
|
|
8026
|
-
} catch {
|
|
8027
|
-
acquiredSandbox = null;
|
|
8028
|
-
}
|
|
8142
|
+
const hintedSandbox = await tryRestoreHintedSandbox();
|
|
8143
|
+
if (hintedSandbox) {
|
|
8144
|
+
return hintedSandbox;
|
|
8029
8145
|
}
|
|
8030
|
-
|
|
8031
|
-
try {
|
|
8032
|
-
await upsertSkillsToSandbox(acquiredSandbox);
|
|
8033
|
-
return assignSandbox(acquiredSandbox);
|
|
8034
|
-
} catch (error) {
|
|
8035
|
-
if (isSandboxUnavailableError(error)) {
|
|
8036
|
-
return recoverUnavailableSandbox("id_hint");
|
|
8037
|
-
}
|
|
8038
|
-
return handleSetupFailure(error);
|
|
8039
|
-
}
|
|
8040
|
-
}
|
|
8041
|
-
return createFreshSandbox();
|
|
8146
|
+
return await createFreshSandbox();
|
|
8042
8147
|
}
|
|
8043
8148
|
);
|
|
8044
8149
|
};
|
|
8045
|
-
const
|
|
8046
|
-
|
|
8047
|
-
|
|
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
|
+
}
|
|
8048
8196
|
}
|
|
8049
|
-
|
|
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) => {
|
|
8050
8222
|
const toolkit = await withSandboxSpan(
|
|
8051
8223
|
"sandbox.bash_tool.init",
|
|
8052
8224
|
"sandbox.tool.init",
|
|
@@ -8054,8 +8226,8 @@ function createSandboxExecutor(options) {
|
|
|
8054
8226
|
"app.sandbox.tool_name": "bash",
|
|
8055
8227
|
"app.sandbox.destination": SANDBOX_WORKSPACE_ROOT
|
|
8056
8228
|
},
|
|
8057
|
-
async () => createBashTool2({
|
|
8058
|
-
sandbox:
|
|
8229
|
+
async () => await createBashTool2({
|
|
8230
|
+
sandbox: sandboxInstance,
|
|
8059
8231
|
destination: SANDBOX_WORKSPACE_ROOT
|
|
8060
8232
|
})
|
|
8061
8233
|
);
|
|
@@ -8064,74 +8236,294 @@ function createSandboxExecutor(options) {
|
|
|
8064
8236
|
if (!executeReadFile || !executeWriteFile) {
|
|
8065
8237
|
throw new Error("bash-tool did not return executable tool handlers");
|
|
8066
8238
|
}
|
|
8067
|
-
|
|
8239
|
+
return {
|
|
8068
8240
|
bash: async (input) => {
|
|
8069
|
-
const restoreNetworkPolicy = activeSandbox.networkPolicy ?? "allow-all";
|
|
8070
|
-
const headerTransforms = input.headerTransforms;
|
|
8071
|
-
if (headerTransforms && headerTransforms.length > 0) {
|
|
8072
|
-
const policy = mergeNetworkPolicyWithHeaderTransforms(
|
|
8073
|
-
restoreNetworkPolicy,
|
|
8074
|
-
headerTransforms
|
|
8075
|
-
);
|
|
8076
|
-
await activeSandbox.updateNetworkPolicy(policy);
|
|
8077
|
-
}
|
|
8078
8241
|
const script = buildNonInteractiveShellScript(input.command, {
|
|
8079
8242
|
env: input.env,
|
|
8080
8243
|
pathPrefix: `${SANDBOX_RUNTIME_BIN_DIR}:$PATH`
|
|
8081
8244
|
});
|
|
8082
|
-
|
|
8083
|
-
|
|
8084
|
-
|
|
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);
|
|
8255
|
+
}
|
|
8256
|
+
);
|
|
8257
|
+
},
|
|
8258
|
+
readFile: async (input) => await executeReadFile(input, {
|
|
8259
|
+
toolCallId: "sandbox-read-file",
|
|
8260
|
+
messages: []
|
|
8261
|
+
}),
|
|
8262
|
+
writeFile: async (input) => await executeWriteFile(input, {
|
|
8263
|
+
toolCallId: "sandbox-write-file",
|
|
8264
|
+
messages: []
|
|
8265
|
+
})
|
|
8266
|
+
};
|
|
8267
|
+
};
|
|
8268
|
+
const ensureReadySandbox = async () => {
|
|
8269
|
+
const activeSandbox = await acquireSandbox();
|
|
8270
|
+
await extendKeepAlive(activeSandbox);
|
|
8271
|
+
return activeSandbox;
|
|
8272
|
+
};
|
|
8273
|
+
const loadToolExecutors = async (activeSandbox) => {
|
|
8274
|
+
if (toolExecutors) {
|
|
8275
|
+
return toolExecutors;
|
|
8276
|
+
}
|
|
8277
|
+
toolExecutors = await buildToolExecutors(activeSandbox);
|
|
8278
|
+
return toolExecutors;
|
|
8279
|
+
};
|
|
8280
|
+
return {
|
|
8281
|
+
configureSkills(skills) {
|
|
8282
|
+
availableSkills = [...skills];
|
|
8283
|
+
},
|
|
8284
|
+
getSandboxId() {
|
|
8285
|
+
return sandbox?.sandboxId ?? sandboxIdHint;
|
|
8286
|
+
},
|
|
8287
|
+
getDependencyProfileHash() {
|
|
8288
|
+
return dependencyProfileHash;
|
|
8289
|
+
},
|
|
8290
|
+
async createSandbox() {
|
|
8291
|
+
return await acquireSandbox();
|
|
8292
|
+
},
|
|
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
|
+
}
|
|
8314
|
+
};
|
|
8315
|
+
}
|
|
8316
|
+
|
|
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)) {
|
|
8321
|
+
return void 0;
|
|
8322
|
+
}
|
|
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
|
+
);
|
|
8333
|
+
}
|
|
8334
|
+
function parseEnv(raw) {
|
|
8335
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
8336
|
+
return void 0;
|
|
8337
|
+
}
|
|
8338
|
+
return Object.fromEntries(
|
|
8339
|
+
Object.entries(raw).filter(([, value]) => typeof value === "string").map(([key, value]) => [key, value])
|
|
8340
|
+
);
|
|
8341
|
+
}
|
|
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);
|
|
8350
|
+
}
|
|
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
|
+
}
|
|
8419
|
+
}
|
|
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 () => {
|
|
8085
8512
|
try {
|
|
8086
|
-
|
|
8087
|
-
cmd: "bash",
|
|
8088
|
-
args: ["-c", script],
|
|
8089
|
-
cwd: SANDBOX_WORKSPACE_ROOT
|
|
8090
|
-
});
|
|
8091
|
-
const maxOutputLength = Number.parseInt(
|
|
8092
|
-
process.env.SANDBOX_BASH_MAX_OUTPUT_CHARS ?? "",
|
|
8093
|
-
10
|
|
8094
|
-
);
|
|
8095
|
-
const boundedOutputLength = Number.isFinite(maxOutputLength) && maxOutputLength > 0 ? maxOutputLength : DEFAULT_MAX_OUTPUT_LENGTH;
|
|
8096
|
-
const stdoutRaw = await commandResult2.stdout();
|
|
8097
|
-
const stderrRaw = await commandResult2.stderr();
|
|
8098
|
-
const stdout = truncateOutput(stdoutRaw, boundedOutputLength);
|
|
8099
|
-
const stderr = truncateOutput(stderrRaw, boundedOutputLength);
|
|
8100
|
-
result = {
|
|
8101
|
-
stdout: stdout.value,
|
|
8102
|
-
stderr: stderr.value,
|
|
8103
|
-
exitCode: commandResult2.exitCode,
|
|
8104
|
-
stdoutTruncated: stdout.truncated,
|
|
8105
|
-
stderrTruncated: stderr.truncated
|
|
8106
|
-
};
|
|
8513
|
+
await executeWriteFile({ path: filePath, content });
|
|
8107
8514
|
} catch (error) {
|
|
8108
|
-
|
|
8109
|
-
throw error;
|
|
8110
|
-
} finally {
|
|
8111
|
-
if (headerTransforms && headerTransforms.length > 0) {
|
|
8112
|
-
try {
|
|
8113
|
-
await activeSandbox.updateNetworkPolicy(restoreNetworkPolicy);
|
|
8114
|
-
} catch (error) {
|
|
8115
|
-
restoreError = error;
|
|
8116
|
-
await invalidateSandboxInstance(activeSandbox, error);
|
|
8117
|
-
}
|
|
8118
|
-
}
|
|
8119
|
-
}
|
|
8120
|
-
if (restoreError && !commandError) {
|
|
8121
|
-
throw restoreError;
|
|
8515
|
+
throwSandboxOperationError("sandbox writeFile", error);
|
|
8122
8516
|
}
|
|
8123
|
-
|
|
8124
|
-
}
|
|
8125
|
-
|
|
8126
|
-
|
|
8127
|
-
|
|
8128
|
-
|
|
8129
|
-
|
|
8130
|
-
|
|
8131
|
-
|
|
8132
|
-
})
|
|
8517
|
+
setSpanStatus("ok");
|
|
8518
|
+
}
|
|
8519
|
+
);
|
|
8520
|
+
return {
|
|
8521
|
+
result: {
|
|
8522
|
+
ok: true,
|
|
8523
|
+
path: filePath,
|
|
8524
|
+
bytes_written: Buffer.byteLength(content, "utf8")
|
|
8525
|
+
}
|
|
8133
8526
|
};
|
|
8134
|
-
return toolExecutors;
|
|
8135
8527
|
};
|
|
8136
8528
|
const execute = async (params) => {
|
|
8137
8529
|
const rawInput = params.input ?? {};
|
|
@@ -8146,195 +8538,37 @@ function createSandboxExecutor(options) {
|
|
|
8146
8538
|
return { result: custom.result };
|
|
8147
8539
|
}
|
|
8148
8540
|
}
|
|
8149
|
-
|
|
8150
|
-
const activeSandbox = await acquireSandbox();
|
|
8151
|
-
const keepAliveMs = Number.parseInt(
|
|
8152
|
-
process.env.VERCEL_SANDBOX_KEEPALIVE_MS ?? "0",
|
|
8153
|
-
10
|
|
8154
|
-
);
|
|
8155
|
-
if (Number.isFinite(keepAliveMs) && keepAliveMs > 0) {
|
|
8156
|
-
try {
|
|
8157
|
-
await withSandboxSpan(
|
|
8158
|
-
"sandbox.keepalive.extend",
|
|
8159
|
-
"sandbox.keepalive",
|
|
8160
|
-
{
|
|
8161
|
-
"app.sandbox.keepalive_ms": keepAliveMs
|
|
8162
|
-
},
|
|
8163
|
-
async () => {
|
|
8164
|
-
await activeSandbox.extendTimeout(keepAliveMs);
|
|
8165
|
-
}
|
|
8166
|
-
);
|
|
8167
|
-
} catch {
|
|
8168
|
-
}
|
|
8169
|
-
}
|
|
8170
|
-
if (params.toolName === "bash") {
|
|
8171
|
-
const command = bashCommand;
|
|
8172
|
-
const headerTransformsInput = rawInput.headerTransforms;
|
|
8173
|
-
const headerTransforms = Array.isArray(headerTransformsInput) ? headerTransformsInput.filter(
|
|
8174
|
-
(value) => Boolean(value && typeof value === "object")
|
|
8175
|
-
).map((transform) => ({
|
|
8176
|
-
domain: String(transform.domain ?? "").trim(),
|
|
8177
|
-
headers: transform.headers && typeof transform.headers === "object" && !Array.isArray(transform.headers) ? Object.fromEntries(
|
|
8178
|
-
Object.entries(
|
|
8179
|
-
transform.headers
|
|
8180
|
-
).filter(([, value]) => typeof value === "string").map(([key, value]) => [key, value])
|
|
8181
|
-
) : {}
|
|
8182
|
-
})).filter(
|
|
8183
|
-
(transform) => transform.domain.length > 0 && Object.keys(transform.headers).length > 0
|
|
8184
|
-
) : void 0;
|
|
8185
|
-
const envInput = rawInput.env;
|
|
8186
|
-
const env = envInput && typeof envInput === "object" && !Array.isArray(envInput) ? Object.fromEntries(
|
|
8187
|
-
Object.entries(envInput).filter(([, value]) => typeof value === "string").map(([key, value]) => [key, value])
|
|
8188
|
-
) : void 0;
|
|
8189
|
-
const executeBash = (await getToolExecutors()).bash;
|
|
8190
|
-
const result = await withSandboxSpan(
|
|
8191
|
-
"bash",
|
|
8192
|
-
"process.exec",
|
|
8193
|
-
{
|
|
8194
|
-
"process.executable.name": "bash"
|
|
8195
|
-
},
|
|
8196
|
-
async () => {
|
|
8197
|
-
try {
|
|
8198
|
-
const response = await executeBash({
|
|
8199
|
-
command,
|
|
8200
|
-
...headerTransforms ? { headerTransforms } : {},
|
|
8201
|
-
...env ? { env } : {}
|
|
8202
|
-
});
|
|
8203
|
-
setSpanAttributes({
|
|
8204
|
-
"process.exit.code": response.exitCode,
|
|
8205
|
-
"app.sandbox.stdout_bytes": Buffer.byteLength(
|
|
8206
|
-
response.stdout ?? "",
|
|
8207
|
-
"utf8"
|
|
8208
|
-
),
|
|
8209
|
-
"app.sandbox.stderr_bytes": Buffer.byteLength(
|
|
8210
|
-
response.stderr ?? "",
|
|
8211
|
-
"utf8"
|
|
8212
|
-
),
|
|
8213
|
-
...response.exitCode !== 0 ? { "error.type": "nonzero_exit" } : {}
|
|
8214
|
-
});
|
|
8215
|
-
setSpanStatus(response.exitCode === 0 ? "ok" : "error");
|
|
8216
|
-
return response;
|
|
8217
|
-
} catch (error) {
|
|
8218
|
-
setSpanAttributes({
|
|
8219
|
-
"error.type": error instanceof Error ? error.name : "sandbox_execute_error"
|
|
8220
|
-
});
|
|
8221
|
-
setSpanStatus("error");
|
|
8222
|
-
throw error;
|
|
8223
|
-
}
|
|
8224
|
-
}
|
|
8225
|
-
);
|
|
8226
|
-
return {
|
|
8227
|
-
result: {
|
|
8228
|
-
ok: result.exitCode === 0,
|
|
8229
|
-
command,
|
|
8230
|
-
cwd: SANDBOX_WORKSPACE_ROOT,
|
|
8231
|
-
exit_code: result.exitCode,
|
|
8232
|
-
signal: null,
|
|
8233
|
-
timed_out: false,
|
|
8234
|
-
stdout: result.stdout,
|
|
8235
|
-
stderr: result.stderr,
|
|
8236
|
-
stdout_truncated: result.stdoutTruncated,
|
|
8237
|
-
stderr_truncated: result.stderrTruncated
|
|
8238
|
-
}
|
|
8239
|
-
};
|
|
8541
|
+
return await executeBashTool(rawInput, bashCommand);
|
|
8240
8542
|
}
|
|
8241
8543
|
if (params.toolName === "readFile") {
|
|
8242
|
-
|
|
8243
|
-
if (!filePath) {
|
|
8244
|
-
throw new Error("path is required");
|
|
8245
|
-
}
|
|
8246
|
-
const executeReadFile = (await getToolExecutors()).readFile;
|
|
8247
|
-
const result = await withSandboxSpan(
|
|
8248
|
-
"sandbox.readFile",
|
|
8249
|
-
"sandbox.fs.read",
|
|
8250
|
-
{
|
|
8251
|
-
"app.sandbox.path.length": filePath.length
|
|
8252
|
-
},
|
|
8253
|
-
async () => {
|
|
8254
|
-
const response = await executeReadFile({ path: filePath });
|
|
8255
|
-
const content = String(response.content ?? "");
|
|
8256
|
-
setSpanAttributes({
|
|
8257
|
-
"app.sandbox.read.bytes": Buffer.byteLength(content, "utf8"),
|
|
8258
|
-
"app.sandbox.read.chars": content.length
|
|
8259
|
-
});
|
|
8260
|
-
setSpanStatus("ok");
|
|
8261
|
-
return {
|
|
8262
|
-
content,
|
|
8263
|
-
path: filePath,
|
|
8264
|
-
success: true
|
|
8265
|
-
};
|
|
8266
|
-
}
|
|
8267
|
-
);
|
|
8268
|
-
return { result };
|
|
8544
|
+
return await executeReadFileTool(rawInput);
|
|
8269
8545
|
}
|
|
8270
8546
|
if (params.toolName === "writeFile") {
|
|
8271
|
-
|
|
8272
|
-
if (!filePath) {
|
|
8273
|
-
throw new Error("path is required");
|
|
8274
|
-
}
|
|
8275
|
-
const content = String(rawInput.content ?? "");
|
|
8276
|
-
const executeWriteFile = (await getToolExecutors()).writeFile;
|
|
8277
|
-
await withSandboxSpan(
|
|
8278
|
-
"sandbox.writeFile",
|
|
8279
|
-
"sandbox.fs.write",
|
|
8280
|
-
{
|
|
8281
|
-
"app.sandbox.path.length": filePath.length,
|
|
8282
|
-
"app.sandbox.write.bytes": Buffer.byteLength(content, "utf8")
|
|
8283
|
-
},
|
|
8284
|
-
async () => {
|
|
8285
|
-
try {
|
|
8286
|
-
await executeWriteFile({ path: filePath, content });
|
|
8287
|
-
setSpanStatus("ok");
|
|
8288
|
-
} catch (error) {
|
|
8289
|
-
throwSandboxOperationError("sandbox writeFile", error);
|
|
8290
|
-
}
|
|
8291
|
-
}
|
|
8292
|
-
);
|
|
8293
|
-
return {
|
|
8294
|
-
result: {
|
|
8295
|
-
ok: true,
|
|
8296
|
-
path: filePath,
|
|
8297
|
-
bytes_written: Buffer.byteLength(content, "utf8")
|
|
8298
|
-
}
|
|
8299
|
-
};
|
|
8547
|
+
return await executeWriteFileTool(rawInput);
|
|
8300
8548
|
}
|
|
8301
8549
|
throw new Error(`unsupported sandbox tool: ${params.toolName}`);
|
|
8302
8550
|
};
|
|
8303
|
-
const dispose = async () => {
|
|
8304
|
-
if (!sandbox) {
|
|
8305
|
-
return;
|
|
8306
|
-
}
|
|
8307
|
-
await withSandboxSpan(
|
|
8308
|
-
"sandbox.stop",
|
|
8309
|
-
"sandbox.stop",
|
|
8310
|
-
{
|
|
8311
|
-
"app.sandbox.stop.blocking": true
|
|
8312
|
-
},
|
|
8313
|
-
async () => {
|
|
8314
|
-
await sandbox.stop({ blocking: true });
|
|
8315
|
-
}
|
|
8316
|
-
);
|
|
8317
|
-
sandbox = null;
|
|
8318
|
-
toolExecutors = void 0;
|
|
8319
|
-
};
|
|
8320
8551
|
return {
|
|
8321
8552
|
configureSkills(skills) {
|
|
8322
8553
|
availableSkills = [...skills];
|
|
8554
|
+
sessionManager.configureSkills(skills);
|
|
8323
8555
|
},
|
|
8324
8556
|
getSandboxId() {
|
|
8325
|
-
return
|
|
8557
|
+
return sessionManager.getSandboxId();
|
|
8326
8558
|
},
|
|
8327
8559
|
getDependencyProfileHash() {
|
|
8328
|
-
return
|
|
8560
|
+
return sessionManager.getDependencyProfileHash();
|
|
8329
8561
|
},
|
|
8330
8562
|
canExecute(toolName) {
|
|
8331
8563
|
return SANDBOX_TOOL_NAMES.has(toolName);
|
|
8332
8564
|
},
|
|
8333
8565
|
async createSandbox() {
|
|
8334
|
-
return await
|
|
8566
|
+
return createSandboxWorkspace(await sessionManager.createSandbox());
|
|
8335
8567
|
},
|
|
8336
8568
|
execute,
|
|
8337
|
-
dispose
|
|
8569
|
+
async dispose() {
|
|
8570
|
+
await sessionManager.dispose();
|
|
8571
|
+
}
|
|
8338
8572
|
};
|
|
8339
8573
|
}
|
|
8340
8574
|
|
|
@@ -9156,6 +9390,14 @@ async function generateAssistantReply(messageText, context = {}) {
|
|
|
9156
9390
|
let lastKnownSandboxDependencyProfileHash = context.sandbox?.sandboxDependencyProfileHash;
|
|
9157
9391
|
let loadedSkillNamesForResume = [];
|
|
9158
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
|
+
};
|
|
9159
9401
|
try {
|
|
9160
9402
|
const shouldTrace = shouldEmitDevAgentTrace();
|
|
9161
9403
|
const spanContext = {
|
|
@@ -9228,7 +9470,7 @@ async function generateAssistantReply(messageText, context = {}) {
|
|
|
9228
9470
|
resolveConfiguration: async (key) => configurationValues[key]
|
|
9229
9471
|
});
|
|
9230
9472
|
const providerAuthActions = /* @__PURE__ */ new Map();
|
|
9231
|
-
|
|
9473
|
+
sandboxExecutor = createSandboxExecutor({
|
|
9232
9474
|
sandboxId: context.sandbox?.sandboxId,
|
|
9233
9475
|
sandboxDependencyProfileHash: context.sandbox?.sandboxDependencyProfileHash,
|
|
9234
9476
|
traceContext: spanContext,
|
|
@@ -9255,10 +9497,52 @@ async function generateAssistantReply(messageText, context = {}) {
|
|
|
9255
9497
|
return result.handled ? { handled: true, result: result.result } : { handled: false };
|
|
9256
9498
|
}
|
|
9257
9499
|
});
|
|
9258
|
-
|
|
9259
|
-
lastKnownSandboxDependencyProfileHash = sandboxExecutor.getDependencyProfileHash();
|
|
9500
|
+
const currentSandboxExecutor = sandboxExecutor;
|
|
9260
9501
|
sandboxExecutor.configureSkills(availableSkills);
|
|
9261
|
-
|
|
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
|
+
};
|
|
9262
9546
|
for (const skillName of existingCheckpoint?.loadedSkillNames ?? []) {
|
|
9263
9547
|
const preloaded = await skillSandbox.loadSkill(skillName);
|
|
9264
9548
|
if (preloaded) {
|
|
@@ -9397,16 +9681,32 @@ async function generateAssistantReply(messageText, context = {}) {
|
|
|
9397
9681
|
});
|
|
9398
9682
|
const userContentParts = [{ type: "text", text: userTurnText }];
|
|
9399
9683
|
for (const attachment of context.userAttachments ?? []) {
|
|
9400
|
-
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
|
+
}
|
|
9401
9693
|
userContentParts.push({
|
|
9402
9694
|
type: "image",
|
|
9403
9695
|
data: attachment.data.toString("base64"),
|
|
9404
9696
|
mimeType: attachment.mediaType
|
|
9405
9697
|
});
|
|
9406
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
|
+
};
|
|
9407
9707
|
userContentParts.push({
|
|
9408
9708
|
type: "text",
|
|
9409
|
-
text: encodeNonImageAttachmentForPrompt(
|
|
9709
|
+
text: encodeNonImageAttachmentForPrompt(promptAttachment)
|
|
9410
9710
|
});
|
|
9411
9711
|
}
|
|
9412
9712
|
}
|
|
@@ -9603,8 +9903,8 @@ async function generateAssistantReply(messageText, context = {}) {
|
|
|
9603
9903
|
replyFiles,
|
|
9604
9904
|
artifactStatePatch,
|
|
9605
9905
|
toolCalls,
|
|
9606
|
-
sandboxId:
|
|
9607
|
-
sandboxDependencyProfileHash:
|
|
9906
|
+
sandboxId: currentSandboxExecutor.getSandboxId(),
|
|
9907
|
+
sandboxDependencyProfileHash: currentSandboxExecutor.getDependencyProfileHash(),
|
|
9608
9908
|
generatedFileCount: generatedFiles.length,
|
|
9609
9909
|
hasTextDeltaCallback: Boolean(context.onTextDelta),
|
|
9610
9910
|
shouldTrace,
|
|
@@ -9655,8 +9955,7 @@ async function generateAssistantReply(messageText, context = {}) {
|
|
|
9655
9955
|
const message = error instanceof Error ? error.message : String(error);
|
|
9656
9956
|
return {
|
|
9657
9957
|
text: `Error: ${message}`,
|
|
9658
|
-
|
|
9659
|
-
sandboxDependencyProfileHash: lastKnownSandboxDependencyProfileHash,
|
|
9958
|
+
...getSandboxMetadata(),
|
|
9660
9959
|
diagnostics: {
|
|
9661
9960
|
outcome: "provider_error",
|
|
9662
9961
|
modelId: botConfig.modelId,
|
|
@@ -10260,7 +10559,7 @@ async function GET4(request, provider, waitUntil) {
|
|
|
10260
10559
|
}
|
|
10261
10560
|
|
|
10262
10561
|
// src/chat/slack/app-home.ts
|
|
10263
|
-
import
|
|
10562
|
+
import fs5 from "fs";
|
|
10264
10563
|
import path5 from "path";
|
|
10265
10564
|
var DEFAULT_ABOUT_TEXT = "I help your team investigate, summarize, and act on work in Slack.";
|
|
10266
10565
|
var MAX_HOME_SKILLS = 6;
|
|
@@ -10275,7 +10574,7 @@ function clampSectionText(text) {
|
|
|
10275
10574
|
function loadAboutText() {
|
|
10276
10575
|
const aboutPath = path5.join(homeDir(), "ABOUT.md");
|
|
10277
10576
|
try {
|
|
10278
|
-
const raw =
|
|
10577
|
+
const raw = fs5.readFileSync(aboutPath, "utf8").trim();
|
|
10279
10578
|
if (raw.length > 0) {
|
|
10280
10579
|
return clampSectionText(raw);
|
|
10281
10580
|
}
|
|
@@ -10625,7 +10924,7 @@ var replyDecisionSchema = z.object({
|
|
|
10625
10924
|
var ROUTER_CONFIDENCE_THRESHOLD = 0.8;
|
|
10626
10925
|
var LEADING_SLACK_MENTION_RE = /^\s*<@([A-Z0-9]+)(?:\|([^>]+))?>[\s,:-]*/i;
|
|
10627
10926
|
var LEADING_NAMED_MENTION_RE = /^\s*@([a-z0-9._-]+)\b[\s,:-]*/i;
|
|
10628
|
-
var TRANSCRIPT_MESSAGE_LINE_RE = /^\[(assistant|system|user)\]\s+[^:]
|
|
10927
|
+
var TRANSCRIPT_MESSAGE_LINE_RE = /^\[(assistant|system|user)\]\s+([^:]+):\s+([\s\S]+)$/i;
|
|
10629
10928
|
var THREAD_OPTOUT_PATTERNS = [
|
|
10630
10929
|
/\bstop (?:watching|replying|participating)\b/i,
|
|
10631
10930
|
/\bstay out\b/i,
|
|
@@ -10633,6 +10932,11 @@ var THREAD_OPTOUT_PATTERNS = [
|
|
|
10633
10932
|
/\bunsubscribe\b/i,
|
|
10634
10933
|
/\bleave (?:this )?thread\b/i
|
|
10635
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;
|
|
10636
10940
|
function escapeRegExp(value) {
|
|
10637
10941
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
10638
10942
|
}
|
|
@@ -10669,36 +10973,113 @@ function isThreadOptOutInstruction(rawText, text) {
|
|
|
10669
10973
|
(pattern) => pattern.test(rawText) || pattern.test(text)
|
|
10670
10974
|
);
|
|
10671
10975
|
}
|
|
10672
|
-
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) {
|
|
10673
10997
|
if (!conversationContext) {
|
|
10674
|
-
return
|
|
10675
|
-
latestPriorMessageRole: "[none]",
|
|
10676
|
-
latestPriorAssistantMessage: "[none]"
|
|
10677
|
-
};
|
|
10998
|
+
return [];
|
|
10678
10999
|
}
|
|
11000
|
+
const messages = [];
|
|
10679
11001
|
const lines = conversationContext.split("\n").map((line) => line.trim()).filter((line) => line.length > 0);
|
|
10680
|
-
|
|
10681
|
-
|
|
10682
|
-
for (let index = lines.length - 1; index >= 0; index -= 1) {
|
|
10683
|
-
const match = lines[index]?.match(TRANSCRIPT_MESSAGE_LINE_RE);
|
|
11002
|
+
for (const line of lines) {
|
|
11003
|
+
const match = line.match(TRANSCRIPT_MESSAGE_LINE_RE);
|
|
10684
11004
|
if (!match) {
|
|
10685
11005
|
continue;
|
|
10686
11006
|
}
|
|
10687
|
-
|
|
10688
|
-
|
|
10689
|
-
|
|
10690
|
-
|
|
10691
|
-
|
|
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;
|
|
10692
11026
|
}
|
|
10693
|
-
if (
|
|
11027
|
+
if (message.role === "assistant") {
|
|
11028
|
+
humanMessagesSinceLastAssistant = humanMessageCount;
|
|
10694
11029
|
break;
|
|
10695
11030
|
}
|
|
11031
|
+
humanMessageCount += 1;
|
|
10696
11032
|
}
|
|
10697
11033
|
return {
|
|
10698
|
-
|
|
10699
|
-
|
|
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
|
|
10700
11042
|
};
|
|
10701
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
|
+
}
|
|
10702
11083
|
function getSubscribedReplyPreflightDecision(args) {
|
|
10703
11084
|
const text = args.text.trim();
|
|
10704
11085
|
const rawText = args.rawText.trim();
|
|
@@ -10719,54 +11100,27 @@ function getSubscribedReplyPreflightDecision(args) {
|
|
|
10719
11100
|
reasonDetail: leadingOtherPartyAddress
|
|
10720
11101
|
};
|
|
10721
11102
|
}
|
|
10722
|
-
function buildRouterSystemPrompt(botUserName
|
|
10723
|
-
const { latestPriorMessageRole, latestPriorAssistantMessage } = getTranscriptMessageHints(conversationContext);
|
|
11103
|
+
function buildRouterSystemPrompt(botUserName) {
|
|
10724
11104
|
return [
|
|
10725
11105
|
"You are a message router for a Slack assistant named Junior in a subscribed Slack thread.",
|
|
10726
11106
|
"Decide whether Junior should reply to the latest message.",
|
|
10727
11107
|
"Subscribed threads are passive by default.",
|
|
10728
|
-
"
|
|
10729
|
-
"
|
|
10730
|
-
"",
|
|
10731
|
-
"
|
|
10732
|
-
"
|
|
10733
|
-
"",
|
|
10734
|
-
"
|
|
10735
|
-
"
|
|
10736
|
-
"
|
|
10737
|
-
"",
|
|
10738
|
-
"
|
|
10739
|
-
"- Questions between humans: 'Is that the right approach?', 'Can you check on this?', 'Did you deploy that?'",
|
|
10740
|
-
"- Acknowledgments: 'thanks', '+1', 'lgtm', 'ok cool', 'sounds good', 'nice'",
|
|
10741
|
-
"- Status updates: 'I just pushed a fix', 'Deploying now', 'Build is green'",
|
|
10742
|
-
"- General thread discussion: 'What about the billing issue?', 'I think we should revert'",
|
|
10743
|
-
"- Reactions to work: 'That looks wrong', 'Nice catch', 'Hmm interesting'",
|
|
10744
|
-
"",
|
|
10745
|
-
"Examples of messages Junior SHOULD reply to (should_reply=true):",
|
|
10746
|
-
"- Direct follow-ups to Junior's response: 'Can you explain that last point in more detail?'",
|
|
10747
|
-
"- 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?'",
|
|
10748
|
-
"- Explicit requests for Junior's help: 'Junior, what's causing this error?'",
|
|
10749
|
-
"",
|
|
10750
|
-
"Treat a message as directed at Junior when it explicitly refers to Junior's immediately previous reply",
|
|
10751
|
-
"using language like 'you just said', 'your last response', 'your last answer', or similar self-reference.",
|
|
10752
|
-
"Do not confuse that with general topic continuation. A message like 'What about the billing worker timeline?'",
|
|
10753
|
-
"still should_reply=false unless it clearly asks Junior for help.",
|
|
10754
|
-
"",
|
|
10755
|
-
"When in doubt, should_reply=false. Most messages in a thread are human-to-human conversation.",
|
|
10756
|
-
"",
|
|
10757
|
-
"If the user is clearly telling Junior to stop watching, replying, or participating in the thread,",
|
|
10758
|
-
"set should_unsubscribe=true and should_reply=false.",
|
|
10759
|
-
"Use should_unsubscribe only for clear thread opt-out instructions, not for ordinary side conversation.",
|
|
10760
|
-
"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.",
|
|
10761
11119
|
"",
|
|
10762
11120
|
"Return JSON with should_reply, should_unsubscribe, confidence, and a short reason.",
|
|
10763
11121
|
"Do not return any extra keys.",
|
|
10764
11122
|
"",
|
|
10765
|
-
`<assistant-name>${escapeXml(botUserName)}</assistant-name
|
|
10766
|
-
`<explicit-mention>${isExplicitMention ? "true" : "false"}</explicit-mention>`,
|
|
10767
|
-
`<latest-prior-message-role>${escapeXml(latestPriorMessageRole)}</latest-prior-message-role>`,
|
|
10768
|
-
`<latest-prior-assistant-message>${escapeXml(latestPriorAssistantMessage)}</latest-prior-assistant-message>`,
|
|
10769
|
-
`<thread-context>${escapeXml(conversationContext?.trim() || "[none]")}</thread-context>`
|
|
11123
|
+
`<assistant-name>${escapeXml(botUserName)}</assistant-name>`
|
|
10770
11124
|
].join("\n");
|
|
10771
11125
|
}
|
|
10772
11126
|
async function decideSubscribedThreadReply(args) {
|
|
@@ -10781,11 +11135,16 @@ async function decideSubscribedThreadReply(args) {
|
|
|
10781
11135
|
if (preflightDecision) {
|
|
10782
11136
|
return preflightDecision;
|
|
10783
11137
|
}
|
|
11138
|
+
const signals = buildRouterSignals(args.input);
|
|
10784
11139
|
if (!text && !args.input.hasAttachments) {
|
|
10785
11140
|
return { shouldReply: false, reason: "empty_message" /* EmptyMessage */ };
|
|
10786
11141
|
}
|
|
10787
|
-
if (!
|
|
10788
|
-
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
|
+
};
|
|
10789
11148
|
}
|
|
10790
11149
|
if (args.input.isExplicitMention) {
|
|
10791
11150
|
if (isThreadOptOutInstruction(rawText, text)) {
|
|
@@ -10801,18 +11160,21 @@ async function decideSubscribedThreadReply(args) {
|
|
|
10801
11160
|
reason: "explicit_mention" /* ExplicitMention */
|
|
10802
11161
|
};
|
|
10803
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
|
+
}
|
|
10804
11170
|
try {
|
|
10805
11171
|
const result = await args.completeObject({
|
|
10806
11172
|
modelId: args.modelId,
|
|
10807
11173
|
schema: replyDecisionSchema,
|
|
10808
11174
|
maxTokens: 120,
|
|
10809
11175
|
temperature: 0,
|
|
10810
|
-
system: buildRouterSystemPrompt(
|
|
10811
|
-
|
|
10812
|
-
args.input.conversationContext,
|
|
10813
|
-
args.input.isExplicitMention
|
|
10814
|
-
),
|
|
10815
|
-
prompt: rawText,
|
|
11176
|
+
system: buildRouterSystemPrompt(args.botUserName),
|
|
11177
|
+
prompt: buildRouterPrompt(rawText, signals),
|
|
10816
11178
|
metadata: {
|
|
10817
11179
|
modelId: args.modelId,
|
|
10818
11180
|
threadId: args.input.context.threadId ?? "",
|
|
@@ -10823,6 +11185,7 @@ async function decideSubscribedThreadReply(args) {
|
|
|
10823
11185
|
});
|
|
10824
11186
|
const parsed = replyDecisionSchema.parse(result.object);
|
|
10825
11187
|
const reason = parsed.reason?.trim() || "classifier";
|
|
11188
|
+
const replyConfidenceThreshold = getReplyConfidenceThreshold(signals);
|
|
10826
11189
|
if (parsed.should_unsubscribe) {
|
|
10827
11190
|
if (parsed.confidence < ROUTER_CONFIDENCE_THRESHOLD) {
|
|
10828
11191
|
return {
|
|
@@ -10845,7 +11208,7 @@ async function decideSubscribedThreadReply(args) {
|
|
|
10845
11208
|
reasonDetail: reason
|
|
10846
11209
|
};
|
|
10847
11210
|
}
|
|
10848
|
-
if (parsed.confidence <
|
|
11211
|
+
if (parsed.confidence < replyConfidenceThreshold) {
|
|
10849
11212
|
return {
|
|
10850
11213
|
shouldReply: false,
|
|
10851
11214
|
reason: "low_confidence" /* LowConfidence */,
|
|
@@ -11440,16 +11803,166 @@ var MAX_USER_ATTACHMENTS = 3;
|
|
|
11440
11803
|
var MAX_USER_ATTACHMENT_BYTES = 5 * 1024 * 1024;
|
|
11441
11804
|
var MAX_MESSAGE_IMAGE_ATTACHMENTS = 3;
|
|
11442
11805
|
var MAX_VISION_SUMMARY_CHARS = 500;
|
|
11443
|
-
|
|
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) {
|
|
11444
11884
|
if (!attachments || attachments.length === 0) {
|
|
11445
11885
|
return [];
|
|
11446
11886
|
}
|
|
11447
11887
|
const results = [];
|
|
11888
|
+
const cachedImageSummaries = getCachedImageSummaries({
|
|
11889
|
+
conversation: context.conversation,
|
|
11890
|
+
messageTs: context.messageTs
|
|
11891
|
+
});
|
|
11892
|
+
let nextCachedImageSummaryIndex = 0;
|
|
11448
11893
|
for (const attachment of attachments) {
|
|
11449
11894
|
if (results.length >= MAX_USER_ATTACHMENTS) break;
|
|
11450
11895
|
if (attachment.type !== "image" && attachment.type !== "file") continue;
|
|
11451
11896
|
const mediaType = attachment.mimeType ?? "application/octet-stream";
|
|
11897
|
+
const isImageAttachment = attachment.type === "image" || mediaType.startsWith("image/");
|
|
11898
|
+
if (isImageAttachment && !isVisionEnabled()) {
|
|
11899
|
+
continue;
|
|
11900
|
+
}
|
|
11452
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
|
+
}
|
|
11453
11966
|
let data = null;
|
|
11454
11967
|
if (attachment.fetchData) {
|
|
11455
11968
|
data = await attachment.fetchData();
|
|
@@ -11476,12 +11989,32 @@ async function resolveUserAttachments(attachments, context) {
|
|
|
11476
11989
|
);
|
|
11477
11990
|
continue;
|
|
11478
11991
|
}
|
|
11479
|
-
|
|
11480
|
-
|
|
11481
|
-
mediaType,
|
|
11482
|
-
filename: attachment.name
|
|
11483
|
-
});
|
|
11992
|
+
resolvedAttachment.data = data;
|
|
11993
|
+
results.push(resolvedAttachment);
|
|
11484
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
|
+
}
|
|
11485
12018
|
logWarn(
|
|
11486
12019
|
"attachment_resolution_failed",
|
|
11487
12020
|
{
|
|
@@ -11503,35 +12036,23 @@ async function resolveUserAttachments(attachments, context) {
|
|
|
11503
12036
|
return results;
|
|
11504
12037
|
}
|
|
11505
12038
|
async function summarizeConversationImage(args, deps) {
|
|
12039
|
+
const visionModelId = botConfig.visionModelId;
|
|
12040
|
+
if (!visionModelId) {
|
|
12041
|
+
return void 0;
|
|
12042
|
+
}
|
|
11506
12043
|
try {
|
|
11507
|
-
const
|
|
11508
|
-
|
|
11509
|
-
|
|
12044
|
+
const summary = await summarizeImageWithVision({
|
|
12045
|
+
completeText: deps.completeText,
|
|
12046
|
+
imageData: args.imageData,
|
|
12047
|
+
mimeType: args.mimeType,
|
|
11510
12048
|
maxTokens: 220,
|
|
11511
|
-
|
|
11512
|
-
|
|
11513
|
-
|
|
11514
|
-
|
|
11515
|
-
|
|
11516
|
-
|
|
11517
|
-
text: [
|
|
11518
|
-
"Extract concise, factual context from this image for future thread turns.",
|
|
11519
|
-
"Focus on visible text, names, titles, companies, and candidate-identifying details.",
|
|
11520
|
-
"Do not speculate.",
|
|
11521
|
-
"Return plain text only."
|
|
11522
|
-
].join(" ")
|
|
11523
|
-
},
|
|
11524
|
-
{
|
|
11525
|
-
type: "image",
|
|
11526
|
-
data: args.imageData.toString("base64"),
|
|
11527
|
-
mimeType: args.mimeType
|
|
11528
|
-
}
|
|
11529
|
-
],
|
|
11530
|
-
timestamp: Date.now()
|
|
11531
|
-
}
|
|
11532
|
-
],
|
|
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(" "),
|
|
11533
12055
|
metadata: {
|
|
11534
|
-
modelId: botConfig.modelId,
|
|
11535
12056
|
threadId: args.context.threadId ?? "",
|
|
11536
12057
|
channelId: args.context.channelId ?? "",
|
|
11537
12058
|
requesterId: args.context.requesterId ?? "",
|
|
@@ -11539,11 +12060,10 @@ async function summarizeConversationImage(args, deps) {
|
|
|
11539
12060
|
fileId: args.fileId
|
|
11540
12061
|
}
|
|
11541
12062
|
});
|
|
11542
|
-
const summary = result.text.trim().replace(/\s+/g, " ");
|
|
11543
12063
|
if (!summary) {
|
|
11544
12064
|
return void 0;
|
|
11545
12065
|
}
|
|
11546
|
-
return summary
|
|
12066
|
+
return truncateVisionSummary(summary);
|
|
11547
12067
|
} catch (error) {
|
|
11548
12068
|
logWarn(
|
|
11549
12069
|
"conversation_image_vision_failed",
|
|
@@ -11553,7 +12073,7 @@ async function summarizeConversationImage(args, deps) {
|
|
|
11553
12073
|
slackChannelId: args.context.channelId,
|
|
11554
12074
|
runId: args.context.runId,
|
|
11555
12075
|
assistantUserName: botConfig.userName,
|
|
11556
|
-
modelId:
|
|
12076
|
+
modelId: visionModelId
|
|
11557
12077
|
},
|
|
11558
12078
|
{
|
|
11559
12079
|
"error.message": error instanceof Error ? error.message : String(error),
|
|
@@ -11566,6 +12086,9 @@ async function summarizeConversationImage(args, deps) {
|
|
|
11566
12086
|
}
|
|
11567
12087
|
}
|
|
11568
12088
|
async function hydrateConversationVisionContextWithDeps(conversation, context, deps) {
|
|
12089
|
+
if (!isVisionEnabled()) {
|
|
12090
|
+
return;
|
|
12091
|
+
}
|
|
11569
12092
|
if (!context.channelId || !context.threadTs) {
|
|
11570
12093
|
return;
|
|
11571
12094
|
}
|
|
@@ -11766,6 +12289,7 @@ async function hydrateConversationVisionContextWithDeps(conversation, context, d
|
|
|
11766
12289
|
}
|
|
11767
12290
|
function createVisionContextService(deps) {
|
|
11768
12291
|
return {
|
|
12292
|
+
resolveUserAttachments: async (attachments, context) => await resolveUserAttachmentsWithDeps(attachments, context, deps),
|
|
11769
12293
|
hydrateConversationVisionContext: async (conversation, context) => await hydrateConversationVisionContextWithDeps(
|
|
11770
12294
|
conversation,
|
|
11771
12295
|
context,
|
|
@@ -11945,13 +12469,15 @@ function createReplyToThread(deps) {
|
|
|
11945
12469
|
if (resolvedUserName) {
|
|
11946
12470
|
setTags({ slackUserName: resolvedUserName });
|
|
11947
12471
|
}
|
|
11948
|
-
const userAttachments = await resolveUserAttachments(
|
|
12472
|
+
const userAttachments = await deps.resolveUserAttachments(
|
|
11949
12473
|
message.attachments,
|
|
11950
12474
|
{
|
|
11951
12475
|
threadId,
|
|
11952
12476
|
requesterId: message.author.userId,
|
|
11953
12477
|
channelId,
|
|
11954
|
-
runId
|
|
12478
|
+
runId,
|
|
12479
|
+
conversation: preparedState.conversation,
|
|
12480
|
+
messageTs: message.id
|
|
11955
12481
|
}
|
|
11956
12482
|
);
|
|
11957
12483
|
const progress = createProgressReporter({
|
|
@@ -12361,7 +12887,7 @@ function createPrepareTurnState(deps) {
|
|
|
12361
12887
|
conversation,
|
|
12362
12888
|
incomingUserMessage
|
|
12363
12889
|
);
|
|
12364
|
-
if (
|
|
12890
|
+
if (isVisionEnabled() && (!conversation.vision.backfillCompletedAtMs || messageHasPotentialImageAttachment)) {
|
|
12365
12891
|
await deps.hydrateConversationVisionContext(conversation, {
|
|
12366
12892
|
threadId: args.context.threadId,
|
|
12367
12893
|
channelId: args.context.channelId,
|
|
@@ -12408,6 +12934,7 @@ function createSlackRuntime(options) {
|
|
|
12408
12934
|
const replyToThread = createReplyToThread({
|
|
12409
12935
|
getSlackAdapter: options.getSlackAdapter,
|
|
12410
12936
|
prepareTurnState,
|
|
12937
|
+
resolveUserAttachments: services.visionContext.resolveUserAttachments,
|
|
12411
12938
|
services: services.replyExecutor
|
|
12412
12939
|
});
|
|
12413
12940
|
return createSlackTurnRuntime({
|