@sentry/junior 0.38.0 → 0.40.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 +1479 -354
- package/dist/{chunk-QZRPUFO6.js → chunk-EU6E7QU2.js} +61 -17
- package/dist/{chunk-LAD5O3RX.js → chunk-SPNY2HJJ.js} +3 -2
- package/dist/{chunk-ERH4OYNB.js → chunk-SY4ULGUN.js} +1 -1
- package/dist/cli/check.js +2 -2
- package/dist/cli/snapshot-warmup.js +2 -2
- package/package.json +2 -2
package/dist/app.js
CHANGED
|
@@ -3,7 +3,7 @@ import {
|
|
|
3
3
|
findSkillByName,
|
|
4
4
|
loadSkillsByName,
|
|
5
5
|
parseSkillInvocation
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-SPNY2HJJ.js";
|
|
7
7
|
import {
|
|
8
8
|
GEN_AI_PROVIDER_NAME,
|
|
9
9
|
MISSING_GATEWAY_CREDENTIALS_ERROR,
|
|
@@ -30,10 +30,11 @@ import {
|
|
|
30
30
|
runNonInteractiveCommand,
|
|
31
31
|
sandboxSkillDir,
|
|
32
32
|
sandboxSkillFile
|
|
33
|
-
} from "./chunk-
|
|
33
|
+
} from "./chunk-SY4ULGUN.js";
|
|
34
34
|
import {
|
|
35
35
|
CredentialUnavailableError,
|
|
36
36
|
buildOAuthTokenRequest,
|
|
37
|
+
buildTurnFailureResponse,
|
|
37
38
|
createChatSdkLogger,
|
|
38
39
|
createPluginBroker,
|
|
39
40
|
createRequestContext,
|
|
@@ -57,7 +58,6 @@ import {
|
|
|
57
58
|
mergeHeaderTransforms,
|
|
58
59
|
parseOAuthTokenResponse,
|
|
59
60
|
resolveAuthTokenPlaceholder,
|
|
60
|
-
resolveErrorReference,
|
|
61
61
|
serializeGenAiAttribute,
|
|
62
62
|
setSpanAttributes,
|
|
63
63
|
setSpanStatus,
|
|
@@ -66,8 +66,10 @@ import {
|
|
|
66
66
|
toOptionalString,
|
|
67
67
|
withContext,
|
|
68
68
|
withSpan
|
|
69
|
-
} from "./chunk-
|
|
70
|
-
import
|
|
69
|
+
} from "./chunk-EU6E7QU2.js";
|
|
70
|
+
import {
|
|
71
|
+
sentry_exports
|
|
72
|
+
} from "./chunk-Z3YD6NHK.js";
|
|
71
73
|
import {
|
|
72
74
|
discoverInstalledPluginPackageContent,
|
|
73
75
|
homeDir,
|
|
@@ -2772,20 +2774,9 @@ function buildSlackOutputMessage(text, files) {
|
|
|
2772
2774
|
files
|
|
2773
2775
|
};
|
|
2774
2776
|
}
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
{},
|
|
2778
|
-
{
|
|
2779
|
-
"app.output.original_length": text.length,
|
|
2780
|
-
"app.output.parsed_length": normalized.length,
|
|
2781
|
-
"app.output.file_count": fileCount
|
|
2782
|
-
},
|
|
2783
|
-
"Slack output normalized to empty content"
|
|
2777
|
+
throw new Error(
|
|
2778
|
+
`Slack output normalized to empty content: original_length=${text.length} parsed_length=${normalized.length}`
|
|
2784
2779
|
);
|
|
2785
|
-
return {
|
|
2786
|
-
markdown: "I couldn't produce a response.",
|
|
2787
|
-
files
|
|
2788
|
-
};
|
|
2789
2780
|
}
|
|
2790
2781
|
return {
|
|
2791
2782
|
markdown: normalized,
|
|
@@ -2983,6 +2974,28 @@ function formatActiveMcpCatalogsForPrompt(catalogs) {
|
|
|
2983
2974
|
}
|
|
2984
2975
|
return lines.join("\n");
|
|
2985
2976
|
}
|
|
2977
|
+
function formatToolGuidanceForPrompt(tools) {
|
|
2978
|
+
const guidedTools = tools.filter(
|
|
2979
|
+
(tool2) => Boolean(tool2.promptSnippet?.trim()) || (tool2.promptGuidelines?.length ?? 0) > 0
|
|
2980
|
+
);
|
|
2981
|
+
if (guidedTools.length === 0) {
|
|
2982
|
+
return null;
|
|
2983
|
+
}
|
|
2984
|
+
const lines = [];
|
|
2985
|
+
for (const tool2 of guidedTools) {
|
|
2986
|
+
lines.push(` <tool name="${escapeXml(tool2.name)}">`);
|
|
2987
|
+
if (tool2.promptSnippet?.trim()) {
|
|
2988
|
+
lines.push(` - ${escapeXml(tool2.promptSnippet.trim())}`);
|
|
2989
|
+
}
|
|
2990
|
+
if (tool2.promptGuidelines && tool2.promptGuidelines.length > 0) {
|
|
2991
|
+
for (const guideline of tool2.promptGuidelines) {
|
|
2992
|
+
lines.push(` - ${escapeXml(guideline)}`);
|
|
2993
|
+
}
|
|
2994
|
+
}
|
|
2995
|
+
lines.push(" </tool>");
|
|
2996
|
+
}
|
|
2997
|
+
return lines.join("\n");
|
|
2998
|
+
}
|
|
2986
2999
|
function formatReferenceFilesLines() {
|
|
2987
3000
|
const files = listReferenceFiles();
|
|
2988
3001
|
if (files.length === 0) {
|
|
@@ -3038,7 +3051,7 @@ function formatSlackCapabilityNames(capabilities) {
|
|
|
3038
3051
|
].filter(Boolean);
|
|
3039
3052
|
return names.length > 0 ? names.join(", ") : "none";
|
|
3040
3053
|
}
|
|
3041
|
-
var HEADER = "You are a Slack-based helper assistant. The behavior and output blocks
|
|
3054
|
+
var HEADER = "You are a Slack-based helper assistant. Follow the personality block for voice and tone in every reply. The behavior and output blocks define platform mechanics and override personality only when those mechanics conflict.";
|
|
3042
3055
|
var TURN_CONTEXT_HEADER = "Per-turn runtime context for this request. Treat these blocks as trusted runtime facts and skill/provider instructions for the current turn; the static system prompt remains authoritative.";
|
|
3043
3056
|
var TURN_CONTEXT_TAG = "runtime-turn-context";
|
|
3044
3057
|
var TOOL_POLICY_RULES = [
|
|
@@ -3203,6 +3216,10 @@ function buildCapabilitiesSection(params) {
|
|
|
3203
3216
|
if (activeCatalogs) {
|
|
3204
3217
|
blocks.push(renderTagBlock("active-mcp-catalogs", activeCatalogs));
|
|
3205
3218
|
}
|
|
3219
|
+
const toolGuidance = formatToolGuidanceForPrompt(params.toolGuidance ?? []);
|
|
3220
|
+
if (toolGuidance) {
|
|
3221
|
+
blocks.push(renderTagBlock("tool-guidance", toolGuidance));
|
|
3222
|
+
}
|
|
3206
3223
|
const providerCatalog = formatProviderCatalogForPrompt();
|
|
3207
3224
|
if (providerCatalog) {
|
|
3208
3225
|
blocks.push(renderTagBlock("providers", providerCatalog));
|
|
@@ -3227,7 +3244,8 @@ function buildTurnContextPrompt(params) {
|
|
|
3227
3244
|
buildCapabilitiesSection({
|
|
3228
3245
|
availableSkills: params.availableSkills,
|
|
3229
3246
|
activeSkills: params.activeSkills,
|
|
3230
|
-
activeMcpCatalogs: params.activeMcpCatalogs ?? []
|
|
3247
|
+
activeMcpCatalogs: params.activeMcpCatalogs ?? [],
|
|
3248
|
+
toolGuidance: params.toolGuidance ?? []
|
|
3231
3249
|
}),
|
|
3232
3250
|
buildContextSection({
|
|
3233
3251
|
requester: params.requester,
|
|
@@ -3476,7 +3494,10 @@ var TestCredentialBroker = class {
|
|
|
3476
3494
|
async issue(input) {
|
|
3477
3495
|
const token = process.env.EVAL_TEST_CREDENTIAL_TOKEN?.trim() || "eval-test-token";
|
|
3478
3496
|
const expiresAt = new Date(Date.now() + 5 * 60 * 1e3).toISOString();
|
|
3479
|
-
const env =
|
|
3497
|
+
const env = {
|
|
3498
|
+
...this.config.env ?? {},
|
|
3499
|
+
...this.config.envKey && this.config.placeholder ? { [this.config.envKey]: this.config.placeholder } : {}
|
|
3500
|
+
};
|
|
3480
3501
|
const tokenTransforms = this.config.domains?.map((domain) => ({
|
|
3481
3502
|
domain,
|
|
3482
3503
|
headers: {
|
|
@@ -3533,7 +3554,8 @@ function createSkillCapabilityRuntime(options = {}) {
|
|
|
3533
3554
|
if (!credentials) {
|
|
3534
3555
|
brokersByProvider[name] = useTestBroker ? new TestCredentialBroker({
|
|
3535
3556
|
provider: name,
|
|
3536
|
-
headerTransforms: () => resolveTestApiHeaderTransforms(plugin.manifest)
|
|
3557
|
+
headerTransforms: () => resolveTestApiHeaderTransforms(plugin.manifest),
|
|
3558
|
+
...plugin.manifest.commandEnv ? { env: plugin.manifest.commandEnv } : {}
|
|
3537
3559
|
}) : createPluginBroker(name, { userTokenStore });
|
|
3538
3560
|
continue;
|
|
3539
3561
|
}
|
|
@@ -3545,6 +3567,7 @@ function createSkillCapabilityRuntime(options = {}) {
|
|
|
3545
3567
|
...apiHeaders ? {
|
|
3546
3568
|
headerTransforms: () => resolveTestApiHeaderTransforms(plugin.manifest)
|
|
3547
3569
|
} : {},
|
|
3570
|
+
...plugin.manifest.commandEnv ? { env: plugin.manifest.commandEnv } : {},
|
|
3548
3571
|
envKey: credentials.authTokenEnv,
|
|
3549
3572
|
placeholder
|
|
3550
3573
|
}) : createPluginBroker(name, { userTokenStore });
|
|
@@ -4591,7 +4614,13 @@ function createBashTool() {
|
|
|
4591
4614
|
command: Type.String({
|
|
4592
4615
|
minLength: 1,
|
|
4593
4616
|
description: "Bash command to run inside the sandbox."
|
|
4594
|
-
})
|
|
4617
|
+
}),
|
|
4618
|
+
timeoutMs: Type.Optional(
|
|
4619
|
+
Type.Integer({
|
|
4620
|
+
minimum: 1e3,
|
|
4621
|
+
description: "Optional command timeout in milliseconds. Use for commands that may hang."
|
|
4622
|
+
})
|
|
4623
|
+
)
|
|
4595
4624
|
},
|
|
4596
4625
|
{ additionalProperties: false }
|
|
4597
4626
|
),
|
|
@@ -4601,9 +4630,635 @@ function createBashTool() {
|
|
|
4601
4630
|
});
|
|
4602
4631
|
}
|
|
4603
4632
|
|
|
4604
|
-
// src/chat/tools/sandbox/
|
|
4633
|
+
// src/chat/tools/sandbox/file-utils.ts
|
|
4605
4634
|
import path4 from "path";
|
|
4635
|
+
var MAX_TEXT_CHARS = 6e4;
|
|
4636
|
+
var SKIPPED_DIRECTORIES = /* @__PURE__ */ new Set([".git", "node_modules"]);
|
|
4637
|
+
function positiveInteger(value) {
|
|
4638
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
4639
|
+
return void 0;
|
|
4640
|
+
}
|
|
4641
|
+
const integer = Math.floor(value);
|
|
4642
|
+
return integer > 0 ? integer : void 0;
|
|
4643
|
+
}
|
|
4644
|
+
function normalizeToLf(value) {
|
|
4645
|
+
return value.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
4646
|
+
}
|
|
4647
|
+
function truncateText(value, maxChars = MAX_TEXT_CHARS) {
|
|
4648
|
+
if (value.length <= maxChars) {
|
|
4649
|
+
return { content: value, truncated: false };
|
|
4650
|
+
}
|
|
4651
|
+
const removed = value.length - maxChars;
|
|
4652
|
+
return {
|
|
4653
|
+
content: `${value.slice(0, maxChars)}
|
|
4654
|
+
|
|
4655
|
+
[output truncated: ${removed} characters removed]`,
|
|
4656
|
+
truncated: true
|
|
4657
|
+
};
|
|
4658
|
+
}
|
|
4659
|
+
function escapeRegExp(value) {
|
|
4660
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
4661
|
+
}
|
|
4662
|
+
function globToRegExp(pattern) {
|
|
4663
|
+
let source = "";
|
|
4664
|
+
for (let index = 0; index < pattern.length; index += 1) {
|
|
4665
|
+
const char = pattern[index];
|
|
4666
|
+
const next = pattern[index + 1];
|
|
4667
|
+
if (char === "*" && next === "*") {
|
|
4668
|
+
if (pattern[index + 2] === "/") {
|
|
4669
|
+
source += "(?:.*/)?";
|
|
4670
|
+
index += 2;
|
|
4671
|
+
continue;
|
|
4672
|
+
}
|
|
4673
|
+
source += ".*";
|
|
4674
|
+
index += 1;
|
|
4675
|
+
continue;
|
|
4676
|
+
}
|
|
4677
|
+
if (char === "*") {
|
|
4678
|
+
source += "[^/]*";
|
|
4679
|
+
continue;
|
|
4680
|
+
}
|
|
4681
|
+
if (char === "?") {
|
|
4682
|
+
source += "[^/]";
|
|
4683
|
+
continue;
|
|
4684
|
+
}
|
|
4685
|
+
source += escapeRegExp(char);
|
|
4686
|
+
}
|
|
4687
|
+
return new RegExp(`^${source}$`);
|
|
4688
|
+
}
|
|
4689
|
+
function matchesGlob(relativePath, pattern) {
|
|
4690
|
+
const matcher = globToRegExp(pattern);
|
|
4691
|
+
if (matcher.test(relativePath)) {
|
|
4692
|
+
return true;
|
|
4693
|
+
}
|
|
4694
|
+
if (pattern.startsWith("**/") && matchesGlob(relativePath, pattern.slice(3))) {
|
|
4695
|
+
return true;
|
|
4696
|
+
}
|
|
4697
|
+
return !pattern.includes("/") && matcher.test(path4.posix.basename(relativePath));
|
|
4698
|
+
}
|
|
4699
|
+
function resolveWorkspacePath(input, fallback = ".") {
|
|
4700
|
+
const requested = (input ?? "").trim() || fallback;
|
|
4701
|
+
const absolute = requested.startsWith("/") ? requested : path4.posix.join(SANDBOX_WORKSPACE_ROOT, requested);
|
|
4702
|
+
const normalized = path4.posix.normalize(absolute);
|
|
4703
|
+
if (normalized !== SANDBOX_WORKSPACE_ROOT && !normalized.startsWith(`${SANDBOX_WORKSPACE_ROOT}/`)) {
|
|
4704
|
+
throw new Error(
|
|
4705
|
+
`Path must stay within ${SANDBOX_WORKSPACE_ROOT}: ${requested}`
|
|
4706
|
+
);
|
|
4707
|
+
}
|
|
4708
|
+
return normalized;
|
|
4709
|
+
}
|
|
4710
|
+
async function collectFiles(params) {
|
|
4711
|
+
const files = [];
|
|
4712
|
+
let limitReached = false;
|
|
4713
|
+
const visit = async (dirPath) => {
|
|
4714
|
+
const entries = (await params.fs.readdir(dirPath)).sort(
|
|
4715
|
+
(a, b) => a.toLowerCase().localeCompare(b.toLowerCase())
|
|
4716
|
+
);
|
|
4717
|
+
for (const entry of entries) {
|
|
4718
|
+
const fullPath = path4.posix.join(dirPath, entry);
|
|
4719
|
+
const stat2 = await params.fs.stat(fullPath);
|
|
4720
|
+
if (stat2.isDirectory()) {
|
|
4721
|
+
if (!SKIPPED_DIRECTORIES.has(entry)) {
|
|
4722
|
+
await visit(fullPath);
|
|
4723
|
+
}
|
|
4724
|
+
if (limitReached) return;
|
|
4725
|
+
continue;
|
|
4726
|
+
}
|
|
4727
|
+
const relativePath = path4.posix.relative(params.root, fullPath);
|
|
4728
|
+
if (!params.pattern || matchesGlob(relativePath, params.pattern)) {
|
|
4729
|
+
files.push(fullPath);
|
|
4730
|
+
if (params.limit && files.length >= params.limit) {
|
|
4731
|
+
limitReached = true;
|
|
4732
|
+
return;
|
|
4733
|
+
}
|
|
4734
|
+
}
|
|
4735
|
+
}
|
|
4736
|
+
};
|
|
4737
|
+
const stat = await params.fs.stat(params.root);
|
|
4738
|
+
if (!stat.isDirectory()) {
|
|
4739
|
+
const relativePath = path4.posix.basename(params.root);
|
|
4740
|
+
return {
|
|
4741
|
+
files: !params.pattern || matchesGlob(relativePath, params.pattern) ? [params.root] : [],
|
|
4742
|
+
limitReached: false
|
|
4743
|
+
};
|
|
4744
|
+
}
|
|
4745
|
+
await visit(params.root);
|
|
4746
|
+
return { files, limitReached };
|
|
4747
|
+
}
|
|
4748
|
+
|
|
4749
|
+
// src/chat/tools/sandbox/edit-file.ts
|
|
4606
4750
|
import { Type as Type2 } from "@sinclair/typebox";
|
|
4751
|
+
function detectLineEnding(value) {
|
|
4752
|
+
return value.includes("\r\n") ? "\r\n" : "\n";
|
|
4753
|
+
}
|
|
4754
|
+
function restoreLineEndings(value, lineEnding) {
|
|
4755
|
+
return lineEnding === "\r\n" ? value.replace(/\n/g, "\r\n") : value;
|
|
4756
|
+
}
|
|
4757
|
+
function stripBom(value) {
|
|
4758
|
+
return value.startsWith("\uFEFF") ? { bom: "\uFEFF", text: value.slice(1) } : { bom: "", text: value };
|
|
4759
|
+
}
|
|
4760
|
+
function countOccurrences(content, target) {
|
|
4761
|
+
let count = 0;
|
|
4762
|
+
let start = 0;
|
|
4763
|
+
while (target.length > 0) {
|
|
4764
|
+
const index = content.indexOf(target, start);
|
|
4765
|
+
if (index === -1) break;
|
|
4766
|
+
count += 1;
|
|
4767
|
+
start = index + target.length;
|
|
4768
|
+
}
|
|
4769
|
+
return count;
|
|
4770
|
+
}
|
|
4771
|
+
function firstChangedLine(oldContent, newContent) {
|
|
4772
|
+
const oldLines = oldContent.split("\n");
|
|
4773
|
+
const newLines = newContent.split("\n");
|
|
4774
|
+
const count = Math.max(oldLines.length, newLines.length);
|
|
4775
|
+
for (let index = 0; index < count; index += 1) {
|
|
4776
|
+
if (oldLines[index] !== newLines[index]) {
|
|
4777
|
+
return index + 1;
|
|
4778
|
+
}
|
|
4779
|
+
}
|
|
4780
|
+
return void 0;
|
|
4781
|
+
}
|
|
4782
|
+
function buildCompactDiff(oldContent, newContent) {
|
|
4783
|
+
const oldLines = oldContent.split("\n");
|
|
4784
|
+
const newLines = newContent.split("\n");
|
|
4785
|
+
let prefix = 0;
|
|
4786
|
+
while (prefix < oldLines.length && prefix < newLines.length && oldLines[prefix] === newLines[prefix]) {
|
|
4787
|
+
prefix += 1;
|
|
4788
|
+
}
|
|
4789
|
+
let oldSuffix = oldLines.length - 1;
|
|
4790
|
+
let newSuffix = newLines.length - 1;
|
|
4791
|
+
while (oldSuffix >= prefix && newSuffix >= prefix && oldLines[oldSuffix] === newLines[newSuffix]) {
|
|
4792
|
+
oldSuffix -= 1;
|
|
4793
|
+
newSuffix -= 1;
|
|
4794
|
+
}
|
|
4795
|
+
const contextStart = Math.max(0, prefix - 3);
|
|
4796
|
+
const newContextEnd = Math.min(newLines.length - 1, newSuffix + 3);
|
|
4797
|
+
const oldContextEnd = Math.min(oldLines.length - 1, oldSuffix + 3);
|
|
4798
|
+
const width = String(Math.max(oldLines.length, newLines.length)).length;
|
|
4799
|
+
const output = [];
|
|
4800
|
+
if (contextStart > 0) {
|
|
4801
|
+
output.push(` ${"".padStart(width)} ...`);
|
|
4802
|
+
}
|
|
4803
|
+
for (let index = contextStart; index < prefix; index += 1) {
|
|
4804
|
+
output.push(` ${String(index + 1).padStart(width)} ${oldLines[index]}`);
|
|
4805
|
+
}
|
|
4806
|
+
for (let index = prefix; index <= oldSuffix; index += 1) {
|
|
4807
|
+
output.push(`-${String(index + 1).padStart(width)} ${oldLines[index]}`);
|
|
4808
|
+
}
|
|
4809
|
+
for (let index = prefix; index <= newSuffix; index += 1) {
|
|
4810
|
+
output.push(`+${String(index + 1).padStart(width)} ${newLines[index]}`);
|
|
4811
|
+
}
|
|
4812
|
+
for (let index = newSuffix + 1; index <= newContextEnd; index += 1) {
|
|
4813
|
+
output.push(` ${String(index + 1).padStart(width)} ${newLines[index]}`);
|
|
4814
|
+
}
|
|
4815
|
+
if (newContextEnd < newLines.length - 1 || oldContextEnd < oldLines.length - 1) {
|
|
4816
|
+
output.push(` ${"".padStart(width)} ...`);
|
|
4817
|
+
}
|
|
4818
|
+
return {
|
|
4819
|
+
diff: output.join("\n"),
|
|
4820
|
+
firstChangedLine: firstChangedLine(oldContent, newContent)
|
|
4821
|
+
};
|
|
4822
|
+
}
|
|
4823
|
+
function validateAndApplyEdits(content, edits, filePath) {
|
|
4824
|
+
if (!Array.isArray(edits) || edits.length === 0) {
|
|
4825
|
+
throw new Error("editFile requires at least one edit.");
|
|
4826
|
+
}
|
|
4827
|
+
const normalizedEdits = edits.map((edit, index) => {
|
|
4828
|
+
if (typeof edit.oldText !== "string" || edit.oldText.length === 0) {
|
|
4829
|
+
throw new Error(
|
|
4830
|
+
`edits[${index}].oldText must not be empty in ${filePath}.`
|
|
4831
|
+
);
|
|
4832
|
+
}
|
|
4833
|
+
if (typeof edit.newText !== "string") {
|
|
4834
|
+
throw new Error(
|
|
4835
|
+
`edits[${index}].newText must be a string in ${filePath}.`
|
|
4836
|
+
);
|
|
4837
|
+
}
|
|
4838
|
+
return {
|
|
4839
|
+
oldText: normalizeToLf(edit.oldText),
|
|
4840
|
+
newText: normalizeToLf(edit.newText)
|
|
4841
|
+
};
|
|
4842
|
+
});
|
|
4843
|
+
const matchedEdits = [];
|
|
4844
|
+
for (let index = 0; index < normalizedEdits.length; index += 1) {
|
|
4845
|
+
const edit = normalizedEdits[index];
|
|
4846
|
+
const matchIndex = content.indexOf(edit.oldText);
|
|
4847
|
+
if (matchIndex === -1) {
|
|
4848
|
+
throw new Error(
|
|
4849
|
+
`Could not find edits[${index}] in ${filePath}. oldText must match exactly including whitespace and newlines.`
|
|
4850
|
+
);
|
|
4851
|
+
}
|
|
4852
|
+
const occurrences = countOccurrences(content, edit.oldText);
|
|
4853
|
+
if (occurrences > 1) {
|
|
4854
|
+
throw new Error(
|
|
4855
|
+
`Found ${occurrences} occurrences of edits[${index}] in ${filePath}. Each oldText must be unique.`
|
|
4856
|
+
);
|
|
4857
|
+
}
|
|
4858
|
+
matchedEdits.push({
|
|
4859
|
+
editIndex: index,
|
|
4860
|
+
matchIndex,
|
|
4861
|
+
matchLength: edit.oldText.length,
|
|
4862
|
+
newText: edit.newText
|
|
4863
|
+
});
|
|
4864
|
+
}
|
|
4865
|
+
matchedEdits.sort((a, b) => a.matchIndex - b.matchIndex);
|
|
4866
|
+
for (let index = 1; index < matchedEdits.length; index += 1) {
|
|
4867
|
+
const previous = matchedEdits[index - 1];
|
|
4868
|
+
const current = matchedEdits[index];
|
|
4869
|
+
if (previous.matchIndex + previous.matchLength > current.matchIndex) {
|
|
4870
|
+
throw new Error(
|
|
4871
|
+
`edits[${previous.editIndex}] and edits[${current.editIndex}] overlap in ${filePath}. Merge overlapping replacements into one edit.`
|
|
4872
|
+
);
|
|
4873
|
+
}
|
|
4874
|
+
}
|
|
4875
|
+
let newContent = content;
|
|
4876
|
+
for (let index = matchedEdits.length - 1; index >= 0; index -= 1) {
|
|
4877
|
+
const edit = matchedEdits[index];
|
|
4878
|
+
newContent = newContent.slice(0, edit.matchIndex) + edit.newText + newContent.slice(edit.matchIndex + edit.matchLength);
|
|
4879
|
+
}
|
|
4880
|
+
if (newContent === content) {
|
|
4881
|
+
throw new Error(`No changes made to ${filePath}.`);
|
|
4882
|
+
}
|
|
4883
|
+
return { baseContent: content, newContent };
|
|
4884
|
+
}
|
|
4885
|
+
function prepareEditFileArguments(input) {
|
|
4886
|
+
if (!input || typeof input !== "object") {
|
|
4887
|
+
return input;
|
|
4888
|
+
}
|
|
4889
|
+
const raw = { ...input };
|
|
4890
|
+
if (typeof raw.edits === "string") {
|
|
4891
|
+
try {
|
|
4892
|
+
raw.edits = JSON.parse(raw.edits);
|
|
4893
|
+
} catch {
|
|
4894
|
+
return raw;
|
|
4895
|
+
}
|
|
4896
|
+
}
|
|
4897
|
+
const edits = Array.isArray(raw.edits) ? [...raw.edits] : [];
|
|
4898
|
+
const oldText = raw.oldText ?? raw.old_text;
|
|
4899
|
+
const newText = raw.newText ?? raw.new_text;
|
|
4900
|
+
if (typeof oldText === "string" && typeof newText === "string") {
|
|
4901
|
+
edits.push({ oldText, newText });
|
|
4902
|
+
}
|
|
4903
|
+
if (edits.length > 0) {
|
|
4904
|
+
raw.edits = edits.map((edit) => {
|
|
4905
|
+
if (!edit || typeof edit !== "object") {
|
|
4906
|
+
return edit;
|
|
4907
|
+
}
|
|
4908
|
+
const record = edit;
|
|
4909
|
+
const { old_text, new_text, ...rest } = record;
|
|
4910
|
+
return {
|
|
4911
|
+
...rest,
|
|
4912
|
+
oldText: record.oldText ?? old_text,
|
|
4913
|
+
newText: record.newText ?? new_text
|
|
4914
|
+
};
|
|
4915
|
+
});
|
|
4916
|
+
}
|
|
4917
|
+
delete raw.oldText;
|
|
4918
|
+
delete raw.old_text;
|
|
4919
|
+
delete raw.newText;
|
|
4920
|
+
delete raw.new_text;
|
|
4921
|
+
return raw;
|
|
4922
|
+
}
|
|
4923
|
+
async function editFile(params) {
|
|
4924
|
+
const filePath = resolveWorkspacePath(params.path);
|
|
4925
|
+
const rawContent = await params.fs.readFile(filePath, { encoding: "utf8" });
|
|
4926
|
+
const { bom, text } = stripBom(rawContent);
|
|
4927
|
+
const lineEnding = detectLineEnding(text);
|
|
4928
|
+
const normalizedContent = normalizeToLf(text);
|
|
4929
|
+
const { baseContent, newContent } = validateAndApplyEdits(
|
|
4930
|
+
normalizedContent,
|
|
4931
|
+
params.edits,
|
|
4932
|
+
params.path
|
|
4933
|
+
);
|
|
4934
|
+
await params.fs.writeFile(
|
|
4935
|
+
filePath,
|
|
4936
|
+
bom + restoreLineEndings(newContent, lineEnding),
|
|
4937
|
+
{ encoding: "utf8" }
|
|
4938
|
+
);
|
|
4939
|
+
const diff = buildCompactDiff(baseContent, newContent);
|
|
4940
|
+
return {
|
|
4941
|
+
content: [
|
|
4942
|
+
{
|
|
4943
|
+
type: "text",
|
|
4944
|
+
text: `Successfully replaced ${params.edits.length} block(s) in ${params.path}.`
|
|
4945
|
+
}
|
|
4946
|
+
],
|
|
4947
|
+
details: {
|
|
4948
|
+
diff: diff.diff,
|
|
4949
|
+
first_changed_line: diff.firstChangedLine,
|
|
4950
|
+
ok: true,
|
|
4951
|
+
path: params.path,
|
|
4952
|
+
replacements: params.edits.length
|
|
4953
|
+
}
|
|
4954
|
+
};
|
|
4955
|
+
}
|
|
4956
|
+
var editReplacementSchema = Type2.Object(
|
|
4957
|
+
{
|
|
4958
|
+
oldText: Type2.String({
|
|
4959
|
+
minLength: 1,
|
|
4960
|
+
description: "Exact text to replace. It must be unique in the original file and must not overlap another edit."
|
|
4961
|
+
}),
|
|
4962
|
+
newText: Type2.String({
|
|
4963
|
+
description: "Replacement text for this edit."
|
|
4964
|
+
})
|
|
4965
|
+
},
|
|
4966
|
+
{ additionalProperties: false }
|
|
4967
|
+
);
|
|
4968
|
+
function createEditFileTool() {
|
|
4969
|
+
return tool({
|
|
4970
|
+
description: "Edit one sandbox workspace file with exact text replacements. Use for precise changes to existing files. Each oldText must match exactly, be unique, and not overlap another edit. Returns a diff.",
|
|
4971
|
+
promptSnippet: "existing-file exact edits; returns diff",
|
|
4972
|
+
promptGuidelines: [
|
|
4973
|
+
"prefer over writeFile for targeted changes",
|
|
4974
|
+
"oldText exact, unique, non-overlapping",
|
|
4975
|
+
"multiple same-file changes: one edits[] call"
|
|
4976
|
+
],
|
|
4977
|
+
prepareArguments: prepareEditFileArguments,
|
|
4978
|
+
executionMode: "sequential",
|
|
4979
|
+
inputSchema: Type2.Object(
|
|
4980
|
+
{
|
|
4981
|
+
path: Type2.String({
|
|
4982
|
+
minLength: 1,
|
|
4983
|
+
description: "Path to edit in the sandbox workspace."
|
|
4984
|
+
}),
|
|
4985
|
+
edits: Type2.Array(editReplacementSchema, {
|
|
4986
|
+
minItems: 1,
|
|
4987
|
+
description: "Exact replacements matched against the original file, not incrementally."
|
|
4988
|
+
})
|
|
4989
|
+
},
|
|
4990
|
+
{ additionalProperties: false }
|
|
4991
|
+
),
|
|
4992
|
+
execute: async () => {
|
|
4993
|
+
throw new Error(
|
|
4994
|
+
"editFile can only run when sandbox execution is enabled."
|
|
4995
|
+
);
|
|
4996
|
+
}
|
|
4997
|
+
});
|
|
4998
|
+
}
|
|
4999
|
+
|
|
5000
|
+
// src/chat/tools/sandbox/find-files.ts
|
|
5001
|
+
import path5 from "path";
|
|
5002
|
+
import { Type as Type3 } from "@sinclair/typebox";
|
|
5003
|
+
var DEFAULT_FIND_LIMIT = 1e3;
|
|
5004
|
+
async function findFiles(params) {
|
|
5005
|
+
if (!params.pattern.trim()) {
|
|
5006
|
+
throw new Error("pattern is required");
|
|
5007
|
+
}
|
|
5008
|
+
const root = resolveWorkspacePath(params.path);
|
|
5009
|
+
const limit = positiveInteger(params.limit) ?? DEFAULT_FIND_LIMIT;
|
|
5010
|
+
const { files, limitReached } = await collectFiles({
|
|
5011
|
+
fs: params.fs,
|
|
5012
|
+
root,
|
|
5013
|
+
pattern: params.pattern,
|
|
5014
|
+
limit
|
|
5015
|
+
});
|
|
5016
|
+
const relativePaths = files.map(
|
|
5017
|
+
(filePath) => path5.posix.relative(root, filePath)
|
|
5018
|
+
);
|
|
5019
|
+
const bounded = truncateText(
|
|
5020
|
+
relativePaths.length > 0 ? relativePaths.join("\n") : "No files found matching pattern"
|
|
5021
|
+
);
|
|
5022
|
+
const notices = [];
|
|
5023
|
+
if (limitReached) {
|
|
5024
|
+
notices.push(
|
|
5025
|
+
`${limit} results limit reached. Refine pattern or raise limit.`
|
|
5026
|
+
);
|
|
5027
|
+
}
|
|
5028
|
+
if (bounded.truncated) {
|
|
5029
|
+
notices.push(`${MAX_TEXT_CHARS} character output limit reached.`);
|
|
5030
|
+
}
|
|
5031
|
+
return {
|
|
5032
|
+
content: [
|
|
5033
|
+
{
|
|
5034
|
+
type: "text",
|
|
5035
|
+
text: notices.length > 0 ? `${bounded.content}
|
|
5036
|
+
|
|
5037
|
+
[${notices.join(" ")}]` : bounded.content
|
|
5038
|
+
}
|
|
5039
|
+
],
|
|
5040
|
+
details: {
|
|
5041
|
+
ok: true,
|
|
5042
|
+
path: params.path ?? ".",
|
|
5043
|
+
truncated: limitReached || bounded.truncated,
|
|
5044
|
+
...limitReached ? { result_limit_reached: limit } : {}
|
|
5045
|
+
}
|
|
5046
|
+
};
|
|
5047
|
+
}
|
|
5048
|
+
function createFindFilesTool() {
|
|
5049
|
+
return tool({
|
|
5050
|
+
description: "Find sandbox workspace files by glob pattern. Returns bounded paths relative to the search root and skips dependency/cache directories.",
|
|
5051
|
+
annotations: { readOnlyHint: true, destructiveHint: false },
|
|
5052
|
+
inputSchema: Type3.Object(
|
|
5053
|
+
{
|
|
5054
|
+
pattern: Type3.String({
|
|
5055
|
+
minLength: 1,
|
|
5056
|
+
description: "Glob pattern to match, for example '*.ts', '**/*.json', or 'src/**/*.test.ts'."
|
|
5057
|
+
}),
|
|
5058
|
+
path: Type3.Optional(
|
|
5059
|
+
Type3.String({
|
|
5060
|
+
minLength: 1,
|
|
5061
|
+
description: "Directory or file path in the sandbox workspace. Defaults to the workspace root."
|
|
5062
|
+
})
|
|
5063
|
+
),
|
|
5064
|
+
limit: Type3.Optional(
|
|
5065
|
+
Type3.Integer({
|
|
5066
|
+
minimum: 1,
|
|
5067
|
+
description: "Maximum number of file paths to return. Defaults to 1000."
|
|
5068
|
+
})
|
|
5069
|
+
)
|
|
5070
|
+
},
|
|
5071
|
+
{ additionalProperties: false }
|
|
5072
|
+
),
|
|
5073
|
+
execute: async () => {
|
|
5074
|
+
throw new Error(
|
|
5075
|
+
"findFiles can only run when sandbox execution is enabled."
|
|
5076
|
+
);
|
|
5077
|
+
}
|
|
5078
|
+
});
|
|
5079
|
+
}
|
|
5080
|
+
|
|
5081
|
+
// src/chat/tools/sandbox/grep.ts
|
|
5082
|
+
import path6 from "path";
|
|
5083
|
+
import { Type as Type4 } from "@sinclair/typebox";
|
|
5084
|
+
var DEFAULT_GREP_LIMIT = 100;
|
|
5085
|
+
var MAX_GREP_LINE_CHARS = 500;
|
|
5086
|
+
function truncateGrepLine(value) {
|
|
5087
|
+
if (value.length <= MAX_GREP_LINE_CHARS) {
|
|
5088
|
+
return { line: value, truncated: false };
|
|
5089
|
+
}
|
|
5090
|
+
return {
|
|
5091
|
+
line: `${value.slice(0, MAX_GREP_LINE_CHARS)}... [line truncated]`,
|
|
5092
|
+
truncated: true
|
|
5093
|
+
};
|
|
5094
|
+
}
|
|
5095
|
+
function lineMatches(params) {
|
|
5096
|
+
if (!params.literal) {
|
|
5097
|
+
return Boolean(params.regex?.test(params.line));
|
|
5098
|
+
}
|
|
5099
|
+
if (params.ignoreCase) {
|
|
5100
|
+
return params.line.toLowerCase().includes(params.pattern.toLowerCase());
|
|
5101
|
+
}
|
|
5102
|
+
return params.line.includes(params.pattern);
|
|
5103
|
+
}
|
|
5104
|
+
async function grepFiles(params) {
|
|
5105
|
+
if (!params.pattern) {
|
|
5106
|
+
throw new Error("pattern is required");
|
|
5107
|
+
}
|
|
5108
|
+
const root = resolveWorkspacePath(params.path);
|
|
5109
|
+
const limit = positiveInteger(params.limit) ?? DEFAULT_GREP_LIMIT;
|
|
5110
|
+
const context = positiveInteger(params.context) ?? 0;
|
|
5111
|
+
const regex = params.literal ? void 0 : new RegExp(params.pattern, params.ignoreCase ? "i" : "");
|
|
5112
|
+
const { files } = await collectFiles({
|
|
5113
|
+
fs: params.fs,
|
|
5114
|
+
root,
|
|
5115
|
+
pattern: params.glob
|
|
5116
|
+
});
|
|
5117
|
+
const output = [];
|
|
5118
|
+
let matchCount = 0;
|
|
5119
|
+
let matchLimitReached = false;
|
|
5120
|
+
let lineTruncated = false;
|
|
5121
|
+
for (const filePath of files) {
|
|
5122
|
+
if (matchLimitReached) break;
|
|
5123
|
+
let content;
|
|
5124
|
+
try {
|
|
5125
|
+
content = await params.fs.readFile(filePath, { encoding: "utf8" });
|
|
5126
|
+
} catch {
|
|
5127
|
+
continue;
|
|
5128
|
+
}
|
|
5129
|
+
if (content.includes("\0")) {
|
|
5130
|
+
continue;
|
|
5131
|
+
}
|
|
5132
|
+
const lines = normalizeToLf(content).split("\n");
|
|
5133
|
+
const relativePath = files.length === 1 && filePath === root ? path6.posix.basename(filePath) : path6.posix.relative(root, filePath);
|
|
5134
|
+
const matchedLines = [];
|
|
5135
|
+
for (let lineIndex = 0; lineIndex < lines.length; lineIndex += 1) {
|
|
5136
|
+
if (!lineMatches({
|
|
5137
|
+
ignoreCase: params.ignoreCase,
|
|
5138
|
+
line: lines[lineIndex],
|
|
5139
|
+
literal: params.literal,
|
|
5140
|
+
pattern: params.pattern,
|
|
5141
|
+
regex
|
|
5142
|
+
})) {
|
|
5143
|
+
continue;
|
|
5144
|
+
}
|
|
5145
|
+
if (matchCount >= limit) {
|
|
5146
|
+
matchLimitReached = true;
|
|
5147
|
+
break;
|
|
5148
|
+
}
|
|
5149
|
+
matchCount += 1;
|
|
5150
|
+
matchedLines.push(lineIndex);
|
|
5151
|
+
}
|
|
5152
|
+
const matchedLineSet = new Set(matchedLines);
|
|
5153
|
+
const emittedLines = /* @__PURE__ */ new Set();
|
|
5154
|
+
for (const lineIndex of matchedLines) {
|
|
5155
|
+
const start = Math.max(0, lineIndex - context);
|
|
5156
|
+
const end = Math.min(lines.length - 1, lineIndex + context);
|
|
5157
|
+
for (let current = start; current <= end; current += 1) {
|
|
5158
|
+
if (emittedLines.has(current)) {
|
|
5159
|
+
continue;
|
|
5160
|
+
}
|
|
5161
|
+
emittedLines.add(current);
|
|
5162
|
+
const truncated = truncateGrepLine(lines[current]);
|
|
5163
|
+
lineTruncated ||= truncated.truncated;
|
|
5164
|
+
const separator = matchedLineSet.has(current) ? ":" : "-";
|
|
5165
|
+
output.push(
|
|
5166
|
+
`${relativePath}${separator}${current + 1}${separator} ${truncated.line}`
|
|
5167
|
+
);
|
|
5168
|
+
}
|
|
5169
|
+
}
|
|
5170
|
+
}
|
|
5171
|
+
const bounded = truncateText(
|
|
5172
|
+
output.length > 0 ? output.join("\n") : "No matches found"
|
|
5173
|
+
);
|
|
5174
|
+
const notices = [];
|
|
5175
|
+
if (matchLimitReached) {
|
|
5176
|
+
notices.push(
|
|
5177
|
+
`${limit} matches limit reached. Refine pattern or raise limit.`
|
|
5178
|
+
);
|
|
5179
|
+
}
|
|
5180
|
+
if (lineTruncated) {
|
|
5181
|
+
notices.push(
|
|
5182
|
+
`Some lines were truncated to ${MAX_GREP_LINE_CHARS} characters.`
|
|
5183
|
+
);
|
|
5184
|
+
}
|
|
5185
|
+
if (bounded.truncated) {
|
|
5186
|
+
notices.push(`${MAX_TEXT_CHARS} character output limit reached.`);
|
|
5187
|
+
}
|
|
5188
|
+
return {
|
|
5189
|
+
content: [
|
|
5190
|
+
{
|
|
5191
|
+
type: "text",
|
|
5192
|
+
text: notices.length > 0 ? `${bounded.content}
|
|
5193
|
+
|
|
5194
|
+
[${notices.join(" ")}]` : bounded.content
|
|
5195
|
+
}
|
|
5196
|
+
],
|
|
5197
|
+
details: {
|
|
5198
|
+
ok: true,
|
|
5199
|
+
path: params.path ?? ".",
|
|
5200
|
+
truncated: matchLimitReached || lineTruncated || bounded.truncated,
|
|
5201
|
+
...matchLimitReached ? { match_limit_reached: limit } : {},
|
|
5202
|
+
...lineTruncated ? { line_truncated: true } : {}
|
|
5203
|
+
}
|
|
5204
|
+
};
|
|
5205
|
+
}
|
|
5206
|
+
function createGrepTool() {
|
|
5207
|
+
return tool({
|
|
5208
|
+
description: "Search sandbox workspace file contents. Returns bounded matching lines with file paths and line numbers. Respects path/glob filters and includes truncation notices.",
|
|
5209
|
+
annotations: { readOnlyHint: true, destructiveHint: false },
|
|
5210
|
+
inputSchema: Type4.Object(
|
|
5211
|
+
{
|
|
5212
|
+
pattern: Type4.String({
|
|
5213
|
+
minLength: 1,
|
|
5214
|
+
description: "Regex pattern or literal text to search for."
|
|
5215
|
+
}),
|
|
5216
|
+
path: Type4.Optional(
|
|
5217
|
+
Type4.String({
|
|
5218
|
+
minLength: 1,
|
|
5219
|
+
description: "Directory or file path in the sandbox workspace. Defaults to the workspace root."
|
|
5220
|
+
})
|
|
5221
|
+
),
|
|
5222
|
+
glob: Type4.Optional(
|
|
5223
|
+
Type4.String({
|
|
5224
|
+
minLength: 1,
|
|
5225
|
+
description: "Optional glob filter such as '*.ts' or '**/*.test.ts'."
|
|
5226
|
+
})
|
|
5227
|
+
),
|
|
5228
|
+
ignoreCase: Type4.Optional(
|
|
5229
|
+
Type4.Boolean({
|
|
5230
|
+
description: "Whether matching should ignore case."
|
|
5231
|
+
})
|
|
5232
|
+
),
|
|
5233
|
+
literal: Type4.Optional(
|
|
5234
|
+
Type4.Boolean({
|
|
5235
|
+
description: "Treat pattern as literal text instead of regex."
|
|
5236
|
+
})
|
|
5237
|
+
),
|
|
5238
|
+
context: Type4.Optional(
|
|
5239
|
+
Type4.Integer({
|
|
5240
|
+
minimum: 0,
|
|
5241
|
+
description: "Number of surrounding context lines to include."
|
|
5242
|
+
})
|
|
5243
|
+
),
|
|
5244
|
+
limit: Type4.Optional(
|
|
5245
|
+
Type4.Integer({
|
|
5246
|
+
minimum: 1,
|
|
5247
|
+
description: "Maximum matches to return. Defaults to 100."
|
|
5248
|
+
})
|
|
5249
|
+
)
|
|
5250
|
+
},
|
|
5251
|
+
{ additionalProperties: false }
|
|
5252
|
+
),
|
|
5253
|
+
execute: async () => {
|
|
5254
|
+
throw new Error("grep can only run when sandbox execution is enabled.");
|
|
5255
|
+
}
|
|
5256
|
+
});
|
|
5257
|
+
}
|
|
5258
|
+
|
|
5259
|
+
// src/chat/tools/sandbox/attach-file.ts
|
|
5260
|
+
import path7 from "path";
|
|
5261
|
+
import { Type as Type5 } from "@sinclair/typebox";
|
|
4607
5262
|
var MAX_ATTACH_FILE_BYTES = 10 * 1024 * 1024;
|
|
4608
5263
|
var MIME_BY_EXTENSION = {
|
|
4609
5264
|
".png": "image/png",
|
|
@@ -4624,20 +5279,20 @@ function normalizeSandboxPath(inputPath) {
|
|
|
4624
5279
|
if (!trimmed) {
|
|
4625
5280
|
throw new Error("path is required");
|
|
4626
5281
|
}
|
|
4627
|
-
if (
|
|
5282
|
+
if (path7.posix.isAbsolute(trimmed)) {
|
|
4628
5283
|
return trimmed;
|
|
4629
5284
|
}
|
|
4630
|
-
return
|
|
5285
|
+
return path7.posix.join(SANDBOX_WORKSPACE_ROOT, trimmed);
|
|
4631
5286
|
}
|
|
4632
5287
|
function sanitizeFilename(value, fallbackPath) {
|
|
4633
5288
|
const candidate = (value ?? "").trim();
|
|
4634
5289
|
if (candidate) {
|
|
4635
|
-
const base =
|
|
5290
|
+
const base = path7.posix.basename(candidate);
|
|
4636
5291
|
if (base && base !== "." && base !== "..") {
|
|
4637
5292
|
return base;
|
|
4638
5293
|
}
|
|
4639
5294
|
}
|
|
4640
|
-
const derived =
|
|
5295
|
+
const derived = path7.posix.basename(fallbackPath);
|
|
4641
5296
|
if (derived && derived !== "." && derived !== "..") {
|
|
4642
5297
|
return derived;
|
|
4643
5298
|
}
|
|
@@ -4648,7 +5303,7 @@ function inferMimeType(filename, explicitMimeType) {
|
|
|
4648
5303
|
if (explicit) {
|
|
4649
5304
|
return explicit;
|
|
4650
5305
|
}
|
|
4651
|
-
const ext =
|
|
5306
|
+
const ext = path7.extname(filename).toLowerCase();
|
|
4652
5307
|
return MIME_BY_EXTENSION[ext] ?? "application/octet-stream";
|
|
4653
5308
|
}
|
|
4654
5309
|
async function detectMimeType(sandbox, targetPath) {
|
|
@@ -4669,20 +5324,20 @@ async function detectMimeType(sandbox, targetPath) {
|
|
|
4669
5324
|
function createAttachFileTool(sandbox, hooks = {}) {
|
|
4670
5325
|
return tool({
|
|
4671
5326
|
description: "Attach a file to the Slack reply. Use this for files that exist in the sandbox, such as screenshots, PDFs, or logs, or for generated image `attachment_path` values returned earlier in the turn.",
|
|
4672
|
-
inputSchema:
|
|
5327
|
+
inputSchema: Type5.Object(
|
|
4673
5328
|
{
|
|
4674
|
-
path:
|
|
5329
|
+
path: Type5.String({
|
|
4675
5330
|
minLength: 1,
|
|
4676
5331
|
description: "Absolute path (for example /tmp/screenshot.png) or workspace-relative path."
|
|
4677
5332
|
}),
|
|
4678
|
-
filename:
|
|
4679
|
-
|
|
5333
|
+
filename: Type5.Optional(
|
|
5334
|
+
Type5.String({
|
|
4680
5335
|
minLength: 1,
|
|
4681
5336
|
description: "Optional filename override shown in Slack."
|
|
4682
5337
|
})
|
|
4683
5338
|
),
|
|
4684
|
-
mimeType:
|
|
4685
|
-
|
|
5339
|
+
mimeType: Type5.Optional(
|
|
5340
|
+
Type5.String({
|
|
4686
5341
|
minLength: 1,
|
|
4687
5342
|
description: "Optional MIME type override (for example image/png)."
|
|
4688
5343
|
})
|
|
@@ -4695,7 +5350,7 @@ function createAttachFileTool(sandbox, hooks = {}) {
|
|
|
4695
5350
|
const fileBuffer = await sandbox.readFileToBuffer({ path: targetPath });
|
|
4696
5351
|
if (!fileBuffer) {
|
|
4697
5352
|
const generatedFile = hooks.getGeneratedFile?.(
|
|
4698
|
-
|
|
5353
|
+
path7.posix.basename(targetPath)
|
|
4699
5354
|
);
|
|
4700
5355
|
if (generatedFile) {
|
|
4701
5356
|
hooks.onGeneratedFiles?.([generatedFile]);
|
|
@@ -4742,8 +5397,95 @@ function createAttachFileTool(sandbox, hooks = {}) {
|
|
|
4742
5397
|
});
|
|
4743
5398
|
}
|
|
4744
5399
|
|
|
5400
|
+
// src/chat/tools/sandbox/list-dir.ts
|
|
5401
|
+
import path8 from "path";
|
|
5402
|
+
import { Type as Type6 } from "@sinclair/typebox";
|
|
5403
|
+
var DEFAULT_LIST_LIMIT = 500;
|
|
5404
|
+
async function listDir(params) {
|
|
5405
|
+
const dirPath = resolveWorkspacePath(params.path);
|
|
5406
|
+
const limit = positiveInteger(params.limit) ?? DEFAULT_LIST_LIMIT;
|
|
5407
|
+
const stat = await params.fs.stat(dirPath);
|
|
5408
|
+
if (!stat.isDirectory()) {
|
|
5409
|
+
throw new Error(`Not a directory: ${params.path ?? "."}`);
|
|
5410
|
+
}
|
|
5411
|
+
const entries = (await params.fs.readdir(dirPath)).sort(
|
|
5412
|
+
(a, b) => a.toLowerCase().localeCompare(b.toLowerCase())
|
|
5413
|
+
);
|
|
5414
|
+
const output = [];
|
|
5415
|
+
let entryLimitReached = false;
|
|
5416
|
+
for (const entry of entries) {
|
|
5417
|
+
if (output.length >= limit) {
|
|
5418
|
+
entryLimitReached = true;
|
|
5419
|
+
break;
|
|
5420
|
+
}
|
|
5421
|
+
const entryPath = path8.posix.join(dirPath, entry);
|
|
5422
|
+
try {
|
|
5423
|
+
const entryStat = await params.fs.stat(entryPath);
|
|
5424
|
+
output.push(`${entry}${entryStat.isDirectory() ? "/" : ""}`);
|
|
5425
|
+
} catch {
|
|
5426
|
+
continue;
|
|
5427
|
+
}
|
|
5428
|
+
}
|
|
5429
|
+
const bounded = truncateText(
|
|
5430
|
+
output.length > 0 ? output.join("\n") : "(empty directory)"
|
|
5431
|
+
);
|
|
5432
|
+
const notices = [];
|
|
5433
|
+
if (entryLimitReached) {
|
|
5434
|
+
notices.push(
|
|
5435
|
+
`${limit} entries limit reached. Use a higher limit to continue.`
|
|
5436
|
+
);
|
|
5437
|
+
}
|
|
5438
|
+
if (bounded.truncated) {
|
|
5439
|
+
notices.push(`${MAX_TEXT_CHARS} character output limit reached.`);
|
|
5440
|
+
}
|
|
5441
|
+
return {
|
|
5442
|
+
content: [
|
|
5443
|
+
{
|
|
5444
|
+
type: "text",
|
|
5445
|
+
text: notices.length > 0 ? `${bounded.content}
|
|
5446
|
+
|
|
5447
|
+
[${notices.join(" ")}]` : bounded.content
|
|
5448
|
+
}
|
|
5449
|
+
],
|
|
5450
|
+
details: {
|
|
5451
|
+
ok: true,
|
|
5452
|
+
path: params.path ?? ".",
|
|
5453
|
+
truncated: entryLimitReached || bounded.truncated,
|
|
5454
|
+
...entryLimitReached ? { entry_limit_reached: limit } : {}
|
|
5455
|
+
}
|
|
5456
|
+
};
|
|
5457
|
+
}
|
|
5458
|
+
function createListDirTool() {
|
|
5459
|
+
return tool({
|
|
5460
|
+
description: "List a sandbox workspace directory. Returns sorted entries with '/' suffixes for directories and bounded truncation notices.",
|
|
5461
|
+
annotations: { readOnlyHint: true, destructiveHint: false },
|
|
5462
|
+
inputSchema: Type6.Object(
|
|
5463
|
+
{
|
|
5464
|
+
path: Type6.Optional(
|
|
5465
|
+
Type6.String({
|
|
5466
|
+
minLength: 1,
|
|
5467
|
+
description: "Directory path in the sandbox workspace. Defaults to the workspace root."
|
|
5468
|
+
})
|
|
5469
|
+
),
|
|
5470
|
+
limit: Type6.Optional(
|
|
5471
|
+
Type6.Integer({
|
|
5472
|
+
minimum: 1,
|
|
5473
|
+
description: "Maximum entries to return. Defaults to 500."
|
|
5474
|
+
})
|
|
5475
|
+
)
|
|
5476
|
+
},
|
|
5477
|
+
{ additionalProperties: false }
|
|
5478
|
+
),
|
|
5479
|
+
execute: async () => {
|
|
5480
|
+
throw new Error(
|
|
5481
|
+
"listDir can only run when sandbox execution is enabled."
|
|
5482
|
+
);
|
|
5483
|
+
}
|
|
5484
|
+
});
|
|
5485
|
+
}
|
|
5486
|
+
|
|
4745
5487
|
// src/chat/tools/web/image-generate.ts
|
|
4746
|
-
import { Type as
|
|
5488
|
+
import { Type as Type7 } from "@sinclair/typebox";
|
|
4747
5489
|
var DEFAULT_IMAGE_MODEL = "google/gemini-3-pro-image";
|
|
4748
5490
|
var ENRICHMENT_SYSTEM_PROMPT = `You are an image prompt enrichment agent. Your job is to rewrite image generation requests to reflect a specific visual identity and mood.
|
|
4749
5491
|
|
|
@@ -4804,8 +5546,8 @@ function parseImageGenerationError(status, body, model) {
|
|
|
4804
5546
|
function createImageGenerateTool(hooks, deps = {}) {
|
|
4805
5547
|
return tool({
|
|
4806
5548
|
description: "Generate images from a prompt. Use when the user wants to visually show or represent something \u2014 feelings, concepts, art, humor, or any visual idea. Also use for explicit image creation requests.",
|
|
4807
|
-
inputSchema:
|
|
4808
|
-
prompt:
|
|
5549
|
+
inputSchema: Type7.Object({
|
|
5550
|
+
prompt: Type7.String({
|
|
4809
5551
|
minLength: 1,
|
|
4810
5552
|
maxLength: 4e3,
|
|
4811
5553
|
description: "Image generation prompt."
|
|
@@ -4888,7 +5630,7 @@ function createImageGenerateTool(hooks, deps = {}) {
|
|
|
4888
5630
|
}
|
|
4889
5631
|
|
|
4890
5632
|
// src/chat/tools/skill/call-mcp-tool.ts
|
|
4891
|
-
import { Type as
|
|
5633
|
+
import { Type as Type8 } from "@sinclair/typebox";
|
|
4892
5634
|
function resolveMcpArguments(input) {
|
|
4893
5635
|
const extraKeys = Object.keys(input).filter(
|
|
4894
5636
|
(key) => key !== "tool_name" && key !== "arguments"
|
|
@@ -4913,14 +5655,14 @@ function resolveMcpArguments(input) {
|
|
|
4913
5655
|
function createCallMcpToolTool(mcpToolManager, getActiveSkills) {
|
|
4914
5656
|
return tool({
|
|
4915
5657
|
description: "Call an active MCP tool by exact tool_name. Use loadSkill to activate the provider, then searchMcpTools to discover tool names and schemas; copy required provider fields into arguments. Do not call with only tool_name unless the discovered tool has no arguments. Authorization is handled by the runtime when required.",
|
|
4916
|
-
inputSchema:
|
|
5658
|
+
inputSchema: Type8.Object(
|
|
4917
5659
|
{
|
|
4918
|
-
tool_name:
|
|
5660
|
+
tool_name: Type8.String({
|
|
4919
5661
|
minLength: 1,
|
|
4920
5662
|
description: "Exact MCP tool_name from searchMcpTools."
|
|
4921
5663
|
}),
|
|
4922
|
-
arguments:
|
|
4923
|
-
|
|
5664
|
+
arguments: Type8.Optional(
|
|
5665
|
+
Type8.Record(Type8.String(), Type8.Unknown(), {
|
|
4924
5666
|
description: 'Arguments matching the disclosed MCP tool schema, for example { "query": "..." } when searchMcpTools shows query is required.'
|
|
4925
5667
|
})
|
|
4926
5668
|
)
|
|
@@ -4941,7 +5683,7 @@ function createCallMcpToolTool(mcpToolManager, getActiveSkills) {
|
|
|
4941
5683
|
}
|
|
4942
5684
|
|
|
4943
5685
|
// src/chat/tools/skill/load-skill.ts
|
|
4944
|
-
import { Type as
|
|
5686
|
+
import { Type as Type9 } from "@sinclair/typebox";
|
|
4945
5687
|
function toLoadedSkill(result, availableSkills) {
|
|
4946
5688
|
if (result.ok !== true || typeof result.skill_name !== "string" || typeof result.description !== "string" || typeof result.skill_dir !== "string" || typeof result.instructions !== "string") {
|
|
4947
5689
|
return null;
|
|
@@ -4988,8 +5730,8 @@ async function loadSkillFromHost(availableSkills, skillName) {
|
|
|
4988
5730
|
function createLoadSkillTool(availableSkills, options) {
|
|
4989
5731
|
return tool({
|
|
4990
5732
|
description: "Load a skill by name for this turn. The result includes working_directory; resolve skill paths there and run skill-owned bash commands from there or with absolute paths. When the result includes mcp_provider, use searchMcpTools before callMcpTool. Use when a request clearly matches a known skill.",
|
|
4991
|
-
inputSchema:
|
|
4992
|
-
skill_name:
|
|
5733
|
+
inputSchema: Type9.Object({
|
|
5734
|
+
skill_name: Type9.String({
|
|
4993
5735
|
minLength: 1,
|
|
4994
5736
|
description: "Skill name to load, without the leading slash."
|
|
4995
5737
|
})
|
|
@@ -5009,7 +5751,7 @@ function createLoadSkillTool(availableSkills, options) {
|
|
|
5009
5751
|
}
|
|
5010
5752
|
|
|
5011
5753
|
// src/chat/tools/skill/search-mcp-tools.ts
|
|
5012
|
-
import { Type as
|
|
5754
|
+
import { Type as Type10 } from "@sinclair/typebox";
|
|
5013
5755
|
|
|
5014
5756
|
// src/chat/tools/skill/mcp-tool-summary.ts
|
|
5015
5757
|
function getSchemaProperties(schema) {
|
|
@@ -5199,22 +5941,22 @@ function searchMcpCatalog(tools, query) {
|
|
|
5199
5941
|
function createSearchMcpToolsTool(mcpToolManager, getActiveSkills) {
|
|
5200
5942
|
return tool({
|
|
5201
5943
|
description: "List or search active MCP tools and return full descriptors, including input/output schemas and annotations. Use after loadSkill when choosing a provider tool or when callMcpTool arguments are unclear.",
|
|
5202
|
-
inputSchema:
|
|
5944
|
+
inputSchema: Type10.Object(
|
|
5203
5945
|
{
|
|
5204
|
-
query:
|
|
5205
|
-
|
|
5946
|
+
query: Type10.Optional(
|
|
5947
|
+
Type10.String({
|
|
5206
5948
|
minLength: 1,
|
|
5207
5949
|
description: "Optional search terms describing the MCP tool or arguments needed."
|
|
5208
5950
|
})
|
|
5209
5951
|
),
|
|
5210
|
-
provider:
|
|
5211
|
-
|
|
5952
|
+
provider: Type10.Optional(
|
|
5953
|
+
Type10.String({
|
|
5212
5954
|
minLength: 1,
|
|
5213
5955
|
description: "Optional provider name to list or search within."
|
|
5214
5956
|
})
|
|
5215
5957
|
),
|
|
5216
|
-
max_results:
|
|
5217
|
-
|
|
5958
|
+
max_results: Type10.Optional(
|
|
5959
|
+
Type10.Integer({
|
|
5218
5960
|
minimum: 1,
|
|
5219
5961
|
maximum: MAX_RESULTS,
|
|
5220
5962
|
description: "Maximum matching tool descriptors to return."
|
|
@@ -5245,16 +5987,55 @@ function createSearchMcpToolsTool(mcpToolManager, getActiveSkills) {
|
|
|
5245
5987
|
}
|
|
5246
5988
|
|
|
5247
5989
|
// src/chat/tools/sandbox/read-file.ts
|
|
5248
|
-
import { Type as
|
|
5990
|
+
import { Type as Type11 } from "@sinclair/typebox";
|
|
5991
|
+
var DEFAULT_READ_LIMIT = 1e3;
|
|
5992
|
+
function sliceFileContent(params) {
|
|
5993
|
+
const normalized = normalizeToLf(params.content);
|
|
5994
|
+
const lines = normalized.length === 0 ? [] : normalized.split("\n");
|
|
5995
|
+
const requestedOffset = positiveInteger(params.offset);
|
|
5996
|
+
const requestedLimit = positiveInteger(params.limit);
|
|
5997
|
+
const startLine = requestedOffset ?? 1;
|
|
5998
|
+
const maxLines = requestedLimit ?? DEFAULT_READ_LIMIT;
|
|
5999
|
+
const startIndex = Math.min(lines.length, startLine - 1);
|
|
6000
|
+
const selected = lines.slice(startIndex, startIndex + maxLines);
|
|
6001
|
+
const endLine = selected.length > 0 ? startLine + selected.length - 1 : startLine - 1;
|
|
6002
|
+
const truncated = startIndex > 0 || endLine < lines.length;
|
|
6003
|
+
const rangeRequested = requestedOffset !== void 0 || requestedLimit !== void 0;
|
|
6004
|
+
return {
|
|
6005
|
+
content: !rangeRequested && !truncated ? params.content : selected.join("\n"),
|
|
6006
|
+
end_line: selected.length > 0 ? endLine : void 0,
|
|
6007
|
+
path: params.path,
|
|
6008
|
+
start_line: startLine,
|
|
6009
|
+
success: true,
|
|
6010
|
+
total_lines: lines.length,
|
|
6011
|
+
truncated,
|
|
6012
|
+
...endLine < lines.length ? {
|
|
6013
|
+
continuation: `Read more with offset=${endLine + 1} and limit=${maxLines}.`
|
|
6014
|
+
} : {}
|
|
6015
|
+
};
|
|
6016
|
+
}
|
|
5249
6017
|
function createReadFileTool() {
|
|
5250
6018
|
return tool({
|
|
5251
|
-
description: "Read a
|
|
5252
|
-
|
|
6019
|
+
description: "Read a bounded line range from a file in the sandbox workspace. Use when you need exact file contents to verify facts or make edits safely. Prefer grep/findFiles/listDir for broad discovery.",
|
|
6020
|
+
annotations: { readOnlyHint: true, destructiveHint: false },
|
|
6021
|
+
inputSchema: Type11.Object(
|
|
5253
6022
|
{
|
|
5254
|
-
path:
|
|
6023
|
+
path: Type11.String({
|
|
5255
6024
|
minLength: 1,
|
|
5256
6025
|
description: "Path to the file in the sandbox workspace."
|
|
5257
|
-
})
|
|
6026
|
+
}),
|
|
6027
|
+
offset: Type11.Optional(
|
|
6028
|
+
Type11.Integer({
|
|
6029
|
+
minimum: 1,
|
|
6030
|
+
description: "1-indexed line number to start reading from."
|
|
6031
|
+
})
|
|
6032
|
+
),
|
|
6033
|
+
limit: Type11.Optional(
|
|
6034
|
+
Type11.Integer({
|
|
6035
|
+
minimum: 1,
|
|
6036
|
+
description: "Maximum number of lines to read. Defaults to 1000."
|
|
6037
|
+
})
|
|
6038
|
+
)
|
|
5258
6039
|
},
|
|
5259
6040
|
{ additionalProperties: false }
|
|
5260
6041
|
),
|
|
@@ -5267,12 +6048,12 @@ function createReadFileTool() {
|
|
|
5267
6048
|
}
|
|
5268
6049
|
|
|
5269
6050
|
// src/chat/tools/runtime/report-progress.ts
|
|
5270
|
-
import { Type as
|
|
6051
|
+
import { Type as Type12 } from "@sinclair/typebox";
|
|
5271
6052
|
function createReportProgressTool() {
|
|
5272
6053
|
return tool({
|
|
5273
6054
|
description: "Update the user-visible assistant loading message with a short progress phase. For every non-trivial turn, call this early with the initial major work phase, then call it again only when the major phase meaningfully changes. Messages must be written in sentence case with a present-participle verb (e.g. 'Searching docs', 'Reviewing results', 'Running checks'). Skip trivial direct answers, generic filler, and minor substeps.",
|
|
5274
|
-
inputSchema:
|
|
5275
|
-
message:
|
|
6055
|
+
inputSchema: Type12.Object({
|
|
6056
|
+
message: Type12.String({
|
|
5276
6057
|
minLength: 1,
|
|
5277
6058
|
description: "Short user-facing progress message."
|
|
5278
6059
|
})
|
|
@@ -5281,7 +6062,7 @@ function createReportProgressTool() {
|
|
|
5281
6062
|
}
|
|
5282
6063
|
|
|
5283
6064
|
// src/chat/tools/slack/channel-list-messages.ts
|
|
5284
|
-
import { Type as
|
|
6065
|
+
import { Type as Type13 } from "@sinclair/typebox";
|
|
5285
6066
|
|
|
5286
6067
|
// src/chat/slack/channel.ts
|
|
5287
6068
|
async function listChannelMessages(input) {
|
|
@@ -5370,39 +6151,40 @@ async function listThreadReplies(input) {
|
|
|
5370
6151
|
function createSlackChannelListMessagesTool(context) {
|
|
5371
6152
|
return tool({
|
|
5372
6153
|
description: "List channel messages from Slack history in the active channel context. Use when the user asks for recent or historical channel context outside this thread. Do not use for live monitoring or when current thread context already answers the question.",
|
|
5373
|
-
|
|
5374
|
-
|
|
5375
|
-
|
|
6154
|
+
annotations: { readOnlyHint: true, destructiveHint: false },
|
|
6155
|
+
inputSchema: Type13.Object({
|
|
6156
|
+
limit: Type13.Optional(
|
|
6157
|
+
Type13.Integer({
|
|
5376
6158
|
minimum: 1,
|
|
5377
6159
|
maximum: 1e3,
|
|
5378
6160
|
description: "Maximum number of messages to return across pages."
|
|
5379
6161
|
})
|
|
5380
6162
|
),
|
|
5381
|
-
cursor:
|
|
5382
|
-
|
|
6163
|
+
cursor: Type13.Optional(
|
|
6164
|
+
Type13.String({
|
|
5383
6165
|
minLength: 1,
|
|
5384
6166
|
description: "Optional cursor to continue from a prior call."
|
|
5385
6167
|
})
|
|
5386
6168
|
),
|
|
5387
|
-
oldest:
|
|
5388
|
-
|
|
6169
|
+
oldest: Type13.Optional(
|
|
6170
|
+
Type13.String({
|
|
5389
6171
|
minLength: 1,
|
|
5390
6172
|
description: "Optional oldest message timestamp (Slack ts) for range filtering."
|
|
5391
6173
|
})
|
|
5392
6174
|
),
|
|
5393
|
-
latest:
|
|
5394
|
-
|
|
6175
|
+
latest: Type13.Optional(
|
|
6176
|
+
Type13.String({
|
|
5395
6177
|
minLength: 1,
|
|
5396
6178
|
description: "Optional latest message timestamp (Slack ts) for range filtering."
|
|
5397
6179
|
})
|
|
5398
6180
|
),
|
|
5399
|
-
inclusive:
|
|
5400
|
-
|
|
6181
|
+
inclusive: Type13.Optional(
|
|
6182
|
+
Type13.Boolean({
|
|
5401
6183
|
description: "Whether oldest/latest bounds should be inclusive."
|
|
5402
6184
|
})
|
|
5403
6185
|
),
|
|
5404
|
-
max_pages:
|
|
5405
|
-
|
|
6186
|
+
max_pages: Type13.Optional(
|
|
6187
|
+
Type13.Integer({
|
|
5406
6188
|
minimum: 1,
|
|
5407
6189
|
maximum: 10,
|
|
5408
6190
|
description: "Maximum number of API pages to traverse in a single call."
|
|
@@ -5456,7 +6238,7 @@ function createSlackChannelListMessagesTool(context) {
|
|
|
5456
6238
|
}
|
|
5457
6239
|
|
|
5458
6240
|
// src/chat/tools/slack/channel-post-message.ts
|
|
5459
|
-
import { Type as
|
|
6241
|
+
import { Type as Type14 } from "@sinclair/typebox";
|
|
5460
6242
|
|
|
5461
6243
|
// src/chat/tools/idempotency.ts
|
|
5462
6244
|
function stableSerialize(value) {
|
|
@@ -5478,8 +6260,8 @@ function createOperationKey(toolName, input) {
|
|
|
5478
6260
|
function createSlackChannelPostMessageTool(context, state) {
|
|
5479
6261
|
return tool({
|
|
5480
6262
|
description: "Post a message in the active Slack channel context (outside the thread). Use this only when the user explicitly asks to post/send/share/say something in the current channel. Do not use it for normal thread replies, speculative broadcasts, or requests targeting another named channel; explain that limitation instead. Do not claim a channel message was posted unless this tool succeeds in this turn.",
|
|
5481
|
-
inputSchema:
|
|
5482
|
-
text:
|
|
6263
|
+
inputSchema: Type14.Object({
|
|
6264
|
+
text: Type14.String({
|
|
5483
6265
|
minLength: 1,
|
|
5484
6266
|
maxLength: 4e4,
|
|
5485
6267
|
description: "Slack mrkdwn text to post."
|
|
@@ -5522,12 +6304,12 @@ function createSlackChannelPostMessageTool(context, state) {
|
|
|
5522
6304
|
}
|
|
5523
6305
|
|
|
5524
6306
|
// src/chat/tools/slack/message-add-reaction.ts
|
|
5525
|
-
import { Type as
|
|
6307
|
+
import { Type as Type15 } from "@sinclair/typebox";
|
|
5526
6308
|
function createSlackMessageAddReactionTool(context, state) {
|
|
5527
6309
|
return tool({
|
|
5528
6310
|
description: "Add an emoji reaction to the current inbound Slack message. Use sparingly for lightweight acknowledgements. Provide a Slack emoji alias name (for example `thumbsup`, `white_check_mark`, or `thumbsup::skin-tone-6`), not a unicode emoji glyph. The target message is injected by runtime context; do not use this for arbitrary historical messages.",
|
|
5529
|
-
inputSchema:
|
|
5530
|
-
emoji:
|
|
6311
|
+
inputSchema: Type15.Object({
|
|
6312
|
+
emoji: Type15.String({
|
|
5531
6313
|
minLength: 1,
|
|
5532
6314
|
maxLength: 64,
|
|
5533
6315
|
description: "Slack emoji alias name to react with (for example `thumbsup`, `white_check_mark`, or `thumbsup::skin-tone-6`). Optional surrounding colons are allowed."
|
|
@@ -5585,7 +6367,7 @@ function createSlackMessageAddReactionTool(context, state) {
|
|
|
5585
6367
|
}
|
|
5586
6368
|
|
|
5587
6369
|
// src/chat/tools/slack/canvas-tools.ts
|
|
5588
|
-
import { Type as
|
|
6370
|
+
import { Type as Type16 } from "@sinclair/typebox";
|
|
5589
6371
|
|
|
5590
6372
|
// src/chat/tools/slack/canvases.ts
|
|
5591
6373
|
function normalizeCanvasMarkdown(markdown) {
|
|
@@ -5801,13 +6583,13 @@ function mergeRecentCanvases(existing, created) {
|
|
|
5801
6583
|
function createSlackCanvasCreateTool(context, state) {
|
|
5802
6584
|
return tool({
|
|
5803
6585
|
description: "Create a Slack canvas for long-form output in the active assistant context channel. Use when the answer is better as a reusable document than a thread reply: long-form research, timelines, bios/profiles, structured notes, plans, comparisons, or anything likely to exceed one compact Slack reply. After creating it, reply with one or two short sentences plus the canvas link; do not recap the canvas contents. Do not use for short answers that fit cleanly in one normal thread reply.",
|
|
5804
|
-
inputSchema:
|
|
5805
|
-
title:
|
|
6586
|
+
inputSchema: Type16.Object({
|
|
6587
|
+
title: Type16.String({
|
|
5806
6588
|
minLength: 1,
|
|
5807
6589
|
maxLength: 160,
|
|
5808
6590
|
description: "Canvas title."
|
|
5809
6591
|
}),
|
|
5810
|
-
markdown:
|
|
6592
|
+
markdown: Type16.String({
|
|
5811
6593
|
minLength: 1,
|
|
5812
6594
|
description: "Canvas markdown body content."
|
|
5813
6595
|
})
|
|
@@ -5873,29 +6655,29 @@ function createSlackCanvasCreateTool(context, state) {
|
|
|
5873
6655
|
function createSlackCanvasUpdateTool(state, _context) {
|
|
5874
6656
|
return tool({
|
|
5875
6657
|
description: "Update the active Slack canvas tracked in artifact context. Use when continuing or correcting a document already tracked in this thread. Do not use to create a brand-new long-form artifact.",
|
|
5876
|
-
inputSchema:
|
|
5877
|
-
markdown:
|
|
6658
|
+
inputSchema: Type16.Object({
|
|
6659
|
+
markdown: Type16.String({
|
|
5878
6660
|
minLength: 1,
|
|
5879
6661
|
description: "Markdown content to insert or use as replacement text."
|
|
5880
6662
|
}),
|
|
5881
|
-
operation:
|
|
5882
|
-
|
|
6663
|
+
operation: Type16.Optional(
|
|
6664
|
+
Type16.Union(
|
|
5883
6665
|
[
|
|
5884
|
-
|
|
5885
|
-
|
|
5886
|
-
|
|
6666
|
+
Type16.Literal("insert_at_end"),
|
|
6667
|
+
Type16.Literal("insert_at_start"),
|
|
6668
|
+
Type16.Literal("replace")
|
|
5887
6669
|
],
|
|
5888
6670
|
{ description: "Canvas update mode." }
|
|
5889
6671
|
)
|
|
5890
6672
|
),
|
|
5891
|
-
section_id:
|
|
5892
|
-
|
|
6673
|
+
section_id: Type16.Optional(
|
|
6674
|
+
Type16.String({
|
|
5893
6675
|
minLength: 1,
|
|
5894
6676
|
description: "Optional section ID required for targeted replace operations."
|
|
5895
6677
|
})
|
|
5896
6678
|
),
|
|
5897
|
-
section_contains_text:
|
|
5898
|
-
|
|
6679
|
+
section_contains_text: Type16.Optional(
|
|
6680
|
+
Type16.String({
|
|
5899
6681
|
minLength: 1,
|
|
5900
6682
|
description: "Optional helper text used to find the target section when section_id is not provided."
|
|
5901
6683
|
})
|
|
@@ -5961,8 +6743,9 @@ function createSlackCanvasUpdateTool(state, _context) {
|
|
|
5961
6743
|
function createSlackCanvasReadTool() {
|
|
5962
6744
|
return tool({
|
|
5963
6745
|
description: "Read a Slack canvas the bot has access to (including canvases the bot created) by canvas ID or Slack canvas/docs URL. Use when the user shares a Slack canvas link (https://*.slack.com/docs/... or /canvas/...) or references a canvas ID and you need its contents. Do not use for generic web pages \u2014 use webFetch for those.",
|
|
5964
|
-
|
|
5965
|
-
|
|
6746
|
+
annotations: { readOnlyHint: true, destructiveHint: false },
|
|
6747
|
+
inputSchema: Type16.Object({
|
|
6748
|
+
canvas: Type16.String({
|
|
5966
6749
|
minLength: 1,
|
|
5967
6750
|
description: "Canvas/file ID (e.g. `F0ABCDEF`) or Slack canvas/docs URL (e.g. `https://team.slack.com/docs/T.../F...`)."
|
|
5968
6751
|
})
|
|
@@ -6012,7 +6795,7 @@ function createSlackCanvasReadTool() {
|
|
|
6012
6795
|
}
|
|
6013
6796
|
|
|
6014
6797
|
// src/chat/tools/slack/list-tools.ts
|
|
6015
|
-
import { Type as
|
|
6798
|
+
import { Type as Type17 } from "@sinclair/typebox";
|
|
6016
6799
|
|
|
6017
6800
|
// src/chat/tools/slack/lists.ts
|
|
6018
6801
|
function normalizeKey(value) {
|
|
@@ -6186,8 +6969,8 @@ async function updateListItem(input) {
|
|
|
6186
6969
|
function createSlackListCreateTool(state) {
|
|
6187
6970
|
return tool({
|
|
6188
6971
|
description: "Create a Slack todo list for action tracking. Use when the user needs structured tasks with ownership/completion tracking. Do not use for one-off notes without task management needs.",
|
|
6189
|
-
inputSchema:
|
|
6190
|
-
name:
|
|
6972
|
+
inputSchema: Type17.Object({
|
|
6973
|
+
name: Type17.String({
|
|
6191
6974
|
minLength: 1,
|
|
6192
6975
|
maxLength: 160,
|
|
6193
6976
|
description: "Name for the new Slack list."
|
|
@@ -6222,20 +7005,20 @@ function createSlackListCreateTool(state) {
|
|
|
6222
7005
|
function createSlackListAddItemsTool(state) {
|
|
6223
7006
|
return tool({
|
|
6224
7007
|
description: "Add tasks to the active Slack list tracked in artifact context. Use when the user wants actionable items recorded in the current thread list. Do not use when no list exists and list creation was not requested.",
|
|
6225
|
-
inputSchema:
|
|
6226
|
-
items:
|
|
7008
|
+
inputSchema: Type17.Object({
|
|
7009
|
+
items: Type17.Array(Type17.String({ minLength: 1 }), {
|
|
6227
7010
|
minItems: 1,
|
|
6228
7011
|
maxItems: 25,
|
|
6229
7012
|
description: "List item titles to create."
|
|
6230
7013
|
}),
|
|
6231
|
-
assignee_user_id:
|
|
6232
|
-
|
|
7014
|
+
assignee_user_id: Type17.Optional(
|
|
7015
|
+
Type17.String({
|
|
6233
7016
|
minLength: 1,
|
|
6234
7017
|
description: "Optional Slack user ID assigned to all created items."
|
|
6235
7018
|
})
|
|
6236
7019
|
),
|
|
6237
|
-
due_date:
|
|
6238
|
-
|
|
7020
|
+
due_date: Type17.Optional(
|
|
7021
|
+
Type17.String({
|
|
6239
7022
|
pattern: "^\\d{4}-\\d{2}-\\d{2}$",
|
|
6240
7023
|
description: "Optional due date in YYYY-MM-DD format."
|
|
6241
7024
|
})
|
|
@@ -6284,9 +7067,10 @@ function createSlackListAddItemsTool(state) {
|
|
|
6284
7067
|
function createSlackListGetItemsTool(state) {
|
|
6285
7068
|
return tool({
|
|
6286
7069
|
description: "Read items from the active Slack list tracked in artifact context. Use when the user asks for task status, open items, or list contents. Do not use when list state is already known from the immediately prior result.",
|
|
6287
|
-
|
|
6288
|
-
|
|
6289
|
-
|
|
7070
|
+
annotations: { readOnlyHint: true, destructiveHint: false },
|
|
7071
|
+
inputSchema: Type17.Object({
|
|
7072
|
+
limit: Type17.Optional(
|
|
7073
|
+
Type17.Integer({
|
|
6290
7074
|
minimum: 1,
|
|
6291
7075
|
maximum: 200,
|
|
6292
7076
|
description: "Maximum number of list items to return."
|
|
@@ -6311,19 +7095,19 @@ function createSlackListGetItemsTool(state) {
|
|
|
6311
7095
|
function createSlackListUpdateItemTool(state) {
|
|
6312
7096
|
return tool({
|
|
6313
7097
|
description: "Update an item in the active Slack list tracked in artifact context (title/completion). Use when the user asks to mark progress or rename a tracked task. Do not use to add new tasks.",
|
|
6314
|
-
inputSchema:
|
|
7098
|
+
inputSchema: Type17.Object(
|
|
6315
7099
|
{
|
|
6316
|
-
item_id:
|
|
7100
|
+
item_id: Type17.String({
|
|
6317
7101
|
minLength: 1,
|
|
6318
7102
|
description: "ID of the Slack list item to update."
|
|
6319
7103
|
}),
|
|
6320
|
-
completed:
|
|
6321
|
-
|
|
7104
|
+
completed: Type17.Optional(
|
|
7105
|
+
Type17.Boolean({
|
|
6322
7106
|
description: "Optional completion status update."
|
|
6323
7107
|
})
|
|
6324
7108
|
),
|
|
6325
|
-
title:
|
|
6326
|
-
|
|
7109
|
+
title: Type17.Optional(
|
|
7110
|
+
Type17.String({
|
|
6327
7111
|
minLength: 1,
|
|
6328
7112
|
description: "Optional new item title."
|
|
6329
7113
|
})
|
|
@@ -6373,11 +7157,12 @@ function createSlackListUpdateItemTool(state) {
|
|
|
6373
7157
|
}
|
|
6374
7158
|
|
|
6375
7159
|
// src/chat/tools/system-time.ts
|
|
6376
|
-
import { Type as
|
|
7160
|
+
import { Type as Type18 } from "@sinclair/typebox";
|
|
6377
7161
|
function createSystemTimeTool() {
|
|
6378
7162
|
return tool({
|
|
6379
7163
|
description: "Return current system time in UTC and local ISO formats. Use when the user asks for current time/date context. Do not use as a substitute for historical or timezone-conversion research.",
|
|
6380
|
-
|
|
7164
|
+
annotations: { readOnlyHint: true, destructiveHint: false },
|
|
7165
|
+
inputSchema: Type18.Object({}),
|
|
6381
7166
|
execute: async () => {
|
|
6382
7167
|
const now = /* @__PURE__ */ new Date();
|
|
6383
7168
|
return {
|
|
@@ -6395,7 +7180,7 @@ function createSystemTimeTool() {
|
|
|
6395
7180
|
import {
|
|
6396
7181
|
Agent
|
|
6397
7182
|
} from "@mariozechner/pi-agent-core";
|
|
6398
|
-
import { Type as
|
|
7183
|
+
import { Type as Type19 } from "@sinclair/typebox";
|
|
6399
7184
|
|
|
6400
7185
|
// src/chat/respond-helpers.ts
|
|
6401
7186
|
var MAX_INLINE_ATTACHMENT_BASE64_CHARS = 12e4;
|
|
@@ -6533,12 +7318,6 @@ function encodeNonImageAttachmentForPrompt(attachment) {
|
|
|
6533
7318
|
"</attachment>"
|
|
6534
7319
|
].join("\n");
|
|
6535
7320
|
}
|
|
6536
|
-
function buildExecutionFailureMessage(toolErrorCount) {
|
|
6537
|
-
if (toolErrorCount > 0) {
|
|
6538
|
-
return "I couldn't complete this because one or more required tools failed in this turn. I've logged the failure details.";
|
|
6539
|
-
}
|
|
6540
|
-
return "I couldn't complete this request in this turn due to an execution failure. I've logged the details for debugging.";
|
|
6541
|
-
}
|
|
6542
7321
|
function isToolResultMessage(value) {
|
|
6543
7322
|
return typeof value === "object" && value !== null && value.role === "toolResult";
|
|
6544
7323
|
}
|
|
@@ -6635,23 +7414,13 @@ function createStateAdvisorSessionStore() {
|
|
|
6635
7414
|
}
|
|
6636
7415
|
|
|
6637
7416
|
// src/chat/tools/advisor/tool.ts
|
|
6638
|
-
var
|
|
6639
|
-
"bash",
|
|
6640
|
-
"readFile",
|
|
6641
|
-
"searchMcpTools",
|
|
6642
|
-
"slackCanvasRead",
|
|
6643
|
-
"slackChannelListMessages",
|
|
6644
|
-
"slackListGetItems",
|
|
6645
|
-
"systemTime",
|
|
6646
|
-
"webFetch",
|
|
6647
|
-
"webSearch"
|
|
6648
|
-
]);
|
|
6649
|
-
var ADVISOR_TOOL_DESCRIPTION = "Ask a stronger advisor for deep technical guidance. Call this when the task has a hard reasoning core: algorithm design, architecture, concurrency, security-sensitive logic, data modeling, unclear requirements, repeated failures, difficult debugging, broad refactors, or final review of nontrivial work. Pass a focused question plus curated context containing the exact evidence, constraints, current plan, alternatives, command output, code snippets, or diffs the advisor should start from. The advisor does not automatically receive the parent transcript, keeps its own advisor history for this parent conversation, can use inspection tools to verify evidence, can reason deeply, and returns guidance for you to apply and verify. Follow-up calls can build on prior advisor guidance but must include any new evidence or changed constraints. Use it after initial orientation reads when repository context matters, before committing to a non-obvious implementation plan, when changing approach, when stuck, and before declaring complex work complete. Do not use it for greetings, simple deterministic edits, routine formatting, or tasks where the next action is already obvious from fresh tool output.";
|
|
7417
|
+
var ADVISOR_TOOL_DESCRIPTION = "Ask a stronger advisor for deep technical guidance. Call this when the task has a hard reasoning core: algorithm design, architecture, concurrency, security-sensitive logic, data modeling, unclear requirements, repeated failures, difficult debugging, broad refactors, or final review of nontrivial work. Pass a focused question plus curated context containing the exact evidence, constraints, current plan, alternatives, command output, code snippets, or diffs the advisor should start from. The advisor does not automatically receive the parent transcript, keeps its own advisor history for this parent conversation, can use read-only inspection tools to verify evidence, can reason deeply, and returns guidance for you to apply and verify. Follow-up calls can build on prior advisor guidance but must include any new evidence or changed constraints. Use it after initial orientation reads when repository context matters, before committing to a non-obvious implementation plan, when changing approach, when stuck, and before declaring complex work complete. Do not use it for greetings, simple deterministic edits, routine formatting, or tasks where the next action is already obvious from fresh tool output.";
|
|
6650
7418
|
var ADVISOR_SYSTEM_PROMPT = [
|
|
6651
7419
|
"You are a senior technical advisor for the executor.",
|
|
6652
|
-
"Analyze the executor-supplied context deeply. Use
|
|
7420
|
+
"Analyze the executor-supplied context deeply. Use read-only tools when direct inspection or verification would materially improve the advice.",
|
|
6653
7421
|
"Distinguish evidence from inference. Treat the advisor task as the focus for this call and the executor context as the starting evidence packet.",
|
|
6654
7422
|
"Do not assume access to parent transcript or tool output that was not included or gathered in this advisor call.",
|
|
7423
|
+
"Use only the read-only tools provided to you.",
|
|
6655
7424
|
"Do not make user-visible side effects, post Slack messages, or mutate files. If a mutating action is needed, recommend it to the executor instead.",
|
|
6656
7425
|
"Identify the hard part, recommend a concrete plan or correction, call out blocking risks, and propose focused verification.",
|
|
6657
7426
|
"If the supplied context is insufficient, say exactly what additional evidence the executor needs to gather before acting.",
|
|
@@ -6684,20 +7453,27 @@ function success(memo) {
|
|
|
6684
7453
|
}
|
|
6685
7454
|
};
|
|
6686
7455
|
}
|
|
6687
|
-
function
|
|
6688
|
-
return
|
|
7456
|
+
function hasReadOnlyToolAnnotations(annotations) {
|
|
7457
|
+
return annotations?.readOnlyHint === true && annotations.destructiveHint !== true;
|
|
7458
|
+
}
|
|
7459
|
+
function createAdvisorToolDefinitions(definitions) {
|
|
7460
|
+
return Object.fromEntries(
|
|
7461
|
+
Object.entries(definitions).filter(
|
|
7462
|
+
([name, definition]) => name !== "callMcpTool" && name !== "searchMcpTools" && hasReadOnlyToolAnnotations(definition.annotations)
|
|
7463
|
+
)
|
|
7464
|
+
);
|
|
6689
7465
|
}
|
|
6690
7466
|
function createAdvisorTool(context) {
|
|
6691
7467
|
const store = context.store ?? createStateAdvisorSessionStore();
|
|
6692
7468
|
const spanContext = context.logContext ?? {};
|
|
6693
7469
|
return tool({
|
|
6694
7470
|
description: ADVISOR_TOOL_DESCRIPTION,
|
|
6695
|
-
inputSchema:
|
|
6696
|
-
question:
|
|
7471
|
+
inputSchema: Type19.Object({
|
|
7472
|
+
question: Type19.String({
|
|
6697
7473
|
minLength: 1,
|
|
6698
7474
|
description: "Focused advisor question or decision point."
|
|
6699
7475
|
}),
|
|
6700
|
-
context:
|
|
7476
|
+
context: Type19.String({
|
|
6701
7477
|
minLength: 1,
|
|
6702
7478
|
description: "Curated evidence packet: relevant requirements, constraints, current plan, alternatives, code snippets, diffs, command output, and open questions."
|
|
6703
7479
|
})
|
|
@@ -6809,7 +7585,7 @@ function createAdvisorTool(context) {
|
|
|
6809
7585
|
}
|
|
6810
7586
|
|
|
6811
7587
|
// src/chat/tools/web/fetch-tool.ts
|
|
6812
|
-
import { Type as
|
|
7588
|
+
import { Type as Type20 } from "@sinclair/typebox";
|
|
6813
7589
|
|
|
6814
7590
|
// src/chat/tools/web/constants.ts
|
|
6815
7591
|
var USER_AGENT = "junior-bot/0.1";
|
|
@@ -7011,13 +7787,16 @@ async function assertPublicUrl(rawUrl) {
|
|
|
7011
7787
|
}
|
|
7012
7788
|
return parsed;
|
|
7013
7789
|
}
|
|
7014
|
-
async function withTimeout(task, timeoutMs, label) {
|
|
7790
|
+
async function withTimeout(task, timeoutMs, label, options) {
|
|
7015
7791
|
let timer;
|
|
7016
7792
|
const timeoutPromise = new Promise((_, reject) => {
|
|
7017
|
-
timer = setTimeout(
|
|
7018
|
-
|
|
7019
|
-
|
|
7020
|
-
|
|
7793
|
+
timer = setTimeout(() => {
|
|
7794
|
+
reject(new Error(`${label} timed out`));
|
|
7795
|
+
try {
|
|
7796
|
+
options?.onTimeout?.();
|
|
7797
|
+
} catch {
|
|
7798
|
+
}
|
|
7799
|
+
}, timeoutMs);
|
|
7021
7800
|
});
|
|
7022
7801
|
try {
|
|
7023
7802
|
return await Promise.race([task, timeoutPromise]);
|
|
@@ -7154,13 +7933,18 @@ function extractHttpStatusFromMessage(message) {
|
|
|
7154
7933
|
function createWebFetchTool(hooks) {
|
|
7155
7934
|
return tool({
|
|
7156
7935
|
description: "Fetch and extract readable content from a specific URL. Use when you need details from a known page or document. Do not use for discovery when search is the first step.",
|
|
7157
|
-
|
|
7158
|
-
|
|
7936
|
+
annotations: {
|
|
7937
|
+
readOnlyHint: true,
|
|
7938
|
+
destructiveHint: false,
|
|
7939
|
+
openWorldHint: true
|
|
7940
|
+
},
|
|
7941
|
+
inputSchema: Type20.Object({
|
|
7942
|
+
url: Type20.String({
|
|
7159
7943
|
minLength: 1,
|
|
7160
7944
|
description: "HTTP(S) URL to fetch."
|
|
7161
7945
|
}),
|
|
7162
|
-
max_chars:
|
|
7163
|
-
|
|
7946
|
+
max_chars: Type20.Optional(
|
|
7947
|
+
Type20.Integer({
|
|
7164
7948
|
minimum: 500,
|
|
7165
7949
|
maximum: MAX_FETCH_CHARS,
|
|
7166
7950
|
description: "Optional maximum number of extracted characters to return."
|
|
@@ -7220,7 +8004,7 @@ function createWebFetchTool(hooks) {
|
|
|
7220
8004
|
// src/chat/tools/web/search.ts
|
|
7221
8005
|
import { generateText } from "ai";
|
|
7222
8006
|
import { createGatewayProvider } from "@ai-sdk/gateway";
|
|
7223
|
-
import { Type as
|
|
8007
|
+
import { Type as Type21 } from "@sinclair/typebox";
|
|
7224
8008
|
var SEARCH_TIMEOUT_MS = 6e4;
|
|
7225
8009
|
var MAX_RESULTS2 = 5;
|
|
7226
8010
|
var DEFAULT_SEARCH_MODEL = "xai/grok-4-fast-reasoning";
|
|
@@ -7263,14 +8047,19 @@ function isAuthFailure(message) {
|
|
|
7263
8047
|
function createWebSearchTool() {
|
|
7264
8048
|
return tool({
|
|
7265
8049
|
description: "Search public web sources and return top snippets/URLs. Use when you need discovery or source candidates. Do not use when the user already provided a specific URL to inspect.",
|
|
7266
|
-
|
|
7267
|
-
|
|
8050
|
+
annotations: {
|
|
8051
|
+
readOnlyHint: true,
|
|
8052
|
+
destructiveHint: false,
|
|
8053
|
+
openWorldHint: true
|
|
8054
|
+
},
|
|
8055
|
+
inputSchema: Type21.Object({
|
|
8056
|
+
query: Type21.String({
|
|
7268
8057
|
minLength: 1,
|
|
7269
8058
|
maxLength: 500,
|
|
7270
8059
|
description: "Search query."
|
|
7271
8060
|
}),
|
|
7272
|
-
max_results:
|
|
7273
|
-
|
|
8061
|
+
max_results: Type21.Optional(
|
|
8062
|
+
Type21.Integer({
|
|
7274
8063
|
minimum: 1,
|
|
7275
8064
|
maximum: MAX_RESULTS2,
|
|
7276
8065
|
description: "Max results to return."
|
|
@@ -7280,6 +8069,7 @@ function createWebSearchTool() {
|
|
|
7280
8069
|
execute: async ({ query, max_results }) => {
|
|
7281
8070
|
const maxResults = max_results ?? 3;
|
|
7282
8071
|
const model = process.env.AI_WEB_SEARCH_MODEL ?? DEFAULT_SEARCH_MODEL;
|
|
8072
|
+
const controller = new AbortController();
|
|
7283
8073
|
try {
|
|
7284
8074
|
const provider = createGatewayProvider();
|
|
7285
8075
|
const response = await withTimeout(
|
|
@@ -7292,10 +8082,12 @@ function createWebSearchTool() {
|
|
|
7292
8082
|
maxResults
|
|
7293
8083
|
})
|
|
7294
8084
|
},
|
|
7295
|
-
toolChoice: { type: "tool", toolName: SEARCH_TOOL_NAME }
|
|
8085
|
+
toolChoice: { type: "tool", toolName: SEARCH_TOOL_NAME },
|
|
8086
|
+
abortSignal: controller.signal
|
|
7296
8087
|
}),
|
|
7297
8088
|
SEARCH_TIMEOUT_MS,
|
|
7298
|
-
"webSearch"
|
|
8089
|
+
"webSearch",
|
|
8090
|
+
{ onTimeout: () => controller.abort() }
|
|
7299
8091
|
);
|
|
7300
8092
|
const results = parseSearchResults(response.toolResults, maxResults);
|
|
7301
8093
|
return {
|
|
@@ -7336,17 +8128,20 @@ function createWebSearchTool() {
|
|
|
7336
8128
|
}
|
|
7337
8129
|
|
|
7338
8130
|
// src/chat/tools/sandbox/write-file.ts
|
|
7339
|
-
import { Type as
|
|
8131
|
+
import { Type as Type22 } from "@sinclair/typebox";
|
|
7340
8132
|
function createWriteFileTool() {
|
|
7341
8133
|
return tool({
|
|
7342
8134
|
description: "Write UTF-8 content to a file in the sandbox workspace. Use for intentional file creation or replacement after validation. Do not use for exploratory analysis-only turns.",
|
|
7343
|
-
|
|
8135
|
+
promptSnippet: "new file or deliberate full-file replacement",
|
|
8136
|
+
promptGuidelines: ["targeted existing-file changes: editFile"],
|
|
8137
|
+
executionMode: "sequential",
|
|
8138
|
+
inputSchema: Type22.Object(
|
|
7344
8139
|
{
|
|
7345
|
-
path:
|
|
8140
|
+
path: Type22.String({
|
|
7346
8141
|
minLength: 1,
|
|
7347
8142
|
description: "Path to write in the sandbox workspace."
|
|
7348
8143
|
}),
|
|
7349
|
-
content:
|
|
8144
|
+
content: Type22.String({
|
|
7350
8145
|
description: "UTF-8 file content to write."
|
|
7351
8146
|
})
|
|
7352
8147
|
},
|
|
@@ -7406,6 +8201,10 @@ function createTools(availableSkills, hooks = {}, context) {
|
|
|
7406
8201
|
bash: createBashTool(),
|
|
7407
8202
|
attachFile: createAttachFileTool(context.sandbox, hooks),
|
|
7408
8203
|
readFile: createReadFileTool(),
|
|
8204
|
+
editFile: createEditFileTool(),
|
|
8205
|
+
grep: createGrepTool(),
|
|
8206
|
+
findFiles: createFindFilesTool(),
|
|
8207
|
+
listDir: createListDirTool(),
|
|
7409
8208
|
writeFile: createWriteFileTool(),
|
|
7410
8209
|
webSearch: createWebSearchTool(),
|
|
7411
8210
|
webFetch: createWebFetchTool(hooks),
|
|
@@ -7672,7 +8471,7 @@ import { createBashTool as createBashTool2 } from "bash-tool";
|
|
|
7672
8471
|
|
|
7673
8472
|
// src/chat/sandbox/skill-sync.ts
|
|
7674
8473
|
import fs3 from "fs/promises";
|
|
7675
|
-
import
|
|
8474
|
+
import path9 from "path";
|
|
7676
8475
|
|
|
7677
8476
|
// src/chat/sandbox/eval-gh-stub.ts
|
|
7678
8477
|
function buildEvalGitHubCliStub() {
|
|
@@ -8041,7 +8840,7 @@ fallbackToRealSentry();
|
|
|
8041
8840
|
|
|
8042
8841
|
// src/chat/sandbox/skill-sync.ts
|
|
8043
8842
|
function toPosixRelative(base, absolute) {
|
|
8044
|
-
return
|
|
8843
|
+
return path9.relative(base, absolute).split(path9.sep).join("/");
|
|
8045
8844
|
}
|
|
8046
8845
|
async function listFilesRecursive(root) {
|
|
8047
8846
|
const queue = [root];
|
|
@@ -8051,7 +8850,7 @@ async function listFilesRecursive(root) {
|
|
|
8051
8850
|
const entries = await fs3.readdir(dir, { withFileTypes: true });
|
|
8052
8851
|
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
8053
8852
|
for (const entry of entries) {
|
|
8054
|
-
const absolute =
|
|
8853
|
+
const absolute = path9.join(dir, entry.name);
|
|
8055
8854
|
if (entry.isDirectory()) {
|
|
8056
8855
|
queue.push(absolute);
|
|
8057
8856
|
} else if (entry.isFile()) {
|
|
@@ -8090,7 +8889,7 @@ async function buildSkillSyncFiles(availableSkills, runtimeBinDir, referenceFile
|
|
|
8090
8889
|
});
|
|
8091
8890
|
if (referenceFiles && referenceFiles.length > 0) {
|
|
8092
8891
|
for (const absoluteFile of referenceFiles) {
|
|
8093
|
-
const fileName =
|
|
8892
|
+
const fileName = path9.basename(absoluteFile);
|
|
8094
8893
|
filesToWrite.push({
|
|
8095
8894
|
path: `${SANDBOX_DATA_ROOT}/${fileName}`,
|
|
8096
8895
|
content: await fs3.readFile(absoluteFile)
|
|
@@ -8118,7 +8917,7 @@ async function buildSkillSyncFiles(availableSkills, runtimeBinDir, referenceFile
|
|
|
8118
8917
|
function collectDirectories(filesToWrite, workspaceRoot) {
|
|
8119
8918
|
const directoriesToEnsure = /* @__PURE__ */ new Set();
|
|
8120
8919
|
for (const file of filesToWrite) {
|
|
8121
|
-
const normalizedPath =
|
|
8920
|
+
const normalizedPath = path9.posix.normalize(file.path);
|
|
8122
8921
|
const parts = normalizedPath.split("/").filter(Boolean);
|
|
8123
8922
|
let current = "";
|
|
8124
8923
|
for (let index = 0; index < parts.length - 1; index += 1) {
|
|
@@ -8131,19 +8930,19 @@ function collectDirectories(filesToWrite, workspaceRoot) {
|
|
|
8131
8930
|
).sort((a, b) => a.length - b.length);
|
|
8132
8931
|
}
|
|
8133
8932
|
function resolveHostSkillPath(availableSkills, sandboxPath) {
|
|
8134
|
-
const normalizedPath =
|
|
8933
|
+
const normalizedPath = path9.posix.normalize(sandboxPath.trim());
|
|
8135
8934
|
for (const skill of availableSkills) {
|
|
8136
8935
|
const virtualRoot = sandboxSkillDir(skill.name);
|
|
8137
8936
|
if (normalizedPath !== virtualRoot && !normalizedPath.startsWith(`${virtualRoot}/`)) {
|
|
8138
8937
|
continue;
|
|
8139
8938
|
}
|
|
8140
|
-
const relativePath =
|
|
8939
|
+
const relativePath = path9.posix.relative(virtualRoot, normalizedPath);
|
|
8141
8940
|
if (!relativePath || relativePath.startsWith("../")) {
|
|
8142
8941
|
return null;
|
|
8143
8942
|
}
|
|
8144
|
-
const hostRoot =
|
|
8145
|
-
const hostPath =
|
|
8146
|
-
if (hostPath !== hostRoot && !hostPath.startsWith(`${hostRoot}${
|
|
8943
|
+
const hostRoot = path9.resolve(skill.skillPath);
|
|
8944
|
+
const hostPath = path9.resolve(hostRoot, ...relativePath.split("/"));
|
|
8945
|
+
if (hostPath !== hostRoot && !hostPath.startsWith(`${hostRoot}${path9.sep}`)) {
|
|
8147
8946
|
return null;
|
|
8148
8947
|
}
|
|
8149
8948
|
return hostPath;
|
|
@@ -8151,16 +8950,16 @@ function resolveHostSkillPath(availableSkills, sandboxPath) {
|
|
|
8151
8950
|
return null;
|
|
8152
8951
|
}
|
|
8153
8952
|
function resolveHostDataPath(referenceFiles, sandboxPath) {
|
|
8154
|
-
const normalizedPath =
|
|
8953
|
+
const normalizedPath = path9.posix.normalize(sandboxPath.trim());
|
|
8155
8954
|
if (normalizedPath !== SANDBOX_DATA_ROOT && !normalizedPath.startsWith(`${SANDBOX_DATA_ROOT}/`)) {
|
|
8156
8955
|
return null;
|
|
8157
8956
|
}
|
|
8158
|
-
const relativePath =
|
|
8957
|
+
const relativePath = path9.posix.relative(SANDBOX_DATA_ROOT, normalizedPath);
|
|
8159
8958
|
if (!relativePath || relativePath.startsWith("../") || relativePath.includes("/")) {
|
|
8160
8959
|
return null;
|
|
8161
8960
|
}
|
|
8162
8961
|
for (const hostFile of referenceFiles) {
|
|
8163
|
-
if (
|
|
8962
|
+
if (path9.basename(hostFile) === relativePath) {
|
|
8164
8963
|
return hostFile;
|
|
8165
8964
|
}
|
|
8166
8965
|
}
|
|
@@ -8658,16 +9457,41 @@ function createSandboxSessionManager(options) {
|
|
|
8658
9457
|
env: input.env,
|
|
8659
9458
|
pathPrefix: `${SANDBOX_RUNTIME_BIN_DIR}:$PATH`
|
|
8660
9459
|
});
|
|
9460
|
+
const controller = input.timeoutMs && input.timeoutMs > 0 ? new AbortController() : void 0;
|
|
9461
|
+
let timedOut = false;
|
|
9462
|
+
const timeoutId = controller ? setTimeout(() => {
|
|
9463
|
+
timedOut = true;
|
|
9464
|
+
controller.abort();
|
|
9465
|
+
}, input.timeoutMs) : void 0;
|
|
8661
9466
|
return await withTemporaryHeaderTransforms(
|
|
8662
9467
|
sandboxInstance,
|
|
8663
9468
|
input.headerTransforms,
|
|
8664
9469
|
async () => {
|
|
8665
|
-
|
|
8666
|
-
|
|
8667
|
-
|
|
8668
|
-
|
|
8669
|
-
|
|
8670
|
-
|
|
9470
|
+
try {
|
|
9471
|
+
const commandResult2 = await sandboxInstance.runCommand({
|
|
9472
|
+
cmd: "bash",
|
|
9473
|
+
args: ["-c", script],
|
|
9474
|
+
cwd: SANDBOX_WORKSPACE_ROOT,
|
|
9475
|
+
...controller ? { signal: controller.signal } : {}
|
|
9476
|
+
});
|
|
9477
|
+
return await readCommandOutput(commandResult2);
|
|
9478
|
+
} catch (error) {
|
|
9479
|
+
if (timedOut) {
|
|
9480
|
+
return {
|
|
9481
|
+
stdout: "",
|
|
9482
|
+
stderr: `Command timed out after ${input.timeoutMs}ms`,
|
|
9483
|
+
exitCode: 124,
|
|
9484
|
+
stdoutTruncated: false,
|
|
9485
|
+
stderrTruncated: false,
|
|
9486
|
+
timedOut: true
|
|
9487
|
+
};
|
|
9488
|
+
}
|
|
9489
|
+
throw error;
|
|
9490
|
+
} finally {
|
|
9491
|
+
if (timeoutId) {
|
|
9492
|
+
clearTimeout(timeoutId);
|
|
9493
|
+
}
|
|
9494
|
+
}
|
|
8671
9495
|
}
|
|
8672
9496
|
);
|
|
8673
9497
|
},
|
|
@@ -8678,7 +9502,8 @@ function createSandboxSessionManager(options) {
|
|
|
8678
9502
|
writeFile: async (input) => await executeWriteFile(input, {
|
|
8679
9503
|
toolCallId: "sandbox-write-file",
|
|
8680
9504
|
messages: []
|
|
8681
|
-
})
|
|
9505
|
+
}),
|
|
9506
|
+
fs: sandboxInstance.fs
|
|
8682
9507
|
};
|
|
8683
9508
|
};
|
|
8684
9509
|
const ensureReadySandbox = async () => {
|
|
@@ -8734,7 +9559,15 @@ function createSandboxSessionManager(options) {
|
|
|
8734
9559
|
}
|
|
8735
9560
|
|
|
8736
9561
|
// src/chat/sandbox/sandbox.ts
|
|
8737
|
-
var SANDBOX_TOOL_NAMES = /* @__PURE__ */ new Set([
|
|
9562
|
+
var SANDBOX_TOOL_NAMES = /* @__PURE__ */ new Set([
|
|
9563
|
+
"bash",
|
|
9564
|
+
"readFile",
|
|
9565
|
+
"editFile",
|
|
9566
|
+
"grep",
|
|
9567
|
+
"findFiles",
|
|
9568
|
+
"listDir",
|
|
9569
|
+
"writeFile"
|
|
9570
|
+
]);
|
|
8738
9571
|
function parseHeaderTransforms(raw) {
|
|
8739
9572
|
if (!Array.isArray(raw)) {
|
|
8740
9573
|
return void 0;
|
|
@@ -8798,6 +9631,7 @@ function createSandboxExecutor(options) {
|
|
|
8798
9631
|
const executeBashTool = async (rawInput, command) => {
|
|
8799
9632
|
const headerTransforms = parseHeaderTransforms(rawInput.headerTransforms);
|
|
8800
9633
|
const env = parseEnv(rawInput.env);
|
|
9634
|
+
const timeoutMs = positiveInteger(rawInput.timeoutMs);
|
|
8801
9635
|
logSandboxBootRequest("tool.bash", {
|
|
8802
9636
|
"app.sandbox.command_length": command.length
|
|
8803
9637
|
});
|
|
@@ -8813,7 +9647,8 @@ function createSandboxExecutor(options) {
|
|
|
8813
9647
|
const response = await executeBash({
|
|
8814
9648
|
command,
|
|
8815
9649
|
...headerTransforms ? { headerTransforms } : {},
|
|
8816
|
-
...env ? { env } : {}
|
|
9650
|
+
...env ? { env } : {},
|
|
9651
|
+
...timeoutMs ? { timeoutMs } : {}
|
|
8817
9652
|
});
|
|
8818
9653
|
setSpanAttributes({
|
|
8819
9654
|
"process.exit.code": response.exitCode,
|
|
@@ -8845,7 +9680,7 @@ function createSandboxExecutor(options) {
|
|
|
8845
9680
|
cwd: SANDBOX_WORKSPACE_ROOT,
|
|
8846
9681
|
exit_code: result.exitCode,
|
|
8847
9682
|
signal: null,
|
|
8848
|
-
timed_out:
|
|
9683
|
+
timed_out: Boolean(result.timedOut),
|
|
8849
9684
|
stdout: result.stdout,
|
|
8850
9685
|
stderr: result.stderr,
|
|
8851
9686
|
stdout_truncated: result.stdoutTruncated,
|
|
@@ -8858,6 +9693,8 @@ function createSandboxExecutor(options) {
|
|
|
8858
9693
|
if (!filePath) {
|
|
8859
9694
|
throw new Error("path is required");
|
|
8860
9695
|
}
|
|
9696
|
+
const offset = positiveInteger(rawInput.offset);
|
|
9697
|
+
const limit = positiveInteger(rawInput.limit);
|
|
8861
9698
|
if (!sessionManager.getSandboxId()) {
|
|
8862
9699
|
const hostPath = resolveHostSkillPath(availableSkills, filePath) ?? resolveHostDataPath(referenceFiles, filePath);
|
|
8863
9700
|
if (hostPath) {
|
|
@@ -8871,11 +9708,12 @@ function createSandboxExecutor(options) {
|
|
|
8871
9708
|
});
|
|
8872
9709
|
setSpanStatus("ok");
|
|
8873
9710
|
return {
|
|
8874
|
-
result: {
|
|
9711
|
+
result: sliceFileContent({
|
|
8875
9712
|
content,
|
|
8876
9713
|
path: filePath,
|
|
8877
|
-
|
|
8878
|
-
|
|
9714
|
+
offset,
|
|
9715
|
+
limit
|
|
9716
|
+
})
|
|
8879
9717
|
};
|
|
8880
9718
|
} catch (error) {
|
|
8881
9719
|
if (!isHostFileMissingError(error)) {
|
|
@@ -8903,9 +9741,12 @@ function createSandboxExecutor(options) {
|
|
|
8903
9741
|
});
|
|
8904
9742
|
setSpanStatus("ok");
|
|
8905
9743
|
return {
|
|
8906
|
-
|
|
8907
|
-
|
|
8908
|
-
|
|
9744
|
+
...sliceFileContent({
|
|
9745
|
+
content,
|
|
9746
|
+
path: filePath,
|
|
9747
|
+
offset,
|
|
9748
|
+
limit
|
|
9749
|
+
})
|
|
8909
9750
|
};
|
|
8910
9751
|
}
|
|
8911
9752
|
);
|
|
@@ -8945,6 +9786,116 @@ function createSandboxExecutor(options) {
|
|
|
8945
9786
|
}
|
|
8946
9787
|
};
|
|
8947
9788
|
};
|
|
9789
|
+
const executeEditFileTool = async (rawInput) => {
|
|
9790
|
+
const filePath = String(rawInput.path ?? "").trim();
|
|
9791
|
+
if (!filePath) {
|
|
9792
|
+
throw new Error("path is required");
|
|
9793
|
+
}
|
|
9794
|
+
if (!Array.isArray(rawInput.edits)) {
|
|
9795
|
+
throw new Error("edits is required");
|
|
9796
|
+
}
|
|
9797
|
+
logSandboxBootRequest("tool.editFile", {
|
|
9798
|
+
"file.path": filePath
|
|
9799
|
+
});
|
|
9800
|
+
const executors = await sessionManager.ensureToolExecutors();
|
|
9801
|
+
const result = await withSandboxSpan(
|
|
9802
|
+
"sandbox.editFile",
|
|
9803
|
+
"sandbox.fs.edit",
|
|
9804
|
+
{
|
|
9805
|
+
"app.sandbox.path.length": filePath.length,
|
|
9806
|
+
"app.sandbox.edit.count": rawInput.edits.length
|
|
9807
|
+
},
|
|
9808
|
+
async () => {
|
|
9809
|
+
const response = await editFile({
|
|
9810
|
+
fs: executors.fs,
|
|
9811
|
+
path: filePath,
|
|
9812
|
+
edits: rawInput.edits
|
|
9813
|
+
});
|
|
9814
|
+
setSpanStatus("ok");
|
|
9815
|
+
return response;
|
|
9816
|
+
}
|
|
9817
|
+
);
|
|
9818
|
+
return { result };
|
|
9819
|
+
};
|
|
9820
|
+
const executeGrepTool = async (rawInput) => {
|
|
9821
|
+
const pattern = String(rawInput.pattern ?? "");
|
|
9822
|
+
if (!pattern) {
|
|
9823
|
+
throw new Error("pattern is required");
|
|
9824
|
+
}
|
|
9825
|
+
logSandboxBootRequest("tool.grep");
|
|
9826
|
+
const contextLines = positiveInteger(rawInput.context);
|
|
9827
|
+
const limit = positiveInteger(rawInput.limit);
|
|
9828
|
+
const executors = await sessionManager.ensureToolExecutors();
|
|
9829
|
+
const result = await withSandboxSpan(
|
|
9830
|
+
"sandbox.grep",
|
|
9831
|
+
"sandbox.fs.search",
|
|
9832
|
+
{
|
|
9833
|
+
"app.sandbox.pattern.length": pattern.length
|
|
9834
|
+
},
|
|
9835
|
+
async () => {
|
|
9836
|
+
const response = await grepFiles({
|
|
9837
|
+
fs: executors.fs,
|
|
9838
|
+
pattern,
|
|
9839
|
+
...typeof rawInput.path === "string" ? { path: rawInput.path } : {},
|
|
9840
|
+
...typeof rawInput.glob === "string" ? { glob: rawInput.glob } : {},
|
|
9841
|
+
...typeof rawInput.ignoreCase === "boolean" ? { ignoreCase: rawInput.ignoreCase } : {},
|
|
9842
|
+
...typeof rawInput.literal === "boolean" ? { literal: rawInput.literal } : {},
|
|
9843
|
+
...contextLines ? { context: contextLines } : {},
|
|
9844
|
+
...limit ? { limit } : {}
|
|
9845
|
+
});
|
|
9846
|
+
setSpanStatus("ok");
|
|
9847
|
+
return response;
|
|
9848
|
+
}
|
|
9849
|
+
);
|
|
9850
|
+
return { result };
|
|
9851
|
+
};
|
|
9852
|
+
const executeFindFilesTool = async (rawInput) => {
|
|
9853
|
+
const pattern = String(rawInput.pattern ?? "");
|
|
9854
|
+
if (!pattern) {
|
|
9855
|
+
throw new Error("pattern is required");
|
|
9856
|
+
}
|
|
9857
|
+
logSandboxBootRequest("tool.findFiles");
|
|
9858
|
+
const limit = positiveInteger(rawInput.limit);
|
|
9859
|
+
const executors = await sessionManager.ensureToolExecutors();
|
|
9860
|
+
const result = await withSandboxSpan(
|
|
9861
|
+
"sandbox.findFiles",
|
|
9862
|
+
"sandbox.fs.find",
|
|
9863
|
+
{
|
|
9864
|
+
"app.sandbox.pattern.length": pattern.length
|
|
9865
|
+
},
|
|
9866
|
+
async () => {
|
|
9867
|
+
const response = await findFiles({
|
|
9868
|
+
fs: executors.fs,
|
|
9869
|
+
pattern,
|
|
9870
|
+
...typeof rawInput.path === "string" ? { path: rawInput.path } : {},
|
|
9871
|
+
...limit ? { limit } : {}
|
|
9872
|
+
});
|
|
9873
|
+
setSpanStatus("ok");
|
|
9874
|
+
return response;
|
|
9875
|
+
}
|
|
9876
|
+
);
|
|
9877
|
+
return { result };
|
|
9878
|
+
};
|
|
9879
|
+
const executeListDirTool = async (rawInput) => {
|
|
9880
|
+
logSandboxBootRequest("tool.listDir");
|
|
9881
|
+
const limit = positiveInteger(rawInput.limit);
|
|
9882
|
+
const executors = await sessionManager.ensureToolExecutors();
|
|
9883
|
+
const result = await withSandboxSpan(
|
|
9884
|
+
"sandbox.listDir",
|
|
9885
|
+
"sandbox.fs.list",
|
|
9886
|
+
{},
|
|
9887
|
+
async () => {
|
|
9888
|
+
const response = await listDir({
|
|
9889
|
+
fs: executors.fs,
|
|
9890
|
+
...typeof rawInput.path === "string" ? { path: rawInput.path } : {},
|
|
9891
|
+
...limit ? { limit } : {}
|
|
9892
|
+
});
|
|
9893
|
+
setSpanStatus("ok");
|
|
9894
|
+
return response;
|
|
9895
|
+
}
|
|
9896
|
+
);
|
|
9897
|
+
return { result };
|
|
9898
|
+
};
|
|
8948
9899
|
const execute = async (params) => {
|
|
8949
9900
|
const rawInput = params.input ?? {};
|
|
8950
9901
|
const bashCommand = params.toolName === "bash" ? String(rawInput.command ?? "").trim() : void 0;
|
|
@@ -8963,6 +9914,18 @@ function createSandboxExecutor(options) {
|
|
|
8963
9914
|
if (params.toolName === "readFile") {
|
|
8964
9915
|
return await executeReadFileTool(rawInput);
|
|
8965
9916
|
}
|
|
9917
|
+
if (params.toolName === "editFile") {
|
|
9918
|
+
return await executeEditFileTool(rawInput);
|
|
9919
|
+
}
|
|
9920
|
+
if (params.toolName === "grep") {
|
|
9921
|
+
return await executeGrepTool(rawInput);
|
|
9922
|
+
}
|
|
9923
|
+
if (params.toolName === "findFiles") {
|
|
9924
|
+
return await executeFindFilesTool(rawInput);
|
|
9925
|
+
}
|
|
9926
|
+
if (params.toolName === "listDir") {
|
|
9927
|
+
return await executeListDirTool(rawInput);
|
|
9928
|
+
}
|
|
8966
9929
|
if (params.toolName === "writeFile") {
|
|
8967
9930
|
return await executeWriteFileTool(rawInput);
|
|
8968
9931
|
}
|
|
@@ -9035,11 +9998,49 @@ function buildReportedProgressStatus(input) {
|
|
|
9035
9998
|
|
|
9036
9999
|
// src/chat/tools/execution/build-sandbox-input.ts
|
|
9037
10000
|
function buildSandboxInput(toolName, params) {
|
|
10001
|
+
const optionalNumber = (value) => typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
9038
10002
|
if (toolName === "bash") {
|
|
9039
|
-
return {
|
|
10003
|
+
return {
|
|
10004
|
+
command: String(params.command ?? ""),
|
|
10005
|
+
...optionalNumber(params.timeoutMs) ? { timeoutMs: optionalNumber(params.timeoutMs) } : {}
|
|
10006
|
+
};
|
|
9040
10007
|
}
|
|
9041
10008
|
if (toolName === "readFile") {
|
|
9042
|
-
return {
|
|
10009
|
+
return {
|
|
10010
|
+
path: String(params.path ?? ""),
|
|
10011
|
+
...optionalNumber(params.offset) ? { offset: optionalNumber(params.offset) } : {},
|
|
10012
|
+
...optionalNumber(params.limit) ? { limit: optionalNumber(params.limit) } : {}
|
|
10013
|
+
};
|
|
10014
|
+
}
|
|
10015
|
+
if (toolName === "editFile") {
|
|
10016
|
+
return {
|
|
10017
|
+
path: String(params.path ?? ""),
|
|
10018
|
+
edits: Array.isArray(params.edits) ? params.edits : []
|
|
10019
|
+
};
|
|
10020
|
+
}
|
|
10021
|
+
if (toolName === "grep") {
|
|
10022
|
+
return {
|
|
10023
|
+
pattern: String(params.pattern ?? ""),
|
|
10024
|
+
...typeof params.path === "string" ? { path: params.path } : {},
|
|
10025
|
+
...typeof params.glob === "string" ? { glob: params.glob } : {},
|
|
10026
|
+
...typeof params.ignoreCase === "boolean" ? { ignoreCase: params.ignoreCase } : {},
|
|
10027
|
+
...typeof params.literal === "boolean" ? { literal: params.literal } : {},
|
|
10028
|
+
...optionalNumber(params.context) ? { context: optionalNumber(params.context) } : {},
|
|
10029
|
+
...optionalNumber(params.limit) ? { limit: optionalNumber(params.limit) } : {}
|
|
10030
|
+
};
|
|
10031
|
+
}
|
|
10032
|
+
if (toolName === "findFiles") {
|
|
10033
|
+
return {
|
|
10034
|
+
pattern: String(params.pattern ?? ""),
|
|
10035
|
+
...typeof params.path === "string" ? { path: params.path } : {},
|
|
10036
|
+
...optionalNumber(params.limit) ? { limit: optionalNumber(params.limit) } : {}
|
|
10037
|
+
};
|
|
10038
|
+
}
|
|
10039
|
+
if (toolName === "listDir") {
|
|
10040
|
+
return {
|
|
10041
|
+
...typeof params.path === "string" ? { path: params.path } : {},
|
|
10042
|
+
...optionalNumber(params.limit) ? { limit: optionalNumber(params.limit) } : {}
|
|
10043
|
+
};
|
|
9043
10044
|
}
|
|
9044
10045
|
if (toolName === "writeFile") {
|
|
9045
10046
|
return {
|
|
@@ -9174,6 +10175,8 @@ function createAgentTools(tools, sandbox, spanContext, onStatus, sandboxExecutor
|
|
|
9174
10175
|
label: toolName,
|
|
9175
10176
|
description: toolDef.description,
|
|
9176
10177
|
parameters: toolDef.inputSchema,
|
|
10178
|
+
prepareArguments: toolDef.prepareArguments,
|
|
10179
|
+
executionMode: toolDef.executionMode,
|
|
9177
10180
|
execute: async (toolCallId, params) => {
|
|
9178
10181
|
const normalizedToolCallId = typeof toolCallId === "string" && toolCallId.length > 0 ? toolCallId : void 0;
|
|
9179
10182
|
const toolArgumentsAttribute = serializeGenAiAttribute(params);
|
|
@@ -9431,18 +10434,17 @@ function buildTurnResult(input) {
|
|
|
9431
10434
|
const errorMessage = typeof lastAssistant?.errorMessage === "string" ? lastAssistant.errorMessage : void 0;
|
|
9432
10435
|
const usedPrimaryText = Boolean(primaryText);
|
|
9433
10436
|
const outcome = primaryText ? stopReason === "error" ? "provider_error" : "success" : sideEffectOnlySuccess ? "success" : "execution_failure";
|
|
9434
|
-
const fallbackText = buildExecutionFailureMessage(toolErrorCount);
|
|
9435
10437
|
const suppressReactionOnlyText = reactionPerformed && !channelPostPerformed && replyFiles.length === 0 && Boolean(primaryText) && isReactionOnlyIntent(userInput);
|
|
9436
|
-
const rawResponseText = suppressReactionOnlyText ? "" : primaryText
|
|
10438
|
+
const rawResponseText = suppressReactionOnlyText ? "" : primaryText;
|
|
9437
10439
|
const responseText = canvasCreated && isVerbosePostCanvasReply(rawResponseText) ? buildBriefPostCanvasReply(artifactStatePatch) : rawResponseText;
|
|
9438
10440
|
const escapedOrRawPayload = Boolean(primaryText) && (isExecutionEscapeResponse(primaryText) || isRawToolPayloadResponse(primaryText));
|
|
9439
|
-
const resolvedText = escapedOrRawPayload ?
|
|
9440
|
-
const
|
|
10441
|
+
const resolvedText = escapedOrRawPayload ? "" : enforceAttachmentClaimTruth(responseText, replyFiles.length > 0);
|
|
10442
|
+
const resolvedOutcome = escapedOrRawPayload ? "execution_failure" : outcome;
|
|
10443
|
+
const deliveryPlan = resolvedOutcome === "success" && !resolvedText && replyFiles.length === 0 && (reactionPerformed || channelPostPerformed) ? {
|
|
9441
10444
|
...baseDeliveryPlan,
|
|
9442
10445
|
postThreadText: false
|
|
9443
10446
|
} : baseDeliveryPlan;
|
|
9444
10447
|
const deliveryMode = deliveryPlan.mode;
|
|
9445
|
-
const resolvedOutcome = escapedOrRawPayload ? "execution_failure" : outcome;
|
|
9446
10448
|
if (shouldTrace) {
|
|
9447
10449
|
logInfo(
|
|
9448
10450
|
"agent_message_out",
|
|
@@ -10734,6 +11736,13 @@ async function generateAssistantReply(messageText, context = {}) {
|
|
|
10734
11736
|
}
|
|
10735
11737
|
}
|
|
10736
11738
|
);
|
|
11739
|
+
const toolGuidance = Object.entries(
|
|
11740
|
+
tools
|
|
11741
|
+
).map(([name, definition]) => ({
|
|
11742
|
+
name,
|
|
11743
|
+
promptGuidelines: definition.promptGuidelines,
|
|
11744
|
+
promptSnippet: definition.promptSnippet
|
|
11745
|
+
}));
|
|
10737
11746
|
syncResumeState();
|
|
10738
11747
|
for (const skill of activeSkills) {
|
|
10739
11748
|
await turnMcpToolManager.activateForSkill(skill);
|
|
@@ -10753,6 +11762,7 @@ async function generateAssistantReply(messageText, context = {}) {
|
|
|
10753
11762
|
availableSkills,
|
|
10754
11763
|
activeSkills,
|
|
10755
11764
|
activeMcpCatalogs,
|
|
11765
|
+
toolGuidance,
|
|
10756
11766
|
runtime: {
|
|
10757
11767
|
channelId: toolChannelId,
|
|
10758
11768
|
fastModelId: botConfig.fastModelId,
|
|
@@ -10806,7 +11816,16 @@ async function generateAssistantReply(messageText, context = {}) {
|
|
|
10806
11816
|
pluginAuth,
|
|
10807
11817
|
onToolCall
|
|
10808
11818
|
);
|
|
10809
|
-
advisorTools =
|
|
11819
|
+
advisorTools = createAgentTools(
|
|
11820
|
+
createAdvisorToolDefinitions(tools),
|
|
11821
|
+
skillSandbox,
|
|
11822
|
+
spanContext,
|
|
11823
|
+
context.onStatus,
|
|
11824
|
+
sandboxExecutor,
|
|
11825
|
+
capabilityRuntime,
|
|
11826
|
+
pluginAuth,
|
|
11827
|
+
onToolCall
|
|
11828
|
+
);
|
|
10810
11829
|
agent = new Agent2({
|
|
10811
11830
|
getApiKey: () => getPiGatewayApiKeyOverride(),
|
|
10812
11831
|
initialState: {
|
|
@@ -11107,6 +12126,97 @@ async function generateAssistantReply(messageText, context = {}) {
|
|
|
11107
12126
|
}
|
|
11108
12127
|
}
|
|
11109
12128
|
|
|
12129
|
+
// src/chat/services/turn-failure-response.ts
|
|
12130
|
+
function requireTurnFailureEventId(eventId, eventName) {
|
|
12131
|
+
if (!eventId) {
|
|
12132
|
+
throw new Error(`Sentry did not return an event ID for ${eventName}`);
|
|
12133
|
+
}
|
|
12134
|
+
return eventId;
|
|
12135
|
+
}
|
|
12136
|
+
function getExecutionFailureReason(reply) {
|
|
12137
|
+
const errorMessage = reply.diagnostics.errorMessage?.trim();
|
|
12138
|
+
if (errorMessage) {
|
|
12139
|
+
return errorMessage;
|
|
12140
|
+
}
|
|
12141
|
+
if (reply.diagnostics.toolErrorCount > 0) {
|
|
12142
|
+
return `${reply.diagnostics.toolErrorCount} tool result error(s)`;
|
|
12143
|
+
}
|
|
12144
|
+
if (reply.diagnostics.assistantMessageCount > 0) {
|
|
12145
|
+
return "assistant returned no text";
|
|
12146
|
+
}
|
|
12147
|
+
return "empty assistant turn";
|
|
12148
|
+
}
|
|
12149
|
+
function getFailureCapture(reply) {
|
|
12150
|
+
if (reply.diagnostics.outcome === "provider_error") {
|
|
12151
|
+
return {
|
|
12152
|
+
eventName: "agent_turn_provider_error",
|
|
12153
|
+
error: reply.diagnostics.providerError ?? new Error(
|
|
12154
|
+
reply.diagnostics.errorMessage ?? "Provider error without explicit message"
|
|
12155
|
+
),
|
|
12156
|
+
attributes: {},
|
|
12157
|
+
body: "Agent turn failed with provider error"
|
|
12158
|
+
};
|
|
12159
|
+
}
|
|
12160
|
+
const failureReason = getExecutionFailureReason(reply);
|
|
12161
|
+
return {
|
|
12162
|
+
eventName: "agent_turn_execution_failure",
|
|
12163
|
+
error: new Error(`Agent turn execution failure: ${failureReason}`),
|
|
12164
|
+
attributes: {
|
|
12165
|
+
"app.ai.execution_failure_reason": failureReason
|
|
12166
|
+
},
|
|
12167
|
+
body: "Agent turn completed with execution failure"
|
|
12168
|
+
};
|
|
12169
|
+
}
|
|
12170
|
+
function getAgentTurnDiagnosticsAttributes(reply) {
|
|
12171
|
+
return {
|
|
12172
|
+
"gen_ai.provider.name": GEN_AI_PROVIDER_NAME,
|
|
12173
|
+
"gen_ai.operation.name": "invoke_agent",
|
|
12174
|
+
"app.ai.outcome": reply.diagnostics.outcome,
|
|
12175
|
+
"app.ai.assistant_messages": reply.diagnostics.assistantMessageCount,
|
|
12176
|
+
"app.ai.tool_results": reply.diagnostics.toolResultCount,
|
|
12177
|
+
"app.ai.tool_error_results": reply.diagnostics.toolErrorCount,
|
|
12178
|
+
"app.ai.tool_call_count": reply.diagnostics.toolCalls.length,
|
|
12179
|
+
"app.ai.used_primary_text": reply.diagnostics.usedPrimaryText,
|
|
12180
|
+
...reply.diagnostics.thinkingLevel ? {
|
|
12181
|
+
"app.ai.reasoning_effort": reply.diagnostics.thinkingLevel
|
|
12182
|
+
} : {},
|
|
12183
|
+
...reply.diagnostics.stopReason ? {
|
|
12184
|
+
"gen_ai.response.finish_reasons": [reply.diagnostics.stopReason]
|
|
12185
|
+
} : {},
|
|
12186
|
+
...reply.diagnostics.errorMessage ? { "error.message": reply.diagnostics.errorMessage } : {}
|
|
12187
|
+
};
|
|
12188
|
+
}
|
|
12189
|
+
function finalizeFailedTurnReply(args) {
|
|
12190
|
+
if (args.reply.diagnostics.outcome === "success") {
|
|
12191
|
+
return args.reply;
|
|
12192
|
+
}
|
|
12193
|
+
const capture = getFailureCapture(args.reply);
|
|
12194
|
+
const eventId = requireTurnFailureEventId(
|
|
12195
|
+
args.logException(
|
|
12196
|
+
capture.error,
|
|
12197
|
+
capture.eventName,
|
|
12198
|
+
args.context,
|
|
12199
|
+
{
|
|
12200
|
+
...getAgentTurnDiagnosticsAttributes(args.reply),
|
|
12201
|
+
...args.attributes,
|
|
12202
|
+
...capture.attributes
|
|
12203
|
+
},
|
|
12204
|
+
capture.body
|
|
12205
|
+
),
|
|
12206
|
+
capture.eventName
|
|
12207
|
+
);
|
|
12208
|
+
return {
|
|
12209
|
+
...args.reply,
|
|
12210
|
+
text: buildTurnFailureResponse(eventId),
|
|
12211
|
+
deliveryMode: "thread",
|
|
12212
|
+
deliveryPlan: {
|
|
12213
|
+
mode: "thread",
|
|
12214
|
+
postThreadText: true,
|
|
12215
|
+
attachFiles: args.reply.files && args.reply.files.length > 0 ? "inline" : "none"
|
|
12216
|
+
}
|
|
12217
|
+
};
|
|
12218
|
+
}
|
|
12219
|
+
|
|
11110
12220
|
// src/chat/slack/assistant-thread/status-render.ts
|
|
11111
12221
|
var DEFAULT_STATUS_CONTEXTS = {
|
|
11112
12222
|
thinking: "\u2026",
|
|
@@ -11376,7 +12486,7 @@ function createSlackAdapterStatusSender(args) {
|
|
|
11376
12486
|
};
|
|
11377
12487
|
}
|
|
11378
12488
|
function createSlackWebApiStatusSender(args) {
|
|
11379
|
-
const
|
|
12489
|
+
const getClient3 = args.getSlackClient ?? getSlackClient;
|
|
11380
12490
|
return async (text, loadingMessages) => {
|
|
11381
12491
|
const channelId = args.channelId;
|
|
11382
12492
|
const threadTs = args.threadTs;
|
|
@@ -11389,7 +12499,7 @@ function createSlackWebApiStatusSender(args) {
|
|
|
11389
12499
|
}
|
|
11390
12500
|
const nextLoadingMessages = text ? loadingMessages ?? [text] : void 0;
|
|
11391
12501
|
try {
|
|
11392
|
-
await
|
|
12502
|
+
await getClient3().assistant.threads.setStatus({
|
|
11393
12503
|
channel_id: normalizedChannelId,
|
|
11394
12504
|
thread_ts: threadTs,
|
|
11395
12505
|
status: text ? SLACK_ASSISTANT_ACTIVE_STATUS : "",
|
|
@@ -11466,9 +12576,56 @@ function createSlackWebApiAssistantStatusSession(args) {
|
|
|
11466
12576
|
}
|
|
11467
12577
|
|
|
11468
12578
|
// src/chat/slack/footer.ts
|
|
12579
|
+
var SENTRY_CONVERSATION_SEARCH_STATS_PERIOD = "14d";
|
|
12580
|
+
var ORG_ID_HOST_RE = /^o(\d+)\./;
|
|
11469
12581
|
function escapeSlackMrkdwn(text) {
|
|
11470
12582
|
return text.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">");
|
|
11471
12583
|
}
|
|
12584
|
+
function escapeSlackLinkUrl(url) {
|
|
12585
|
+
return url.replaceAll("&", "&").replaceAll("<", "%3C").replaceAll(">", "%3E");
|
|
12586
|
+
}
|
|
12587
|
+
function toOptionalString2(value) {
|
|
12588
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
12589
|
+
return String(value);
|
|
12590
|
+
}
|
|
12591
|
+
return typeof value === "string" && value.trim() ? value.trim() : void 0;
|
|
12592
|
+
}
|
|
12593
|
+
function quoteSentrySearchValue(value) {
|
|
12594
|
+
return `"${value.replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"`;
|
|
12595
|
+
}
|
|
12596
|
+
function getDsnOrgId(host) {
|
|
12597
|
+
return host?.match(ORG_ID_HOST_RE)?.[1];
|
|
12598
|
+
}
|
|
12599
|
+
function isSentrySaasDsnHost(host) {
|
|
12600
|
+
return host === "sentry.io" || host.endsWith(".sentry.io");
|
|
12601
|
+
}
|
|
12602
|
+
function buildSentryWebBaseUrl(dsn) {
|
|
12603
|
+
if (isSentrySaasDsnHost(dsn.host)) {
|
|
12604
|
+
return "https://sentry.io";
|
|
12605
|
+
}
|
|
12606
|
+
const port = dsn.port ? `:${dsn.port}` : "";
|
|
12607
|
+
const path11 = dsn.path ? `/${dsn.path}` : "";
|
|
12608
|
+
return `${dsn.protocol}://${dsn.host}${port}${path11}`;
|
|
12609
|
+
}
|
|
12610
|
+
function getSentryConversationSearchUrl(conversationId) {
|
|
12611
|
+
const client2 = sentry_exports.getClient();
|
|
12612
|
+
const dsn = client2?.getDsn();
|
|
12613
|
+
if (!dsn?.host || !dsn.projectId) {
|
|
12614
|
+
return void 0;
|
|
12615
|
+
}
|
|
12616
|
+
const orgId = toOptionalString2(client2?.getOptions().orgId) ?? getDsnOrgId(dsn.host);
|
|
12617
|
+
if (!orgId) {
|
|
12618
|
+
return void 0;
|
|
12619
|
+
}
|
|
12620
|
+
const params = new URLSearchParams();
|
|
12621
|
+
params.set(
|
|
12622
|
+
"query",
|
|
12623
|
+
`gen_ai.conversation.id:${quoteSentrySearchValue(conversationId)}`
|
|
12624
|
+
);
|
|
12625
|
+
params.set("project", dsn.projectId);
|
|
12626
|
+
params.set("statsPeriod", SENTRY_CONVERSATION_SEARCH_STATS_PERIOD);
|
|
12627
|
+
return `${buildSentryWebBaseUrl(dsn)}/organizations/${orgId}/explore/traces/?${params.toString()}`;
|
|
12628
|
+
}
|
|
11472
12629
|
function formatSlackTokenCount(value) {
|
|
11473
12630
|
if (value >= 1e6) {
|
|
11474
12631
|
const millions = value / 1e6;
|
|
@@ -11518,10 +12675,15 @@ function buildSlackReplyFooter(args) {
|
|
|
11518
12675
|
const items = [];
|
|
11519
12676
|
const conversationId = args.conversationId?.trim();
|
|
11520
12677
|
if (conversationId) {
|
|
11521
|
-
|
|
12678
|
+
const idItem = {
|
|
11522
12679
|
label: "ID",
|
|
11523
12680
|
value: conversationId
|
|
11524
|
-
}
|
|
12681
|
+
};
|
|
12682
|
+
const conversationUrl = getSentryConversationSearchUrl(conversationId);
|
|
12683
|
+
if (conversationUrl) {
|
|
12684
|
+
idItem.url = conversationUrl;
|
|
12685
|
+
}
|
|
12686
|
+
items.push(idItem);
|
|
11525
12687
|
}
|
|
11526
12688
|
const totalTokens = resolveTotalTokens(args.usage);
|
|
11527
12689
|
if (totalTokens !== void 0) {
|
|
@@ -11560,7 +12722,7 @@ function buildSlackReplyBlocks(text, footer) {
|
|
|
11560
12722
|
type: "context",
|
|
11561
12723
|
elements: footer.items.map((item) => ({
|
|
11562
12724
|
type: "mrkdwn",
|
|
11563
|
-
text: `*${escapeSlackMrkdwn(item.label)}:* ${escapeSlackMrkdwn(item.value)}`
|
|
12725
|
+
text: item.url ? `*${escapeSlackMrkdwn(item.label)}:* <${escapeSlackLinkUrl(item.url)}|${escapeSlackMrkdwn(item.value)}>` : `*${escapeSlackMrkdwn(item.label)}:* ${escapeSlackMrkdwn(item.value)}`
|
|
11564
12726
|
}))
|
|
11565
12727
|
});
|
|
11566
12728
|
}
|
|
@@ -11569,9 +12731,6 @@ function buildSlackReplyBlocks(text, footer) {
|
|
|
11569
12731
|
|
|
11570
12732
|
// src/chat/slack/reply.ts
|
|
11571
12733
|
import { Buffer as Buffer2 } from "buffer";
|
|
11572
|
-
function isInterruptedVisibleReply(reply) {
|
|
11573
|
-
return reply.diagnostics.outcome === "provider_error";
|
|
11574
|
-
}
|
|
11575
12734
|
function resolveReplyDelivery(reply) {
|
|
11576
12735
|
const replyHasFiles = Boolean(reply.files && reply.files.length > 0);
|
|
11577
12736
|
const deliveryPlan = reply.deliveryPlan ?? {
|
|
@@ -11595,9 +12754,7 @@ function buildReplyText(text) {
|
|
|
11595
12754
|
return "";
|
|
11596
12755
|
}
|
|
11597
12756
|
function buildTextPosts(args) {
|
|
11598
|
-
const chunks = splitSlackReplyText(args.text
|
|
11599
|
-
interrupted: args.interrupted
|
|
11600
|
-
});
|
|
12757
|
+
const chunks = splitSlackReplyText(args.text);
|
|
11601
12758
|
return chunks.map((chunk, index) => ({
|
|
11602
12759
|
text: chunk,
|
|
11603
12760
|
...index === 0 && args.firstFiles ? { files: args.firstFiles } : {},
|
|
@@ -11648,11 +12805,9 @@ function planSlackReplyPosts(args) {
|
|
|
11648
12805
|
const { shouldPostThreadReply, attachFiles } = resolveReplyDelivery(
|
|
11649
12806
|
args.reply
|
|
11650
12807
|
);
|
|
11651
|
-
const interrupted = isInterruptedVisibleReply(args.reply);
|
|
11652
12808
|
const posts = [];
|
|
11653
12809
|
const textPosts = shouldPostThreadReply ? buildTextPosts({
|
|
11654
12810
|
text: args.reply.text,
|
|
11655
|
-
interrupted,
|
|
11656
12811
|
firstFiles: attachFiles === "inline" ? replyFiles : void 0
|
|
11657
12812
|
}) : [];
|
|
11658
12813
|
posts.push(...textPosts);
|
|
@@ -11777,6 +12932,64 @@ var ResumeTurnBusyError = class extends Error {
|
|
|
11777
12932
|
function getDefaultLockKey(channelId, threadTs) {
|
|
11778
12933
|
return `slack:${channelId}:${threadTs}`;
|
|
11779
12934
|
}
|
|
12935
|
+
function getResumeLogContext(args, lockKey) {
|
|
12936
|
+
return {
|
|
12937
|
+
conversationId: args.replyContext?.correlation?.conversationId ?? lockKey,
|
|
12938
|
+
slackThreadId: args.replyContext?.correlation?.threadId ?? lockKey,
|
|
12939
|
+
slackUserId: args.replyContext?.requester?.userId ?? args.replyContext?.correlation?.requesterId,
|
|
12940
|
+
slackUserName: args.replyContext?.requester?.userName,
|
|
12941
|
+
slackChannelId: args.channelId,
|
|
12942
|
+
runId: args.replyContext?.correlation?.runId,
|
|
12943
|
+
assistantUserName: botConfig.userName,
|
|
12944
|
+
modelId: botConfig.modelId
|
|
12945
|
+
};
|
|
12946
|
+
}
|
|
12947
|
+
async function postResumeFailureReply(args) {
|
|
12948
|
+
try {
|
|
12949
|
+
await postSlackMessage({
|
|
12950
|
+
channelId: args.channelId,
|
|
12951
|
+
threadTs: args.threadTs,
|
|
12952
|
+
text: buildTurnFailureResponse(args.eventId)
|
|
12953
|
+
});
|
|
12954
|
+
} catch (error) {
|
|
12955
|
+
logException(
|
|
12956
|
+
error,
|
|
12957
|
+
"slack_resume_failure_reply_post_failed",
|
|
12958
|
+
args.logContext,
|
|
12959
|
+
{
|
|
12960
|
+
"app.error.original_event_id": args.eventId
|
|
12961
|
+
},
|
|
12962
|
+
"Failed to post resumed turn failure reply"
|
|
12963
|
+
);
|
|
12964
|
+
throw error;
|
|
12965
|
+
}
|
|
12966
|
+
}
|
|
12967
|
+
async function handleResumeFailure(args) {
|
|
12968
|
+
const logContext = getResumeLogContext(args.resumeArgs, args.lockKey);
|
|
12969
|
+
const capturedEventId = logException(
|
|
12970
|
+
args.error,
|
|
12971
|
+
args.eventName,
|
|
12972
|
+
logContext,
|
|
12973
|
+
{},
|
|
12974
|
+
args.body
|
|
12975
|
+
);
|
|
12976
|
+
await args.resumeArgs.onFailure?.(args.error);
|
|
12977
|
+
const eventId = requireTurnFailureEventId(capturedEventId, args.eventName);
|
|
12978
|
+
let postError;
|
|
12979
|
+
try {
|
|
12980
|
+
await postResumeFailureReply({
|
|
12981
|
+
channelId: args.resumeArgs.channelId,
|
|
12982
|
+
threadTs: args.resumeArgs.threadTs,
|
|
12983
|
+
eventId,
|
|
12984
|
+
logContext
|
|
12985
|
+
});
|
|
12986
|
+
} catch (error) {
|
|
12987
|
+
postError = error;
|
|
12988
|
+
}
|
|
12989
|
+
if (postError) {
|
|
12990
|
+
throw postError;
|
|
12991
|
+
}
|
|
12992
|
+
}
|
|
11780
12993
|
function createResumeReplyContext(args, statusSession) {
|
|
11781
12994
|
const replyContext = args.replyContext ?? {};
|
|
11782
12995
|
const threadId = args.lockKey ?? getDefaultLockKey(args.channelId, args.threadTs);
|
|
@@ -11844,7 +13057,7 @@ async function resumeSlackTurn(args) {
|
|
|
11844
13057
|
...replyContext
|
|
11845
13058
|
});
|
|
11846
13059
|
const replyTimeoutMs = resolveReplyTimeoutMs(args.replyTimeoutMs);
|
|
11847
|
-
|
|
13060
|
+
let reply = typeof replyTimeoutMs === "number" ? await Promise.race([
|
|
11848
13061
|
replyPromise,
|
|
11849
13062
|
new Promise(
|
|
11850
13063
|
(_, reject) => setTimeout(
|
|
@@ -11857,6 +13070,11 @@ async function resumeSlackTurn(args) {
|
|
|
11857
13070
|
)
|
|
11858
13071
|
)
|
|
11859
13072
|
]) : await replyPromise;
|
|
13073
|
+
reply = finalizeFailedTurnReply({
|
|
13074
|
+
reply,
|
|
13075
|
+
logException,
|
|
13076
|
+
context: getResumeLogContext(args, lockKey)
|
|
13077
|
+
});
|
|
11860
13078
|
await status.stop();
|
|
11861
13079
|
const footer = buildSlackReplyFooter({
|
|
11862
13080
|
conversationId: args.replyContext?.correlation?.conversationId ?? lockKey,
|
|
@@ -11884,14 +13102,13 @@ async function resumeSlackTurn(args) {
|
|
|
11884
13102
|
};
|
|
11885
13103
|
} else {
|
|
11886
13104
|
deferredFailureHandler = async () => {
|
|
11887
|
-
await
|
|
11888
|
-
|
|
11889
|
-
|
|
11890
|
-
|
|
11891
|
-
|
|
11892
|
-
|
|
11893
|
-
|
|
11894
|
-
}
|
|
13105
|
+
await handleResumeFailure({
|
|
13106
|
+
body: "Failed to resume Slack turn",
|
|
13107
|
+
error,
|
|
13108
|
+
eventName: "slack_resume_turn_failed",
|
|
13109
|
+
lockKey,
|
|
13110
|
+
resumeArgs: args
|
|
13111
|
+
});
|
|
11895
13112
|
};
|
|
11896
13113
|
}
|
|
11897
13114
|
} finally {
|
|
@@ -11902,14 +13119,13 @@ async function resumeSlackTurn(args) {
|
|
|
11902
13119
|
await deferredPauseHandler();
|
|
11903
13120
|
return;
|
|
11904
13121
|
} catch (pauseError) {
|
|
11905
|
-
await
|
|
11906
|
-
|
|
11907
|
-
|
|
11908
|
-
|
|
11909
|
-
|
|
11910
|
-
|
|
11911
|
-
|
|
11912
|
-
}
|
|
13122
|
+
await handleResumeFailure({
|
|
13123
|
+
body: "Failed to handle resumed turn pause",
|
|
13124
|
+
error: pauseError,
|
|
13125
|
+
eventName: "slack_resume_pause_handler_failed",
|
|
13126
|
+
lockKey,
|
|
13127
|
+
resumeArgs: args
|
|
13128
|
+
});
|
|
11913
13129
|
return;
|
|
11914
13130
|
}
|
|
11915
13131
|
}
|
|
@@ -11925,7 +13141,6 @@ async function resumeAuthorizedRequest(args) {
|
|
|
11925
13141
|
replyContext: args.replyContext,
|
|
11926
13142
|
lockKey: args.lockKey,
|
|
11927
13143
|
initialText: args.connectedText,
|
|
11928
|
-
failureText: args.failureText,
|
|
11929
13144
|
generateReply: args.generateReply,
|
|
11930
13145
|
onSuccess: args.onSuccess,
|
|
11931
13146
|
onFailure: args.onFailure,
|
|
@@ -12233,7 +13448,6 @@ async function resumeAuthorizedMcpTurn(args) {
|
|
|
12233
13448
|
threadTs: authSession.threadTs,
|
|
12234
13449
|
lockKey: authSession.conversationId,
|
|
12235
13450
|
connectedText: "",
|
|
12236
|
-
failureText: "MCP authorization completed, but resuming the request failed. Please retry the original command.",
|
|
12237
13451
|
replyContext: {
|
|
12238
13452
|
requester: {
|
|
12239
13453
|
userId: authSession.userId,
|
|
@@ -12283,14 +13497,7 @@ async function resumeAuthorizedMcpTurn(args) {
|
|
|
12283
13497
|
);
|
|
12284
13498
|
}
|
|
12285
13499
|
},
|
|
12286
|
-
onFailure: async (
|
|
12287
|
-
logException(
|
|
12288
|
-
error,
|
|
12289
|
-
"mcp_oauth_callback_resume_failed",
|
|
12290
|
-
{},
|
|
12291
|
-
{ "app.credential.provider": provider },
|
|
12292
|
-
"Failed to resume MCP-authorized turn"
|
|
12293
|
-
);
|
|
13500
|
+
onFailure: async () => {
|
|
12294
13501
|
try {
|
|
12295
13502
|
await persistFailedReplyState(
|
|
12296
13503
|
authSession.channelId,
|
|
@@ -12403,7 +13610,7 @@ async function GET4(request, provider, waitUntil) {
|
|
|
12403
13610
|
|
|
12404
13611
|
// src/chat/slack/app-home.ts
|
|
12405
13612
|
import fs5 from "fs";
|
|
12406
|
-
import
|
|
13613
|
+
import path10 from "path";
|
|
12407
13614
|
var DEFAULT_DESCRIPTION_TEXT = "I help your team investigate, summarize, and act on work in Slack.";
|
|
12408
13615
|
var MAX_HOME_SKILLS = 6;
|
|
12409
13616
|
var MAX_SECTION_TEXT_CHARS = 3e3;
|
|
@@ -12415,7 +13622,7 @@ function clampSectionText(text) {
|
|
|
12415
13622
|
return `${text.slice(0, MAX_SECTION_TEXT_CHARS - 1)}\u2026`;
|
|
12416
13623
|
}
|
|
12417
13624
|
function loadDescriptionText() {
|
|
12418
|
-
const descriptionPath =
|
|
13625
|
+
const descriptionPath = path10.join(homeDir(), "DESCRIPTION.md");
|
|
12419
13626
|
try {
|
|
12420
13627
|
const raw = fs5.readFileSync(descriptionPath, "utf8").trim();
|
|
12421
13628
|
if (raw.length > 0) {
|
|
@@ -12682,7 +13889,6 @@ async function resumeCheckpointedOAuthTurn(stored) {
|
|
|
12682
13889
|
threadTs: stored.threadTs,
|
|
12683
13890
|
lockKey: stored.resumeConversationId,
|
|
12684
13891
|
initialText: "",
|
|
12685
|
-
failureText: "I connected your account but hit an error processing your request. Please try the command again.",
|
|
12686
13892
|
replyContext: {
|
|
12687
13893
|
requester: {
|
|
12688
13894
|
userId: userMessage.author.userId,
|
|
@@ -12730,14 +13936,7 @@ async function resumeCheckpointedOAuthTurn(stored) {
|
|
|
12730
13936
|
reply
|
|
12731
13937
|
});
|
|
12732
13938
|
},
|
|
12733
|
-
onFailure: async (
|
|
12734
|
-
logException(
|
|
12735
|
-
error,
|
|
12736
|
-
"oauth_callback_resume_failed",
|
|
12737
|
-
{},
|
|
12738
|
-
{ "app.credential.provider": stored.provider },
|
|
12739
|
-
"Failed to auto-resume checkpointed turn after OAuth callback"
|
|
12740
|
-
);
|
|
13939
|
+
onFailure: async () => {
|
|
12741
13940
|
await persistFailedOAuthReplyState({
|
|
12742
13941
|
conversationId: stored.resumeConversationId,
|
|
12743
13942
|
sessionId: resolvedSessionId
|
|
@@ -12789,7 +13988,6 @@ async function resumePendingOAuthMessage(stored) {
|
|
|
12789
13988
|
channelId: stored.channelId,
|
|
12790
13989
|
threadTs: stored.threadTs,
|
|
12791
13990
|
connectedText: "",
|
|
12792
|
-
failureText: `I connected your account but hit an error processing your request. Please try \`${stored.pendingMessage}\` again.`,
|
|
12793
13991
|
replyContext: {
|
|
12794
13992
|
requester: { userId: stored.userId },
|
|
12795
13993
|
conversationContext,
|
|
@@ -12807,15 +14005,6 @@ async function resumePendingOAuthMessage(stored) {
|
|
|
12807
14005
|
},
|
|
12808
14006
|
"OAuth callback auto-resumed pending message finished replying"
|
|
12809
14007
|
);
|
|
12810
|
-
},
|
|
12811
|
-
onFailure: async (error) => {
|
|
12812
|
-
logException(
|
|
12813
|
-
error,
|
|
12814
|
-
"oauth_callback_resume_failed",
|
|
12815
|
-
{},
|
|
12816
|
-
{ "app.credential.provider": stored.provider },
|
|
12817
|
-
"Failed to auto-resume pending message after OAuth callback"
|
|
12818
|
-
);
|
|
12819
14008
|
}
|
|
12820
14009
|
});
|
|
12821
14010
|
}
|
|
@@ -13142,7 +14331,6 @@ async function resumeTimedOutTurn(payload) {
|
|
|
13142
14331
|
channelId: thread.channelId,
|
|
13143
14332
|
threadTs: thread.threadTs,
|
|
13144
14333
|
lockKey: payload.conversationId,
|
|
13145
|
-
failureText: "I hit an error while resuming that request. Please try the command again.",
|
|
13146
14334
|
replyContext: {
|
|
13147
14335
|
requester: {
|
|
13148
14336
|
userId: userMessage.author.userId,
|
|
@@ -13191,18 +14379,7 @@ async function resumeTimedOutTurn(payload) {
|
|
|
13191
14379
|
);
|
|
13192
14380
|
}
|
|
13193
14381
|
},
|
|
13194
|
-
onFailure: async (
|
|
13195
|
-
logException(
|
|
13196
|
-
error,
|
|
13197
|
-
"timeout_resume_failed",
|
|
13198
|
-
{},
|
|
13199
|
-
{
|
|
13200
|
-
"app.ai.conversation_id": payload.conversationId,
|
|
13201
|
-
"app.ai.session_id": payload.sessionId,
|
|
13202
|
-
...isRetryableTurnError(error) ? { "app.turn.retryable_reason": error.reason } : {}
|
|
13203
|
-
},
|
|
13204
|
-
"Failed to resume timed-out turn"
|
|
13205
|
-
);
|
|
14382
|
+
onFailure: async () => {
|
|
13206
14383
|
await persistFailedReplyState2(checkpoint);
|
|
13207
14384
|
},
|
|
13208
14385
|
onAuthPause: async () => {
|
|
@@ -13314,11 +14491,11 @@ var DIRECTED_FOLLOW_UP_CUE_RE = /\b(?:you said|you just said|your last response|
|
|
|
13314
14491
|
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;
|
|
13315
14492
|
var GENERIC_IMMEDIATE_SIDE_CONVERSATION_RE = /^(?:is that (?:the )?right (?:approach|call|move)|(?:can|could|would) you check on this)\??$/i;
|
|
13316
14493
|
var RECENT_THREAD_WINDOW = 6;
|
|
13317
|
-
function
|
|
14494
|
+
function escapeRegExp2(value) {
|
|
13318
14495
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
13319
14496
|
}
|
|
13320
14497
|
function containsAssistantInvocation(text, botUserName) {
|
|
13321
|
-
const escapedUserName =
|
|
14498
|
+
const escapedUserName = escapeRegExp2(botUserName);
|
|
13322
14499
|
const plainNameMentionRe = new RegExp(`(^|\\s)@${escapedUserName}\\b`, "i");
|
|
13323
14500
|
const labeledEntityMentionRe = new RegExp(
|
|
13324
14501
|
`<@[^>|]+\\|${escapedUserName}>`,
|
|
@@ -13614,7 +14791,7 @@ async function decideSubscribedThreadReply(args) {
|
|
|
13614
14791
|
}
|
|
13615
14792
|
|
|
13616
14793
|
// src/chat/runtime/thread-context.ts
|
|
13617
|
-
function
|
|
14794
|
+
function escapeRegExp3(value) {
|
|
13618
14795
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
13619
14796
|
}
|
|
13620
14797
|
function stripLeadingBotMention(text, options = {}) {
|
|
@@ -13624,12 +14801,12 @@ function stripLeadingBotMention(text, options = {}) {
|
|
|
13624
14801
|
next = next.replace(/^\s*<@[^>]+>[\s,:-]*/, "").trim();
|
|
13625
14802
|
}
|
|
13626
14803
|
const mentionByNameRe = new RegExp(
|
|
13627
|
-
`^\\s*@${
|
|
14804
|
+
`^\\s*@${escapeRegExp3(botConfig.userName)}\\b[\\s,:-]*`,
|
|
13628
14805
|
"i"
|
|
13629
14806
|
);
|
|
13630
14807
|
next = next.replace(mentionByNameRe, "").trim();
|
|
13631
14808
|
const mentionByLabeledEntityRe = new RegExp(
|
|
13632
|
-
`^\\s*<@[^>|]+\\|${
|
|
14809
|
+
`^\\s*<@[^>|]+\\|${escapeRegExp3(botConfig.userName)}>[\\s,:-]*`,
|
|
13633
14810
|
"i"
|
|
13634
14811
|
);
|
|
13635
14812
|
next = next.replace(mentionByLabeledEntityRe, "").trim();
|
|
@@ -13730,15 +14907,6 @@ async function maybeHandleThreadOptOutDecision(args) {
|
|
|
13730
14907
|
await args.thread.post(THREAD_OPTOUT_ACK);
|
|
13731
14908
|
return true;
|
|
13732
14909
|
}
|
|
13733
|
-
function buildFailureMessage(reference) {
|
|
13734
|
-
if (!reference) {
|
|
13735
|
-
return "I ran into an internal error while processing that. Please try again.";
|
|
13736
|
-
}
|
|
13737
|
-
if (reference.eventId) {
|
|
13738
|
-
return `I ran into an internal error while processing that. Reference: \`event_id=${reference.eventId} trace_id=${reference.traceId}\`.`;
|
|
13739
|
-
}
|
|
13740
|
-
return `I ran into an internal error while processing that. Reference: \`trace_id=${reference.traceId}\`.`;
|
|
13741
|
-
}
|
|
13742
14910
|
function buildLogContext(deps, args) {
|
|
13743
14911
|
return {
|
|
13744
14912
|
conversationId: args.threadId ?? args.runId,
|
|
@@ -13755,7 +14923,7 @@ function createSlackTurnRuntime(deps) {
|
|
|
13755
14923
|
const logContext = (args) => buildLogContext(deps, args);
|
|
13756
14924
|
const postFallbackErrorReplyWithLogging = async (args) => {
|
|
13757
14925
|
try {
|
|
13758
|
-
await args.thread.post(
|
|
14926
|
+
await args.thread.post(buildTurnFailureResponse(args.eventId));
|
|
13759
14927
|
} catch (postError) {
|
|
13760
14928
|
deps.logException(
|
|
13761
14929
|
postError,
|
|
@@ -13763,7 +14931,7 @@ function createSlackTurnRuntime(deps) {
|
|
|
13763
14931
|
args.errorContext,
|
|
13764
14932
|
{
|
|
13765
14933
|
"app.slack.reply_stage": "error_fallback_post",
|
|
13766
|
-
|
|
14934
|
+
"app.error.original_event_id": args.eventId,
|
|
13767
14935
|
...getSlackErrorObservabilityAttributes(postError)
|
|
13768
14936
|
},
|
|
13769
14937
|
args.postFailureBody
|
|
@@ -13849,11 +15017,14 @@ function createSlackTurnRuntime(deps) {
|
|
|
13849
15017
|
{},
|
|
13850
15018
|
"onNewMention failed"
|
|
13851
15019
|
);
|
|
15020
|
+
if (!eventId) {
|
|
15021
|
+
throw new Error(
|
|
15022
|
+
"Sentry did not return an event ID for mention_handler_failed"
|
|
15023
|
+
);
|
|
15024
|
+
}
|
|
13852
15025
|
await hooks?.beforeFirstResponsePost?.();
|
|
13853
|
-
const reference = deps.getErrorReference(eventId);
|
|
13854
15026
|
await postFallbackErrorReplyWithLogging({
|
|
13855
15027
|
thread,
|
|
13856
|
-
reference,
|
|
13857
15028
|
errorContext,
|
|
13858
15029
|
eventId,
|
|
13859
15030
|
postFailureEventName: "mention_handler_failure_reply_post_failed",
|
|
@@ -13981,11 +15152,14 @@ function createSlackTurnRuntime(deps) {
|
|
|
13981
15152
|
{},
|
|
13982
15153
|
"onSubscribedMessage failed"
|
|
13983
15154
|
);
|
|
15155
|
+
if (!eventId) {
|
|
15156
|
+
throw new Error(
|
|
15157
|
+
"Sentry did not return an event ID for subscribed_message_handler_failed"
|
|
15158
|
+
);
|
|
15159
|
+
}
|
|
13984
15160
|
await hooks?.beforeFirstResponsePost?.();
|
|
13985
|
-
const reference = deps.getErrorReference(eventId);
|
|
13986
15161
|
await postFallbackErrorReplyWithLogging({
|
|
13987
15162
|
thread,
|
|
13988
|
-
reference,
|
|
13989
15163
|
errorContext,
|
|
13990
15164
|
eventId,
|
|
13991
15165
|
postFailureEventName: "subscribed_message_handler_failure_reply_post_failed",
|
|
@@ -14773,19 +15947,6 @@ function maybeUpdateAssistantTitle(args) {
|
|
|
14773
15947
|
}
|
|
14774
15948
|
|
|
14775
15949
|
// src/chat/runtime/reply-executor.ts
|
|
14776
|
-
function getExecutionFailureReason(reply) {
|
|
14777
|
-
const errorMessage = reply.diagnostics.errorMessage?.trim();
|
|
14778
|
-
if (errorMessage) {
|
|
14779
|
-
return errorMessage;
|
|
14780
|
-
}
|
|
14781
|
-
if (reply.diagnostics.toolErrorCount > 0) {
|
|
14782
|
-
return `${reply.diagnostics.toolErrorCount} tool result error(s)`;
|
|
14783
|
-
}
|
|
14784
|
-
if (reply.diagnostics.assistantMessageCount > 0) {
|
|
14785
|
-
return "assistant returned no text";
|
|
14786
|
-
}
|
|
14787
|
-
return "empty assistant turn";
|
|
14788
|
-
}
|
|
14789
15950
|
function createReplyToThread(deps) {
|
|
14790
15951
|
return async function replyToThread(thread, message, options = {}) {
|
|
14791
15952
|
if (message.author.isMe) {
|
|
@@ -14935,7 +16096,7 @@ function createReplyToThread(deps) {
|
|
|
14935
16096
|
let shouldPersistFailureState = true;
|
|
14936
16097
|
try {
|
|
14937
16098
|
const toolChannelId = preparedState.artifacts.assistantContextChannelId ?? channelId;
|
|
14938
|
-
|
|
16099
|
+
let reply = await deps.services.generateAssistantReply(userText, {
|
|
14939
16100
|
requester: {
|
|
14940
16101
|
userId: message.author.userId,
|
|
14941
16102
|
userName: message.author.userName ?? fallbackIdentity?.userName,
|
|
@@ -14994,49 +16155,14 @@ function createReplyToThread(deps) {
|
|
|
14994
16155
|
assistantUserName: botConfig.userName,
|
|
14995
16156
|
modelId: reply.diagnostics.modelId
|
|
14996
16157
|
};
|
|
14997
|
-
const diagnosticsAttributes =
|
|
14998
|
-
"gen_ai.provider.name": GEN_AI_PROVIDER_NAME,
|
|
14999
|
-
"gen_ai.operation.name": "invoke_agent",
|
|
15000
|
-
"app.ai.outcome": reply.diagnostics.outcome,
|
|
15001
|
-
"app.ai.assistant_messages": reply.diagnostics.assistantMessageCount,
|
|
15002
|
-
"app.ai.tool_results": reply.diagnostics.toolResultCount,
|
|
15003
|
-
"app.ai.tool_error_results": reply.diagnostics.toolErrorCount,
|
|
15004
|
-
"app.ai.tool_call_count": reply.diagnostics.toolCalls.length,
|
|
15005
|
-
"app.ai.used_primary_text": reply.diagnostics.usedPrimaryText,
|
|
15006
|
-
...reply.diagnostics.thinkingLevel ? {
|
|
15007
|
-
"app.ai.reasoning_effort": reply.diagnostics.thinkingLevel
|
|
15008
|
-
} : {},
|
|
15009
|
-
...reply.diagnostics.stopReason ? {
|
|
15010
|
-
"gen_ai.response.finish_reasons": [
|
|
15011
|
-
reply.diagnostics.stopReason
|
|
15012
|
-
]
|
|
15013
|
-
} : {},
|
|
15014
|
-
...reply.diagnostics.errorMessage ? { "error.message": reply.diagnostics.errorMessage } : {}
|
|
15015
|
-
};
|
|
16158
|
+
const diagnosticsAttributes = getAgentTurnDiagnosticsAttributes(reply);
|
|
15016
16159
|
setSpanAttributes(diagnosticsAttributes);
|
|
15017
|
-
if (reply.diagnostics.outcome
|
|
15018
|
-
|
|
15019
|
-
reply
|
|
15020
|
-
|
|
15021
|
-
|
|
15022
|
-
|
|
15023
|
-
"agent_turn_provider_error",
|
|
15024
|
-
diagnosticsContext,
|
|
15025
|
-
diagnosticsAttributes,
|
|
15026
|
-
"Agent turn failed with provider error"
|
|
15027
|
-
);
|
|
15028
|
-
} else if (reply.diagnostics.outcome !== "success") {
|
|
15029
|
-
const failureReason = getExecutionFailureReason(reply);
|
|
15030
|
-
logException(
|
|
15031
|
-
new Error(`Agent turn execution failure: ${failureReason}`),
|
|
15032
|
-
"agent_turn_execution_failure",
|
|
15033
|
-
diagnosticsContext,
|
|
15034
|
-
{
|
|
15035
|
-
...diagnosticsAttributes,
|
|
15036
|
-
"app.ai.execution_failure_reason": failureReason
|
|
15037
|
-
},
|
|
15038
|
-
"Agent turn completed with execution failure"
|
|
15039
|
-
);
|
|
16160
|
+
if (reply.diagnostics.outcome !== "success") {
|
|
16161
|
+
reply = finalizeFailedTurnReply({
|
|
16162
|
+
reply,
|
|
16163
|
+
logException,
|
|
16164
|
+
context: diagnosticsContext
|
|
16165
|
+
});
|
|
15040
16166
|
}
|
|
15041
16167
|
markConversationMessage(
|
|
15042
16168
|
preparedState.conversation,
|
|
@@ -15408,7 +16534,6 @@ function createSlackRuntime(options) {
|
|
|
15408
16534
|
assistantUserName: botConfig.userName,
|
|
15409
16535
|
modelId: botConfig.modelId,
|
|
15410
16536
|
now: options.now ?? (() => Date.now()),
|
|
15411
|
-
getErrorReference: resolveErrorReference,
|
|
15412
16537
|
getThreadId,
|
|
15413
16538
|
getChannelId,
|
|
15414
16539
|
getRunId,
|