wave-agent-sdk 0.14.1 → 0.14.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 (92) hide show
  1. package/builtin/skills/settings/HOOKS.md +69 -0
  2. package/builtin/skills/settings/PLUGINS.md +171 -0
  3. package/builtin/skills/settings/SKILL.md +8 -3
  4. package/builtin/skills/settings/SUBAGENTS.md +21 -2
  5. package/dist/agent.d.ts +2 -2
  6. package/dist/agent.d.ts.map +1 -1
  7. package/dist/agent.js +12 -3
  8. package/dist/managers/aiManager.d.ts +6 -6
  9. package/dist/managers/aiManager.d.ts.map +1 -1
  10. package/dist/managers/aiManager.js +122 -59
  11. package/dist/managers/backgroundTaskManager.d.ts.map +1 -1
  12. package/dist/managers/backgroundTaskManager.js +28 -18
  13. package/dist/managers/hookManager.d.ts +16 -1
  14. package/dist/managers/hookManager.d.ts.map +1 -1
  15. package/dist/managers/hookManager.js +97 -8
  16. package/dist/managers/messageManager.d.ts +19 -4
  17. package/dist/managers/messageManager.d.ts.map +1 -1
  18. package/dist/managers/messageManager.js +63 -18
  19. package/dist/managers/pluginManager.d.ts +1 -0
  20. package/dist/managers/pluginManager.d.ts.map +1 -1
  21. package/dist/managers/pluginManager.js +7 -0
  22. package/dist/managers/subagentManager.d.ts +5 -0
  23. package/dist/managers/subagentManager.d.ts.map +1 -1
  24. package/dist/managers/subagentManager.js +35 -0
  25. package/dist/prompts/index.d.ts +1 -1
  26. package/dist/prompts/index.d.ts.map +1 -1
  27. package/dist/prompts/index.js +1 -1
  28. package/dist/services/MarketplaceService.d.ts +0 -11
  29. package/dist/services/MarketplaceService.d.ts.map +1 -1
  30. package/dist/services/MarketplaceService.js +21 -89
  31. package/dist/services/aiService.d.ts +3 -3
  32. package/dist/services/aiService.d.ts.map +1 -1
  33. package/dist/services/aiService.js +7 -7
  34. package/dist/services/hook.d.ts.map +1 -1
  35. package/dist/services/hook.js +15 -0
  36. package/dist/services/initializationService.d.ts.map +1 -1
  37. package/dist/services/initializationService.js +24 -1
  38. package/dist/services/interactionService.js +1 -1
  39. package/dist/services/pluginLoader.d.ts +5 -6
  40. package/dist/services/pluginLoader.d.ts.map +1 -1
  41. package/dist/services/pluginLoader.js +43 -53
  42. package/dist/services/session.d.ts +1 -1
  43. package/dist/services/session.js +7 -7
  44. package/dist/services/taskManager.d.ts +1 -1
  45. package/dist/services/taskManager.js +1 -1
  46. package/dist/types/core.d.ts +1 -1
  47. package/dist/types/core.d.ts.map +1 -1
  48. package/dist/types/hooks.d.ts +9 -1
  49. package/dist/types/hooks.d.ts.map +1 -1
  50. package/dist/types/hooks.js +2 -0
  51. package/dist/types/marketplace.d.ts +1 -26
  52. package/dist/types/marketplace.d.ts.map +1 -1
  53. package/dist/types/messaging.d.ts +3 -3
  54. package/dist/types/messaging.d.ts.map +1 -1
  55. package/dist/types/plugins.d.ts +3 -13
  56. package/dist/types/plugins.d.ts.map +1 -1
  57. package/dist/utils/convertMessagesForAPI.d.ts +1 -1
  58. package/dist/utils/convertMessagesForAPI.d.ts.map +1 -1
  59. package/dist/utils/convertMessagesForAPI.js +18 -7
  60. package/dist/utils/groupMessagesByApiRound.d.ts +1 -1
  61. package/dist/utils/groupMessagesByApiRound.js +6 -6
  62. package/dist/utils/messageOperations.d.ts.map +1 -1
  63. package/dist/utils/messageOperations.js +3 -3
  64. package/dist/utils/subagentParser.d.ts +8 -1
  65. package/dist/utils/subagentParser.d.ts.map +1 -1
  66. package/dist/utils/subagentParser.js +18 -3
  67. package/package.json +1 -1
  68. package/src/agent.ts +16 -3
  69. package/src/managers/aiManager.ts +142 -63
  70. package/src/managers/backgroundTaskManager.ts +32 -22
  71. package/src/managers/hookManager.ts +125 -10
  72. package/src/managers/messageManager.ts +76 -22
  73. package/src/managers/pluginManager.ts +10 -0
  74. package/src/managers/subagentManager.ts +47 -0
  75. package/src/prompts/index.ts +1 -1
  76. package/src/services/MarketplaceService.ts +26 -127
  77. package/src/services/aiService.ts +11 -11
  78. package/src/services/hook.ts +17 -0
  79. package/src/services/initializationService.ts +33 -1
  80. package/src/services/interactionService.ts +1 -1
  81. package/src/services/pluginLoader.ts +51 -67
  82. package/src/services/session.ts +7 -7
  83. package/src/services/taskManager.ts +1 -1
  84. package/src/types/core.ts +1 -1
  85. package/src/types/hooks.ts +16 -2
  86. package/src/types/marketplace.ts +1 -24
  87. package/src/types/messaging.ts +3 -3
  88. package/src/types/plugins.ts +3 -13
  89. package/src/utils/convertMessagesForAPI.ts +24 -9
  90. package/src/utils/groupMessagesByApiRound.ts +6 -6
  91. package/src/utils/messageOperations.ts +3 -5
  92. package/src/utils/subagentParser.ts +31 -4
