@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 CHANGED
@@ -3,7 +3,7 @@ import {
3
3
  findSkillByName,
4
4
  loadSkillsByName,
5
5
  parseSkillInvocation
6
- } from "./chunk-LAD5O3RX.js";
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-ERH4OYNB.js";
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-QZRPUFO6.js";
70
- import "./chunk-Z3YD6NHK.js";
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
- logWarn(
2776
- "slack_output_normalized_empty",
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 below are authoritative; the personality block sets voice only.";
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 = this.config.envKey && this.config.placeholder ? { [this.config.envKey]: this.config.placeholder } : {};
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/attach-file.ts
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 (path4.posix.isAbsolute(trimmed)) {
5282
+ if (path7.posix.isAbsolute(trimmed)) {
4628
5283
  return trimmed;
4629
5284
  }
4630
- return path4.posix.join(SANDBOX_WORKSPACE_ROOT, trimmed);
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 = path4.posix.basename(candidate);
5290
+ const base = path7.posix.basename(candidate);
4636
5291
  if (base && base !== "." && base !== "..") {
4637
5292
  return base;
4638
5293
  }
4639
5294
  }
4640
- const derived = path4.posix.basename(fallbackPath);
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 = path4.extname(filename).toLowerCase();
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: Type2.Object(
5327
+ inputSchema: Type5.Object(
4673
5328
  {
4674
- path: Type2.String({
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: Type2.Optional(
4679
- Type2.String({
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: Type2.Optional(
4685
- Type2.String({
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
- path4.posix.basename(targetPath)
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 Type3 } from "@sinclair/typebox";
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: Type3.Object({
4808
- prompt: Type3.String({
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 Type4 } from "@sinclair/typebox";
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: Type4.Object(
5658
+ inputSchema: Type8.Object(
4917
5659
  {
4918
- tool_name: Type4.String({
5660
+ tool_name: Type8.String({
4919
5661
  minLength: 1,
4920
5662
  description: "Exact MCP tool_name from searchMcpTools."
4921
5663
  }),
4922
- arguments: Type4.Optional(
4923
- Type4.Record(Type4.String(), Type4.Unknown(), {
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 Type5 } from "@sinclair/typebox";
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: Type5.Object({
4992
- skill_name: Type5.String({
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 Type6 } from "@sinclair/typebox";
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: Type6.Object(
5944
+ inputSchema: Type10.Object(
5203
5945
  {
5204
- query: Type6.Optional(
5205
- Type6.String({
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: Type6.Optional(
5211
- Type6.String({
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: Type6.Optional(
5217
- Type6.Integer({
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 Type7 } from "@sinclair/typebox";
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 file from the sandbox workspace. Use when you need exact file contents to verify facts or make edits safely. Do not use for broad discovery when search tools are better.",
5252
- inputSchema: Type7.Object(
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: Type7.String({
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 Type8 } from "@sinclair/typebox";
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: Type8.Object({
5275
- message: Type8.String({
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 Type9 } from "@sinclair/typebox";
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
- inputSchema: Type9.Object({
5374
- limit: Type9.Optional(
5375
- Type9.Integer({
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: Type9.Optional(
5382
- Type9.String({
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: Type9.Optional(
5388
- Type9.String({
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: Type9.Optional(
5394
- Type9.String({
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: Type9.Optional(
5400
- Type9.Boolean({
6181
+ inclusive: Type13.Optional(
6182
+ Type13.Boolean({
5401
6183
  description: "Whether oldest/latest bounds should be inclusive."
5402
6184
  })
5403
6185
  ),
5404
- max_pages: Type9.Optional(
5405
- Type9.Integer({
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 Type10 } from "@sinclair/typebox";
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: Type10.Object({
5482
- text: Type10.String({
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 Type11 } from "@sinclair/typebox";
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: Type11.Object({
5530
- emoji: Type11.String({
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 Type12 } from "@sinclair/typebox";
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: Type12.Object({
5805
- title: Type12.String({
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: Type12.String({
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: Type12.Object({
5877
- markdown: Type12.String({
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: Type12.Optional(
5882
- Type12.Union(
6663
+ operation: Type16.Optional(
6664
+ Type16.Union(
5883
6665
  [
5884
- Type12.Literal("insert_at_end"),
5885
- Type12.Literal("insert_at_start"),
5886
- Type12.Literal("replace")
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: Type12.Optional(
5892
- Type12.String({
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: Type12.Optional(
5898
- Type12.String({
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
- inputSchema: Type12.Object({
5965
- canvas: Type12.String({
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 Type13 } from "@sinclair/typebox";
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: Type13.Object({
6190
- name: Type13.String({
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: Type13.Object({
6226
- items: Type13.Array(Type13.String({ minLength: 1 }), {
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: Type13.Optional(
6232
- Type13.String({
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: Type13.Optional(
6238
- Type13.String({
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
- inputSchema: Type13.Object({
6288
- limit: Type13.Optional(
6289
- Type13.Integer({
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: Type13.Object(
7098
+ inputSchema: Type17.Object(
6315
7099
  {
6316
- item_id: Type13.String({
7100
+ item_id: Type17.String({
6317
7101
  minLength: 1,
6318
7102
  description: "ID of the Slack list item to update."
6319
7103
  }),
6320
- completed: Type13.Optional(
6321
- Type13.Boolean({
7104
+ completed: Type17.Optional(
7105
+ Type17.Boolean({
6322
7106
  description: "Optional completion status update."
6323
7107
  })
6324
7108
  ),
6325
- title: Type13.Optional(
6326
- Type13.String({
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 Type14 } from "@sinclair/typebox";
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
- inputSchema: Type14.Object({}),
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 Type15 } from "@sinclair/typebox";
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 ADVISOR_ALLOWED_TOOL_NAMES = /* @__PURE__ */ new Set([
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 inspection tools when direct inspection or verification would materially improve the advice.",
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 isAdvisorToolAllowed(toolName) {
6688
- return ADVISOR_ALLOWED_TOOL_NAMES.has(toolName);
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: Type15.Object({
6696
- question: Type15.String({
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: Type15.String({
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 Type16 } from "@sinclair/typebox";
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
- () => reject(new Error(`${label} timed out`)),
7019
- timeoutMs
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
- inputSchema: Type16.Object({
7158
- url: Type16.String({
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: Type16.Optional(
7163
- Type16.Integer({
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 Type17 } from "@sinclair/typebox";
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
- inputSchema: Type17.Object({
7267
- query: Type17.String({
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: Type17.Optional(
7273
- Type17.Integer({
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 Type18 } from "@sinclair/typebox";
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
- inputSchema: Type18.Object(
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: Type18.String({
8140
+ path: Type22.String({
7346
8141
  minLength: 1,
7347
8142
  description: "Path to write in the sandbox workspace."
7348
8143
  }),
7349
- content: Type18.String({
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 path5 from "path";
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 path5.relative(base, absolute).split(path5.sep).join("/");
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 = path5.join(dir, entry.name);
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 = path5.basename(absoluteFile);
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 = path5.posix.normalize(file.path);
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 = path5.posix.normalize(sandboxPath.trim());
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 = path5.posix.relative(virtualRoot, normalizedPath);
8939
+ const relativePath = path9.posix.relative(virtualRoot, normalizedPath);
8141
8940
  if (!relativePath || relativePath.startsWith("../")) {
8142
8941
  return null;
8143
8942
  }
8144
- const hostRoot = path5.resolve(skill.skillPath);
8145
- const hostPath = path5.resolve(hostRoot, ...relativePath.split("/"));
8146
- if (hostPath !== hostRoot && !hostPath.startsWith(`${hostRoot}${path5.sep}`)) {
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 = path5.posix.normalize(sandboxPath.trim());
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 = path5.posix.relative(SANDBOX_DATA_ROOT, normalizedPath);
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 (path5.basename(hostFile) === relativePath) {
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
- const commandResult2 = await sandboxInstance.runCommand({
8666
- cmd: "bash",
8667
- args: ["-c", script],
8668
- cwd: SANDBOX_WORKSPACE_ROOT
8669
- });
8670
- return await readCommandOutput(commandResult2);
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(["bash", "readFile", "writeFile"]);
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: false,
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
- success: true
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
- content,
8907
- path: filePath,
8908
- success: true
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 { command: String(params.command ?? "") };
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 { path: String(params.path ?? "") };
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 || (sideEffectOnlySuccess ? "" : fallbackText);
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 ? fallbackText : enforceAttachmentClaimTruth(responseText, replyFiles.length > 0);
9440
- const deliveryPlan = reactionPerformed && !resolvedText && replyFiles.length === 0 && !channelPostPerformed ? {
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 = agentTools.filter((tool2) => isAdvisorToolAllowed(tool2.name));
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 getClient2 = args.getSlackClient ?? getSlackClient;
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 getClient2().assistant.threads.setStatus({
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("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
11471
12583
  }
12584
+ function escapeSlackLinkUrl(url) {
12585
+ return url.replaceAll("&", "&amp;").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
- items.push({
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
- const reply = typeof replyTimeoutMs === "number" ? await Promise.race([
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 args.onFailure?.(error);
11888
- if (args.failureText) {
11889
- await postSlackMessageBestEffort(
11890
- args.channelId,
11891
- args.threadTs,
11892
- args.failureText
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 args.onFailure?.(pauseError);
11906
- if (args.failureText) {
11907
- await postSlackMessageBestEffort(
11908
- args.channelId,
11909
- args.threadTs,
11910
- args.failureText
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 (error) => {
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 path6 from "path";
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 = path6.join(homeDir(), "DESCRIPTION.md");
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 (error) => {
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 (error) => {
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 escapeRegExp(value) {
14494
+ function escapeRegExp2(value) {
13318
14495
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
13319
14496
  }
13320
14497
  function containsAssistantInvocation(text, botUserName) {
13321
- const escapedUserName = escapeRegExp(botUserName);
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 escapeRegExp2(value) {
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*@${escapeRegExp2(botConfig.userName)}\\b[\\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*<@[^>|]+\\|${escapeRegExp2(botConfig.userName)}>[\\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(buildFailureMessage(args.reference));
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
- ...args.eventId ? { "app.error.original_event_id": args.eventId } : {},
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
- const reply = await deps.services.generateAssistantReply(userText, {
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 === "provider_error") {
15018
- const providerError = reply.diagnostics.providerError ?? new Error(
15019
- reply.diagnostics.errorMessage ?? "Provider error without explicit message"
15020
- );
15021
- logException(
15022
- providerError,
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,