wave-agent-sdk 0.0.1

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 (170) hide show
  1. package/README.md +32 -0
  2. package/dist/agent.d.ts +96 -0
  3. package/dist/agent.d.ts.map +1 -0
  4. package/dist/agent.js +286 -0
  5. package/dist/hooks/executor.d.ts +56 -0
  6. package/dist/hooks/executor.d.ts.map +1 -0
  7. package/dist/hooks/executor.js +312 -0
  8. package/dist/hooks/index.d.ts +17 -0
  9. package/dist/hooks/index.d.ts.map +1 -0
  10. package/dist/hooks/index.js +14 -0
  11. package/dist/hooks/manager.d.ts +90 -0
  12. package/dist/hooks/manager.d.ts.map +1 -0
  13. package/dist/hooks/manager.js +395 -0
  14. package/dist/hooks/matcher.d.ts +49 -0
  15. package/dist/hooks/matcher.d.ts.map +1 -0
  16. package/dist/hooks/matcher.js +147 -0
  17. package/dist/hooks/settings.d.ts +46 -0
  18. package/dist/hooks/settings.d.ts.map +1 -0
  19. package/dist/hooks/settings.js +100 -0
  20. package/dist/hooks/types.d.ts +80 -0
  21. package/dist/hooks/types.d.ts.map +1 -0
  22. package/dist/hooks/types.js +59 -0
  23. package/dist/index.d.ts +16 -0
  24. package/dist/index.d.ts.map +1 -0
  25. package/dist/index.js +20 -0
  26. package/dist/managers/aiManager.d.ts +61 -0
  27. package/dist/managers/aiManager.d.ts.map +1 -0
  28. package/dist/managers/aiManager.js +415 -0
  29. package/dist/managers/backgroundBashManager.d.ts +27 -0
  30. package/dist/managers/backgroundBashManager.d.ts.map +1 -0
  31. package/dist/managers/backgroundBashManager.js +166 -0
  32. package/dist/managers/bashManager.d.ts +20 -0
  33. package/dist/managers/bashManager.d.ts.map +1 -0
  34. package/dist/managers/bashManager.js +66 -0
  35. package/dist/managers/mcpManager.d.ts +63 -0
  36. package/dist/managers/mcpManager.d.ts.map +1 -0
  37. package/dist/managers/mcpManager.js +378 -0
  38. package/dist/managers/messageManager.d.ts +85 -0
  39. package/dist/managers/messageManager.d.ts.map +1 -0
  40. package/dist/managers/messageManager.js +265 -0
  41. package/dist/managers/skillManager.d.ts +59 -0
  42. package/dist/managers/skillManager.d.ts.map +1 -0
  43. package/dist/managers/skillManager.js +317 -0
  44. package/dist/managers/slashCommandManager.d.ts +77 -0
  45. package/dist/managers/slashCommandManager.d.ts.map +1 -0
  46. package/dist/managers/slashCommandManager.js +208 -0
  47. package/dist/managers/toolManager.d.ts +23 -0
  48. package/dist/managers/toolManager.d.ts.map +1 -0
  49. package/dist/managers/toolManager.js +79 -0
  50. package/dist/services/aiService.d.ts +28 -0
  51. package/dist/services/aiService.d.ts.map +1 -0
  52. package/dist/services/aiService.js +180 -0
  53. package/dist/services/memory.d.ts +8 -0
  54. package/dist/services/memory.d.ts.map +1 -0
  55. package/dist/services/memory.js +128 -0
  56. package/dist/services/session.d.ts +54 -0
  57. package/dist/services/session.d.ts.map +1 -0
  58. package/dist/services/session.js +196 -0
  59. package/dist/tools/bashTool.d.ts +14 -0
  60. package/dist/tools/bashTool.d.ts.map +1 -0
  61. package/dist/tools/bashTool.js +351 -0
  62. package/dist/tools/deleteFileTool.d.ts +6 -0
  63. package/dist/tools/deleteFileTool.d.ts.map +1 -0
  64. package/dist/tools/deleteFileTool.js +67 -0
  65. package/dist/tools/editTool.d.ts +6 -0
  66. package/dist/tools/editTool.d.ts.map +1 -0
  67. package/dist/tools/editTool.js +168 -0
  68. package/dist/tools/globTool.d.ts +6 -0
  69. package/dist/tools/globTool.d.ts.map +1 -0
  70. package/dist/tools/globTool.js +113 -0
  71. package/dist/tools/grepTool.d.ts +6 -0
  72. package/dist/tools/grepTool.d.ts.map +1 -0
  73. package/dist/tools/grepTool.js +268 -0
  74. package/dist/tools/lsTool.d.ts +6 -0
  75. package/dist/tools/lsTool.d.ts.map +1 -0
  76. package/dist/tools/lsTool.js +160 -0
  77. package/dist/tools/multiEditTool.d.ts +6 -0
  78. package/dist/tools/multiEditTool.d.ts.map +1 -0
  79. package/dist/tools/multiEditTool.js +222 -0
  80. package/dist/tools/readTool.d.ts +6 -0
  81. package/dist/tools/readTool.d.ts.map +1 -0
  82. package/dist/tools/readTool.js +136 -0
  83. package/dist/tools/types.d.ts +35 -0
  84. package/dist/tools/types.d.ts.map +1 -0
  85. package/dist/tools/types.js +4 -0
  86. package/dist/tools/writeTool.d.ts +6 -0
  87. package/dist/tools/writeTool.d.ts.map +1 -0
  88. package/dist/tools/writeTool.js +138 -0
  89. package/dist/types.d.ts +212 -0
  90. package/dist/types.d.ts.map +1 -0
  91. package/dist/types.js +13 -0
  92. package/dist/utils/bashHistory.d.ts +46 -0
  93. package/dist/utils/bashHistory.d.ts.map +1 -0
  94. package/dist/utils/bashHistory.js +236 -0
  95. package/dist/utils/commandArgumentParser.d.ts +34 -0
  96. package/dist/utils/commandArgumentParser.d.ts.map +1 -0
  97. package/dist/utils/commandArgumentParser.js +123 -0
  98. package/dist/utils/constants.d.ts +27 -0
  99. package/dist/utils/constants.d.ts.map +1 -0
  100. package/dist/utils/constants.js +28 -0
  101. package/dist/utils/convertMessagesForAPI.d.ts +9 -0
  102. package/dist/utils/convertMessagesForAPI.d.ts.map +1 -0
  103. package/dist/utils/convertMessagesForAPI.js +189 -0
  104. package/dist/utils/customCommands.d.ts +14 -0
  105. package/dist/utils/customCommands.d.ts.map +1 -0
  106. package/dist/utils/customCommands.js +71 -0
  107. package/dist/utils/fileFilter.d.ts +26 -0
  108. package/dist/utils/fileFilter.d.ts.map +1 -0
  109. package/dist/utils/fileFilter.js +177 -0
  110. package/dist/utils/markdownParser.d.ts +27 -0
  111. package/dist/utils/markdownParser.d.ts.map +1 -0
  112. package/dist/utils/markdownParser.js +109 -0
  113. package/dist/utils/mcpUtils.d.ts +24 -0
  114. package/dist/utils/mcpUtils.d.ts.map +1 -0
  115. package/dist/utils/mcpUtils.js +51 -0
  116. package/dist/utils/messageOperations.d.ts +118 -0
  117. package/dist/utils/messageOperations.d.ts.map +1 -0
  118. package/dist/utils/messageOperations.js +334 -0
  119. package/dist/utils/path.d.ts +25 -0
  120. package/dist/utils/path.d.ts.map +1 -0
  121. package/dist/utils/path.js +109 -0
  122. package/dist/utils/skillParser.d.ts +18 -0
  123. package/dist/utils/skillParser.d.ts.map +1 -0
  124. package/dist/utils/skillParser.js +147 -0
  125. package/dist/utils/stringUtils.d.ts +13 -0
  126. package/dist/utils/stringUtils.d.ts.map +1 -0
  127. package/dist/utils/stringUtils.js +44 -0
  128. package/package.json +51 -0
  129. package/src/agent.ts +405 -0
  130. package/src/hooks/executor.ts +440 -0
  131. package/src/hooks/index.ts +52 -0
  132. package/src/hooks/manager.ts +618 -0
  133. package/src/hooks/matcher.ts +187 -0
  134. package/src/hooks/settings.ts +129 -0
  135. package/src/hooks/types.ts +169 -0
  136. package/src/index.ts +24 -0
  137. package/src/managers/aiManager.ts +573 -0
  138. package/src/managers/backgroundBashManager.ts +203 -0
  139. package/src/managers/bashManager.ts +97 -0
  140. package/src/managers/mcpManager.ts +493 -0
  141. package/src/managers/messageManager.ts +415 -0
  142. package/src/managers/skillManager.ts +404 -0
  143. package/src/managers/slashCommandManager.ts +293 -0
  144. package/src/managers/toolManager.ts +106 -0
  145. package/src/services/aiService.ts +252 -0
  146. package/src/services/memory.ts +149 -0
  147. package/src/services/session.ts +265 -0
  148. package/src/tools/bashTool.ts +402 -0
  149. package/src/tools/deleteFileTool.ts +81 -0
  150. package/src/tools/editTool.ts +192 -0
  151. package/src/tools/globTool.ts +135 -0
  152. package/src/tools/grepTool.ts +326 -0
  153. package/src/tools/lsTool.ts +187 -0
  154. package/src/tools/multiEditTool.ts +268 -0
  155. package/src/tools/readTool.ts +165 -0
  156. package/src/tools/types.ts +47 -0
  157. package/src/tools/writeTool.ts +163 -0
  158. package/src/types.ts +260 -0
  159. package/src/utils/bashHistory.ts +303 -0
  160. package/src/utils/commandArgumentParser.ts +153 -0
  161. package/src/utils/constants.ts +37 -0
  162. package/src/utils/convertMessagesForAPI.ts +236 -0
  163. package/src/utils/customCommands.ts +85 -0
  164. package/src/utils/fileFilter.ts +202 -0
  165. package/src/utils/markdownParser.ts +156 -0
  166. package/src/utils/mcpUtils.ts +81 -0
  167. package/src/utils/messageOperations.ts +506 -0
  168. package/src/utils/path.ts +118 -0
  169. package/src/utils/skillParser.ts +188 -0
  170. package/src/utils/stringUtils.ts +50 -0
