wave-agent-sdk 0.0.4 → 0.0.6

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 (155) hide show
  1. package/dist/agent.d.ts +63 -9
  2. package/dist/agent.d.ts.map +1 -1
  3. package/dist/agent.js +103 -27
  4. package/dist/index.d.ts +3 -2
  5. package/dist/index.d.ts.map +1 -1
  6. package/dist/index.js +3 -3
  7. package/dist/managers/aiManager.d.ts +5 -2
  8. package/dist/managers/aiManager.d.ts.map +1 -1
  9. package/dist/managers/aiManager.js +121 -53
  10. package/dist/managers/backgroundBashManager.d.ts +1 -1
  11. package/dist/managers/backgroundBashManager.d.ts.map +1 -1
  12. package/dist/{hooks/manager.d.ts → managers/hookManager.d.ts} +26 -7
  13. package/dist/managers/hookManager.d.ts.map +1 -0
  14. package/dist/{hooks/manager.js → managers/hookManager.js} +108 -18
  15. package/dist/managers/mcpManager.d.ts +1 -1
  16. package/dist/managers/mcpManager.d.ts.map +1 -1
  17. package/dist/managers/mcpManager.js +5 -5
  18. package/dist/managers/messageManager.d.ts +29 -5
  19. package/dist/managers/messageManager.d.ts.map +1 -1
  20. package/dist/managers/messageManager.js +33 -12
  21. package/dist/managers/skillManager.d.ts +1 -1
  22. package/dist/managers/skillManager.d.ts.map +1 -1
  23. package/dist/managers/skillManager.js +3 -3
  24. package/dist/managers/slashCommandManager.d.ts +1 -1
  25. package/dist/managers/slashCommandManager.d.ts.map +1 -1
  26. package/dist/managers/slashCommandManager.js +1 -1
  27. package/dist/managers/subagentManager.d.ts +9 -12
  28. package/dist/managers/subagentManager.d.ts.map +1 -1
  29. package/dist/managers/subagentManager.js +43 -45
  30. package/dist/managers/toolManager.d.ts +1 -1
  31. package/dist/managers/toolManager.d.ts.map +1 -1
  32. package/dist/services/aiService.d.ts +10 -2
  33. package/dist/services/aiService.d.ts.map +1 -1
  34. package/dist/services/aiService.js +25 -4
  35. package/dist/services/hook.d.ts +56 -0
  36. package/dist/services/hook.d.ts.map +1 -0
  37. package/dist/services/hook.js +276 -0
  38. package/dist/services/memory.js +3 -3
  39. package/dist/services/session.d.ts +65 -16
  40. package/dist/services/session.d.ts.map +1 -1
  41. package/dist/services/session.js +85 -34
  42. package/dist/tools/bashTool.js +2 -2
  43. package/dist/tools/deleteFileTool.js +1 -1
  44. package/dist/tools/editTool.js +1 -1
  45. package/dist/tools/multiEditTool.js +2 -2
  46. package/dist/tools/taskTool.d.ts.map +1 -1
  47. package/dist/tools/taskTool.js +7 -3
  48. package/dist/tools/writeTool.js +1 -1
  49. package/dist/types/commands.d.ts +24 -0
  50. package/dist/types/commands.d.ts.map +1 -0
  51. package/dist/types/commands.js +5 -0
  52. package/dist/types/config.d.ts +13 -0
  53. package/dist/types/config.d.ts.map +1 -0
  54. package/dist/types/config.js +5 -0
  55. package/dist/types/core.d.ts +38 -0
  56. package/dist/types/core.d.ts.map +1 -0
  57. package/dist/{types.js → types/core.js} +4 -13
  58. package/dist/{hooks/types.d.ts → types/hooks.d.ts} +2 -1
  59. package/dist/types/hooks.d.ts.map +1 -0
  60. package/dist/types/index.d.ts +20 -0
  61. package/dist/types/index.d.ts.map +1 -0
  62. package/dist/types/index.js +21 -0
  63. package/dist/types/mcp.d.ts +28 -0
  64. package/dist/types/mcp.d.ts.map +1 -0
  65. package/dist/types/mcp.js +5 -0
  66. package/dist/types/messaging.d.ts +80 -0
  67. package/dist/types/messaging.d.ts.map +1 -0
  68. package/dist/types/messaging.js +5 -0
  69. package/dist/types/processes.d.ts +17 -0
  70. package/dist/types/processes.d.ts.map +1 -0
  71. package/dist/types/processes.js +5 -0
  72. package/dist/types/skills.d.ts +78 -0
  73. package/dist/types/skills.d.ts.map +1 -0
  74. package/dist/types/skills.js +17 -0
  75. package/dist/utils/configResolver.d.ts +1 -1
  76. package/dist/utils/configResolver.d.ts.map +1 -1
  77. package/dist/utils/configResolver.js +1 -1
  78. package/dist/utils/configValidator.d.ts +1 -1
  79. package/dist/utils/configValidator.d.ts.map +1 -1
  80. package/dist/utils/configValidator.js +1 -1
  81. package/dist/utils/convertMessagesForAPI.d.ts +1 -1
  82. package/dist/utils/convertMessagesForAPI.d.ts.map +1 -1
  83. package/dist/utils/customCommands.d.ts +1 -1
  84. package/dist/utils/customCommands.d.ts.map +1 -1
  85. package/dist/{hooks/matcher.d.ts → utils/hookMatcher.d.ts} +1 -1
  86. package/dist/utils/hookMatcher.d.ts.map +1 -0
  87. package/dist/utils/markdownParser.d.ts +1 -1
  88. package/dist/utils/markdownParser.d.ts.map +1 -1
  89. package/dist/utils/mcpUtils.d.ts +1 -1
  90. package/dist/utils/mcpUtils.d.ts.map +1 -1
  91. package/dist/utils/messageOperations.d.ts +7 -2
  92. package/dist/utils/messageOperations.d.ts.map +1 -1
  93. package/dist/utils/messageOperations.js +18 -1
  94. package/dist/utils/skillParser.d.ts +1 -1
  95. package/dist/utils/skillParser.d.ts.map +1 -1
  96. package/package.json +1 -1
  97. package/src/agent.ts +150 -50
  98. package/src/index.ts +3 -4
  99. package/src/managers/aiManager.ts +282 -164
  100. package/src/managers/backgroundBashManager.ts +1 -1
  101. package/src/{hooks/manager.ts → managers/hookManager.ts} +163 -28
  102. package/src/managers/mcpManager.ts +6 -6
  103. package/src/managers/messageManager.ts +69 -10
  104. package/src/managers/skillManager.ts +4 -4
  105. package/src/managers/slashCommandManager.ts +6 -2
  106. package/src/managers/subagentManager.ts +58 -53
  107. package/src/managers/toolManager.ts +1 -1
  108. package/src/services/aiService.ts +37 -7
  109. package/src/services/hook.ts +360 -0
  110. package/src/services/memory.ts +3 -3
  111. package/src/services/session.ts +99 -33
  112. package/src/tools/bashTool.ts +2 -2
  113. package/src/tools/deleteFileTool.ts +1 -1
  114. package/src/tools/editTool.ts +1 -1
  115. package/src/tools/multiEditTool.ts +2 -2
  116. package/src/tools/taskTool.ts +13 -5
  117. package/src/tools/writeTool.ts +1 -1
  118. package/src/types/commands.ts +26 -0
  119. package/src/types/config.ts +14 -0
  120. package/src/types/core.ts +49 -0
  121. package/src/{hooks/types.ts → types/hooks.ts} +1 -0
  122. package/src/types/index.ts +23 -0
  123. package/src/{types.ts → types/index.ts.backup} +13 -0
  124. package/src/types/mcp.ts +31 -0
  125. package/src/types/messaging.ts +103 -0
  126. package/src/types/processes.ts +18 -0
  127. package/src/types/skills.ts +91 -0
  128. package/src/utils/configResolver.ts +1 -1
  129. package/src/utils/configValidator.ts +5 -1
  130. package/src/utils/convertMessagesForAPI.ts +1 -1
  131. package/src/utils/customCommands.ts +1 -1
  132. package/src/utils/markdownParser.ts +1 -1
  133. package/src/utils/mcpUtils.ts +1 -1
  134. package/src/utils/messageOperations.ts +22 -1
  135. package/src/utils/skillParser.ts +1 -1
  136. package/dist/hooks/executor.d.ts +0 -56
  137. package/dist/hooks/executor.d.ts.map +0 -1
  138. package/dist/hooks/executor.js +0 -312
  139. package/dist/hooks/index.d.ts +0 -17
  140. package/dist/hooks/index.d.ts.map +0 -1
  141. package/dist/hooks/index.js +0 -14
  142. package/dist/hooks/manager.d.ts.map +0 -1
  143. package/dist/hooks/matcher.d.ts.map +0 -1
  144. package/dist/hooks/settings.d.ts +0 -46
  145. package/dist/hooks/settings.d.ts.map +0 -1
  146. package/dist/hooks/settings.js +0 -100
  147. package/dist/hooks/types.d.ts.map +0 -1
  148. package/dist/types.d.ts +0 -276
  149. package/dist/types.d.ts.map +0 -1
  150. package/src/hooks/executor.ts +0 -440
  151. package/src/hooks/index.ts +0 -52
  152. package/src/hooks/settings.ts +0 -129
  153. /package/dist/{hooks/types.js → types/hooks.js} +0 -0
  154. /package/dist/{hooks/matcher.js → utils/hookMatcher.js} +0 -0
  155. /package/src/{hooks/matcher.ts → utils/hookMatcher.ts} +0 -0
