wave-agent-sdk 0.13.2 → 0.13.3

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 (66) hide show
  1. package/dist/agent.d.ts +7 -0
  2. package/dist/agent.d.ts.map +1 -1
  3. package/dist/agent.js +37 -0
  4. package/dist/index.d.ts +1 -0
  5. package/dist/index.d.ts.map +1 -1
  6. package/dist/index.js +1 -0
  7. package/dist/managers/aiManager.d.ts.map +1 -1
  8. package/dist/managers/aiManager.js +65 -33
  9. package/dist/managers/backgroundTaskManager.d.ts +1 -0
  10. package/dist/managers/backgroundTaskManager.d.ts.map +1 -1
  11. package/dist/managers/backgroundTaskManager.js +49 -0
  12. package/dist/managers/forkedAgentManager.d.ts +49 -0
  13. package/dist/managers/forkedAgentManager.d.ts.map +1 -0
  14. package/dist/managers/forkedAgentManager.js +111 -0
  15. package/dist/managers/messageManager.d.ts +8 -1
  16. package/dist/managers/messageManager.d.ts.map +1 -1
  17. package/dist/managers/messageManager.js +14 -1
  18. package/dist/managers/notificationQueue.d.ts +8 -0
  19. package/dist/managers/notificationQueue.d.ts.map +1 -0
  20. package/dist/managers/notificationQueue.js +17 -0
  21. package/dist/managers/permissionManager.d.ts.map +1 -1
  22. package/dist/managers/permissionManager.js +2 -0
  23. package/dist/managers/subagentManager.d.ts +0 -10
  24. package/dist/managers/subagentManager.d.ts.map +1 -1
  25. package/dist/managers/subagentManager.js +60 -20
  26. package/dist/services/autoMemoryService.d.ts +1 -1
  27. package/dist/services/autoMemoryService.d.ts.map +1 -1
  28. package/dist/services/autoMemoryService.js +7 -9
  29. package/dist/services/interactionService.d.ts.map +1 -1
  30. package/dist/services/interactionService.js +12 -0
  31. package/dist/types/agent.d.ts +1 -0
  32. package/dist/types/agent.d.ts.map +1 -1
  33. package/dist/types/messaging.d.ts +9 -1
  34. package/dist/types/messaging.d.ts.map +1 -1
  35. package/dist/utils/containerSetup.d.ts.map +1 -1
  36. package/dist/utils/containerSetup.js +6 -0
  37. package/dist/utils/convertMessagesForAPI.d.ts.map +1 -1
  38. package/dist/utils/convertMessagesForAPI.js +8 -0
  39. package/dist/utils/messageOperations.d.ts +9 -0
  40. package/dist/utils/messageOperations.d.ts.map +1 -1
  41. package/dist/utils/messageOperations.js +17 -0
  42. package/dist/utils/notificationXml.d.ts +4 -0
  43. package/dist/utils/notificationXml.d.ts.map +1 -0
  44. package/dist/utils/notificationXml.js +40 -0
  45. package/dist/utils/pathEncoder.d.ts +0 -1
  46. package/dist/utils/pathEncoder.d.ts.map +1 -1
  47. package/dist/utils/pathEncoder.js +1 -5
  48. package/package.json +1 -1
  49. package/src/agent.ts +44 -0
  50. package/src/index.ts +1 -0
  51. package/src/managers/aiManager.ts +76 -41
  52. package/src/managers/backgroundTaskManager.ts +72 -1
  53. package/src/managers/forkedAgentManager.ts +193 -0
  54. package/src/managers/messageManager.ts +25 -0
  55. package/src/managers/notificationQueue.ts +19 -0
  56. package/src/managers/permissionManager.ts +2 -0
  57. package/src/managers/subagentManager.ts +86 -42
  58. package/src/services/autoMemoryService.ts +11 -18
  59. package/src/services/interactionService.ts +18 -0
  60. package/src/types/agent.ts +1 -0
  61. package/src/types/messaging.ts +11 -1
  62. package/src/utils/containerSetup.ts +8 -0
  63. package/src/utils/convertMessagesForAPI.ts +9 -0
  64. package/src/utils/messageOperations.ts +42 -1
  65. package/src/utils/notificationXml.ts +52 -0
  66. package/src/utils/pathEncoder.ts +1 -6
