wave-agent-sdk 1.1.5 → 1.3.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.
Files changed (210) hide show
  1. package/dist/agent.d.ts +128 -28
  2. package/dist/agent.js +201 -49
  3. package/dist/builtin/index.js +2 -0
  4. package/dist/builtin/plugins.js +11 -20
  5. package/dist/builtin/skills/settings.js +7 -20
  6. package/dist/builtin/skills/wave-daemon.d.ts +1 -0
  7. package/dist/builtin/skills/wave-daemon.js +194 -0
  8. package/dist/constants/images.d.ts +26 -0
  9. package/dist/constants/images.js +26 -0
  10. package/dist/constants/index.d.ts +16 -0
  11. package/dist/constants/index.js +16 -0
  12. package/dist/constants/memory.d.ts +26 -0
  13. package/dist/constants/memory.js +34 -0
  14. package/dist/constants/messages.d.ts +11 -0
  15. package/dist/constants/messages.js +11 -0
  16. package/dist/constants/plugins.d.ts +8 -0
  17. package/dist/constants/plugins.js +8 -0
  18. package/dist/constants/tools.d.ts +1 -0
  19. package/dist/constants/tools.js +1 -0
  20. package/dist/core/plugin.d.ts +54 -10
  21. package/dist/core/plugin.js +137 -23
  22. package/dist/core/session.d.ts +1 -1
  23. package/dist/core/session.js +1 -1
  24. package/dist/exec/catalog.d.ts +140 -0
  25. package/dist/exec/catalog.js +470 -0
  26. package/dist/exec/catalogAnnouncement.d.ts +89 -0
  27. package/dist/exec/catalogAnnouncement.js +293 -0
  28. package/dist/exec/constants.d.ts +51 -0
  29. package/dist/exec/constants.js +51 -0
  30. package/dist/exec/execRuntime.d.ts +55 -0
  31. package/dist/exec/execRuntime.js +217 -0
  32. package/dist/exec/workerSource.d.ts +28 -0
  33. package/dist/exec/workerSource.js +299 -0
  34. package/dist/host/index.d.ts +23 -0
  35. package/dist/host/index.js +23 -0
  36. package/dist/index.d.ts +7 -1
  37. package/dist/index.js +8 -1
  38. package/dist/managers/MemoryRuleManager.d.ts +6 -0
  39. package/dist/managers/MemoryRuleManager.js +12 -0
  40. package/dist/managers/aiManager.d.ts +35 -25
  41. package/dist/managers/aiManager.js +204 -202
  42. package/dist/managers/backgroundTaskManager.js +14 -0
  43. package/dist/managers/bashModeManager.d.ts +33 -0
  44. package/dist/managers/bashModeManager.js +110 -0
  45. package/dist/managers/hookManager.d.ts +18 -0
  46. package/dist/managers/hookManager.js +37 -3
  47. package/dist/managers/liveConfigManager.d.ts +33 -0
  48. package/dist/managers/liveConfigManager.js +106 -11
  49. package/dist/managers/lspManager.d.ts +9 -0
  50. package/dist/managers/lspManager.js +47 -18
  51. package/dist/managers/mcpManager.d.ts +68 -10
  52. package/dist/managers/mcpManager.js +265 -15
  53. package/dist/managers/messageManager.d.ts +60 -18
  54. package/dist/managers/messageManager.js +170 -81
  55. package/dist/managers/permissionManager.d.ts +69 -0
  56. package/dist/managers/permissionManager.js +221 -78
  57. package/dist/managers/planManager.d.ts +9 -0
  58. package/dist/managers/planManager.js +19 -1
  59. package/dist/managers/pluginManager.d.ts +46 -2
  60. package/dist/managers/pluginManager.js +117 -11
  61. package/dist/managers/pluginScopeManager.d.ts +15 -2
  62. package/dist/managers/pluginScopeManager.js +20 -1
  63. package/dist/managers/skillManager.d.ts +50 -0
  64. package/dist/managers/skillManager.js +166 -12
  65. package/dist/managers/slashCommandManager.d.ts +10 -0
  66. package/dist/managers/slashCommandManager.js +44 -31
  67. package/dist/managers/subagentManager.d.ts +15 -0
  68. package/dist/managers/subagentManager.js +81 -9
  69. package/dist/managers/toolManager.d.ts +29 -3
  70. package/dist/managers/toolManager.js +87 -13
  71. package/dist/managers/workflowManager.js +6 -0
  72. package/dist/prompts/autoMemory.d.ts +9 -0
  73. package/dist/prompts/autoMemory.js +30 -31
  74. package/dist/prompts/autoMemoryExtraction.d.ts +4 -0
  75. package/dist/prompts/autoMemoryExtraction.js +8 -111
  76. package/dist/prompts/index.d.ts +0 -1
  77. package/dist/prompts/index.js +0 -4
  78. package/dist/prompts/memoryTypes.d.ts +63 -0
  79. package/dist/prompts/memoryTypes.js +191 -0
  80. package/dist/services/GitService.d.ts +7 -0
  81. package/dist/services/GitService.js +23 -0
  82. package/dist/services/MarketplaceService.d.ts +101 -17
  83. package/dist/services/MarketplaceService.js +323 -102
  84. package/dist/services/artifactContent.d.ts +84 -0
  85. package/dist/services/artifactContent.js +204 -0
  86. package/dist/services/artifactSession.d.ts +6 -0
  87. package/dist/services/artifactSession.js +17 -0
  88. package/dist/services/autoMemoryService.js +5 -13
  89. package/dist/services/configurationService.d.ts +92 -9
  90. package/dist/services/configurationService.js +246 -64
  91. package/dist/services/contentSummarizer.d.ts +15 -0
  92. package/dist/services/contentSummarizer.js +45 -0
  93. package/dist/services/execAvailability.d.ts +9 -0
  94. package/dist/services/execAvailability.js +32 -0
  95. package/dist/services/fileWatcher.js +61 -6
  96. package/dist/services/initializationService.js +21 -17
  97. package/dist/services/interactionService.d.ts +9 -1
  98. package/dist/services/interactionService.js +28 -8
  99. package/dist/services/jsonlHandler.d.ts +98 -0
  100. package/dist/services/jsonlHandler.js +250 -12
  101. package/dist/services/memory.d.ts +17 -1
  102. package/dist/services/memory.js +44 -7
  103. package/dist/services/officialMarketplaceMirror.d.ts +85 -0
  104. package/dist/services/officialMarketplaceMirror.js +290 -0
  105. package/dist/services/pluginLoader.d.ts +12 -4
  106. package/dist/services/pluginLoader.js +38 -7
  107. package/dist/services/remoteSettingsService.js +20 -6
  108. package/dist/services/session.d.ts +74 -0
  109. package/dist/services/session.js +174 -16
  110. package/dist/services/sessionEntries.d.ts +2 -0
  111. package/dist/services/sessionEntries.js +20 -0
  112. package/dist/services/worktreeHooks.js +6 -1
  113. package/dist/stdio/index.d.ts +12 -0
  114. package/dist/stdio/index.js +12 -0
  115. package/dist/stdio/notificationRouter.d.ts +38 -0
  116. package/dist/stdio/notificationRouter.js +97 -0
  117. package/dist/stdio/rpcClient.d.ts +18 -0
  118. package/dist/stdio/rpcClient.js +10 -0
  119. package/dist/stdio/stdioAgent.d.ts +229 -0
  120. package/dist/stdio/stdioAgent.js +360 -0
  121. package/dist/tools/artifactTool.js +406 -273
  122. package/dist/tools/bashTool.js +10 -6
  123. package/dist/tools/editTool.js +6 -3
  124. package/dist/tools/execTool.d.ts +2 -0
  125. package/dist/tools/execTool.js +165 -0
  126. package/dist/tools/exitPlanMode.js +10 -2
  127. package/dist/tools/grepTool.js +7 -1
  128. package/dist/tools/readTool.js +30 -2
  129. package/dist/tools/types.d.ts +34 -8
  130. package/dist/tools/webFetchTool.js +15 -166
  131. package/dist/tools/workflowTool.js +40 -8
  132. package/dist/tools/writeTool.js +6 -3
  133. package/dist/types/agent.d.ts +24 -1
  134. package/dist/types/commands.d.ts +7 -0
  135. package/dist/types/configuration.d.ts +45 -2
  136. package/dist/types/hooks.d.ts +1 -0
  137. package/dist/types/hooks.js +19 -0
  138. package/dist/types/marketplace.d.ts +40 -2
  139. package/dist/types/mcp.d.ts +42 -0
  140. package/dist/types/messaging.d.ts +1 -8
  141. package/dist/types/permissions.d.ts +22 -0
  142. package/dist/types/permissions.js +17 -0
  143. package/dist/types/plugins.d.ts +26 -2
  144. package/dist/types/skills.d.ts +26 -0
  145. package/dist/utils/bashParser.d.ts +17 -0
  146. package/dist/utils/bashParser.js +72 -0
  147. package/dist/utils/bashStructure/bashLexer.d.ts +96 -0
  148. package/dist/utils/bashStructure/bashLexer.js +676 -0
  149. package/dist/utils/bashStructure/bashParser.d.ts +144 -0
  150. package/dist/utils/bashStructure/bashParser.js +606 -0
  151. package/dist/utils/bashStructure/bashSemantics.d.ts +70 -0
  152. package/dist/utils/bashStructure/bashSemantics.js +477 -0
  153. package/dist/utils/bashStructure/index.d.ts +26 -0
  154. package/dist/utils/bashStructure/index.js +27 -0
  155. package/dist/utils/bashStructure/types.d.ts +62 -0
  156. package/dist/utils/bashStructure/types.js +47 -0
  157. package/dist/utils/constants.d.ts +10 -0
  158. package/dist/utils/constants.js +10 -0
  159. package/dist/utils/containerSetup.js +48 -6
  160. package/dist/utils/convertMessagesForAPI.d.ts +7 -1
  161. package/dist/utils/convertMessagesForAPI.js +64 -14
  162. package/dist/utils/fileChangeReminder.d.ts +20 -0
  163. package/dist/utils/fileChangeReminder.js +153 -0
  164. package/dist/utils/fileSearch.js +4 -3
  165. package/dist/utils/fileUtils.d.ts +44 -0
  166. package/dist/utils/fileUtils.js +118 -0
  167. package/dist/utils/frontmatterYaml.d.ts +33 -0
  168. package/dist/utils/frontmatterYaml.js +192 -0
  169. package/dist/utils/imageBudget.d.ts +85 -0
  170. package/dist/utils/imageBudget.js +109 -0
  171. package/dist/utils/imageDimensions.d.ts +83 -0
  172. package/dist/utils/imageDimensions.js +232 -0
  173. package/dist/utils/imageProcessor.d.ts +66 -0
  174. package/dist/utils/imageProcessor.js +84 -0
  175. package/dist/utils/imageRewrite.d.ts +29 -0
  176. package/dist/utils/imageRewrite.js +251 -0
  177. package/dist/utils/markdownParser.d.ts +5 -1
  178. package/dist/utils/markdownParser.js +9 -51
  179. package/dist/utils/mcpInstructions.d.ts +61 -0
  180. package/dist/utils/mcpInstructions.js +126 -0
  181. package/dist/utils/mcpUtils.d.ts +7 -0
  182. package/dist/utils/mcpUtils.js +11 -2
  183. package/dist/utils/memoryAge.d.ts +32 -0
  184. package/dist/utils/memoryAge.js +47 -0
  185. package/dist/utils/memoryEntrypoint.d.ts +20 -0
  186. package/dist/utils/memoryEntrypoint.js +49 -0
  187. package/dist/utils/memoryIndex.d.ts +30 -0
  188. package/dist/utils/memoryIndex.js +76 -0
  189. package/dist/utils/messageOperations.d.ts +6 -20
  190. package/dist/utils/messageOperations.js +40 -91
  191. package/dist/utils/nestedMemory.d.ts +22 -0
  192. package/dist/utils/nestedMemory.js +61 -0
  193. package/dist/utils/npmTarball.d.ts +19 -0
  194. package/dist/utils/npmTarball.js +92 -0
  195. package/dist/utils/pluginSource.d.ts +37 -0
  196. package/dist/utils/pluginSource.js +73 -0
  197. package/dist/utils/ripgrep.d.ts +18 -4
  198. package/dist/utils/ripgrep.js +56 -4
  199. package/dist/utils/runtimeDeps.d.ts +35 -0
  200. package/dist/utils/runtimeDeps.js +426 -0
  201. package/dist/utils/skillParser.js +22 -52
  202. package/dist/utils/subagentParser.js +48 -45
  203. package/dist/utils/tokenCalculation.js +0 -8
  204. package/dist/utils/userSettings.d.ts +90 -0
  205. package/dist/utils/userSettings.js +291 -0
  206. package/dist/utils/worktreeUtils.d.ts +2 -1
  207. package/dist/utils/worktreeUtils.js +64 -34
  208. package/package.json +12 -4
  209. package/dist/managers/bangManager.d.ts +0 -26
  210. package/dist/managers/bangManager.js +0 -78
