@sentry/junior 0.38.0 → 0.39.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-7QMPV6YJ.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-DVMGFG4W.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-EQPY4742.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,
@@ -4591,7 +4609,13 @@ function createBashTool() {
4591
4609
  command: Type.String({
4592
4610
  minLength: 1,
4593
4611
  description: "Bash command to run inside the sandbox."
4594
- })
4612
+ }),
4613
+ timeoutMs: Type.Optional(
4614
+ Type.Integer({
4615
+ minimum: 1e3,
4616
+ description: "Optional command timeout in milliseconds. Use for commands that may hang."
4617
+ })
4618
+ )
4595
4619
  },
4596
4620
  { additionalProperties: false }
4597
4621
  ),
@@ -4601,9 +4625,635 @@ function createBashTool() {
4601
4625
  });
4602
4626
  }
4603
4627
 
4604
- // src/chat/tools/sandbox/attach-file.ts
4628
+ // src/chat/tools/sandbox/file-utils.ts
4605
4629
  import path4 from "path";
4630
+ var MAX_TEXT_CHARS = 6e4;
4631
+ var SKIPPED_DIRECTORIES = /* @__PURE__ */ new Set([".git", "node_modules"]);
4632
+ function positiveInteger(value) {
4633
+ if (typeof value !== "number" || !Number.isFinite(value)) {
4634
+ return void 0;
4635
+ }
4636
+ const integer = Math.floor(value);
4637
+ return integer > 0 ? integer : void 0;
4638
+ }
4639
+ function normalizeToLf(value) {
4640
+ return value.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
4641
+ }
4642
+ function truncateText(value, maxChars = MAX_TEXT_CHARS) {
4643
+ if (value.length <= maxChars) {
4644
+ return { content: value, truncated: false };
4645
+ }
4646
+ const removed = value.length - maxChars;
4647
+ return {
4648
+ content: `${value.slice(0, maxChars)}
4649
+
4650
+ [output truncated: ${removed} characters removed]`,
4651
+ truncated: true
4652
+ };
4653
+ }
4654
+ function escapeRegExp(value) {
4655
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
4656
+ }
4657
+ function globToRegExp(pattern) {
4658
+ let source = "";
4659
+ for (let index = 0; index < pattern.length; index += 1) {
4660
+ const char = pattern[index];
4661
+ const next = pattern[index + 1];
4662
+ if (char === "*" && next === "*") {
4663
+ if (pattern[index + 2] === "/") {
4664
+ source += "(?:.*/)?";
4665
+ index += 2;
4666
+ continue;
4667
+ }
4668
+ source += ".*";
4669
+ index += 1;
4670
+ continue;
4671
+ }
4672
+ if (char === "*") {
4673
+ source += "[^/]*";
4674
+ continue;
4675
+ }
4676
+ if (char === "?") {
4677
+ source += "[^/]";
4678
+ continue;
4679
+ }
4680
+ source += escapeRegExp(char);
4681
+ }
4682
+ return new RegExp(`^${source}$`);
4683
+ }
4684
+ function matchesGlob(relativePath, pattern) {
4685
+ const matcher = globToRegExp(pattern);
4686
+ if (matcher.test(relativePath)) {
4687
+ return true;
4688
+ }
4689
+ if (pattern.startsWith("**/") && matchesGlob(relativePath, pattern.slice(3))) {
4690
+ return true;
4691
+ }
4692
+ return !pattern.includes("/") && matcher.test(path4.posix.basename(relativePath));
4693
+ }
4694
+ function resolveWorkspacePath(input, fallback = ".") {
4695
+ const requested = (input ?? "").trim() || fallback;
4696
+ const absolute = requested.startsWith("/") ? requested : path4.posix.join(SANDBOX_WORKSPACE_ROOT, requested);
4697
+ const normalized = path4.posix.normalize(absolute);
4698
+ if (normalized !== SANDBOX_WORKSPACE_ROOT && !normalized.startsWith(`${SANDBOX_WORKSPACE_ROOT}/`)) {
4699
+ throw new Error(
4700
+ `Path must stay within ${SANDBOX_WORKSPACE_ROOT}: ${requested}`
4701
+ );
4702
+ }
4703
+ return normalized;
4704
+ }
4705
+ async function collectFiles(params) {
4706
+ const files = [];
4707
+ let limitReached = false;
4708
+ const visit = async (dirPath) => {
4709
+ const entries = (await params.fs.readdir(dirPath)).sort(
4710
+ (a, b) => a.toLowerCase().localeCompare(b.toLowerCase())
4711
+ );
4712
+ for (const entry of entries) {
4713
+ const fullPath = path4.posix.join(dirPath, entry);
4714
+ const stat2 = await params.fs.stat(fullPath);
4715
+ if (stat2.isDirectory()) {
4716
+ if (!SKIPPED_DIRECTORIES.has(entry)) {
4717
+ await visit(fullPath);
4718
+ }
4719
+ if (limitReached) return;
4720
+ continue;
4721
+ }
4722
+ const relativePath = path4.posix.relative(params.root, fullPath);
4723
+ if (!params.pattern || matchesGlob(relativePath, params.pattern)) {
4724
+ files.push(fullPath);
4725
+ if (params.limit && files.length >= params.limit) {
4726
+ limitReached = true;
4727
+ return;
4728
+ }
4729
+ }
4730
+ }
4731
+ };
4732
+ const stat = await params.fs.stat(params.root);
4733
+ if (!stat.isDirectory()) {
4734
+ const relativePath = path4.posix.basename(params.root);
4735
+ return {
4736
+ files: !params.pattern || matchesGlob(relativePath, params.pattern) ? [params.root] : [],
4737
+ limitReached: false
4738
+ };
4739
+ }
4740
+ await visit(params.root);
4741
+ return { files, limitReached };
4742
+ }
4743
+
4744
+ // src/chat/tools/sandbox/edit-file.ts
4606
4745
  import { Type as Type2 } from "@sinclair/typebox";