@@ -2,17 +2,23 @@ import { callAgent, compressMessages } from "../services/aiService.js";
2
2
  import { getMessagesToCompress } from "../utils/messageOperations.js";
3
3
  import { convertMessagesForAPI } from "../utils/convertMessagesForAPI.js";
4
4
  import * as memory from "../services/memory.js";
5
- import type { Logger, GatewayConfig, ModelConfig } from "../types.js";
5
+ import type {
6
+ Logger,
7
+ GatewayConfig,
8
+ ModelConfig,
9
+ Usage,
10
+ } from "../types/index.js";
6
11
  import type { ToolManager } from "./toolManager.js";
7
12
  import type { ToolContext, ToolResult } from "../tools/types.js";
8
13
  import type { MessageManager } from "./messageManager.js";
9
14
  import type { BackgroundBashManager } from "./backgroundBashManager.js";
10
15
  import { ChatCompletionMessageFunctionToolCall } from "openai/resources.js";
11
- import type { HookManager } from "../hooks/index.js";
12
- import type { ExtendedHookExecutionContext } from "../hooks/types.js";
16
+ import type { HookManager } from "./hookManager.js";
17
+ import type { ExtendedHookExecutionContext } from "../types/hooks.js";
13
18
 
14
19
  export interface AIManagerCallbacks {
15
20
  onCompressionStateChange?: (isCompressing: boolean) => void;
21
+ onUsageAdded?: (usage: Usage) => void;
16
22
  }
