wave-agent-sdk 0.0.7 → 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 (172) hide show
  1. package/dist/agent.d.ts +32 -20
  2. package/dist/agent.d.ts.map +1 -1
  3. package/dist/agent.js +202 -20
  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 +243 -128
  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 +9 -4
  16. package/dist/managers/hookManager.d.ts.map +1 -1
  17. package/dist/managers/hookManager.js +62 -30
  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 +38 -13
  22. package/dist/managers/messageManager.d.ts.map +1 -1
  23. package/dist/managers/messageManager.js +163 -30
  24. package/dist/managers/slashCommandManager.d.ts.map +1 -1
  25. package/dist/managers/slashCommandManager.js +4 -1
  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 +189 -18
  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/writeTool.d.ts.map +1 -1
  64. package/dist/tools/writeTool.js +5 -6
  65. package/dist/types/commands.d.ts +4 -0
  66. package/dist/types/commands.d.ts.map +1 -1
  67. package/dist/types/core.d.ts +35 -0
  68. package/dist/types/core.d.ts.map +1 -1
  69. package/dist/types/environment.d.ts +42 -0
  70. package/dist/types/environment.d.ts.map +1 -0
  71. package/dist/types/environment.js +21 -0
  72. package/dist/types/hooks.d.ts +8 -2
  73. package/dist/types/hooks.d.ts.map +1 -1
  74. package/dist/types/hooks.js +8 -2
  75. package/dist/types/index.d.ts +2 -0
  76. package/dist/types/index.d.ts.map +1 -1
  77. package/dist/types/index.js +2 -0
  78. package/dist/types/memoryStore.d.ts +82 -0
  79. package/dist/types/memoryStore.d.ts.map +1 -0
  80. package/dist/types/memoryStore.js +7 -0
  81. package/dist/types/messaging.d.ts +14 -2
  82. package/dist/types/messaging.d.ts.map +1 -1
  83. package/dist/types/session.d.ts +20 -0
  84. package/dist/types/session.d.ts.map +1 -0
  85. package/dist/types/session.js +7 -0
  86. package/dist/utils/bashHistory.d.ts.map +1 -1
  87. package/dist/utils/bashHistory.js +27 -26
  88. package/dist/utils/cacheControlUtils.d.ts +121 -0
  89. package/dist/utils/cacheControlUtils.d.ts.map +1 -0
  90. package/dist/utils/cacheControlUtils.js +367 -0
  91. package/dist/utils/commandPathResolver.d.ts +52 -0
  92. package/dist/utils/commandPathResolver.d.ts.map +1 -0
  93. package/dist/utils/commandPathResolver.js +145 -0
  94. package/dist/utils/configPaths.d.ts +85 -0
  95. package/dist/utils/configPaths.d.ts.map +1 -0
  96. package/dist/utils/configPaths.js +121 -0
  97. package/dist/utils/configResolver.d.ts +37 -10
  98. package/dist/utils/configResolver.d.ts.map +1 -1
  99. package/dist/utils/configResolver.js +127 -23
  100. package/dist/utils/constants.d.ts +1 -1
  101. package/dist/utils/constants.js +1 -1
  102. package/dist/utils/convertMessagesForAPI.d.ts.map +1 -1
  103. package/dist/utils/convertMessagesForAPI.js +7 -5
  104. package/dist/utils/customCommands.d.ts.map +1 -1
  105. package/dist/utils/customCommands.js +66 -21
  106. package/dist/utils/fileUtils.d.ts +15 -0
  107. package/dist/utils/fileUtils.d.ts.map +1 -0
  108. package/dist/utils/fileUtils.js +61 -0
  109. package/dist/utils/globalLogger.d.ts +102 -0
  110. package/dist/utils/globalLogger.d.ts.map +1 -0
  111. package/dist/utils/globalLogger.js +136 -0
  112. package/dist/utils/mcpUtils.d.ts.map +1 -1
  113. package/dist/utils/mcpUtils.js +25 -3
  114. package/dist/utils/messageOperations.d.ts +20 -8
  115. package/dist/utils/messageOperations.d.ts.map +1 -1
  116. package/dist/utils/messageOperations.js +25 -16
  117. package/dist/utils/pathEncoder.d.ts +104 -0
  118. package/dist/utils/pathEncoder.d.ts.map +1 -0
  119. package/dist/utils/pathEncoder.js +272 -0
  120. package/dist/utils/subagentParser.d.ts.map +1 -1
  121. package/dist/utils/subagentParser.js +2 -1
  122. package/dist/utils/tokenCalculation.d.ts +26 -0
  123. package/dist/utils/tokenCalculation.d.ts.map +1 -0
  124. package/dist/utils/tokenCalculation.js +36 -0
  125. package/package.json +6 -3
  126. package/src/agent.ts +298 -34
  127. package/src/constants/events.ts +38 -0
  128. package/src/index.ts +2 -0
  129. package/src/managers/aiManager.ts +323 -170
  130. package/src/managers/backgroundBashManager.ts +7 -6
  131. package/src/managers/hookManager.ts +83 -40
  132. package/src/managers/liveConfigManager.ts +248 -0
  133. package/src/managers/messageManager.ts +230 -63
  134. package/src/managers/slashCommandManager.ts +4 -1
  135. package/src/managers/subagentManager.ts +283 -21
  136. package/src/services/aiService.ts +474 -83
  137. package/src/services/configurationWatcher.ts +622 -0
  138. package/src/services/fileWatcher.ts +301 -0
  139. package/src/services/hook.ts +538 -47
  140. package/src/services/jsonlHandler.ts +319 -0
  141. package/src/services/memory.ts +92 -12
  142. package/src/services/memoryStore.ts +279 -0
  143. package/src/services/session.ts +381 -157
  144. package/src/tools/bashTool.ts +5 -4
  145. package/src/tools/deleteFileTool.ts +2 -1
  146. package/src/tools/editTool.ts +3 -2
  147. package/src/tools/multiEditTool.ts +4 -3
  148. package/src/tools/readTool.ts +2 -1
  149. package/src/tools/writeTool.ts +7 -6
  150. package/src/types/commands.ts +6 -0
  151. package/src/types/core.ts +44 -0
  152. package/src/types/environment.ts +60 -0
  153. package/src/types/hooks.ts +21 -8
  154. package/src/types/index.ts +2 -0
  155. package/src/types/memoryStore.ts +94 -0
  156. package/src/types/messaging.ts +14 -2
  157. package/src/types/session.ts +25 -0
  158. package/src/utils/bashHistory.ts +27 -27
  159. package/src/utils/cacheControlUtils.ts +540 -0
  160. package/src/utils/commandPathResolver.ts +189 -0
  161. package/src/utils/configPaths.ts +163 -0
  162. package/src/utils/configResolver.ts +182 -22
  163. package/src/utils/constants.ts +1 -1
  164. package/src/utils/convertMessagesForAPI.ts +7 -5
  165. package/src/utils/customCommands.ts +90 -22
  166. package/src/utils/fileUtils.ts +65 -0
  167. package/src/utils/globalLogger.ts +145 -0
  168. package/src/utils/mcpUtils.ts +34 -3
  169. package/src/utils/messageOperations.ts +42 -20
  170. package/src/utils/pathEncoder.ts +379 -0
  171. package/src/utils/subagentParser.ts +2 -1
  172. package/src/utils/tokenCalculation.ts +43 -0
