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
  export class AIManager {
6
7
  constructor(options) {
@@ -15,12 +16,60 @@ export class AIManager {
15
16
  this.logger = options.logger;
16
17
  this.workdir = options.workdir;
17
18
  this.systemPrompt = options.systemPrompt;
19
+ this.subagentType = options.subagentType; // Store subagent type
18
20
  this.callbacks = options.callbacks ?? {};
19
21
  // Store resolved configuration
20
22
  this.gatewayConfig = options.gatewayConfig;
21
23
  this.modelConfig = options.modelConfig;
22
24
  this.tokenLimit = options.tokenLimit;
23
25
  }
26
+ /**
27
+ * Update gateway configuration at runtime for live config reload
28
+ * @param newConfig - New gateway configuration
29
+ */
30
+ updateGatewayConfig(newConfig) {
31
+ this.logger?.info(`Live Config: Updating AIManager gateway config - baseURL: ${newConfig.baseURL}`);
32
+ this.gatewayConfig = newConfig;
33
+ }
34
+ /**
35
+ * Update model configuration at runtime for live config reload
36
+ * @param newConfig - New model configuration
37
+ */
38
+ updateModelConfig(newConfig) {
39
+ this.logger?.info(`Live Config: Updating AIManager model config - agent: ${newConfig.agentModel}, fast: ${newConfig.fastModel}`);
40
+ this.modelConfig = newConfig;
41
+ }
42
+ /**
43
+ * Update token limit at runtime for live config reload
44
+ * @param newLimit - New token limit
45
+ */
46
+ updateTokenLimit(newLimit) {
47
+ this.logger?.info(`Live Config: Updating AIManager token limit: ${newLimit}`);
48
+ this.tokenLimit = newLimit;
49
+ }
50
+ /**
51
+ * Update all configurations at once for live config reload
52
+ * @param newGatewayConfig - New gateway configuration
53
+ * @param newModelConfig - New model configuration
54
+ * @param newTokenLimit - New token limit
55
+ */
56
+ updateConfiguration(newGatewayConfig, newModelConfig, newTokenLimit) {
57
+ this.logger?.info("Live Config: Updating all AIManager configuration");
58
+ this.gatewayConfig = newGatewayConfig;
59
+ this.modelConfig = newModelConfig;
60
+ this.tokenLimit = newTokenLimit;
61
+ this.logger?.info(`Live Config: Configuration updated - model: ${newModelConfig.agentModel}, tokenLimit: ${newTokenLimit}`);
62
+ }
63
+ /**
64
+ * Get current configuration for debugging
65
+ */
66
+ getCurrentConfiguration() {
67
+ return {
68
+ gatewayConfig: { ...this.gatewayConfig },
69
+ modelConfig: { ...this.modelConfig },
70
+ tokenLimit: this.tokenLimit,
71
+ };
72
+ }
24
73
  /**
25
74
  * Get filtered tool configuration
26
75
  */
@@ -73,22 +122,28 @@ export class AIManager {
73
122
  catch (error) {
74
123
  this.logger?.warn("Failed to generate compactParams", error);
75
124
  }
76
- return undefined;
125
+ return "";
77
126
  }
78
127
  // Private method to handle token statistics and message compression
79
128
  async handleTokenUsageAndCompression(usage, abortController) {
80
129
  if (!usage)
81
130
  return;
82
- // Update token statistics - display latest token usage
83
- this.messageManager.setlatestTotalTokens(usage.total_tokens);
131
+ // Update token statistics - display comprehensive token usage including cache tokens
132
+ const comprehensiveTotalTokens = calculateComprehensiveTotalTokens(usage);
133
+ this.messageManager.setlatestTotalTokens(comprehensiveTotalTokens);
84
134
  // Check if token limit exceeded - use injected configuration
85
- if (usage.total_tokens > this.tokenLimit) {
135
+ if (usage.total_tokens +
136
+ (usage.cache_read_input_tokens || 0) +
137
+ (usage.cache_creation_input_tokens || 0) >
138
+ this.tokenLimit) {
86
139
  this.logger?.debug(`Token usage exceeded ${this.tokenLimit}, compressing messages...`);
87
140
  // Check if messages need compression
88
141
  const { messagesToCompress, insertIndex } = getMessagesToCompress(this.messageManager.getMessages(), 7);
89
142
  // If there are messages to compress, perform compression
90
143
  if (messagesToCompress.length > 0) {
91
144
  const recentChatMessages = convertMessagesForAPI(messagesToCompress);
145
+ // Save session before compression to preserve original messages
146
+ await this.messageManager.saveSession();
92
147
  this.setIsCompressing(true);
93
148
  try {
94
149
  const compressionResult = await compressMessages({
@@ -97,21 +152,22 @@ export class AIManager {
97
152
  messages: recentChatMessages,
98
153
  abortSignal: abortController.signal,
99
154
  });
100
- // Execute message reconstruction and sessionId update after compression
101
- this.messageManager.compressMessagesAndUpdateSession(insertIndex, compressionResult.content);
102
155
  // Handle usage tracking for compression operations
156
+ let compressionUsage;
103
157
  if (compressionResult.usage) {
104
- const usage = {
158
+ compressionUsage = {
105
159
  prompt_tokens: compressionResult.usage.prompt_tokens,
106
160
  completion_tokens: compressionResult.usage.completion_tokens,
107
161
  total_tokens: compressionResult.usage.total_tokens,
108
162
  model: this.modelConfig.fastModel,
109
163
  operation_type: "compress",
110
164
  };
111
- // Notify Agent to add to usage tracking
112
- if (this.callbacks?.onUsageAdded) {
113
- this.callbacks.onUsageAdded(usage);
114
- }
165
+ }
166
+ // Execute message reconstruction and sessionId update after compression
167
+ this.messageManager.compressMessagesAndUpdateSession(insertIndex, compressionResult.content, compressionUsage);
168
+ // Notify Agent to add to usage tracking
169
+ if (compressionUsage && this.callbacks?.onUsageAdded) {
170
+ this.callbacks.onUsageAdded(compressionUsage);
115
171
  }
116
172
  this.logger?.debug(`Successfully compressed ${messagesToCompress.length} messages and updated session`);
117
173
  }
@@ -139,6 +195,8 @@ export class AIManager {
139
195
  if (recursionDepth === 0 && this.isLoading) {
140
196
  return;
141
197
  }
198
+ // Save session in each recursion to ensure message persistence
199
+ await this.messageManager.saveSession();
142
200
  // Only create new AbortControllers for the initial call (recursionDepth === 0)
143
201
  // For recursive calls, reuse existing controllers to maintain abort signal
144
202
  let abortController;
@@ -164,7 +222,9 @@ export class AIManager {
164
222
  try {
165
223
  // Get combined memory content
166
224
  const combinedMemory = await memory.getCombinedMemoryContent(this.workdir);
167
- // Call AI service (non-streaming)
225
+ // Add assistant message first (for streaming updates)
226
+ this.messageManager.addAssistantMessage();
227
+ // Call AI service with streaming callbacks
168
228
  const result = await callAgent({
169
229
  gatewayConfig: this.gatewayConfig,
170
230
  modelConfig: this.modelConfig,
@@ -176,16 +236,39 @@ export class AIManager {
176
236
  tools: this.getFilteredToolsConfig(allowedTools), // Pass filtered tool configuration
177
237
  model: model, // Use passed model
178
238
  systemPrompt: this.systemPrompt, // Pass custom system prompt
239
+ // Streaming callbacks
240
+ onContentUpdate: (content) => {
241
+ this.messageManager.updateCurrentMessageContent(content);
242
+ },
243
+ onToolUpdate: (toolCall) => {
244
+ // Use parametersChunk as compact param for better performance
245
+ // No need to extract params or generate compact params during streaming
246
+ this.logger?.debug("Tool streaming update:", toolCall);
247
+ // Update tool block with streaming parameters using parametersChunk as compact param
248
+ this.messageManager.updateToolBlock({
249
+ id: toolCall.id,
250
+ name: toolCall.name,
251
+ parameters: toolCall.parameters,
252
+ parametersChunk: toolCall.parametersChunk,
253
+ compactParams: toolCall.parameters?.split("\n").pop()?.slice(-30),
254
+ stage: toolCall.stage || "streaming", // Default to streaming if stage not provided
255
+ });
256
+ },
179
257
  });
180
- // Collect content and tool calls
181
- const content = result.content || "";
182
- const toolCalls = [];
183
- if (result.tool_calls) {
184
- for (const toolCall of result.tool_calls) {
185
- if (toolCall.type === "function") {
186
- toolCalls.push(toolCall);
187
- }
188
- }
258
+ // Log finish reason and response headers if available
259
+ if (result.finish_reason) {
260
+ this.logger?.debug(`AI response finished with reason: ${result.finish_reason}`);
261
+ }
262
+ if (result.response_headers &&
263
+ Object.keys(result.response_headers).length > 0) {
264
+ this.logger?.debug("AI response headers:", result.response_headers);
265
+ }
266
+ if (result.metadata && Object.keys(result.metadata).length > 0) {
267
+ this.messageManager.mergeAssistantMetadata(result.metadata);
268
+ }
269
+ // Handle result content from non-streaming mode
270
+ if (result.content) {
271
+ this.messageManager.updateCurrentMessageContent(result.content);
189
272
  }
190
273
  // Handle usage tracking for agent operations
191
274
  let usage;
@@ -196,113 +279,140 @@ export class AIManager {
196
279
  total_tokens: result.usage.total_tokens,
197
280
  model: model || this.modelConfig.agentModel,
198
281
  operation_type: "agent",
282
+ // Preserve cache fields if present
283
+ ...(result.usage.cache_read_input_tokens !== undefined && {
284
+ cache_read_input_tokens: result.usage.cache_read_input_tokens,
285
+ }),
286
+ ...(result.usage.cache_creation_input_tokens !== undefined && {
287
+ cache_creation_input_tokens: result.usage.cache_creation_input_tokens,
288
+ }),
289
+ ...(result.usage.cache_creation && {
290
+ cache_creation: result.usage.cache_creation,
291
+ }),
199
292
  };
200
293
  }
201
- // Add assistant message at once (including content, tool calls, and usage)
202
- this.messageManager.addAssistantMessage(content, toolCalls, usage);
203
- // Notify Agent to add to usage tracking
294
+ // Set usage on the assistant message if available
204
295
  if (usage) {
296
+ const messages = this.messageManager.getMessages();
297
+ const lastMessage = messages[messages.length - 1];
298
+ if (lastMessage && lastMessage.role === "assistant") {
299
+ lastMessage.usage = usage;
300
+ this.messageManager.setMessages(messages);
301
+ }
302
+ // Notify Agent to add to usage tracking
205
303
  if (this.callbacks?.onUsageAdded) {
206
304
  this.callbacks.onUsageAdded(usage);
207
305
  }
208
306
  }
307
+ // Collect tool calls for processing
308
+ const toolCalls = [];
309
+ if (result.tool_calls) {
310
+ for (const toolCall of result.tool_calls) {
311
+ if (toolCall.type === "function") {
312
+ toolCalls.push(toolCall);
313
+ }
314
+ }
315
+ }
209
316
  if (toolCalls.length > 0) {
210
317
  // Execute all tools in parallel using Promise.all
211
318
  const toolExecutionPromises = toolCalls.map(async (functionToolCall) => {
212
319
  const toolId = functionToolCall.id || "";
213
- try {
214
- // Check if already interrupted, skip tool execution if so
215
- if (abortController.signal.aborted ||
216
- toolAbortController.signal.aborted) {
217
- return;
218
- }
219
- // Safely parse tool parameters, handle tools without parameters
220
- let toolArgs = {};
221
- const argsString = functionToolCall.function?.arguments?.trim();
222
- if (!argsString || argsString === "") {
223
- // Tool without parameters, use empty object
224
- toolArgs = {};
225
- }
226
- else {
227
- try {
228
- toolArgs = JSON.parse(argsString);
229
- }
230
- catch (parseError) {
231
- // For non-empty but malformed JSON, still throw exception
232
- const errorMessage = `Failed to parse tool arguments: ${argsString}`;
233
- this.logger?.error(errorMessage, parseError);
234
- throw new Error(errorMessage);
235
- }
236
- }
237
- // Set tool start execution state
238
- const toolName = functionToolCall.function?.name || "";
239
- const compactParams = this.generateCompactParams(toolName, toolArgs);
240
- this.messageManager.updateToolBlock({
241
- toolId,
242
- args: JSON.stringify(toolArgs, null, 2),
243
- isRunning: true, // isRunning: true
244
- name: toolName,
245
- compactParams,
246
- });
320
+ // Check if already interrupted, skip tool execution if so
321
+ if (abortController.signal.aborted ||
322
+ toolAbortController.signal.aborted) {
323
+ return;
324
+ }
325
+ const toolName = functionToolCall.function?.name || "";
326
+ // Safely parse tool parameters, handle tools without parameters
327
+ let toolArgs = {};
328
+ const argsString = functionToolCall.function?.arguments?.trim();
329
+ if (!argsString || argsString === "") {
330
+ // Tool without parameters, use empty object
331
+ toolArgs = {};
332
+ }
333
+ else {
247
334
  try {
248
- // Execute PreToolUse hooks before tool execution
249
- const shouldExecuteTool = await this.executePreToolUseHooks(toolName, toolArgs, toolId);
250
- // If PreToolUse hooks blocked execution, skip tool execution
251
- if (!shouldExecuteTool) {
252
- this.logger?.info(`Tool ${toolName} execution blocked by PreToolUse hooks`);
253
- return; // Skip this tool and return from this map function
254
- }
255
- // Create tool execution context
256
- const context = {
257
- abortSignal: toolAbortController.signal,
258
- backgroundBashManager: this.backgroundBashManager,
259
- workdir: this.workdir,
260
- };
261
- // Execute tool
262
- const toolResult = await this.toolManager.execute(functionToolCall.function?.name || "", toolArgs, context);
263
- // Update message state - tool execution completed
264
- this.messageManager.updateToolBlock({
265
- toolId,
266
- args: JSON.stringify(toolArgs, null, 2),
267
- result: toolResult.content ||
268
- (toolResult.error ? `Error: ${toolResult.error}` : ""),
269
- success: toolResult.success,
270
- error: toolResult.error,
271
- isRunning: false, // isRunning: false
272
- name: toolName,
273
- shortResult: toolResult.shortResult,
274
- compactParams,
275
- });
276
- // If tool returns diff information, add diff block
277
- if (toolResult.success &&
278
- toolResult.diffResult &&
279
- toolResult.filePath) {
280
- this.messageManager.addDiffBlock(toolResult.filePath, toolResult.diffResult);
281
- }
282
- // Execute PostToolUse hooks after successful tool completion
283
- await this.executePostToolUseHooks(toolId, toolName, toolArgs, toolResult);
335
+ toolArgs = JSON.parse(argsString);
284
336
  }
285
- catch (toolError) {
286
- const errorMessage = toolError instanceof Error
287
- ? toolError.message
288
- : String(toolError);
337
+ catch (parseError) {
338
+ // For non-empty but malformed JSON, still throw exception
339
+ const errorMessage = `Failed to parse tool arguments, finish_reason: ${result.finish_reason}`;
340
+ const fullErrorMessage = `${errorMessage}\nAI response headers:, ${JSON.stringify(result.response_headers)}`;
341
+ this.logger?.error(fullErrorMessage, parseError);
289
342
  this.messageManager.updateToolBlock({
290
- toolId,
291
- args: JSON.stringify(toolArgs, null, 2),
292
- result: `Tool execution failed: ${errorMessage}`,
343
+ id: toolId,
344
+ parameters: argsString,
345
+ result: errorMessage,
293
346
  success: false,
294
- error: errorMessage,
295
- isRunning: false,
347
+ error: fullErrorMessage,
348
+ stage: "end",
296
349
  name: toolName,
297
- compactParams,
350
+ compactParams: "",
298
351
  });
352
+ return;
353
+ }
354
+ }
355
+ const compactParams = this.generateCompactParams(toolName, toolArgs);
356
+ // Emit running stage for non-streaming tool calls (tool execution about to start)
357
+ this.messageManager.updateToolBlock({
358
+ id: toolId,
359
+ stage: "running",
360
+ name: toolName,
361
+ compactParams,
362
+ parameters: argsString,
363
+ parametersChunk: "",
364
+ });
365
+ try {
366
+ // Execute PreToolUse hooks before tool execution
367
+ const shouldExecuteTool = await this.executePreToolUseHooks(toolName, toolArgs, toolId);
368
+ // If PreToolUse hooks blocked execution, skip tool execution
369
+ if (!shouldExecuteTool) {
370
+ this.logger?.info(`Tool ${toolName} execution blocked by PreToolUse hooks`);
371
+ return; // Skip this tool and return from this map function
372
+ }
373
+ // Create tool execution context
374
+ const context = {
375
+ abortSignal: toolAbortController.signal,
376
+ backgroundBashManager: this.backgroundBashManager,
377
+ workdir: this.workdir,
378
+ };
379
+ // Execute tool
380
+ const toolResult = await this.toolManager.execute(functionToolCall.function?.name || "", toolArgs, context);
381
+ // Update message state - tool execution completed
382
+ this.messageManager.updateToolBlock({
383
+ id: toolId,
384
+ parameters: argsString,
385
+ result: toolResult.content ||
386
+ (toolResult.error ? `Error: ${toolResult.error}` : ""),
387
+ success: toolResult.success,
388
+ error: toolResult.error,
389
+ stage: "end",
390
+ name: toolName,
391
+ shortResult: toolResult.shortResult,
392
+ });
393
+ // If tool returns diff information, add diff block
394
+ if (toolResult.success &&
395
+ toolResult.diffResult &&
396
+ toolResult.filePath) {
397
+ this.messageManager.addDiffBlock(toolResult.filePath, toolResult.diffResult);
299
398
  }
399
+ // Execute PostToolUse hooks after successful tool completion
400
+ await this.executePostToolUseHooks(toolId, toolName, toolArgs, toolResult);
300
401
  }
301
- catch (parseError) {
302
- const errorMessage = parseError instanceof Error
303
- ? parseError.message
304
- : String(parseError);
305
- this.messageManager.addErrorBlock(`Failed to parse tool arguments for ${functionToolCall.function?.name}: ${errorMessage}`);
402
+ catch (toolError) {
403
+ const errorMessage = toolError instanceof Error
404
+ ? toolError.message
405
+ : String(toolError);
406
+ this.messageManager.updateToolBlock({
407
+ id: toolId,
408
+ parameters: JSON.stringify(toolArgs, null, 2),
409
+ result: `Tool execution failed: ${errorMessage}`,
410
+ success: false,
411
+ error: errorMessage,
412
+ stage: "end",
413
+ name: toolName,
414
+ compactParams,
415
+ });
306
416
  }
307
417
  });
308
418
  // Wait for all tools to complete execution in parallel
@@ -328,66 +438,70 @@ export class AIManager {
328
438
  this.messageManager.addErrorBlock(error instanceof Error ? error.message : "Unknown error occurred");
329
439
  }
330
440
  finally {
331
- // Only execute Stop hooks for the initial call
441
+ // Only execute cleanup and hooks for the initial call
332
442
  if (recursionDepth === 0) {
333
- // Execute Stop hooks only if the operation was not aborted
443
+ // Save session in each recursion to ensure message persistence
444
+ await this.messageManager.saveSession();
445
+ // Set loading to false first
446
+ this.setIsLoading(false);
447
+ // Clear abort controllers
448
+ this.abortController = null;
449
+ this.toolAbortController = null;
450
+ // Execute Stop/SubagentStop hooks only if the operation was not aborted
334
451
  const isCurrentlyAborted = abortController.signal.aborted || toolAbortController.signal.aborted;
335
452
  if (!isCurrentlyAborted) {
336
453
  const shouldContinue = await this.executeStopHooks();
337
- // If Stop hooks indicate we should continue (due to blocking errors),
454
+ // If Stop/SubagentStop hooks indicate we should continue (due to blocking errors),
338
455
  // restart the AI conversation cycle
339
456
  if (shouldContinue) {
340
- this.logger?.info("Stop hooks indicate issues need fixing, continuing conversation...");
457
+ this.logger?.info(`${this.subagentType ? "SubagentStop" : "Stop"} hooks indicate issues need fixing, continuing conversation...`);
341
458
  // Restart the conversation to let AI fix the issues
342
- // Use recursionDepth = 1 to prevent Stop hooks from running again in continuation
459
+ // Use recursionDepth = 0 to set loading false again for continuation
343
460
  await this.sendAIMessage({
344
- recursionDepth: 1,
461
+ recursionDepth: 0,
345
462
  model,
346
463
  allowedTools,
347
464
  });
348
465
  }
349
466
  }
350
- // Save session after all operations (including continuation) are complete
351
- await this.messageManager.saveSession();
352
- // Clear abort controllers and loading state after all operations are complete
353
- this.abortController = null;
354
- this.toolAbortController = null;
355
- // Set loading to false at the very end, after all operations including continuation
356
- this.setIsLoading(false);
357
467
  }
358
468
  }
359
469
  }
360
470
  /**
361
- * Execute Stop hooks when AI response cycle completes
471
+ * Execute Stop or SubagentStop hooks when AI response cycle completes
472
+ * Uses "SubagentStop" hook name when triggered by a subagent, otherwise uses "Stop"
362
473
  * @returns Promise<boolean> - true if should continue conversation, false if should stop
363
474
  */
364
475
  async executeStopHooks() {
365
476
  if (!this.hookManager)
366
477
  return false;
367
478
  try {
479
+ // Use "SubagentStop" hook name when triggered by a subagent, otherwise use "Stop"
480
+ const hookName = this.subagentType ? "SubagentStop" : "Stop";
368
481
  const context = {
369
- event: "Stop",
482
+ event: hookName,
370
483
  projectDir: this.workdir,
371
484
  timestamp: new Date(),
372
485
  sessionId: this.messageManager.getSessionId(),
373
486
  transcriptPath: this.messageManager.getTranscriptPath(),
374
487
  cwd: this.workdir,
488
+ subagentType: this.subagentType, // Include subagent type in hook context
375
489
  // Stop hooks don't need toolName, toolInput, toolResponse, or userPrompt
376
490
  };
377
- const results = await this.hookManager.executeHooks("Stop", context);
491
+ const results = await this.hookManager.executeHooks(hookName, context);
378
492
  // Process hook results to handle exit codes and appropriate responses
379
493
  let shouldContinue = false;
380
494
  if (results.length > 0) {
381
- const processResult = this.hookManager.processHookResults("Stop", results, this.messageManager);
495
+ const processResult = this.hookManager.processHookResults(hookName, results, this.messageManager);
382
496
  // If hook processing indicates we should block (exit code 2), continue conversation
383
497
  if (processResult.shouldBlock) {
384
- this.logger?.info("Stop hook blocked stopping with error:", processResult.errorMessage);
498
+ this.logger?.info(`${hookName} hook blocked stopping with error:`, processResult.errorMessage);
385
499
  shouldContinue = true;
386
500
  }
387
501
  }
388
502
  // Log hook execution results for debugging
389
503
  if (results.length > 0) {
390
- this.logger?.debug(`Executed ${results.length} Stop hook(s):`, results.map((r) => ({
504
+ this.logger?.debug(`Executed ${results.length} ${hookName} hook(s):`, results.map((r) => ({
391
505
  success: r.success,
392
506
  duration: r.duration,
393
507
  exitCode: r.exitCode,
@@ -399,7 +513,7 @@ export class AIManager {
399
513
  }
400
514
  catch (error) {
401
515
  // Hook execution errors should not interrupt the main workflow
402
- this.logger?.error("Stop hook execution failed:", error);
516
+ this.logger?.error(`${this.subagentType ? "SubagentStop" : "Stop"} hook execution failed:`, error);
403
517
  return false;
404
518
  }
405
519
  }
@@ -420,12 +534,14 @@ export class AIManager {
420
534
  transcriptPath: this.messageManager.getTranscriptPath(),
421
535
  cwd: this.workdir,
422
536
  toolInput,
537
+ subagentType: this.subagentType, // Include subagent type in hook context
423
538
  };
424
539
  const results = await this.hookManager.executeHooks("PreToolUse", context);
425
540
  // Process hook results to handle exit codes and determine if tool should be blocked
426
541
  let shouldContinue = true;
427
542
  if (results.length > 0) {
428
- const processResult = this.hookManager.processHookResults("PreToolUse", results, this.messageManager, toolId);
543
+ const processResult = this.hookManager.processHookResults("PreToolUse", results, this.messageManager, toolId, // Pass toolId for proper PreToolUse blocking error handling
544
+ JSON.stringify(toolInput || {}, null, 2));
429
545
  shouldContinue = !processResult.shouldBlock;
430
546
  }
431
547
  // Log hook execution results for debugging
@@ -463,12 +579,12 @@ export class AIManager {
463
579
  cwd: this.workdir,
464
580
  toolInput,
465
581
  toolResponse,
582
+ subagentType: this.subagentType, // Include subagent type in hook context
466
583
  };
467
584
  const results = await this.hookManager.executeHooks("PostToolUse", context);
468
585
  // Process hook results to handle exit codes and update tool results
469
586
  if (results.length > 0) {
470
- const originalToolResult = toolResponse?.content || "";
471
- this.hookManager.processHookResults("PostToolUse", results, this.messageManager, toolId, originalToolResult);
587
+ this.hookManager.processHookResults("PostToolUse", results, this.messageManager, toolId);
472
588
  }
473
589
  // Log hook execution results for debugging
474
590
  if (results.length > 0) {
@@ -1 +1 @@
1
- {"version":3,"file":"backgroundBashManager.d.ts","sourceRoot":"","sources":["../../src/managers/backgroundBashManager.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAEzD,MAAM,WAAW,8BAA8B;IAC7C,cAAc,CAAC,EAAE,CAAC,MAAM,EAAE,eAAe,EAAE,KAAK,IAAI,CAAC;CACtD;AAED,MAAM,WAAW,4BAA4B;IAC3C,SAAS,CAAC,EAAE,8BAA8B,CAAC;IAC3C,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,qBAAa,qBAAqB;IAChC,OAAO,CAAC,MAAM,CAAsC;IACpD,OAAO,CAAC,MAAM,CAAK;IACnB,OAAO,CAAC,SAAS,CAAiC;IAClD,OAAO,CAAC,OAAO,CAAS;gBAEZ,OAAO,EAAE,4BAA4B;IAKjD,OAAO,CAAC,kBAAkB;IAInB,UAAU,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM;IAsErD,QAAQ,CAAC,EAAE,EAAE,MAAM,GAAG,eAAe,GAAG,SAAS;IAIjD,YAAY,IAAI,eAAe,EAAE;IAIjC,SAAS,CACd,EAAE,EAAE,MAAM,EACV,MAAM,CAAC,EAAE,MAAM,GACd;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI;IAiCrD,SAAS,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO;IAmD9B,OAAO,IAAI,IAAI;CAUvB"}
1
+ {"version":3,"file":"backgroundBashManager.d.ts","sourceRoot":"","sources":["../../src/managers/backgroundBashManager.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAEzD,MAAM,WAAW,8BAA8B;IAC7C,cAAc,CAAC,EAAE,CAAC,MAAM,EAAE,eAAe,EAAE,KAAK,IAAI,CAAC;CACtD;AAED,MAAM,WAAW,4BAA4B;IAC3C,SAAS,CAAC,EAAE,8BAA8B,CAAC;IAC3C,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,qBAAa,qBAAqB;IAChC,OAAO,CAAC,MAAM,CAAsC;IACpD,OAAO,CAAC,MAAM,CAAK;IACnB,OAAO,CAAC,SAAS,CAAiC;IAClD,OAAO,CAAC,OAAO,CAAS;gBAEZ,OAAO,EAAE,4BAA4B;IAKjD,OAAO,CAAC,kBAAkB;IAInB,UAAU,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM;IAsErD,QAAQ,CAAC,EAAE,EAAE,MAAM,GAAG,eAAe,GAAG,SAAS;IAIjD,YAAY,IAAI,eAAe,EAAE;IAIjC,SAAS,CACd,EAAE,EAAE,MAAM,EACV,MAAM,CAAC,EAAE,MAAM,GACd;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI;IAiCrD,SAAS,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO;IAmD9B,OAAO,IAAI,IAAI;CAUvB"}
@@ -1,4 +1,5 @@
1
1
  import { spawn } from "child_process";
2
+ import { logger } from "../utils/globalLogger.js";
2
3
  export class BackgroundBashManager {
3
4
  constructor(options) {
4
5
  this.shells = new Map();
@@ -95,8 +96,8 @@ export class BackgroundBashManager {
95
96
  .filter((line) => regex.test(line))
96
97
  .join("\n");
97
98
  }
98
- catch {
99
- // logger.warn(`Invalid filter regex: ${filter}`, error);
99
+ catch (error) {
100
+ logger.warn(`Invalid filter regex: ${filter}`, error);
100
101
  }
101
102
  }
102
103
  return {
@@ -122,8 +123,8 @@ export class BackgroundBashManager {
122
123
  try {
123
124
  process.kill(-shell.process.pid, "SIGKILL");
124
125
  }
125
- catch {
126
- // logger.error("Failed to force kill process:", error);
126
+ catch (error) {
127
+ logger.error("Failed to force kill process:", error);
127
128
  }
128
129
  }
129
130
  }, 1000);
@@ -147,8 +148,8 @@ export class BackgroundBashManager {
147
148
  this.notifyShellsChange();
148
149
  return true;
149
150
  }
150
- catch {
151
- // logger.error("Failed to kill child process:", directKillError);
151
+ catch (directKillError) {
152
+ logger.error("Failed to kill child process:", directKillError);
152
153
  return false;
153
154
  }
154
155
  }