wave-agent-sdk 0.5.1 → 0.6.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 (148) hide show
  1. package/dist/agent.d.ts +7 -2
  2. package/dist/agent.d.ts.map +1 -1
  3. package/dist/agent.js +58 -69
  4. package/dist/constants/prompts.d.ts +18 -14
  5. package/dist/constants/prompts.d.ts.map +1 -1
  6. package/dist/constants/prompts.js +134 -54
  7. package/dist/constants/tools.d.ts +4 -1
  8. package/dist/constants/tools.d.ts.map +1 -1
  9. package/dist/constants/tools.js +4 -1
  10. package/dist/index.d.ts +1 -0
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +1 -0
  13. package/dist/managers/aiManager.d.ts +2 -0
  14. package/dist/managers/aiManager.d.ts.map +1 -1
  15. package/dist/managers/aiManager.js +54 -16
  16. package/dist/managers/backgroundTaskManager.d.ts.map +1 -1
  17. package/dist/managers/backgroundTaskManager.js +59 -53
  18. package/dist/managers/foregroundTaskManager.d.ts.map +1 -1
  19. package/dist/managers/foregroundTaskManager.js +3 -2
  20. package/dist/managers/mcpManager.d.ts.map +1 -1
  21. package/dist/managers/messageManager.d.ts +7 -3
  22. package/dist/managers/messageManager.d.ts.map +1 -1
  23. package/dist/managers/messageManager.js +28 -24
  24. package/dist/managers/permissionManager.d.ts.map +1 -1
  25. package/dist/managers/permissionManager.js +25 -15
  26. package/dist/managers/planManager.d.ts +1 -1
  27. package/dist/managers/planManager.d.ts.map +1 -1
  28. package/dist/managers/planManager.js +2 -2
  29. package/dist/managers/subagentManager.d.ts +4 -0
  30. package/dist/managers/subagentManager.d.ts.map +1 -1
  31. package/dist/managers/subagentManager.js +18 -2
  32. package/dist/managers/toolManager.d.ts +11 -0
  33. package/dist/managers/toolManager.d.ts.map +1 -1
  34. package/dist/managers/toolManager.js +20 -2
  35. package/dist/services/aiService.d.ts +0 -1
  36. package/dist/services/aiService.d.ts.map +1 -1
  37. package/dist/services/aiService.js +4 -140
  38. package/dist/services/memory.d.ts +0 -3
  39. package/dist/services/memory.d.ts.map +1 -1
  40. package/dist/services/memory.js +0 -59
  41. package/dist/services/session.d.ts +3 -1
  42. package/dist/services/session.d.ts.map +1 -1
  43. package/dist/services/session.js +16 -1
  44. package/dist/services/taskManager.d.ts +21 -0
  45. package/dist/services/taskManager.d.ts.map +1 -0
  46. package/dist/services/taskManager.js +158 -0
  47. package/dist/tools/askUserQuestion.d.ts.map +1 -1
  48. package/dist/tools/askUserQuestion.js +39 -25
  49. package/dist/tools/bashTool.d.ts.map +1 -1
  50. package/dist/tools/bashTool.js +5 -1
  51. package/dist/tools/editTool.d.ts.map +1 -1
  52. package/dist/tools/editTool.js +2 -1
  53. package/dist/tools/exitPlanMode.d.ts.map +1 -1
  54. package/dist/tools/exitPlanMode.js +25 -1
  55. package/dist/tools/globTool.d.ts.map +1 -1
  56. package/dist/tools/globTool.js +8 -2
  57. package/dist/tools/grepTool.d.ts.map +1 -1
  58. package/dist/tools/grepTool.js +17 -6
  59. package/dist/tools/lsTool.d.ts.map +1 -1
  60. package/dist/tools/lsTool.js +3 -1
  61. package/dist/tools/readTool.d.ts.map +1 -1
  62. package/dist/tools/readTool.js +16 -1
  63. package/dist/tools/taskManagementTools.d.ts +6 -0
  64. package/dist/tools/taskManagementTools.d.ts.map +1 -0
  65. package/dist/tools/taskManagementTools.js +453 -0
  66. package/dist/tools/taskOutputTool.d.ts.map +1 -1
  67. package/dist/tools/taskOutputTool.js +32 -8
  68. package/dist/tools/taskStopTool.d.ts.map +1 -1
  69. package/dist/tools/taskStopTool.js +7 -1
  70. package/dist/tools/taskTool.d.ts.map +1 -1
  71. package/dist/tools/taskTool.js +6 -1
  72. package/dist/tools/types.d.ts +9 -0
  73. package/dist/tools/types.d.ts.map +1 -1
  74. package/dist/tools/writeTool.d.ts.map +1 -1
  75. package/dist/tools/writeTool.js +9 -1
  76. package/dist/types/index.d.ts +1 -0
  77. package/dist/types/index.d.ts.map +1 -1
  78. package/dist/types/index.js +1 -0
  79. package/dist/types/messaging.d.ts +2 -8
  80. package/dist/types/messaging.d.ts.map +1 -1
  81. package/dist/types/processes.d.ts +11 -6
  82. package/dist/types/processes.d.ts.map +1 -1
  83. package/dist/types/tasks.d.ts +13 -0
  84. package/dist/types/tasks.d.ts.map +1 -0
  85. package/dist/types/tasks.js +1 -0
  86. package/dist/types/tools.d.ts +4 -1
  87. package/dist/types/tools.d.ts.map +1 -1
  88. package/dist/utils/builtinSubagents.d.ts.map +1 -1
  89. package/dist/utils/builtinSubagents.js +38 -1
  90. package/dist/utils/cacheControlUtils.d.ts.map +1 -1
  91. package/dist/utils/cacheControlUtils.js +18 -12
  92. package/dist/utils/constants.d.ts +0 -4
  93. package/dist/utils/constants.d.ts.map +1 -1
  94. package/dist/utils/constants.js +0 -4
  95. package/dist/utils/convertMessagesForAPI.js +2 -2
  96. package/dist/utils/messageOperations.d.ts +2 -30
  97. package/dist/utils/messageOperations.d.ts.map +1 -1
  98. package/dist/utils/messageOperations.js +4 -79
  99. package/dist/utils/nameGenerator.d.ts +1 -1
  100. package/dist/utils/nameGenerator.d.ts.map +1 -1
  101. package/dist/utils/nameGenerator.js +19 -3
  102. package/package.json +1 -1
  103. package/src/agent.ts +79 -78
  104. package/src/constants/prompts.ts +161 -65
  105. package/src/constants/tools.ts +4 -1
  106. package/src/index.ts +1 -0
  107. package/src/managers/aiManager.ts +71 -26
  108. package/src/managers/backgroundTaskManager.ts +53 -54
  109. package/src/managers/foregroundTaskManager.ts +3 -2
  110. package/src/managers/mcpManager.ts +6 -3
  111. package/src/managers/messageManager.ts +37 -26
  112. package/src/managers/permissionManager.ts +32 -21
  113. package/src/managers/planManager.ts +2 -2
  114. package/src/managers/subagentManager.ts +29 -2
  115. package/src/managers/toolManager.ts +32 -2
  116. package/src/services/aiService.ts +3 -145
  117. package/src/services/memory.ts +0 -72
  118. package/src/services/session.ts +21 -0
  119. package/src/services/taskManager.ts +188 -0
  120. package/src/tools/askUserQuestion.ts +51 -29
  121. package/src/tools/bashTool.ts +5 -1
  122. package/src/tools/editTool.ts +3 -1
  123. package/src/tools/exitPlanMode.ts +26 -2
  124. package/src/tools/globTool.ts +10 -2
  125. package/src/tools/grepTool.ts +17 -6
  126. package/src/tools/lsTool.ts +3 -1
  127. package/src/tools/readTool.ts +17 -1
  128. package/src/tools/taskManagementTools.ts +498 -0
  129. package/src/tools/taskOutputTool.ts +34 -12
  130. package/src/tools/taskStopTool.ts +7 -1
  131. package/src/tools/taskTool.ts +7 -1
  132. package/src/tools/types.ts +10 -0
  133. package/src/tools/writeTool.ts +9 -2
  134. package/src/types/index.ts +1 -0
  135. package/src/types/messaging.ts +1 -9
  136. package/src/types/processes.ts +13 -7
  137. package/src/types/tasks.ts +13 -0
  138. package/src/types/tools.ts +4 -1
  139. package/src/utils/builtinSubagents.ts +47 -1
  140. package/src/utils/cacheControlUtils.ts +26 -18
  141. package/src/utils/constants.ts +0 -5
  142. package/src/utils/convertMessagesForAPI.ts +2 -2
  143. package/src/utils/messageOperations.ts +5 -116
  144. package/src/utils/nameGenerator.ts +20 -3
  145. package/dist/tools/todoWriteTool.d.ts +0 -6
  146. package/dist/tools/todoWriteTool.d.ts.map +0 -1
  147. package/dist/tools/todoWriteTool.js +0 -220
  148. package/src/tools/todoWriteTool.ts +0 -257