@@ -11,6 +11,7 @@ import { processToolResult } from "../utils/toolResultStorage.js";
11
11
  import { BASH_MAX_OUTPUT_CHARS } from "../constants/toolLimits.js";
12
12
  import { BASH_TOOL_NAME, GLOB_TOOL_NAME, GREP_TOOL_NAME, READ_TOOL_NAME, EDIT_TOOL_NAME, WRITE_TOOL_NAME, } from "../constants/tools.js";
13
13
  const BASH_DEFAULT_TIMEOUT_MS = 120000;
14
+ const BASH_MAX_TIMEOUT_MS = 600000;
14
15
  // After the shell exits, its last stdout/stderr chunks may still be in flight:
15
16
  // Node emits the child's 'exit' event as soon as the process terminates, then
16
17
  // delivers the remaining pipe data on a later event-loop turn and finally
@@ -64,7 +65,7 @@ export const bashTool = {
64
65
  },
65
66
  timeout: {
66
67
  type: "number",
67
- description: "Optional timeout in milliseconds (max 600000)",
68
+ description: `Optional timeout in milliseconds (max ${BASH_MAX_TIMEOUT_MS})`,
68
69
  },
69
70
  description: {
70
71
  type: "string",
@@ -102,7 +103,7 @@ Before executing the command, please follow these steps:
102
103
 
103
104
  Usage notes:
104
105
  - The command argument is required.
105
- - You can specify an optional timeout in milliseconds (up to ${BASH_DEFAULT_TIMEOUT_MS}ms / ${BASH_DEFAULT_TIMEOUT_MS / 60000} minutes). If not specified, commands will timeout after ${BASH_DEFAULT_TIMEOUT_MS}ms (${BASH_DEFAULT_TIMEOUT_MS / 60000} minutes).
106
+ - You may specify an optional timeout in milliseconds (up to ${BASH_MAX_TIMEOUT_MS}ms / ${BASH_MAX_TIMEOUT_MS / 60000} minutes). By default, your command will timeout after ${BASH_DEFAULT_TIMEOUT_MS}ms (${BASH_DEFAULT_TIMEOUT_MS / 60000} minutes).
106
107
  - It is very helpful if you write a clear, concise description of what this command does in 5-10 words.
107
108
  - If the output exceeds ${BASH_MAX_OUTPUT_CHARS.toLocaleString()} characters, output will be truncated and the full output will be persisted to a file you can read with the Read tool.
108
109
  - You can use the \`run_in_background\` parameter to run the command in the background, which allows you to continue working while the command runs. You can monitor the output using the ${READ_TOOL_NAME} tool as it becomes available. You do not need to use '&' at the end of the command when using this parameter.
@@ -179,13 +180,14 @@ The working directory persists between commands. Try to maintain your current wo
179
180
  : "No suitable shell found. Please ensure bash or zsh is installed, or set WAVE_SHELL environment variable.",
180
181
  };
181
182
  }
182
- // Validate timeout
183
- if (timeout !== undefined &&
184
- (typeof timeout !== "number" || timeout < 0 || timeout > 600000)) {
183
+ // Validate the timeout's type only. An upper bound is deliberately not
184
+ // enforced (matching Claude Code, which leaves the type check to its zod
185
+ // schema and applies whatever value the model sends).
186
+ if (timeout !== undefined && (typeof timeout !== "number" || timeout < 0)) {
185
187
  return {
186
188
  success: false,
187
189
  content: "",
188
- error: "Timeout must be a number between 0 and 600000 milliseconds",
190
+ error: "Timeout must be a non-negative number of milliseconds",
189
191
  };
190
192
  }
191
193
  // Permission check after validation but before real operation
@@ -413,6 +415,7 @@ The working directory persists between commands. Try to maintain your current wo
413
415
  ? `${processedOutput}\n\n${reason}`
414
416
  : reason,
415
417
  error: reason,
418
+ metadata: { exitCode: 130 },
416
419
  });
417
420
  }
418
421
  };
@@ -554,6 +557,7 @@ The working directory persists between commands. Try to maintain your current wo
554
557
  error: exitCode !== 0
555
558
  ? `Command failed with exit code: ${exitCode}`
556
559
  : undefined,
560
+ metadata: { exitCode },
557
561
  });
558
562
  };
559
563
  child.on("exit", (code) => {
@@ -1,6 +1,7 @@
1
- import { readFile, writeFile, stat } from "fs/promises";
1
+ import { readFile, stat } from "fs/promises";
2
2
  import { createHash } from "crypto";
3
3
  import { logger } from "../utils/globalLogger.js";
4
+ import { atomicWriteFile } from "../utils/atomicWrite.js";
4
5
  import { resolvePath, getDisplayPath } from "../utils/path.js";
5
6
  import { escapeRegExp, analyzeEditMismatch } from "../utils/editUtils.js";
6
7
  import { EDIT_TOOL_NAME, READ_TOOL_NAME } from "../constants/tools.js";
@@ -221,9 +222,10 @@ Usage:
221
222
  if (context.reversionManager && context.messageId) {
222
223
  snapshotId = await context.reversionManager.recordSnapshot(context.messageId, resolvedPath, "modify");
223
224
  }
224
- // Write file
225
+ // Write file (atomic: temp file + rename, so concurrent readers never
226
+ // observe a truncated file). Staleness/OCC was checked above.
225
227
  try {
226
- await writeFile(resolvedPath, newContent, "utf-8");
228
+ await atomicWriteFile(resolvedPath, newContent);
227
229
  // Commit snapshot on success
228
230
  if (context.reversionManager && snapshotId) {
229
231
  await context.reversionManager.commitSnapshot(snapshotId);
@@ -244,6 +246,7 @@ Usage:
244
246
  mtime: newStats.mtime.getTime(),
245
247
  hash,
246
248
  source: "edit",
249
+ content: newContent,
247
250
  offset: undefined,
248
251
  limit: undefined,
249
252
  });
@@ -0,0 +1,2 @@
1
+ import type { ToolPlugin } from "./types.js";
2
+ export declare const execTool: ToolPlugin;
@@ -0,0 +1,165 @@
1
+ import { EXEC_TOOL_NAME } from "../constants/tools.js";
2
+ import { EXEC_RESERVED_NAMESPACE } from "../exec/constants.js";
3
+ import { buildExecPool } from "../exec/catalog.js";
4
+ import { runExecScript } from "../exec/execRuntime.js";
5
+ /**
6
+ * The `search` bullet of the sandbox API blurb. It names the entry point and stops
7
+ * there: the call form is taught by the catalog announcement, and only while the
8
+ * catalog is actually truncated. Printing it here would advertise a search on every
9
+ * turn, complete catalog or not — the tool has no idea how many tools exist.
10
+ *
11
+ * The path is derived from the reserved namespace, the same constant the sandbox
12
+ * builds its `tools` object from, so the two cannot drift.
13
+ */
14
+ const SEARCH_ENTRY = `- \`tools[${JSON.stringify(EXEC_RESERVED_NAMESPACE)}].search(...)\` — ` +
15
+ "search the whole pool from inside the script.";
16
+ /**
17
+ * Model-visible API description.
18
+ *
19
+ * Static on purpose, and not merely "no tunable limits in it": it must be identical
20
+ * for any tool pool. `tools[]` sits in the cached prefix, so a description that
21
+ * mentioned the MCP servers (or their tools) would rewrite that prefix every time a
22
+ * server connected or dropped. The catalog is a tail announcement instead — see
23
+ * `exec/catalogAnnouncement.ts`.
24
+ */
25
+ const EXEC_DESCRIPTION = `Run a JavaScript script in a sandbox where every MCP tool of this session is exposed as a function, so a whole sequence of MCP calls can be composed in a single turn instead of one model round-trip per call.
26
+
27
+ Sandbox API:
28
+ - \`await tools.<name>(args)\` — call an MCP tool, passing that tool's own arguments object directly. Resolves to the tool's output: its \`structuredContent\` object when it returned one, otherwise its text, otherwise \`null\`. The catalog gives each tool's return type.
29
+ ${SEARCH_ENTRY}
30
+ - \`console.log(...)\` — collected and returned alongside the result. Use it to inspect intermediate values.
31
+ - \`return <value>\` — the returned value is JSON-serialized and given back to you.
32
+
33
+ It is very helpful if you write a clear, concise description of what this script does in 5-10 words.
34
+
35
+ Which MCP tools are reachable is announced in the conversation as the catalog changes. The script has no filesystem, no network, no \`import\`, and no \`eval\`/\`new Function\`. It stops when it exceeds its time or tool-call budget. Every nested MCP call goes through the normal permission check, so it can still be denied — a denied call rejects with the reason.`;
36
+ /**
37
+ * How many of the most recent nested calls the result summary lists, mirroring
38
+ * how the `Agent` tool lists the subagent tools it just ran.
39
+ */
40
+ const RECENT_CALLS_SHOWN = 2;
41
+ /**
42
+ * The collapsed result row: the call count, then the most recent calls by name.
43
+ *
44
+ * Names only — MCP tools have no compact-params summary of their own, and a
45
+ * flat MCP call's collapsed row shows just the name. No "Exec" prefix either:
46
+ * the row this text sits in already prints the tool name.
47
+ *
48
+ * Derived from the calls the sandbox actually issued, so it can be rendered
49
+ * mid-run as well as at the end.
50
+ */
51
+ function formatSummary(calls) {
52
+ const count = calls.length;
53
+ const lines = [
54
+ `${count > RECENT_CALLS_SHOWN ? "... " : ""}${count} tool call${count === 1 ? "" : "s"}`,
55
+ ];
56
+ for (const name of calls.slice(-RECENT_CALLS_SHOWN)) {
57
+ lines.push(name);
58
+ }
59
+ return lines.join("\n");
60
+ }
61
+ function formatRun(result, calls) {
62
+ const lines = [];
63
+ if (result.logs.length > 0) {
64
+ lines.push(result.logs.join("\n"));
65
+ }
66
+ if (result.ok) {
67
+ if (result.value !== undefined && result.value !== "undefined") {
68
+ lines.push(result.value);
69
+ }
70
+ else if (lines.length === 0) {
71
+ lines.push("Exec finished without a return value or console output.");
72
+ }
73
+ }
74
+ else {
75
+ lines.push(result.error ?? "Exec script failed");
76
+ }
77
+ const content = lines.join("\n");
78
+ return {
79
+ success: result.ok,
80
+ content,
81
+ ...(result.ok ? {} : { error: result.error }),
82
+ shortResult: result.ok ? formatSummary(calls) : "failed",
83
+ ...(result.images.length > 0 ? { images: result.images } : {}),
84
+ };
85
+ }
86
+ export const execTool = {
87
+ name: EXEC_TOOL_NAME,
88
+ // Nested calls reach arbitrary MCP tools, which are conservatively non-safe.
89
+ isConcurrencySafe: false,
90
+ config: {
91
+ type: "function",
92
+ function: {
93
+ name: EXEC_TOOL_NAME,
94
+ description: EXEC_DESCRIPTION,
95
+ parameters: {
96
+ type: "object",
97
+ properties: {
98
+ code: {
99
+ type: "string",
100
+ description: "JavaScript to run in the sandbox. It may `await` tool calls and `return` a value.",
101
+ },
102
+ description: {
103
+ type: "string",
104
+ description: "Clear, concise description of what this script does in 5-10 words.",
105
+ },
106
+ },
107
+ required: ["code"],
108
+ },
109
+ },
110
+ },
111
+ prompt: () => EXEC_DESCRIPTION,
112
+ /**
113
+ * The parameter slot of the collapsed row, shared by the TUI and the desktop
114
+ * UI: the model's own one-line summary of the script, taken verbatim — no
115
+ * prefix (the row already prints `Exec`), no rewrite, no truncation.
116
+ *
117
+ * `Bash` can fall back to the command string; a script has no equivalent, and
118
+ * the first line of a multi-line blob usually says nothing, so an absent or
119
+ * empty description leaves the slot blank rather than inventing text. What the
120
+ * script actually did is the result slot's job (`formatSummary`).
121
+ *
122
+ * Deliberately reads nothing but the argument: no code scan, no pool, no
123
+ * catalog, so the preview cannot claim a call the script never makes.
124
+ */
125
+ formatCompactParams: (params) => {
126
+ const description = params.description;
127
+ return typeof description === "string" ? description : "";
128
+ },
129
+ execute: async (args, context) => {
130
+ const code = typeof args.code === "string" ? args.code : "";
131
+ if (code.trim().length === 0) {
132
+ return {
133
+ success: false,
134
+ content: "",
135
+ error: `${EXEC_TOOL_NAME}: missing required parameter "code"`,
136
+ };
137
+ }
138
+ const mcpManager = context.mcpManager;
139
+ if (!mcpManager) {
140
+ return {
141
+ success: false,
142
+ content: "",
143
+ error: `${EXEC_TOOL_NAME}: MCP manager is not available in this session`,
144
+ };
145
+ }
146
+ // Recomputed here rather than reused from the declaration-time catalog: a
147
+ // server may have connected or dropped since. Both are derived from the same
148
+ // `buildExecPool`, so the sandbox can never reach a tool the agent could not
149
+ // already call directly.
150
+ const pool = buildExecPool(mcpManager, context.permissionManager);
151
+ const calls = [];
152
+ const result = await runExecScript({
153
+ code,
154
+ pool,
155
+ context,
156
+ // Report each call as it is issued, so the collapsed row shows what the
157
+ // script is doing while it runs — the same live update the Agent tool does.
158
+ onToolCall: (name) => {
159
+ calls.push(name);
160
+ context.onShortResultUpdate?.(formatSummary(calls));
161
+ },
162
+ });
163
+ return formatRun(result, calls);
164
+ },
165
+ };
@@ -2,6 +2,11 @@ import { readFile } from "fs/promises";
2
2
  import { logger } from "../utils/globalLogger.js";
3
3
  import { EXIT_PLAN_MODE_TOOL_NAME } from "../constants/tools.js";
4
4
  import { OPERATION_CANCELLED_BY_USER } from "../types/permissions.js";
5
+ // Rejection feedback format aligned with Claude Code (messages.ts):
6
+ // the model reads the fixed prefix + the user's verbatim feedback, without
7
+ // any additional prompt injection.
8
+ const REJECT_MESSAGE = "The user doesn't want to proceed with this tool use. The tool use was rejected (eg. if it was a file edit, the new_string was NOT written to the file). STOP what you are doing and wait for the user to tell you how to proceed.";
9
+ const REJECT_MESSAGE_WITH_REASON_PREFIX = "The user doesn't want to proceed with this tool use. The tool use was rejected (eg. if it was a file edit, the new_string was NOT written to the file). To tell you how to proceed, the user said:\n";
5
10
  /**
6
11
  * Exit Plan Mode Tool Plugin
7
12
  */
@@ -90,10 +95,13 @@ Ensure your plan is complete and unambiguous:
90
95
  content: OPERATION_CANCELLED_BY_USER,
91
96
  };