@@ -12,6 +12,7 @@ import {
12
12
  type ExtendedHookExecutionContext,
13
13
  type HookExecutionResult,
14
14
  type HookValidationResult,
15
+ type SessionEndSource,
15
16
  HookConfigurationError,
16
17
  isValidHookEvent,
17
18
  isValidHookEventConfig,
@@ -80,9 +81,8 @@ export class HookManager {
80
81
  */
81
82
  loadConfigurationFromWaveConfig(waveConfig: WaveConfiguration | null): void {
82
83
  try {
83
- this.configuration = waveConfig?.hooks || undefined;
84
-
85
- // Validate the loaded configuration if it exists
84
+ // Merge Wave configuration hooks with existing plugin hooks
85
+ // (plugin hooks were registered earlier via registerPluginHooks)
86
86
  if (waveConfig?.hooks) {
87
87
  const validation = this.validatePartialConfiguration(waveConfig.hooks);
88
88
  if (!validation.valid) {
@@ -91,17 +91,18 @@ export class HookManager {
91
91
  validation.errors,
92
92
  );
93
93
  }
94
+ if (!this.configuration) {
95
+ this.configuration = {};
96
+ }
97
+ this.mergeHooksConfiguration(this.configuration, waveConfig.hooks);
94
98
  }
95
99
  } catch (error) {
96
- // If loading fails, start with undefined configuration (no hooks)
97
- this.configuration = undefined;
98
-
99
100
  // Re-throw configuration errors, but handle other errors gracefully
100
101
  if (error instanceof HookConfigurationError) {
101
102
  throw error;
102
103
  } else {
103
104
  logger?.warn(
104
- `[HookManager] Failed to load configuration, continuing with no hooks: ${(error as Error).message}`,
105
+ `[HookManager] Failed to load configuration, continuing with existing hooks: ${(error as Error).message}`,
105
106
  );
106
107
  }
107
108
  }
@@ -298,6 +299,8 @@ export class HookManager {
298
299
  source: MessageSource.HOOK,
299
300
  });
300
301
  }
302
+ // For SessionStart, stdout is processed separately in executeSessionStartHooks
303
+ // For SessionEnd, stdout is ignored (fire-and-forget cleanup)
301
304
  // For other hook types (PreToolUse, PostToolUse, Stop, PermissionRequest), ignore stdout
302
305
  }
303
306
 
