wave-agent-sdk 0.0.6 → 0.0.8

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 (180) hide show
  1. package/dist/agent.d.ts +32 -20
  2. package/dist/agent.d.ts.map +1 -1
  3. package/dist/agent.js +209 -24
  4. package/dist/constants/events.d.ts +28 -0
  5. package/dist/constants/events.d.ts.map +1 -0
  6. package/dist/constants/events.js +27 -0
  7. package/dist/index.d.ts +2 -0
  8. package/dist/index.d.ts.map +1 -1
  9. package/dist/index.js +2 -0
  10. package/dist/managers/aiManager.d.ts +34 -1
  11. package/dist/managers/aiManager.d.ts.map +1 -1
  12. package/dist/managers/aiManager.js +248 -132
  13. package/dist/managers/backgroundBashManager.d.ts.map +1 -1
  14. package/dist/managers/backgroundBashManager.js +7 -6
  15. package/dist/managers/hookManager.d.ts +13 -16
  16. package/dist/managers/hookManager.d.ts.map +1 -1
  17. package/dist/managers/hookManager.js +81 -44
  18. package/dist/managers/liveConfigManager.d.ts +58 -0
  19. package/dist/managers/liveConfigManager.d.ts.map +1 -0
  20. package/dist/managers/liveConfigManager.js +160 -0
  21. package/dist/managers/messageManager.d.ts +41 -24
  22. package/dist/managers/messageManager.d.ts.map +1 -1
  23. package/dist/managers/messageManager.js +168 -49
  24. package/dist/managers/slashCommandManager.d.ts.map +1 -1
  25. package/dist/managers/slashCommandManager.js +9 -3
  26. package/dist/managers/subagentManager.d.ts +51 -0
  27. package/dist/managers/subagentManager.d.ts.map +1 -1
  28. package/dist/managers/subagentManager.js +190 -19
  29. package/dist/services/aiService.d.ts +13 -5
  30. package/dist/services/aiService.d.ts.map +1 -1
  31. package/dist/services/aiService.js +350 -74
  32. package/dist/services/configurationWatcher.d.ts +120 -0
  33. package/dist/services/configurationWatcher.d.ts.map +1 -0
  34. package/dist/services/configurationWatcher.js +439 -0
  35. package/dist/services/fileWatcher.d.ts +69 -0
  36. package/dist/services/fileWatcher.d.ts.map +1 -0
  37. package/dist/services/fileWatcher.js +213 -0
  38. package/dist/services/hook.d.ts +91 -9
  39. package/dist/services/hook.d.ts.map +1 -1
  40. package/dist/services/hook.js +393 -43
  41. package/dist/services/jsonlHandler.d.ts +62 -0
  42. package/dist/services/jsonlHandler.d.ts.map +1 -0
  43. package/dist/services/jsonlHandler.js +257 -0
  44. package/dist/services/memory.d.ts +9 -0
  45. package/dist/services/memory.d.ts.map +1 -1
  46. package/dist/services/memory.js +81 -12
  47. package/dist/services/memoryStore.d.ts +81 -0
  48. package/dist/services/memoryStore.d.ts.map +1 -0
  49. package/dist/services/memoryStore.js +200 -0
  50. package/dist/services/session.d.ts +64 -49
  51. package/dist/services/session.d.ts.map +1 -1
  52. package/dist/services/session.js +310 -132
  53. package/dist/tools/bashTool.d.ts.map +1 -1
  54. package/dist/tools/bashTool.js +5 -4
  55. package/dist/tools/deleteFileTool.d.ts.map +1 -1
  56. package/dist/tools/deleteFileTool.js +2 -1
  57. package/dist/tools/editTool.d.ts.map +1 -1
  58. package/dist/tools/editTool.js +3 -2
  59. package/dist/tools/multiEditTool.d.ts.map +1 -1
  60. package/dist/tools/multiEditTool.js +4 -3
  61. package/dist/tools/readTool.d.ts.map +1 -1
  62. package/dist/tools/readTool.js +2 -1
  63. package/dist/tools/todoWriteTool.d.ts.map +1 -1
  64. package/dist/tools/todoWriteTool.js +3 -10
  65. package/dist/tools/writeTool.d.ts.map +1 -1
  66. package/dist/tools/writeTool.js +5 -6
  67. package/dist/types/commands.d.ts +4 -0
  68. package/dist/types/commands.d.ts.map +1 -1
  69. package/dist/types/core.d.ts +35 -0
  70. package/dist/types/core.d.ts.map +1 -1
  71. package/dist/types/environment.d.ts +42 -0
  72. package/dist/types/environment.d.ts.map +1 -0
  73. package/dist/types/environment.js +21 -0
  74. package/dist/types/hooks.d.ts +8 -2
  75. package/dist/types/hooks.d.ts.map +1 -1
  76. package/dist/types/hooks.js +8 -2
  77. package/dist/types/index.d.ts +2 -0
  78. package/dist/types/index.d.ts.map +1 -1
  79. package/dist/types/index.js +2 -0
  80. package/dist/types/memoryStore.d.ts +82 -0
  81. package/dist/types/memoryStore.d.ts.map +1 -0
  82. package/dist/types/memoryStore.js +7 -0
  83. package/dist/types/messaging.d.ts +21 -9
  84. package/dist/types/messaging.d.ts.map +1 -1
  85. package/dist/types/messaging.js +5 -1
  86. package/dist/types/session.d.ts +20 -0
  87. package/dist/types/session.d.ts.map +1 -0
  88. package/dist/types/session.js +7 -0
  89. package/dist/utils/bashHistory.d.ts.map +1 -1
  90. package/dist/utils/bashHistory.js +27 -26
  91. package/dist/utils/cacheControlUtils.d.ts +121 -0
  92. package/dist/utils/cacheControlUtils.d.ts.map +1 -0
  93. package/dist/utils/cacheControlUtils.js +367 -0
  94. package/dist/utils/commandPathResolver.d.ts +52 -0
  95. package/dist/utils/commandPathResolver.d.ts.map +1 -0
  96. package/dist/utils/commandPathResolver.js +145 -0
  97. package/dist/utils/configPaths.d.ts +85 -0
  98. package/dist/utils/configPaths.d.ts.map +1 -0
  99. package/dist/utils/configPaths.js +121 -0
  100. package/dist/utils/configResolver.d.ts +37 -10
  101. package/dist/utils/configResolver.d.ts.map +1 -1
  102. package/dist/utils/configResolver.js +127 -23
  103. package/dist/utils/constants.d.ts +1 -1
  104. package/dist/utils/constants.js +1 -1
  105. package/dist/utils/convertMessagesForAPI.d.ts.map +1 -1
  106. package/dist/utils/convertMessagesForAPI.js +8 -13
  107. package/dist/utils/customCommands.d.ts.map +1 -1
  108. package/dist/utils/customCommands.js +66 -21
  109. package/dist/utils/fileUtils.d.ts +15 -0
  110. package/dist/utils/fileUtils.d.ts.map +1 -0
  111. package/dist/utils/fileUtils.js +61 -0
  112. package/dist/utils/globalLogger.d.ts +102 -0
  113. package/dist/utils/globalLogger.d.ts.map +1 -0
  114. package/dist/utils/globalLogger.js +136 -0
  115. package/dist/utils/hookMatcher.d.ts +1 -6
  116. package/dist/utils/hookMatcher.d.ts.map +1 -1
  117. package/dist/utils/mcpUtils.d.ts.map +1 -1
  118. package/dist/utils/mcpUtils.js +25 -3
  119. package/dist/utils/messageOperations.d.ts +27 -27
  120. package/dist/utils/messageOperations.d.ts.map +1 -1
  121. package/dist/utils/messageOperations.js +46 -36
  122. package/dist/utils/pathEncoder.d.ts +104 -0
  123. package/dist/utils/pathEncoder.d.ts.map +1 -0
  124. package/dist/utils/pathEncoder.js +272 -0
  125. package/dist/utils/subagentParser.d.ts.map +1 -1
  126. package/dist/utils/subagentParser.js +2 -1
  127. package/dist/utils/tokenCalculation.d.ts +26 -0
  128. package/dist/utils/tokenCalculation.d.ts.map +1 -0
  129. package/dist/utils/tokenCalculation.js +36 -0
  130. package/package.json +6 -3
  131. package/src/agent.ts +301 -37
  132. package/src/constants/events.ts +38 -0
  133. package/src/index.ts +2 -0
  134. package/src/managers/aiManager.ts +325 -173
  135. package/src/managers/backgroundBashManager.ts +7 -6
  136. package/src/managers/hookManager.ts +106 -84
  137. package/src/managers/liveConfigManager.ts +248 -0
  138. package/src/managers/messageManager.ts +237 -100
  139. package/src/managers/slashCommandManager.ts +9 -7
  140. package/src/managers/subagentManager.ts +284 -22
  141. package/src/services/aiService.ts +474 -83
  142. package/src/services/configurationWatcher.ts +622 -0
  143. package/src/services/fileWatcher.ts +301 -0
  144. package/src/services/hook.ts +538 -47
  145. package/src/services/jsonlHandler.ts +319 -0
  146. package/src/services/memory.ts +92 -12
  147. package/src/services/memoryStore.ts +279 -0
  148. package/src/services/session.ts +381 -157
  149. package/src/tools/bashTool.ts +5 -4
  150. package/src/tools/deleteFileTool.ts +2 -1
  151. package/src/tools/editTool.ts +3 -2
  152. package/src/tools/multiEditTool.ts +4 -3
  153. package/src/tools/readTool.ts +2 -1
  154. package/src/tools/todoWriteTool.ts +3 -11
  155. package/src/tools/writeTool.ts +7 -6
  156. package/src/types/commands.ts +6 -0
  157. package/src/types/core.ts +44 -0
  158. package/src/types/environment.ts +60 -0
  159. package/src/types/hooks.ts +21 -8
  160. package/src/types/index.ts +2 -0
  161. package/src/types/memoryStore.ts +94 -0
  162. package/src/types/messaging.ts +21 -10
  163. package/src/types/session.ts +25 -0
  164. package/src/utils/bashHistory.ts +27 -27
  165. package/src/utils/cacheControlUtils.ts +540 -0
  166. package/src/utils/commandPathResolver.ts +189 -0
  167. package/src/utils/configPaths.ts +163 -0
  168. package/src/utils/configResolver.ts +182 -22
  169. package/src/utils/constants.ts +1 -1
  170. package/src/utils/convertMessagesForAPI.ts +8 -14
  171. package/src/utils/customCommands.ts +90 -22
  172. package/src/utils/fileUtils.ts +65 -0
  173. package/src/utils/globalLogger.ts +145 -0
  174. package/src/utils/hookMatcher.ts +1 -12
  175. package/src/utils/mcpUtils.ts +34 -3
  176. package/src/utils/messageOperations.ts +77 -60
  177. package/src/utils/pathEncoder.ts +379 -0
  178. package/src/utils/subagentParser.ts +2 -1
  179. package/src/utils/tokenCalculation.ts +43 -0
  180. package/src/types/index.ts.backup +0 -357
