wave-agent-sdk 0.5.0 → 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 -74
  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 -5
  14. package/dist/managers/aiManager.d.ts.map +1 -1
  15. package/dist/managers/aiManager.js +59 -48
  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 +22 -14
  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 +7 -9
  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 -84
  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 +79 -70
  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 +33 -14
  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 +9 -15
  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;
@@ -159,15 +178,6 @@ export class AIManager {
159
178
  this.setIsLoading(false);
160
179
  }
161
180
 
162
- /**
163
- * Abort the AI recursion loop immediately.
164
- * This is used when a tool is backgrounded via Ctrl-B, even if no foreground task was active.
165
- */
166
- public abortRecursion(): void {
167
- this.logger?.info("Aborting AI recursion loop");
168
- this.abortAIMessage();
169
- }
170
-
171
181
  // Helper method to generate compactParams
172
182
  private generateCompactParams(
173
183
  toolName: string,
@@ -180,6 +190,7 @@ export class AIManager {
180
190
  if (toolPlugin?.formatCompactParams) {
181
191
  const context: ToolContext = {
182
192
  workdir: this.workdir,
193
+ taskManager: this.taskManager,
183
194
  };
184
195
  return toolPlugin.formatCompactParams(toolArgs, context);
185
196
  }
@@ -213,9 +224,7 @@ export class AIManager {
213
224
  );
214
225
 
215
226
  // Check if messages need compression
216
- const { messagesToCompress, insertIndex } = getMessagesToCompress(
217
- this.messageManager.getMessages(),
218
- );
227
+ const messagesToCompress = this.messageManager.getMessages();
219
228
 
220
229
  // If there are messages to compress, perform compression
221
230
  if (messagesToCompress.length > 0) {
@@ -248,7 +257,6 @@ export class AIManager {
248
257
 
249
258
  // Execute message reconstruction and sessionId update after compression
250
259
  this.messageManager.compressMessagesAndUpdateSession(
251
- insertIndex,
252
260
  compressionResult.content,
253
261
  compressionUsage,
254
262
  );
@@ -305,6 +313,9 @@ export class AIManager {
305
313
  return;
306
314
  }
307
315
 
316
+ // Save session in each recursion to ensure message persistence
317
+ await this.messageManager.saveSession();
318
+
308
319
  // Only create new AbortControllers for the initial call (recursionDepth === 0)
309
320
  // For recursive calls, reuse existing controllers to maintain abort signal
310
321
  let abortController: AbortController;
@@ -319,14 +330,10 @@ export class AIManager {
319
330
  this.toolAbortController = toolAbortController;
320
331
  } else {
321
332
  // Reuse existing controllers for recursive calls
322
- // Fallback to new controllers if they were cleared (should not happen in normal flow but good for tests)
323
- abortController = this.abortController || new AbortController();
324
- toolAbortController = this.toolAbortController || new AbortController();
333
+ abortController = this.abortController!;
334
+ toolAbortController = this.toolAbortController!;
325
335
  }
326
336
 
327
- // Save session in each recursion to ensure message persistence
328
- await this.messageManager.saveSession();
329
-
330
337
  // Only set loading state for the initial call
331
338
  if (recursionDepth === 0) {
332
339
  this.setIsLoading(true);
@@ -354,17 +361,14 @@ export class AIManager {
354
361
  this.getModelConfig().permissionMode,
355
362
  );
356
363
  const toolsConfig = this.getFilteredToolsConfig(tools);
357
- let effectiveSystemPrompt = buildSystemPrompt(
358
- this.systemPrompt || DEFAULT_SYSTEM_PROMPT,
359
- toolsConfig,
360
- );
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));
361
368
 
362
- // Inject language prompt if configured
363
- const language = this.getLanguage();
364
- if (language) {
365
- 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.`;
366
- effectiveSystemPrompt = (effectiveSystemPrompt || "") + languagePrompt;
367
- }
369
+ let planModeOptions:
370
+ | { planFilePath: string; planExists: boolean }
371
+ | undefined;
368
372
 
369
373
  if (currentMode === "plan") {
370
374
  const planFilePath = this.permissionManager?.getPlanFilePath();
@@ -376,10 +380,7 @@ export class AIManager {
376
380
  } catch {
377
381
  planExists = false;
378
382
  }
379
-
380
- 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.`;
381
-
382
- effectiveSystemPrompt = (effectiveSystemPrompt || "") + reminder;
383
+ planModeOptions = { planFilePath, planExists };
383
384
  }
384
385
  }
385
386
 
@@ -390,11 +391,23 @@ export class AIManager {
390
391
  messages: recentMessages,
391
392
  sessionId: this.messageManager.getSessionId(),
392
393
  abortSignal: abortController.signal,
393
- memory: combinedMemory, // Pass combined memory content
394
394
  workdir: this.workdir, // Pass working directory
395
395
  tools: toolsConfig, // Pass filtered tool configuration
396
396
  model: model, // Use passed model
397
- 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
398
411
  maxTokens: maxTokens, // Pass max tokens override
399
412
  };
400
413
 
@@ -628,6 +641,8 @@ export class AIManager {
628
641
  backgroundTaskManager: this.backgroundTaskManager,
629
642
  workdir: this.workdir,
630
643
  messageId: this.messageManager.getMessages().slice(-1)[0]?.id,
644
+ sessionId: this.messageManager.getSessionId(),
645
+ taskManager: this.taskManager,
631
646
  };
632
647
 
633
648
  // Execute tool
@@ -637,23 +652,6 @@ export class AIManager {
637
652
  context,
638
653
  );
639
654
 
640
- // Check if the tool was backgrounded via Ctrl-B
641
- // If it was backgrounded, we should abort the AI recursion
642
- if (
643
- toolResult.success &&
644
- toolResult.content.includes(
645
- "Command was manually backgrounded by user",
646
- )
647
- ) {
648
- this.logger?.info(
649
- `Tool ${toolName} was backgrounded via Ctrl-B, aborting AI recursion`,
650
- );
651
- // Use abortAIMessage directly instead of abortRecursion to avoid double logging
652
- // and ensure we don't trigger the "Request was aborted" error block
653
- this.abortAIMessage();
654
- return;
655
- }
656
-
657
655
  // Update message state - tool execution completed
658
656
  this.messageManager.updateToolBlock({
659
657
  id: toolId,
@@ -666,6 +664,7 @@ export class AIManager {
666
664
  stage: "end",
667
665
  name: toolName,
668
666
  shortResult: toolResult.shortResult,
667
+ isManuallyBackgrounded: toolResult.isManuallyBackgrounded,
669
668
  });
670
669
 
671
670
  // Execute PostToolUse hooks after successful tool completion
@@ -690,6 +689,7 @@ export class AIManager {
690
689
  stage: "end",
691
690
  name: toolName,
692
691
  compactParams,
692
+ isManuallyBackgrounded: false,
693
693
  });
694
694
  }
695
695
  },