@@ -374,6 +377,16 @@ export class HookManager {
374
377
  messageManager.addErrorBlock(errorMessage);
375
378
  return { shouldBlock: false };
376
379
 
380
+ case "SessionStart":
381
+ // Non-blocking for startup, show error in error block
382
+ messageManager.addErrorBlock(errorMessage);
383
+ return { shouldBlock: false };
384
+
385
+ case "SessionEnd":
386
+ // Blocking error (exit code 2): show in error block, don't block shutdown
387
+ messageManager.addErrorBlock(errorMessage);
388
+ return { shouldBlock: false };
389
+
377
390
  default:
378
391
  return { shouldBlock: false };
379
392
  }
@@ -577,7 +590,9 @@ export class HookManager {
577
590
  (event === "UserPromptSubmit" ||
578
591
  event === "Stop" ||
579
592
  event === "SubagentStop" ||
580
- event === "WorktreeCreate") &&
593
+ event === "WorktreeCreate" ||
594
+ event === "SessionStart" ||
595
+ event === "SessionEnd") &&
581
596
  context.toolName !== undefined
582
597
  ) {
583
598
  logger?.warn(
@@ -654,7 +669,9 @@ export class HookManager {
654
669
  event === "Stop" ||
655
670
  event === "SubagentStop" ||
656
671
  event === "WorktreeCreate" ||
657
- event === "CwdChanged"
672
+ event === "CwdChanged" ||
673
+ event === "SessionStart" ||
674
+ event === "SessionEnd"
658
675
  ) {
659
676
  return true;
660
677
  }
@@ -714,7 +731,9 @@ export class HookManager {
714
731
  (event === "UserPromptSubmit" ||
715
732
  event === "Stop" ||
716
733
  event === "SubagentStop" ||
717
- event === "WorktreeCreate") &&
734
+ event === "WorktreeCreate" ||
735
+ event === "SessionStart" ||
736
+ event === "SessionEnd") &&
718
737
  config.matcher
719
738
  ) {
720
739
  errors.push(`${prefix}: Event ${event} should not have a matcher`);
@@ -755,6 +774,8 @@ export class HookManager {
755
774
  PermissionRequest: 0,
756
775
  WorktreeCreate: 0,
757
776
  CwdChanged: 0,
777
+ SessionStart: 0,
778
+ SessionEnd: 0,
758
779
  },
759
780
  };
760
781
  }
@@ -768,6 +789,8 @@ export class HookManager {
768
789
  PermissionRequest: 0,
769
790
  WorktreeCreate: 0,
770
791
  CwdChanged: 0,
792
+ SessionStart: 0,
793
+ SessionEnd: 0,
771
794
  };
772
795
 
773
796
  let totalConfigs = 0;
@@ -844,4 +867,96 @@ export class HookManager {
844
867
 
845
868
  this.mergeHooksConfiguration(this.configuration, stampedHooks);
846
869
  }