@@ -1,6 +1,7 @@
1
1
  import { callAgent, compressMessages } from "../services/aiService.js";
2
2
  import { getMessagesToCompress } from "../utils/messageOperations.js";
3
3
  import { convertMessagesForAPI } from "../utils/convertMessagesForAPI.js";
4
+ import { calculateComprehensiveTotalTokens } from "../utils/tokenCalculation.js";
4
5
  import * as memory from "../services/memory.js";
5
6
  import type {
6
7
  Logger,
@@ -30,6 +31,7 @@ export interface AIManagerOptions {
30
31
  callbacks?: AIManagerCallbacks;
31
32
  workdir: string;
32
33
  systemPrompt?: string;
34
+ subagentType?: string; // Optional subagent type for hook context
33
35
  // Resolved configuration
34
36
  gatewayConfig: GatewayConfig;
35
37
  modelConfig: ModelConfig;
@@ -47,6 +49,7 @@ export class AIManager {
47
49
  private hookManager?: HookManager;
48
50
  private workdir: string;
49
51
  private systemPrompt?: string;
52
+ private subagentType?: string; // Store subagent type for hook context
50
53
 
51
54
  // Configuration properties
52
55
  private gatewayConfig: GatewayConfig;
@@ -61,6 +64,7 @@ export class AIManager {
61
64
  this.logger = options.logger;
62
65
  this.workdir = options.workdir;
63
66
  this.systemPrompt = options.systemPrompt;
67
+ this.subagentType = options.subagentType; // Store subagent type
64
68
  this.callbacks = options.callbacks ?? {};
65
69
 
66
70
  // Store resolved configuration
@@ -72,6 +76,74 @@ export class AIManager {
72
76
  private isCompressing: boolean = false;
73
77
  private callbacks: AIManagerCallbacks;
74
78
 
79
+ /**
80
+ * Update gateway configuration at runtime for live config reload
81
+ * @param newConfig - New gateway configuration
82
+ */
83
+ updateGatewayConfig(newConfig: GatewayConfig): void {
84
+ this.logger?.info(
85
+ `Live Config: Updating AIManager gateway config - baseURL: ${newConfig.baseURL}`,
86
+ );
87
+ this.gatewayConfig = newConfig;
88
+ }
89
+
90
+ /**
91
+ * Update model configuration at runtime for live config reload
92
+ * @param newConfig - New model configuration
93
+ */
94
+ updateModelConfig(newConfig: ModelConfig): void {
95
+ this.logger?.info(
96
+ `Live Config: Updating AIManager model config - agent: ${newConfig.agentModel}, fast: ${newConfig.fastModel}`,
97
+ );
98
+ this.modelConfig = newConfig;
99
+ }
100
+
101
+ /**
102
+ * Update token limit at runtime for live config reload
103
+ * @param newLimit - New token limit
104
+ */
105
+ updateTokenLimit(newLimit: number): void {
106
+ this.logger?.info(
107
+ `Live Config: Updating AIManager token limit: ${newLimit}`,
108
+ );
109
+ this.tokenLimit = newLimit;
110
+ }
111
+
112
+ /**
113
+ * Update all configurations at once for live config reload
114
+ * @param newGatewayConfig - New gateway configuration
115
+ * @param newModelConfig - New model configuration
116
+ * @param newTokenLimit - New token limit
117
+ */
118
+ updateConfiguration(
119
+ newGatewayConfig: GatewayConfig,
120
+ newModelConfig: ModelConfig,
121
+ newTokenLimit: number,
122
+ ): void {
123
+ this.logger?.info("Live Config: Updating all AIManager configuration");
124
+ this.gatewayConfig = newGatewayConfig;
125
+ this.modelConfig = newModelConfig;
126
+ this.tokenLimit = newTokenLimit;
127
+ this.logger?.info(
128
+ `Live Config: Configuration updated - model: ${newModelConfig.agentModel}, tokenLimit: ${newTokenLimit}`,
129
+ );
130
+ }
131
+
132
+ /**
133
+ * Get current configuration for debugging
134
+ */
135
+ getCurrentConfiguration(): {
136
+ gatewayConfig: GatewayConfig;
137
+ modelConfig: ModelConfig;
138
+ tokenLimit: number;
139
+ } {
140
+ return {
141
+ gatewayConfig: { ...this.gatewayConfig },
142
+ modelConfig: { ...this.modelConfig },
143
+ tokenLimit: this.tokenLimit,
144
+ };
145
+ }
146
+
75
147
  /**
76
148
  * Get filtered tool configuration
77
149
  */
@@ -117,7 +189,7 @@ export class AIManager {
117
189
  private generateCompactParams(
118
190
  toolName: string,
119
191
  toolArgs: Record<string, unknown>,
120
- ): string | undefined {
192
+ ): string {
121
193
  try {
122
194
  const toolPlugin = this.toolManager
123
195
  .list()
@@ -131,21 +203,27 @@ export class AIManager {
131
203
  } catch (error) {
132
204
  this.logger?.warn("Failed to generate compactParams", error);
133
205
  }
134
- return undefined;
206
+ return "";
135
207
  }
136
208
 
137
209
  // Private method to handle token statistics and message compression
138
210
  private async handleTokenUsageAndCompression(
139
- usage: { total_tokens: number } | undefined,
211
+ usage: Usage | undefined,
140
212
  abortController: AbortController,
141
213
  ): Promise<void> {
142
214
  if (!usage) return;
143
215
 
144
- // Update token statistics - display latest token usage
145
- this.messageManager.setlatestTotalTokens(usage.total_tokens);
216
+ // Update token statistics - display comprehensive token usage including cache tokens
217
+ const comprehensiveTotalTokens = calculateComprehensiveTotalTokens(usage);
218
+ this.messageManager.setlatestTotalTokens(comprehensiveTotalTokens);
146
219
 
147
220
  // Check if token limit exceeded - use injected configuration
148
- if (usage.total_tokens > this.tokenLimit) {
221
+ if (
222
+ usage.total_tokens +
223
+ (usage.cache_read_input_tokens || 0) +
224
+ (usage.cache_creation_input_tokens || 0) >
225
+ this.tokenLimit
226
+ ) {
149
227
  this.logger?.debug(
150
228
  `Token usage exceeded ${this.tokenLimit}, compressing messages...`,
151
229
  );
@@ -160,6 +238,9 @@ export class AIManager {
160
238
  if (messagesToCompress.length > 0) {
161
239
  const recentChatMessages = convertMessagesForAPI(messagesToCompress);
162
240
 
241
+ // Save session before compression to preserve original messages
242
+ await this.messageManager.saveSession();
243
+
163
244
  this.setIsCompressing(true);
164
245
  try {
165
246
  const compressionResult = await compressMessages({
@@ -169,26 +250,28 @@ export class AIManager {
169
250
  abortSignal: abortController.signal,
170
251
  });
171
252
 
172
- // Execute message reconstruction and sessionId update after compression
173
- this.messageManager.compressMessagesAndUpdateSession(
174
- insertIndex,
175
- compressionResult.content,
176
- );
177
-
178
253
  // Handle usage tracking for compression operations
254
+ let compressionUsage: Usage | undefined;
179
255
  if (compressionResult.usage) {
180
- const usage: Usage = {
256
+ compressionUsage = {
181
257
  prompt_tokens: compressionResult.usage.prompt_tokens,
182
258
  completion_tokens: compressionResult.usage.completion_tokens,
183
259
  total_tokens: compressionResult.usage.total_tokens,
184
260
  model: this.modelConfig.fastModel,
185
261
  operation_type: "compress",
186
262
  };
263
+ }
187
264
 
188
- // Notify Agent to add to usage tracking
189
- if (this.callbacks?.onUsageAdded) {
190
- this.callbacks.onUsageAdded(usage);
191
- }
265
+ // Execute message reconstruction and sessionId update after compression
266
+ this.messageManager.compressMessagesAndUpdateSession(
267
+ insertIndex,
268
+ compressionResult.content,
269
+ compressionUsage,
270
+ );
271
+
272
+ // Notify Agent to add to usage tracking
273
+ if (compressionUsage && this.callbacks?.onUsageAdded) {
274
+ this.callbacks.onUsageAdded(compressionUsage);
192
275
  }
193
276
 
194
277
  this.logger?.debug(
@@ -228,6 +311,9 @@ export class AIManager {
228
311
  return;
229
312
  }
230
313
 
314
+ // Save session in each recursion to ensure message persistence
315
+ await this.messageManager.saveSession();
316
+
231
317
  // Only create new AbortControllers for the initial call (recursionDepth === 0)
232
318
  // For recursive calls, reuse existing controllers to maintain abort signal
233
319
  let abortController: AbortController;
@@ -262,7 +348,10 @@ export class AIManager {
262
348
  this.workdir,
263
349
  );
264
350
 
265
- // Call AI service (non-streaming)
351
+ // Add assistant message first (for streaming updates)
352
+ this.messageManager.addAssistantMessage();
353
+
354
+ // Call AI service with streaming callbacks
266
355
  const result = await callAgent({
267
356
  gatewayConfig: this.gatewayConfig,
268
357
  modelConfig: this.modelConfig,
@@ -274,18 +363,47 @@ export class AIManager {
274
363
  tools: this.getFilteredToolsConfig(allowedTools), // Pass filtered tool configuration
275
364
  model: model, // Use passed model
276
365
  systemPrompt: this.systemPrompt, // Pass custom system prompt
366
+ // Streaming callbacks
367
+ onContentUpdate: (content: string) => {
368
+ this.messageManager.updateCurrentMessageContent(content);
369
+ },
370
+ onToolUpdate: (toolCall) => {
371
+ // Use parametersChunk as compact param for better performance
372
+ // No need to extract params or generate compact params during streaming
373
+ this.logger?.debug("Tool streaming update:", toolCall);
374
+
375
+ // Update tool block with streaming parameters using parametersChunk as compact param
376
+ this.messageManager.updateToolBlock({
377
+ id: toolCall.id,
378
+ name: toolCall.name,
379
+ parameters: toolCall.parameters,
380
+ parametersChunk: toolCall.parametersChunk,
381
+ compactParams: toolCall.parameters?.split("\n").pop()?.slice(-30),
382
+ stage: toolCall.stage || "streaming", // Default to streaming if stage not provided
383
+ });
384
+ },
277
385
  });
278
386
 
279
- // Collect content and tool calls
280
- const content = result.content || "";
281
- const toolCalls: ChatCompletionMessageFunctionToolCall[] = [];
387
+ // Log finish reason and response headers if available
388
+ if (result.finish_reason) {
389
+ this.logger?.debug(
390
+ `AI response finished with reason: ${result.finish_reason}`,
391
+ );
392
+ }
393
+ if (
394
+ result.response_headers &&
395
+ Object.keys(result.response_headers).length > 0
396
+ ) {
397
+ this.logger?.debug("AI response headers:", result.response_headers);
398
+ }
282
399
 
283
- if (result.tool_calls) {
284
- for (const toolCall of result.tool_calls) {
285
- if (toolCall.type === "function") {
286
- toolCalls.push(toolCall);
287
- }
288
- }
400
+ if (result.metadata && Object.keys(result.metadata).length > 0) {
401
+ this.messageManager.mergeAssistantMetadata(result.metadata);
402
+ }
403
+
404
+ // Handle result content from non-streaming mode
405
+ if (result.content) {
406
+ this.messageManager.updateCurrentMessageContent(result.content);
289
407
  }
290
408
 
291
409
  // Handle usage tracking for agent operations
@@ -297,156 +415,183 @@ export class AIManager {
297
415
  total_tokens: result.usage.total_tokens,
298
416
  model: model || this.modelConfig.agentModel,
299
417
  operation_type: "agent",
418
+ // Preserve cache fields if present
419
+ ...(result.usage.cache_read_input_tokens !== undefined && {
420
+ cache_read_input_tokens: result.usage.cache_read_input_tokens,
421
+ }),
422
+ ...(result.usage.cache_creation_input_tokens !== undefined && {
423
+ cache_creation_input_tokens:
424
+ result.usage.cache_creation_input_tokens,
425
+ }),
426
+ ...(result.usage.cache_creation && {
427
+ cache_creation: result.usage.cache_creation,
428
+ }),
300
429
  };
301
430
  }
302
431
 
303
- // Add assistant message at once (including content, tool calls, and usage)
304
- this.messageManager.addAssistantMessage(content, toolCalls, usage);
305
-
306
- // Notify Agent to add to usage tracking
432
+ // Set usage on the assistant message if available
307
433
  if (usage) {
434
+ const messages = this.messageManager.getMessages();
435
+ const lastMessage = messages[messages.length - 1];
436
+ if (lastMessage && lastMessage.role === "assistant") {
437
+ lastMessage.usage = usage;
438
+ this.messageManager.setMessages(messages);
439
+ }
440
+
441
+ // Notify Agent to add to usage tracking
308
442
  if (this.callbacks?.onUsageAdded) {
309
443
  this.callbacks.onUsageAdded(usage);
310
444
  }
311
445
  }
312
446
 
447
+ // Collect tool calls for processing
448
+ const toolCalls: ChatCompletionMessageFunctionToolCall[] = [];
449
+ if (result.tool_calls) {
450
+ for (const toolCall of result.tool_calls) {
451
+ if (toolCall.type === "function") {
452
+ toolCalls.push(toolCall);
453
+ }
454
+ }
455
+ }
456
+
313
457
  if (toolCalls.length > 0) {
314
458
  // Execute all tools in parallel using Promise.all
315
459
  const toolExecutionPromises = toolCalls.map(
316
460
  async (functionToolCall) => {
317
461
  const toolId = functionToolCall.id || "";
318
462
 
319
- try {
320
- // Check if already interrupted, skip tool execution if so
321
- if (
322
- abortController.signal.aborted ||
323
- toolAbortController.signal.aborted
324
- ) {
463
+ // Check if already interrupted, skip tool execution if so
464
+ if (
465
+ abortController.signal.aborted ||
466
+ toolAbortController.signal.aborted
467
+ ) {
468
+ return;
469
+ }
470
+
471
+ const toolName = functionToolCall.function?.name || "";
472
+ // Safely parse tool parameters, handle tools without parameters
473
+ let toolArgs: Record<string, unknown> = {};
474
+ const argsString = functionToolCall.function?.arguments?.trim();
475
+
476
+ if (!argsString || argsString === "") {
477
+ // Tool without parameters, use empty object
478
+ toolArgs = {};
479
+ } else {
480
+ try {
481
+ toolArgs = JSON.parse(argsString);
482
+ } catch (parseError) {
483
+ // For non-empty but malformed JSON, still throw exception
484
+ const errorMessage = `Failed to parse tool arguments, finish_reason: ${result.finish_reason}`;
485
+ const fullErrorMessage = `${errorMessage}\nAI response headers:, ${JSON.stringify(result.response_headers)}`;
486
+ this.logger?.error(fullErrorMessage, parseError);
487
+ this.messageManager.updateToolBlock({
488
+ id: toolId,
489
+ parameters: argsString,
490
+ result: errorMessage,
491
+ success: false,
492
+ error: fullErrorMessage,
493
+ stage: "end",
494
+ name: toolName,
495
+ compactParams: "",
496
+ });
325
497
  return;
326
498
  }
499
+ }
327
500
 
328
- // Safely parse tool parameters, handle tools without parameters
329
- let toolArgs: Record<string, unknown> = {};
330
- const argsString = functionToolCall.function?.arguments?.trim();
331
-
332
- if (!argsString || argsString === "") {
333
- // Tool without parameters, use empty object
334
- toolArgs = {};
335
- } else {
336
- try {
337
- toolArgs = JSON.parse(argsString);
338
- } catch (parseError) {
339
- // For non-empty but malformed JSON, still throw exception
340
- const errorMessage = `Failed to parse tool arguments: ${argsString}`;
341
- this.logger?.error(errorMessage, parseError);
342
- throw new Error(errorMessage);
343
- }
344
- }
501
+ const compactParams = this.generateCompactParams(
502
+ toolName,
503
+ toolArgs,
504
+ );
345
505
 
346
- // Set tool start execution state
347
- const toolName = functionToolCall.function?.name || "";
348
- const compactParams = this.generateCompactParams(
506
+ // Emit running stage for non-streaming tool calls (tool execution about to start)
507
+ this.messageManager.updateToolBlock({
508
+ id: toolId,
509
+ stage: "running",
510
+ name: toolName,
511
+ compactParams,
512
+ parameters: argsString,
513
+ parametersChunk: "",
514
+ });
515
+
516
+ try {
517
+ // Execute PreToolUse hooks before tool execution
518
+ const shouldExecuteTool = await this.executePreToolUseHooks(
349
519
  toolName,
350
520
  toolArgs,
351
- );
352
-
353
- this.messageManager.updateToolBlock({
354
521
  toolId,
355
- args: JSON.stringify(toolArgs, null, 2),
356
- isRunning: true, // isRunning: true
357
- name: toolName,
358
- compactParams,
359
- });
522
+ );
360
523
 
361
- try {
362
- // Execute PreToolUse hooks before tool execution
363
- const shouldExecuteTool = await this.executePreToolUseHooks(
364
- toolName,
365
- toolArgs,
366
- toolId,
524
+ // If PreToolUse hooks blocked execution, skip tool execution
525
+ if (!shouldExecuteTool) {
526
+ this.logger?.info(
527
+ `Tool ${toolName} execution blocked by PreToolUse hooks`,
367
528
  );
529
+ return; // Skip this tool and return from this map function
530
+ }
368
531
 
369
- // If PreToolUse hooks blocked execution, skip tool execution
370
- if (!shouldExecuteTool) {
371
- this.logger?.info(
372
- `Tool ${toolName} execution blocked by PreToolUse hooks`,
373
- );
374
- return; // Skip this tool and return from this map function
375
- }
376
-
377
- // Create tool execution context
378
- const context: ToolContext = {
379
- abortSignal: toolAbortController.signal,
380
- backgroundBashManager: this.backgroundBashManager,
381
- workdir: this.workdir,
382
- };
383
-
384
- // Execute tool
385
- const toolResult = await this.toolManager.execute(
386
- functionToolCall.function?.name || "",
387
- toolArgs,
388
- context,
389
- );
532
+ // Create tool execution context
533
+ const context: ToolContext = {
534
+ abortSignal: toolAbortController.signal,
535
+ backgroundBashManager: this.backgroundBashManager,
536
+ workdir: this.workdir,
537
+ };
390
538
 
391
- // Update message state - tool execution completed
392
- this.messageManager.updateToolBlock({
393
- toolId,
394
- args: JSON.stringify(toolArgs, null, 2),
395
- result:
396
- toolResult.content ||
397
- (toolResult.error ? `Error: ${toolResult.error}` : ""),
398
- success: toolResult.success,
399
- error: toolResult.error,
400
- isRunning: false, // isRunning: false
401
- name: toolName,
402
- shortResult: toolResult.shortResult,
403
- compactParams,
404
- });
539
+ // Execute tool
540
+ const toolResult = await this.toolManager.execute(
541
+ functionToolCall.function?.name || "",
542
+ toolArgs,
543
+ context,
544
+ );
405
545
 
406
- // If tool returns diff information, add diff block
407
- if (
408
- toolResult.success &&
409
- toolResult.diffResult &&
410
- toolResult.filePath
411
- ) {
412
- this.messageManager.addDiffBlock(
413
- toolResult.filePath,
414
- toolResult.diffResult,
415
- );
416
- }
417
-
418
- // Execute PostToolUse hooks after successful tool completion
419
- await this.executePostToolUseHooks(
420
- toolId,
421
- toolName,
422
- toolArgs,
423
- toolResult,
424
- );
425
- } catch (toolError) {
426
- const errorMessage =
427
- toolError instanceof Error
428
- ? toolError.message
429
- : String(toolError);
546
+ // Update message state - tool execution completed
547
+ this.messageManager.updateToolBlock({
548
+ id: toolId,
549
+ parameters: argsString,
550
+ result:
551
+ toolResult.content ||
552
+ (toolResult.error ? `Error: ${toolResult.error}` : ""),
553
+ success: toolResult.success,
554
+ error: toolResult.error,
555
+ stage: "end",
556
+ name: toolName,
557
+ shortResult: toolResult.shortResult,
558
+ });
430
559
 
431
- this.messageManager.updateToolBlock({
432
- toolId,
433
- args: JSON.stringify(toolArgs, null, 2),
434
- result: `Tool execution failed: ${errorMessage}`,
435
- success: false,
436
- error: errorMessage,
437
- isRunning: false,
438
- name: toolName,
439
- compactParams,
440
- });
560
+ // If tool returns diff information, add diff block
561
+ if (
562
+ toolResult.success &&
563
+ toolResult.diffResult &&
564
+ toolResult.filePath
565
+ ) {
566
+ this.messageManager.addDiffBlock(
567
+ toolResult.filePath,
568
+ toolResult.diffResult,
569
+ );
441
570
  }
442
- } catch (parseError) {
443
- const errorMessage =
444
- parseError instanceof Error
445
- ? parseError.message
446
- : String(parseError);
447
- this.messageManager.addErrorBlock(
448
- `Failed to parse tool arguments for ${functionToolCall.function?.name}: ${errorMessage}`,
571
+
572
+ // Execute PostToolUse hooks after successful tool completion
573
+ await this.executePostToolUseHooks(
574
+ toolId,
575
+ toolName,
576
+ toolArgs,
577
+ toolResult,
449
578
  );
579
+ } catch (toolError) {
580
+ const errorMessage =
581
+ toolError instanceof Error
582
+ ? toolError.message
583
+ : String(toolError);
584
+
585
+ this.messageManager.updateToolBlock({
586
+ id: toolId,
587
+ parameters: JSON.stringify(toolArgs, null, 2),
588
+ result: `Tool execution failed: ${errorMessage}`,
589
+ success: false,
590
+ error: errorMessage,
591
+ stage: "end",
592
+ name: toolName,
593
+ compactParams,
594
+ });
450
595
  }
451
596
  },
452
597
  );
@@ -478,70 +623,74 @@ export class AIManager {
478
623
  error instanceof Error ? error.message : "Unknown error occurred",
479
624
  );
480
625
  } finally {
481
- // Only execute Stop hooks for the initial call
626
+ // Only execute cleanup and hooks for the initial call
482
627
  if (recursionDepth === 0) {
483
- // Execute Stop hooks only if the operation was not aborted
628
+ // Save session in each recursion to ensure message persistence
629
+ await this.messageManager.saveSession();
630
+ // Set loading to false first
631
+ this.setIsLoading(false);
632
+
633
+ // Clear abort controllers
634
+ this.abortController = null;
635
+ this.toolAbortController = null;
636
+
637
+ // Execute Stop/SubagentStop hooks only if the operation was not aborted
484
638
  const isCurrentlyAborted =
485
639
  abortController.signal.aborted || toolAbortController.signal.aborted;
486
640
 
487
641
  if (!isCurrentlyAborted) {
488
642
  const shouldContinue = await this.executeStopHooks();
489
643
 
490
- // If Stop hooks indicate we should continue (due to blocking errors),
644
+ // If Stop/SubagentStop hooks indicate we should continue (due to blocking errors),
491
645
  // restart the AI conversation cycle
492
646
  if (shouldContinue) {
493
647
  this.logger?.info(
494
- "Stop hooks indicate issues need fixing, continuing conversation...",
648
+ `${this.subagentType ? "SubagentStop" : "Stop"} hooks indicate issues need fixing, continuing conversation...`,
495
649
  );
496
650
 
497
651
  // Restart the conversation to let AI fix the issues
498
- // Use recursionDepth = 1 to prevent Stop hooks from running again in continuation
652
+ // Use recursionDepth = 0 to set loading false again for continuation
499
653
  await this.sendAIMessage({
500
- recursionDepth: 1,
654
+ recursionDepth: 0,
501
655
  model,
502
656
  allowedTools,
503
657
  });
504
658
  }
505
659
  }
506
-
507
- // Save session after all operations (including continuation) are complete
508
- await this.messageManager.saveSession();
509
-
510
- // Clear abort controllers and loading state after all operations are complete
511
- this.abortController = null;
512
- this.toolAbortController = null;
513
-
514
- // Set loading to false at the very end, after all operations including continuation
515
- this.setIsLoading(false);
516
660
  }
517
661
  }
518
662
  }
519
663
 
520
664
  /**
521
- * Execute Stop hooks when AI response cycle completes
665
+ * Execute Stop or SubagentStop hooks when AI response cycle completes
666
+ * Uses "SubagentStop" hook name when triggered by a subagent, otherwise uses "Stop"
522
667
  * @returns Promise<boolean> - true if should continue conversation, false if should stop
523
668
  */
524
669
  private async executeStopHooks(): Promise<boolean> {
525
670
  if (!this.hookManager) return false;
526
671
 
527
672
  try {
673
+ // Use "SubagentStop" hook name when triggered by a subagent, otherwise use "Stop"
674
+ const hookName = this.subagentType ? "SubagentStop" : "Stop";
675
+
528
676
  const context: ExtendedHookExecutionContext = {
529
- event: "Stop",
677
+ event: hookName,
530
678
  projectDir: this.workdir,
531
679
  timestamp: new Date(),
532
680
  sessionId: this.messageManager.getSessionId(),
533
681
  transcriptPath: this.messageManager.getTranscriptPath(),
534
682
  cwd: this.workdir,
683
+ subagentType: this.subagentType, // Include subagent type in hook context
535
684
  // Stop hooks don't need toolName, toolInput, toolResponse, or userPrompt
536
685
  };
537
686
 
538
- const results = await this.hookManager.executeHooks("Stop", context);
687
+ const results = await this.hookManager.executeHooks(hookName, context);
539
688
 
540
689
  // Process hook results to handle exit codes and appropriate responses
541
690
  let shouldContinue = false;
542
691
  if (results.length > 0) {
543
692
  const processResult = this.hookManager.processHookResults(
544
- "Stop",
693
+ hookName,
545
694
  results,
546
695
  this.messageManager,
547
696
  );
@@ -549,7 +698,7 @@ export class AIManager {
549
698
  // If hook processing indicates we should block (exit code 2), continue conversation
550
699
  if (processResult.shouldBlock) {
551
700
  this.logger?.info(
552
- "Stop hook blocked stopping with error:",
701
+ `${hookName} hook blocked stopping with error:`,
553
702
  processResult.errorMessage,
554
703
  );
555
704
  shouldContinue = true;
@@ -559,7 +708,7 @@ export class AIManager {
559
708
  // Log hook execution results for debugging
560
709
  if (results.length > 0) {
561
710
  this.logger?.debug(
562
- `Executed ${results.length} Stop hook(s):`,
711
+ `Executed ${results.length} ${hookName} hook(s):`,
563
712
  results.map((r) => ({
564
713
  success: r.success,
565
714
  duration: r.duration,
@@ -573,7 +722,10 @@ export class AIManager {
573
722
  return shouldContinue;
574
723
  } catch (error) {
575
724
  // Hook execution errors should not interrupt the main workflow
576
- this.logger?.error("Stop hook execution failed:", error);
725
+ this.logger?.error(
726
+ `${this.subagentType ? "SubagentStop" : "Stop"} hook execution failed:`,
727
+ error,
728
+ );
577
729
  return false;
578
730
  }
579
731
  }
@@ -599,6 +751,7 @@ export class AIManager {
599
751
  transcriptPath: this.messageManager.getTranscriptPath(),
600
752
  cwd: this.workdir,
601
753
  toolInput,
754
+ subagentType: this.subagentType, // Include subagent type in hook context
602
755
  };
603
756
 
604
757
  const results = await this.hookManager.executeHooks(
@@ -614,6 +767,7 @@ export class AIManager {
614
767
  results,
615
768
  this.messageManager,
616
769
  toolId, // Pass toolId for proper PreToolUse blocking error handling
770
+ JSON.stringify(toolInput || {}, null, 2), // Pass serialized tool parameters
617
771
  );
618
772
  shouldContinue = !processResult.shouldBlock;
619
773
  }
@@ -662,6 +816,7 @@ export class AIManager {
662
816
  cwd: this.workdir,
663
817
  toolInput,
664
818
  toolResponse,
819
+ subagentType: this.subagentType, // Include subagent type in hook context
665
820
  };
666
821
 
667
822
  const results = await this.hookManager.executeHooks(
@@ -671,14 +826,11 @@ export class AIManager {
671
826
 
672
827
  // Process hook results to handle exit codes and update tool results
673
828
  if (results.length > 0) {
674
- const originalToolResult = toolResponse?.content || "";
675
-
676
829
  this.hookManager.processHookResults(
677
830
  "PostToolUse",
678
831
  results,
679
832
  this.messageManager,
680
833
  toolId,
681
- originalToolResult,
682
834
  );
683
835
  }
684
836