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
@@ -9,39 +9,14 @@ export class SlashCommandManager {
9
9
  this.commands = new Map();
10
10
  this.customCommands = new Map();
11
11
  this.skillCommandIds = new Set();
12
+ /** Ids registered by plugins, so they can be dropped per-plugin and are not
13
+ * mistaken for disk-sourced custom commands. */
14
+ this.pluginCommandIds = new Set();
12
15
  this.currentCommandAbortController = null;
13
16
  this.workdir = options.workdir;
14
17
  }
15
18
  initialize() {
16
19
  this.loadCustomCommands();
17
- // Builtin /subtask: manual fork subagent (aligned with Claude Code's
18
- // /subtask — inherits the parent's full context, runs in the background,
19
- // result returns to the main conversation via a task notification).
20
- this.registerCommand({
21
- id: "subtask",
22
- name: "subtask",
23
- description: "Start a fork subagent with the current conversation context in the background",
24
- handler: async (args, signal) => {
25
- const taskDescription = args?.trim();
26
- if (!taskDescription) {
27
- this.messageManager.addErrorBlock("Usage: /subtask <task description>");
28
- return;
29
- }
30
- try {
31
- // The fork snapshots its context synchronously on entry, so the
32
- // transcript echo below never duplicates the prompt inside the fork.
33
- await this.aiManager.runForkSubagent(taskDescription, { description: taskDescription }, signal);
34
- this.messageManager.addUserMessage({
35
- content: `/subtask ${taskDescription}`,
36
- });
37
- }
38
- catch (error) {
39
- this.aiManager.setIsLoading(false);
40
- logger?.error("Failed to execute /subtask:", error);
41
- this.messageManager.addErrorBlock(`Failed to execute /subtask: ${error instanceof Error ? error.message : String(error)}`);
42
- }
43
- },
44
- });
45
20
  // Listen for skill refreshes and update skill commands
46
21
  const skillManager = this.container.get("SkillManager");
47
22
  if (skillManager) {
@@ -103,6 +78,13 @@ export class SlashCommandManager {
103
78
  this.unregisterCommand(commandId);
104
79
  }
105
80
  this.skillCommandIds.clear();
81
+ const skillSourceFor = (skill) => {
82
+ if (skill.pluginName)
83
+ return "plugin";
84
+ if (skill.type === "personal")
85
+ return "user";
86
+ return skill.type; // "project" | "builtin"
87
+ };
106
88
  for (const skill of skills) {
107
89
  if (skill.userInvocable === false) {
108
90
  continue;
@@ -113,6 +95,8 @@ export class SlashCommandManager {
113
95
  id: commandId,
114
96
  name: skill.name,
115
97
  description: `Skill: ${skill.description}`,
98
+ // UI source tag: plugin skills carry pluginName; personal → 用户
99
+ skillSource: skillSourceFor(skill),
116
100
  handler: async (args, signal) => {
117
101
  try {
118
102
  // 1. Prepare skill content immediately
@@ -252,6 +236,7 @@ export class SlashCommandManager {
252
236
  const namespacedId = `${pluginName}:${command.id}`;
253
237
  const namespacedName = `${pluginName}:${command.name}`;
254
238
  this.customCommands.set(namespacedId, command);
239
+ this.pluginCommandIds.add(namespacedId);
255
240
  // Generate description: prioritize custom description, otherwise use default description
256
241
  const description = command.description ||
257
242
  `Plugin command: ${namespacedName}${hasParameterPlaceholders(command.content) ? " (supports parameters)" : ""}`;
@@ -271,15 +256,43 @@ export class SlashCommandManager {
271
256
  }
272
257
  logger?.debug(`Registered ${commands.length} commands from plugin '${pluginName}'`);
273
258
  }
259
+ /**
260
+ * Drop every slash command contributed by a plugin, addressed by its plugin
261
+ * name. Removes the registry entry and the customCommands source entry.
262
+ * Called by the in-place plugin reload path before re-registering.
263
+ * @returns the number of commands removed
264
+ */
265
+ unregisterPluginCommands(pluginName) {
266
+ const prefix = `${pluginName}:`;
267
+ let removed = 0;
268
+ for (const commandId of Array.from(this.pluginCommandIds)) {
269
+ if (!commandId.startsWith(prefix)) {
270
+ continue;
271
+ }
272
+ this.pluginCommandIds.delete(commandId);
273
+ this.customCommands.delete(commandId);
274
+ this.unregisterCommand(commandId);
275
+ removed += 1;
276
+ }
277
+ if (removed > 0) {
278
+ logger?.debug(`Unregistered ${removed} commands from plugin '${pluginName}'`);
279
+ }
280
+ return removed;
281
+ }
274
282
  /**
275
283
  * Reload custom commands (useful for development)
276
284
  */
277
285
  reloadCustomCommands() {
278
- // Clear existing custom commands
279
- for (const commandId of this.customCommands.keys()) {
286
+ // Reload only disk-sourced custom commands. Plugin commands live in the
287
+ // same map but are owned by the plugin lifecycle, so re-reading the disk
288
+ // must not drop them.
289
+ for (const [commandId] of this.customCommands) {
290
+ if (this.pluginCommandIds.has(commandId)) {
291
+ continue;
292
+ }
280
293
  this.unregisterCommand(commandId);
294
+ this.customCommands.delete(commandId);
281
295
  }
282
- this.customCommands.clear();
283
296
  // Reload
284
297
  this.loadCustomCommands();
285
298
  }
@@ -100,6 +100,13 @@ export declare class SubagentManager {
100
100
  * for conditional subagent registration (e.g. WAVE_VISION_MODEL).
101
101
  */
102
102
  private getMergedEnv;
103
+ /**
104
+ * Delete a user-level or project-level subagent by removing its markdown
105
+ * file. Builtin and plugin agents are read-only and cannot be deleted.
106
+ * @param name - The subagent name as shown in configurations
107
+ * @returns true if the subagent was deleted
108
+ */
109
+ deleteSubagent(name: string): Promise<boolean>;
103
110
  /**
104
111
  * Get cached configurations synchronously (must call loadConfigurations first)
105
112
  */
@@ -113,6 +120,14 @@ export declare class SubagentManager {
113
120
  * Names each agent as `pluginName:agentName` to avoid collisions.
114
121
  */
115
122
  registerPluginAgents(pluginName: string, agents: SubagentConfiguration[]): void;
123
+ /**
124
+ * Drop every subagent contributed by a plugin, addressed by its plugin name.
125
+ * Running instances are deliberately left alone — an in-place reload must not
126
+ * interrupt work already in flight. Called by the plugin reload path before
127
+ * re-registering.
128
+ * @returns the number of configurations removed
129
+ */
130
+ unregisterPluginAgents(pluginName: string): number;
116
131
  /**
117
132
  * Create a new subagent instance with isolated managers
118
133
  */
@@ -107,6 +107,31 @@ export class SubagentManager {
107
107
  return (this.configurationService?.getMergedEnv?.() ??
108
108
  process.env);
109
109
  }
110
+ /**
111
+ * Delete a user-level or project-level subagent by removing its markdown
112
+ * file. Builtin and plugin agents are read-only and cannot be deleted.
113
+ * @param name - The subagent name as shown in configurations
114
+ * @returns true if the subagent was deleted
115
+ */
116
+ async deleteSubagent(name) {
117
+ const configs = this.cachedConfigurations ?? [];
118
+ const config = configs.find((c) => c.name === name);
119
+ if (!config) {
120
+ return false;
121
+ }
122
+ // Builtin and plugin agents are read-only
123
+ if (config.scope !== "user" && config.scope !== "project") {
124
+ return false;
125
+ }
126
+ const { unlink } = await import("fs/promises");
127
+ await unlink(config.filePath);
128
+ // Drop the deleted agent from the cached configurations immediately.
129
+ // A later refreshConfigurations() re-scans the filesystem and reflects
130
+ // the deletion persistently.
131
+ this.cachedConfigurations = configs.filter((c) => c.name !== name);
132
+ logger?.debug(`Deleted subagent '${name}' at ${config.filePath}`);
133
+ return true;
134
+ }
110
135
  /**
111
136
  * Get cached configurations synchronously (must call loadConfigurations first)
112
137
  */
@@ -158,6 +183,22 @@ export class SubagentManager {
158
183
  return a.name.localeCompare(b.name);
159
184
  });
160
185
  }
186
+ /**
187
+ * Drop every subagent contributed by a plugin, addressed by its plugin name.
188
+ * Running instances are deliberately left alone — an in-place reload must not
189
+ * interrupt work already in flight. Called by the plugin reload path before
190
+ * re-registering.
191
+ * @returns the number of configurations removed
192
+ */
193
+ unregisterPluginAgents(pluginName) {
194
+ if (this.cachedConfigurations === null) {
195
+ return 0;
196
+ }
197
+ const prefix = `${pluginName}:`;
198
+ const before = this.cachedConfigurations.length;
199
+ this.cachedConfigurations = this.cachedConfigurations.filter((config) => !config.name.startsWith(prefix));
200
+ return before - this.cachedConfigurations.length;
201
+ }
161
202
  /**
162
203
  * Create a new subagent instance with isolated managers
163
204
  */
@@ -207,6 +248,10 @@ export class SubagentManager {
207
248
  const subagentPermissionManager = new PermissionManager(subagentContainer, {
208
249
  configuredPermissionMode: parameters.permissionModeOverride ??
209
250
  parentPermissionManager?.getConfiguredPermissionMode(),
251
+ // Inherit the parent session's bypass authorization: the subagent is
252
+ // created mid-session, so re-deriving it from its own mode would
253
+ // (wrongly) deny an authorized session's plan-mode subagent.
254
+ bypassAuthorization: parentPermissionManager?.getBypassAuthorization?.(),
210
255
  allowedRules: parentPermissionManager?.getAllowedRules(),
211
256
  deniedRules: parentPermissionManager?.getDeniedRules(),
212
257
  instanceAllowedRules: parentPermissionManager?.getInstanceAllowedRules?.(),
@@ -339,8 +384,13 @@ export class SubagentManager {
339
384
  const result = await this.internalExecute(instance, prompt, abortSignal);
340
385
  const task = backgroundTaskManager?.getTask(taskId);
341
386
  if (task) {
342
- task.status = "completed";
343
- task.stdout = result;
387
+ // internalExecute already recorded the terminal state; only fill
388
+ // in a "completed" status when the task was not concurrently
389
+ // killed (mirrors the catch branch below).
390
+ if (task.status !== "killed") {
391
+ task.status = "completed";
392
+ task.stdout = result;
393
+ }
344
394
  task.endTime = Date.now();
345
395
  task.runtime = task.endTime - startTime;
346
396
  }
@@ -348,9 +398,13 @@ export class SubagentManager {
348
398
  catch (error) {
349
399
  const task = backgroundTaskManager?.getTask(taskId);
350
400
  if (task) {
351
- task.status = "failed";
352
- task.stderr =
353
- error instanceof Error ? error.message : String(error);
401
+ // Preserve a "killed" terminal state (internalExecute already
402
+ // notified it) instead of flipping the in-map task to failed.
403
+ if (task.status !== "killed") {
404
+ task.status = "failed";
405
+ task.stderr =
406
+ error instanceof Error ? error.message : String(error);
407
+ }
354
408
  task.endTime = Date.now();
355
409
  task.runtime = task.endTime - startTime;
356
410
  }
@@ -480,8 +534,13 @@ export class SubagentManager {
480
534
  const task = backgroundTaskManager.getTask(instance.backgroundTaskId);
481
535
  if (task) {
482
536
  const wasAlreadyKilled = task.status === "killed";
483
- task.status = "completed";
484
- task.stdout = response || "Agent completed with no text response";
537
+ // Preserve a "killed" terminal state (stopTask already notified it)
538
+ // instead of overwriting it with "completed" — mirrors shell exit
539
+ // semantics in BackgroundTaskManager.onExit.
540
+ if (!wasAlreadyKilled) {
541
+ task.status = "completed";
542
+ task.stdout = response || "Agent completed with no text response";
543
+ }
485
544
  task.endTime = Date.now();
486
545
  if (task.startTime) {
487
546
  task.runtime = task.endTime - task.startTime;
@@ -496,6 +555,11 @@ export class SubagentManager {
496
555
  messageQueue.enqueueNotification(`<task-notification>\n<task-id>${instance.backgroundTaskId}</task-id>\n<task-type>agent</task-type>\n<status>completed</status>\n<summary>${summary}</summary>\n</task-notification>`);
497
556
  }
498
557
  }
558
+ // Push a terminal snapshot: SubagentManager mutates the task object in
559
+ // place, so without an explicit notify the UI/hosts keep the last
560
+ // snapshot — the task created with status "running" — and the IDE
561
+ // background-task gate (new chat / history restore) stays locked.
562
+ backgroundTaskManager.notifyTasksChange();
499
563
  }
500
564
  }
501
565
  return response || "Agent completed with no text response";
@@ -512,12 +576,20 @@ export class SubagentManager {
512
576
  const task = backgroundTaskManager.getTask(instance.backgroundTaskId);
513
577
  if (task) {
514
578
  const wasAlreadyKilled = task.status === "killed";
515
- task.status = "failed";
516
- task.stderr = error instanceof Error ? error.message : String(error);
579
+ // Preserve a user/agent-initiated "killed" terminal state instead of
580
+ // overwriting it with "failed" (mirrors shell exit semantics) — the
581
+ // abort that stopped the task surfaces here as a thrown error.
582
+ if (!wasAlreadyKilled) {
583
+ task.status = "failed";
584
+ task.stderr =
585
+ error instanceof Error ? error.message : String(error);
586
+ }
517
587
  task.endTime = Date.now();
518
588
  if (task.startTime) {
519
589
  task.runtime = task.endTime - task.startTime;
520
590
  }
591
+ // Push a terminal snapshot (see the completion branch above).
592
+ backgroundTaskManager.notifyTasksChange();
521
593
  // Skip notification if task was already stopped (e.g. by main agent shutdown)
522
594
  if (!wasAlreadyKilled) {
523
595
  const messageQueue = this.container.has("MessageQueue")
@@ -1,4 +1,5 @@
1
1
  import type { ToolContext, ToolPlugin, ToolResult } from "../tools/types.js";
2
+ import type { RenderedCatalog } from "../exec/catalog.js";
2
3
  import { PermissionManager } from "./permissionManager.js";
3
4
  import { ChatCompletionFunctionTool } from "openai/resources.js";
4
5
  import type { PermissionMode } from "../types/index.js";
@@ -56,15 +57,21 @@ declare class ToolManager {
56
57
  /**
57
58
  * Re-evaluate feature-gated built-in tools after a live configuration
58
59
  * reload. Currently gates the Artifact tool on settings.json
59
- * `enableArtifact`. Safe to call multiple times: gated tools are removed
60
- * from the registry first, then initializeBuiltInTools() re-registers them
61
- * only if still enabled (so toggling the flag off actually unregisters).
60
+ * `enableArtifact` and the Exec tool on `enableExec`. Safe to call multiple
61
+ * times: gated tools are removed from the registry first, then
62
+ * initializeBuiltInTools() re-registers them only if still enabled (so
63
+ * toggling a flag off actually unregisters).
62
64
  */
63
65
  reloadFeatureGatedTools(): void;
64
66
  /**
65
67
  * Check if a tool should be enabled based on tools configuration and permission rules
66
68
  */
67
69
  private shouldEnableTool;
70
+ /**
71
+ * The auto-memory directory for this session, or undefined when the feature
72
+ * is off. Consumers (Read) treat "unset" as "not a memory file".
73
+ */
74
+ private resolveAutoMemoryDir;
68
75
  /**
69
76
  * Execute a tool by name with the provided arguments and context
70
77
  *
@@ -80,6 +87,25 @@ declare class ToolManager {
80
87
  */
81
88
  execute(name: string, args: Record<string, unknown>, context: ToolContext): Promise<ToolResult>;
82
89
  list(): ToolPlugin[];
90
+ /**
91
+ * Whether `Exec` is declared at all: registered and not denied. Exactly when the
92
+ * catalog channel is open — an undeclared `Exec` cannot be called, so a catalog
93
+ * for it would advertise a way in that does not exist.
94
+ */
95
+ private isExecDeclared;
96
+ /** The tools the sandbox may reach — the pool the flat declarations give up. */
97
+ private execPool;
98
+ /**
99
+ * The MCP catalog as the model is meant to see it, or `undefined` when the
100
+ * catalog channel is closed.
101
+ *
102
+ * A function of the pool alone, and computed per call: with the catalog out of
103
+ * `tools[]` the declaration no longer moves when a server comes or goes, and the
104
+ * announcement channel diffs this against the history. Caching the rendering would
105
+ * mean caching pool state, which is exactly the process-side bookkeeping the
106
+ * channel is built to avoid.
107
+ */
108
+ getExecCatalog(): RenderedCatalog | undefined;
83
109
  getToolsConfig(options?: {
84
110
  availableSubagents?: SubagentConfiguration[];
85
111
  availableSkills?: SkillMetadata[];
@@ -10,7 +10,12 @@ import { cronDeleteTool } from "../tools/cronDeleteTool.js";
10
10
  import { cronListTool } from "../tools/cronListTool.js";
11
11
  import { webFetchTool } from "../tools/webFetchTool.js";
12
12
  import { artifactTool } from "../tools/artifactTool.js";
13
+ import { execTool } from "../tools/execTool.js";
13
14
  import { isArtifactEnabled } from "../services/artifactAvailability.js";
15
+ import { isExecEnabled } from "../services/execAvailability.js";
16
+ import { buildExecPool, renderCatalog } from "../exec/catalog.js";
17
+ import { EXEC_DEFAULT_CATALOG_TOKENS } from "../exec/constants.js";
18
+ import { EXEC_TOOL_NAME } from "../constants/tools.js";
14
19
  // New tools
15
20
  import { globTool } from "../tools/globTool.js";
16
21
  import { grepTool } from "../tools/grepTool.js";
@@ -102,6 +107,13 @@ class ToolManager {
102
107
  if (isArtifactEnabled(this.container.get("Workdir"))) {
103
108
  builtInTools.push(artifactTool);
104
109
  }
110
+ // Exec is on by default; enableExec: false restores flat MCP declarations.
111
+ // Registration is decoupled from declaration: getToolsConfig() only declares
112
+ // it (and only then collapses the MCP pool) once MCP servers have connected
113
+ // and yielded a non-empty pool, which cannot be known until then.
114
+ if (isExecEnabled(this.container.get("Workdir"))) {
115
+ builtInTools.push(execTool);
116
+ }
105
117
  for (const tool of builtInTools) {
106
118
  if (this.shouldEnableTool(tool.name)) {
107
119
  this.toolsRegistry.set(tool.name, tool);
@@ -117,12 +129,14 @@ class ToolManager {
117
129
  /**
118
130
  * Re-evaluate feature-gated built-in tools after a live configuration
119
131
  * reload. Currently gates the Artifact tool on settings.json
120
- * `enableArtifact`. Safe to call multiple times: gated tools are removed
121
- * from the registry first, then initializeBuiltInTools() re-registers them
122
- * only if still enabled (so toggling the flag off actually unregisters).
132
+ * `enableArtifact` and the Exec tool on `enableExec`. Safe to call multiple
133
+ * times: gated tools are removed from the registry first, then
134
+ * initializeBuiltInTools() re-registers them only if still enabled (so
135
+ * toggling a flag off actually unregisters).
123
136
  */
124
137
  reloadFeatureGatedTools() {
125
138
  this.toolsRegistry.delete(artifactTool.name);
139
+ this.toolsRegistry.delete(execTool.name);
126
140
  this.initializeBuiltInTools();
127
141
  }
128
142
  /**
@@ -139,6 +153,22 @@ class ToolManager {
139
153
  }
140
154
  return this.tools.some((toolName) => toolName.toLowerCase() === name.toLowerCase());
141
155
  }
156
+ /**
157
+ * The auto-memory directory for this session, or undefined when the feature
158
+ * is off. Consumers (Read) treat "unset" as "not a memory file".
159
+ */
160
+ resolveAutoMemoryDir(workdir) {
161
+ if (!this.container.has("MemoryService"))
162
+ return undefined;
163
+ const configuration = this.container.has("ConfigurationService")
164
+ ? this.container.get("ConfigurationService")
165
+ : undefined;
166
+ if (!configuration?.resolveAutoMemoryEnabled?.())
167
+ return undefined;
168
+ return this.container
169
+ .get("MemoryService")
170
+ ?.getAutoMemoryDirectory?.(workdir);
171
+ }
142
172
  /**
143
173
  * Execute a tool by name with the provided arguments and context
144
174
  *
@@ -202,6 +232,7 @@ class ToolManager {
202
232
  .get("ConfigurationService")
203
233
  ?.getMergedEnv?.()
204
234
  : undefined,
235
+ autoMemoryDir: this.resolveAutoMemoryDir(context.workdir),
205
236
  sessionId: context.sessionId,
206
237
  toolCallId: context.toolCallId,
207
238
  };
@@ -287,10 +318,53 @@ class ToolManager {
287
318
  .filter((tool) => !permissionManager?.isToolDenied(tool.name));
288
319
  return [...builtInTools, ...mcpTools];
289
320
  }
321
+ /**
322
+ * Whether `Exec` is declared at all: registered and not denied. Exactly when the
323
+ * catalog channel is open — an undeclared `Exec` cannot be called, so a catalog
324
+ * for it would advertise a way in that does not exist.
325
+ */
326
+ isExecDeclared() {
327
+ return (this.toolsRegistry.has(EXEC_TOOL_NAME) &&
328
+ !this.getPermissionManager()?.isToolDenied(EXEC_TOOL_NAME));
329
+ }
330
+ /** The tools the sandbox may reach — the pool the flat declarations give up. */
331
+ execPool() {
332
+ return buildExecPool(this.mcpManager, this.getPermissionManager());
333
+ }
334
+ /**
335
+ * The MCP catalog as the model is meant to see it, or `undefined` when the
336
+ * catalog channel is closed.
337
+ *
338
+ * A function of the pool alone, and computed per call: with the catalog out of
339
+ * `tools[]` the declaration no longer moves when a server comes or goes, and the
340
+ * announcement channel diffs this against the history. Caching the rendering would
341
+ * mean caching pool state, which is exactly the process-side bookkeeping the
342
+ * channel is built to avoid.
343
+ */
344
+ getExecCatalog() {
345
+ if (!this.isExecDeclared())
346
+ return undefined;
347
+ return renderCatalog(this.execPool(), EXEC_DEFAULT_CATALOG_TOKENS);
348
+ }
290
349
  getToolsConfig(options) {
291
- const permissionManager = this.container.get("PermissionManager");
350
+ const permissionManager = this.getPermissionManager();
351
+ // Exec either replaces the flat MCP declarations or is absent: the two must
352
+ // never coexist, or the model would see the same tool twice while the
353
+ // catalog claimed to be the only way in. Both halves are derived from the
354
+ // same pool, and the pool is exactly what the agent could already call
355
+ // directly, so collapsing it cannot widen access.
356
+ const execPool = this.execPool();
357
+ // No minimum pool size: any catalogable tool collapses the pool, matching
358
+ // opencode. The switch (`enableExec`), not a count, decides whether Exec is
359
+ // used at all.
360
+ const collapseMcp = this.isExecDeclared() && execPool.length > 0;
292
361
  const builtInToolsConfig = Array.from(this.toolsRegistry.values())
293
362
  .filter((tool) => {
363
+ // With an empty pool there is nothing to collapse, so Exec stays
364
+ // registered but undeclared rather than declaring an empty catalog.
365
+ if (tool.name === EXEC_TOOL_NAME && !collapseMcp) {
366
+ return false;
367
+ }
294
368
  // If tool is explicitly denied by name in permission rules, filter it out
295
369
  if (permissionManager?.isToolDenied(tool.name)) {
296
370
  return false;
@@ -307,18 +381,18 @@ class ToolManager {
307
381
  };
308
382
  // Override description with prompt if available
309
383
  if (tool.prompt) {
310
- config.function.description = tool.prompt(options);
384
+ config.function.description = tool.prompt({ ...options });
311
385
  }
312
386
  return config;
313
387
  });
314
- const mcpToolsConfig = this.mcpManager
315
- .getMcpToolsConfig()
316
- .filter((tool) => {
317
- if (permissionManager?.isToolDenied(tool.function.name)) {
318
- return false;
319
- }
320
- return true;
321
- });
388
+ const mcpToolsConfig = collapseMcp
389
+ ? []
390
+ : this.mcpManager.getMcpToolsConfig().filter((tool) => {
391
+ if (permissionManager?.isToolDenied(tool.function.name)) {
392
+ return false;
393
+ }
394
+ return true;
395
+ });
322
396
  return [...builtInToolsConfig, ...mcpToolsConfig];
323
397
  }
324
398
  /**
@@ -207,6 +207,10 @@ export class WorkflowManager {
207
207
  task.status = "completed";
208
208
  task.endTime = Date.now();
209
209
  }
210
+ // Push the terminal snapshot: without it the last snapshot holds the
211
+ // "running" registration and UI consumers (background-task gate) never
212
+ // see the workflow finish.
213
+ this.backgroundTaskManager.notifyTasksChange();
210
214
  // Enqueue completion notification
211
215
  const journalPath = journal.filePath;
212
216
  this.messageQueue.enqueueNotification(taskNotificationToXml({
@@ -244,6 +248,8 @@ export class WorkflowManager {
244
248
  task.stderr = run.error || "";
245
249
  task.endTime = Date.now();
246
250
  }
251
+ // Push the terminal snapshot (see the completion branch above).
252
+ this.backgroundTaskManager.notifyTasksChange();
247
253
  this.messageQueue.enqueueNotification(taskNotificationToXml({
248
254
  type: "task_notification",
249
255
  taskId: runId,
@@ -1 +1,10 @@
1
+ /**
2
+ * Build the main agent's auto-memory prompt. The section order mirrors Claude
3
+ * Code's `buildMemoryLines()` (individual-only variant). The index content of
4
+ * `MEMORY.md` is appended separately by the system prompt builder, so this
5
+ * function only produces the behavioral instructions.
6
+ *
7
+ * Every section except the directory line is shared with the extraction fork
8
+ * prompt (`autoMemoryExtraction.ts`) — see `memoryTypes.ts` for why.
9
+ */
1
10
  export declare function buildAutoMemoryPrompt(memoryDir: string): string;
@@ -1,33 +1,32 @@
1
+ import { DIR_EXISTS_GUIDANCE, MEMORY_AND_OTHER_PERSISTENCE_SECTION, TYPES_SECTION, TRUSTING_RECALL_SECTION, WHAT_NOT_TO_SAVE_SECTION, WHEN_TO_ACCESS_SECTION, buildHowToSaveSection, } from "./memoryTypes.js";
2
+ /**
3
+ * Build the main agent's auto-memory prompt. The section order mirrors Claude
4
+ * Code's `buildMemoryLines()` (individual-only variant). The index content of
5
+ * `MEMORY.md` is appended separately by the system prompt builder, so this
6
+ * function only produces the behavioral instructions.
7
+ *
8
+ * Every section except the directory line is shared with the extraction fork
9
+ * prompt (`autoMemoryExtraction.ts`) — see `memoryTypes.ts` for why.
10
+ */
1
11
  export function buildAutoMemoryPrompt(memoryDir) {
2
- return `
3
- # auto memory
4
-
5
- You have a persistent auto memory directory at \`${memoryDir}\`. Its contents persist across conversations.
6
-
7
- As you work, consult your memory files to build on previous experience.
8
-
9
- ## How to save memories:
10
- - Organize memory semantically by topic, not chronologically
11
- - Use the Write and Edit tools to update your memory files
12
- - \`MEMORY.md\` is always loaded into your conversation context — lines after 200 will be truncated, so keep it concise
13
- - Create separate topic files (e.g., \`debugging.md\`, \`patterns.md\`) for detailed notes and link to them from MEMORY.md
14
- - Update or remove memories that turn out to be wrong or outdated
15
- - Do not write duplicate memories. First check if there is an existing memory you can update before writing a new one.
16
-
17
- ## What to save:
18
- - Stable patterns and conventions confirmed across multiple interactions
19
- - Key architectural decisions, important file paths, and project structure
20
- - User preferences for workflow, tools, and communication style
21
- - Solutions to recurring problems and debugging insights
22
-
23
- ## What NOT to save:
24
- - Session-specific context (current task details, in-progress work, temporary state)
25
- - Information that might be incomplete — verify against project docs before writing
26
- - Anything that duplicates or contradicts existing AGENTS.md instructions
27
- - Speculative or unverified conclusions from reading a single file
28
-
29
- ## Explicit user requests:
30
- - When the user asks you to remember something across sessions (e.g., "always use bun", "never auto-commit"), save it — no need to wait for multiple interactions
31
- - When the user asks to forget or stop remembering something, find and remove the relevant entries from your memory files
32
- `;
12
+ return [
13
+ "# auto memory",
14
+ "",
15
+ `You have a persistent, file-based memory system at \`${memoryDir}\`. ${DIR_EXISTS_GUIDANCE}`,
16
+ "",
17
+ "You should build up this memory system over time so that future conversations can have a complete picture of who the user is, how they'd like to collaborate with you, what behaviors to avoid or repeat, and the context behind the work the user gives you.",
18
+ "",
19
+ "If the user explicitly asks you to remember something, save it immediately as whichever type fits best. If they ask you to forget something, find and remove the relevant entry.",
20
+ "",
21
+ ...TYPES_SECTION,
22
+ ...WHAT_NOT_TO_SAVE_SECTION,
23
+ "",
24
+ ...buildHowToSaveSection(),
25
+ "",
26
+ ...WHEN_TO_ACCESS_SECTION,
27
+ "",
28
+ ...TRUSTING_RECALL_SECTION,
29
+ "",
30
+ ...MEMORY_AND_OTHER_PERSISTENCE_SECTION,
31
+ ].join("\n");
33
32
  }
@@ -1,4 +1,8 @@
1
1
  /**
2
2
  * Build the auto-memory extraction prompt for the background agent.
3
+ *
4
+ * The taxonomy, the "what not to save" list, and the save procedure are shared
5
+ * with the main agent's memory prompt (`memoryTypes.ts`): the fork reuses the
6
+ * main system prompt, so both texts are in the same request.
3
7
  */
4
8
  export declare function buildAutoMemoryExtractionPrompt(newMessageCount: number, existingMemoriesManifest: string): string;