@@ -16,7 +16,6 @@ export class PathEncoder {
16
16
  pathSeparatorReplacement: options.pathSeparatorReplacement ?? "-",
17
17
  spaceReplacement: options.spaceReplacement ?? "_",
18
18
  invalidCharReplacement: options.invalidCharReplacement ?? "_",
19
- preserveCase: options.preserveCase ?? false,
20
19
  hashLength: options.hashLength ?? 8,
21
20
  };
22
21
  this.constraints = this.getFilesystemConstraints();
@@ -50,10 +49,7 @@ export class PathEncoder {
50
49
  .join("");
51
50
  const invalidChars = new RegExp(`[${escapedChars}]`, "g");
52
51
  encoded = encoded.replace(invalidChars, this.options.invalidCharReplacement);
53
- // Convert to lowercase unless preserveCase is true
54
- if (!this.options.preserveCase) {
55
- encoded = encoded.toLowerCase();
56
- }
52
+ // Case is preserved
57
53
  // Handle length limit with hash
58
54
  if (encoded.length > this.options.maxLength) {
59
55
  const hash = this.generateHash(pathToEncode, this.options.hashLength);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wave-agent-sdk",
3
- "version": "0.13.2",
3
+ "version": "0.13.3",
4
4
  "description": "SDK for building AI-powered development tools and agents",
5
5
  "keywords": [
6
6
  "ai",
package/src/agent.ts CHANGED
@@ -3,11 +3,13 @@ import { MessageManager } from "./managers/messageManager.js";
3
3
  import { AIManager } from "./managers/aiManager.js";
4
4
  import { ToolManager } from "./managers/toolManager.js";
5
5
  import { SubagentManager } from "./managers/subagentManager.js";
6
+ import { ForkedAgentManager } from "./managers/forkedAgentManager.js";
6
7
  import { McpManager } from "./managers/mcpManager.js";
7
8
  import { LspManager } from "./managers/lspManager.js";
8
9
  import { BangManager } from "./managers/bangManager.js";
9
10
  import { CronManager } from "./managers/cronManager.js";
10
11
  import { BackgroundTaskManager } from "./managers/backgroundTaskManager.js";
12
+ import { NotificationQueue } from "./managers/notificationQueue.js";
11
13
  import { SlashCommandManager } from "./managers/slashCommandManager.js";
12
14
  import { PluginManager } from "./managers/pluginManager.js";
13
15
  import { HookManager } from "./managers/hookManager.js";
@@ -37,6 +39,7 @@ import { SkillManager } from "./managers/skillManager.js";
37
39
  import { TaskManager } from "./services/taskManager.js";
38
40
  import { btw } from "./services/aiService.js";
39
41
  import { convertMessagesForAPI } from "./utils/convertMessagesForAPI.js";
42
+ import { parseTaskNotificationXml } from "./utils/notificationXml.js";
40
43
  import { InitializationService } from "./services/initializationService.js";
41
44
  import { InteractionService } from "./services/interactionService.js";
42
45
  import { ConfigurationService } from "./services/configurationService.js";
@@ -56,12 +59,14 @@ export class Agent {
56
59
  private permissionManager: PermissionManager; // Add permission manager instance
57
60
  private planManager: PlanManager; // Add plan manager instance
58
61
  private subagentManager: SubagentManager; // Add subagent manager instance
62
+ private forkedAgentManager: ForkedAgentManager; // Add forked agent manager instance
59
63
  private slashCommandManager: SlashCommandManager; // Add slash command manager instance
60
64
  private pluginManager: PluginManager; // Add plugin manager instance
61
65
  private skillManager: SkillManager; // Add skill manager instance
62
66
  private cronManager: CronManager; // Add cron manager instance
63
67
  private hookManager: HookManager; // Add hooks manager instance
64
68
  private reversionManager: ReversionManager;
69
+ private notificationQueue: NotificationQueue; // Add notification queue instance
65
70
  private memoryRuleManager: MemoryRuleManager; // Add memory rule manager instance
66
71
  private liveConfigManager: LiveConfigManager; // Add live configuration manager
67
72
  private taskManager: TaskManager;
@@ -185,11 +190,23 @@ export class Agent {
185
190
  this.toolManager = this.container.get("ToolManager")!;
186
191
  this.liveConfigManager = this.container.get("LiveConfigManager")!;
187
192
  this.subagentManager = this.container.get("SubagentManager")!;
193
+ this.forkedAgentManager = this.container.get("ForkedAgentManager")!;
188
194
  this.aiManager = this.container.get("AIManager")!;
189
195
  this.slashCommandManager = this.container.get("SlashCommandManager")!;
190
196
  this.pluginManager = this.container.get("PluginManager")!;
191
197
  this.bangManager = this.container.get("BangManager")!;
192
198
  this.cronManager = this.container.get("CronManager")!;
199
+ this.notificationQueue = this.container.get("NotificationQueue")!;
200
+
201
+ // Wire up notification queue to trigger AI when notifications arrive while idle
202
+ this.notificationQueue.onNotificationsEnqueued = () => {
203
+ // If the AI is NOT loading (idle), trigger a new AI cycle to process notifications
204
+ if (!this.aiManager.isLoading) {
205
+ this.processPendingNotifications().catch((error) => {
206
+ this.logger?.error("Failed to process pending notifications:", error);
207
+ });
208
+ }
209
+ };
193
210
 
194
211
  // Set initial permission mode if provided
195
212
  if (options.permissionMode) {
@@ -425,6 +442,31 @@ export class Agent {
425
442
  this.aiManager.abortAIMessage();
426
443
  }
427
444
 
445
+ /**
446
+ * Process pending background task notifications by injecting them as user messages
447
+ * and triggering a new AI response cycle.
448
+ */
449
+ private async processPendingNotifications(): Promise<void> {
450
+ const notifications = this.notificationQueue.dequeueAll();
451
+ if (notifications.length === 0) return;
452
+
453
+ for (const notification of notifications) {
454
+ const block = parseTaskNotificationXml(notification);
455
+ if (block) {
456
+ this.messageManager.addNotificationMessage({
457
+ taskId: block.taskId,
458
+ taskType: block.taskType,
459
+ status: block.status,
460
+ summary: block.summary,
461
+ outputFile: block.outputFile,
462
+ });
463
+ }
464
+ }
465
+
466
+ // Trigger AI to process the notifications
467
+ await this.aiManager.sendAIMessage({ recursionDepth: 0 });
468
+ }
469
+
428
470
  /** Execute bash command (bang command) */
429
471
  public async bang(command: string): Promise<void> {
430
472
  await this.bangManager?.executeCommand(command);
@@ -491,6 +533,8 @@ export class Agent {
491
533
  }
492
534
  // Cleanup subagent manager
493
535
  this.subagentManager.cleanup();
536
+ // Cleanup forked agent manager
537
+ this.forkedAgentManager.cleanup();
494
538
  // Cleanup skill manager
495
539
  await this.skillManager.destroy();
496
540
  // Cleanup live configuration reload
package/src/index.ts CHANGED
@@ -16,6 +16,7 @@ export * from "./utils/fileSearch.js";
16
16
  export * from "./utils/globalLogger.js";
17
17
  export * from "./utils/mcpUtils.js";
18
18
  export * from "./utils/messageOperations.js";
19
+ export * from "./utils/notificationXml.js";
19
20
  export * from "./utils/path.js";
20
21
  export * from "./utils/promptHistory.js";
21
22
  export * from "./utils/stringUtils.js";
@@ -1,6 +1,7 @@
1
1
  import { type CallAgentOptions } from "../services/aiService.js";
2
2
  import * as aiService from "../services/aiService.js";
3
3
  import { convertMessagesForAPI } from "../utils/convertMessagesForAPI.js";
4
+ import { parseTaskNotificationXml } from "../utils/notificationXml.js";
4
5
  import { calculateComprehensiveTotalTokens } from "../utils/tokenCalculation.js";
5
6
  import * as fs from "node:fs/promises";
6
7
  import type {
@@ -14,6 +15,7 @@ import type { ToolManager } from "./toolManager.js";
14
15
  import type { ToolContext, ToolResult } from "../tools/types.js";
15
16
  import type { MessageManager } from "./messageManager.js";
16
17
  import type { BackgroundTaskManager } from "./backgroundTaskManager.js";
18
+ import type { NotificationQueue } from "./notificationQueue.js";
17
19
  import { ChatCompletionMessageFunctionToolCall } from "openai/resources.js";
18
20
  import type { HookManager } from "./hookManager.js";
19
21
  import type { ExtendedHookExecutionContext } from "../types/hooks.js";
@@ -178,6 +180,11 @@ export class AIManager {
178
180
 
179
181
  public setIsLoading(isLoading: boolean): void {
180
182
  this.isLoading = isLoading;
183
+ const options =
184
+ this.container.get<import("../types/agent.js").AgentOptions>(
185
+ "AgentOptions",
186
+ );
187
+ options?.callbacks?.onLoadingChange?.(isLoading);
181
188
  }
182
189
 
183
190
  public abortAIMessage(): void {
@@ -340,6 +347,14 @@ export class AIManager {
340
347
  return;
341
348
  }
342
349
 
350
+ // Set loading state early for the initial call, before any async work
351
+ if (recursionDepth === 0) {
352
+ this.setIsLoading(true);
353
+ if (allowedRules && allowedRules.length > 0) {
354
+ this.permissionManager?.addTemporaryRules(allowedRules);
355
+ }
356
+ }
357
+
343
358
  // Scan for file mentions in the last user message
344
359
  if (recursionDepth === 0) {
345
360
  const messages = this.messageManager.getMessages();
@@ -380,14 +395,6 @@ export class AIManager {
380
395
  toolAbortController = this.toolAbortController!;
381
396
  }
382
397
 
383
- // Only set loading state for the initial call
384
- if (recursionDepth === 0) {
385
- this.setIsLoading(true);
386
- if (allowedRules && allowedRules.length > 0) {
387
- this.permissionManager?.addTemporaryRules(allowedRules);
388
- }
389
- }
390
-
391
398
  // Get recent message history
392
399
  const recentMessages = convertMessagesForAPI(
393
400
  this.messageManager.getMessages(),
@@ -899,44 +906,72 @@ export class AIManager {
899
906
  // Set loading to false first
900
907
  this.setIsLoading(false);
901
908
 
902
- // Clear temporary rules
903
- this.permissionManager?.clearTemporaryRules();
904
-
905
- // Clear abort controllers
906
- this.abortController = null;
907
- this.toolAbortController = null;
908
-
909
- // Execute Stop/SubagentStop hooks only if the operation was not aborted
910
- const isCurrentlyAborted =
911
- abortController.signal.aborted || toolAbortController.signal.aborted;
912
-
913
- if (!isCurrentlyAborted) {
914
- // Record committed snapshots to message history for the final turn
915
- if (this.reversionManager) {
916
- const snapshots =
917
- this.reversionManager.getAndClearCommittedSnapshots();
918
- if (snapshots.length > 0) {
919
- this.messageManager.addFileHistoryBlock(snapshots);
909
+ // Inject pending notifications from background tasks
910
+ const notificationQueue = this.container.has("NotificationQueue")
911
+ ? this.container.get<NotificationQueue>("NotificationQueue")
912
+ : undefined;
913
+ if (notificationQueue && notificationQueue.hasPending()) {
914
+ const notifications = notificationQueue.dequeueAll();
915
+ for (const notification of notifications) {
916
+ const block = parseTaskNotificationXml(notification);
917
+ if (block) {
918
+ this.messageManager.addNotificationMessage({
919
+ taskId: block.taskId,
920
+ taskType: block.taskType,
921
+ status: block.status,
922
+ summary: block.summary,
923
+ outputFile: block.outputFile,
924
+ });
920
925
  }
921
926
  }
927
+ // Recursively process the notifications
928
+ await this.sendAIMessage({
929
+ recursionDepth: 0,
930
+ model,
931
+ allowedRules,
932
+ maxTokens,
933
+ });
934
+ } else {
935
+ // Clear temporary rules
936
+ this.permissionManager?.clearTemporaryRules();
937
+
938
+ // Clear abort controllers
939
+ this.abortController = null;
940
+ this.toolAbortController = null;
941
+
942
+ // Execute Stop/SubagentStop hooks only if the operation was not aborted
943
+ const isCurrentlyAborted =
944
+ abortController.signal.aborted ||
945
+ toolAbortController.signal.aborted;
946
+
947
+ if (!isCurrentlyAborted) {
948
+ // Record committed snapshots to message history for the final turn
949
+ if (this.reversionManager) {
950
+ const snapshots =
951
+ this.reversionManager.getAndClearCommittedSnapshots();
952
+ if (snapshots.length > 0) {
953
+ this.messageManager.addFileHistoryBlock(snapshots);
954
+ }
955
+ }
922
956
 
923
- const shouldContinue = await this.executeStopHooks();
957
+ const shouldContinue = await this.executeStopHooks();
924
958
 
925
- // If Stop/SubagentStop hooks indicate we should continue (due to blocking errors),
926
- // restart the AI conversation cycle
927
- if (shouldContinue) {
928
- logger?.info(
929
- `${this.subagentType ? "SubagentStop" : "Stop"} hooks indicate issues need fixing, continuing conversation...`,
930
- );
959
+ // If Stop/SubagentStop hooks indicate we should continue (due to blocking errors),
960
+ // restart the AI conversation cycle
961
+ if (shouldContinue) {
962
+ logger?.info(
963
+ `${this.subagentType ? "SubagentStop" : "Stop"} hooks indicate issues need fixing, continuing conversation...`,
964
+ );
931
965
 
932
- // Restart the conversation to let AI fix the issues
933
- // Use recursionDepth = 0 to set loading false again for continuation
934
- await this.sendAIMessage({
935
- recursionDepth: 0,
936
- model,
937
- allowedRules,
938
- maxTokens,
939
- });
966
+ // Restart the conversation to let AI fix the issues
967
+ // Use recursionDepth = 0 to set loading false again for continuation
968
+ await this.sendAIMessage({
969
+ recursionDepth: 0,
970
+ model,
971
+ allowedRules,
972
+ maxTokens,
973
+ });
974
+ }
940
975
  }
941
976
  }
942
977
  }
@@ -2,10 +2,15 @@ import { spawn, type ChildProcess } from "child_process";
2
2
  import * as os from "os";
3
3
  import * as fs from "fs";
4
4
  import * as path from "path";
5
- import { BackgroundTask, BackgroundShell } from "../types/processes.js";
5
+ import {
6
+ BackgroundTask,
7
+ BackgroundShell,
8
+ BackgroundSubagent,
9
+ } from "../types/processes.js";
6
10
  import { stripAnsiColors } from "../utils/stringUtils.js";
7
11
  import { logger } from "../utils/globalLogger.js";
8
12
  import { Container } from "../utils/container.js";
13
+ import { NotificationQueue } from "./notificationQueue.js";
9
14
 
10
15
  export interface BackgroundTaskManagerCallbacks {
11
16
  onBackgroundTasksChange?: (tasks: BackgroundTask[]) => void;
@@ -30,6 +35,10 @@ export class BackgroundTaskManager {
30
35
  this.workdir = options.workdir;
31
36
  }
32
37
 
38
+ private get notificationQueue(): NotificationQueue {
39
+ return this.container.get<NotificationQueue>("NotificationQueue")!;
40
+ }
41
+
33
42
  private notifyTasksChange(): void {
34
43
  this.callbacks.onBackgroundTasksChange?.(Array.from(this.tasks.values()));
35
44
  }
@@ -153,6 +162,18 @@ export class BackgroundTaskManager {
153
162
  shell.endTime = Date.now();
154
163
  shell.runtime = shell.endTime - startTime;
155
164
  this.notifyTasksChange();
165
+
166
+ // Enqueue completion notification
167
+ const notificationQueue = this.container.has("NotificationQueue")
168
+ ? this.container.get<NotificationQueue>("NotificationQueue")
169
+ : undefined;
170
+ if (notificationQueue) {
171
+ const statusStr = shell.status;
172
+ const summary = `Command "${command}" ${statusStr} with exit code ${code ?? 0}`;
173
+ notificationQueue.enqueue(
174
+ `<task-notification>\n<task-id>${id}</task-id>\n<task-type>shell</task-type>\n<output-file>${logPath}</output-file>\n<status>${statusStr}</status>\n<summary>${summary}</summary>\n</task-notification>`,
175
+ );
176
+ }
156
177
  };
157
178
 
158
179
  const onError = (error: Error) => {
@@ -170,6 +191,17 @@ export class BackgroundTaskManager {
170
191
  shell.endTime = Date.now();
171
192
  shell.runtime = shell.endTime - startTime;
172
193
  this.notifyTasksChange();
194
+
195
+ // Enqueue error notification
196
+ const notificationQueue = this.container.has("NotificationQueue")
197
+ ? this.container.get<NotificationQueue>("NotificationQueue")
198
+ : undefined;
199
+ if (notificationQueue) {
200
+ const summary = `Command "${command}" failed with error: ${stripAnsiColors(error.message)}`;
201
+ notificationQueue.enqueue(
202
+ `<task-notification>\n<task-id>${id}</task-id>\n<task-type>shell</task-type>\n<output-file>${logPath}</output-file>\n<status>failed</status>\n<summary>${summary}</summary>\n</task-notification>`,
203
+ );
204
+ }
173
205
  };
174
206
 
175
207
  child.stdout?.on("data", onStdout);
@@ -278,6 +310,18 @@ export class BackgroundTaskManager {
278
310
  shell.endTime = Date.now();
279
311
  shell.runtime = shell.endTime - startTime;
280
312
  this.notifyTasksChange();
313
+
314
+ // Enqueue completion notification
315
+ const notificationQueue = this.container.has("NotificationQueue")
316
+ ? this.container.get<NotificationQueue>("NotificationQueue")
317
+ : undefined;
318
+ if (notificationQueue) {
319
+ const statusStr = shell.status;
320
+ const summary = `Command "${command}" ${statusStr} with exit code ${code ?? 0}`;
321
+ notificationQueue.enqueue(
322
+ `<task-notification>\n<task-id>${id}</task-id>\n<task-type>shell</task-type>\n<output-file>${logPath}</output-file>\n<status>${statusStr}</status>\n<summary>${summary}</summary>\n</task-notification>`,
323
+ );
324
+ }
281
325
  });
282
326
 
283
327
  child.on("error", (error) => {
@@ -292,6 +336,17 @@ export class BackgroundTaskManager {
292
336
  shell.endTime = Date.now();
293
337
  shell.runtime = shell.endTime - startTime;
294
338
  this.notifyTasksChange();
339
+
340
+ // Enqueue error notification
341
+ const notificationQueue = this.container.has("NotificationQueue")
342
+ ? this.container.get<NotificationQueue>("NotificationQueue")
343
+ : undefined;
344
+ if (notificationQueue) {
345
+ const summary = `Command "${command}" failed with error: ${stripAnsiColors(error.message)}`;
346
+ notificationQueue.enqueue(
347
+ `<task-notification>\n<task-id>${id}</task-id>\n<task-type>shell</task-type>\n<output-file>${logPath}</output-file>\n<status>failed</status>\n<summary>${summary}</summary>\n</task-notification>`,
348
+ );
349
+ }
295
350
  });
296
351
 
297
352
  return id;
@@ -371,6 +426,22 @@ export class BackgroundTaskManager {
371
426
  task.endTime = Date.now();
372
427
  task.runtime = task.endTime - task.startTime;
373
428
  this.notifyTasksChange();
429
+
430
+ // Enqueue killed notification
431
+ const notificationQueue = this.container.has("NotificationQueue")
432
+ ? this.container.get<NotificationQueue>("NotificationQueue")
433
+ : undefined;
434
+ if (notificationQueue) {
435
+ const description = (task as BackgroundSubagent).description || "";
436
+ const command = (task as BackgroundShell).command || "";
437
+ const summary =
438
+ task.type === "subagent"
439
+ ? `Agent task "${description}" was stopped`
440
+ : `Command "${command}" was stopped`;
441
+ notificationQueue.enqueue(
442
+ `<task-notification>\n<task-id>${id}</task-id>\n<task-type>${task.type}</task-type>\n<status>killed</status>\n<summary>${summary}</summary>\n</task-notification>`,
443
+ );
444
+ }
374
445
  return true;
375
446
  }
376
447
 
@@ -0,0 +1,193 @@
1
+ import { randomUUID } from "crypto";
2
+ import * as os from "os";
3
+ import * as fs from "fs";
4
+ import * as path from "path";
5
+ import type { Message } from "../types/index.js";
6
+ import { logger } from "../utils/globalLogger.js";
7
+ import { Container } from "../utils/container.js";
8
+ import { SubagentManager, type SubagentInstance } from "./subagentManager.js";
9
+ import type { PermissionMode } from "../types/permissions.js";
10
+
11
+ export interface ForkedAgentEntry {
12
+ id: string;
13
+ instance: SubagentInstance;
14
+ logPath: string;
15
+ logStream?: fs.WriteStream;
16
+ status: "running" | "completed" | "failed";
17
+ }
18
+
19
+ export interface ForkedAgentManagerCallbacks {
20
+ onForkedAgentStatusChange?: (entries: ForkedAgentEntry[]) => void;
21
+ }
22
+
23
+ export class ForkedAgentManager {
24
+ private activeForks = new Map<string, ForkedAgentEntry>();
25
+ private callbacks: ForkedAgentManagerCallbacks;
26
+
27
+ constructor(
28
+ private container: Container,
29
+ options: { callbacks?: ForkedAgentManagerCallbacks } = {},
30
+ ) {
31
+ this.callbacks = options.callbacks || {};
32
+ }
33
+
34
+ private get subagentManager(): SubagentManager {
35
+ return this.container.get<SubagentManager>("SubagentManager")!;
36
+ }
37
+
38
+ /**
39
+ * Creates a forked subagent with conversation history and executes it asynchronously (fire-and-forget).
40
+ * Does NOT interact with BackgroundTaskManager.
41
+ */
42
+ async forkAndExecute(
43
+ subagentType: string,
44
+ messages: Message[],
45
+ parameters: {
46
+ description: string;
47
+ allowedTools?: string[];
48
+ model?: string;
49
+ permissionModeOverride?: PermissionMode;
50
+ },
51
+ prompt: string,
52
+ ): Promise<string> {
53
+ const id = randomUUID();
54
+
55
+ // Create log file for debugging
56
+ const logPath = path.join(os.tmpdir(), `wave-forked-agent-${id}.log`);
57
+ const logStream = fs.createWriteStream(logPath, { flags: "a" });
58
+
59
+ const entry: ForkedAgentEntry = {
60
+ id,
61
+ instance: {} as SubagentInstance, // Temporary placeholder
62
+ logPath,
63
+ logStream,
64
+ status: "running",
65
+ };
66
+ this.activeForks.set(id, entry);
67
+
68
+ // Fire-and-forget execution
69
+ this.executeFork(entry, subagentType, messages, parameters, prompt).catch(
70
+ (error) => {
71
+ logger.error("Forked agent execution failed:", error);
72
+ },
73
+ );
74
+
75
+ return id;
76
+ }
77
+
78
+ private async executeFork(
79
+ entry: ForkedAgentEntry,
80
+ subagentType: string,
81
+ messages: Message[],
82
+ parameters: {
83
+ description: string;
84
+ allowedTools?: string[];
85
+ model?: string;
86
+ permissionModeOverride?: PermissionMode;
87
+ },
88
+ prompt: string,
89
+ ): Promise<void> {
90
+ try {
91
+ const configuration =
92
+ await this.subagentManager.findSubagent(subagentType);
93
+ if (!configuration) {
94
+ throw new Error(`Subagent type ${subagentType} not found`);
95
+ }
96
+
97
+ const instance = await this.subagentManager.createInstance(
98
+ configuration,
99
+ {
100
+ description: parameters.description,
101
+ subagent_type: subagentType,
102
+ prompt: "",
103
+ allowedTools: parameters.allowedTools,
104
+ model: parameters.model,
105
+ permissionModeOverride: parameters.permissionModeOverride,
106
+ },
107
+ false,
108
+ );
109
+
110
+ // Pre-load the message manager with conversation history
111
+ instance.messageManager.setMessages(messages);
112
+ instance.logStream = entry.logStream;
113
+
114
+ entry.instance = instance;
115
+
116
+ // Execute the agent asynchronously
117
+ const result = await this.subagentManager.executeAgent(
118
+ instance,
119
+ prompt,
120
+ undefined,
121
+ false, // NOT runInBackground — we handle logging ourselves
122
+ );
123
+
124
+ // Write final response and completion to log
125
+ if (entry.logStream) {
126
+ entry.logStream.write(
127
+ `[${new Date().toISOString()}] Final response:\n${result}\n`,
128
+ );
129
+ entry.logStream.write(
130
+ `[${new Date().toISOString()}] Agent completed successfully\n`,
131
+ );
132
+ entry.logStream.end();
133
+ }
134
+
135
+ entry.status = "completed";
136
+ this.notifyChange();
137
+ } catch (error) {
138
+ const errorMessage =
139
+ error instanceof Error ? error.message : String(error);
140
+
141
+ if (entry.logStream) {
142
+ entry.logStream.write(
143
+ `[${new Date().toISOString()}] Agent failed: ${errorMessage}\n`,
144
+ );
145
+ entry.logStream.end();
146
+ }
147
+
148
+ entry.status = "failed";
149
+ this.notifyChange();
150
+ }
151
+ }
152
+
153
+ /**
154
+ * Abort a running forked agent.
155
+ */
156
+ stop(id: string): boolean {
157
+ const entry = this.activeForks.get(id);
158
+ if (!entry) {
159
+ return false;
160
+ }
161
+
162
+ entry.instance?.aiManager?.abortAIMessage();
163
+ entry.logStream?.destroy();
164
+ this.activeForks.delete(id);
165
+ this.notifyChange();
166
+ return true;
167
+ }
168
+
169
+ /**
170
+ * Stop all running forked agents and clear the map.
171
+ */
172
+ cleanup(): void {
173
+ for (const [, entry] of this.activeForks) {
174
+ entry.instance?.aiManager?.abortAIMessage();
175
+ entry.logStream?.destroy();
176
+ }
177
+ this.activeForks.clear();
178
+ this.notifyChange();
179
+ }
180
+
181
+ /**
182
+ * Returns list of active forked agents.
183
+ */
184
+ getActiveForks(): ForkedAgentEntry[] {
185
+ return Array.from(this.activeForks.values());
186
+ }
187
+
188
+ private notifyChange(): void {
189
+ this.callbacks.onForkedAgentStatusChange?.(
190
+ Array.from(this.activeForks.values()),
191
+ );
192
+ }
193
+ }
@@ -9,8 +9,10 @@ import {
9
9
  completeBangInMessage,
10
10
  removeLastUserMessage,
11
11
  addToolBlockToMessageInMessages,
12
+ addNotificationMessageToMessages,
12
13
  UserMessageParams,
13
14
  type AgentToolBlockUpdateParams,
15
+ type AddNotificationMessageParams,
14
16
  generateMessageId,
15
17
  } from "../utils/messageOperations.js";
16
18
  import type { Message, Usage } from "../types/index.js";
@@ -56,6 +58,13 @@ export interface MessageManagerCallbacks {
56
58
  onFileHistoryBlockAdded?: (
57
59
  snapshots: import("../types/reversion.js").FileSnapshot[],
58
60
  ) => void;
61
+ // Notification callback
62
+ onNotificationMessageAdded?: (params: {
63
+ taskId: string;
64
+ taskType: "shell" | "agent";
65
+ status: "completed" | "failed" | "killed";
66
+ summary: string;
67
+ }) => void;
59
68
  }
60
69
 
61
70
  import { logger } from "../utils/globalLogger.js";
@@ -587,6 +596,22 @@ export class MessageManager {
587
596
  this.callbacks.onCompleteBangMessage?.(command, exitCode);
588
597
  }
589
598
 
599
+ public addNotificationMessage(
600
+ params: Omit<AddNotificationMessageParams, "messages">,
601
+ ): void {
602
+ const newMessages = addNotificationMessageToMessages({
603
+ messages: this.messages,
604
+ ...params,
605
+ });
606
+ this.setMessages(newMessages);
607
+ this.callbacks.onNotificationMessageAdded?.({
608
+ taskId: params.taskId,
609
+ taskType: params.taskType,
610
+ status: params.status,
611
+ summary: params.summary,
612
+ });
613
+ }
614
+
590
615
  /**
591
616
  * Rebuild usage array from messages containing usage metadata
592
617
  * Called during session restoration to reconstruct usage tracking