@@ -1,11 +1,13 @@
1
+ import * as os from "node:os";
2
+ import * as path from "node:path";
1
3
  import {
2
4
  callAgent,
3
5
  compressMessages,
4
6
  type CallAgentOptions,
5
7
  } from "../services/aiService.js";
6
- import { getMessagesToCompress } from "../utils/messageOperations.js";
7
8
  import { convertMessagesForAPI } from "../utils/convertMessagesForAPI.js";
8
9
  import { calculateComprehensiveTotalTokens } from "../utils/tokenCalculation.js";
10
+ import * as fsSync from "node:fs";
9
11
  import * as fs from "node:fs/promises";
10
12
  import type {
11
13
  Logger,
@@ -21,10 +23,24 @@ import { ChatCompletionMessageFunctionToolCall } from "openai/resources.js";
21
23
  import type { HookManager } from "./hookManager.js";
22
24
  import type { ExtendedHookExecutionContext } from "../types/hooks.js";
23
25
  import type { PermissionManager } from "./permissionManager.js";
24
- import {
25
- DEFAULT_SYSTEM_PROMPT,
26
- buildSystemPrompt,
27
- } from "../constants/prompts.js";
26
+ import { buildSystemPrompt } from "../constants/prompts.js";
27
+
28
+ function isGitRepository(dirPath: string): string {
29
+ try {
30
+ // Check if .git directory exists in current directory or any parent directory
31
+ let currentPath = path.resolve(dirPath);
32
+ while (currentPath !== path.dirname(currentPath)) {
33
+ const gitPath = path.join(currentPath, ".git");
34
+ if (fsSync.existsSync(gitPath)) {
35
+ return "Yes";
36
+ }
37
+ currentPath = path.dirname(currentPath);
38
+ }
39
+ return "No";
40
+ } catch {
41
+ return "No";
42
+ }
43
+ }
28
44
 
29
45
  export interface AIManagerCallbacks {
30
46
  onCompressionStateChange?: (isCompressing: boolean) => void;
@@ -34,6 +50,7 @@ export interface AIManagerCallbacks {
34
50
  export interface AIManagerOptions {
35
51
  messageManager: MessageManager;
36
52
  toolManager: ToolManager;
53
+ taskManager: import("../services/taskManager.js").TaskManager;
37
54
  logger?: Logger;
38
55
  backgroundTaskManager?: BackgroundTaskManager;
39
56
  hookManager?: HookManager;
@@ -60,6 +77,7 @@ export class AIManager {
60
77
  private logger?: Logger;
61
78
  private toolManager: ToolManager;
62
79
  private messageManager: MessageManager;
80
+ private taskManager: import("../services/taskManager.js").TaskManager;
63
81
  private backgroundTaskManager?: BackgroundTaskManager;
64
82
  private hookManager?: HookManager;
65
83
  private reversionManager?: import("./reversionManager.js").ReversionManager;
@@ -79,6 +97,7 @@ export class AIManager {
79
97
  constructor(options: AIManagerOptions) {
80
98
  this.messageManager = options.messageManager;
81
99
  this.toolManager = options.toolManager;
100
+ this.taskManager = options.taskManager;
82
101
  this.backgroundTaskManager = options.backgroundTaskManager;
83
102
  this.hookManager = options.hookManager;
84
103
  this.reversionManager = options.reversionManager;
@@ -171,6 +190,7 @@ export class AIManager {
171
190
  if (toolPlugin?.formatCompactParams) {
172
191
  const context: ToolContext = {
173
192
  workdir: this.workdir,
193
+ taskManager: this.taskManager,
174
194
  };
175
195
  return toolPlugin.formatCompactParams(toolArgs, context);
176
196
  }
@@ -204,9 +224,7 @@ export class AIManager {
204
224
  );
205
225
 
206
226
  // Check if messages need compression
207
- const { messagesToCompress, insertIndex } = getMessagesToCompress(
208
- this.messageManager.getMessages(),
209
- );
227
+ const messagesToCompress = this.messageManager.getMessages();
210
228
 
211
229
  // If there are messages to compress, perform compression
212
230
  if (messagesToCompress.length > 0) {
@@ -239,7 +257,6 @@ export class AIManager {
239
257
 
240
258
  // Execute message reconstruction and sessionId update after compression
241
259
  this.messageManager.compressMessagesAndUpdateSession(
242
- insertIndex,
243
260
  compressionResult.content,
244
261
  compressionUsage,
245
262
  );
@@ -344,17 +361,14 @@ export class AIManager {
344
361
  this.getModelConfig().permissionMode,
345
362
  );
346
363
  const toolsConfig = this.getFilteredToolsConfig(tools);
347
- let effectiveSystemPrompt = buildSystemPrompt(
348
- this.systemPrompt || DEFAULT_SYSTEM_PROMPT,
349
- toolsConfig,
350
- );
364
+ const toolNames = new Set(toolsConfig.map((t) => t.function.name));
365
+ const filteredToolPlugins = this.toolManager
366
+ .getTools()
367
+ .filter((t) => toolNames.has(t.name));
351
368
 
352
- // Inject language prompt if configured
353
- const language = this.getLanguage();
354
- if (language) {
355
- const languagePrompt = `\n\n# Language\nAlways respond in ${language}. Technical terms (e.g., code, tool names, file paths) should remain in their original language or English where appropriate.`;
356
- effectiveSystemPrompt = (effectiveSystemPrompt || "") + languagePrompt;
357
- }
369
+ let planModeOptions:
370
+ | { planFilePath: string; planExists: boolean }
371
+ | undefined;
358
372
 
359
373
  if (currentMode === "plan") {
360
374
  const planFilePath = this.permissionManager?.getPlanFilePath();
@@ -366,10 +380,7 @@ export class AIManager {
366
380
  } catch {
367
381
  planExists = false;
368
382
  }
369
-
370
- const reminder = `\n\nPlan mode is active. The user indicated that they do not want you to execute yet -- you MUST NOT make any edits (with the exception of the plan file mentioned below), run any non-readonly tools (including changing configs or making commits), or otherwise make any changes to the system. This supercedes any other instructions you have received.\n\n## Plan File Info:\n${planExists ? `A plan file already exists at ${planFilePath}. You can read it and make incremental edits using the Edit tool if you need to.` : `No plan file exists yet. You should create your plan at ${planFilePath} using the Write tool if you need to.`}\nYou should build your plan incrementally by writing to or editing this file. NOTE that this is the only file you are allowed to edit - other than this you are only allowed to take READ-ONLY actions. You may also use the AskUserQuestion tool to gather requirements or clarify intent before finalizing your plan.`;
371
-
372
- effectiveSystemPrompt = (effectiveSystemPrompt || "") + reminder;
383
+ planModeOptions = { planFilePath, planExists };
373
384
  }
374
385
  }
375
386
 
@@ -380,11 +391,23 @@ export class AIManager {
380
391
  messages: recentMessages,
381
392
  sessionId: this.messageManager.getSessionId(),
382
393
  abortSignal: abortController.signal,
383
- memory: combinedMemory, // Pass combined memory content
384
394
  workdir: this.workdir, // Pass working directory
385
395
  tools: toolsConfig, // Pass filtered tool configuration
386
396
  model: model, // Use passed model
387
- systemPrompt: effectiveSystemPrompt, // Pass custom system prompt
397
+ systemPrompt: buildSystemPrompt(
398
+ this.systemPrompt,
399
+ filteredToolPlugins,
400
+ {
401
+ workdir: this.workdir,
402
+ isGitRepo: isGitRepository(this.workdir),
403
+ platform: os.platform(),
404
+ osVersion: `${os.type()} ${os.release()}`,
405
+ today: new Date().toISOString().split("T")[0],
406
+ memory: combinedMemory,
407
+ language: this.getLanguage(),
408
+ planMode: planModeOptions,
409
+ },
410
+ ), // Pass custom system prompt
388
411
  maxTokens: maxTokens, // Pass max tokens override
389
412
  };
390
413
 
@@ -618,6 +641,8 @@ export class AIManager {
618
641
  backgroundTaskManager: this.backgroundTaskManager,
619
642
  workdir: this.workdir,
620
643
  messageId: this.messageManager.getMessages().slice(-1)[0]?.id,
644
+ sessionId: this.messageManager.getSessionId(),
645
+ taskManager: this.taskManager,
621
646
  };
622
647
 
623
648
  // Execute tool
@@ -639,6 +664,7 @@ export class AIManager {
639
664
  stage: "end",
640
665
  name: toolName,
641
666
  shortResult: toolResult.shortResult,
667
+ isManuallyBackgrounded: toolResult.isManuallyBackgrounded,
642
668
  });
643
669
 
644
670
  // Execute PostToolUse hooks after successful tool completion
@@ -663,6 +689,7 @@ export class AIManager {
663
689
  stage: "end",
664
690
  name: toolName,
665
691
  compactParams,
692
+ isManuallyBackgrounded: false,
666
693
  });
667
694
  }
668
695
  },
@@ -694,7 +721,25 @@ export class AIManager {
694
721
  const isCurrentlyAborted =
695
722
  abortController.signal.aborted || toolAbortController.signal.aborted;
696
723
 
697
- if (!isCurrentlyAborted) {
724
+ // Check if all tools were manually backgrounded
725
+ const lastMessage =
726
+ this.messageManager.getMessages()[
727
+ this.messageManager.getMessages().length - 1
728
+ ];
729
+ const toolBlocks =
730
+ lastMessage?.blocks.filter(
731
+ (block): block is import("../types/messaging.js").ToolBlock =>
732
+ block.type === "tool",
733
+ ) || [];
734
+ const hasBackgrounded =
735
+ toolBlocks.length > 0 &&
736
+ toolBlocks.some((block) => block.isManuallyBackgrounded);
737
+
738
+ if (hasBackgrounded) {
739
+ this.logger?.info(
740
+ "Some tools were manually backgrounded, stopping recursion.",
741
+ );
742
+ } else if (!isCurrentlyAborted) {
698
743
  // Recursively call AI service, increment recursion depth, and pass same configuration
699
744
  await this.sendAIMessage({
700
745
  recursionDepth: recursionDepth + 1,
@@ -69,6 +69,26 @@ export class BackgroundTaskManager {
69
69
  status: "running",
70
70
  stdout: "",
71
71
  stderr: "",
72
+ onStop: () => {
73
+ try {
74
+ if (child.pid) {
75
+ process.kill(-child.pid, "SIGTERM");
76
+ setTimeout(() => {
77
+ if (child.pid && !child.killed) {
78
+ try {
79
+ process.kill(-child.pid, "SIGKILL");
80
+ } catch (error) {
81
+ logger.error("Failed to force kill process:", error);
82
+ }
83
+ }
84
+ }, 1000);
85
+ } else {
86
+ child.kill("SIGTERM");
87
+ }
88
+ } catch {
89
+ child.kill("SIGTERM");
90
+ }
91
+ },
72
92
  };
73
93
 
74
94
  this.tasks.set(id, shell);
@@ -155,6 +175,26 @@ export class BackgroundTaskManager {
155
175
  status: "running",
156
176
  stdout: initialStdout,
157
177
  stderr: initialStderr,
178
+ onStop: () => {
179
+ try {
180
+ if (child.pid) {
181
+ process.kill(-child.pid, "SIGTERM");
182
+ setTimeout(() => {
183
+ if (child.pid && !child.killed) {
184
+ try {
185
+ process.kill(-child.pid, "SIGKILL");
186
+ } catch (error) {
187
+ logger.error("Failed to force kill process:", error);
188
+ }
189
+ }
190
+ }, 1000);
191
+ } else {
192
+ child.kill("SIGTERM");
193
+ }
194
+ } catch {
195
+ child.kill("SIGTERM");
196
+ }
197
+ },
158
198
  };
159
199
 
160
200
  this.tasks.set(id, shell);
@@ -232,65 +272,24 @@ export class BackgroundTaskManager {
232
272
  return false;
233
273
  }
234
274
 
235
- if (task.type === "shell") {
236
- const shell = task as BackgroundShell;
275
+ if (task.onStop) {
237
276
  try {
238
- // Try to kill process group first
239
- if (shell.process.pid) {
240
- process.kill(-shell.process.pid, "SIGTERM");
241
-
242
- // Force kill after timeout
243
- setTimeout(() => {
244
- if (
245
- shell.status === "running" &&
246
- shell.process.pid &&
247
- !shell.process.killed
248
- ) {
249
- try {
250
- process.kill(-shell.process.pid, "SIGKILL");
251
- } catch (error) {
252
- logger.error("Failed to force kill process:", error);
253
- }
254
- }
255
- }, 1000);
256
- }
257
-
258
- shell.status = "killed";
259
- shell.endTime = Date.now();
260
- shell.runtime = shell.endTime - shell.startTime;
261
- this.notifyTasksChange();
262
- return true;
263
- } catch {
264
- // Fallback to direct process kill
265
- try {
266
- shell.process.kill("SIGTERM");
267
- setTimeout(() => {
268
- if (!shell.process.killed) {
269
- shell.process.kill("SIGKILL");
270
- }
271
- }, 1000);
272
- shell.status = "killed";
273
- shell.endTime = Date.now();
274
- shell.runtime = shell.endTime - shell.startTime;
275
- this.notifyTasksChange();
276
- return true;
277
- } catch (directKillError) {
278
- logger.error("Failed to kill child process:", directKillError);
279
- return false;
277
+ const result = task.onStop();
278
+ if (result instanceof Promise) {
279
+ result.catch((error) => {
280
+ logger.error("Error in background task onStop callback:", error);
281
+ });
280
282
  }
283
+ } catch (error) {
284
+ logger.error("Error in background task onStop callback:", error);
281
285
  }
282
- } else if (task.type === "subagent") {
283
- // Subagent termination logic will be handled by aborting the AI loop
284
- // which is already managed by the SubagentManager and AIManager.
285
- // Here we just update the status.
286
- task.status = "killed";
287
- task.endTime = Date.now();
288
- task.runtime = task.endTime - task.startTime;
289
- this.notifyTasksChange();
290
- return true;
291
286
  }
292
287
 
293
- return false;
288
+ task.status = "killed";
289
+ task.endTime = Date.now();
290
+ task.runtime = task.endTime - task.startTime;
291
+ this.notifyTasksChange();
292
+ return true;
294
293
  }
295
294
 
296
295
  public cleanup(): void {
@@ -14,8 +14,9 @@ export class ForegroundTaskManager implements IForegroundTaskManager {
14
14
  }
15
15
 
16
16
  public async backgroundCurrentTask(): Promise<void> {
17
- const task = this.activeForegroundTasks.pop();
18
- if (task) {
17
+ const tasks = [...this.activeForegroundTasks].reverse();
18
+ this.activeForegroundTasks = [];
19
+ for (const task of tasks) {
19
20
  await task.backgroundHandler();
20
21
  }
21
22
  }
@@ -3,7 +3,7 @@ import { join } from "path";
3
3
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
4
4
  import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
5
5
  import { ChatCompletionFunctionTool } from "openai/resources.js";
6
- import { createMcpToolPlugin, findToolServer } from "@/utils/mcpUtils.js";
6
+ import { createMcpToolPlugin, findToolServer } from "../utils/mcpUtils.js";
7
7
  import type { ToolPlugin, ToolResult, ToolContext } from "../tools/types.js";
8
8
  import type {
9
9
  Logger,
@@ -476,8 +476,11 @@ export class McpManager {
476
476
  const server = findToolServer(tool.name, servers);
477
477
 
478
478
  if (server) {
479
- const plugin = createMcpToolPlugin(tool, server.name, (name, args) =>
480
- this.executeMcpTool(name, args),
479
+ const plugin = createMcpToolPlugin(
480
+ tool,
481
+ server.name,
482
+ (name: string, args: Record<string, unknown>) =>
483
+ this.executeMcpTool(name, args),
481
484
  );
482
485
  mcpTools.set(plugin.name, plugin);
483
486
  }
@@ -45,7 +45,7 @@ export interface MessageManagerCallbacks {
45
45
  onAssistantReasoningUpdated?: (chunk: string, accumulated: string) => void;
46
46
  onToolBlockUpdated?: (params: AgentToolBlockUpdateParams) => void;
47
47
  onErrorBlockAdded?: (error: string) => void;
48
- onCompressBlockAdded?: (insertIndex: number, content: string) => void;
48
+ onCompressBlockAdded?: (content: string) => void;
49
49
  onCompressionStateChange?: (isCompressing: boolean) => void;
50
50
  onMemoryBlockAdded?: (
51
51
  content: string,
@@ -74,6 +74,10 @@ export interface MessageManagerCallbacks {
74
74
  subagentId: string,
75
75
  status: "active" | "completed" | "error" | "aborted",
76
76
  ) => void;
77
+ onFileHistoryBlockAdded?: (
78
+ snapshots: import("../types/reversion.js").FileSnapshot[],
79
+ ) => void;
80
+ onSubagentTaskStopRequested?: (subagentId: string) => void;
77
81
  }
78
82
 
79
83
  export interface MessageManagerOptions {
@@ -88,6 +92,7 @@ export interface MessageManagerOptions {
88
92
  export class MessageManager {
89
93
  // Private state properties
90
94
  private sessionId: string;
95
+ private rootSessionId: string;
91
96
  private messages: Message[];
92
97
  private latestTotalTokens: number;
93
98
  private userInputHistory: string[];
@@ -104,6 +109,7 @@ export class MessageManager {
104
109
 
105
110
  constructor(options: MessageManagerOptions) {
106
111
  this.sessionId = generateSessionId();
112
+ this.rootSessionId = this.sessionId;
107
113
  this.messages = [];
108
114
  this.latestTotalTokens = 0;
109
115
  this.userInputHistory = [];
@@ -125,6 +131,10 @@ export class MessageManager {
125
131
  return this.sessionId;
126
132
  }
127
133
 
134
+ public getRootSessionId(): string {
135
+ return this.rootSessionId;
136
+ }
137
+
128
138
  public getMessages(): Message[] {
129
139
  return [...this.messages];
130
140
  }
@@ -247,6 +257,7 @@ export class MessageManager {
247
257
  unsavedMessages, // Only append new messages
248
258
  this.workdir,
249
259
  this.sessionType,
260
+ this.rootSessionId,
250
261
  );
251
262
 
252
263
  // Update the saved message count
@@ -297,6 +308,7 @@ export class MessageManager {
297
308
  // Initialize state from session data
298
309
  public initializeFromSession(sessionData: SessionData): void {
299
310
  this.setSessionId(sessionData.id);
311
+ this.rootSessionId = sessionData.rootSessionId || sessionData.id;
300
312
  this.setMessages([...sessionData.messages]);
301
313
  this.updateFilesInContext(sessionData.messages);
302
314
  this.setlatestTotalTokens(sessionData.metadata.latestTotalTokens);
@@ -402,17 +414,7 @@ export class MessageManager {
402
414
  public updateToolBlock(params: AgentToolBlockUpdateParams): void {
403
415
  const newMessages = updateToolBlockInMessage({
404
416
  messages: this.messages,
405
- id: params.id,
406
- parameters: params.parameters,
407
- result: params.result,
408
- success: params.success,
409
- error: params.error,
410
- stage: params.stage,
411
- name: params.name,
412
- shortResult: params.shortResult,
413
- images: params.images,
414
- compactParams: params.compactParams,
415
- parametersChunk: params.parametersChunk,
417
+ ...params,
416
418
  });
417
419
  this.setMessages(newMessages);
418
420
  this.callbacks.onToolBlockUpdated?.(params);
@@ -442,14 +444,14 @@ export class MessageManager {
442
444
  }
443
445
 
444
446
  /**
445
- * Compress messages and update session, delete compressed messages, only keep compressed messages and subsequent messages
447
+ * Compress messages and update session, delete compressed messages, only keep compressed messages and last 3 messages
446
448
  */
447
449
  public compressMessagesAndUpdateSession(
448
- insertIndex: number,
449
450
  compressedContent: string,
450
451
  usage?: Usage,
451
452
  ): void {
452
- const currentMessages = this.messages;
453
+ // Get last 3 messages to preserve
454
+ const lastThreeMessages = this.messages.slice(-3);
453
455
 
454
456
  // Create compressed message
455
457
  const compressMessage: Message = {
@@ -464,24 +466,22 @@ export class MessageManager {
464
466
  ...(usage && { usage }),
465
467
  };
466
468
 
467
- // Convert negative index to positive index
468
- const actualIndex =
469
- insertIndex < 0 ? currentMessages.length + insertIndex : insertIndex;
470
-
471
- // Build new message array: keep compressed message and all messages from actualIndex onwards
472
- const newMessages: Message[] = [
473
- compressMessage,
474
- ...currentMessages.slice(actualIndex),
475
- ];
469
+ // Build new message array: keep the compressed message and last 3 messages
470
+ const newMessages: Message[] = [compressMessage, ...lastThreeMessages];
476
471
 
477
472
  // Update sessionId
478
473
  this.setSessionId(generateSessionId());
479
474
 
475
+ // Trigger task list update if this is the main session to ensure continuity
476
+ if (this.sessionType === "main") {
477
+ this.callbacks.onSessionIdChange?.(this.sessionId);
478
+ }
479
+
480
480
  // Set new message list
481
481
  this.setMessages(newMessages);
482
482
 
483
- // Trigger compression callback, insertIndex remains unchanged
484
- this.callbacks.onCompressBlockAdded?.(insertIndex, compressedContent);
483
+ // Trigger compression callback
484
+ this.callbacks.onCompressBlockAdded?.(compressedContent);
485
485
  }
486
486
 
487
487
  public addFileHistoryBlock(
@@ -496,6 +496,7 @@ export class MessageManager {
496
496
  snapshots,
497
497
  } as unknown as import("../types/index.js").MessageBlock);
498
498
  this.setMessages([...this.messages]);
499
+ this.callbacks.onFileHistoryBlockAdded?.(snapshots);
499
500
  }
500
501
  }
501
502
 
@@ -729,6 +730,16 @@ export class MessageManager {
729
730
 
730
731
  // Truncate messages in memory
731
732
  const newMessages = this.messages.slice(0, index);
733
+
734
+ // Identify subagent tasks to stop
735
+ for (const message of messagesToRemove) {
736
+ for (const block of message.blocks) {
737
+ if (block.type === "subagent" && block.subagentId) {
738
+ this.callbacks.onSubagentTaskStopRequested?.(block.subagentId);
739
+ }
740
+ }
741
+ }
742
+
732
743
  this.setMessages(newMessages);
733
744
 
734
745
  // Update persistence: rewrite the session file
@@ -323,40 +323,30 @@ export class PermissionManager {
323
323
  workdir,
324
324
  );
325
325
  if (!isInside) {
326
- this.logger?.warn(
327
- "File operation outside the Safe Zone in acceptEdits mode",
326
+ this.logger?.info(
327
+ "File operation outside the Safe Zone in acceptEdits mode, falling back to manual confirmation",
328
328
  {
329
329
  toolName: context.toolName,
330
330
  targetPath,
331
331
  resolvedPath,
332
332
  },
333
333
  );
334
- return {
335
- behavior: "deny",
336
- message: `Tool '${context.toolName}' attempted to modify a file outside the Safe Zone: ${targetPath}. Operations outside the Safe Zone always require manual confirmation.`,
337
- };
334
+ // Fall through to normal permission check flow to trigger confirmation prompt
335
+ } else {
336
+ this.logger?.debug(
337
+ "Permission automatically accepted for tool in acceptEdits mode",
338
+ {
339
+ toolName: context.toolName,
340
+ },
341
+ );
342
+ return { behavior: "allow" };
338
343
  }
339
344
  }
340
-
341
- this.logger?.debug(
342
- "Permission automatically accepted for tool in acceptEdits mode",
343
- {
344
- toolName: context.toolName,
345
- },
346
- );
347
- return { behavior: "allow" };
348
345
  }
349
346
  }
350
347
 
351
348
  // 1.3 If plan mode, allow Read-only tools and Edit/Write for plan file
352
349
  if (context.permissionMode === "plan") {
353
- if (context.toolName === BASH_TOOL_NAME) {
354
- return {
355
- behavior: "deny",
356
- message: "Bash commands are not allowed in plan mode.",
357
- };
358
- }
359
-
360
350
  const writeTools = [
361
351
  EDIT_TOOL_NAME,
362
352
  MULTI_EDIT_TOOL_NAME,
@@ -492,6 +482,27 @@ export class PermissionManager {
492
482
  suggestedPrefix,
493
483
  };
494
484
 
485
+ // Set hidePersistentOption for out-of-bounds file operations
486
+ const fileTools = [
487
+ EDIT_TOOL_NAME,
488
+ MULTI_EDIT_TOOL_NAME,
489
+ DELETE_FILE_TOOL_NAME,
490
+ WRITE_TOOL_NAME,
491
+ ];
492
+ if (fileTools.includes(toolName)) {
493
+ const targetPath = (toolInput?.file_path || toolInput?.target_file) as
494
+ | string
495
+ | undefined;
496
+ const workdir = toolInput?.workdir as string | undefined;
497
+
498
+ if (targetPath) {
499
+ const { isInside } = this.isInsideSafeZone(targetPath, workdir);
500
+ if (!isInside) {
501
+ context.hidePersistentOption = true;
502
+ }
503
+ }
504
+ }
505
+
495
506
  // Set hidePersistentOption for dangerous or out-of-bounds bash commands
496
507
  if (toolName === BASH_TOOL_NAME && toolInput?.command) {
497
508
  const command = String(toolInput.command);
@@ -17,7 +17,7 @@ export class PlanManager {
17
17
  /**
18
18
  * Ensures the plan directory exists and generates a new plan file path with a random name
19
19
  */
20
- public async getOrGeneratePlanFilePath(): Promise<{
20
+ public async getOrGeneratePlanFilePath(seed?: string): Promise<{
21
21
  path: string;
22
22
  name: string;
23
23
  }> {
@@ -30,7 +30,7 @@ export class PlanManager {
30
30
  );
31
31
  throw error;
32
32
  }
33
- const name = generateRandomName();
33
+ const name = generateRandomName(seed);
34
34
  const filePath = path.join(this.planDir, `${name}.md`);
35
35
  this.logger?.info(`Generated plan file path: ${filePath}`);
36
36
  return { path: filePath, name };