@@ -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,19 +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
- this.logger?.debug("ToolCall", toolCall);
286
- if (toolCall.type === "function") {
287
- toolCalls.push(toolCall);
288
- }
289
- }
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);
290
407
  }
291
408
 
292
409
  // Handle usage tracking for agent operations
@@ -298,156 +415,183 @@ export class AIManager {
298
415
  total_tokens: result.usage.total_tokens,
299
416
  model: model || this.modelConfig.agentModel,
300
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
+ }),
301
429
  };
302
430
  }
303
431
 
304
- // Add assistant message at once (including content, tool calls, and usage)
305
- this.messageManager.addAssistantMessage(content, toolCalls, usage);
306
-
307
- // Notify Agent to add to usage tracking
432
+ // Set usage on the assistant message if available
308
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
309
442
  if (this.callbacks?.onUsageAdded) {
310
443
  this.callbacks.onUsageAdded(usage);
311
444
  }
312
445
  }
313
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
+
314
457
  if (toolCalls.length > 0) {
315
458
  // Execute all tools in parallel using Promise.all
316
459
  const toolExecutionPromises = toolCalls.map(
317
460
  async (functionToolCall) => {
318
461
  const toolId = functionToolCall.id || "";
319
462
 
320
- try {
321
- // Check if already interrupted, skip tool execution if so
322
- if (
323
- abortController.signal.aborted ||
324
- toolAbortController.signal.aborted
325
- ) {
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
+ });
326
497
  return;
327
498
  }
499
+ }
328
500
 
329
- // Safely parse tool parameters, handle tools without parameters
330
- let toolArgs: Record<string, unknown> = {};
331
- const argsString = functionToolCall.function?.arguments?.trim();
332
-
333
- if (!argsString || argsString === "") {
334
- // Tool without parameters, use empty object
335
- toolArgs = {};
336
- } else {
337
- try {
338
- toolArgs = JSON.parse(argsString);
339
- } catch (parseError) {
340
- // For non-empty but malformed JSON, still throw exception
341
- const errorMessage = `Failed to parse tool arguments: ${argsString}`;
342
- this.logger?.error(errorMessage, parseError);
343
- throw new Error(errorMessage);
344
- }
345
- }
501
+ const compactParams = this.generateCompactParams(
502
+ toolName,
503
+ toolArgs,
504
+ );
346
505
 
347
- // Set tool start execution state
348
- const toolName = functionToolCall.function?.name || "";
349
- 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(
350
519
  toolName,
351
520
  toolArgs,
521
+ toolId,
352
522
  );
353
523
 
524
+ // If PreToolUse hooks blocked execution, skip tool execution
525
+ if (!shouldExecuteTool) {
526
+ this.logger?.info(
527
+ `Tool ${toolName} execution blocked by PreToolUse hooks`,
528
+ );
529
+ return; // Skip this tool and return from this map function
530
+ }
531
+
532
+ // Create tool execution context
533
+ const context: ToolContext = {
534
+ abortSignal: toolAbortController.signal,
535
+ backgroundBashManager: this.backgroundBashManager,
536
+ workdir: this.workdir,
537
+ };
538
+
539
+ // Execute tool
540
+ const toolResult = await this.toolManager.execute(
541
+ functionToolCall.function?.name || "",
542
+ toolArgs,
543
+ context,
544
+ );
545
+
546
+ // Update message state - tool execution completed
354
547
  this.messageManager.updateToolBlock({
355
548
  id: toolId,
356
- parameters: JSON.stringify(toolArgs, null, 2),
357
- isRunning: true, // isRunning: true
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",
358
556
  name: toolName,
359
- compactParams,
557
+ shortResult: toolResult.shortResult,
360
558
  });
361
559
 
362
- try {
363
- // Execute PreToolUse hooks before tool execution
364
- const shouldExecuteTool = await this.executePreToolUseHooks(
365
- toolName,
366
- toolArgs,
367
- toolId,
368
- );
369
-
370
- // If PreToolUse hooks blocked execution, skip tool execution
371
- if (!shouldExecuteTool) {
372
- this.logger?.info(
373
- `Tool ${toolName} execution blocked by PreToolUse hooks`,
374
- );
375
- return; // Skip this tool and return from this map function
376
- }
377
-
378
- // Create tool execution context
379
- const context: ToolContext = {
380
- abortSignal: toolAbortController.signal,
381
- backgroundBashManager: this.backgroundBashManager,
382
- workdir: this.workdir,
383
- };
384
-
385
- // Execute tool
386
- const toolResult = await this.toolManager.execute(
387
- functionToolCall.function?.name || "",
388
- toolArgs,
389
- context,
390
- );
391
-
392
- // Update message state - tool execution completed
393
- this.messageManager.updateToolBlock({
394
- id: toolId,
395
- parameters: JSON.stringify(toolArgs, null, 2),
396
- result:
397
- toolResult.content ||
398
- (toolResult.error ? `Error: ${toolResult.error}` : ""),
399
- success: toolResult.success,
400
- error: toolResult.error,
401
- isRunning: false, // isRunning: false
402
- name: toolName,
403
- shortResult: toolResult.shortResult,
404
- compactParams,
405
- });
406
-
407
- // If tool returns diff information, add diff block
408
- if (
409
- toolResult.success &&
410
- toolResult.diffResult &&
411
- toolResult.filePath
412
- ) {
413
- this.messageManager.addDiffBlock(
414
- toolResult.filePath,
415
- toolResult.diffResult,
416
- );
417
- }
418
-
419
- // Execute PostToolUse hooks after successful tool completion
420
- await this.executePostToolUseHooks(
421
- toolId,
422
- toolName,
423
- toolArgs,
424
- toolResult,
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,
425
569
  );
426
- } catch (toolError) {
427
- const errorMessage =
428
- toolError instanceof Error
429
- ? toolError.message
430
- : String(toolError);
431
-
432
- this.messageManager.updateToolBlock({
433
- id: toolId,
434
- parameters: JSON.stringify(toolArgs, null, 2),
435
- result: `Tool execution failed: ${errorMessage}`,
436
- success: false,
437
- error: errorMessage,
438
- isRunning: false,
439
- name: toolName,
440
- compactParams,
441
- });
442
570
  }
443
- } catch (parseError) {
444
- const errorMessage =
445
- parseError instanceof Error
446
- ? parseError.message
447
- : String(parseError);
448
- this.messageManager.addErrorBlock(
449
- `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,
450
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
+ });
451
595
  }
452
596
  },
453
597
  );
@@ -479,70 +623,74 @@ export class AIManager {
479
623
  error instanceof Error ? error.message : "Unknown error occurred",
480
624
  );
481
625
  } finally {
482
- // Only execute Stop hooks for the initial call
626
+ // Only execute cleanup and hooks for the initial call
483
627
  if (recursionDepth === 0) {
484
- // 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
485
638
  const isCurrentlyAborted =
486
639
  abortController.signal.aborted || toolAbortController.signal.aborted;
487
640
 
488
641
  if (!isCurrentlyAborted) {
489
642
  const shouldContinue = await this.executeStopHooks();
490
643
 
491
- // If Stop hooks indicate we should continue (due to blocking errors),
644
+ // If Stop/SubagentStop hooks indicate we should continue (due to blocking errors),
492
645
  // restart the AI conversation cycle
493
646
  if (shouldContinue) {
494
647
  this.logger?.info(
495
- "Stop hooks indicate issues need fixing, continuing conversation...",
648
+ `${this.subagentType ? "SubagentStop" : "Stop"} hooks indicate issues need fixing, continuing conversation...`,
496
649
  );
497
650
 
498
651
  // Restart the conversation to let AI fix the issues
499
- // Use recursionDepth = 1 to prevent Stop hooks from running again in continuation
652
+ // Use recursionDepth = 0 to set loading false again for continuation
500
653
  await this.sendAIMessage({
501
- recursionDepth: 1,
654
+ recursionDepth: 0,
502
655
  model,
503
656
  allowedTools,
504
657
  });
505
658
  }
506
659
  }
507
-
508
- // Save session after all operations (including continuation) are complete
509
- await this.messageManager.saveSession();
510
-
511
- // Clear abort controllers and loading state after all operations are complete
512
- this.abortController = null;
513
- this.toolAbortController = null;
514
-
515
- // Set loading to false at the very end, after all operations including continuation
516
- this.setIsLoading(false);
517
660
  }
518
661
  }
519
662
  }
520
663
 
521
664
  /**
522
- * 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"
523
667
  * @returns Promise<boolean> - true if should continue conversation, false if should stop
524
668
  */
525
669
  private async executeStopHooks(): Promise<boolean> {
526
670
  if (!this.hookManager) return false;
527
671
 
528
672
  try {
673
+ // Use "SubagentStop" hook name when triggered by a subagent, otherwise use "Stop"
674
+ const hookName = this.subagentType ? "SubagentStop" : "Stop";
675
+
529
676
  const context: ExtendedHookExecutionContext = {
530
- event: "Stop",
677
+ event: hookName,
531
678
  projectDir: this.workdir,
532
679
  timestamp: new Date(),
533
680
  sessionId: this.messageManager.getSessionId(),
534
681
  transcriptPath: this.messageManager.getTranscriptPath(),
535
682
  cwd: this.workdir,
683
+ subagentType: this.subagentType, // Include subagent type in hook context
536
684
  // Stop hooks don't need toolName, toolInput, toolResponse, or userPrompt
537
685
  };
538
686
 
539
- const results = await this.hookManager.executeHooks("Stop", context);
687
+ const results = await this.hookManager.executeHooks(hookName, context);
540
688
 
541
689
  // Process hook results to handle exit codes and appropriate responses
542
690
  let shouldContinue = false;
543
691
  if (results.length > 0) {
544
692
  const processResult = this.hookManager.processHookResults(
545
- "Stop",
693
+ hookName,
546
694
  results,
547
695
  this.messageManager,
548
696
  );
@@ -550,7 +698,7 @@ export class AIManager {
550
698
  // If hook processing indicates we should block (exit code 2), continue conversation
551
699
  if (processResult.shouldBlock) {
552
700
  this.logger?.info(
553
- "Stop hook blocked stopping with error:",
701
+ `${hookName} hook blocked stopping with error:`,
554
702
  processResult.errorMessage,
555
703
  );
556
704
  shouldContinue = true;
@@ -560,7 +708,7 @@ export class AIManager {
560
708
  // Log hook execution results for debugging
561
709
  if (results.length > 0) {
562
710
  this.logger?.debug(
563
- `Executed ${results.length} Stop hook(s):`,
711
+ `Executed ${results.length} ${hookName} hook(s):`,
564
712
  results.map((r) => ({
565
713
  success: r.success,
566
714
  duration: r.duration,
@@ -574,7 +722,10 @@ export class AIManager {
574
722
  return shouldContinue;
575
723
  } catch (error) {
576
724
  // Hook execution errors should not interrupt the main workflow
577
- this.logger?.error("Stop hook execution failed:", error);
725
+ this.logger?.error(
726
+ `${this.subagentType ? "SubagentStop" : "Stop"} hook execution failed:`,
727
+ error,
728
+ );
578
729
  return false;
579
730
  }
580
731
  }
@@ -600,6 +751,7 @@ export class AIManager {
600
751
  transcriptPath: this.messageManager.getTranscriptPath(),
601
752
  cwd: this.workdir,
602
753
  toolInput,
754
+ subagentType: this.subagentType, // Include subagent type in hook context
603
755
  };
604
756
 
605
757
  const results = await this.hookManager.executeHooks(
@@ -664,6 +816,7 @@ export class AIManager {
664
816
  cwd: this.workdir,
665
817
  toolInput,
666
818
  toolResponse,
819
+ subagentType: this.subagentType, // Include subagent type in hook context
667
820
  };
668
821
 
669
822
  const results = await this.hookManager.executeHooks(