@@ -721,7 +721,25 @@ export class AIManager {
721
721
  const isCurrentlyAborted =
722
722
  abortController.signal.aborted || toolAbortController.signal.aborted;
723
723
 
724
- 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) {
725
743
  // Recursively call AI service, increment recursion depth, and pass same configuration
726
744
  await this.sendAIMessage({
727
745
  recursionDepth: recursionDepth + 1,
@@ -733,18 +751,9 @@ export class AIManager {
733
751
  }
734
752
  }
735
753
  } catch (error) {
736
- // Check if the error is an abort error
737
- // Use the local variables to avoid null reference if this.abortController was cleared
738
- const isCurrentlyAborted =
739
- abortController.signal.aborted || toolAbortController.signal.aborted;
740
-
741
- if (isCurrentlyAborted) {
742
- this.logger?.info("AI message processing was aborted");
743
- } else {
744
- this.messageManager.addErrorBlock(
745
- error instanceof Error ? error.message : "Unknown error occurred",
746
- );
747
- }
754
+ this.messageManager.addErrorBlock(
755
+ error instanceof Error ? error.message : "Unknown error occurred",
756
+ );
748
757
  } finally {
749
758
  // Only execute cleanup and hooks for the initial call
750
759
  if (recursionDepth === 0) {
@@ -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