92
97
  }
98
+ const feedback = permissionResult.message?.trim();
93
99
  return {
94
100
  success: false,
95
- content: `Please update your proposal based on the following user feedback: ${permissionResult.message || "Plan rejected by user"}`,
96
- error: permissionResult.message ? undefined : "Plan rejected by user",
101
+ content: feedback
102
+ ? `${REJECT_MESSAGE_WITH_REASON_PREFIX}${feedback}`
103
+ : REJECT_MESSAGE,
104
+ error: feedback ? undefined : "Plan rejected by user",
97
105
  };
98
106
  }
99
107
  context.permissionManager.setHasExitedPlanMode(true);
@@ -1,5 +1,6 @@
1
1
  import { spawn } from "child_process";
2
- import { rgPath } from "../utils/ripgrep.js";
2
+ import { getRgPath } from "../utils/ripgrep.js";
3
+ import { ensureRuntimeDeps } from "../utils/runtimeDeps.js";
3
4
  import { getDisplayPath } from "../utils/path.js";
4
5
  import { logger } from "../utils/globalLogger.js";
5
6
  import { GREP_TOOL_NAME, BASH_TOOL_NAME, AGENT_TOOL_NAME, } from "../constants/tools.js";
@@ -122,7 +123,12 @@ export const grepTool = {
122
123
  error: "pattern parameter is required and must be a string",
123
124
  };