@@ -0,0 +1,573 @@
1
+ import { callAgent, compressMessages } from "../services/aiService.js";
2
+ import { getMessagesToCompress } from "../utils/messageOperations.js";
3
+ import { convertMessagesForAPI } from "../utils/convertMessagesForAPI.js";
4
+ import * as memory from "../services/memory.js";
5
+ import type { Logger } from "../types.js";
6
+ import type { ToolManager } from "./toolManager.js";
7
+ import type { ToolContext, ToolResult } from "../tools/types.js";
8
+ import type { MessageManager } from "./messageManager.js";
9
+ import type { BackgroundBashManager } from "./backgroundBashManager.js";
10
+ import { DEFAULT_TOKEN_LIMIT } from "../utils/constants.js";
11
+ import { ChatCompletionMessageFunctionToolCall } from "openai/resources.js";
12
+ import type { HookManager } from "../hooks/index.js";
13
+ import type { ExtendedHookExecutionContext } from "../hooks/types.js";
14
+
15
+ export interface AIManagerCallbacks {
16
+ onCompressionStateChange?: (isCompressing: boolean) => void;
17
+ }
18
+
19
+ export interface AIManagerOptions {
20
+ messageManager: MessageManager;
21
+ toolManager: ToolManager;
22
+ logger?: Logger;
23
+ backgroundBashManager?: BackgroundBashManager;
24
+ hookManager?: HookManager;
25
+ callbacks?: AIManagerCallbacks;
26
+ workdir: string;
27
+ systemPrompt?: string;
28
+ }
29
+
30
+ export class AIManager {
31
+ public isLoading: boolean = false;
32
+ private abortController: AbortController | null = null;
33
+ private toolAbortController: AbortController | null = null;
34
+ private logger?: Logger;
35
+ private toolManager: ToolManager;
36
+ private messageManager: MessageManager;
37
+ private backgroundBashManager?: BackgroundBashManager;
38
+ private hookManager?: HookManager;
39
+ private workdir: string;
40
+ private systemPrompt?: string;
41
+
42
+ constructor(options: AIManagerOptions) {
43
+ this.messageManager = options.messageManager;
44
+ this.toolManager = options.toolManager;
45
+ this.backgroundBashManager = options.backgroundBashManager;
46
+ this.hookManager = options.hookManager;
47
+ this.logger = options.logger;
48
+ this.workdir = options.workdir;
49
+ this.systemPrompt = options.systemPrompt;
50
+ this.callbacks = options.callbacks ?? {};
51
+ }
52
+
53
+ private isCompressing: boolean = false;
54
+ private callbacks: AIManagerCallbacks;
55
+
56
+ /**
57
+ * Get filtered tool configuration
58
+ */
59
+ private getFilteredToolsConfig(allowedTools?: string[]) {
60
+ const allTools = this.toolManager.getToolsConfig();
61
+
62
+ // If no allowedTools specified, return all tools
63
+ if (!allowedTools || allowedTools.length === 0) {
64
+ return allTools;
65
+ }
66
+
67
+ // Filter allowed tools
68
+ return allTools.filter((tool) => allowedTools.includes(tool.function.name));
69
+ }
70
+
71
+ public setIsLoading(isLoading: boolean): void {
72
+ this.isLoading = isLoading;
73
+ }
74
+
75
+ public abortAIMessage(): void {
76
+ // Interrupt AI service
77
+ if (this.abortController) {
78
+ try {
79
+ this.abortController.abort();
80
+ } catch (error) {
81
+ this.logger?.error("Failed to abort AI service:", error);
82
+ }
83
+ }
84
+
85
+ // Interrupt tool execution
86
+ if (this.toolAbortController) {
87
+ try {
88
+ this.toolAbortController.abort();
89
+ } catch (error) {
90
+ this.logger?.error("Failed to abort tool execution:", error);
91
+ }
92
+ }
93
+
94
+ this.setIsLoading(false);
95
+ }
96
+
97
+ // Helper method to generate compactParams
98
+ private generateCompactParams(
99
+ toolName: string,
100
+ toolArgs: Record<string, unknown>,
101
+ ): string | undefined {
102
+ try {
103
+ const toolPlugin = this.toolManager
104
+ .list()
105
+ .find((plugin) => plugin.name === toolName);
106
+ if (toolPlugin?.formatCompactParams) {
107
+ const context: ToolContext = {
108
+ workdir: this.workdir,
109
+ };
110
+ return toolPlugin.formatCompactParams(toolArgs, context);
111
+ }
112
+ } catch (error) {
113
+ this.logger?.warn("Failed to generate compactParams", error);
114
+ }
115
+ return undefined;
116
+ }
117
+
118
+ // Private method to handle token statistics and message compression
119
+ private async handleTokenUsageAndCompression(
120
+ usage: { total_tokens: number } | undefined,
121
+ abortController: AbortController,
122
+ ): Promise<void> {
123
+ if (!usage) return;
124
+
125
+ // Update token statistics - display latest token usage
126
+ this.messageManager.setlatestTotalTokens(usage.total_tokens);
127
+
128
+ // Check if token limit exceeded
129
+ const tokenLimit = parseInt(
130
+ process.env.TOKEN_LIMIT || `${DEFAULT_TOKEN_LIMIT}`,
131
+ 10,
132
+ );
133
+
134
+ if (usage.total_tokens > tokenLimit) {
135
+ this.logger?.info(
136
+ `Token usage exceeded ${tokenLimit}, compressing messages...`,
137
+ );
138
+
139
+ // Check if messages need compression
140
+ const { messagesToCompress, insertIndex } = getMessagesToCompress(
141
+ this.messageManager.getMessages(),
142
+ 7,
143
+ );
144
+
145
+ // If there are messages to compress, perform compression
146
+ if (messagesToCompress.length > 0) {
147
+ const recentChatMessages = convertMessagesForAPI(messagesToCompress);
148
+
149
+ this.setIsCompressing(true);
150
+ try {
151
+ const compressedContent = await compressMessages({
152
+ messages: recentChatMessages,
153
+ abortSignal: abortController.signal,
154
+ });
155
+
156
+ // Execute message reconstruction and sessionId update after compression
157
+ this.messageManager.compressMessagesAndUpdateSession(
158
+ insertIndex,
159
+ compressedContent,
160
+ );
161
+
162
+ this.logger?.info(
163
+ `Successfully compressed ${messagesToCompress.length} messages and updated session`,
164
+ );
165
+ } catch (compressError) {
166
+ this.logger?.error("Failed to compress messages:", compressError);
167
+ } finally {
168
+ this.setIsCompressing(false);
169
+ }
170
+ }
171
+ }
172
+ }
173
+
174
+ public getIsCompressing(): boolean {
175
+ return this.isCompressing;
176
+ }
177
+
178
+ public setIsCompressing(isCompressing: boolean): void {
179
+ if (this.isCompressing !== isCompressing) {
180
+ this.isCompressing = isCompressing;
181
+ this.callbacks.onCompressionStateChange?.(isCompressing);
182
+ }
183
+ }
184
+
185
+ public async sendAIMessage(
186
+ options: {
187
+ recursionDepth?: number;
188
+ model?: string;
189
+ allowedTools?: string[];
190
+ } = {},
191
+ ): Promise<void> {
192
+ const { recursionDepth = 0, model, allowedTools } = options;
193
+ // Only check isLoading for the initial call (recursionDepth === 0)
194
+ if (recursionDepth === 0 && this.isLoading) {
195
+ return;
196
+ }
197
+
198
+ // Create new AbortController
199
+ const abortController = new AbortController();
200
+ this.abortController = abortController;
201
+
202
+ // Create separate AbortController for tool execution
203
+ const toolAbortController = new AbortController();
204
+ this.toolAbortController = toolAbortController;
205
+
206
+ // Only set loading state for the initial call
207
+ if (recursionDepth === 0) {
208
+ this.setIsLoading(true);
209
+ }
210
+
211
+ // Get recent message history
212
+ const recentMessages = convertMessagesForAPI(
213
+ this.messageManager.getMessages(),
214
+ );
215
+
216
+ try {
217
+ // Get combined memory content
218
+ const combinedMemory = await memory.getCombinedMemoryContent(
219
+ this.workdir,
220
+ );
221
+
222
+ // Call AI service (non-streaming)
223
+ const result = await callAgent({
224
+ messages: recentMessages,
225
+ sessionId: this.messageManager.getSessionId(),
226
+ abortSignal: abortController.signal,
227
+ memory: combinedMemory, // Pass combined memory content
228
+ workdir: this.workdir, // Pass working directory
229
+ tools: this.getFilteredToolsConfig(allowedTools), // Pass filtered tool configuration
230
+ model: model, // Use passed model
231
+ systemPrompt: this.systemPrompt, // Pass custom system prompt
232
+ });
233
+
234
+ // Collect content and tool calls
235
+ const content = result.content || "";
236
+ const toolCalls: ChatCompletionMessageFunctionToolCall[] = [];
237
+
238
+ if (result.tool_calls) {
239
+ for (const toolCall of result.tool_calls) {
240
+ if (toolCall.type === "function") {
241
+ toolCalls.push(toolCall);
242
+ }
243
+ }
244
+ }
245
+
246
+ // Add assistant message at once (including content and tool calls)
247
+ this.messageManager.addAssistantMessage(content, toolCalls);
248
+
249
+ if (toolCalls.length > 0) {
250
+ for (const functionToolCall of toolCalls) {
251
+ const toolId = functionToolCall.id || "";
252
+ // Execute tool
253
+ try {
254
+ // Check if already interrupted, skip tool execution if so
255
+ if (
256
+ abortController.signal.aborted ||
257
+ toolAbortController.signal.aborted
258
+ ) {
259
+ return;
260
+ }
261
+
262
+ // Safely parse tool parameters, handle tools without parameters
263
+ let toolArgs: Record<string, unknown> = {};
264
+ const argsString = functionToolCall.function?.arguments?.trim();
265
+
266
+ if (!argsString || argsString === "") {
267
+ // Tool without parameters, use empty object
268
+ toolArgs = {};
269
+ } else {
270
+ try {
271
+ toolArgs = JSON.parse(argsString);
272
+ } catch (parseError) {
273
+ // For non-empty but malformed JSON, still throw exception
274
+ const errorMessage = `Failed to parse tool arguments: ${argsString}`;
275
+ this.logger?.error(errorMessage, parseError);
276
+ throw new Error(errorMessage);
277
+ }
278
+ }
279
+
280
+ // Set tool start execution state
281
+ const toolName = functionToolCall.function?.name || "";
282
+ const compactParams = this.generateCompactParams(
283
+ toolName,
284
+ toolArgs,
285
+ );
286
+
287
+ this.messageManager.updateToolBlock({
288
+ toolId,
289
+ args: JSON.stringify(toolArgs, null, 2),
290
+ isRunning: true, // isRunning: true
291
+ name: toolName,
292
+ compactParams,
293
+ });
294
+
295
+ try {
296
+ // Execute PreToolUse hooks before tool execution
297
+ await this.executePreToolUseHooks(toolName, toolArgs);
298
+
299
+ // Create tool execution context
300
+ const context: ToolContext = {
301
+ abortSignal: toolAbortController.signal,
302
+ backgroundBashManager: this.backgroundBashManager,
303
+ workdir: this.workdir,
304
+ };
305
+
306
+ // Execute tool
307
+ const toolResult = await this.toolManager.execute(
308
+ functionToolCall.function?.name || "",
309
+ toolArgs,
310
+ context,
311
+ );
312
+
313
+ // Update message state - tool execution completed
314
+ this.messageManager.updateToolBlock({
315
+ toolId,
316
+ args: JSON.stringify(toolArgs, null, 2),
317
+ result:
318
+ toolResult.content ||
319
+ (toolResult.error ? `Error: ${toolResult.error}` : ""),
320
+ success: toolResult.success,
321
+ error: toolResult.error,
322
+ isRunning: false, // isRunning: false
323
+ name: toolName,
324
+ shortResult: toolResult.shortResult,
325
+ compactParams,
326
+ });
327
+
328
+ // If tool returns diff information, add diff block
329
+ if (
330
+ toolResult.success &&
331
+ toolResult.diffResult &&
332
+ toolResult.filePath
333
+ ) {
334
+ this.messageManager.addDiffBlock(
335
+ toolResult.filePath,
336
+ toolResult.diffResult,
337
+ );
338
+ }
339
+
340
+ // Execute PostToolUse hooks after successful tool completion
341
+ await this.executePostToolUseHooks(
342
+ toolName,
343
+ toolArgs,
344
+ toolResult,
345
+ );
346
+ } catch (toolError) {
347
+ const errorMessage =
348
+ toolError instanceof Error
349
+ ? toolError.message
350
+ : String(toolError);
351
+
352
+ this.messageManager.updateToolBlock({
353
+ toolId,
354
+ args: JSON.stringify(toolArgs, null, 2),
355
+ result: `Tool execution failed: ${errorMessage}`,
356
+ success: false,
357
+ error: errorMessage,
358
+ isRunning: false,
359
+ name: toolName,
360
+ compactParams,
361
+ });
362
+ }
363
+ } catch (parseError) {
364
+ // Check if it's a parsing error due to interruption
365
+ const isAborted =
366
+ abortController.signal.aborted ||
367
+ toolAbortController.signal.aborted;
368
+
369
+ if (isAborted) {
370
+ // If interrupted, return directly without showing error
371
+ return;
372
+ }
373
+
374
+ const errorMessage =
375
+ parseError instanceof Error
376
+ ? parseError.message
377
+ : String(parseError);
378
+ this.messageManager.addErrorBlock(
379
+ `Failed to parse tool arguments for ${functionToolCall.function?.name}: ${errorMessage}`,
380
+ );
381
+ }
382
+ }
383
+ }
384
+
385
+ // Handle token statistics and message compression
386
+ await this.handleTokenUsageAndCompression(result.usage, abortController);
387
+
388
+ // Check if there are tool operations, if so automatically initiate next AI service call
389
+ if (toolCalls.length > 0) {
390
+ // Check interruption status
391
+ const isCurrentlyAborted =
392
+ abortController.signal.aborted || toolAbortController.signal.aborted;
393
+
394
+ // AI service call ends, clear abort controller
395
+ this.abortController = null;
396
+
397
+ // Clear tool AbortController after tool execution completes
398
+ this.toolAbortController = null;
399
+
400
+ if (!isCurrentlyAborted) {
401
+ // Recursively call AI service, increment recursion depth, and pass same configuration
402
+ await this.sendAIMessage({
403
+ recursionDepth: recursionDepth + 1,
404
+ model,
405
+ allowedTools,
406
+ });
407
+ }
408
+ } else {
409
+ // Clear abort controller when no tool operations
410
+ this.abortController = null;
411
+ this.toolAbortController = null;
412
+ }
413
+ } catch (error) {
414
+ // Check if error is due to user interrupt operation
415
+ const isAborted =
416
+ abortController.signal.aborted ||
417
+ toolAbortController.signal.aborted ||
418
+ (error instanceof Error &&
419
+ (error.name === "AbortError" || error.message.includes("aborted")));
420
+
421
+ if (!isAborted) {
422
+ this.messageManager.addErrorBlock(
423
+ error instanceof Error ? error.message : "Unknown error occurred",
424
+ );
425
+ }
426
+
427
+ // Reset abort controller on error
428
+ this.abortController = null;
429
+ this.toolAbortController = null;
430
+ } finally {
431
+ // Only clear loading state for the initial call
432
+ if (recursionDepth === 0) {
433
+ this.setIsLoading(false);
434
+
435
+ // Save session before executing Stop hooks
436
+ await this.messageManager.saveSession();
437
+
438
+ // Execute Stop hooks when AI response cycle completes
439
+ await this.executeStopHooks();
440
+ }
441
+ }
442
+ }
443
+
444
+ /**
445
+ * Execute Stop hooks when AI response cycle completes
446
+ */
447
+ private async executeStopHooks(): Promise<void> {
448
+ if (!this.hookManager) return;
449
+
450
+ try {
451
+ const context: ExtendedHookExecutionContext = {
452
+ event: "Stop",
453
+ projectDir: this.workdir,
454
+ timestamp: new Date(),
455
+ sessionId: this.messageManager.getSessionId(),
456
+ transcriptPath: this.messageManager.getTranscriptPath(),
457
+ cwd: this.workdir,
458
+ // Stop hooks don't need toolName, toolInput, toolResponse, or userPrompt
459
+ };
460
+
461
+ const results = await this.hookManager.executeHooks("Stop", context);
462
+
463
+ // Log hook execution results for debugging
464
+ if (results.length > 0) {
465
+ this.logger?.debug(
466
+ `Executed ${results.length} Stop hook(s):`,
467
+ results.map((r) => ({
468
+ success: r.success,
469
+ duration: r.duration,
470
+ exitCode: r.exitCode,
471
+ timedOut: r.timedOut,
472
+ stderr: r.stderr,
473
+ })),
474
+ );
475
+ }
476
+ } catch (error) {
477
+ // Hook execution errors should not interrupt the main workflow
478
+ this.logger?.error("Stop hook execution failed:", error);
479
+ }
480
+ }
481
+
482
+ /**
483
+ * Execute PreToolUse hooks before tool execution
484
+ */
485
+ private async executePreToolUseHooks(
486
+ toolName: string,
487
+ toolInput?: Record<string, unknown>,
488
+ ): Promise<void> {
489
+ if (!this.hookManager) return;
490
+
491
+ try {
492
+ const context: ExtendedHookExecutionContext = {
493
+ event: "PreToolUse",
494
+ projectDir: this.workdir,
495
+ timestamp: new Date(),
496
+ toolName,
497
+ sessionId: this.messageManager.getSessionId(),
498
+ transcriptPath: this.messageManager.getTranscriptPath(),
499
+ cwd: this.workdir,
500
+ toolInput,
501
+ };
502
+
503
+ const results = await this.hookManager.executeHooks(
504
+ "PreToolUse",
505
+ context,
506
+ );
507
+
508
+ // Log hook execution results for debugging
509
+ if (results.length > 0) {
510
+ this.logger?.debug(
511
+ `Executed ${results.length} PreToolUse hook(s) for ${toolName}:`,
512
+ results.map((r) => ({
513
+ success: r.success,
514
+ duration: r.duration,
515
+ exitCode: r.exitCode,
516
+ timedOut: r.timedOut,
517
+ stderr: r.stderr,
518
+ })),
519
+ );
520
+ }
521
+ } catch (error) {
522
+ // Hook execution errors should not interrupt the main workflow
523
+ this.logger?.error("PreToolUse hook execution failed:", error);
524
+ }
525
+ }
526
+
527
+ /**
528
+ * Execute PostToolUse hooks after tool completion
529
+ */
530
+ private async executePostToolUseHooks(
531
+ toolName: string,
532
+ toolInput?: Record<string, unknown>,
533
+ toolResponse?: ToolResult,
534
+ ): Promise<void> {
535
+ if (!this.hookManager) return;
536
+
537
+ try {
538
+ const context: ExtendedHookExecutionContext = {
539
+ event: "PostToolUse",
540
+ projectDir: this.workdir,
541
+ timestamp: new Date(),
542
+ toolName,
543
+ sessionId: this.messageManager.getSessionId(),
544
+ transcriptPath: this.messageManager.getTranscriptPath(),
545
+ cwd: this.workdir,
546
+ toolInput,
547
+ toolResponse,
548
+ };
549
+
550
+ const results = await this.hookManager.executeHooks(
551
+ "PostToolUse",
552
+ context,
553
+ );
554
+
555
+ // Log hook execution results for debugging
556
+ if (results.length > 0) {
557
+ this.logger?.debug(
558
+ `Executed ${results.length} PostToolUse hook(s) for ${toolName}:`,
559
+ results.map((r) => ({
560
+ success: r.success,
561
+ duration: r.duration,
562
+ exitCode: r.exitCode,
563
+ timedOut: r.timedOut,
564
+ stderr: r.stderr,
565
+ })),
566
+ );
567
+ }
568
+ } catch (error) {
569
+ // Hook execution errors should not interrupt the main workflow
570
+ this.logger?.error("PostToolUse hook execution failed:", error);
571
+ }
572
+ }
573
+ }