4746
+ function detectLineEnding(value) {
4747
+ return value.includes("\r\n") ? "\r\n" : "\n";
4748
+ }
4749
+ function restoreLineEndings(value, lineEnding) {
4750
+ return lineEnding === "\r\n" ? value.replace(/\n/g, "\r\n") : value;
4751
+ }
4752
+ function stripBom(value) {
4753
+ return value.startsWith("\uFEFF") ? { bom: "\uFEFF", text: value.slice(1) } : { bom: "", text: value };
4754
+ }
4755
+ function countOccurrences(content, target) {
4756
+ let count = 0;
4757
+ let start = 0;
4758
+ while (target.length > 0) {
4759
+ const index = content.indexOf(target, start);
4760
+ if (index === -1) break;
4761
+ count += 1;
4762
+ start = index + target.length;
4763
+ }
4764
+ return count;
4765
+ }
4766
+ function firstChangedLine(oldContent, newContent) {
4767
+ const oldLines = oldContent.split("\n");
4768
+ const newLines = newContent.split("\n");
4769
+ const count = Math.max(oldLines.length, newLines.length);
4770
+ for (let index = 0; index < count; index += 1) {
4771
+ if (oldLines[index] !== newLines[index]) {
4772
+ return index + 1;
4773
+ }
4774
+ }
4775
+ return void 0;
4776
+ }
4777
+ function buildCompactDiff(oldContent, newContent) {
4778
+ const oldLines = oldContent.split("\n");
4779
+ const newLines = newContent.split("\n");
4780
+ let prefix = 0;
4781
+ while (prefix < oldLines.length && prefix < newLines.length && oldLines[prefix] === newLines[prefix]) {
4782
+ prefix += 1;
4783
+ }
4784
+ let oldSuffix = oldLines.length - 1;
4785
+ let newSuffix = newLines.length - 1;
4786
+ while (oldSuffix >= prefix && newSuffix >= prefix && oldLines[oldSuffix] === newLines[newSuffix]) {
4787
+ oldSuffix -= 1;
4788
+ newSuffix -= 1;
4789
+ }
4790
+ const contextStart = Math.max(0, prefix - 3);
4791
+ const newContextEnd = Math.min(newLines.length - 1, newSuffix + 3);
4792
+ const oldContextEnd = Math.min(oldLines.length - 1, oldSuffix + 3);
4793
+ const width = String(Math.max(oldLines.length, newLines.length)).length;
4794
+ const output = [];
4795
+ if (contextStart > 0) {
4796
+ output.push(` ${"".padStart(width)} ...`);
4797
+ }
4798
+ for (let index = contextStart; index < prefix; index += 1) {
4799
+ output.push(` ${String(index + 1).padStart(width)} ${oldLines[index]}`);
4800
+ }
4801
+ for (let index = prefix; index <= oldSuffix; index += 1) {
4802
+ output.push(`-${String(index + 1).padStart(width)} ${oldLines[index]}`);
4803
+ }
4804
+ for (let index = prefix; index <= newSuffix; index += 1) {
4805
+ output.push(`+${String(index + 1).padStart(width)} ${newLines[index]}`);
4806
+ }
4807
+ for (let index = newSuffix + 1; index <= newContextEnd; index += 1) {
4808
+ output.push(` ${String(index + 1).padStart(width)} ${newLines[index]}`);
4809
+ }
4810
+ if (newContextEnd < newLines.length - 1 || oldContextEnd < oldLines.length - 1) {
4811
+ output.push(` ${"".padStart(width)} ...`);
4812
+ }
4813
+ return {
4814
+ diff: output.join("\n"),
4815
+ firstChangedLine: firstChangedLine(oldContent, newContent)
4816
+ };
4817
+ }
4818
+ function validateAndApplyEdits(content, edits, filePath) {
4819
+ if (!Array.isArray(edits) || edits.length === 0) {
4820
+ throw new Error("editFile requires at least one edit.");
4821
+ }
4822
+ const normalizedEdits = edits.map((edit, index) => {
4823
+ if (typeof edit.oldText !== "string" || edit.oldText.length === 0) {
4824
+ throw new Error(
4825
+ `edits[${index}].oldText must not be empty in ${filePath}.`
4826
+ );
4827
+ }
4828
+ if (typeof edit.newText !== "string") {
4829
+ throw new Error(
4830
+ `edits[${index}].newText must be a string in ${filePath}.`
4831
+ );
4832
+ }
4833
+ return {
4834
+ oldText: normalizeToLf(edit.oldText),
4835
+ newText: normalizeToLf(edit.newText)
4836
+ };
4837
+ });
4838
+ const matchedEdits = [];
4839
+ for (let index = 0; index < normalizedEdits.length; index += 1) {
4840
+ const edit = normalizedEdits[index];
4841
+ const matchIndex = content.indexOf(edit.oldText);
4842
+ if (matchIndex === -1) {
4843
+ throw new Error(
4844
+ `Could not find edits[${index}] in ${filePath}. oldText must match exactly including whitespace and newlines.`
4845
+ );
4846
+ }
4847
+ const occurrences = countOccurrences(content, edit.oldText);
4848
+ if (occurrences > 1) {
4849
+ throw new Error(
4850
+ `Found ${occurrences} occurrences of edits[${index}] in ${filePath}. Each oldText must be unique.`
4851
+ );
4852
+ }
4853
+ matchedEdits.push({
4854
+ editIndex: index,
4855
+ matchIndex,
4856
+ matchLength: edit.oldText.length,
4857
+ newText: edit.newText
4858
+ });
4859
+ }
4860
+ matchedEdits.sort((a, b) => a.matchIndex - b.matchIndex);
4861
+ for (let index = 1; index < matchedEdits.length; index += 1) {
4862
+ const previous = matchedEdits[index - 1];
4863
+ const current = matchedEdits[index];
4864
+ if (previous.matchIndex + previous.matchLength > current.matchIndex) {
4865
+ throw new Error(
4866
+ `edits[${previous.editIndex}] and edits[${current.editIndex}] overlap in ${filePath}. Merge overlapping replacements into one edit.`
4867
+ );
4868
+ }
4869
+ }
4870
+ let newContent = content;
4871
+ for (let index = matchedEdits.length - 1; index >= 0; index -= 1) {
4872
+ const edit = matchedEdits[index];
4873
+ newContent = newContent.slice(0, edit.matchIndex) + edit.newText + newContent.slice(edit.matchIndex + edit.matchLength);
4874
+ }
4875
+ if (newContent === content) {
4876
+ throw new Error(`No changes made to ${filePath}.`);
4877
+ }
4878
+ return { baseContent: content, newContent };
4879
+ }
4880
+ function prepareEditFileArguments(input) {
4881
+ if (!input || typeof input !== "object") {
4882
+ return input;
4883
+ }
4884
+ const raw = { ...input };
4885
+ if (typeof raw.edits === "string") {
4886
+ try {
4887
+ raw.edits = JSON.parse(raw.edits);
4888
+ } catch {
4889
+ return raw;
4890
+ }
4891
+ }
4892
+ const edits = Array.isArray(raw.edits) ? [...raw.edits] : [];
4893
+ const oldText = raw.oldText ?? raw.old_text;
4894
+ const newText = raw.newText ?? raw.new_text;
4895
+ if (typeof oldText === "string" && typeof newText === "string") {
4896
+ edits.push({ oldText, newText });
4897
+ }
4898
+ if (edits.length > 0) {
4899
+ raw.edits = edits.map((edit) => {
4900
+ if (!edit || typeof edit !== "object") {
4901
+ return edit;
4902
+ }
4903
+ const record = edit;
4904
+ const { old_text, new_text, ...rest } = record;
4905
+ return {
4906
+ ...rest,
4907
+ oldText: record.oldText ?? old_text,
4908
+ newText: record.newText ?? new_text
4909
+ };
4910
+ });
4911
+ }
4912
+ delete raw.oldText;
4913
+ delete raw.old_text;
4914
+ delete raw.newText;
4915
+ delete raw.new_text;
4916
+ return raw;
4917
+ }
4918
+ async function editFile(params) {
4919
+ const filePath = resolveWorkspacePath(params.path);
4920
+ const rawContent = await params.fs.readFile(filePath, { encoding: "utf8" });
4921
+ const { bom, text } = stripBom(rawContent);
4922
+ const lineEnding = detectLineEnding(text);
4923
+ const normalizedContent = normalizeToLf(text);
4924
+ const { baseContent, newContent } = validateAndApplyEdits(
4925
+ normalizedContent,
4926
+ params.edits,
4927
+ params.path
4928
+ );
4929
+ await params.fs.writeFile(
4930
+ filePath,
4931
+ bom + restoreLineEndings(newContent, lineEnding),
4932
+ { encoding: "utf8" }
4933
+ );
4934
+ const diff = buildCompactDiff(baseContent, newContent);
4935
+ return {
4936
+ content: [
4937
+ {
4938
+ type: "text",
4939
+ text: `Successfully replaced ${params.edits.length} block(s) in ${params.path}.`
4940
+ }
4941
+ ],
4942
+ details: {
4943
+ diff: diff.diff,
4944
+ first_changed_line: diff.firstChangedLine,
4945
+ ok: true,
4946
+ path: params.path,
4947
+ replacements: params.edits.length
4948
+ }
4949
+ };
4950
+ }
4951
+ var editReplacementSchema = Type2.Object(
4952
+ {
4953
+ oldText: Type2.String({
4954
+ minLength: 1,
4955
+ description: "Exact text to replace. It must be unique in the original file and must not overlap another edit."
4956
+ }),
4957
+ newText: Type2.String({
4958
+ description: "Replacement text for this edit."
4959
+ })
4960
+ },
4961
+ { additionalProperties: false }
4962
+ );
4963
+ function createEditFileTool() {
4964
+ return tool({
4965
+ 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.",
4966
+ promptSnippet: "existing-file exact edits; returns diff",
4967
+ promptGuidelines: [
4968
+ "prefer over writeFile for targeted changes",
4969
+ "oldText exact, unique, non-overlapping",
4970
+ "multiple same-file changes: one edits[] call"
4971
+ ],
4972
+ prepareArguments: prepareEditFileArguments,
4973
+ executionMode: "sequential",
4974
+ inputSchema: Type2.Object(
4975
+ {
4976
+ path: Type2.String({
4977
+ minLength: 1,
4978
+ description: "Path to edit in the sandbox workspace."
4979
+ }),
4980
+ edits: Type2.Array(editReplacementSchema, {
4981
+ minItems: 1,
4982
+ description: "Exact replacements matched against the original file, not incrementally."
4983
+ })
4984
+ },
4985
+ { additionalProperties: false }
4986
+ ),
4987
+ execute: async () => {
4988
+ throw new Error(
4989
+ "editFile can only run when sandbox execution is enabled."
4990
+ );
4991
+ }
4992
+ });
4993
+ }
4994
+
4995
+ // src/chat/tools/sandbox/find-files.ts
4996
+ import path5 from "path";
4997
+ import { Type as Type3 } from "@sinclair/typebox";
4998
+ var DEFAULT_FIND_LIMIT = 1e3;
4999
+ async function findFiles(params) {
5000
+ if (!params.pattern.trim()) {
5001
+ throw new Error("pattern is required");
5002
+ }
5003
+ const root = resolveWorkspacePath(params.path);
5004
+ const limit = positiveInteger(params.limit) ?? DEFAULT_FIND_LIMIT;
5005
+ const { files, limitReached } = await collectFiles({
5006
+ fs: params.fs,
5007
+ root,
5008
+ pattern: params.pattern,
5009
+ limit
5010
+ });
5011
+ const relativePaths = files.map(
5012
+ (filePath) => path5.posix.relative(root, filePath)
5013
+ );
5014
+ const bounded = truncateText(
5015
+ relativePaths.length > 0 ? relativePaths.join("\n") : "No files found matching pattern"
5016
+ );
5017
+ const notices = [];
5018
+ if (limitReached) {
5019
+ notices.push(
5020
+ `${limit} results limit reached. Refine pattern or raise limit.`
5021
+ );
5022
+ }
5023
+ if (bounded.truncated) {
5024
+ notices.push(`${MAX_TEXT_CHARS} character output limit reached.`);
5025
+ }
5026
+ return {
5027
+ content: [
5028
+ {
5029
+ type: "text",
5030
+ text: notices.length > 0 ? `${bounded.content}
5031
+
5032
+ [${notices.join(" ")}]` : bounded.content
5033
+ }
5034
+ ],
5035
+ details: {
5036
+ ok: true,
5037
+ path: params.path ?? ".",
5038
+ truncated: limitReached || bounded.truncated,
5039
+ ...limitReached ? { result_limit_reached: limit } : {}
5040
+ }
5041
+ };
5042
+ }
5043
+ function createFindFilesTool() {
5044
+ return tool({
5045
+ description: "Find sandbox workspace files by glob pattern. Returns bounded paths relative to the search root and skips dependency/cache directories.",
5046
+ annotations: { readOnlyHint: true, destructiveHint: false },
5047
+ inputSchema: Type3.Object(
5048
+ {
5049
+ pattern: Type3.String({
5050
+ minLength: 1,
5051
+ description: "Glob pattern to match, for example '*.ts', '**/*.json', or 'src/**/*.test.ts'."
5052
+ }),
5053
+ path: Type3.Optional(
5054
+ Type3.String({
5055
+ minLength: 1,
5056
+ description: "Directory or file path in the sandbox workspace. Defaults to the workspace root."
5057
+ })
5058
+ ),
5059
+ limit: Type3.Optional(
5060
+ Type3.Integer({
5061
+ minimum: 1,
5062
+ description: "Maximum number of file paths to return. Defaults to 1000."
5063
+ })
5064
+ )
5065
+ },
5066
+ { additionalProperties: false }
5067
+ ),
5068
+ execute: async () => {
5069
+ throw new Error(
5070
+ "findFiles can only run when sandbox execution is enabled."
5071
+ );
5072
+ }
5073
+ });
5074
+ }
5075
+
5076
+ // src/chat/tools/sandbox/grep.ts
5077
+ import path6 from "path";
5078
+ import { Type as Type4 } from "@sinclair/typebox";
5079
+ var DEFAULT_GREP_LIMIT = 100;
5080
+ var MAX_GREP_LINE_CHARS = 500;
5081
+ function truncateGrepLine(value) {
5082
+ if (value.length <= MAX_GREP_LINE_CHARS) {
5083
+ return { line: value, truncated: false };
5084
+ }
5085
+ return {
5086
+ line: `${value.slice(0, MAX_GREP_LINE_CHARS)}... [line truncated]`,
5087
+ truncated: true
5088
+ };
5089
+ }
5090
+ function lineMatches(params) {
5091
+ if (!params.literal) {
5092
+ return Boolean(params.regex?.test(params.line));
5093
+ }
5094
+ if (params.ignoreCase) {
5095
+ return params.line.toLowerCase().includes(params.pattern.toLowerCase());
5096
+ }
5097
+ return params.line.includes(params.pattern);
5098
+ }
5099
+ async function grepFiles(params) {
5100
+ if (!params.pattern) {
5101
+ throw new Error("pattern is required");
5102
+ }
5103
+ const root = resolveWorkspacePath(params.path);
5104
+ const limit = positiveInteger(params.limit) ?? DEFAULT_GREP_LIMIT;
5105
+ const context = positiveInteger(params.context) ?? 0;
5106
+ const regex = params.literal ? void 0 : new RegExp(params.pattern, params.ignoreCase ? "i" : "");
5107
+ const { files } = await collectFiles({
5108
+ fs: params.fs,
5109
+ root,
5110
+ pattern: params.glob
5111
+ });
5112
+ const output = [];
5113
+ let matchCount = 0;
5114
+ let matchLimitReached = false;
5115
+ let lineTruncated = false;
5116
+ for (const filePath of files) {
5117
+ if (matchLimitReached) break;
5118
+ let content;
5119
+ try {
5120
+ content = await params.fs.readFile(filePath, { encoding: "utf8" });
5121
+ } catch {
5122
+ continue;
5123
+ }
5124
+ if (content.includes("\0")) {
5125
+ continue;
5126
+ }
5127
+ const lines = normalizeToLf(content).split("\n");
5128
+ const relativePath = files.length === 1 && filePath === root ? path6.posix.basename(filePath) : path6.posix.relative(root, filePath);
5129
+ const matchedLines = [];
5130
+ for (let lineIndex = 0; lineIndex < lines.length; lineIndex += 1) {
5131
+ if (!lineMatches({
5132
+ ignoreCase: params.ignoreCase,
5133
+ line: lines[lineIndex],
5134
+ literal: params.literal,
5135
+ pattern: params.pattern,
5136
+ regex
5137
+ })) {
5138
+ continue;
5139
+ }
5140
+ if (matchCount >= limit) {
5141
+ matchLimitReached = true;
5142
+ break;
5143
+ }
5144
+ matchCount += 1;
5145
+ matchedLines.push(lineIndex);
5146
+ }
5147
+ const matchedLineSet = new Set(matchedLines);
5148
+ const emittedLines = /* @__PURE__ */ new Set();
5149
+ for (const lineIndex of matchedLines) {
5150
+ const start = Math.max(0, lineIndex - context);
5151
+ const end = Math.min(lines.length - 1, lineIndex + context);
5152
+ for (let current = start; current <= end; current += 1) {
5153
+ if (emittedLines.has(current)) {
5154
+ continue;
5155
+ }
5156
+ emittedLines.add(current);
5157
+ const truncated = truncateGrepLine(lines[current]);
5158
+ lineTruncated ||= truncated.truncated;
5159
+ const separator = matchedLineSet.has(current) ? ":" : "-";
5160
+ output.push(
5161
+ `${relativePath}${separator}${current + 1}${separator} ${truncated.line}`
5162
+ );
5163
+ }
5164
+ }
5165
+ }
5166
+ const bounded = truncateText(
5167
+ output.length > 0 ? output.join("\n") : "No matches found"
5168
+ );
5169
+ const notices = [];
5170
+ if (matchLimitReached) {
5171
+ notices.push(
5172
+ `${limit} matches limit reached. Refine pattern or raise limit.`
5173
+ );
5174
+ }
5175
+ if (lineTruncated) {
5176
+ notices.push(
5177
+ `Some lines were truncated to ${MAX_GREP_LINE_CHARS} characters.`
5178
+ );
5179
+ }
5180
+ if (bounded.truncated) {
5181
+ notices.push(`${MAX_TEXT_CHARS} character output limit reached.`);
5182
+ }
5183
+ return {
5184
+ content: [
5185
+ {
5186
+ type: "text",
5187
+ text: notices.length > 0 ? `${bounded.content}
5188
+
5189
+ [${notices.join(" ")}]` : bounded.content
5190
+ }
5191
+ ],
5192
+ details: {
5193
+ ok: true,
5194
+ path: params.path ?? ".",
5195
+ truncated: matchLimitReached || lineTruncated || bounded.truncated,
5196
+ ...matchLimitReached ? { match_limit_reached: limit } : {},
5197
+ ...lineTruncated ? { line_truncated: true } : {}
5198
+ }
5199
+ };
5200
+ }
5201
+ function createGrepTool() {
5202
+ return tool({
5203
+ description: "Search sandbox workspace file contents. Returns bounded matching lines with file paths and line numbers. Respects path/glob filters and includes truncation notices.",
5204
+ annotations: { readOnlyHint: true, destructiveHint: false },
5205
+ inputSchema: Type4.Object(
5206
+ {
5207
+ pattern: Type4.String({
5208
+ minLength: 1,
5209
+ description: "Regex pattern or literal text to search for."
5210
+ }),
5211
+ path: Type4.Optional(
5212
+ Type4.String({
5213
+ minLength: 1,
5214
+ description: "Directory or file path in the sandbox workspace. Defaults to the workspace root."
5215
+ })
5216
+ ),
5217
+ glob: Type4.Optional(
5218
+ Type4.String({
5219
+ minLength: 1,
5220
+ description: "Optional glob filter such as '*.ts' or '**/*.test.ts'."
5221
+ })
5222
+ ),
5223
+ ignoreCase: Type4.Optional(
5224
+ Type4.Boolean({
5225
+ description: "Whether matching should ignore case."
5226
+ })
5227
+ ),
5228
+ literal: Type4.Optional(
5229
+ Type4.Boolean({
5230
+ description: "Treat pattern as literal text instead of regex."
5231
+ })
5232
+ ),
5233
+ context: Type4.Optional(
5234
+ Type4.Integer({
5235
+ minimum: 0,
5236
+ description: "Number of surrounding context lines to include."
5237
+ })
5238
+ ),
5239
+ limit: Type4.Optional(
5240
+ Type4.Integer({
5241
+ minimum: 1,
5242
+ description: "Maximum matches to return. Defaults to 100."
5243
+ })
5244
+ )
5245
+ },
5246
+ { additionalProperties: false }
5247
+ ),
5248
+ execute: async () => {
5249
+ throw new Error("grep can only run when sandbox execution is enabled.");
5250
+ }
5251
+ });
5252
+ }
5253
+
5254
+ // src/chat/tools/sandbox/attach-file.ts
5255
+ import path7 from "path";
5256
+ import { Type as Type5 } from "@sinclair/typebox";
4607
5257
  var MAX_ATTACH_FILE_BYTES = 10 * 1024 * 1024;