124
125
  }
126
+ // Resolved per call, not at import time: the CLI installs this dependency
127
+ // itself, possibly after the module graph has been evaluated.
128
+ const rgPath = getRgPath();
125
129
  if (!rgPath) {
130
+ // Cover the entry points that do not await the installer at startup.
131
+ void ensureRuntimeDeps();
126
132
  return {
127
133
  success: false,
128
134
  content: "",
@@ -7,6 +7,8 @@ import { formatLineNumberPrefix } from "../utils/stringUtils.js";
7
7
  import { estimateTokens } from "../utils/tokenEstimate.js";
8
8
  import { isBinaryDocument, getBinaryDocumentError, } from "../utils/fileFormat.js";
9
9
  import { convertImageToBase64 } from "../utils/messageOperations.js";
10
+ import { isPathInside } from "../utils/pathSafety.js";
11
+ import { memoryFreshnessNote } from "../utils/memoryAge.js";
10
12
  import { READ_TOOL_NAME } from "../constants/tools.js";
11
13
  /**
12
14
  * Supported image file extensions
@@ -88,6 +90,15 @@ async function processImageFile(filePath, context) {
88
90
  }
89
91
  // Convert image to base64
90
92
  const imageDataUrl = convertImageToBase64(actualFilePath);
93
+ if (!imageDataUrl) {
94
+ // Unreadable, empty or unknown-format file: fail instead of handing the
95
+ // model an empty image payload.
96
+ return {
97
+ success: false,
98
+ content: "",
99
+ error: `Failed to process image: ${getDisplayPath(filePath, context.workdir)} is empty or not a readable image file`,
100
+ };
101
+ }
91
102
  const mimeType = getImageMimeType(actualFilePath);
92
103
  // Extract base64 data from data URL (remove data:image/type;base64, prefix)
93
104
  const base64Data = imageDataUrl.split(",")[1] || "";
@@ -254,8 +265,17 @@ Usage:
254
265
  source: "read",
255
266
  offset, // undefined for full reads
256
267
  limit, // undefined for full reads
268
+ // Retain the raw content only for full reads: it is the diff baseline
269
+ // for the external-change notification, and partial entries are
270
+ // excluded from that comparison anyway.
271
+ ...(offset === undefined && limit === undefined
272
+ ? { content: fileContent }
273
+ : {}),
257
274
  });
258
275
  }
276
+ // A successful read may pull in the memory files of its ancestor
277
+ // directories (see `collectNestedMemoryFiles`); only Read triggers this.
278
+ context.messageManager?.triggerNestedMemory(actualFilePath);
259
279
  // Check if file is empty
260
280
  if (fileContent.length === 0) {
261
281
  logger.warn(`File ${filePath} exists but has empty contents`);
@@ -315,8 +335,16 @@ Usage:
315
335
  },
316
336
  };
317
337
  }
318
- // Add file information header
319
- let content = `File: ${filePath}\n`;
338
+ // Add file information header. A file inside the auto-memory directory
339
+ // gets a staleness note in front of it: memory is a point-in-time
340
+ // observation, and a stale `file:line` citation reads as authoritative.
341
+ // `autoMemoryDir` is unset when auto-memory is off, so the note can never
342
+ // fire for a disabled feature.
343
+ const memoryNote = context.autoMemoryDir &&
344
+ isPathInside(actualFilePath, context.autoMemoryDir)
345
+ ? memoryFreshnessNote(stats.mtime.getTime())
346
+ : "";
347
+ let content = `${memoryNote}File: ${filePath}\n`;
320
348
  if (startLine > 1 || endLine < totalLines) {
321
349
  content += `Lines ${startLine}-${endLine} of ${totalLines}\n`;
322
350
  }
@@ -5,6 +5,32 @@ import { ChatCompletionFunctionTool } from "openai/resources.js";
5
5
  import type { PermissionMode, PermissionCallback } from "../types/permissions.js";
6
6
  import type { SubagentConfiguration } from "../utils/subagentParser.js";
7
7
  import type { SkillMetadata } from "../types/skills.js";
8
+ /**
9
+ * Read state recorded for a file by the Read/Write/Edit tools. Used for
10
+ * read-before-write enforcement, staleness detection, Read dedup, and the
11
+ * external-change notification (diffing `content` against the disk).
12
+ */
13
+ export interface ReadFileStateEntry {
14
+ /** File mtime (ms) observed when this entry was recorded. */
15
+ mtime: number;
16
+ /** sha256 of the content observed when this entry was recorded. */
17
+ hash: string;
18
+ /**
19
+ * Which tool recorded the entry. Read dedups only entries from Read;
20
+ * "changed" is recorded when an external change was observed and reported.
21
+ */
22
+ source: "read" | "edit" | "write" | "changed";
23
+ /**
24
+ * Content observed when this entry was recorded (full reads and writes
25
+ * only). Serves as the diff baseline for the external-change notification.
26
+ */
27
+ content?: string;
28
+ /** Line offset the entry was recorded at; undefined = full read. */
29
+ offset?: number;
30
+ limit?: number;
31
+ }
32
+ /** Per-session read state keyed by resolved absolute path. */
33
+ export type ReadFileState = Map<string, ReadFileStateEntry>;
8
34
  export interface ToolPlugin {
9
35
  name: string;
10
36
  config: ChatCompletionFunctionTool;
@@ -46,7 +72,7 @@ export interface ToolContext {
46
72
  abortSignal?: AbortSignal;
47
73
  backgroundTaskManager?: import("../managers/backgroundTaskManager.js").BackgroundTaskManager;
48
74
  workdir: string;
49
- /** Tool manager instance for tool discovery (used by ToolSearchTool) */
75
+ /** Tool manager instance, for tools that need to change session tool state */
50
76
  toolManager?: import("../managers/toolManager.js").ToolManager;
51
77
  /** Permission mode for this tool execution */
52
78
  permissionMode?: PermissionMode;
@@ -92,13 +118,7 @@ export interface ToolContext {
92
118
  maxTokens: number;
93
119
  };
94
120
  /** State of files read in the current session for deduplication */
95
- readFileState?: Map<string, {
96
- mtime: number;
97
- hash: string;
98
- source: "read" | "edit" | "write";
99
- offset?: number;
100
- limit?: number;
101
- }>;
121
+ readFileState?: ReadFileState;
102
122
  /** Hook manager instance for executing hooks */
103
123
  hookManager?: import("../managers/hookManager.js").HookManager;
104
124
  /** Callback to notify when the current working directory changes */
@@ -114,4 +134,10 @@ export interface ToolContext {
114
134
  * the subprocess without polluting other sessions in one stdio process.
115
135
  */
116
136
  sessionEnv?: Record<string, string>;
137
+ /**
138
+ * Auto-memory directory for this session, or undefined when auto-memory is
139
+ * off. Read uses it to prepend a staleness note to files inside it — leaving
140
+ * it unset is what keeps that note off when the feature is disabled.
141
+ */
142
+ autoMemoryDir?: string;
117
143
  }