17
23
 
18
24
  export interface AIManagerOptions {
@@ -140,7 +146,7 @@ export class AIManager {
140
146
 
141
147
  // Check if token limit exceeded - use injected configuration
142
148
  if (usage.total_tokens > this.tokenLimit) {
143
- this.logger?.info(
149
+ this.logger?.debug(
144
150
  `Token usage exceeded ${this.tokenLimit}, compressing messages...`,
145
151
  );
146
152
 
@@ -156,7 +162,7 @@ export class AIManager {
156
162
 
157
163
  this.setIsCompressing(true);
158
164
  try {
159
- const compressedContent = await compressMessages({
165
+ const compressionResult = await compressMessages({
160
166
  gatewayConfig: this.gatewayConfig,
161
167
  modelConfig: this.modelConfig,
162
168
  messages: recentChatMessages,
@@ -166,10 +172,26 @@ export class AIManager {
166
172
  // Execute message reconstruction and sessionId update after compression
167
173
  this.messageManager.compressMessagesAndUpdateSession(
168
174
  insertIndex,
169
- compressedContent,
175
+ compressionResult.content,
170
176
  );
171
177
 
172
- this.logger?.info(
178
+ // Handle usage tracking for compression operations
179
+ if (compressionResult.usage) {
180
+ const usage: Usage = {
181
+ prompt_tokens: compressionResult.usage.prompt_tokens,
182
+ completion_tokens: compressionResult.usage.completion_tokens,
183
+ total_tokens: compressionResult.usage.total_tokens,
184
+ model: this.modelConfig.fastModel,
185
+ operation_type: "compress",
186
+ };
187
+
188
+ // Notify Agent to add to usage tracking
189
+ if (this.callbacks?.onUsageAdded) {
190
+ this.callbacks.onUsageAdded(usage);
191
+ }
192
+ }
193
+
194
+ this.logger?.debug(
173
195
  `Successfully compressed ${messagesToCompress.length} messages and updated session`,
174
196
  );
175
197
  } catch (compressError) {
@@ -200,18 +222,29 @@ export class AIManager {
200
222
  } = {},
201
223
  ): Promise<void> {
202
224
  const { recursionDepth = 0, model, allowedTools } = options;
225
+
203
226
  // Only check isLoading for the initial call (recursionDepth === 0)
204
227
  if (recursionDepth === 0 && this.isLoading) {
205
228
  return;
206
229
  }
207
230
 
208
- // Create new AbortController
209
- const abortController = new AbortController();
210
- this.abortController = abortController;
231
+ // Only create new AbortControllers for the initial call (recursionDepth === 0)
232
+ // For recursive calls, reuse existing controllers to maintain abort signal
233
+ let abortController: AbortController;
234
+ let toolAbortController: AbortController;
211
235
 
212
- // Create separate AbortController for tool execution
213
- const toolAbortController = new AbortController();
214
- this.toolAbortController = toolAbortController;
236
+ if (recursionDepth === 0) {
237
+ // Create new AbortControllers for initial call
238
+ abortController = new AbortController();
239
+ this.abortController = abortController;
240
+
241
+ toolAbortController = new AbortController();
242
+ this.toolAbortController = toolAbortController;
243
+ } else {
244
+ // Reuse existing controllers for recursive calls
245
+ abortController = this.abortController!;
246
+ toolAbortController = this.toolAbortController!;
247
+ }
215
248
 
216
249
  // Only set loading state for the initial call
217
250
  if (recursionDepth === 0) {
@@ -255,143 +288,171 @@ export class AIManager {
255
288
  }
256
289
  }
257
290
 
258
- // Add assistant message at once (including content and tool calls)
259
- this.messageManager.addAssistantMessage(content, toolCalls);
260
-
261
- if (toolCalls.length > 0) {
262
- for (const functionToolCall of toolCalls) {
263
- const toolId = functionToolCall.id || "";
264
- // Execute tool
265
- try {
266
- // Check if already interrupted, skip tool execution if so
267
- if (
268
- abortController.signal.aborted ||
269
- toolAbortController.signal.aborted
270
- ) {
271
- return;
272
- }
273
-
274
- // Safely parse tool parameters, handle tools without parameters
275
- let toolArgs: Record<string, unknown> = {};
276
- const argsString = functionToolCall.function?.arguments?.trim();
291
+ // Handle usage tracking for agent operations
292
+ let usage: Usage | undefined;
293
+ if (result.usage) {
294
+ usage = {
295
+ prompt_tokens: result.usage.prompt_tokens,
296
+ completion_tokens: result.usage.completion_tokens,
297
+ total_tokens: result.usage.total_tokens,
298
+ model: model || this.modelConfig.agentModel,
299
+ operation_type: "agent",
300
+ };
301
+ }
277
302
 
278
- if (!argsString || argsString === "") {
279
- // Tool without parameters, use empty object
280
- toolArgs = {};
281
- } else {
282
- try {
283
- toolArgs = JSON.parse(argsString);
284
- } catch (parseError) {
285
- // For non-empty but malformed JSON, still throw exception
286
- const errorMessage = `Failed to parse tool arguments: ${argsString}`;
287
- this.logger?.error(errorMessage, parseError);
288
- throw new Error(errorMessage);
289
- }
290
- }
303
+ // Add assistant message at once (including content, tool calls, and usage)
304
+ this.messageManager.addAssistantMessage(content, toolCalls, usage);
291
305
 
292
- // Set tool start execution state
293
- const toolName = functionToolCall.function?.name || "";
294
- const compactParams = this.generateCompactParams(
295
- toolName,
296
- toolArgs,
297
- );
306
+ // Notify Agent to add to usage tracking
307
+ if (usage) {
308
+ if (this.callbacks?.onUsageAdded) {
309
+ this.callbacks.onUsageAdded(usage);
310
+ }
311
+ }
298
312
 
299
- this.messageManager.updateToolBlock({
300
- toolId,
301
- args: JSON.stringify(toolArgs, null, 2),
302
- isRunning: true, // isRunning: true
303
- name: toolName,
304
- compactParams,
305
- });
313
+ if (toolCalls.length > 0) {
314
+ // Execute all tools in parallel using Promise.all
315
+ const toolExecutionPromises = toolCalls.map(
316
+ async (functionToolCall) => {
317
+ const toolId = functionToolCall.id || "";
306
318
 
307
319
  try {
308
- // Execute PreToolUse hooks before tool execution
309
- await this.executePreToolUseHooks(toolName, toolArgs);
310
-
311
- // Create tool execution context
312
- const context: ToolContext = {
313
- abortSignal: toolAbortController.signal,
314
- backgroundBashManager: this.backgroundBashManager,
315
- workdir: this.workdir,
316
- };
317
-
318
- // Execute tool
319
- const toolResult = await this.toolManager.execute(
320
- functionToolCall.function?.name || "",
321
- toolArgs,
322
- context,
323
- );
324
-
325
- // Update message state - tool execution completed
326
- this.messageManager.updateToolBlock({
327
- toolId,
328
- args: JSON.stringify(toolArgs, null, 2),
329
- result:
330
- toolResult.content ||
331
- (toolResult.error ? `Error: ${toolResult.error}` : ""),
332
- success: toolResult.success,
333
- error: toolResult.error,
334
- isRunning: false, // isRunning: false
335
- name: toolName,
336
- shortResult: toolResult.shortResult,
337
- compactParams,
338
- });
339
-
340
- // If tool returns diff information, add diff block
320
+ // Check if already interrupted, skip tool execution if so
341
321
  if (
342
- toolResult.success &&
343
- toolResult.diffResult &&
344
- toolResult.filePath
322
+ abortController.signal.aborted ||
323
+ toolAbortController.signal.aborted
345
324
  ) {
346
- this.messageManager.addDiffBlock(
347
- toolResult.filePath,
348
- toolResult.diffResult,
349
- );
325
+ return;
326
+ }
327
+
328
+ // Safely parse tool parameters, handle tools without parameters
329
+ let toolArgs: Record<string, unknown> = {};
330
+ const argsString = functionToolCall.function?.arguments?.trim();
331
+
332
+ if (!argsString || argsString === "") {
333
+ // Tool without parameters, use empty object
334
+ toolArgs = {};
335
+ } else {
336
+ try {
337
+ toolArgs = JSON.parse(argsString);
338
+ } catch (parseError) {
339
+ // For non-empty but malformed JSON, still throw exception
340
+ const errorMessage = `Failed to parse tool arguments: ${argsString}`;
341
+ this.logger?.error(errorMessage, parseError);
342
+ throw new Error(errorMessage);
343
+ }
350
344
  }
351
345
 
352
- // Execute PostToolUse hooks after successful tool completion
353
- await this.executePostToolUseHooks(
346
+ // Set tool start execution state
347
+ const toolName = functionToolCall.function?.name || "";
348
+ const compactParams = this.generateCompactParams(
354
349
  toolName,
355
350
  toolArgs,
356
- toolResult,
357
351
  );
358
- } catch (toolError) {
359
- const errorMessage =
360
- toolError instanceof Error
361
- ? toolError.message
362
- : String(toolError);
363
352
 
364
353
  this.messageManager.updateToolBlock({
365
354
  toolId,
366
355
  args: JSON.stringify(toolArgs, null, 2),
367
- result: `Tool execution failed: ${errorMessage}`,
368
- success: false,
369
- error: errorMessage,
370
- isRunning: false,
356
+ isRunning: true, // isRunning: true
371
357
  name: toolName,
372
358
  compactParams,
373
359
  });
360
+
361
+ try {
362
+ // Execute PreToolUse hooks before tool execution
363
+ const shouldExecuteTool = await this.executePreToolUseHooks(
364
+ toolName,
365
+ toolArgs,
366
+ toolId,
367
+ );
368
+
369
+ // If PreToolUse hooks blocked execution, skip tool execution
370
+ if (!shouldExecuteTool) {
371
+ this.logger?.info(
372
+ `Tool ${toolName} execution blocked by PreToolUse hooks`,
373
+ );
374
+ return; // Skip this tool and return from this map function
375
+ }
376
+
377
+ // Create tool execution context
378
+ const context: ToolContext = {
379
+ abortSignal: toolAbortController.signal,
380
+ backgroundBashManager: this.backgroundBashManager,
381
+ workdir: this.workdir,
382
+ };
383
+
384
+ // Execute tool
385
+ const toolResult = await this.toolManager.execute(
386
+ functionToolCall.function?.name || "",
387
+ toolArgs,
388
+ context,
389
+ );
390
+
391
+ // Update message state - tool execution completed
392
+ this.messageManager.updateToolBlock({
393
+ toolId,
394
+ args: JSON.stringify(toolArgs, null, 2),
395
+ result:
396
+ toolResult.content ||
397
+ (toolResult.error ? `Error: ${toolResult.error}` : ""),
398
+ success: toolResult.success,
399
+ error: toolResult.error,
400
+ isRunning: false, // isRunning: false
401
+ name: toolName,
402
+ shortResult: toolResult.shortResult,
403
+ compactParams,
404
+ });
405
+
406
+ // If tool returns diff information, add diff block
407
+ if (
408
+ toolResult.success &&
409
+ toolResult.diffResult &&
410
+ toolResult.filePath
411
+ ) {
412
+ this.messageManager.addDiffBlock(
413
+ toolResult.filePath,
414
+ toolResult.diffResult,
415
+ );
416
+ }
417
+
418
+ // Execute PostToolUse hooks after successful tool completion
419
+ await this.executePostToolUseHooks(
420
+ toolId,
421
+ toolName,
422
+ toolArgs,
423
+ toolResult,
424
+ );
425
+ } catch (toolError) {
426
+ const errorMessage =
427
+ toolError instanceof Error
428
+ ? toolError.message
429
+ : String(toolError);
430
+
431
+ this.messageManager.updateToolBlock({
432
+ toolId,
433
+ args: JSON.stringify(toolArgs, null, 2),
434
+ result: `Tool execution failed: ${errorMessage}`,
435
+ success: false,
436
+ error: errorMessage,
437
+ isRunning: false,
438
+ name: toolName,
439
+ compactParams,
440
+ });
441
+ }
442
+ } catch (parseError) {
443
+ const errorMessage =
444
+ parseError instanceof Error
445
+ ? parseError.message
446
+ : String(parseError);
447
+ this.messageManager.addErrorBlock(
448
+ `Failed to parse tool arguments for ${functionToolCall.function?.name}: ${errorMessage}`,
449
+ );
374
450
  }
375
- } catch (parseError) {
376
- // Check if it's a parsing error due to interruption
377
- const isAborted =
378
- abortController.signal.aborted ||
379
- toolAbortController.signal.aborted;
380
-
381
- if (isAborted) {
382
- // If interrupted, return directly without showing error
383
- return;
384
- }
451
+ },
452
+ );
385
453
 
386
- const errorMessage =
387
- parseError instanceof Error
388
- ? parseError.message
389
- : String(parseError);
390
- this.messageManager.addErrorBlock(
391
- `Failed to parse tool arguments for ${functionToolCall.function?.name}: ${errorMessage}`,
392
- );
393
- }
394
- }
454
+ // Wait for all tools to complete execution in parallel
455
+ await Promise.all(toolExecutionPromises);
395
456
  }
396
457
 
397
458
  // Handle token statistics and message compression
@@ -403,12 +464,6 @@ export class AIManager {
403
464
  const isCurrentlyAborted =
404
465
  abortController.signal.aborted || toolAbortController.signal.aborted;
405
466
 
406
- // AI service call ends, clear abort controller
407
- this.abortController = null;
408
-
409
- // Clear tool AbortController after tool execution completes
410
- this.toolAbortController = null;
411
-
412
467
  if (!isCurrentlyAborted) {
413
468
  // Recursively call AI service, increment recursion depth, and pass same configuration
414
469
  await this.sendAIMessage({
@@ -417,47 +472,57 @@ export class AIManager {
417
472
  allowedTools,
418
473
  });
419
474
  }
420
- } else {
421
- // Clear abort controller when no tool operations
422
- this.abortController = null;
423
- this.toolAbortController = null;
424
475
  }
425
476
  } catch (error) {
426
- // Check if error is due to user interrupt operation
427
- const isAborted =
428
- abortController.signal.aborted ||
429
- toolAbortController.signal.aborted ||
430
- (error instanceof Error &&
431
- (error.name === "AbortError" || error.message.includes("aborted")));
432
-
433
- if (!isAborted) {
434
- this.messageManager.addErrorBlock(
435
- error instanceof Error ? error.message : "Unknown error occurred",
436
- );
437
- }
438
-
439
- // Reset abort controller on error
440
- this.abortController = null;
441
- this.toolAbortController = null;
477
+ this.messageManager.addErrorBlock(
478
+ error instanceof Error ? error.message : "Unknown error occurred",
479
+ );
442
480
  } finally {
443
- // Only clear loading state for the initial call
481
+ // Only execute Stop hooks for the initial call
444
482
  if (recursionDepth === 0) {
445
- this.setIsLoading(false);
483
+ // Execute Stop hooks only if the operation was not aborted
484
+ const isCurrentlyAborted =
485
+ abortController.signal.aborted || toolAbortController.signal.aborted;
486
+
487
+ if (!isCurrentlyAborted) {
488
+ const shouldContinue = await this.executeStopHooks();
489
+
490
+ // If Stop hooks indicate we should continue (due to blocking errors),
491
+ // restart the AI conversation cycle
492
+ if (shouldContinue) {
493
+ this.logger?.info(
494
+ "Stop hooks indicate issues need fixing, continuing conversation...",
495
+ );
496
+
497
+ // Restart the conversation to let AI fix the issues
498
+ // Use recursionDepth = 1 to prevent Stop hooks from running again in continuation
499
+ await this.sendAIMessage({
500
+ recursionDepth: 1,
501
+ model,
502
+ allowedTools,
503
+ });
504
+ }
505
+ }
446
506
 
447
- // Save session before executing Stop hooks
507
+ // Save session after all operations (including continuation) are complete
448
508
  await this.messageManager.saveSession();
449
509
 
450
- // Execute Stop hooks when AI response cycle completes
451
- await this.executeStopHooks();
510
+ // Clear abort controllers and loading state after all operations are complete
511
+ this.abortController = null;
512
+ this.toolAbortController = null;
513
+
514
+ // Set loading to false at the very end, after all operations including continuation
515
+ this.setIsLoading(false);
452
516
  }
453
517
  }
454
518
  }
455
519
 
456
520
  /**
457
521
  * Execute Stop hooks when AI response cycle completes
522
+ * @returns Promise<boolean> - true if should continue conversation, false if should stop
458
523
  */
459
- private async executeStopHooks(): Promise<void> {
460
- if (!this.hookManager) return;
524
+ private async executeStopHooks(): Promise<boolean> {
525
+ if (!this.hookManager) return false;
461
526
 
462
527
  try {
463
528
  const context: ExtendedHookExecutionContext = {
@@ -472,6 +537,25 @@ export class AIManager {
472
537
 
473
538
  const results = await this.hookManager.executeHooks("Stop", context);
474
539
 
540
+ // Process hook results to handle exit codes and appropriate responses
541
+ let shouldContinue = false;
542
+ if (results.length > 0) {
543
+ const processResult = this.hookManager.processHookResults(
544
+ "Stop",
545
+ results,
546
+ this.messageManager,
547
+ );
548
+
549
+ // If hook processing indicates we should block (exit code 2), continue conversation
550
+ if (processResult.shouldBlock) {
551
+ this.logger?.info(
552
+ "Stop hook blocked stopping with error:",
553
+ processResult.errorMessage,
554
+ );
555
+ shouldContinue = true;
556
+ }
557
+ }
558
+
475
559
  // Log hook execution results for debugging
476
560
  if (results.length > 0) {
477
561
  this.logger?.debug(
@@ -485,20 +569,25 @@ export class AIManager {
485
569
  })),
486
570
  );
487
571
  }
572
+
573
+ return shouldContinue;
488
574
  } catch (error) {
489
575
  // Hook execution errors should not interrupt the main workflow
490
576
  this.logger?.error("Stop hook execution failed:", error);
577
+ return false;
491
578
  }
492
579
  }
493
580
 
494
581
  /**
495
582
  * Execute PreToolUse hooks before tool execution
583
+ * Returns true if hooks allow tool execution, false if blocked
496
584
  */
497
585
  private async executePreToolUseHooks(
498
586
  toolName: string,
499
587
  toolInput?: Record<string, unknown>,
500
- ): Promise<void> {
501
- if (!this.hookManager) return;
588
+ toolId?: string,
589
+ ): Promise<boolean> {
590
+ if (!this.hookManager) return true;
502
591
 
503
592
  try {
504
593
  const context: ExtendedHookExecutionContext = {
@@ -517,6 +606,18 @@ export class AIManager {
517
606
  context,
518
607
  );
519
608
 
609
+ // Process hook results to handle exit codes and determine if tool should be blocked
610
+ let shouldContinue = true;
611
+ if (results.length > 0) {
612
+ const processResult = this.hookManager.processHookResults(
613
+ "PreToolUse",
614
+ results,
615
+ this.messageManager,
616
+ toolId, // Pass toolId for proper PreToolUse blocking error handling
617
+ );
618
+ shouldContinue = !processResult.shouldBlock;
619
+ }
620
+
520
621
  // Log hook execution results for debugging
521
622
  if (results.length > 0) {
522
623
  this.logger?.debug(
@@ -530,9 +631,12 @@ export class AIManager {
530
631
  })),
531
632
  );
532
633
  }
634
+
635
+ return shouldContinue;
533
636
  } catch (error) {
534
637
  // Hook execution errors should not interrupt the main workflow
535
638
  this.logger?.error("PreToolUse hook execution failed:", error);
639
+ return true; // Allow tool execution on hook errors
536
640
  }
537
641
  }
538
642
 
@@ -540,6 +644,7 @@ export class AIManager {
540
644
  * Execute PostToolUse hooks after tool completion
541
645
  */
542
646
  private async executePostToolUseHooks(
647
+ toolId: string,
543
648
  toolName: string,
544
649
  toolInput?: Record<string, unknown>,
545
650
  toolResponse?: ToolResult,
@@ -564,6 +669,19 @@ export class AIManager {
564
669
  context,
565
670
  );
566
671
 
672
+ // Process hook results to handle exit codes and update tool results
673
+ if (results.length > 0) {
674
+ const originalToolResult = toolResponse?.content || "";
675
+
676
+ this.hookManager.processHookResults(
677
+ "PostToolUse",
678
+ results,
679
+ this.messageManager,
680
+ toolId,
681
+ originalToolResult,
682
+ );
683
+ }
684
+
567
685
  // Log hook execution results for debugging
568
686
  if (results.length > 0) {
569
687
  this.logger?.debug(
@@ -1,5 +1,5 @@
1
1
  import { spawn } from "child_process";
2
- import type { BackgroundShell } from "../types.js";
2
+ import type { BackgroundShell } from "../types/index.js";
3
3
 
4
4
  export interface BackgroundBashManagerCallbacks {
5
5
  onShellsChange?: (shells: BackgroundShell[]) => void;