4608
5258
  var MIME_BY_EXTENSION = {
4609
5259
  ".png": "image/png",
@@ -4624,20 +5274,20 @@ function normalizeSandboxPath(inputPath) {
4624
5274
  if (!trimmed) {
4625
5275
  throw new Error("path is required");
4626
5276
  }
4627
- if (path4.posix.isAbsolute(trimmed)) {
5277
+ if (path7.posix.isAbsolute(trimmed)) {
4628
5278
  return trimmed;
4629
5279
  }
4630
- return path4.posix.join(SANDBOX_WORKSPACE_ROOT, trimmed);
5280
+ return path7.posix.join(SANDBOX_WORKSPACE_ROOT, trimmed);
4631
5281
  }
4632
5282
  function sanitizeFilename(value, fallbackPath) {
4633
5283
  const candidate = (value ?? "").trim();
4634
5284
  if (candidate) {
4635
- const base = path4.posix.basename(candidate);
5285
+ const base = path7.posix.basename(candidate);
4636
5286
  if (base && base !== "." && base !== "..") {
4637
5287
  return base;
4638
5288
  }
4639
5289
  }
4640
- const derived = path4.posix.basename(fallbackPath);
5290
+ const derived = path7.posix.basename(fallbackPath);
4641
5291
  if (derived && derived !== "." && derived !== "..") {
4642
5292
  return derived;
4643
5293
  }
@@ -4648,7 +5298,7 @@ function inferMimeType(filename, explicitMimeType) {
4648
5298
  if (explicit) {
4649
5299
  return explicit;
4650
5300
  }
4651
- const ext = path4.extname(filename).toLowerCase();
5301
+ const ext = path7.extname(filename).toLowerCase();
4652
5302
  return MIME_BY_EXTENSION[ext] ?? "application/octet-stream";
4653
5303
  }
4654
5304
  async function detectMimeType(sandbox, targetPath) {
@@ -4669,20 +5319,20 @@ async function detectMimeType(sandbox, targetPath) {
4669
5319
  function createAttachFileTool(sandbox, hooks = {}) {
4670
5320
  return tool({
4671
5321
  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(
5322
+ inputSchema: Type5.Object(
4673
5323
  {
4674
- path: Type2.String({
5324
+ path: Type5.String({
4675
5325
  minLength: 1,
4676
5326
  description: "Absolute path (for example /tmp/screenshot.png) or workspace-relative path."
4677
5327
  }),
4678
- filename: Type2.Optional(
4679
- Type2.String({
5328
+ filename: Type5.Optional(
5329
+ Type5.String({
4680
5330
  minLength: 1,
4681
5331
  description: "Optional filename override shown in Slack."
4682
5332
  })
4683
5333
  ),
4684
- mimeType: Type2.Optional(
4685
- Type2.String({
5334
+ mimeType: Type5.Optional(
5335
+ Type5.String({
4686
5336
  minLength: 1,
4687
5337
  description: "Optional MIME type override (for example image/png)."
4688
5338
  })
@@ -4695,7 +5345,7 @@ function createAttachFileTool(sandbox, hooks = {}) {
4695
5345
  const fileBuffer = await sandbox.readFileToBuffer({ path: targetPath });
4696
5346
  if (!fileBuffer) {
4697
5347
  const generatedFile = hooks.getGeneratedFile?.(
4698
- path4.posix.basename(targetPath)
5348
+ path7.posix.basename(targetPath)
4699
5349
  );
4700
5350
  if (generatedFile) {
4701
5351
  hooks.onGeneratedFiles?.([generatedFile]);
@@ -4742,8 +5392,95 @@ function createAttachFileTool(sandbox, hooks = {}) {
4742
5392
  });
4743
5393
  }
4744
5394
 
5395
+ // src/chat/tools/sandbox/list-dir.ts
5396
+ import path8 from "path";
5397
+ import { Type as Type6 } from "@sinclair/typebox";
5398
+ var DEFAULT_LIST_LIMIT = 500;
5399
+ async function listDir(params) {
5400
+ const dirPath = resolveWorkspacePath(params.path);
5401
+ const limit = positiveInteger(params.limit) ?? DEFAULT_LIST_LIMIT;
5402
+ const stat = await params.fs.stat(dirPath);
5403
+ if (!stat.isDirectory()) {
5404
+ throw new Error(`Not a directory: ${params.path ?? "."}`);
5405
+ }
5406
+ const entries = (await params.fs.readdir(dirPath)).sort(
5407
+ (a, b) => a.toLowerCase().localeCompare(b.toLowerCase())
5408
+ );
5409
+ const output = [];
5410
+ let entryLimitReached = false;
5411
+ for (const entry of entries) {
5412
+ if (output.length >= limit) {
5413
+ entryLimitReached = true;
5414
+ break;
5415
+ }
5416
+ const entryPath = path8.posix.join(dirPath, entry);
5417
+ try {
5418
+ const entryStat = await params.fs.stat(entryPath);
5419
+ output.push(`${entry}${entryStat.isDirectory() ? "/" : ""}`);
5420
+ } catch {
5421
+ continue;
5422
+ }
5423
+ }
5424
+ const bounded = truncateText(
5425
+ output.length > 0 ? output.join("\n") : "(empty directory)"
5426
+ );
5427
+ const notices = [];
5428
+ if (entryLimitReached) {
5429
+ notices.push(
5430
+ `${limit} entries limit reached. Use a higher limit to continue.`
5431
+ );
5432
+ }
5433
+ if (bounded.truncated) {
5434
+ notices.push(`${MAX_TEXT_CHARS} character output limit reached.`);
5435
+ }
5436
+ return {
5437
+ content: [
5438
+ {
5439
+ type: "text",
5440
+ text: notices.length > 0 ? `${bounded.content}
5441
+
5442
+ [${notices.join(" ")}]` : bounded.content
5443
+ }
5444
+ ],
5445
+ details: {
5446
+ ok: true,
5447
+ path: params.path ?? ".",
5448
+ truncated: entryLimitReached || bounded.truncated,
5449
+ ...entryLimitReached ? { entry_limit_reached: limit } : {}
5450
+ }
5451
+ };
5452
+ }
5453
+ function createListDirTool() {
5454
+ return tool({
5455
+ description: "List a sandbox workspace directory. Returns sorted entries with '/' suffixes for directories and bounded truncation notices.",
5456
+ annotations: { readOnlyHint: true, destructiveHint: false },
5457
+ inputSchema: Type6.Object(
5458
+ {
5459
+ path: Type6.Optional(
5460
+ Type6.String({
5461
+ minLength: 1,
5462
+ description: "Directory path in the sandbox workspace. Defaults to the workspace root."
5463
+ })
5464
+ ),
5465
+ limit: Type6.Optional(
5466
+ Type6.Integer({
5467
+ minimum: 1,
5468
+ description: "Maximum entries to return. Defaults to 500."
5469
+ })
5470
+ )
5471
+ },
5472
+ { additionalProperties: false }
5473
+ ),
5474
+ execute: async () => {
5475
+ throw new Error(
5476
+ "listDir can only run when sandbox execution is enabled."
5477
+ );
5478
+ }
5479
+ });
5480
+ }
5481
+
4745
5482
  // src/chat/tools/web/image-generate.ts
4746
- import { Type as Type3 } from "@sinclair/typebox";
5483
+ import { Type as Type7 } from "@sinclair/typebox";
4747
5484
  var DEFAULT_IMAGE_MODEL = "google/gemini-3-pro-image";
4748
5485
  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
5486
 
@@ -4804,8 +5541,8 @@ function parseImageGenerationError(status, body, model) {
4804
5541
  function createImageGenerateTool(hooks, deps = {}) {
4805
5542
  return tool({
4806
5543
  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({
5544
+ inputSchema: Type7.Object({
5545
+ prompt: Type7.String({
4809
5546
  minLength: 1,
4810
5547
  maxLength: 4e3,
4811
5548
  description: "Image generation prompt."
@@ -4888,7 +5625,7 @@ function createImageGenerateTool(hooks, deps = {}) {
4888
5625
  }
4889
5626
 
4890
5627
  // src/chat/tools/skill/call-mcp-tool.ts
4891
- import { Type as Type4 } from "@sinclair/typebox";
5628
+ import { Type as Type8 } from "@sinclair/typebox";
4892
5629
  function resolveMcpArguments(input) {
4893
5630
  const extraKeys = Object.keys(input).filter(
4894
5631
  (key) => key !== "tool_name" && key !== "arguments"
@@ -4913,14 +5650,14 @@ function resolveMcpArguments(input) {
4913
5650
  function createCallMcpToolTool(mcpToolManager, getActiveSkills) {
4914
5651
  return tool({
4915
5652
  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(
5653
+ inputSchema: Type8.Object(
4917
5654
  {
4918
- tool_name: Type4.String({
5655
+ tool_name: Type8.String({
4919
5656
  minLength: 1,
4920
5657
  description: "Exact MCP tool_name from searchMcpTools."
4921
5658
  }),
4922
- arguments: Type4.Optional(
4923
- Type4.Record(Type4.String(), Type4.Unknown(), {
5659
+ arguments: Type8.Optional(
5660
+ Type8.Record(Type8.String(), Type8.Unknown(), {
4924
5661
  description: 'Arguments matching the disclosed MCP tool schema, for example { "query": "..." } when searchMcpTools shows query is required.'
4925
5662
  })
4926
5663
  )
@@ -4941,7 +5678,7 @@ function createCallMcpToolTool(mcpToolManager, getActiveSkills) {
4941
5678
  }
4942
5679
 
4943
5680
  // src/chat/tools/skill/load-skill.ts
4944
- import { Type as Type5 } from "@sinclair/typebox";
5681
+ import { Type as Type9 } from "@sinclair/typebox";
4945
5682
  function toLoadedSkill(result, availableSkills) {
4946
5683
  if (result.ok !== true || typeof result.skill_name !== "string" || typeof result.description !== "string" || typeof result.skill_dir !== "string" || typeof result.instructions !== "string") {
4947
5684
  return null;
@@ -4988,8 +5725,8 @@ async function loadSkillFromHost(availableSkills, skillName) {
4988
5725
  function createLoadSkillTool(availableSkills, options) {
4989
5726
  return tool({
4990
5727
  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({
5728
+ inputSchema: Type9.Object({
5729
+ skill_name: Type9.String({
4993
5730
  minLength: 1,
4994
5731
  description: "Skill name to load, without the leading slash."
4995
5732
  })
@@ -5009,7 +5746,7 @@ function createLoadSkillTool(availableSkills, options) {
5009
5746
  }
5010
5747
 
5011
5748
  // src/chat/tools/skill/search-mcp-tools.ts
5012
- import { Type as Type6 } from "@sinclair/typebox";
5749
+ import { Type as Type10 } from "@sinclair/typebox";
5013
5750
 
5014
5751
  // src/chat/tools/skill/mcp-tool-summary.ts
5015
5752
  function getSchemaProperties(schema) {
@@ -5199,22 +5936,22 @@ function searchMcpCatalog(tools, query) {
5199
5936
  function createSearchMcpToolsTool(mcpToolManager, getActiveSkills) {
5200
5937
  return tool({
5201
5938
  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(
5939
+ inputSchema: Type10.Object(
5203
5940
  {
5204
- query: Type6.Optional(
5205
- Type6.String({
5941
+ query: Type10.Optional(
5942
+ Type10.String({
5206
5943
  minLength: 1,
5207
5944
  description: "Optional search terms describing the MCP tool or arguments needed."
5208
5945
  })
5209
5946
  ),
5210
- provider: Type6.Optional(
5211
- Type6.String({
5947
+ provider: Type10.Optional(
5948
+ Type10.String({
5212
5949
  minLength: 1,
5213
5950
  description: "Optional provider name to list or search within."
5214
5951
  })
5215
5952
  ),
5216
- max_results: Type6.Optional(
5217
- Type6.Integer({
5953
+ max_results: Type10.Optional(
5954
+ Type10.Integer({
5218
5955
  minimum: 1,
5219
5956
  maximum: MAX_RESULTS,
5220
5957
  description: "Maximum matching tool descriptors to return."
@@ -5245,16 +5982,55 @@ function createSearchMcpToolsTool(mcpToolManager, getActiveSkills) {
5245
5982
  }
5246
5983
 
5247
5984
  // src/chat/tools/sandbox/read-file.ts
5248
- import { Type as Type7 } from "@sinclair/typebox";
5985
+ import { Type as Type11 } from "@sinclair/typebox";
5986
+ var DEFAULT_READ_LIMIT = 1e3;
5987
+ function sliceFileContent(params) {
5988
+ const normalized = normalizeToLf(params.content);
5989
+ const lines = normalized.length === 0 ? [] : normalized.split("\n");
5990
+ const requestedOffset = positiveInteger(params.offset);
5991
+ const requestedLimit = positiveInteger(params.limit);
5992
+ const startLine = requestedOffset ?? 1;
5993
+ const maxLines = requestedLimit ?? DEFAULT_READ_LIMIT;
5994
+ const startIndex = Math.min(lines.length, startLine - 1);
5995
+ const selected = lines.slice(startIndex, startIndex + maxLines);
5996
+ const endLine = selected.length > 0 ? startLine + selected.length - 1 : startLine - 1;
5997
+ const truncated = startIndex > 0 || endLine < lines.length;
5998
+ const rangeRequested = requestedOffset !== void 0 || requestedLimit !== void 0;
5999
+ return {
6000
+ content: !rangeRequested && !truncated ? params.content : selected.join("\n"),
6001
+ end_line: selected.length > 0 ? endLine : void 0,
6002
+ path: params.path,
6003
+ start_line: startLine,
6004
+ success: true,
6005
+ total_lines: lines.length,
6006
+ truncated,
6007
+ ...endLine < lines.length ? {
6008
+ continuation: `Read more with offset=${endLine + 1} and limit=${maxLines}.`
6009
+ } : {}
6010
+ };
6011
+ }
5249
6012
  function createReadFileTool() {
5250
6013
  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(
6014
+ 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.",
6015
+ annotations: { readOnlyHint: true, destructiveHint: false },
6016
+ inputSchema: Type11.Object(
5253
6017
  {
5254
- path: Type7.String({
6018
+ path: Type11.String({
5255
6019
  minLength: 1,
5256
6020
  description: "Path to the file in the sandbox workspace."
5257
- })
6021
+ }),
6022
+ offset: Type11.Optional(
6023
+ Type11.Integer({
6024
+ minimum: 1,
6025
+ description: "1-indexed line number to start reading from."
6026
+ })
6027
+ ),
6028
+ limit: Type11.Optional(
6029
+ Type11.Integer({
6030
+ minimum: 1,
6031
+ description: "Maximum number of lines to read. Defaults to 1000."
6032
+ })
6033
+ )
5258
6034
  },
5259
6035
  { additionalProperties: false }
5260
6036
  ),
@@ -5267,12 +6043,12 @@ function createReadFileTool() {
5267
6043
  }
5268
6044
 
5269
6045
  // src/chat/tools/runtime/report-progress.ts
5270
- import { Type as Type8 } from "@sinclair/typebox";
6046
+ import { Type as Type12 } from "@sinclair/typebox";
5271
6047
  function createReportProgressTool() {
5272
6048
  return tool({
5273
6049
  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({
6050
+ inputSchema: Type12.Object({
6051
+ message: Type12.String({
5276
6052
  minLength: 1,
5277
6053
  description: "Short user-facing progress message."
5278
6054
  })
@@ -5281,7 +6057,7 @@ function createReportProgressTool() {
5281
6057
  }
5282
6058
 
5283
6059
  // src/chat/tools/slack/channel-list-messages.ts
5284
- import { Type as Type9 } from "@sinclair/typebox";
6060
+ import { Type as Type13 } from "@sinclair/typebox";
5285
6061
 
5286
6062
  // src/chat/slack/channel.ts
5287
6063
  async function listChannelMessages(input) {
@@ -5370,39 +6146,40 @@ async function listThreadReplies(input) {
5370
6146
  function createSlackChannelListMessagesTool(context) {
5371
6147
  return tool({
5372
6148
  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({
6149
+ annotations: { readOnlyHint: true, destructiveHint: false },
6150
+ inputSchema: Type13.Object({
6151
+ limit: Type13.Optional(
6152
+ Type13.Integer({
5376
6153
  minimum: 1,
5377
6154
  maximum: 1e3,
5378
6155
  description: "Maximum number of messages to return across pages."
5379
6156
  })
5380
6157
  ),
5381
- cursor: Type9.Optional(
5382
- Type9.String({
6158
+ cursor: Type13.Optional(
6159
+ Type13.String({
5383
6160
  minLength: 1,
5384
6161
  description: "Optional cursor to continue from a prior call."
5385
6162
  })
5386
6163
  ),
5387
- oldest: Type9.Optional(
5388
- Type9.String({
6164
+ oldest: Type13.Optional(
6165
+ Type13.String({
5389
6166
  minLength: 1,
5390
6167
  description: "Optional oldest message timestamp (Slack ts) for range filtering."
5391
6168
  })
5392
6169
  ),
5393
- latest: Type9.Optional(
5394
- Type9.String({
6170
+ latest: Type13.Optional(
6171
+ Type13.String({
5395
6172
  minLength: 1,
5396
6173
  description: "Optional latest message timestamp (Slack ts) for range filtering."
5397
6174
  })
5398
6175
  ),
5399
- inclusive: Type9.Optional(
5400
- Type9.Boolean({
6176
+ inclusive: Type13.Optional(
6177
+ Type13.Boolean({
5401
6178
  description: "Whether oldest/latest bounds should be inclusive."
5402
6179
  })
5403
6180
  ),
5404
- max_pages: Type9.Optional(
5405
- Type9.Integer({
6181
+ max_pages: Type13.Optional(
6182
+ Type13.Integer({
5406
6183
  minimum: 1,
5407
6184
  maximum: 10,
5408
6185
  description: "Maximum number of API pages to traverse in a single call."
@@ -5456,7 +6233,7 @@ function createSlackChannelListMessagesTool(context) {
5456
6233
  }
5457
6234
 
5458
6235
  // src/chat/tools/slack/channel-post-message.ts
5459
- import { Type as Type10 } from "@sinclair/typebox";
6236
+ import { Type as Type14 } from "@sinclair/typebox";
5460
6237
 
5461
6238
  // src/chat/tools/idempotency.ts
5462
6239
  function stableSerialize(value) {
@@ -5478,8 +6255,8 @@ function createOperationKey(toolName, input) {
5478
6255
  function createSlackChannelPostMessageTool(context, state) {
5479
6256
  return tool({
5480
6257
  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({
6258
+ inputSchema: Type14.Object({
6259
+ text: Type14.String({
5483
6260
  minLength: 1,
5484
6261
  maxLength: 4e4,
5485
6262
  description: "Slack mrkdwn text to post."
@@ -5522,12 +6299,12 @@ function createSlackChannelPostMessageTool(context, state) {
5522
6299
  }
5523
6300
 
5524
6301
  // src/chat/tools/slack/message-add-reaction.ts
5525
- import { Type as Type11 } from "@sinclair/typebox";
6302
+ import { Type as Type15 } from "@sinclair/typebox";
5526
6303
  function createSlackMessageAddReactionTool(context, state) {
5527
6304
  return tool({
5528
6305
  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({
6306
+ inputSchema: Type15.Object({
6307
+ emoji: Type15.String({
5531
6308
  minLength: 1,
5532
6309
  maxLength: 64,
5533
6310
  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 +6362,7 @@ function createSlackMessageAddReactionTool(context, state) {
5585
6362
  }
5586
6363
 
5587
6364
  // src/chat/tools/slack/canvas-tools.ts
5588
- import { Type as Type12 } from "@sinclair/typebox";
6365
+ import { Type as Type16 } from "@sinclair/typebox";
5589
6366
 
5590
6367
  // src/chat/tools/slack/canvases.ts
5591
6368
  function normalizeCanvasMarkdown(markdown) {
@@ -5801,13 +6578,13 @@ function mergeRecentCanvases(existing, created) {
5801
6578
  function createSlackCanvasCreateTool(context, state) {
5802
6579
  return tool({
5803
6580
  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({
6581
+ inputSchema: Type16.Object({
6582
+ title: Type16.String({
5806
6583
  minLength: 1,
5807
6584
  maxLength: 160,
5808
6585
  description: "Canvas title."
5809
6586
  }),
5810
- markdown: Type12.String({
6587
+ markdown: Type16.String({
5811
6588
  minLength: 1,
5812
6589
  description: "Canvas markdown body content."
5813
6590
  })
@@ -5873,29 +6650,29 @@ function createSlackCanvasCreateTool(context, state) {
5873
6650
  function createSlackCanvasUpdateTool(state, _context) {
5874
6651
  return tool({
5875
6652
  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({
6653
+ inputSchema: Type16.Object({
6654
+ markdown: Type16.String({
5878
6655
  minLength: 1,
5879
6656
  description: "Markdown content to insert or use as replacement text."
5880
6657
  }),
5881
- operation: Type12.Optional(
5882
- Type12.Union(
6658
+ operation: Type16.Optional(
6659
+ Type16.Union(
5883
6660
  [
5884
- Type12.Literal("insert_at_end"),
5885
- Type12.Literal("insert_at_start"),
5886
- Type12.Literal("replace")
6661
+ Type16.Literal("insert_at_end"),
6662
+ Type16.Literal("insert_at_start"),
6663
+ Type16.Literal("replace")
5887
6664
  ],
5888
6665
  { description: "Canvas update mode." }
5889
6666
  )
5890
6667
  ),
5891
- section_id: Type12.Optional(
5892
- Type12.String({
6668
+ section_id: Type16.Optional(
6669
+ Type16.String({
5893
6670
  minLength: 1,
5894
6671
  description: "Optional section ID required for targeted replace operations."
5895
6672
  })
5896
6673
  ),
5897
- section_contains_text: Type12.Optional(
5898
- Type12.String({
6674
+ section_contains_text: Type16.Optional(
6675
+ Type16.String({
5899
6676
  minLength: 1,
5900
6677
  description: "Optional helper text used to find the target section when section_id is not provided."
5901
6678
  })
@@ -5961,8 +6738,9 @@ function createSlackCanvasUpdateTool(state, _context) {
5961
6738
  function createSlackCanvasReadTool() {
5962
6739
  return tool({
5963
6740
  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({
6741
+ annotations: { readOnlyHint: true, destructiveHint: false },
6742
+ inputSchema: Type16.Object({
6743
+ canvas: Type16.String({
5966
6744
  minLength: 1,
5967
6745
  description: "Canvas/file ID (e.g. `F0ABCDEF`) or Slack canvas/docs URL (e.g. `https://team.slack.com/docs/T.../F...`)."
5968
6746
  })
@@ -6012,7 +6790,7 @@ function createSlackCanvasReadTool() {
6012
6790
  }
6013
6791
 
6014
6792
  // src/chat/tools/slack/list-tools.ts
6015
- import { Type as Type13 } from "@sinclair/typebox";
6793
+ import { Type as Type17 } from "@sinclair/typebox";
6016
6794
 
6017
6795
  // src/chat/tools/slack/lists.ts
6018
6796
  function normalizeKey(value) {
@@ -6186,8 +6964,8 @@ async function updateListItem(input) {
6186
6964
  function createSlackListCreateTool(state) {
6187
6965
  return tool({
6188
6966
  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({
6967
+ inputSchema: Type17.Object({
6968
+ name: Type17.String({
6191
6969
  minLength: 1,
6192
6970
  maxLength: 160,
6193
6971
  description: "Name for the new Slack list."
@@ -6222,20 +7000,20 @@ function createSlackListCreateTool(state) {
6222
7000
  function createSlackListAddItemsTool(state) {
6223
7001
  return tool({
6224
7002
  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 }), {
7003
+ inputSchema: Type17.Object({
7004
+ items: Type17.Array(Type17.String({ minLength: 1 }), {
6227
7005
  minItems: 1,
6228
7006
  maxItems: 25,
6229
7007
  description: "List item titles to create."
6230
7008
  }),
6231
- assignee_user_id: Type13.Optional(
6232
- Type13.String({
7009
+ assignee_user_id: Type17.Optional(
7010
+ Type17.String({
6233
7011
  minLength: 1,
6234
7012
  description: "Optional Slack user ID assigned to all created items."
6235
7013
  })
6236
7014
  ),
6237
- due_date: Type13.Optional(
6238
- Type13.String({
7015
+ due_date: Type17.Optional(
7016
+ Type17.String({
6239
7017
  pattern: "^\\d{4}-\\d{2}-\\d{2}$",
6240
7018
  description: "Optional due date in YYYY-MM-DD format."
6241
7019
  })
@@ -6284,9 +7062,10 @@ function createSlackListAddItemsTool(state) {
6284
7062
  function createSlackListGetItemsTool(state) {
6285
7063
  return tool({
6286
7064
  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({
7065
+ annotations: { readOnlyHint: true, destructiveHint: false },
7066
+ inputSchema: Type17.Object({
7067
+ limit: Type17.Optional(
7068
+ Type17.Integer({
6290
7069
  minimum: 1,
6291
7070
  maximum: 200,
6292
7071
  description: "Maximum number of list items to return."
@@ -6311,19 +7090,19 @@ function createSlackListGetItemsTool(state) {
6311
7090
  function createSlackListUpdateItemTool(state) {
6312
7091
  return tool({
6313
7092
  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(
7093
+ inputSchema: Type17.Object(
6315
7094
  {
6316
- item_id: Type13.String({
7095
+ item_id: Type17.String({
6317
7096
  minLength: 1,
6318
7097
  description: "ID of the Slack list item to update."
6319
7098
  }),
6320
- completed: Type13.Optional(
6321
- Type13.Boolean({
7099
+ completed: Type17.Optional(
7100
+ Type17.Boolean({
6322
7101
  description: "Optional completion status update."
6323
7102
  })
6324
7103
  ),
6325
- title: Type13.Optional(
6326
- Type13.String({
7104
+ title: Type17.Optional(
7105
+ Type17.String({
6327
7106
  minLength: 1,
6328
7107
  description: "Optional new item title."
6329
7108
  })
@@ -6373,11 +7152,12 @@ function createSlackListUpdateItemTool(state) {
6373
7152
  }
6374
7153
 
6375
7154
  // src/chat/tools/system-time.ts
6376
- import { Type as Type14 } from "@sinclair/typebox";
7155
+ import { Type as Type18 } from "@sinclair/typebox";
6377
7156
  function createSystemTimeTool() {
6378
7157
  return tool({
6379
7158
  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({}),
7159
+ annotations: { readOnlyHint: true, destructiveHint: false },
7160
+ inputSchema: Type18.Object({}),
6381
7161
  execute: async () => {
6382
7162
  const now = /* @__PURE__ */ new Date();
6383
7163
  return {
@@ -6395,7 +7175,7 @@ function createSystemTimeTool() {
6395
7175
  import {
6396
7176
  Agent
6397
7177
  } from "@mariozechner/pi-agent-core";
6398
- import { Type as Type15 } from "@sinclair/typebox";
7178
+ import { Type as Type19 } from "@sinclair/typebox";
6399
7179
 
6400
7180
  // src/chat/respond-helpers.ts
6401
7181
  var MAX_INLINE_ATTACHMENT_BASE64_CHARS = 12e4;
@@ -6533,12 +7313,6 @@ function encodeNonImageAttachmentForPrompt(attachment) {
6533
7313
  "</attachment>"
6534
7314
  ].join("\n");
6535
7315
  }
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
7316
  function isToolResultMessage(value) {
6543
7317
  return typeof value === "object" && value !== null && value.role === "toolResult";
6544
7318
  }
@@ -6635,23 +7409,13 @@ function createStateAdvisorSessionStore() {
6635
7409
  }
6636
7410
 
6637
7411
  // 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.";
7412
+ 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
7413
  var ADVISOR_SYSTEM_PROMPT = [
6651
7414
  "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.",
7415
+ "Analyze the executor-supplied context deeply. Use read-only tools when direct inspection or verification would materially improve the advice.",
6653
7416
  "Distinguish evidence from inference. Treat the advisor task as the focus for this call and the executor context as the starting evidence packet.",
6654
7417
  "Do not assume access to parent transcript or tool output that was not included or gathered in this advisor call.",
7418
+ "Use only the read-only tools provided to you.",
6655
7419
  "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
7420
  "Identify the hard part, recommend a concrete plan or correction, call out blocking risks, and propose focused verification.",
6657
7421
  "If the supplied context is insufficient, say exactly what additional evidence the executor needs to gather before acting.",
@@ -6684,20 +7448,27 @@ function success(memo) {
6684
7448
  }
6685
7449
  };
6686
7450
  }
6687
- function isAdvisorToolAllowed(toolName) {
6688
- return ADVISOR_ALLOWED_TOOL_NAMES.has(toolName);
7451
+ function hasReadOnlyToolAnnotations(annotations) {
7452
+ return annotations?.readOnlyHint === true && annotations.destructiveHint !== true;
7453
+ }
7454
+ function createAdvisorToolDefinitions(definitions) {
7455
+ return Object.fromEntries(
7456
+ Object.entries(definitions).filter(
7457
+ ([name, definition]) => name !== "callMcpTool" && name !== "searchMcpTools" && hasReadOnlyToolAnnotations(definition.annotations)
7458
+ )
7459
+ );
6689
7460
  }
6690
7461
  function createAdvisorTool(context) {
6691
7462
  const store = context.store ?? createStateAdvisorSessionStore();
6692
7463
  const spanContext = context.logContext ?? {};
6693
7464
  return tool({
6694
7465
  description: ADVISOR_TOOL_DESCRIPTION,
6695
- inputSchema: Type15.Object({
6696
- question: Type15.String({
7466
+ inputSchema: Type19.Object({
7467
+ question: Type19.String({
6697
7468
  minLength: 1,
6698
7469
  description: "Focused advisor question or decision point."
6699
7470
  }),
6700
- context: Type15.String({
7471
+ context: Type19.String({
6701
7472
  minLength: 1,
6702
7473
  description: "Curated evidence packet: relevant requirements, constraints, current plan, alternatives, code snippets, diffs, command output, and open questions."
6703
7474
  })
@@ -6809,7 +7580,7 @@ function createAdvisorTool(context) {
6809
7580
  }
6810
7581
 
6811
7582
  // src/chat/tools/web/fetch-tool.ts
6812
- import { Type as Type16 } from "@sinclair/typebox";
7583
+ import { Type as Type20 } from "@sinclair/typebox";
6813
7584
 
6814
7585
  // src/chat/tools/web/constants.ts
6815
7586
  var USER_AGENT = "junior-bot/0.1";
@@ -7011,13 +7782,16 @@ async function assertPublicUrl(rawUrl) {
7011
7782
  }
7012
7783
  return parsed;
7013
7784
  }
7014
- async function withTimeout(task, timeoutMs, label) {
7785
+ async function withTimeout(task, timeoutMs, label, options) {
7015
7786
  let timer;
7016
7787
  const timeoutPromise = new Promise((_, reject) => {
7017
- timer = setTimeout(
7018
- () => reject(new Error(`${label} timed out`)),
7019
- timeoutMs
7020
- );
7788
+ timer = setTimeout(() => {
7789
+ reject(new Error(`${label} timed out`));
7790
+ try {
7791
+ options?.onTimeout?.();
7792
+ } catch {
7793
+ }
7794
+ }, timeoutMs);
7021
7795
  });
7022
7796
  try {
7023
7797
  return await Promise.race([task, timeoutPromise]);
@@ -7154,13 +7928,18 @@ function extractHttpStatusFromMessage(message) {
7154
7928
  function createWebFetchTool(hooks) {
7155
7929
  return tool({
7156
7930
  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({
7931
+ annotations: {
7932
+ readOnlyHint: true,
7933
+ destructiveHint: false,
7934
+ openWorldHint: true
7935
+ },
7936
+ inputSchema: Type20.Object({
7937
+ url: Type20.String({
7159
7938
  minLength: 1,
7160
7939
  description: "HTTP(S) URL to fetch."
7161
7940
  }),
7162
- max_chars: Type16.Optional(
7163
- Type16.Integer({
7941
+ max_chars: Type20.Optional(
7942
+ Type20.Integer({
7164
7943
  minimum: 500,
7165
7944
  maximum: MAX_FETCH_CHARS,
7166
7945
  description: "Optional maximum number of extracted characters to return."
@@ -7220,7 +7999,7 @@ function createWebFetchTool(hooks) {
7220
7999
  // src/chat/tools/web/search.ts
7221
8000
  import { generateText } from "ai";
7222
8001
  import { createGatewayProvider } from "@ai-sdk/gateway";
7223
- import { Type as Type17 } from "@sinclair/typebox";
8002
+ import { Type as Type21 } from "@sinclair/typebox";
7224
8003
  var SEARCH_TIMEOUT_MS = 6e4;
7225
8004
  var MAX_RESULTS2 = 5;
7226
8005
  var DEFAULT_SEARCH_MODEL = "xai/grok-4-fast-reasoning";
@@ -7263,14 +8042,19 @@ function isAuthFailure(message) {
7263
8042
  function createWebSearchTool() {
7264
8043
  return tool({
7265
8044
  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({
8045
+ annotations: {
8046
+ readOnlyHint: true,
8047
+ destructiveHint: false,
8048
+ openWorldHint: true
8049
+ },
8050
+ inputSchema: Type21.Object({
8051
+ query: Type21.String({
7268
8052
  minLength: 1,
7269
8053
  maxLength: 500,
7270
8054
  description: "Search query."
7271
8055
  }),
7272
- max_results: Type17.Optional(
7273
- Type17.Integer({
8056
+ max_results: Type21.Optional(
8057
+ Type21.Integer({
7274
8058
  minimum: 1,
7275
8059
  maximum: MAX_RESULTS2,
7276
8060
  description: "Max results to return."
@@ -7280,6 +8064,7 @@ function createWebSearchTool() {
7280
8064
  execute: async ({ query, max_results }) => {
7281
8065
  const maxResults = max_results ?? 3;
7282
8066
  const model = process.env.AI_WEB_SEARCH_MODEL ?? DEFAULT_SEARCH_MODEL;
8067
+ const controller = new AbortController();
7283
8068
  try {
7284
8069
  const provider = createGatewayProvider();
7285
8070
  const response = await withTimeout(
@@ -7292,10 +8077,12 @@ function createWebSearchTool() {
7292
8077
  maxResults
7293
8078
  })
7294
8079
  },
7295
- toolChoice: { type: "tool", toolName: SEARCH_TOOL_NAME }
8080
+ toolChoice: { type: "tool", toolName: SEARCH_TOOL_NAME },
8081
+ abortSignal: controller.signal
7296
8082
  }),
7297
8083
  SEARCH_TIMEOUT_MS,
7298
- "webSearch"
8084
+ "webSearch",
8085
+ { onTimeout: () => controller.abort() }
7299
8086
  );
7300
8087
  const results = parseSearchResults(response.toolResults, maxResults);
7301
8088
  return {
@@ -7336,17 +8123,20 @@ function createWebSearchTool() {
7336
8123
  }
7337
8124
 
7338
8125
  // src/chat/tools/sandbox/write-file.ts
7339
- import { Type as Type18 } from "@sinclair/typebox";
8126
+ import { Type as Type22 } from "@sinclair/typebox";
7340
8127
  function createWriteFileTool() {
7341
8128
  return tool({
7342
8129
  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(
8130
+ promptSnippet: "new file or deliberate full-file replacement",
8131
+ promptGuidelines: ["targeted existing-file changes: editFile"],
8132
+ executionMode: "sequential",
8133
+ inputSchema: Type22.Object(
7344
8134
  {
7345
- path: Type18.String({
8135
+ path: Type22.String({
7346
8136
  minLength: 1,
7347
8137
  description: "Path to write in the sandbox workspace."
7348
8138
  }),
7349
- content: Type18.String({
8139
+ content: Type22.String({
7350
8140
  description: "UTF-8 file content to write."
7351
8141
  })
7352
8142
  },
@@ -7406,6 +8196,10 @@ function createTools(availableSkills, hooks = {}, context) {
7406
8196
  bash: createBashTool(),
7407
8197
  attachFile: createAttachFileTool(context.sandbox, hooks),
7408
8198
  readFile: createReadFileTool(),
8199
+ editFile: createEditFileTool(),
8200
+ grep: createGrepTool(),
8201
+ findFiles: createFindFilesTool(),
8202
+ listDir: createListDirTool(),
7409
8203
  writeFile: createWriteFileTool(),
7410
8204
  webSearch: createWebSearchTool(),
7411
8205
  webFetch: createWebFetchTool(hooks),
@@ -7672,7 +8466,7 @@ import { createBashTool as createBashTool2 } from "bash-tool";
7672
8466
 
7673
8467
  // src/chat/sandbox/skill-sync.ts
7674
8468
  import fs3 from "fs/promises";
7675
- import path5 from "path";
8469
+ import path9 from "path";
7676
8470
 
7677
8471
  // src/chat/sandbox/eval-gh-stub.ts
7678
8472
  function buildEvalGitHubCliStub() {
@@ -8041,7 +8835,7 @@ fallbackToRealSentry();
8041
8835
 
8042
8836
  // src/chat/sandbox/skill-sync.ts
8043
8837
  function toPosixRelative(base, absolute) {
8044
- return path5.relative(base, absolute).split(path5.sep).join("/");
8838
+ return path9.relative(base, absolute).split(path9.sep).join("/");
8045
8839
  }
8046
8840
  async function listFilesRecursive(root) {
8047
8841
  const queue = [root];
@@ -8051,7 +8845,7 @@ async function listFilesRecursive(root) {
8051
8845
  const entries = await fs3.readdir(dir, { withFileTypes: true });
8052
8846
  entries.sort((a, b) => a.name.localeCompare(b.name));
8053
8847
  for (const entry of entries) {
8054
- const absolute = path5.join(dir, entry.name);
8848
+ const absolute = path9.join(dir, entry.name);
8055
8849
  if (entry.isDirectory()) {
8056
8850
  queue.push(absolute);
8057
8851
  } else if (entry.isFile()) {
@@ -8090,7 +8884,7 @@ async function buildSkillSyncFiles(availableSkills, runtimeBinDir, referenceFile
8090
8884
  });
8091
8885
  if (referenceFiles && referenceFiles.length > 0) {
8092
8886
  for (const absoluteFile of referenceFiles) {
8093
- const fileName = path5.basename(absoluteFile);
8887
+ const fileName = path9.basename(absoluteFile);
8094
8888
  filesToWrite.push({
8095
8889
  path: `${SANDBOX_DATA_ROOT}/${fileName}`,
8096
8890
  content: await fs3.readFile(absoluteFile)
@@ -8118,7 +8912,7 @@ async function buildSkillSyncFiles(availableSkills, runtimeBinDir, referenceFile
8118
8912
  function collectDirectories(filesToWrite, workspaceRoot) {
8119
8913
  const directoriesToEnsure = /* @__PURE__ */ new Set();
8120
8914
  for (const file of filesToWrite) {
8121
- const normalizedPath = path5.posix.normalize(file.path);
8915
+ const normalizedPath = path9.posix.normalize(file.path);
8122
8916
  const parts = normalizedPath.split("/").filter(Boolean);
8123
8917
  let current = "";
8124
8918
  for (let index = 0; index < parts.length - 1; index += 1) {
@@ -8131,19 +8925,19 @@ function collectDirectories(filesToWrite, workspaceRoot) {
8131
8925
  ).sort((a, b) => a.length - b.length);
8132
8926
  }
8133
8927
  function resolveHostSkillPath(availableSkills, sandboxPath) {
8134
- const normalizedPath = path5.posix.normalize(sandboxPath.trim());
8928
+ const normalizedPath = path9.posix.normalize(sandboxPath.trim());
8135
8929
  for (const skill of availableSkills) {
8136
8930
  const virtualRoot = sandboxSkillDir(skill.name);
8137
8931
  if (normalizedPath !== virtualRoot && !normalizedPath.startsWith(`${virtualRoot}/`)) {
8138
8932
  continue;
8139
8933
  }
8140
- const relativePath = path5.posix.relative(virtualRoot, normalizedPath);
8934
+ const relativePath = path9.posix.relative(virtualRoot, normalizedPath);
8141
8935
  if (!relativePath || relativePath.startsWith("../")) {
8142
8936
  return null;
8143
8937
  }
8144
- const hostRoot = path5.resolve(skill.skillPath);
8145
- const hostPath = path5.resolve(hostRoot, ...relativePath.split("/"));
8146
- if (hostPath !== hostRoot && !hostPath.startsWith(`${hostRoot}${path5.sep}`)) {
8938
+ const hostRoot = path9.resolve(skill.skillPath);
8939
+ const hostPath = path9.resolve(hostRoot, ...relativePath.split("/"));
8940
+ if (hostPath !== hostRoot && !hostPath.startsWith(`${hostRoot}${path9.sep}`)) {
8147
8941
  return null;
8148
8942
  }
8149
8943
  return hostPath;
@@ -8151,16 +8945,16 @@ function resolveHostSkillPath(availableSkills, sandboxPath) {
8151
8945
  return null;
8152
8946
  }
8153
8947
  function resolveHostDataPath(referenceFiles, sandboxPath) {
8154
- const normalizedPath = path5.posix.normalize(sandboxPath.trim());
8948
+ const normalizedPath = path9.posix.normalize(sandboxPath.trim());
8155
8949
  if (normalizedPath !== SANDBOX_DATA_ROOT && !normalizedPath.startsWith(`${SANDBOX_DATA_ROOT}/`)) {
8156
8950
  return null;
8157
8951
  }
8158
- const relativePath = path5.posix.relative(SANDBOX_DATA_ROOT, normalizedPath);
8952
+ const relativePath = path9.posix.relative(SANDBOX_DATA_ROOT, normalizedPath);
8159
8953
  if (!relativePath || relativePath.startsWith("../") || relativePath.includes("/")) {
8160
8954
  return null;
8161
8955
  }
8162
8956
  for (const hostFile of referenceFiles) {
8163
- if (path5.basename(hostFile) === relativePath) {
8957
+ if (path9.basename(hostFile) === relativePath) {
8164
8958
  return hostFile;
8165
8959
  }
8166
8960
  }
@@ -8658,16 +9452,41 @@ function createSandboxSessionManager(options) {
8658
9452
  env: input.env,
8659
9453
  pathPrefix: `${SANDBOX_RUNTIME_BIN_DIR}:$PATH`
8660
9454
  });
9455
+ const controller = input.timeoutMs && input.timeoutMs > 0 ? new AbortController() : void 0;
9456
+ let timedOut = false;
9457
+ const timeoutId = controller ? setTimeout(() => {
9458
+ timedOut = true;
9459
+ controller.abort();
9460
+ }, input.timeoutMs) : void 0;
8661
9461
  return await withTemporaryHeaderTransforms(
8662
9462
  sandboxInstance,
8663
9463
  input.headerTransforms,
8664
9464
  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);
9465
+ try {
9466
+ const commandResult2 = await sandboxInstance.runCommand({
9467
+ cmd: "bash",
9468
+ args: ["-c", script],
9469
+ cwd: SANDBOX_WORKSPACE_ROOT,
9470
+ ...controller ? { signal: controller.signal } : {}
9471
+ });
9472
+ return await readCommandOutput(commandResult2);
9473
+ } catch (error) {
9474
+ if (timedOut) {
9475
+ return {
9476
+ stdout: "",
9477
+ stderr: `Command timed out after ${input.timeoutMs}ms`,
9478
+ exitCode: 124,
9479
+ stdoutTruncated: false,
9480
+ stderrTruncated: false,
9481
+ timedOut: true
9482
+ };
9483
+ }
9484
+ throw error;
9485
+ } finally {
9486
+ if (timeoutId) {
9487
+ clearTimeout(timeoutId);
9488
+ }
9489
+ }
8671
9490
  }
8672
9491
  );
8673
9492
  },
@@ -8678,7 +9497,8 @@ function createSandboxSessionManager(options) {
8678
9497
  writeFile: async (input) => await executeWriteFile(input, {
8679
9498
  toolCallId: "sandbox-write-file",
8680
9499
  messages: []
8681
- })
9500
+ }),
9501
+ fs: sandboxInstance.fs
8682
9502
  };
8683
9503
  };
8684
9504
  const ensureReadySandbox = async () => {
@@ -8734,7 +9554,15 @@ function createSandboxSessionManager(options) {
8734
9554
  }
8735
9555
 
8736
9556
  // src/chat/sandbox/sandbox.ts
8737
- var SANDBOX_TOOL_NAMES = /* @__PURE__ */ new Set(["bash", "readFile", "writeFile"]);
9557
+ var SANDBOX_TOOL_NAMES = /* @__PURE__ */ new Set([
9558
+ "bash",
9559
+ "readFile",
9560
+ "editFile",
9561
+ "grep",
9562
+ "findFiles",
9563
+ "listDir",
9564
+ "writeFile"
9565
+ ]);
8738
9566
  function parseHeaderTransforms(raw) {
8739
9567
  if (!Array.isArray(raw)) {
8740
9568
  return void 0;
@@ -8798,6 +9626,7 @@ function createSandboxExecutor(options) {
8798
9626
  const executeBashTool = async (rawInput, command) => {
8799
9627
  const headerTransforms = parseHeaderTransforms(rawInput.headerTransforms);
8800
9628
  const env = parseEnv(rawInput.env);
9629
+ const timeoutMs = positiveInteger(rawInput.timeoutMs);
8801
9630
  logSandboxBootRequest("tool.bash", {
8802
9631
  "app.sandbox.command_length": command.length
8803
9632
  });
@@ -8813,7 +9642,8 @@ function createSandboxExecutor(options) {
8813
9642
  const response = await executeBash({
8814
9643
  command,
8815
9644
  ...headerTransforms ? { headerTransforms } : {},
8816
- ...env ? { env } : {}
9645
+ ...env ? { env } : {},
9646
+ ...timeoutMs ? { timeoutMs } : {}
8817
9647
  });
8818
9648
  setSpanAttributes({
8819
9649
  "process.exit.code": response.exitCode,
@@ -8845,7 +9675,7 @@ function createSandboxExecutor(options) {
8845
9675
  cwd: SANDBOX_WORKSPACE_ROOT,
8846
9676
  exit_code: result.exitCode,
8847
9677
  signal: null,
8848
- timed_out: false,
9678
+ timed_out: Boolean(result.timedOut),
8849
9679
  stdout: result.stdout,
8850
9680
  stderr: result.stderr,
8851
9681
  stdout_truncated: result.stdoutTruncated,
@@ -8858,6 +9688,8 @@ function createSandboxExecutor(options) {
8858
9688
  if (!filePath) {
8859
9689
  throw new Error("path is required");
8860
9690
  }
9691
+ const offset = positiveInteger(rawInput.offset);
9692
+ const limit = positiveInteger(rawInput.limit);
8861
9693
  if (!sessionManager.getSandboxId()) {
8862
9694
  const hostPath = resolveHostSkillPath(availableSkills, filePath) ?? resolveHostDataPath(referenceFiles, filePath);
8863
9695
  if (hostPath) {
@@ -8871,11 +9703,12 @@ function createSandboxExecutor(options) {
8871
9703
  });
8872
9704
  setSpanStatus("ok");
8873
9705
  return {
8874
- result: {
9706
+ result: sliceFileContent({
8875
9707
  content,
8876
9708
  path: filePath,
8877
- success: true
8878
- }
9709
+ offset,
9710
+ limit
9711
+ })
8879
9712
  };
8880
9713
  } catch (error) {
8881
9714
  if (!isHostFileMissingError(error)) {
@@ -8903,9 +9736,12 @@ function createSandboxExecutor(options) {
8903
9736
  });
8904
9737
  setSpanStatus("ok");
8905
9738
  return {
8906
- content,
8907
- path: filePath,
8908
- success: true
9739
+ ...sliceFileContent({
9740
+ content,
9741
+ path: filePath,
9742
+ offset,
9743
+ limit
9744
+ })
8909
9745
  };
8910
9746
  }
8911
9747
  );
@@ -8945,6 +9781,116 @@ function createSandboxExecutor(options) {
8945
9781
  }
8946
9782
  };
8947
9783
  };
9784
+ const executeEditFileTool = async (rawInput) => {
9785
+ const filePath = String(rawInput.path ?? "").trim();
9786
+ if (!filePath) {
9787
+ throw new Error("path is required");
9788
+ }
9789
+ if (!Array.isArray(rawInput.edits)) {
9790
+ throw new Error("edits is required");
9791
+ }
9792
+ logSandboxBootRequest("tool.editFile", {
9793
+ "file.path": filePath
9794
+ });
9795
+ const executors = await sessionManager.ensureToolExecutors();
9796
+ const result = await withSandboxSpan(
9797
+ "sandbox.editFile",
9798
+ "sandbox.fs.edit",
9799
+ {
9800
+ "app.sandbox.path.length": filePath.length,
9801
+ "app.sandbox.edit.count": rawInput.edits.length
9802
+ },
9803
+ async () => {
9804
+ const response = await editFile({
9805
+ fs: executors.fs,
9806
+ path: filePath,
9807
+ edits: rawInput.edits
9808
+ });
9809
+ setSpanStatus("ok");
9810
+ return response;
9811
+ }
9812
+ );
9813
+ return { result };
9814
+ };
9815
+ const executeGrepTool = async (rawInput) => {
9816
+ const pattern = String(rawInput.pattern ?? "");
9817
+ if (!pattern) {
9818
+ throw new Error("pattern is required");
9819
+ }
9820
+ logSandboxBootRequest("tool.grep");
9821
+ const contextLines = positiveInteger(rawInput.context);
9822
+ const limit = positiveInteger(rawInput.limit);
9823
+ const executors = await sessionManager.ensureToolExecutors();
9824
+ const result = await withSandboxSpan(
9825
+ "sandbox.grep",
9826
+ "sandbox.fs.search",
9827
+ {
9828
+ "app.sandbox.pattern.length": pattern.length
9829
+ },
9830
+ async () => {
9831
+ const response = await grepFiles({
9832
+ fs: executors.fs,
9833
+ pattern,
9834
+ ...typeof rawInput.path === "string" ? { path: rawInput.path } : {},
9835
+ ...typeof rawInput.glob === "string" ? { glob: rawInput.glob } : {},
9836
+ ...typeof rawInput.ignoreCase === "boolean" ? { ignoreCase: rawInput.ignoreCase } : {},
9837
+ ...typeof rawInput.literal === "boolean" ? { literal: rawInput.literal } : {},
9838
+ ...contextLines ? { context: contextLines } : {},
9839
+ ...limit ? { limit } : {}
9840
+ });
9841
+ setSpanStatus("ok");
9842
+ return response;
9843
+ }
9844
+ );
9845
+ return { result };
9846
+ };
9847
+ const executeFindFilesTool = async (rawInput) => {
9848
+ const pattern = String(rawInput.pattern ?? "");
9849
+ if (!pattern) {
9850
+ throw new Error("pattern is required");
9851
+ }
9852
+ logSandboxBootRequest("tool.findFiles");
9853
+ const limit = positiveInteger(rawInput.limit);
9854
+ const executors = await sessionManager.ensureToolExecutors();
9855
+ const result = await withSandboxSpan(
9856
+ "sandbox.findFiles",
9857
+ "sandbox.fs.find",
9858
+ {
9859
+ "app.sandbox.pattern.length": pattern.length
9860
+ },
9861
+ async () => {
9862
+ const response = await findFiles({
9863
+ fs: executors.fs,
9864
+ pattern,
9865
+ ...typeof rawInput.path === "string" ? { path: rawInput.path } : {},
9866
+ ...limit ? { limit } : {}
9867
+ });
9868
+ setSpanStatus("ok");
9869
+ return response;
9870
+ }
9871
+ );
9872
+ return { result };
9873
+ };
9874
+ const executeListDirTool = async (rawInput) => {
9875
+ logSandboxBootRequest("tool.listDir");
9876
+ const limit = positiveInteger(rawInput.limit);
9877
+ const executors = await sessionManager.ensureToolExecutors();
9878
+ const result = await withSandboxSpan(
9879
+ "sandbox.listDir",
9880
+ "sandbox.fs.list",
9881
+ {},
9882
+ async () => {
9883
+ const response = await listDir({
9884
+ fs: executors.fs,
9885
+ ...typeof rawInput.path === "string" ? { path: rawInput.path } : {},
9886
+ ...limit ? { limit } : {}
9887
+ });
9888
+ setSpanStatus("ok");
9889
+ return response;
9890
+ }
9891
+ );
9892
+ return { result };
9893
+ };
8948
9894
  const execute = async (params) => {
8949
9895
  const rawInput = params.input ?? {};
8950
9896
  const bashCommand = params.toolName === "bash" ? String(rawInput.command ?? "").trim() : void 0;
@@ -8963,6 +9909,18 @@ function createSandboxExecutor(options) {
8963
9909
  if (params.toolName === "readFile") {
8964
9910
  return await executeReadFileTool(rawInput);
8965
9911
  }
9912
+ if (params.toolName === "editFile") {
9913
+ return await executeEditFileTool(rawInput);
9914
+ }
9915
+ if (params.toolName === "grep") {
9916
+ return await executeGrepTool(rawInput);
9917
+ }
9918
+ if (params.toolName === "findFiles") {
9919
+ return await executeFindFilesTool(rawInput);
9920
+ }
9921
+ if (params.toolName === "listDir") {
9922
+ return await executeListDirTool(rawInput);
9923
+ }
8966
9924
  if (params.toolName === "writeFile") {
8967
9925
  return await executeWriteFileTool(rawInput);
8968
9926
  }
@@ -9035,11 +9993,49 @@ function buildReportedProgressStatus(input) {
9035
9993
 
9036
9994
  // src/chat/tools/execution/build-sandbox-input.ts
9037
9995
  function buildSandboxInput(toolName, params) {
9996
+ const optionalNumber = (value) => typeof value === "number" && Number.isFinite(value) ? value : void 0;
9038
9997
  if (toolName === "bash") {
9039
- return { command: String(params.command ?? "") };
9998
+ return {
9999
+ command: String(params.command ?? ""),
10000
+ ...optionalNumber(params.timeoutMs) ? { timeoutMs: optionalNumber(params.timeoutMs) } : {}
10001
+ };
9040
10002
  }
9041
10003
  if (toolName === "readFile") {
9042
- return { path: String(params.path ?? "") };
10004
+ return {
10005
+ path: String(params.path ?? ""),
10006
+ ...optionalNumber(params.offset) ? { offset: optionalNumber(params.offset) } : {},
10007
+ ...optionalNumber(params.limit) ? { limit: optionalNumber(params.limit) } : {}
10008
+ };
10009
+ }
10010
+ if (toolName === "editFile") {
10011
+ return {
10012
+ path: String(params.path ?? ""),
10013
+ edits: Array.isArray(params.edits) ? params.edits : []
10014
+ };
10015
+ }
10016
+ if (toolName === "grep") {
10017
+ return {
10018
+ pattern: String(params.pattern ?? ""),
10019
+ ...typeof params.path === "string" ? { path: params.path } : {},
10020
+ ...typeof params.glob === "string" ? { glob: params.glob } : {},
10021
+ ...typeof params.ignoreCase === "boolean" ? { ignoreCase: params.ignoreCase } : {},
10022
+ ...typeof params.literal === "boolean" ? { literal: params.literal } : {},
10023
+ ...optionalNumber(params.context) ? { context: optionalNumber(params.context) } : {},
10024
+ ...optionalNumber(params.limit) ? { limit: optionalNumber(params.limit) } : {}
10025
+ };
10026
+ }
10027
+ if (toolName === "findFiles") {
10028
+ return {
10029
+ pattern: String(params.pattern ?? ""),
10030
+ ...typeof params.path === "string" ? { path: params.path } : {},
10031
+ ...optionalNumber(params.limit) ? { limit: optionalNumber(params.limit) } : {}
10032
+ };
10033
+ }
10034
+ if (toolName === "listDir") {
10035
+ return {
10036
+ ...typeof params.path === "string" ? { path: params.path } : {},
10037
+ ...optionalNumber(params.limit) ? { limit: optionalNumber(params.limit) } : {}
10038
+ };
9043
10039
  }
9044
10040
  if (toolName === "writeFile") {
9045
10041
  return {
@@ -9174,6 +10170,8 @@ function createAgentTools(tools, sandbox, spanContext, onStatus, sandboxExecutor
9174
10170
  label: toolName,
9175
10171
  description: toolDef.description,
9176
10172
  parameters: toolDef.inputSchema,
10173
+ prepareArguments: toolDef.prepareArguments,
10174
+ executionMode: toolDef.executionMode,
9177
10175
  execute: async (toolCallId, params) => {
9178
10176
  const normalizedToolCallId = typeof toolCallId === "string" && toolCallId.length > 0 ? toolCallId : void 0;
9179
10177
  const toolArgumentsAttribute = serializeGenAiAttribute(params);
@@ -9431,18 +10429,17 @@ function buildTurnResult(input) {
9431
10429
  const errorMessage = typeof lastAssistant?.errorMessage === "string" ? lastAssistant.errorMessage : void 0;
9432
10430
  const usedPrimaryText = Boolean(primaryText);
9433
10431
  const outcome = primaryText ? stopReason === "error" ? "provider_error" : "success" : sideEffectOnlySuccess ? "success" : "execution_failure";
9434
- const fallbackText = buildExecutionFailureMessage(toolErrorCount);
9435
10432
  const suppressReactionOnlyText = reactionPerformed && !channelPostPerformed && replyFiles.length === 0 && Boolean(primaryText) && isReactionOnlyIntent(userInput);
9436
- const rawResponseText = suppressReactionOnlyText ? "" : primaryText || (sideEffectOnlySuccess ? "" : fallbackText);
10433
+ const rawResponseText = suppressReactionOnlyText ? "" : primaryText;
9437
10434
  const responseText = canvasCreated && isVerbosePostCanvasReply(rawResponseText) ? buildBriefPostCanvasReply(artifactStatePatch) : rawResponseText;
9438
10435
  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 ? {
10436
+ const resolvedText = escapedOrRawPayload ? "" : enforceAttachmentClaimTruth(responseText, replyFiles.length > 0);
10437
+ const resolvedOutcome = escapedOrRawPayload ? "execution_failure" : outcome;
10438
+ const deliveryPlan = resolvedOutcome === "success" && !resolvedText && replyFiles.length === 0 && (reactionPerformed || channelPostPerformed) ? {
9441
10439
  ...baseDeliveryPlan,
9442
10440
  postThreadText: false
9443
10441
  } : baseDeliveryPlan;
9444
10442
  const deliveryMode = deliveryPlan.mode;
9445
- const resolvedOutcome = escapedOrRawPayload ? "execution_failure" : outcome;
9446
10443
  if (shouldTrace) {
9447
10444
  logInfo(
9448
10445
  "agent_message_out",
@@ -10734,6 +11731,13 @@ async function generateAssistantReply(messageText, context = {}) {
10734
11731
  }
10735
11732
  }
10736
11733
  );
11734
+ const toolGuidance = Object.entries(
11735
+ tools
11736
+ ).map(([name, definition]) => ({
11737
+ name,
11738
+ promptGuidelines: definition.promptGuidelines,
11739
+ promptSnippet: definition.promptSnippet
11740
+ }));
10737
11741
  syncResumeState();
10738
11742
  for (const skill of activeSkills) {
10739
11743
  await turnMcpToolManager.activateForSkill(skill);
@@ -10753,6 +11757,7 @@ async function generateAssistantReply(messageText, context = {}) {
10753
11757
  availableSkills,
10754
11758
  activeSkills,
10755
11759
  activeMcpCatalogs,
11760
+ toolGuidance,
10756
11761
  runtime: {
10757
11762
  channelId: toolChannelId,
10758
11763
  fastModelId: botConfig.fastModelId,
@@ -10806,7 +11811,16 @@ async function generateAssistantReply(messageText, context = {}) {
10806
11811
  pluginAuth,
10807
11812
  onToolCall
10808
11813
  );
10809
- advisorTools = agentTools.filter((tool2) => isAdvisorToolAllowed(tool2.name));
11814
+ advisorTools = createAgentTools(
11815
+ createAdvisorToolDefinitions(tools),
11816
+ skillSandbox,
11817
+ spanContext,
11818
+ context.onStatus,
11819
+ sandboxExecutor,
11820
+ capabilityRuntime,
11821
+ pluginAuth,
11822
+ onToolCall
11823
+ );
10810
11824
  agent = new Agent2({
10811
11825
  getApiKey: () => getPiGatewayApiKeyOverride(),
10812
11826
  initialState: {
@@ -11107,6 +12121,97 @@ async function generateAssistantReply(messageText, context = {}) {
11107
12121
  }
11108
12122
  }
11109
12123
 
12124
+ // src/chat/services/turn-failure-response.ts
12125
+ function requireTurnFailureEventId(eventId, eventName) {
12126
+ if (!eventId) {
12127
+ throw new Error(`Sentry did not return an event ID for ${eventName}`);
12128
+ }
12129
+ return eventId;
12130
+ }
12131
+ function getExecutionFailureReason(reply) {
12132
+ const errorMessage = reply.diagnostics.errorMessage?.trim();
12133
+ if (errorMessage) {
12134
+ return errorMessage;
12135
+ }
12136
+ if (reply.diagnostics.toolErrorCount > 0) {
12137
+ return `${reply.diagnostics.toolErrorCount} tool result error(s)`;
12138
+ }
12139
+ if (reply.diagnostics.assistantMessageCount > 0) {
12140
+ return "assistant returned no text";
12141
+ }
12142
+ return "empty assistant turn";
12143
+ }
12144
+ function getFailureCapture(reply) {
12145
+ if (reply.diagnostics.outcome === "provider_error") {
12146
+ return {
12147
+ eventName: "agent_turn_provider_error",
12148
+ error: reply.diagnostics.providerError ?? new Error(
12149
+ reply.diagnostics.errorMessage ?? "Provider error without explicit message"
12150
+ ),
12151
+ attributes: {},
12152
+ body: "Agent turn failed with provider error"
12153
+ };
12154
+ }
12155
+ const failureReason = getExecutionFailureReason(reply);
12156
+ return {
12157
+ eventName: "agent_turn_execution_failure",
12158
+ error: new Error(`Agent turn execution failure: ${failureReason}`),
12159
+ attributes: {
12160
+ "app.ai.execution_failure_reason": failureReason
12161
+ },
12162
+ body: "Agent turn completed with execution failure"
12163
+ };
12164
+ }
12165
+ function getAgentTurnDiagnosticsAttributes(reply) {
12166
+ return {
12167
+ "gen_ai.provider.name": GEN_AI_PROVIDER_NAME,
12168
+ "gen_ai.operation.name": "invoke_agent",
12169
+ "app.ai.outcome": reply.diagnostics.outcome,
12170
+ "app.ai.assistant_messages": reply.diagnostics.assistantMessageCount,
12171
+ "app.ai.tool_results": reply.diagnostics.toolResultCount,
12172
+ "app.ai.tool_error_results": reply.diagnostics.toolErrorCount,
12173
+ "app.ai.tool_call_count": reply.diagnostics.toolCalls.length,
12174
+ "app.ai.used_primary_text": reply.diagnostics.usedPrimaryText,
12175
+ ...reply.diagnostics.thinkingLevel ? {
12176
+ "app.ai.reasoning_effort": reply.diagnostics.thinkingLevel
12177
+ } : {},
12178
+ ...reply.diagnostics.stopReason ? {
12179
+ "gen_ai.response.finish_reasons": [reply.diagnostics.stopReason]
12180
+ } : {},
12181
+ ...reply.diagnostics.errorMessage ? { "error.message": reply.diagnostics.errorMessage } : {}
12182
+ };
12183
+ }
12184
+ function finalizeFailedTurnReply(args) {
12185
+ if (args.reply.diagnostics.outcome === "success") {
12186
+ return args.reply;
12187
+ }
12188
+ const capture = getFailureCapture(args.reply);
12189
+ const eventId = requireTurnFailureEventId(
12190
+ args.logException(
12191
+ capture.error,
12192
+ capture.eventName,
12193
+ args.context,
12194
+ {
12195
+ ...getAgentTurnDiagnosticsAttributes(args.reply),
12196
+ ...args.attributes,
12197
+ ...capture.attributes
12198
+ },
12199
+ capture.body
12200
+ ),
12201
+ capture.eventName
12202
+ );
12203
+ return {
12204
+ ...args.reply,
12205
+ text: buildTurnFailureResponse(eventId),
12206
+ deliveryMode: "thread",
12207
+ deliveryPlan: {
12208
+ mode: "thread",
12209
+ postThreadText: true,
12210
+ attachFiles: args.reply.files && args.reply.files.length > 0 ? "inline" : "none"
12211
+ }
12212
+ };
12213
+ }
12214
+
11110
12215
  // src/chat/slack/assistant-thread/status-render.ts
11111
12216
  var DEFAULT_STATUS_CONTEXTS = {
11112
12217
  thinking: "\u2026",
@@ -11376,7 +12481,7 @@ function createSlackAdapterStatusSender(args) {
11376
12481
  };
11377
12482
  }
11378
12483
  function createSlackWebApiStatusSender(args) {
11379
- const getClient2 = args.getSlackClient ?? getSlackClient;
12484
+ const getClient3 = args.getSlackClient ?? getSlackClient;
11380
12485
  return async (text, loadingMessages) => {
11381
12486
  const channelId = args.channelId;
11382
12487
  const threadTs = args.threadTs;
@@ -11389,7 +12494,7 @@ function createSlackWebApiStatusSender(args) {
11389
12494
  }
11390
12495
  const nextLoadingMessages = text ? loadingMessages ?? [text] : void 0;
11391
12496
  try {
11392
- await getClient2().assistant.threads.setStatus({
12497
+ await getClient3().assistant.threads.setStatus({
11393
12498
  channel_id: normalizedChannelId,
11394
12499
  thread_ts: threadTs,
11395
12500
  status: text ? SLACK_ASSISTANT_ACTIVE_STATUS : "",
@@ -11466,9 +12571,56 @@ function createSlackWebApiAssistantStatusSession(args) {
11466
12571
  }
11467
12572
 
11468
12573
  // src/chat/slack/footer.ts
12574
+ var SENTRY_CONVERSATION_SEARCH_STATS_PERIOD = "14d";
12575
+ var ORG_ID_HOST_RE = /^o(\d+)\./;
11469
12576
  function escapeSlackMrkdwn(text) {
11470
12577
  return text.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
11471
12578
  }
12579
+ function escapeSlackLinkUrl(url) {
12580
+ return url.replaceAll("&", "&amp;").replaceAll("<", "%3C").replaceAll(">", "%3E");
12581
+ }
12582
+ function toOptionalString2(value) {
12583
+ if (typeof value === "number" && Number.isFinite(value)) {
12584
+ return String(value);
12585
+ }
12586
+ return typeof value === "string" && value.trim() ? value.trim() : void 0;
12587
+ }
12588
+ function quoteSentrySearchValue(value) {
12589
+ return `"${value.replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"`;
12590
+ }
12591
+ function getDsnOrgId(host) {
12592
+ return host?.match(ORG_ID_HOST_RE)?.[1];
12593
+ }
12594
+ function isSentrySaasDsnHost(host) {
12595
+ return host === "sentry.io" || host.endsWith(".sentry.io");
12596
+ }
12597
+ function buildSentryWebBaseUrl(dsn) {
12598
+ if (isSentrySaasDsnHost(dsn.host)) {
12599
+ return "https://sentry.io";
12600
+ }
12601
+ const port = dsn.port ? `:${dsn.port}` : "";
12602
+ const path11 = dsn.path ? `/${dsn.path}` : "";
12603
+ return `${dsn.protocol}://${dsn.host}${port}${path11}`;
12604
+ }
12605
+ function getSentryConversationSearchUrl(conversationId) {
12606
+ const client2 = sentry_exports.getClient();
12607
+ const dsn = client2?.getDsn();
12608
+ if (!dsn?.host || !dsn.projectId) {
12609
+ return void 0;
12610
+ }
12611
+ const orgId = toOptionalString2(client2?.getOptions().orgId) ?? getDsnOrgId(dsn.host);
12612
+ if (!orgId) {
12613
+ return void 0;
12614
+ }
12615
+ const params = new URLSearchParams();
12616
+ params.set(
12617
+ "query",
12618
+ `gen_ai.conversation.id:${quoteSentrySearchValue(conversationId)}`
12619
+ );
12620
+ params.set("project", dsn.projectId);
12621
+ params.set("statsPeriod", SENTRY_CONVERSATION_SEARCH_STATS_PERIOD);
12622
+ return `${buildSentryWebBaseUrl(dsn)}/organizations/${orgId}/explore/traces/?${params.toString()}`;
12623
+ }
11472
12624
  function formatSlackTokenCount(value) {
11473
12625
  if (value >= 1e6) {
11474
12626
  const millions = value / 1e6;
@@ -11518,10 +12670,15 @@ function buildSlackReplyFooter(args) {
11518
12670
  const items = [];
11519
12671
  const conversationId = args.conversationId?.trim();
11520
12672
  if (conversationId) {
11521
- items.push({
12673
+ const idItem = {
11522
12674
  label: "ID",
11523
12675
  value: conversationId
11524
- });
12676
+ };
12677
+ const conversationUrl = getSentryConversationSearchUrl(conversationId);
12678
+ if (conversationUrl) {
12679
+ idItem.url = conversationUrl;
12680
+ }
12681
+ items.push(idItem);
11525
12682
  }
11526
12683
  const totalTokens = resolveTotalTokens(args.usage);
11527
12684
  if (totalTokens !== void 0) {
@@ -11560,7 +12717,7 @@ function buildSlackReplyBlocks(text, footer) {
11560
12717
  type: "context",
11561
12718
  elements: footer.items.map((item) => ({
11562
12719
  type: "mrkdwn",
11563
- text: `*${escapeSlackMrkdwn(item.label)}:* ${escapeSlackMrkdwn(item.value)}`
12720
+ text: item.url ? `*${escapeSlackMrkdwn(item.label)}:* <${escapeSlackLinkUrl(item.url)}|${escapeSlackMrkdwn(item.value)}>` : `*${escapeSlackMrkdwn(item.label)}:* ${escapeSlackMrkdwn(item.value)}`
11564
12721
  }))
11565
12722
  });
11566
12723
  }
@@ -11569,9 +12726,6 @@ function buildSlackReplyBlocks(text, footer) {
11569
12726
 
11570
12727
  // src/chat/slack/reply.ts
11571
12728
  import { Buffer as Buffer2 } from "buffer";
11572
- function isInterruptedVisibleReply(reply) {
11573
- return reply.diagnostics.outcome === "provider_error";
11574
- }
11575
12729
  function resolveReplyDelivery(reply) {
11576
12730
  const replyHasFiles = Boolean(reply.files && reply.files.length > 0);
11577
12731
  const deliveryPlan = reply.deliveryPlan ?? {
@@ -11595,9 +12749,7 @@ function buildReplyText(text) {
11595
12749
  return "";
11596
12750
  }
11597
12751
  function buildTextPosts(args) {
11598
- const chunks = splitSlackReplyText(args.text, {
11599
- interrupted: args.interrupted
11600
- });
12752
+ const chunks = splitSlackReplyText(args.text);
11601
12753
  return chunks.map((chunk, index) => ({
11602
12754
  text: chunk,
11603
12755
  ...index === 0 && args.firstFiles ? { files: args.firstFiles } : {},
@@ -11648,11 +12800,9 @@ function planSlackReplyPosts(args) {
11648
12800
  const { shouldPostThreadReply, attachFiles } = resolveReplyDelivery(
11649
12801
  args.reply
11650
12802
  );
11651
- const interrupted = isInterruptedVisibleReply(args.reply);
11652
12803
  const posts = [];
11653
12804
  const textPosts = shouldPostThreadReply ? buildTextPosts({
11654
12805
  text: args.reply.text,
11655
- interrupted,
11656
12806
  firstFiles: attachFiles === "inline" ? replyFiles : void 0
11657
12807
  }) : [];
11658
12808
  posts.push(...textPosts);
@@ -11777,6 +12927,64 @@ var ResumeTurnBusyError = class extends Error {
11777
12927
  function getDefaultLockKey(channelId, threadTs) {
11778
12928
  return `slack:${channelId}:${threadTs}`;
11779
12929
  }
12930
+ function getResumeLogContext(args, lockKey) {
12931
+ return {
12932
+ conversationId: args.replyContext?.correlation?.conversationId ?? lockKey,
12933
+ slackThreadId: args.replyContext?.correlation?.threadId ?? lockKey,
12934
+ slackUserId: args.replyContext?.requester?.userId ?? args.replyContext?.correlation?.requesterId,
12935
+ slackUserName: args.replyContext?.requester?.userName,
12936
+ slackChannelId: args.channelId,
12937
+ runId: args.replyContext?.correlation?.runId,
12938
+ assistantUserName: botConfig.userName,
12939
+ modelId: botConfig.modelId
12940
+ };
12941
+ }
12942
+ async function postResumeFailureReply(args) {
12943
+ try {
12944
+ await postSlackMessage({
12945
+ channelId: args.channelId,
12946
+ threadTs: args.threadTs,
12947
+ text: buildTurnFailureResponse(args.eventId)
12948
+ });
12949
+ } catch (error) {
12950
+ logException(
12951
+ error,
12952
+ "slack_resume_failure_reply_post_failed",
12953
+ args.logContext,
12954
+ {
12955
+ "app.error.original_event_id": args.eventId
12956
+ },
12957
+ "Failed to post resumed turn failure reply"
12958
+ );
12959
+ throw error;
12960
+ }
12961
+ }
12962
+ async function handleResumeFailure(args) {
12963
+ const logContext = getResumeLogContext(args.resumeArgs, args.lockKey);
12964
+ const capturedEventId = logException(
12965
+ args.error,
12966
+ args.eventName,
12967
+ logContext,
12968
+ {},
12969
+ args.body
12970
+ );
12971
+ await args.resumeArgs.onFailure?.(args.error);
12972
+ const eventId = requireTurnFailureEventId(capturedEventId, args.eventName);
12973
+ let postError;
12974
+ try {
12975
+ await postResumeFailureReply({
12976
+ channelId: args.resumeArgs.channelId,
12977
+ threadTs: args.resumeArgs.threadTs,
12978
+ eventId,
12979
+ logContext
12980
+ });
12981
+ } catch (error) {
12982
+ postError = error;
12983
+ }
12984
+ if (postError) {
12985
+ throw postError;
12986
+ }
12987
+ }
11780
12988
  function createResumeReplyContext(args, statusSession) {
11781
12989
  const replyContext = args.replyContext ?? {};
11782
12990
  const threadId = args.lockKey ?? getDefaultLockKey(args.channelId, args.threadTs);
@@ -11844,7 +13052,7 @@ async function resumeSlackTurn(args) {
11844
13052
  ...replyContext
11845
13053
  });
11846
13054
  const replyTimeoutMs = resolveReplyTimeoutMs(args.replyTimeoutMs);
11847
- const reply = typeof replyTimeoutMs === "number" ? await Promise.race([
13055
+ let reply = typeof replyTimeoutMs === "number" ? await Promise.race([
11848
13056
  replyPromise,
11849
13057
  new Promise(
11850
13058
  (_, reject) => setTimeout(
@@ -11857,6 +13065,11 @@ async function resumeSlackTurn(args) {
11857
13065
  )
11858
13066
  )
11859
13067
  ]) : await replyPromise;
13068
+ reply = finalizeFailedTurnReply({
13069
+ reply,
13070
+ logException,
13071
+ context: getResumeLogContext(args, lockKey)
13072
+ });
11860
13073
  await status.stop();
11861
13074
  const footer = buildSlackReplyFooter({
11862
13075
  conversationId: args.replyContext?.correlation?.conversationId ?? lockKey,
@@ -11884,14 +13097,13 @@ async function resumeSlackTurn(args) {
11884
13097
  };
11885
13098
  } else {
11886
13099
  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
- }
13100
+ await handleResumeFailure({
13101
+ body: "Failed to resume Slack turn",
13102
+ error,
13103
+ eventName: "slack_resume_turn_failed",
13104
+ lockKey,
13105
+ resumeArgs: args
13106
+ });
11895
13107
  };
11896
13108
  }
11897
13109
  } finally {
@@ -11902,14 +13114,13 @@ async function resumeSlackTurn(args) {
11902
13114
  await deferredPauseHandler();
11903
13115
  return;
11904
13116
  } 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
- }
13117
+ await handleResumeFailure({
13118
+ body: "Failed to handle resumed turn pause",
13119
+ error: pauseError,
13120
+ eventName: "slack_resume_pause_handler_failed",
13121
+ lockKey,
13122
+ resumeArgs: args
13123
+ });
11913
13124
  return;
11914
13125
  }
11915
13126
  }
@@ -11925,7 +13136,6 @@ async function resumeAuthorizedRequest(args) {
11925
13136
  replyContext: args.replyContext,
11926
13137
  lockKey: args.lockKey,
11927
13138
  initialText: args.connectedText,
11928
- failureText: args.failureText,
11929
13139
  generateReply: args.generateReply,
11930
13140
  onSuccess: args.onSuccess,
11931
13141
  onFailure: args.onFailure,
@@ -12233,7 +13443,6 @@ async function resumeAuthorizedMcpTurn(args) {
12233
13443
  threadTs: authSession.threadTs,
12234
13444
  lockKey: authSession.conversationId,
12235
13445
  connectedText: "",
12236
- failureText: "MCP authorization completed, but resuming the request failed. Please retry the original command.",
12237
13446
  replyContext: {
12238
13447
  requester: {
12239
13448
  userId: authSession.userId,
@@ -12283,14 +13492,7 @@ async function resumeAuthorizedMcpTurn(args) {
12283
13492
  );
12284
13493
  }
12285
13494
  },
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
- );
13495
+ onFailure: async () => {
12294
13496
  try {
12295
13497
  await persistFailedReplyState(
12296
13498
  authSession.channelId,
@@ -12403,7 +13605,7 @@ async function GET4(request, provider, waitUntil) {
12403
13605
 
12404
13606
  // src/chat/slack/app-home.ts
12405
13607
  import fs5 from "fs";
12406
- import path6 from "path";
13608
+ import path10 from "path";
12407
13609
  var DEFAULT_DESCRIPTION_TEXT = "I help your team investigate, summarize, and act on work in Slack.";
12408
13610
  var MAX_HOME_SKILLS = 6;
12409
13611
  var MAX_SECTION_TEXT_CHARS = 3e3;
@@ -12415,7 +13617,7 @@ function clampSectionText(text) {
12415
13617
  return `${text.slice(0, MAX_SECTION_TEXT_CHARS - 1)}\u2026`;
12416
13618
  }
12417
13619
  function loadDescriptionText() {
12418
- const descriptionPath = path6.join(homeDir(), "DESCRIPTION.md");
13620
+ const descriptionPath = path10.join(homeDir(), "DESCRIPTION.md");
12419
13621
  try {
12420
13622
  const raw = fs5.readFileSync(descriptionPath, "utf8").trim();
12421
13623
  if (raw.length > 0) {
@@ -12682,7 +13884,6 @@ async function resumeCheckpointedOAuthTurn(stored) {
12682
13884
  threadTs: stored.threadTs,
12683
13885
  lockKey: stored.resumeConversationId,
12684
13886
  initialText: "",
12685
- failureText: "I connected your account but hit an error processing your request. Please try the command again.",
12686
13887
  replyContext: {
12687
13888
  requester: {
12688
13889
  userId: userMessage.author.userId,
@@ -12730,14 +13931,7 @@ async function resumeCheckpointedOAuthTurn(stored) {
12730
13931
  reply
12731
13932
  });
12732
13933
  },
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
- );
13934
+ onFailure: async () => {
12741
13935
  await persistFailedOAuthReplyState({
12742
13936
  conversationId: stored.resumeConversationId,
12743
13937
  sessionId: resolvedSessionId
@@ -12789,7 +13983,6 @@ async function resumePendingOAuthMessage(stored) {
12789
13983
  channelId: stored.channelId,
12790
13984
  threadTs: stored.threadTs,
12791
13985
  connectedText: "",
12792
- failureText: `I connected your account but hit an error processing your request. Please try \`${stored.pendingMessage}\` again.`,
12793
13986
  replyContext: {
12794
13987
  requester: { userId: stored.userId },
12795
13988
  conversationContext,
@@ -12807,15 +14000,6 @@ async function resumePendingOAuthMessage(stored) {
12807
14000
  },
12808
14001
  "OAuth callback auto-resumed pending message finished replying"
12809
14002
  );
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
14003
  }
12820
14004
  });
12821
14005
  }
@@ -13142,7 +14326,6 @@ async function resumeTimedOutTurn(payload) {
13142
14326
  channelId: thread.channelId,
13143
14327
  threadTs: thread.threadTs,
13144
14328
  lockKey: payload.conversationId,
13145
- failureText: "I hit an error while resuming that request. Please try the command again.",
13146
14329
  replyContext: {
13147
14330
  requester: {
13148
14331
  userId: userMessage.author.userId,
@@ -13191,18 +14374,7 @@ async function resumeTimedOutTurn(payload) {
13191
14374
  );
13192
14375
  }
13193
14376
  },
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
- );
14377
+ onFailure: async () => {
13206
14378
  await persistFailedReplyState2(checkpoint);
13207
14379
  },
13208
14380
  onAuthPause: async () => {
@@ -13314,11 +14486,11 @@ var DIRECTED_FOLLOW_UP_CUE_RE = /\b(?:you said|you just said|your last response|
13314
14486
  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
14487
  var GENERIC_IMMEDIATE_SIDE_CONVERSATION_RE = /^(?:is that (?:the )?right (?:approach|call|move)|(?:can|could|would) you check on this)\??$/i;
13316
14488
  var RECENT_THREAD_WINDOW = 6;
13317
- function escapeRegExp(value) {
14489
+ function escapeRegExp2(value) {
13318
14490
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
13319
14491
  }
13320
14492
  function containsAssistantInvocation(text, botUserName) {
13321
- const escapedUserName = escapeRegExp(botUserName);
14493
+ const escapedUserName = escapeRegExp2(botUserName);
13322
14494
  const plainNameMentionRe = new RegExp(`(^|\\s)@${escapedUserName}\\b`, "i");
13323
14495
  const labeledEntityMentionRe = new RegExp(
13324
14496
  `<@[^>|]+\\|${escapedUserName}>`,
@@ -13614,7 +14786,7 @@ async function decideSubscribedThreadReply(args) {
13614
14786
  }
13615
14787
 
13616
14788
  // src/chat/runtime/thread-context.ts
13617
- function escapeRegExp2(value) {
14789
+ function escapeRegExp3(value) {
13618
14790
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
13619
14791
  }
13620
14792
  function stripLeadingBotMention(text, options = {}) {
@@ -13624,12 +14796,12 @@ function stripLeadingBotMention(text, options = {}) {
13624
14796
  next = next.replace(/^\s*<@[^>]+>[\s,:-]*/, "").trim();
13625
14797
  }
13626
14798
  const mentionByNameRe = new RegExp(
13627
- `^\\s*@${escapeRegExp2(botConfig.userName)}\\b[\\s,:-]*`,
14799
+ `^\\s*@${escapeRegExp3(botConfig.userName)}\\b[\\s,:-]*`,
13628
14800
  "i"
13629
14801
  );
13630
14802
  next = next.replace(mentionByNameRe, "").trim();
13631
14803
  const mentionByLabeledEntityRe = new RegExp(
13632
- `^\\s*<@[^>|]+\\|${escapeRegExp2(botConfig.userName)}>[\\s,:-]*`,
14804
+ `^\\s*<@[^>|]+\\|${escapeRegExp3(botConfig.userName)}>[\\s,:-]*`,
13633
14805
  "i"
13634
14806
  );
13635
14807
  next = next.replace(mentionByLabeledEntityRe, "").trim();
@@ -13730,15 +14902,6 @@ async function maybeHandleThreadOptOutDecision(args) {
13730
14902
  await args.thread.post(THREAD_OPTOUT_ACK);
13731
14903
  return true;
13732
14904
  }
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
14905
  function buildLogContext(deps, args) {
13743
14906
  return {
13744
14907
  conversationId: args.threadId ?? args.runId,
@@ -13755,7 +14918,7 @@ function createSlackTurnRuntime(deps) {
13755
14918
  const logContext = (args) => buildLogContext(deps, args);
13756
14919
  const postFallbackErrorReplyWithLogging = async (args) => {
13757
14920
  try {
13758
- await args.thread.post(buildFailureMessage(args.reference));
14921
+ await args.thread.post(buildTurnFailureResponse(args.eventId));
13759
14922
  } catch (postError) {
13760
14923
  deps.logException(
13761
14924
  postError,
@@ -13763,7 +14926,7 @@ function createSlackTurnRuntime(deps) {
13763
14926
  args.errorContext,
13764
14927
  {
13765
14928
  "app.slack.reply_stage": "error_fallback_post",
13766
- ...args.eventId ? { "app.error.original_event_id": args.eventId } : {},
14929
+ "app.error.original_event_id": args.eventId,
13767
14930
  ...getSlackErrorObservabilityAttributes(postError)
13768
14931
  },
13769
14932
  args.postFailureBody
@@ -13849,11 +15012,14 @@ function createSlackTurnRuntime(deps) {
13849
15012
  {},
13850
15013
  "onNewMention failed"
13851
15014
  );
15015
+ if (!eventId) {
15016
+ throw new Error(
15017
+ "Sentry did not return an event ID for mention_handler_failed"
15018
+ );
15019
+ }
13852
15020
  await hooks?.beforeFirstResponsePost?.();
13853
- const reference = deps.getErrorReference(eventId);
13854
15021
  await postFallbackErrorReplyWithLogging({
13855
15022
  thread,
13856
- reference,
13857
15023
  errorContext,
13858
15024
  eventId,
13859
15025
  postFailureEventName: "mention_handler_failure_reply_post_failed",
@@ -13981,11 +15147,14 @@ function createSlackTurnRuntime(deps) {
13981
15147
  {},
13982
15148
  "onSubscribedMessage failed"
13983
15149
  );
15150
+ if (!eventId) {
15151
+ throw new Error(
15152
+ "Sentry did not return an event ID for subscribed_message_handler_failed"
15153
+ );
15154
+ }
13984
15155
  await hooks?.beforeFirstResponsePost?.();
13985
- const reference = deps.getErrorReference(eventId);
13986
15156
  await postFallbackErrorReplyWithLogging({
13987
15157
  thread,
13988
- reference,
13989
15158
  errorContext,
13990
15159
  eventId,
13991
15160
  postFailureEventName: "subscribed_message_handler_failure_reply_post_failed",
@@ -14773,19 +15942,6 @@ function maybeUpdateAssistantTitle(args) {
14773
15942
  }
14774
15943
 
14775
15944
  // 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
15945
  function createReplyToThread(deps) {
14790
15946
  return async function replyToThread(thread, message, options = {}) {
14791
15947
  if (message.author.isMe) {
@@ -14935,7 +16091,7 @@ function createReplyToThread(deps) {
14935
16091
  let shouldPersistFailureState = true;
14936
16092
  try {
14937
16093
  const toolChannelId = preparedState.artifacts.assistantContextChannelId ?? channelId;
14938
- const reply = await deps.services.generateAssistantReply(userText, {
16094
+ let reply = await deps.services.generateAssistantReply(userText, {
14939
16095
  requester: {
14940
16096
  userId: message.author.userId,
14941
16097
  userName: message.author.userName ?? fallbackIdentity?.userName,
@@ -14994,49 +16150,14 @@ function createReplyToThread(deps) {
14994
16150
  assistantUserName: botConfig.userName,
14995
16151
  modelId: reply.diagnostics.modelId
14996
16152
  };
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
- };
16153
+ const diagnosticsAttributes = getAgentTurnDiagnosticsAttributes(reply);
15016
16154
  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
- );
16155
+ if (reply.diagnostics.outcome !== "success") {
16156
+ reply = finalizeFailedTurnReply({
16157
+ reply,
16158
+ logException,
16159
+ context: diagnosticsContext
16160
+ });
15040
16161
  }
15041
16162
  markConversationMessage(
15042
16163
  preparedState.conversation,
@@ -15408,7 +16529,6 @@ function createSlackRuntime(options) {
15408
16529
  assistantUserName: botConfig.userName,
15409
16530
  modelId: botConfig.modelId,
15410
16531
  now: options.now ?? (() => Date.now()),
15411
- getErrorReference: resolveErrorReference,
15412
16532
  getThreadId,
15413
16533
  getChannelId,
15414
16534
  getRunId,