870
+
871
+ /**
872
+ * Execute SessionStart hooks during initialization.
873
+ * Collects additionalContext and initialUserMessage from hook stdout.
874
+ */
875
+ async executeSessionStartHooks(
876
+ source: "startup" | "resume" | "compact",
877
+ sessionId: string,
878
+ transcriptPath: string,
879
+ agentType?: string,
880
+ ): Promise<{
881
+ results: HookExecutionResult[];
882
+ additionalContext?: string;
883
+ initialUserMessage?: string;
884
+ }> {
885
+ const context: ExtendedHookExecutionContext = {
886
+ event: "SessionStart",
887
+ projectDir: this.workdir,
888
+ timestamp: new Date(),
889
+ sessionId,
890
+ transcriptPath,
891
+ cwd: this.workdir,
892
+ source,
893
+ agentType,
894
+ env: Object.fromEntries(
895
+ Object.entries(process.env).filter((e) => e[1] !== undefined),
896
+ ) as Record<string, string>,
897
+ };
898
+
899
+ const results = await this.executeHooks("SessionStart", context);
900
+
901
+ let additionalContext: string | undefined;
902
+ let initialUserMessage: string | undefined;
903
+
904
+ // Process stdout from successful hooks
905
+ for (const result of results) {
906
+ if (result.success && result.stdout?.trim()) {
907
+ const trimmed = result.stdout.trim();
908
+ // Try to parse as JSON for structured output
909
+ try {
910
+ const parsed = JSON.parse(trimmed);
911
+ if (parsed.hookSpecificOutput?.additionalContext) {
912
+ additionalContext =
913
+ (additionalContext ? additionalContext + "\n" : "") +
914
+ parsed.hookSpecificOutput.additionalContext;
915
+ }
916
+ if (parsed.initialUserMessage) {
917
+ initialUserMessage = parsed.initialUserMessage;
918
+ }
919
+ } catch {
920
+ // Not JSON, treat as additional context
921
+ additionalContext =
922
+ (additionalContext ? additionalContext + "\n" : "") + trimmed;
923
+ }
924
+ }
925
+ }
926
+
927
+ return { results, additionalContext, initialUserMessage };
928
+ }
929
+
930
+ /**
931
+ * Execute SessionEnd hooks during agent destruction.
932
+ * Non-blocking: always continues shutdown even if hooks fail.
933
+ * No stdout processing needed (SessionEnd hooks are fire-and-forget cleanup).
934
+ */
935
+ async executeSessionEndHooks(
936
+ source: SessionEndSource,
937
+ sessionId: string,
938
+ transcriptPath: string,
939
+ ): Promise<HookExecutionResult[]> {
940
+ const context: ExtendedHookExecutionContext = {
941
+ event: "SessionEnd",
942
+ projectDir: this.workdir,
943
+ timestamp: new Date(),
944
+ sessionId,
945
+ transcriptPath,
946
+ cwd: this.workdir,
947
+ endSource: source,
948
+ env: Object.fromEntries(
949
+ Object.entries(process.env).filter((e) => e[1] !== undefined),
950
+ ) as Record<string, string>,
951
+ };
952
+
953
+ const results = await this.executeHooks("SessionEnd", context);
954
+
955
+ // Process results but never block shutdown
956
+ if (results.length > 0) {
957
+ this.processHookResults("SessionEnd", results);
958
+ }
959
+
960
+ return results;
961
+ }
847
962
  }
@@ -47,8 +47,8 @@ export interface MessageManagerCallbacks {
47
47
  onAssistantReasoningUpdated?: (chunk: string, accumulated: string) => void;
48
48
  onToolBlockUpdated?: (params: AgentToolBlockUpdateParams) => void;
49
49
  onErrorBlockAdded?: (error: string) => void;
50
- onCompressBlockAdded?: (content: string) => void;
51
- onCompressionStateChange?: (isCompressing: boolean) => void;
50
+ onCompactBlockAdded?: (content: string) => void;
51
+ onCompactionStateChange?: (isCompacting: boolean) => void;
52
52
  // Bang callback
53
53
  onAddBangMessage?: (command: string) => void;
54
54
  onUpdateBangMessage?: (command: string, output: string) => void;
@@ -92,6 +92,8 @@ export class MessageManager {
92
92
  private filesInContext: Set<string> = new Set(); // Track files mentioned in the conversation
93
93
  private recentFileReads: Map<string, { content: string; timestamp: number }> =
94
94
  new Map(); // Track file read contents
95
+ private invokedSkills: Map<string, { skillName: string; timestamp: number }> =
96
+ new Map(); // Track invoked skill names
95
97
  private sessionType: "main" | "subagent";
96
98
  private subagentType?: string;
97
99
  private _usages: Usage[] = [];
@@ -270,12 +272,14 @@ export class MessageManager {
270
272
  for (const message of newMessages) {
271
273
  this.addPathsFromMessage(message);
272
274
  this.extractFileReadsFromMessage(message);
275
+ this.extractSkillInvocationsFromMessage(message);
273
276
  }
274
277
 
275
278
  // Also check if the last message was updated (common for tool blocks)
276
279
  if (messages.length > 0 && messages.length === oldLength) {
277
280
  this.addPathsFromMessage(messages[messages.length - 1]);
278
281
  this.extractFileReadsFromMessage(messages[messages.length - 1]);
282
+ this.extractSkillInvocationsFromMessage(messages[messages.length - 1]);
279
283
  }
280
284
 
281
285
  this.callbacks.onMessagesChange?.([...messages]);
@@ -494,31 +498,31 @@ export class MessageManager {
494
498
  }
495
499
 
496
500
  /**
497
- * Compress messages and update session, delete compressed messages, only keep compressed messages and last 3 messages
501
+ * Compact messages and update session, delete compacted messages, only keep compacted messages and last 3 messages
498
502
  */
499
- public compressMessagesAndUpdateSession(
500
- compressedContent: string,
503
+ public compactMessagesAndUpdateSession(
504
+ compactedContent: string,
501
505
  usage?: Usage,
502
506
  ): void {
503
507
  // Get last 2 API rounds to preserve (structurally safe boundary)
504
508
  const lastThreeMessages = getLastApiRounds(this.messages, 2);
505
509
 
506
- // Create compressed message
507
- const compressMessage: Message = {
510
+ // Create compacted message
511
+ const compactMessage: Message = {
508
512
  id: generateMessageId(),
509
513
  role: "assistant",
510
514
  blocks: [
511
515
  {
512
- type: "compress",
513
- content: compressedContent,
516
+ type: "compact",
517
+ content: compactedContent,
514
518
  sessionId: this.sessionId,
515
519
  },
516
520
  ],
517
521
  ...(usage && { usage }),
518
522
  };
519
523
 
520
- // Build new message array: keep the compressed message and last 3 messages
521
- const newMessages: Message[] = [compressMessage, ...lastThreeMessages];
524
+ // Build new message array: keep the compacted message and last 3 messages
525
+ const newMessages: Message[] = [compactMessage, ...lastThreeMessages];
522
526
 
523
527
  // Update sessionId and parentSessionId
524
528
  const oldSessionId = this.sessionId;
@@ -539,15 +543,15 @@ export class MessageManager {
539
543
  this.addPathsFromMessage(message);
540
544
  }
541
545
 
542
- // Scan compressedContent for file mentions
546
+ // Scan compactedContent for file mentions
543
547
  const fileMentionRegex = /(?:^|\s)@([\w.\-/]+)/g;
544
548
  let match;
545
- while ((match = fileMentionRegex.exec(compressedContent)) !== null) {
549
+ while ((match = fileMentionRegex.exec(compactedContent)) !== null) {
546
550
  this.touchFile(match[1]);
547
551
  }
548
552
 
549
- // Trigger compression callback
550
- this.callbacks.onCompressBlockAdded?.(compressedContent);
553
+ // Trigger compaction callback
554
+ this.callbacks.onCompactBlockAdded?.(compactedContent);
551
555
  }
552
556
 
553
557
  public addFileHistoryBlock(
@@ -861,7 +865,7 @@ export class MessageManager {
861
865
  let targetSessionId = this.sessionId;
862
866
  let targetIndexInSession = index;
863
867
 
864
- // We need to be careful here because loadFullMessageThread might have removed "compress" blocks
868
+ // We need to be careful here because loadFullMessageThread might have removed "compact" blocks
865
869
  // Let's re-calculate based on the actual messages returned.
866
870
  // Actually, it's easier to just load sessions one by one again or keep track of counts.
867
871
 
@@ -883,19 +887,19 @@ export class MessageManager {
883
887
  if (!sessionData) continue;
884
888
 
885
889
  const sessionMessages = sessionData.messages;
886
- // If this is not the first session in the thread, it might have a compress block at the start
890
+ // If this is not the first session in the thread, it might have a compact block at the start
887
891
  // that was removed in getFullMessageThread.
888
- const hasCompressBlock = sessionMessages[0]?.blocks.some(
889
- (b) => b.type === "compress",
892
+ const hasCompactBlock = sessionMessages[0]?.blocks.some(
893
+ (b) => b.type === "compact",
890
894
  );
891
895
  const effectiveMessages =
892
- hasCompressBlock && sid !== sessionIds[0]
896
+ hasCompactBlock && sid !== sessionIds[0]
893
897
  ? sessionMessages.slice(1)
894
898
  : sessionMessages;
895
899
 
896
900
  if (remainingIndex < effectiveMessages.length) {
897
901
  targetSessionId = sid;
898
- targetIndexInSession = hasCompressBlock
902
+ targetIndexInSession = hasCompactBlock
899
903
  ? remainingIndex + 1
900
904
  : remainingIndex;
901
905
  break;
@@ -938,7 +942,7 @@ export class MessageManager {
938
942
 
939
943
  // Update in-memory messages to the truncated session messages
940
944
  // We do NOT include ancestor messages here to avoid exceeding context limits.
941
- // The 'compress' block at the start of the session (if any) already summarizes them.
945
+ // The 'compact' block at the start of the session (if any) already summarizes them.
942
946
  this.setMessages(newMessagesInSession);
943
947
 
944
948
  // Update saved message count
@@ -1056,4 +1060,54 @@ export class MessageManager {
1056
1060
  }
1057
1061
  return result;
1058
1062
  }
1063
+
1064
+ /**
1065
+ * Extract skill invocations from tool blocks in a message.
1066
+ */
1067
+ private extractSkillInvocationsFromMessage(message: Message): void {
1068
+ for (const block of message.blocks) {
1069
+ if (
1070
+ block.type === "tool" &&
1071
+ block.name === "Skill" &&
1072
+ block.stage === "end" &&
1073
+ block.parameters
1074
+ ) {
1075
+ try {
1076
+ const params = JSON.parse(block.parameters) as Record<
1077
+ string,
1078
+ unknown
1079
+ >;
1080
+ const skillName = params.skill_name as string | undefined;
1081
+ if (skillName) {
1082
+ this.invokedSkills.set(skillName, {
1083
+ skillName,
1084
+ timestamp: Date.now(),
1085
+ });
1086
+ }
1087
+ } catch {
1088
+ // Ignore parse errors
1089
+ }
1090
+ }
1091
+ }
1092
+ }
1093
+
1094
+ /**
1095
+ * Get recently invoked skill names, sorted by timestamp (newest first).
1096
+ * @param maxSkills - Maximum number of skills to return
1097
+ * @returns Array of skill names sorted by recency
1098
+ */
1099
+ public getInvokedSkillNames(maxSkills = 10): string[] {
1100
+ const sorted = Array.from(this.invokedSkills.entries())
1101
+ .sort(([, a], [, b]) => b.timestamp - a.timestamp)
1102
+ .slice(0, maxSkills);
1103
+
1104
+ return sorted.map(([, { skillName }]) => skillName);
1105
+ }
1106
+
1107
+ /**
1108
+ * Clear all invoked skills (e.g., after compaction).
1109
+ */
1110
+ public clearInvokedSkills(): void {
1111
+ this.invokedSkills.clear();
1112
+ }
1059
1113
  }
@@ -7,6 +7,7 @@ import { HookManager } from "./hookManager.js";
7
7
  import { LspManager } from "./lspManager.js";
8
8
  import { McpManager } from "./mcpManager.js";
9
9
  import { SlashCommandManager } from "./slashCommandManager.js";
10
+ import { SubagentManager } from "./subagentManager.js";
10
11
  import { MarketplaceService } from "../services/MarketplaceService.js";
11
12
  import { ConfigurationService } from "../services/configurationService.js";
12
13
  import { Container } from "../utils/container.js";
@@ -53,6 +54,10 @@ export class PluginManager {
53
54
  return this.container.get<ConfigurationService>("ConfigurationService");
54
55
  }
55
56
 
57
+ private get subagentManager(): SubagentManager | undefined {
58
+ return this.container.get<SubagentManager>("SubagentManager");
59
+ }
60
+
56
61
  /**
57
62
  * Update enabled plugins configuration
58
63
  */
@@ -155,6 +160,7 @@ export class PluginManager {
155
160
  path: absolutePath,
156
161
  commands: PluginLoader.loadCommands(absolutePath),
157
162
  skills: await PluginLoader.loadSkills(absolutePath),
163
+ agents: await PluginLoader.loadAgents(absolutePath),
158
164
  lspConfig: await PluginLoader.loadLspConfig(absolutePath),
159
165
  mcpConfig: await PluginLoader.loadMcpConfig(absolutePath),
160
166
  hooksConfig: await PluginLoader.loadHooksConfig(absolutePath),
@@ -192,6 +198,10 @@ export class PluginManager {
192
198
  this.hookManager.registerPluginHooks(plugin.path, plugin.hooksConfig);
193
199
  }
194
200
 
201
+ if (this.subagentManager && plugin.agents.length > 0) {
202
+ this.subagentManager.registerPluginAgents(plugin.name, plugin.agents);
203
+ }
204
+
195
205
  this.plugins.set(manifest.name, plugin);
196
206
  logger?.info(`Loaded plugin: ${manifest.name} v${manifest.version}`);
197
207
  } catch (error) {
@@ -187,10 +187,57 @@ export class SubagentManager {
187
187
  * Find subagent by exact name match
188
188
  */
189
189
  async findSubagent(name: string) {
190
+ // Check cached configurations first (includes plugin agents)
191
+ if (this.cachedConfigurations !== null) {
192
+ const cached = this.cachedConfigurations.find(
193
+ (config) => config.name === name,
194
+ );
195
+ if (cached) return cached;
196
+ }
197
+ // Fall back to filesystem scan for non-plugin agents
190
198
  const { findSubagentByName } = await import("../utils/subagentParser.js");
191
199
  return findSubagentByName(name, this.workdir);
192
200
  }
193
201
 
202
+ /**
203
+ * Register plugin agents into the cached configurations.
204
+ * Names each agent as `pluginName:agentName` to avoid collisions.
205
+ */
206
+ registerPluginAgents(
207
+ pluginName: string,
208
+ agents: SubagentConfiguration[],
209
+ ): void {
210
+ if (this.cachedConfigurations === null) {
211
+ // Should not happen if initialization order is correct
212
+ this.cachedConfigurations = [];
213
+ }
214
+
215
+ // Remove any previously registered agents for this plugin (by name prefix)
216
+ this.cachedConfigurations = this.cachedConfigurations.filter(
217
+ (config) => !config.name.startsWith(`${pluginName}:`),
218
+ );
219
+
220
+ for (const agent of agents) {
221
+ const namespacedName = `${pluginName}:${agent.name}`;
222
+ const namespacedAgent: SubagentConfiguration = {
223
+ ...agent,
224
+ name: namespacedName,
225
+ // Safety net: substitute any remaining ${WAVE_PLUGIN_ROOT} placeholders
226
+ systemPrompt: agent.systemPrompt.replace(
227
+ /\$\{WAVE_PLUGIN_ROOT\}/g,
228
+ agent.pluginRoot ?? "",
229
+ ),
230
+ };
231
+ this.cachedConfigurations!.push(namespacedAgent);
232
+ }
233
+
234
+ // Re-sort by priority then name
235
+ this.cachedConfigurations!.sort((a, b) => {
236
+ if (a.priority !== b.priority) return a.priority - b.priority;
237
+ return a.name.localeCompare(b.name);
238
+ });
239
+ }
240
+
194
241
  /**
195
242
  * Create a new subagent instance with isolated managers
196
243
  */
@@ -179,7 +179,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the
179
179
 
180
180
  export const DEFAULT_SYSTEM_PROMPT = BASE_SYSTEM_PROMPT;
181
181
 
182
- export const COMPRESS_MESSAGES_SYSTEM_PROMPT = `You are continuing work on a software engineering task. Write a detailed continuation summary that will allow you (or another instance of yourself) to resume work efficiently in a future context window where the conversation history will be replaced with this summary.
182
+ export const COMPACT_MESSAGES_SYSTEM_PROMPT = `You are continuing work on a software engineering task. Write a detailed continuation summary that will allow you (or another instance of yourself) to resume work efficiently in a future context window where the conversation history will be replaced with this summary.
183
183
 
184
184
  First, write your analysis in <analysis> tags as a thinking scratchpad:
185
185
  - Chronologically review the conversation